mirror of
https://gitea.tendokyu.moe/beerpsi/fsdecrypt.git
synced 2026-09-22 22:17:55 +03:00
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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
a73f71e3de
commit
d79ded6ee5
+11
-2
@@ -10,5 +10,14 @@ flamegraph.svg
|
|||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
memory/
|
memory/
|
||||||
|
|
||||||
# Local e2e test fixtures (real, proprietary containers — never committed)
|
# e2e fixtures: ignore anything dropped into fixtures/ (your own real containers
|
||||||
/fixtures/
|
# stay private) EXCEPT the committed synthetic test fixtures and their README.
|
||||||
|
/fixtures/*
|
||||||
|
!/fixtures/README.md
|
||||||
|
!/fixtures/TEST.bin
|
||||||
|
!/fixtures/TEST_1.00.00_20240101120000_0.app
|
||||||
|
!/fixtures/TEST_1.01.00_20240102120000_1_1.00.00.app
|
||||||
|
!/fixtures/TEST_T001_20240101120000_0.opt
|
||||||
|
|
||||||
|
# Fixture generator (kept local, not committed)
|
||||||
|
/tests/fixtures_gen/
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# Test fixtures
|
||||||
|
|
||||||
|
These are **synthetic** SEGA fscrypt containers used by `tests/e2e.rs`. They
|
||||||
|
contain only dummy files (a few text/XML/stub binaries), so they are safe to
|
||||||
|
commit — there is no proprietary game content inside.
|
||||||
|
|
||||||
|
| File | Type | What it exercises |
|
||||||
|
|------|------|-------------------|
|
||||||
|
| `TEST_T001_20240101120000_0.opt` | OPTION | exFAT decrypt + extract (built-in OPTION key) |
|
||||||
|
| `TEST_1.00.00_20240101120000_0.app` | APP (base) | outer NTFS → `internal_0.vhd` (fixed VHD) → inner NTFS extract |
|
||||||
|
| `TEST_1.01.00_20240102120000_1_1.00.00.app` | APP (delta) | differencing `internal_1.vhd`, auto-merged against the base by VHD GUID |
|
||||||
|
| `TEST.bin` | key | external key (16-byte key + 16-byte IV) for game id `TEST` |
|
||||||
|
|
||||||
|
The delta is a real differencing VHD linked to the base; decrypting it (with the
|
||||||
|
base alongside) merges the two and yields the base files plus the delta's added
|
||||||
|
`data/patch_notes.txt` and a modified `readme.txt`.
|
||||||
|
|
||||||
|
The `.app` fixtures use the synthetic game id `TEST`, which is **not** in the
|
||||||
|
built-in key table — so they are decrypted via the external-key-file fallback
|
||||||
|
using the committed `TEST.bin` (this also exercises that fallback path).
|
||||||
|
`tests/e2e.rs` copies `TEST.bin` next to the containers and runs from there so
|
||||||
|
the binary finds it. The OPTION fixture uses the built-in OPTION key. Everything
|
||||||
|
here — keys and filesystem data alike — is synthetic.
|
||||||
|
|
||||||
|
You can also drop your own **real** `.app`/`.opt` files in this folder to test
|
||||||
|
against them; everything here except these committed fixtures is git-ignored.
|
||||||
|
|
||||||
|
## Regenerating
|
||||||
|
|
||||||
|
These are generated by a local, uncommitted script set (NTFS/exFAT image
|
||||||
|
creation in Docker + a small SEGA fscrypt packer adapted from
|
||||||
|
[`beerpsi/x`](https://gitea.tendokyu.moe/beerpsi/x)). The byte output is not
|
||||||
|
reproducible (NTFS/exFAT embed creation timestamps), but regenerated fixtures are
|
||||||
|
functionally equivalent.
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+30
-13
@@ -1,10 +1,11 @@
|
|||||||
//! End-to-end decryption test driven by local fixtures.
|
//! End-to-end decryption test driven by the fixtures in `fixtures/`.
|
||||||
//!
|
//!
|
||||||
//! Drop real `.app` / `.opt` containers into the git-ignored `fixtures/`
|
//! The committed fixtures are small, fully synthetic containers (see
|
||||||
//! directory at the crate root. Each one is copied to a temp dir, run through
|
//! `fixtures/README.md`) — a base APP, a delta APP, and an OPTION — so this runs
|
||||||
//! the built binary, and checked for a non-empty extraction output. When
|
//! the whole decrypt-and-extract path, including the delta/base VHD merge, with
|
||||||
//! `fixtures/` is absent or empty the test is a no-op, so the suite stays green
|
//! no proprietary data. You can also drop your own real `.app`/`.opt` files in
|
||||||
//! without the (proprietary) sample data.
|
//! `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::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
@@ -41,18 +42,34 @@ fn decrypts_fixture_containers() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Work in a temp dir so extraction never touches the fixtures folder. The
|
// Work in a temp dir so extraction never touches the fixtures folder. The
|
||||||
// tool extracts next to its input, so we copy each fixture in first.
|
// 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 workdir = std::env::temp_dir().join(format!("fsdecrypt-e2e-{}", std::process::id()));
|
||||||
let _ = fs::remove_dir_all(&workdir);
|
let _ = fs::remove_dir_all(&workdir);
|
||||||
fs::create_dir_all(&workdir).expect("create work dir");
|
fs::create_dir_all(&workdir).expect("create work dir");
|
||||||
|
|
||||||
for src in &files {
|
// Copy *every* fixture file in — the containers plus any external
|
||||||
let name = src.file_name().unwrap().to_os_string();
|
// `{game_id}.bin` key files an unlisted game needs.
|
||||||
let input = workdir.join(&name);
|
let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("fixtures");
|
||||||
fs::copy(src, &input).expect("copy fixture into work dir");
|
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"))
|
let output = Command::new(env!("CARGO_BIN_EXE_fsdecrypt"))
|
||||||
.arg(&input)
|
.current_dir(&workdir)
|
||||||
|
.arg(input)
|
||||||
.output()
|
.output()
|
||||||
.expect("failed to run fsdecrypt");
|
.expect("failed to run fsdecrypt");
|
||||||
|
|
||||||
@@ -69,7 +86,7 @@ fn decrypts_fixture_containers() {
|
|||||||
let produced = fs::read_dir(&out_dir).map(|rd| rd.count()).unwrap_or(0);
|
let produced = fs::read_dir(&out_dir).map(|rd| rd.count()).unwrap_or(0);
|
||||||
assert!(
|
assert!(
|
||||||
produced > 0,
|
produced > 0,
|
||||||
"{name:?}: expected extracted entries in {}",
|
"{name:?}: expected extracted entries in {}\n--- stdout ---\n{stdout}",
|
||||||
out_dir.display()
|
out_dir.display()
|
||||||
);
|
);
|
||||||
println!("ok: {name:?} -> {produced} top-level entr(y/ies) extracted");
|
println!("ok: {name:?} -> {produced} top-level entr(y/ies) extracted");
|
||||||
|
|||||||
Reference in New Issue
Block a user