Compare commits

...

23 Commits

Author SHA1 Message Date
Luke Street df8ab228c8 Update README.md and repo URLs 2024-10-18 00:09:18 -06:00
Luke Street 32e08f9543 Export LaggedFibonacci & add more helper methods 2024-10-18 00:04:11 -06:00
Luke Street e0d735dd39 Export more constants & minor cleanup 2024-10-18 00:03:23 -06:00
Luke Street d4bca2caa8 Ignore Shift JIS decoding errors in Fst::get_name 2024-10-18 00:02:37 -06:00
Luke Street be4672471d Move region info from PartitionMeta to Disc 2024-10-04 20:17:16 -06:00
Luke Street f4638369d1 Extract Wii region.bin 2024-10-04 19:53:07 -06:00
Luke Street d99ef72fe9 Fix matching paths with repeated slashes in Fst::find 2024-10-04 17:25:07 -06:00
Luke Street e6a3871d28 Resolve +nightly clippy warning 2024-10-03 21:00:06 -06:00
Luke Street 30bcf4936b Upgrade to zerocopy 0.8 2024-10-03 20:57:02 -06:00
Luke Street 5f537f0e7b Various minor API adjustments 2024-10-03 20:18:44 -06:00
Luke Street 8abe674cb9 Fix building without compress features 2024-10-03 01:00:51 -06:00
Luke Street 54890674a2 Add Disc::detect for detecting disc image format 2024-10-03 00:55:03 -06:00
Luke Street 370d03fa9a Add Disc::new_stream/new_stream_with_options
Allows opening a disc image from a custom stream,
rather than a filesystem path.
2024-10-02 23:49:20 -06:00
Luke Street 5ad514d59c Use mimalloc when targeting musl
Also removes the armv7 linux build.
If you used it, let me know!
2024-09-29 12:10:59 -06:00
Luke Street 312dd6f080 SharedWindowedReadStream -> FileStream & impl BufRead 2024-09-10 23:19:57 -06:00
Luke Street 6f3052e05d Use workspace keys in Cargo.toml 2024-09-10 23:19:19 -06:00
Luke Street d2b8135cdb Use full LTO, update dependencies & CI 2024-09-08 16:29:48 -06:00
Luke Street a8f91ff9c2 Update README.md 2024-09-04 20:26:19 -06:00
Luke Street 22434fbba3 Add nod version to nodtool Cargo.toml 2024-09-04 20:04:57 -06:00
Luke Street f1cc0949f3 Remove TGC version check 2024-09-04 19:59:50 -06:00
Luke Street c2e029db6b use std::mem::size_of; 2024-09-04 00:07:44 -06:00
Luke Street 83367def99 TGC cleanup & renaming 2024-09-04 00:02:48 -06:00
Luke Street 551f966c80 Add TGC support 2024-09-03 23:34:05 -06:00
37 changed files with 1421 additions and 703 deletions

View File

@ -1,11 +1,17 @@
name: Build name: Build
on: [ push, pull_request ] on:
pull_request:
push:
paths-ignore:
- '*.md'
- 'LICENSE*'
workflow_dispatch:
env: env:
BUILD_PROFILE: release-lto BUILD_PROFILE: release-lto
CARGO_BIN_NAME: nodtool
CARGO_TARGET_DIR: target CARGO_TARGET_DIR: target
CARGO_INCREMENTAL: 0
jobs: jobs:
check: check:
@ -13,7 +19,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
toolchain: [ stable, 1.73.0, nightly ] toolchain: [ stable, 1.74.0, nightly ]
fail-fast: false fail-fast: false
env: env:
RUSTFLAGS: -D warnings RUSTFLAGS: -D warnings
@ -79,7 +85,9 @@ jobs:
run: cargo test --release run: cargo test --release
build: build:
name: Build name: Build nodtool
env:
CARGO_BIN_NAME: nodtool
strategy: strategy:
matrix: matrix:
include: include:
@ -88,20 +96,20 @@ jobs:
name: linux-x86_64 name: linux-x86_64
build: zigbuild build: zigbuild
features: asm features: asm
# - platform: ubuntu-latest - platform: ubuntu-latest
# target: i686-unknown-linux-musl target: i686-unknown-linux-musl
# name: linux-i686 name: linux-i686
# build: zigbuild build: zigbuild
# features: asm features: asm
- platform: ubuntu-latest - platform: ubuntu-latest
target: aarch64-unknown-linux-musl target: aarch64-unknown-linux-musl
name: linux-aarch64 name: linux-aarch64
build: zigbuild build: zigbuild
features: nightly features: nightly
- platform: ubuntu-latest - platform: windows-latest
target: armv7-unknown-linux-musleabi target: i686-pc-windows-msvc
name: linux-armv7l name: windows-x86
build: zigbuild build: build
features: default features: default
- platform: windows-latest - platform: windows-latest
target: x86_64-pc-windows-msvc target: x86_64-pc-windows-msvc
@ -135,20 +143,20 @@ jobs:
sudo apt-get -y install ${{ matrix.packages }} sudo apt-get -y install ${{ matrix.packages }}
- name: Install cargo-zigbuild - name: Install cargo-zigbuild
if: matrix.build == 'zigbuild' if: matrix.build == 'zigbuild'
run: pip install ziglang==0.11.0 cargo-zigbuild==0.18.3 run: pip install ziglang==0.13.0 cargo-zigbuild==0.19.1
- name: Setup Rust toolchain - name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@nightly uses: dtolnay/rust-toolchain@stable
with: with:
targets: ${{ matrix.target }} targets: ${{ matrix.target }}
- name: Cargo build - name: Cargo build
run: cargo ${{ matrix.build }} --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }} --bin ${{ env.CARGO_BIN_NAME }} --features ${{ matrix.features }} run: >
cargo ${{ matrix.build }} --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }}
--bin ${{ env.CARGO_BIN_NAME }} --features ${{ matrix.features }}
- name: Upload artifacts - name: Upload artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: ${{ matrix.name }} name: ${{ env.CARGO_BIN_NAME }}-${{ matrix.name }}
path: | path: |
${{ env.CARGO_TARGET_DIR }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}
${{ env.CARGO_TARGET_DIR }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}.exe
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }} ${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}.exe ${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}.exe
if-no-files-found: error if-no-files-found: error
@ -159,6 +167,20 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [ build ] needs: [ build ]
steps: steps:
- name: Checkout
uses: actions/checkout@v4
- name: Check git tag against Cargo version
shell: bash
run: |
set -eou pipefail
tag='${{github.ref}}'
tag="${tag#refs/tags/}"
version=$(grep '^version' Cargo.toml | head -1 | awk -F' = ' '{print $2}' | tr -d '"')
version="v$version"
if [ "$tag" != "$version" ]; then
echo "::error::Git tag doesn't match the Cargo version! ($tag != $version)"
exit 1
fi
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
@ -166,12 +188,28 @@ jobs:
- name: Rename artifacts - name: Rename artifacts
working-directory: artifacts working-directory: artifacts
run: | run: |
set -euo pipefail
mkdir ../out mkdir ../out
for i in */*/$BUILD_PROFILE/$CARGO_BIN_NAME*; do for dir in */; do
mv "$i" "../out/$(sed -E "s/([^/]+)\/[^/]+\/$BUILD_PROFILE\/($CARGO_BIN_NAME)/\2-\1/" <<< "$i")" for file in "$dir"*; do
base=$(basename "$file")
name="${base%.*}"
ext="${base##*.}"
if [ "$ext" = "$base" ]; then
ext=""
else
ext=".$ext"
fi
arch="${dir%/}" # remove trailing slash
arch="${arch##"$name-"}" # remove bin name
dst="../out/${name}-${arch}${ext}"
mv "$file" "$dst"
done
done done
ls -R ../out ls -R ../out
- name: Release - name: Release
uses: softprops/action-gh-release@4634c16e79c963813287e889244c50009e7f0981 uses: softprops/action-gh-release@v2
with: with:
files: out/* files: out/*
draft: true
generate_release_notes: true

314
Cargo.lock generated
View File

@ -8,6 +8,12 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "adler2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]] [[package]]
name = "aes" name = "aes"
version = "0.8.4" version = "0.8.4"
@ -21,9 +27,9 @@ dependencies = [
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "1.1.2" version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@ -57,9 +63,9 @@ checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.4.2" version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
[[package]] [[package]]
name = "block-buffer" name = "block-buffer"
@ -79,12 +85,6 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]] [[package]]
name = "bzip2" name = "bzip2"
version = "0.4.4" version = "0.4.4"
@ -117,11 +117,13 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.0.86" version = "1.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f9fa1897e4325be0d68d48df6aa1a71ac2ed4d27723887e7754192705350730" checksum = "812acba72f0a070b003d3697490d2b55b837230ae7c6c6497f05cc2ddbb8d938"
dependencies = [ dependencies = [
"jobserver",
"libc", "libc",
"shlex",
] ]
[[package]] [[package]]
@ -155,9 +157,9 @@ dependencies = [
[[package]] [[package]]
name = "cpufeatures" name = "cpufeatures"
version = "0.2.12" version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0"
dependencies = [ dependencies = [
"libc", "libc",
] ]
@ -192,9 +194,9 @@ dependencies = [
[[package]] [[package]]
name = "crossbeam-utils" name = "crossbeam-utils"
version = "0.8.19" version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
[[package]] [[package]]
name = "crypto-common" name = "crypto-common"
@ -218,15 +220,15 @@ dependencies = [
[[package]] [[package]]
name = "dyn-clone" name = "dyn-clone"
version = "1.0.16" version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
[[package]] [[package]]
name = "either" name = "either"
version = "1.10.0" version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
[[package]] [[package]]
name = "enable-ansi-support" name = "enable-ansi-support"
@ -245,9 +247,9 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
[[package]] [[package]]
name = "encoding_rs" name = "encoding_rs"
version = "0.8.33" version = "0.8.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
] ]
@ -305,9 +307,9 @@ dependencies = [
[[package]] [[package]]
name = "instant" name = "instant"
version = "0.1.12" version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
] ]
@ -320,39 +322,48 @@ checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45"
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.12.1" version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [ dependencies = [
"either", "either",
] ]
[[package]] [[package]]
name = "lazy_static" name = "jobserver"
version = "1.4.0" version = "0.1.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0"
dependencies = [
"libc",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.153" version = "0.2.159"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
[[package]] [[package]]
name = "liblzma" name = "liblzma"
version = "0.2.3" version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "599133771f99c14ca089a8db3a4565f482ea6eeb66991b262bffc2b72acff69c" checksum = "a7c45fc6fcf5b527d3cf89c1dee8c327943984b0dc8bfcf6e100473b00969e63"
dependencies = [ dependencies = [
"liblzma-sys", "liblzma-sys",
] ]
[[package]] [[package]]
name = "liblzma-sys" name = "liblzma-sys"
version = "0.2.5" version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be9aaba5f9c8f8f615d41570909338b6284fbb1813dc057ecc68563d98a65097" checksum = "6630cb23edeb2e563cd6c30d4117554c69646871455843c33ddcb1d9aef82ecf"
dependencies = [ dependencies = [
"cc", "cc",
"libc", "libc",
@ -360,10 +371,20 @@ dependencies = [
] ]
[[package]] [[package]]
name = "log" name = "libmimalloc-sys"
version = "0.4.20" version = "0.1.39"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" checksum = "23aa6811d3bd4deb8a84dde645f943476d13b248d818edcf8ce0b2f37f036b44"
dependencies = [
"cc",
"libc",
]
[[package]]
name = "log"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]] [[package]]
name = "matchers" name = "matchers"
@ -387,31 +408,40 @@ dependencies = [
[[package]] [[package]]
name = "md5-asm" name = "md5-asm"
version = "0.5.1" version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61d33bc4cdfe5c60340e282bbbee0a6e2bc57f0b9279bb3489c5004d12492e5c" checksum = "d19b8ee7fc7d812058d3b708c7f719efd0713d53854648e4223c6fcae709e2df"
dependencies = [ dependencies = [
"cc", "cc",
] ]
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.7.1" version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "mimalloc"
version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68914350ae34959d83f732418d51e2427a794055d0b9529f48259ac07af65633"
dependencies = [
"libmimalloc-sys",
]
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.7.2" version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1"
dependencies = [ dependencies = [
"adler", "adler2",
] ]
[[package]] [[package]]
name = "nod" name = "nod"
version = "1.2.0" version = "1.4.4"
dependencies = [ dependencies = [
"adler", "adler",
"aes", "aes",
@ -434,7 +464,7 @@ dependencies = [
[[package]] [[package]]
name = "nodtool" name = "nodtool"
version = "1.2.0" version = "1.4.4"
dependencies = [ dependencies = [
"argp", "argp",
"base16ct", "base16ct",
@ -446,6 +476,7 @@ dependencies = [
"itertools", "itertools",
"log", "log",
"md-5", "md-5",
"mimalloc",
"nod", "nod",
"quick-xml", "quick-xml",
"serde", "serde",
@ -478,9 +509,12 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.19.0" version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" checksum = "82881c4be219ab5faaf2ad5e5e5ecdff8c66bd7402ca3160975c93b24961afd1"
dependencies = [
"portable-atomic",
]
[[package]] [[package]]
name = "overload" name = "overload"
@ -490,27 +524,27 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.13" version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
[[package]] [[package]]
name = "pkg-config" name = "pkg-config"
version = "0.3.30" version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
[[package]] [[package]]
name = "portable-atomic" name = "portable-atomic"
version = "1.6.0" version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.78" version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -529,9 +563,9 @@ dependencies = [
[[package]] [[package]]
name = "quick-xml" name = "quick-xml"
version = "0.31.0" version = "0.36.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe"
dependencies = [ dependencies = [
"memchr", "memchr",
"serde", "serde",
@ -539,18 +573,18 @@ dependencies = [
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.35" version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]] [[package]]
name = "rayon" name = "rayon"
version = "1.8.1" version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7237101a77a10773db45d62004a272517633fbcc3df19d96455ede1122e051" checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
dependencies = [ dependencies = [
"either", "either",
"rayon-core", "rayon-core",
@ -568,14 +602,14 @@ dependencies = [
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.10.3" version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
"regex-automata 0.4.5", "regex-automata 0.4.8",
"regex-syntax 0.8.2", "regex-syntax 0.8.5",
] ]
[[package]] [[package]]
@ -589,13 +623,13 @@ dependencies = [
[[package]] [[package]]
name = "regex-automata" name = "regex-automata"
version = "0.4.5" version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
"regex-syntax 0.8.2", "regex-syntax 0.8.5",
] ]
[[package]] [[package]]
@ -606,28 +640,28 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.8.2" version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.197" version = "1.0.210"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.197" version = "1.0.210"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.50", "syn 2.0.79",
] ]
[[package]] [[package]]
@ -644,9 +678,9 @@ dependencies = [
[[package]] [[package]]
name = "sha1-asm" name = "sha1-asm"
version = "0.5.2" version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ba6947745e7f86be3b8af00b7355857085dbdf8901393c89514510eb61f4e21" checksum = "286acebaf8b67c1130aedffad26f594eff0c1292389158135327d2e23aed582b"
dependencies = [ dependencies = [
"cc", "cc",
] ]
@ -660,6 +694,12 @@ dependencies = [
"lazy_static", "lazy_static",
] ]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]] [[package]]
name = "size" name = "size"
version = "0.4.1" version = "0.4.1"
@ -668,15 +708,15 @@ checksum = "9fed904c7fb2856d868b92464fc8fa597fce366edea1a9cbfaa8cb5fe080bd6d"
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.13.1" version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]] [[package]]
name = "supports-color" name = "supports-color"
version = "3.0.0" version = "3.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9829b314621dfc575df4e409e79f9d6a66a3bd707ab73f23cb4aa3a854ac854f" checksum = "8775305acf21c96926c900ad056abeef436701108518cf890020387236ac5a77"
dependencies = [ dependencies = [
"is_ci", "is_ci",
] ]
@ -694,9 +734,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.50" version = "2.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74f1bdc9872430ce9b75da68329d1c1746faf50ffac5f19e02b71e37ff881ffb" checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -705,22 +745,22 @@ dependencies = [
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.57" version = "1.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl",
] ]
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "1.0.57" version = "1.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.50", "syn 2.0.79",
] ]
[[package]] [[package]]
@ -752,7 +792,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.50", "syn 2.0.79",
] ]
[[package]] [[package]]
@ -811,15 +851,15 @@ dependencies = [
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.12" version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
[[package]] [[package]]
name = "unicode-width" name = "unicode-width"
version = "0.1.11" version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]] [[package]]
name = "valuable" name = "valuable"
@ -829,9 +869,9 @@ checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.4" version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]] [[package]]
name = "winapi" name = "winapi"
@ -881,17 +921,18 @@ dependencies = [
[[package]] [[package]]
name = "windows-targets" name = "windows-targets"
version = "0.52.0" version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [ dependencies = [
"windows_aarch64_gnullvm 0.52.0", "windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.0", "windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.0", "windows_i686_gnu 0.52.6",
"windows_i686_msvc 0.52.0", "windows_i686_gnullvm",
"windows_x86_64_gnu 0.52.0", "windows_i686_msvc 0.52.6",
"windows_x86_64_gnullvm 0.52.0", "windows_x86_64_gnu 0.52.6",
"windows_x86_64_msvc 0.52.0", "windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
] ]
[[package]] [[package]]
@ -902,9 +943,9 @@ checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
[[package]] [[package]]
name = "windows_aarch64_gnullvm" name = "windows_aarch64_gnullvm"
version = "0.52.0" version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]] [[package]]
name = "windows_aarch64_msvc" name = "windows_aarch64_msvc"
@ -914,9 +955,9 @@ checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
[[package]] [[package]]
name = "windows_aarch64_msvc" name = "windows_aarch64_msvc"
version = "0.52.0" version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]] [[package]]
name = "windows_i686_gnu" name = "windows_i686_gnu"
@ -926,9 +967,15 @@ checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
[[package]] [[package]]
name = "windows_i686_gnu" name = "windows_i686_gnu"
version = "0.52.0" version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
@ -938,9 +985,9 @@ checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
[[package]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
version = "0.52.0" version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]] [[package]]
name = "windows_x86_64_gnu" name = "windows_x86_64_gnu"
@ -950,9 +997,9 @@ checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
[[package]] [[package]]
name = "windows_x86_64_gnu" name = "windows_x86_64_gnu"
version = "0.52.0" version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]] [[package]]
name = "windows_x86_64_gnullvm" name = "windows_x86_64_gnullvm"
@ -962,9 +1009,9 @@ checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
[[package]] [[package]]
name = "windows_x86_64_gnullvm" name = "windows_x86_64_gnullvm"
version = "0.52.0" version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
@ -974,60 +1021,59 @@ checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
[[package]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
version = "0.52.0" version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]] [[package]]
name = "xxhash-rust" name = "xxhash-rust"
version = "0.8.10" version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "927da81e25be1e1a2901d59b81b37dd2efd1fc9c9345a55007f09bf5a2d3ee03" checksum = "6a5cbf750400958819fb6178eaa83bee5cd9c29a26a40cc241df8c70fdd46984"
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.7.32" version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" checksum = "dd2e5ce961dea177d282ec084dca2aa411b7411199a68d79eb1beacb305a6cd9"
dependencies = [ dependencies = [
"byteorder",
"zerocopy-derive", "zerocopy-derive",
] ]
[[package]] [[package]]
name = "zerocopy-derive" name = "zerocopy-derive"
version = "0.7.32" version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" checksum = "b06304eeddb6081af98ac59db08c868ac197e586086b996d15a86ed70e09a754"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.50", "syn 2.0.79",
] ]
[[package]] [[package]]
name = "zstd" name = "zstd"
version = "0.13.1" version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d789b1514203a1120ad2429eae43a7bd32b90976a7bb8a05f7ec02fa88cc23a" checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9"
dependencies = [ dependencies = [
"zstd-safe", "zstd-safe",
] ]
[[package]] [[package]]
name = "zstd-safe" name = "zstd-safe"
version = "7.1.0" version = "7.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cd99b45c6bc03a018c8b8a86025678c87e55526064e38f9df301989dce7ec0a" checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059"
dependencies = [ dependencies = [
"zstd-sys", "zstd-sys",
] ]
[[package]] [[package]]
name = "zstd-sys" name = "zstd-sys"
version = "2.0.11+zstd.1.5.6" version = "2.0.13+zstd.1.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75652c55c0b6f3e6f12eb786fe1bc960396bf05a1eb3bf1f3691c3610ac2e6d4" checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa"
dependencies = [ dependencies = [
"cc", "cc",
"pkg-config", "pkg-config",

View File

@ -4,5 +4,15 @@ resolver = "2"
[profile.release-lto] [profile.release-lto]
inherits = "release" inherits = "release"
lto = "thin" lto = "fat"
strip = "debuginfo" strip = "debuginfo"
codegen-units = 1
[workspace.package]
version = "1.4.4"
edition = "2021"
rust-version = "1.74"
authors = ["Luke Street <luke@street.dev>"]
license = "MIT OR Apache-2.0"
repository = "https://github.com/encounter/nod"
keywords = ["gamecube", "wii", "iso", "wbfs", "rvz"]

View File

@ -1,12 +1,12 @@
# nod [![Build Status]][actions] [![Latest Version]][crates.io] [![Api Rustdoc]][rustdoc] ![Rust Version] # nod [![Build Status]][actions] [![Latest Version]][crates.io] [![Api Rustdoc]][rustdoc] ![Rust Version]
[Build Status]: https://github.com/encounter/nod-rs/actions/workflows/build.yaml/badge.svg [Build Status]: https://github.com/encounter/nod/actions/workflows/build.yaml/badge.svg
[actions]: https://github.com/encounter/nod-rs/actions [actions]: https://github.com/encounter/nod/actions
[Latest Version]: https://img.shields.io/crates/v/nod.svg [Latest Version]: https://img.shields.io/crates/v/nod.svg
[crates.io]: https://crates.io/crates/nod [crates.io]: https://crates.io/crates/nod
[Api Rustdoc]: https://img.shields.io/badge/api-rustdoc-blue.svg [Api Rustdoc]: https://img.shields.io/badge/api-rustdoc-blue.svg
[rustdoc]: https://docs.rs/nod [rustdoc]: https://docs.rs/nod
[Rust Version]: https://img.shields.io/badge/rust-1.73+-blue.svg?maxAge=3600 [Rust Version]: https://img.shields.io/badge/rust-1.74+-blue.svg?maxAge=3600
Library for traversing & reading Nintendo Optical Disc (GameCube and Wii) images. Library for traversing & reading Nintendo Optical Disc (GameCube and Wii) images.
@ -14,17 +14,26 @@ Originally based on the C++ library [nod](https://github.com/AxioDL/nod),
but does not currently support authoring. but does not currently support authoring.
Currently supported file formats: Currently supported file formats:
- ISO (GCM) - ISO (GCM)
- WIA / RVZ - WIA / RVZ
- WBFS (+ NKit 2 lossless) - WBFS (+ NKit 2 lossless)
- CISO (+ NKit 2 lossless) - CISO (+ NKit 2 lossless)
- NFS (Wii U VC) - NFS (Wii U VC)
- GCZ - GCZ
- TGC
## CLI tool ## CLI tool
This crate includes a command-line tool called `nodtool`. This crate includes a command-line tool called `nodtool`.
Download the latest release from the [releases page](https://github.com/encounter/nod-rs/releases),
or install it using Cargo:
```shell
cargo install --locked nodtool
```
### info ### info
Displays information about a disc image. Displays information about a disc image.
@ -113,8 +122,8 @@ std::io::copy(&mut disc, &mut out)
Licensed under either of Licensed under either of
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) - Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or <http://www.apache.org/licenses/LICENSE-2.0>)
* MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) - MIT license ([LICENSE-MIT](LICENSE-MIT) or <http://opensource.org/licenses/MIT>)
at your option. at your option.

View File

@ -1,17 +1,17 @@
[package] [package]
name = "nod" name = "nod"
version = "1.2.0" version.workspace = true
edition = "2021" edition.workspace = true
rust-version = "1.73.0" rust-version.workspace = true
authors = ["Luke Street <luke@street.dev>"] authors.workspace = true
license = "MIT OR Apache-2.0" license.workspace = true
repository = "https://github.com/encounter/nod-rs" repository.workspace = true
documentation = "https://docs.rs/nod" documentation = "https://docs.rs/nod"
readme = "../README.md" readme = "../README.md"
description = """ description = """
Library for reading GameCube and Wii disc images. Library for reading GameCube and Wii disc images.
""" """
keywords = ["gamecube", "wii", "iso", "wbfs", "rvz"] keywords.workspace = true
categories = ["command-line-utilities", "parser-implementations"] categories = ["command-line-utilities", "parser-implementations"]
[features] [features]
@ -31,12 +31,12 @@ cbc = "0.1"
digest = "0.10" digest = "0.10"
dyn-clone = "1.0" dyn-clone = "1.0"
encoding_rs = "0.8" encoding_rs = "0.8"
itertools = "0.12" itertools = "0.13"
liblzma = { version = "0.2", features = ["static"], optional = true } liblzma = { version = "0.3", features = ["static"], optional = true }
log = "0.4" log = "0.4"
miniz_oxide = { version = "0.7", optional = true } miniz_oxide = { version = "0.8", optional = true }
rayon = "1.8" rayon = "1.10"
sha1 = "0.10" sha1 = "0.10"
thiserror = "1.0" thiserror = "1.0"
zerocopy = { version = "0.7", features = ["alloc", "derive"] } zerocopy = { version = "0.8", features = ["alloc", "derive"] }
zstd = { version = "0.13", optional = true } zstd = { version = "0.13", optional = true }

View File

@ -3,7 +3,7 @@
use std::{borrow::Cow, ffi::CStr, mem::size_of}; use std::{borrow::Cow, ffi::CStr, mem::size_of};
use encoding_rs::SHIFT_JIS; use encoding_rs::SHIFT_JIS;
use zerocopy::{big_endian::*, AsBytes, FromBytes, FromZeroes}; use zerocopy::{big_endian::*, FromBytes, Immutable, IntoBytes, KnownLayout};
use crate::{static_assert, Result}; use crate::{static_assert, Result};
@ -19,13 +19,13 @@ pub enum NodeKind {
} }
/// An individual file system node. /// An individual file system node.
#[derive(Clone, Debug, PartialEq, FromBytes, FromZeroes, AsBytes)] #[derive(Copy, Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(4))] #[repr(C, align(4))]
pub struct Node { pub struct Node {
kind: u8, kind: u8,
// u24 big-endian // u24 big-endian
name_offset: [u8; 3], name_offset: [u8; 3],
offset: U32, pub(crate) offset: U32,
length: U32, length: U32,
} }
@ -33,6 +33,7 @@ static_assert!(size_of::<Node>() == 12);
impl Node { impl Node {
/// File system node kind. /// File system node kind.
#[inline]
pub fn kind(&self) -> NodeKind { pub fn kind(&self) -> NodeKind {
match self.kind { match self.kind {
0 => NodeKind::File, 0 => NodeKind::File,
@ -42,12 +43,15 @@ impl Node {
} }
/// Whether the node is a file. /// Whether the node is a file.
#[inline]
pub fn is_file(&self) -> bool { self.kind == 0 } pub fn is_file(&self) -> bool { self.kind == 0 }
/// Whether the node is a directory. /// Whether the node is a directory.
#[inline]
pub fn is_dir(&self) -> bool { self.kind == 1 } pub fn is_dir(&self) -> bool { self.kind == 1 }
/// Offset in the string table to the filename. /// Offset in the string table to the filename.
#[inline]
pub fn name_offset(&self) -> u32 { pub fn name_offset(&self) -> u32 {
u32::from_be_bytes([0, self.name_offset[0], self.name_offset[1], self.name_offset[2]]) u32::from_be_bytes([0, self.name_offset[0], self.name_offset[1], self.name_offset[2]])
} }
@ -55,8 +59,9 @@ impl Node {
/// For files, this is the partition offset of the file data. (Wii: >> 2) /// For files, this is the partition offset of the file data. (Wii: >> 2)
/// ///
/// For directories, this is the parent node index in the FST. /// For directories, this is the parent node index in the FST.
#[inline]
pub fn offset(&self, is_wii: bool) -> u64 { pub fn offset(&self, is_wii: bool) -> u64 {
if is_wii && self.kind == 0 { if is_wii && self.is_file() {
self.offset.get() as u64 * 4 self.offset.get() as u64 * 4
} else { } else {
self.offset.get() as u64 self.offset.get() as u64
@ -68,6 +73,7 @@ impl Node {
/// For directories, this is the child end index in the FST. /// For directories, this is the child end index in the FST.
/// ///
/// Number of child files and directories recursively is `length - offset`. /// Number of child files and directories recursively is `length - offset`.
#[inline]
pub fn length(&self) -> u64 { self.length.get() as u64 } pub fn length(&self) -> u64 { self.length.get() as u64 }
} }
@ -81,8 +87,9 @@ pub struct Fst<'a> {
impl<'a> Fst<'a> { impl<'a> Fst<'a> {
/// Create a new FST view from a buffer. /// Create a new FST view from a buffer.
#[allow(clippy::missing_inline_in_public_items)]
pub fn new(buf: &'a [u8]) -> Result<Self, &'static str> { pub fn new(buf: &'a [u8]) -> Result<Self, &'static str> {
let Some(root_node) = Node::ref_from_prefix(buf) else { let Ok((root_node, _)) = Node::ref_from_prefix(buf) else {
return Err("FST root node not found"); return Err("FST root node not found");
}; };
// String table starts after the last node // String table starts after the last node
@ -91,15 +98,17 @@ impl<'a> Fst<'a> {
return Err("FST string table out of bounds"); return Err("FST string table out of bounds");
} }
let (node_buf, string_table) = buf.split_at(string_base as usize); let (node_buf, string_table) = buf.split_at(string_base as usize);
let nodes = Node::slice_from(node_buf).unwrap(); let nodes = <[Node]>::ref_from_bytes(node_buf).unwrap();
Ok(Self { nodes, string_table }) Ok(Self { nodes, string_table })
} }
/// Iterate over the nodes in the FST. /// Iterate over the nodes in the FST.
#[inline]
pub fn iter(&self) -> FstIter { FstIter { fst: self, idx: 1 } } pub fn iter(&self) -> FstIter { FstIter { fst: self, idx: 1 } }
/// Get the name of a node. /// Get the name of a node.
pub fn get_name(&self, node: &Node) -> Result<Cow<str>, String> { #[allow(clippy::missing_inline_in_public_items)]
pub fn get_name(&self, node: Node) -> Result<Cow<'a, str>, String> {
let name_buf = self.string_table.get(node.name_offset() as usize..).ok_or_else(|| { let name_buf = self.string_table.get(node.name_offset() as usize..).ok_or_else(|| {
format!( format!(
"FST: name offset {} out of bounds (string table size: {})", "FST: name offset {} out of bounds (string table size: {})",
@ -110,25 +119,27 @@ impl<'a> Fst<'a> {
let c_string = CStr::from_bytes_until_nul(name_buf).map_err(|_| { let c_string = CStr::from_bytes_until_nul(name_buf).map_err(|_| {
format!("FST: name at offset {} not null-terminated", node.name_offset()) format!("FST: name at offset {} not null-terminated", node.name_offset())
})?; })?;
let (decoded, _, errors) = SHIFT_JIS.decode(c_string.to_bytes()); let (decoded, _, _) = SHIFT_JIS.decode(c_string.to_bytes());
if errors { // Ignore decoding errors, we can't do anything about them. Consumers may check for
return Err(format!("FST: Failed to decode name at offset {}", node.name_offset())); // U+FFFD (REPLACEMENT CHARACTER), or fetch the raw bytes from the string table.
}
Ok(decoded) Ok(decoded)
} }
/// Finds a particular file or directory by path. /// Finds a particular file or directory by path.
pub fn find(&self, path: &str) -> Option<(usize, &Node)> { #[allow(clippy::missing_inline_in_public_items)]
pub fn find(&self, path: &str) -> Option<(usize, Node)> {
let mut split = path.trim_matches('/').split('/'); let mut split = path.trim_matches('/').split('/');
let mut current = split.next()?; let mut current = next_non_empty(&mut split);
if current.is_empty() {
return Some((0, self.nodes[0]));
}
let mut idx = 1; let mut idx = 1;
let mut stop_at = None; let mut stop_at = None;
while let Some(node) = self.nodes.get(idx) { while let Some(node) = self.nodes.get(idx).copied() {
if self.get_name(node).as_ref().map_or(false, |name| name.eq_ignore_ascii_case(current)) if self.get_name(node).as_ref().map_or(false, |name| name.eq_ignore_ascii_case(current))
{ {
if let Some(next) = split.next() { current = next_non_empty(&mut split);
current = next; if current.is_empty() {
} else {
return Some((idx, node)); return Some((idx, node));
} }
// Descend into directory // Descend into directory
@ -158,13 +169,24 @@ pub struct FstIter<'a> {
} }
impl<'a> Iterator for FstIter<'a> { impl<'a> Iterator for FstIter<'a> {
type Item = (usize, &'a Node, Result<Cow<'a, str>, String>); type Item = (usize, Node, Result<Cow<'a, str>, String>);
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
let idx = self.idx; let idx = self.idx;
let node = self.fst.nodes.get(idx)?; let node = self.fst.nodes.get(idx).copied()?;
let name = self.fst.get_name(node); let name = self.fst.get_name(node);
self.idx += 1; self.idx += 1;
Some((idx, node, name)) Some((idx, node, name))
} }
} }
#[inline]
fn next_non_empty<'a>(iter: &mut impl Iterator<Item = &'a str>) -> &'a str {
loop {
match iter.next() {
Some("") => continue,
Some(next) => break next,
None => break "",
}
}
}

View File

@ -1,20 +1,18 @@
use std::{ use std::{
cmp::min,
io, io,
io::{Read, Seek, SeekFrom}, io::{BufRead, Read, Seek, SeekFrom},
mem::size_of, mem::size_of,
}; };
use zerocopy::{FromBytes, FromZeroes}; use zerocopy::{FromBytes, FromZeros};
use super::{
ApploaderHeader, DiscHeader, DolHeader, FileStream, Node, PartitionBase, PartitionHeader,
PartitionMeta, BI2_SIZE, BOOT_SIZE, SECTOR_SIZE,
};
use crate::{ use crate::{
disc::{ disc::streams::OwnedFileStream,
ApploaderHeader, DiscHeader, DolHeader, PartitionBase, PartitionHeader, PartitionMeta,
BI2_SIZE, BOOT_SIZE, SECTOR_SIZE,
},
fst::{Node, NodeKind},
io::block::{Block, BlockIO}, io::block::{Block, BlockIO},
streams::{ReadStream, SharedWindowedReadStream},
util::read::{read_box, read_box_slice, read_vec}, util::read::{read_box, read_box_slice, read_vec},
Result, ResultContext, Result, ResultContext,
}; };
@ -35,9 +33,9 @@ impl Clone for PartitionGC {
Self { Self {
io: self.io.clone(), io: self.io.clone(),
block: Block::default(), block: Block::default(),
block_buf: <u8>::new_box_slice_zeroed(self.block_buf.len()), block_buf: <[u8]>::new_box_zeroed_with_elems(self.block_buf.len()).unwrap(),
block_idx: u32::MAX, block_idx: u32::MAX,
sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed(), sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed().unwrap(),
sector: u32::MAX, sector: u32::MAX,
pos: 0, pos: 0,
disc_header: self.disc_header.clone(), disc_header: self.disc_header.clone(),
@ -51,9 +49,9 @@ impl PartitionGC {
Ok(Box::new(Self { Ok(Box::new(Self {
io: inner, io: inner,
block: Block::default(), block: Block::default(),
block_buf: <u8>::new_box_slice_zeroed(block_size as usize), block_buf: <[u8]>::new_box_zeroed_with_elems(block_size as usize).unwrap(),
block_idx: u32::MAX, block_idx: u32::MAX,
sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed(), sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed().unwrap(),
sector: u32::MAX, sector: u32::MAX,
pos: 0, pos: 0,
disc_header, disc_header,
@ -63,8 +61,8 @@ impl PartitionGC {
pub fn into_inner(self) -> Box<dyn BlockIO> { self.io } pub fn into_inner(self) -> Box<dyn BlockIO> { self.io }
} }
impl Read for PartitionGC { impl BufRead for PartitionGC {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { fn fill_buf(&mut self) -> io::Result<&[u8]> {
let sector = (self.pos / SECTOR_SIZE as u64) as u32; let sector = (self.pos / SECTOR_SIZE as u64) as u32;
let block_idx = (sector as u64 * SECTOR_SIZE as u64 / self.block_buf.len() as u64) as u32; let block_idx = (sector as u64 * SECTOR_SIZE as u64 / self.block_buf.len() as u64) as u32;
@ -86,9 +84,20 @@ impl Read for PartitionGC {
} }
let offset = (self.pos % SECTOR_SIZE as u64) as usize; let offset = (self.pos % SECTOR_SIZE as u64) as usize;
let len = min(buf.len(), SECTOR_SIZE - offset); Ok(&self.sector_buf[offset..])
buf[..len].copy_from_slice(&self.sector_buf[offset..offset + len]); }
self.pos += len as u64;
#[inline]
fn consume(&mut self, amt: usize) { self.pos += amt as u64; }
}
impl Read for PartitionGC {
#[inline]
fn read(&mut self, out: &mut [u8]) -> io::Result<usize> {
let buf = self.fill_buf()?;
let len = buf.len().min(out.len());
out[..len].copy_from_slice(&buf[..len]);
self.consume(len);
Ok(len) Ok(len)
} }
} }
@ -115,21 +124,35 @@ impl PartitionBase for PartitionGC {
read_part_meta(self, false) read_part_meta(self, false)
} }
fn open_file(&mut self, node: &Node) -> io::Result<SharedWindowedReadStream> { fn open_file(&mut self, node: Node) -> io::Result<FileStream> {
assert_eq!(node.kind(), NodeKind::File); if !node.is_file() {
self.new_window(node.offset(false), node.length()) return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Node is not a file".to_string(),
));
}
FileStream::new(self, node.offset(false), node.length())
} }
fn ideal_buffer_size(&self) -> usize { SECTOR_SIZE } fn into_open_file(self: Box<Self>, node: Node) -> io::Result<OwnedFileStream> {
if !node.is_file() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Node is not a file".to_string(),
));
}
OwnedFileStream::new(self, node.offset(false), node.length())
}
} }
pub(crate) fn read_part_meta( pub(crate) fn read_part_meta(
reader: &mut dyn ReadStream, reader: &mut dyn PartitionBase,
is_wii: bool, is_wii: bool,
) -> Result<Box<PartitionMeta>> { ) -> Result<Box<PartitionMeta>> {
// boot.bin // boot.bin
let raw_boot: Box<[u8; BOOT_SIZE]> = read_box(reader).context("Reading boot.bin")?; let raw_boot: Box<[u8; BOOT_SIZE]> = read_box(reader).context("Reading boot.bin")?;
let partition_header = PartitionHeader::ref_from(&raw_boot[size_of::<DiscHeader>()..]).unwrap(); let partition_header =
PartitionHeader::ref_from_bytes(&raw_boot[size_of::<DiscHeader>()..]).unwrap();
// bi2.bin // bi2.bin
let raw_bi2: Box<[u8; BI2_SIZE]> = read_box(reader).context("Reading bi2.bin")?; let raw_bi2: Box<[u8; BI2_SIZE]> = read_box(reader).context("Reading bi2.bin")?;
@ -137,7 +160,7 @@ pub(crate) fn read_part_meta(
// apploader.bin // apploader.bin
let mut raw_apploader: Vec<u8> = let mut raw_apploader: Vec<u8> =
read_vec(reader, size_of::<ApploaderHeader>()).context("Reading apploader header")?; read_vec(reader, size_of::<ApploaderHeader>()).context("Reading apploader header")?;
let apploader_header = ApploaderHeader::ref_from(raw_apploader.as_slice()).unwrap(); let apploader_header = ApploaderHeader::ref_from_bytes(raw_apploader.as_slice()).unwrap();
raw_apploader.resize( raw_apploader.resize(
size_of::<ApploaderHeader>() size_of::<ApploaderHeader>()
+ apploader_header.size.get() as usize + apploader_header.size.get() as usize
@ -147,6 +170,7 @@ pub(crate) fn read_part_meta(
reader reader
.read_exact(&mut raw_apploader[size_of::<ApploaderHeader>()..]) .read_exact(&mut raw_apploader[size_of::<ApploaderHeader>()..])
.context("Reading apploader")?; .context("Reading apploader")?;
let raw_apploader = raw_apploader.into_boxed_slice();
// fst.bin // fst.bin
reader reader
@ -167,7 +191,7 @@ pub(crate) fn read_part_meta(
.context("Seeking to DOL offset")?; .context("Seeking to DOL offset")?;
let mut raw_dol: Vec<u8> = let mut raw_dol: Vec<u8> =
read_vec(reader, size_of::<DolHeader>()).context("Reading DOL header")?; read_vec(reader, size_of::<DolHeader>()).context("Reading DOL header")?;
let dol_header = DolHeader::ref_from(raw_dol.as_slice()).unwrap(); let dol_header = DolHeader::ref_from_bytes(raw_dol.as_slice()).unwrap();
let dol_size = dol_header let dol_size = dol_header
.text_offs .text_offs
.iter() .iter()
@ -184,13 +208,14 @@ pub(crate) fn read_part_meta(
.unwrap_or(size_of::<DolHeader>() as u32); .unwrap_or(size_of::<DolHeader>() as u32);
raw_dol.resize(dol_size as usize, 0); raw_dol.resize(dol_size as usize, 0);
reader.read_exact(&mut raw_dol[size_of::<DolHeader>()..]).context("Reading DOL")?; reader.read_exact(&mut raw_dol[size_of::<DolHeader>()..]).context("Reading DOL")?;
let raw_dol = raw_dol.into_boxed_slice();
Ok(Box::new(PartitionMeta { Ok(Box::new(PartitionMeta {
raw_boot, raw_boot,
raw_bi2, raw_bi2,
raw_apploader: raw_apploader.into_boxed_slice(), raw_apploader,
raw_fst, raw_fst,
raw_dol: raw_dol.into_boxed_slice(), raw_dol,
raw_ticket: None, raw_ticket: None,
raw_tmd: None, raw_tmd: None,
raw_cert_chain: None, raw_cert_chain: None,

View File

@ -6,7 +6,7 @@ use std::{
use rayon::iter::{IntoParallelIterator, ParallelIterator}; use rayon::iter::{IntoParallelIterator, ParallelIterator};
use sha1::{Digest, Sha1}; use sha1::{Digest, Sha1};
use zerocopy::FromZeroes; use zerocopy::FromZeros;
use crate::{ use crate::{
array_ref, array_ref_mut, array_ref, array_ref_mut,
@ -39,7 +39,7 @@ pub struct HashTable {
pub h3_hashes: Box<[HashBytes]>, pub h3_hashes: Box<[HashBytes]>,
} }
#[derive(Clone, FromZeroes)] #[derive(Clone, FromZeros)]
struct HashResult { struct HashResult {
h0_hashes: [HashBytes; 1984], h0_hashes: [HashBytes; 1984],
h1_hashes: [HashBytes; 64], h1_hashes: [HashBytes; 64],
@ -54,10 +54,10 @@ impl HashTable {
let num_subgroups = num_sectors / 8; let num_subgroups = num_sectors / 8;
let num_groups = num_subgroups / 8; let num_groups = num_subgroups / 8;
Self { Self {
h0_hashes: HashBytes::new_box_slice_zeroed(num_data_hashes), h0_hashes: <[HashBytes]>::new_box_zeroed_with_elems(num_data_hashes).unwrap(),
h1_hashes: HashBytes::new_box_slice_zeroed(num_sectors), h1_hashes: <[HashBytes]>::new_box_zeroed_with_elems(num_sectors).unwrap(),
h2_hashes: HashBytes::new_box_slice_zeroed(num_subgroups), h2_hashes: <[HashBytes]>::new_box_zeroed_with_elems(num_subgroups).unwrap(),
h3_hashes: HashBytes::new_box_slice_zeroed(num_groups), h3_hashes: <[HashBytes]>::new_box_zeroed_with_elems(num_groups).unwrap(),
} }
} }
@ -100,8 +100,8 @@ pub fn rebuild_hashes(reader: &mut DiscReader) -> Result<()> {
(0..group_count).into_par_iter().try_for_each_with( (0..group_count).into_par_iter().try_for_each_with(
(reader.open_partition(part.index, &OpenOptions::default())?, mutex.clone()), (reader.open_partition(part.index, &OpenOptions::default())?, mutex.clone()),
|(stream, mutex), h3_index| -> Result<()> { |(stream, mutex), h3_index| -> Result<()> {
let mut result = HashResult::new_box_zeroed(); let mut result = HashResult::new_box_zeroed()?;
let mut data_buf = <u8>::new_box_slice_zeroed(SECTOR_DATA_SIZE); let mut data_buf = <[u8]>::new_box_zeroed_with_elems(SECTOR_DATA_SIZE)?;
let mut h3_hasher = Sha1::new(); let mut h3_hasher = Sha1::new();
for h2_index in 0..8 { for h2_index in 0..8 {
let mut h2_hasher = Sha1::new(); let mut h2_hasher = Sha1::new();

View File

@ -5,33 +5,40 @@ use std::{
ffi::CStr, ffi::CStr,
fmt::{Debug, Display, Formatter}, fmt::{Debug, Display, Formatter},
io, io,
io::{BufRead, Seek},
mem::size_of, mem::size_of,
str::from_utf8, str::from_utf8,
}; };
use dyn_clone::DynClone; use dyn_clone::DynClone;
use zerocopy::{big_endian::*, AsBytes, FromBytes, FromZeroes}; use zerocopy::{big_endian::*, FromBytes, Immutable, IntoBytes, KnownLayout};
use crate::{ use crate::{io::MagicBytes, static_assert, Result};
disc::wii::{Ticket, TmdHeader},
fst::Node,
static_assert,
streams::{ReadStream, SharedWindowedReadStream},
Fst, Result,
};
pub(crate) mod fst;
pub(crate) mod gcn; pub(crate) mod gcn;
pub(crate) mod hashes; pub(crate) mod hashes;
pub(crate) mod reader; pub(crate) mod reader;
pub(crate) mod streams;
pub(crate) mod wii; pub(crate) mod wii;
/// Size in bytes of a disc sector. pub use fst::{Fst, Node, NodeKind};
pub use streams::{FileStream, OwnedFileStream, WindowedStream};
pub use wii::{SignedHeader, Ticket, TicketLimit, TmdHeader, REGION_SIZE};
/// Size in bytes of a disc sector. (32 KiB)
pub const SECTOR_SIZE: usize = 0x8000; pub const SECTOR_SIZE: usize = 0x8000;
/// Magic bytes for Wii discs. Located at offset 0x18.
pub const WII_MAGIC: MagicBytes = [0x5D, 0x1C, 0x9E, 0xA3];
/// Magic bytes for GameCube discs. Located at offset 0x1C.
pub const GCN_MAGIC: MagicBytes = [0xC2, 0x33, 0x9F, 0x3D];
/// Shared GameCube & Wii disc header. /// Shared GameCube & Wii disc header.
/// ///
/// This header is always at the start of the disc image and within each Wii partition. /// This header is always at the start of the disc image and within each Wii partition.
#[derive(Clone, Debug, PartialEq, FromBytes, FromZeroes, AsBytes)] #[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(4))] #[repr(C, align(4))]
pub struct DiscHeader { pub struct DiscHeader {
/// Game ID (e.g. GM8E01 for Metroid Prime) /// Game ID (e.g. GM8E01 for Metroid Prime)
@ -47,9 +54,9 @@ pub struct DiscHeader {
/// Padding /// Padding
_pad1: [u8; 14], _pad1: [u8; 14],
/// If this is a Wii disc, this will be 0x5D1C9EA3 /// If this is a Wii disc, this will be 0x5D1C9EA3
pub wii_magic: U32, pub wii_magic: MagicBytes,
/// If this is a GameCube disc, this will be 0xC2339F3D /// If this is a GameCube disc, this will be 0xC2339F3D
pub gcn_magic: U32, pub gcn_magic: MagicBytes,
/// Game title /// Game title
pub game_title: [u8; 64], pub game_title: [u8; 64],
/// If 1, disc omits partition hashes /// If 1, disc omits partition hashes
@ -64,9 +71,11 @@ static_assert!(size_of::<DiscHeader>() == 0x400);
impl DiscHeader { impl DiscHeader {
/// Game ID as a string. /// Game ID as a string.
#[inline]
pub fn game_id_str(&self) -> &str { from_utf8(&self.game_id).unwrap_or("[invalid]") } pub fn game_id_str(&self) -> &str { from_utf8(&self.game_id).unwrap_or("[invalid]") }
/// Game title as a string. /// Game title as a string.
#[inline]
pub fn game_title_str(&self) -> &str { pub fn game_title_str(&self) -> &str {
CStr::from_bytes_until_nul(&self.game_title) CStr::from_bytes_until_nul(&self.game_title)
.ok() .ok()
@ -75,10 +84,12 @@ impl DiscHeader {
} }
/// Whether this is a GameCube disc. /// Whether this is a GameCube disc.
pub fn is_gamecube(&self) -> bool { self.gcn_magic.get() == 0xC2339F3D } #[inline]
pub fn is_gamecube(&self) -> bool { self.gcn_magic == GCN_MAGIC }
/// Whether this is a Wii disc. /// Whether this is a Wii disc.
pub fn is_wii(&self) -> bool { self.wii_magic.get() == 0x5D1C9EA3 } #[inline]
pub fn is_wii(&self) -> bool { self.wii_magic == WII_MAGIC }
} }
/// A header describing the contents of a disc partition. /// A header describing the contents of a disc partition.
@ -86,7 +97,7 @@ impl DiscHeader {
/// **GameCube**: Always follows the disc header. /// **GameCube**: Always follows the disc header.
/// ///
/// **Wii**: Follows the disc header within each partition. /// **Wii**: Follows the disc header within each partition.
#[derive(Clone, Debug, PartialEq, FromBytes, FromZeroes, AsBytes)] #[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(4))] #[repr(C, align(4))]
pub struct PartitionHeader { pub struct PartitionHeader {
/// Debug monitor offset /// Debug monitor offset
@ -105,9 +116,9 @@ pub struct PartitionHeader {
pub fst_max_size: U32, pub fst_max_size: U32,
/// File system table load address /// File system table load address
pub fst_memory_address: U32, pub fst_memory_address: U32,
/// User position /// User data offset
pub user_position: U32, pub user_offset: U32,
/// User size /// User data size
pub user_size: U32, pub user_size: U32,
/// Padding /// Padding
_pad2: [u8; 4], _pad2: [u8; 4],
@ -117,6 +128,7 @@ static_assert!(size_of::<PartitionHeader>() == 0x40);
impl PartitionHeader { impl PartitionHeader {
/// Offset within the partition to the main DOL. /// Offset within the partition to the main DOL.
#[inline]
pub fn dol_offset(&self, is_wii: bool) -> u64 { pub fn dol_offset(&self, is_wii: bool) -> u64 {
if is_wii { if is_wii {
self.dol_offset.get() as u64 * 4 self.dol_offset.get() as u64 * 4
@ -126,6 +138,7 @@ impl PartitionHeader {
} }
/// Offset within the partition to the file system table (FST). /// Offset within the partition to the file system table (FST).
#[inline]
pub fn fst_offset(&self, is_wii: bool) -> u64 { pub fn fst_offset(&self, is_wii: bool) -> u64 {
if is_wii { if is_wii {
self.fst_offset.get() as u64 * 4 self.fst_offset.get() as u64 * 4
@ -135,6 +148,7 @@ impl PartitionHeader {
} }
/// Size of the file system table (FST). /// Size of the file system table (FST).
#[inline]
pub fn fst_size(&self, is_wii: bool) -> u64 { pub fn fst_size(&self, is_wii: bool) -> u64 {
if is_wii { if is_wii {
self.fst_size.get() as u64 * 4 self.fst_size.get() as u64 * 4
@ -144,6 +158,7 @@ impl PartitionHeader {
} }
/// Maximum size of the file system table (FST) across multi-disc games. /// Maximum size of the file system table (FST) across multi-disc games.
#[inline]
pub fn fst_max_size(&self, is_wii: bool) -> u64 { pub fn fst_max_size(&self, is_wii: bool) -> u64 {
if is_wii { if is_wii {
self.fst_max_size.get() as u64 * 4 self.fst_max_size.get() as u64 * 4
@ -154,7 +169,7 @@ impl PartitionHeader {
} }
/// Apploader header. /// Apploader header.
#[derive(Debug, PartialEq, Clone, FromBytes, FromZeroes, AsBytes)] #[derive(Debug, PartialEq, Clone, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(4))] #[repr(C, align(4))]
pub struct ApploaderHeader { pub struct ApploaderHeader {
/// Apploader build date /// Apploader build date
@ -171,6 +186,7 @@ pub struct ApploaderHeader {
impl ApploaderHeader { impl ApploaderHeader {
/// Apploader build date as a string. /// Apploader build date as a string.
#[inline]
pub fn date_str(&self) -> Option<&str> { pub fn date_str(&self) -> Option<&str> {
CStr::from_bytes_until_nul(&self.date).ok().and_then(|c| c.to_str().ok()) CStr::from_bytes_until_nul(&self.date).ok().and_then(|c| c.to_str().ok())
} }
@ -182,7 +198,7 @@ pub const DOL_MAX_TEXT_SECTIONS: usize = 7;
pub const DOL_MAX_DATA_SECTIONS: usize = 11; pub const DOL_MAX_DATA_SECTIONS: usize = 11;
/// Dolphin executable (DOL) header. /// Dolphin executable (DOL) header.
#[derive(Debug, Clone, FromBytes, FromZeroes)] #[derive(Debug, Clone, FromBytes, Immutable, KnownLayout)]
pub struct DolHeader { pub struct DolHeader {
/// Text section offsets /// Text section offsets
pub text_offs: [U32; DOL_MAX_TEXT_SECTIONS], pub text_offs: [U32; DOL_MAX_TEXT_SECTIONS],
@ -222,6 +238,7 @@ pub enum PartitionKind {
} }
impl Display for PartitionKind { impl Display for PartitionKind {
#[inline]
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self { match self {
Self::Data => write!(f, "Data"), Self::Data => write!(f, "Data"),
@ -237,6 +254,7 @@ impl Display for PartitionKind {
impl PartitionKind { impl PartitionKind {
/// Returns the directory name for the partition kind. /// Returns the directory name for the partition kind.
#[inline]
pub fn dir_name(&self) -> Cow<str> { pub fn dir_name(&self) -> Cow<str> {
match self { match self {
Self::Data => Cow::Borrowed("DATA"), Self::Data => Cow::Borrowed("DATA"),
@ -251,6 +269,7 @@ impl PartitionKind {
} }
impl From<u32> for PartitionKind { impl From<u32> for PartitionKind {
#[inline]
fn from(v: u32) -> Self { fn from(v: u32) -> Self {
match v { match v {
0 => Self::Data, 0 => Self::Data,
@ -262,11 +281,11 @@ impl From<u32> for PartitionKind {
} }
/// An open disc partition. /// An open disc partition.
pub trait PartitionBase: DynClone + ReadStream + Send + Sync { pub trait PartitionBase: DynClone + BufRead + Seek + Send + Sync {
/// Reads the partition header and file system table. /// Reads the partition header and file system table.
fn meta(&mut self) -> Result<Box<PartitionMeta>>; fn meta(&mut self) -> Result<Box<PartitionMeta>>;
/// Seeks the read stream to the specified file system node /// Seeks the partition stream to the specified file system node
/// and returns a windowed stream. /// and returns a windowed stream.
/// ///
/// # Examples /// # Examples
@ -294,12 +313,36 @@ pub trait PartitionBase: DynClone + ReadStream + Send + Sync {
/// Ok(()) /// Ok(())
/// } /// }
/// ``` /// ```
fn open_file(&mut self, node: &Node) -> io::Result<SharedWindowedReadStream>; fn open_file(&mut self, node: Node) -> io::Result<FileStream>;
/// The ideal size for buffered reads from this partition. /// Consumes the partition instance and returns a windowed stream.
/// GameCube discs have a data block size of 0x8000, ///
/// whereas Wii discs have a data block size of 0x7C00. /// # Examples
fn ideal_buffer_size(&self) -> usize; ///
/// ```no_run
/// use std::io::Read;
///
/// use nod::{Disc, PartitionKind, OwnedFileStream};
///
/// fn main() -> nod::Result<()> {
/// let disc = Disc::new("path/to/file.iso")?;
/// let mut partition = disc.open_partition_kind(PartitionKind::Data)?;
/// let meta = partition.meta()?;
/// let fst = meta.fst()?;
/// if let Some((_, node)) = fst.find("/disc.tgc") {
/// let file: OwnedFileStream = partition
/// .clone() // Clone the Box<dyn PartitionBase>
/// .into_open_file(node) // Get an OwnedFileStream
/// .expect("Failed to open file stream");
/// // Open the inner disc image using the owned stream
/// let inner_disc = Disc::new_stream(Box::new(file))
/// .expect("Failed to open inner disc");
/// // ...
/// }
/// Ok(())
/// }
/// ```
fn into_open_file(self: Box<Self>, node: Node) -> io::Result<OwnedFileStream>;
} }
dyn_clone::clone_trait_object!(PartitionBase); dyn_clone::clone_trait_object!(PartitionBase);
@ -318,10 +361,10 @@ pub struct PartitionMeta {
pub raw_bi2: Box<[u8; BI2_SIZE]>, pub raw_bi2: Box<[u8; BI2_SIZE]>,
/// Apploader (apploader.bin) /// Apploader (apploader.bin)
pub raw_apploader: Box<[u8]>, pub raw_apploader: Box<[u8]>,
/// File system table (fst.bin)
pub raw_fst: Box<[u8]>,
/// Main binary (main.dol) /// Main binary (main.dol)
pub raw_dol: Box<[u8]>, pub raw_dol: Box<[u8]>,
/// File system table (fst.bin)
pub raw_fst: Box<[u8]>,
/// Ticket (ticket.bin, Wii only) /// Ticket (ticket.bin, Wii only)
pub raw_ticket: Option<Box<[u8]>>, pub raw_ticket: Option<Box<[u8]>>,
/// TMD (tmd.bin, Wii only) /// TMD (tmd.bin, Wii only)
@ -334,34 +377,41 @@ pub struct PartitionMeta {
impl PartitionMeta { impl PartitionMeta {
/// A view into the disc header. /// A view into the disc header.
#[inline]
pub fn header(&self) -> &DiscHeader { pub fn header(&self) -> &DiscHeader {
DiscHeader::ref_from(&self.raw_boot[..size_of::<DiscHeader>()]).unwrap() DiscHeader::ref_from_bytes(&self.raw_boot[..size_of::<DiscHeader>()]).unwrap()
} }
/// A view into the partition header. /// A view into the partition header.
#[inline]
pub fn partition_header(&self) -> &PartitionHeader { pub fn partition_header(&self) -> &PartitionHeader {
PartitionHeader::ref_from(&self.raw_boot[size_of::<DiscHeader>()..]).unwrap() PartitionHeader::ref_from_bytes(&self.raw_boot[size_of::<DiscHeader>()..]).unwrap()
} }
/// A view into the apploader header. /// A view into the apploader header.
#[inline]
pub fn apploader_header(&self) -> &ApploaderHeader { pub fn apploader_header(&self) -> &ApploaderHeader {
ApploaderHeader::ref_from_prefix(&self.raw_apploader).unwrap() ApploaderHeader::ref_from_prefix(&self.raw_apploader).unwrap().0
} }
/// A view into the file system table (FST). /// A view into the file system table (FST).
#[inline]
pub fn fst(&self) -> Result<Fst, &'static str> { Fst::new(&self.raw_fst) } pub fn fst(&self) -> Result<Fst, &'static str> { Fst::new(&self.raw_fst) }
/// A view into the DOL header. /// A view into the DOL header.
pub fn dol_header(&self) -> &DolHeader { DolHeader::ref_from_prefix(&self.raw_dol).unwrap() } #[inline]
pub fn dol_header(&self) -> &DolHeader { DolHeader::ref_from_prefix(&self.raw_dol).unwrap().0 }
/// A view into the ticket. (Wii only) /// A view into the ticket. (Wii only)
#[inline]
pub fn ticket(&self) -> Option<&Ticket> { pub fn ticket(&self) -> Option<&Ticket> {
self.raw_ticket.as_ref().and_then(|v| Ticket::ref_from(v)) self.raw_ticket.as_ref().and_then(|v| Ticket::ref_from_bytes(v).ok())
} }
/// A view into the TMD. (Wii only) /// A view into the TMD. (Wii only)
#[inline]
pub fn tmd_header(&self) -> Option<&TmdHeader> { pub fn tmd_header(&self) -> Option<&TmdHeader> {
self.raw_tmd.as_ref().and_then(|v| TmdHeader::ref_from_prefix(v)) self.raw_tmd.as_ref().and_then(|v| TmdHeader::ref_from_prefix(v).ok().map(|(v, _)| v))
} }
} }

View File

@ -1,22 +1,22 @@
use std::{ use std::{
cmp::min,
io, io,
io::{Read, Seek, SeekFrom}, io::{BufRead, Read, Seek, SeekFrom},
}; };
use zerocopy::FromZeroes; use zerocopy::FromZeros;
use crate::{ use super::{
disc::{
gcn::PartitionGC, gcn::PartitionGC,
hashes::{rebuild_hashes, HashTable}, hashes::{rebuild_hashes, HashTable},
wii::{PartitionWii, WiiPartEntry, WiiPartGroup, WiiPartitionHeader, WII_PART_GROUP_OFF}, wii::{PartitionWii, WiiPartEntry, WiiPartGroup, WiiPartitionHeader, WII_PART_GROUP_OFF},
DL_DVD_SIZE, MINI_DVD_SIZE, SL_DVD_SIZE, DiscHeader, PartitionBase, PartitionHeader, PartitionKind, DL_DVD_SIZE, MINI_DVD_SIZE,
}, REGION_SIZE, SL_DVD_SIZE,
};
use crate::{
disc::wii::REGION_OFFSET,
io::block::{Block, BlockIO, PartitionInfo}, io::block::{Block, BlockIO, PartitionInfo},
util::read::{read_box, read_from, read_vec}, util::read::{read_box, read_from, read_vec},
DiscHeader, DiscMeta, Error, OpenOptions, PartitionBase, PartitionHeader, PartitionKind, DiscMeta, Error, OpenOptions, Result, ResultContext, SECTOR_SIZE,
Result, ResultContext, SECTOR_SIZE,
}; };
#[derive(Debug, Eq, PartialEq, Copy, Clone)] #[derive(Debug, Eq, PartialEq, Copy, Clone)]
@ -37,6 +37,7 @@ pub struct DiscReader {
disc_header: Box<DiscHeader>, disc_header: Box<DiscHeader>,
pub(crate) partitions: Vec<PartitionInfo>, pub(crate) partitions: Vec<PartitionInfo>,
hash_tables: Vec<HashTable>, hash_tables: Vec<HashTable>,
region: Option<[u8; REGION_SIZE]>,
} }
impl Clone for DiscReader { impl Clone for DiscReader {
@ -44,15 +45,16 @@ impl Clone for DiscReader {
Self { Self {
io: self.io.clone(), io: self.io.clone(),
block: Block::default(), block: Block::default(),
block_buf: <u8>::new_box_slice_zeroed(self.block_buf.len()), block_buf: <[u8]>::new_box_zeroed_with_elems(self.block_buf.len()).unwrap(),
block_idx: u32::MAX, block_idx: u32::MAX,
sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed(), sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed().unwrap(),
sector_idx: u32::MAX, sector_idx: u32::MAX,
pos: 0, pos: 0,
mode: self.mode, mode: self.mode,
disc_header: self.disc_header.clone(), disc_header: self.disc_header.clone(),
partitions: self.partitions.clone(), partitions: self.partitions.clone(),
hash_tables: self.hash_tables.clone(), hash_tables: self.hash_tables.clone(),
region: self.region,
} }
} }
} }
@ -64,9 +66,9 @@ impl DiscReader {
let mut reader = Self { let mut reader = Self {
io: inner, io: inner,
block: Block::default(), block: Block::default(),
block_buf: <u8>::new_box_slice_zeroed(block_size as usize), block_buf: <[u8]>::new_box_zeroed_with_elems(block_size as usize)?,
block_idx: u32::MAX, block_idx: u32::MAX,
sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed(), sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed()?,
sector_idx: u32::MAX, sector_idx: u32::MAX,
pos: 0, pos: 0,
mode: if options.rebuild_encryption { mode: if options.rebuild_encryption {
@ -74,13 +76,16 @@ impl DiscReader {
} else { } else {
EncryptionMode::Decrypted EncryptionMode::Decrypted
}, },
disc_header: DiscHeader::new_box_zeroed(), disc_header: DiscHeader::new_box_zeroed()?,
partitions: vec![], partitions: vec![],
hash_tables: vec![], hash_tables: vec![],
region: None,
}; };
let disc_header: Box<DiscHeader> = read_box(&mut reader).context("Reading disc header")?; let disc_header: Box<DiscHeader> = read_box(&mut reader).context("Reading disc header")?;
reader.disc_header = disc_header; reader.disc_header = disc_header;
if reader.disc_header.is_wii() { if reader.disc_header.is_wii() {
reader.seek(SeekFrom::Start(REGION_OFFSET)).context("Seeking to region info")?;
reader.region = Some(read_from(&mut reader).context("Reading region info")?);
reader.partitions = read_partition_info(&mut reader)?; reader.partitions = read_partition_info(&mut reader)?;
// Rebuild hashes if the format requires it // Rebuild hashes if the format requires it
if (options.rebuild_encryption || options.validate_hashes) && meta.needs_hash_recovery { if (options.rebuild_encryption || options.validate_hashes) && meta.needs_hash_recovery {
@ -104,10 +109,16 @@ impl DiscReader {
self.io.meta().disc_size.unwrap_or_else(|| guess_disc_size(&self.partitions)) self.io.meta().disc_size.unwrap_or_else(|| guess_disc_size(&self.partitions))
} }
#[inline]
pub fn header(&self) -> &DiscHeader { &self.disc_header } pub fn header(&self) -> &DiscHeader { &self.disc_header }
#[inline]
pub fn region(&self) -> Option<&[u8; REGION_SIZE]> { self.region.as_ref() }
#[inline]
pub fn partitions(&self) -> &[PartitionInfo] { &self.partitions } pub fn partitions(&self) -> &[PartitionInfo] { &self.partitions }
#[inline]
pub fn meta(&self) -> DiscMeta { self.io.meta() } pub fn meta(&self) -> DiscMeta { self.io.meta() }
/// Opens a new, decrypted partition read stream for the specified partition index. /// Opens a new, decrypted partition read stream for the specified partition index.
@ -150,8 +161,8 @@ impl DiscReader {
} }
} }
impl Read for DiscReader { impl BufRead for DiscReader {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { fn fill_buf(&mut self) -> io::Result<&[u8]> {
let block_idx = (self.pos / self.block_buf.len() as u64) as u32; let block_idx = (self.pos / self.block_buf.len() as u64) as u32;
let abs_sector = (self.pos / SECTOR_SIZE as u64) as u32; let abs_sector = (self.pos / SECTOR_SIZE as u64) as u32;
@ -199,9 +210,20 @@ impl Read for DiscReader {
// Read from sector buffer // Read from sector buffer
let offset = (self.pos % SECTOR_SIZE as u64) as usize; let offset = (self.pos % SECTOR_SIZE as u64) as usize;
let len = min(buf.len(), SECTOR_SIZE - offset); Ok(&self.sector_buf[offset..])
buf[..len].copy_from_slice(&self.sector_buf[offset..offset + len]); }
self.pos += len as u64;
#[inline]
fn consume(&mut self, amt: usize) { self.pos += amt as u64; }
}
impl Read for DiscReader {
#[inline]
fn read(&mut self, out: &mut [u8]) -> io::Result<usize> {
let buf = self.fill_buf()?;
let len = buf.len().min(out.len());
out[..len].copy_from_slice(&buf[..len]);
self.consume(len);
Ok(len) Ok(len)
} }
} }
@ -268,8 +290,8 @@ fn read_partition_info(reader: &mut DiscReader) -> Result<Vec<PartitionInfo>> {
data_end_sector: (data_end_offset / SECTOR_SIZE as u64) as u32, data_end_sector: (data_end_offset / SECTOR_SIZE as u64) as u32,
key, key,
header, header,
disc_header: DiscHeader::new_box_zeroed(), disc_header: DiscHeader::new_box_zeroed()?,
partition_header: PartitionHeader::new_box_zeroed(), partition_header: PartitionHeader::new_box_zeroed()?,
hash_table: None, hash_table: None,
}; };

101
nod/src/disc/streams.rs Normal file
View File

@ -0,0 +1,101 @@
//! Partition file read stream.
use std::{
io,
io::{BufRead, Read, Seek, SeekFrom},
};
use super::PartitionBase;
/// A file read stream borrowing a [`PartitionBase`].
pub type FileStream<'a> = WindowedStream<&'a mut dyn PartitionBase>;
/// A file read stream owning a [`PartitionBase`].
pub type OwnedFileStream = WindowedStream<Box<dyn PartitionBase>>;
/// A read stream with a fixed window.
#[derive(Clone)]
pub struct WindowedStream<T>
where T: BufRead + Seek
{
base: T,
pos: u64,
begin: u64,
end: u64,
}
impl<T> WindowedStream<T>
where T: BufRead + Seek
{
/// Creates a new windowed stream with offset and size.
///
/// Seeks underlying stream immediately.
#[inline]
pub fn new(mut base: T, offset: u64, size: u64) -> io::Result<Self> {
base.seek(SeekFrom::Start(offset))?;
Ok(Self { base, pos: offset, begin: offset, end: offset + size })
}
/// Returns the length of the window.
#[inline]
#[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> u64 { self.end - self.begin }
}
impl<T> Read for WindowedStream<T>
where T: BufRead + Seek
{
#[inline]
fn read(&mut self, out: &mut [u8]) -> io::Result<usize> {
let buf = self.fill_buf()?;
let len = buf.len().min(out.len());
out[..len].copy_from_slice(&buf[..len]);
self.consume(len);
Ok(len)
}
}
impl<T> BufRead for WindowedStream<T>
where T: BufRead + Seek
{
#[inline]
fn fill_buf(&mut self) -> io::Result<&[u8]> {
let limit = self.end.saturating_sub(self.pos);
if limit == 0 {
return Ok(&[]);
}
let buf = self.base.fill_buf()?;
let max = (buf.len() as u64).min(limit) as usize;
Ok(&buf[..max])
}
#[inline]
fn consume(&mut self, amt: usize) {
self.base.consume(amt);
self.pos += amt as u64;
}
}
impl<T> Seek for WindowedStream<T>
where T: BufRead + Seek
{
#[inline]
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
let mut pos = match pos {
SeekFrom::Start(p) => self.begin + p,
SeekFrom::End(p) => self.end.saturating_add_signed(p),
SeekFrom::Current(p) => self.pos.saturating_add_signed(p),
};
if pos < self.begin {
pos = self.begin;
} else if pos > self.end {
pos = self.end;
}
let result = self.base.seek(SeekFrom::Start(pos))?;
self.pos = result;
Ok(result - self.begin)
}
#[inline]
fn stream_position(&mut self) -> io::Result<u64> { Ok(self.pos) }
}

View File

@ -1,30 +1,28 @@
use std::{ use std::{
cmp::min,
ffi::CStr, ffi::CStr,
io, io,
io::{Read, Seek, SeekFrom}, io::{BufRead, Read, Seek, SeekFrom},
mem::size_of, mem::size_of,
}; };
use sha1::{Digest, Sha1}; use sha1::{Digest, Sha1};
use zerocopy::{big_endian::*, AsBytes, FromBytes, FromZeroes}; use zerocopy::{big_endian::*, FromBytes, FromZeros, Immutable, IntoBytes, KnownLayout};
use super::{
gcn::{read_part_meta, PartitionGC},
DiscHeader, FileStream, Node, PartitionBase, PartitionMeta, SECTOR_SIZE,
};
use crate::{ use crate::{
array_ref, array_ref,
disc::{ disc::streams::OwnedFileStream,
gcn::{read_part_meta, PartitionGC},
PartitionBase, PartitionMeta, SECTOR_SIZE,
},
fst::{Node, NodeKind},
io::{ io::{
aes_decrypt, aes_decrypt,
block::{Block, BlockIO, PartitionInfo}, block::{Block, BlockIO, PartitionInfo},
KeyBytes, KeyBytes,
}, },
static_assert, static_assert,
streams::{ReadStream, SharedWindowedReadStream},
util::{div_rem, read::read_box_slice}, util::{div_rem, read::read_box_slice},
DiscHeader, Error, OpenOptions, Result, ResultContext, Error, OpenOptions, Result, ResultContext,
}; };
/// Size in bytes of the hashes block in a Wii disc sector /// Size in bytes of the hashes block in a Wii disc sector
@ -33,6 +31,12 @@ pub(crate) const HASHES_SIZE: usize = 0x400;
/// Size in bytes of the data block in a Wii disc sector (excluding hashes) /// Size in bytes of the data block in a Wii disc sector (excluding hashes)
pub(crate) const SECTOR_DATA_SIZE: usize = SECTOR_SIZE - HASHES_SIZE; // 0x7C00 pub(crate) const SECTOR_DATA_SIZE: usize = SECTOR_SIZE - HASHES_SIZE; // 0x7C00
/// Size of the disc region info (region.bin)
pub const REGION_SIZE: usize = 0x20;
/// Offset of the disc region info
pub const REGION_OFFSET: u64 = 0x4E000;
// ppki (Retail) // ppki (Retail)
const RVL_CERT_ISSUER_PPKI_TICKET: &str = "Root-CA00000001-XS00000003"; const RVL_CERT_ISSUER_PPKI_TICKET: &str = "Root-CA00000001-XS00000003";
#[rustfmt::skip] #[rustfmt::skip]
@ -57,7 +61,7 @@ const DEBUG_COMMON_KEYS: [KeyBytes; 3] = [
[0x2f, 0x5c, 0x1b, 0x29, 0x44, 0xe7, 0xfd, 0x6f, 0xc3, 0x97, 0x96, 0x4b, 0x05, 0x76, 0x91, 0xfa], [0x2f, 0x5c, 0x1b, 0x29, 0x44, 0xe7, 0xfd, 0x6f, 0xc3, 0x97, 0x96, 0x4b, 0x05, 0x76, 0x91, 0xfa],
]; ];
#[derive(Debug, PartialEq, FromBytes, FromZeroes, AsBytes)] #[derive(Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(4))] #[repr(C, align(4))]
pub(crate) struct WiiPartEntry { pub(crate) struct WiiPartEntry {
pub(crate) offset: U32, pub(crate) offset: U32,
@ -72,7 +76,7 @@ impl WiiPartEntry {
pub(crate) const WII_PART_GROUP_OFF: u64 = 0x40000; pub(crate) const WII_PART_GROUP_OFF: u64 = 0x40000;
#[derive(Debug, PartialEq, FromBytes, FromZeroes, AsBytes)] #[derive(Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(4))] #[repr(C, align(4))]
pub(crate) struct WiiPartGroup { pub(crate) struct WiiPartGroup {
pub(crate) part_count: U32, pub(crate) part_count: U32,
@ -85,7 +89,8 @@ impl WiiPartGroup {
pub(crate) fn part_entry_off(&self) -> u64 { (self.part_entry_off.get() as u64) << 2 } pub(crate) fn part_entry_off(&self) -> u64 { (self.part_entry_off.get() as u64) << 2 }
} }
#[derive(Debug, Clone, PartialEq, FromBytes, FromZeroes, AsBytes)] /// Signed blob header
#[derive(Debug, Clone, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(4))] #[repr(C, align(4))]
pub struct SignedHeader { pub struct SignedHeader {
/// Signature type, always 0x00010001 (RSA-2048) /// Signature type, always 0x00010001 (RSA-2048)
@ -97,43 +102,64 @@ pub struct SignedHeader {
static_assert!(size_of::<SignedHeader>() == 0x140); static_assert!(size_of::<SignedHeader>() == 0x140);
#[derive(Debug, Clone, PartialEq, Default, FromBytes, FromZeroes, AsBytes)] /// Ticket limit
#[derive(Debug, Clone, PartialEq, Default, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(4))] #[repr(C, align(4))]
pub struct TicketTimeLimit { pub struct TicketLimit {
pub enable_time_limit: U32, /// Limit type
pub time_limit: U32, pub limit_type: U32,
/// Maximum value for the limit
pub max_value: U32,
} }
static_assert!(size_of::<TicketTimeLimit>() == 8); static_assert!(size_of::<TicketLimit>() == 8);
#[derive(Debug, Clone, PartialEq, FromBytes, FromZeroes, AsBytes)] /// Wii ticket
#[derive(Debug, Clone, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(4))] #[repr(C, align(4))]
pub struct Ticket { pub struct Ticket {
/// Signed blob header
pub header: SignedHeader, pub header: SignedHeader,
/// Signature issuer
pub sig_issuer: [u8; 64], pub sig_issuer: [u8; 64],
/// ECDH data
pub ecdh: [u8; 60], pub ecdh: [u8; 60],
/// Ticket format version
pub version: u8, pub version: u8,
_pad1: U16, _pad1: U16,
/// Title key (encrypted)
pub title_key: KeyBytes, pub title_key: KeyBytes,
_pad2: u8, _pad2: u8,
/// Ticket ID
pub ticket_id: [u8; 8], pub ticket_id: [u8; 8],
/// Console ID
pub console_id: [u8; 4], pub console_id: [u8; 4],
/// Title ID
pub title_id: [u8; 8], pub title_id: [u8; 8],
_pad3: U16, _pad3: U16,
/// Ticket title version
pub ticket_title_version: U16, pub ticket_title_version: U16,
/// Permitted titles mask
pub permitted_titles_mask: U32, pub permitted_titles_mask: U32,
/// Permit mask
pub permit_mask: U32, pub permit_mask: U32,
/// Title export allowed
pub title_export_allowed: u8, pub title_export_allowed: u8,
/// Common key index
pub common_key_idx: u8, pub common_key_idx: u8,
_pad4: [u8; 48], _pad4: [u8; 48],
/// Content access permissions
pub content_access_permissions: [u8; 64], pub content_access_permissions: [u8; 64],
_pad5: [u8; 2], _pad5: [u8; 2],
pub time_limits: [TicketTimeLimit; 8], /// Ticket limits
pub limits: [TicketLimit; 8],
} }
static_assert!(size_of::<Ticket>() == 0x2A4); static_assert!(size_of::<Ticket>() == 0x2A4);
impl Ticket { impl Ticket {
/// Decrypts the ticket title key using the appropriate common key
#[allow(clippy::missing_inline_in_public_items)]
pub fn decrypt_title_key(&self) -> Result<KeyBytes> { pub fn decrypt_title_key(&self) -> Result<KeyBytes> {
let mut iv: KeyBytes = [0; 16]; let mut iv: KeyBytes = [0; 16];
iv[..8].copy_from_slice(&self.title_id); iv[..8].copy_from_slice(&self.title_id);
@ -158,29 +184,48 @@ impl Ticket {
} }
} }
#[derive(Debug, Clone, PartialEq, FromBytes, FromZeroes, AsBytes)] /// Title metadata header
#[derive(Debug, Clone, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(4))] #[repr(C, align(4))]
pub struct TmdHeader { pub struct TmdHeader {
/// Signed blob header
pub header: SignedHeader, pub header: SignedHeader,
/// Signature issuer
pub sig_issuer: [u8; 64], pub sig_issuer: [u8; 64],
/// Version
pub version: u8, pub version: u8,
/// CA CRL version
pub ca_crl_version: u8, pub ca_crl_version: u8,
/// Signer CRL version
pub signer_crl_version: u8, pub signer_crl_version: u8,
/// Is vWii title
pub is_vwii: u8, pub is_vwii: u8,
/// IOS ID
pub ios_id: [u8; 8], pub ios_id: [u8; 8],
/// Title ID
pub title_id: [u8; 8], pub title_id: [u8; 8],
/// Title type
pub title_type: u32, pub title_type: u32,
/// Group ID
pub group_id: U16, pub group_id: U16,
_pad1: [u8; 2], _pad1: [u8; 2],
/// Region
pub region: U16, pub region: U16,
/// Ratings
pub ratings: KeyBytes, pub ratings: KeyBytes,
_pad2: [u8; 12], _pad2: [u8; 12],
/// IPC mask
pub ipc_mask: [u8; 12], pub ipc_mask: [u8; 12],
_pad3: [u8; 18], _pad3: [u8; 18],
/// Access flags
pub access_flags: U32, pub access_flags: U32,
/// Title version
pub title_version: U16, pub title_version: U16,
/// Number of contents
pub num_contents: U16, pub num_contents: U16,
/// Boot index
pub boot_idx: U16, pub boot_idx: U16,
/// Minor version (unused)
pub minor_version: U16, pub minor_version: U16,
} }
@ -188,7 +233,7 @@ static_assert!(size_of::<TmdHeader>() == 0x1E4);
pub const H3_TABLE_SIZE: usize = 0x18000; pub const H3_TABLE_SIZE: usize = 0x18000;
#[derive(Debug, Clone, PartialEq, FromBytes, FromZeroes, AsBytes)] #[derive(Debug, Clone, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(4))] #[repr(C, align(4))]
pub struct WiiPartitionHeader { pub struct WiiPartitionHeader {
pub ticket: Ticket, pub ticket: Ticket,
@ -242,9 +287,9 @@ impl Clone for PartitionWii {
io: self.io.clone(), io: self.io.clone(),
partition: self.partition.clone(), partition: self.partition.clone(),
block: Block::default(), block: Block::default(),
block_buf: <u8>::new_box_slice_zeroed(self.block_buf.len()), block_buf: <[u8]>::new_box_zeroed_with_elems(self.block_buf.len()).unwrap(),
block_idx: u32::MAX, block_idx: u32::MAX,
sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed(), sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed().unwrap(),
sector: u32::MAX, sector: u32::MAX,
pos: 0, pos: 0,
verify: self.verify, verify: self.verify,
@ -288,9 +333,9 @@ impl PartitionWii {
io: reader.into_inner(), io: reader.into_inner(),
partition: partition.clone(), partition: partition.clone(),
block: Block::default(), block: Block::default(),
block_buf: <u8>::new_box_slice_zeroed(block_size as usize), block_buf: <[u8]>::new_box_zeroed_with_elems(block_size as usize)?,
block_idx: u32::MAX, block_idx: u32::MAX,
sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed(), sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed()?,
sector: u32::MAX, sector: u32::MAX,
pos: 0, pos: 0,
verify: options.validate_hashes, verify: options.validate_hashes,
@ -301,12 +346,12 @@ impl PartitionWii {
} }
} }
impl Read for PartitionWii { impl BufRead for PartitionWii {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { fn fill_buf(&mut self) -> io::Result<&[u8]> {
let part_sector = (self.pos / SECTOR_DATA_SIZE as u64) as u32; let part_sector = (self.pos / SECTOR_DATA_SIZE as u64) as u32;
let abs_sector = self.partition.data_start_sector + part_sector; let abs_sector = self.partition.data_start_sector + part_sector;
if abs_sector >= self.partition.data_end_sector { if abs_sector >= self.partition.data_end_sector {
return Ok(0); return Ok(&[]);
} }
let block_idx = let block_idx =
(abs_sector as u64 * SECTOR_SIZE as u64 / self.block_buf.len() as u64) as u32; (abs_sector as u64 * SECTOR_SIZE as u64 / self.block_buf.len() as u64) as u32;
@ -333,10 +378,20 @@ impl Read for PartitionWii {
} }
let offset = (self.pos % SECTOR_DATA_SIZE as u64) as usize; let offset = (self.pos % SECTOR_DATA_SIZE as u64) as usize;
let len = min(buf.len(), SECTOR_DATA_SIZE - offset); Ok(&self.sector_buf[HASHES_SIZE + offset..])
buf[..len] }
.copy_from_slice(&self.sector_buf[HASHES_SIZE + offset..HASHES_SIZE + offset + len]);
self.pos += len as u64; #[inline]
fn consume(&mut self, amt: usize) { self.pos += amt as u64; }
}
impl Read for PartitionWii {
#[inline]
fn read(&mut self, out: &mut [u8]) -> io::Result<usize> {
let buf = self.fill_buf()?;
let len = buf.len().min(out.len());
out[..len].copy_from_slice(&buf[..len]);
self.consume(len);
Ok(len) Ok(len)
} }
} }
@ -440,10 +495,23 @@ impl PartitionBase for PartitionWii {
Ok(meta) Ok(meta)
} }
fn open_file(&mut self, node: &Node) -> io::Result<SharedWindowedReadStream> { fn open_file(&mut self, node: Node) -> io::Result<FileStream> {
assert_eq!(node.kind(), NodeKind::File); if !node.is_file() {
self.new_window(node.offset(true), node.length()) return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Node is not a file".to_string(),
));
}
FileStream::new(self, node.offset(true), node.length())
} }
fn ideal_buffer_size(&self) -> usize { SECTOR_DATA_SIZE } fn into_open_file(self: Box<Self>, node: Node) -> io::Result<OwnedFileStream> {
if !node.is_file() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Node is not a file".to_string(),
));
}
OwnedFileStream::new(self, node.offset(true), node.length())
}
} }

View File

@ -1,4 +1,8 @@
use std::{cmp::min, fs, fs::File, io, path::Path}; use std::{
fs, io,
io::{Read, Seek},
path::Path,
};
use dyn_clone::DynClone; use dyn_clone::DynClone;
use zerocopy::transmute_ref; use zerocopy::transmute_ref;
@ -8,13 +12,22 @@ use crate::{
disc::{ disc::{
hashes::HashTable, hashes::HashTable,
wii::{WiiPartitionHeader, HASHES_SIZE, SECTOR_DATA_SIZE}, wii::{WiiPartitionHeader, HASHES_SIZE, SECTOR_DATA_SIZE},
SECTOR_SIZE, DiscHeader, PartitionHeader, PartitionKind, GCN_MAGIC, SECTOR_SIZE, WII_MAGIC,
},
io::{
aes_decrypt, aes_encrypt, split::SplitFileReader, DiscMeta, Format, KeyBytes, MagicBytes,
}, },
io::{aes_decrypt, aes_encrypt, KeyBytes, MagicBytes},
util::{lfg::LaggedFibonacci, read::read_from}, util::{lfg::LaggedFibonacci, read::read_from},
DiscHeader, DiscMeta, Error, PartitionHeader, PartitionKind, Result, ResultContext, Error, Result, ResultContext,
}; };
/// Required trait bounds for reading disc images.
pub trait DiscStream: Read + Seek + DynClone + Send + Sync {}
impl<T> DiscStream for T where T: Read + Seek + DynClone + Send + Sync + ?Sized {}
dyn_clone::clone_trait_object!(DiscStream);
/// Block I/O trait for reading disc images. /// Block I/O trait for reading disc images.
pub trait BlockIO: DynClone + Send + Sync { pub trait BlockIO: DynClone + Send + Sync {
/// Reads a block from the disc image. /// Reads a block from the disc image.
@ -78,7 +91,32 @@ pub trait BlockIO: DynClone + Send + Sync {
dyn_clone::clone_trait_object!(BlockIO); dyn_clone::clone_trait_object!(BlockIO);
/// Creates a new [`BlockIO`] instance. /// Creates a new [`BlockIO`] instance from a stream.
pub fn new(mut stream: Box<dyn DiscStream>) -> Result<Box<dyn BlockIO>> {
let io: Box<dyn BlockIO> = match detect(stream.as_mut()).context("Detecting file type")? {
Some(Format::Iso) => crate::io::iso::DiscIOISO::new(stream)?,
Some(Format::Ciso) => crate::io::ciso::DiscIOCISO::new(stream)?,
Some(Format::Gcz) => {
#[cfg(feature = "compress-zlib")]
{
crate::io::gcz::DiscIOGCZ::new(stream)?
}
#[cfg(not(feature = "compress-zlib"))]
return Err(Error::DiscFormat("GCZ support is disabled".to_string()));
}
Some(Format::Nfs) => {
return Err(Error::DiscFormat("NFS requires a filesystem path".to_string()))
}
Some(Format::Wbfs) => crate::io::wbfs::DiscIOWBFS::new(stream)?,
Some(Format::Wia | Format::Rvz) => crate::io::wia::DiscIOWIA::new(stream)?,
Some(Format::Tgc) => crate::io::tgc::DiscIOTGC::new(stream)?,
None => return Err(Error::DiscFormat("Unknown disc format".to_string())),
};
check_block_size(io.as_ref())?;
Ok(io)
}
/// Creates a new [`BlockIO`] instance from a filesystem path.
pub fn open(filename: &Path) -> Result<Box<dyn BlockIO>> { pub fn open(filename: &Path) -> Result<Box<dyn BlockIO>> {
let path_result = fs::canonicalize(filename); let path_result = fs::canonicalize(filename);
if let Err(err) = path_result { if let Err(err) = path_result {
@ -92,17 +130,19 @@ pub fn open(filename: &Path) -> Result<Box<dyn BlockIO>> {
if !meta.unwrap().is_file() { if !meta.unwrap().is_file() {
return Err(Error::DiscFormat(format!("Input is not a file: {}", filename.display()))); return Err(Error::DiscFormat(format!("Input is not a file: {}", filename.display())));
} }
let magic: MagicBytes = { let mut stream = Box::new(SplitFileReader::new(filename)?);
let mut file = let io: Box<dyn BlockIO> = match detect(stream.as_mut()).context("Detecting file type")? {
File::open(path).with_context(|| format!("Opening file {}", filename.display()))?; Some(Format::Iso) => crate::io::iso::DiscIOISO::new(stream)?,
read_from(&mut file) Some(Format::Ciso) => crate::io::ciso::DiscIOCISO::new(stream)?,
.with_context(|| format!("Reading magic bytes from {}", filename.display()))? Some(Format::Gcz) => {
};
let io: Box<dyn BlockIO> = match magic {
crate::io::ciso::CISO_MAGIC => crate::io::ciso::DiscIOCISO::new(path)?,
#[cfg(feature = "compress-zlib")] #[cfg(feature = "compress-zlib")]
crate::io::gcz::GCZ_MAGIC => crate::io::gcz::DiscIOGCZ::new(path)?, {
crate::io::nfs::NFS_MAGIC => match path.parent() { crate::io::gcz::DiscIOGCZ::new(stream)?
}
#[cfg(not(feature = "compress-zlib"))]
return Err(Error::DiscFormat("GCZ support is disabled".to_string()));
}
Some(Format::Nfs) => match path.parent() {
Some(parent) if parent.is_dir() => { Some(parent) if parent.is_dir() => {
crate::io::nfs::DiscIONFS::new(path.parent().unwrap())? crate::io::nfs::DiscIONFS::new(path.parent().unwrap())?
} }
@ -110,12 +150,46 @@ pub fn open(filename: &Path) -> Result<Box<dyn BlockIO>> {
return Err(Error::DiscFormat("Failed to locate NFS parent directory".to_string())); return Err(Error::DiscFormat("Failed to locate NFS parent directory".to_string()));
} }
}, },
crate::io::wbfs::WBFS_MAGIC => crate::io::wbfs::DiscIOWBFS::new(path)?, Some(Format::Tgc) => crate::io::tgc::DiscIOTGC::new(stream)?,
crate::io::wia::WIA_MAGIC | crate::io::wia::RVZ_MAGIC => { Some(Format::Wbfs) => crate::io::wbfs::DiscIOWBFS::new(stream)?,
crate::io::wia::DiscIOWIA::new(path)? Some(Format::Wia | Format::Rvz) => crate::io::wia::DiscIOWIA::new(stream)?,
} None => return Err(Error::DiscFormat("Unknown disc format".to_string())),
_ => crate::io::iso::DiscIOISO::new(path)?,
}; };
check_block_size(io.as_ref())?;
Ok(io)
}
pub const CISO_MAGIC: MagicBytes = *b"CISO";
pub const GCZ_MAGIC: MagicBytes = [0x01, 0xC0, 0x0B, 0xB1];
pub const NFS_MAGIC: MagicBytes = *b"EGGS";
pub const TGC_MAGIC: MagicBytes = [0xae, 0x0f, 0x38, 0xa2];
pub const WBFS_MAGIC: MagicBytes = *b"WBFS";
pub const WIA_MAGIC: MagicBytes = *b"WIA\x01";
pub const RVZ_MAGIC: MagicBytes = *b"RVZ\x01";
pub fn detect<R: Read + ?Sized>(stream: &mut R) -> io::Result<Option<Format>> {
let data: [u8; 0x20] = match read_from(stream) {
Ok(magic) => magic,
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(None),
Err(e) => return Err(e),
};
let out = match *array_ref!(data, 0, 4) {
CISO_MAGIC => Some(Format::Ciso),
GCZ_MAGIC => Some(Format::Gcz),
NFS_MAGIC => Some(Format::Nfs),
TGC_MAGIC => Some(Format::Tgc),
WBFS_MAGIC => Some(Format::Wbfs),
WIA_MAGIC => Some(Format::Wia),
RVZ_MAGIC => Some(Format::Rvz),
_ if *array_ref!(data, 0x18, 4) == WII_MAGIC || *array_ref!(data, 0x1C, 4) == GCN_MAGIC => {
Some(Format::Iso)
}
_ => None,
};
Ok(out)
}
fn check_block_size(io: &dyn BlockIO) -> Result<()> {
if io.block_size_internal() < SECTOR_SIZE as u32 if io.block_size_internal() < SECTOR_SIZE as u32
&& SECTOR_SIZE as u32 % io.block_size_internal() != 0 && SECTOR_SIZE as u32 % io.block_size_internal() != 0
{ {
@ -132,7 +206,7 @@ pub fn open(filename: &Path) -> Result<Box<dyn BlockIO>> {
SECTOR_SIZE SECTOR_SIZE
))); )));
} }
Ok(io) Ok(())
} }
/// Wii partition information. /// Wii partition information.
@ -300,23 +374,19 @@ fn generate_junk(
partition: Option<&PartitionInfo>, partition: Option<&PartitionInfo>,
disc_header: &DiscHeader, disc_header: &DiscHeader,
) { ) {
let (mut pos, mut offset) = if partition.is_some() { let (pos, offset) = if partition.is_some() {
(sector as u64 * SECTOR_DATA_SIZE as u64, HASHES_SIZE) (sector as u64 * SECTOR_DATA_SIZE as u64, HASHES_SIZE)
} else { } else {
(sector as u64 * SECTOR_SIZE as u64, 0) (sector as u64 * SECTOR_SIZE as u64, 0)
}; };
out[..offset].fill(0); out[..offset].fill(0);
while offset < SECTOR_SIZE {
// The LFG spans a single sector of the decrypted data,
// so we may need to initialize it multiple times
let mut lfg = LaggedFibonacci::default(); let mut lfg = LaggedFibonacci::default();
lfg.init_with_seed(*array_ref![disc_header.game_id, 0, 4], disc_header.disc_num, pos); lfg.fill_sector_chunked(
let sector_end = (pos + SECTOR_SIZE as u64) & !(SECTOR_SIZE as u64 - 1); &mut out[offset..],
let len = min(SECTOR_SIZE - offset, (sector_end - pos) as usize); *array_ref![disc_header.game_id, 0, 4],
lfg.fill(&mut out[offset..offset + len]); disc_header.disc_num,
pos += len as u64; pos,
offset += len; );
}
} }
fn rebuild_hash_block(out: &mut [u8; SECTOR_SIZE], part_sector: u32, partition: &PartitionInfo) { fn rebuild_hash_block(out: &mut [u8; SECTOR_SIZE], part_sector: u32, partition: &PartitionInfo) {

View File

@ -2,17 +2,15 @@ use std::{
io, io,
io::{Read, Seek, SeekFrom}, io::{Read, Seek, SeekFrom},
mem::size_of, mem::size_of,
path::Path,
}; };
use zerocopy::{little_endian::*, AsBytes, FromBytes, FromZeroes}; use zerocopy::{little_endian::*, FromBytes, Immutable, IntoBytes, KnownLayout};
use crate::{ use crate::{
disc::SECTOR_SIZE, disc::SECTOR_SIZE,
io::{ io::{
block::{Block, BlockIO, PartitionInfo}, block::{Block, BlockIO, DiscStream, PartitionInfo, CISO_MAGIC},
nkit::NKitHeader, nkit::NKitHeader,
split::SplitFileReader,
Format, MagicBytes, Format, MagicBytes,
}, },
static_assert, static_assert,
@ -20,11 +18,10 @@ use crate::{
DiscMeta, Error, Result, ResultContext, DiscMeta, Error, Result, ResultContext,
}; };
pub const CISO_MAGIC: MagicBytes = *b"CISO";
pub const CISO_MAP_SIZE: usize = SECTOR_SIZE - 8; pub const CISO_MAP_SIZE: usize = SECTOR_SIZE - 8;
/// CISO header (little endian) /// CISO header (little endian)
#[derive(Clone, Debug, PartialEq, FromBytes, FromZeroes, AsBytes)] #[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(4))] #[repr(C, align(4))]
struct CISOHeader { struct CISOHeader {
magic: MagicBytes, magic: MagicBytes,
@ -36,18 +33,17 @@ static_assert!(size_of::<CISOHeader>() == SECTOR_SIZE);
#[derive(Clone)] #[derive(Clone)]
pub struct DiscIOCISO { pub struct DiscIOCISO {
inner: SplitFileReader, inner: Box<dyn DiscStream>,
header: CISOHeader, header: CISOHeader,
block_map: [u16; CISO_MAP_SIZE], block_map: [u16; CISO_MAP_SIZE],
nkit_header: Option<NKitHeader>, nkit_header: Option<NKitHeader>,
} }
impl DiscIOCISO { impl DiscIOCISO {
pub fn new(filename: &Path) -> Result<Box<Self>> { pub fn new(mut inner: Box<dyn DiscStream>) -> Result<Box<Self>> {
let mut inner = SplitFileReader::new(filename)?;
// Read header // Read header
let header: CISOHeader = read_from(&mut inner).context("Reading CISO header")?; inner.seek(SeekFrom::Start(0)).context("Seeking to start")?;
let header: CISOHeader = read_from(inner.as_mut()).context("Reading CISO header")?;
if header.magic != CISO_MAGIC { if header.magic != CISO_MAGIC {
return Err(Error::DiscFormat("Invalid CISO magic".to_string())); return Err(Error::DiscFormat("Invalid CISO magic".to_string()));
} }
@ -64,18 +60,18 @@ impl DiscIOCISO {
} }
} }
let file_size = SECTOR_SIZE as u64 + block as u64 * header.block_size.get() as u64; let file_size = SECTOR_SIZE as u64 + block as u64 * header.block_size.get() as u64;
if file_size > inner.len() { let len = inner.seek(SeekFrom::End(0)).context("Determining stream length")?;
if file_size > len {
return Err(Error::DiscFormat(format!( return Err(Error::DiscFormat(format!(
"CISO file size mismatch: expected at least {} bytes, got {}", "CISO file size mismatch: expected at least {} bytes, got {}",
file_size, file_size, len
inner.len()
))); )));
} }
// Read NKit header if present (after CISO data) // Read NKit header if present (after CISO data)
let nkit_header = if inner.len() > file_size + 4 { let nkit_header = if len > file_size + 4 {
inner.seek(SeekFrom::Start(file_size)).context("Seeking to NKit header")?; inner.seek(SeekFrom::Start(file_size)).context("Seeking to NKit header")?;
NKitHeader::try_read_from(&mut inner, header.block_size.get(), true) NKitHeader::try_read_from(inner.as_mut(), header.block_size.get(), true)
} else { } else {
None None
}; };

View File

@ -2,18 +2,16 @@ use std::{
io, io,
io::{Read, Seek, SeekFrom}, io::{Read, Seek, SeekFrom},
mem::size_of, mem::size_of,
path::Path,
}; };
use adler::adler32_slice; use adler::adler32_slice;
use miniz_oxide::{inflate, inflate::core::inflate_flags}; use miniz_oxide::{inflate, inflate::core::inflate_flags};
use zerocopy::{little_endian::*, AsBytes, FromBytes, FromZeroes}; use zerocopy::{little_endian::*, FromBytes, FromZeros, Immutable, IntoBytes, KnownLayout};
use zstd::zstd_safe::WriteBuf; use zstd::zstd_safe::WriteBuf;
use crate::{ use crate::{
io::{ io::{
block::{Block, BlockIO}, block::{Block, BlockIO, DiscStream, GCZ_MAGIC},
split::SplitFileReader,
MagicBytes, MagicBytes,
}, },
static_assert, static_assert,
@ -21,10 +19,8 @@ use crate::{
Compression, DiscMeta, Error, Format, PartitionInfo, Result, ResultContext, Compression, DiscMeta, Error, Format, PartitionInfo, Result, ResultContext,
}; };
pub const GCZ_MAGIC: MagicBytes = [0x01, 0xC0, 0x0B, 0xB1];
/// GCZ header (little endian) /// GCZ header (little endian)
#[derive(Clone, Debug, PartialEq, FromBytes, FromZeroes, AsBytes)] #[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(4))] #[repr(C, align(4))]
struct GCZHeader { struct GCZHeader {
magic: MagicBytes, magic: MagicBytes,
@ -38,7 +34,7 @@ struct GCZHeader {
static_assert!(size_of::<GCZHeader>() == 32); static_assert!(size_of::<GCZHeader>() == 32);
pub struct DiscIOGCZ { pub struct DiscIOGCZ {
inner: SplitFileReader, inner: Box<dyn DiscStream>,
header: GCZHeader, header: GCZHeader,
block_map: Box<[U64]>, block_map: Box<[U64]>,
block_hashes: Box<[U32]>, block_hashes: Box<[U32]>,
@ -53,32 +49,31 @@ impl Clone for DiscIOGCZ {
header: self.header.clone(), header: self.header.clone(),
block_map: self.block_map.clone(), block_map: self.block_map.clone(),
block_hashes: self.block_hashes.clone(), block_hashes: self.block_hashes.clone(),
block_buf: <u8>::new_box_slice_zeroed(self.block_buf.len()), block_buf: <[u8]>::new_box_zeroed_with_elems(self.block_buf.len()).unwrap(),
data_offset: self.data_offset, data_offset: self.data_offset,
} }
} }
} }
impl DiscIOGCZ { impl DiscIOGCZ {
pub fn new(filename: &Path) -> Result<Box<Self>> { pub fn new(mut inner: Box<dyn DiscStream>) -> Result<Box<Self>> {
let mut inner = SplitFileReader::new(filename)?;
// Read header // Read header
let header: GCZHeader = read_from(&mut inner).context("Reading GCZ header")?; inner.seek(SeekFrom::Start(0)).context("Seeking to start")?;
let header: GCZHeader = read_from(inner.as_mut()).context("Reading GCZ header")?;
if header.magic != GCZ_MAGIC { if header.magic != GCZ_MAGIC {
return Err(Error::DiscFormat("Invalid GCZ magic".to_string())); return Err(Error::DiscFormat("Invalid GCZ magic".to_string()));
} }
// Read block map and hashes // Read block map and hashes
let block_count = header.block_count.get(); let block_count = header.block_count.get();
let block_map = let block_map = read_box_slice(inner.as_mut(), block_count as usize)
read_box_slice(&mut inner, block_count as usize).context("Reading GCZ block map")?; .context("Reading GCZ block map")?;
let block_hashes = let block_hashes = read_box_slice(inner.as_mut(), block_count as usize)
read_box_slice(&mut inner, block_count as usize).context("Reading GCZ block hashes")?; .context("Reading GCZ block hashes")?;
// header + block_count * (u64 + u32) // header + block_count * (u64 + u32)
let data_offset = size_of::<GCZHeader>() as u64 + block_count as u64 * 12; let data_offset = size_of::<GCZHeader>() as u64 + block_count as u64 * 12;
let block_buf = <u8>::new_box_slice_zeroed(header.block_size.get() as usize); let block_buf = <[u8]>::new_box_zeroed_with_elems(header.block_size.get() as usize)?;
Ok(Box::new(Self { inner, header, block_map, block_hashes, block_buf, data_offset })) Ok(Box::new(Self { inner, header, block_map, block_hashes, block_buf, data_offset }))
} }
} }

View File

@ -1,28 +1,28 @@
use std::{ use std::{
io, io,
io::{Read, Seek, SeekFrom}, io::{Read, Seek, SeekFrom},
path::Path,
}; };
use crate::{ use crate::{
disc::SECTOR_SIZE, disc::SECTOR_SIZE,
io::{ io::{
block::{Block, BlockIO, PartitionInfo}, block::{Block, BlockIO, DiscStream, PartitionInfo},
split::SplitFileReader,
Format, Format,
}, },
DiscMeta, Result, DiscMeta, Result, ResultContext,
}; };
#[derive(Clone)] #[derive(Clone)]
pub struct DiscIOISO { pub struct DiscIOISO {
inner: SplitFileReader, inner: Box<dyn DiscStream>,
stream_len: u64,
} }
impl DiscIOISO { impl DiscIOISO {
pub fn new(filename: &Path) -> Result<Box<Self>> { pub fn new(mut inner: Box<dyn DiscStream>) -> Result<Box<Self>> {
let inner = SplitFileReader::new(filename)?; let stream_len = inner.seek(SeekFrom::End(0)).context("Determining stream length")?;
Ok(Box::new(Self { inner })) inner.seek(SeekFrom::Start(0)).context("Seeking to start")?;
Ok(Box::new(Self { inner, stream_len }))
} }
} }
@ -34,16 +34,15 @@ impl BlockIO for DiscIOISO {
_partition: Option<&PartitionInfo>, _partition: Option<&PartitionInfo>,
) -> io::Result<Block> { ) -> io::Result<Block> {
let offset = block as u64 * SECTOR_SIZE as u64; let offset = block as u64 * SECTOR_SIZE as u64;
let total_size = self.inner.len(); if offset >= self.stream_len {
if offset >= total_size {
// End of file // End of file
return Ok(Block::Zero); return Ok(Block::Zero);
} }
self.inner.seek(SeekFrom::Start(offset))?; self.inner.seek(SeekFrom::Start(offset))?;
if offset + SECTOR_SIZE as u64 > total_size { if offset + SECTOR_SIZE as u64 > self.stream_len {
// If the last block is not a full sector, fill the rest with zeroes // If the last block is not a full sector, fill the rest with zeroes
let read = (total_size - offset) as usize; let read = (self.stream_len - offset) as usize;
self.inner.read_exact(&mut out[..read])?; self.inner.read_exact(&mut out[..read])?;
out[read..].fill(0); out[read..].fill(0);
} else { } else {
@ -58,7 +57,7 @@ impl BlockIO for DiscIOISO {
DiscMeta { DiscMeta {
format: Format::Iso, format: Format::Iso,
lossless: true, lossless: true,
disc_size: Some(self.inner.len()), disc_size: Some(self.stream_len),
..Default::default() ..Default::default()
} }
} }

View File

@ -10,17 +10,18 @@ pub(crate) mod iso;
pub(crate) mod nfs; pub(crate) mod nfs;
pub(crate) mod nkit; pub(crate) mod nkit;
pub(crate) mod split; pub(crate) mod split;
pub(crate) mod tgc;
pub(crate) mod wbfs; pub(crate) mod wbfs;
pub(crate) mod wia; pub(crate) mod wia;
/// SHA-1 hash bytes /// SHA-1 hash bytes
pub(crate) type HashBytes = [u8; 20]; pub type HashBytes = [u8; 20];
/// AES key bytes /// AES key bytes
pub(crate) type KeyBytes = [u8; 16]; pub type KeyBytes = [u8; 16];
/// Magic bytes /// Magic bytes
pub(crate) type MagicBytes = [u8; 4]; pub type MagicBytes = [u8; 4];
/// The disc file format. /// The disc file format.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
@ -40,9 +41,12 @@ pub enum Format {
Wbfs, Wbfs,
/// WIA /// WIA
Wia, Wia,
/// TGC
Tgc,
} }
impl fmt::Display for Format { impl fmt::Display for Format {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
Format::Iso => write!(f, "ISO"), Format::Iso => write!(f, "ISO"),
@ -52,6 +56,7 @@ impl fmt::Display for Format {
Format::Rvz => write!(f, "RVZ"), Format::Rvz => write!(f, "RVZ"),
Format::Wbfs => write!(f, "WBFS"), Format::Wbfs => write!(f, "WBFS"),
Format::Wia => write!(f, "WIA"), Format::Wia => write!(f, "WIA"),
Format::Tgc => write!(f, "TGC"),
} }
} }
} }
@ -77,6 +82,7 @@ pub enum Compression {
} }
impl fmt::Display for Compression { impl fmt::Display for Compression {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
Compression::None => write!(f, "None"), Compression::None => write!(f, "None"),

View File

@ -6,13 +6,13 @@ use std::{
path::{Component, Path, PathBuf}, path::{Component, Path, PathBuf},
}; };
use zerocopy::{big_endian::U32, AsBytes, FromBytes, FromZeroes}; use zerocopy::{big_endian::U32, FromBytes, FromZeros, Immutable, IntoBytes, KnownLayout};
use crate::{ use crate::{
disc::SECTOR_SIZE, disc::SECTOR_SIZE,
io::{ io::{
aes_decrypt, aes_decrypt,
block::{Block, BlockIO, PartitionInfo}, block::{Block, BlockIO, PartitionInfo, NFS_MAGIC},
split::SplitFileReader, split::SplitFileReader,
Format, KeyBytes, MagicBytes, Format, KeyBytes, MagicBytes,
}, },
@ -21,17 +21,16 @@ use crate::{
DiscMeta, Error, Result, ResultContext, DiscMeta, Error, Result, ResultContext,
}; };
pub const NFS_MAGIC: MagicBytes = *b"EGGS";
pub const NFS_END_MAGIC: MagicBytes = *b"SGGE"; pub const NFS_END_MAGIC: MagicBytes = *b"SGGE";
#[derive(Clone, Debug, PartialEq, FromBytes, FromZeroes, AsBytes)] #[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(4))] #[repr(C, align(4))]
struct LBARange { struct LBARange {
start_sector: U32, start_sector: U32,
num_sectors: U32, num_sectors: U32,
} }
#[derive(Clone, Debug, PartialEq, FromBytes, FromZeroes, AsBytes)] #[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(4))] #[repr(C, align(4))]
struct NFSHeader { struct NFSHeader {
magic: MagicBytes, magic: MagicBytes,
@ -192,7 +191,7 @@ impl DiscIONFS {
let resolved_path = key_path.unwrap(); let resolved_path = key_path.unwrap();
File::open(resolved_path.as_path()) File::open(resolved_path.as_path())
.map_err(|v| Error::Io(format!("Failed to open {}", resolved_path.display()), v))? .map_err(|v| Error::Io(format!("Failed to open {}", resolved_path.display()), v))?
.read(&mut self.key) .read_exact(&mut self.key)
.map_err(|v| Error::Io(format!("Failed to read {}", resolved_path.display()), v))?; .map_err(|v| Error::Io(format!("Failed to read {}", resolved_path.display()), v))?;
} }

View File

@ -136,8 +136,6 @@ impl Seek for SplitFileReader {
if split.contains(self.pos) { if split.contains(self.pos) {
// Seek within the open file // Seek within the open file
split.inner.seek(SeekFrom::Start(self.pos - split.begin))?; split.inner.seek(SeekFrom::Start(self.pos - split.begin))?;
} else {
self.open_file = None;
} }
} }
Ok(self.pos) Ok(self.pos)

153
nod/src/io/tgc.rs Normal file
View File

@ -0,0 +1,153 @@
use std::{
io,
io::{Read, Seek, SeekFrom},
mem::size_of,
};
use zerocopy::{big_endian::U32, FromBytes, Immutable, IntoBytes, KnownLayout};
use crate::{
disc::SECTOR_SIZE,
io::{
block::{Block, BlockIO, DiscStream, PartitionInfo, TGC_MAGIC},
Format, MagicBytes,
},
util::read::{read_box_slice, read_from},
DiscHeader, DiscMeta, Error, Node, PartitionHeader, Result, ResultContext,
};
/// TGC header (big endian)
#[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(4))]
struct TGCHeader {
/// Magic bytes
magic: MagicBytes,
/// TGC version
version: U32,
/// Offset to the start of the GCM header
header_offset: U32,
/// Size of the GCM header
header_size: U32,
/// Offset to the FST
fst_offset: U32,
/// Size of the FST
fst_size: U32,
/// Maximum size of the FST across discs
fst_max_size: U32,
/// Offset to the DOL
dol_offset: U32,
/// Size of the DOL
dol_size: U32,
/// Offset to user data
user_offset: U32,
/// Size of user data
user_size: U32,
/// Offset to the banner
banner_offset: U32,
/// Size of the banner
banner_size: U32,
/// Original user data offset in the GCM
gcm_user_offset: U32,
}
#[derive(Clone)]
pub struct DiscIOTGC {
inner: Box<dyn DiscStream>,
stream_len: u64,
header: TGCHeader,
fst: Box<[u8]>,
}
impl DiscIOTGC {
pub fn new(mut inner: Box<dyn DiscStream>) -> Result<Box<Self>> {
let stream_len = inner.seek(SeekFrom::End(0)).context("Determining stream length")?;
inner.seek(SeekFrom::Start(0)).context("Seeking to start")?;
// Read header
let header: TGCHeader = read_from(inner.as_mut()).context("Reading TGC header")?;
if header.magic != TGC_MAGIC {
return Err(Error::DiscFormat("Invalid TGC magic".to_string()));
}
// Read FST and adjust offsets
inner
.seek(SeekFrom::Start(header.fst_offset.get() as u64))
.context("Seeking to TGC FST")?;
let mut fst = read_box_slice(inner.as_mut(), header.fst_size.get() as usize)
.context("Reading TGC FST")?;
let (root_node, _) = Node::ref_from_prefix(&fst)
.map_err(|_| Error::DiscFormat("Invalid TGC FST".to_string()))?;
let node_count = root_node.length() as usize;
let (nodes, _) = <[Node]>::mut_from_prefix_with_elems(&mut fst, node_count)
.map_err(|_| Error::DiscFormat("Invalid TGC FST".to_string()))?;
for node in nodes {
if node.is_file() {
node.offset = node.offset - header.gcm_user_offset
+ (header.user_offset - header.header_offset);
}
}
Ok(Box::new(Self { inner, stream_len, header, fst }))
}
}
impl BlockIO for DiscIOTGC {
fn read_block_internal(
&mut self,
out: &mut [u8],
block: u32,
_partition: Option<&PartitionInfo>,
) -> io::Result<Block> {
let offset = self.header.header_offset.get() as u64 + block as u64 * SECTOR_SIZE as u64;
if offset >= self.stream_len {
// End of file
return Ok(Block::Zero);
}
self.inner.seek(SeekFrom::Start(offset))?;
if offset + SECTOR_SIZE as u64 > self.stream_len {
// If the last block is not a full sector, fill the rest with zeroes
let read = (self.stream_len - offset) as usize;
self.inner.read_exact(&mut out[..read])?;
out[read..].fill(0);
} else {
self.inner.read_exact(out)?;
}
// Adjust internal GCM header
if block == 0 {
let partition_header = PartitionHeader::mut_from_bytes(
&mut out[size_of::<DiscHeader>()
..size_of::<DiscHeader>() + size_of::<PartitionHeader>()],
)
.unwrap();
partition_header.dol_offset = self.header.dol_offset - self.header.header_offset;
partition_header.fst_offset = self.header.fst_offset - self.header.header_offset;
}
// Copy modified FST to output
if offset + out.len() as u64 > self.header.fst_offset.get() as u64
&& offset < self.header.fst_offset.get() as u64 + self.header.fst_size.get() as u64
{
let out_offset = (self.header.fst_offset.get() as u64).saturating_sub(offset) as usize;
let fst_offset = offset.saturating_sub(self.header.fst_offset.get() as u64) as usize;
let copy_len =
(out.len() - out_offset).min(self.header.fst_size.get() as usize - fst_offset);
out[out_offset..out_offset + copy_len]
.copy_from_slice(&self.fst[fst_offset..fst_offset + copy_len]);
}
Ok(Block::Raw)
}
fn block_size_internal(&self) -> u32 { SECTOR_SIZE as u32 }
fn meta(&self) -> DiscMeta {
DiscMeta {
format: Format::Tgc,
lossless: true,
disc_size: Some(self.stream_len - self.header.header_offset.get() as u64),
..Default::default()
}
}
}

View File

@ -2,25 +2,21 @@ use std::{
io, io,
io::{Read, Seek, SeekFrom}, io::{Read, Seek, SeekFrom},
mem::size_of, mem::size_of,
path::Path,
}; };
use zerocopy::{big_endian::*, AsBytes, FromBytes, FromZeroes}; use zerocopy::{big_endian::*, FromBytes, Immutable, IntoBytes, KnownLayout};
use crate::{ use crate::{
io::{ io::{
block::{Block, BlockIO, PartitionInfo}, block::{Block, BlockIO, DiscStream, PartitionInfo, WBFS_MAGIC},
nkit::NKitHeader, nkit::NKitHeader,
split::SplitFileReader,
DiscMeta, Format, MagicBytes, DiscMeta, Format, MagicBytes,
}, },
util::read::{read_box_slice, read_from}, util::read::{read_box_slice, read_from},
Error, Result, ResultContext, Error, Result, ResultContext,
}; };
pub const WBFS_MAGIC: MagicBytes = *b"WBFS"; #[derive(Debug, Clone, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[derive(Debug, Clone, PartialEq, FromBytes, FromZeroes, AsBytes)]
#[repr(C, align(4))] #[repr(C, align(4))]
struct WBFSHeader { struct WBFSHeader {
magic: MagicBytes, magic: MagicBytes,
@ -35,18 +31,6 @@ impl WBFSHeader {
fn block_size(&self) -> u32 { 1 << self.block_size_shift } fn block_size(&self) -> u32 { 1 << self.block_size_shift }
// fn align_lba(&self, x: u32) -> u32 { (x + self.sector_size() - 1) & !(self.sector_size() - 1) }
//
// fn num_wii_sectors(&self) -> u32 {
// (self.num_sectors.get() / SECTOR_SIZE as u32) * self.sector_size()
// }
//
// fn max_wii_sectors(&self) -> u32 { NUM_WII_SECTORS }
//
// fn num_wbfs_sectors(&self) -> u32 {
// self.num_wii_sectors() >> (self.wbfs_sector_size_shift - 15)
// }
fn max_blocks(&self) -> u32 { NUM_WII_SECTORS >> (self.block_size_shift - 15) } fn max_blocks(&self) -> u32 { NUM_WII_SECTORS >> (self.block_size_shift - 15) }
} }
@ -55,7 +39,7 @@ const NUM_WII_SECTORS: u32 = 143432 * 2; // Double layer discs
#[derive(Clone)] #[derive(Clone)]
pub struct DiscIOWBFS { pub struct DiscIOWBFS {
inner: SplitFileReader, inner: Box<dyn DiscStream>,
/// WBFS header /// WBFS header
header: WBFSHeader, header: WBFSHeader,
/// Map of Wii LBAs to WBFS LBAs /// Map of Wii LBAs to WBFS LBAs
@ -65,14 +49,13 @@ pub struct DiscIOWBFS {
} }
impl DiscIOWBFS { impl DiscIOWBFS {
pub fn new(filename: &Path) -> Result<Box<Self>> { pub fn new(mut inner: Box<dyn DiscStream>) -> Result<Box<Self>> {
let mut inner = SplitFileReader::new(filename)?; inner.seek(SeekFrom::Start(0)).context("Seeking to start")?;
let header: WBFSHeader = read_from(inner.as_mut()).context("Reading WBFS header")?;
let header: WBFSHeader = read_from(&mut inner).context("Reading WBFS header")?;
if header.magic != WBFS_MAGIC { if header.magic != WBFS_MAGIC {
return Err(Error::DiscFormat("Invalid WBFS magic".to_string())); return Err(Error::DiscFormat("Invalid WBFS magic".to_string()));
} }
let file_len = inner.len(); let file_len = inner.seek(SeekFrom::End(0)).context("Determining stream length")?;
let expected_file_len = header.num_sectors.get() as u64 * header.sector_size() as u64; let expected_file_len = header.num_sectors.get() as u64 * header.sector_size() as u64;
if file_len != expected_file_len { if file_len != expected_file_len {
return Err(Error::DiscFormat(format!( return Err(Error::DiscFormat(format!(
@ -81,8 +64,11 @@ impl DiscIOWBFS {
))); )));
} }
inner
.seek(SeekFrom::Start(size_of::<WBFSHeader>() as u64))
.context("Seeking to WBFS disc table")?;
let disc_table: Box<[u8]> = let disc_table: Box<[u8]> =
read_box_slice(&mut inner, header.sector_size() as usize - size_of::<WBFSHeader>()) read_box_slice(inner.as_mut(), header.sector_size() as usize - size_of::<WBFSHeader>())
.context("Reading WBFS disc table")?; .context("Reading WBFS disc table")?;
if disc_table[0] != 1 { if disc_table[0] != 1 {
return Err(Error::DiscFormat("WBFS doesn't contain a disc".to_string())); return Err(Error::DiscFormat("WBFS doesn't contain a disc".to_string()));
@ -95,12 +81,12 @@ impl DiscIOWBFS {
inner inner
.seek(SeekFrom::Start(header.sector_size() as u64 + DISC_HEADER_SIZE as u64)) .seek(SeekFrom::Start(header.sector_size() as u64 + DISC_HEADER_SIZE as u64))
.context("Seeking to WBFS LBA table")?; // Skip header .context("Seeking to WBFS LBA table")?; // Skip header
let block_map: Box<[U16]> = read_box_slice(&mut inner, header.max_blocks() as usize) let block_map: Box<[U16]> = read_box_slice(inner.as_mut(), header.max_blocks() as usize)
.context("Reading WBFS LBA table")?; .context("Reading WBFS LBA table")?;
// Read NKit header if present (always at 0x10000) // Read NKit header if present (always at 0x10000)
inner.seek(SeekFrom::Start(0x10000)).context("Seeking to NKit header")?; inner.seek(SeekFrom::Start(0x10000)).context("Seeking to NKit header")?;
let nkit_header = NKitHeader::try_read_from(&mut inner, header.block_size(), true); let nkit_header = NKitHeader::try_read_from(inner.as_mut(), header.block_size(), true);
Ok(Box::new(Self { inner, header, block_map, nkit_header })) Ok(Box::new(Self { inner, header, block_map, nkit_header }))
} }

View File

@ -2,10 +2,9 @@ use std::{
io, io,
io::{Read, Seek, SeekFrom}, io::{Read, Seek, SeekFrom},
mem::size_of, mem::size_of,
path::Path,
}; };
use zerocopy::{big_endian::*, AsBytes, FromBytes, FromZeroes}; use zerocopy::{big_endian::*, FromBytes, Immutable, IntoBytes, KnownLayout};
use crate::{ use crate::{
disc::{ disc::{
@ -14,14 +13,12 @@ use crate::{
SECTOR_SIZE, SECTOR_SIZE,
}, },
io::{ io::{
block::{Block, BlockIO, PartitionInfo}, block::{Block, BlockIO, DiscStream, PartitionInfo, RVZ_MAGIC, WIA_MAGIC},
nkit::NKitHeader, nkit::NKitHeader,
split::SplitFileReader,
Compression, Format, HashBytes, KeyBytes, MagicBytes, Compression, Format, HashBytes, KeyBytes, MagicBytes,
}, },
static_assert, static_assert,
util::{ util::{
compress::{lzma2_props_decode, lzma_props_decode, new_lzma2_decoder, new_lzma_decoder},
lfg::LaggedFibonacci, lfg::LaggedFibonacci,
read::{read_box_slice, read_from, read_u16_be, read_vec}, read::{read_box_slice, read_from, read_u16_be, read_vec},
take_seek::TakeSeekExt, take_seek::TakeSeekExt,
@ -29,12 +26,9 @@ use crate::{
DiscMeta, Error, Result, ResultContext, DiscMeta, Error, Result, ResultContext,
}; };
pub const WIA_MAGIC: MagicBytes = *b"WIA\x01";
pub const RVZ_MAGIC: MagicBytes = *b"RVZ\x01";
/// This struct is stored at offset 0x0 and is 0x48 bytes long. The wit source code says its format /// This struct is stored at offset 0x0 and is 0x48 bytes long. The wit source code says its format
/// will never be changed. /// will never be changed.
#[derive(Clone, Debug, PartialEq, FromBytes, FromZeroes, AsBytes)] #[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(4))] #[repr(C, align(4))]
pub struct WIAFileHeader { pub struct WIAFileHeader {
pub magic: MagicBytes, pub magic: MagicBytes,
@ -148,7 +142,7 @@ impl TryFrom<u32> for WIACompression {
const DISC_HEAD_SIZE: usize = 0x80; const DISC_HEAD_SIZE: usize = 0x80;
/// This struct is stored at offset 0x48, immediately after [WIAFileHeader]. /// This struct is stored at offset 0x48, immediately after [WIAFileHeader].
#[derive(Clone, Debug, PartialEq, FromBytes, FromZeroes, AsBytes)] #[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(4))] #[repr(C, align(4))]
pub struct WIADisc { pub struct WIADisc {
/// The disc type. (1 = GameCube, 2 = Wii) /// The disc type. (1 = GameCube, 2 = Wii)
@ -242,7 +236,7 @@ impl WIADisc {
} }
} }
#[derive(Clone, Debug, PartialEq, FromBytes, FromZeroes, AsBytes)] #[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(4))] #[repr(C, align(4))]
pub struct WIAPartitionData { pub struct WIAPartitionData {
/// The sector on the disc at which this data starts. /// The sector on the disc at which this data starts.
@ -277,7 +271,7 @@ impl WIAPartitionData {
/// the reading program must first recalculate the hashes as done when creating a Wii disc image /// the reading program must first recalculate the hashes as done when creating a Wii disc image
/// from scratch (see <https://wiibrew.org/wiki/Wii_Disc>), and must then apply the hash exceptions /// from scratch (see <https://wiibrew.org/wiki/Wii_Disc>), and must then apply the hash exceptions
/// which are stored along with the data (see the [WIAExceptionList] section). /// which are stored along with the data (see the [WIAExceptionList] section).
#[derive(Clone, Debug, PartialEq, FromBytes, FromZeroes, AsBytes)] #[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(4))] #[repr(C, align(4))]
pub struct WIAPartition { pub struct WIAPartition {
/// The title key for this partition (128-bit AES), which can be used for re-encrypting the /// The title key for this partition (128-bit AES), which can be used for re-encrypting the
@ -304,7 +298,7 @@ static_assert!(size_of::<WIAPartition>() == 0x30);
/// should be read from [WIADisc] instead.) This should be handled by rounding the offset down to /// should be read from [WIADisc] instead.) This should be handled by rounding the offset down to
/// the previous multiple of 0x8000 (and adding the equivalent amount to the size so that the end /// the previous multiple of 0x8000 (and adding the equivalent amount to the size so that the end
/// offset stays the same), not by special casing the first [WIARawData]. /// offset stays the same), not by special casing the first [WIARawData].
#[derive(Clone, Debug, PartialEq, FromBytes, FromZeroes, AsBytes)] #[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(4))] #[repr(C, align(4))]
pub struct WIARawData { pub struct WIARawData {
/// The offset on the disc at which this data starts. /// The offset on the disc at which this data starts.
@ -342,7 +336,7 @@ impl WIARawData {
/// counting any [WIAExceptionList] structs. However, the last [WIAGroup] of a [WIAPartitionData] /// counting any [WIAExceptionList] structs. However, the last [WIAGroup] of a [WIAPartitionData]
/// or [WIARawData] contains less data than that if `num_sectors * 0x8000` (for [WIAPartitionData]) /// or [WIARawData] contains less data than that if `num_sectors * 0x8000` (for [WIAPartitionData])
/// or `raw_data_size` (for [WIARawData]) is not evenly divisible by `chunk_size`. /// or `raw_data_size` (for [WIARawData]) is not evenly divisible by `chunk_size`.
#[derive(Clone, Debug, PartialEq, FromBytes, FromZeroes, AsBytes)] #[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(4))] #[repr(C, align(4))]
pub struct WIAGroup { pub struct WIAGroup {
/// The offset in the file where the compressed data is stored. /// The offset in the file where the compressed data is stored.
@ -357,7 +351,7 @@ pub struct WIAGroup {
/// Compared to [WIAGroup], [RVZGroup] changes the meaning of the most significant bit of /// Compared to [WIAGroup], [RVZGroup] changes the meaning of the most significant bit of
/// [data_size](Self::data_size) and adds one additional attribute. /// [data_size](Self::data_size) and adds one additional attribute.
#[derive(Clone, Debug, PartialEq, FromBytes, FromZeroes, AsBytes)] #[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(4))] #[repr(C, align(4))]
pub struct RVZGroup { pub struct RVZGroup {
/// The offset in the file where the compressed data is stored, divided by 4. /// The offset in the file where the compressed data is stored, divided by 4.
@ -404,7 +398,7 @@ impl From<&WIAGroup> for RVZGroup {
/// write [WIAException] structs for a padding area which is 32 bytes long, it writes one which /// write [WIAException] structs for a padding area which is 32 bytes long, it writes one which
/// covers the first 20 bytes of the padding area and one which covers the last 20 bytes of the /// covers the first 20 bytes of the padding area and one which covers the last 20 bytes of the
/// padding area, generating 12 bytes of overlap between the [WIAException] structs. /// padding area, generating 12 bytes of overlap between the [WIAException] structs.
#[derive(Clone, Debug, PartialEq, FromBytes, FromZeroes, AsBytes)] #[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(2))] #[repr(C, align(2))]
pub struct WIAException { pub struct WIAException {
/// The offset among the hashes. The offsets 0x0000-0x0400 here map to the offsets 0x0000-0x0400 /// The offset among the hashes. The offsets 0x0000-0x0400 here map to the offsets 0x0000-0x0400
@ -464,15 +458,15 @@ pub enum Decompressor {
impl Decompressor { impl Decompressor {
pub fn new(disc: &WIADisc) -> Result<Self> { pub fn new(disc: &WIADisc) -> Result<Self> {
let data = &disc.compr_data[..disc.compr_data_len as usize]; let _data = &disc.compr_data[..disc.compr_data_len as usize];
match disc.compression() { match disc.compression() {
WIACompression::None => Ok(Self::None), WIACompression::None => Ok(Self::None),
#[cfg(feature = "compress-bzip2")] #[cfg(feature = "compress-bzip2")]
WIACompression::Bzip2 => Ok(Self::Bzip2), WIACompression::Bzip2 => Ok(Self::Bzip2),
#[cfg(feature = "compress-lzma")] #[cfg(feature = "compress-lzma")]
WIACompression::Lzma => Ok(Self::Lzma(Box::from(data))), WIACompression::Lzma => Ok(Self::Lzma(Box::from(_data))),
#[cfg(feature = "compress-lzma")] #[cfg(feature = "compress-lzma")]
WIACompression::Lzma2 => Ok(Self::Lzma2(Box::from(data))), WIACompression::Lzma2 => Ok(Self::Lzma2(Box::from(_data))),
#[cfg(feature = "compress-zstd")] #[cfg(feature = "compress-zstd")]
WIACompression::Zstandard => Ok(Self::Zstandard), WIACompression::Zstandard => Ok(Self::Zstandard),
comp => Err(Error::DiscFormat(format!("Unsupported WIA/RVZ compression: {:?}", comp))), comp => Err(Error::DiscFormat(format!("Unsupported WIA/RVZ compression: {:?}", comp))),
@ -487,11 +481,13 @@ impl Decompressor {
Decompressor::Bzip2 => Box::new(bzip2::read::BzDecoder::new(reader)), Decompressor::Bzip2 => Box::new(bzip2::read::BzDecoder::new(reader)),
#[cfg(feature = "compress-lzma")] #[cfg(feature = "compress-lzma")]
Decompressor::Lzma(data) => { Decompressor::Lzma(data) => {
use crate::util::compress::{lzma_props_decode, new_lzma_decoder};
let options = lzma_props_decode(data)?; let options = lzma_props_decode(data)?;
Box::new(new_lzma_decoder(reader, &options)?) Box::new(new_lzma_decoder(reader, &options)?)
} }
#[cfg(feature = "compress-lzma")] #[cfg(feature = "compress-lzma")]
Decompressor::Lzma2(data) => { Decompressor::Lzma2(data) => {
use crate::util::compress::{lzma2_props_decode, new_lzma2_decoder};
let options = lzma2_props_decode(data)?; let options = lzma2_props_decode(data)?;
Box::new(new_lzma2_decoder(reader, &options)?) Box::new(new_lzma2_decoder(reader, &options)?)
} }
@ -502,7 +498,7 @@ impl Decompressor {
} }
pub struct DiscIOWIA { pub struct DiscIOWIA {
inner: SplitFileReader, inner: Box<dyn DiscStream>,
header: WIAFileHeader, header: WIAFileHeader,
disc: WIADisc, disc: WIADisc,
partitions: Box<[WIAPartition]>, partitions: Box<[WIAPartition]>,
@ -549,21 +545,21 @@ fn verify_hash(buf: &[u8], expected: &HashBytes) -> Result<()> {
} }
impl DiscIOWIA { impl DiscIOWIA {
pub fn new(filename: &Path) -> Result<Box<Self>> { pub fn new(mut inner: Box<dyn DiscStream>) -> Result<Box<Self>> {
let mut inner = SplitFileReader::new(filename)?;
// Load & verify file header // Load & verify file header
let header: WIAFileHeader = read_from(&mut inner).context("Reading WIA/RVZ file header")?; inner.seek(SeekFrom::Start(0)).context("Seeking to start")?;
let header: WIAFileHeader =
read_from(inner.as_mut()).context("Reading WIA/RVZ file header")?;
header.validate()?; header.validate()?;
let is_rvz = header.is_rvz(); let is_rvz = header.is_rvz();
// log::debug!("Header: {:?}", header); // log::debug!("Header: {:?}", header);
// Load & verify disc header // Load & verify disc header
let mut disc_buf: Vec<u8> = read_vec(&mut inner, header.disc_size.get() as usize) let mut disc_buf: Vec<u8> = read_vec(inner.as_mut(), header.disc_size.get() as usize)
.context("Reading WIA/RVZ disc header")?; .context("Reading WIA/RVZ disc header")?;
verify_hash(&disc_buf, &header.disc_hash)?; verify_hash(&disc_buf, &header.disc_hash)?;
disc_buf.resize(size_of::<WIADisc>(), 0); disc_buf.resize(size_of::<WIADisc>(), 0);
let disc = WIADisc::read_from(disc_buf.as_slice()).unwrap(); let disc = WIADisc::read_from_bytes(disc_buf.as_slice()).unwrap();
disc.validate()?; disc.validate()?;
// if !options.rebuild_hashes { // if !options.rebuild_hashes {
// // If we're not rebuilding hashes, disable partition hashes in disc header // // If we're not rebuilding hashes, disable partition hashes in disc header
@ -576,14 +572,14 @@ impl DiscIOWIA {
// log::debug!("Disc: {:?}", disc); // log::debug!("Disc: {:?}", disc);
// Read NKit header if present (after disc header) // Read NKit header if present (after disc header)
let nkit_header = NKitHeader::try_read_from(&mut inner, disc.chunk_size.get(), false); let nkit_header = NKitHeader::try_read_from(inner.as_mut(), disc.chunk_size.get(), false);
// Load & verify partition headers // Load & verify partition headers
inner inner
.seek(SeekFrom::Start(disc.partition_offset.get())) .seek(SeekFrom::Start(disc.partition_offset.get()))
.context("Seeking to WIA/RVZ partition headers")?; .context("Seeking to WIA/RVZ partition headers")?;
let partitions: Box<[WIAPartition]> = let partitions: Box<[WIAPartition]> =
read_box_slice(&mut inner, disc.num_partitions.get() as usize) read_box_slice(inner.as_mut(), disc.num_partitions.get() as usize)
.context("Reading WIA/RVZ partition headers")?; .context("Reading WIA/RVZ partition headers")?;
verify_hash(partitions.as_ref().as_bytes(), &disc.partition_hash)?; verify_hash(partitions.as_ref().as_bytes(), &disc.partition_hash)?;
// log::debug!("Partitions: {:?}", partitions); // log::debug!("Partitions: {:?}", partitions);
@ -597,7 +593,7 @@ impl DiscIOWIA {
.seek(SeekFrom::Start(disc.raw_data_offset.get())) .seek(SeekFrom::Start(disc.raw_data_offset.get()))
.context("Seeking to WIA/RVZ raw data headers")?; .context("Seeking to WIA/RVZ raw data headers")?;
let mut reader = decompressor let mut reader = decompressor
.wrap((&mut inner).take(disc.raw_data_size.get() as u64)) .wrap(inner.as_mut().take(disc.raw_data_size.get() as u64))
.context("Creating WIA/RVZ decompressor")?; .context("Creating WIA/RVZ decompressor")?;
read_box_slice(&mut reader, disc.num_raw_data.get() as usize) read_box_slice(&mut reader, disc.num_raw_data.get() as usize)
.context("Reading WIA/RVZ raw data headers")? .context("Reading WIA/RVZ raw data headers")?
@ -621,7 +617,7 @@ impl DiscIOWIA {
.seek(SeekFrom::Start(disc.group_offset.get())) .seek(SeekFrom::Start(disc.group_offset.get()))
.context("Seeking to WIA/RVZ group headers")?; .context("Seeking to WIA/RVZ group headers")?;
let mut reader = decompressor let mut reader = decompressor
.wrap((&mut inner).take(disc.group_size.get() as u64)) .wrap(inner.as_mut().take(disc.group_size.get() as u64))
.context("Creating WIA/RVZ decompressor")?; .context("Creating WIA/RVZ decompressor")?;
if is_rvz { if is_rvz {
read_box_slice(&mut reader, disc.num_groups.get() as usize) read_box_slice(&mut reader, disc.num_groups.get() as usize)

View File

@ -1,4 +1,4 @@
#![warn(missing_docs)] #![warn(missing_docs, clippy::missing_inline_in_public_items)]
//! Library for traversing & reading Nintendo Optical Disc (GameCube and Wii) images. //! Library for traversing & reading Nintendo Optical Disc (GameCube and Wii) images.
//! //!
//! Originally based on the C++ library [nod](https://github.com/AxioDL/nod), //! Originally based on the C++ library [nod](https://github.com/AxioDL/nod),
@ -59,22 +59,24 @@
//! ``` //! ```
use std::{ use std::{
io::{Read, Seek}, io::{BufRead, Read, Seek},
path::Path, path::Path,
}; };
pub use disc::{ pub use disc::{
ApploaderHeader, DiscHeader, DolHeader, PartitionBase, PartitionHeader, PartitionKind, ApploaderHeader, DiscHeader, DolHeader, FileStream, Fst, Node, NodeKind, OwnedFileStream,
PartitionMeta, BI2_SIZE, BOOT_SIZE, SECTOR_SIZE, PartitionBase, PartitionHeader, PartitionKind, PartitionMeta, SignedHeader, Ticket,
TicketLimit, TmdHeader, WindowedStream, BI2_SIZE, BOOT_SIZE, DL_DVD_SIZE, GCN_MAGIC,
MINI_DVD_SIZE, REGION_SIZE, SECTOR_SIZE, SL_DVD_SIZE, WII_MAGIC,
}; };
pub use fst::{Fst, Node, NodeKind}; pub use io::{
pub use io::{block::PartitionInfo, Compression, DiscMeta, Format}; block::{DiscStream, PartitionInfo},
pub use streams::ReadStream; Compression, DiscMeta, Format, KeyBytes, MagicBytes,
};
pub use util::lfg::LaggedFibonacci;
mod disc; mod disc;
mod fst;
mod io; mod io;
mod streams;
mod util; mod util;
/// Error types for nod. /// Error types for nod.
@ -89,16 +91,26 @@ pub enum Error {
/// An unknown error. /// An unknown error.
#[error("error: {0}")] #[error("error: {0}")]
Other(String), Other(String),
/// An error occurred while allocating memory.
#[error("allocation failed")]
Alloc(zerocopy::AllocError),
} }
impl From<&str> for Error { impl From<&str> for Error {
#[inline]
fn from(s: &str) -> Error { Error::Other(s.to_string()) } fn from(s: &str) -> Error { Error::Other(s.to_string()) }
} }
impl From<String> for Error { impl From<String> for Error {
#[inline]
fn from(s: String) -> Error { Error::Other(s) } fn from(s: String) -> Error { Error::Other(s) }
} }
impl From<zerocopy::AllocError> for Error {
#[inline]
fn from(e: zerocopy::AllocError) -> Error { Error::Alloc(e) }
}
/// Helper result type for [`Error`]. /// Helper result type for [`Error`].
pub type Result<T, E = Error> = core::result::Result<T, E>; pub type Result<T, E = Error> = core::result::Result<T, E>;
@ -109,6 +121,7 @@ pub trait ErrorContext {
} }
impl ErrorContext for std::io::Error { impl ErrorContext for std::io::Error {
#[inline]
fn context(self, context: impl Into<String>) -> Error { Error::Io(context.into(), self) } fn context(self, context: impl Into<String>) -> Error { Error::Io(context.into(), self) }
} }
@ -125,10 +138,12 @@ pub trait ResultContext<T> {
impl<T, E> ResultContext<T> for Result<T, E> impl<T, E> ResultContext<T> for Result<T, E>
where E: ErrorContext where E: ErrorContext
{ {
#[inline]
fn context(self, context: impl Into<String>) -> Result<T> { fn context(self, context: impl Into<String>) -> Result<T> {
self.map_err(|e| e.context(context)) self.map_err(|e| e.context(context))
} }
#[inline]
fn with_context<F>(self, f: F) -> Result<T> fn with_context<F>(self, f: F) -> Result<T>
where F: FnOnce() -> String { where F: FnOnce() -> String {
self.map_err(|e| e.context(f())) self.map_err(|e| e.context(f()))
@ -155,34 +170,71 @@ pub struct Disc {
impl Disc { impl Disc {
/// Opens a disc image from a file path. /// Opens a disc image from a file path.
#[inline]
pub fn new<P: AsRef<Path>>(path: P) -> Result<Disc> { pub fn new<P: AsRef<Path>>(path: P) -> Result<Disc> {
Disc::new_with_options(path, &OpenOptions::default()) Disc::new_with_options(path, &OpenOptions::default())
} }
/// Opens a disc image from a file path with custom options. /// Opens a disc image from a file path with custom options.
#[inline]
pub fn new_with_options<P: AsRef<Path>>(path: P, options: &OpenOptions) -> Result<Disc> { pub fn new_with_options<P: AsRef<Path>>(path: P, options: &OpenOptions) -> Result<Disc> {
let io = io::block::open(path.as_ref())?; let io = io::block::open(path.as_ref())?;
let reader = disc::reader::DiscReader::new(io, options)?; let reader = disc::reader::DiscReader::new(io, options)?;
Ok(Disc { reader, options: options.clone() }) Ok(Disc { reader, options: options.clone() })
} }
/// Opens a disc image from a read stream.
#[inline]
pub fn new_stream(stream: Box<dyn DiscStream>) -> Result<Disc> {
Disc::new_stream_with_options(stream, &OpenOptions::default())
}
/// Opens a disc image from a read stream with custom options.
#[inline]
pub fn new_stream_with_options(
stream: Box<dyn DiscStream>,
options: &OpenOptions,
) -> Result<Disc> {
let io = io::block::new(stream)?;
let reader = disc::reader::DiscReader::new(io, options)?;
Ok(Disc { reader, options: options.clone() })
}
/// Detects the format of a disc image from a read stream.
#[inline]
pub fn detect<R>(stream: &mut R) -> std::io::Result<Option<Format>>
where R: Read + ?Sized {
io::block::detect(stream)
}
/// The disc's primary header. /// The disc's primary header.
#[inline]
pub fn header(&self) -> &DiscHeader { self.reader.header() } pub fn header(&self) -> &DiscHeader { self.reader.header() }
/// The Wii disc's region information.
///
/// **GameCube**: This will return `None`.
#[inline]
pub fn region(&self) -> Option<&[u8; REGION_SIZE]> { self.reader.region() }
/// Returns extra metadata included in the disc file format, if any. /// Returns extra metadata included in the disc file format, if any.
#[inline]
pub fn meta(&self) -> DiscMeta { self.reader.meta() } pub fn meta(&self) -> DiscMeta { self.reader.meta() }
/// The disc's size in bytes, or an estimate if not stored by the format. /// The disc's size in bytes, or an estimate if not stored by the format.
#[inline]
pub fn disc_size(&self) -> u64 { self.reader.disc_size() } pub fn disc_size(&self) -> u64 { self.reader.disc_size() }
/// A list of Wii partitions on the disc. /// A list of Wii partitions on the disc.
/// ///
/// **GameCube**: This will return an empty slice. /// **GameCube**: This will return an empty slice.
#[inline]
pub fn partitions(&self) -> &[PartitionInfo] { self.reader.partitions() } pub fn partitions(&self) -> &[PartitionInfo] { self.reader.partitions() }
/// Opens a decrypted partition read stream for the specified partition index. /// Opens a decrypted partition read stream for the specified partition index.
/// ///
/// **GameCube**: `index` must always be 0. /// **GameCube**: `index` must always be 0.
#[inline]
pub fn open_partition(&self, index: usize) -> Result<Box<dyn PartitionBase>> { pub fn open_partition(&self, index: usize) -> Result<Box<dyn PartitionBase>> {
self.reader.open_partition(index, &self.options) self.reader.open_partition(index, &self.options)
} }
@ -191,15 +243,26 @@ impl Disc {
/// the specified kind. /// the specified kind.
/// ///
/// **GameCube**: `kind` must always be [`PartitionKind::Data`]. /// **GameCube**: `kind` must always be [`PartitionKind::Data`].
#[inline]
pub fn open_partition_kind(&self, kind: PartitionKind) -> Result<Box<dyn PartitionBase>> { pub fn open_partition_kind(&self, kind: PartitionKind) -> Result<Box<dyn PartitionBase>> {
self.reader.open_partition_kind(kind, &self.options) self.reader.open_partition_kind(kind, &self.options)
} }
} }
impl BufRead for Disc {
#[inline]
fn fill_buf(&mut self) -> std::io::Result<&[u8]> { self.reader.fill_buf() }
#[inline]
fn consume(&mut self, amt: usize) { self.reader.consume(amt) }
}
impl Read for Disc { impl Read for Disc {
#[inline]
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> { self.reader.read(buf) } fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> { self.reader.read(buf) }
} }
impl Seek for Disc { impl Seek for Disc {
#[inline]
fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> { self.reader.seek(pos) } fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> { self.reader.seek(pos) }
} }

View File

@ -1,80 +0,0 @@
//! Common stream types
use std::{
io,
io::{Read, Seek, SeekFrom},
};
/// A helper trait for seekable read streams.
pub trait ReadStream: Read + Seek {
/// Creates a windowed read sub-stream with offset and size.
///
/// Seeks underlying stream immediately.
fn new_window(&mut self, offset: u64, size: u64) -> io::Result<SharedWindowedReadStream> {
self.seek(SeekFrom::Start(offset))?;
Ok(SharedWindowedReadStream { base: self.as_dyn(), begin: offset, end: offset + size })
}
/// Retrieves a type-erased reference to the stream.
fn as_dyn(&mut self) -> &mut dyn ReadStream;
}
impl<T> ReadStream for T
where T: Read + Seek
{
fn as_dyn(&mut self) -> &mut dyn ReadStream { self }
}
/// A non-owning window into an existing [`ReadStream`].
pub struct SharedWindowedReadStream<'a> {
/// A reference to the base stream.
pub base: &'a mut dyn ReadStream,
/// The beginning of the window in bytes.
pub begin: u64,
/// The end of the window in bytes.
pub end: u64,
}
impl<'a> SharedWindowedReadStream<'a> {
/// Modifies the current window & seeks to the beginning of the window.
pub fn set_window(&mut self, begin: u64, end: u64) -> io::Result<()> {
self.base.seek(SeekFrom::Start(begin))?;
self.begin = begin;
self.end = end;
Ok(())
}
}
impl<'a> Read for SharedWindowedReadStream<'a> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let pos = self.stream_position()?;
let size = self.end - self.begin;
if pos == size {
return Ok(0);
}
self.base.read(if pos + buf.len() as u64 > size {
&mut buf[..(size - pos) as usize]
} else {
buf
})
}
}
impl<'a> Seek for SharedWindowedReadStream<'a> {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
let result = self.base.seek(match pos {
SeekFrom::Start(p) => SeekFrom::Start(self.begin + p),
SeekFrom::End(p) => SeekFrom::End(self.end as i64 + p),
SeekFrom::Current(_) => pos,
})?;
if result < self.begin || result > self.end {
Err(io::Error::from(io::ErrorKind::UnexpectedEof))
} else {
Ok(result - self.begin)
}
}
fn stream_position(&mut self) -> io::Result<u64> {
Ok(self.base.stream_position()? - self.begin)
}
}

View File

@ -1,13 +1,14 @@
use std::{io, io::Read};
/// Decodes the LZMA Properties byte (lc/lp/pb). /// Decodes the LZMA Properties byte (lc/lp/pb).
/// See `lzma_lzma_lclppb_decode` in `liblzma/lzma/lzma_decoder.c`. /// See `lzma_lzma_lclppb_decode` in `liblzma/lzma/lzma_decoder.c`.
#[cfg(feature = "compress-lzma")] #[cfg(feature = "compress-lzma")]
pub fn lzma_lclppb_decode(options: &mut liblzma::stream::LzmaOptions, byte: u8) -> io::Result<()> { pub fn lzma_lclppb_decode(
options: &mut liblzma::stream::LzmaOptions,
byte: u8,
) -> std::io::Result<()> {
let mut d = byte as u32; let mut d = byte as u32;
if d >= (9 * 5 * 5) { if d >= (9 * 5 * 5) {
return Err(io::Error::new( return Err(std::io::Error::new(
io::ErrorKind::InvalidData, std::io::ErrorKind::InvalidData,
format!("Invalid LZMA props byte: {}", d), format!("Invalid LZMA props byte: {}", d),
)); ));
} }
@ -21,11 +22,11 @@ pub fn lzma_lclppb_decode(options: &mut liblzma::stream::LzmaOptions, byte: u8)
/// Decodes LZMA properties. /// Decodes LZMA properties.
/// See `lzma_lzma_props_decode` in `liblzma/lzma/lzma_decoder.c`. /// See `lzma_lzma_props_decode` in `liblzma/lzma/lzma_decoder.c`.
#[cfg(feature = "compress-lzma")] #[cfg(feature = "compress-lzma")]
pub fn lzma_props_decode(props: &[u8]) -> io::Result<liblzma::stream::LzmaOptions> { pub fn lzma_props_decode(props: &[u8]) -> std::io::Result<liblzma::stream::LzmaOptions> {
use crate::array_ref; use crate::array_ref;
if props.len() != 5 { if props.len() != 5 {
return Err(io::Error::new( return Err(std::io::Error::new(
io::ErrorKind::InvalidData, std::io::ErrorKind::InvalidData,
format!("Invalid LZMA props length: {}", props.len()), format!("Invalid LZMA props length: {}", props.len()),
)); ));
} }
@ -38,11 +39,11 @@ pub fn lzma_props_decode(props: &[u8]) -> io::Result<liblzma::stream::LzmaOption
/// Decodes LZMA2 properties. /// Decodes LZMA2 properties.
/// See `lzma_lzma2_props_decode` in `liblzma/lzma/lzma2_decoder.c`. /// See `lzma_lzma2_props_decode` in `liblzma/lzma/lzma2_decoder.c`.
#[cfg(feature = "compress-lzma")] #[cfg(feature = "compress-lzma")]
pub fn lzma2_props_decode(props: &[u8]) -> io::Result<liblzma::stream::LzmaOptions> { pub fn lzma2_props_decode(props: &[u8]) -> std::io::Result<liblzma::stream::LzmaOptions> {
use std::cmp::Ordering; use std::cmp::Ordering;
if props.len() != 1 { if props.len() != 1 {
return Err(io::Error::new( return Err(std::io::Error::new(
io::ErrorKind::InvalidData, std::io::ErrorKind::InvalidData,
format!("Invalid LZMA2 props length: {}", props.len()), format!("Invalid LZMA2 props length: {}", props.len()),
)); ));
} }
@ -50,8 +51,8 @@ pub fn lzma2_props_decode(props: &[u8]) -> io::Result<liblzma::stream::LzmaOptio
let mut options = liblzma::stream::LzmaOptions::new(); let mut options = liblzma::stream::LzmaOptions::new();
options.dict_size(match d.cmp(&40) { options.dict_size(match d.cmp(&40) {
Ordering::Greater => { Ordering::Greater => {
return Err(io::Error::new( return Err(std::io::Error::new(
io::ErrorKind::InvalidData, std::io::ErrorKind::InvalidData,
format!("Invalid LZMA2 props byte: {}", d), format!("Invalid LZMA2 props byte: {}", d),
)); ));
} }
@ -66,13 +67,14 @@ pub fn lzma2_props_decode(props: &[u8]) -> io::Result<liblzma::stream::LzmaOptio
pub fn new_lzma_decoder<R>( pub fn new_lzma_decoder<R>(
reader: R, reader: R,
options: &liblzma::stream::LzmaOptions, options: &liblzma::stream::LzmaOptions,
) -> io::Result<liblzma::read::XzDecoder<R>> ) -> std::io::Result<liblzma::read::XzDecoder<R>>
where where
R: Read, R: std::io::Read,
{ {
let mut filters = liblzma::stream::Filters::new(); let mut filters = liblzma::stream::Filters::new();
filters.lzma1(options); filters.lzma1(options);
let stream = liblzma::stream::Stream::new_raw_decoder(&filters).map_err(io::Error::from)?; let stream =
liblzma::stream::Stream::new_raw_decoder(&filters).map_err(std::io::Error::from)?;
Ok(liblzma::read::XzDecoder::new_stream(reader, stream)) Ok(liblzma::read::XzDecoder::new_stream(reader, stream))
} }
@ -81,12 +83,13 @@ where
pub fn new_lzma2_decoder<R>( pub fn new_lzma2_decoder<R>(
reader: R, reader: R,
options: &liblzma::stream::LzmaOptions, options: &liblzma::stream::LzmaOptions,
) -> io::Result<liblzma::read::XzDecoder<R>> ) -> std::io::Result<liblzma::read::XzDecoder<R>>
where where
R: Read, R: std::io::Read,
{ {
let mut filters = liblzma::stream::Filters::new(); let mut filters = liblzma::stream::Filters::new();
filters.lzma2(options); filters.lzma2(options);
let stream = liblzma::stream::Stream::new_raw_decoder(&filters).map_err(io::Error::from)?; let stream =
liblzma::stream::Stream::new_raw_decoder(&filters).map_err(std::io::Error::from)?;
Ok(liblzma::read::XzDecoder::new_stream(reader, stream)) Ok(liblzma::read::XzDecoder::new_stream(reader, stream))
} }

View File

@ -1,6 +1,10 @@
use std::{cmp::min, io, io::Read}; use std::{
cmp::min,
io,
io::{Read, Write},
};
use zerocopy::{transmute_ref, AsBytes}; use zerocopy::{transmute_ref, IntoBytes};
use crate::disc::SECTOR_SIZE; use crate::disc::SECTOR_SIZE;
@ -8,7 +12,7 @@ pub const LFG_K: usize = 521;
pub const LFG_J: usize = 32; pub const LFG_J: usize = 32;
pub const SEED_SIZE: usize = 17; pub const SEED_SIZE: usize = 17;
/// Lagged Fibonacci generator for Wii partition junk data. /// Lagged Fibonacci generator for GC / Wii partition junk data.
/// ///
/// References (license CC0-1.0): /// References (license CC0-1.0):
/// https://github.com/dolphin-emu/dolphin/blob/a0f555648c27ec0c928f6b1e1fcad5e2d7c4d0c4/docs/WiaAndRvz.md /// https://github.com/dolphin-emu/dolphin/blob/a0f555648c27ec0c928f6b1e1fcad5e2d7c4d0c4/docs/WiaAndRvz.md
@ -19,6 +23,7 @@ pub struct LaggedFibonacci {
} }
impl Default for LaggedFibonacci { impl Default for LaggedFibonacci {
#[inline]
fn default() -> Self { Self { buffer: [0u32; LFG_K], position: 0 } } fn default() -> Self { Self { buffer: [0u32; LFG_K], position: 0 } }
} }
@ -38,12 +43,16 @@ impl LaggedFibonacci {
} }
} }
pub fn init_with_seed(&mut self, init: [u8; 4], disc_num: u8, partition_offset: u64) { /// Initializes the LFG with the standard seed for a given disc ID, disc number, and sector.
/// The partition offset is used to determine the sector and how many bytes to skip within the
/// sector.
#[allow(clippy::missing_inline_in_public_items)]
pub fn init_with_seed(&mut self, disc_id: [u8; 4], disc_num: u8, partition_offset: u64) {
let seed = u32::from_be_bytes([ let seed = u32::from_be_bytes([
init[2], disc_id[2],
init[1], disc_id[1],
init[3].wrapping_add(init[2]), disc_id[3].wrapping_add(disc_id[2]),
init[0].wrapping_add(init[1]), disc_id[0].wrapping_add(disc_id[1]),
]) ^ disc_num as u32; ]) ^ disc_num as u32;
let sector = (partition_offset / SECTOR_SIZE as u64) as u32; let sector = (partition_offset / SECTOR_SIZE as u64) as u32;
let sector_offset = partition_offset % SECTOR_SIZE as u64; let sector_offset = partition_offset % SECTOR_SIZE as u64;
@ -62,9 +71,12 @@ impl LaggedFibonacci {
self.skip(sector_offset as usize); self.skip(sector_offset as usize);
} }
/// Initializes the LFG with the seed read from a reader. The seed is assumed to be big-endian.
/// This is used for rebuilding junk data in WIA/RVZ files.
#[allow(clippy::missing_inline_in_public_items)]
pub fn init_with_reader<R>(&mut self, reader: &mut R) -> io::Result<()> pub fn init_with_reader<R>(&mut self, reader: &mut R) -> io::Result<()>
where R: Read + ?Sized { where R: Read + ?Sized {
reader.read_exact(self.buffer[..SEED_SIZE].as_bytes_mut())?; reader.read_exact(self.buffer[..SEED_SIZE].as_mut_bytes())?;
for x in self.buffer[..SEED_SIZE].iter_mut() { for x in self.buffer[..SEED_SIZE].iter_mut() {
*x = u32::from_be(*x); *x = u32::from_be(*x);
} }
@ -73,7 +85,8 @@ impl LaggedFibonacci {
Ok(()) Ok(())
} }
pub fn forward(&mut self) { /// Advances the LFG by one step.
fn forward(&mut self) {
for i in 0..LFG_J { for i in 0..LFG_J {
self.buffer[i] ^= self.buffer[i + LFG_K - LFG_J]; self.buffer[i] ^= self.buffer[i + LFG_K - LFG_J];
} }
@ -82,6 +95,8 @@ impl LaggedFibonacci {
} }
} }
/// Skips `n` bytes of junk data.
#[allow(clippy::missing_inline_in_public_items)]
pub fn skip(&mut self, n: usize) { pub fn skip(&mut self, n: usize) {
self.position += n; self.position += n;
while self.position >= LFG_K * 4 { while self.position >= LFG_K * 4 {
@ -90,6 +105,8 @@ impl LaggedFibonacci {
} }
} }
/// Fills the buffer with junk data.
#[allow(clippy::missing_inline_in_public_items)]
pub fn fill(&mut self, mut buf: &mut [u8]) { pub fn fill(&mut self, mut buf: &mut [u8]) {
while !buf.is_empty() { while !buf.is_empty() {
let len = min(buf.len(), LFG_K * 4 - self.position); let len = min(buf.len(), LFG_K * 4 - self.position);
@ -103,6 +120,68 @@ impl LaggedFibonacci {
} }
} }
} }
/// Writes junk data to the output stream.
#[allow(clippy::missing_inline_in_public_items)]
pub fn write<W>(&mut self, w: &mut W, mut len: u64) -> io::Result<()>
where W: Write + ?Sized {
while len > 0 {
let write_len = min(len, LFG_K as u64 * 4 - self.position as u64) as usize;
let bytes: &[u8; LFG_K * 4] = transmute_ref!(&self.buffer);
w.write_all(&bytes[self.position..self.position + write_len])?;
self.position += write_len;
len -= write_len as u64;
if self.position == LFG_K * 4 {
self.forward();
self.position = 0;
}
}
Ok(())
}
/// The junk data on GC / Wii discs is reinitialized every 32KB. This functions handles the
/// wrapping logic and reinitializes the LFG at sector boundaries.
#[allow(clippy::missing_inline_in_public_items)]
pub fn fill_sector_chunked(
&mut self,
mut buf: &mut [u8],
disc_id: [u8; 4],
disc_num: u8,
mut partition_offset: u64,
) {
while !buf.is_empty() {
self.init_with_seed(disc_id, disc_num, partition_offset);
let len =
(SECTOR_SIZE - (partition_offset % SECTOR_SIZE as u64) as usize).min(buf.len());
self.fill(&mut buf[..len]);
buf = &mut buf[len..];
partition_offset += len as u64;
}
}
/// The junk data on GC / Wii discs is reinitialized every 32KB. This functions handles the
/// wrapping logic and reinitializes the LFG at sector boundaries.
#[allow(clippy::missing_inline_in_public_items)]
pub fn write_sector_chunked<W>(
&mut self,
w: &mut W,
mut len: u64,
disc_id: [u8; 4],
disc_num: u8,
mut partition_offset: u64,
) -> io::Result<()>
where
W: Write + ?Sized,
{
while len > 0 {
self.init_with_seed(disc_id, disc_num, partition_offset);
let write_len = (SECTOR_SIZE as u64 - (partition_offset % SECTOR_SIZE as u64)).min(len);
self.write(w, write_len)?;
len -= write_len;
partition_offset += write_len;
}
Ok(())
}
} }
#[cfg(test)] #[cfg(test)]
@ -132,4 +211,53 @@ mod tests {
0xEA, 0xD0 0xEA, 0xD0
]); ]);
} }
#[test]
fn test_init_with_seed_3() {
let mut lfg = LaggedFibonacci::default();
lfg.init_with_seed([0x47, 0x50, 0x49, 0x45], 0, 0x322904);
let mut buf = [0u8; 16];
lfg.fill(&mut buf);
assert_eq!(buf, [
0x97, 0xD8, 0x23, 0x0B, 0x12, 0xAA, 0x20, 0x45, 0xC2, 0xBD, 0x71, 0x8C, 0x30, 0x32,
0xC5, 0x2F
]);
}
#[test]
fn test_write() {
let mut lfg = LaggedFibonacci::default();
lfg.init_with_seed([0x47, 0x50, 0x49, 0x45], 0, 0x322904);
let mut buf = [0u8; 16];
lfg.write(&mut buf.as_mut_slice(), 16).unwrap();
assert_eq!(buf, [
0x97, 0xD8, 0x23, 0x0B, 0x12, 0xAA, 0x20, 0x45, 0xC2, 0xBD, 0x71, 0x8C, 0x30, 0x32,
0xC5, 0x2F
]);
}
#[test]
fn test_fill_sector_chunked() {
let mut lfg = LaggedFibonacci::default();
let mut buf = [0u8; 32];
lfg.fill_sector_chunked(&mut buf, [0x47, 0x4D, 0x38, 0x45], 0, 0x27FF0);
assert_eq!(buf, [
0xAD, 0x6F, 0x21, 0xBE, 0x05, 0x57, 0x10, 0xED, 0xEA, 0xB0, 0x8E, 0xFD, 0x91, 0x58,
0xA2, 0x0E, 0xDC, 0x0D, 0x59, 0xC0, 0x02, 0x98, 0xA5, 0x00, 0x39, 0x5B, 0x68, 0xA6,
0x5D, 0x53, 0x2D, 0xB6
]);
}
#[test]
fn test_write_sector_chunked() {
let mut lfg = LaggedFibonacci::default();
let mut buf = [0u8; 32];
lfg.write_sector_chunked(&mut buf.as_mut_slice(), 32, [0x47, 0x4D, 0x38, 0x45], 0, 0x27FF0)
.unwrap();
assert_eq!(buf, [
0xAD, 0x6F, 0x21, 0xBE, 0x05, 0x57, 0x10, 0xED, 0xEA, 0xB0, 0x8E, 0xFD, 0x91, 0x58,
0xA2, 0x0E, 0xDC, 0x0D, 0x59, 0xC0, 0x02, 0x98, 0xA5, 0x00, 0x39, 0x5B, 0x68, 0xA6,
0x5D, 0x53, 0x2D, 0xB6
]);
}
} }

View File

@ -1,48 +1,50 @@
use std::{io, io::Read}; use std::{io, io::Read};
use zerocopy::{AsBytes, FromBytes, FromZeroes}; use zerocopy::{FromBytes, FromZeros, IntoBytes};
#[inline(always)] #[inline(always)]
pub fn read_from<T, R>(reader: &mut R) -> io::Result<T> pub fn read_from<T, R>(reader: &mut R) -> io::Result<T>
where where
T: FromBytes + FromZeroes + AsBytes, T: FromBytes + IntoBytes,
R: Read + ?Sized, R: Read + ?Sized,
{ {
let mut ret = <T>::new_zeroed(); let mut ret = <T>::new_zeroed();
reader.read_exact(ret.as_bytes_mut())?; reader.read_exact(ret.as_mut_bytes())?;
Ok(ret) Ok(ret)
} }
#[inline(always)] #[inline(always)]
pub fn read_vec<T, R>(reader: &mut R, count: usize) -> io::Result<Vec<T>> pub fn read_vec<T, R>(reader: &mut R, count: usize) -> io::Result<Vec<T>>
where where
T: FromBytes + FromZeroes + AsBytes, T: FromBytes + IntoBytes,
R: Read + ?Sized, R: Read + ?Sized,
{ {
let mut ret = <T>::new_vec_zeroed(count); let mut ret =
reader.read_exact(ret.as_mut_slice().as_bytes_mut())?; <T>::new_vec_zeroed(count).map_err(|_| io::Error::from(io::ErrorKind::OutOfMemory))?;
reader.read_exact(ret.as_mut_slice().as_mut_bytes())?;
Ok(ret) Ok(ret)
} }
#[inline(always)] #[inline(always)]
pub fn read_box<T, R>(reader: &mut R) -> io::Result<Box<T>> pub fn read_box<T, R>(reader: &mut R) -> io::Result<Box<T>>
where where
T: FromBytes + FromZeroes + AsBytes, T: FromBytes + IntoBytes,
R: Read + ?Sized, R: Read + ?Sized,
{ {
let mut ret = <T>::new_box_zeroed(); let mut ret = <T>::new_box_zeroed().map_err(|_| io::Error::from(io::ErrorKind::OutOfMemory))?;
reader.read_exact(ret.as_mut().as_bytes_mut())?; reader.read_exact(ret.as_mut().as_mut_bytes())?;
Ok(ret) Ok(ret)
} }
#[inline(always)] #[inline(always)]
pub fn read_box_slice<T, R>(reader: &mut R, count: usize) -> io::Result<Box<[T]>> pub fn read_box_slice<T, R>(reader: &mut R, count: usize) -> io::Result<Box<[T]>>
where where
T: FromBytes + FromZeroes + AsBytes, T: FromBytes + IntoBytes,
R: Read + ?Sized, R: Read + ?Sized,
{ {
let mut ret = <T>::new_box_slice_zeroed(count); let mut ret = <[T]>::new_box_zeroed_with_elems(count)
reader.read_exact(ret.as_mut().as_bytes_mut())?; .map_err(|_| io::Error::from(io::ErrorKind::OutOfMemory))?;
reader.read_exact(ret.as_mut().as_mut_bytes())?;
Ok(ret) Ok(ret)
} }

View File

@ -1,17 +1,17 @@
[package] [package]
name = "nodtool" name = "nodtool"
version = "1.2.0" version.workspace = true
edition = "2021" edition.workspace = true
rust-version = "1.73.0" rust-version.workspace = true
authors = ["Luke Street <luke@street.dev>"] authors.workspace = true
license = "MIT OR Apache-2.0" license.workspace = true
repository = "https://github.com/encounter/nod-rs" repository.workspace = true
documentation = "https://docs.rs/nod" documentation = "https://docs.rs/nodtool"
readme = "../README.md" readme = "../README.md"
description = """ description = """
CLI tool for verifying and converting GameCube and Wii disc images. CLI tool for verifying and converting GameCube and Wii disc images.
""" """
keywords = ["gamecube", "wii", "iso", "wbfs", "rvz"] keywords.workspace = true
categories = ["command-line-utilities", "parser-implementations"] categories = ["command-line-utilities", "parser-implementations"]
build = "build.rs" build = "build.rs"
@ -20,32 +20,35 @@ asm = ["md-5/asm", "nod/asm", "sha1/asm"]
nightly = ["crc32fast/nightly"] nightly = ["crc32fast/nightly"]
[dependencies] [dependencies]
argp = "0.3.0" argp = "0.3"
base16ct = "0.2.0" base16ct = "0.2"
crc32fast = "1.4.2" crc32fast = "1.4"
digest = "0.10.7" digest = "0.10"
enable-ansi-support = "0.2.1" enable-ansi-support = "0.2"
hex = { version = "0.4.3", features = ["serde"] } hex = { version = "0.4", features = ["serde"] }
indicatif = "0.17.8" indicatif = "0.17"
itertools = "0.12.1" itertools = "0.13"
log = "0.4.20" log = "0.4"
md-5 = "0.10.6" md-5 = "0.10"
nod = { path = "../nod" } nod = { version = "1.2", path = "../nod" }
quick-xml = { version = "0.31.0", features = ["serialize"] } quick-xml = { version = "0.36", features = ["serialize"] }
serde = { version = "1.0.197", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
sha1 = "0.10.6" sha1 = "0.10"
size = "0.4.1" size = "0.4"
supports-color = "3.0.0" supports-color = "3.0"
tracing = "0.1.40" tracing = "0.1"
tracing-attributes = "0.1.27" tracing-attributes = "0.1"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
xxhash-rust = { version = "0.8.10", features = ["xxh64"] } xxhash-rust = { version = "0.8", features = ["xxh64"] }
zerocopy = { version = "0.7.32", features = ["alloc", "derive"] } zerocopy = { version = "0.8", features = ["alloc", "derive"] }
zstd = "0.13.1" zstd = "0.13"
[target.'cfg(target_env = "musl")'.dependencies]
mimalloc = "0.1"
[build-dependencies] [build-dependencies]
hex = { version = "0.4.3", features = ["serde"] } hex = { version = "0.4", features = ["serde"] }
quick-xml = { version = "0.31.0", features = ["serialize"] } quick-xml = { version = "0.36", features = ["serialize"] }
serde = { version = "1.0.197", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
zerocopy = { version = "0.7.32", features = ["alloc", "derive"] } zerocopy = { version = "0.8", features = ["alloc", "derive"] }
zstd = "0.13.1" zstd = "0.13"

View File

@ -8,10 +8,10 @@ use std::{
use hex::deserialize as deserialize_hex; use hex::deserialize as deserialize_hex;
use serde::Deserialize; use serde::Deserialize;
use zerocopy::AsBytes; use zerocopy::{Immutable, IntoBytes, KnownLayout};
// Keep in sync with build.rs // Keep in sync with build.rs
#[derive(Clone, Debug, AsBytes)] #[derive(Clone, Debug, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(4))] #[repr(C, align(4))]
struct Header { struct Header {
entry_count: u32, entry_count: u32,
@ -19,7 +19,7 @@ struct Header {
} }
// Keep in sync with redump.rs // Keep in sync with redump.rs
#[derive(Clone, Debug, AsBytes)] #[derive(Clone, Debug, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(4))] #[repr(C, align(4))]
struct GameEntry { struct GameEntry {
crc32: u32, crc32: u32,

View File

@ -11,7 +11,7 @@ use std::{
use argp::FromArgs; use argp::FromArgs;
use indicatif::{ProgressBar, ProgressState, ProgressStyle}; use indicatif::{ProgressBar, ProgressState, ProgressStyle};
use nod::{Disc, OpenOptions, Result, ResultContext}; use nod::{Disc, OpenOptions, Result, ResultContext};
use zerocopy::FromZeroes; use zerocopy::FromZeros;
use crate::util::{ use crate::util::{
digest::{digest_thread, DigestResult}, digest::{digest_thread, DigestResult},
@ -199,7 +199,7 @@ fn load_disc(path: &Path, name: &str, full_verify: bool) -> Result<DiscHashes> {
}); });
let mut total_read = 0u64; let mut total_read = 0u64;
let mut buf = <u8>::new_box_slice_zeroed(BUFFER_SIZE); let mut buf = <[u8]>::new_box_zeroed_with_elems(BUFFER_SIZE)?;
while total_read < disc_size { while total_read < disc_size {
let read = min(BUFFER_SIZE as u64, disc_size - total_read) as usize; let read = min(BUFFER_SIZE as u64, disc_size - total_read) as usize;
disc.read_exact(&mut buf[..read]).with_context(|| { disc.read_exact(&mut buf[..read]).with_context(|| {

View File

@ -2,19 +2,17 @@ use std::{
borrow::Cow, borrow::Cow,
fs, fs,
fs::File, fs::File,
io, io::{BufRead, Write},
io::{BufWriter, Write},
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use argp::FromArgs; use argp::FromArgs;
use itertools::Itertools; use itertools::Itertools;
use nod::{ use nod::{
Disc, DiscHeader, Fst, Node, OpenOptions, PartitionBase, PartitionKind, PartitionMeta, Disc, Fst, Node, OpenOptions, PartitionBase, PartitionKind, PartitionMeta, ResultContext,
ResultContext,
}; };
use size::{Base, Size}; use size::{Base, Size};
use zerocopy::AsBytes; use zerocopy::IntoBytes;
use crate::util::{display, has_extension}; use crate::util::{display, has_extension};
@ -66,38 +64,38 @@ pub fn run(args: Args) -> nod::Result<()> {
let mut out_dir = output_dir.clone(); let mut out_dir = output_dir.clone();
out_dir.push(info.kind.dir_name().as_ref()); out_dir.push(info.kind.dir_name().as_ref());
let mut partition = disc.open_partition(info.index)?; let mut partition = disc.open_partition(info.index)?;
extract_partition(header, partition.as_mut(), &out_dir, is_wii, args.quiet)?; extract_partition(&disc, partition.as_mut(), &out_dir, is_wii, args.quiet)?;
} }
} else if partition.eq_ignore_ascii_case("data") { } else if partition.eq_ignore_ascii_case("data") {
let mut partition = disc.open_partition_kind(PartitionKind::Data)?; let mut partition = disc.open_partition_kind(PartitionKind::Data)?;
extract_partition(header, partition.as_mut(), &output_dir, is_wii, args.quiet)?; extract_partition(&disc, partition.as_mut(), &output_dir, is_wii, args.quiet)?;
} else if partition.eq_ignore_ascii_case("update") { } else if partition.eq_ignore_ascii_case("update") {
let mut partition = disc.open_partition_kind(PartitionKind::Update)?; let mut partition = disc.open_partition_kind(PartitionKind::Update)?;
extract_partition(header, partition.as_mut(), &output_dir, is_wii, args.quiet)?; extract_partition(&disc, partition.as_mut(), &output_dir, is_wii, args.quiet)?;
} else if partition.eq_ignore_ascii_case("channel") { } else if partition.eq_ignore_ascii_case("channel") {
let mut partition = disc.open_partition_kind(PartitionKind::Channel)?; let mut partition = disc.open_partition_kind(PartitionKind::Channel)?;
extract_partition(header, partition.as_mut(), &output_dir, is_wii, args.quiet)?; extract_partition(&disc, partition.as_mut(), &output_dir, is_wii, args.quiet)?;
} else { } else {
let idx = partition.parse::<usize>().map_err(|_| "Invalid partition index")?; let idx = partition.parse::<usize>().map_err(|_| "Invalid partition index")?;
let mut partition = disc.open_partition(idx)?; let mut partition = disc.open_partition(idx)?;
extract_partition(header, partition.as_mut(), &output_dir, is_wii, args.quiet)?; extract_partition(&disc, partition.as_mut(), &output_dir, is_wii, args.quiet)?;
} }
} else { } else {
let mut partition = disc.open_partition_kind(PartitionKind::Data)?; let mut partition = disc.open_partition_kind(PartitionKind::Data)?;
extract_partition(header, partition.as_mut(), &output_dir, is_wii, args.quiet)?; extract_partition(&disc, partition.as_mut(), &output_dir, is_wii, args.quiet)?;
} }
Ok(()) Ok(())
} }
fn extract_partition( fn extract_partition(
header: &DiscHeader, disc: &Disc,
partition: &mut dyn PartitionBase, partition: &mut dyn PartitionBase,
out_dir: &Path, out_dir: &Path,
is_wii: bool, is_wii: bool,
quiet: bool, quiet: bool,
) -> nod::Result<()> { ) -> nod::Result<()> {
let meta = partition.meta()?; let meta = partition.meta()?;
extract_sys_files(header, meta.as_ref(), out_dir, quiet)?; extract_sys_files(disc, meta.as_ref(), out_dir, quiet)?;
// Extract FST // Extract FST
let files_dir = out_dir.join("files"); let files_dir = out_dir.join("files");
@ -133,7 +131,7 @@ fn extract_partition(
} }
fn extract_sys_files( fn extract_sys_files(
header: &DiscHeader, disc: &Disc,
data: &PartitionMeta, data: &PartitionMeta,
out_dir: &Path, out_dir: &Path,
quiet: bool, quiet: bool,
@ -148,11 +146,14 @@ fn extract_sys_files(
extract_file(data.raw_dol.as_ref(), &sys_dir.join("main.dol"), quiet)?; extract_file(data.raw_dol.as_ref(), &sys_dir.join("main.dol"), quiet)?;
// Wii files // Wii files
if header.is_wii() { let disc_header = disc.header();
if disc_header.is_wii() {
let disc_dir = out_dir.join("disc"); let disc_dir = out_dir.join("disc");
fs::create_dir_all(&disc_dir) fs::create_dir_all(&disc_dir)
.with_context(|| format!("Creating directory {}", display(&disc_dir)))?; .with_context(|| format!("Creating directory {}", display(&disc_dir)))?;
extract_file(&header.as_bytes()[..0x100], &disc_dir.join("header.bin"), quiet)?; extract_file(&disc_header.as_bytes()[..0x100], &disc_dir.join("header.bin"), quiet)?;
if let Some(region) = disc.region() {
extract_file(region, &disc_dir.join("region.bin"), quiet)?;
} }
if let Some(ticket) = data.raw_ticket.as_deref() { if let Some(ticket) = data.raw_ticket.as_deref() {
extract_file(ticket, &out_dir.join("ticket.bin"), quiet)?; extract_file(ticket, &out_dir.join("ticket.bin"), quiet)?;
@ -166,6 +167,7 @@ fn extract_sys_files(
if let Some(h3_table) = data.raw_h3_table.as_deref() { if let Some(h3_table) = data.raw_h3_table.as_deref() {
extract_file(h3_table, &out_dir.join("h3.bin"), quiet)?; extract_file(h3_table, &out_dir.join("h3.bin"), quiet)?;
} }
}
Ok(()) Ok(())
} }
@ -182,7 +184,7 @@ fn extract_file(bytes: &[u8], out_path: &Path, quiet: bool) -> nod::Result<()> {
} }
fn extract_node( fn extract_node(
node: &Node, node: Node,
partition: &mut dyn PartitionBase, partition: &mut dyn PartitionBase,
base_path: &Path, base_path: &Path,
name: &str, name: &str,
@ -197,9 +199,8 @@ fn extract_node(
Size::from_bytes(node.length()).format().with_base(Base::Base10) Size::from_bytes(node.length()).format().with_base(Base::Base10)
); );
} }
let file = File::create(&file_path) let mut file = File::create(&file_path)
.with_context(|| format!("Creating file {}", display(&file_path)))?; .with_context(|| format!("Creating file {}", display(&file_path)))?;
let mut w = BufWriter::with_capacity(partition.ideal_buffer_size(), file);
let mut r = partition.open_file(node).with_context(|| { let mut r = partition.open_file(node).with_context(|| {
format!( format!(
"Opening file {} on disc for reading (offset {}, size {})", "Opening file {} on disc for reading (offset {}, size {})",
@ -208,7 +209,16 @@ fn extract_node(
node.length() node.length()
) )
})?; })?;
io::copy(&mut r, &mut w).with_context(|| format!("Extracting file {}", display(&file_path)))?; loop {
w.flush().with_context(|| format!("Flushing file {}", display(&file_path)))?; let buf =
r.fill_buf().with_context(|| format!("Extracting file {}", display(&file_path)))?;
let len = buf.len();
if len == 0 {
break;
}
file.write_all(buf).with_context(|| format!("Writing file {}", display(&file_path)))?;
r.consume(len);
}
file.flush().with_context(|| format!("Flushing file {}", display(&file_path)))?;
Ok(()) Ok(())
} }

View File

@ -92,11 +92,7 @@ fn info_file(path: &Path) -> nod::Result<()> {
} else if header.is_gamecube() { } else if header.is_gamecube() {
// TODO // TODO
} else { } else {
println!( println!("Invalid GC/Wii magic: {:#x?}/{:#x?}", header.gcn_magic, header.wii_magic);
"Invalid GC/Wii magic: {:#010X}/{:#010X}",
header.gcn_magic.get(),
header.wii_magic.get()
);
} }
println!(); println!();
Ok(()) Ok(())

View File

@ -3,6 +3,9 @@ use argp::FromArgs;
pub mod cmd; pub mod cmd;
pub(crate) mod util; pub(crate) mod util;
// Re-export nod
pub use nod;
#[derive(FromArgs, Debug)] #[derive(FromArgs, Debug)]
#[argp(subcommand)] #[argp(subcommand)]
pub enum SubCommand { pub enum SubCommand {

View File

@ -1,5 +1,11 @@
mod argp_version; mod argp_version;
// musl's allocator is very slow, so use mimalloc when targeting musl.
// Otherwise, use the system allocator to avoid extra code size.
#[cfg(target_env = "musl")]
#[global_allocator]
static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc;
use std::{env, error::Error, ffi::OsStr, fmt, path::PathBuf, str::FromStr}; use std::{env, error::Error, ffi::OsStr, fmt, path::PathBuf, str::FromStr};
use argp::{FromArgValue, FromArgs}; use argp::{FromArgValue, FromArgs};

View File

@ -14,7 +14,7 @@ pub struct PathDisplay<'a> {
path: &'a Path, path: &'a Path,
} }
impl<'a> fmt::Display for PathDisplay<'a> { impl fmt::Display for PathDisplay<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut first = true; let mut first = true;
for segment in self.path.iter() { for segment in self.path.iter() {

View File

@ -10,7 +10,7 @@ use std::{
use hex::deserialize as deserialize_hex; use hex::deserialize as deserialize_hex;
use nod::{array_ref, Result}; use nod::{array_ref, Result};
use serde::Deserialize; use serde::Deserialize;
use zerocopy::{AsBytes, FromBytes, FromZeroes}; use zerocopy::{FromBytes, FromZeros, Immutable, IntoBytes, KnownLayout};
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct GameResult<'a> { pub struct GameResult<'a> {
@ -33,18 +33,15 @@ impl<'a> Iterator for EntryIter<'a> {
type Item = GameResult<'a>; type Item = GameResult<'a>;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
let header: &Header = Header::ref_from_prefix(self.data).unwrap(); let (header, remaining) = Header::ref_from_prefix(self.data).ok()?;
assert_eq!(header.entry_size as usize, size_of::<GameEntry>()); assert_eq!(header.entry_size as usize, size_of::<GameEntry>());
if self.index >= header.entry_count as usize { if self.index >= header.entry_count as usize {
return None; return None;
} }
let entries_size = header.entry_count as usize * size_of::<GameEntry>(); let entries_size = header.entry_count as usize * size_of::<GameEntry>();
let entries: &[GameEntry] = GameEntry::slice_from( let entries = <[GameEntry]>::ref_from_bytes(&remaining[..entries_size]).ok()?;
&self.data[size_of::<Header>()..size_of::<Header>() + entries_size], let string_table = &self.data[size_of::<Header>() + entries_size..];
)
.unwrap();
let string_table: &[u8] = &self.data[size_of::<Header>() + entries_size..];
let entry = &entries[self.index]; let entry = &entries[self.index];
let offset = entry.string_table_offset as usize; let offset = entry.string_table_offset as usize;
@ -57,14 +54,12 @@ impl<'a> Iterator for EntryIter<'a> {
pub fn find_by_crc32(crc32: u32) -> Option<GameResult<'static>> { pub fn find_by_crc32(crc32: u32) -> Option<GameResult<'static>> {
let data = loaded_data(); let data = loaded_data();
let header: &Header = Header::ref_from_prefix(data).unwrap(); let (header, remaining) = Header::ref_from_prefix(data).ok()?;
assert_eq!(header.entry_size as usize, size_of::<GameEntry>()); assert_eq!(header.entry_size as usize, size_of::<GameEntry>());
let entries_size = header.entry_count as usize * size_of::<GameEntry>(); let entries_size = header.entry_count as usize * size_of::<GameEntry>();
let entries: &[GameEntry] = let (entries_buf, string_table) = remaining.split_at(entries_size);
GameEntry::slice_from(&data[size_of::<Header>()..size_of::<Header>() + entries_size]) let entries = <[GameEntry]>::ref_from_bytes(entries_buf).ok()?;
.unwrap();
let string_table: &[u8] = &data[size_of::<Header>() + entries_size..];
// Binary search by CRC32 // Binary search by CRC32
let index = entries.binary_search_by_key(&crc32, |entry| entry.crc32).ok()?; let index = entries.binary_search_by_key(&crc32, |entry| entry.crc32).ok()?;
@ -84,7 +79,7 @@ fn loaded_data() -> &'static [u8] {
LOADED LOADED
.get_or_init(|| { .get_or_init(|| {
let size = zstd::zstd_safe::get_frame_content_size(BUILTIN).unwrap().unwrap() as usize; let size = zstd::zstd_safe::get_frame_content_size(BUILTIN).unwrap().unwrap() as usize;
let mut out = <u8>::new_box_slice_zeroed(size); let mut out = <[u8]>::new_box_zeroed_with_elems(size).unwrap();
let out_size = zstd::bulk::Decompressor::new() let out_size = zstd::bulk::Decompressor::new()
.unwrap() .unwrap()
.decompress_to_buffer(BUILTIN, out.as_mut()) .decompress_to_buffer(BUILTIN, out.as_mut())
@ -126,7 +121,7 @@ pub fn load_dats<'a>(paths: impl Iterator<Item = &'a Path>) -> Result<()> {
let entries_size = entries.len() * size_of::<GameEntry>(); let entries_size = entries.len() * size_of::<GameEntry>();
let string_table_size = entries.iter().map(|(_, name)| name.len() + 4).sum::<usize>(); let string_table_size = entries.iter().map(|(_, name)| name.len() + 4).sum::<usize>();
let total_size = size_of::<Header>() + entries_size + string_table_size; let total_size = size_of::<Header>() + entries_size + string_table_size;
let mut result = <u8>::new_box_slice_zeroed(total_size); let mut result = <[u8]>::new_box_zeroed_with_elems(total_size)?;
let mut out = Cursor::new(result.as_mut()); let mut out = Cursor::new(result.as_mut());
// Write game entries // Write game entries
@ -152,7 +147,7 @@ pub fn load_dats<'a>(paths: impl Iterator<Item = &'a Path>) -> Result<()> {
} }
// Keep in sync with build.rs // Keep in sync with build.rs
#[derive(Clone, Debug, AsBytes, FromBytes, FromZeroes)] #[derive(Clone, Debug, IntoBytes, FromBytes, Immutable, KnownLayout)]
#[repr(C, align(4))] #[repr(C, align(4))]
struct Header { struct Header {
entry_count: u32, entry_count: u32,
@ -160,7 +155,7 @@ struct Header {
} }
// Keep in sync with build.rs // Keep in sync with build.rs
#[derive(Clone, Debug, AsBytes, FromBytes, FromZeroes)] #[derive(Clone, Debug, IntoBytes, FromBytes, Immutable, KnownLayout)]
#[repr(C, align(4))] #[repr(C, align(4))]
struct GameEntry { struct GameEntry {
crc32: u32, crc32: u32,

View File

@ -11,7 +11,7 @@ use std::{
use indicatif::{ProgressBar, ProgressState, ProgressStyle}; use indicatif::{ProgressBar, ProgressState, ProgressStyle};
use nod::{Compression, Disc, DiscHeader, DiscMeta, OpenOptions, Result, ResultContext}; use nod::{Compression, Disc, DiscHeader, DiscMeta, OpenOptions, Result, ResultContext};
use size::Size; use size::Size;
use zerocopy::FromZeroes; use zerocopy::FromZeros;
use crate::util::{ use crate::util::{
digest::{digest_thread, DigestResult}, digest::{digest_thread, DigestResult},
@ -117,7 +117,7 @@ pub fn convert_and_verify(in_file: &Path, out_file: Option<&Path>, md5: bool) ->
}); });
let mut total_read = 0u64; let mut total_read = 0u64;
let mut buf = <u8>::new_box_slice_zeroed(BUFFER_SIZE); let mut buf = <[u8]>::new_box_zeroed_with_elems(BUFFER_SIZE)?;
while total_read < disc_size { while total_read < disc_size {
let read = min(BUFFER_SIZE as u64, disc_size - total_read) as usize; let read = min(BUFFER_SIZE as u64, disc_size - total_read) as usize;
disc.read_exact(&mut buf[..read]).with_context(|| { disc.read_exact(&mut buf[..read]).with_context(|| {