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>
fsdecrypt
Decryptor and extractor for SEGA arcade filesystem containers (fscrypt format). Works on Windows and Linux, no admin privileges required.
Installation
Requires Rust.
cargo build --release
The binary will be at target/release/fsdecrypt (or fsdecrypt.exe on Windows).
Usage
fsdecrypt <FILES>...
Examples
# Extract a game APP container
fsdecrypt ABCD_1.00.00_20240101120000_0.app
# Extract an OPTION/DLC container
fsdecrypt ABCD_A001_20240101120000_0.opt
# Extract multiple files at once
fsdecrypt game_v1.app game_v2.app extras.opt
# Decrypt only (outputs raw .ntfs/.exfat image, no extraction)
fsdecrypt --no-extract ABCD_1.00.00_20240101120000_0.app
Delta Updates
Games often ship incremental updates as a separate .app file. Just pass the update file — fsdecrypt will automatically find the base in the same folder and merge them:
fsdecrypt ABCD_1.01.00_20240215143000_1_1.00.00.app
You can also pass both explicitly:
fsdecrypt ABCD_1.00.00_20240101120000_0.app ABCD_1.01.00_20240215143000_1_1.00.00.app
The output folder is named after the input file (e.g. ABCD_1.01.00_20240215143000_1_1.00.00/).
Windows Right-Click Menu
Two batch files add an "Unpack with fsdecrypt" entry (with an unpack icon)
to the right-click menu of .app and .opt files:
install-context-menu.bat— install the menu entryuninstall-context-menu.bat— remove it
Just double-click install-context-menu.bat. No administrator rights are
needed — it installs under the current user only (HKCU) and leaves the normal
double-click association untouched. Right-clicking a file then runs the full
extraction and keeps the console window open until you press a key.
install-context-menu.bat finds fsdecrypt.exe automatically when run from the
repo root (it looks in target\release then target\debug) or when placed next
to the binary. The icon is taken from assets\fsdecrypt.ico (or fsdecrypt.ico
beside the script). The menu stores the absolute path it found, so re-run it if
you move the exe.
On Windows 11 the entry appears under Show more options (the classic menu).
External Key Files
For games not in the built-in key database, place a file named {GAME_ID}.bin in the working directory:
- 16 bytes for key only (IV derived automatically)
- 32 bytes for key + IV
Testing
Tests run locally (there is no hosted CI). The unit tests are plain cargo test;
the Linux x86_64-musl release target is exercised via Docker when you need it.
cargo test # unit tests (IV derivation, timestamp decoding, key lookup)
scripts/test.sh # native tests + smoke test, then the same in a musl container
scripts/test.sh --no-docker # skip the Docker/Linux step
scripts/test.sh uses the clux/muslrust
image to build and test the static Linux binary — the same way release artifacts
are produced — so both targets can be validated from any host.
End-to-end fixtures
The tests/e2e.rs test decrypts real containers placed in a fixtures/
directory at the repo root and checks that each extracts successfully. That
folder is git-ignored — the proprietary sample containers are never committed —
and the test is a no-op when it is empty, so just drop a few .app/.opt files
in fixtures/ and run cargo test (or scripts/test.sh) to exercise the full
decrypt-and-extract path.
License
BSD Zero Clause License (0BSD)