oci-image 0.1.0

OCI container images on disk: image references, sha256 digests, one-pass layer unpack (whiteouts and opaque directories), gzip (parallel via lgz when the stream allows it, streaming zlib-rs otherwise) 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), overlay snapshot prep, and an optional anonymous registry pull.
Documentation
use std::fs;
use std::path::{Path, PathBuf};

use crate::error::Context;
use crate::{Error, Result};

/// The directories of one overlay mount: read-only lower layers (top first),
/// and the container's own upper, work and merged directories.
///
/// [`Overlay::prepare`] only creates directories; mounting is the caller's
/// (`mount -t overlay overlay -o <mount_data> <merged>`, or the equivalent
/// `mount(2)`), since it needs privileges this crate does not assume. The
/// mount data must fit one page (4096 bytes); with long store paths and many
/// layers, mount from inside the store with relative lower paths.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Overlay {
    pub lower: Vec<PathBuf>,
    pub upper: PathBuf,
    pub work: PathBuf,
    pub merged: PathBuf,
    /// Mount with `userxattr` (markers in `user.overlay.*`, user namespaces).
    pub userxattr: bool,
}

impl Overlay {
    /// Create `<dir>/{upper,work,merged}` over `lower` (topmost first).
    pub fn prepare(lower: Vec<PathBuf>, dir: &Path) -> Result<Self> {
        let ov = Self {
            lower,
            upper: dir.join("upper"),
            work: dir.join("work"),
            merged: dir.join("merged"),
            userxattr: false,
        };
        ov.create()?;
        Ok(ov)
    }

    /// An overlay over image layers given bottom first (as
    /// [`crate::LayerStore::image_layers`] returns them), with the xattr
    /// namespace the layers were unpacked with.
    pub fn for_layers(layers: &[PathBuf], dir: &Path, xattr: crate::OverlayXattr) -> Result<Self> {
        let mut ov = Self::prepare(Self::stack(layers), dir)?;
        ov.userxattr = xattr == crate::OverlayXattr::User;
        Ok(ov)
    }

    /// The lowerdir order for image layers given bottom first: top first,
    /// and a layer that repeats only at its topmost place (overlayfs refuses
    /// a lower directory twice; the upper copy re-applies everything the
    /// lower one did).
    pub fn stack(layers: &[PathBuf]) -> Vec<PathBuf> {
        let mut lower: Vec<PathBuf> = Vec::with_capacity(layers.len());
        for l in layers.iter().rev() {
            if !lower.contains(l) {
                lower.push(l.clone());
            }
        }
        lower
    }

    /// Create the upper, work and merged directories of an overlay laid out
    /// by the caller.
    pub fn create(&self) -> Result<()> {
        if self.lower.is_empty() {
            return Err(Error::Invalid("an overlay needs a lower directory".into()));
        }
        for d in [&self.upper, &self.work, &self.merged] {
            fs::create_dir_all(d).ctx(|| format!("creating {}", d.display()))?;
        }
        Ok(())
    }

    /// The overlayfs mount data, with `\`, `,` and `:` in paths escaped.
    pub fn mount_data(&self) -> String {
        let lower: Vec<String> = self.lower.iter().map(|p| escape(p)).collect();
        let mut data = format!(
            "lowerdir={},upperdir={},workdir={}",
            lower.join(":"),
            escape(&self.upper),
            escape(&self.work)
        );
        if self.userxattr {
            data.push_str(",userxattr");
        }
        data
    }
}

fn escape(p: &Path) -> String {
    let s = p.display().to_string();
    let mut out = String::with_capacity(s.len());
    for c in s.chars() {
        if matches!(c, '\\' | ',' | ':') {
            out.push('\\');
        }
        out.push(c);
    }
    out
}