Skip to content

Commit e88564e

Browse files
committed
new_empty_manifest: create a valid manifest with an empty config descriptor
The empty descriptor is described in the image spec [1]. `new_empty_manifest` writes an empty config descriptor in the blobs directory, which results in a valid image layout specification. This is validated using the fsck function in the `test_new_empty_manifest` test. Fixes #27 [1] https://github.com/opencontainers/image-spec/blob/main/manifest.md#guidance-for-an-empty-descriptor Signed-off-by: Ariel Miculas-Trif <[email protected]>
1 parent 6817b48 commit e88564e

File tree

2 files changed

+75
-32
lines changed

2 files changed

+75
-32
lines changed

examples/zstd.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ fn main() {
44

55
use cap_tempfile::TempDir;
66
use oci_spec::image::Platform;
7-
use ocidir::{new_empty_manifest, OciDir};
7+
use ocidir::OciDir;
88
let dir = TempDir::new(ocidir::cap_std::ambient_authority()).unwrap();
99
let oci_dir = OciDir::ensure(dir.try_clone().unwrap()).unwrap();
1010

11-
let mut manifest = new_empty_manifest().build().unwrap();
11+
let mut manifest = oci_dir.new_empty_manifest().unwrap().build().unwrap();
1212
let mut config = ocidir::oci_spec::image::ImageConfigurationBuilder::default()
1313
.build()
1414
.unwrap();

src/lib.rs

Lines changed: 73 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use oci_spec::image::{
1111
};
1212
use olpc_cjson::CanonicalFormatter;
1313
use openssl::hash::{Hasher, MessageDigest};
14-
use serde::Serialize;
14+
use serde::{Deserialize, Serialize};
1515
use std::collections::{HashMap, HashSet};
1616
use std::fmt::Debug;
1717
use std::fs::File;
@@ -57,6 +57,9 @@ pub enum Error {
5757
#[error("Cannot find the Image Index (index.json)")]
5858
/// Returned when the OCI Image Index (index.json) is missing
5959
MissingImageIndex,
60+
#[error("Unexpected media type {media_type}")]
61+
/// Returned when the OCI Image Index (index.json) is missing
62+
UnexpectedMediaType { media_type: MediaType },
6063
#[error("error")]
6164
/// An unknown other error
6265
Other(Box<str>),
@@ -77,6 +80,11 @@ impl From<openssl::error::ErrorStack> for Error {
7780
}
7881
}
7982

83+
// This is intentionally an empty struct
84+
// See https://github.com/opencontainers/image-spec/blob/main/manifest.md#guidance-for-an-empty-descriptor
85+
#[derive(Serialize, Deserialize)]
86+
struct EmptyDescriptor {}
87+
8088
/// Completed blob metadata
8189
#[derive(Debug)]
8290
pub struct Blob {
@@ -161,32 +169,6 @@ pub struct OciDir {
161169
blobs_dir: Dir,
162170
}
163171

164-
/// Create a dummy config descriptor.
165-
/// Our API right now always mutates a manifest, which means we need
166-
/// a "valid" manifest, which requires a "valid" config descriptor.
167-
/// This digest should never actually be used for anything.
168-
fn empty_config_descriptor() -> oci_image::Descriptor {
169-
oci_image::DescriptorBuilder::default()
170-
.media_type(MediaType::ImageConfig)
171-
.size(7023u64)
172-
.digest(
173-
Sha256Digest::from_str(
174-
"a5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7",
175-
)
176-
.unwrap(),
177-
)
178-
.build()
179-
.unwrap()
180-
}
181-
182-
/// Generate a "valid" empty manifest. See above.
183-
pub fn new_empty_manifest() -> oci_image::ImageManifestBuilder {
184-
oci_image::ImageManifestBuilder::default()
185-
.schema_version(oci_image::SCHEMA_VERSION)
186-
.config(empty_config_descriptor())
187-
.layers(Vec::new())
188-
}
189-
190172
fn sha256_of_descriptor(desc: &Descriptor) -> Result<&str> {
191173
desc.as_digest_sha256()
192174
.ok_or_else(|| Error::UnsupportedDigestAlgorithm {
@@ -195,6 +177,40 @@ fn sha256_of_descriptor(desc: &Descriptor) -> Result<&str> {
195177
}
196178

197179
impl OciDir {
180+
/// Create an empty config descriptor.
181+
/// See https://github.com/opencontainers/image-spec/blob/main/manifest.md#guidance-for-an-empty-descriptor
182+
/// Our API right now always mutates a manifest, which means we need
183+
/// a "valid" manifest, which requires a "valid" config descriptor.
184+
fn empty_config_descriptor(&self) -> Result<oci_image::Descriptor> {
185+
let empty_descriptor = oci_image::DescriptorBuilder::default()
186+
.media_type(MediaType::EmptyJSON)
187+
.size(2_u32)
188+
.digest(Sha256Digest::from_str(
189+
"44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
190+
)?)
191+
.data("e30=")
192+
.build()?;
193+
194+
if !self
195+
.dir
196+
.exists(OciDir::parse_descriptor_to_path(&empty_descriptor)?)
197+
{
198+
let mut blob = self.create_blob()?;
199+
serde_json::to_writer(&mut blob, &EmptyDescriptor {})?;
200+
blob.complete_verified_as(&empty_descriptor)?;
201+
}
202+
203+
Ok(empty_descriptor)
204+
}
205+
206+
/// Generate a valid empty manifest. See above.
207+
pub fn new_empty_manifest(&self) -> Result<oci_image::ImageManifestBuilder> {
208+
Ok(oci_image::ImageManifestBuilder::default()
209+
.schema_version(oci_image::SCHEMA_VERSION)
210+
.config(self.empty_config_descriptor()?)
211+
.layers(Vec::new()))
212+
}
213+
198214
/// Open the OCI directory at the target path; if it does not already
199215
/// have the standard OCI metadata, it is created.
200216
pub fn ensure(dir: Dir) -> Result<Self> {
@@ -548,7 +564,19 @@ impl OciDir {
548564
validated: &mut HashSet<Box<str>>,
549565
) -> Result<()> {
550566
let config_digest = sha256_of_descriptor(manifest.config())?;
551-
let _: ImageConfiguration = self.read_json_blob(manifest.config())?;
567+
match manifest.config().media_type() {
568+
MediaType::ImageConfig => {
569+
let _: ImageConfiguration = self.read_json_blob(manifest.config())?;
570+
}
571+
MediaType::EmptyJSON => {
572+
let _: EmptyDescriptor = self.read_json_blob(manifest.config())?;
573+
}
574+
media_type => {
575+
return Err(Error::UnexpectedMediaType {
576+
media_type: media_type.clone(),
577+
})
578+
}
579+
}
552580
validated.insert(config_digest.into());
553581
for layer in manifest.layers() {
554582
let expected = sha256_of_descriptor(layer)?;
@@ -833,7 +861,7 @@ mod tests {
833861
))
834862
.unwrap());
835863

836-
let mut manifest = new_empty_manifest().build().unwrap();
864+
let mut manifest = w.new_empty_manifest()?.build()?;
837865
let mut config = oci_image::ImageConfigurationBuilder::default()
838866
.build()
839867
.unwrap();
@@ -877,7 +905,7 @@ mod tests {
877905
let mut layerw = w.create_gzip_layer(None)?;
878906
layerw.write_all(b"pretend this is an updated tarball")?;
879907
let root_layer = layerw.complete()?;
880-
let mut manifest = new_empty_manifest().build().unwrap();
908+
let mut manifest = w.new_empty_manifest()?.build()?;
881909
let mut config = oci_image::ImageConfigurationBuilder::default()
882910
.build()
883911
.unwrap();
@@ -942,4 +970,19 @@ mod tests {
942970

943971
Ok(())
944972
}
973+
974+
#[test]
975+
fn test_new_empty_manifest() -> Result<()> {
976+
let td = cap_tempfile::tempdir(cap_std::ambient_authority())?;
977+
let w = OciDir::ensure(td.try_clone()?)?;
978+
979+
let manifest = w.new_empty_manifest()?.build()?;
980+
let desc: Descriptor =
981+
w.insert_manifest(manifest, Some("latest"), oci_image::Platform::default())?;
982+
assert!(w.has_manifest(&desc).unwrap());
983+
984+
// We expect two validated blobs: the manifest and the image configuration
985+
assert_eq!(w.fsck()?, 2);
986+
Ok(())
987+
}
945988
}

0 commit comments

Comments
 (0)