oci_spec/image/
descriptor.rs

1use super::{Arch, Digest, MediaType, Os};
2use crate::error::OciSpecError;
3use derive_builder::Builder;
4use getset::{CopyGetters, Getters, Setters};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8#[derive(
9    Builder, Clone, CopyGetters, Debug, Deserialize, Eq, Getters, Setters, PartialEq, Serialize,
10)]
11#[serde(rename_all = "camelCase")]
12#[builder(
13    pattern = "owned",
14    setter(into, strip_option),
15    build_fn(error = "OciSpecError")
16)]
17/// A Content Descriptor (or simply Descriptor) describes the disposition of
18/// the targeted content. It includes the type of the content, a content
19/// identifier (digest), and the byte-size of the raw content.
20/// Descriptors SHOULD be embedded in other formats to securely reference
21/// external content.
22pub struct Descriptor {
23    /// This REQUIRED property contains the media type of the referenced
24    /// content. Values MUST comply with RFC 6838, including the naming
25    /// requirements in its section 4.2.
26    #[getset(get = "pub", set = "pub")]
27    media_type: MediaType,
28    /// This REQUIRED property is the digest of the targeted content,
29    /// conforming to the requirements outlined in Digests. Retrieved
30    /// content SHOULD be verified against this digest when consumed via
31    /// untrusted sources.
32    #[getset(get = "pub", set = "pub")]
33    digest: Digest,
34    /// This REQUIRED property specifies the size, in bytes, of the raw
35    /// content. This property exists so that a client will have an
36    /// expected size for the content before processing. If the
37    /// length of the retrieved content does not match the specified
38    /// length, the content SHOULD NOT be trusted.
39    #[getset(get_copy = "pub", set = "pub")]
40    size: u64,
41    /// This OPTIONAL property specifies a list of URIs from which this
42    /// object MAY be downloaded. Each entry MUST conform to [RFC 3986](https://tools.ietf.org/html/rfc3986).
43    /// Entries SHOULD use the http and https schemes, as defined
44    /// in [RFC 7230](https://tools.ietf.org/html/rfc7230#section-2.7).
45    #[serde(skip_serializing_if = "Option::is_none")]
46    #[getset(get = "pub", set = "pub")]
47    #[builder(default)]
48    urls: Option<Vec<String>>,
49    /// This OPTIONAL property contains arbitrary metadata for this
50    /// descriptor. This OPTIONAL property MUST use the annotation
51    /// rules.
52    #[serde(skip_serializing_if = "Option::is_none")]
53    #[getset(get = "pub", set = "pub")]
54    #[builder(default)]
55    annotations: Option<HashMap<String, String>>,
56    /// This OPTIONAL property describes the minimum runtime requirements of
57    /// the image. This property SHOULD be present if its target is
58    /// platform-specific.
59    #[serde(skip_serializing_if = "Option::is_none")]
60    #[getset(get = "pub", set = "pub")]
61    #[builder(default)]
62    platform: Option<Platform>,
63    /// This OPTIONAL property contains the type of an artifact when the descriptor points to an
64    /// artifact. This is the value of the config descriptor mediaType when the descriptor
65    /// references an image manifest. If defined, the value MUST comply with RFC 6838, including
66    /// the naming requirements in its section 4.2, and MAY be registered with IANA.
67    #[serde(skip_serializing_if = "Option::is_none")]
68    #[getset(get = "pub", set = "pub")]
69    #[builder(default)]
70    artifact_type: Option<MediaType>,
71    /// This OPTIONAL property contains an embedded representation of the referenced content.
72    /// Values MUST conform to the Base 64 encoding, as defined in RFC 4648. The decoded data MUST
73    /// be identical to the referenced content and SHOULD be verified against the digest and size
74    /// fields by content consumers. See Embedded Content for when this is appropriate.
75    #[serde(skip_serializing_if = "Option::is_none")]
76    #[getset(get = "pub", set = "pub")]
77    #[builder(default)]
78    data: Option<String>,
79}
80
81#[derive(
82    Builder, Clone, Debug, Default, Deserialize, Eq, Getters, Setters, PartialEq, Serialize,
83)]
84#[builder(
85    pattern = "owned",
86    setter(into, strip_option),
87    build_fn(error = "OciSpecError")
88)]
89#[getset(get = "pub", set = "pub")]
90/// Describes the minimum runtime requirements of the image.
91pub struct Platform {
92    /// This REQUIRED property specifies the CPU architecture.
93    /// Image indexes SHOULD use, and implementations SHOULD understand,
94    /// values listed in the Go Language document for GOARCH.
95    architecture: Arch,
96    /// This REQUIRED property specifies the operating system.
97    /// Image indexes SHOULD use, and implementations SHOULD understand,
98    /// values listed in the Go Language document for GOOS.
99    os: Os,
100    /// This OPTIONAL property specifies the version of the operating system
101    /// targeted by the referenced blob. Implementations MAY refuse to use
102    /// manifests where os.version is not known to work with the host OS
103    /// version. Valid values are implementation-defined. e.g.
104    /// 10.0.14393.1066 on windows.
105    #[serde(skip_serializing_if = "Option::is_none")]
106    #[builder(default)]
107    os_version: Option<String>,
108    /// This OPTIONAL property specifies an array of strings, each
109    /// specifying a mandatory OS feature. When os is windows, image
110    /// indexes SHOULD use, and implementations SHOULD understand
111    /// the following values:
112    /// - win32k: image requires win32k.sys on the host (Note: win32k.sys is
113    ///   missing on Nano Server)
114    ///
115    /// When os is not windows, values are implementation-defined and SHOULD
116    /// be submitted to this specification for standardization.
117    #[serde(skip_serializing_if = "Option::is_none")]
118    #[builder(default)]
119    os_features: Option<Vec<String>>,
120    /// This OPTIONAL property specifies the variant of the CPU.
121    /// Image indexes SHOULD use, and implementations SHOULD understand,
122    /// variant values listed in the [Platform Variants]
123    /// (<https://github.com/opencontainers/image-spec/blob/main/image-index.md#platform-variants>)
124    /// table.
125    #[serde(skip_serializing_if = "Option::is_none")]
126    #[builder(default)]
127    variant: Option<String>,
128    /// This property is RESERVED for future versions of the specification.
129    #[serde(skip_serializing_if = "Option::is_none")]
130    #[builder(default)]
131    features: Option<Vec<String>>,
132}
133
134impl Descriptor {
135    /// Construct a new descriptor with the required fields.
136    pub fn new(media_type: MediaType, size: u64, digest: impl Into<Digest>) -> Self {
137        Self {
138            media_type,
139            size,
140            digest: digest.into(),
141            urls: Default::default(),
142            annotations: Default::default(),
143            platform: Default::default(),
144            artifact_type: Default::default(),
145            data: Default::default(),
146        }
147    }
148
149    /// Return a view of [`Self::digest()`] that has been parsed as a valid SHA-256.
150    pub fn as_digest_sha256(&self) -> Option<&str> {
151        match self.digest.algorithm() {
152            super::DigestAlgorithm::Sha256 => Some(self.digest.digest()),
153            _ => None,
154        }
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use std::str::FromStr;
161
162    use super::*;
163
164    #[test]
165    fn test_deserialize() {
166        let descriptor_str = r#"{
167            "mediaType": "application/vnd.oci.image.manifest.v1+json",
168            "digest":"sha256:c2b8beca588702777e5f35dafdbeae9ec16c2bab802331f81cacd2a92f1d5356",
169            "size":769,
170            "annotations":{"org.opencontainers.image.created": "2023-10-11T22:37:26Z"},
171            "artifactType":"application/spdx+json"}"#;
172        let descriptor: Descriptor = serde_json::from_str(descriptor_str).unwrap();
173        assert_eq!(descriptor.media_type, MediaType::ImageManifest);
174        assert_eq!(
175            descriptor.digest,
176            Digest::from_str(
177                "sha256:c2b8beca588702777e5f35dafdbeae9ec16c2bab802331f81cacd2a92f1d5356"
178            )
179            .unwrap()
180        );
181        assert_eq!(descriptor.size, 769);
182        assert_eq!(
183            descriptor
184                .annotations
185                .unwrap()
186                .get("org.opencontainers.image.created"),
187            Some(&"2023-10-11T22:37:26Z".to_string())
188        );
189        assert_eq!(
190            descriptor.artifact_type.unwrap(),
191            MediaType::Other("application/spdx+json".to_string())
192        );
193    }
194
195    #[test]
196    fn test_malformed_digest() {
197        let descriptor_str = r#"{
198            "mediaType": "application/vnd.oci.image.manifest.v1+json",
199            "digest":"../blah:this-is-an-attack",
200            "size":769,
201            "annotations":{"org.opencontainers.image.created": "2023-10-11T22:37:26Z"},
202            "artifactType":"application/spdx+json"}"#;
203        assert!(serde_json::from_str::<Descriptor>(descriptor_str).is_err());
204    }
205}