Skip to main content

containerregistry_image/
media_type.rs

1//! OCI and Docker media type constants and utilities.
2
3use std::fmt;
4use std::str::FromStr;
5
6use serde::{Deserialize, Serialize};
7
8use crate::Error;
9
10/// Known media types for OCI and Docker images.
11#[derive(Clone, Debug, PartialEq, Eq, Hash)]
12pub enum MediaType {
13    // OCI types
14    /// OCI image manifest.
15    OciManifest,
16    /// OCI image index (multi-arch).
17    OciIndex,
18    /// OCI image config.
19    OciConfig,
20    /// OCI layer (gzip compressed).
21    OciLayerGzip,
22    /// OCI layer (uncompressed).
23    OciLayer,
24    /// OCI layer (zstd compressed).
25    OciLayerZstd,
26    /// OCI non-distributable layer (uncompressed, deprecated).
27    OciLayerNondistributable,
28    /// OCI non-distributable layer (gzip, deprecated).
29    OciLayerNondistributableGzip,
30    /// OCI non-distributable layer (zstd, deprecated).
31    OciLayerNondistributableZstd,
32    /// OCI empty/scratch config descriptor.
33    OciEmptyJson,
34
35    // Docker types
36    /// Docker v2 schema 2 manifest.
37    DockerManifest,
38    /// Docker manifest list.
39    DockerManifestList,
40    /// Docker container config.
41    DockerConfig,
42    /// Docker layer (gzip compressed).
43    DockerLayerGzip,
44    /// Docker foreign/non-distributable layer.
45    DockerForeignLayer,
46
47    /// Unknown or custom media type.
48    Other(String),
49}
50
51impl MediaType {
52    /// Returns the string representation of the media type.
53    pub fn as_str(&self) -> &str {
54        match self {
55            MediaType::OciManifest => "application/vnd.oci.image.manifest.v1+json",
56            MediaType::OciIndex => "application/vnd.oci.image.index.v1+json",
57            MediaType::OciConfig => "application/vnd.oci.image.config.v1+json",
58            MediaType::OciLayerGzip => "application/vnd.oci.image.layer.v1.tar+gzip",
59            MediaType::OciLayer => "application/vnd.oci.image.layer.v1.tar",
60            MediaType::OciLayerZstd => "application/vnd.oci.image.layer.v1.tar+zstd",
61            MediaType::OciLayerNondistributable => {
62                "application/vnd.oci.image.layer.nondistributable.v1.tar"
63            }
64            MediaType::OciLayerNondistributableGzip => {
65                "application/vnd.oci.image.layer.nondistributable.v1.tar+gzip"
66            }
67            MediaType::OciLayerNondistributableZstd => {
68                "application/vnd.oci.image.layer.nondistributable.v1.tar+zstd"
69            }
70            MediaType::OciEmptyJson => "application/vnd.oci.empty.v1+json",
71            MediaType::DockerManifest => "application/vnd.docker.distribution.manifest.v2+json",
72            MediaType::DockerManifestList => {
73                "application/vnd.docker.distribution.manifest.list.v2+json"
74            }
75            MediaType::DockerConfig => "application/vnd.docker.container.image.v1+json",
76            MediaType::DockerLayerGzip => "application/vnd.docker.image.rootfs.diff.tar.gzip",
77            MediaType::DockerForeignLayer => {
78                "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip"
79            }
80            MediaType::Other(s) => s,
81        }
82    }
83
84    /// Returns true if this is a manifest type (single image).
85    pub fn is_manifest(&self) -> bool {
86        matches!(self, MediaType::OciManifest | MediaType::DockerManifest)
87    }
88
89    /// Returns true if this is an index/manifest list type (multi-arch).
90    pub fn is_index(&self) -> bool {
91        matches!(self, MediaType::OciIndex | MediaType::DockerManifestList)
92    }
93
94    /// Returns true if this is a config type.
95    pub fn is_config(&self) -> bool {
96        matches!(
97            self,
98            MediaType::OciConfig | MediaType::DockerConfig | MediaType::OciEmptyJson
99        )
100    }
101
102    /// Returns true if this is a layer type (distributable or not).
103    pub fn is_layer(&self) -> bool {
104        matches!(
105            self,
106            MediaType::OciLayerGzip
107                | MediaType::OciLayer
108                | MediaType::OciLayerZstd
109                | MediaType::OciLayerNondistributable
110                | MediaType::OciLayerNondistributableGzip
111                | MediaType::OciLayerNondistributableZstd
112                | MediaType::DockerLayerGzip
113                | MediaType::DockerForeignLayer
114        )
115    }
116
117    /// Returns true if this is a non-distributable/foreign layer type.
118    pub fn is_nondistributable(&self) -> bool {
119        matches!(
120            self,
121            MediaType::OciLayerNondistributable
122                | MediaType::OciLayerNondistributableGzip
123                | MediaType::OciLayerNondistributableZstd
124                | MediaType::DockerForeignLayer
125        )
126    }
127
128    /// Returns the OCI equivalent of this media type, if applicable.
129    pub fn to_oci(&self) -> MediaType {
130        match self {
131            MediaType::DockerManifest => MediaType::OciManifest,
132            MediaType::DockerManifestList => MediaType::OciIndex,
133            MediaType::DockerConfig => MediaType::OciConfig,
134            MediaType::DockerLayerGzip => MediaType::OciLayerGzip,
135            MediaType::DockerForeignLayer => MediaType::OciLayerNondistributableGzip,
136            other => other.clone(),
137        }
138    }
139
140    /// Returns the Docker equivalent of this media type, if applicable.
141    ///
142    /// Note: This only changes the media type string. It does NOT transform
143    /// the actual content. For layers with incompatible compression formats
144    /// (uncompressed, zstd), this will return the same media type unchanged
145    /// since Docker does not support those formats.
146    pub fn to_docker(&self) -> MediaType {
147        match self {
148            MediaType::OciManifest => MediaType::DockerManifest,
149            MediaType::OciIndex => MediaType::DockerManifestList,
150            MediaType::OciConfig | MediaType::OciEmptyJson => MediaType::DockerConfig,
151            MediaType::OciLayerGzip => MediaType::DockerLayerGzip,
152            // Non-distributable gzip can map to Docker foreign layer
153            MediaType::OciLayerNondistributableGzip => MediaType::DockerForeignLayer,
154            // These cannot be safely converted - return unchanged
155            MediaType::OciLayer
156            | MediaType::OciLayerZstd
157            | MediaType::OciLayerNondistributable
158            | MediaType::OciLayerNondistributableZstd => self.clone(),
159            other => other.clone(),
160        }
161    }
162
163    /// Returns true if this media type can be safely converted to Docker format.
164    ///
165    /// Returns false for OCI-specific compression formats that Docker doesn't support.
166    pub fn is_docker_compatible(&self) -> bool {
167        !matches!(
168            self,
169            MediaType::OciLayer
170                | MediaType::OciLayerZstd
171                | MediaType::OciLayerNondistributable
172                | MediaType::OciLayerNondistributableZstd
173        )
174    }
175}
176
177impl fmt::Display for MediaType {
178    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179        write!(f, "{}", self.as_str())
180    }
181}
182
183impl FromStr for MediaType {
184    type Err = Error;
185
186    fn from_str(s: &str) -> Result<Self, Self::Err> {
187        Ok(match s {
188            "application/vnd.oci.image.manifest.v1+json" => MediaType::OciManifest,
189            "application/vnd.oci.image.index.v1+json" => MediaType::OciIndex,
190            "application/vnd.oci.image.config.v1+json" => MediaType::OciConfig,
191            "application/vnd.oci.image.layer.v1.tar+gzip" => MediaType::OciLayerGzip,
192            "application/vnd.oci.image.layer.v1.tar" => MediaType::OciLayer,
193            "application/vnd.oci.image.layer.v1.tar+zstd" => MediaType::OciLayerZstd,
194            "application/vnd.oci.image.layer.nondistributable.v1.tar" => {
195                MediaType::OciLayerNondistributable
196            }
197            "application/vnd.oci.image.layer.nondistributable.v1.tar+gzip" => {
198                MediaType::OciLayerNondistributableGzip
199            }
200            "application/vnd.oci.image.layer.nondistributable.v1.tar+zstd" => {
201                MediaType::OciLayerNondistributableZstd
202            }
203            "application/vnd.oci.empty.v1+json" => MediaType::OciEmptyJson,
204            "application/vnd.docker.distribution.manifest.v2+json" => MediaType::DockerManifest,
205            "application/vnd.docker.distribution.manifest.list.v2+json" => {
206                MediaType::DockerManifestList
207            }
208            "application/vnd.docker.container.image.v1+json" => MediaType::DockerConfig,
209            "application/vnd.docker.image.rootfs.diff.tar.gzip" => MediaType::DockerLayerGzip,
210            "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip" => {
211                MediaType::DockerForeignLayer
212            }
213            other => MediaType::Other(other.to_string()),
214        })
215    }
216}
217
218impl Serialize for MediaType {
219    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
220    where
221        S: serde::Serializer,
222    {
223        serializer.serialize_str(self.as_str())
224    }
225}
226
227impl<'de> Deserialize<'de> for MediaType {
228    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
229    where
230        D: serde::Deserializer<'de>,
231    {
232        let s = String::deserialize(deserializer)?;
233        // FromStr is infallible for MediaType (unknown types become Other)
234        Ok(s.parse().unwrap())
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241
242    #[test]
243    fn test_media_type_parse() {
244        let mt: MediaType = "application/vnd.oci.image.manifest.v1+json"
245            .parse()
246            .unwrap();
247        assert_eq!(mt, MediaType::OciManifest);
248    }
249
250    #[test]
251    fn test_media_type_unknown() {
252        let mt: MediaType = "application/custom".parse().unwrap();
253        assert_eq!(mt, MediaType::Other("application/custom".to_string()));
254    }
255
256    #[test]
257    fn test_media_type_to_oci() {
258        assert_eq!(MediaType::DockerManifest.to_oci(), MediaType::OciManifest);
259        assert_eq!(MediaType::OciManifest.to_oci(), MediaType::OciManifest);
260    }
261
262    #[test]
263    fn test_media_type_to_docker_compatible() {
264        // gzip layers can convert
265        assert_eq!(
266            MediaType::OciLayerGzip.to_docker(),
267            MediaType::DockerLayerGzip
268        );
269        assert!(MediaType::OciLayerGzip.is_docker_compatible());
270    }
271
272    #[test]
273    fn test_media_type_to_docker_incompatible() {
274        // zstd and uncompressed cannot convert - return unchanged
275        assert_eq!(MediaType::OciLayerZstd.to_docker(), MediaType::OciLayerZstd);
276        assert_eq!(MediaType::OciLayer.to_docker(), MediaType::OciLayer);
277        assert!(!MediaType::OciLayerZstd.is_docker_compatible());
278        assert!(!MediaType::OciLayer.is_docker_compatible());
279    }
280
281    #[test]
282    fn test_media_type_nondistributable() {
283        assert!(MediaType::OciLayerNondistributableGzip.is_nondistributable());
284        assert!(MediaType::DockerForeignLayer.is_nondistributable());
285        assert!(!MediaType::OciLayerGzip.is_nondistributable());
286    }
287
288    #[test]
289    fn test_media_type_is_layer() {
290        assert!(MediaType::OciLayerGzip.is_layer());
291        assert!(MediaType::OciLayerNondistributableGzip.is_layer());
292        assert!(MediaType::DockerForeignLayer.is_layer());
293        assert!(!MediaType::OciManifest.is_layer());
294    }
295
296    #[test]
297    fn test_media_type_serde_roundtrip() {
298        let mt = MediaType::OciManifest;
299        let json = serde_json::to_string(&mt).unwrap();
300        let parsed: MediaType = serde_json::from_str(&json).unwrap();
301        assert_eq!(mt, parsed);
302    }
303
304    #[test]
305    fn test_media_type_nondistributable_roundtrip() {
306        let mt = MediaType::OciLayerNondistributableGzip;
307        let json = serde_json::to_string(&mt).unwrap();
308        let parsed: MediaType = serde_json::from_str(&json).unwrap();
309        assert_eq!(mt, parsed);
310    }
311}