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
//! OCI Manifest
use std::collections::HashMap;

/// 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 OCI manifest.
pub const IMAGE_MANIFEST_MEDIA_TYPE: &str = "application/vnd.docker.distribution.manifest.v2+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 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";

// TODO: Annotation key constants. https://github.com/opencontainers/image-spec/blob/master/annotations.md#pre-defined-annotation-keys

/// The OCI manifest describes an OCI image.
///
/// It is part of the OCI specification, and is defined here:
/// https://github.com/opencontainers/image-spec/blob/master/manifest.md
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OciManifest {
    /// 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.
    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 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.
    pub annotations: Option<HashMap<String, String>>,
}

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

/// 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.
    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/master/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.
    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/master/annotations.md#rules
    pub annotations: Option<HashMap<String, String>>,
}

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,
        }
    }
}

#[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"
        },
        "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: OciManifest = 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!(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()
        );
    }
}