Skip to main content

oci_zero/
metadata.rs

1//! Allocation-free borrowed views over OCI JSON metadata.
2
3pub use crate::json::{JsonError, JsonString};
4use crate::{
5    digest::{Digest, DigestError},
6    json::{ArrayIter, Object, ObjectIter, Value},
7};
8
9pub const OCI_INDEX_MEDIA_TYPE: &str = "application/vnd.oci.image.index.v1+json";
10pub const OCI_MANIFEST_MEDIA_TYPE: &str = "application/vnd.oci.image.manifest.v1+json";
11pub const DOCKER_INDEX_MEDIA_TYPE: &str =
12    "application/vnd.docker.distribution.manifest.list.v2+json";
13pub const DOCKER_MANIFEST_MEDIA_TYPE: &str = "application/vnd.docker.distribution.manifest.v2+json";
14
15#[derive(Clone, Copy, Debug, Eq, PartialEq)]
16pub enum DocumentKind {
17    Index,
18    Manifest,
19}
20
21#[derive(Clone, Copy, Debug, Eq, PartialEq)]
22pub struct Document<'a> {
23    object: Object<'a>,
24    kind: DocumentKind,
25}
26
27impl<'a> Document<'a> {
28    pub fn parse(bytes: &'a [u8]) -> Result<Self, MetadataError> {
29        let object = Value::parse_document(bytes)?.object()?;
30        let schema = object.required("schemaVersion")?.u64()?;
31        if schema != 2 {
32            return Err(MetadataError::UnsupportedSchema(schema));
33        }
34        let has_manifests = object.get("manifests")?.is_some();
35        let has_config = object.get("config")?.is_some();
36        let has_layers = object.get("layers")?.is_some();
37        let kind = match (has_manifests, has_config, has_layers) {
38            (true, false, false) => DocumentKind::Index,
39            (false, true, true) => DocumentKind::Manifest,
40            _ => return Err(MetadataError::UnknownDocument),
41        };
42        Ok(Self { object, kind })
43    }
44
45    pub const fn kind(&self) -> DocumentKind {
46        self.kind
47    }
48
49    pub fn index(self) -> Result<ImageIndex<'a>, MetadataError> {
50        if self.kind != DocumentKind::Index {
51            return Err(MetadataError::WrongDocumentKind);
52        }
53        Ok(ImageIndex {
54            object: self.object,
55        })
56    }
57
58    pub fn manifest(self) -> Result<ImageManifest<'a>, MetadataError> {
59        if self.kind != DocumentKind::Manifest {
60            return Err(MetadataError::WrongDocumentKind);
61        }
62        Ok(ImageManifest {
63            object: self.object,
64        })
65    }
66}
67
68#[derive(Clone, Copy, Debug, Eq, PartialEq)]
69pub struct ImageIndex<'a> {
70    object: Object<'a>,
71}
72
73impl<'a> ImageIndex<'a> {
74    pub fn media_type(self) -> Result<Option<JsonString<'a>>, MetadataError> {
75        optional_string(self.object, "mediaType")
76    }
77
78    pub fn artifact_type(self) -> Result<Option<JsonString<'a>>, MetadataError> {
79        optional_string(self.object, "artifactType")
80    }
81
82    pub fn subject(self) -> Result<Option<Descriptor<'a>>, MetadataError> {
83        self.object
84            .get("subject")?
85            .map(Descriptor::parse)
86            .transpose()
87    }
88
89    pub fn manifests(self) -> Result<DescriptorIter<'a>, MetadataError> {
90        Ok(DescriptorIter {
91            inner: self.object.required("manifests")?.array()?.iter(),
92        })
93    }
94
95    pub fn annotations(self) -> Result<AnnotationIter<'a>, MetadataError> {
96        annotations(self.object)
97    }
98}
99
100#[derive(Clone, Copy, Debug, Eq, PartialEq)]
101pub struct ImageManifest<'a> {
102    object: Object<'a>,
103}
104
105impl<'a> ImageManifest<'a> {
106    pub fn media_type(self) -> Result<Option<JsonString<'a>>, MetadataError> {
107        optional_string(self.object, "mediaType")
108    }
109
110    pub fn artifact_type(self) -> Result<Option<JsonString<'a>>, MetadataError> {
111        optional_string(self.object, "artifactType")
112    }
113
114    pub fn subject(self) -> Result<Option<Descriptor<'a>>, MetadataError> {
115        self.object
116            .get("subject")?
117            .map(Descriptor::parse)
118            .transpose()
119    }
120
121    pub fn config(self) -> Result<Descriptor<'a>, MetadataError> {
122        Descriptor::parse(self.object.required("config")?)
123    }
124
125    pub fn layers(self) -> Result<DescriptorIter<'a>, MetadataError> {
126        Ok(DescriptorIter {
127            inner: self.object.required("layers")?.array()?.iter(),
128        })
129    }
130
131    pub fn annotations(self) -> Result<AnnotationIter<'a>, MetadataError> {
132        annotations(self.object)
133    }
134}
135
136#[derive(Clone, Copy, Debug, Eq, PartialEq)]
137pub struct Descriptor<'a> {
138    object: Object<'a>,
139    media_type: JsonString<'a>,
140    digest: Digest,
141    size: u64,
142}
143
144impl<'a> Descriptor<'a> {
145    fn parse(value: Value<'a>) -> Result<Self, MetadataError> {
146        let object = value.object()?;
147        let media_type = object.required("mediaType")?.string()?;
148        let digest_string = object.required("digest")?.string()?;
149        let mut digest_buffer = [0; 71];
150        let digest = Digest::parse(digest_string.decode_into(&mut digest_buffer)?)?;
151        let size = object.required("size")?.u64()?;
152        Ok(Self {
153            object,
154            media_type,
155            digest,
156            size,
157        })
158    }
159
160    pub const fn media_type(&self) -> JsonString<'a> {
161        self.media_type
162    }
163
164    pub const fn digest(&self) -> Digest {
165        self.digest
166    }
167
168    pub const fn size(&self) -> u64 {
169        self.size
170    }
171
172    pub fn artifact_type(self) -> Result<Option<JsonString<'a>>, MetadataError> {
173        optional_string(self.object, "artifactType")
174    }
175
176    pub fn platform(self) -> Result<Option<Platform<'a>>, MetadataError> {
177        self.object
178            .get("platform")?
179            .map(Platform::parse)
180            .transpose()
181    }
182
183    pub fn annotations(self) -> Result<AnnotationIter<'a>, MetadataError> {
184        annotations(self.object)
185    }
186
187    pub fn urls(self) -> Result<StringIter<'a>, MetadataError> {
188        strings(self.object, "urls")
189    }
190
191    pub fn data(self) -> Result<Option<JsonString<'a>>, MetadataError> {
192        optional_string(self.object, "data")
193    }
194
195    /// Decodes an optional inline descriptor payload into caller-owned storage.
196    ///
197    /// The same buffer is first used to unescape the JSON string and is then
198    /// decoded in place, so it must hold the base64 text rather than only the
199    /// smaller decoded payload.
200    pub fn decode_data(self, buffer: &mut [u8]) -> Result<Option<&[u8]>, MetadataError> {
201        let Some(data) = self.data()? else {
202            return Ok(None);
203        };
204        let encoded_length = data.decode_into(buffer)?.len();
205        let decoded_length = decode_base64_in_place(buffer, encoded_length)?;
206        Ok(Some(&buffer[..decoded_length]))
207    }
208}
209
210#[derive(Clone, Copy, Debug, Eq, PartialEq)]
211pub struct Platform<'a> {
212    object: Object<'a>,
213    architecture: JsonString<'a>,
214    os: JsonString<'a>,
215}
216
217impl<'a> Platform<'a> {
218    fn parse(value: Value<'a>) -> Result<Self, MetadataError> {
219        let object = value.object()?;
220        Ok(Self {
221            object,
222            architecture: object.required("architecture")?.string()?,
223            os: object.required("os")?.string()?,
224        })
225    }
226
227    pub const fn architecture(&self) -> JsonString<'a> {
228        self.architecture
229    }
230
231    pub const fn os(&self) -> JsonString<'a> {
232        self.os
233    }
234
235    pub fn variant(self) -> Result<Option<JsonString<'a>>, MetadataError> {
236        optional_string(self.object, "variant")
237    }
238
239    pub fn os_version(self) -> Result<Option<JsonString<'a>>, MetadataError> {
240        optional_string(self.object, "os.version")
241    }
242
243    pub fn os_features(self) -> Result<StringIter<'a>, MetadataError> {
244        strings(self.object, "os.features")
245    }
246}
247
248pub struct DescriptorIter<'a> {
249    inner: ArrayIter<'a>,
250}
251
252impl<'a> Iterator for DescriptorIter<'a> {
253    type Item = Result<Descriptor<'a>, MetadataError>;
254
255    fn next(&mut self) -> Option<Self::Item> {
256        self.inner.next().map(|value| Descriptor::parse(value?))
257    }
258}
259
260pub struct AnnotationIter<'a> {
261    inner: Option<ObjectIter<'a>>,
262}
263
264impl<'a> Iterator for AnnotationIter<'a> {
265    type Item = Result<(JsonString<'a>, JsonString<'a>), MetadataError>;
266
267    fn next(&mut self) -> Option<Self::Item> {
268        let inner = self.inner.as_mut()?;
269        inner.next().map(|member| {
270            let (key, value) = member?;
271            Ok((key, value.string()?))
272        })
273    }
274}
275
276pub struct StringIter<'a> {
277    inner: Option<ArrayIter<'a>>,
278}
279
280impl<'a> Iterator for StringIter<'a> {
281    type Item = Result<JsonString<'a>, MetadataError>;
282
283    fn next(&mut self) -> Option<Self::Item> {
284        let inner = self.inner.as_mut()?;
285        inner.next().map(|value| Ok(value?.string()?))
286    }
287}
288
289#[derive(Clone, Copy, Debug, Eq, PartialEq)]
290pub struct ImageConfig<'a> {
291    object: Object<'a>,
292}
293
294impl<'a> ImageConfig<'a> {
295    pub fn parse(bytes: &'a [u8]) -> Result<Self, MetadataError> {
296        Ok(Self {
297            object: Value::parse_document(bytes)?.object()?,
298        })
299    }
300
301    pub fn diff_ids(self) -> Result<Option<DigestIter<'a>>, MetadataError> {
302        let Some(rootfs) = self.object.get("rootfs")? else {
303            return Ok(None);
304        };
305        let rootfs = rootfs.object()?;
306        let Some(diff_ids) = rootfs.get("diff_ids")? else {
307            return Ok(None);
308        };
309        Ok(Some(DigestIter {
310            inner: diff_ids.array()?.iter(),
311        }))
312    }
313}
314
315pub struct DigestIter<'a> {
316    inner: ArrayIter<'a>,
317}
318
319impl Iterator for DigestIter<'_> {
320    type Item = Result<Digest, MetadataError>;
321
322    fn next(&mut self) -> Option<Self::Item> {
323        self.inner.next().map(|value| {
324            let string = value?.string()?;
325            let mut buffer = [0; 71];
326            Ok(Digest::parse(string.decode_into(&mut buffer)?)?)
327        })
328    }
329}
330
331#[derive(Clone, Copy, Debug, Eq, PartialEq)]
332pub struct TagList<'a> {
333    object: Object<'a>,
334}
335
336impl<'a> TagList<'a> {
337    pub fn parse(bytes: &'a [u8]) -> Result<Self, MetadataError> {
338        Ok(Self {
339            object: Value::parse_document(bytes)?.object()?,
340        })
341    }
342
343    pub fn name(self) -> Result<JsonString<'a>, MetadataError> {
344        Ok(self.object.required("name")?.string()?)
345    }
346
347    pub fn tags(self) -> Result<StringIter<'a>, MetadataError> {
348        strings(self.object, "tags")
349    }
350}
351
352/// A Docker Distribution registry catalog response.
353#[derive(Clone, Copy, Debug, Eq, PartialEq)]
354pub struct Catalog<'a> {
355    object: Object<'a>,
356}
357
358impl<'a> Catalog<'a> {
359    pub fn parse(bytes: &'a [u8]) -> Result<Self, MetadataError> {
360        Ok(Self {
361            object: Value::parse_document(bytes)?.object()?,
362        })
363    }
364
365    pub fn repositories(self) -> Result<StringIter<'a>, MetadataError> {
366        strings(self.object, "repositories")
367    }
368}
369
370#[derive(Clone, Copy, Debug, Eq, PartialEq, derive_more::Display, derive_more::From)]
371pub enum MetadataError {
372    #[display("invalid OCI JSON: {_0}")]
373    #[from]
374    Json(JsonError),
375    #[display("invalid descriptor digest: {_0}")]
376    #[from]
377    Digest(DigestError),
378    #[display("unsupported OCI schema version {_0}")]
379    UnsupportedSchema(u64),
380    #[display("unknown OCI document shape")]
381    UnknownDocument,
382    #[display("unexpected OCI document kind")]
383    WrongDocumentKind,
384    #[display("invalid inline descriptor base64 data")]
385    InvalidBase64,
386}
387
388fn decode_base64_in_place(bytes: &mut [u8], encoded_length: usize) -> Result<usize, MetadataError> {
389    use base64ct::{Base64, Encoding};
390    // The in-place decoder does not check unused bits in the final quartet.
391    if encoded_length >= 4 {
392        Base64::decode(&bytes[encoded_length - 4..encoded_length], &mut [0; 3])
393            .map_err(|_| MetadataError::InvalidBase64)?;
394    }
395    Base64::decode_in_place(&mut bytes[..encoded_length])
396        .map(|decoded| decoded.len())
397        .map_err(|_| MetadataError::InvalidBase64)
398}
399
400fn optional_string<'a>(
401    object: Object<'a>,
402    name: &'static str,
403) -> Result<Option<JsonString<'a>>, MetadataError> {
404    Ok(object.get(name)?.map(Value::string).transpose()?)
405}
406
407fn annotations<'a>(object: Object<'a>) -> Result<AnnotationIter<'a>, MetadataError> {
408    let inner = object
409        .get("annotations")?
410        .filter(|value| !value.is_null())
411        .map(Value::object)
412        .transpose()?
413        .map(Object::iter);
414    Ok(AnnotationIter { inner })
415}
416
417fn strings<'a>(object: Object<'a>, name: &'static str) -> Result<StringIter<'a>, MetadataError> {
418    let inner = object
419        .get(name)?
420        .filter(|value| !value.is_null())
421        .map(Value::array)
422        .transpose()?
423        .map(|array| array.iter());
424    Ok(StringIter { inner })
425}
426
427#[cfg(test)]
428mod tests {
429    use std::{format, string::ToString, vec::Vec};
430
431    use super::{Catalog, Document, DocumentKind, ImageConfig, MetadataError, TagList};
432
433    const DIGEST: &str = "sha256:ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad";
434
435    #[test]
436    fn parses_index_descriptors_lazily() {
437        let json = format!(
438            r#"{{"schemaVersion":2,"artifactType":"application/example","subject":{{"mediaType":"application/vnd.oci.image.manifest.v1+json","size":3,"digest":"{DIGEST}"}},"unknown":{{"large":[1,2,3]}},"manifests":[{{"mediaType":"application/vnd.oci.image.manifest.v1+json","size":3,"digest":"{DIGEST}","platform":{{"architecture":"amd64","os":"linux"}},"annotations":{{"title":"value\n"}}}}]}}"#
439        );
440        let document = Document::parse(json.as_bytes()).unwrap();
441        assert_eq!(document.kind(), DocumentKind::Index);
442        let index = document.index().unwrap();
443        assert_eq!(
444            index.artifact_type().unwrap().unwrap().as_str(),
445            Some("application/example")
446        );
447        assert_eq!(
448            index.subject().unwrap().unwrap().digest().to_string(),
449            DIGEST
450        );
451        let descriptor = document
452            .index()
453            .unwrap()
454            .manifests()
455            .unwrap()
456            .next()
457            .unwrap()
458            .unwrap();
459        assert_eq!(descriptor.size(), 3);
460        let platform = descriptor.platform().unwrap().unwrap();
461        assert_eq!(platform.os().as_str(), Some("linux"));
462        let (_, title) = descriptor.annotations().unwrap().next().unwrap().unwrap();
463        let mut decoded = [0; 16];
464        assert_eq!(title.decode_into(&mut decoded).unwrap(), "value\n");
465    }
466
467    #[test]
468    fn parses_manifest_and_diff_ids() {
469        let json = format!(
470            r#"{{"schemaVersion":2,"config":{{"mediaType":"x","size":3,"digest":"{DIGEST}"}},"layers":[{{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","size":3,"digest":"{DIGEST}"}}]}}"#
471        );
472        let manifest = Document::parse(json.as_bytes())
473            .unwrap()
474            .manifest()
475            .unwrap();
476        assert_eq!(manifest.config().unwrap().size(), 3);
477        assert_eq!(manifest.layers().unwrap().count(), 1);
478
479        let config = format!(r#"{{"rootfs":{{"type":"layers","diff_ids":["{DIGEST}"]}}}}"#);
480        assert_eq!(
481            ImageConfig::parse(config.as_bytes())
482                .unwrap()
483                .diff_ids()
484                .unwrap()
485                .unwrap()
486                .next()
487                .unwrap()
488                .unwrap()
489                .to_string(),
490            DIGEST
491        );
492    }
493
494    #[test]
495    fn parses_catalogs_and_tag_lists() {
496        let catalog = Catalog::parse(br#"{"repositories":["a","team/b"]}"#).unwrap();
497        let repositories = catalog
498            .repositories()
499            .unwrap()
500            .map(|value| value.unwrap().as_str().unwrap())
501            .collect::<Vec<_>>();
502        assert_eq!(repositories, ["a", "team/b"]);
503
504        let tags = TagList::parse(br#"{"name":"team/b","tags":["latest","v1"]}"#).unwrap();
505        assert_eq!(tags.name().unwrap().as_str(), Some("team/b"));
506        assert_eq!(tags.tags().unwrap().count(), 2);
507
508        let empty = TagList::parse(br#"{"name":"empty","tags":null}"#).unwrap();
509        assert_eq!(empty.tags().unwrap().count(), 0);
510    }
511
512    #[test]
513    fn decodes_inline_descriptor_data_without_allocating() {
514        let json = format!(
515            r#"{{"schemaVersion":2,"manifests":[{{"mediaType":"x","size":5,"digest":"{DIGEST}","data":"aGVs\u0062G8="}}]}}"#
516        );
517        let descriptor = Document::parse(json.as_bytes())
518            .unwrap()
519            .index()
520            .unwrap()
521            .manifests()
522            .unwrap()
523            .next()
524            .unwrap()
525            .unwrap();
526        let mut buffer = [0; 32];
527        assert_eq!(
528            descriptor.decode_data(&mut buffer).unwrap(),
529            Some(b"hello".as_slice())
530        );
531
532        let invalid = format!(
533            r#"{{"schemaVersion":2,"manifests":[{{"mediaType":"x","size":1,"digest":"{DIGEST}","data":"A==="}}]}}"#
534        );
535        let descriptor = Document::parse(invalid.as_bytes())
536            .unwrap()
537            .index()
538            .unwrap()
539            .manifests()
540            .unwrap()
541            .next()
542            .unwrap()
543            .unwrap();
544        assert_eq!(
545            descriptor.decode_data(&mut buffer),
546            Err(MetadataError::InvalidBase64)
547        );
548    }
549}