Files
JujuforceandClaude Opus 4.8 d79ded6ee5 test: add synthetic, committable e2e fixtures (base/delta app + opt)
Forge three small fixtures with no proprietary content so the e2e test runs by
default and covers the full decrypt+extract path, including the delta/base VHD merge:

- TEST_T001_..._0.opt              encrypted exFAT (OPTION key)
- TEST_1.00.00_..._0.app           base APP: outer NTFS -> internal_0.vhd (fixed) -> inner NTFS
- TEST_1.01.00_..._1_1.00.00.app   delta APP: differencing internal_1.vhd, auto-merged with base by GUID

The APP fixtures use the synthetic game id TEST, decrypted via the committed
TEST.bin external key (also exercising that key-file fallback). e2e.rs copies the
fixtures + key into a temp dir and runs from there so the delta finds its base and
the key. .gitignore whitelists only these synthetic files so real containers
dropped in fixtures/ stay private. The generator is kept local, not committed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 13:37:37 +02:00

97 lines
3.6 KiB
Rust

//! End-to-end decryption test driven by the fixtures in `fixtures/`.
//!
//! The committed fixtures are small, fully synthetic containers (see
//! `fixtures/README.md`) — a base APP, a delta APP, and an OPTION — so this runs
//! the whole decrypt-and-extract path, including the delta/base VHD merge, with
//! no proprietary data. You can also drop your own real `.app`/`.opt` files in
//! `fixtures/` (they are git-ignored) and they'll be exercised too. When
//! `fixtures/` is empty the test is a no-op, so the suite stays green regardless.
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
fn fixture_files() -> Vec<PathBuf> {
let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("fixtures");
let Ok(entries) = fs::read_dir(dir) else {
return Vec::new();
};
let mut files: Vec<PathBuf> = entries
.filter_map(|e| e.ok().map(|e| e.path()))
.filter(|p| {
p.is_file()
&& matches!(
p.extension().and_then(|e| e.to_str()),
Some("app") | Some("opt")
)
})
.collect();
files.sort();
files
}
#[test]
fn decrypts_fixture_containers() {
let files = fixture_files();
if files.is_empty() {
eprintln!(
"skipping e2e: no .app/.opt fixtures in {}/fixtures — add containers to run this test",
env!("CARGO_MANIFEST_DIR")
);
return;
}
// Work in a temp dir so extraction never touches the fixtures folder. The
// tool extracts next to its input, so we copy every fixture in *first* — a
// delta APP needs its base APP sitting alongside for the auto-merge.
let workdir = std::env::temp_dir().join(format!("fsdecrypt-e2e-{}", std::process::id()));
let _ = fs::remove_dir_all(&workdir);
fs::create_dir_all(&workdir).expect("create work dir");
// Copy *every* fixture file in — the containers plus any external
// `{game_id}.bin` key files an unlisted game needs.
let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("fixtures");
if let Ok(entries) = fs::read_dir(&dir) {
for e in entries.filter_map(|e| e.ok()) {
let p = e.path();
if p.is_file() {
let _ = fs::copy(&p, workdir.join(p.file_name().unwrap()));
}
}
}
let inputs: Vec<PathBuf> = files
.iter()
.map(|f| workdir.join(f.file_name().unwrap()))
.collect();
for input in &inputs {
let name = input.file_name().unwrap().to_os_string();
// Run from the work dir so the binary resolves `{game_id}.bin` keys there.
let output = Command::new(env!("CARGO_BIN_EXE_fsdecrypt"))
.current_dir(&workdir)
.arg(input)
.output()
.expect("failed to run fsdecrypt");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"decrypting {name:?} failed (exit {:?})\n--- stdout ---\n{stdout}\n--- stderr ---\n{stderr}",
output.status.code()
);
// A default run extracts into a folder named after the input (no extension).
let out_dir = input.with_extension("");
let produced = fs::read_dir(&out_dir).map(|rd| rd.count()).unwrap_or(0);
assert!(
produced > 0,
"{name:?}: expected extracted entries in {}\n--- stdout ---\n{stdout}",
out_dir.display()
);
println!("ok: {name:?} -> {produced} top-level entr(y/ies) extracted");
}
let _ = fs::remove_dir_all(&workdir);
}