//! 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 { 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 = 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 = 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); }