diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a9ac753..3a1ff2b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,6 +43,14 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + # libewf's ewfexport is the independent oracle for core/tests/corpus_differential.rs. + # Without it the differential resolves no binary and silently no-ops, so the + # reader is never cross-validated in CI. Linux only: the Debian `ewf-tools` + # package ships /usr/bin/ewfexport, and the differential skips cleanly on the + # macOS and Windows legs where no oracle is installed. + - name: Install ewfexport oracle + if: matrix.os == 'ubuntu-latest' + run: sudo apt-get update && sudo apt-get install -y ewf-tools # Whole workspace across Linux/macOS/Windows: reader (core, incl. real-corpus # e2e_dftt / corpus_differential / lazy_byte_identity / segment_source_identity), # the CLI, and the forensic analyzer's integrity suite. diff --git a/core/tests/corpus_differential.rs b/core/tests/corpus_differential.rs index 7ee73c3..b4fbb2e 100644 --- a/core/tests/corpus_differential.rs +++ b/core/tests/corpus_differential.rs @@ -1,20 +1,56 @@ /// Byte-level differential tests: EwfReader bytes must match `ewfexport -f raw -u` output. /// -/// These tests skip automatically if libewf's `ewfexport` is not installed, -/// so they run in CI only on machines where libewf is available. +/// These tests skip automatically if libewf's `ewfexport` is not installed. /// They verify correctness against an independent authoritative reference rather /// than against the library's own MD5 hashes (which share the same blind spots). use ewf::EwfReader; use std::io::{Read, Seek, SeekFrom}; use std::path::Path; +use std::process::Command; -const EWFEXPORT: &str = "/usr/local/bin/ewfexport"; const DATA_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data"); +/// Resolve a usable `ewfexport`, or `None` (the differential then skips). +/// +/// `PATH` is probed first, so any install location works — including ones a +/// fixed list cannot anticipate: another package manager's prefix, or a +/// hand-built install. The absolute +/// candidates are the fallback for a stripped `PATH`: Homebrew on Apple silicon +/// and Intel, then `/usr/bin`, where Debian/Ubuntu's `libewf-tools` package +/// lands it. `EWFEXPORT_BIN` overrides both. +/// +/// The hardcoded `/usr/local/bin` path this replaces matched only an Intel-Homebrew +/// or hand-built install, so this differential skipped silently everywhere else. +fn ewfexport_bin() -> Option { + if let Ok(explicit) = std::env::var("EWFEXPORT_BIN") { + return usable(&explicit); + } + [ + "ewfexport", + "/opt/homebrew/bin/ewfexport", + "/usr/local/bin/ewfexport", + "/usr/bin/ewfexport", + ] + .into_iter() + .find_map(usable) +} + +/// A candidate counts only if it actually executes. `ewfexport` has no `--version` +/// flag and exits non-zero on `-h`, so presence-plus-executability is established +/// by spawning it and accepting any completed run; a bare `Path::exists()` check +/// would accept a non-executable file. +fn usable(candidate: &str) -> Option { + Command::new(candidate) + .arg("-h") + .output() + .ok() + .map(|_| candidate.to_string()) +} + fn ewf_matches_ewfexport(e01_name: &str) { - if !Path::new(EWFEXPORT).exists() { + let Some(ewfexport) = ewfexport_bin() else { return; - } + }; let e01 = format!("{DATA_DIR}/{e01_name}"); if !Path::new(&e01).exists() { return; @@ -24,7 +60,7 @@ fn ewf_matches_ewfexport(e01_name: &str) { let raw_stem = tmp.path().join("reference"); let raw_path = tmp.path().join("reference.raw"); - let ok = std::process::Command::new(EWFEXPORT) + let ok = Command::new(&ewfexport) .args(["-f", "raw", "-u", "-t", raw_stem.to_str().unwrap(), &e01]) .status() .expect("spawn ewfexport")