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
on: [ push, pull_request ]
on:
pull_request:
push:
paths-ignore:
- '*.md'
- 'LICENSE*'
workflow_dispatch:
env:
BUILD_PROFILE: release-lto
CARGO_BIN_NAME: nodtool
CARGO_TARGET_DIR: target
CARGO_INCREMENTAL: 0
jobs:
check:
@ -13,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
toolchain: [ stable, 1.73.0, nightly ]
toolchain: [ stable, 1.74.0, nightly ]
fail-fast: false
env:
RUSTFLAGS: -D warnings
@ -79,7 +85,9 @@ jobs:
run: cargo test --release
build:
name: Build
name: Build nodtool
env:
CARGO_BIN_NAME: nodtool
strategy:
matrix:
include:
@ -88,20 +96,20 @@ jobs:
name: linux-x86_64
build: zigbuild
features: asm
# - platform: ubuntu-latest
# target: i686-unknown-linux-musl
# name: linux-i686
# build: zigbuild
# features: asm
- platform: ubuntu-latest
target: i686-unknown-linux-musl
name: linux-i686
build: zigbuild
features: asm
- platform: ubuntu-latest
target: aarch64-unknown-linux-musl
name: linux-aarch64
build: zigbuild
features: nightly
- platform: ubuntu-latest
target: armv7-unknown-linux-musleabi
name: linux-armv7l
build: zigbuild
- platform: windows-latest
target: i686-pc-windows-msvc
name: windows-x86
build: build
features: default
- platform: windows-latest
target: x86_64-pc-windows-msvc
@ -135,20 +143,20 @@ jobs:
sudo apt-get -y install ${{ matrix.packages }}
- name: Install cargo-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
uses: dtolnay/rust-toolchain@nightly
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- 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
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.name }}
name: ${{ env.CARGO_BIN_NAME }}-${{ matrix.name }}
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 }}.exe
if-no-files-found: error
@ -159,6 +167,20 @@ jobs:
runs-on: ubuntu-latest
needs: [ build ]
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
uses: actions/download-artifact@v4
with:
@ -166,12 +188,28 @@ jobs:
- name: Rename artifacts
working-directory: artifacts
run: |
set -euo pipefail
mkdir ../out
for i in */*/$BUILD_PROFILE/$CARGO_BIN_NAME*; do
mv "$i" "../out/$(sed -E "s/([^/]+)\/[^/]+\/$BUILD_PROFILE\/($CARGO_BIN_NAME)/\2-\1/" <<< "$i")"
for dir in */; do
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
ls -R ../out
- name: Release
uses: softprops/action-gh-release@4634c16e79c963813287e889244c50009e7f0981
uses: softprops/action-gh-release@v2
with:
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"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "adler2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "aes"
version = "0.8.4"
@ -21,9 +27,9 @@ dependencies = [
[[package]]
name = "aho-corasick"
version = "1.1.2"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
@ -57,9 +63,9 @@ checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
[[package]]
name = "bitflags"
version = "2.4.2"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
[[package]]
name = "block-buffer"
@ -79,12 +85,6 @@ dependencies = [
"generic-array",
]
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bzip2"
version = "0.4.4"
@ -117,11 +117,13 @@ dependencies = [
[[package]]
name = "cc"
version = "1.0.86"
version = "1.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f9fa1897e4325be0d68d48df6aa1a71ac2ed4d27723887e7754192705350730"
checksum = "812acba72f0a070b003d3697490d2b55b837230ae7c6c6497f05cc2ddbb8d938"
dependencies = [
"jobserver",
"libc",
"shlex",
]
[[package]]
@ -155,9 +157,9 @@ dependencies = [
[[package]]
name = "cpufeatures"
version = "0.2.12"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504"
checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0"
dependencies = [
"libc",
]
@ -192,9 +194,9 @@ dependencies = [
[[package]]
name = "crossbeam-utils"
version = "0.8.19"
version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
[[package]]
name = "crypto-common"
@ -218,15 +220,15 @@ dependencies = [
[[package]]
name = "dyn-clone"
version = "1.0.16"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d"
checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
[[package]]
name = "either"
version = "1.10.0"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a"
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
[[package]]
name = "enable-ansi-support"
@ -245,9 +247,9 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
[[package]]
name = "encoding_rs"
version = "0.8.33"
version = "0.8.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1"
checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59"
dependencies = [
"cfg-if",
]
@ -305,9 +307,9 @@ dependencies = [
[[package]]
name = "instant"
version = "0.1.12"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
dependencies = [
"cfg-if",
]
@ -320,39 +322,48 @@ checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45"
[[package]]
name = "itertools"
version = "0.12.1"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
name = "jobserver"
version = "0.1.32"
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]]
name = "libc"
version = "0.2.153"
version = "0.2.159"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
[[package]]
name = "liblzma"
version = "0.2.3"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "599133771f99c14ca089a8db3a4565f482ea6eeb66991b262bffc2b72acff69c"
checksum = "a7c45fc6fcf5b527d3cf89c1dee8c327943984b0dc8bfcf6e100473b00969e63"
dependencies = [
"liblzma-sys",
]
[[package]]
name = "liblzma-sys"
version = "0.2.5"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be9aaba5f9c8f8f615d41570909338b6284fbb1813dc057ecc68563d98a65097"
checksum = "6630cb23edeb2e563cd6c30d4117554c69646871455843c33ddcb1d9aef82ecf"
dependencies = [
"cc",
"libc",
@ -360,10 +371,20 @@ dependencies = [
]
[[package]]
name = "log"
version = "0.4.20"
name = "libmimalloc-sys"
version = "0.1.39"
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]]
name = "matchers"
@ -387,31 +408,40 @@ dependencies = [
[[package]]
name = "md5-asm"
version = "0.5.1"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61d33bc4cdfe5c60340e282bbbee0a6e2bc57f0b9279bb3489c5004d12492e5c"
checksum = "d19b8ee7fc7d812058d3b708c7f719efd0713d53854648e4223c6fcae709e2df"
dependencies = [
"cc",
]
[[package]]
name = "memchr"
version = "2.7.1"
version = "2.7.4"
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]]
name = "miniz_oxide"
version = "0.7.2"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7"
checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1"
dependencies = [
"adler",
"adler2",
]
[[package]]
name = "nod"
version = "1.2.0"
version = "1.4.4"
dependencies = [
"adler",
"aes",
@ -434,7 +464,7 @@ dependencies = [
[[package]]
name = "nodtool"
version = "1.2.0"
version = "1.4.4"
dependencies = [
"argp",
"base16ct",
@ -446,6 +476,7 @@ dependencies = [
"itertools",
"log",
"md-5",
"mimalloc",
"nod",
"quick-xml",
"serde",
@ -478,9 +509,12 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
[[package]]
name = "once_cell"
version = "1.19.0"
version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
checksum = "82881c4be219ab5faaf2ad5e5e5ecdff8c66bd7402ca3160975c93b24961afd1"
dependencies = [
"portable-atomic",
]
[[package]]
name = "overload"
@ -490,27 +524,27 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "pin-project-lite"
version = "0.2.13"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
[[package]]
name = "pkg-config"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
[[package]]
name = "portable-atomic"
version = "1.6.0"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0"
checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2"
[[package]]
name = "proc-macro2"
version = "1.0.78"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
dependencies = [
"unicode-ident",
]
@ -529,9 +563,9 @@ dependencies = [
[[package]]
name = "quick-xml"
version = "0.31.0"
version = "0.36.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33"
checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe"
dependencies = [
"memchr",
"serde",
@ -539,18 +573,18 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.35"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rayon"
version = "1.8.1"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7237101a77a10773db45d62004a272517633fbcc3df19d96455ede1122e051"
checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
dependencies = [
"either",
"rayon-core",
@ -568,14 +602,14 @@ dependencies = [
[[package]]
name = "regex"
version = "1.10.3"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15"
checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata 0.4.5",
"regex-syntax 0.8.2",
"regex-automata 0.4.8",
"regex-syntax 0.8.5",
]
[[package]]
@ -589,13 +623,13 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.4.5"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd"
checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax 0.8.2",
"regex-syntax 0.8.5",
]
[[package]]
@ -606,28 +640,28 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
name = "regex-syntax"
version = "0.8.2"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "serde"
version = "1.0.197"
version = "1.0.210"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.197"
version = "1.0.210"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.50",
"syn 2.0.79",
]
[[package]]
@ -644,9 +678,9 @@ dependencies = [
[[package]]
name = "sha1-asm"
version = "0.5.2"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ba6947745e7f86be3b8af00b7355857085dbdf8901393c89514510eb61f4e21"
checksum = "286acebaf8b67c1130aedffad26f594eff0c1292389158135327d2e23aed582b"
dependencies = [
"cc",
]
@ -660,6 +694,12 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "size"
version = "0.4.1"
@ -668,15 +708,15 @@ checksum = "9fed904c7fb2856d868b92464fc8fa597fce366edea1a9cbfaa8cb5fe080bd6d"
[[package]]
name = "smallvec"
version = "1.13.1"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "supports-color"
version = "3.0.0"
version = "3.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9829b314621dfc575df4e409e79f9d6a66a3bd707ab73f23cb4aa3a854ac854f"
checksum = "8775305acf21c96926c900ad056abeef436701108518cf890020387236ac5a77"
dependencies = [
"is_ci",
]
@ -694,9 +734,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.50"
version = "2.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74f1bdc9872430ce9b75da68329d1c1746faf50ffac5f19e02b71e37ff881ffb"
checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590"
dependencies = [
"proc-macro2",
"quote",
@ -705,22 +745,22 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.57"
version = "1.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b"
checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.57"
version = "1.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81"
checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.50",
"syn 2.0.79",
]
[[package]]
@ -752,7 +792,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.50",
"syn 2.0.79",
]
[[package]]
@ -811,15 +851,15 @@ dependencies = [
[[package]]
name = "unicode-ident"
version = "1.0.12"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
[[package]]
name = "unicode-width"
version = "0.1.11"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]]
name = "valuable"
@ -829,9 +869,9 @@ checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "version_check"
version = "0.9.4"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "winapi"
@ -881,17 +921,18 @@ dependencies = [
[[package]]
name = "windows-targets"
version = "0.52.0"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm 0.52.0",
"windows_aarch64_msvc 0.52.0",
"windows_i686_gnu 0.52.0",
"windows_i686_msvc 0.52.0",
"windows_x86_64_gnu 0.52.0",
"windows_x86_64_gnullvm 0.52.0",
"windows_x86_64_msvc 0.52.0",
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
@ -902,9 +943,9 @@ checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.0"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
@ -914,9 +955,9 @@ checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.0"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
@ -926,9 +967,15 @@ checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
[[package]]
name = "windows_i686_gnu"
version = "0.52.0"
version = "0.52.6"
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]]
name = "windows_i686_msvc"
@ -938,9 +985,9 @@ checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
[[package]]
name = "windows_i686_msvc"
version = "0.52.0"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
@ -950,9 +997,9 @@ checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.0"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
@ -962,9 +1009,9 @@ checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.0"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
@ -974,60 +1021,59 @@ checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.0"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "xxhash-rust"
version = "0.8.10"
version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "927da81e25be1e1a2901d59b81b37dd2efd1fc9c9345a55007f09bf5a2d3ee03"
checksum = "6a5cbf750400958819fb6178eaa83bee5cd9c29a26a40cc241df8c70fdd46984"
[[package]]
name = "zerocopy"
version = "0.7.32"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be"
checksum = "dd2e5ce961dea177d282ec084dca2aa411b7411199a68d79eb1beacb305a6cd9"
dependencies = [
"byteorder",
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.32"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
checksum = "b06304eeddb6081af98ac59db08c868ac197e586086b996d15a86ed70e09a754"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.50",
"syn 2.0.79",
]
[[package]]
name = "zstd"
version = "0.13.1"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d789b1514203a1120ad2429eae43a7bd32b90976a7bb8a05f7ec02fa88cc23a"
checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9"
dependencies = [
"zstd-safe",
]
[[package]]
name = "zstd-safe"
version = "7.1.0"
version = "7.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cd99b45c6bc03a018c8b8a86025678c87e55526064e38f9df301989dce7ec0a"
checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059"
dependencies = [
"zstd-sys",
]
[[package]]
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"
checksum = "75652c55c0b6f3e6f12eb786fe1bc960396bf05a1eb3bf1f3691c3610ac2e6d4"
checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa"
dependencies = [
"cc",
"pkg-config",

View File

@ -4,5 +4,15 @@ resolver = "2"
[profile.release-lto]
inherits = "release"
lto = "thin"
lto = "fat"
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]
[Build Status]: https://github.com/encounter/nod-rs/actions/workflows/build.yaml/badge.svg
[actions]: https://github.com/encounter/nod-rs/actions
[Build Status]: https://github.com/encounter/nod/actions/workflows/build.yaml/badge.svg
[actions]: https://github.com/encounter/nod/actions
[Latest Version]: https://img.shields.io/crates/v/nod.svg
[crates.io]: https://crates.io/crates/nod
[Api Rustdoc]: https://img.shields.io/badge/api-rustdoc-blue.svg
[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.
@ -14,16 +14,25 @@ Originally based on the C++ library [nod](https://github.com/AxioDL/nod),
but does not currently support authoring.
Currently supported file formats:
- ISO (GCM)
- WIA / RVZ
- WBFS (+ NKit 2 lossless)
- CISO (+ NKit 2 lossless)
- NFS (Wii U VC)
- GCZ
- TGC
## 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
@ -53,7 +62,7 @@ Converts any supported format to raw ISO.
```shell
nodtool convert /path/to/game.wia /path/to/game.iso
```
```
### verify
@ -113,8 +122,8 @@ std::io::copy(&mut disc, &mut out)
Licensed under either of
* 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)
- 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>)
at your option.

View File

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

View File

@ -3,7 +3,7 @@
use std::{borrow::Cow, ffi::CStr, mem::size_of};
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};
@ -19,13 +19,13 @@ pub enum NodeKind {
}
/// 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))]
pub struct Node {
kind: u8,
// u24 big-endian
name_offset: [u8; 3],
offset: U32,
pub(crate) offset: U32,
length: U32,
}
@ -33,6 +33,7 @@ static_assert!(size_of::<Node>() == 12);
impl Node {
/// File system node kind.
#[inline]
pub fn kind(&self) -> NodeKind {
match self.kind {
0 => NodeKind::File,
@ -42,12 +43,15 @@ impl Node {
}
/// Whether the node is a file.
#[inline]
pub fn is_file(&self) -> bool { self.kind == 0 }
/// Whether the node is a directory.
#[inline]
pub fn is_dir(&self) -> bool { self.kind == 1 }
/// Offset in the string table to the filename.
#[inline]
pub fn name_offset(&self) -> u32 {
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 directories, this is the parent node index in the FST.
#[inline]
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
} else {
self.offset.get() as u64
@ -68,6 +73,7 @@ impl Node {
/// For directories, this is the child end index in the FST.
///
/// Number of child files and directories recursively is `length - offset`.
#[inline]
pub fn length(&self) -> u64 { self.length.get() as u64 }
}
@ -81,8 +87,9 @@ pub struct Fst<'a> {
impl<'a> Fst<'a> {
/// 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> {
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");
};
// String table starts after the last node
@ -91,15 +98,17 @@ impl<'a> Fst<'a> {
return Err("FST string table out of bounds");
}
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 })
}
/// Iterate over the nodes in the FST.
#[inline]
pub fn iter(&self) -> FstIter { FstIter { fst: self, idx: 1 } }
/// 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(|| {
format!(
"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(|_| {
format!("FST: name at offset {} not null-terminated", node.name_offset())
})?;
let (decoded, _, errors) = SHIFT_JIS.decode(c_string.to_bytes());
if errors {
return Err(format!("FST: Failed to decode name at offset {}", node.name_offset()));
}
let (decoded, _, _) = SHIFT_JIS.decode(c_string.to_bytes());
// Ignore decoding errors, we can't do anything about them. Consumers may check for
// U+FFFD (REPLACEMENT CHARACTER), or fetch the raw bytes from the string table.
Ok(decoded)
}
/// 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 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 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 let Some(next) = split.next() {
current = next;
} else {
current = next_non_empty(&mut split);
if current.is_empty() {
return Some((idx, node));
}
// Descend into directory
@ -158,13 +169,24 @@ pub struct 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> {
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);
self.idx += 1;
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::{
cmp::min,
io,
io::{Read, Seek, SeekFrom},
io::{BufRead, Read, Seek, SeekFrom},
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::{
disc::{
ApploaderHeader, DiscHeader, DolHeader, PartitionBase, PartitionHeader, PartitionMeta,
BI2_SIZE, BOOT_SIZE, SECTOR_SIZE,
},
fst::{Node, NodeKind},
disc::streams::OwnedFileStream,
io::block::{Block, BlockIO},
streams::{ReadStream, SharedWindowedReadStream},
util::read::{read_box, read_box_slice, read_vec},
Result, ResultContext,
};
@ -35,9 +33,9 @@ impl Clone for PartitionGC {
Self {
io: self.io.clone(),
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,
sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed(),
sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed().unwrap(),
sector: u32::MAX,
pos: 0,
disc_header: self.disc_header.clone(),
@ -51,9 +49,9 @@ impl PartitionGC {
Ok(Box::new(Self {
io: inner,
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,
sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed(),
sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed().unwrap(),
sector: u32::MAX,
pos: 0,
disc_header,
@ -63,8 +61,8 @@ impl PartitionGC {
pub fn into_inner(self) -> Box<dyn BlockIO> { self.io }
}
impl Read for PartitionGC {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
impl BufRead for PartitionGC {
fn fill_buf(&mut self) -> io::Result<&[u8]> {
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;
@ -86,9 +84,20 @@ impl Read for PartitionGC {
}
let offset = (self.pos % SECTOR_SIZE as u64) as usize;
let len = min(buf.len(), SECTOR_SIZE - offset);
buf[..len].copy_from_slice(&self.sector_buf[offset..offset + len]);
self.pos += len as u64;
Ok(&self.sector_buf[offset..])
}
#[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)
}
}
@ -115,21 +124,35 @@ impl PartitionBase for PartitionGC {
read_part_meta(self, false)
}
fn open_file(&mut self, node: &Node) -> io::Result<SharedWindowedReadStream> {
assert_eq!(node.kind(), NodeKind::File);
self.new_window(node.offset(false), node.length())
fn open_file(&mut self, node: Node) -> io::Result<FileStream> {
if !node.is_file() {
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(
reader: &mut dyn ReadStream,
reader: &mut dyn PartitionBase,
is_wii: bool,
) -> Result<Box<PartitionMeta>> {
// 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
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
let mut raw_apploader: Vec<u8> =
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(
size_of::<ApploaderHeader>()
+ apploader_header.size.get() as usize
@ -147,6 +170,7 @@ pub(crate) fn read_part_meta(
reader
.read_exact(&mut raw_apploader[size_of::<ApploaderHeader>()..])
.context("Reading apploader")?;
let raw_apploader = raw_apploader.into_boxed_slice();
// fst.bin
reader
@ -167,7 +191,7 @@ pub(crate) fn read_part_meta(
.context("Seeking to DOL offset")?;
let mut raw_dol: Vec<u8> =
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
.text_offs
.iter()
@ -184,13 +208,14 @@ pub(crate) fn read_part_meta(
.unwrap_or(size_of::<DolHeader>() as u32);
raw_dol.resize(dol_size as usize, 0);
reader.read_exact(&mut raw_dol[size_of::<DolHeader>()..]).context("Reading DOL")?;
let raw_dol = raw_dol.into_boxed_slice();
Ok(Box::new(PartitionMeta {
raw_boot,
raw_bi2,
raw_apploader: raw_apploader.into_boxed_slice(),
raw_apploader,
raw_fst,
raw_dol: raw_dol.into_boxed_slice(),
raw_dol,
raw_ticket: None,
raw_tmd: None,
raw_cert_chain: None,

View File

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

View File

@ -5,33 +5,40 @@ use std::{
ffi::CStr,
fmt::{Debug, Display, Formatter},
io,
io::{BufRead, Seek},
mem::size_of,
str::from_utf8,
};
use dyn_clone::DynClone;
use zerocopy::{big_endian::*, AsBytes, FromBytes, FromZeroes};
use zerocopy::{big_endian::*, FromBytes, Immutable, IntoBytes, KnownLayout};
use crate::{
disc::wii::{Ticket, TmdHeader},
fst::Node,
static_assert,
streams::{ReadStream, SharedWindowedReadStream},
Fst, Result,
};
use crate::{io::MagicBytes, static_assert, Result};
pub(crate) mod fst;
pub(crate) mod gcn;
pub(crate) mod hashes;
pub(crate) mod reader;
pub(crate) mod streams;
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;
/// 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.
///
/// 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))]
pub struct DiscHeader {
/// Game ID (e.g. GM8E01 for Metroid Prime)
@ -47,9 +54,9 @@ pub struct DiscHeader {
/// Padding
_pad1: [u8; 14],
/// 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
pub gcn_magic: U32,
pub gcn_magic: MagicBytes,
/// Game title
pub game_title: [u8; 64],
/// If 1, disc omits partition hashes
@ -64,9 +71,11 @@ static_assert!(size_of::<DiscHeader>() == 0x400);
impl DiscHeader {
/// Game ID as a string.
#[inline]
pub fn game_id_str(&self) -> &str { from_utf8(&self.game_id).unwrap_or("[invalid]") }
/// Game title as a string.
#[inline]
pub fn game_title_str(&self) -> &str {
CStr::from_bytes_until_nul(&self.game_title)
.ok()
@ -75,10 +84,12 @@ impl DiscHeader {
}
/// 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.
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.
@ -86,7 +97,7 @@ impl DiscHeader {
/// **GameCube**: Always follows the disc header.
///
/// **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))]
pub struct PartitionHeader {
/// Debug monitor offset
@ -105,9 +116,9 @@ pub struct PartitionHeader {
pub fst_max_size: U32,
/// File system table load address
pub fst_memory_address: U32,
/// User position
pub user_position: U32,
/// User size
/// User data offset
pub user_offset: U32,
/// User data size
pub user_size: U32,
/// Padding
_pad2: [u8; 4],
@ -117,6 +128,7 @@ static_assert!(size_of::<PartitionHeader>() == 0x40);
impl PartitionHeader {
/// Offset within the partition to the main DOL.
#[inline]
pub fn dol_offset(&self, is_wii: bool) -> u64 {
if is_wii {
self.dol_offset.get() as u64 * 4
@ -126,6 +138,7 @@ impl PartitionHeader {
}
/// Offset within the partition to the file system table (FST).
#[inline]
pub fn fst_offset(&self, is_wii: bool) -> u64 {
if is_wii {
self.fst_offset.get() as u64 * 4
@ -135,6 +148,7 @@ impl PartitionHeader {
}
/// Size of the file system table (FST).
#[inline]
pub fn fst_size(&self, is_wii: bool) -> u64 {
if is_wii {
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.
#[inline]
pub fn fst_max_size(&self, is_wii: bool) -> u64 {
if is_wii {
self.fst_max_size.get() as u64 * 4
@ -154,7 +169,7 @@ impl PartitionHeader {
}
/// Apploader header.
#[derive(Debug, PartialEq, Clone, FromBytes, FromZeroes, AsBytes)]
#[derive(Debug, PartialEq, Clone, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(4))]
pub struct ApploaderHeader {
/// Apploader build date
@ -171,6 +186,7 @@ pub struct ApploaderHeader {
impl ApploaderHeader {
/// Apploader build date as a string.
#[inline]
pub fn date_str(&self) -> Option<&str> {
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;
/// Dolphin executable (DOL) header.
#[derive(Debug, Clone, FromBytes, FromZeroes)]
#[derive(Debug, Clone, FromBytes, Immutable, KnownLayout)]
pub struct DolHeader {
/// Text section offsets
pub text_offs: [U32; DOL_MAX_TEXT_SECTIONS],
@ -222,6 +238,7 @@ pub enum PartitionKind {
}
impl Display for PartitionKind {
#[inline]
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Data => write!(f, "Data"),
@ -237,6 +254,7 @@ impl Display for PartitionKind {
impl PartitionKind {
/// Returns the directory name for the partition kind.
#[inline]
pub fn dir_name(&self) -> Cow<str> {
match self {
Self::Data => Cow::Borrowed("DATA"),
@ -251,6 +269,7 @@ impl PartitionKind {
}
impl From<u32> for PartitionKind {
#[inline]
fn from(v: u32) -> Self {
match v {
0 => Self::Data,
@ -262,11 +281,11 @@ impl From<u32> for PartitionKind {
}
/// 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.
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.
///
/// # Examples
@ -294,12 +313,36 @@ pub trait PartitionBase: DynClone + ReadStream + Send + Sync {
/// 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.
/// GameCube discs have a data block size of 0x8000,
/// whereas Wii discs have a data block size of 0x7C00.
fn ideal_buffer_size(&self) -> usize;
/// Consumes the partition instance and returns a windowed stream.
///
/// # Examples
///
/// ```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);
@ -318,10 +361,10 @@ pub struct PartitionMeta {
pub raw_bi2: Box<[u8; BI2_SIZE]>,
/// Apploader (apploader.bin)
pub raw_apploader: Box<[u8]>,
/// File system table (fst.bin)
pub raw_fst: Box<[u8]>,
/// Main binary (main.dol)
pub raw_dol: Box<[u8]>,
/// File system table (fst.bin)
pub raw_fst: Box<[u8]>,
/// Ticket (ticket.bin, Wii only)
pub raw_ticket: Option<Box<[u8]>>,
/// TMD (tmd.bin, Wii only)
@ -334,34 +377,41 @@ pub struct PartitionMeta {
impl PartitionMeta {
/// A view into the disc header.
#[inline]
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.
#[inline]
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.
#[inline]
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).
#[inline]
pub fn fst(&self) -> Result<Fst, &'static str> { Fst::new(&self.raw_fst) }
/// 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)
#[inline]
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)
#[inline]
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::{
cmp::min,
io,
io::{Read, Seek, SeekFrom},
io::{BufRead, Read, Seek, SeekFrom},
};
use zerocopy::FromZeroes;
use zerocopy::FromZeros;
use super::{
gcn::PartitionGC,
hashes::{rebuild_hashes, HashTable},
wii::{PartitionWii, WiiPartEntry, WiiPartGroup, WiiPartitionHeader, WII_PART_GROUP_OFF},
DiscHeader, PartitionBase, PartitionHeader, PartitionKind, DL_DVD_SIZE, MINI_DVD_SIZE,
REGION_SIZE, SL_DVD_SIZE,
};
use crate::{
disc::{
gcn::PartitionGC,
hashes::{rebuild_hashes, HashTable},
wii::{PartitionWii, WiiPartEntry, WiiPartGroup, WiiPartitionHeader, WII_PART_GROUP_OFF},
DL_DVD_SIZE, MINI_DVD_SIZE, SL_DVD_SIZE,
},
disc::wii::REGION_OFFSET,
io::block::{Block, BlockIO, PartitionInfo},
util::read::{read_box, read_from, read_vec},
DiscHeader, DiscMeta, Error, OpenOptions, PartitionBase, PartitionHeader, PartitionKind,
Result, ResultContext, SECTOR_SIZE,
DiscMeta, Error, OpenOptions, Result, ResultContext, SECTOR_SIZE,
};
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
@ -37,6 +37,7 @@ pub struct DiscReader {
disc_header: Box<DiscHeader>,
pub(crate) partitions: Vec<PartitionInfo>,
hash_tables: Vec<HashTable>,
region: Option<[u8; REGION_SIZE]>,
}
impl Clone for DiscReader {
@ -44,15 +45,16 @@ impl Clone for DiscReader {
Self {
io: self.io.clone(),
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,
sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed(),
sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed().unwrap(),
sector_idx: u32::MAX,
pos: 0,
mode: self.mode,
disc_header: self.disc_header.clone(),
partitions: self.partitions.clone(),
hash_tables: self.hash_tables.clone(),
region: self.region,
}
}
}
@ -64,9 +66,9 @@ impl DiscReader {
let mut reader = Self {
io: inner,
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,
sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed(),
sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed()?,
sector_idx: u32::MAX,
pos: 0,
mode: if options.rebuild_encryption {
@ -74,13 +76,16 @@ impl DiscReader {
} else {
EncryptionMode::Decrypted
},
disc_header: DiscHeader::new_box_zeroed(),
disc_header: DiscHeader::new_box_zeroed()?,
partitions: vec![],
hash_tables: vec![],
region: None,
};
let disc_header: Box<DiscHeader> = read_box(&mut reader).context("Reading disc header")?;
reader.disc_header = disc_header;
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)?;
// Rebuild hashes if the format requires it
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))
}
#[inline]
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 }
#[inline]
pub fn meta(&self) -> DiscMeta { self.io.meta() }
/// Opens a new, decrypted partition read stream for the specified partition index.
@ -150,8 +161,8 @@ impl DiscReader {
}
}
impl Read for DiscReader {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
impl BufRead for DiscReader {
fn fill_buf(&mut self) -> io::Result<&[u8]> {
let block_idx = (self.pos / self.block_buf.len() 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
let offset = (self.pos % SECTOR_SIZE as u64) as usize;
let len = min(buf.len(), SECTOR_SIZE - offset);
buf[..len].copy_from_slice(&self.sector_buf[offset..offset + len]);
self.pos += len as u64;
Ok(&self.sector_buf[offset..])
}
#[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)
}
}
@ -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,
key,
header,
disc_header: DiscHeader::new_box_zeroed(),
partition_header: PartitionHeader::new_box_zeroed(),
disc_header: DiscHeader::new_box_zeroed()?,
partition_header: PartitionHeader::new_box_zeroed()?,
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::{
cmp::min,
ffi::CStr,
io,
io::{Read, Seek, SeekFrom},
io::{BufRead, Read, Seek, SeekFrom},
mem::size_of,
};
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::{
array_ref,
disc::{
gcn::{read_part_meta, PartitionGC},
PartitionBase, PartitionMeta, SECTOR_SIZE,
},
fst::{Node, NodeKind},
disc::streams::OwnedFileStream,
io::{
aes_decrypt,
block::{Block, BlockIO, PartitionInfo},
KeyBytes,
},
static_assert,
streams::{ReadStream, SharedWindowedReadStream},
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
@ -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)
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)
const RVL_CERT_ISSUER_PPKI_TICKET: &str = "Root-CA00000001-XS00000003";
#[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],
];
#[derive(Debug, PartialEq, FromBytes, FromZeroes, AsBytes)]
#[derive(Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(4))]
pub(crate) struct WiiPartEntry {
pub(crate) offset: U32,
@ -72,7 +76,7 @@ impl WiiPartEntry {
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))]
pub(crate) struct WiiPartGroup {
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 }
}
#[derive(Debug, Clone, PartialEq, FromBytes, FromZeroes, AsBytes)]
/// Signed blob header
#[derive(Debug, Clone, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(4))]
pub struct SignedHeader {
/// Signature type, always 0x00010001 (RSA-2048)
@ -97,43 +102,64 @@ pub struct SignedHeader {
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))]
pub struct TicketTimeLimit {
pub enable_time_limit: U32,
pub time_limit: U32,
pub struct TicketLimit {
/// Limit type
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))]
pub struct Ticket {
/// Signed blob header
pub header: SignedHeader,
/// Signature issuer
pub sig_issuer: [u8; 64],
/// ECDH data
pub ecdh: [u8; 60],
/// Ticket format version
pub version: u8,
_pad1: U16,
/// Title key (encrypted)
pub title_key: KeyBytes,
_pad2: u8,
/// Ticket ID
pub ticket_id: [u8; 8],
/// Console ID
pub console_id: [u8; 4],
/// Title ID
pub title_id: [u8; 8],
_pad3: U16,
/// Ticket title version
pub ticket_title_version: U16,
/// Permitted titles mask
pub permitted_titles_mask: U32,
/// Permit mask
pub permit_mask: U32,
/// Title export allowed
pub title_export_allowed: u8,
/// Common key index
pub common_key_idx: u8,
_pad4: [u8; 48],
/// Content access permissions
pub content_access_permissions: [u8; 64],
_pad5: [u8; 2],
pub time_limits: [TicketTimeLimit; 8],
/// Ticket limits
pub limits: [TicketLimit; 8],
}
static_assert!(size_of::<Ticket>() == 0x2A4);
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> {
let mut iv: KeyBytes = [0; 16];
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))]
pub struct TmdHeader {
/// Signed blob header
pub header: SignedHeader,
/// Signature issuer
pub sig_issuer: [u8; 64],
/// Version
pub version: u8,
/// CA CRL version
pub ca_crl_version: u8,
/// Signer CRL version
pub signer_crl_version: u8,
/// Is vWii title
pub is_vwii: u8,
/// IOS ID
pub ios_id: [u8; 8],
/// Title ID
pub title_id: [u8; 8],
/// Title type
pub title_type: u32,
/// Group ID
pub group_id: U16,
_pad1: [u8; 2],
/// Region
pub region: U16,
/// Ratings
pub ratings: KeyBytes,
_pad2: [u8; 12],
/// IPC mask
pub ipc_mask: [u8; 12],
_pad3: [u8; 18],
/// Access flags
pub access_flags: U32,
/// Title version
pub title_version: U16,
/// Number of contents
pub num_contents: U16,
/// Boot index
pub boot_idx: U16,
/// Minor version (unused)
pub minor_version: U16,
}
@ -188,7 +233,7 @@ static_assert!(size_of::<TmdHeader>() == 0x1E4);
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))]
pub struct WiiPartitionHeader {
pub ticket: Ticket,
@ -242,9 +287,9 @@ impl Clone for PartitionWii {
io: self.io.clone(),
partition: self.partition.clone(),
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,
sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed(),
sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed().unwrap(),
sector: u32::MAX,
pos: 0,
verify: self.verify,
@ -288,9 +333,9 @@ impl PartitionWii {
io: reader.into_inner(),
partition: partition.clone(),
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,
sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed(),
sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed()?,
sector: u32::MAX,
pos: 0,
verify: options.validate_hashes,
@ -301,12 +346,12 @@ impl PartitionWii {
}
}
impl Read for PartitionWii {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
impl BufRead for PartitionWii {
fn fill_buf(&mut self) -> io::Result<&[u8]> {
let part_sector = (self.pos / SECTOR_DATA_SIZE as u64) as u32;
let abs_sector = self.partition.data_start_sector + part_sector;
if abs_sector >= self.partition.data_end_sector {
return Ok(0);
return Ok(&[]);
}
let block_idx =
(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 len = min(buf.len(), SECTOR_DATA_SIZE - offset);
buf[..len]
.copy_from_slice(&self.sector_buf[HASHES_SIZE + offset..HASHES_SIZE + offset + len]);
self.pos += len as u64;
Ok(&self.sector_buf[HASHES_SIZE + offset..])
}
#[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)
}
}
@ -440,10 +495,23 @@ impl PartitionBase for PartitionWii {
Ok(meta)
}
fn open_file(&mut self, node: &Node) -> io::Result<SharedWindowedReadStream> {
assert_eq!(node.kind(), NodeKind::File);
self.new_window(node.offset(true), node.length())
fn open_file(&mut self, node: Node) -> io::Result<FileStream> {
if !node.is_file() {
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 zerocopy::transmute_ref;
@ -8,13 +12,22 @@ use crate::{
disc::{
hashes::HashTable,
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},
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.
pub trait BlockIO: DynClone + Send + Sync {
/// Reads a block from the disc image.
@ -78,7 +91,32 @@ pub trait BlockIO: DynClone + Send + Sync {
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>> {
let path_result = fs::canonicalize(filename);
if let Err(err) = path_result {
@ -92,17 +130,19 @@ pub fn open(filename: &Path) -> Result<Box<dyn BlockIO>> {
if !meta.unwrap().is_file() {
return Err(Error::DiscFormat(format!("Input is not a file: {}", filename.display())));
}
let magic: MagicBytes = {
let mut file =
File::open(path).with_context(|| format!("Opening file {}", filename.display()))?;
read_from(&mut file)
.with_context(|| format!("Reading magic bytes from {}", filename.display()))?
};
let io: Box<dyn BlockIO> = match magic {
crate::io::ciso::CISO_MAGIC => crate::io::ciso::DiscIOCISO::new(path)?,
#[cfg(feature = "compress-zlib")]
crate::io::gcz::GCZ_MAGIC => crate::io::gcz::DiscIOGCZ::new(path)?,
crate::io::nfs::NFS_MAGIC => match path.parent() {
let mut stream = Box::new(SplitFileReader::new(filename)?);
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) => match path.parent() {
Some(parent) if parent.is_dir() => {
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()));
}
},
crate::io::wbfs::WBFS_MAGIC => crate::io::wbfs::DiscIOWBFS::new(path)?,
crate::io::wia::WIA_MAGIC | crate::io::wia::RVZ_MAGIC => {
crate::io::wia::DiscIOWIA::new(path)?
}
_ => crate::io::iso::DiscIOISO::new(path)?,
Some(Format::Tgc) => crate::io::tgc::DiscIOTGC::new(stream)?,
Some(Format::Wbfs) => crate::io::wbfs::DiscIOWBFS::new(stream)?,
Some(Format::Wia | Format::Rvz) => crate::io::wia::DiscIOWIA::new(stream)?,
None => return Err(Error::DiscFormat("Unknown disc format".to_string())),
};
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
&& SECTOR_SIZE as u32 % io.block_size_internal() != 0
{
@ -132,7 +206,7 @@ pub fn open(filename: &Path) -> Result<Box<dyn BlockIO>> {
SECTOR_SIZE
)));
}
Ok(io)
Ok(())
}
/// Wii partition information.
@ -300,23 +374,19 @@ fn generate_junk(
partition: Option<&PartitionInfo>,
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)
} else {
(sector as u64 * SECTOR_SIZE as u64, 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();
lfg.init_with_seed(*array_ref![disc_header.game_id, 0, 4], disc_header.disc_num, pos);
let sector_end = (pos + SECTOR_SIZE as u64) & !(SECTOR_SIZE as u64 - 1);
let len = min(SECTOR_SIZE - offset, (sector_end - pos) as usize);
lfg.fill(&mut out[offset..offset + len]);
pos += len as u64;
offset += len;
}
let mut lfg = LaggedFibonacci::default();
lfg.fill_sector_chunked(
&mut out[offset..],
*array_ref![disc_header.game_id, 0, 4],
disc_header.disc_num,
pos,
);
}
fn rebuild_hash_block(out: &mut [u8; SECTOR_SIZE], part_sector: u32, partition: &PartitionInfo) {

View File

@ -2,17 +2,15 @@ use std::{
io,
io::{Read, Seek, SeekFrom},
mem::size_of,
path::Path,
};
use zerocopy::{little_endian::*, AsBytes, FromBytes, FromZeroes};
use zerocopy::{little_endian::*, FromBytes, Immutable, IntoBytes, KnownLayout};
use crate::{
disc::SECTOR_SIZE,
io::{
block::{Block, BlockIO, PartitionInfo},
block::{Block, BlockIO, DiscStream, PartitionInfo, CISO_MAGIC},
nkit::NKitHeader,
split::SplitFileReader,
Format, MagicBytes,
},
static_assert,
@ -20,11 +18,10 @@ use crate::{
DiscMeta, Error, Result, ResultContext,
};
pub const CISO_MAGIC: MagicBytes = *b"CISO";
pub const CISO_MAP_SIZE: usize = SECTOR_SIZE - 8;
/// CISO header (little endian)
#[derive(Clone, Debug, PartialEq, FromBytes, FromZeroes, AsBytes)]
#[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(4))]
struct CISOHeader {
magic: MagicBytes,
@ -36,18 +33,17 @@ static_assert!(size_of::<CISOHeader>() == SECTOR_SIZE);
#[derive(Clone)]
pub struct DiscIOCISO {
inner: SplitFileReader,
inner: Box<dyn DiscStream>,
header: CISOHeader,
block_map: [u16; CISO_MAP_SIZE],
nkit_header: Option<NKitHeader>,
}
impl DiscIOCISO {
pub fn new(filename: &Path) -> Result<Box<Self>> {
let mut inner = SplitFileReader::new(filename)?;
pub fn new(mut inner: Box<dyn DiscStream>) -> Result<Box<Self>> {
// 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 {
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;
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!(
"CISO file size mismatch: expected at least {} bytes, got {}",
file_size,
inner.len()
file_size, len
)));
}
// 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")?;
NKitHeader::try_read_from(&mut inner, header.block_size.get(), true)
NKitHeader::try_read_from(inner.as_mut(), header.block_size.get(), true)
} else {
None
};

View File

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

View File

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

View File

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

View File

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

View File

@ -136,8 +136,6 @@ impl Seek for SplitFileReader {
if split.contains(self.pos) {
// Seek within the open file
split.inner.seek(SeekFrom::Start(self.pos - split.begin))?;
} else {
self.open_file = None;
}
}
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::{Read, Seek, SeekFrom},
mem::size_of,
path::Path,
};
use zerocopy::{big_endian::*, AsBytes, FromBytes, FromZeroes};
use zerocopy::{big_endian::*, FromBytes, Immutable, IntoBytes, KnownLayout};
use crate::{
io::{
block::{Block, BlockIO, PartitionInfo},
block::{Block, BlockIO, DiscStream, PartitionInfo, WBFS_MAGIC},
nkit::NKitHeader,
split::SplitFileReader,
DiscMeta, Format, MagicBytes,
},
util::read::{read_box_slice, read_from},
Error, Result, ResultContext,
};
pub const WBFS_MAGIC: MagicBytes = *b"WBFS";
#[derive(Debug, Clone, PartialEq, FromBytes, FromZeroes, AsBytes)]
#[derive(Debug, Clone, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(4))]
struct WBFSHeader {
magic: MagicBytes,
@ -35,18 +31,6 @@ impl WBFSHeader {
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) }
}
@ -55,7 +39,7 @@ const NUM_WII_SECTORS: u32 = 143432 * 2; // Double layer discs
#[derive(Clone)]
pub struct DiscIOWBFS {
inner: SplitFileReader,
inner: Box<dyn DiscStream>,
/// WBFS header
header: WBFSHeader,
/// Map of Wii LBAs to WBFS LBAs
@ -65,14 +49,13 @@ pub struct DiscIOWBFS {
}
impl DiscIOWBFS {
pub fn new(filename: &Path) -> Result<Box<Self>> {
let mut inner = SplitFileReader::new(filename)?;
let header: WBFSHeader = read_from(&mut inner).context("Reading WBFS header")?;
pub fn new(mut inner: Box<dyn DiscStream>) -> Result<Box<Self>> {
inner.seek(SeekFrom::Start(0)).context("Seeking to start")?;
let header: WBFSHeader = read_from(inner.as_mut()).context("Reading WBFS header")?;
if header.magic != WBFS_MAGIC {
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;
if file_len != expected_file_len {
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]> =
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")?;
if disc_table[0] != 1 {
return Err(Error::DiscFormat("WBFS doesn't contain a disc".to_string()));
@ -95,12 +81,12 @@ impl DiscIOWBFS {
inner
.seek(SeekFrom::Start(header.sector_size() as u64 + DISC_HEADER_SIZE as u64))
.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")?;
// Read NKit header if present (always at 0x10000)
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 }))
}

View File

@ -2,10 +2,9 @@ use std::{
io,
io::{Read, Seek, SeekFrom},
mem::size_of,
path::Path,
};
use zerocopy::{big_endian::*, AsBytes, FromBytes, FromZeroes};
use zerocopy::{big_endian::*, FromBytes, Immutable, IntoBytes, KnownLayout};
use crate::{
disc::{
@ -14,14 +13,12 @@ use crate::{
SECTOR_SIZE,
},
io::{
block::{Block, BlockIO, PartitionInfo},
block::{Block, BlockIO, DiscStream, PartitionInfo, RVZ_MAGIC, WIA_MAGIC},
nkit::NKitHeader,
split::SplitFileReader,
Compression, Format, HashBytes, KeyBytes, MagicBytes,
},
static_assert,
util::{
compress::{lzma2_props_decode, lzma_props_decode, new_lzma2_decoder, new_lzma_decoder},
lfg::LaggedFibonacci,
read::{read_box_slice, read_from, read_u16_be, read_vec},
take_seek::TakeSeekExt,
@ -29,12 +26,9 @@ use crate::{
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
/// will never be changed.
#[derive(Clone, Debug, PartialEq, FromBytes, FromZeroes, AsBytes)]
#[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(4))]
pub struct WIAFileHeader {
pub magic: MagicBytes,
@ -148,7 +142,7 @@ impl TryFrom<u32> for WIACompression {
const DISC_HEAD_SIZE: usize = 0x80;
/// 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))]
pub struct WIADisc {
/// 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))]
pub struct WIAPartitionData {
/// 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
/// 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).
#[derive(Clone, Debug, PartialEq, FromBytes, FromZeroes, AsBytes)]
#[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(4))]
pub struct WIAPartition {
/// 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
/// 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].
#[derive(Clone, Debug, PartialEq, FromBytes, FromZeroes, AsBytes)]
#[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(4))]
pub struct WIARawData {
/// 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]
/// 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`.
#[derive(Clone, Debug, PartialEq, FromBytes, FromZeroes, AsBytes)]
#[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(4))]
pub struct WIAGroup {
/// 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
/// [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))]
pub struct RVZGroup {
/// 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
/// 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.
#[derive(Clone, Debug, PartialEq, FromBytes, FromZeroes, AsBytes)]
#[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(2))]
pub struct WIAException {
/// 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 {
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() {
WIACompression::None => Ok(Self::None),
#[cfg(feature = "compress-bzip2")]
WIACompression::Bzip2 => Ok(Self::Bzip2),
#[cfg(feature = "compress-lzma")]
WIACompression::Lzma => Ok(Self::Lzma(Box::from(data))),
WIACompression::Lzma => Ok(Self::Lzma(Box::from(_data))),
#[cfg(feature = "compress-lzma")]
WIACompression::Lzma2 => Ok(Self::Lzma2(Box::from(data))),
WIACompression::Lzma2 => Ok(Self::Lzma2(Box::from(_data))),
#[cfg(feature = "compress-zstd")]
WIACompression::Zstandard => Ok(Self::Zstandard),
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)),
#[cfg(feature = "compress-lzma")]
Decompressor::Lzma(data) => {
use crate::util::compress::{lzma_props_decode, new_lzma_decoder};
let options = lzma_props_decode(data)?;
Box::new(new_lzma_decoder(reader, &options)?)
}
#[cfg(feature = "compress-lzma")]
Decompressor::Lzma2(data) => {
use crate::util::compress::{lzma2_props_decode, new_lzma2_decoder};
let options = lzma2_props_decode(data)?;
Box::new(new_lzma2_decoder(reader, &options)?)
}
@ -502,7 +498,7 @@ impl Decompressor {
}
pub struct DiscIOWIA {
inner: SplitFileReader,
inner: Box<dyn DiscStream>,
header: WIAFileHeader,
disc: WIADisc,
partitions: Box<[WIAPartition]>,
@ -549,21 +545,21 @@ fn verify_hash(buf: &[u8], expected: &HashBytes) -> Result<()> {
}
impl DiscIOWIA {
pub fn new(filename: &Path) -> Result<Box<Self>> {
let mut inner = SplitFileReader::new(filename)?;
pub fn new(mut inner: Box<dyn DiscStream>) -> Result<Box<Self>> {
// 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()?;
let is_rvz = header.is_rvz();
// log::debug!("Header: {:?}", 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")?;
verify_hash(&disc_buf, &header.disc_hash)?;
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()?;
// if !options.rebuild_hashes {
// // If we're not rebuilding hashes, disable partition hashes in disc header
@ -576,14 +572,14 @@ impl DiscIOWIA {
// log::debug!("Disc: {:?}", disc);
// 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
inner
.seek(SeekFrom::Start(disc.partition_offset.get()))
.context("Seeking to WIA/RVZ partition headers")?;
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")?;
verify_hash(partitions.as_ref().as_bytes(), &disc.partition_hash)?;
// log::debug!("Partitions: {:?}", partitions);
@ -597,7 +593,7 @@ impl DiscIOWIA {
.seek(SeekFrom::Start(disc.raw_data_offset.get()))
.context("Seeking to WIA/RVZ raw data headers")?;
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")?;
read_box_slice(&mut reader, disc.num_raw_data.get() as usize)
.context("Reading WIA/RVZ raw data headers")?
@ -621,7 +617,7 @@ impl DiscIOWIA {
.seek(SeekFrom::Start(disc.group_offset.get()))
.context("Seeking to WIA/RVZ group headers")?;
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")?;
if is_rvz {
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.
//!
//! Originally based on the C++ library [nod](https://github.com/AxioDL/nod),
@ -59,22 +59,24 @@
//! ```
use std::{
io::{Read, Seek},
io::{BufRead, Read, Seek},
path::Path,
};
pub use disc::{
ApploaderHeader, DiscHeader, DolHeader, PartitionBase, PartitionHeader, PartitionKind,
PartitionMeta, BI2_SIZE, BOOT_SIZE, SECTOR_SIZE,
ApploaderHeader, DiscHeader, DolHeader, FileStream, Fst, Node, NodeKind, OwnedFileStream,
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::{block::PartitionInfo, Compression, DiscMeta, Format};
pub use streams::ReadStream;
pub use io::{
block::{DiscStream, PartitionInfo},
Compression, DiscMeta, Format, KeyBytes, MagicBytes,
};
pub use util::lfg::LaggedFibonacci;
mod disc;
mod fst;
mod io;
mod streams;
mod util;
/// Error types for nod.
@ -89,16 +91,26 @@ pub enum Error {
/// An unknown error.
#[error("error: {0}")]
Other(String),
/// An error occurred while allocating memory.
#[error("allocation failed")]
Alloc(zerocopy::AllocError),
}
impl From<&str> for Error {
#[inline]
fn from(s: &str) -> Error { Error::Other(s.to_string()) }
}
impl From<String> for Error {
#[inline]
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`].
pub type Result<T, E = Error> = core::result::Result<T, E>;
@ -109,6 +121,7 @@ pub trait ErrorContext {
}
impl ErrorContext for std::io::Error {
#[inline]
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>
where E: ErrorContext
{
#[inline]
fn context(self, context: impl Into<String>) -> Result<T> {
self.map_err(|e| e.context(context))
}
#[inline]
fn with_context<F>(self, f: F) -> Result<T>
where F: FnOnce() -> String {
self.map_err(|e| e.context(f()))
@ -155,34 +170,71 @@ pub struct Disc {
impl Disc {
/// Opens a disc image from a file path.
#[inline]
pub fn new<P: AsRef<Path>>(path: P) -> Result<Disc> {
Disc::new_with_options(path, &OpenOptions::default())
}
/// 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> {
let io = io::block::open(path.as_ref())?;
let reader = disc::reader::DiscReader::new(io, options)?;
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.
#[inline]
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.
#[inline]
pub fn meta(&self) -> DiscMeta { self.reader.meta() }
/// 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() }
/// A list of Wii partitions on the disc.
///
/// **GameCube**: This will return an empty slice.
#[inline]
pub fn partitions(&self) -> &[PartitionInfo] { self.reader.partitions() }
/// Opens a decrypted partition read stream for the specified partition index.
///
/// **GameCube**: `index` must always be 0.
#[inline]
pub fn open_partition(&self, index: usize) -> Result<Box<dyn PartitionBase>> {
self.reader.open_partition(index, &self.options)
}
@ -191,15 +243,26 @@ impl Disc {
/// the specified kind.
///
/// **GameCube**: `kind` must always be [`PartitionKind::Data`].
#[inline]
pub fn open_partition_kind(&self, kind: PartitionKind) -> Result<Box<dyn PartitionBase>> {
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 {
#[inline]
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> { self.reader.read(buf) }
}
impl Seek for Disc {
#[inline]
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).
/// See `lzma_lzma_lclppb_decode` in `liblzma/lzma/lzma_decoder.c`.
#[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;
if d >= (9 * 5 * 5) {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
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.
/// See `lzma_lzma_props_decode` in `liblzma/lzma/lzma_decoder.c`.
#[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;
if props.len() != 5 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
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.
/// See `lzma_lzma2_props_decode` in `liblzma/lzma/lzma2_decoder.c`.
#[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;
if props.len() != 1 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
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();
options.dict_size(match d.cmp(&40) {
Ordering::Greater => {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
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>(
reader: R,
options: &liblzma::stream::LzmaOptions,
) -> io::Result<liblzma::read::XzDecoder<R>>
) -> std::io::Result<liblzma::read::XzDecoder<R>>
where
R: Read,
R: std::io::Read,
{
let mut filters = liblzma::stream::Filters::new();
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))
}
@ -81,12 +83,13 @@ where
pub fn new_lzma2_decoder<R>(
reader: R,
options: &liblzma::stream::LzmaOptions,
) -> io::Result<liblzma::read::XzDecoder<R>>
) -> std::io::Result<liblzma::read::XzDecoder<R>>
where
R: Read,
R: std::io::Read,
{
let mut filters = liblzma::stream::Filters::new();
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))
}

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;
@ -8,7 +12,7 @@ pub const LFG_K: usize = 521;
pub const LFG_J: usize = 32;
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):
/// https://github.com/dolphin-emu/dolphin/blob/a0f555648c27ec0c928f6b1e1fcad5e2d7c4d0c4/docs/WiaAndRvz.md
@ -19,6 +23,7 @@ pub struct LaggedFibonacci {
}
impl Default for LaggedFibonacci {
#[inline]
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([
init[2],
init[1],
init[3].wrapping_add(init[2]),
init[0].wrapping_add(init[1]),
disc_id[2],
disc_id[1],
disc_id[3].wrapping_add(disc_id[2]),
disc_id[0].wrapping_add(disc_id[1]),
]) ^ disc_num as u32;
let sector = (partition_offset / SECTOR_SIZE as u64) as u32;
let sector_offset = partition_offset % SECTOR_SIZE as u64;
@ -62,9 +71,12 @@ impl LaggedFibonacci {
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<()>
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() {
*x = u32::from_be(*x);
}
@ -73,7 +85,8 @@ impl LaggedFibonacci {
Ok(())
}
pub fn forward(&mut self) {
/// Advances the LFG by one step.
fn forward(&mut self) {
for i in 0..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) {
self.position += n;
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]) {
while !buf.is_empty() {
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)]
@ -132,4 +211,53 @@ mod tests {
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 zerocopy::{AsBytes, FromBytes, FromZeroes};
use zerocopy::{FromBytes, FromZeros, IntoBytes};
#[inline(always)]
pub fn read_from<T, R>(reader: &mut R) -> io::Result<T>
where
T: FromBytes + FromZeroes + AsBytes,
T: FromBytes + IntoBytes,
R: Read + ?Sized,
{
let mut ret = <T>::new_zeroed();
reader.read_exact(ret.as_bytes_mut())?;
reader.read_exact(ret.as_mut_bytes())?;
Ok(ret)
}
#[inline(always)]
pub fn read_vec<T, R>(reader: &mut R, count: usize) -> io::Result<Vec<T>>
where
T: FromBytes + FromZeroes + AsBytes,
T: FromBytes + IntoBytes,
R: Read + ?Sized,
{
let mut ret = <T>::new_vec_zeroed(count);
reader.read_exact(ret.as_mut_slice().as_bytes_mut())?;
let mut ret =
<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)
}
#[inline(always)]
pub fn read_box<T, R>(reader: &mut R) -> io::Result<Box<T>>
where
T: FromBytes + FromZeroes + AsBytes,
T: FromBytes + IntoBytes,
R: Read + ?Sized,
{
let mut ret = <T>::new_box_zeroed();
reader.read_exact(ret.as_mut().as_bytes_mut())?;
let mut ret = <T>::new_box_zeroed().map_err(|_| io::Error::from(io::ErrorKind::OutOfMemory))?;
reader.read_exact(ret.as_mut().as_mut_bytes())?;
Ok(ret)
}
#[inline(always)]
pub fn read_box_slice<T, R>(reader: &mut R, count: usize) -> io::Result<Box<[T]>>
where
T: FromBytes + FromZeroes + AsBytes,
T: FromBytes + IntoBytes,
R: Read + ?Sized,
{
let mut ret = <T>::new_box_slice_zeroed(count);
reader.read_exact(ret.as_mut().as_bytes_mut())?;
let mut ret = <[T]>::new_box_zeroed_with_elems(count)
.map_err(|_| io::Error::from(io::ErrorKind::OutOfMemory))?;
reader.read_exact(ret.as_mut().as_mut_bytes())?;
Ok(ret)
}

View File

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

View File

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

View File

@ -11,7 +11,7 @@ use std::{
use argp::FromArgs;
use indicatif::{ProgressBar, ProgressState, ProgressStyle};
use nod::{Disc, OpenOptions, Result, ResultContext};
use zerocopy::FromZeroes;
use zerocopy::FromZeros;
use crate::util::{
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 buf = <u8>::new_box_slice_zeroed(BUFFER_SIZE);
let mut buf = <[u8]>::new_box_zeroed_with_elems(BUFFER_SIZE)?;
while total_read < disc_size {
let read = min(BUFFER_SIZE as u64, disc_size - total_read) as usize;
disc.read_exact(&mut buf[..read]).with_context(|| {

View File

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

View File

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

View File

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

View File

@ -1,5 +1,11 @@
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 argp::{FromArgValue, FromArgs};

View File

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

View File

@ -10,7 +10,7 @@ use std::{
use hex::deserialize as deserialize_hex;
use nod::{array_ref, Result};
use serde::Deserialize;
use zerocopy::{AsBytes, FromBytes, FromZeroes};
use zerocopy::{FromBytes, FromZeros, Immutable, IntoBytes, KnownLayout};
#[derive(Clone, Debug)]
pub struct GameResult<'a> {
@ -33,18 +33,15 @@ impl<'a> Iterator for EntryIter<'a> {
type Item = GameResult<'a>;
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>());
if self.index >= header.entry_count as usize {
return None;
}
let entries_size = header.entry_count as usize * size_of::<GameEntry>();
let entries: &[GameEntry] = GameEntry::slice_from(
&self.data[size_of::<Header>()..size_of::<Header>() + entries_size],
)
.unwrap();
let string_table: &[u8] = &self.data[size_of::<Header>() + entries_size..];
let entries = <[GameEntry]>::ref_from_bytes(&remaining[..entries_size]).ok()?;
let string_table = &self.data[size_of::<Header>() + entries_size..];
let entry = &entries[self.index];
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>> {
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>());
let entries_size = header.entry_count as usize * size_of::<GameEntry>();
let entries: &[GameEntry] =
GameEntry::slice_from(&data[size_of::<Header>()..size_of::<Header>() + entries_size])
.unwrap();
let string_table: &[u8] = &data[size_of::<Header>() + entries_size..];
let (entries_buf, string_table) = remaining.split_at(entries_size);
let entries = <[GameEntry]>::ref_from_bytes(entries_buf).ok()?;
// Binary search by CRC32
let index = entries.binary_search_by_key(&crc32, |entry| entry.crc32).ok()?;
@ -84,7 +79,7 @@ fn loaded_data() -> &'static [u8] {
LOADED
.get_or_init(|| {
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()
.unwrap()
.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 string_table_size = entries.iter().map(|(_, name)| name.len() + 4).sum::<usize>();
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());
// 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
#[derive(Clone, Debug, AsBytes, FromBytes, FromZeroes)]
#[derive(Clone, Debug, IntoBytes, FromBytes, Immutable, KnownLayout)]
#[repr(C, align(4))]
struct Header {
entry_count: u32,
@ -160,7 +155,7 @@ struct Header {
}
// Keep in sync with build.rs
#[derive(Clone, Debug, AsBytes, FromBytes, FromZeroes)]
#[derive(Clone, Debug, IntoBytes, FromBytes, Immutable, KnownLayout)]
#[repr(C, align(4))]
struct GameEntry {
crc32: u32,

View File

@ -11,7 +11,7 @@ use std::{
use indicatif::{ProgressBar, ProgressState, ProgressStyle};
use nod::{Compression, Disc, DiscHeader, DiscMeta, OpenOptions, Result, ResultContext};
use size::Size;
use zerocopy::FromZeroes;
use zerocopy::FromZeros;
use crate::util::{
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 buf = <u8>::new_box_slice_zeroed(BUFFER_SIZE);
let mut buf = <[u8]>::new_box_zeroed_with_elems(BUFFER_SIZE)?;
while total_read < disc_size {
let read = min(BUFFER_SIZE as u64, disc_size - total_read) as usize;
disc.read_exact(&mut buf[..read]).with_context(|| {