Rust implementation of core Nintendo Switch content workflows inspired by NSC_Builder squirrel.py.
Implemented operations:
- Merge (
--direct_multi,-d) - Verify container integrity and parity-style checks (
--verify) - Rename files/folders using package metadata + NUTDB (
--renamef) - Split (
--splitter) - Split to repacked files (
--dspl) - Create/Repack NSP from folder (
--create+--ifolder) - Convert NSP/XCI (
--direct_creation,-c) - Compress NSP/XCI (
--compress,-z) - Decompress NSZ/XCZ/NCZ (
--decompress) - Content viewer (
--ADVcontentlist) - Metadata/file list (
--ADVfilelist) - Firmware controls on merge (
--RSVcap,--keypatch,--pv)
- Rust toolchain (stable)
prod.keys(or pass--keys <path>)
cargo build --releaseBinary path:
target/release/nscb- On tag push, GitHub Actions builds and publishes:
nscb_rust.exenscb_rust-linux-amd64nscb_rust-macos-arm64- Trigger pattern:
v*(example:v0.1.0)
- Workflow file:
.github/workflows/release.yml
rustup target add x86_64-pc-windows-gnu
sudo apt-get update && sudo apt-get install -y mingw-w64
cargo build --release --target x86_64-pc-windows-gnuOutput:
target/x86_64-pc-windows-gnu/release/nscb.exetarget/release/nscb --help--keys <path>: path toprod.keys-o, --ofolder <dir>: output folder-t, --type <nsp|xci>: target type for convert/merge output mode--level <1-22>: compression level (default:3)-n, --nodelta: exclude delta NCAs during merge
--nutdb-refresh: refresh the local NUTDB cache--nutdb-lookup <titleid>: inspect a cached NUTDB entry--nutdb-cache-dir <dir>: override the NUTDB cache directory--nutdb-url <url>: override the NUTDB source URL
target/release/nscb \
-d "base.nsp" "update.nsz" "dlc.nsp" \
--keys /path/to/prod.keys \
-o /path/to/outputtarget/release/nscb \
--renamef "/path/to/library_or_file" \
--renmode skip_corr_tid \
--addlangue true \
--noversion false \
--dlcrname false \
--keys /path/to/prod.keysBehavior:
- renames supported files recursively:
.nsp,.nsx,.nsz,.xci,.xcz - uses package metadata first, then cached NUTDB names as fallback
- auto-refreshes the NUTDB cache on demand using conditional HTTP when supported
- keeps Python
squirrel.pyCLI names and accepted values for the main rename path:--renamef <path>--renmode <force|skip_corr_tid|skip_if_tid>--addlangue <true|false>--noversion <false|true|xci_no_v0>--dlcrname <false|true|tag> --dlcrname tagmatches Python rename behavior: defaultskip_corr_tiduses an exact DLC NUTDB entry when present, otherwise falls back toDLC <number>;--renmode force --dlcrname tagkeeps the resolved name and appends[DLC <number>]- parity-tested against Python for exact rename output in these cases:
basic rename,
force,skip_corr_tid,skip_if_tid,addlangue,noversion=true,noversion=xci_no_v0,dlcrname=true,dlcrname=tag,force + dlcrname=tag, anddlcrname=tagwith a base-only NUTDB fixture - appends
(SeemsDuplicate)when the target filename already exists - appends
(needscheck)when a valid title name/title ID cannot be resolved
target/release/nscb --nutdb-refresh
target/release/nscb --nutdb-lookup 0100F8F0000A2000target/release/nscb \
--splitter "merged.nsp_or_xci" \
--keys /path/to/prod.keys \
-o /path/to/splitExpected split output:
- Creates one folder per title group (base/update/DLC), not
.nspfiles. - Folder names are title-aware, for example:
Hollow Knight [0100633007D48000]Hollow Knight [0100633007D48800][v458752][UPD]
- Each folder contains extracted title content files, primarily
.nca/.ncz. - Tickets/certs are not guaranteed in split output;
--splitteris designed for content grouping.
target/release/nscb \
--create "/path/to/repacked.nsp" \
--ifolder "/path/to/split/Game Name [0100...000]" \
--keys /path/to/prod.keysExpected create behavior:
- Reads top-level files from
--ifolder. - Rebuilds a single
.nspwith deterministic packing order. - Typical workflow:
- Split merged file with
--splitter - Repack one split folder with
--create
- Split merged file with
target/release/nscb \
--dspl "merged.xci" \
--type nsp \
--keys /path/to/prod.keys \
-o /path/to/outputtarget/release/nscb \
--ADVcontentlist "game.nsp_or_xci" \
--keys /path/to/prod.keystarget/release/nscb \
--ADVfilelist "game.nsp_or_xci" \
--keys /path/to/prod.keystarget/release/nscb \
--direct_creation "game.nsp" \
--type xci \
--keys /path/to/prod.keys \
-o /path/to/outputtarget/release/nscb \
--direct_creation "game.xci" \
--type nsp \
--keys /path/to/prod.keys \
-o /path/to/outputtarget/release/nscb \
--compress "game.nsp" \
--level 3 \
--keys /path/to/prod.keys \
-o /path/to/outputtarget/release/nscb \
--decompress "game.nsz" \
--keys /path/to/prod.keys \
-o /path/to/outputtarget/release/nscb \
-d "base.xci" "update.nsz" "dlc1.nsp" "dlc2.nsp" \
--type xci \
--RSVcap 0 \
--keypatch 4 \
--pv \
--keys /path/to/prod.keys \
-o /path/to/outputtarget/release/nscb \
--verify "game.nsp_or_xci_or_nsz" \
--vertype full \
--keys /path/to/prod.keysVerify modes:
--vertype dec: decryption test--vertype sig: signature test--vertype full: full flow including hash verification prompt
- Progress bars are implemented for merge/decompress/convert operations, and also for compress/split.
- Split uses title-aware grouping and writes separate base/update/DLC folders.
- For large files, always use an output folder (
-o) to avoid overwriting source content. - If
--keysis not set, the app also checks common default key locations.
Use the included parity runner to compare Rust outputs against NSC_BUILDER Python behavior:
./run_parity_exact.shrun_parity_exact.sh is the single canonical regression entrypoint. It covers:
- verify parity smoke on two datasets (
TEST_DIRandMULTI_UPDATE_DIR), including base.nszverification - merge parity (
nspandxci) - split parity
- create parity
- compress/decompress parity
- XCZ mixed-input merge parity
- mixed
XCI base + NSP update -> NSPregression ADVcontentlistoutput parityADVfilelistoutput paritydsplfilename parity- firmware-control regression
- multi-update selection regression (
v1.0.4+v1.0.5-> keepv1.0.5)
The verify smoke currently exercises:
- base decryption and full verify on the default dataset (
TEST_DIR, default/mnt/e/test/uo) - base decryption and full verify on the multi-update dataset (
MULTI_UPDATE_DIR, default/mnt/e/test/op) - base
.nszdecryption and full verify onMULTI_UPDATE_DIR - Python-style multi-input verify behavior (
--verify file1 file2-> last explicit file wins) - Python-style mass verify behavior (
--verify all --text_file filelist.txt-> first line from the filelist is used)
To run only the verify regression slice instead of the full parity suite:
PARITY_ONLY=verify ./run_parity_exact.shThe Rust binary never delegates to squirrel.py. The Python reference is only used by the parity harness for comparison.
The parity runner expects a local NSC_BUILDER checkout plus a Python virtualenv:
mkdir -p .qa_suite/reference
git clone https://github.com/cxfcxf/NSC_BUILDER .qa_suite/reference/NSC_BUILDER
python3 -m venv .qa_suite/reference/NSC_BUILDER/.venv
.qa_suite/reference/NSC_BUILDER/.venv/bin/pip install --upgrade pip setuptools wheel
.qa_suite/reference/NSC_BUILDER/.venv/bin/pip install \
pycryptodome \
tqdm \
zstandard \
eel \
bottle \
bottle-websocket \
pywebview \
urllib3 \
beautifulsoup4 \
requests \
pillow \
chardet \
pykakasi \
googletrans==4.0.0rc1Modules that were required in practice to boot squirrel.py and run the parity suite:
pycryptodometqdmzstandardeelbottlebottle-websocketpywebviewurllib3beautifulsoup4requestspillowchardetpykakasigoogletrans==4.0.0rc1
Expected layout:
$PWD/.qa_suite/reference/NSC_BUILDER
├── .venv/
└── py/ztools/If you keep the Python reference tree there, run_parity_exact.sh works without extra env vars.
Common env vars:
TEST_DIR: folder containing test inputs andprod.keysMULTI_UPDATE_DIR: separate folder for multi-update selection fixturesTF_DIR: mixedXCI base + NSP updateregression fixture folderBASE_FILE: base input fileUPD_FILE: update input fileMULTI_BASE_FILE: base file for the multi-update regression caseMULTI_UPD_OLD_FILE: older update file for the multi-update regression caseMULTI_UPD_NEW_FILE: newer update file for the multi-update regression caseSMALL_NSZ: small NSZ file for compress/decompress checksOUT_DIR: output artifacts/logs folderPY_REPO: local NSC_BUILDER clone pathPY_ZTOOLS: overridepy/ztoolsinside the Python reference treePYTHON_BIN: override the Python interpreter used for parity runs
Current local fixture layout used by the parity script:
/mnt/e/test/prod.keys/mnt/e/test/uoOriginal Unicorn Overlord parity set: base.xci, one update.nsz, two DLC.nsp/mnt/e/test/opMulti-update Octopath Traveler regression set: base.nsp, updatev1.0.4, updatev1.0.5/mnt/e/test/tfTelenet Fuku-Bukuro mixed-input regression set: base.xci, update.nsp
The suite is parity-first, but these differences are intentional and documented:
ADVfilelistlinePatchable to:is not parity-gated for higher key generations. Rust uses the extended RSV floor table so firmware downgrade reporting stays consistent with actual merge behavior. The Python reference falls back incorrectly for higher keygens.- Multi-update
--direct_multiNSP merges with duplicate-named update tickets/certs are not forced to match Python bit-for-bit. Rust keeps the highest-version update cleanly. Python's NSP text-file merge path can append stale ticket/cert bytes from the older update while still advertising only the newer update in the header, producing a larger malformed-but-usable NSP. Rust intentionally does not reproduce that container bug. - Mixed
XCI -> NSPmerges are not treated as Python-authoritative when the Python reference emits an invalid NSP. This affects both the Unicorn Overlord mixed-input parity case and the Telenet Fuku-Bukuro regression fixture. In those cases the harness verifies that Rust produces a valid merged NSP with the expected content, and only performs Python split/hash comparison if the Python output is itself readable. - Raw compressed
.nsz/.xczbytes are not parity-gated. The suite enforces decompressed payload parity and filename parity instead. - XCI files ignore the first
0x100bytes for exact byte comparison. Python randomizes the XCI signature block on each run, and Rust intentionally mimics that behavior.
Compression note:
- decompressed payload parity is enforced
- compressed
.nsz/.xczbyte streams are not required to match Python bit-for-bit
Example (E:\dumps\game_set on WSL as /mnt/e/dumps/game_set):
BASE_FILE="$(find /mnt/e/dumps/game_set -maxdepth 1 -type f -iname '*.nsz' | rg '\[APP\]' | head -n1)"
UPD_FILE="$(find /mnt/e/dumps/game_set -maxdepth 1 -type f -iname '*.nsz' | rg '\[UPD\]' | head -n1)"
SMALL_NSZ="$(find /mnt/e/dumps/game_set -maxdepth 1 -type f -iname '*.nsz' | rg '\[DLC' | head -n1)"
TEST_DIR=/mnt/e/dumps/game_set \
OUT_DIR=.qa_suite/parity_example \
PY_REPO=.qa_suite/reference/NSC_BUILDER \
BASE_FILE="$BASE_FILE" \
UPD_FILE="$UPD_FILE" \
SMALL_NSZ="$SMALL_NSZ" \
./run_parity_exact.shExample for the current local split fixture layout:
TEST_DIR=/mnt/e/test/uo \
MULTI_UPDATE_DIR=/mnt/e/test/op \
TF_DIR=/mnt/e/test/tf \
KEYS=/mnt/e/test/prod.keys \
PY_REPO=.qa_suite/reference/NSC_BUILDER \
PY_ZTOOLS=.qa_suite/reference/NSC_BUILDER/py/ztools \
PYTHON_BIN=.qa_suite/reference/NSC_BUILDER/.venv/bin/python \
./run_parity_exact.sh