mirror of https://github.com/encounter/nod-rs.git
Compare commits
23 Commits
da8d5fda79
...
df8ab228c8
Author | SHA1 | Date |
---|---|---|
Luke Street | df8ab228c8 | |
Luke Street | 32e08f9543 | |
Luke Street | e0d735dd39 | |
Luke Street | d4bca2caa8 | |
Luke Street | be4672471d | |
Luke Street | f4638369d1 | |
Luke Street | d99ef72fe9 | |
Luke Street | e6a3871d28 | |
Luke Street | 30bcf4936b | |
Luke Street | 5f537f0e7b | |
Luke Street | 8abe674cb9 | |
Luke Street | 54890674a2 | |
Luke Street | 370d03fa9a | |
Luke Street | 5ad514d59c | |
Luke Street | 312dd6f080 | |
Luke Street | 6f3052e05d | |
Luke Street | d2b8135cdb | |
Luke Street | a8f91ff9c2 | |
Luke Street | 22434fbba3 | |
Luke Street | f1cc0949f3 | |
Luke Street | c2e029db6b | |
Luke Street | 83367def99 | |
Luke Street | 551f966c80 |
|
@ -1,11 +1,17 @@
|
|||
name: Build
|
||||
|
||||
on: [ push, pull_request ]
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
paths-ignore:
|
||||
- '*.md'
|
||||
- 'LICENSE*'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
BUILD_PROFILE: release-lto
|
||||
CARGO_BIN_NAME: nodtool
|
||||
CARGO_TARGET_DIR: target
|
||||
CARGO_INCREMENTAL: 0
|
||||
|
||||
jobs:
|
||||
check:
|
||||
|
@ -13,7 +19,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
toolchain: [ stable, 1.73.0, nightly ]
|
||||
toolchain: [ stable, 1.74.0, nightly ]
|
||||
fail-fast: false
|
||||
env:
|
||||
RUSTFLAGS: -D warnings
|
||||
|
@ -79,7 +85,9 @@ jobs:
|
|||
run: cargo test --release
|
||||
|
||||
build:
|
||||
name: Build
|
||||
name: Build nodtool
|
||||
env:
|
||||
CARGO_BIN_NAME: nodtool
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
|
@ -88,20 +96,20 @@ jobs:
|
|||
name: linux-x86_64
|
||||
build: zigbuild
|
||||
features: asm
|
||||
# - platform: ubuntu-latest
|
||||
# target: i686-unknown-linux-musl
|
||||
# name: linux-i686
|
||||
# build: zigbuild
|
||||
# features: asm
|
||||
- platform: ubuntu-latest
|
||||
target: i686-unknown-linux-musl
|
||||
name: linux-i686
|
||||
build: zigbuild
|
||||
features: asm
|
||||
- platform: ubuntu-latest
|
||||
target: aarch64-unknown-linux-musl
|
||||
name: linux-aarch64
|
||||
build: zigbuild
|
||||
features: nightly
|
||||
- platform: ubuntu-latest
|
||||
target: armv7-unknown-linux-musleabi
|
||||
name: linux-armv7l
|
||||
build: zigbuild
|
||||
- platform: windows-latest
|
||||
target: i686-pc-windows-msvc
|
||||
name: windows-x86
|
||||
build: build
|
||||
features: default
|
||||
- platform: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
|
@ -135,20 +143,20 @@ jobs:
|
|||
sudo apt-get -y install ${{ matrix.packages }}
|
||||
- name: Install cargo-zigbuild
|
||||
if: matrix.build == 'zigbuild'
|
||||
run: pip install ziglang==0.11.0 cargo-zigbuild==0.18.3
|
||||
run: pip install ziglang==0.13.0 cargo-zigbuild==0.19.1
|
||||
- name: Setup Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
- name: Cargo build
|
||||
run: cargo ${{ matrix.build }} --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }} --bin ${{ env.CARGO_BIN_NAME }} --features ${{ matrix.features }}
|
||||
run: >
|
||||
cargo ${{ matrix.build }} --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }}
|
||||
--bin ${{ env.CARGO_BIN_NAME }} --features ${{ matrix.features }}
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.name }}
|
||||
name: ${{ env.CARGO_BIN_NAME }}-${{ matrix.name }}
|
||||
path: |
|
||||
${{ env.CARGO_TARGET_DIR }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}
|
||||
${{ env.CARGO_TARGET_DIR }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}.exe
|
||||
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}
|
||||
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}.exe
|
||||
if-no-files-found: error
|
||||
|
@ -159,6 +167,20 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
needs: [ build ]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Check git tag against Cargo version
|
||||
shell: bash
|
||||
run: |
|
||||
set -eou pipefail
|
||||
tag='${{github.ref}}'
|
||||
tag="${tag#refs/tags/}"
|
||||
version=$(grep '^version' Cargo.toml | head -1 | awk -F' = ' '{print $2}' | tr -d '"')
|
||||
version="v$version"
|
||||
if [ "$tag" != "$version" ]; then
|
||||
echo "::error::Git tag doesn't match the Cargo version! ($tag != $version)"
|
||||
exit 1
|
||||
fi
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
|
@ -166,12 +188,28 @@ jobs:
|
|||
- name: Rename artifacts
|
||||
working-directory: artifacts
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir ../out
|
||||
for i in */*/$BUILD_PROFILE/$CARGO_BIN_NAME*; do
|
||||
mv "$i" "../out/$(sed -E "s/([^/]+)\/[^/]+\/$BUILD_PROFILE\/($CARGO_BIN_NAME)/\2-\1/" <<< "$i")"
|
||||
for dir in */; do
|
||||
for file in "$dir"*; do
|
||||
base=$(basename "$file")
|
||||
name="${base%.*}"
|
||||
ext="${base##*.}"
|
||||
if [ "$ext" = "$base" ]; then
|
||||
ext=""
|
||||
else
|
||||
ext=".$ext"
|
||||
fi
|
||||
arch="${dir%/}" # remove trailing slash
|
||||
arch="${arch##"$name-"}" # remove bin name
|
||||
dst="../out/${name}-${arch}${ext}"
|
||||
mv "$file" "$dst"
|
||||
done
|
||||
done
|
||||
ls -R ../out
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@4634c16e79c963813287e889244c50009e7f0981
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: out/*
|
||||
draft: true
|
||||
generate_release_notes: true
|
||||
|
|
|
@ -8,6 +8,12 @@ version = "1.0.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
||||
|
||||
[[package]]
|
||||
name = "aes"
|
||||
version = "0.8.4"
|
||||
|
@ -21,9 +27,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.2"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0"
|
||||
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
@ -57,9 +63,9 @@ checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
|
|||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.4.2"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf"
|
||||
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
|
@ -79,12 +85,6 @@ dependencies = [
|
|||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "bzip2"
|
||||
version = "0.4.4"
|
||||
|
@ -117,11 +117,13 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.86"
|
||||
version = "1.1.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f9fa1897e4325be0d68d48df6aa1a71ac2ed4d27723887e7754192705350730"
|
||||
checksum = "812acba72f0a070b003d3697490d2b55b837230ae7c6c6497f05cc2ddbb8d938"
|
||||
dependencies = [
|
||||
"jobserver",
|
||||
"libc",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -155,9 +157,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.12"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504"
|
||||
checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
@ -192,9 +194,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.19"
|
||||
version = "0.8.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
|
||||
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
|
@ -218,15 +220,15 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "dyn-clone"
|
||||
version = "1.0.16"
|
||||
version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d"
|
||||
checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.10.0"
|
||||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a"
|
||||
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
|
||||
|
||||
[[package]]
|
||||
name = "enable-ansi-support"
|
||||
|
@ -245,9 +247,9 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
|
|||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.33"
|
||||
version = "0.8.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1"
|
||||
checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
@ -305,9 +307,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.12"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
|
||||
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
@ -320,39 +322,48 @@ checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45"
|
|||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.12.1"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
|
||||
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
name = "jobserver"
|
||||
version = "0.1.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.153"
|
||||
version = "0.2.159"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
|
||||
checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
|
||||
|
||||
[[package]]
|
||||
name = "liblzma"
|
||||
version = "0.2.3"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "599133771f99c14ca089a8db3a4565f482ea6eeb66991b262bffc2b72acff69c"
|
||||
checksum = "a7c45fc6fcf5b527d3cf89c1dee8c327943984b0dc8bfcf6e100473b00969e63"
|
||||
dependencies = [
|
||||
"liblzma-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "liblzma-sys"
|
||||
version = "0.2.5"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be9aaba5f9c8f8f615d41570909338b6284fbb1813dc057ecc68563d98a65097"
|
||||
checksum = "6630cb23edeb2e563cd6c30d4117554c69646871455843c33ddcb1d9aef82ecf"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
|
@ -360,10 +371,20 @@ dependencies = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.20"
|
||||
name = "libmimalloc-sys"
|
||||
version = "0.1.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
|
||||
checksum = "23aa6811d3bd4deb8a84dde645f943476d13b248d818edcf8ce0b2f37f036b44"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
|
@ -387,31 +408,40 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "md5-asm"
|
||||
version = "0.5.1"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61d33bc4cdfe5c60340e282bbbee0a6e2bc57f0b9279bb3489c5004d12492e5c"
|
||||
checksum = "d19b8ee7fc7d812058d3b708c7f719efd0713d53854648e4223c6fcae709e2df"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.1"
|
||||
version = "2.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
|
||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||
|
||||
[[package]]
|
||||
name = "mimalloc"
|
||||
version = "0.1.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68914350ae34959d83f732418d51e2427a794055d0b9529f48259ac07af65633"
|
||||
dependencies = [
|
||||
"libmimalloc-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.7.2"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7"
|
||||
checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1"
|
||||
dependencies = [
|
||||
"adler",
|
||||
"adler2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nod"
|
||||
version = "1.2.0"
|
||||
version = "1.4.4"
|
||||
dependencies = [
|
||||
"adler",
|
||||
"aes",
|
||||
|
@ -434,7 +464,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "nodtool"
|
||||
version = "1.2.0"
|
||||
version = "1.4.4"
|
||||
dependencies = [
|
||||
"argp",
|
||||
"base16ct",
|
||||
|
@ -446,6 +476,7 @@ dependencies = [
|
|||
"itertools",
|
||||
"log",
|
||||
"md-5",
|
||||
"mimalloc",
|
||||
"nod",
|
||||
"quick-xml",
|
||||
"serde",
|
||||
|
@ -478,9 +509,12 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
|
|||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.19.0"
|
||||
version = "1.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
checksum = "82881c4be219ab5faaf2ad5e5e5ecdff8c66bd7402ca3160975c93b24961afd1"
|
||||
dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "overload"
|
||||
|
@ -490,27 +524,27 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
|||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.13"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
|
||||
checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.30"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
|
||||
checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.6.0"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0"
|
||||
checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.78"
|
||||
version = "1.0.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
|
||||
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
@ -529,9 +563,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.31.0"
|
||||
version = "0.36.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33"
|
||||
checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"serde",
|
||||
|
@ -539,18 +573,18 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.35"
|
||||
version = "1.0.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
|
||||
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.8.1"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa7237101a77a10773db45d62004a272517633fbcc3df19d96455ede1122e051"
|
||||
checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
|
||||
dependencies = [
|
||||
"either",
|
||||
"rayon-core",
|
||||
|
@ -568,14 +602,14 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.10.3"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15"
|
||||
checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata 0.4.5",
|
||||
"regex-syntax 0.8.2",
|
||||
"regex-automata 0.4.8",
|
||||
"regex-syntax 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -589,13 +623,13 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.5"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd"
|
||||
checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax 0.8.2",
|
||||
"regex-syntax 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -606,28 +640,28 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
|
|||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.2"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
|
||||
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.197"
|
||||
version = "1.0.210"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
|
||||
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.197"
|
||||
version = "1.0.210"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
|
||||
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.50",
|
||||
"syn 2.0.79",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -644,9 +678,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "sha1-asm"
|
||||
version = "0.5.2"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ba6947745e7f86be3b8af00b7355857085dbdf8901393c89514510eb61f4e21"
|
||||
checksum = "286acebaf8b67c1130aedffad26f594eff0c1292389158135327d2e23aed582b"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
@ -660,6 +694,12 @@ dependencies = [
|
|||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "size"
|
||||
version = "0.4.1"
|
||||
|
@ -668,15 +708,15 @@ checksum = "9fed904c7fb2856d868b92464fc8fa597fce366edea1a9cbfaa8cb5fe080bd6d"
|
|||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.13.1"
|
||||
version = "1.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7"
|
||||
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
||||
|
||||
[[package]]
|
||||
name = "supports-color"
|
||||
version = "3.0.0"
|
||||
version = "3.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9829b314621dfc575df4e409e79f9d6a66a3bd707ab73f23cb4aa3a854ac854f"
|
||||
checksum = "8775305acf21c96926c900ad056abeef436701108518cf890020387236ac5a77"
|
||||
dependencies = [
|
||||
"is_ci",
|
||||
]
|
||||
|
@ -694,9 +734,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.50"
|
||||
version = "2.0.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74f1bdc9872430ce9b75da68329d1c1746faf50ffac5f19e02b71e37ff881ffb"
|
||||
checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -705,22 +745,22 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.57"
|
||||
version = "1.0.64"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b"
|
||||
checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.57"
|
||||
version = "1.0.64"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81"
|
||||
checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.50",
|
||||
"syn 2.0.79",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -752,7 +792,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.50",
|
||||
"syn 2.0.79",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -811,15 +851,15 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.12"
|
||||
version = "1.0.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.11"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
|
||||
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
|
@ -829,9 +869,9 @@ checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
|
|||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.4"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
|
@ -881,17 +921,18 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.0"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.52.0",
|
||||
"windows_aarch64_msvc 0.52.0",
|
||||
"windows_i686_gnu 0.52.0",
|
||||
"windows_i686_msvc 0.52.0",
|
||||
"windows_x86_64_gnu 0.52.0",
|
||||
"windows_x86_64_gnullvm 0.52.0",
|
||||
"windows_x86_64_msvc 0.52.0",
|
||||
"windows_aarch64_gnullvm 0.52.6",
|
||||
"windows_aarch64_msvc 0.52.6",
|
||||
"windows_i686_gnu 0.52.6",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc 0.52.6",
|
||||
"windows_x86_64_gnu 0.52.6",
|
||||
"windows_x86_64_gnullvm 0.52.6",
|
||||
"windows_x86_64_msvc 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -902,9 +943,9 @@ checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
|
|||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.0"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
|
@ -914,9 +955,9 @@ checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
|
|||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.0"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
|
@ -926,9 +967,15 @@ checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
|
|||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.0"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
|
@ -938,9 +985,9 @@ checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
|
|||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.0"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
|
@ -950,9 +997,9 @@ checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
|
|||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.0"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
|
@ -962,9 +1009,9 @@ checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
|
|||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.0"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
|
@ -974,60 +1021,59 @@ checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
|
|||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.0"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "xxhash-rust"
|
||||
version = "0.8.10"
|
||||
version = "0.8.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "927da81e25be1e1a2901d59b81b37dd2efd1fc9c9345a55007f09bf5a2d3ee03"
|
||||
checksum = "6a5cbf750400958819fb6178eaa83bee5cd9c29a26a40cc241df8c70fdd46984"
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.32"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be"
|
||||
checksum = "dd2e5ce961dea177d282ec084dca2aa411b7411199a68d79eb1beacb305a6cd9"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.7.32"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
|
||||
checksum = "b06304eeddb6081af98ac59db08c868ac197e586086b996d15a86ed70e09a754"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.50",
|
||||
"syn 2.0.79",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd"
|
||||
version = "0.13.1"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d789b1514203a1120ad2429eae43a7bd32b90976a7bb8a05f7ec02fa88cc23a"
|
||||
checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9"
|
||||
dependencies = [
|
||||
"zstd-safe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-safe"
|
||||
version = "7.1.0"
|
||||
version = "7.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1cd99b45c6bc03a018c8b8a86025678c87e55526064e38f9df301989dce7ec0a"
|
||||
checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059"
|
||||
dependencies = [
|
||||
"zstd-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-sys"
|
||||
version = "2.0.11+zstd.1.5.6"
|
||||
version = "2.0.13+zstd.1.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75652c55c0b6f3e6f12eb786fe1bc960396bf05a1eb3bf1f3691c3610ac2e6d4"
|
||||
checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
|
|
12
Cargo.toml
12
Cargo.toml
|
@ -4,5 +4,15 @@ resolver = "2"
|
|||
|
||||
[profile.release-lto]
|
||||
inherits = "release"
|
||||
lto = "thin"
|
||||
lto = "fat"
|
||||
strip = "debuginfo"
|
||||
codegen-units = 1
|
||||
|
||||
[workspace.package]
|
||||
version = "1.4.4"
|
||||
edition = "2021"
|
||||
rust-version = "1.74"
|
||||
authors = ["Luke Street <luke@street.dev>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/encounter/nod"
|
||||
keywords = ["gamecube", "wii", "iso", "wbfs", "rvz"]
|
||||
|
|
23
README.md
23
README.md
|
@ -1,12 +1,12 @@
|
|||
# nod [![Build Status]][actions] [![Latest Version]][crates.io] [![Api Rustdoc]][rustdoc] ![Rust Version]
|
||||
|
||||
[Build Status]: https://github.com/encounter/nod-rs/actions/workflows/build.yaml/badge.svg
|
||||
[actions]: https://github.com/encounter/nod-rs/actions
|
||||
[Build Status]: https://github.com/encounter/nod/actions/workflows/build.yaml/badge.svg
|
||||
[actions]: https://github.com/encounter/nod/actions
|
||||
[Latest Version]: https://img.shields.io/crates/v/nod.svg
|
||||
[crates.io]: https://crates.io/crates/nod
|
||||
[Api Rustdoc]: https://img.shields.io/badge/api-rustdoc-blue.svg
|
||||
[rustdoc]: https://docs.rs/nod
|
||||
[Rust Version]: https://img.shields.io/badge/rust-1.73+-blue.svg?maxAge=3600
|
||||
[Rust Version]: https://img.shields.io/badge/rust-1.74+-blue.svg?maxAge=3600
|
||||
|
||||
Library for traversing & reading Nintendo Optical Disc (GameCube and Wii) images.
|
||||
|
||||
|
@ -14,16 +14,25 @@ Originally based on the C++ library [nod](https://github.com/AxioDL/nod),
|
|||
but does not currently support authoring.
|
||||
|
||||
Currently supported file formats:
|
||||
|
||||
- ISO (GCM)
|
||||
- WIA / RVZ
|
||||
- WBFS (+ NKit 2 lossless)
|
||||
- CISO (+ NKit 2 lossless)
|
||||
- NFS (Wii U VC)
|
||||
- GCZ
|
||||
- TGC
|
||||
|
||||
## CLI tool
|
||||
|
||||
This crate includes a command-line tool called `nodtool`.
|
||||
This crate includes a command-line tool called `nodtool`.
|
||||
|
||||
Download the latest release from the [releases page](https://github.com/encounter/nod-rs/releases),
|
||||
or install it using Cargo:
|
||||
|
||||
```shell
|
||||
cargo install --locked nodtool
|
||||
```
|
||||
|
||||
### info
|
||||
|
||||
|
@ -53,7 +62,7 @@ Converts any supported format to raw ISO.
|
|||
|
||||
```shell
|
||||
nodtool convert /path/to/game.wia /path/to/game.iso
|
||||
```
|
||||
```
|
||||
|
||||
### verify
|
||||
|
||||
|
@ -113,8 +122,8 @@ std::io::copy(&mut disc, &mut out)
|
|||
|
||||
Licensed under either of
|
||||
|
||||
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
|
||||
* MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
|
||||
- Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or <http://www.apache.org/licenses/LICENSE-2.0>)
|
||||
- MIT license ([LICENSE-MIT](LICENSE-MIT) or <http://opensource.org/licenses/MIT>)
|
||||
|
||||
at your option.
|
||||
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
[package]
|
||||
name = "nod"
|
||||
version = "1.2.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.73.0"
|
||||
authors = ["Luke Street <luke@street.dev>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/encounter/nod-rs"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
documentation = "https://docs.rs/nod"
|
||||
readme = "../README.md"
|
||||
description = """
|
||||
Library for reading GameCube and Wii disc images.
|
||||
"""
|
||||
keywords = ["gamecube", "wii", "iso", "wbfs", "rvz"]
|
||||
keywords.workspace = true
|
||||
categories = ["command-line-utilities", "parser-implementations"]
|
||||
|
||||
[features]
|
||||
|
@ -31,12 +31,12 @@ cbc = "0.1"
|
|||
digest = "0.10"
|
||||
dyn-clone = "1.0"
|
||||
encoding_rs = "0.8"
|
||||
itertools = "0.12"
|
||||
liblzma = { version = "0.2", features = ["static"], optional = true }
|
||||
itertools = "0.13"
|
||||
liblzma = { version = "0.3", features = ["static"], optional = true }
|
||||
log = "0.4"
|
||||
miniz_oxide = { version = "0.7", optional = true }
|
||||
rayon = "1.8"
|
||||
miniz_oxide = { version = "0.8", optional = true }
|
||||
rayon = "1.10"
|
||||
sha1 = "0.10"
|
||||
thiserror = "1.0"
|
||||
zerocopy = { version = "0.7", features = ["alloc", "derive"] }
|
||||
zerocopy = { version = "0.8", features = ["alloc", "derive"] }
|
||||
zstd = { version = "0.13", optional = true }
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
use std::{borrow::Cow, ffi::CStr, mem::size_of};
|
||||
|
||||
use encoding_rs::SHIFT_JIS;
|
||||
use zerocopy::{big_endian::*, AsBytes, FromBytes, FromZeroes};
|
||||
use zerocopy::{big_endian::*, FromBytes, Immutable, IntoBytes, KnownLayout};
|
||||
|
||||
use crate::{static_assert, Result};
|
||||
|
||||
|
@ -19,13 +19,13 @@ pub enum NodeKind {
|
|||
}
|
||||
|
||||
/// An individual file system node.
|
||||
#[derive(Clone, Debug, PartialEq, FromBytes, FromZeroes, AsBytes)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
|
||||
#[repr(C, align(4))]
|
||||
pub struct Node {
|
||||
kind: u8,
|
||||
// u24 big-endian
|
||||
name_offset: [u8; 3],
|
||||
offset: U32,
|
||||
pub(crate) offset: U32,
|
||||
length: U32,
|
||||
}
|
||||
|
||||
|
@ -33,6 +33,7 @@ static_assert!(size_of::<Node>() == 12);
|
|||
|
||||
impl Node {
|
||||
/// File system node kind.
|
||||
#[inline]
|
||||
pub fn kind(&self) -> NodeKind {
|
||||
match self.kind {
|
||||
0 => NodeKind::File,
|
||||
|
@ -42,12 +43,15 @@ impl Node {
|
|||
}
|
||||
|
||||
/// Whether the node is a file.
|
||||
#[inline]
|
||||
pub fn is_file(&self) -> bool { self.kind == 0 }
|
||||
|
||||
/// Whether the node is a directory.
|
||||
#[inline]
|
||||
pub fn is_dir(&self) -> bool { self.kind == 1 }
|
||||
|
||||
/// Offset in the string table to the filename.
|
||||
#[inline]
|
||||
pub fn name_offset(&self) -> u32 {
|
||||
u32::from_be_bytes([0, self.name_offset[0], self.name_offset[1], self.name_offset[2]])
|
||||
}
|
||||
|
@ -55,8 +59,9 @@ impl Node {
|
|||
/// For files, this is the partition offset of the file data. (Wii: >> 2)
|
||||
///
|
||||
/// For directories, this is the parent node index in the FST.
|
||||
#[inline]
|
||||
pub fn offset(&self, is_wii: bool) -> u64 {
|
||||
if is_wii && self.kind == 0 {
|
||||
if is_wii && self.is_file() {
|
||||
self.offset.get() as u64 * 4
|
||||
} else {
|
||||
self.offset.get() as u64
|
||||
|
@ -68,6 +73,7 @@ impl Node {
|
|||
/// For directories, this is the child end index in the FST.
|
||||
///
|
||||
/// Number of child files and directories recursively is `length - offset`.
|
||||
#[inline]
|
||||
pub fn length(&self) -> u64 { self.length.get() as u64 }
|
||||
}
|
||||
|
||||
|
@ -81,8 +87,9 @@ pub struct Fst<'a> {
|
|||
|
||||
impl<'a> Fst<'a> {
|
||||
/// Create a new FST view from a buffer.
|
||||
#[allow(clippy::missing_inline_in_public_items)]
|
||||
pub fn new(buf: &'a [u8]) -> Result<Self, &'static str> {
|
||||
let Some(root_node) = Node::ref_from_prefix(buf) else {
|
||||
let Ok((root_node, _)) = Node::ref_from_prefix(buf) else {
|
||||
return Err("FST root node not found");
|
||||
};
|
||||
// String table starts after the last node
|
||||
|
@ -91,15 +98,17 @@ impl<'a> Fst<'a> {
|
|||
return Err("FST string table out of bounds");
|
||||
}
|
||||
let (node_buf, string_table) = buf.split_at(string_base as usize);
|
||||
let nodes = Node::slice_from(node_buf).unwrap();
|
||||
let nodes = <[Node]>::ref_from_bytes(node_buf).unwrap();
|
||||
Ok(Self { nodes, string_table })
|
||||
}
|
||||
|
||||
/// Iterate over the nodes in the FST.
|
||||
#[inline]
|
||||
pub fn iter(&self) -> FstIter { FstIter { fst: self, idx: 1 } }
|
||||
|
||||
/// Get the name of a node.
|
||||
pub fn get_name(&self, node: &Node) -> Result<Cow<str>, String> {
|
||||
#[allow(clippy::missing_inline_in_public_items)]
|
||||
pub fn get_name(&self, node: Node) -> Result<Cow<'a, str>, String> {
|
||||
let name_buf = self.string_table.get(node.name_offset() as usize..).ok_or_else(|| {
|
||||
format!(
|
||||
"FST: name offset {} out of bounds (string table size: {})",
|
||||
|
@ -110,25 +119,27 @@ impl<'a> Fst<'a> {
|
|||
let c_string = CStr::from_bytes_until_nul(name_buf).map_err(|_| {
|
||||
format!("FST: name at offset {} not null-terminated", node.name_offset())
|
||||
})?;
|
||||
let (decoded, _, errors) = SHIFT_JIS.decode(c_string.to_bytes());
|
||||
if errors {
|
||||
return Err(format!("FST: Failed to decode name at offset {}", node.name_offset()));
|
||||
}
|
||||
let (decoded, _, _) = SHIFT_JIS.decode(c_string.to_bytes());
|
||||
// Ignore decoding errors, we can't do anything about them. Consumers may check for
|
||||
// U+FFFD (REPLACEMENT CHARACTER), or fetch the raw bytes from the string table.
|
||||
Ok(decoded)
|
||||
}
|
||||
|
||||
/// Finds a particular file or directory by path.
|
||||
pub fn find(&self, path: &str) -> Option<(usize, &Node)> {
|
||||
#[allow(clippy::missing_inline_in_public_items)]
|
||||
pub fn find(&self, path: &str) -> Option<(usize, Node)> {
|
||||
let mut split = path.trim_matches('/').split('/');
|
||||
let mut current = split.next()?;
|
||||
let mut current = next_non_empty(&mut split);
|
||||
if current.is_empty() {
|
||||
return Some((0, self.nodes[0]));
|
||||
}
|
||||
let mut idx = 1;
|
||||
let mut stop_at = None;
|
||||
while let Some(node) = self.nodes.get(idx) {
|
||||
while let Some(node) = self.nodes.get(idx).copied() {
|
||||
if self.get_name(node).as_ref().map_or(false, |name| name.eq_ignore_ascii_case(current))
|
||||
{
|
||||
if let Some(next) = split.next() {
|
||||
current = next;
|
||||
} else {
|
||||
current = next_non_empty(&mut split);
|
||||
if current.is_empty() {
|
||||
return Some((idx, node));
|
||||
}
|
||||
// Descend into directory
|
||||
|
@ -158,13 +169,24 @@ pub struct FstIter<'a> {
|
|||
}
|
||||
|
||||
impl<'a> Iterator for FstIter<'a> {
|
||||
type Item = (usize, &'a Node, Result<Cow<'a, str>, String>);
|
||||
type Item = (usize, Node, Result<Cow<'a, str>, String>);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let idx = self.idx;
|
||||
let node = self.fst.nodes.get(idx)?;
|
||||
let node = self.fst.nodes.get(idx).copied()?;
|
||||
let name = self.fst.get_name(node);
|
||||
self.idx += 1;
|
||||
Some((idx, node, name))
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn next_non_empty<'a>(iter: &mut impl Iterator<Item = &'a str>) -> &'a str {
|
||||
loop {
|
||||
match iter.next() {
|
||||
Some("") => continue,
|
||||
Some(next) => break next,
|
||||
None => break "",
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,20 +1,18 @@
|
|||
use std::{
|
||||
cmp::min,
|
||||
io,
|
||||
io::{Read, Seek, SeekFrom},
|
||||
io::{BufRead, Read, Seek, SeekFrom},
|
||||
mem::size_of,
|
||||
};
|
||||
|
||||
use zerocopy::{FromBytes, FromZeroes};
|
||||
use zerocopy::{FromBytes, FromZeros};
|
||||
|
||||
use super::{
|
||||
ApploaderHeader, DiscHeader, DolHeader, FileStream, Node, PartitionBase, PartitionHeader,
|
||||
PartitionMeta, BI2_SIZE, BOOT_SIZE, SECTOR_SIZE,
|
||||
};
|
||||
use crate::{
|
||||
disc::{
|
||||
ApploaderHeader, DiscHeader, DolHeader, PartitionBase, PartitionHeader, PartitionMeta,
|
||||
BI2_SIZE, BOOT_SIZE, SECTOR_SIZE,
|
||||
},
|
||||
fst::{Node, NodeKind},
|
||||
disc::streams::OwnedFileStream,
|
||||
io::block::{Block, BlockIO},
|
||||
streams::{ReadStream, SharedWindowedReadStream},
|
||||
util::read::{read_box, read_box_slice, read_vec},
|
||||
Result, ResultContext,
|
||||
};
|
||||
|
@ -35,9 +33,9 @@ impl Clone for PartitionGC {
|
|||
Self {
|
||||
io: self.io.clone(),
|
||||
block: Block::default(),
|
||||
block_buf: <u8>::new_box_slice_zeroed(self.block_buf.len()),
|
||||
block_buf: <[u8]>::new_box_zeroed_with_elems(self.block_buf.len()).unwrap(),
|
||||
block_idx: u32::MAX,
|
||||
sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed(),
|
||||
sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed().unwrap(),
|
||||
sector: u32::MAX,
|
||||
pos: 0,
|
||||
disc_header: self.disc_header.clone(),
|
||||
|
@ -51,9 +49,9 @@ impl PartitionGC {
|
|||
Ok(Box::new(Self {
|
||||
io: inner,
|
||||
block: Block::default(),
|
||||
block_buf: <u8>::new_box_slice_zeroed(block_size as usize),
|
||||
block_buf: <[u8]>::new_box_zeroed_with_elems(block_size as usize).unwrap(),
|
||||
block_idx: u32::MAX,
|
||||
sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed(),
|
||||
sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed().unwrap(),
|
||||
sector: u32::MAX,
|
||||
pos: 0,
|
||||
disc_header,
|
||||
|
@ -63,8 +61,8 @@ impl PartitionGC {
|
|||
pub fn into_inner(self) -> Box<dyn BlockIO> { self.io }
|
||||
}
|
||||
|
||||
impl Read for PartitionGC {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
impl BufRead for PartitionGC {
|
||||
fn fill_buf(&mut self) -> io::Result<&[u8]> {
|
||||
let sector = (self.pos / SECTOR_SIZE as u64) as u32;
|
||||
let block_idx = (sector as u64 * SECTOR_SIZE as u64 / self.block_buf.len() as u64) as u32;
|
||||
|
||||
|
@ -86,9 +84,20 @@ impl Read for PartitionGC {
|
|||
}
|
||||
|
||||
let offset = (self.pos % SECTOR_SIZE as u64) as usize;
|
||||
let len = min(buf.len(), SECTOR_SIZE - offset);
|
||||
buf[..len].copy_from_slice(&self.sector_buf[offset..offset + len]);
|
||||
self.pos += len as u64;
|
||||
Ok(&self.sector_buf[offset..])
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn consume(&mut self, amt: usize) { self.pos += amt as u64; }
|
||||
}
|
||||
|
||||
impl Read for PartitionGC {
|
||||
#[inline]
|
||||
fn read(&mut self, out: &mut [u8]) -> io::Result<usize> {
|
||||
let buf = self.fill_buf()?;
|
||||
let len = buf.len().min(out.len());
|
||||
out[..len].copy_from_slice(&buf[..len]);
|
||||
self.consume(len);
|
||||
Ok(len)
|
||||
}
|
||||
}
|
||||
|
@ -115,21 +124,35 @@ impl PartitionBase for PartitionGC {
|
|||
read_part_meta(self, false)
|
||||
}
|
||||
|
||||
fn open_file(&mut self, node: &Node) -> io::Result<SharedWindowedReadStream> {
|
||||
assert_eq!(node.kind(), NodeKind::File);
|
||||
self.new_window(node.offset(false), node.length())
|
||||
fn open_file(&mut self, node: Node) -> io::Result<FileStream> {
|
||||
if !node.is_file() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"Node is not a file".to_string(),
|
||||
));
|
||||
}
|
||||
FileStream::new(self, node.offset(false), node.length())
|
||||
}
|
||||
|
||||
fn ideal_buffer_size(&self) -> usize { SECTOR_SIZE }
|
||||
fn into_open_file(self: Box<Self>, node: Node) -> io::Result<OwnedFileStream> {
|
||||
if !node.is_file() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"Node is not a file".to_string(),
|
||||
));
|
||||
}
|
||||
OwnedFileStream::new(self, node.offset(false), node.length())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn read_part_meta(
|
||||
reader: &mut dyn ReadStream,
|
||||
reader: &mut dyn PartitionBase,
|
||||
is_wii: bool,
|
||||
) -> Result<Box<PartitionMeta>> {
|
||||
// boot.bin
|
||||
let raw_boot: Box<[u8; BOOT_SIZE]> = read_box(reader).context("Reading boot.bin")?;
|
||||
let partition_header = PartitionHeader::ref_from(&raw_boot[size_of::<DiscHeader>()..]).unwrap();
|
||||
let partition_header =
|
||||
PartitionHeader::ref_from_bytes(&raw_boot[size_of::<DiscHeader>()..]).unwrap();
|
||||
|
||||
// bi2.bin
|
||||
let raw_bi2: Box<[u8; BI2_SIZE]> = read_box(reader).context("Reading bi2.bin")?;
|
||||
|
@ -137,7 +160,7 @@ pub(crate) fn read_part_meta(
|
|||
// apploader.bin
|
||||
let mut raw_apploader: Vec<u8> =
|
||||
read_vec(reader, size_of::<ApploaderHeader>()).context("Reading apploader header")?;
|
||||
let apploader_header = ApploaderHeader::ref_from(raw_apploader.as_slice()).unwrap();
|
||||
let apploader_header = ApploaderHeader::ref_from_bytes(raw_apploader.as_slice()).unwrap();
|
||||
raw_apploader.resize(
|
||||
size_of::<ApploaderHeader>()
|
||||
+ apploader_header.size.get() as usize
|
||||
|
@ -147,6 +170,7 @@ pub(crate) fn read_part_meta(
|
|||
reader
|
||||
.read_exact(&mut raw_apploader[size_of::<ApploaderHeader>()..])
|
||||
.context("Reading apploader")?;
|
||||
let raw_apploader = raw_apploader.into_boxed_slice();
|
||||
|
||||
// fst.bin
|
||||
reader
|
||||
|
@ -167,7 +191,7 @@ pub(crate) fn read_part_meta(
|
|||
.context("Seeking to DOL offset")?;
|
||||
let mut raw_dol: Vec<u8> =
|
||||
read_vec(reader, size_of::<DolHeader>()).context("Reading DOL header")?;
|
||||
let dol_header = DolHeader::ref_from(raw_dol.as_slice()).unwrap();
|
||||
let dol_header = DolHeader::ref_from_bytes(raw_dol.as_slice()).unwrap();
|
||||
let dol_size = dol_header
|
||||
.text_offs
|
||||
.iter()
|
||||
|
@ -184,13 +208,14 @@ pub(crate) fn read_part_meta(
|
|||
.unwrap_or(size_of::<DolHeader>() as u32);
|
||||
raw_dol.resize(dol_size as usize, 0);
|
||||
reader.read_exact(&mut raw_dol[size_of::<DolHeader>()..]).context("Reading DOL")?;
|
||||
let raw_dol = raw_dol.into_boxed_slice();
|
||||
|
||||
Ok(Box::new(PartitionMeta {
|
||||
raw_boot,
|
||||
raw_bi2,
|
||||
raw_apploader: raw_apploader.into_boxed_slice(),
|
||||
raw_apploader,
|
||||
raw_fst,
|
||||
raw_dol: raw_dol.into_boxed_slice(),
|
||||
raw_dol,
|
||||
raw_ticket: None,
|
||||
raw_tmd: None,
|
||||
raw_cert_chain: None,
|
||||
|
|
|
@ -6,7 +6,7 @@ use std::{
|
|||
|
||||
use rayon::iter::{IntoParallelIterator, ParallelIterator};
|
||||
use sha1::{Digest, Sha1};
|
||||
use zerocopy::FromZeroes;
|
||||
use zerocopy::FromZeros;
|
||||
|
||||
use crate::{
|
||||
array_ref, array_ref_mut,
|
||||
|
@ -39,7 +39,7 @@ pub struct HashTable {
|
|||
pub h3_hashes: Box<[HashBytes]>,
|
||||
}
|
||||
|
||||
#[derive(Clone, FromZeroes)]
|
||||
#[derive(Clone, FromZeros)]
|
||||
struct HashResult {
|
||||
h0_hashes: [HashBytes; 1984],
|
||||
h1_hashes: [HashBytes; 64],
|
||||
|
@ -54,10 +54,10 @@ impl HashTable {
|
|||
let num_subgroups = num_sectors / 8;
|
||||
let num_groups = num_subgroups / 8;
|
||||
Self {
|
||||
h0_hashes: HashBytes::new_box_slice_zeroed(num_data_hashes),
|
||||
h1_hashes: HashBytes::new_box_slice_zeroed(num_sectors),
|
||||
h2_hashes: HashBytes::new_box_slice_zeroed(num_subgroups),
|
||||
h3_hashes: HashBytes::new_box_slice_zeroed(num_groups),
|
||||
h0_hashes: <[HashBytes]>::new_box_zeroed_with_elems(num_data_hashes).unwrap(),
|
||||
h1_hashes: <[HashBytes]>::new_box_zeroed_with_elems(num_sectors).unwrap(),
|
||||
h2_hashes: <[HashBytes]>::new_box_zeroed_with_elems(num_subgroups).unwrap(),
|
||||
h3_hashes: <[HashBytes]>::new_box_zeroed_with_elems(num_groups).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -100,8 +100,8 @@ pub fn rebuild_hashes(reader: &mut DiscReader) -> Result<()> {
|
|||
(0..group_count).into_par_iter().try_for_each_with(
|
||||
(reader.open_partition(part.index, &OpenOptions::default())?, mutex.clone()),
|
||||
|(stream, mutex), h3_index| -> Result<()> {
|
||||
let mut result = HashResult::new_box_zeroed();
|
||||
let mut data_buf = <u8>::new_box_slice_zeroed(SECTOR_DATA_SIZE);
|
||||
let mut result = HashResult::new_box_zeroed()?;
|
||||
let mut data_buf = <[u8]>::new_box_zeroed_with_elems(SECTOR_DATA_SIZE)?;
|
||||
let mut h3_hasher = Sha1::new();
|
||||
for h2_index in 0..8 {
|
||||
let mut h2_hasher = Sha1::new();
|
||||
|
|
|
@ -5,33 +5,40 @@ use std::{
|
|||
ffi::CStr,
|
||||
fmt::{Debug, Display, Formatter},
|
||||
io,
|
||||
io::{BufRead, Seek},
|
||||
mem::size_of,
|
||||
str::from_utf8,
|
||||
};
|
||||
|
||||
use dyn_clone::DynClone;
|
||||
use zerocopy::{big_endian::*, AsBytes, FromBytes, FromZeroes};
|
||||
use zerocopy::{big_endian::*, FromBytes, Immutable, IntoBytes, KnownLayout};
|
||||
|
||||
use crate::{
|
||||
disc::wii::{Ticket, TmdHeader},
|
||||
fst::Node,
|
||||
static_assert,
|
||||
streams::{ReadStream, SharedWindowedReadStream},
|
||||
Fst, Result,
|
||||
};
|
||||
use crate::{io::MagicBytes, static_assert, Result};
|
||||
|
||||
pub(crate) mod fst;
|
||||
pub(crate) mod gcn;
|
||||
pub(crate) mod hashes;
|
||||
pub(crate) mod reader;
|
||||
pub(crate) mod streams;
|
||||
pub(crate) mod wii;
|
||||
|
||||
/// Size in bytes of a disc sector.
|
||||
pub use fst::{Fst, Node, NodeKind};
|
||||
pub use streams::{FileStream, OwnedFileStream, WindowedStream};
|
||||
pub use wii::{SignedHeader, Ticket, TicketLimit, TmdHeader, REGION_SIZE};
|
||||
|
||||
/// Size in bytes of a disc sector. (32 KiB)
|
||||
pub const SECTOR_SIZE: usize = 0x8000;
|
||||
|
||||
/// Magic bytes for Wii discs. Located at offset 0x18.
|
||||
pub const WII_MAGIC: MagicBytes = [0x5D, 0x1C, 0x9E, 0xA3];
|
||||
|
||||
/// Magic bytes for GameCube discs. Located at offset 0x1C.
|
||||
pub const GCN_MAGIC: MagicBytes = [0xC2, 0x33, 0x9F, 0x3D];
|
||||
|
||||
/// Shared GameCube & Wii disc header.
|
||||
///
|
||||
/// This header is always at the start of the disc image and within each Wii partition.
|
||||
#[derive(Clone, Debug, PartialEq, FromBytes, FromZeroes, AsBytes)]
|
||||
#[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
|
||||
#[repr(C, align(4))]
|
||||
pub struct DiscHeader {
|
||||
/// Game ID (e.g. GM8E01 for Metroid Prime)
|
||||
|
@ -47,9 +54,9 @@ pub struct DiscHeader {
|
|||
/// Padding
|
||||
_pad1: [u8; 14],
|
||||
/// If this is a Wii disc, this will be 0x5D1C9EA3
|
||||
pub wii_magic: U32,
|
||||
pub wii_magic: MagicBytes,
|
||||
/// If this is a GameCube disc, this will be 0xC2339F3D
|
||||
pub gcn_magic: U32,
|
||||
pub gcn_magic: MagicBytes,
|
||||
/// Game title
|
||||
pub game_title: [u8; 64],
|
||||
/// If 1, disc omits partition hashes
|
||||
|
@ -64,9 +71,11 @@ static_assert!(size_of::<DiscHeader>() == 0x400);
|
|||
|
||||
impl DiscHeader {
|
||||
/// Game ID as a string.
|
||||
#[inline]
|
||||
pub fn game_id_str(&self) -> &str { from_utf8(&self.game_id).unwrap_or("[invalid]") }
|
||||
|
||||
/// Game title as a string.
|
||||
#[inline]
|
||||
pub fn game_title_str(&self) -> &str {
|
||||
CStr::from_bytes_until_nul(&self.game_title)
|
||||
.ok()
|
||||
|
@ -75,10 +84,12 @@ impl DiscHeader {
|
|||
}
|
||||
|
||||
/// Whether this is a GameCube disc.
|
||||
pub fn is_gamecube(&self) -> bool { self.gcn_magic.get() == 0xC2339F3D }
|
||||
#[inline]
|
||||
pub fn is_gamecube(&self) -> bool { self.gcn_magic == GCN_MAGIC }
|
||||
|
||||
/// Whether this is a Wii disc.
|
||||
pub fn is_wii(&self) -> bool { self.wii_magic.get() == 0x5D1C9EA3 }
|
||||
#[inline]
|
||||
pub fn is_wii(&self) -> bool { self.wii_magic == WII_MAGIC }
|
||||
}
|
||||
|
||||
/// A header describing the contents of a disc partition.
|
||||
|
@ -86,7 +97,7 @@ impl DiscHeader {
|
|||
/// **GameCube**: Always follows the disc header.
|
||||
///
|
||||
/// **Wii**: Follows the disc header within each partition.
|
||||
#[derive(Clone, Debug, PartialEq, FromBytes, FromZeroes, AsBytes)]
|
||||
#[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
|
||||
#[repr(C, align(4))]
|
||||
pub struct PartitionHeader {
|
||||
/// Debug monitor offset
|
||||
|
@ -105,9 +116,9 @@ pub struct PartitionHeader {
|
|||
pub fst_max_size: U32,
|
||||
/// File system table load address
|
||||
pub fst_memory_address: U32,
|
||||
/// User position
|
||||
pub user_position: U32,
|
||||
/// User size
|
||||
/// User data offset
|
||||
pub user_offset: U32,
|
||||
/// User data size
|
||||
pub user_size: U32,
|
||||
/// Padding
|
||||
_pad2: [u8; 4],
|
||||
|
@ -117,6 +128,7 @@ static_assert!(size_of::<PartitionHeader>() == 0x40);
|
|||
|
||||
impl PartitionHeader {
|
||||
/// Offset within the partition to the main DOL.
|
||||
#[inline]
|
||||
pub fn dol_offset(&self, is_wii: bool) -> u64 {
|
||||
if is_wii {
|
||||
self.dol_offset.get() as u64 * 4
|
||||
|
@ -126,6 +138,7 @@ impl PartitionHeader {
|
|||
}
|
||||
|
||||
/// Offset within the partition to the file system table (FST).
|
||||
#[inline]
|
||||
pub fn fst_offset(&self, is_wii: bool) -> u64 {
|
||||
if is_wii {
|
||||
self.fst_offset.get() as u64 * 4
|
||||
|
@ -135,6 +148,7 @@ impl PartitionHeader {
|
|||
}
|
||||
|
||||
/// Size of the file system table (FST).
|
||||
#[inline]
|
||||
pub fn fst_size(&self, is_wii: bool) -> u64 {
|
||||
if is_wii {
|
||||
self.fst_size.get() as u64 * 4
|
||||
|
@ -144,6 +158,7 @@ impl PartitionHeader {
|
|||
}
|
||||
|
||||
/// Maximum size of the file system table (FST) across multi-disc games.
|
||||
#[inline]
|
||||
pub fn fst_max_size(&self, is_wii: bool) -> u64 {
|
||||
if is_wii {
|
||||
self.fst_max_size.get() as u64 * 4
|
||||
|
@ -154,7 +169,7 @@ impl PartitionHeader {
|
|||
}
|
||||
|
||||
/// Apploader header.
|
||||
#[derive(Debug, PartialEq, Clone, FromBytes, FromZeroes, AsBytes)]
|
||||
#[derive(Debug, PartialEq, Clone, FromBytes, IntoBytes, Immutable, KnownLayout)]
|
||||
#[repr(C, align(4))]
|
||||
pub struct ApploaderHeader {
|
||||
/// Apploader build date
|
||||
|
@ -171,6 +186,7 @@ pub struct ApploaderHeader {
|
|||
|
||||
impl ApploaderHeader {
|
||||
/// Apploader build date as a string.
|
||||
#[inline]
|
||||
pub fn date_str(&self) -> Option<&str> {
|
||||
CStr::from_bytes_until_nul(&self.date).ok().and_then(|c| c.to_str().ok())
|
||||
}
|
||||
|
@ -182,7 +198,7 @@ pub const DOL_MAX_TEXT_SECTIONS: usize = 7;
|
|||
pub const DOL_MAX_DATA_SECTIONS: usize = 11;
|
||||
|
||||
/// Dolphin executable (DOL) header.
|
||||
#[derive(Debug, Clone, FromBytes, FromZeroes)]
|
||||
#[derive(Debug, Clone, FromBytes, Immutable, KnownLayout)]
|
||||
pub struct DolHeader {
|
||||
/// Text section offsets
|
||||
pub text_offs: [U32; DOL_MAX_TEXT_SECTIONS],
|
||||
|
@ -222,6 +238,7 @@ pub enum PartitionKind {
|
|||
}
|
||||
|
||||
impl Display for PartitionKind {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Data => write!(f, "Data"),
|
||||
|
@ -237,6 +254,7 @@ impl Display for PartitionKind {
|
|||
|
||||
impl PartitionKind {
|
||||
/// Returns the directory name for the partition kind.
|
||||
#[inline]
|
||||
pub fn dir_name(&self) -> Cow<str> {
|
||||
match self {
|
||||
Self::Data => Cow::Borrowed("DATA"),
|
||||
|
@ -251,6 +269,7 @@ impl PartitionKind {
|
|||
}
|
||||
|
||||
impl From<u32> for PartitionKind {
|
||||
#[inline]
|
||||
fn from(v: u32) -> Self {
|
||||
match v {
|
||||
0 => Self::Data,
|
||||
|
@ -262,11 +281,11 @@ impl From<u32> for PartitionKind {
|
|||
}
|
||||
|
||||
/// An open disc partition.
|
||||
pub trait PartitionBase: DynClone + ReadStream + Send + Sync {
|
||||
pub trait PartitionBase: DynClone + BufRead + Seek + Send + Sync {
|
||||
/// Reads the partition header and file system table.
|
||||
fn meta(&mut self) -> Result<Box<PartitionMeta>>;
|
||||
|
||||
/// Seeks the read stream to the specified file system node
|
||||
/// Seeks the partition stream to the specified file system node
|
||||
/// and returns a windowed stream.
|
||||
///
|
||||
/// # Examples
|
||||
|
@ -294,12 +313,36 @@ pub trait PartitionBase: DynClone + ReadStream + Send + Sync {
|
|||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
fn open_file(&mut self, node: &Node) -> io::Result<SharedWindowedReadStream>;
|
||||
fn open_file(&mut self, node: Node) -> io::Result<FileStream>;
|
||||
|
||||
/// The ideal size for buffered reads from this partition.
|
||||
/// GameCube discs have a data block size of 0x8000,
|
||||
/// whereas Wii discs have a data block size of 0x7C00.
|
||||
fn ideal_buffer_size(&self) -> usize;
|
||||
/// Consumes the partition instance and returns a windowed stream.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use std::io::Read;
|
||||
///
|
||||
/// use nod::{Disc, PartitionKind, OwnedFileStream};
|
||||
///
|
||||
/// fn main() -> nod::Result<()> {
|
||||
/// let disc = Disc::new("path/to/file.iso")?;
|
||||
/// let mut partition = disc.open_partition_kind(PartitionKind::Data)?;
|
||||
/// let meta = partition.meta()?;
|
||||
/// let fst = meta.fst()?;
|
||||
/// if let Some((_, node)) = fst.find("/disc.tgc") {
|
||||
/// let file: OwnedFileStream = partition
|
||||
/// .clone() // Clone the Box<dyn PartitionBase>
|
||||
/// .into_open_file(node) // Get an OwnedFileStream
|
||||
/// .expect("Failed to open file stream");
|
||||
/// // Open the inner disc image using the owned stream
|
||||
/// let inner_disc = Disc::new_stream(Box::new(file))
|
||||
/// .expect("Failed to open inner disc");
|
||||
/// // ...
|
||||
/// }
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
fn into_open_file(self: Box<Self>, node: Node) -> io::Result<OwnedFileStream>;
|
||||
}
|
||||
|
||||
dyn_clone::clone_trait_object!(PartitionBase);
|
||||
|
@ -318,10 +361,10 @@ pub struct PartitionMeta {
|
|||
pub raw_bi2: Box<[u8; BI2_SIZE]>,
|
||||
/// Apploader (apploader.bin)
|
||||
pub raw_apploader: Box<[u8]>,
|
||||
/// File system table (fst.bin)
|
||||
pub raw_fst: Box<[u8]>,
|
||||
/// Main binary (main.dol)
|
||||
pub raw_dol: Box<[u8]>,
|
||||
/// File system table (fst.bin)
|
||||
pub raw_fst: Box<[u8]>,
|
||||
/// Ticket (ticket.bin, Wii only)
|
||||
pub raw_ticket: Option<Box<[u8]>>,
|
||||
/// TMD (tmd.bin, Wii only)
|
||||
|
@ -334,34 +377,41 @@ pub struct PartitionMeta {
|
|||
|
||||
impl PartitionMeta {
|
||||
/// A view into the disc header.
|
||||
#[inline]
|
||||
pub fn header(&self) -> &DiscHeader {
|
||||
DiscHeader::ref_from(&self.raw_boot[..size_of::<DiscHeader>()]).unwrap()
|
||||
DiscHeader::ref_from_bytes(&self.raw_boot[..size_of::<DiscHeader>()]).unwrap()
|
||||
}
|
||||
|
||||
/// A view into the partition header.
|
||||
#[inline]
|
||||
pub fn partition_header(&self) -> &PartitionHeader {
|
||||
PartitionHeader::ref_from(&self.raw_boot[size_of::<DiscHeader>()..]).unwrap()
|
||||
PartitionHeader::ref_from_bytes(&self.raw_boot[size_of::<DiscHeader>()..]).unwrap()
|
||||
}
|
||||
|
||||
/// A view into the apploader header.
|
||||
#[inline]
|
||||
pub fn apploader_header(&self) -> &ApploaderHeader {
|
||||
ApploaderHeader::ref_from_prefix(&self.raw_apploader).unwrap()
|
||||
ApploaderHeader::ref_from_prefix(&self.raw_apploader).unwrap().0
|
||||
}
|
||||
|
||||
/// A view into the file system table (FST).
|
||||
#[inline]
|
||||
pub fn fst(&self) -> Result<Fst, &'static str> { Fst::new(&self.raw_fst) }
|
||||
|
||||
/// A view into the DOL header.
|
||||
pub fn dol_header(&self) -> &DolHeader { DolHeader::ref_from_prefix(&self.raw_dol).unwrap() }
|
||||
#[inline]
|
||||
pub fn dol_header(&self) -> &DolHeader { DolHeader::ref_from_prefix(&self.raw_dol).unwrap().0 }
|
||||
|
||||
/// A view into the ticket. (Wii only)
|
||||
#[inline]
|
||||
pub fn ticket(&self) -> Option<&Ticket> {
|
||||
self.raw_ticket.as_ref().and_then(|v| Ticket::ref_from(v))
|
||||
self.raw_ticket.as_ref().and_then(|v| Ticket::ref_from_bytes(v).ok())
|
||||
}
|
||||
|
||||
/// A view into the TMD. (Wii only)
|
||||
#[inline]
|
||||
pub fn tmd_header(&self) -> Option<&TmdHeader> {
|
||||
self.raw_tmd.as_ref().and_then(|v| TmdHeader::ref_from_prefix(v))
|
||||
self.raw_tmd.as_ref().and_then(|v| TmdHeader::ref_from_prefix(v).ok().map(|(v, _)| v))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,22 +1,22 @@
|
|||
use std::{
|
||||
cmp::min,
|
||||
io,
|
||||
io::{Read, Seek, SeekFrom},
|
||||
io::{BufRead, Read, Seek, SeekFrom},
|
||||
};
|
||||
|
||||
use zerocopy::FromZeroes;
|
||||
use zerocopy::FromZeros;
|
||||
|
||||
use super::{
|
||||
gcn::PartitionGC,
|
||||
hashes::{rebuild_hashes, HashTable},
|
||||
wii::{PartitionWii, WiiPartEntry, WiiPartGroup, WiiPartitionHeader, WII_PART_GROUP_OFF},
|
||||
DiscHeader, PartitionBase, PartitionHeader, PartitionKind, DL_DVD_SIZE, MINI_DVD_SIZE,
|
||||
REGION_SIZE, SL_DVD_SIZE,
|
||||
};
|
||||
use crate::{
|
||||
disc::{
|
||||
gcn::PartitionGC,
|
||||
hashes::{rebuild_hashes, HashTable},
|
||||
wii::{PartitionWii, WiiPartEntry, WiiPartGroup, WiiPartitionHeader, WII_PART_GROUP_OFF},
|
||||
DL_DVD_SIZE, MINI_DVD_SIZE, SL_DVD_SIZE,
|
||||
},
|
||||
disc::wii::REGION_OFFSET,
|
||||
io::block::{Block, BlockIO, PartitionInfo},
|
||||
util::read::{read_box, read_from, read_vec},
|
||||
DiscHeader, DiscMeta, Error, OpenOptions, PartitionBase, PartitionHeader, PartitionKind,
|
||||
Result, ResultContext, SECTOR_SIZE,
|
||||
DiscMeta, Error, OpenOptions, Result, ResultContext, SECTOR_SIZE,
|
||||
};
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
|
||||
|
@ -37,6 +37,7 @@ pub struct DiscReader {
|
|||
disc_header: Box<DiscHeader>,
|
||||
pub(crate) partitions: Vec<PartitionInfo>,
|
||||
hash_tables: Vec<HashTable>,
|
||||
region: Option<[u8; REGION_SIZE]>,
|
||||
}
|
||||
|
||||
impl Clone for DiscReader {
|
||||
|
@ -44,15 +45,16 @@ impl Clone for DiscReader {
|
|||
Self {
|
||||
io: self.io.clone(),
|
||||
block: Block::default(),
|
||||
block_buf: <u8>::new_box_slice_zeroed(self.block_buf.len()),
|
||||
block_buf: <[u8]>::new_box_zeroed_with_elems(self.block_buf.len()).unwrap(),
|
||||
block_idx: u32::MAX,
|
||||
sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed(),
|
||||
sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed().unwrap(),
|
||||
sector_idx: u32::MAX,
|
||||
pos: 0,
|
||||
mode: self.mode,
|
||||
disc_header: self.disc_header.clone(),
|
||||
partitions: self.partitions.clone(),
|
||||
hash_tables: self.hash_tables.clone(),
|
||||
region: self.region,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -64,9 +66,9 @@ impl DiscReader {
|
|||
let mut reader = Self {
|
||||
io: inner,
|
||||
block: Block::default(),
|
||||
block_buf: <u8>::new_box_slice_zeroed(block_size as usize),
|
||||
block_buf: <[u8]>::new_box_zeroed_with_elems(block_size as usize)?,
|
||||
block_idx: u32::MAX,
|
||||
sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed(),
|
||||
sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed()?,
|
||||
sector_idx: u32::MAX,
|
||||
pos: 0,
|
||||
mode: if options.rebuild_encryption {
|
||||
|
@ -74,13 +76,16 @@ impl DiscReader {
|
|||
} else {
|
||||
EncryptionMode::Decrypted
|
||||
},
|
||||
disc_header: DiscHeader::new_box_zeroed(),
|
||||
disc_header: DiscHeader::new_box_zeroed()?,
|
||||
partitions: vec![],
|
||||
hash_tables: vec![],
|
||||
region: None,
|
||||
};
|
||||
let disc_header: Box<DiscHeader> = read_box(&mut reader).context("Reading disc header")?;
|
||||
reader.disc_header = disc_header;
|
||||
if reader.disc_header.is_wii() {
|
||||
reader.seek(SeekFrom::Start(REGION_OFFSET)).context("Seeking to region info")?;
|
||||
reader.region = Some(read_from(&mut reader).context("Reading region info")?);
|
||||
reader.partitions = read_partition_info(&mut reader)?;
|
||||
// Rebuild hashes if the format requires it
|
||||
if (options.rebuild_encryption || options.validate_hashes) && meta.needs_hash_recovery {
|
||||
|
@ -104,10 +109,16 @@ impl DiscReader {
|
|||
self.io.meta().disc_size.unwrap_or_else(|| guess_disc_size(&self.partitions))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn header(&self) -> &DiscHeader { &self.disc_header }
|
||||
|
||||
#[inline]
|
||||
pub fn region(&self) -> Option<&[u8; REGION_SIZE]> { self.region.as_ref() }
|
||||
|
||||
#[inline]
|
||||
pub fn partitions(&self) -> &[PartitionInfo] { &self.partitions }
|
||||
|
||||
#[inline]
|
||||
pub fn meta(&self) -> DiscMeta { self.io.meta() }
|
||||
|
||||
/// Opens a new, decrypted partition read stream for the specified partition index.
|
||||
|
@ -150,8 +161,8 @@ impl DiscReader {
|
|||
}
|
||||
}
|
||||
|
||||
impl Read for DiscReader {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
impl BufRead for DiscReader {
|
||||
fn fill_buf(&mut self) -> io::Result<&[u8]> {
|
||||
let block_idx = (self.pos / self.block_buf.len() as u64) as u32;
|
||||
let abs_sector = (self.pos / SECTOR_SIZE as u64) as u32;
|
||||
|
||||
|
@ -199,9 +210,20 @@ impl Read for DiscReader {
|
|||
|
||||
// Read from sector buffer
|
||||
let offset = (self.pos % SECTOR_SIZE as u64) as usize;
|
||||
let len = min(buf.len(), SECTOR_SIZE - offset);
|
||||
buf[..len].copy_from_slice(&self.sector_buf[offset..offset + len]);
|
||||
self.pos += len as u64;
|
||||
Ok(&self.sector_buf[offset..])
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn consume(&mut self, amt: usize) { self.pos += amt as u64; }
|
||||
}
|
||||
|
||||
impl Read for DiscReader {
|
||||
#[inline]
|
||||
fn read(&mut self, out: &mut [u8]) -> io::Result<usize> {
|
||||
let buf = self.fill_buf()?;
|
||||
let len = buf.len().min(out.len());
|
||||
out[..len].copy_from_slice(&buf[..len]);
|
||||
self.consume(len);
|
||||
Ok(len)
|
||||
}
|
||||
}
|
||||
|
@ -268,8 +290,8 @@ fn read_partition_info(reader: &mut DiscReader) -> Result<Vec<PartitionInfo>> {
|
|||
data_end_sector: (data_end_offset / SECTOR_SIZE as u64) as u32,
|
||||
key,
|
||||
header,
|
||||
disc_header: DiscHeader::new_box_zeroed(),
|
||||
partition_header: PartitionHeader::new_box_zeroed(),
|
||||
disc_header: DiscHeader::new_box_zeroed()?,
|
||||
partition_header: PartitionHeader::new_box_zeroed()?,
|
||||
hash_table: None,
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
//! Partition file read stream.
|
||||
|
||||
use std::{
|
||||
io,
|
||||
io::{BufRead, Read, Seek, SeekFrom},
|
||||
};
|
||||
|
||||
use super::PartitionBase;
|
||||
|
||||
/// A file read stream borrowing a [`PartitionBase`].
|
||||
pub type FileStream<'a> = WindowedStream<&'a mut dyn PartitionBase>;
|
||||
|
||||
/// A file read stream owning a [`PartitionBase`].
|
||||
pub type OwnedFileStream = WindowedStream<Box<dyn PartitionBase>>;
|
||||
|
||||
/// A read stream with a fixed window.
|
||||
#[derive(Clone)]
|
||||
pub struct WindowedStream<T>
|
||||
where T: BufRead + Seek
|
||||
{
|
||||
base: T,
|
||||
pos: u64,
|
||||
begin: u64,
|
||||
end: u64,
|
||||
}
|
||||
|
||||
impl<T> WindowedStream<T>
|
||||
where T: BufRead + Seek
|
||||
{
|
||||
/// Creates a new windowed stream with offset and size.
|
||||
///
|
||||
/// Seeks underlying stream immediately.
|
||||
#[inline]
|
||||
pub fn new(mut base: T, offset: u64, size: u64) -> io::Result<Self> {
|
||||
base.seek(SeekFrom::Start(offset))?;
|
||||
Ok(Self { base, pos: offset, begin: offset, end: offset + size })
|
||||
}
|
||||
|
||||
/// Returns the length of the window.
|
||||
#[inline]
|
||||
#[allow(clippy::len_without_is_empty)]
|
||||
pub fn len(&self) -> u64 { self.end - self.begin }
|
||||
}
|
||||
|
||||
impl<T> Read for WindowedStream<T>
|
||||
where T: BufRead + Seek
|
||||
{
|
||||
#[inline]
|
||||
fn read(&mut self, out: &mut [u8]) -> io::Result<usize> {
|
||||
let buf = self.fill_buf()?;
|
||||
let len = buf.len().min(out.len());
|
||||
out[..len].copy_from_slice(&buf[..len]);
|
||||
self.consume(len);
|
||||
Ok(len)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> BufRead for WindowedStream<T>
|
||||
where T: BufRead + Seek
|
||||
{
|
||||
#[inline]
|
||||
fn fill_buf(&mut self) -> io::Result<&[u8]> {
|
||||
let limit = self.end.saturating_sub(self.pos);
|
||||
if limit == 0 {
|
||||
return Ok(&[]);
|
||||
}
|
||||
let buf = self.base.fill_buf()?;
|
||||
let max = (buf.len() as u64).min(limit) as usize;
|
||||
Ok(&buf[..max])
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn consume(&mut self, amt: usize) {
|
||||
self.base.consume(amt);
|
||||
self.pos += amt as u64;
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Seek for WindowedStream<T>
|
||||
where T: BufRead + Seek
|
||||
{
|
||||
#[inline]
|
||||
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
||||
let mut pos = match pos {
|
||||
SeekFrom::Start(p) => self.begin + p,
|
||||
SeekFrom::End(p) => self.end.saturating_add_signed(p),
|
||||
SeekFrom::Current(p) => self.pos.saturating_add_signed(p),
|
||||
};
|
||||
if pos < self.begin {
|
||||
pos = self.begin;
|
||||
} else if pos > self.end {
|
||||
pos = self.end;
|
||||
}
|
||||
let result = self.base.seek(SeekFrom::Start(pos))?;
|
||||
self.pos = result;
|
||||
Ok(result - self.begin)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn stream_position(&mut self) -> io::Result<u64> { Ok(self.pos) }
|
||||
}
|
|
@ -1,30 +1,28 @@
|
|||
use std::{
|
||||
cmp::min,
|
||||
ffi::CStr,
|
||||
io,
|
||||
io::{Read, Seek, SeekFrom},
|
||||
io::{BufRead, Read, Seek, SeekFrom},
|
||||
mem::size_of,
|
||||
};
|
||||
|
||||
use sha1::{Digest, Sha1};
|
||||
use zerocopy::{big_endian::*, AsBytes, FromBytes, FromZeroes};
|
||||
use zerocopy::{big_endian::*, FromBytes, FromZeros, Immutable, IntoBytes, KnownLayout};
|
||||
|
||||
use super::{
|
||||
gcn::{read_part_meta, PartitionGC},
|
||||
DiscHeader, FileStream, Node, PartitionBase, PartitionMeta, SECTOR_SIZE,
|
||||
};
|
||||
use crate::{
|
||||
array_ref,
|
||||
disc::{
|
||||
gcn::{read_part_meta, PartitionGC},
|
||||
PartitionBase, PartitionMeta, SECTOR_SIZE,
|
||||
},
|
||||
fst::{Node, NodeKind},
|
||||
disc::streams::OwnedFileStream,
|
||||
io::{
|
||||
aes_decrypt,
|
||||
block::{Block, BlockIO, PartitionInfo},
|
||||
KeyBytes,
|
||||
},
|
||||
static_assert,
|
||||
streams::{ReadStream, SharedWindowedReadStream},
|
||||
util::{div_rem, read::read_box_slice},
|
||||
DiscHeader, Error, OpenOptions, Result, ResultContext,
|
||||
Error, OpenOptions, Result, ResultContext,
|
||||
};
|
||||
|
||||
/// Size in bytes of the hashes block in a Wii disc sector
|
||||
|
@ -33,6 +31,12 @@ pub(crate) const HASHES_SIZE: usize = 0x400;
|
|||
/// Size in bytes of the data block in a Wii disc sector (excluding hashes)
|
||||
pub(crate) const SECTOR_DATA_SIZE: usize = SECTOR_SIZE - HASHES_SIZE; // 0x7C00
|
||||
|
||||
/// Size of the disc region info (region.bin)
|
||||
pub const REGION_SIZE: usize = 0x20;
|
||||
|
||||
/// Offset of the disc region info
|
||||
pub const REGION_OFFSET: u64 = 0x4E000;
|
||||
|
||||
// ppki (Retail)
|
||||
const RVL_CERT_ISSUER_PPKI_TICKET: &str = "Root-CA00000001-XS00000003";
|
||||
#[rustfmt::skip]
|
||||
|
@ -57,7 +61,7 @@ const DEBUG_COMMON_KEYS: [KeyBytes; 3] = [
|
|||
[0x2f, 0x5c, 0x1b, 0x29, 0x44, 0xe7, 0xfd, 0x6f, 0xc3, 0x97, 0x96, 0x4b, 0x05, 0x76, 0x91, 0xfa],
|
||||
];
|
||||
|
||||
#[derive(Debug, PartialEq, FromBytes, FromZeroes, AsBytes)]
|
||||
#[derive(Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
|
||||
#[repr(C, align(4))]
|
||||
pub(crate) struct WiiPartEntry {
|
||||
pub(crate) offset: U32,
|
||||
|
@ -72,7 +76,7 @@ impl WiiPartEntry {
|
|||
|
||||
pub(crate) const WII_PART_GROUP_OFF: u64 = 0x40000;
|
||||
|
||||
#[derive(Debug, PartialEq, FromBytes, FromZeroes, AsBytes)]
|
||||
#[derive(Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
|
||||
#[repr(C, align(4))]
|
||||
pub(crate) struct WiiPartGroup {
|
||||
pub(crate) part_count: U32,
|
||||
|
@ -85,7 +89,8 @@ impl WiiPartGroup {
|
|||
pub(crate) fn part_entry_off(&self) -> u64 { (self.part_entry_off.get() as u64) << 2 }
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, FromBytes, FromZeroes, AsBytes)]
|
||||
/// Signed blob header
|
||||
#[derive(Debug, Clone, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
|
||||
#[repr(C, align(4))]
|
||||
pub struct SignedHeader {
|
||||
/// Signature type, always 0x00010001 (RSA-2048)
|
||||
|
@ -97,43 +102,64 @@ pub struct SignedHeader {
|
|||
|
||||
static_assert!(size_of::<SignedHeader>() == 0x140);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Default, FromBytes, FromZeroes, AsBytes)]
|
||||
/// Ticket limit
|
||||
#[derive(Debug, Clone, PartialEq, Default, FromBytes, IntoBytes, Immutable, KnownLayout)]
|
||||
#[repr(C, align(4))]
|
||||
pub struct TicketTimeLimit {
|
||||
pub enable_time_limit: U32,
|
||||
pub time_limit: U32,
|
||||
pub struct TicketLimit {
|
||||
/// Limit type
|
||||
pub limit_type: U32,
|
||||
/// Maximum value for the limit
|
||||
pub max_value: U32,
|
||||
}
|
||||
|
||||
static_assert!(size_of::<TicketTimeLimit>() == 8);
|
||||
static_assert!(size_of::<TicketLimit>() == 8);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, FromBytes, FromZeroes, AsBytes)]
|
||||
/// Wii ticket
|
||||
#[derive(Debug, Clone, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
|
||||
#[repr(C, align(4))]
|
||||
pub struct Ticket {
|
||||
/// Signed blob header
|
||||
pub header: SignedHeader,
|
||||
/// Signature issuer
|
||||
pub sig_issuer: [u8; 64],
|
||||
/// ECDH data
|
||||
pub ecdh: [u8; 60],
|
||||
/// Ticket format version
|
||||
pub version: u8,
|
||||
_pad1: U16,
|
||||
/// Title key (encrypted)
|
||||
pub title_key: KeyBytes,
|
||||
_pad2: u8,
|
||||
/// Ticket ID
|
||||
pub ticket_id: [u8; 8],
|
||||
/// Console ID
|
||||
pub console_id: [u8; 4],
|
||||
/// Title ID
|
||||
pub title_id: [u8; 8],
|
||||
_pad3: U16,
|
||||
/// Ticket title version
|
||||
pub ticket_title_version: U16,
|
||||
/// Permitted titles mask
|
||||
pub permitted_titles_mask: U32,
|
||||
/// Permit mask
|
||||
pub permit_mask: U32,
|
||||
/// Title export allowed
|
||||
pub title_export_allowed: u8,
|
||||
/// Common key index
|
||||
pub common_key_idx: u8,
|
||||
_pad4: [u8; 48],
|
||||
/// Content access permissions
|
||||
pub content_access_permissions: [u8; 64],
|
||||
_pad5: [u8; 2],
|
||||
pub time_limits: [TicketTimeLimit; 8],
|
||||
/// Ticket limits
|
||||
pub limits: [TicketLimit; 8],
|
||||
}
|
||||
|
||||
static_assert!(size_of::<Ticket>() == 0x2A4);
|
||||
|
||||
impl Ticket {
|
||||
/// Decrypts the ticket title key using the appropriate common key
|
||||
#[allow(clippy::missing_inline_in_public_items)]
|
||||
pub fn decrypt_title_key(&self) -> Result<KeyBytes> {
|
||||
let mut iv: KeyBytes = [0; 16];
|
||||
iv[..8].copy_from_slice(&self.title_id);
|
||||
|
@ -158,29 +184,48 @@ impl Ticket {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, FromBytes, FromZeroes, AsBytes)]
|
||||
/// Title metadata header
|
||||
#[derive(Debug, Clone, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
|
||||
#[repr(C, align(4))]
|
||||
pub struct TmdHeader {
|
||||
/// Signed blob header
|
||||
pub header: SignedHeader,
|
||||
/// Signature issuer
|
||||
pub sig_issuer: [u8; 64],
|
||||
/// Version
|
||||
pub version: u8,
|
||||
/// CA CRL version
|
||||
pub ca_crl_version: u8,
|
||||
/// Signer CRL version
|
||||
pub signer_crl_version: u8,
|
||||
/// Is vWii title
|
||||
pub is_vwii: u8,
|
||||
/// IOS ID
|
||||
pub ios_id: [u8; 8],
|
||||
/// Title ID
|
||||
pub title_id: [u8; 8],
|
||||
/// Title type
|
||||
pub title_type: u32,
|
||||
/// Group ID
|
||||
pub group_id: U16,
|
||||
_pad1: [u8; 2],
|
||||
/// Region
|
||||
pub region: U16,
|
||||
/// Ratings
|
||||
pub ratings: KeyBytes,
|
||||
_pad2: [u8; 12],
|
||||
/// IPC mask
|
||||
pub ipc_mask: [u8; 12],
|
||||
_pad3: [u8; 18],
|
||||
/// Access flags
|
||||
pub access_flags: U32,
|
||||
/// Title version
|
||||
pub title_version: U16,
|
||||
/// Number of contents
|
||||
pub num_contents: U16,
|
||||
/// Boot index
|
||||
pub boot_idx: U16,
|
||||
/// Minor version (unused)
|
||||
pub minor_version: U16,
|
||||
}
|
||||
|
||||
|
@ -188,7 +233,7 @@ static_assert!(size_of::<TmdHeader>() == 0x1E4);
|
|||
|
||||
pub const H3_TABLE_SIZE: usize = 0x18000;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, FromBytes, FromZeroes, AsBytes)]
|
||||
#[derive(Debug, Clone, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
|
||||
#[repr(C, align(4))]
|
||||
pub struct WiiPartitionHeader {
|
||||
pub ticket: Ticket,
|
||||
|
@ -242,9 +287,9 @@ impl Clone for PartitionWii {
|
|||
io: self.io.clone(),
|
||||
partition: self.partition.clone(),
|
||||
block: Block::default(),
|
||||
block_buf: <u8>::new_box_slice_zeroed(self.block_buf.len()),
|
||||
block_buf: <[u8]>::new_box_zeroed_with_elems(self.block_buf.len()).unwrap(),
|
||||
block_idx: u32::MAX,
|
||||
sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed(),
|
||||
sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed().unwrap(),
|
||||
sector: u32::MAX,
|
||||
pos: 0,
|
||||
verify: self.verify,
|
||||
|
@ -288,9 +333,9 @@ impl PartitionWii {
|
|||
io: reader.into_inner(),
|
||||
partition: partition.clone(),
|
||||
block: Block::default(),
|
||||
block_buf: <u8>::new_box_slice_zeroed(block_size as usize),
|
||||
block_buf: <[u8]>::new_box_zeroed_with_elems(block_size as usize)?,
|
||||
block_idx: u32::MAX,
|
||||
sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed(),
|
||||
sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed()?,
|
||||
sector: u32::MAX,
|
||||
pos: 0,
|
||||
verify: options.validate_hashes,
|
||||
|
@ -301,12 +346,12 @@ impl PartitionWii {
|
|||
}
|
||||
}
|
||||
|
||||
impl Read for PartitionWii {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
impl BufRead for PartitionWii {
|
||||
fn fill_buf(&mut self) -> io::Result<&[u8]> {
|
||||
let part_sector = (self.pos / SECTOR_DATA_SIZE as u64) as u32;
|
||||
let abs_sector = self.partition.data_start_sector + part_sector;
|
||||
if abs_sector >= self.partition.data_end_sector {
|
||||
return Ok(0);
|
||||
return Ok(&[]);
|
||||
}
|
||||
let block_idx =
|
||||
(abs_sector as u64 * SECTOR_SIZE as u64 / self.block_buf.len() as u64) as u32;
|
||||
|
@ -333,10 +378,20 @@ impl Read for PartitionWii {
|
|||
}
|
||||
|
||||
let offset = (self.pos % SECTOR_DATA_SIZE as u64) as usize;
|
||||
let len = min(buf.len(), SECTOR_DATA_SIZE - offset);
|
||||
buf[..len]
|
||||
.copy_from_slice(&self.sector_buf[HASHES_SIZE + offset..HASHES_SIZE + offset + len]);
|
||||
self.pos += len as u64;
|
||||
Ok(&self.sector_buf[HASHES_SIZE + offset..])
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn consume(&mut self, amt: usize) { self.pos += amt as u64; }
|
||||
}
|
||||
|
||||
impl Read for PartitionWii {
|
||||
#[inline]
|
||||
fn read(&mut self, out: &mut [u8]) -> io::Result<usize> {
|
||||
let buf = self.fill_buf()?;
|
||||
let len = buf.len().min(out.len());
|
||||
out[..len].copy_from_slice(&buf[..len]);
|
||||
self.consume(len);
|
||||
Ok(len)
|
||||
}
|
||||
}
|
||||
|
@ -440,10 +495,23 @@ impl PartitionBase for PartitionWii {
|
|||
Ok(meta)
|
||||
}
|
||||
|
||||
fn open_file(&mut self, node: &Node) -> io::Result<SharedWindowedReadStream> {
|
||||
assert_eq!(node.kind(), NodeKind::File);
|
||||
self.new_window(node.offset(true), node.length())
|
||||
fn open_file(&mut self, node: Node) -> io::Result<FileStream> {
|
||||
if !node.is_file() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"Node is not a file".to_string(),
|
||||
));
|
||||
}
|
||||
FileStream::new(self, node.offset(true), node.length())
|
||||
}
|
||||
|
||||
fn ideal_buffer_size(&self) -> usize { SECTOR_DATA_SIZE }
|
||||
fn into_open_file(self: Box<Self>, node: Node) -> io::Result<OwnedFileStream> {
|
||||
if !node.is_file() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"Node is not a file".to_string(),
|
||||
));
|
||||
}
|
||||
OwnedFileStream::new(self, node.offset(true), node.length())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
use std::{cmp::min, fs, fs::File, io, path::Path};
|
||||
use std::{
|
||||
fs, io,
|
||||
io::{Read, Seek},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use dyn_clone::DynClone;
|
||||
use zerocopy::transmute_ref;
|
||||
|
@ -8,13 +12,22 @@ use crate::{
|
|||
disc::{
|
||||
hashes::HashTable,
|
||||
wii::{WiiPartitionHeader, HASHES_SIZE, SECTOR_DATA_SIZE},
|
||||
SECTOR_SIZE,
|
||||
DiscHeader, PartitionHeader, PartitionKind, GCN_MAGIC, SECTOR_SIZE, WII_MAGIC,
|
||||
},
|
||||
io::{
|
||||
aes_decrypt, aes_encrypt, split::SplitFileReader, DiscMeta, Format, KeyBytes, MagicBytes,
|
||||
},
|
||||
io::{aes_decrypt, aes_encrypt, KeyBytes, MagicBytes},
|
||||
util::{lfg::LaggedFibonacci, read::read_from},
|
||||
DiscHeader, DiscMeta, Error, PartitionHeader, PartitionKind, Result, ResultContext,
|
||||
Error, Result, ResultContext,
|
||||
};
|
||||
|
||||
/// Required trait bounds for reading disc images.
|
||||
pub trait DiscStream: Read + Seek + DynClone + Send + Sync {}
|
||||
|
||||
impl<T> DiscStream for T where T: Read + Seek + DynClone + Send + Sync + ?Sized {}
|
||||
|
||||
dyn_clone::clone_trait_object!(DiscStream);
|
||||
|
||||
/// Block I/O trait for reading disc images.
|
||||
pub trait BlockIO: DynClone + Send + Sync {
|
||||
/// Reads a block from the disc image.
|
||||
|
@ -78,7 +91,32 @@ pub trait BlockIO: DynClone + Send + Sync {
|
|||
|
||||
dyn_clone::clone_trait_object!(BlockIO);
|
||||
|
||||
/// Creates a new [`BlockIO`] instance.
|
||||
/// Creates a new [`BlockIO`] instance from a stream.
|
||||
pub fn new(mut stream: Box<dyn DiscStream>) -> Result<Box<dyn BlockIO>> {
|
||||
let io: Box<dyn BlockIO> = match detect(stream.as_mut()).context("Detecting file type")? {
|
||||
Some(Format::Iso) => crate::io::iso::DiscIOISO::new(stream)?,
|
||||
Some(Format::Ciso) => crate::io::ciso::DiscIOCISO::new(stream)?,
|
||||
Some(Format::Gcz) => {
|
||||
#[cfg(feature = "compress-zlib")]
|
||||
{
|
||||
crate::io::gcz::DiscIOGCZ::new(stream)?
|
||||
}
|
||||
#[cfg(not(feature = "compress-zlib"))]
|
||||
return Err(Error::DiscFormat("GCZ support is disabled".to_string()));
|
||||
}
|
||||
Some(Format::Nfs) => {
|
||||
return Err(Error::DiscFormat("NFS requires a filesystem path".to_string()))
|
||||
}
|
||||
Some(Format::Wbfs) => crate::io::wbfs::DiscIOWBFS::new(stream)?,
|
||||
Some(Format::Wia | Format::Rvz) => crate::io::wia::DiscIOWIA::new(stream)?,
|
||||
Some(Format::Tgc) => crate::io::tgc::DiscIOTGC::new(stream)?,
|
||||
None => return Err(Error::DiscFormat("Unknown disc format".to_string())),
|
||||
};
|
||||
check_block_size(io.as_ref())?;
|
||||
Ok(io)
|
||||
}
|
||||
|
||||
/// Creates a new [`BlockIO`] instance from a filesystem path.
|
||||
pub fn open(filename: &Path) -> Result<Box<dyn BlockIO>> {
|
||||
let path_result = fs::canonicalize(filename);
|
||||
if let Err(err) = path_result {
|
||||
|
@ -92,17 +130,19 @@ pub fn open(filename: &Path) -> Result<Box<dyn BlockIO>> {
|
|||
if !meta.unwrap().is_file() {
|
||||
return Err(Error::DiscFormat(format!("Input is not a file: {}", filename.display())));
|
||||
}
|
||||
let magic: MagicBytes = {
|
||||
let mut file =
|
||||
File::open(path).with_context(|| format!("Opening file {}", filename.display()))?;
|
||||
read_from(&mut file)
|
||||
.with_context(|| format!("Reading magic bytes from {}", filename.display()))?
|
||||
};
|
||||
let io: Box<dyn BlockIO> = match magic {
|
||||
crate::io::ciso::CISO_MAGIC => crate::io::ciso::DiscIOCISO::new(path)?,
|
||||
#[cfg(feature = "compress-zlib")]
|
||||
crate::io::gcz::GCZ_MAGIC => crate::io::gcz::DiscIOGCZ::new(path)?,
|
||||
crate::io::nfs::NFS_MAGIC => match path.parent() {
|
||||
let mut stream = Box::new(SplitFileReader::new(filename)?);
|
||||
let io: Box<dyn BlockIO> = match detect(stream.as_mut()).context("Detecting file type")? {
|
||||
Some(Format::Iso) => crate::io::iso::DiscIOISO::new(stream)?,
|
||||
Some(Format::Ciso) => crate::io::ciso::DiscIOCISO::new(stream)?,
|
||||
Some(Format::Gcz) => {
|
||||
#[cfg(feature = "compress-zlib")]
|
||||
{
|
||||
crate::io::gcz::DiscIOGCZ::new(stream)?
|
||||
}
|
||||
#[cfg(not(feature = "compress-zlib"))]
|
||||
return Err(Error::DiscFormat("GCZ support is disabled".to_string()));
|
||||
}
|
||||
Some(Format::Nfs) => match path.parent() {
|
||||
Some(parent) if parent.is_dir() => {
|
||||
crate::io::nfs::DiscIONFS::new(path.parent().unwrap())?
|
||||
}
|
||||
|
@ -110,12 +150,46 @@ pub fn open(filename: &Path) -> Result<Box<dyn BlockIO>> {
|
|||
return Err(Error::DiscFormat("Failed to locate NFS parent directory".to_string()));
|
||||
}
|
||||
},
|
||||
crate::io::wbfs::WBFS_MAGIC => crate::io::wbfs::DiscIOWBFS::new(path)?,
|
||||
crate::io::wia::WIA_MAGIC | crate::io::wia::RVZ_MAGIC => {
|
||||
crate::io::wia::DiscIOWIA::new(path)?
|
||||
}
|
||||
_ => crate::io::iso::DiscIOISO::new(path)?,
|
||||
Some(Format::Tgc) => crate::io::tgc::DiscIOTGC::new(stream)?,
|
||||
Some(Format::Wbfs) => crate::io::wbfs::DiscIOWBFS::new(stream)?,
|
||||
Some(Format::Wia | Format::Rvz) => crate::io::wia::DiscIOWIA::new(stream)?,
|
||||
None => return Err(Error::DiscFormat("Unknown disc format".to_string())),
|
||||
};
|
||||
check_block_size(io.as_ref())?;
|
||||
Ok(io)
|
||||
}
|
||||
|
||||
pub const CISO_MAGIC: MagicBytes = *b"CISO";
|
||||
pub const GCZ_MAGIC: MagicBytes = [0x01, 0xC0, 0x0B, 0xB1];
|
||||
pub const NFS_MAGIC: MagicBytes = *b"EGGS";
|
||||
pub const TGC_MAGIC: MagicBytes = [0xae, 0x0f, 0x38, 0xa2];
|
||||
pub const WBFS_MAGIC: MagicBytes = *b"WBFS";
|
||||
pub const WIA_MAGIC: MagicBytes = *b"WIA\x01";
|
||||
pub const RVZ_MAGIC: MagicBytes = *b"RVZ\x01";
|
||||
|
||||
pub fn detect<R: Read + ?Sized>(stream: &mut R) -> io::Result<Option<Format>> {
|
||||
let data: [u8; 0x20] = match read_from(stream) {
|
||||
Ok(magic) => magic,
|
||||
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(None),
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
let out = match *array_ref!(data, 0, 4) {
|
||||
CISO_MAGIC => Some(Format::Ciso),
|
||||
GCZ_MAGIC => Some(Format::Gcz),
|
||||
NFS_MAGIC => Some(Format::Nfs),
|
||||
TGC_MAGIC => Some(Format::Tgc),
|
||||
WBFS_MAGIC => Some(Format::Wbfs),
|
||||
WIA_MAGIC => Some(Format::Wia),
|
||||
RVZ_MAGIC => Some(Format::Rvz),
|
||||
_ if *array_ref!(data, 0x18, 4) == WII_MAGIC || *array_ref!(data, 0x1C, 4) == GCN_MAGIC => {
|
||||
Some(Format::Iso)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn check_block_size(io: &dyn BlockIO) -> Result<()> {
|
||||
if io.block_size_internal() < SECTOR_SIZE as u32
|
||||
&& SECTOR_SIZE as u32 % io.block_size_internal() != 0
|
||||
{
|
||||
|
@ -132,7 +206,7 @@ pub fn open(filename: &Path) -> Result<Box<dyn BlockIO>> {
|
|||
SECTOR_SIZE
|
||||
)));
|
||||
}
|
||||
Ok(io)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Wii partition information.
|
||||
|
@ -300,23 +374,19 @@ fn generate_junk(
|
|||
partition: Option<&PartitionInfo>,
|
||||
disc_header: &DiscHeader,
|
||||
) {
|
||||
let (mut pos, mut offset) = if partition.is_some() {
|
||||
let (pos, offset) = if partition.is_some() {
|
||||
(sector as u64 * SECTOR_DATA_SIZE as u64, HASHES_SIZE)
|
||||
} else {
|
||||
(sector as u64 * SECTOR_SIZE as u64, 0)
|
||||
};
|
||||
out[..offset].fill(0);
|
||||
while offset < SECTOR_SIZE {
|
||||
// The LFG spans a single sector of the decrypted data,
|
||||
// so we may need to initialize it multiple times
|
||||
let mut lfg = LaggedFibonacci::default();
|
||||
lfg.init_with_seed(*array_ref![disc_header.game_id, 0, 4], disc_header.disc_num, pos);
|
||||
let sector_end = (pos + SECTOR_SIZE as u64) & !(SECTOR_SIZE as u64 - 1);
|
||||
let len = min(SECTOR_SIZE - offset, (sector_end - pos) as usize);
|
||||
lfg.fill(&mut out[offset..offset + len]);
|
||||
pos += len as u64;
|
||||
offset += len;
|
||||
}
|
||||
let mut lfg = LaggedFibonacci::default();
|
||||
lfg.fill_sector_chunked(
|
||||
&mut out[offset..],
|
||||
*array_ref![disc_header.game_id, 0, 4],
|
||||
disc_header.disc_num,
|
||||
pos,
|
||||
);
|
||||
}
|
||||
|
||||
fn rebuild_hash_block(out: &mut [u8; SECTOR_SIZE], part_sector: u32, partition: &PartitionInfo) {
|
||||
|
|
|
@ -2,17 +2,15 @@ use std::{
|
|||
io,
|
||||
io::{Read, Seek, SeekFrom},
|
||||
mem::size_of,
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use zerocopy::{little_endian::*, AsBytes, FromBytes, FromZeroes};
|
||||
use zerocopy::{little_endian::*, FromBytes, Immutable, IntoBytes, KnownLayout};
|
||||
|
||||
use crate::{
|
||||
disc::SECTOR_SIZE,
|
||||
io::{
|
||||
block::{Block, BlockIO, PartitionInfo},
|
||||
block::{Block, BlockIO, DiscStream, PartitionInfo, CISO_MAGIC},
|
||||
nkit::NKitHeader,
|
||||
split::SplitFileReader,
|
||||
Format, MagicBytes,
|
||||
},
|
||||
static_assert,
|
||||
|
@ -20,11 +18,10 @@ use crate::{
|
|||
DiscMeta, Error, Result, ResultContext,
|
||||
};
|
||||
|
||||
pub const CISO_MAGIC: MagicBytes = *b"CISO";
|
||||
pub const CISO_MAP_SIZE: usize = SECTOR_SIZE - 8;
|
||||
|
||||
/// CISO header (little endian)
|
||||
#[derive(Clone, Debug, PartialEq, FromBytes, FromZeroes, AsBytes)]
|
||||
#[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
|
||||
#[repr(C, align(4))]
|
||||
struct CISOHeader {
|
||||
magic: MagicBytes,
|
||||
|
@ -36,18 +33,17 @@ static_assert!(size_of::<CISOHeader>() == SECTOR_SIZE);
|
|||
|
||||
#[derive(Clone)]
|
||||
pub struct DiscIOCISO {
|
||||
inner: SplitFileReader,
|
||||
inner: Box<dyn DiscStream>,
|
||||
header: CISOHeader,
|
||||
block_map: [u16; CISO_MAP_SIZE],
|
||||
nkit_header: Option<NKitHeader>,
|
||||
}
|
||||
|
||||
impl DiscIOCISO {
|
||||
pub fn new(filename: &Path) -> Result<Box<Self>> {
|
||||
let mut inner = SplitFileReader::new(filename)?;
|
||||
|
||||
pub fn new(mut inner: Box<dyn DiscStream>) -> Result<Box<Self>> {
|
||||
// Read header
|
||||
let header: CISOHeader = read_from(&mut inner).context("Reading CISO header")?;
|
||||
inner.seek(SeekFrom::Start(0)).context("Seeking to start")?;
|
||||
let header: CISOHeader = read_from(inner.as_mut()).context("Reading CISO header")?;
|
||||
if header.magic != CISO_MAGIC {
|
||||
return Err(Error::DiscFormat("Invalid CISO magic".to_string()));
|
||||
}
|
||||
|
@ -64,18 +60,18 @@ impl DiscIOCISO {
|
|||
}
|
||||
}
|
||||
let file_size = SECTOR_SIZE as u64 + block as u64 * header.block_size.get() as u64;
|
||||
if file_size > inner.len() {
|
||||
let len = inner.seek(SeekFrom::End(0)).context("Determining stream length")?;
|
||||
if file_size > len {
|
||||
return Err(Error::DiscFormat(format!(
|
||||
"CISO file size mismatch: expected at least {} bytes, got {}",
|
||||
file_size,
|
||||
inner.len()
|
||||
file_size, len
|
||||
)));
|
||||
}
|
||||
|
||||
// Read NKit header if present (after CISO data)
|
||||
let nkit_header = if inner.len() > file_size + 4 {
|
||||
let nkit_header = if len > file_size + 4 {
|
||||
inner.seek(SeekFrom::Start(file_size)).context("Seeking to NKit header")?;
|
||||
NKitHeader::try_read_from(&mut inner, header.block_size.get(), true)
|
||||
NKitHeader::try_read_from(inner.as_mut(), header.block_size.get(), true)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
|
|
@ -2,18 +2,16 @@ use std::{
|
|||
io,
|
||||
io::{Read, Seek, SeekFrom},
|
||||
mem::size_of,
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use adler::adler32_slice;
|
||||
use miniz_oxide::{inflate, inflate::core::inflate_flags};
|
||||
use zerocopy::{little_endian::*, AsBytes, FromBytes, FromZeroes};
|
||||
use zerocopy::{little_endian::*, FromBytes, FromZeros, Immutable, IntoBytes, KnownLayout};
|
||||
use zstd::zstd_safe::WriteBuf;
|
||||
|
||||
use crate::{
|
||||
io::{
|
||||
block::{Block, BlockIO},
|
||||
split::SplitFileReader,
|
||||
block::{Block, BlockIO, DiscStream, GCZ_MAGIC},
|
||||
MagicBytes,
|
||||
},
|
||||
static_assert,
|
||||
|
@ -21,10 +19,8 @@ use crate::{
|
|||
Compression, DiscMeta, Error, Format, PartitionInfo, Result, ResultContext,
|
||||
};
|
||||
|
||||
pub const GCZ_MAGIC: MagicBytes = [0x01, 0xC0, 0x0B, 0xB1];
|
||||
|
||||
/// GCZ header (little endian)
|
||||
#[derive(Clone, Debug, PartialEq, FromBytes, FromZeroes, AsBytes)]
|
||||
#[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
|
||||
#[repr(C, align(4))]
|
||||
struct GCZHeader {
|
||||
magic: MagicBytes,
|
||||
|
@ -38,7 +34,7 @@ struct GCZHeader {
|
|||
static_assert!(size_of::<GCZHeader>() == 32);
|
||||
|
||||
pub struct DiscIOGCZ {
|
||||
inner: SplitFileReader,
|
||||
inner: Box<dyn DiscStream>,
|
||||
header: GCZHeader,
|
||||
block_map: Box<[U64]>,
|
||||
block_hashes: Box<[U32]>,
|
||||
|
@ -53,32 +49,31 @@ impl Clone for DiscIOGCZ {
|
|||
header: self.header.clone(),
|
||||
block_map: self.block_map.clone(),
|
||||
block_hashes: self.block_hashes.clone(),
|
||||
block_buf: <u8>::new_box_slice_zeroed(self.block_buf.len()),
|
||||
block_buf: <[u8]>::new_box_zeroed_with_elems(self.block_buf.len()).unwrap(),
|
||||
data_offset: self.data_offset,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DiscIOGCZ {
|
||||
pub fn new(filename: &Path) -> Result<Box<Self>> {
|
||||
let mut inner = SplitFileReader::new(filename)?;
|
||||
|
||||
pub fn new(mut inner: Box<dyn DiscStream>) -> Result<Box<Self>> {
|
||||
// Read header
|
||||
let header: GCZHeader = read_from(&mut inner).context("Reading GCZ header")?;
|
||||
inner.seek(SeekFrom::Start(0)).context("Seeking to start")?;
|
||||
let header: GCZHeader = read_from(inner.as_mut()).context("Reading GCZ header")?;
|
||||
if header.magic != GCZ_MAGIC {
|
||||
return Err(Error::DiscFormat("Invalid GCZ magic".to_string()));
|
||||
}
|
||||
|
||||
// Read block map and hashes
|
||||
let block_count = header.block_count.get();
|
||||
let block_map =
|
||||
read_box_slice(&mut inner, block_count as usize).context("Reading GCZ block map")?;
|
||||
let block_hashes =
|
||||
read_box_slice(&mut inner, block_count as usize).context("Reading GCZ block hashes")?;
|
||||
let block_map = read_box_slice(inner.as_mut(), block_count as usize)
|
||||
.context("Reading GCZ block map")?;
|
||||
let block_hashes = read_box_slice(inner.as_mut(), block_count as usize)
|
||||
.context("Reading GCZ block hashes")?;
|
||||
|
||||
// header + block_count * (u64 + u32)
|
||||
let data_offset = size_of::<GCZHeader>() as u64 + block_count as u64 * 12;
|
||||
let block_buf = <u8>::new_box_slice_zeroed(header.block_size.get() as usize);
|
||||
let block_buf = <[u8]>::new_box_zeroed_with_elems(header.block_size.get() as usize)?;
|
||||
Ok(Box::new(Self { inner, header, block_map, block_hashes, block_buf, data_offset }))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,28 +1,28 @@
|
|||
use std::{
|
||||
io,
|
||||
io::{Read, Seek, SeekFrom},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
disc::SECTOR_SIZE,
|
||||
io::{
|
||||
block::{Block, BlockIO, PartitionInfo},
|
||||
split::SplitFileReader,
|
||||
block::{Block, BlockIO, DiscStream, PartitionInfo},
|
||||
Format,
|
||||
},
|
||||
DiscMeta, Result,
|
||||
DiscMeta, Result, ResultContext,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DiscIOISO {
|
||||
inner: SplitFileReader,
|
||||
inner: Box<dyn DiscStream>,
|
||||
stream_len: u64,
|
||||
}
|
||||
|
||||
impl DiscIOISO {
|
||||
pub fn new(filename: &Path) -> Result<Box<Self>> {
|
||||
let inner = SplitFileReader::new(filename)?;
|
||||
Ok(Box::new(Self { inner }))
|
||||
pub fn new(mut inner: Box<dyn DiscStream>) -> Result<Box<Self>> {
|
||||
let stream_len = inner.seek(SeekFrom::End(0)).context("Determining stream length")?;
|
||||
inner.seek(SeekFrom::Start(0)).context("Seeking to start")?;
|
||||
Ok(Box::new(Self { inner, stream_len }))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -34,16 +34,15 @@ impl BlockIO for DiscIOISO {
|
|||
_partition: Option<&PartitionInfo>,
|
||||
) -> io::Result<Block> {
|
||||
let offset = block as u64 * SECTOR_SIZE as u64;
|
||||
let total_size = self.inner.len();
|
||||
if offset >= total_size {
|
||||
if offset >= self.stream_len {
|
||||
// End of file
|
||||
return Ok(Block::Zero);
|
||||
}
|
||||
|
||||
self.inner.seek(SeekFrom::Start(offset))?;
|
||||
if offset + SECTOR_SIZE as u64 > total_size {
|
||||
if offset + SECTOR_SIZE as u64 > self.stream_len {
|
||||
// If the last block is not a full sector, fill the rest with zeroes
|
||||
let read = (total_size - offset) as usize;
|
||||
let read = (self.stream_len - offset) as usize;
|
||||
self.inner.read_exact(&mut out[..read])?;
|
||||
out[read..].fill(0);
|
||||
} else {
|
||||
|
@ -58,7 +57,7 @@ impl BlockIO for DiscIOISO {
|
|||
DiscMeta {
|
||||
format: Format::Iso,
|
||||
lossless: true,
|
||||
disc_size: Some(self.inner.len()),
|
||||
disc_size: Some(self.stream_len),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,17 +10,18 @@ pub(crate) mod iso;
|
|||
pub(crate) mod nfs;
|
||||
pub(crate) mod nkit;
|
||||
pub(crate) mod split;
|
||||
pub(crate) mod tgc;
|
||||
pub(crate) mod wbfs;
|
||||
pub(crate) mod wia;
|
||||
|
||||
/// SHA-1 hash bytes
|
||||
pub(crate) type HashBytes = [u8; 20];
|
||||
pub type HashBytes = [u8; 20];
|
||||
|
||||
/// AES key bytes
|
||||
pub(crate) type KeyBytes = [u8; 16];
|
||||
pub type KeyBytes = [u8; 16];
|
||||
|
||||
/// Magic bytes
|
||||
pub(crate) type MagicBytes = [u8; 4];
|
||||
pub type MagicBytes = [u8; 4];
|
||||
|
||||
/// The disc file format.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
|
@ -40,9 +41,12 @@ pub enum Format {
|
|||
Wbfs,
|
||||
/// WIA
|
||||
Wia,
|
||||
/// TGC
|
||||
Tgc,
|
||||
}
|
||||
|
||||
impl fmt::Display for Format {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Format::Iso => write!(f, "ISO"),
|
||||
|
@ -52,6 +56,7 @@ impl fmt::Display for Format {
|
|||
Format::Rvz => write!(f, "RVZ"),
|
||||
Format::Wbfs => write!(f, "WBFS"),
|
||||
Format::Wia => write!(f, "WIA"),
|
||||
Format::Tgc => write!(f, "TGC"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -77,6 +82,7 @@ pub enum Compression {
|
|||
}
|
||||
|
||||
impl fmt::Display for Compression {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Compression::None => write!(f, "None"),
|
||||
|
|
|
@ -6,13 +6,13 @@ use std::{
|
|||
path::{Component, Path, PathBuf},
|
||||
};
|
||||
|
||||
use zerocopy::{big_endian::U32, AsBytes, FromBytes, FromZeroes};
|
||||
use zerocopy::{big_endian::U32, FromBytes, FromZeros, Immutable, IntoBytes, KnownLayout};
|
||||
|
||||
use crate::{
|
||||
disc::SECTOR_SIZE,
|
||||
io::{
|
||||
aes_decrypt,
|
||||
block::{Block, BlockIO, PartitionInfo},
|
||||
block::{Block, BlockIO, PartitionInfo, NFS_MAGIC},
|
||||
split::SplitFileReader,
|
||||
Format, KeyBytes, MagicBytes,
|
||||
},
|
||||
|
@ -21,17 +21,16 @@ use crate::{
|
|||
DiscMeta, Error, Result, ResultContext,
|
||||
};
|
||||
|
||||
pub const NFS_MAGIC: MagicBytes = *b"EGGS";
|
||||
pub const NFS_END_MAGIC: MagicBytes = *b"SGGE";
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, FromBytes, FromZeroes, AsBytes)]
|
||||
#[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
|
||||
#[repr(C, align(4))]
|
||||
struct LBARange {
|
||||
start_sector: U32,
|
||||
num_sectors: U32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, FromBytes, FromZeroes, AsBytes)]
|
||||
#[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
|
||||
#[repr(C, align(4))]
|
||||
struct NFSHeader {
|
||||
magic: MagicBytes,
|
||||
|
@ -192,7 +191,7 @@ impl DiscIONFS {
|
|||
let resolved_path = key_path.unwrap();
|
||||
File::open(resolved_path.as_path())
|
||||
.map_err(|v| Error::Io(format!("Failed to open {}", resolved_path.display()), v))?
|
||||
.read(&mut self.key)
|
||||
.read_exact(&mut self.key)
|
||||
.map_err(|v| Error::Io(format!("Failed to read {}", resolved_path.display()), v))?;
|
||||
}
|
||||
|
||||
|
|
|
@ -136,8 +136,6 @@ impl Seek for SplitFileReader {
|
|||
if split.contains(self.pos) {
|
||||
// Seek within the open file
|
||||
split.inner.seek(SeekFrom::Start(self.pos - split.begin))?;
|
||||
} else {
|
||||
self.open_file = None;
|
||||
}
|
||||
}
|
||||
Ok(self.pos)
|
||||
|
|
|
@ -0,0 +1,153 @@
|
|||
use std::{
|
||||
io,
|
||||
io::{Read, Seek, SeekFrom},
|
||||
mem::size_of,
|
||||
};
|
||||
|
||||
use zerocopy::{big_endian::U32, FromBytes, Immutable, IntoBytes, KnownLayout};
|
||||
|
||||
use crate::{
|
||||
disc::SECTOR_SIZE,
|
||||
io::{
|
||||
block::{Block, BlockIO, DiscStream, PartitionInfo, TGC_MAGIC},
|
||||
Format, MagicBytes,
|
||||
},
|
||||
util::read::{read_box_slice, read_from},
|
||||
DiscHeader, DiscMeta, Error, Node, PartitionHeader, Result, ResultContext,
|
||||
};
|
||||
|
||||
/// TGC header (big endian)
|
||||
#[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
|
||||
#[repr(C, align(4))]
|
||||
struct TGCHeader {
|
||||
/// Magic bytes
|
||||
magic: MagicBytes,
|
||||
/// TGC version
|
||||
version: U32,
|
||||
/// Offset to the start of the GCM header
|
||||
header_offset: U32,
|
||||
/// Size of the GCM header
|
||||
header_size: U32,
|
||||
/// Offset to the FST
|
||||
fst_offset: U32,
|
||||
/// Size of the FST
|
||||
fst_size: U32,
|
||||
/// Maximum size of the FST across discs
|
||||
fst_max_size: U32,
|
||||
/// Offset to the DOL
|
||||
dol_offset: U32,
|
||||
/// Size of the DOL
|
||||
dol_size: U32,
|
||||
/// Offset to user data
|
||||
user_offset: U32,
|
||||
/// Size of user data
|
||||
user_size: U32,
|
||||
/// Offset to the banner
|
||||
banner_offset: U32,
|
||||
/// Size of the banner
|
||||
banner_size: U32,
|
||||
/// Original user data offset in the GCM
|
||||
gcm_user_offset: U32,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DiscIOTGC {
|
||||
inner: Box<dyn DiscStream>,
|
||||
stream_len: u64,
|
||||
header: TGCHeader,
|
||||
fst: Box<[u8]>,
|
||||
}
|
||||
|
||||
impl DiscIOTGC {
|
||||
pub fn new(mut inner: Box<dyn DiscStream>) -> Result<Box<Self>> {
|
||||
let stream_len = inner.seek(SeekFrom::End(0)).context("Determining stream length")?;
|
||||
inner.seek(SeekFrom::Start(0)).context("Seeking to start")?;
|
||||
|
||||
// Read header
|
||||
let header: TGCHeader = read_from(inner.as_mut()).context("Reading TGC header")?;
|
||||
if header.magic != TGC_MAGIC {
|
||||
return Err(Error::DiscFormat("Invalid TGC magic".to_string()));
|
||||
}
|
||||
|
||||
// Read FST and adjust offsets
|
||||
inner
|
||||
.seek(SeekFrom::Start(header.fst_offset.get() as u64))
|
||||
.context("Seeking to TGC FST")?;
|
||||
let mut fst = read_box_slice(inner.as_mut(), header.fst_size.get() as usize)
|
||||
.context("Reading TGC FST")?;
|
||||
let (root_node, _) = Node::ref_from_prefix(&fst)
|
||||
.map_err(|_| Error::DiscFormat("Invalid TGC FST".to_string()))?;
|
||||
let node_count = root_node.length() as usize;
|
||||
let (nodes, _) = <[Node]>::mut_from_prefix_with_elems(&mut fst, node_count)
|
||||
.map_err(|_| Error::DiscFormat("Invalid TGC FST".to_string()))?;
|
||||
for node in nodes {
|
||||
if node.is_file() {
|
||||
node.offset = node.offset - header.gcm_user_offset
|
||||
+ (header.user_offset - header.header_offset);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Box::new(Self { inner, stream_len, header, fst }))
|
||||
}
|
||||
}
|
||||
|
||||
impl BlockIO for DiscIOTGC {
|
||||
fn read_block_internal(
|
||||
&mut self,
|
||||
out: &mut [u8],
|
||||
block: u32,
|
||||
_partition: Option<&PartitionInfo>,
|
||||
) -> io::Result<Block> {
|
||||
let offset = self.header.header_offset.get() as u64 + block as u64 * SECTOR_SIZE as u64;
|
||||
if offset >= self.stream_len {
|
||||
// End of file
|
||||
return Ok(Block::Zero);
|
||||
}
|
||||
|
||||
self.inner.seek(SeekFrom::Start(offset))?;
|
||||
if offset + SECTOR_SIZE as u64 > self.stream_len {
|
||||
// If the last block is not a full sector, fill the rest with zeroes
|
||||
let read = (self.stream_len - offset) as usize;
|
||||
self.inner.read_exact(&mut out[..read])?;
|
||||
out[read..].fill(0);
|
||||
} else {
|
||||
self.inner.read_exact(out)?;
|
||||
}
|
||||
|
||||
// Adjust internal GCM header
|
||||
if block == 0 {
|
||||
let partition_header = PartitionHeader::mut_from_bytes(
|
||||
&mut out[size_of::<DiscHeader>()
|
||||
..size_of::<DiscHeader>() + size_of::<PartitionHeader>()],
|
||||
)
|
||||
.unwrap();
|
||||
partition_header.dol_offset = self.header.dol_offset - self.header.header_offset;
|
||||
partition_header.fst_offset = self.header.fst_offset - self.header.header_offset;
|
||||
}
|
||||
|
||||
// Copy modified FST to output
|
||||
if offset + out.len() as u64 > self.header.fst_offset.get() as u64
|
||||
&& offset < self.header.fst_offset.get() as u64 + self.header.fst_size.get() as u64
|
||||
{
|
||||
let out_offset = (self.header.fst_offset.get() as u64).saturating_sub(offset) as usize;
|
||||
let fst_offset = offset.saturating_sub(self.header.fst_offset.get() as u64) as usize;
|
||||
let copy_len =
|
||||
(out.len() - out_offset).min(self.header.fst_size.get() as usize - fst_offset);
|
||||
out[out_offset..out_offset + copy_len]
|
||||
.copy_from_slice(&self.fst[fst_offset..fst_offset + copy_len]);
|
||||
}
|
||||
|
||||
Ok(Block::Raw)
|
||||
}
|
||||
|
||||
fn block_size_internal(&self) -> u32 { SECTOR_SIZE as u32 }
|
||||
|
||||
fn meta(&self) -> DiscMeta {
|
||||
DiscMeta {
|
||||
format: Format::Tgc,
|
||||
lossless: true,
|
||||
disc_size: Some(self.stream_len - self.header.header_offset.get() as u64),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,25 +2,21 @@ use std::{
|
|||
io,
|
||||
io::{Read, Seek, SeekFrom},
|
||||
mem::size_of,
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use zerocopy::{big_endian::*, AsBytes, FromBytes, FromZeroes};
|
||||
use zerocopy::{big_endian::*, FromBytes, Immutable, IntoBytes, KnownLayout};
|
||||
|
||||
use crate::{
|
||||
io::{
|
||||
block::{Block, BlockIO, PartitionInfo},
|
||||
block::{Block, BlockIO, DiscStream, PartitionInfo, WBFS_MAGIC},
|
||||
nkit::NKitHeader,
|
||||
split::SplitFileReader,
|
||||
DiscMeta, Format, MagicBytes,
|
||||
},
|
||||
util::read::{read_box_slice, read_from},
|
||||
Error, Result, ResultContext,
|
||||
};
|
||||
|
||||
pub const WBFS_MAGIC: MagicBytes = *b"WBFS";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, FromBytes, FromZeroes, AsBytes)]
|
||||
#[derive(Debug, Clone, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
|
||||
#[repr(C, align(4))]
|
||||
struct WBFSHeader {
|
||||
magic: MagicBytes,
|
||||
|
@ -35,18 +31,6 @@ impl WBFSHeader {
|
|||
|
||||
fn block_size(&self) -> u32 { 1 << self.block_size_shift }
|
||||
|
||||
// fn align_lba(&self, x: u32) -> u32 { (x + self.sector_size() - 1) & !(self.sector_size() - 1) }
|
||||
//
|
||||
// fn num_wii_sectors(&self) -> u32 {
|
||||
// (self.num_sectors.get() / SECTOR_SIZE as u32) * self.sector_size()
|
||||
// }
|
||||
//
|
||||
// fn max_wii_sectors(&self) -> u32 { NUM_WII_SECTORS }
|
||||
//
|
||||
// fn num_wbfs_sectors(&self) -> u32 {
|
||||
// self.num_wii_sectors() >> (self.wbfs_sector_size_shift - 15)
|
||||
// }
|
||||
|
||||
fn max_blocks(&self) -> u32 { NUM_WII_SECTORS >> (self.block_size_shift - 15) }
|
||||
}
|
||||
|
||||
|
@ -55,7 +39,7 @@ const NUM_WII_SECTORS: u32 = 143432 * 2; // Double layer discs
|
|||
|
||||
#[derive(Clone)]
|
||||
pub struct DiscIOWBFS {
|
||||
inner: SplitFileReader,
|
||||
inner: Box<dyn DiscStream>,
|
||||
/// WBFS header
|
||||
header: WBFSHeader,
|
||||
/// Map of Wii LBAs to WBFS LBAs
|
||||
|
@ -65,14 +49,13 @@ pub struct DiscIOWBFS {
|
|||
}
|
||||
|
||||
impl DiscIOWBFS {
|
||||
pub fn new(filename: &Path) -> Result<Box<Self>> {
|
||||
let mut inner = SplitFileReader::new(filename)?;
|
||||
|
||||
let header: WBFSHeader = read_from(&mut inner).context("Reading WBFS header")?;
|
||||
pub fn new(mut inner: Box<dyn DiscStream>) -> Result<Box<Self>> {
|
||||
inner.seek(SeekFrom::Start(0)).context("Seeking to start")?;
|
||||
let header: WBFSHeader = read_from(inner.as_mut()).context("Reading WBFS header")?;
|
||||
if header.magic != WBFS_MAGIC {
|
||||
return Err(Error::DiscFormat("Invalid WBFS magic".to_string()));
|
||||
}
|
||||
let file_len = inner.len();
|
||||
let file_len = inner.seek(SeekFrom::End(0)).context("Determining stream length")?;
|
||||
let expected_file_len = header.num_sectors.get() as u64 * header.sector_size() as u64;
|
||||
if file_len != expected_file_len {
|
||||
return Err(Error::DiscFormat(format!(
|
||||
|
@ -81,8 +64,11 @@ impl DiscIOWBFS {
|
|||
)));
|
||||
}
|
||||
|
||||
inner
|
||||
.seek(SeekFrom::Start(size_of::<WBFSHeader>() as u64))
|
||||
.context("Seeking to WBFS disc table")?;
|
||||
let disc_table: Box<[u8]> =
|
||||
read_box_slice(&mut inner, header.sector_size() as usize - size_of::<WBFSHeader>())
|
||||
read_box_slice(inner.as_mut(), header.sector_size() as usize - size_of::<WBFSHeader>())
|
||||
.context("Reading WBFS disc table")?;
|
||||
if disc_table[0] != 1 {
|
||||
return Err(Error::DiscFormat("WBFS doesn't contain a disc".to_string()));
|
||||
|
@ -95,12 +81,12 @@ impl DiscIOWBFS {
|
|||
inner
|
||||
.seek(SeekFrom::Start(header.sector_size() as u64 + DISC_HEADER_SIZE as u64))
|
||||
.context("Seeking to WBFS LBA table")?; // Skip header
|
||||
let block_map: Box<[U16]> = read_box_slice(&mut inner, header.max_blocks() as usize)
|
||||
let block_map: Box<[U16]> = read_box_slice(inner.as_mut(), header.max_blocks() as usize)
|
||||
.context("Reading WBFS LBA table")?;
|
||||
|
||||
// Read NKit header if present (always at 0x10000)
|
||||
inner.seek(SeekFrom::Start(0x10000)).context("Seeking to NKit header")?;
|
||||
let nkit_header = NKitHeader::try_read_from(&mut inner, header.block_size(), true);
|
||||
let nkit_header = NKitHeader::try_read_from(inner.as_mut(), header.block_size(), true);
|
||||
|
||||
Ok(Box::new(Self { inner, header, block_map, nkit_header }))
|
||||
}
|
||||
|
|
|
@ -2,10 +2,9 @@ use std::{
|
|||
io,
|
||||
io::{Read, Seek, SeekFrom},
|
||||
mem::size_of,
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use zerocopy::{big_endian::*, AsBytes, FromBytes, FromZeroes};
|
||||
use zerocopy::{big_endian::*, FromBytes, Immutable, IntoBytes, KnownLayout};
|
||||
|
||||
use crate::{
|
||||
disc::{
|
||||
|
@ -14,14 +13,12 @@ use crate::{
|
|||
SECTOR_SIZE,
|
||||
},
|
||||
io::{
|
||||
block::{Block, BlockIO, PartitionInfo},
|
||||
block::{Block, BlockIO, DiscStream, PartitionInfo, RVZ_MAGIC, WIA_MAGIC},
|
||||
nkit::NKitHeader,
|
||||
split::SplitFileReader,
|
||||
Compression, Format, HashBytes, KeyBytes, MagicBytes,
|
||||
},
|
||||
static_assert,
|
||||
util::{
|
||||
compress::{lzma2_props_decode, lzma_props_decode, new_lzma2_decoder, new_lzma_decoder},
|
||||
lfg::LaggedFibonacci,
|
||||
read::{read_box_slice, read_from, read_u16_be, read_vec},
|
||||
take_seek::TakeSeekExt,
|
||||
|
@ -29,12 +26,9 @@ use crate::{
|
|||
DiscMeta, Error, Result, ResultContext,
|
||||
};
|
||||
|
||||
pub const WIA_MAGIC: MagicBytes = *b"WIA\x01";
|
||||
pub const RVZ_MAGIC: MagicBytes = *b"RVZ\x01";
|
||||
|
||||
/// This struct is stored at offset 0x0 and is 0x48 bytes long. The wit source code says its format
|
||||
/// will never be changed.
|
||||
#[derive(Clone, Debug, PartialEq, FromBytes, FromZeroes, AsBytes)]
|
||||
#[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
|
||||
#[repr(C, align(4))]
|
||||
pub struct WIAFileHeader {
|
||||
pub magic: MagicBytes,
|
||||
|
@ -148,7 +142,7 @@ impl TryFrom<u32> for WIACompression {
|
|||
const DISC_HEAD_SIZE: usize = 0x80;
|
||||
|
||||
/// This struct is stored at offset 0x48, immediately after [WIAFileHeader].
|
||||
#[derive(Clone, Debug, PartialEq, FromBytes, FromZeroes, AsBytes)]
|
||||
#[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
|
||||
#[repr(C, align(4))]
|
||||
pub struct WIADisc {
|
||||
/// The disc type. (1 = GameCube, 2 = Wii)
|
||||
|
@ -242,7 +236,7 @@ impl WIADisc {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, FromBytes, FromZeroes, AsBytes)]
|
||||
#[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
|
||||
#[repr(C, align(4))]
|
||||
pub struct WIAPartitionData {
|
||||
/// The sector on the disc at which this data starts.
|
||||
|
@ -277,7 +271,7 @@ impl WIAPartitionData {
|
|||
/// the reading program must first recalculate the hashes as done when creating a Wii disc image
|
||||
/// from scratch (see <https://wiibrew.org/wiki/Wii_Disc>), and must then apply the hash exceptions
|
||||
/// which are stored along with the data (see the [WIAExceptionList] section).
|
||||
#[derive(Clone, Debug, PartialEq, FromBytes, FromZeroes, AsBytes)]
|
||||
#[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
|
||||
#[repr(C, align(4))]
|
||||
pub struct WIAPartition {
|
||||
/// The title key for this partition (128-bit AES), which can be used for re-encrypting the
|
||||
|
@ -304,7 +298,7 @@ static_assert!(size_of::<WIAPartition>() == 0x30);
|
|||
/// should be read from [WIADisc] instead.) This should be handled by rounding the offset down to
|
||||
/// the previous multiple of 0x8000 (and adding the equivalent amount to the size so that the end
|
||||
/// offset stays the same), not by special casing the first [WIARawData].
|
||||
#[derive(Clone, Debug, PartialEq, FromBytes, FromZeroes, AsBytes)]
|
||||
#[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
|
||||
#[repr(C, align(4))]
|
||||
pub struct WIARawData {
|
||||
/// The offset on the disc at which this data starts.
|
||||
|
@ -342,7 +336,7 @@ impl WIARawData {
|
|||
/// counting any [WIAExceptionList] structs. However, the last [WIAGroup] of a [WIAPartitionData]
|
||||
/// or [WIARawData] contains less data than that if `num_sectors * 0x8000` (for [WIAPartitionData])
|
||||
/// or `raw_data_size` (for [WIARawData]) is not evenly divisible by `chunk_size`.
|
||||
#[derive(Clone, Debug, PartialEq, FromBytes, FromZeroes, AsBytes)]
|
||||
#[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
|
||||
#[repr(C, align(4))]
|
||||
pub struct WIAGroup {
|
||||
/// The offset in the file where the compressed data is stored.
|
||||
|
@ -357,7 +351,7 @@ pub struct WIAGroup {
|
|||
|
||||
/// Compared to [WIAGroup], [RVZGroup] changes the meaning of the most significant bit of
|
||||
/// [data_size](Self::data_size) and adds one additional attribute.
|
||||
#[derive(Clone, Debug, PartialEq, FromBytes, FromZeroes, AsBytes)]
|
||||
#[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
|
||||
#[repr(C, align(4))]
|
||||
pub struct RVZGroup {
|
||||
/// The offset in the file where the compressed data is stored, divided by 4.
|
||||
|
@ -404,7 +398,7 @@ impl From<&WIAGroup> for RVZGroup {
|
|||
/// write [WIAException] structs for a padding area which is 32 bytes long, it writes one which
|
||||
/// covers the first 20 bytes of the padding area and one which covers the last 20 bytes of the
|
||||
/// padding area, generating 12 bytes of overlap between the [WIAException] structs.
|
||||
#[derive(Clone, Debug, PartialEq, FromBytes, FromZeroes, AsBytes)]
|
||||
#[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
|
||||
#[repr(C, align(2))]
|
||||
pub struct WIAException {
|
||||
/// The offset among the hashes. The offsets 0x0000-0x0400 here map to the offsets 0x0000-0x0400
|
||||
|
@ -464,15 +458,15 @@ pub enum Decompressor {
|
|||
|
||||
impl Decompressor {
|
||||
pub fn new(disc: &WIADisc) -> Result<Self> {
|
||||
let data = &disc.compr_data[..disc.compr_data_len as usize];
|
||||
let _data = &disc.compr_data[..disc.compr_data_len as usize];
|
||||
match disc.compression() {
|
||||
WIACompression::None => Ok(Self::None),
|
||||
#[cfg(feature = "compress-bzip2")]
|
||||
WIACompression::Bzip2 => Ok(Self::Bzip2),
|
||||
#[cfg(feature = "compress-lzma")]
|
||||
WIACompression::Lzma => Ok(Self::Lzma(Box::from(data))),
|
||||
WIACompression::Lzma => Ok(Self::Lzma(Box::from(_data))),
|
||||
#[cfg(feature = "compress-lzma")]
|
||||
WIACompression::Lzma2 => Ok(Self::Lzma2(Box::from(data))),
|
||||
WIACompression::Lzma2 => Ok(Self::Lzma2(Box::from(_data))),
|
||||
#[cfg(feature = "compress-zstd")]
|
||||
WIACompression::Zstandard => Ok(Self::Zstandard),
|
||||
comp => Err(Error::DiscFormat(format!("Unsupported WIA/RVZ compression: {:?}", comp))),
|
||||
|
@ -487,11 +481,13 @@ impl Decompressor {
|
|||
Decompressor::Bzip2 => Box::new(bzip2::read::BzDecoder::new(reader)),
|
||||
#[cfg(feature = "compress-lzma")]
|
||||
Decompressor::Lzma(data) => {
|
||||
use crate::util::compress::{lzma_props_decode, new_lzma_decoder};
|
||||
let options = lzma_props_decode(data)?;
|
||||
Box::new(new_lzma_decoder(reader, &options)?)
|
||||
}
|
||||
#[cfg(feature = "compress-lzma")]
|
||||
Decompressor::Lzma2(data) => {
|
||||
use crate::util::compress::{lzma2_props_decode, new_lzma2_decoder};
|
||||
let options = lzma2_props_decode(data)?;
|
||||
Box::new(new_lzma2_decoder(reader, &options)?)
|
||||
}
|
||||
|
@ -502,7 +498,7 @@ impl Decompressor {
|
|||
}
|
||||
|
||||
pub struct DiscIOWIA {
|
||||
inner: SplitFileReader,
|
||||
inner: Box<dyn DiscStream>,
|
||||
header: WIAFileHeader,
|
||||
disc: WIADisc,
|
||||
partitions: Box<[WIAPartition]>,
|
||||
|
@ -549,21 +545,21 @@ fn verify_hash(buf: &[u8], expected: &HashBytes) -> Result<()> {
|
|||
}
|
||||
|
||||
impl DiscIOWIA {
|
||||
pub fn new(filename: &Path) -> Result<Box<Self>> {
|
||||
let mut inner = SplitFileReader::new(filename)?;
|
||||
|
||||
pub fn new(mut inner: Box<dyn DiscStream>) -> Result<Box<Self>> {
|
||||
// Load & verify file header
|
||||
let header: WIAFileHeader = read_from(&mut inner).context("Reading WIA/RVZ file header")?;
|
||||
inner.seek(SeekFrom::Start(0)).context("Seeking to start")?;
|
||||
let header: WIAFileHeader =
|
||||
read_from(inner.as_mut()).context("Reading WIA/RVZ file header")?;
|
||||
header.validate()?;
|
||||
let is_rvz = header.is_rvz();
|
||||
// log::debug!("Header: {:?}", header);
|
||||
|
||||
// Load & verify disc header
|
||||
let mut disc_buf: Vec<u8> = read_vec(&mut inner, header.disc_size.get() as usize)
|
||||
let mut disc_buf: Vec<u8> = read_vec(inner.as_mut(), header.disc_size.get() as usize)
|
||||
.context("Reading WIA/RVZ disc header")?;
|
||||
verify_hash(&disc_buf, &header.disc_hash)?;
|
||||
disc_buf.resize(size_of::<WIADisc>(), 0);
|
||||
let disc = WIADisc::read_from(disc_buf.as_slice()).unwrap();
|
||||
let disc = WIADisc::read_from_bytes(disc_buf.as_slice()).unwrap();
|
||||
disc.validate()?;
|
||||
// if !options.rebuild_hashes {
|
||||
// // If we're not rebuilding hashes, disable partition hashes in disc header
|
||||
|
@ -576,14 +572,14 @@ impl DiscIOWIA {
|
|||
// log::debug!("Disc: {:?}", disc);
|
||||
|
||||
// Read NKit header if present (after disc header)
|
||||
let nkit_header = NKitHeader::try_read_from(&mut inner, disc.chunk_size.get(), false);
|
||||
let nkit_header = NKitHeader::try_read_from(inner.as_mut(), disc.chunk_size.get(), false);
|
||||
|
||||
// Load & verify partition headers
|
||||
inner
|
||||
.seek(SeekFrom::Start(disc.partition_offset.get()))
|
||||
.context("Seeking to WIA/RVZ partition headers")?;
|
||||
let partitions: Box<[WIAPartition]> =
|
||||
read_box_slice(&mut inner, disc.num_partitions.get() as usize)
|
||||
read_box_slice(inner.as_mut(), disc.num_partitions.get() as usize)
|
||||
.context("Reading WIA/RVZ partition headers")?;
|
||||
verify_hash(partitions.as_ref().as_bytes(), &disc.partition_hash)?;
|
||||
// log::debug!("Partitions: {:?}", partitions);
|
||||
|
@ -597,7 +593,7 @@ impl DiscIOWIA {
|
|||
.seek(SeekFrom::Start(disc.raw_data_offset.get()))
|
||||
.context("Seeking to WIA/RVZ raw data headers")?;
|
||||
let mut reader = decompressor
|
||||
.wrap((&mut inner).take(disc.raw_data_size.get() as u64))
|
||||
.wrap(inner.as_mut().take(disc.raw_data_size.get() as u64))
|
||||
.context("Creating WIA/RVZ decompressor")?;
|
||||
read_box_slice(&mut reader, disc.num_raw_data.get() as usize)
|
||||
.context("Reading WIA/RVZ raw data headers")?
|
||||
|
@ -621,7 +617,7 @@ impl DiscIOWIA {
|
|||
.seek(SeekFrom::Start(disc.group_offset.get()))
|
||||
.context("Seeking to WIA/RVZ group headers")?;
|
||||
let mut reader = decompressor
|
||||
.wrap((&mut inner).take(disc.group_size.get() as u64))
|
||||
.wrap(inner.as_mut().take(disc.group_size.get() as u64))
|
||||
.context("Creating WIA/RVZ decompressor")?;
|
||||
if is_rvz {
|
||||
read_box_slice(&mut reader, disc.num_groups.get() as usize)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#![warn(missing_docs)]
|
||||
#![warn(missing_docs, clippy::missing_inline_in_public_items)]
|
||||
//! Library for traversing & reading Nintendo Optical Disc (GameCube and Wii) images.
|
||||
//!
|
||||
//! Originally based on the C++ library [nod](https://github.com/AxioDL/nod),
|
||||
|
@ -59,22 +59,24 @@
|
|||
//! ```
|
||||
|
||||
use std::{
|
||||
io::{Read, Seek},
|
||||
io::{BufRead, Read, Seek},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
pub use disc::{
|
||||
ApploaderHeader, DiscHeader, DolHeader, PartitionBase, PartitionHeader, PartitionKind,
|
||||
PartitionMeta, BI2_SIZE, BOOT_SIZE, SECTOR_SIZE,
|
||||
ApploaderHeader, DiscHeader, DolHeader, FileStream, Fst, Node, NodeKind, OwnedFileStream,
|
||||
PartitionBase, PartitionHeader, PartitionKind, PartitionMeta, SignedHeader, Ticket,
|
||||
TicketLimit, TmdHeader, WindowedStream, BI2_SIZE, BOOT_SIZE, DL_DVD_SIZE, GCN_MAGIC,
|
||||
MINI_DVD_SIZE, REGION_SIZE, SECTOR_SIZE, SL_DVD_SIZE, WII_MAGIC,
|
||||
};
|
||||
pub use fst::{Fst, Node, NodeKind};
|
||||
pub use io::{block::PartitionInfo, Compression, DiscMeta, Format};
|
||||
pub use streams::ReadStream;
|
||||
pub use io::{
|
||||
block::{DiscStream, PartitionInfo},
|
||||
Compression, DiscMeta, Format, KeyBytes, MagicBytes,
|
||||
};
|
||||
pub use util::lfg::LaggedFibonacci;
|
||||
|
||||
mod disc;
|
||||
mod fst;
|
||||
mod io;
|
||||
mod streams;
|
||||
mod util;
|
||||
|
||||
/// Error types for nod.
|
||||
|
@ -89,16 +91,26 @@ pub enum Error {
|
|||
/// An unknown error.
|
||||
#[error("error: {0}")]
|
||||
Other(String),
|
||||
/// An error occurred while allocating memory.
|
||||
#[error("allocation failed")]
|
||||
Alloc(zerocopy::AllocError),
|
||||
}
|
||||
|
||||
impl From<&str> for Error {
|
||||
#[inline]
|
||||
fn from(s: &str) -> Error { Error::Other(s.to_string()) }
|
||||
}
|
||||
|
||||
impl From<String> for Error {
|
||||
#[inline]
|
||||
fn from(s: String) -> Error { Error::Other(s) }
|
||||
}
|
||||
|
||||
impl From<zerocopy::AllocError> for Error {
|
||||
#[inline]
|
||||
fn from(e: zerocopy::AllocError) -> Error { Error::Alloc(e) }
|
||||
}
|
||||
|
||||
/// Helper result type for [`Error`].
|
||||
pub type Result<T, E = Error> = core::result::Result<T, E>;
|
||||
|
||||
|
@ -109,6 +121,7 @@ pub trait ErrorContext {
|
|||
}
|
||||
|
||||
impl ErrorContext for std::io::Error {
|
||||
#[inline]
|
||||
fn context(self, context: impl Into<String>) -> Error { Error::Io(context.into(), self) }
|
||||
}
|
||||
|
||||
|
@ -125,10 +138,12 @@ pub trait ResultContext<T> {
|
|||
impl<T, E> ResultContext<T> for Result<T, E>
|
||||
where E: ErrorContext
|
||||
{
|
||||
#[inline]
|
||||
fn context(self, context: impl Into<String>) -> Result<T> {
|
||||
self.map_err(|e| e.context(context))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn with_context<F>(self, f: F) -> Result<T>
|
||||
where F: FnOnce() -> String {
|
||||
self.map_err(|e| e.context(f()))
|
||||
|
@ -155,34 +170,71 @@ pub struct Disc {
|
|||
|
||||
impl Disc {
|
||||
/// Opens a disc image from a file path.
|
||||
#[inline]
|
||||
pub fn new<P: AsRef<Path>>(path: P) -> Result<Disc> {
|
||||
Disc::new_with_options(path, &OpenOptions::default())
|
||||
}
|
||||
|
||||
/// Opens a disc image from a file path with custom options.
|
||||
#[inline]
|
||||
pub fn new_with_options<P: AsRef<Path>>(path: P, options: &OpenOptions) -> Result<Disc> {
|
||||
let io = io::block::open(path.as_ref())?;
|
||||
let reader = disc::reader::DiscReader::new(io, options)?;
|
||||
Ok(Disc { reader, options: options.clone() })
|
||||
}
|
||||
|
||||
/// Opens a disc image from a read stream.
|
||||
#[inline]
|
||||
pub fn new_stream(stream: Box<dyn DiscStream>) -> Result<Disc> {
|
||||
Disc::new_stream_with_options(stream, &OpenOptions::default())
|
||||
}
|
||||
|
||||
/// Opens a disc image from a read stream with custom options.
|
||||
#[inline]
|
||||
pub fn new_stream_with_options(
|
||||
stream: Box<dyn DiscStream>,
|
||||
options: &OpenOptions,
|
||||
) -> Result<Disc> {
|
||||
let io = io::block::new(stream)?;
|
||||
let reader = disc::reader::DiscReader::new(io, options)?;
|
||||
Ok(Disc { reader, options: options.clone() })
|
||||
}
|
||||
|
||||
/// Detects the format of a disc image from a read stream.
|
||||
#[inline]
|
||||
pub fn detect<R>(stream: &mut R) -> std::io::Result<Option<Format>>
|
||||
where R: Read + ?Sized {
|
||||
io::block::detect(stream)
|
||||
}
|
||||
|
||||
/// The disc's primary header.
|
||||
#[inline]
|
||||
pub fn header(&self) -> &DiscHeader { self.reader.header() }
|
||||
|
||||
/// The Wii disc's region information.
|
||||
///
|
||||
/// **GameCube**: This will return `None`.
|
||||
#[inline]
|
||||
pub fn region(&self) -> Option<&[u8; REGION_SIZE]> { self.reader.region() }
|
||||
|
||||
/// Returns extra metadata included in the disc file format, if any.
|
||||
#[inline]
|
||||
pub fn meta(&self) -> DiscMeta { self.reader.meta() }
|
||||
|
||||
/// The disc's size in bytes, or an estimate if not stored by the format.
|
||||
#[inline]
|
||||
pub fn disc_size(&self) -> u64 { self.reader.disc_size() }
|
||||
|
||||
/// A list of Wii partitions on the disc.
|
||||
///
|
||||
/// **GameCube**: This will return an empty slice.
|
||||
#[inline]
|
||||
pub fn partitions(&self) -> &[PartitionInfo] { self.reader.partitions() }
|
||||
|
||||
/// Opens a decrypted partition read stream for the specified partition index.
|
||||
///
|
||||
/// **GameCube**: `index` must always be 0.
|
||||
#[inline]
|
||||
pub fn open_partition(&self, index: usize) -> Result<Box<dyn PartitionBase>> {
|
||||
self.reader.open_partition(index, &self.options)
|
||||
}
|
||||
|
@ -191,15 +243,26 @@ impl Disc {
|
|||
/// the specified kind.
|
||||
///
|
||||
/// **GameCube**: `kind` must always be [`PartitionKind::Data`].
|
||||
#[inline]
|
||||
pub fn open_partition_kind(&self, kind: PartitionKind) -> Result<Box<dyn PartitionBase>> {
|
||||
self.reader.open_partition_kind(kind, &self.options)
|
||||
}
|
||||
}
|
||||
|
||||
impl BufRead for Disc {
|
||||
#[inline]
|
||||
fn fill_buf(&mut self) -> std::io::Result<&[u8]> { self.reader.fill_buf() }
|
||||
|
||||
#[inline]
|
||||
fn consume(&mut self, amt: usize) { self.reader.consume(amt) }
|
||||
}
|
||||
|
||||
impl Read for Disc {
|
||||
#[inline]
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> { self.reader.read(buf) }
|
||||
}
|
||||
|
||||
impl Seek for Disc {
|
||||
#[inline]
|
||||
fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> { self.reader.seek(pos) }
|
||||
}
|
||||
|
|
|
@ -1,80 +0,0 @@
|
|||
//! Common stream types
|
||||
|
||||
use std::{
|
||||
io,
|
||||
io::{Read, Seek, SeekFrom},
|
||||
};
|
||||
|
||||
/// A helper trait for seekable read streams.
|
||||
pub trait ReadStream: Read + Seek {
|
||||
/// Creates a windowed read sub-stream with offset and size.
|
||||
///
|
||||
/// Seeks underlying stream immediately.
|
||||
fn new_window(&mut self, offset: u64, size: u64) -> io::Result<SharedWindowedReadStream> {
|
||||
self.seek(SeekFrom::Start(offset))?;
|
||||
Ok(SharedWindowedReadStream { base: self.as_dyn(), begin: offset, end: offset + size })
|
||||
}
|
||||
|
||||
/// Retrieves a type-erased reference to the stream.
|
||||
fn as_dyn(&mut self) -> &mut dyn ReadStream;
|
||||
}
|
||||
|
||||
impl<T> ReadStream for T
|
||||
where T: Read + Seek
|
||||
{
|
||||
fn as_dyn(&mut self) -> &mut dyn ReadStream { self }
|
||||
}
|
||||
|
||||
/// A non-owning window into an existing [`ReadStream`].
|
||||
pub struct SharedWindowedReadStream<'a> {
|
||||
/// A reference to the base stream.
|
||||
pub base: &'a mut dyn ReadStream,
|
||||
/// The beginning of the window in bytes.
|
||||
pub begin: u64,
|
||||
/// The end of the window in bytes.
|
||||
pub end: u64,
|
||||
}
|
||||
|
||||
impl<'a> SharedWindowedReadStream<'a> {
|
||||
/// Modifies the current window & seeks to the beginning of the window.
|
||||
pub fn set_window(&mut self, begin: u64, end: u64) -> io::Result<()> {
|
||||
self.base.seek(SeekFrom::Start(begin))?;
|
||||
self.begin = begin;
|
||||
self.end = end;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Read for SharedWindowedReadStream<'a> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let pos = self.stream_position()?;
|
||||
let size = self.end - self.begin;
|
||||
if pos == size {
|
||||
return Ok(0);
|
||||
}
|
||||
self.base.read(if pos + buf.len() as u64 > size {
|
||||
&mut buf[..(size - pos) as usize]
|
||||
} else {
|
||||
buf
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Seek for SharedWindowedReadStream<'a> {
|
||||
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
||||
let result = self.base.seek(match pos {
|
||||
SeekFrom::Start(p) => SeekFrom::Start(self.begin + p),
|
||||
SeekFrom::End(p) => SeekFrom::End(self.end as i64 + p),
|
||||
SeekFrom::Current(_) => pos,
|
||||
})?;
|
||||
if result < self.begin || result > self.end {
|
||||
Err(io::Error::from(io::ErrorKind::UnexpectedEof))
|
||||
} else {
|
||||
Ok(result - self.begin)
|
||||
}
|
||||
}
|
||||
|
||||
fn stream_position(&mut self) -> io::Result<u64> {
|
||||
Ok(self.base.stream_position()? - self.begin)
|
||||
}
|
||||
}
|
|
@ -1,13 +1,14 @@
|
|||
use std::{io, io::Read};
|
||||
|
||||
/// Decodes the LZMA Properties byte (lc/lp/pb).
|
||||
/// See `lzma_lzma_lclppb_decode` in `liblzma/lzma/lzma_decoder.c`.
|
||||
#[cfg(feature = "compress-lzma")]
|
||||
pub fn lzma_lclppb_decode(options: &mut liblzma::stream::LzmaOptions, byte: u8) -> io::Result<()> {
|
||||
pub fn lzma_lclppb_decode(
|
||||
options: &mut liblzma::stream::LzmaOptions,
|
||||
byte: u8,
|
||||
) -> std::io::Result<()> {
|
||||
let mut d = byte as u32;
|
||||
if d >= (9 * 5 * 5) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!("Invalid LZMA props byte: {}", d),
|
||||
));
|
||||
}
|
||||
|
@ -21,11 +22,11 @@ pub fn lzma_lclppb_decode(options: &mut liblzma::stream::LzmaOptions, byte: u8)
|
|||
/// Decodes LZMA properties.
|
||||
/// See `lzma_lzma_props_decode` in `liblzma/lzma/lzma_decoder.c`.
|
||||
#[cfg(feature = "compress-lzma")]
|
||||
pub fn lzma_props_decode(props: &[u8]) -> io::Result<liblzma::stream::LzmaOptions> {
|
||||
pub fn lzma_props_decode(props: &[u8]) -> std::io::Result<liblzma::stream::LzmaOptions> {
|
||||
use crate::array_ref;
|
||||
if props.len() != 5 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!("Invalid LZMA props length: {}", props.len()),
|
||||
));
|
||||
}
|
||||
|
@ -38,11 +39,11 @@ pub fn lzma_props_decode(props: &[u8]) -> io::Result<liblzma::stream::LzmaOption
|
|||
/// Decodes LZMA2 properties.
|
||||
/// See `lzma_lzma2_props_decode` in `liblzma/lzma/lzma2_decoder.c`.
|
||||
#[cfg(feature = "compress-lzma")]
|
||||
pub fn lzma2_props_decode(props: &[u8]) -> io::Result<liblzma::stream::LzmaOptions> {
|
||||
pub fn lzma2_props_decode(props: &[u8]) -> std::io::Result<liblzma::stream::LzmaOptions> {
|
||||
use std::cmp::Ordering;
|
||||
if props.len() != 1 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!("Invalid LZMA2 props length: {}", props.len()),
|
||||
));
|
||||
}
|
||||
|
@ -50,8 +51,8 @@ pub fn lzma2_props_decode(props: &[u8]) -> io::Result<liblzma::stream::LzmaOptio
|
|||
let mut options = liblzma::stream::LzmaOptions::new();
|
||||
options.dict_size(match d.cmp(&40) {
|
||||
Ordering::Greater => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!("Invalid LZMA2 props byte: {}", d),
|
||||
));
|
||||
}
|
||||
|
@ -66,13 +67,14 @@ pub fn lzma2_props_decode(props: &[u8]) -> io::Result<liblzma::stream::LzmaOptio
|
|||
pub fn new_lzma_decoder<R>(
|
||||
reader: R,
|
||||
options: &liblzma::stream::LzmaOptions,
|
||||
) -> io::Result<liblzma::read::XzDecoder<R>>
|
||||
) -> std::io::Result<liblzma::read::XzDecoder<R>>
|
||||
where
|
||||
R: Read,
|
||||
R: std::io::Read,
|
||||
{
|
||||
let mut filters = liblzma::stream::Filters::new();
|
||||
filters.lzma1(options);
|
||||
let stream = liblzma::stream::Stream::new_raw_decoder(&filters).map_err(io::Error::from)?;
|
||||
let stream =
|
||||
liblzma::stream::Stream::new_raw_decoder(&filters).map_err(std::io::Error::from)?;
|
||||
Ok(liblzma::read::XzDecoder::new_stream(reader, stream))
|
||||
}
|
||||
|
||||
|
@ -81,12 +83,13 @@ where
|
|||
pub fn new_lzma2_decoder<R>(
|
||||
reader: R,
|
||||
options: &liblzma::stream::LzmaOptions,
|
||||
) -> io::Result<liblzma::read::XzDecoder<R>>
|
||||
) -> std::io::Result<liblzma::read::XzDecoder<R>>
|
||||
where
|
||||
R: Read,
|
||||
R: std::io::Read,
|
||||
{
|
||||
let mut filters = liblzma::stream::Filters::new();
|
||||
filters.lzma2(options);
|
||||
let stream = liblzma::stream::Stream::new_raw_decoder(&filters).map_err(io::Error::from)?;
|
||||
let stream =
|
||||
liblzma::stream::Stream::new_raw_decoder(&filters).map_err(std::io::Error::from)?;
|
||||
Ok(liblzma::read::XzDecoder::new_stream(reader, stream))
|
||||
}
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
use std::{cmp::min, io, io::Read};
|
||||
use std::{
|
||||
cmp::min,
|
||||
io,
|
||||
io::{Read, Write},
|
||||
};
|
||||
|
||||
use zerocopy::{transmute_ref, AsBytes};
|
||||
use zerocopy::{transmute_ref, IntoBytes};
|
||||
|
||||
use crate::disc::SECTOR_SIZE;
|
||||
|
||||
|
@ -8,7 +12,7 @@ pub const LFG_K: usize = 521;
|
|||
pub const LFG_J: usize = 32;
|
||||
pub const SEED_SIZE: usize = 17;
|
||||
|
||||
/// Lagged Fibonacci generator for Wii partition junk data.
|
||||
/// Lagged Fibonacci generator for GC / Wii partition junk data.
|
||||
///
|
||||
/// References (license CC0-1.0):
|
||||
/// https://github.com/dolphin-emu/dolphin/blob/a0f555648c27ec0c928f6b1e1fcad5e2d7c4d0c4/docs/WiaAndRvz.md
|
||||
|
@ -19,6 +23,7 @@ pub struct LaggedFibonacci {
|
|||
}
|
||||
|
||||
impl Default for LaggedFibonacci {
|
||||
#[inline]
|
||||
fn default() -> Self { Self { buffer: [0u32; LFG_K], position: 0 } }
|
||||
}
|
||||
|
||||
|
@ -38,12 +43,16 @@ impl LaggedFibonacci {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn init_with_seed(&mut self, init: [u8; 4], disc_num: u8, partition_offset: u64) {
|
||||
/// Initializes the LFG with the standard seed for a given disc ID, disc number, and sector.
|
||||
/// The partition offset is used to determine the sector and how many bytes to skip within the
|
||||
/// sector.
|
||||
#[allow(clippy::missing_inline_in_public_items)]
|
||||
pub fn init_with_seed(&mut self, disc_id: [u8; 4], disc_num: u8, partition_offset: u64) {
|
||||
let seed = u32::from_be_bytes([
|
||||
init[2],
|
||||
init[1],
|
||||
init[3].wrapping_add(init[2]),
|
||||
init[0].wrapping_add(init[1]),
|
||||
disc_id[2],
|
||||
disc_id[1],
|
||||
disc_id[3].wrapping_add(disc_id[2]),
|
||||
disc_id[0].wrapping_add(disc_id[1]),
|
||||
]) ^ disc_num as u32;
|
||||
let sector = (partition_offset / SECTOR_SIZE as u64) as u32;
|
||||
let sector_offset = partition_offset % SECTOR_SIZE as u64;
|
||||
|
@ -62,9 +71,12 @@ impl LaggedFibonacci {
|
|||
self.skip(sector_offset as usize);
|
||||
}
|
||||
|
||||
/// Initializes the LFG with the seed read from a reader. The seed is assumed to be big-endian.
|
||||
/// This is used for rebuilding junk data in WIA/RVZ files.
|
||||
#[allow(clippy::missing_inline_in_public_items)]
|
||||
pub fn init_with_reader<R>(&mut self, reader: &mut R) -> io::Result<()>
|
||||
where R: Read + ?Sized {
|
||||
reader.read_exact(self.buffer[..SEED_SIZE].as_bytes_mut())?;
|
||||
reader.read_exact(self.buffer[..SEED_SIZE].as_mut_bytes())?;
|
||||
for x in self.buffer[..SEED_SIZE].iter_mut() {
|
||||
*x = u32::from_be(*x);
|
||||
}
|
||||
|
@ -73,7 +85,8 @@ impl LaggedFibonacci {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn forward(&mut self) {
|
||||
/// Advances the LFG by one step.
|
||||
fn forward(&mut self) {
|
||||
for i in 0..LFG_J {
|
||||
self.buffer[i] ^= self.buffer[i + LFG_K - LFG_J];
|
||||
}
|
||||
|
@ -82,6 +95,8 @@ impl LaggedFibonacci {
|
|||
}
|
||||
}
|
||||
|
||||
/// Skips `n` bytes of junk data.
|
||||
#[allow(clippy::missing_inline_in_public_items)]
|
||||
pub fn skip(&mut self, n: usize) {
|
||||
self.position += n;
|
||||
while self.position >= LFG_K * 4 {
|
||||
|
@ -90,6 +105,8 @@ impl LaggedFibonacci {
|
|||
}
|
||||
}
|
||||
|
||||
/// Fills the buffer with junk data.
|
||||
#[allow(clippy::missing_inline_in_public_items)]
|
||||
pub fn fill(&mut self, mut buf: &mut [u8]) {
|
||||
while !buf.is_empty() {
|
||||
let len = min(buf.len(), LFG_K * 4 - self.position);
|
||||
|
@ -103,6 +120,68 @@ impl LaggedFibonacci {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes junk data to the output stream.
|
||||
#[allow(clippy::missing_inline_in_public_items)]
|
||||
pub fn write<W>(&mut self, w: &mut W, mut len: u64) -> io::Result<()>
|
||||
where W: Write + ?Sized {
|
||||
while len > 0 {
|
||||
let write_len = min(len, LFG_K as u64 * 4 - self.position as u64) as usize;
|
||||
let bytes: &[u8; LFG_K * 4] = transmute_ref!(&self.buffer);
|
||||
w.write_all(&bytes[self.position..self.position + write_len])?;
|
||||
self.position += write_len;
|
||||
len -= write_len as u64;
|
||||
if self.position == LFG_K * 4 {
|
||||
self.forward();
|
||||
self.position = 0;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The junk data on GC / Wii discs is reinitialized every 32KB. This functions handles the
|
||||
/// wrapping logic and reinitializes the LFG at sector boundaries.
|
||||
#[allow(clippy::missing_inline_in_public_items)]
|
||||
pub fn fill_sector_chunked(
|
||||
&mut self,
|
||||
mut buf: &mut [u8],
|
||||
disc_id: [u8; 4],
|
||||
disc_num: u8,
|
||||
mut partition_offset: u64,
|
||||
) {
|
||||
while !buf.is_empty() {
|
||||
self.init_with_seed(disc_id, disc_num, partition_offset);
|
||||
let len =
|
||||
(SECTOR_SIZE - (partition_offset % SECTOR_SIZE as u64) as usize).min(buf.len());
|
||||
self.fill(&mut buf[..len]);
|
||||
buf = &mut buf[len..];
|
||||
partition_offset += len as u64;
|
||||
}
|
||||
}
|
||||
|
||||
/// The junk data on GC / Wii discs is reinitialized every 32KB. This functions handles the
|
||||
/// wrapping logic and reinitializes the LFG at sector boundaries.
|
||||
#[allow(clippy::missing_inline_in_public_items)]
|
||||
pub fn write_sector_chunked<W>(
|
||||
&mut self,
|
||||
w: &mut W,
|
||||
mut len: u64,
|
||||
disc_id: [u8; 4],
|
||||
disc_num: u8,
|
||||
mut partition_offset: u64,
|
||||
) -> io::Result<()>
|
||||
where
|
||||
W: Write + ?Sized,
|
||||
{
|
||||
while len > 0 {
|
||||
self.init_with_seed(disc_id, disc_num, partition_offset);
|
||||
let write_len = (SECTOR_SIZE as u64 - (partition_offset % SECTOR_SIZE as u64)).min(len);
|
||||
self.write(w, write_len)?;
|
||||
len -= write_len;
|
||||
partition_offset += write_len;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -132,4 +211,53 @@ mod tests {
|
|||
0xEA, 0xD0
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_init_with_seed_3() {
|
||||
let mut lfg = LaggedFibonacci::default();
|
||||
lfg.init_with_seed([0x47, 0x50, 0x49, 0x45], 0, 0x322904);
|
||||
let mut buf = [0u8; 16];
|
||||
lfg.fill(&mut buf);
|
||||
assert_eq!(buf, [
|
||||
0x97, 0xD8, 0x23, 0x0B, 0x12, 0xAA, 0x20, 0x45, 0xC2, 0xBD, 0x71, 0x8C, 0x30, 0x32,
|
||||
0xC5, 0x2F
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write() {
|
||||
let mut lfg = LaggedFibonacci::default();
|
||||
lfg.init_with_seed([0x47, 0x50, 0x49, 0x45], 0, 0x322904);
|
||||
let mut buf = [0u8; 16];
|
||||
lfg.write(&mut buf.as_mut_slice(), 16).unwrap();
|
||||
assert_eq!(buf, [
|
||||
0x97, 0xD8, 0x23, 0x0B, 0x12, 0xAA, 0x20, 0x45, 0xC2, 0xBD, 0x71, 0x8C, 0x30, 0x32,
|
||||
0xC5, 0x2F
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fill_sector_chunked() {
|
||||
let mut lfg = LaggedFibonacci::default();
|
||||
let mut buf = [0u8; 32];
|
||||
lfg.fill_sector_chunked(&mut buf, [0x47, 0x4D, 0x38, 0x45], 0, 0x27FF0);
|
||||
assert_eq!(buf, [
|
||||
0xAD, 0x6F, 0x21, 0xBE, 0x05, 0x57, 0x10, 0xED, 0xEA, 0xB0, 0x8E, 0xFD, 0x91, 0x58,
|
||||
0xA2, 0x0E, 0xDC, 0x0D, 0x59, 0xC0, 0x02, 0x98, 0xA5, 0x00, 0x39, 0x5B, 0x68, 0xA6,
|
||||
0x5D, 0x53, 0x2D, 0xB6
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_sector_chunked() {
|
||||
let mut lfg = LaggedFibonacci::default();
|
||||
let mut buf = [0u8; 32];
|
||||
lfg.write_sector_chunked(&mut buf.as_mut_slice(), 32, [0x47, 0x4D, 0x38, 0x45], 0, 0x27FF0)
|
||||
.unwrap();
|
||||
assert_eq!(buf, [
|
||||
0xAD, 0x6F, 0x21, 0xBE, 0x05, 0x57, 0x10, 0xED, 0xEA, 0xB0, 0x8E, 0xFD, 0x91, 0x58,
|
||||
0xA2, 0x0E, 0xDC, 0x0D, 0x59, 0xC0, 0x02, 0x98, 0xA5, 0x00, 0x39, 0x5B, 0x68, 0xA6,
|
||||
0x5D, 0x53, 0x2D, 0xB6
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,48 +1,50 @@
|
|||
use std::{io, io::Read};
|
||||
|
||||
use zerocopy::{AsBytes, FromBytes, FromZeroes};
|
||||
use zerocopy::{FromBytes, FromZeros, IntoBytes};
|
||||
|
||||
#[inline(always)]
|
||||
pub fn read_from<T, R>(reader: &mut R) -> io::Result<T>
|
||||
where
|
||||
T: FromBytes + FromZeroes + AsBytes,
|
||||
T: FromBytes + IntoBytes,
|
||||
R: Read + ?Sized,
|
||||
{
|
||||
let mut ret = <T>::new_zeroed();
|
||||
reader.read_exact(ret.as_bytes_mut())?;
|
||||
reader.read_exact(ret.as_mut_bytes())?;
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn read_vec<T, R>(reader: &mut R, count: usize) -> io::Result<Vec<T>>
|
||||
where
|
||||
T: FromBytes + FromZeroes + AsBytes,
|
||||
T: FromBytes + IntoBytes,
|
||||
R: Read + ?Sized,
|
||||
{
|
||||
let mut ret = <T>::new_vec_zeroed(count);
|
||||
reader.read_exact(ret.as_mut_slice().as_bytes_mut())?;
|
||||
let mut ret =
|
||||
<T>::new_vec_zeroed(count).map_err(|_| io::Error::from(io::ErrorKind::OutOfMemory))?;
|
||||
reader.read_exact(ret.as_mut_slice().as_mut_bytes())?;
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn read_box<T, R>(reader: &mut R) -> io::Result<Box<T>>
|
||||
where
|
||||
T: FromBytes + FromZeroes + AsBytes,
|
||||
T: FromBytes + IntoBytes,
|
||||
R: Read + ?Sized,
|
||||
{
|
||||
let mut ret = <T>::new_box_zeroed();
|
||||
reader.read_exact(ret.as_mut().as_bytes_mut())?;
|
||||
let mut ret = <T>::new_box_zeroed().map_err(|_| io::Error::from(io::ErrorKind::OutOfMemory))?;
|
||||
reader.read_exact(ret.as_mut().as_mut_bytes())?;
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn read_box_slice<T, R>(reader: &mut R, count: usize) -> io::Result<Box<[T]>>
|
||||
where
|
||||
T: FromBytes + FromZeroes + AsBytes,
|
||||
T: FromBytes + IntoBytes,
|
||||
R: Read + ?Sized,
|
||||
{
|
||||
let mut ret = <T>::new_box_slice_zeroed(count);
|
||||
reader.read_exact(ret.as_mut().as_bytes_mut())?;
|
||||
let mut ret = <[T]>::new_box_zeroed_with_elems(count)
|
||||
.map_err(|_| io::Error::from(io::ErrorKind::OutOfMemory))?;
|
||||
reader.read_exact(ret.as_mut().as_mut_bytes())?;
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
[package]
|
||||
name = "nodtool"
|
||||
version = "1.2.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.73.0"
|
||||
authors = ["Luke Street <luke@street.dev>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/encounter/nod-rs"
|
||||
documentation = "https://docs.rs/nod"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
documentation = "https://docs.rs/nodtool"
|
||||
readme = "../README.md"
|
||||
description = """
|
||||
CLI tool for verifying and converting GameCube and Wii disc images.
|
||||
"""
|
||||
keywords = ["gamecube", "wii", "iso", "wbfs", "rvz"]
|
||||
keywords.workspace = true
|
||||
categories = ["command-line-utilities", "parser-implementations"]
|
||||
build = "build.rs"
|
||||
|
||||
|
@ -20,32 +20,35 @@ asm = ["md-5/asm", "nod/asm", "sha1/asm"]
|
|||
nightly = ["crc32fast/nightly"]
|
||||
|
||||
[dependencies]
|
||||
argp = "0.3.0"
|
||||
base16ct = "0.2.0"
|
||||
crc32fast = "1.4.2"
|
||||
digest = "0.10.7"
|
||||
enable-ansi-support = "0.2.1"
|
||||
hex = { version = "0.4.3", features = ["serde"] }
|
||||
indicatif = "0.17.8"
|
||||
itertools = "0.12.1"
|
||||
log = "0.4.20"
|
||||
md-5 = "0.10.6"
|
||||
nod = { path = "../nod" }
|
||||
quick-xml = { version = "0.31.0", features = ["serialize"] }
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
sha1 = "0.10.6"
|
||||
size = "0.4.1"
|
||||
supports-color = "3.0.0"
|
||||
tracing = "0.1.40"
|
||||
tracing-attributes = "0.1.27"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
xxhash-rust = { version = "0.8.10", features = ["xxh64"] }
|
||||
zerocopy = { version = "0.7.32", features = ["alloc", "derive"] }
|
||||
zstd = "0.13.1"
|
||||
argp = "0.3"
|
||||
base16ct = "0.2"
|
||||
crc32fast = "1.4"
|
||||
digest = "0.10"
|
||||
enable-ansi-support = "0.2"
|
||||
hex = { version = "0.4", features = ["serde"] }
|
||||
indicatif = "0.17"
|
||||
itertools = "0.13"
|
||||
log = "0.4"
|
||||
md-5 = "0.10"
|
||||
nod = { version = "1.2", path = "../nod" }
|
||||
quick-xml = { version = "0.36", features = ["serialize"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
sha1 = "0.10"
|
||||
size = "0.4"
|
||||
supports-color = "3.0"
|
||||
tracing = "0.1"
|
||||
tracing-attributes = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
xxhash-rust = { version = "0.8", features = ["xxh64"] }
|
||||
zerocopy = { version = "0.8", features = ["alloc", "derive"] }
|
||||
zstd = "0.13"
|
||||
|
||||
[target.'cfg(target_env = "musl")'.dependencies]
|
||||
mimalloc = "0.1"
|
||||
|
||||
[build-dependencies]
|
||||
hex = { version = "0.4.3", features = ["serde"] }
|
||||
quick-xml = { version = "0.31.0", features = ["serialize"] }
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
zerocopy = { version = "0.7.32", features = ["alloc", "derive"] }
|
||||
zstd = "0.13.1"
|
||||
hex = { version = "0.4", features = ["serde"] }
|
||||
quick-xml = { version = "0.36", features = ["serialize"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
zerocopy = { version = "0.8", features = ["alloc", "derive"] }
|
||||
zstd = "0.13"
|
||||
|
|
|
@ -8,10 +8,10 @@ use std::{
|
|||
|
||||
use hex::deserialize as deserialize_hex;
|
||||
use serde::Deserialize;
|
||||
use zerocopy::AsBytes;
|
||||
use zerocopy::{Immutable, IntoBytes, KnownLayout};
|
||||
|
||||
// Keep in sync with build.rs
|
||||
#[derive(Clone, Debug, AsBytes)]
|
||||
#[derive(Clone, Debug, IntoBytes, Immutable, KnownLayout)]
|
||||
#[repr(C, align(4))]
|
||||
struct Header {
|
||||
entry_count: u32,
|
||||
|
@ -19,7 +19,7 @@ struct Header {
|
|||
}
|
||||
|
||||
// Keep in sync with redump.rs
|
||||
#[derive(Clone, Debug, AsBytes)]
|
||||
#[derive(Clone, Debug, IntoBytes, Immutable, KnownLayout)]
|
||||
#[repr(C, align(4))]
|
||||
struct GameEntry {
|
||||
crc32: u32,
|
||||
|
|
|
@ -11,7 +11,7 @@ use std::{
|
|||
use argp::FromArgs;
|
||||
use indicatif::{ProgressBar, ProgressState, ProgressStyle};
|
||||
use nod::{Disc, OpenOptions, Result, ResultContext};
|
||||
use zerocopy::FromZeroes;
|
||||
use zerocopy::FromZeros;
|
||||
|
||||
use crate::util::{
|
||||
digest::{digest_thread, DigestResult},
|
||||
|
@ -199,7 +199,7 @@ fn load_disc(path: &Path, name: &str, full_verify: bool) -> Result<DiscHashes> {
|
|||
});
|
||||
|
||||
let mut total_read = 0u64;
|
||||
let mut buf = <u8>::new_box_slice_zeroed(BUFFER_SIZE);
|
||||
let mut buf = <[u8]>::new_box_zeroed_with_elems(BUFFER_SIZE)?;
|
||||
while total_read < disc_size {
|
||||
let read = min(BUFFER_SIZE as u64, disc_size - total_read) as usize;
|
||||
disc.read_exact(&mut buf[..read]).with_context(|| {
|
||||
|
|
|
@ -2,19 +2,17 @@ use std::{
|
|||
borrow::Cow,
|
||||
fs,
|
||||
fs::File,
|
||||
io,
|
||||
io::{BufWriter, Write},
|
||||
io::{BufRead, Write},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use argp::FromArgs;
|
||||
use itertools::Itertools;
|
||||
use nod::{
|
||||
Disc, DiscHeader, Fst, Node, OpenOptions, PartitionBase, PartitionKind, PartitionMeta,
|
||||
ResultContext,
|
||||
Disc, Fst, Node, OpenOptions, PartitionBase, PartitionKind, PartitionMeta, ResultContext,
|
||||
};
|
||||
use size::{Base, Size};
|
||||
use zerocopy::AsBytes;
|
||||
use zerocopy::IntoBytes;
|
||||
|
||||
use crate::util::{display, has_extension};
|
||||
|
||||
|
@ -66,38 +64,38 @@ pub fn run(args: Args) -> nod::Result<()> {
|
|||
let mut out_dir = output_dir.clone();
|
||||
out_dir.push(info.kind.dir_name().as_ref());
|
||||
let mut partition = disc.open_partition(info.index)?;
|
||||
extract_partition(header, partition.as_mut(), &out_dir, is_wii, args.quiet)?;
|
||||
extract_partition(&disc, partition.as_mut(), &out_dir, is_wii, args.quiet)?;
|
||||
}
|
||||
} else if partition.eq_ignore_ascii_case("data") {
|
||||
let mut partition = disc.open_partition_kind(PartitionKind::Data)?;
|
||||
extract_partition(header, partition.as_mut(), &output_dir, is_wii, args.quiet)?;
|
||||
extract_partition(&disc, partition.as_mut(), &output_dir, is_wii, args.quiet)?;
|
||||
} else if partition.eq_ignore_ascii_case("update") {
|
||||
let mut partition = disc.open_partition_kind(PartitionKind::Update)?;
|
||||
extract_partition(header, partition.as_mut(), &output_dir, is_wii, args.quiet)?;
|
||||
extract_partition(&disc, partition.as_mut(), &output_dir, is_wii, args.quiet)?;
|
||||
} else if partition.eq_ignore_ascii_case("channel") {
|
||||
let mut partition = disc.open_partition_kind(PartitionKind::Channel)?;
|
||||
extract_partition(header, partition.as_mut(), &output_dir, is_wii, args.quiet)?;
|
||||
extract_partition(&disc, partition.as_mut(), &output_dir, is_wii, args.quiet)?;
|
||||
} else {
|
||||
let idx = partition.parse::<usize>().map_err(|_| "Invalid partition index")?;
|
||||
let mut partition = disc.open_partition(idx)?;
|
||||
extract_partition(header, partition.as_mut(), &output_dir, is_wii, args.quiet)?;
|
||||
extract_partition(&disc, partition.as_mut(), &output_dir, is_wii, args.quiet)?;
|
||||
}
|
||||
} else {
|
||||
let mut partition = disc.open_partition_kind(PartitionKind::Data)?;
|
||||
extract_partition(header, partition.as_mut(), &output_dir, is_wii, args.quiet)?;
|
||||
extract_partition(&disc, partition.as_mut(), &output_dir, is_wii, args.quiet)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn extract_partition(
|
||||
header: &DiscHeader,
|
||||
disc: &Disc,
|
||||
partition: &mut dyn PartitionBase,
|
||||
out_dir: &Path,
|
||||
is_wii: bool,
|
||||
quiet: bool,
|
||||
) -> nod::Result<()> {
|
||||
let meta = partition.meta()?;
|
||||
extract_sys_files(header, meta.as_ref(), out_dir, quiet)?;
|
||||
extract_sys_files(disc, meta.as_ref(), out_dir, quiet)?;
|
||||
|
||||
// Extract FST
|
||||
let files_dir = out_dir.join("files");
|
||||
|
@ -133,7 +131,7 @@ fn extract_partition(
|
|||
}
|
||||
|
||||
fn extract_sys_files(
|
||||
header: &DiscHeader,
|
||||
disc: &Disc,
|
||||
data: &PartitionMeta,
|
||||
out_dir: &Path,
|
||||
quiet: bool,
|
||||
|
@ -148,23 +146,27 @@ fn extract_sys_files(
|
|||
extract_file(data.raw_dol.as_ref(), &sys_dir.join("main.dol"), quiet)?;
|
||||
|
||||
// Wii files
|
||||
if header.is_wii() {
|
||||
let disc_header = disc.header();
|
||||
if disc_header.is_wii() {
|
||||
let disc_dir = out_dir.join("disc");
|
||||
fs::create_dir_all(&disc_dir)
|
||||
.with_context(|| format!("Creating directory {}", display(&disc_dir)))?;
|
||||
extract_file(&header.as_bytes()[..0x100], &disc_dir.join("header.bin"), quiet)?;
|
||||
}
|
||||
if let Some(ticket) = data.raw_ticket.as_deref() {
|
||||
extract_file(ticket, &out_dir.join("ticket.bin"), quiet)?;
|
||||
}
|
||||
if let Some(tmd) = data.raw_tmd.as_deref() {
|
||||
extract_file(tmd, &out_dir.join("tmd.bin"), quiet)?;
|
||||
}
|
||||
if let Some(cert_chain) = data.raw_cert_chain.as_deref() {
|
||||
extract_file(cert_chain, &out_dir.join("cert.bin"), quiet)?;
|
||||
}
|
||||
if let Some(h3_table) = data.raw_h3_table.as_deref() {
|
||||
extract_file(h3_table, &out_dir.join("h3.bin"), quiet)?;
|
||||
extract_file(&disc_header.as_bytes()[..0x100], &disc_dir.join("header.bin"), quiet)?;
|
||||
if let Some(region) = disc.region() {
|
||||
extract_file(region, &disc_dir.join("region.bin"), quiet)?;
|
||||
}
|
||||
if let Some(ticket) = data.raw_ticket.as_deref() {
|
||||
extract_file(ticket, &out_dir.join("ticket.bin"), quiet)?;
|
||||
}
|
||||
if let Some(tmd) = data.raw_tmd.as_deref() {
|
||||
extract_file(tmd, &out_dir.join("tmd.bin"), quiet)?;
|
||||
}
|
||||
if let Some(cert_chain) = data.raw_cert_chain.as_deref() {
|
||||
extract_file(cert_chain, &out_dir.join("cert.bin"), quiet)?;
|
||||
}
|
||||
if let Some(h3_table) = data.raw_h3_table.as_deref() {
|
||||
extract_file(h3_table, &out_dir.join("h3.bin"), quiet)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -182,7 +184,7 @@ fn extract_file(bytes: &[u8], out_path: &Path, quiet: bool) -> nod::Result<()> {
|
|||
}
|
||||
|
||||
fn extract_node(
|
||||
node: &Node,
|
||||
node: Node,
|
||||
partition: &mut dyn PartitionBase,
|
||||
base_path: &Path,
|
||||
name: &str,
|
||||
|
@ -197,9 +199,8 @@ fn extract_node(
|
|||
Size::from_bytes(node.length()).format().with_base(Base::Base10)
|
||||
);
|
||||
}
|
||||
let file = File::create(&file_path)
|
||||
let mut file = File::create(&file_path)
|
||||
.with_context(|| format!("Creating file {}", display(&file_path)))?;
|
||||
let mut w = BufWriter::with_capacity(partition.ideal_buffer_size(), file);
|
||||
let mut r = partition.open_file(node).with_context(|| {
|
||||
format!(
|
||||
"Opening file {} on disc for reading (offset {}, size {})",
|
||||
|
@ -208,7 +209,16 @@ fn extract_node(
|
|||
node.length()
|
||||
)
|
||||
})?;
|
||||
io::copy(&mut r, &mut w).with_context(|| format!("Extracting file {}", display(&file_path)))?;
|
||||
w.flush().with_context(|| format!("Flushing file {}", display(&file_path)))?;
|
||||
loop {
|
||||
let buf =
|
||||
r.fill_buf().with_context(|| format!("Extracting file {}", display(&file_path)))?;
|
||||
let len = buf.len();
|
||||
if len == 0 {
|
||||
break;
|
||||
}
|
||||
file.write_all(buf).with_context(|| format!("Writing file {}", display(&file_path)))?;
|
||||
r.consume(len);
|
||||
}
|
||||
file.flush().with_context(|| format!("Flushing file {}", display(&file_path)))?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -92,11 +92,7 @@ fn info_file(path: &Path) -> nod::Result<()> {
|
|||
} else if header.is_gamecube() {
|
||||
// TODO
|
||||
} else {
|
||||
println!(
|
||||
"Invalid GC/Wii magic: {:#010X}/{:#010X}",
|
||||
header.gcn_magic.get(),
|
||||
header.wii_magic.get()
|
||||
);
|
||||
println!("Invalid GC/Wii magic: {:#x?}/{:#x?}", header.gcn_magic, header.wii_magic);
|
||||
}
|
||||
println!();
|
||||
Ok(())
|
||||
|
|
|
@ -3,6 +3,9 @@ use argp::FromArgs;
|
|||
pub mod cmd;
|
||||
pub(crate) mod util;
|
||||
|
||||
// Re-export nod
|
||||
pub use nod;
|
||||
|
||||
#[derive(FromArgs, Debug)]
|
||||
#[argp(subcommand)]
|
||||
pub enum SubCommand {
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
mod argp_version;
|
||||
|
||||
// musl's allocator is very slow, so use mimalloc when targeting musl.
|
||||
// Otherwise, use the system allocator to avoid extra code size.
|
||||
#[cfg(target_env = "musl")]
|
||||
#[global_allocator]
|
||||
static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
||||
|
||||
use std::{env, error::Error, ffi::OsStr, fmt, path::PathBuf, str::FromStr};
|
||||
|
||||
use argp::{FromArgValue, FromArgs};
|
||||
|
|
|
@ -14,7 +14,7 @@ pub struct PathDisplay<'a> {
|
|||
path: &'a Path,
|
||||
}
|
||||
|
||||
impl<'a> fmt::Display for PathDisplay<'a> {
|
||||
impl fmt::Display for PathDisplay<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut first = true;
|
||||
for segment in self.path.iter() {
|
||||
|
|
|
@ -10,7 +10,7 @@ use std::{
|
|||
use hex::deserialize as deserialize_hex;
|
||||
use nod::{array_ref, Result};
|
||||
use serde::Deserialize;
|
||||
use zerocopy::{AsBytes, FromBytes, FromZeroes};
|
||||
use zerocopy::{FromBytes, FromZeros, Immutable, IntoBytes, KnownLayout};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GameResult<'a> {
|
||||
|
@ -33,18 +33,15 @@ impl<'a> Iterator for EntryIter<'a> {
|
|||
type Item = GameResult<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let header: &Header = Header::ref_from_prefix(self.data).unwrap();
|
||||
let (header, remaining) = Header::ref_from_prefix(self.data).ok()?;
|
||||
assert_eq!(header.entry_size as usize, size_of::<GameEntry>());
|
||||
if self.index >= header.entry_count as usize {
|
||||
return None;
|
||||
}
|
||||
|
||||
let entries_size = header.entry_count as usize * size_of::<GameEntry>();
|
||||
let entries: &[GameEntry] = GameEntry::slice_from(
|
||||
&self.data[size_of::<Header>()..size_of::<Header>() + entries_size],
|
||||
)
|
||||
.unwrap();
|
||||
let string_table: &[u8] = &self.data[size_of::<Header>() + entries_size..];
|
||||
let entries = <[GameEntry]>::ref_from_bytes(&remaining[..entries_size]).ok()?;
|
||||
let string_table = &self.data[size_of::<Header>() + entries_size..];
|
||||
|
||||
let entry = &entries[self.index];
|
||||
let offset = entry.string_table_offset as usize;
|
||||
|
@ -57,14 +54,12 @@ impl<'a> Iterator for EntryIter<'a> {
|
|||
|
||||
pub fn find_by_crc32(crc32: u32) -> Option<GameResult<'static>> {
|
||||
let data = loaded_data();
|
||||
let header: &Header = Header::ref_from_prefix(data).unwrap();
|
||||
let (header, remaining) = Header::ref_from_prefix(data).ok()?;
|
||||
assert_eq!(header.entry_size as usize, size_of::<GameEntry>());
|
||||
|
||||
let entries_size = header.entry_count as usize * size_of::<GameEntry>();
|
||||
let entries: &[GameEntry] =
|
||||
GameEntry::slice_from(&data[size_of::<Header>()..size_of::<Header>() + entries_size])
|
||||
.unwrap();
|
||||
let string_table: &[u8] = &data[size_of::<Header>() + entries_size..];
|
||||
let (entries_buf, string_table) = remaining.split_at(entries_size);
|
||||
let entries = <[GameEntry]>::ref_from_bytes(entries_buf).ok()?;
|
||||
|
||||
// Binary search by CRC32
|
||||
let index = entries.binary_search_by_key(&crc32, |entry| entry.crc32).ok()?;
|
||||
|
@ -84,7 +79,7 @@ fn loaded_data() -> &'static [u8] {
|
|||
LOADED
|
||||
.get_or_init(|| {
|
||||
let size = zstd::zstd_safe::get_frame_content_size(BUILTIN).unwrap().unwrap() as usize;
|
||||
let mut out = <u8>::new_box_slice_zeroed(size);
|
||||
let mut out = <[u8]>::new_box_zeroed_with_elems(size).unwrap();
|
||||
let out_size = zstd::bulk::Decompressor::new()
|
||||
.unwrap()
|
||||
.decompress_to_buffer(BUILTIN, out.as_mut())
|
||||
|
@ -126,7 +121,7 @@ pub fn load_dats<'a>(paths: impl Iterator<Item = &'a Path>) -> Result<()> {
|
|||
let entries_size = entries.len() * size_of::<GameEntry>();
|
||||
let string_table_size = entries.iter().map(|(_, name)| name.len() + 4).sum::<usize>();
|
||||
let total_size = size_of::<Header>() + entries_size + string_table_size;
|
||||
let mut result = <u8>::new_box_slice_zeroed(total_size);
|
||||
let mut result = <[u8]>::new_box_zeroed_with_elems(total_size)?;
|
||||
let mut out = Cursor::new(result.as_mut());
|
||||
|
||||
// Write game entries
|
||||
|
@ -152,7 +147,7 @@ pub fn load_dats<'a>(paths: impl Iterator<Item = &'a Path>) -> Result<()> {
|
|||
}
|
||||
|
||||
// Keep in sync with build.rs
|
||||
#[derive(Clone, Debug, AsBytes, FromBytes, FromZeroes)]
|
||||
#[derive(Clone, Debug, IntoBytes, FromBytes, Immutable, KnownLayout)]
|
||||
#[repr(C, align(4))]
|
||||
struct Header {
|
||||
entry_count: u32,
|
||||
|
@ -160,7 +155,7 @@ struct Header {
|
|||
}
|
||||
|
||||
// Keep in sync with build.rs
|
||||
#[derive(Clone, Debug, AsBytes, FromBytes, FromZeroes)]
|
||||
#[derive(Clone, Debug, IntoBytes, FromBytes, Immutable, KnownLayout)]
|
||||
#[repr(C, align(4))]
|
||||
struct GameEntry {
|
||||
crc32: u32,
|
||||
|
|
|
@ -11,7 +11,7 @@ use std::{
|
|||
use indicatif::{ProgressBar, ProgressState, ProgressStyle};
|
||||
use nod::{Compression, Disc, DiscHeader, DiscMeta, OpenOptions, Result, ResultContext};
|
||||
use size::Size;
|
||||
use zerocopy::FromZeroes;
|
||||
use zerocopy::FromZeros;
|
||||
|
||||
use crate::util::{
|
||||
digest::{digest_thread, DigestResult},
|
||||
|
@ -117,7 +117,7 @@ pub fn convert_and_verify(in_file: &Path, out_file: Option<&Path>, md5: bool) ->
|
|||
});
|
||||
|
||||
let mut total_read = 0u64;
|
||||
let mut buf = <u8>::new_box_slice_zeroed(BUFFER_SIZE);
|
||||
let mut buf = <[u8]>::new_box_zeroed_with_elems(BUFFER_SIZE)?;
|
||||
while total_read < disc_size {
|
||||
let read = min(BUFFER_SIZE as u64, disc_size - total_read) as usize;
|
||||
disc.read_exact(&mut buf[..read]).with_context(|| {
|
||||
|
|
Loading…
Reference in New Issue