oci_distribution/
manifest.rs

1//! OCI Manifest
2use std::collections::HashMap;
3
4use crate::{
5    client::{Config, ImageLayer},
6    sha256_digest,
7};
8
9/// The mediatype for WASM layers.
10pub const WASM_LAYER_MEDIA_TYPE: &str = "application/vnd.wasm.content.layer.v1+wasm";
11/// The mediatype for a WASM image config.
12pub const WASM_CONFIG_MEDIA_TYPE: &str = "application/vnd.wasm.config.v1+json";
13/// The mediatype for an docker v2 schema 2 manifest.
14pub const IMAGE_MANIFEST_MEDIA_TYPE: &str = "application/vnd.docker.distribution.manifest.v2+json";
15/// The mediatype for an docker v2 shema 2 manifest list.
16pub const IMAGE_MANIFEST_LIST_MEDIA_TYPE: &str =
17    "application/vnd.docker.distribution.manifest.list.v2+json";
18/// The mediatype for an OCI image index manifest.
19pub const OCI_IMAGE_INDEX_MEDIA_TYPE: &str = "application/vnd.oci.image.index.v1+json";
20/// The mediatype for an OCI image manifest.
21pub const OCI_IMAGE_MEDIA_TYPE: &str = "application/vnd.oci.image.manifest.v1+json";
22/// The mediatype for an image config (manifest).
23pub const IMAGE_CONFIG_MEDIA_TYPE: &str = "application/vnd.oci.image.config.v1+json";
24/// The mediatype that Docker uses for image configs.
25pub const IMAGE_DOCKER_CONFIG_MEDIA_TYPE: &str = "application/vnd.docker.container.image.v1+json";
26/// The mediatype for a layer.
27pub const IMAGE_LAYER_MEDIA_TYPE: &str = "application/vnd.oci.image.layer.v1.tar";
28/// The mediatype for a layer that is gzipped.
29pub const IMAGE_LAYER_GZIP_MEDIA_TYPE: &str = "application/vnd.oci.image.layer.v1.tar+gzip";
30/// The mediatype that Docker uses for a layer that is tarred.
31pub const IMAGE_DOCKER_LAYER_TAR_MEDIA_TYPE: &str = "application/vnd.docker.image.rootfs.diff.tar";
32/// The mediatype that Docker uses for a layer that is gzipped.
33pub const IMAGE_DOCKER_LAYER_GZIP_MEDIA_TYPE: &str =
34    "application/vnd.docker.image.rootfs.diff.tar.gzip";
35/// The mediatype for a layer that is nondistributable.
36pub const IMAGE_LAYER_NONDISTRIBUTABLE_MEDIA_TYPE: &str =
37    "application/vnd.oci.image.layer.nondistributable.v1.tar";
38/// The mediatype for a layer that is nondistributable and gzipped.
39pub const IMAGE_LAYER_NONDISTRIBUTABLE_GZIP_MEDIA_TYPE: &str =
40    "application/vnd.oci.image.layer.nondistributable.v1.tar+gzip";
41
42/// An image, or image index, OCI manifest
43#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
44#[serde(untagged)]
45pub enum OciManifest {
46    /// An OCI image manifest
47    Image(OciImageManifest),
48    /// An OCI image index manifest
49    ImageIndex(OciImageIndex),
50}
51
52impl OciManifest {
53    /// Returns the appropriate content-type for each variant.
54    pub fn content_type(&self) -> &str {
55        match self {
56            OciManifest::Image(image) => {
57                image.media_type.as_deref().unwrap_or(OCI_IMAGE_MEDIA_TYPE)
58            }
59            OciManifest::ImageIndex(image) => image
60                .media_type
61                .as_deref()
62                .unwrap_or(IMAGE_MANIFEST_LIST_MEDIA_TYPE),
63        }
64    }
65}
66
67/// The OCI image manifest describes an OCI image.
68///
69/// It is part of the OCI specification, and is defined [here](https://github.com/opencontainers/image-spec/blob/main/manifest.md)
70#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
71#[serde(rename_all = "camelCase")]
72pub struct OciImageManifest {
73    /// This is a schema version.
74    ///
75    /// The specification does not specify the width of this integer.
76    /// However, the only version allowed by the specification is `2`.
77    /// So we have made this a u8.
78    pub schema_version: u8,
79
80    /// This is an optional media type describing this manifest.
81    ///
82    /// This property SHOULD be used and [remain compatible](https://github.com/opencontainers/image-spec/blob/main/media-types.md#compatibility-matrix)
83    /// with earlier versions of this specification and with other similar external formats.
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub media_type: Option<String>,
86
87    /// The image configuration.
88    ///
89    /// This object is required.
90    pub config: OciDescriptor,
91
92    /// The OCI image layers
93    ///
94    /// The specification is unclear whether this is required. We have left it
95    /// required, assuming an empty vector can be used if necessary.
96    pub layers: Vec<OciDescriptor>,
97
98    /// The OCI artifact type
99    ///
100    /// This OPTIONAL property contains the type of an artifact when the manifest is used for an
101    /// artifact. This MUST be set when config.mediaType is set to the empty value. If defined,
102    /// the value MUST comply with RFC 6838, including the naming requirements in its section 4.2,
103    /// and MAY be registered with IANA. Implementations storing or copying image manifests
104    /// MUST NOT error on encountering an artifactType that is unknown to the implementation.
105    ///
106    /// Introduced in OCI Image Format spec v1.1
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub artifact_type: Option<String>,
109
110    /// The annotations for this manifest
111    ///
112    /// The specification says "If there are no annotations then this property
113    /// MUST either be absent or be an empty map."
114    /// TO accomodate either, this is optional.
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub annotations: Option<HashMap<String, String>>,
117}
118
119impl Default for OciImageManifest {
120    fn default() -> Self {
121        OciImageManifest {
122            schema_version: 2,
123            media_type: None,
124            config: OciDescriptor::default(),
125            layers: vec![],
126            artifact_type: None,
127            annotations: None,
128        }
129    }
130}
131
132impl OciImageManifest {
133    /// Create a new OciImageManifest using the given parameters
134    ///
135    /// This can be useful to create an OCI Image Manifest with
136    /// custom annotations.
137    pub fn build(
138        layers: &[ImageLayer],
139        config: &Config,
140        annotations: Option<HashMap<String, String>>,
141    ) -> Self {
142        let mut manifest = OciImageManifest::default();
143
144        manifest.config.media_type = config.media_type.to_string();
145        manifest.config.size = config.data.len() as i64;
146        manifest.config.digest = sha256_digest(&config.data);
147        manifest.annotations = annotations;
148
149        for layer in layers {
150            let digest = sha256_digest(&layer.data);
151
152            let descriptor = OciDescriptor {
153                size: layer.data.len() as i64,
154                digest,
155                media_type: layer.media_type.to_string(),
156                annotations: layer.annotations.clone(),
157                ..Default::default()
158            };
159
160            manifest.layers.push(descriptor);
161        }
162
163        manifest
164    }
165}
166
167impl From<OciImageIndex> for OciManifest {
168    fn from(m: OciImageIndex) -> Self {
169        Self::ImageIndex(m)
170    }
171}
172impl From<OciImageManifest> for OciManifest {
173    fn from(m: OciImageManifest) -> Self {
174        Self::Image(m)
175    }
176}
177
178impl std::fmt::Display for OciManifest {
179    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
180        match self {
181            OciManifest::Image(oci_image_manifest) => write!(f, "{}", oci_image_manifest),
182            OciManifest::ImageIndex(oci_image_index) => write!(f, "{}", oci_image_index),
183        }
184    }
185}
186
187impl std::fmt::Display for OciImageIndex {
188    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189        let media_type = self
190            .media_type
191            .clone()
192            .unwrap_or_else(|| String::from("N/A"));
193        let manifests: Vec<String> = self.manifests.iter().map(|m| m.to_string()).collect();
194        write!(
195            f,
196            "OCI Image Index( schema-version: '{}', media-type: '{}', manifests: '{}' )",
197            self.schema_version,
198            media_type,
199            manifests.join(","),
200        )
201    }
202}
203
204impl std::fmt::Display for OciImageManifest {
205    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
206        let media_type = self
207            .media_type
208            .clone()
209            .unwrap_or_else(|| String::from("N/A"));
210        let annotations = self.annotations.clone().unwrap_or_default();
211        let layers: Vec<String> = self.layers.iter().map(|l| l.to_string()).collect();
212
213        write!(
214            f,
215            "OCI Image Manifest( schema-version: '{}', media-type: '{}', config: '{}', artifact-type: '{:?}', layers: '{:?}', annotations: '{:?}' )",
216            self.schema_version,
217            media_type,
218            self.config,
219            self.artifact_type,
220            layers,
221            annotations,
222        )
223    }
224}
225
226/// Versioned provides a struct with the manifest's schemaVersion and mediaType.
227/// Incoming content with unknown schema versions can be decoded against this
228/// struct to check the version.
229#[derive(Clone, Debug, serde::Deserialize)]
230#[serde(rename_all = "camelCase")]
231pub struct Versioned {
232    /// schema_version is the image manifest schema that this image follows
233    pub schema_version: i32,
234
235    /// media_type is the media type of this schema.
236    #[serde(skip_serializing_if = "Option::is_none")]
237    pub media_type: Option<String>,
238}
239
240/// The OCI descriptor is a generic object used to describe other objects.
241///
242/// It is defined in the [OCI Image Specification](https://github.com/opencontainers/image-spec/blob/main/descriptor.md#properties):
243#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
244#[serde(rename_all = "camelCase")]
245pub struct OciDescriptor {
246    /// The media type of this descriptor.
247    ///
248    /// Layers, config, and manifests may all have descriptors. Each
249    /// is differentiated by its mediaType.
250    ///
251    /// This REQUIRED property contains the media type of the referenced
252    /// content. Values MUST comply with RFC 6838, including the naming
253    /// requirements in its section 4.2.
254    pub media_type: String,
255    /// The SHA 256 or 512 digest of the object this describes.
256    ///
257    /// This REQUIRED property is the digest of the targeted content, conforming
258    /// to the requirements outlined in Digests. Retrieved content SHOULD be
259    /// verified against this digest when consumed via untrusted sources.
260    pub digest: String,
261    /// The size, in bytes, of the object this describes.
262    ///
263    /// This REQUIRED property specifies the size, in bytes, of the raw
264    /// content. This property exists so that a client will have an expected
265    /// size for the content before processing. If the length of the retrieved
266    /// content does not match the specified length, the content SHOULD NOT be
267    /// trusted.
268    pub size: i64,
269    /// This OPTIONAL property specifies a list of URIs from which this
270    /// object MAY be downloaded. Each entry MUST conform to RFC 3986.
271    /// Entries SHOULD use the http and https schemes, as defined in RFC 7230.
272    #[serde(skip_serializing_if = "Option::is_none")]
273    pub urls: Option<Vec<String>>,
274
275    /// This OPTIONAL property contains arbitrary metadata for this descriptor.
276    /// This OPTIONAL property MUST use the annotation rules.
277    /// <https://github.com/opencontainers/image-spec/blob/main/annotations.md#rules>
278    #[serde(skip_serializing_if = "Option::is_none")]
279    pub annotations: Option<HashMap<String, String>>,
280}
281
282impl std::fmt::Display for OciDescriptor {
283    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
284        let urls = self.urls.clone().unwrap_or_default();
285        let annotations = self.annotations.clone().unwrap_or_default();
286
287        write!(
288            f,
289            "( media-type: '{}', digest: '{}', size: '{}', urls: '{:?}', annotations: '{:?}' )",
290            self.media_type, self.digest, self.size, urls, annotations,
291        )
292    }
293}
294
295impl Default for OciDescriptor {
296    fn default() -> Self {
297        OciDescriptor {
298            media_type: IMAGE_CONFIG_MEDIA_TYPE.to_owned(),
299            digest: "".to_owned(),
300            size: 0,
301            urls: None,
302            annotations: None,
303        }
304    }
305}
306
307/// The image index is a higher-level manifest which points to specific image manifest.
308///
309/// It is part of the OCI specification, and is defined [here](https://github.com/opencontainers/image-spec/blob/main/image-index.md):
310#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
311#[serde(rename_all = "camelCase")]
312pub struct OciImageIndex {
313    /// This is a schema version.
314    ///
315    /// The specification does not specify the width of this integer.
316    /// However, the only version allowed by the specification is `2`.
317    /// So we have made this a u8.
318    pub schema_version: u8,
319
320    /// This is an optional media type describing this manifest.
321    ///
322    /// It is reserved for compatibility, but the specification does not seem
323    /// to recommend setting it.
324    #[serde(skip_serializing_if = "Option::is_none")]
325    pub media_type: Option<String>,
326
327    /// This property contains a list of manifests for specific platforms.
328    /// The spec says this field must be present but the value may be an empty array.
329    pub manifests: Vec<ImageIndexEntry>,
330
331    /// The annotations for this manifest
332    ///
333    /// The specification says "If there are no annotations then this property
334    /// MUST either be absent or be an empty map."
335    /// TO accomodate either, this is optional.
336    #[serde(skip_serializing_if = "Option::is_none")]
337    pub annotations: Option<HashMap<String, String>>,
338}
339
340/// The manifest entry of an `ImageIndex`.
341///
342/// It is part of the OCI specification, and is defined in the `manifests`
343/// section [here](https://github.com/opencontainers/image-spec/blob/main/image-index.md#image-index-property-descriptions):
344#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
345#[serde(rename_all = "camelCase")]
346pub struct ImageIndexEntry {
347    /// The media type of this descriptor.
348    ///
349    /// Layers, config, and manifests may all have descriptors. Each
350    /// is differentiated by its mediaType.
351    ///
352    /// This REQUIRED property contains the media type of the referenced
353    /// content. Values MUST comply with RFC 6838, including the naming
354    /// requirements in its section 4.2.
355    pub media_type: String,
356    /// The SHA 256 or 512 digest of the object this describes.
357    ///
358    /// This REQUIRED property is the digest of the targeted content, conforming
359    /// to the requirements outlined in Digests. Retrieved content SHOULD be
360    /// verified against this digest when consumed via untrusted sources.
361    pub digest: String,
362    /// The size, in bytes, of the object this describes.
363    ///
364    /// This REQUIRED property specifies the size, in bytes, of the raw
365    /// content. This property exists so that a client will have an expected
366    /// size for the content before processing. If the length of the retrieved
367    /// content does not match the specified length, the content SHOULD NOT be
368    /// trusted.
369    pub size: i64,
370    /// This OPTIONAL property describes the minimum runtime requirements of the image.
371    /// This property SHOULD be present if its target is platform-specific.
372    #[serde(skip_serializing_if = "Option::is_none")]
373    pub platform: Option<Platform>,
374
375    /// This OPTIONAL property contains arbitrary metadata for the image index.
376    /// This OPTIONAL property MUST use the [annotation rules](https://github.com/opencontainers/image-spec/blob/main/annotations.md#rules).
377    #[serde(skip_serializing_if = "Option::is_none")]
378    pub annotations: Option<HashMap<String, String>>,
379}
380
381impl std::fmt::Display for ImageIndexEntry {
382    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
383        let platform = self
384            .platform
385            .clone()
386            .map(|p| p.to_string())
387            .unwrap_or_else(|| String::from("N/A"));
388        let annotations = self.annotations.clone().unwrap_or_default();
389
390        write!(
391            f,
392            "(media-type: '{}', digest: '{}', size: '{}', platform: '{}', annotations: {:?})",
393            self.media_type, self.digest, self.size, platform, annotations,
394        )
395    }
396}
397
398/// Platform specific fields of an Image Index manifest entry.
399///
400/// It is part of the OCI specification, and is in the `platform`
401/// section [here](https://github.com/opencontainers/image-spec/blob/main/image-index.md#image-index-property-descriptions):
402#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, PartialEq, Eq)]
403#[serde(rename_all = "camelCase")]
404pub struct Platform {
405    /// This REQUIRED property specifies the CPU architecture.
406    /// Image indexes SHOULD use, and implementations SHOULD understand, values
407    /// listed in the Go Language document for [`GOARCH`](https://golang.org/doc/install/source#environment).
408    pub architecture: String,
409    /// This REQUIRED property specifies the operating system.
410    /// Image indexes SHOULD use, and implementations SHOULD understand, values
411    /// listed in the Go Language document for [`GOOS`](https://golang.org/doc/install/source#environment).
412    pub os: String,
413    /// This OPTIONAL property specifies the version of the operating system
414    /// targeted by the referenced blob.
415    /// Implementations MAY refuse to use manifests where `os.version` is not known
416    /// to work with the host OS version.
417    /// Valid values are implementation-defined. e.g. `10.0.14393.1066` on `windows`.
418    #[serde(rename = "os.version")]
419    #[serde(skip_serializing_if = "Option::is_none")]
420    pub os_version: Option<String>,
421    /// This OPTIONAL property specifies an array of strings, each specifying a mandatory OS feature.
422    /// When `os` is `windows`, image indexes SHOULD use, and implementations SHOULD understand the following values:
423    /// - `win32k`: image requires `win32k.sys` on the host (Note: `win32k.sys` is missing on Nano Server)
424    /// When `os` is not `windows`, values are implementation-defined and SHOULD be submitted to this specification for standardization.
425    #[serde(rename = "os.features")]
426    #[serde(skip_serializing_if = "Option::is_none")]
427    pub os_features: Option<Vec<String>>,
428    /// This OPTIONAL property specifies the variant of the CPU.
429    /// Image indexes SHOULD use, and implementations SHOULD understand, `variant` values listed in the [Platform Variants](#platform-variants) table.
430    #[serde(skip_serializing_if = "Option::is_none")]
431    pub variant: Option<String>,
432    /// This property is RESERVED for future versions of the specification.
433    #[serde(skip_serializing_if = "Option::is_none")]
434    pub features: Option<Vec<String>>,
435}
436
437impl std::fmt::Display for Platform {
438    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
439        let os_version = self
440            .os_version
441            .clone()
442            .unwrap_or_else(|| String::from("N/A"));
443        let os_features = self.os_features.clone().unwrap_or_default();
444        let variant = self.variant.clone().unwrap_or_else(|| String::from("N/A"));
445        let features = self.os_features.clone().unwrap_or_default();
446        write!(f, "( architecture: '{}', os: '{}', os-version: '{}', os-features: '{:?}', variant: '{}', features: '{:?}' )",
447            self.architecture,
448            self.os,
449            os_version,
450            os_features,
451            variant,
452            features,
453        )
454    }
455}
456
457#[cfg(test)]
458mod test {
459    use super::*;
460
461    const TEST_MANIFEST: &str = r#"{
462        "schemaVersion": 2,
463        "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
464        "config": {
465            "mediaType": "application/vnd.docker.container.image.v1+json",
466            "size": 2,
467            "digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"
468        },
469        "artifactType": "application/vnd.wasm.component.v1+wasm",
470        "layers": [
471            {
472                "mediaType": "application/vnd.wasm.content.layer.v1+wasm",
473                "size": 1615998,
474                "digest": "sha256:f9c91f4c280ab92aff9eb03b279c4774a80b84428741ab20855d32004b2b983f",
475                "annotations": {
476                    "org.opencontainers.image.title": "module.wasm"
477                }
478            }
479        ]
480    }
481    "#;
482
483    #[test]
484    fn test_manifest() {
485        let manifest: OciImageManifest =
486            serde_json::from_str(TEST_MANIFEST).expect("parsed manifest");
487        assert_eq!(2, manifest.schema_version);
488        assert_eq!(
489            Some(IMAGE_MANIFEST_MEDIA_TYPE.to_owned()),
490            manifest.media_type
491        );
492        let config = manifest.config;
493        // Note that this is the Docker config media type, not the OCI one.
494        assert_eq!(IMAGE_DOCKER_CONFIG_MEDIA_TYPE.to_owned(), config.media_type);
495        assert_eq!(2, config.size);
496        assert_eq!(
497            "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a".to_owned(),
498            config.digest
499        );
500        assert_eq!(
501            "application/vnd.wasm.component.v1+wasm".to_owned(),
502            manifest.artifact_type.unwrap()
503        );
504
505        assert_eq!(1, manifest.layers.len());
506        let wasm_layer = &manifest.layers[0];
507        assert_eq!(1_615_998, wasm_layer.size);
508        assert_eq!(WASM_LAYER_MEDIA_TYPE.to_owned(), wasm_layer.media_type);
509        assert_eq!(
510            1,
511            wasm_layer
512                .annotations
513                .as_ref()
514                .expect("annotations map")
515                .len()
516        );
517    }
518}