tarzan 0.2.0

Random-access, seekable .tar.zst archives with an embedded table-of-contents index
Documentation
use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;

use tempfile::tempdir;

fn fixture_root() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("testdata/fixtures/tiny-tree")
        .canonicalize()
        .expect("fixture path should exist")
}

fn ensure_tools() {
    for tool in ["tar", "zstd", "sh"] {
        let status = Command::new("sh")
            .arg("-c")
            .arg(format!("command -v {tool} >/dev/null"))
            .status()
            .expect("failed to check command availability");
        assert!(status.success(), "required tool is not available: {tool}");
    }
}

fn create_tar_from_fixture(output_tar: &Path) {
    let fixture = fixture_root();
    let mut cmd = Command::new("tar");
    #[cfg(target_os = "macos")]
    cmd.env("COPYFILE_DISABLE", "1");
    let status = cmd
        .arg("-cf")
        .arg(output_tar)
        .arg("-C")
        .arg(&fixture)
        .arg(".")
        .status()
        .expect("failed to run tar command");
    assert!(status.success(), "tar command failed with status {status}");
}

fn list_files(root: &Path, current: &Path, out: &mut BTreeMap<PathBuf, Vec<u8>>) {
    let entries = fs::read_dir(current).expect("failed to read directory");
    for entry in entries {
        let entry = entry.expect("failed to read directory entry");
        let path = entry.path();
        let metadata = entry.metadata().expect("failed to read metadata");
        if metadata.is_dir() {
            list_files(root, &path, out);
            continue;
        }
        if metadata.is_file() {
            let relative = path
                .strip_prefix(root)
                .expect("path should be under root")
                .to_path_buf();
            out.insert(relative, fs::read(path).expect("failed to read file bytes"));
        }
    }
}

fn file_map(root: &Path) -> BTreeMap<PathBuf, Vec<u8>> {
    let mut out = BTreeMap::new();
    list_files(root, root, &mut out);
    out
}

#[test]
fn wrapped_archives_extract_with_standard_tools() {
    ensure_tools();

    let temp = tempdir().expect("failed to create tempdir");
    let tar_path = temp.path().join("source.tar");
    let archive_path = temp.path().join("archive.tar.zst");
    let out_pipe = temp.path().join("out-pipe");
    let out_tar = temp.path().join("out-tar-zstd");
    fs::create_dir_all(&out_pipe).expect("failed to create output dir");
    fs::create_dir_all(&out_tar).expect("failed to create output dir");

    create_tar_from_fixture(&tar_path);
    let input = fs::File::open(&tar_path).expect("failed to open source tar");
    let output = fs::File::create(&archive_path).expect("failed to create archive");
    tarzan::wrap(input, output, tarzan::WrapOptions::default()).expect("wrap should succeed");

    let pipe_status = Command::new("sh")
        .arg("-c")
        .arg("zstd -d -q -c \"$1\" | tar -x -C \"$2\"")
        .arg("sh")
        .arg(&archive_path)
        .arg(&out_pipe)
        .status()
        .expect("failed to run zstd|tar pipeline");
    assert!(
        pipe_status.success(),
        "zstd -d | tar x failed with status {pipe_status}"
    );

    let tar_status = Command::new("tar")
        .arg("--zstd")
        .arg("-xf")
        .arg(&archive_path)
        .arg("-C")
        .arg(&out_tar)
        .status()
        .expect("failed to run tar --zstd -xf");
    assert!(
        tar_status.success(),
        "tar --zstd -xf failed with status {tar_status}"
    );

    let expected = file_map(&fixture_root());
    let extracted_pipe = file_map(&out_pipe);
    let extracted_tar = file_map(&out_tar);

    assert_eq!(extracted_pipe, expected);
    assert_eq!(extracted_tar, expected);
}