The exfat-fs empty-file fix branch was rebuilt on top of the upstream v0.1.3
release tag (instead of an orphan commit) so it has proper shared history and
reads as a clean "1 commit ahead of v0.1.3". Update the pinned rev accordingly;
tree and behavior are identical (v0.1.3 + the one-function fix).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
exfat-fs 0.1.3's StreamExtensionEntry::valid() rejects any entry with
FirstCluster < 2, but a zero-length file (or empty dir) legitimately has
FirstCluster = 0 / DataLength = 0 per the exFAT spec. The parser returned
InvalidStreamExtension and aborted the *entire* directory tree the moment it
hit one empty file, so real OPTION (.opt) containers failed to extract with:
WARNING: Failed to extract exfat contents: InvalidFileEntry(InvalidStreamExtension)
even though unsega/7-Zip read them fine.
0.1.3 is the latest published release and its `main` has an incompatible
public API, so override the dependency via [patch.crates-io] with a fork
branch that is the 0.1.3 release plus only the one-function fix (gate the
cluster check on DataLength: 0 -> FirstCluster must be 0, else >= 2).
Regenerate the synthetic OPTION fixture with a zero-byte file so tests/e2e.rs
reproduces the bug: without the patch it fails with InvalidStreamExtension;
with it, extraction succeeds. Verified against a real 191 MB OPTION container
(1602 files / 1217 dirs, empty file written as 0 bytes) and the full suite.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
install-context-menu.bat / uninstall-context-menu.bat add a per-user (HKCU)
Explorer entry for .app and .opt files, with an embedded unpack icon
(assets/fsdecrypt.ico). No admin needed; the normal double-click association is
untouched. The registered command keeps the console open after extraction.
Documented in the README.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When a file or directory has a separate 8.3 short name, it appears in the NTFS
directory index twice: once under its Win32 long name and once under the Dos
short name. The extractor iterated all index entries, so every such item was
written a second time under its mangled NAME~1.EXT alias — and short-named
directories had their entire subtree re-extracted.
Skip index entries in the Dos namespace (the file is still extracted via its
Win32 entry). Add unit tests for the skip decision.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Decrypts real .app/.opt containers dropped into a git-ignored fixtures/ dir
(copied to a temp dir first so extraction never touches the fixtures), asserting
each produces a non-empty extraction. No-op when fixtures/ is empty, so the suite
stays green without the proprietary sample data.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Runs the built binary with --help and asserts it exits 0 and prints the usage, options, and about text (visible via cargo test -- --nocapture).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Unit tests for calculate_page_iv (incl. 8-byte wrap), calculate_file_iv round-trip, and game-key lookup
- Extract exfat_utc_offset_seconds and nt_timestamp_to_system_time as pure helpers and test them (covers the recent exFAT UtcOffset fix)
- scripts/test.sh runs cargo test + a smoke test natively, then repeats for the x86_64-musl target in a clux/muslrust container (no hosted CI needed)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Output directory is now derived from the input .app path (without
extension) instead of the intermediate VHD filename.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace Set-VHD + Merge-VHD PowerShell cmdlets with a pure Rust
MergedVhdReader that overlays delta blocks on the base VHD using
the sector bitmap to decide which sectors come from which disk.
No actual merge to disk needed — reads directly from both files.
- No admin privileges or UAC prompts required
- No Hyper-V or PowerShell dependency
- Cross-platform (works on Windows and Unix)
- Remove #[cfg(windows)] from merge/extract path
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Move VHD parsing and NTFS extraction to src/vhd.rs
- Replace all hardcoded magic values with named constants
- Extract shared progress bar style to constant
- Simplify main.rs merge/resolve logic with helper functions
- Condense formatting without changing behavior
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace PowerShell-based VHD mounting (Set-VHD/Mount-VHD/Copy-Item)
with a pure Rust implementation. New src/vhd.rs module:
- VhdReader<R> parses fixed and dynamic VHD formats
- Handles MBR partition tables and NTFS offset detection
- Dynamic VHD support with BAT (Block Allocation Table) parsing
- Recursive NTFS directory extraction with timestamp preservation
- No elevation, no Hyper-V, works cross-platform
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
After decryption and merging, VHDs are now mounted via Hyper-V,
their contents copied to a same-named folder (excluding system
dirs), then dismounted and deleted. Handles partitions without
drive letters by assigning one temporarily. Uses a temp .ps1
script to avoid nested PowerShell quoting issues.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When no base VHD is found, fsdecrypt now also searches for the base
.app file ({game_id}_*_0.app) in the same directory, extracts its
VHD, and then merges the delta into it automatically.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When only a delta .app is provided without the base, fsdecrypt now
scans the same directory for an existing base VHD matching the
game ID pattern ({game_id}_*_0.vhd) before giving up.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When multiple APP files are provided (base seq=0 + delta seq>0),
fsdecrypt now automatically links and merges the differencing VHD
into the parent using Set-VHD + Merge-VHD (Windows/Hyper-V only).
Triggers a single UAC prompt for elevation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>