Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Port to thiserror, drop get_manifest() API #16

Merged
merged 1 commit into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,15 @@ repository = "https://github.com/containers/ocidir-rs"
keywords = ["oci", "opencontainers", "docker", "podman", "containers"]

[dependencies]
anyhow = "1.0"
camino = "1.0.4"
chrono = "0.4.19"
olpc-cjson = "0.1.1"
cap-std-ext = "4.0"
flate2 = { features = ["zlib"], default-features = false, version = "1.0.20" }
fn-error-context = "0.2.0"
hex = "0.4.3"
openssl = "0.10.33"
serde = { features = ["derive"], version = "1.0.125" }
serde_json = "1.0.64"
tar = "0.4.38"
thiserror = "1"
oci-spec = "0.7.0"
182 changes: 92 additions & 90 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,10 @@
//! [OCI images]: https://github.com/opencontainers/image-spec
//!

use anyhow::{anyhow, Context, Result};
use cap_std::fs::{Dir, DirBuilderExt};
use cap_std_ext::cap_tempfile;
use cap_std_ext::dirext::CapStdExtDirExt;
use flate2::write::GzEncoder;
use fn_error_context::context;
use oci_image::MediaType;
use oci_spec::image::{
self as oci_image, Descriptor, Digest, ImageConfiguration, ImageIndex, ImageManifest,
Expand All @@ -54,6 +52,7 @@ use std::fs::File;
use std::io::{prelude::*, BufReader};
use std::path::{Path, PathBuf};
use std::str::FromStr;
use thiserror::Error;

// Re-export our dependencies that are used as part of the public API.
pub use cap_std_ext::cap_std;
Expand All @@ -64,6 +63,51 @@ const BLOBDIR: &str = "blobs/sha256";

const OCI_TAG_ANNOTATION: &str = "org.opencontainers.image.ref.name";

/// Errors returned by this crate.
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum Error {
#[error("i/o error")]
/// An input/output error
Io(#[from] std::io::Error),
#[error("serialization error")]
/// Returned when serialization or deserialization fails
SerDe(#[from] serde_json::Error),
#[error("parsing OCI value")]
/// Returned when an OCI spec error occurs
OciSpecError(#[from] oci_spec::OciSpecError),
#[error("unexpected cryptographic routine error")]
/// Returned when a cryptographic routine encounters an unexpected problem
CryptographicError(Box<str>),
#[error("Expected digest {expected} but found {found}")]
/// Returned when a digest does not match
DigestMismatch { expected: Box<str>, found: Box<str> },
#[error("Expected size {expected} but found {found}")]
/// Returned when a descriptor digest does not match what was expected
SizeMismatch { expected: u64, found: u64 },
#[error("Expected digest algorithm sha256 but found {found}")]
/// Returned when a digest algorithm is not supported
UnsupportedDigestAlgorithm { found: Box<str> },
#[error("error")]
/// An unknown other error
Other(Box<str>),
}

/// The error type returned from this crate.
pub type Result<T> = std::result::Result<T, Error>;

impl From<openssl::error::Error> for Error {
fn from(value: openssl::error::Error) -> Self {
Self::CryptographicError(value.to_string().into())
}
}

impl From<openssl::error::ErrorStack> for Error {
fn from(value: openssl::error::ErrorStack) -> Self {
Self::CryptographicError(value.to_string().into())
}
}

/// Completed blob metadata
#[derive(Debug)]
pub struct Blob {
Expand Down Expand Up @@ -150,7 +194,6 @@ pub struct OciDir {
}

/// Write a serializable data (JSON) as an OCI blob
#[context("Writing json blob")]
#[deprecated = "Use OciDir::write_json_blob instead"]
pub fn write_json_blob<S: serde::Serialize>(
ocidir: &Dir,
Expand All @@ -159,7 +202,7 @@ pub fn write_json_blob<S: serde::Serialize>(
) -> Result<oci_image::DescriptorBuilder> {
let mut w = BlobWriter::new(ocidir)?;
let mut ser = serde_json::Serializer::with_formatter(&mut w, CanonicalFormatter::new());
v.serialize(&mut ser).context("Failed to serialize")?;
v.serialize(&mut ser)?;
let blob = w.complete()?;
Ok(blob.descriptor().media_type(media_type))
}
Expand Down Expand Up @@ -190,10 +233,16 @@ pub fn new_empty_manifest() -> oci_image::ImageManifestBuilder {
.layers(Vec::new())
}

fn sha256_of_descriptor(desc: &Descriptor) -> Result<&str> {
desc.as_digest_sha256()
.ok_or_else(|| Error::UnsupportedDigestAlgorithm {
found: desc.digest().to_string().into(),
})
}

impl OciDir {
/// Open the OCI directory at the target path; if it does not already
/// have the standard OCI metadata, it is created.
#[context("Opening OCI dir")]
pub fn ensure(dir: &Dir) -> Result<Self> {
let mut db = cap_std::fs::DirBuilder::new();
db.recursive(true).mode(0o755);
Expand Down Expand Up @@ -226,7 +275,6 @@ impl OciDir {
}

/// Write a serializable data (JSON) as an OCI blob
#[context("Writing json blob")]
pub fn write_json_blob<S: serde::Serialize>(
&self,
v: &S,
Expand Down Expand Up @@ -317,21 +365,18 @@ impl OciDir {
}

fn parse_descriptor_to_path(desc: &oci_spec::image::Descriptor) -> Result<PathBuf> {
let digest = desc
.as_digest_sha256()
.ok_or_else(|| anyhow!("Unsupported non-sha256 digest in descriptor"))?;
let digest = sha256_of_descriptor(desc)?;
Ok(Path::new(BLOBDIR).join(digest))
}

/// Open a blob; its size is validated as a sanity check.
#[context("Reading blob {}", desc.digest())]
pub fn read_blob(&self, desc: &oci_spec::image::Descriptor) -> Result<File> {
let path = Self::parse_descriptor_to_path(desc)?;
let f = self.dir.open(path).map(|f| f.into_std())?;
let expected_size: u64 = desc.size();
let found_size = f.metadata()?.len();
if expected_size != found_size {
anyhow::bail!("Expected size {expected_size} but found {found_size}");
let expected: u64 = desc.size();
let found = f.metadata()?.len();
if expected != found {
return Err(Error::SizeMismatch { expected, found });
}
Ok(f)
}
Expand Down Expand Up @@ -359,7 +404,7 @@ impl OciDir {
desc: &oci_spec::image::Descriptor,
) -> Result<T> {
let blob = BufReader::new(self.read_blob(desc)?);
serde_json::from_reader(blob).with_context(|| format!("Parsing object {}", desc.digest()))
serde_json::from_reader(blob).map_err(Into::into)
}

/// Write a configuration blob.
Expand Down Expand Up @@ -423,7 +468,7 @@ impl OciDir {
.atomic_replace_with("index.json", |mut w| -> Result<()> {
let mut ser =
serde_json::Serializer::with_formatter(&mut w, CanonicalFormatter::new());
index.serialize(&mut ser).context("Failed to serialize")?;
index.serialize(&mut ser)?;
Ok(())
})?;
Ok(manifest)
Expand Down Expand Up @@ -463,19 +508,12 @@ impl OciDir {
.atomic_replace_with("index.json", |mut w| -> Result<()> {
let mut ser =
serde_json::Serializer::with_formatter(&mut w, CanonicalFormatter::new());
index_data
.serialize(&mut ser)
.context("Failed to serialize")?;
index_data.serialize(&mut ser)?;
Ok(())
})?;
Ok(())
}

/// If this OCI directory has a single manifest, return it. Otherwise, an error is returned.
pub fn read_manifest(&self) -> Result<oci_image::ImageManifest> {
self.read_manifest_and_descriptor().map(|r| r.0)
}

fn descriptor_is_tagged(d: &Descriptor, tag: &str) -> bool {
d.annotations()
.as_ref()
Expand All @@ -486,10 +524,7 @@ impl OciDir {

/// Find the manifest with the provided tag
pub fn find_manifest_with_tag(&self, tag: &str) -> Result<Option<oci_image::ImageManifest>> {
let f = self
.dir
.open("index.json")
.context("Failed to open index.json")?;
let f = self.dir.open("index.json")?;
let idx: oci_image::ImageIndex = serde_json::from_reader(BufReader::new(f))?;
for img in idx.manifests() {
if Self::descriptor_is_tagged(img, tag) {
Expand All @@ -499,21 +534,6 @@ impl OciDir {
Ok(None)
}

/// If this OCI directory has a single manifest, return it. Otherwise, an error is returned.
pub fn read_manifest_and_descriptor(&self) -> Result<(oci_image::ImageManifest, Descriptor)> {
let f = self
.dir
.open("index.json")
.context("Failed to open index.json")?;
let idx: oci_image::ImageIndex = serde_json::from_reader(BufReader::new(f))?;
let desc = match idx.manifests().as_slice() {
[] => anyhow::bail!("No manifests found"),
[desc] => desc.clone(),
manifests => anyhow::bail!("Expected exactly 1 manifest, found {}", manifests.len()),
};
Ok((self.read_json_blob(&desc)?, desc))
}

/// Verify a single manifest and all of its referenced objects.
/// Skips already validated blobs referenced by digest in `validated`,
/// and updates that set with ones we did validate.
Expand All @@ -522,29 +542,29 @@ impl OciDir {
manifest: &ImageManifest,
validated: &mut HashSet<Box<str>>,
) -> Result<()> {
let config_digest = sha256_of_descriptor(manifest.config())?;
let _: ImageConfiguration = self.read_json_blob(manifest.config())?;
validated.insert(
manifest
.config()
.as_digest_sha256()
.ok_or_else(|| anyhow!("Unsupported digest for config"))?
.into(),
);
validated.insert(config_digest.into());
for layer in manifest.layers() {
let expected_digest = layer
.as_digest_sha256()
.ok_or_else(|| anyhow!("Unsupported digest for layer {}", layer.digest()))?;
if validated.contains(expected_digest) {
let expected = sha256_of_descriptor(layer)?;
if validated.contains(expected) {
continue;
}
let mut f = self.read_blob(layer)?;
let mut digest = Hasher::new(MessageDigest::sha256())?;
std::io::copy(&mut f, &mut digest)?;
let found_digest = hex::encode(digest.finish()?);
if expected_digest != found_digest {
anyhow::bail!("Expected blob digest {expected_digest} but found {found_digest}");
let found = hex::encode(
digest
.finish()
.map_err(|e| Error::Other(e.to_string().into()))?,
);
if expected != found {
return Err(Error::DigestMismatch {
expected: expected.into(),
found: found.into(),
});
}
validated.insert(expected_digest.into());
validated.insert(expected.into());
}
Ok(())
}
Expand All @@ -557,12 +577,7 @@ impl OciDir {
};
let mut validated_blobs = HashSet::new();
for manifest_descriptor in index.manifests() {
let expected_sha256 = manifest_descriptor.as_digest_sha256().ok_or_else(|| {
anyhow!(
"Unsupported digest for manifest: {}",
manifest_descriptor.digest()
)
})?;
let expected_sha256 = sha256_of_descriptor(manifest_descriptor)?;
let manifest: ImageManifest = self.read_json_blob(manifest_descriptor)?;
validated_blobs.insert(expected_sha256.into());
self.fsck_one_manifest(&manifest, &mut validated_blobs)?;
Expand All @@ -572,7 +587,6 @@ impl OciDir {
}

impl<'a> BlobWriter<'a> {
#[context("Creating blob writer")]
fn new(ocidir: &'a Dir) -> Result<Self> {
Ok(Self {
hash: Hasher::new(MessageDigest::sha256())?,
Expand All @@ -583,37 +597,25 @@ impl<'a> BlobWriter<'a> {
}

/// Finish writing this blob, verifying its digest and size against the expected descriptor.
#[context("Completing blob")]
pub fn complete_verified_as(mut self, descriptor: &Descriptor) -> Result<Blob> {
let expected_digest = descriptor
.as_digest_sha256()
.ok_or_else(|| anyhow!("Unsupported digest for descriptor: {}", descriptor.digest()))?;
let expected_digest = sha256_of_descriptor(descriptor)?;
let found_digest = hex::encode(self.hash.finish()?);
let mut errs = Vec::new();
if found_digest.as_str() != expected_digest {
errs.push(format!(
"Digest mismatch; found={} expected={}",
found_digest.as_str(),
descriptor.digest()
));
return Err(Error::DigestMismatch {
expected: expected_digest.into(),
found: found_digest.into(),
});
}
let descriptor_size: u64 = descriptor.size();
if self.size != descriptor_size {
errs.push(format!(
"Size mismatch; found={} expected={}",
self.size, descriptor_size
));
}
match errs.as_slice() {
[] => self.complete(),
o => {
let o = o.join(" and ");
anyhow::bail!("{o}")
}
return Err(Error::SizeMismatch {
expected: descriptor_size,
found: self.size,
});
}
self.complete()
}

#[context("Completing blob")]
/// Finish writing this blob object.
pub fn complete(mut self) -> Result<Blob> {
let sha256 = hex::encode(self.hash.finish()?);
Expand Down Expand Up @@ -655,7 +657,6 @@ impl<'a> GzipLayerWriter<'a> {
})
}

#[context("Completing layer")]
/// Consume this writer, flushing buffered data and put the blob in place.
pub fn complete(mut self) -> Result<Layer> {
self.compressor.get_mut().clear();
Expand Down Expand Up @@ -779,14 +780,15 @@ mod tests {
assert_eq!(w.fsck().unwrap(), 3);
}

let read_manifest = w.read_manifest().unwrap();
let idx = w.read_index()?.unwrap();
let manifest_desc = idx.manifests().first().unwrap();
let read_manifest = w.read_json_blob(manifest_desc).unwrap();
assert_eq!(&read_manifest, &manifest);

let desc: Descriptor =
w.insert_manifest(manifest, Some("latest"), oci_image::Platform::default())?;
assert!(w.has_manifest(&desc).unwrap());
// There's more than one now
assert!(w.read_manifest().is_err());
assert_eq!(w.read_index().unwrap().unwrap().manifests().len(), 2);

assert!(w.find_manifest_with_tag("noent").unwrap().is_none());
Expand Down