oci-image 0.3.1

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
//! Per-layer store: each layer unpacked once into its own directory in
//! overlayfs format, shared between images, refcounted and collected.

mod common;

use std::fs;
use std::os::unix::fs::{FileTypeExt, MetadataExt};
use std::path::Path;

use common::*;
use oci_image::{Digest, ImageLayer, LayerStore, Layout, Overlay, OverlayXattr, UnpackOptions};

fn opts() -> UnpackOptions {
    UnpackOptions {
        threads: 4,
        // User-namespace overlays (rootless): user.overlay.* and `userxattr`.
        layout: Layout::Overlay(OverlayXattr::User),
        ..UnpackOptions::default()
    }
}

/// Put a layer tar (gzip) in the store; return blob digest and diff_id.
fn put(store: &LayerStore, entries: &[E]) -> ImageLayer {
    let tar = tar_bytes(entries);
    let gz = gzip(&tar);
    let blob = Digest::of(&gz);
    store.put_blob(&blob, &gz[..]).unwrap();
    ImageLayer {
        blob,
        diff_id: Digest::of(&tar),
    }
}

fn xattr(p: &Path, name: &str) -> Option<Vec<u8>> {
    let c = std::ffi::CString::new(p.as_os_str().as_encoded_bytes()).unwrap();
    let n = std::ffi::CString::new(name).unwrap();
    let mut buf = [0u8; 16];
    let r = unsafe { libc::lgetxattr(c.as_ptr(), n.as_ptr(), buf.as_mut_ptr().cast(), buf.len()) };
    (r >= 0).then(|| buf[..r as usize].to_vec())
}

/// Whether this process may make an overlayfs whiteout (a 0/0 character device) in `dir`.
/// Root may; so may anybody since Linux 5.8 on a filesystem that supports whiteouts (tmpfs,
/// ext4, xfs), but not on an overlayfs upper such as a rootless container's `/`.
fn may_make_whiteouts(dir: &Path) -> bool {
    let c = std::ffi::CString::new(dir.join("probe").as_os_str().as_encoded_bytes()).unwrap();
    unsafe { libc::mknod(c.as_ptr(), libc::S_IFCHR | 0o600, 0) == 0 }
}

#[test]
fn whiteouts_become_overlay_whiteouts_and_opaque_dirs_get_the_xattr() {
    let dir = tempfile::tempdir().unwrap();
    if !may_make_whiteouts(dir.path()) {
        // Written to stderr directly, not through eprintln!, so the skip shows in a CI log
        // even though the test passes (the test harness captures only the print macros).
        use std::io::Write;
        let _ = writeln!(
            std::io::stderr(),
            "SKIPPED whiteouts_become_overlay_whiteouts_and_opaque_dirs_get_the_xattr: this \
             process may not mknod a 0/0 character device in {}; run it as root",
            dir.path().display()
        );
        return;
    }
    let store = LayerStore::open(dir.path()).unwrap();
    let base = put(
        &store,
        &[
            E::Dir("etc/"),
            E::File("etc/a", b"1"),
            E::Dir("opt/"),
            E::File("opt/x", b"x"),
        ],
    );
    let top = put(
        &store,
        &[
            E::File("etc/.wh.a", b""),
            E::File("opt/.wh..wh..opq", b""),
            E::File("opt/y", b"y"),
        ],
    );
    let id = Digest::of(b"image");
    let lowers = store
        .add_image(&id, &[base.clone(), top.clone()], &opts())
        .unwrap();
    assert_eq!(lowers.len(), 2);
    assert_eq!(lowers[0], store.layer_path(&base.diff_id));
    // The lower layer is untouched by the upper one's whiteouts.
    assert_eq!(fs::read(lowers[0].join("etc/a")).unwrap(), b"1");
    assert!(lowers[0].join("opt/x").exists());
    // The upper layer holds them in overlayfs form.
    let wh = fs::symlink_metadata(lowers[1].join("etc/a")).unwrap();
    assert!(wh.file_type().is_char_device());
    assert_eq!(wh.rdev(), 0);
    assert_eq!(
        xattr(&lowers[1].join("opt"), "user.overlay.opaque").as_deref(),
        Some(&b"y"[..])
    );
    assert_eq!(fs::read(lowers[1].join("opt/y")).unwrap(), b"y");
    assert!(!lowers[1].join("etc/.wh.a").exists());
    assert!(!lowers[1].join("opt/.wh..wh..opq").exists());
}

#[test]
fn an_updated_image_unpacks_only_its_new_layers() {
    let dir = tempfile::tempdir().unwrap();
    let store = LayerStore::open(dir.path()).unwrap();
    let l1 = put(&store, &[E::Dir("usr/"), E::File("usr/lib", b"base")]);
    let l2 = put(&store, &[E::File("app", b"v1")]);
    let l2b = put(&store, &[E::File("app", b"v2")]);
    let (v1, v2) = (Digest::of(b"v1"), Digest::of(b"v2"));

    store
        .add_image(&v1, &[l1.clone(), l2.clone()], &opts())
        .unwrap();
    // Unpacked once and kept: put a marker in the shared layer.
    fs::write(store.layer_path(&l1.diff_id).join("marker"), b"").unwrap();
    let missing = store.missing_layers(&[l1.clone(), l2b.clone()]);
    assert_eq!(missing, vec![l2b.clone()]);
    let lowers = store
        .add_image(&v2, &[l1.clone(), l2b.clone()], &opts())
        .unwrap();
    assert!(
        lowers[0].join("marker").exists(),
        "the shared layer was not unpacked again"
    );
    assert_eq!(fs::read(lowers[1].join("app")).unwrap(), b"v2");

    assert_eq!(store.refcount(&l1.diff_id).unwrap(), 2);
    assert_eq!(store.refcount(&l2.diff_id).unwrap(), 1);
    assert_eq!(store.image_layers(&v2).unwrap(), lowers);
}

#[test]
fn gc_removes_what_no_image_holds() {
    let dir = tempfile::tempdir().unwrap();
    let store = LayerStore::open(dir.path()).unwrap();
    let l1 = put(&store, &[E::File("a", b"1")]);
    let l2 = put(&store, &[E::File("b", b"2")]);
    let l3 = put(&store, &[E::File("c", b"3")]);
    let (v1, v2) = (Digest::of(b"v1"), Digest::of(b"v2"));
    store
        .add_image(&v1, &[l1.clone(), l2.clone()], &opts())
        .unwrap();
    store
        .add_image(&v2, &[l1.clone(), l3.clone()], &opts())
        .unwrap();

    store.remove_image(&v1).unwrap();
    assert_eq!(store.refcount(&l1.diff_id).unwrap(), 1);
    let gc = store.gc().unwrap();
    assert_eq!(gc.layers, 1);
    assert!(!store.layer_path(&l2.diff_id).exists());
    assert!(!store.has_blob(&l2.blob));
    assert!(store.layer_path(&l1.diff_id).exists());
    assert!(store.layer_path(&l3.diff_id).exists());
    assert!(store.image_layers(&v1).is_err());
}

#[test]
fn a_layer_whose_content_is_not_its_diff_id_is_refused() {
    let dir = tempfile::tempdir().unwrap();
    let store = LayerStore::open(dir.path()).unwrap();
    let mut l = put(&store, &[E::File("a", b"1")]);
    l.diff_id = Digest::of(b"something else");
    assert!(store
        .add_image(&Digest::of(b"x"), &[l.clone()], &opts())
        .is_err());
    assert!(!store.layer_path(&l.diff_id).exists());
}

#[test]
fn overlays_stack_layers_top_first() {
    let dir = tempfile::tempdir().unwrap();
    let (a, b) = (dir.path().join("L1"), dir.path().join("L2"));
    fs::create_dir_all(&a).unwrap();
    fs::create_dir_all(&b).unwrap();
    let ov = Overlay::for_layers(
        &[a.clone(), b.clone()],
        &dir.path().join("c"),
        OverlayXattr::User,
    )
    .unwrap();
    assert_eq!(ov.lower, vec![b.clone(), a.clone()]);
    let data = ov.mount_data();
    assert!(data.starts_with(&format!("lowerdir={}:{},", b.display(), a.display())));
    assert!(data.ends_with(",userxattr"));
}

#[test]
fn a_layer_twice_in_one_image_is_stacked_once_at_its_top_position() {
    // overlayfs refuses the same lower directory twice (ELOOP); images do
    // repeat layers (the empty layer). The upper copy re-applies everything
    // the lower one did, so the lower one can go.
    let dir = tempfile::tempdir().unwrap();
    let (e, a, b) = (
        dir.path().join("E"),
        dir.path().join("A"),
        dir.path().join("B"),
    );
    for d in [&e, &a, &b] {
        fs::create_dir_all(d).unwrap();
    }
    let layers = [a.clone(), e.clone(), b.clone(), e.clone()];
    let ov = Overlay::for_layers(&layers, &dir.path().join("c"), OverlayXattr::Trusted).unwrap();
    assert_eq!(ov.lower, vec![e, b, a]);
}

/// A layer's blob is pruned once the layer is unpacked (`prune_unpacked_blobs`), and a later pull
/// of an image with that layer must not download it again: the store still holds what the blob
/// unpacks to. `holds_blob` answers for both, and for nothing else.
#[test]
fn a_pruned_blob_whose_layer_is_unpacked_is_still_held() {
    let dir = tempfile::tempdir().unwrap();
    let store = LayerStore::open(dir.path()).unwrap();
    let base = put(&store, &[E::Dir("etc/"), E::File("etc/a", b"1")]);
    let loose = put(&store, &[E::File("b", b"2")]);
    assert!(store.holds_blob(&base.blob) && store.holds_blob(&loose.blob));

    store
        .add_image(&Digest::of(b"image"), std::slice::from_ref(&base), &opts())
        .unwrap();
    assert_eq!(store.prune_unpacked_blobs().unwrap(), 1);
    assert!(!store.has_blob(&base.blob));
    assert!(store.holds_blob(&base.blob), "the layer is unpacked");
    assert!(store.holds_blob(&loose.blob), "the blob is there");
    assert!(!store.holds_blob(&Digest::of(b"never seen")));

    // A layer directory removed (gc) is no longer held through its record.
    fs::remove_dir_all(store.layer_path(&base.diff_id)).unwrap();
    assert!(!store.holds_blob(&base.blob));
}