1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
//! OCI Manifest
use std::collections::BTreeMap;

use crate::{
    client::{Config, ImageLayer},
    sha256_digest,
};

/// The mediatype for WASM layers.
pub const WASM_LAYER_MEDIA_TYPE: &str = "application/vnd.wasm.content.layer.v1+wasm";
/// The mediatype for a WASM image config.
pub const WASM_CONFIG_MEDIA_TYPE: &str = "application/vnd.wasm.config.v1+json";
/// The mediatype for an docker v2 schema 2 manifest.
pub const IMAGE_MANIFEST_MEDIA_TYPE: &str = "application/vnd.docker.distribution.manifest.v2+json";
/// The mediatype for an docker v2 shema 2 manifest list.
pub const IMAGE_MANIFEST_LIST_MEDIA_TYPE: &str =
    "application/vnd.docker.distribution.manifest.list.v2+json";
/// The mediatype for an OCI image index manifest.
pub const OCI_IMAGE_INDEX_MEDIA_TYPE: &str = "application/vnd.oci.image.index.v1+json";
/// The mediatype for an OCI image manifest.
pub const OCI_IMAGE_MEDIA_TYPE: &str = "application/vnd.oci.image.manifest.v1+json";
/// The mediatype for an image config (manifest).
pub const IMAGE_CONFIG_MEDIA_TYPE: &str = "application/vnd.oci.image.config.v1+json";
/// The mediatype that Docker uses for image configs.
pub const IMAGE_DOCKER_CONFIG_MEDIA_TYPE: &str = "application/vnd.docker.container.image.v1+json";
/// The mediatype for a layer.
pub const IMAGE_LAYER_MEDIA_TYPE: &str = "application/vnd.oci.image.layer.v1.tar";
/// The mediatype for a layer that is gzipped.
pub const IMAGE_LAYER_GZIP_MEDIA_TYPE: &str = "application/vnd.oci.image.layer.v1.tar+gzip";
/// The mediatype that Docker uses for a layer that is tarred.
pub const IMAGE_DOCKER_LAYER_TAR_MEDIA_TYPE: &str = "application/vnd.docker.image.rootfs.diff.tar";
/// The mediatype that Docker uses for a layer that is gzipped.
pub const IMAGE_DOCKER_LAYER_GZIP_MEDIA_TYPE: &str =
    "application/vnd.docker.image.rootfs.diff.tar.gzip";
/// The mediatype for a layer that is nondistributable.
pub const IMAGE_LAYER_NONDISTRIBUTABLE_MEDIA_TYPE: &str =
    "application/vnd.oci.image.layer.nondistributable.v1.tar";
/// The mediatype for a layer that is nondistributable and gzipped.
pub const IMAGE_LAYER_NONDISTRIBUTABLE_GZIP_MEDIA_TYPE: &str =
    "application/vnd.oci.image.layer.nondistributable.v1.tar+gzip";

/// An image, or image index, OCI manifest
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
#[serde(untagged)]
pub enum OciManifest {
    /// An OCI image manifest
    Image(OciImageManifest),
    /// An OCI image index manifest
    ImageIndex(OciImageIndex),
}

impl OciManifest {
    /// Returns the appropriate content-type for each variant.
    pub fn content_type(&self) -> &str {
        match self {
            OciManifest::Image(image) => {
                image.media_type.as_deref().unwrap_or(OCI_IMAGE_MEDIA_TYPE)
            }
            OciManifest::ImageIndex(image) => image
                .media_type
                .as_deref()
                .unwrap_or(IMAGE_MANIFEST_LIST_MEDIA_TYPE),
        }
    }
}

/// The OCI image manifest describes an OCI image.
///
/// It is part of the OCI specification, and is defined [here](https://github.com/opencontainers/image-spec/blob/main/manifest.md)
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OciImageManifest {
    /// This is a schema version.
    ///
    /// The specification does not specify the width of this integer.
    /// However, the only version allowed by the specification is `2`.
    /// So we have made this a u8.
    pub schema_version: u8,

    /// This is an optional media type describing this manifest.
    ///
    /// This property SHOULD be used and [remain compatible](https://github.com/opencontainers/image-spec/blob/main/media-types.md#compatibility-matrix)
    /// with earlier versions of this specification and with other similar external formats.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub media_type: Option<String>,

    /// The image configuration.
    ///
    /// This object is required.
    pub config: OciDescriptor,

    /// The OCI image layers
    ///
    /// The specification is unclear whether this is required. We have left it
    /// required, assuming an empty vector can be used if necessary.
    pub layers: Vec<OciDescriptor>,

    /// The OCI artifact type
    ///
    /// This OPTIONAL property contains the type of an artifact when the manifest is used for an
    /// artifact. This MUST be set when config.mediaType is set to the empty value. If defined,
    /// the value MUST comply with RFC 6838, including the naming requirements in its section 4.2,
    /// and MAY be registered with IANA. Implementations storing or copying image manifests
    /// MUST NOT error on encountering an artifactType that is unknown to the implementation.
    ///
    /// Introduced in OCI Image Format spec v1.1
    #[serde(skip_serializing_if = "Option::is_none")]
    pub artifact_type: Option<String>,

    /// The annotations for this manifest
    ///
    /// The specification says "If there are no annotations then this property
    /// MUST either be absent or be an empty map."
    /// TO accomodate either, this is optional.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub annotations: Option<BTreeMap<String, String>>,
}

impl Default for OciImageManifest {
    fn default() -> Self {
        OciImageManifest {
            schema_version: 2,
            media_type: None,
            config: OciDescriptor::default(),
            layers: vec![],
            artifact_type: None,
            annotations: None,
        }
    }
}

impl OciImageManifest {
    /// Create a new OciImageManifest using the given parameters
    ///
    /// This can be useful to create an OCI Image Manifest with
    /// custom annotations.
    pub fn build(
        layers: &[ImageLayer],
        config: &Config,
        annotations: Option<BTreeMap<String, String>>,
    ) -> Self {
        let mut manifest = OciImageManifest::default();

        manifest.config.media_type = config.media_type.to_string();
        manifest.config.size = config.data.len() as i64;
        manifest.config.digest = sha256_digest(&config.data);
        manifest.annotations = annotations;

        for layer in layers {
            let digest = sha256_digest(&layer.data);

            let descriptor = OciDescriptor {
                size: layer.data.len() as i64,
                digest,
                media_type: layer.media_type.to_string(),
                annotations: layer.annotations.clone(),
                ..Default::default()
            };

            manifest.layers.push(descriptor);
        }

        manifest
    }
}

impl From<OciImageIndex> for OciManifest {
    fn from(m: OciImageIndex) -> Self {
        Self::ImageIndex(m)
    }
}
impl From<OciImageManifest> for OciManifest {
    fn from(m: OciImageManifest) -> Self {
        Self::Image(m)
    }
}

impl std::fmt::Display for OciManifest {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            OciManifest::Image(oci_image_manifest) => write!(f, "{}", oci_image_manifest),
            OciManifest::ImageIndex(oci_image_index) => write!(f, "{}", oci_image_index),
        }
    }
}

impl std::fmt::Display for OciImageIndex {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let media_type = self
            .media_type
            .clone()
            .unwrap_or_else(|| String::from("N/A"));
        let manifests: Vec<String> = self.manifests.iter().map(|m| m.to_string()).collect();
        write!(
            f,
            "OCI Image Index( schema-version: '{}', media-type: '{}', manifests: '{}' )",
            self.schema_version,
            media_type,
            manifests.join(","),
        )
    }
}

impl std::fmt::Display for OciImageManifest {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let media_type = self
            .media_type
            .clone()
            .unwrap_or_else(|| String::from("N/A"));
        let annotations = self.annotations.clone().unwrap_or_default();
        let layers: Vec<String> = self.layers.iter().map(|l| l.to_string()).collect();

        write!(
            f,
            "OCI Image Manifest( schema-version: '{}', media-type: '{}', config: '{}', artifact-type: '{:?}', layers: '{:?}', annotations: '{:?}' )",
            self.schema_version,
            media_type,
            self.config,
            self.artifact_type,
            layers,
            annotations,
        )
    }
}

/// Versioned provides a struct with the manifest's schemaVersion and mediaType.
/// Incoming content with unknown schema versions can be decoded against this
/// struct to check the version.
#[derive(Clone, Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Versioned {
    /// schema_version is the image manifest schema that this image follows
    pub schema_version: i32,

    /// media_type is the media type of this schema.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub media_type: Option<String>,
}

/// The OCI descriptor is a generic object used to describe other objects.
///
/// It is defined in the [OCI Image Specification](https://github.com/opencontainers/image-spec/blob/main/descriptor.md#properties):
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OciDescriptor {
    /// The media type of this descriptor.
    ///
    /// Layers, config, and manifests may all have descriptors. Each
    /// is differentiated by its mediaType.
    ///
    /// This REQUIRED property contains the media type of the referenced
    /// content. Values MUST comply with RFC 6838, including the naming
    /// requirements in its section 4.2.
    pub media_type: String,
    /// The SHA 256 or 512 digest of the object this describes.
    ///
    /// This REQUIRED property is the digest of the targeted content, conforming
    /// to the requirements outlined in Digests. Retrieved content SHOULD be
    /// verified against this digest when consumed via untrusted sources.
    pub digest: String,
    /// The size, in bytes, of the object this describes.
    ///
    /// This REQUIRED property specifies the size, in bytes, of the raw
    /// content. This property exists so that a client will have an expected
    /// size for the content before processing. If the length of the retrieved
    /// content does not match the specified length, the content SHOULD NOT be
    /// trusted.
    pub size: i64,
    /// This OPTIONAL property specifies a list of URIs from which this
    /// object MAY be downloaded. Each entry MUST conform to RFC 3986.
    /// Entries SHOULD use the http and https schemes, as defined in RFC 7230.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub urls: Option<Vec<String>>,

    /// This OPTIONAL property contains arbitrary metadata for this descriptor.
    /// This OPTIONAL property MUST use the annotation rules.
    /// <https://github.com/opencontainers/image-spec/blob/main/annotations.md#rules>
    #[serde(skip_serializing_if = "Option::is_none")]
    pub annotations: Option<BTreeMap<String, String>>,
}

impl std::fmt::Display for OciDescriptor {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let urls = self.urls.clone().unwrap_or_default();
        let annotations = self.annotations.clone().unwrap_or_default();

        write!(
            f,
            "( media-type: '{}', digest: '{}', size: '{}', urls: '{:?}', annotations: '{:?}' )",
            self.media_type, self.digest, self.size, urls, annotations,
        )
    }
}

impl Default for OciDescriptor {
    fn default() -> Self {
        OciDescriptor {
            media_type: IMAGE_CONFIG_MEDIA_TYPE.to_owned(),
            digest: "".to_owned(),
            size: 0,
            urls: None,
            annotations: None,
        }
    }
}

/// The image index is a higher-level manifest which points to specific image manifest.
///
/// It is part of the OCI specification, and is defined [here](https://github.com/opencontainers/image-spec/blob/main/image-index.md):
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OciImageIndex {
    /// This is a schema version.
    ///
    /// The specification does not specify the width of this integer.
    /// However, the only version allowed by the specification is `2`.
    /// So we have made this a u8.
    pub schema_version: u8,

    /// This is an optional media type describing this manifest.
    ///
    /// It is reserved for compatibility, but the specification does not seem
    /// to recommend setting it.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub media_type: Option<String>,

    /// This property contains a list of manifests for specific platforms.
    /// The spec says this field must be present but the value may be an empty array.
    pub manifests: Vec<ImageIndexEntry>,

    /// The annotations for this manifest
    ///
    /// The specification says "If there are no annotations then this property
    /// MUST either be absent or be an empty map."
    /// TO accomodate either, this is optional.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub annotations: Option<BTreeMap<String, String>>,
}

/// The manifest entry of an `ImageIndex`.
///
/// It is part of the OCI specification, and is defined in the `manifests`
/// section [here](https://github.com/opencontainers/image-spec/blob/main/image-index.md#image-index-property-descriptions):
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ImageIndexEntry {
    /// The media type of this descriptor.
    ///
    /// Layers, config, and manifests may all have descriptors. Each
    /// is differentiated by its mediaType.
    ///
    /// This REQUIRED property contains the media type of the referenced
    /// content. Values MUST comply with RFC 6838, including the naming
    /// requirements in its section 4.2.
    pub media_type: String,
    /// The SHA 256 or 512 digest of the object this describes.
    ///
    /// This REQUIRED property is the digest of the targeted content, conforming
    /// to the requirements outlined in Digests. Retrieved content SHOULD be
    /// verified against this digest when consumed via untrusted sources.
    pub digest: String,
    /// The size, in bytes, of the object this describes.
    ///
    /// This REQUIRED property specifies the size, in bytes, of the raw
    /// content. This property exists so that a client will have an expected
    /// size for the content before processing. If the length of the retrieved
    /// content does not match the specified length, the content SHOULD NOT be
    /// trusted.
    pub size: i64,
    /// This OPTIONAL property describes the minimum runtime requirements of the image.
    /// This property SHOULD be present if its target is platform-specific.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub platform: Option<Platform>,

    /// This OPTIONAL property contains arbitrary metadata for the image index.
    /// This OPTIONAL property MUST use the [annotation rules](https://github.com/opencontainers/image-spec/blob/main/annotations.md#rules).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub annotations: Option<BTreeMap<String, String>>,
}

impl std::fmt::Display for ImageIndexEntry {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let platform = self
            .platform
            .clone()
            .map(|p| p.to_string())
            .unwrap_or_else(|| String::from("N/A"));
        let annotations = self.annotations.clone().unwrap_or_default();

        write!(
            f,
            "(media-type: '{}', digest: '{}', size: '{}', platform: '{}', annotations: {:?})",
            self.media_type, self.digest, self.size, platform, annotations,
        )
    }
}

/// Platform specific fields of an Image Index manifest entry.
///
/// It is part of the OCI specification, and is in the `platform`
/// section [here](https://github.com/opencontainers/image-spec/blob/main/image-index.md#image-index-property-descriptions):
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Platform {
    /// This REQUIRED property specifies the CPU architecture.
    /// Image indexes SHOULD use, and implementations SHOULD understand, values
    /// listed in the Go Language document for [`GOARCH`](https://golang.org/doc/install/source#environment).
    pub architecture: String,
    /// This REQUIRED property specifies the operating system.
    /// Image indexes SHOULD use, and implementations SHOULD understand, values
    /// listed in the Go Language document for [`GOOS`](https://golang.org/doc/install/source#environment).
    pub os: String,
    /// This OPTIONAL property specifies the version of the operating system
    /// targeted by the referenced blob.
    /// Implementations MAY refuse to use manifests where `os.version` is not known
    /// to work with the host OS version.
    /// Valid values are implementation-defined. e.g. `10.0.14393.1066` on `windows`.
    #[serde(rename = "os.version")]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub os_version: Option<String>,
    /// This OPTIONAL property specifies an array of strings, each specifying a mandatory OS feature.
    /// When `os` is `windows`, image indexes SHOULD use, and implementations SHOULD understand the following values:
    /// - `win32k`: image requires `win32k.sys` on the host (Note: `win32k.sys` is missing on Nano Server)
    ///
    /// When `os` is not `windows`, values are implementation-defined and SHOULD be submitted to this specification for standardization.
    #[serde(rename = "os.features")]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub os_features: Option<Vec<String>>,
    /// This OPTIONAL property specifies the variant of the CPU.
    /// Image indexes SHOULD use, and implementations SHOULD understand, `variant` values listed in the [Platform Variants](#platform-variants) table.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub variant: Option<String>,
    /// This property is RESERVED for future versions of the specification.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub features: Option<Vec<String>>,
}

impl std::fmt::Display for Platform {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let os_version = self
            .os_version
            .clone()
            .unwrap_or_else(|| String::from("N/A"));
        let os_features = self.os_features.clone().unwrap_or_default();
        let variant = self.variant.clone().unwrap_or_else(|| String::from("N/A"));
        let features = self.os_features.clone().unwrap_or_default();
        write!(f, "( architecture: '{}', os: '{}', os-version: '{}', os-features: '{:?}', variant: '{}', features: '{:?}' )",
            self.architecture,
            self.os,
            os_version,
            os_features,
            variant,
            features,
        )
    }
}

#[cfg(test)]
mod test {
    use super::*;

    const TEST_MANIFEST: &str = r#"{
        "schemaVersion": 2,
        "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
        "config": {
            "mediaType": "application/vnd.docker.container.image.v1+json",
            "size": 2,
            "digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"
        },
        "artifactType": "application/vnd.wasm.component.v1+wasm",
        "layers": [
            {
                "mediaType": "application/vnd.wasm.content.layer.v1+wasm",
                "size": 1615998,
                "digest": "sha256:f9c91f4c280ab92aff9eb03b279c4774a80b84428741ab20855d32004b2b983f",
                "annotations": {
                    "org.opencontainers.image.title": "module.wasm"
                }
            }
        ]
    }
    "#;

    #[test]
    fn test_manifest() {
        let manifest: OciImageManifest =
            serde_json::from_str(TEST_MANIFEST).expect("parsed manifest");
        assert_eq!(2, manifest.schema_version);
        assert_eq!(
            Some(IMAGE_MANIFEST_MEDIA_TYPE.to_owned()),
            manifest.media_type
        );
        let config = manifest.config;
        // Note that this is the Docker config media type, not the OCI one.
        assert_eq!(IMAGE_DOCKER_CONFIG_MEDIA_TYPE.to_owned(), config.media_type);
        assert_eq!(2, config.size);
        assert_eq!(
            "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a".to_owned(),
            config.digest
        );
        assert_eq!(
            "application/vnd.wasm.component.v1+wasm".to_owned(),
            manifest.artifact_type.unwrap()
        );

        assert_eq!(1, manifest.layers.len());
        let wasm_layer = &manifest.layers[0];
        assert_eq!(1_615_998, wasm_layer.size);
        assert_eq!(WASM_LAYER_MEDIA_TYPE.to_owned(), wasm_layer.media_type);
        assert_eq!(
            1,
            wasm_layer
                .annotations
                .as_ref()
                .expect("annotations map")
                .len()
        );
    }
}