Skip to main content

docker_registry/
mediatypes.rs

1//! Media-types for API objects.
2
3use strum::{Display, EnumProperty, EnumString};
4
5use crate::errors::Result;
6
7// For Docker schema1 types, see https://docs.docker.com/registry/spec/manifest-v2-1/
8// For Docker schema2 types, see https://docs.docker.com/registry/spec/manifest-v2-2/
9// For OCI types, see https://github.com/opencontainers/image-spec/blob/main/media-types.md
10
11#[derive(EnumProperty, EnumString, Display, Debug, Hash, PartialEq, Eq, Clone)]
12pub enum MediaTypes {
13  // --- Docker types ---
14  /// Manifest, version 2 schema 1.
15  #[strum(serialize = "application/vnd.docker.distribution.manifest.v1+json")]
16  #[strum(props(Sub = "vnd.docker.distribution.manifest.v1+json"))]
17  ManifestV2S1,
18  /// Signed manifest, version 2 schema 1.
19  #[strum(serialize = "application/vnd.docker.distribution.manifest.v1+prettyjws")]
20  #[strum(props(Sub = "vnd.docker.distribution.manifest.v1+prettyjws"))]
21  ManifestV2S1Signed,
22  /// Manifest, version 2 schema 2.
23  #[strum(serialize = "application/vnd.docker.distribution.manifest.v2+json")]
24  #[strum(props(Sub = "vnd.docker.distribution.manifest.v2+json"))]
25  ManifestV2S2,
26  /// Manifest List (aka "fat manifest").
27  #[strum(serialize = "application/vnd.docker.distribution.manifest.list.v2+json")]
28  #[strum(props(Sub = "vnd.docker.distribution.manifest.list.v2+json"))]
29  ManifestList,
30  /// Image layer, as a gzip-compressed tar.
31  #[strum(serialize = "application/vnd.docker.image.rootfs.diff.tar.gzip")]
32  #[strum(props(Sub = "vnd.docker.image.rootfs.diff.tar.gzip"))]
33  ImageLayerTgz,
34  /// Foreign image layer, as a gzip-compressed tar (e.g. Windows base layers).
35  #[strum(serialize = "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip")]
36  #[strum(props(Sub = "vnd.docker.image.rootfs.foreign.diff.tar.gzip"))]
37  ImageLayerForeignTgz,
38  /// Configuration object for a container.
39  #[strum(serialize = "application/vnd.docker.container.image.v1+json")]
40  #[strum(props(Sub = "vnd.docker.container.image.v1+json"))]
41  ContainerConfigV1,
42
43  // --- OCI types ---
44  /// OCI Image Manifest.
45  #[strum(serialize = "application/vnd.oci.image.manifest.v1+json")]
46  #[strum(props(Sub = "vnd.oci.image.manifest.v1+json"))]
47  OciImageManifest,
48  /// OCI Image Index (multi-platform manifest).
49  #[strum(serialize = "application/vnd.oci.image.index.v1+json")]
50  #[strum(props(Sub = "vnd.oci.image.index.v1+json"))]
51  OciImageIndexV1,
52  /// OCI Image Config.
53  #[strum(serialize = "application/vnd.oci.image.config.v1+json")]
54  #[strum(props(Sub = "vnd.oci.image.config.v1+json"))]
55  OciImageConfig,
56  /// OCI Image Layer, as an uncompressed tar.
57  #[strum(serialize = "application/vnd.oci.image.layer.v1.tar")]
58  #[strum(props(Sub = "vnd.oci.image.layer.v1.tar"))]
59  OciImageLayerTar,
60  /// OCI Image Layer, as a gzip-compressed tar.
61  #[strum(serialize = "application/vnd.oci.image.layer.v1.tar+gzip")]
62  #[strum(props(Sub = "vnd.oci.image.layer.v1.tar+gzip"))]
63  OciImageLayerTgz,
64  /// OCI Image Layer, as a zstd-compressed tar.
65  #[strum(serialize = "application/vnd.oci.image.layer.v1.tar+zstd")]
66  #[strum(props(Sub = "vnd.oci.image.layer.v1.tar+zstd"))]
67  OciImageLayerZstd,
68  /// OCI Empty descriptor (scratch/unused).
69  #[strum(serialize = "application/vnd.oci.empty.v1+json")]
70  #[strum(props(Sub = "vnd.oci.empty.v1+json"))]
71  OciEmptyV1,
72
73  // --- Generic ---
74  /// Generic JSON
75  #[strum(serialize = "application/json")]
76  #[strum(props(Sub = "json"))]
77  ApplicationJson,
78}
79
80impl MediaTypes {
81  // TODO(lucab): proper error types
82  pub fn from_mime(mtype: &mime::Mime) -> Result<Self> {
83    match (mtype.type_(), mtype.subtype(), mtype.suffix()) {
84      (mime::APPLICATION, mime::JSON, _) => Ok(MediaTypes::ApplicationJson),
85      (mime::APPLICATION, subt, None) if subt == "vnd.docker.image.rootfs.diff.tar.gzip" => {
86        Ok(MediaTypes::ImageLayerTgz)
87      }
88      (mime::APPLICATION, subt, None) if subt == "vnd.docker.image.rootfs.foreign.diff.tar.gzip" => {
89        Ok(MediaTypes::ImageLayerForeignTgz)
90      }
91      (mime::APPLICATION, subt, None) if subt == "vnd.oci.image.layer.v1.tar" => Ok(MediaTypes::OciImageLayerTar),
92      (mime::APPLICATION, subt, Some(suff)) => match (subt.to_string().as_str(), suff.to_string().as_str()) {
93        // Docker
94        ("vnd.docker.distribution.manifest.v1", "json") => Ok(MediaTypes::ManifestV2S1),
95        ("vnd.docker.distribution.manifest.v1", "prettyjws") => Ok(MediaTypes::ManifestV2S1Signed),
96        ("vnd.docker.distribution.manifest.v2", "json") => Ok(MediaTypes::ManifestV2S2),
97        ("vnd.docker.distribution.manifest.list.v2", "json") => Ok(MediaTypes::ManifestList),
98        ("vnd.docker.image.rootfs.diff.tar.gzip", _) => Ok(MediaTypes::ImageLayerTgz),
99        ("vnd.docker.image.rootfs.foreign.diff.tar.gzip", _) => Ok(MediaTypes::ImageLayerForeignTgz),
100        ("vnd.docker.container.image.v1", "json") => Ok(MediaTypes::ContainerConfigV1),
101        // OCI
102        ("vnd.oci.image.manifest.v1", "json") => Ok(MediaTypes::OciImageManifest),
103        ("vnd.oci.image.index.v1", "json") => Ok(MediaTypes::OciImageIndexV1),
104        ("vnd.oci.image.config.v1", "json") => Ok(MediaTypes::OciImageConfig),
105        ("vnd.oci.image.layer.v1.tar", "gzip") => Ok(MediaTypes::OciImageLayerTgz),
106        ("vnd.oci.image.layer.v1.tar", "zstd") => Ok(MediaTypes::OciImageLayerZstd),
107        ("vnd.oci.empty.v1", "json") => Ok(MediaTypes::OciEmptyV1),
108        _ => Err(crate::Error::UnknownMimeType(mtype.clone())),
109      },
110      _ => Err(crate::Error::UnknownMimeType(mtype.clone())),
111    }
112  }
113  pub fn to_mime(&self) -> mime::Mime {
114    match self {
115      &MediaTypes::ApplicationJson => Ok(mime::APPLICATION_JSON),
116      m => {
117        if let Some(s) = m.get_str("Sub") {
118          ("application/".to_string() + s).parse()
119        } else {
120          "application/star".parse()
121        }
122      }
123    }
124    .expect("to_mime should be always successful")
125  }
126}
127
128#[cfg(test)]
129mod tests {
130  use std::str::FromStr;
131
132  use super::*;
133
134  #[test]
135  fn test_roundtrip_to_mime_from_mime() {
136    let types = [
137      MediaTypes::ManifestV2S1,
138      MediaTypes::ManifestV2S1Signed,
139      MediaTypes::ManifestV2S2,
140      MediaTypes::ManifestList,
141      MediaTypes::ImageLayerTgz,
142      MediaTypes::ImageLayerForeignTgz,
143      MediaTypes::ContainerConfigV1,
144      MediaTypes::OciImageManifest,
145      MediaTypes::OciImageIndexV1,
146      MediaTypes::OciImageConfig,
147      MediaTypes::OciImageLayerTar,
148      MediaTypes::OciImageLayerTgz,
149      MediaTypes::OciImageLayerZstd,
150      MediaTypes::OciEmptyV1,
151      MediaTypes::ApplicationJson,
152    ];
153    for mt in &types {
154      let mime = mt.to_mime();
155      let back = MediaTypes::from_mime(&mime).unwrap();
156      assert_eq!(&back, mt, "roundtrip failed for {mt:?}");
157    }
158  }
159
160  #[test]
161  fn test_from_str_roundtrip() {
162    let types = [
163      "application/vnd.docker.distribution.manifest.v2+json",
164      "application/vnd.docker.distribution.manifest.v1+prettyjws",
165      "application/vnd.docker.distribution.manifest.list.v2+json",
166      "application/vnd.oci.image.manifest.v1+json",
167      "application/vnd.oci.image.index.v1+json",
168      "application/json",
169    ];
170    for s in &types {
171      let mt = MediaTypes::from_str(s).unwrap();
172      assert_eq!(&mt.to_string(), s, "roundtrip failed for {s}");
173    }
174  }
175
176  #[test]
177  fn test_from_mime_unknown_type() {
178    let mime: mime::Mime = "text/plain".parse().unwrap();
179    let result = MediaTypes::from_mime(&mime);
180    assert!(result.is_err());
181  }
182
183  #[test]
184  fn test_from_mime_unknown_application_subtype() {
185    let mime: mime::Mime = "application/vnd.unknown.type.v1+json".parse().unwrap();
186    let result = MediaTypes::from_mime(&mime);
187    assert!(result.is_err());
188  }
189
190  #[test]
191  fn test_from_str_invalid() {
192    let result = MediaTypes::from_str("not/a/valid/media/type");
193    assert!(result.is_err());
194  }
195}