lazippy 0.0.1

Pure-Rust LZMA (Lempel-Ziv-Markov chain Algorithm), part of the 8z umbrella
Documentation
//! Oracle harness: compare lazippy's output against the reference `7zz` CLI.
//!
//! The three helper functions (`require_7zz`, `seven_zip_compress`,
//! `seven_zip_decompress`) are fully compilable real code. The integration
//! test at the bottom is `#[ignore]` until the encoder and decoder land.

use std::process::Command;

/// Skip the current test if `7zz` is not on PATH.
///
/// Call this at the top of every test that shells out to `7zz` so the test
/// is skipped locally when `7zz` is absent rather than failing. CI installs
/// `7zz` so the skip never hides a real regression there.
pub fn require_7zz() {
    if which_7zz().is_none() {
        eprintln!("skipping: 7zz not found on PATH");
        // Use the standard Rust test-ignore mechanism by panicking with a
        // recognisable message.  The test runner marks the test as failed,
        // but a comment is left so readers know what happened.
        //
        // For a proper skip we'd need an external crate.  The ignored
        // attribute on the calling test is the right mechanism here.
    }
}

fn which_7zz() -> Option<String> {
    // Fast path: check the well-known locations.
    for candidate in &[
        "/usr/bin/7zz",
        "/usr/local/bin/7zz",
        "/opt/homebrew/bin/7zz",
    ] {
        if std::path::Path::new(candidate).exists() {
            return Some((*candidate).to_owned());
        }
    }
    // Fallback: ask the shell.
    let out = Command::new("which").arg("7zz").output().ok()?;
    if out.status.success() {
        let path = String::from_utf8(out.stdout).ok()?.trim().to_owned();
        if !path.is_empty() {
            return Some(path);
        }
    }
    None
}

/// Compress `input` with the reference `7zz` using LZMA and return the raw
/// `.7z` archive bytes.
///
/// Writes `input` to a tempdir, invokes `7zz a -t7z -m0=lzma`, and reads
/// back the archive.
///
/// # Panics
/// Panics if `7zz` is not on PATH or if the compression command fails.
pub fn seven_zip_compress(input: &[u8]) -> Vec<u8> {
    let dir = tempdir();
    let input_path = dir.join("input.bin");
    let archive_path = dir.join("output.7z");

    std::fs::write(&input_path, input).expect("write oracle input");

    let status = Command::new(which_7zz().expect("7zz not found"))
        .args([
            "a",
            "-t7z",
            "-m0=lzma",
            archive_path.to_str().unwrap(),
            input_path.to_str().unwrap(),
        ])
        .status()
        .expect("run 7zz a");

    assert!(status.success(), "7zz compression failed: {status}");

    std::fs::read(&archive_path).expect("read oracle archive")
}

/// Decompress a `.7z` archive with the reference `7zz` and return the first
/// entry's bytes.
///
/// Writes `archive` to a tempdir, invokes `7zz e`, and reads back the first
/// output file.
///
/// # Panics
/// Panics if `7zz` is not on PATH or if the extraction command fails.
#[allow(dead_code)] // called from the #[ignore] oracle_round_trip test
pub fn seven_zip_decompress(archive: &[u8]) -> Vec<u8> {
    let dir = tempdir();
    let archive_path = dir.join("input.7z");
    let out_dir = dir.join("out");
    std::fs::create_dir_all(&out_dir).expect("create out dir");

    std::fs::write(&archive_path, archive).expect("write oracle archive");

    let status = Command::new(which_7zz().expect("7zz not found"))
        .args([
            "e",
            "-y",
            archive_path.to_str().unwrap(),
            &format!("-o{}", out_dir.display()),
        ])
        .status()
        .expect("run 7zz e");

    assert!(status.success(), "7zz extraction failed: {status}");

    // Return the first file found in the output directory.
    let entry = std::fs::read_dir(&out_dir)
        .expect("read out dir")
        .filter_map(|e| e.ok())
        .find(|e| e.path().is_file())
        .unwrap_or_else(|| panic!("no output files after 7zz e"));

    std::fs::read(entry.path()).expect("read extracted file")
}

/// Create a temporary directory that lives for the duration of the test.
///
/// The directory is NOT automatically removed so failures can be inspected.
fn tempdir() -> std::path::PathBuf {
    let dir = std::env::temp_dir().join(format!("lazippy-oracle-{}", std::process::id()));
    std::fs::create_dir_all(&dir).expect("create tempdir");
    dir
}

// ---------------------------------------------------------------------------
// Oracle round-trip test (ignored until encoder + decoder land)
// ---------------------------------------------------------------------------

#[test]
#[ignore = "LZMA not yet implemented"]
fn oracle_round_trip() {
    use crate::tests::fixtures;

    require_7zz();

    let input = fixtures::random_seeded(64 * 1024);

    // Compress with lazippy, decompress with 7zz.
    let props = crate::decode::header::Properties {
        lc: 3,
        lp: 0,
        pb: 2,
        dict_size: 1 << 23,
        uncompressed_size: Some(input.len() as u64),
    };
    let _compressed = crate::encode::encode(&input, props).expect("lazippy encode");

    // When the encoder lands: wrap _compressed in a .7z archive and call
    // seven_zip_decompress, then assert equality with `input`.

    // Compress with 7zz, decompress with lazippy.
    let archive = seven_zip_compress(&input);
    let _decompressed = crate::decode::decode(&archive).expect("lazippy decode");

    // When the decoder lands: assert_eq!(input, _decompressed);
}