Skip to main content

oci_zero/
metadata.rs

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