oci-image 0.3.0

OCI container images on disk: image references, sha256 digests, one-pass layer unpack (whiteouts and opaque directories), gzip (all cores via lgz one-pass streaming decode; small layers on one core) and zstd layers, a content-addressed store of blobs and per-layer directories in overlayfs format (each layer unpacked once, shared between images, refcounted, collected), and overlay snapshot prep.
Documentation
use std::fmt;
use std::io::{self, Read};
use std::str::FromStr;

use sha2::{Digest as _, Sha256};

use crate::{Error, Result};

/// A `sha256:<hex>` content digest. Parsing is strict (64 lowercase hex
/// digits), so a digest is always safe to use as a file name.
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Digest {
    hex: String,
}

impl Digest {
    /// The digest of `bytes`.
    pub fn of(bytes: &[u8]) -> Self {
        Self::from_hasher(Sha256::new().chain_update(bytes))
    }

    /// The digest of everything `reader` yields.
    pub fn of_reader(mut reader: impl Read) -> io::Result<Self> {
        let mut h = Hasher::default();
        let mut buf = vec![0u8; 1 << 16];
        loop {
            let n = reader.read(&mut buf)?;
            if n == 0 {
                return Ok(h.finish());
            }
            h.update(&buf[..n]);
        }
    }

    fn from_hasher(h: Sha256) -> Self {
        let hex = h.finalize().iter().map(|b| format!("{b:02x}")).collect();
        Self { hex }
    }

    /// The 64 hex digits.
    pub fn hex(&self) -> &str {
        &self.hex
    }

    /// `Ok` when `actual` is this digest, a [`Error::DigestMismatch`] otherwise.
    pub fn verify(&self, actual: &Digest) -> Result<()> {
        if self == actual {
            Ok(())
        } else {
            Err(Error::DigestMismatch {
                expected: self.clone(),
                actual: actual.clone(),
            })
        }
    }
}

impl fmt::Display for Digest {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "sha256:{}", self.hex)
    }
}

impl FromStr for Digest {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self> {
        let hex = s
            .strip_prefix("sha256:")
            .filter(|h| h.len() == 64 && h.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f')))
            .ok_or_else(|| Error::BadDigest(s.to_string()))?;
        Ok(Self {
            hex: hex.to_string(),
        })
    }
}

impl serde::Serialize for Digest {
    fn serialize<S: serde::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
        s.collect_str(self)
    }
}

impl<'de> serde::Deserialize<'de> for Digest {
    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
        let s = String::deserialize(d)?;
        s.parse().map_err(serde::de::Error::custom)
    }
}

/// Incremental sha256, for blobs that arrive in chunks.
#[derive(Default, Clone)]
pub struct Hasher(Sha256);

impl Hasher {
    pub fn update(&mut self, bytes: &[u8]) {
        self.0.update(bytes);
    }

    pub fn finish(self) -> Digest {
        Digest::from_hasher(self.0)
    }
}