1use 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 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#[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}