Skip to main content

oci_client/
manifest.rs

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