Compare commits

..

No commits in common. "df8ab228c8a8e8f906048daddc3ea07f43f18a5c" and "da8d5fda79b144737f7ec645e6423e4236af2cef" have entirely different histories.

37 changed files with 700 additions and 1418 deletions

View File

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

310
Cargo.lock generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

80
nod/src/streams.rs Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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