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