oci_spec/image/
mod.rs

1//! [OCI image spec](https://github.com/opencontainers/image-spec) types and definitions.
2
3mod annotations;
4mod artifact;
5mod config;
6mod descriptor;
7mod digest;
8mod index;
9mod manifest;
10mod oci_layout;
11mod version;
12
13use std::fmt::Display;
14
15use serde::{Deserialize, Serialize};
16
17pub use annotations::*;
18pub use artifact::*;
19pub use config::*;
20pub use descriptor::*;
21pub use digest::*;
22pub use index::*;
23pub use manifest::*;
24pub use oci_layout::*;
25pub use version::*;
26
27/// Media types used by OCI image format spec. Values MUST comply with RFC 6838,
28/// including the naming requirements in its section 4.2.
29#[derive(Clone, Debug, PartialEq, Eq)]
30pub enum MediaType {
31    /// MediaType Descriptor specifies the media type for a content descriptor.
32    Descriptor,
33    /// MediaType LayoutHeader specifies the media type for the oci-layout.
34    LayoutHeader,
35    /// MediaType ImageManifest specifies the media type for an image manifest.
36    ImageManifest,
37    /// MediaType ImageIndex specifies the media type for an image index.
38    ImageIndex,
39    /// MediaType ImageLayer is the media type used for layers referenced by the
40    /// manifest.
41    ImageLayer,
42    /// MediaType ImageLayerGzip is the media type used for gzipped layers
43    /// referenced by the manifest.
44    ImageLayerGzip,
45    /// MediaType ImageLayerZstd is the media type used for zstd compressed
46    /// layers referenced by the manifest.
47    ImageLayerZstd,
48    /// MediaType ImageLayerNonDistributable is the media type for layers
49    /// referenced by the manifest but with distribution restrictions.
50    ImageLayerNonDistributable,
51    /// MediaType ImageLayerNonDistributableGzip is the media type for
52    /// gzipped layers referenced by the manifest but with distribution
53    /// restrictions.
54    ImageLayerNonDistributableGzip,
55    /// MediaType ImageLayerNonDistributableZstd is the media type for zstd
56    /// compressed layers referenced by the manifest but with distribution
57    /// restrictions.
58    ImageLayerNonDistributableZstd,
59    /// MediaType ImageConfig specifies the media type for the image
60    /// configuration.
61    ImageConfig,
62    /// MediaType ArtifactManifest specifies the media type used for content addressable
63    /// artifacts to store them along side container images in a registry.
64    ArtifactManifest,
65    /// MediaType EmptyJSON specifies a descriptor that has no content for the implementation. The
66    /// blob payload is the most minimal content that is still a valid JSON object: {} (size of 2).
67    /// The blob digest of {} is
68    /// sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a.
69    EmptyJSON,
70    /// MediaType not specified by OCI image format.
71    Other(String),
72}
73
74impl Display for MediaType {
75    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76        write!(f, "{}", self.as_ref())
77    }
78}
79
80impl From<&str> for MediaType {
81    fn from(media_type: &str) -> Self {
82        match media_type {
83            "application/vnd.oci.descriptor" => MediaType::Descriptor,
84            "application/vnd.oci.layout.header.v1+json" => MediaType::LayoutHeader,
85            "application/vnd.oci.image.manifest.v1+json" => MediaType::ImageManifest,
86            "application/vnd.oci.image.index.v1+json" => MediaType::ImageIndex,
87            "application/vnd.oci.image.layer.v1.tar" => MediaType::ImageLayer,
88            "application/vnd.oci.image.layer.v1.tar+gzip" => MediaType::ImageLayerGzip,
89            "application/vnd.oci.image.layer.v1.tar+zstd" => MediaType::ImageLayerZstd,
90            "application/vnd.oci.image.layer.nondistributable.v1.tar" => {
91                MediaType::ImageLayerNonDistributable
92            }
93            "application/vnd.oci.image.layer.nondistributable.v1.tar+gzip" => {
94                MediaType::ImageLayerNonDistributableGzip
95            }
96            "application/vnd.oci.image.layer.nondistributable.v1.tar+zstd" => {
97                MediaType::ImageLayerNonDistributableZstd
98            }
99            "application/vnd.oci.image.config.v1+json" => MediaType::ImageConfig,
100            "application/vnd.oci.artifact.manifest.v1+json" => MediaType::ArtifactManifest,
101            "application/vnd.oci.empty.v1+json" => MediaType::EmptyJSON,
102            media => MediaType::Other(media.to_owned()),
103        }
104    }
105}
106
107impl From<MediaType> for String {
108    fn from(media_type: MediaType) -> Self {
109        media_type.as_ref().to_owned()
110    }
111}
112
113impl AsRef<str> for MediaType {
114    fn as_ref(&self) -> &str {
115        match self {
116            Self::Descriptor => "application/vnd.oci.descriptor",
117            Self::LayoutHeader => "application/vnd.oci.layout.header.v1+json",
118            Self::ImageManifest => "application/vnd.oci.image.manifest.v1+json",
119            Self::ImageIndex => "application/vnd.oci.image.index.v1+json",
120            Self::ImageLayer => "application/vnd.oci.image.layer.v1.tar",
121            Self::ImageLayerGzip => "application/vnd.oci.image.layer.v1.tar+gzip",
122            Self::ImageLayerZstd => "application/vnd.oci.image.layer.v1.tar+zstd",
123            Self::ImageLayerNonDistributable => {
124                "application/vnd.oci.image.layer.nondistributable.v1.tar"
125            }
126            Self::ImageLayerNonDistributableGzip => {
127                "application/vnd.oci.image.layer.nondistributable.v1.tar+gzip"
128            }
129            Self::ImageLayerNonDistributableZstd => {
130                "application/vnd.oci.image.layer.nondistributable.v1.tar+zstd"
131            }
132            Self::ImageConfig => "application/vnd.oci.image.config.v1+json",
133            Self::ArtifactManifest => "application/vnd.oci.artifact.manifest.v1+json",
134            Self::EmptyJSON => "application/vnd.oci.empty.v1+json",
135            Self::Other(media_type) => media_type.as_str(),
136        }
137    }
138}
139
140/// Trait to get the Docker Image Manifest V2 Schema 2 media type for an OCI media type
141///
142/// This may be necessary for compatibility with tools that do not recognize the OCI media types.
143/// Where a [`MediaType`] is expected you can use `MediaType::ImageManifest.to_docker_v2s2()?` instead and
144/// `impl From<&str> for MediaType` will create a [`MediaType::Other`] for it.
145///
146/// Not all OCI Media Types have an equivalent Docker V2S2 Media Type. In those cases, `to_docker_v2s2` will error.
147pub trait ToDockerV2S2 {
148    /// Get the [Docker Image Manifest V2 Schema 2](https://docs.docker.com/registry/spec/manifest-v2-2/)
149    /// media type equivalent for an OCI media type
150    fn to_docker_v2s2(&self) -> Result<&str, std::fmt::Error>;
151}
152
153impl ToDockerV2S2 for MediaType {
154    fn to_docker_v2s2(&self) -> Result<&str, std::fmt::Error> {
155        Ok(match self {
156            Self::ImageIndex => "application/vnd.docker.distribution.manifest.list.v2+json",
157            Self::ImageManifest => "application/vnd.docker.distribution.manifest.v2+json",
158            Self::ImageConfig => "application/vnd.docker.container.image.v1+json",
159            Self::ImageLayerGzip => "application/vnd.docker.image.rootfs.diff.tar.gzip",
160            _ => return Err(std::fmt::Error),
161        })
162    }
163}
164
165impl Serialize for MediaType {
166    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
167    where
168        S: serde::Serializer,
169    {
170        let media_type = format!("{self}");
171        media_type.serialize(serializer)
172    }
173}
174
175impl<'de> Deserialize<'de> for MediaType {
176    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
177    where
178        D: serde::Deserializer<'de>,
179    {
180        let media_type = String::deserialize(deserializer)?;
181        Ok(media_type.as_str().into())
182    }
183}
184
185/// Name of the target operating system.
186#[allow(missing_docs)]
187#[derive(Clone, Debug, PartialEq, Eq)]
188pub enum Os {
189    AIX,
190    Android,
191    Darwin,
192    DragonFlyBSD,
193    FreeBSD,
194    Hurd,
195    Illumos,
196    #[allow(non_camel_case_types)]
197    iOS,
198    Js,
199    Linux,
200    Nacl,
201    NetBSD,
202    OpenBSD,
203    Plan9,
204    Solaris,
205    Windows,
206    #[allow(non_camel_case_types)]
207    zOS,
208    Other(String),
209}
210
211impl From<&str> for Os {
212    fn from(os: &str) -> Self {
213        match os {
214            "aix" => Os::AIX,
215            "android" => Os::Android,
216            "darwin" => Os::Darwin,
217            "dragonfly" => Os::DragonFlyBSD,
218            "freebsd" => Os::FreeBSD,
219            "hurd" => Os::Hurd,
220            "illumos" => Os::Illumos,
221            "ios" => Os::iOS,
222            "js" => Os::Js,
223            "linux" => Os::Linux,
224            "nacl" => Os::Nacl,
225            "netbsd" => Os::NetBSD,
226            "openbsd" => Os::OpenBSD,
227            "plan9" => Os::Plan9,
228            "solaris" => Os::Solaris,
229            "windows" => Os::Windows,
230            "zos" => Os::zOS,
231            name => Os::Other(name.to_owned()),
232        }
233    }
234}
235
236impl Display for Os {
237    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
238        let print = match self {
239            Os::AIX => "aix",
240            Os::Android => "android",
241            Os::Darwin => "darwin",
242            Os::DragonFlyBSD => "dragonfly",
243            Os::FreeBSD => "freebsd",
244            Os::Hurd => "hurd",
245            Os::Illumos => "illumos",
246            Os::iOS => "ios",
247            Os::Js => "js",
248            Os::Linux => "linux",
249            Os::Nacl => "nacl",
250            Os::NetBSD => "netbsd",
251            Os::OpenBSD => "openbsd",
252            Os::Plan9 => "plan9",
253            Os::Solaris => "solaris",
254            Os::Windows => "windows",
255            Os::zOS => "zos",
256            Os::Other(name) => name,
257        };
258
259        write!(f, "{print}")
260    }
261}
262
263impl Serialize for Os {
264    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
265    where
266        S: serde::Serializer,
267    {
268        let os = format!("{self}");
269        os.serialize(serializer)
270    }
271}
272
273impl<'de> Deserialize<'de> for Os {
274    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
275    where
276        D: serde::Deserializer<'de>,
277    {
278        let os = String::deserialize(deserializer)?;
279        Ok(os.as_str().into())
280    }
281}
282
283impl Default for Os {
284    fn default() -> Self {
285        Os::from(std::env::consts::OS)
286    }
287}
288
289/// Name of the CPU target architecture.
290#[derive(Clone, Debug, PartialEq, Eq)]
291pub enum Arch {
292    /// 32 bit x86, little-endian
293    #[allow(non_camel_case_types)]
294    i386,
295    /// 64 bit x86, little-endian
296    Amd64,
297    /// 64 bit x86 with 32 bit pointers, little-endian
298    Amd64p32,
299    /// 32 bit ARM, little-endian
300    ARM,
301    /// 32 bit ARM, big-endian
302    ARMbe,
303    /// 64 bit ARM, little-endian
304    ARM64,
305    /// 64 bit ARM, big-endian
306    ARM64be,
307    /// 64 bit Loongson RISC CPU, little-endian
308    LoongArch64,
309    /// 32 bit Mips, big-endian
310    Mips,
311    /// 32 bit Mips, little-endian
312    Mipsle,
313    /// 64 bit Mips, big-endian
314    Mips64,
315    /// 64 bit Mips, little-endian
316    Mips64le,
317    /// 64 bit Mips with 32 bit pointers, big-endian
318    Mips64p32,
319    /// 64 bit Mips with 32 bit pointers, little-endian
320    Mips64p32le,
321    /// 32 bit PowerPC, big endian
322    PowerPC,
323    /// 64 bit PowerPC, big-endian
324    PowerPC64,
325    /// 64 bit PowerPC, little-endian
326    PowerPC64le,
327    /// 32 bit RISC-V, little-endian
328    RISCV,
329    /// 64 bit RISC-V, little-endian
330    RISCV64,
331    /// 32 bit IBM System/390, big-endian
332    #[allow(non_camel_case_types)]
333    s390,
334    /// 64 bit IBM System/390, big-endian
335    #[allow(non_camel_case_types)]
336    s390x,
337    /// 32 bit SPARC, big-endian
338    SPARC,
339    /// 64 bit SPARC, bi-endian
340    SPARC64,
341    /// 32 bit Web Assembly
342    Wasm,
343    /// Architecture not specified by OCI image format
344    Other(String),
345}
346
347impl Display for Arch {
348    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
349        let print = match self {
350            Arch::i386 => "386",
351            Arch::Amd64 => "amd64",
352            Arch::Amd64p32 => "amd64p32",
353            Arch::ARM => "arm",
354            Arch::ARMbe => "armbe",
355            Arch::ARM64 => "arm64",
356            Arch::ARM64be => "arm64be",
357            Arch::LoongArch64 => "loong64",
358            Arch::Mips => "mips",
359            Arch::Mipsle => "mipsle",
360            Arch::Mips64 => "mips64",
361            Arch::Mips64le => "mips64le",
362            Arch::Mips64p32 => "mips64p32",
363            Arch::Mips64p32le => "mips64p32le",
364            Arch::PowerPC => "ppc",
365            Arch::PowerPC64 => "ppc64",
366            Arch::PowerPC64le => "ppc64le",
367            Arch::RISCV => "riscv",
368            Arch::RISCV64 => "riscv64",
369            Arch::s390 => "s390",
370            Arch::s390x => "s390x",
371            Arch::SPARC => "sparc",
372            Arch::SPARC64 => "sparc64",
373            Arch::Wasm => "wasm",
374            Arch::Other(arch) => arch,
375        };
376
377        write!(f, "{print}")
378    }
379}
380
381impl From<&str> for Arch {
382    fn from(arch: &str) -> Self {
383        match arch {
384            "386" => Arch::i386,
385            "amd64" => Arch::Amd64,
386            "amd64p32" => Arch::Amd64p32,
387            "arm" => Arch::ARM,
388            "armbe" => Arch::ARM64be,
389            "arm64" => Arch::ARM64,
390            "arm64be" => Arch::ARM64be,
391            "loong64" => Arch::LoongArch64,
392            "mips" => Arch::Mips,
393            "mipsle" => Arch::Mipsle,
394            "mips64" => Arch::Mips64,
395            "mips64le" => Arch::Mips64le,
396            "mips64p32" => Arch::Mips64p32,
397            "mips64p32le" => Arch::Mips64p32le,
398            "ppc" => Arch::PowerPC,
399            "ppc64" => Arch::PowerPC64,
400            "ppc64le" => Arch::PowerPC64le,
401            "riscv" => Arch::RISCV,
402            "riscv64" => Arch::RISCV64,
403            "s390" => Arch::s390,
404            "s390x" => Arch::s390x,
405            "sparc" => Arch::SPARC,
406            "sparc64" => Arch::SPARC64,
407            "wasm" => Arch::Wasm,
408            arch => Arch::Other(arch.to_owned()),
409        }
410    }
411}
412
413impl Serialize for Arch {
414    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
415    where
416        S: serde::Serializer,
417    {
418        let arch = format!("{self}");
419        arch.serialize(serializer)
420    }
421}
422
423impl<'de> Deserialize<'de> for Arch {
424    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
425    where
426        D: serde::Deserializer<'de>,
427    {
428        let arch = String::deserialize(deserializer)?;
429        Ok(arch.as_str().into())
430    }
431}
432
433impl Default for Arch {
434    fn default() -> Self {
435        // Translate from the Rust architecture names to the Go versions.
436        // It seems like the Rust ones are the same GNU/Linux...except for `powerpc64` and not `ppc64le`?
437        // This list just contains exceptions, everything else is passed through literally.
438        // See also https://github.com/containerd/containerd/blob/140ecc9247386d3be21616fe285021c081f4ea08/platforms/database.go
439        let goarch = match std::env::consts::ARCH {
440            "x86_64" => "amd64",
441            "aarch64" => "arm64",
442            "powerpc64" if cfg!(target_endian = "big") => "ppc64",
443            "powerpc64" if cfg!(target_endian = "little") => "ppc64le",
444            o => o,
445        };
446        Arch::from(goarch)
447    }
448}
449
450#[cfg(test)]
451mod tests {
452    use super::*;
453
454    #[test]
455    fn test_arch_translation() {
456        let a = Arch::default();
457        // If you hit this, please update the mapping above.
458        if let Arch::Other(o) = a {
459            panic!("Architecture {o} not mapped between Rust and OCI")
460        }
461    }
462
463    #[test]
464    fn test_asref() {
465        // This just spot checks a few conversions
466        assert_eq!(
467            MediaType::ImageConfig.as_ref(),
468            "application/vnd.oci.image.config.v1+json"
469        );
470        assert_eq!(
471            String::from(MediaType::ImageConfig).as_str(),
472            "application/vnd.oci.image.config.v1+json"
473        );
474    }
475}