WIP WIA/RVZ & more

This commit is contained in:
Luke Street 2024-02-02 16:17:35 -07:00
parent 97c726c209
commit 4f794f06cb
19 changed files with 3507 additions and 590 deletions

View File

@ -2,59 +2,174 @@ name: Build
on: [ push, pull_request ]
env:
BUILD_PROFILE: release-lto
CARGO_BIN_NAME: nodtool
CARGO_TARGET_DIR: target
jobs:
check:
name: Check
runs-on: ubuntu-latest
strategy:
matrix:
toolchain: [ stable, 1.51.0, nightly ]
toolchain: [ stable, 1.56.0, nightly ]
env:
RUSTFLAGS: -D warnings
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
- name: Checkout
uses: actions/checkout@v3
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@master
with:
profile: minimal
toolchain: ${{ matrix.toolchain }}
override: true
components: rustfmt, clippy
- name: Cargo check
run: cargo check --all-features --all-targets
- name: Cargo clippy
run: cargo clippy --all-features --all-targets
fmt:
name: Format
runs-on: ubuntu-latest
env:
RUSTFLAGS: -D warnings
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Rust toolchain
# We use nightly options in rustfmt.toml
uses: dtolnay/rust-toolchain@nightly
with:
components: rustfmt
- name: Cargo fmt
run: cargo fmt --all --check
deny:
name: Deny
runs-on: ubuntu-latest
strategy:
matrix:
checks:
- advisories
- bans licenses sources
# Prevent new advisories from failing CI
continue-on-error: ${{ matrix.checks == 'advisories' }}
steps:
- uses: actions/checkout@v3
- uses: EmbarkStudios/cargo-deny-action@v1
- uses: actions-rs/cargo@v1
with:
command: check
args: --all-features
- uses: actions-rs/cargo@v1
with:
command: clippy
args: --all-features
command: check ${{ matrix.checks }}
test:
name: Test
strategy:
matrix:
platform: [ ubuntu-latest, windows-latest, macos-latest ]
fail-fast: false
runs-on: ${{ matrix.platform }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Cargo test
run: cargo test --release --all-features
build:
name: Build
strategy:
matrix:
platform: [ ubuntu-latest, macos-latest, windows-latest ]
toolchain: [ stable, 1.51.0, nightly ]
include:
- platform: ubuntu-latest
target: x86_64-unknown-linux-musl
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: aarch64-unknown-linux-musl
name: linux-aarch64
build: zigbuild
features: nightly
- platform: ubuntu-latest
target: armv7-unknown-linux-musleabi
name: linux-armv7l
build: zigbuild
features: default
- platform: windows-latest
target: x86_64-pc-windows-msvc
name: windows-x86_64
build: build
features: asm
- platform: windows-latest
target: aarch64-pc-windows-msvc
name: windows-arm64
build: build
features: nightly
- platform: macos-latest
target: x86_64-apple-darwin
name: macos-x86_64
build: build
features: asm
- platform: macos-latest
target: aarch64-apple-darwin
name: macos-arm64
build: build
features: nightly
fail-fast: false
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
- name: Checkout
uses: actions/checkout@v3
- name: Install dependencies
if: matrix.packages != ''
run: |
sudo apt-get -y update
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
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@nightly
with:
profile: minimal
toolchain: ${{ matrix.toolchain }}
override: true
- uses: actions-rs/cargo@v1
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 }}
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
command: test
args: --release --all-features
- uses: actions-rs/cargo@v1
with:
command: build
args: --release --all-features
- uses: actions/upload-artifact@v2
with:
name: ${{ matrix.platform }}-${{ matrix.toolchain }}
name: ${{ matrix.name }}
path: |
target/release/nodtool
target/release/nodtool.exe
${{ 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
release:
name: Release
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
needs: [ build ]
steps:
- name: Download artifacts
uses: actions/download-artifact@v3
with:
path: artifacts
- name: Rename artifacts
working-directory: artifacts
run: |
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")"
done
ls -R ../out
- name: Release
uses: softprops/action-gh-release@v1
with:
files: out/*

View File

@ -1,7 +1,8 @@
[package]
name = "nod"
version = "0.1.2"
edition = "2018"
version = "0.2.0"
edition = "2021"
rust-version = "1.56.0"
authors = ["Luke Street <luke@street.dev>"]
license = "MIT OR Apache-2.0"
repository = "https://github.com/encounter/nod-rs"
@ -17,16 +18,33 @@ categories = ["command-line-utilities", "parser-implementations"]
name = "nodtool"
path = "src/bin.rs"
[profile.release]
lto = true
[profile.release-lto]
inherits = "release"
lto = "thin"
strip = "debuginfo"
[features]
default = ["compress-bzip2", "compress-zstd"] #, "compress-lzma"
asm = ["md-5/asm", "sha1/asm"]
compress-bzip2 = ["bzip2"]
compress-zstd = ["zstd"]
#compress-lzma = ["xz2"]
nightly = ["crc32fast/nightly"]
[dependencies]
aes = "0.7.5"
anyhow = "1.0.53"
binrw = "0.8.4"
block-modes = "0.8.1"
clap = "2.34.0"
encoding_rs = "0.8.30"
aes = "0.8.3"
argh = "0.1.12"
argh_derive = "0.1.12"
base16ct = "0.2.0"
binrw = "0.13.3"
bytemuck = "1.14.1"
bzip2 = { version = "0.4.4", optional = true }
cbc = "0.1.2"
crc32fast = "1.3.2"
encoding_rs = "0.8.33"
file-size = "1.0.3"
sha-1 = "0.10.0"
thiserror = "1.0.30"
md-5 = "0.10.6"
sha1 = "0.10.6"
thiserror = "1.0.56"
xz2 = { version = "0.1.7", optional = true }
zstd = { version = "0.13.0", optional = true }

View File

@ -14,7 +14,9 @@ Based on the C++ library [nod](https://github.com/AxioDL/nod),
but does not currently support authoring.
Currently supported file formats:
- ISO
- ISO (GCM)
- WIA / RVZ
- WBFS
- NFS (Wii U VC files, e.g. `hif_000000.nfs`)
### CLI tool
@ -34,20 +36,28 @@ nodtool extract /path/to/game/content/hif_000000.nfs [outdir]
### Library example
Opening a disc image and reading a file:
```rust
use nod::disc::{new_disc_base, PartHeader};
use nod::fst::NodeType;
use nod::io::new_disc_io;
use std::io::Read;
let mut disc_io = new_disc_io("path/to/file".as_ref())?;
let disc_base = new_disc_base(disc_io.as_mut())?;
let mut partition = disc_base.get_data_partition(disc_io.as_mut())?;
let header = partition.read_header()?;
if let Some(NodeType::File(node)) = header.find_node("/MP3/Worlds.txt") {
let mut s = String::new();
partition.begin_file_stream(node)?.read_to_string(&mut s);
println!(s);
use nod::{
disc::{new_disc_base, PartHeader},
fst::NodeType,
io::{new_disc_io, DiscIOOptions},
};
fn main() -> nod::Result<()> {
let options = DiscIOOptions::default();
let mut disc_io = new_disc_io("path/to/file.iso".as_ref(), &options)?;
let disc_base = new_disc_base(disc_io.as_mut())?;
let mut partition = disc_base.get_data_partition(disc_io.as_mut(), false)?;
let header = partition.read_header()?;
if let Some(NodeType::File(node)) = header.find_node("/MP3/Worlds.txt") {
let mut s = String::new();
partition.begin_file_stream(node)?.read_to_string(&mut s).expect("Failed to read file");
println!("{}", s);
}
Ok(())
}
```

View File

@ -6,3 +6,4 @@ reorder_impl_items = true
use_field_init_shorthand = true
use_small_heuristics = "Max"
where_single_line = true
format_code_in_doc_comments = true

View File

@ -1,74 +1,205 @@
use std::{
env, fs, io,
io::BufWriter,
error::Error,
fs,
fs::File,
io,
io::{BufWriter, Write},
path::{Path, PathBuf},
};
use clap::{clap_app, AppSettings};
use argh_derive::FromArgs;
use nod::{
disc::{new_disc_base, PartReadStream},
disc::{new_disc_base, PartHeader, PartReadStream, PartitionType},
fst::NodeType,
io::{has_extension, new_disc_io},
Result,
io::{has_extension, new_disc_io, DiscIOOptions},
Result, ResultContext,
};
use sha1::Digest;
fn main() -> Result<()> {
let matches = clap_app!(nodtool =>
(settings: &[
AppSettings::SubcommandRequiredElseHelp,
AppSettings::GlobalVersion,
AppSettings::DeriveDisplayOrder,
AppSettings::VersionlessSubcommands,
])
(global_settings: &[
AppSettings::ColoredHelp,
AppSettings::UnifiedHelpMessage,
])
(version: env!("CARGO_PKG_VERSION"))
(author: "Luke Street <luke@street.dev>")
(about: "Tool for reading GameCube and Wii disc images.")
(long_about: "Tool for reading GameCube and Wii disc images.
#[derive(FromArgs, Debug)]
/// Tool for reading GameCube and Wii disc images.
struct TopLevel {
#[argh(subcommand)]
command: SubCommand,
}
Based on <https://github.com/AxioDL/nod>, original authors:
Jack Andersen (jackoalan)
Phillip Stephens (Antidote)")
(@subcommand extract =>
(about: "Extract GameCube & Wii disc images")
(@arg FILE: +required "Path to disc image (ISO or NFS)")
(@arg DIR: "Output directory (optional)")
(@arg quiet: -q "Quiet output")
(@arg validate: -h "Validate disc hashes (Wii only)")
)
)
.get_matches();
if let Some(matches) = matches.subcommand_matches("extract") {
let file: PathBuf = PathBuf::from(matches.value_of("FILE").unwrap());
let output_dir: PathBuf;
if let Some(dir) = matches.value_of("DIR") {
output_dir = PathBuf::from(dir);
} else if has_extension(file.as_path(), "nfs") {
// Special logic to extract from content/hif_*.nfs to extracted/..
if let Some(parent) = file.parent() {
output_dir = parent.with_file_name("extracted");
} else {
output_dir = file.with_extension("");
}
} else {
output_dir = file.with_extension("");
#[derive(FromArgs, Debug)]
#[argh(subcommand)]
enum SubCommand {
Extract(ExtractArgs),
Convert(ConvertArgs),
Verify(VerifyArgs),
}
#[derive(FromArgs, Debug)]
/// Extract a disc image.
#[argh(subcommand, name = "extract")]
struct ExtractArgs {
#[argh(positional)]
/// path to disc image (ISO or NFS)
file: PathBuf,
#[argh(positional)]
/// output directory (optional)
out: Option<PathBuf>,
#[argh(switch, short = 'q')]
/// quiet output
quiet: bool,
#[argh(switch, short = 'h')]
/// validate disc hashes (Wii only)
validate: bool,
}
#[derive(FromArgs, Debug)]
/// Extract a disc image.
#[argh(subcommand, name = "convert")]
struct ConvertArgs {
#[argh(positional)]
/// path to disc image
file: PathBuf,
#[argh(positional)]
/// output ISO file
out: PathBuf,
}
#[derive(FromArgs, Debug)]
/// Verifies a disc image.
#[argh(subcommand, name = "verify")]
struct VerifyArgs {
#[argh(positional)]
/// path to disc image
file: PathBuf,
}
fn main() {
let args: TopLevel = argh::from_env();
let result = match args.command {
SubCommand::Convert(c_args) => convert(c_args),
SubCommand::Extract(c_args) => extract(c_args),
SubCommand::Verify(c_args) => verify(c_args),
};
if let Err(e) = result {
eprintln!("Failed: {}", e);
if let Some(source) = e.source() {
eprintln!("Caused by: {}", source);
}
let mut disc_io = new_disc_io(file.as_path())?;
let disc_base = new_disc_base(disc_io.as_mut())?;
let mut partition =
disc_base.get_data_partition(disc_io.as_mut(), matches.is_present("validate"))?;
let header = partition.read_header()?;
extract_node(
header.root_node(),
partition.as_mut(),
output_dir.as_path(),
matches.is_present("quiet"),
)?;
std::process::exit(1);
}
Result::Ok(())
}
fn convert(args: ConvertArgs) -> Result<()> { convert_and_verify(&args.file, Some(&args.out)) }
fn verify(args: VerifyArgs) -> Result<()> { convert_and_verify(&args.file, None) }
fn convert_and_verify(in_file: &Path, out_file: Option<&Path>) -> Result<()> {
println!("Loading {}", in_file.display());
let mut disc_io = new_disc_io(in_file, &DiscIOOptions { rebuild_hashes: true })?;
let disc_base = new_disc_base(disc_io.as_mut())?;
let header = disc_base.get_header();
println!(
"\nGame ID: {}{}{}{}{}{}",
header.game_id[0] as char,
header.game_id[1] as char,
header.game_id[2] as char,
header.game_id[3] as char,
header.game_id[4] as char,
header.game_id[5] as char
);
println!("Game title: {}", header.game_title);
println!("Disc num: {}", header.disc_num);
println!("Disc version: {}", header.disc_version);
let mut stream = disc_io.begin_read_stream(0).context("Creating disc read stream")?;
let mut crc = crc32fast::Hasher::new();
let mut md5 = md5::Md5::new();
let mut sha1 = sha1::Sha1::new();
let mut file = if let Some(out_file) = out_file {
Some(
File::create(out_file)
.with_context(|| format!("Creating file {}", out_file.display()))?,
)
} else {
None
};
const BUFFER_SIZE: usize = 1015808; // LCM(0x8000, 0x7C00)
let mut buf = vec![0u8; BUFFER_SIZE];
let mut total_read = 0u64;
loop {
let read = stream.read(&mut buf).with_context(|| {
format!("Reading {} bytes at disc offset {}", BUFFER_SIZE, total_read)
})?;
if read == 0 {
break;
}
let slice = &buf[..read];
crc.update(slice);
md5.update(slice);
sha1.update(slice);
if let Some(file) = &mut file {
file.write_all(slice).with_context(|| {
format!("Writing {} bytes at offset {}", slice.len(), total_read)
})?;
}
total_read += read as u64;
}
println!();
println!("CRC32: {:08x}", crc.finalize());
println!("MD5: {:032x}", md5.finalize());
println!("SHA-1: {:040x}", sha1.finalize());
if let (Some(path), Some(file)) = (out_file, &mut file) {
file.flush().context("Flushing output file")?;
println!("Wrote {} to {}", file_size::fit_4(total_read), path.display());
}
Ok(())
}
fn extract(args: ExtractArgs) -> Result<()> {
let output_dir: PathBuf;
if let Some(dir) = args.out {
output_dir = dir;
} else if has_extension(&args.file, "nfs") {
// Special logic to extract from content/hif_*.nfs to extracted/..
if let Some(parent) = args.file.parent() {
output_dir = parent.with_file_name("extracted");
} else {
output_dir = args.file.with_extension("");
}
} else {
output_dir = args.file.with_extension("");
}
let mut disc_io = new_disc_io(&args.file, &DiscIOOptions { rebuild_hashes: args.validate })?;
let disc_base = new_disc_base(disc_io.as_mut())?;
let mut partition =
disc_base.get_partition(disc_io.as_mut(), PartitionType::Data, args.validate)?;
let header = partition.read_header()?;
extract_sys_files(header.as_ref(), &output_dir.join("sys"), args.quiet)?;
extract_node(header.root_node(), partition.as_mut(), &output_dir.join("files"), args.quiet)?;
Ok(())
}
fn extract_sys_files(header: &dyn PartHeader, out_dir: &Path, quiet: bool) -> Result<()> {
fs::create_dir_all(out_dir)
.with_context(|| format!("Creating output directory {}", out_dir.display()))?;
extract_file(header.boot_bytes(), &out_dir.join("boot.bin"), quiet)?;
extract_file(header.bi2_bytes(), &out_dir.join("bi2.bin"), quiet)?;
extract_file(header.apploader_bytes(), &out_dir.join("apploader.img"), quiet)?;
extract_file(header.fst_bytes(), &out_dir.join("fst.bin"), quiet)?;
extract_file(header.dol_bytes(), &out_dir.join("main.dol"), quiet)?;
Ok(())
}
fn extract_file(bytes: &[u8], out_path: &Path, quiet: bool) -> Result<()> {
if !quiet {
println!(
"Extracting {} (size: {})",
out_path.display(),
file_size::fit_4(bytes.len() as u64)
);
}
fs::write(out_path, bytes).with_context(|| format!("Writing file {}", out_path.display()))?;
Ok(())
}
fn extract_node(
@ -76,37 +207,49 @@ fn extract_node(
partition: &mut dyn PartReadStream,
base_path: &Path,
quiet: bool,
) -> io::Result<()> {
) -> Result<()> {
match node {
NodeType::File(v) => {
let mut file_path = base_path.to_owned();
file_path.push(v.name.as_ref());
let mut file_path = base_path.to_path_buf();
file_path.push(v.name.as_str());
if !quiet {
println!(
"Extracting {} (size: {})",
file_path.to_string_lossy(),
file_path.display(),
file_size::fit_4(v.length as u64)
);
}
let file = fs::File::create(file_path)?;
let file = File::create(&file_path)
.with_context(|| format!("Creating file {}", file_path.display()))?;
let mut buf_writer = BufWriter::with_capacity(partition.ideal_buffer_size(), file);
io::copy(&mut partition.begin_file_stream(v)?, &mut buf_writer)?;
let mut stream = partition.begin_file_stream(v).with_context(|| {
format!(
"Opening file {} on disc for reading (offset {}, size {})",
v.name, v.offset, v.length
)
})?;
io::copy(&mut stream, &mut buf_writer)
.with_context(|| format!("Extracting file {}", file_path.display()))?;
buf_writer.flush().with_context(|| format!("Flushing file {}", file_path.display()))?;
}
NodeType::Directory(v, c) => {
if v.name.is_empty() {
fs::create_dir_all(base_path)?;
fs::create_dir_all(base_path).with_context(|| {
format!("Creating output directory {}", base_path.display())
})?;
for x in c {
extract_node(x, partition, base_path, quiet)?;
}
} else {
let mut new_base = base_path.to_owned();
new_base.push(v.name.as_ref());
fs::create_dir_all(&new_base)?;
let mut new_base = base_path.to_path_buf();
new_base.push(v.name.as_str());
fs::create_dir_all(&new_base)
.with_context(|| format!("Creating output directory {}", new_base.display()))?;
for x in c {
extract_node(x, partition, new_base.as_path(), quiet)?;
}
}
}
}
io::Result::Ok(())
Ok(())
}

View File

@ -1,16 +1,20 @@
use std::{
io,
io::{Read, Seek, SeekFrom},
io::{Cursor, Read, Seek, SeekFrom},
};
use binrw::{BinRead, BinReaderExt};
use crate::{
disc::{BI2Header, DiscBase, DiscIO, Header, PartHeader, PartReadStream, BUFFER_SIZE},
div_rem,
fst::{find_node, node_parser, Node, NodeKind, NodeType},
disc::{
AppLoaderHeader, DiscBase, DiscIO, DolHeader, Header, PartHeader, PartReadStream,
PartitionHeader, PartitionType, SECTOR_SIZE,
},
fst::{find_node, read_fst, Node, NodeKind, NodeType},
streams::{ReadStream, SharedWindowedReadStream},
Result,
util::{
div_rem,
reader::{read_bytes, FromReader},
},
Error, Result, ResultContext,
};
pub(crate) struct DiscGCN {
@ -18,7 +22,7 @@ pub(crate) struct DiscGCN {
}
impl DiscGCN {
pub(crate) fn new(header: Header) -> Result<DiscGCN> { Result::Ok(DiscGCN { header }) }
pub(crate) fn new(header: Header) -> Result<DiscGCN> { Ok(DiscGCN { header }) }
}
impl DiscBase for DiscGCN {
@ -29,37 +33,62 @@ impl DiscBase for DiscGCN {
disc_io: &'a mut dyn DiscIO,
_validate_hashes: bool,
) -> Result<Box<dyn PartReadStream + 'a>> {
Result::Ok(Box::from(GCPartReadStream {
stream: disc_io.begin_read_stream(0)?,
let stream = disc_io.begin_read_stream(0).context("Opening data partition stream")?;
Ok(Box::from(GCPartReadStream {
stream,
offset: 0,
cur_block: u64::MAX,
buf: [0; BUFFER_SIZE],
cur_block: u32::MAX,
buf: [0; SECTOR_SIZE],
}))
}
fn get_partition<'a>(
&self,
disc_io: &'a mut dyn DiscIO,
part_type: PartitionType,
_validate_hashes: bool,
) -> Result<Box<dyn PartReadStream + 'a>> {
if part_type == PartitionType::Data {
Ok(Box::from(GCPartReadStream {
stream: disc_io.begin_read_stream(0).context("Opening partition read stream")?,
offset: 0,
cur_block: u32::MAX,
buf: [0; SECTOR_SIZE],
}))
} else {
Err(Error::DiscFormat(format!(
"Invalid partition type {:?} for GameCube disc",
part_type
)))
}
}
}
struct GCPartReadStream<'a> {
stream: Box<dyn ReadStream + 'a>,
offset: u64,
cur_block: u64,
buf: [u8; BUFFER_SIZE],
cur_block: u32,
buf: [u8; SECTOR_SIZE],
}
impl<'a> Read for GCPartReadStream<'a> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let (mut block, mut block_offset) = div_rem(self.offset as usize, BUFFER_SIZE);
let (block, block_offset) = div_rem(self.offset, SECTOR_SIZE as u64);
let mut block = block as u32;
let mut block_offset = block_offset as usize;
let mut rem = buf.len();
let mut read: usize = 0;
while rem > 0 {
if block != self.cur_block as usize {
if block != self.cur_block {
self.stream.read_exact(&mut self.buf)?;
self.cur_block = block as u64;
self.cur_block = block;
}
let mut cache_size = rem;
if cache_size + block_offset > BUFFER_SIZE {
cache_size = BUFFER_SIZE - block_offset;
if cache_size + block_offset > SECTOR_SIZE {
cache_size = SECTOR_SIZE - block_offset;
}
buf[read..read + cache_size]
@ -71,7 +100,7 @@ impl<'a> Read for GCPartReadStream<'a> {
}
self.offset += buf.len() as u64;
io::Result::Ok(buf.len())
Ok(buf.len())
}
}
@ -82,15 +111,15 @@ impl<'a> Seek for GCPartReadStream<'a> {
SeekFrom::End(v) => (self.stable_stream_len()? as i64 + v) as u64,
SeekFrom::Current(v) => (self.offset as i64 + v) as u64,
};
let block = self.offset / BUFFER_SIZE as u64;
if block != self.cur_block {
self.stream.seek(SeekFrom::Start(block * BUFFER_SIZE as u64))?;
self.cur_block = u64::MAX;
let block = self.offset / SECTOR_SIZE as u64;
if block as u32 != self.cur_block {
self.stream.seek(SeekFrom::Start(block * SECTOR_SIZE as u64))?;
self.cur_block = u32::MAX;
}
io::Result::Ok(self.offset)
Ok(self.offset)
}
fn stream_position(&mut self) -> io::Result<u64> { io::Result::Ok(self.offset) }
fn stream_position(&mut self) -> io::Result<u64> { Ok(self.offset) }
}
impl<'a> ReadStream for GCPartReadStream<'a> {
@ -102,28 +131,129 @@ impl<'a> ReadStream for GCPartReadStream<'a> {
impl<'a> PartReadStream for GCPartReadStream<'a> {
fn begin_file_stream(&mut self, node: &Node) -> io::Result<SharedWindowedReadStream> {
assert_eq!(node.kind, NodeKind::File);
io::Result::Ok(self.new_window(node.offset as u64, node.length as u64)?)
self.new_window(node.offset as u64, node.length as u64)
}
fn read_header(&mut self) -> Result<Box<dyn PartHeader>> {
self.seek(SeekFrom::Start(0))?;
Result::Ok(Box::from(self.read_be::<GCPartition>()?))
self.seek(SeekFrom::Start(0)).context("Seeking to partition header")?;
Ok(Box::from(read_part_header(self)?))
}
fn ideal_buffer_size(&self) -> usize { BUFFER_SIZE }
fn ideal_buffer_size(&self) -> usize { SECTOR_SIZE }
}
#[derive(Clone, Debug, PartialEq, BinRead)]
const BOOT_SIZE: usize = Header::STATIC_SIZE + PartitionHeader::STATIC_SIZE;
const BI2_SIZE: usize = 0x2000;
#[derive(Clone, Debug)]
pub(crate) struct GCPartition {
raw_boot: [u8; BOOT_SIZE],
raw_bi2: [u8; BI2_SIZE],
raw_apploader: Vec<u8>,
raw_fst: Vec<u8>,
raw_dol: Vec<u8>,
// Parsed
header: Header,
bi2_header: BI2Header,
#[br(seek_before = SeekFrom::Start(header.fst_off as u64))]
#[br(parse_with = node_parser)]
partition_header: PartitionHeader,
apploader_header: AppLoaderHeader,
root_node: NodeType,
dol_header: DolHeader,
}
fn read_part_header<R>(reader: &mut R) -> Result<GCPartition>
where R: Read + Seek + ?Sized {
// boot.bin
let raw_boot = <[u8; BOOT_SIZE]>::from_reader(reader).context("Reading boot.bin")?;
let mut boot_bytes = raw_boot.as_slice();
let header = Header::from_reader(&mut boot_bytes).context("Parsing disc header")?;
let partition_header =
PartitionHeader::from_reader(&mut boot_bytes).context("Parsing partition header")?;
debug_assert_eq!(boot_bytes.len(), 0, "failed to consume boot.bin");
// bi2.bin
let raw_bi2 = <[u8; BI2_SIZE]>::from_reader(reader).context("Reading bi2.bin")?;
// apploader.bin
let mut raw_apploader =
read_bytes(reader, AppLoaderHeader::STATIC_SIZE).context("Reading apploader header")?;
let apploader_header = AppLoaderHeader::from_reader(&mut raw_apploader.as_slice())
.context("Parsing apploader header")?;
raw_apploader.resize(
AppLoaderHeader::STATIC_SIZE
+ apploader_header.size as usize
+ apploader_header.trailer_size as usize,
0,
);
reader
.read_exact(&mut raw_apploader[AppLoaderHeader::STATIC_SIZE..])
.context("Reading apploader")?;
// fst.bin
reader
.seek(SeekFrom::Start(partition_header.fst_off as u64))
.context("Seeking to FST offset")?;
let raw_fst = read_bytes(reader, partition_header.fst_sz as usize).with_context(|| {
format!(
"Reading partition FST (offset {}, size {})",
partition_header.fst_off, partition_header.fst_sz
)
})?;
let root_node = read_fst(&mut Cursor::new(&*raw_fst))?;
// main.dol
reader
.seek(SeekFrom::Start(partition_header.dol_off as u64))
.context("Seeking to DOL offset")?;
let mut raw_dol = read_bytes(reader, DolHeader::STATIC_SIZE).context("Reading DOL header")?;
let dol_header =
DolHeader::from_reader(&mut raw_dol.as_slice()).context("Parsing DOL header")?;
let dol_size = dol_header
.text_offs
.iter()
.zip(&dol_header.text_sizes)
.map(|(offs, size)| offs + size)
.chain(
dol_header.data_offs.iter().zip(&dol_header.data_sizes).map(|(offs, size)| offs + size),
)
.max()
.unwrap_or(DolHeader::STATIC_SIZE as u32);
raw_dol.resize(dol_size as usize, 0);
reader.read_exact(&mut raw_dol[DolHeader::STATIC_SIZE..]).context("Reading DOL")?;
Ok(GCPartition {
raw_boot,
raw_bi2,
raw_apploader,
raw_fst,
raw_dol,
header,
partition_header,
apploader_header,
root_node,
dol_header,
})
}
impl PartHeader for GCPartition {
fn root_node(&self) -> &NodeType { &self.root_node }
fn find_node(&self, path: &str) -> Option<&NodeType> { find_node(&self.root_node, path) }
fn boot_bytes(&self) -> &[u8] { &self.raw_boot }
fn bi2_bytes(&self) -> &[u8] { &self.raw_bi2 }
fn apploader_bytes(&self) -> &[u8] { &self.raw_apploader }
fn fst_bytes(&self) -> &[u8] { &self.raw_fst }
fn dol_bytes(&self) -> &[u8] { &self.raw_dol }
fn disc_header(&self) -> &Header { &self.header }
fn partition_header(&self) -> &PartitionHeader { &self.partition_header }
fn apploader_header(&self) -> &AppLoaderHeader { &self.apploader_header }
fn dol_header(&self) -> &DolHeader { &self.dol_header }
}

View File

@ -1,22 +1,21 @@
//! Disc type related logic (GameCube, Wii)
use std::{fmt::Debug, io};
use binrw::{BinRead, BinReaderExt, NullString};
use std::{ffi::CStr, fmt::Debug, io, io::Read};
use crate::{
disc::{gcn::DiscGCN, wii::DiscWii},
fst::{Node, NodeType},
io::DiscIO,
streams::{ReadStream, SharedWindowedReadStream},
Error, Result,
util::reader::{skip_bytes, struct_size, FromReader},
Error, Result, ResultContext,
};
pub(crate) mod gcn;
pub(crate) mod wii;
/// Shared GameCube & Wii disc header
#[derive(Clone, Debug, PartialEq, BinRead)]
#[derive(Clone, Debug, PartialEq)]
pub struct Header {
/// Game ID (e.g. GM8E01 for Metroid Prime)
pub game_id: [u8; 6],
@ -28,24 +27,67 @@ pub struct Header {
pub audio_streaming: u8,
/// Audio streaming buffer size
pub audio_stream_buf_size: u8,
#[br(pad_before(14))]
/// If this is a Wii disc, this will be 0x5D1C9EA3
pub wii_magic: u32,
/// If this is a GameCube disc, this will be 0xC2339F3D
pub gcn_magic: u32,
/// Game title
#[br(pad_size_to(64), map = NullString::into_string)]
pub game_title: String,
/// Disable hash verification
pub disable_hash_verification: u8,
/// Disable disc encryption and H3 hash table loading and verification
pub disable_disc_enc: u8,
}
fn from_c_str(bytes: &[u8]) -> io::Result<String> {
CStr::from_bytes_until_nul(bytes)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?
.to_str()
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
.map(|s| s.to_string())
}
impl FromReader for Header {
type Args<'a> = ();
const STATIC_SIZE: usize = 0x400;
fn from_reader_args<R>(reader: &mut R, _args: Self::Args<'_>) -> io::Result<Self>
where R: Read + ?Sized {
let game_id = <[u8; 6]>::from_reader(reader)?;
let disc_num = u8::from_reader(reader)?;
let disc_version = u8::from_reader(reader)?;
let audio_streaming = u8::from_reader(reader)?;
let audio_stream_buf_size = u8::from_reader(reader)?;
skip_bytes::<14, _>(reader)?; // padding
let wii_magic = u32::from_reader(reader)?;
let gcn_magic = u32::from_reader(reader)?;
let game_title = from_c_str(&<[u8; 64]>::from_reader(reader)?)?;
let disable_hash_verification = u8::from_reader(reader)?;
let disable_disc_enc = u8::from_reader(reader)?;
skip_bytes::<926, _>(reader)?; // padding
Ok(Self {
game_id,
disc_num,
disc_version,
audio_streaming,
audio_stream_buf_size,
wii_magic,
gcn_magic,
game_title,
disable_hash_verification,
disable_disc_enc,
})
}
}
/// Partition header
#[derive(Clone, Debug, PartialEq)]
pub struct PartitionHeader {
/// Debug monitor offset
#[br(pad_before(0x39e))]
pub debug_mon_off: u32,
/// Debug monitor load address
pub debug_load_addr: u32,
#[br(pad_before(0x18))]
/// Offset to main DOL (Wii: >> 2)
pub dol_off: u32,
/// Offset to file system table (Wii: >> 2)
@ -59,28 +101,130 @@ pub struct Header {
/// User position
pub user_position: u32,
/// User size
#[br(pad_after(4))]
pub user_sz: u32,
}
#[derive(Debug, PartialEq, BinRead, Copy, Clone)]
pub(crate) struct BI2Header {
pub(crate) debug_monitor_size: i32,
pub(crate) sim_mem_size: i32,
pub(crate) arg_offset: u32,
pub(crate) debug_flag: u32,
pub(crate) trk_address: u32,
pub(crate) trk_size: u32,
pub(crate) country_code: u32,
pub(crate) unk1: u32,
pub(crate) unk2: u32,
pub(crate) unk3: u32,
pub(crate) dol_limit: u32,
#[br(pad_after(0x1fd0))]
pub(crate) unk4: u32,
impl FromReader for PartitionHeader {
type Args<'a> = ();
const STATIC_SIZE: usize = struct_size([
u32::STATIC_SIZE, // debug_mon_off
u32::STATIC_SIZE, // debug_load_addr
0x18, // padding
u32::STATIC_SIZE, // dol_off
u32::STATIC_SIZE, // fst_off
u32::STATIC_SIZE, // fst_sz
u32::STATIC_SIZE, // fst_max_sz
u32::STATIC_SIZE, // fst_memory_address
u32::STATIC_SIZE, // user_position
u32::STATIC_SIZE, // user_sz
4, // padding
]);
fn from_reader_args<R>(reader: &mut R, _args: Self::Args<'_>) -> io::Result<Self>
where R: Read + ?Sized {
let debug_mon_off = u32::from_reader(reader)?;
let debug_load_addr = u32::from_reader(reader)?;
skip_bytes::<0x18, _>(reader)?; // padding
let dol_off = u32::from_reader(reader)?;
let fst_off = u32::from_reader(reader)?;
let fst_sz = u32::from_reader(reader)?;
let fst_max_sz = u32::from_reader(reader)?;
let fst_memory_address = u32::from_reader(reader)?;
let user_position = u32::from_reader(reader)?;
let user_sz = u32::from_reader(reader)?;
skip_bytes::<4, _>(reader)?; // padding
Ok(Self {
debug_mon_off,
debug_load_addr,
dol_off,
fst_off,
fst_sz,
fst_max_sz,
fst_memory_address,
user_position,
user_sz,
})
}
}
pub(crate) const BUFFER_SIZE: usize = 0x8000;
#[derive(Debug, PartialEq, Clone)]
pub struct AppLoaderHeader {
pub date: String,
pub entry_point: u32,
pub size: u32,
pub trailer_size: u32,
}
impl FromReader for AppLoaderHeader {
type Args<'a> = ();
const STATIC_SIZE: usize = struct_size([
16, // date
u32::STATIC_SIZE, // entry_point
u32::STATIC_SIZE, // size
u32::STATIC_SIZE, // trailer_size
4, // padding
]);
fn from_reader_args<R>(reader: &mut R, _args: Self::Args<'_>) -> io::Result<Self>
where R: Read + ?Sized {
let date = from_c_str(&<[u8; 16]>::from_reader(reader)?)?;
let entry_point = u32::from_reader(reader)?;
let size = u32::from_reader(reader)?;
let trailer_size = u32::from_reader(reader)?;
skip_bytes::<4, _>(reader)?; // padding
Ok(Self { date, entry_point, size, trailer_size })
}
}
pub const DOL_MAX_TEXT_SECTIONS: usize = 7;
pub const DOL_MAX_DATA_SECTIONS: usize = 11;
#[derive(Debug, Clone)]
pub struct DolHeader {
pub text_offs: [u32; DOL_MAX_TEXT_SECTIONS],
pub data_offs: [u32; DOL_MAX_DATA_SECTIONS],
pub text_addrs: [u32; DOL_MAX_TEXT_SECTIONS],
pub data_addrs: [u32; DOL_MAX_DATA_SECTIONS],
pub text_sizes: [u32; DOL_MAX_TEXT_SECTIONS],
pub data_sizes: [u32; DOL_MAX_DATA_SECTIONS],
pub bss_addr: u32,
pub bss_size: u32,
pub entry_point: u32,
}
impl FromReader for DolHeader {
type Args<'a> = ();
const STATIC_SIZE: usize = 0x100;
fn from_reader_args<R>(reader: &mut R, _args: Self::Args<'_>) -> io::Result<Self>
where R: Read + ?Sized {
let result = Self {
text_offs: <_>::from_reader(reader)?,
data_offs: <_>::from_reader(reader)?,
text_addrs: <_>::from_reader(reader)?,
data_addrs: <_>::from_reader(reader)?,
text_sizes: <_>::from_reader(reader)?,
data_sizes: <_>::from_reader(reader)?,
bss_addr: <_>::from_reader(reader)?,
bss_size: <_>::from_reader(reader)?,
entry_point: <_>::from_reader(reader)?,
};
skip_bytes::<0x1C, _>(reader)?; // padding
Ok(result)
}
}
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub enum PartitionType {
Data,
Update,
Channel,
}
pub(crate) const SECTOR_SIZE: usize = 0x8000;
/// Contains a disc's header & partition information.
pub trait DiscBase: Send + Sync {
@ -95,19 +239,35 @@ pub trait DiscBase: Send + Sync {
///
/// Basic usage:
/// ```no_run
/// use nod::disc::new_disc_base;
/// use nod::io::new_disc_io;
/// use nod::{
/// disc::new_disc_base,
/// io::{new_disc_io, DiscIOOptions},
/// };
///
/// let mut disc_io = new_disc_io("path/to/file".as_ref())?;
/// # fn main() -> nod::Result<()> {
/// let options = DiscIOOptions::default();
/// let mut disc_io = new_disc_io("path/to/file.iso".as_ref(), &options)?;
/// let disc_base = new_disc_base(disc_io.as_mut())?;
/// let mut partition = disc_base.get_data_partition(disc_io.as_mut(), false)?;
/// # Ok::<(), nod::Error>(())
/// # Ok(())
/// # }
/// ```
fn get_data_partition<'a>(
&self,
disc_io: &'a mut dyn DiscIO,
validate_hashes: bool,
) -> Result<Box<dyn PartReadStream + 'a>>;
/// Opens a new partition read stream for the first partition matching
/// the specified type.
///
/// `validate_hashes`: Validate Wii disc hashes while reading (slow!)
fn get_partition<'a>(
&self,
disc_io: &'a mut dyn DiscIO,
part_type: PartitionType,
validate_hashes: bool,
) -> Result<Box<dyn PartReadStream + 'a>>;
}
/// Creates a new [`DiscBase`] instance.
@ -116,23 +276,31 @@ pub trait DiscBase: Send + Sync {
///
/// Basic usage:
/// ```no_run
/// use nod::io::new_disc_io;
/// use nod::disc::new_disc_base;
/// use nod::{
/// disc::new_disc_base,
/// io::{new_disc_io, DiscIOOptions},
/// };
///
/// let mut disc_io = new_disc_io("path/to/file".as_ref())?;
/// # fn main() -> nod::Result<()> {
/// let options = DiscIOOptions::default();
/// let mut disc_io = new_disc_io("path/to/file.iso".as_ref(), &options)?;
/// let disc_base = new_disc_base(disc_io.as_mut())?;
/// disc_base.get_header();
/// # Ok::<(), nod::Error>(())
/// # Ok(())
/// # }
/// ```
pub fn new_disc_base(disc_io: &mut dyn DiscIO) -> Result<Box<dyn DiscBase>> {
let mut stream = disc_io.begin_read_stream(0)?;
let header: Header = stream.read_be()?;
let mut stream = disc_io.begin_read_stream(0).context("Opening disc stream")?;
let header_bytes =
<[u8; Header::STATIC_SIZE]>::from_reader(&mut stream).context("Reading disc header")?;
let header =
Header::from_reader(&mut header_bytes.as_slice()).context("Parsing disc header")?;
if header.wii_magic == 0x5D1C9EA3 {
Result::Ok(Box::from(DiscWii::new(stream.as_mut(), header)?))
Ok(Box::from(DiscWii::new(stream.as_mut(), header)?))
} else if header.gcn_magic == 0xC2339F3D {
Result::Ok(Box::from(DiscGCN::new(header)?))
Ok(Box::from(DiscGCN::new(header)?))
} else {
Result::Err(Error::DiscFormat("Invalid GC/Wii magic".to_string()))
Err(Error::DiscFormat(format!("Invalid GC/Wii magic: {:#010X}", header.wii_magic)))
}
}
@ -145,21 +313,27 @@ pub trait PartReadStream: ReadStream {
///
/// Basic usage:
/// ```no_run
/// use nod::disc::{new_disc_base, PartHeader};
/// use nod::fst::NodeType;
/// use nod::io::new_disc_io;
/// use std::io::Read;
///
/// let mut disc_io = new_disc_io("path/to/file".as_ref())?;
/// let disc_base = new_disc_base(disc_io.as_mut())?;
/// let mut partition = disc_base.get_data_partition(disc_io.as_mut(), false)?;
/// let header = partition.read_header()?;
/// if let Some(NodeType::File(node)) = header.find_node("/MP3/Worlds.txt") {
/// let mut s = String::new();
/// partition.begin_file_stream(node)?.read_to_string(&mut s);
/// println!("{}", s);
/// use nod::{
/// disc::{new_disc_base, PartHeader},
/// fst::NodeType,
/// io::{new_disc_io, DiscIOOptions},
/// };
///
/// fn main() -> nod::Result<()> {
/// let options = DiscIOOptions::default();
/// let mut disc_io = new_disc_io("path/to/file.iso".as_ref(), &options)?;
/// let disc_base = new_disc_base(disc_io.as_mut())?;
/// let mut partition = disc_base.get_data_partition(disc_io.as_mut(), false)?;
/// let header = partition.read_header()?;
/// if let Some(NodeType::File(node)) = header.find_node("/MP3/Worlds.txt") {
/// let mut s = String::new();
/// partition.begin_file_stream(node)?.read_to_string(&mut s).expect("Failed to read file");
/// println!("{}", s);
/// }
/// Ok(())
/// }
/// # Ok::<(), nod::Error>(())
/// ```
fn begin_file_stream(&mut self, node: &Node) -> io::Result<SharedWindowedReadStream>;
@ -183,21 +357,53 @@ pub trait PartHeader: Debug + Send + Sync {
///
/// Basic usage:
/// ```no_run
/// use nod::disc::{new_disc_base, PartHeader};
/// use nod::fst::NodeType;
/// use nod::io::new_disc_io;
/// use nod::{
/// disc::{new_disc_base, PartHeader},
/// fst::NodeType,
/// io::{new_disc_io, DiscIOOptions},
/// };
///
/// let mut disc_io = new_disc_io("path/to/file".as_ref())?;
/// let disc_base = new_disc_base(disc_io.as_mut())?;
/// let mut partition = disc_base.get_data_partition(disc_io.as_mut(), false)?;
/// let header = partition.read_header()?;
/// if let Some(NodeType::File(node)) = header.find_node("/MP1/Metroid1.pak") {
/// println!("{}", node.name);
/// fn main() -> nod::Result<()> {
/// let options = DiscIOOptions::default();
/// let mut disc_io = new_disc_io("path/to/file.iso".as_ref(), &options)?;
/// let disc_base = new_disc_base(disc_io.as_mut())?;
/// let mut partition = disc_base.get_data_partition(disc_io.as_mut(), false)?;
/// let header = partition.read_header()?;
/// if let Some(NodeType::File(node)) = header.find_node("/MP1/Metroid1.pak") {
/// println!("{}", node.name);
/// }
/// if let Some(NodeType::Directory(node, children)) = header.find_node("/MP1") {
/// println!("Number of files: {}", children.len());
/// }
/// Ok(())
/// }
/// if let Some(NodeType::Directory(node, children)) = header.find_node("/MP1") {
/// println!("Number of files: {}", children.len());
/// }
/// # Ok::<(), nod::Error>(())
/// ```
fn find_node(&self, path: &str) -> Option<&NodeType>;
/// Disc and partition header (boot.bin)
fn boot_bytes(&self) -> &[u8];
/// Debug and region information (bi2.bin)
fn bi2_bytes(&self) -> &[u8];
/// Apploader (apploader.bin)
fn apploader_bytes(&self) -> &[u8];
/// File system table (fst.bin)
fn fst_bytes(&self) -> &[u8];
/// Main binary (main.dol)
fn dol_bytes(&self) -> &[u8];
/// Disc header
fn disc_header(&self) -> &Header;
/// Partition header
fn partition_header(&self) -> &PartitionHeader;
/// Apploader header
fn apploader_header(&self) -> &AppLoaderHeader;
/// DOL header
fn dol_header(&self) -> &DolHeader;
}

View File

@ -3,24 +3,33 @@ use std::{
io::{Read, Seek, SeekFrom},
};
use aes::{Aes128, Block, NewBlockCipher};
use binrw::{BinRead, BinReaderExt};
use block_modes::{block_padding::NoPadding, BlockMode, Cbc};
use aes::{
cipher::{block_padding::NoPadding, BlockDecryptMut, KeyIvInit},
Aes128, Block,
};
use sha1::{digest, Digest, Sha1};
use crate::{
array_ref,
disc::{BI2Header, DiscBase, DiscIO, Header, PartHeader, PartReadStream, BUFFER_SIZE},
div_rem,
fst::{find_node, node_parser, Node, NodeKind, NodeType},
disc::{
AppLoaderHeader, DiscBase, DiscIO, DolHeader, Header, PartHeader, PartReadStream,
PartitionHeader, PartitionType, SECTOR_SIZE,
},
fst::{find_node, Node, NodeKind, NodeType},
streams::{wrap_windowed, OwningWindowedReadStream, ReadStream, SharedWindowedReadStream},
Error, Result,
util::{
div_rem,
reader::{skip_bytes, struct_size, FromReader},
},
Error, Result, ResultContext,
};
type Aes128Cbc = Cbc<Aes128, NoPadding>;
pub(crate) const HASHES_SIZE: usize = 0x400;
pub(crate) const BLOCK_SIZE: usize = SECTOR_SIZE - HASHES_SIZE; // 0x7C00
/// AES-128-CBC decryptor
type Aes128Cbc = cbc::Decryptor<Aes128>;
const BLOCK_SIZE: usize = 0x7c00;
const BUFFER_OFFSET: usize = BUFFER_SIZE - BLOCK_SIZE;
#[rustfmt::skip]
const COMMON_KEYS: [[u8; 16]; 2] = [
/* Normal */
@ -29,82 +38,186 @@ const COMMON_KEYS: [[u8; 16]; 2] = [
[0x63, 0xb8, 0x2b, 0xb4, 0xf4, 0x61, 0x4e, 0x2e, 0x13, 0xf2, 0xfe, 0xfb, 0xba, 0x4c, 0x9b, 0x7e],
];
#[derive(Debug, PartialEq, BinRead)]
#[br(repr = u32)]
enum WiiPartType {
Data,
Update,
Channel,
}
#[derive(Debug, PartialEq, BinRead)]
#[br(repr = u32)]
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
enum SigType {
Rsa4096 = 0x00010000,
Rsa2048 = 0x00010001,
EllipticalCurve = 0x00010002,
Rsa4096,
Rsa2048,
EllipticalCurve,
}
#[derive(Debug, PartialEq, BinRead)]
#[br(repr = u32)]
impl FromReader for SigType {
type Args<'a> = ();
const STATIC_SIZE: usize = u32::STATIC_SIZE;
fn from_reader_args<R>(reader: &mut R, _args: Self::Args<'_>) -> io::Result<Self>
where R: Read + ?Sized {
match u32::from_reader(reader)? {
0x00010000 => Ok(SigType::Rsa4096),
0x00010001 => Ok(SigType::Rsa2048),
0x00010002 => Ok(SigType::EllipticalCurve),
_ => Err(io::Error::new(io::ErrorKind::InvalidData, "invalid signature type")),
}
}
}
impl SigType {
fn size(self) -> usize {
match self {
SigType::Rsa4096 => 512,
SigType::Rsa2048 => 256,
SigType::EllipticalCurve => 64,
}
}
}
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
enum KeyType {
Rsa4096 = 0x00000000,
Rsa2048 = 0x00000001,
Rsa4096,
Rsa2048,
}
#[derive(Debug, PartialEq, BinRead)]
impl FromReader for KeyType {
type Args<'a> = ();
const STATIC_SIZE: usize = u32::STATIC_SIZE;
fn from_reader_args<R>(reader: &mut R, _args: Self::Args<'_>) -> io::Result<Self>
where R: Read + ?Sized {
match u32::from_reader(reader)? {
0x00000000 => Ok(KeyType::Rsa4096),
0x00000001 => Ok(KeyType::Rsa2048),
_ => Err(io::Error::new(io::ErrorKind::InvalidData, "invalid key type")),
}
}
}
impl KeyType {
fn size(self) -> usize {
match self {
KeyType::Rsa4096 => 512,
KeyType::Rsa2048 => 256,
}
}
}
#[derive(Debug, PartialEq)]
struct WiiPart {
#[br(map = | x: u32 | (x as u64) << 2)]
// #[br(map = |x: u32| (x as u64) << 2)]
part_data_off: u64,
part_type: WiiPartType,
#[br(restore_position, args(part_data_off))]
part_type: PartitionType,
// #[br(restore_position, args(part_data_off))]
part_header: WiiPartitionHeader,
}
#[derive(Debug, PartialEq, BinRead)]
#[derive(Debug, PartialEq)]
struct WiiPartInfo {
#[br(seek_before = SeekFrom::Start(0x40000))]
// #[br(seek_before = SeekFrom::Start(0x40000))]
part_count: u32,
#[br(map = | x: u32 | (x as u64) << 2)]
// #[br(map = |x: u32| (x as u64) << 2)]
part_info_off: u64,
#[br(seek_before = SeekFrom::Start(part_info_off), count = part_count)]
// #[br(seek_before = SeekFrom::Start(part_info_off), count = part_count)]
parts: Vec<WiiPart>,
}
#[derive(Debug, PartialEq, BinRead)]
#[derive(Debug, PartialEq, Default)]
struct TicketTimeLimit {
enable_time_limit: u32,
time_limit: u32,
}
#[derive(Debug, PartialEq, BinRead)]
impl FromReader for TicketTimeLimit {
type Args<'a> = ();
const STATIC_SIZE: usize = struct_size([
u32::STATIC_SIZE, // enable_time_limit
u32::STATIC_SIZE, // time_limit
]);
fn from_reader_args<R>(reader: &mut R, _args: Self::Args<'_>) -> io::Result<Self>
where R: Read + ?Sized {
let enable_time_limit = u32::from_reader(reader)?;
let time_limit = u32::from_reader(reader)?;
Ok(TicketTimeLimit { enable_time_limit, time_limit })
}
}
#[derive(Debug, PartialEq)]
struct Ticket {
sig_type: SigType,
#[br(count = 256)]
sig: Vec<u8>,
#[br(pad_before = 60, count = 64)]
sig_issuer: Vec<u8>,
#[br(count = 60)]
ecdh: Vec<u8>,
#[br(pad_before = 3)]
sig: [u8; 256],
sig_issuer: [u8; 64],
ecdh: [u8; 60],
enc_key: [u8; 16],
#[br(pad_before = 1)]
ticket_id: [u8; 8],
console_id: [u8; 4],
title_id: [u8; 8],
#[br(pad_before = 2)]
ticket_version: u16,
permitted_titles_mask: u32,
permit_mask: u32,
title_export_allowed: u8,
common_key_idx: u8,
#[br(pad_before = 48, count = 64)]
content_access_permissions: Vec<u8>,
#[br(pad_before = 2, count = 8)]
time_limits: Vec<TicketTimeLimit>,
content_access_permissions: [u8; 64],
time_limits: [TicketTimeLimit; 8],
}
#[derive(Debug, PartialEq, BinRead)]
impl FromReader for Ticket {
type Args<'a> = ();
const STATIC_SIZE: usize = 0x2A4;
fn from_reader_args<R>(reader: &mut R, _args: Self::Args<'_>) -> io::Result<Self>
where R: Read + ?Sized {
let sig_type = SigType::from_reader(reader)?;
let sig = <[u8; 256]>::from_reader(reader)?;
skip_bytes::<0x3C, _>(reader)?;
let sig_issuer = <[u8; 64]>::from_reader(reader)?;
let ecdh = <[u8; 60]>::from_reader(reader)?;
skip_bytes::<3, _>(reader)?;
let enc_key = <[u8; 16]>::from_reader(reader)?;
skip_bytes::<1, _>(reader)?;
let ticket_id = <[u8; 8]>::from_reader(reader)?;
let console_id = <[u8; 4]>::from_reader(reader)?;
let title_id = <[u8; 8]>::from_reader(reader)?;
skip_bytes::<2, _>(reader)?;
let ticket_version = u16::from_reader(reader)?;
let permitted_titles_mask = u32::from_reader(reader)?;
let permit_mask = u32::from_reader(reader)?;
let title_export_allowed = u8::from_reader(reader)?;
let common_key_idx = u8::from_reader(reader)?;
skip_bytes::<48, _>(reader)?;
let content_access_permissions = <[u8; 64]>::from_reader(reader)?;
let time_limits = [
TicketTimeLimit::from_reader(reader)?,
TicketTimeLimit::from_reader(reader)?,
TicketTimeLimit::from_reader(reader)?,
TicketTimeLimit::from_reader(reader)?,
TicketTimeLimit::from_reader(reader)?,
TicketTimeLimit::from_reader(reader)?,
TicketTimeLimit::from_reader(reader)?,
TicketTimeLimit::from_reader(reader)?,
];
Ok(Ticket {
sig_type,
sig,
sig_issuer,
ecdh,
enc_key,
ticket_id,
console_id,
title_id,
ticket_version,
permitted_titles_mask,
permit_mask,
title_export_allowed,
common_key_idx,
content_access_permissions,
time_limits,
})
}
}
#[derive(Debug, PartialEq)]
struct TmdContent {
id: u32,
index: u16,
@ -113,78 +226,89 @@ struct TmdContent {
hash: [u8; 20],
}
#[derive(Debug, PartialEq, BinRead)]
#[derive(Debug, PartialEq)]
struct Tmd {
sig_type: SigType,
#[br(count = 256)]
// #[br(count = 256)]
sig: Vec<u8>,
#[br(pad_before = 60, count = 64)]
// #[br(pad_before = 60, count = 64)]
sig_issuer: Vec<u8>,
version: u8,
ca_crl_version: u8,
signer_crl_version: u8,
#[br(pad_before = 1)]
// #[br(pad_before = 1)]
ios_id_major: u32,
ios_id_minor: u32,
title_id_major: u32,
title_id_minor: [char; 4],
title_id_minor: [u8; 4],
title_type: u32,
group_id: u16,
#[br(pad_before = 62)]
// #[br(pad_before = 62)]
access_flags: u32,
title_version: u16,
num_contents: u16,
#[br(pad_after = 2)]
// #[br(pad_after = 2)]
boot_idx: u16,
#[br(count = num_contents)]
// #[br(count = num_contents)]
contents: Vec<TmdContent>,
}
#[derive(Debug, PartialEq, BinRead)]
#[derive(Debug, PartialEq)]
struct Certificate {
sig_type: SigType,
#[br(count = if sig_type == SigType::Rsa4096 { 512 } else if sig_type == SigType::Rsa2048 { 256 } else if sig_type == SigType::EllipticalCurve { 64 } else { 0 })]
// #[br(count = sig_size(sig_type))]
sig: Vec<u8>,
#[br(pad_before = 60, count = 64)]
// #[br(pad_before = 60, count = 64)]
issuer: Vec<u8>,
key_type: KeyType,
#[br(count = 64)]
// #[br(count = 64)]
subject: Vec<u8>,
#[br(count = if key_type == KeyType::Rsa4096 { 512 } else if key_type == KeyType::Rsa2048 { 256 } else { 0 })]
// #[br(count = key_size(key_type))]
key: Vec<u8>,
modulus: u32,
#[br(pad_after = 52)]
// #[br(pad_after = 52)]
pub_exp: u32,
}
#[derive(Debug, PartialEq, BinRead)]
#[br(import(partition_off: u64))]
#[derive(Debug, PartialEq)]
// #[br(import(partition_off: u64))]
struct WiiPartitionHeader {
#[br(seek_before = SeekFrom::Start(partition_off))]
// #[br(seek_before = SeekFrom::Start(partition_off))]
ticket: Ticket,
tmd_size: u32,
#[br(map = | x: u32 | ((x as u64) << 2) + partition_off)]
// #[br(map = |x: u32| ((x as u64) << 2) + partition_off)]
tmd_off: u64,
cert_chain_size: u32,
#[br(map = | x: u32 | ((x as u64) << 2) + partition_off)]
// #[br(map = |x: u32| ((x as u64) << 2) + partition_off)]
cert_chain_off: u64,
#[br(map = | x: u32 | ((x as u64) << 2) + partition_off)]
// #[br(map = |x: u32| ((x as u64) << 2) + partition_off)]
global_hash_table_off: u64,
#[br(map = | x: u32 | ((x as u64) << 2) + partition_off)]
// #[br(map = |x: u32| ((x as u64) << 2) + partition_off)]
data_off: u64,
#[br(map = | x: u32 | (x as u64) << 2)]
// #[br(map = |x: u32| (x as u64) << 2)]
data_size: u64,
#[br(seek_before = SeekFrom::Start(tmd_off))]
// #[br(seek_before = SeekFrom::Start(tmd_off))]
tmd: Tmd,
#[br(seek_before = SeekFrom::Start(cert_chain_off))]
// #[br(seek_before = SeekFrom::Start(cert_chain_off))]
ca_cert: Certificate,
tmd_cert: Certificate,
ticket_cert: Certificate,
#[br(seek_before = SeekFrom::Start(global_hash_table_off), count = 0x18000)]
// #[br(seek_before = SeekFrom::Start(global_hash_table_off), count = 0x18000)]
h3_data: Vec<u8>,
}
impl FromReader for WiiPartitionHeader {
type Args<'a> = u64;
const STATIC_SIZE: usize = Ticket::STATIC_SIZE;
fn from_reader_args<R>(reader: &mut R, args: Self::Args<'_>) -> io::Result<Self>
where R: Read + ?Sized {
todo!()
}
}
pub(crate) struct DiscWii {
header: Header,
part_info: WiiPartInfo,
@ -192,9 +316,9 @@ pub(crate) struct DiscWii {
impl DiscWii {
pub(crate) fn new(mut stream: &mut dyn ReadStream, header: Header) -> Result<DiscWii> {
let mut disc = DiscWii { header, part_info: stream.read_be()? };
let mut disc = DiscWii { header, part_info: todo!() }; // stream.read_be()?
disc.decrypt_partition_keys()?;
Result::Ok(disc)
Ok(disc)
}
}
@ -204,13 +328,10 @@ impl DiscWii {
let ticket = &mut part.part_header.ticket;
let mut iv: [u8; 16] = [0; 16];
iv[..8].copy_from_slice(&ticket.title_id);
Aes128Cbc::new(
Aes128::new(&COMMON_KEYS[ticket.common_key_idx as usize].into()),
&iv.into(),
)
.decrypt(&mut ticket.enc_key)?;
Aes128Cbc::new(&COMMON_KEYS[ticket.common_key_idx as usize].into(), &iv.into())
.decrypt_padded_mut::<NoPadding>(&mut ticket.enc_key)?;
}
Result::Ok(())
Ok(())
}
}
@ -226,48 +347,74 @@ impl DiscBase for DiscWii {
.part_info
.parts
.iter()
.find(|v| v.part_type == WiiPartType::Data)
.find(|v| v.part_type == PartitionType::Data)
.ok_or_else(|| Error::DiscFormat("Failed to locate data partition".to_string()))?;
let data_off = part.part_header.data_off;
let has_crypto = disc_io.has_wii_crypto();
let base = disc_io
.begin_read_stream(data_off)
.map_err(|e| Error::Io("Opening data partition stream".to_string(), e))?;
let stream = wrap_windowed(base, data_off, part.part_header.data_size)
.context("Wrapping data partition stream")?;
let result = Box::new(WiiPartReadStream {
stream: wrap_windowed(
disc_io.begin_read_stream(data_off)?,
data_off,
part.part_header.data_size,
)?,
crypto: if has_crypto {
Aes128::new(&part.part_header.ticket.enc_key.into()).into()
} else {
Option::None
},
stream,
crypto: if has_crypto { Some(part.part_header.ticket.enc_key) } else { None },
offset: 0,
cur_block: u64::MAX,
cur_block: u32::MAX,
buf: [0; 0x8000],
validate_hashes,
});
Result::Ok(result)
Ok(result)
}
fn get_partition<'a>(
&self,
disc_io: &'a mut dyn DiscIO,
part_type: PartitionType,
validate_hashes: bool,
) -> Result<Box<dyn PartReadStream + 'a>> {
let part =
self.part_info.parts.iter().find(|v| v.part_type == part_type).ok_or_else(|| {
Error::DiscFormat(format!("Failed to locate {:?} partition", part_type))
})?;
let data_off = part.part_header.data_off;
let has_crypto = disc_io.has_wii_crypto();
let base = disc_io
.begin_read_stream(data_off)
.with_context(|| format!("Opening {:?} partition stream", part_type))?;
let stream = wrap_windowed(base, data_off, part.part_header.data_size)
.with_context(|| format!("Wrapping {:?} partition stream", part_type))?;
let result = Box::new(WiiPartReadStream {
stream,
crypto: if has_crypto { Some(part.part_header.ticket.enc_key) } else { None },
offset: 0,
cur_block: u32::MAX,
buf: [0; 0x8000],
validate_hashes,
});
Ok(result)
}
}
struct WiiPartReadStream<'a> {
stream: OwningWindowedReadStream<'a>,
crypto: Option<Aes128>,
crypto: Option<[u8; 16]>,
offset: u64,
cur_block: u64,
buf: [u8; BUFFER_SIZE],
cur_block: u32,
buf: [u8; SECTOR_SIZE],
validate_hashes: bool,
}
impl<'a> PartReadStream for WiiPartReadStream<'a> {
fn begin_file_stream(&mut self, node: &Node) -> io::Result<SharedWindowedReadStream> {
assert_eq!(node.kind, NodeKind::File);
io::Result::Ok(self.new_window((node.offset as u64) << 2, node.length as u64)?)
self.new_window((node.offset as u64) << 2, node.length as u64)
}
fn read_header(&mut self) -> Result<Box<dyn PartHeader>> {
self.seek(SeekFrom::Start(0))?;
Result::Ok(Box::from(self.read_be::<WiiPartition>()?))
self.seek(SeekFrom::Start(0)).context("Seeking to partition header")?;
todo!()
// Ok(Box::from(self.read_be::<WiiPartition>()?))
}
fn ideal_buffer_size(&self) -> usize { BLOCK_SIZE }
@ -276,25 +423,24 @@ impl<'a> PartReadStream for WiiPartReadStream<'a> {
#[inline(always)]
fn as_digest(slice: &[u8; 20]) -> digest::Output<Sha1> { (*slice).into() }
fn decrypt_block(part: &mut WiiPartReadStream, cluster: usize) -> io::Result<()> {
fn decrypt_block(part: &mut WiiPartReadStream, cluster: u32) -> io::Result<()> {
part.stream.read_exact(&mut part.buf)?;
if part.crypto.is_some() {
if let Some(key) = &part.crypto {
// Fetch IV before decrypting header
let iv = Block::from(*array_ref![part.buf, 0x3d0, 16]);
let iv_bytes = array_ref![part.buf, 0x3d0, 16];
let iv = Block::from(*iv_bytes);
// Don't need to decrypt header if we're not validating hashes
if part.validate_hashes {
Aes128Cbc::new(part.crypto.as_ref().unwrap().clone(), &Block::from([0; 16]))
.decrypt(&mut part.buf[..BUFFER_OFFSET])
Aes128Cbc::new(key.into(), &Block::from([0; 16]))
.decrypt_padded_mut::<NoPadding>(&mut part.buf[..HASHES_SIZE])
.expect("Failed to decrypt header");
}
Aes128Cbc::new(part.crypto.as_ref().unwrap().clone(), &iv)
.decrypt(&mut part.buf[BUFFER_OFFSET..])
Aes128Cbc::new(key.into(), &iv)
.decrypt_padded_mut::<NoPadding>(&mut part.buf[HASHES_SIZE..])
.expect("Failed to decrypt block");
}
if part.validate_hashes && part.crypto.is_some()
/* FIXME NFS validation? */
{
let (mut group, sub_group) = div_rem(cluster, 8);
if part.validate_hashes {
let (mut group, sub_group) = div_rem(cluster as usize, 8);
group %= 8;
// H0 hashes
for i in 0..31 {
@ -303,12 +449,7 @@ fn decrypt_block(part: &mut WiiPartReadStream, cluster: usize) -> io::Result<()>
let expected = as_digest(array_ref![part.buf, i * 20, 20]);
let output = hash.finalize();
if output != expected {
panic!(
"Invalid hash! (block {:?}) {:?}\n\texpected {:?}",
i,
output.as_slice(),
expected
);
panic!("Invalid H0 hash! (block {:?}) {:x}\n\texpected {:x}", i, output, expected);
}
}
// H1 hash
@ -319,10 +460,8 @@ fn decrypt_block(part: &mut WiiPartReadStream, cluster: usize) -> io::Result<()>
let output = hash.finalize();
if output != expected {
panic!(
"Invalid hash! (subgroup {:?}) {:?}\n\texpected {:?}",
sub_group,
output.as_slice(),
expected
"Invalid H1 hash! (subgroup {:?}) {:x}\n\texpected {:x}",
sub_group, output, expected
);
}
}
@ -334,27 +473,28 @@ fn decrypt_block(part: &mut WiiPartReadStream, cluster: usize) -> io::Result<()>
let output = hash.finalize();
if output != expected {
panic!(
"Invalid hash! (group {:?}) {:?}\n\texpected {:?}",
group,
output.as_slice(),
expected
"Invalid H2 hash! (group {:?}) {:x}\n\texpected {:x}",
group, output, expected
);
}
}
}
io::Result::Ok(())
Ok(())
}
impl<'a> Read for WiiPartReadStream<'a> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let (mut block, mut block_offset) = div_rem(self.offset as usize, BLOCK_SIZE);
let (block, block_offset) = div_rem(self.offset, BLOCK_SIZE as u64);
let mut block = block as u32;
let mut block_offset = block_offset as usize;
let mut rem = buf.len();
let mut read: usize = 0;
while rem > 0 {
if block != self.cur_block as usize {
if block != self.cur_block {
decrypt_block(self, block)?;
self.cur_block = block as u64;
self.cur_block = block;
}
let mut cache_size = rem;
@ -363,7 +503,7 @@ impl<'a> Read for WiiPartReadStream<'a> {
}
buf[read..read + cache_size].copy_from_slice(
&self.buf[BUFFER_OFFSET + block_offset..BUFFER_OFFSET + block_offset + cache_size],
&self.buf[HASHES_SIZE + block_offset..HASHES_SIZE + block_offset + cache_size],
);
read += cache_size;
rem -= cache_size;
@ -372,13 +512,13 @@ impl<'a> Read for WiiPartReadStream<'a> {
}
self.offset += buf.len() as u64;
io::Result::Ok(buf.len())
Ok(buf.len())
}
}
#[inline(always)]
fn to_block_size(v: u64) -> u64 {
(v / BUFFER_SIZE as u64) * BLOCK_SIZE as u64 + (v % BUFFER_SIZE as u64)
(v / SECTOR_SIZE as u64) * BLOCK_SIZE as u64 + (v % SECTOR_SIZE as u64)
}
impl<'a> Seek for WiiPartReadStream<'a> {
@ -388,31 +528,33 @@ impl<'a> Seek for WiiPartReadStream<'a> {
SeekFrom::End(v) => (self.stable_stream_len()? as i64 + v) as u64,
SeekFrom::Current(v) => (self.offset as i64 + v) as u64,
};
let block = self.offset / BLOCK_SIZE as u64;
let block = (self.offset / BLOCK_SIZE as u64) as u32;
if block != self.cur_block {
self.stream.seek(SeekFrom::Start(block * BUFFER_SIZE as u64))?;
self.cur_block = u64::MAX;
self.stream.seek(SeekFrom::Start(block as u64 * SECTOR_SIZE as u64))?;
self.cur_block = u32::MAX;
}
io::Result::Ok(self.offset)
Ok(self.offset)
}
fn stream_position(&mut self) -> io::Result<u64> { io::Result::Ok(self.offset) }
fn stream_position(&mut self) -> io::Result<u64> { Ok(self.offset) }
}
impl<'a> ReadStream for WiiPartReadStream<'a> {
fn stable_stream_len(&mut self) -> io::Result<u64> {
io::Result::Ok(to_block_size(self.stream.stable_stream_len()?))
Ok(to_block_size(self.stream.stable_stream_len()?))
}
fn as_dyn(&mut self) -> &mut dyn ReadStream { self }
}
#[derive(Clone, Debug, PartialEq, BinRead)]
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct WiiPartition {
header: Header,
bi2_header: BI2Header,
#[br(seek_before = SeekFrom::Start((header.fst_off as u64) << 2))]
#[br(parse_with = node_parser)]
// #[br(seek_before = SeekFrom::Start(0x400))]
part_header: PartitionHeader,
// bi2_header: BI2Header,
// #[br(seek_before = SeekFrom::Start((part_header.fst_off as u64) << 2))]
// #[br(parse_with = node_parser)]
root_node: NodeType,
}
@ -420,4 +562,22 @@ impl PartHeader for WiiPartition {
fn root_node(&self) -> &NodeType { &self.root_node }
fn find_node(&self, path: &str) -> Option<&NodeType> { find_node(&self.root_node, path) }
fn boot_bytes(&self) -> &[u8] { todo!() }
fn bi2_bytes(&self) -> &[u8] { todo!() }
fn apploader_bytes(&self) -> &[u8] { todo!() }
fn fst_bytes(&self) -> &[u8] { todo!() }
fn dol_bytes(&self) -> &[u8] { todo!() }
fn disc_header(&self) -> &Header { todo!() }
fn partition_header(&self) -> &PartitionHeader { todo!() }
fn apploader_header(&self) -> &AppLoaderHeader { todo!() }
fn dol_header(&self) -> &DolHeader { todo!() }
}

View File

@ -1,10 +1,18 @@
//! Disc file system types
use std::io::{Read, Seek, SeekFrom};
use std::{
ffi::CString,
io,
io::{Read, Seek, SeekFrom},
};
use binrw::{binread, BinReaderExt, BinResult, NullString, ReadOptions};
use encoding_rs::SHIFT_JIS;
use crate::{
util::reader::{struct_size, FromReader, DYNAMIC_SIZE, U24},
Result, ResultContext,
};
/// File system node kind.
#[derive(Clone, Debug, PartialEq)]
pub enum NodeKind {
@ -14,17 +22,30 @@ pub enum NodeKind {
Directory,
}
impl FromReader for NodeKind {
type Args<'a> = ();
const STATIC_SIZE: usize = 1;
fn from_reader_args<R>(_reader: &mut R, _args: Self::Args<'_>) -> io::Result<Self>
where R: Read + ?Sized {
match u8::from_reader(_reader)? {
0 => Ok(NodeKind::File),
1 => Ok(NodeKind::Directory),
_ => Err(io::Error::new(io::ErrorKind::InvalidData, "invalid node kind")),
}
}
}
/// An individual file system node.
#[binread]
#[derive(Clone, Debug, PartialEq)]
pub struct Node {
#[br(temp)]
type_and_name_offset: u32,
/// File system node type.
#[br(calc = if (type_and_name_offset >> 24) != 0 { NodeKind::Directory } else { NodeKind::File })]
pub kind: NodeKind,
/// Offset in the string table to the filename.
pub name_offset: u32,
/// For files, this is the partition offset of the file data. (Wii: >> 2)
///
/// For directories, this is the children start offset in the FST.
@ -37,11 +58,28 @@ pub struct Node {
/// Number of child files and directories recursively is `length - offset`.
pub length: u32,
#[br(calc = type_and_name_offset & 0xffffff)]
name_offset: u32,
#[br(ignore)]
/// The node name.
pub name: Box<str>,
pub name: String,
}
impl FromReader for Node {
type Args<'a> = ();
const STATIC_SIZE: usize = struct_size([
NodeKind::STATIC_SIZE, // type
U24::STATIC_SIZE, // name_offset
u32::STATIC_SIZE, // offset
u32::STATIC_SIZE, // length
]);
fn from_reader_args<R>(reader: &mut R, _args: Self::Args<'_>) -> io::Result<Self>
where R: Read + ?Sized {
let kind = NodeKind::from_reader(reader)?;
let name_offset = U24::from_reader(reader)?.0;
let offset = u32::from_reader(reader)?;
let length = u32::from_reader(reader)?;
Ok(Node { kind, offset, length, name_offset, name: Default::default() })
}
}
/// Contains a file system node, and if a directory, its children.
@ -53,74 +91,77 @@ pub enum NodeType {
Directory(Node, Vec<NodeType>),
}
fn read_node<R: Read + Seek>(reader: &mut R, ro: &ReadOptions, i: &mut u32) -> BinResult<NodeType> {
let node = reader.read_type::<Node>(ro.endian())?;
*i += 1;
BinResult::Ok(if node.kind == NodeKind::Directory {
let mut children: Vec<NodeType> = Vec::new();
children.reserve((node.length - *i) as usize);
while *i < node.length {
children.push(read_node(reader, ro, i)?);
}
NodeType::Directory(node, children)
} else {
NodeType::File(node)
})
impl FromReader for NodeType {
type Args<'a> = &'a mut u32;
const STATIC_SIZE: usize = DYNAMIC_SIZE;
fn from_reader_args<R>(reader: &mut R, idx: &mut u32) -> io::Result<Self>
where R: Read + ?Sized {
let node = Node::from_reader(reader)?;
*idx += 1;
Ok(if node.kind == NodeKind::Directory {
let mut children = Vec::with_capacity((node.length - *idx) as usize);
while *idx < node.length {
children.push(NodeType::from_reader_args(reader, idx)?);
}
NodeType::Directory(node, children)
} else {
NodeType::File(node)
})
}
}
fn read_node_name<R: Read + Seek>(
fn read_node_name<R>(
reader: &mut R,
ro: &ReadOptions,
base: u64,
string_base: u64,
node: &mut NodeType,
root: bool,
) -> BinResult<()> {
let mut decode_name = |v: &mut Node| -> BinResult<()> {
) -> io::Result<()>
where
R: Read + Seek + ?Sized,
{
let mut decode_name = |v: &mut Node| -> io::Result<()> {
if !root {
let offset = base + v.name_offset as u64;
let offset = string_base + v.name_offset as u64;
reader.seek(SeekFrom::Start(offset))?;
let null_string = reader.read_type::<NullString>(ro.endian())?;
let (res, _, errors) = SHIFT_JIS.decode(&*null_string.0);
let c_string = CString::from_reader(reader)?;
let (decoded, _, errors) = SHIFT_JIS.decode(c_string.as_bytes());
if errors {
return BinResult::Err(binrw::Error::Custom {
pos: offset,
err: Box::new("Failed to decode node name"),
});
return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid shift-jis"));
}
v.name = res.into();
v.name = decoded.into_owned();
}
BinResult::Ok(())
Ok(())
};
match node {
NodeType::File(v) => {
decode_name(v)?;
NodeType::File(inner) => {
decode_name(inner)?;
}
NodeType::Directory(v, c) => {
decode_name(v)?;
for x in c {
read_node_name(reader, ro, base, x, false)?;
NodeType::Directory(inner, children) => {
decode_name(inner)?;
for child in children {
read_node_name(reader, string_base, child, false)?;
}
}
}
BinResult::Ok(())
Ok(())
}
pub(crate) fn node_parser<R: Read + Seek>(
reader: &mut R,
ro: &ReadOptions,
_: (),
) -> BinResult<NodeType> {
let mut node = read_node(reader, ro, &mut 0)?;
let base = reader.stream_position()?;
read_node_name(reader, ro, base, &mut node, true)?;
BinResult::Ok(node)
pub(crate) fn read_fst<R>(reader: &mut R) -> Result<NodeType>
where R: Read + Seek + ?Sized {
let mut node = NodeType::from_reader_args(reader, &mut 0).context("Parsing FST nodes")?;
let string_base = reader.stream_position().context("Reading FST end position")?;
read_node_name(reader, string_base, &mut node, true).context("Reading FST node names")?;
Ok(node)
}
fn matches_name(node: &NodeType, name: &str) -> bool {
match node {
NodeType::File(v) => v.name.as_ref().eq_ignore_ascii_case(name),
NodeType::File(v) => v.name.as_str().eq_ignore_ascii_case(name),
NodeType::Directory(v, _) => {
v.name.is_empty() /* root */ || v.name.as_ref().eq_ignore_ascii_case(name)
v.name.is_empty() /* root */ || v.name.as_str().eq_ignore_ascii_case(name)
}
}
}
@ -132,7 +173,7 @@ pub(crate) fn find_node<'a>(mut node: &'a NodeType, path: &str) -> Option<&'a No
if matches_name(node, current.unwrap()) {
match node {
NodeType::File(_) => {
return if split.next().is_none() { Option::Some(node) } else { Option::None };
return if split.next().is_none() { Some(node) } else { None };
}
NodeType::Directory(v, c) => {
// Find child
@ -140,11 +181,7 @@ pub(crate) fn find_node<'a>(mut node: &'a NodeType, path: &str) -> Option<&'a No
current = split.next();
}
if current.is_none() || current.unwrap().is_empty() {
return if split.next().is_none() {
Option::Some(node)
} else {
Option::None
};
return if split.next().is_none() { Some(node) } else { None };
}
for x in c {
if matches_name(x, current.unwrap()) {
@ -158,5 +195,5 @@ pub(crate) fn find_node<'a>(mut node: &'a NodeType, path: &str) -> Option<&'a No
break;
}
}
Option::None
None
}

View File

@ -13,7 +13,7 @@ pub(crate) struct DiscIOISO {
impl DiscIOISO {
pub(crate) fn new(filename: &Path) -> Result<DiscIOISO> {
Result::Ok(DiscIOISO { filename: filename.to_owned() })
Ok(DiscIOISO { filename: filename.to_owned() })
}
}
@ -21,21 +21,25 @@ impl DiscIO for DiscIOISO {
fn begin_read_stream(&mut self, offset: u64) -> io::Result<Box<dyn ReadStream>> {
let mut file = File::open(&*self.filename)?;
file.seek(SeekFrom::Start(offset))?;
io::Result::Ok(Box::from(file))
Ok(Box::from(file))
}
}
pub(crate) struct DiscIOISOStream<T: ReadStream + Sized> {
pub(crate) struct DiscIOISOStream<T>
where T: ReadStream + Sized
{
pub(crate) stream: T,
}
impl<T: ReadStream + Sized> DiscIOISOStream<T> {
pub(crate) fn new(stream: T) -> Result<DiscIOISOStream<T>> {
Result::Ok(DiscIOISOStream { stream })
}
impl<T> DiscIOISOStream<T>
where T: ReadStream + Sized
{
pub(crate) fn new(stream: T) -> Result<DiscIOISOStream<T>> { Ok(DiscIOISOStream { stream }) }
}
impl<T: ReadStream + Sized + Send + Sync> DiscIO for DiscIOISOStream<T> {
impl<T> DiscIO for DiscIOISOStream<T>
where T: ReadStream + Sized + Send + Sync
{
fn begin_read_stream<'a>(&'a mut self, offset: u64) -> io::Result<Box<dyn ReadStream + 'a>> {
let size = self.stream.stable_stream_len()?;
let mut stream = self.stream.new_window(0, size)?;

View File

@ -6,6 +6,7 @@ use crate::{
io::{
iso::{DiscIOISO, DiscIOISOStream},
nfs::DiscIONFS,
wia::DiscIOWIA,
},
streams::{ByteReadStream, ReadStream},
Error, Result,
@ -13,6 +14,13 @@ use crate::{
pub(crate) mod iso;
pub(crate) mod nfs;
pub(crate) mod wia;
#[derive(Default, Debug, Clone)]
pub struct DiscIOOptions {
/// Rebuild hashes for the disc image.
pub rebuild_hashes: bool,
}
/// Abstraction over supported disc file types.
pub trait DiscIO: Send + Sync {
@ -30,43 +38,40 @@ pub trait DiscIO: Send + Sync {
///
/// Basic usage:
/// ```no_run
/// use nod::io::new_disc_io;
/// use nod::io::{new_disc_io, DiscIOOptions};
///
/// let mut disc_io = new_disc_io("path/to/file".as_ref())?;
/// # Ok::<(), nod::Error>(())
/// # fn main() -> nod::Result<()> {
/// let options = DiscIOOptions::default();
/// let mut disc_io = new_disc_io("path/to/file.iso".as_ref(), &options)?;
/// # Ok(())
/// # }
/// ```
pub fn new_disc_io(filename: &Path) -> Result<Box<dyn DiscIO>> {
pub fn new_disc_io(filename: &Path, options: &DiscIOOptions) -> Result<Box<dyn DiscIO>> {
let path_result = fs::canonicalize(filename);
if let Err(err) = path_result {
return Result::Err(Error::Io(
format!("Failed to open {}", filename.to_string_lossy()),
err,
));
return Err(Error::Io(format!("Failed to open {}", filename.display()), err));
}
let path = path_result.as_ref().unwrap();
let meta = fs::metadata(path);
if let Err(err) = meta {
return Result::Err(Error::Io(
format!("Failed to open {}", filename.to_string_lossy()),
err,
));
return Err(Error::Io(format!("Failed to open {}", filename.display()), err));
}
if !meta.unwrap().is_file() {
return Result::Err(Error::DiscFormat(format!(
"Input is not a file: {}",
filename.to_string_lossy()
)));
return Err(Error::DiscFormat(format!("Input is not a file: {}", filename.display())));
}
if has_extension(path, "iso") {
Result::Ok(Box::from(DiscIOISO::new(path)?))
Ok(Box::from(DiscIOISO::new(path)?))
} else if has_extension(path, "nfs") {
if matches!(path.parent(), Some(parent) if parent.is_dir()) {
Result::Ok(Box::from(DiscIONFS::new(path.parent().unwrap())?))
} else {
Result::Err(Error::DiscFormat("Failed to locate NFS parent directory".to_string()))
match path.parent() {
Some(parent) if parent.is_dir() => {
Ok(Box::from(DiscIONFS::new(path.parent().unwrap())?))
}
_ => Err(Error::DiscFormat("Failed to locate NFS parent directory".to_string())),
}
} else if has_extension(path, "wia") || has_extension(path, "rvz") {
Ok(Box::from(DiscIOWIA::new(path, options)?))
} else {
Result::Err(Error::DiscFormat("Unknown file type".to_string()))
Err(Error::DiscFormat("Unknown file type".to_string()))
}
}
@ -78,9 +83,11 @@ pub fn new_disc_io(filename: &Path) -> Result<Box<dyn DiscIO>> {
/// ```no_run
/// use nod::io::new_disc_io_from_buf;
///
/// # #[allow(non_upper_case_globals)] const buf: [u8; 0] = [];
/// let mut disc_io = new_disc_io_from_buf(&buf)?;
/// # Ok::<(), nod::Error>(())
/// # fn main() -> nod::Result<()> {
/// # #[allow(non_upper_case_globals)] const buf: &[u8] = &[0u8; 0];
/// let mut disc_io = new_disc_io_from_buf(buf)?;
/// # Ok(())
/// # }
/// ```
pub fn new_disc_io_from_buf(buf: &[u8]) -> Result<Box<dyn DiscIO + '_>> {
new_disc_io_from_stream(ByteReadStream { bytes: buf, position: 0 })
@ -92,11 +99,15 @@ pub fn new_disc_io_from_buf(buf: &[u8]) -> Result<Box<dyn DiscIO + '_>> {
///
/// Basic usage:
/// ```no_run
/// use nod::io::new_disc_io_from_buf;
/// use nod::io::new_disc_io_from_stream;
/// use nod::streams::ByteReadStream;
///
/// # #[allow(non_upper_case_globals)] const buf: [u8; 0] = [];
/// let mut disc_io = new_disc_io_from_buf(&buf)?;
/// # Ok::<(), nod::Error>(())
/// # fn main() -> nod::Result<()> {
/// # #[allow(non_upper_case_globals)] const buf: &[u8] = &[0u8; 0];
/// let stream = ByteReadStream { bytes: buf, position: 0 };
/// let mut disc_io = new_disc_io_from_stream(stream)?;
/// # Ok(())
/// # }
/// ```
pub fn new_disc_io_from_stream<'a, T: 'a + ReadStream + Sized + Send + Sync>(
stream: T,
@ -107,11 +118,8 @@ pub fn new_disc_io_from_stream<'a, T: 'a + ReadStream + Sized + Send + Sync>(
/// Helper function for checking a file extension.
#[inline(always)]
pub fn has_extension(filename: &Path, extension: &str) -> bool {
if let Some(ext) = filename.extension() {
// TODO use with Rust 1.53+
// ext.eq_ignore_ascii_case(extension)
ext.to_str().unwrap_or("").eq_ignore_ascii_case(extension)
} else {
false
match filename.extension() {
Some(ext) => ext.eq_ignore_ascii_case(extension),
None => false,
}
}

View File

@ -1,36 +1,87 @@
use std::{
fs::File,
io,
io::{Read, Seek, SeekFrom},
io::{BufReader, Read, Seek, SeekFrom},
path::{Component, Path, PathBuf},
};
use aes::{Aes128, NewBlockCipher};
use binrw::{binread, BinRead, BinReaderExt};
use block_modes::{block_padding::NoPadding, BlockMode, Cbc};
use aes::{
cipher::{block_padding::NoPadding, BlockDecryptMut, KeyIvInit},
Aes128,
};
use crate::{disc::BUFFER_SIZE, io::DiscIO, streams::ReadStream, Error, Result};
use crate::{
disc::SECTOR_SIZE,
io::DiscIO,
streams::ReadStream,
util::reader::{read_vec, struct_size, FromReader},
Error, Result, ResultContext,
};
type Aes128Cbc = Cbc<Aes128, NoPadding>;
type Aes128Cbc = cbc::Decryptor<Aes128>;
#[derive(Clone, Debug, PartialEq, BinRead)]
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct LBARange {
pub(crate) start_block: u32,
pub(crate) num_blocks: u32,
}
#[binread]
impl FromReader for LBARange {
type Args<'a> = ();
const STATIC_SIZE: usize = struct_size([
u32::STATIC_SIZE, // start_block
u32::STATIC_SIZE, // num_blocks
]);
fn from_reader_args<R>(reader: &mut R, _args: Self::Args<'_>) -> io::Result<Self>
where R: Read + ?Sized {
Ok(LBARange {
start_block: u32::from_reader(reader)?,
num_blocks: u32::from_reader(reader)?,
})
}
}
type MagicBytes = [u8; 4];
#[derive(Clone, Debug, PartialEq)]
#[br(magic = b"EGGS", assert(end_magic == * b"SGGE"))]
pub(crate) struct NFSHeader {
pub(crate) version: u32,
pub(crate) unk1: u32,
pub(crate) unk2: u32,
pub(crate) lba_range_count: u32,
#[br(count = 61)]
pub(crate) lba_ranges: Vec<LBARange>,
#[br(temp)]
pub(crate) end_magic: [u8; 4],
}
impl FromReader for NFSHeader {
type Args<'a> = ();
const STATIC_SIZE: usize = struct_size([
MagicBytes::STATIC_SIZE, // magic
u32::STATIC_SIZE, // version
u32::STATIC_SIZE, // unk1
u32::STATIC_SIZE, // unk2
u32::STATIC_SIZE, // lba_range_count
LBARange::STATIC_SIZE * 61, // lba_ranges
MagicBytes::STATIC_SIZE, // end_magic
]);
fn from_reader_args<R>(reader: &mut R, _args: Self::Args<'_>) -> io::Result<Self>
where R: Read + ?Sized {
if MagicBytes::from_reader(reader)? != *b"EGGS" {
return Err(io::Error::new(io::ErrorKind::InvalidData, "Invalid NFS magic"));
}
let version = u32::from_reader(reader)?;
let unk1 = u32::from_reader(reader)?;
let unk2 = u32::from_reader(reader)?;
let lba_range_count = u32::from_reader(reader)?;
let mut lba_ranges = read_vec(reader, 61)?;
lba_ranges.truncate(lba_range_count as usize);
if MagicBytes::from_reader(reader)? != *b"SGGE" {
return Err(io::Error::new(io::ErrorKind::InvalidData, "Invalid NFS end magic"));
}
Ok(NFSHeader { version, unk1, unk2, lba_ranges })
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
@ -49,11 +100,8 @@ impl Default for Fbo {
impl NFSHeader {
pub(crate) fn calculate_num_files(&self) -> u32 {
let total_block_count = self
.lba_ranges
.iter()
.take(self.lba_range_count as usize)
.fold(0u32, |acc, range| acc + range.num_blocks);
let total_block_count =
self.lba_ranges.iter().fold(0u32, |acc, range| acc + range.num_blocks);
(((total_block_count as u64) * 0x8000u64 + (0x200u64 + 0xF9FFFFFu64)) / 0xFA00000u64) as u32
}
@ -62,7 +110,7 @@ impl NFSHeader {
let block_off = (offset % 0x8000) as u32;
let mut block = u32::MAX;
let mut physical_block = 0u32;
for range in self.lba_ranges.iter().take(self.lba_range_count as usize) {
for range in self.lba_ranges.iter() {
if block_div >= range.start_block && block_div - range.start_block < range.num_blocks {
block = physical_block + (block_div - range.start_block);
break;
@ -85,17 +133,16 @@ pub(crate) struct DiscIONFS {
impl DiscIONFS {
pub(crate) fn new(directory: &Path) -> Result<DiscIONFS> {
let mut disc_io =
DiscIONFS { directory: directory.to_owned(), key: [0; 16], header: Option::None };
let mut disc_io = DiscIONFS { directory: directory.to_owned(), key: [0; 16], header: None };
disc_io.validate_files()?;
Result::Ok(disc_io)
Ok(disc_io)
}
}
pub(crate) struct NFSReadStream<'a> {
disc_io: &'a DiscIONFS,
file: Option<File>,
crypto: Aes128,
crypto: [u8; 16],
// Physical address - all UINT32_MAX indicates logical zero block
phys_addr: Fbo,
// Logical address
@ -104,18 +151,21 @@ pub(crate) struct NFSReadStream<'a> {
// Block is typically one ahead of the presently decrypted block.
cur_file: u32,
cur_block: u32,
buf: [u8; BUFFER_SIZE],
buf: [u8; SECTOR_SIZE],
}
impl<'a> NFSReadStream<'a> {
fn set_cur_file(&mut self, cur_file: u32) -> Result<()> {
if cur_file >= self.disc_io.header.as_ref().unwrap().calculate_num_files() {
return Result::Err(Error::DiscFormat("Out of bounds NFS file access".to_string()));
return Err(Error::DiscFormat(format!("Out of bounds NFS file access: {}", cur_file)));
}
self.cur_file = cur_file;
self.cur_block = u32::MAX;
self.file = Option::from(File::open(self.disc_io.get_nfs(cur_file)?)?);
Result::Ok(())
let path = self.disc_io.get_nfs(cur_file)?;
self.file = Option::from(
File::open(&path).with_context(|| format!("Opening file {}", path.display()))?,
);
Ok(())
}
fn set_cur_block(&mut self, cur_block: u32) -> io::Result<()> {
@ -123,22 +173,22 @@ impl<'a> NFSReadStream<'a> {
self.file
.as_ref()
.unwrap()
.seek(SeekFrom::Start(self.cur_block as u64 * BUFFER_SIZE as u64 + 0x200u64))?;
io::Result::Ok(())
.seek(SeekFrom::Start(self.cur_block as u64 * SECTOR_SIZE as u64 + 0x200u64))?;
Ok(())
}
fn set_phys_addr(&mut self, phys_addr: Fbo) -> Result<()> {
// If we're just changing the offset, nothing else needs to be done
if self.phys_addr.file == phys_addr.file && self.phys_addr.block == phys_addr.block {
self.phys_addr.offset = phys_addr.offset;
return Result::Ok(());
return Ok(());
}
self.phys_addr = phys_addr;
// Set logical zero block
if phys_addr.file == u32::MAX {
self.buf.fill(0u8);
return Result::Ok(());
return Ok(());
}
// Make necessary file and block current with system
@ -146,17 +196,30 @@ impl<'a> NFSReadStream<'a> {
self.set_cur_file(phys_addr.file)?;
}
if phys_addr.block != self.cur_block {
self.set_cur_block(phys_addr.block)?;
self.set_cur_block(phys_addr.block)
.with_context(|| format!("Seeking to NFS block {}", phys_addr.block))?;
}
// Read block, handling 0x200 overlap case
if phys_addr.block == 7999 {
self.file.as_ref().unwrap().read_exact(&mut self.buf[..BUFFER_SIZE - 0x200])?;
self.file
.as_ref()
.unwrap()
.read_exact(&mut self.buf[..SECTOR_SIZE - 0x200])
.context("Reading NFS block 7999 part 1")?;
self.set_cur_file(self.cur_file + 1)?;
self.file.as_ref().unwrap().read_exact(&mut self.buf[BUFFER_SIZE - 0x200..])?;
self.file
.as_ref()
.unwrap()
.read_exact(&mut self.buf[SECTOR_SIZE - 0x200..])
.context("Reading NFS block 7999 part 2")?;
self.cur_block = 0;
} else {
self.file.as_ref().unwrap().read_exact(&mut self.buf)?;
self.file
.as_ref()
.unwrap()
.read_exact(&mut self.buf)
.with_context(|| format!("Reading NFS block {}", phys_addr.block))?;
self.cur_block += 1;
}
@ -169,9 +232,10 @@ impl<'a> NFSReadStream<'a> {
((phys_addr.l_block >> 16) & 0xFF) as u8,
((phys_addr.l_block >> 24) & 0xFF) as u8,
];
Aes128Cbc::new(self.crypto.clone(), &iv.into()).decrypt(&mut self.buf)?;
Aes128Cbc::new(self.crypto.as_ref().into(), &iv.into())
.decrypt_padded_mut::<NoPadding>(&mut self.buf)?;
Result::Ok(())
Ok(())
}
fn set_logical_addr(&mut self, addr: u64) -> Result<()> {
@ -187,20 +251,20 @@ impl<'a> Read for NFSReadStream<'a> {
let mut read_size = rem;
let block_offset: usize =
if self.phys_addr.offset == u32::MAX { 0 } else { self.phys_addr.offset as usize };
if read_size + block_offset > BUFFER_SIZE {
read_size = BUFFER_SIZE - block_offset
if read_size + block_offset > SECTOR_SIZE {
read_size = SECTOR_SIZE - block_offset
}
buf[read..read + read_size]
.copy_from_slice(&self.buf[block_offset..block_offset + read_size]);
read += read_size;
rem -= read_size;
self.offset += read_size as u64;
self.set_logical_addr(self.offset).map_err(|v| match v {
Error::Io(_, v) => v,
self.set_logical_addr(self.offset).map_err(|e| match e {
Error::Io(s, e) => io::Error::new(e.kind(), s),
_ => io::Error::from(io::ErrorKind::Other),
})?;
}
io::Result::Ok(read)
Ok(read)
}
}
@ -215,10 +279,10 @@ impl<'a> Seek for NFSReadStream<'a> {
Error::Io(_, v) => v,
_ => io::Error::from(io::ErrorKind::Other),
})?;
io::Result::Ok(self.offset)
Ok(self.offset)
}
fn stream_position(&mut self) -> io::Result<u64> { io::Result::Ok(self.offset) }
fn stream_position(&mut self) -> io::Result<u64> { Ok(self.offset) }
}
impl<'a> ReadStream for NFSReadStream<'a> {
@ -229,15 +293,15 @@ impl<'a> ReadStream for NFSReadStream<'a> {
impl DiscIO for DiscIONFS {
fn begin_read_stream(&mut self, offset: u64) -> io::Result<Box<dyn ReadStream + '_>> {
io::Result::Ok(Box::from(NFSReadStream {
Ok(Box::from(NFSReadStream {
disc_io: self,
file: Option::None,
crypto: Aes128::new(&self.key.into()),
file: None,
crypto: self.key,
phys_addr: Fbo::default(),
offset,
cur_file: u32::MAX,
cur_block: u32::MAX,
buf: [0; BUFFER_SIZE],
buf: [0; SECTOR_SIZE],
}))
}
@ -245,7 +309,8 @@ impl DiscIO for DiscIONFS {
}
impl DiscIONFS {
fn get_path<P: AsRef<Path>>(&self, path: P) -> PathBuf {
fn get_path<P>(&self, path: P) -> PathBuf
where P: AsRef<Path> {
let mut buf = self.directory.clone();
for component in path.as_ref().components() {
match component {
@ -261,9 +326,9 @@ impl DiscIONFS {
fn get_nfs(&self, num: u32) -> Result<PathBuf> {
let path = self.get_path(format!("hif_{:06}.nfs", num));
if path.exists() {
Result::Ok(path)
Ok(path)
} else {
Result::Err(Error::DiscFormat(format!("Failed to locate {}", path.to_string_lossy())))
Err(Error::DiscFormat(format!("Failed to locate {}", path.display())))
}
}
@ -278,31 +343,32 @@ impl DiscIONFS {
key_path = secondary_key_path.canonicalize();
}
if key_path.is_err() {
return Result::Err(Error::DiscFormat(format!(
return Err(Error::DiscFormat(format!(
"Failed to locate {} or {}",
primary_key_path.to_string_lossy(),
secondary_key_path.to_string_lossy()
primary_key_path.display(),
secondary_key_path.display()
)));
}
let resolved_path = key_path.unwrap();
File::open(resolved_path.as_path())
.map_err(|v| {
Error::Io(format!("Failed to open {}", resolved_path.to_string_lossy()), v)
})?
.map_err(|v| Error::Io(format!("Failed to open {}", resolved_path.display()), v))?
.read(&mut self.key)
.map_err(|v| {
Error::Io(format!("Failed to read {}", resolved_path.to_string_lossy()), v)
})?;
.map_err(|v| Error::Io(format!("Failed to read {}", resolved_path.display()), v))?;
}
{
// Load header from first file
let header: NFSHeader = File::open(self.get_nfs(0)?)?.read_be()?;
let path = self.get_nfs(0)?;
let mut file = BufReader::new(
File::open(&path).with_context(|| format!("Opening file {}", path.display()))?,
);
let header = NFSHeader::from_reader(&mut file)
.with_context(|| format!("Reading NFS header from file {}", path.display()))?;
// Ensure remaining files exist
for i in 1..header.calculate_num_files() {
self.get_nfs(i)?;
}
self.header = Option::from(header)
self.header = Option::from(header);
}
Result::Ok(())
Ok(())
}
}

1541
src/io/wia.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,71 +1,93 @@
#![warn(missing_docs)]
#![warn(rustdoc::missing_doc_code_examples)]
#![warn(missing_docs, rustdoc::missing_doc_code_examples)]
//! Library for traversing & reading GameCube and Wii disc images.
//!
//! Based on the C++ library [nod](https://github.com/AxioDL/nod),
//! but does not currently support authoring.
//!
//! Currently supported file formats:
//! - ISO
//! - ISO (GCM)
//! - WIA / RVZ
//! - WBFS
//! - NFS (Wii U VC files, e.g. `hif_000000.nfs`)
//!
//! # Examples
//!
//! Opening a disc image and reading a file:
//! ```no_run
//! use nod::disc::{new_disc_base, PartHeader};
//! use nod::fst::NodeType;
//! use nod::io::new_disc_io;
//! use std::io::Read;
//!
//! let mut disc_io = new_disc_io("path/to/file".as_ref())?;
//! let disc_base = new_disc_base(disc_io.as_mut())?;
//! let mut partition = disc_base.get_data_partition(disc_io.as_mut(), false)?;
//! let header = partition.read_header()?;
//! if let Some(NodeType::File(node)) = header.find_node("/MP3/Worlds.txt") {
//! let mut s = String::new();
//! partition.begin_file_stream(node)?.read_to_string(&mut s);
//! println!("{}", s);
//! use nod::{
//! disc::{new_disc_base, PartHeader},
//! fst::NodeType,
//! io::{new_disc_io, DiscIOOptions},
//! };
//!
//! fn main() -> nod::Result<()> {
//! let options = DiscIOOptions::default();
//! let mut disc_io = new_disc_io("path/to/file.iso".as_ref(), &options)?;
//! let disc_base = new_disc_base(disc_io.as_mut())?;
//! let mut partition = disc_base.get_data_partition(disc_io.as_mut(), false)?;
//! let header = partition.read_header()?;
//! if let Some(NodeType::File(node)) = header.find_node("/MP3/Worlds.txt") {
//! let mut s = String::new();
//! partition.begin_file_stream(node)?.read_to_string(&mut s).expect("Failed to read file");
//! println!("{}", s);
//! }
//! Ok(())
//! }
//! # Ok::<(), nod::Error>(())
//! ```
use thiserror::Error;
pub mod disc;
pub mod fst;
pub mod io;
pub mod streams;
pub mod util;
/// Error types for nod.
#[derive(Error, Debug)]
#[derive(thiserror::Error, Debug)]
pub enum Error {
/// An error during binary format parsing.
#[error("binary format")]
BinaryFormat(#[from] binrw::Error),
/// An error during Wii disc decryption.
#[error("encryption")]
Encryption(#[from] block_modes::BlockModeError),
/// A general I/O error.
#[error("io error: `{0}`")]
Io(String, #[source] std::io::Error),
/// An error for disc format related issues.
#[error("disc format error: `{0}`")]
#[error("disc format error: {0}")]
DiscFormat(String),
/// A general I/O error.
#[error("I/O error: {0}")]
Io(String, #[source] std::io::Error),
}
/// Helper result type for [`enum@Error`].
/// Helper result type for [`Error`].
pub type Result<T, E = Error> = core::result::Result<T, E>;
impl From<std::io::Error> for Error {
fn from(v: std::io::Error) -> Self { Error::Io("I/O error".to_string(), v) }
impl From<aes::cipher::block_padding::UnpadError> for Error {
fn from(_: aes::cipher::block_padding::UnpadError) -> Self { unreachable!() }
}
#[inline(always)]
pub(crate) fn div_rem<T: std::ops::Div<Output = T> + std::ops::Rem<Output = T> + Copy>(
x: T,
y: T,
) -> (T, T) {
let quot = x / y;
let rem = x % y;
(quot, rem)
impl From<base16ct::Error> for Error {
fn from(_: base16ct::Error) -> Self { unreachable!() }
}
pub trait ErrorContext {
fn context(self, context: impl Into<String>) -> Error;
}
impl ErrorContext for std::io::Error {
fn context(self, context: impl Into<String>) -> Error { Error::Io(context.into(), self) }
}
pub trait ResultContext<T> {
fn context(self, context: impl Into<String>) -> Result<T>;
fn with_context<F>(self, f: F) -> Result<T>
where F: FnOnce() -> String;
}
impl<T, E> ResultContext<T> for Result<T, E>
where E: ErrorContext
{
fn context(self, context: impl Into<String>) -> Result<T> {
self.map_err(|e| e.context(context))
}
fn with_context<F>(self, f: F) -> Result<T>
where F: FnOnce() -> String {
self.map_err(|e| e.context(f()))
}
}

View File

@ -43,11 +43,7 @@ pub trait ReadStream: Read + Seek {
/// Seeks underlying stream immediately.
fn new_window(&mut self, offset: u64, size: u64) -> io::Result<SharedWindowedReadStream> {
self.seek(SeekFrom::Start(offset))?;
io::Result::Ok(SharedWindowedReadStream {
base: self.as_dyn(),
begin: offset,
end: offset + size,
})
Ok(SharedWindowedReadStream { base: self.as_dyn(), begin: offset, end: offset + size })
}
/// Retrieves a type-erased reference to the stream.
@ -91,7 +87,7 @@ pub fn wrap_windowed<'a>(
size: u64,
) -> io::Result<OwningWindowedReadStream<'a>> {
base.seek(SeekFrom::Start(offset))?;
io::Result::Ok(OwningWindowedReadStream { base, begin: offset, end: offset + size })
Ok(OwningWindowedReadStream { base, begin: offset, end: offset + size })
}
/// A non-owning window into an existing [`ReadStream`].
@ -110,7 +106,7 @@ impl<'a> SharedWindowedReadStream<'a> {
self.base.seek(SeekFrom::Start(begin))?;
self.begin = begin;
self.end = end;
io::Result::Ok(())
Ok(())
}
}
@ -137,9 +133,9 @@ fn windowed_seek(stream: &mut dyn WindowedReadStream, pos: SeekFrom) -> io::Resu
SeekFrom::Current(_) => pos,
})?;
if result < begin || result > end {
io::Result::Err(io::Error::from(io::ErrorKind::UnexpectedEof))
Err(io::Error::from(io::ErrorKind::UnexpectedEof))
} else {
io::Result::Ok(result - begin)
Ok(result - begin)
}
}
@ -151,12 +147,12 @@ impl<'a> Seek for OwningWindowedReadStream<'a> {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> { windowed_seek(self, pos) }
fn stream_position(&mut self) -> io::Result<u64> {
Result::Ok(self.base.stream_position()? - self.begin)
Ok(self.base.stream_position()? - self.begin)
}
}
impl<'a> ReadStream for OwningWindowedReadStream<'a> {
fn stable_stream_len(&mut self) -> io::Result<u64> { Result::Ok(self.end - self.begin) }
fn stable_stream_len(&mut self) -> io::Result<u64> { Ok(self.end - self.begin) }
fn as_dyn(&mut self) -> &mut dyn ReadStream { self }
}
@ -175,12 +171,12 @@ impl<'a> Seek for SharedWindowedReadStream<'a> {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> { windowed_seek(self, pos) }
fn stream_position(&mut self) -> io::Result<u64> {
Result::Ok(self.base.stream_position()? - self.begin)
Ok(self.base.stream_position()? - self.begin)
}
}
impl<'a> ReadStream for SharedWindowedReadStream<'a> {
fn stable_stream_len(&mut self) -> io::Result<u64> { Result::Ok(self.end - self.begin) }
fn stable_stream_len(&mut self) -> io::Result<u64> { Ok(self.end - self.begin) }
fn as_dyn(&mut self) -> &mut dyn ReadStream { self }
}

77
src/util/lfg.rs Normal file
View File

@ -0,0 +1,77 @@
use std::{cmp::min, io, io::Read};
pub(crate) const LFG_K: usize = 521;
pub(crate) const LFG_J: usize = 32;
pub(crate) const SEED_SIZE: usize = 17;
/// Lagged Fibonacci generator for Wii partition junk data.
/// https://github.com/dolphin-emu/dolphin/blob/master/docs/WiaAndRvz.md#prng-algorithm
pub(crate) struct LaggedFibonacci {
buffer: [u32; LFG_K],
position: usize,
}
impl Default for LaggedFibonacci {
fn default() -> Self { Self { buffer: [0u32; LFG_K], position: 0 } }
}
impl LaggedFibonacci {
fn init(&mut self) {
for i in SEED_SIZE..LFG_K {
self.buffer[i] =
(self.buffer[i - 17] << 23) ^ (self.buffer[i - 16] >> 9) ^ self.buffer[i - 1];
}
for x in self.buffer.iter_mut() {
*x = ((*x & 0xFF00FFFF) | (*x >> 2 & 0x00FF0000)).swap_bytes();
}
for _ in 0..4 {
self.forward();
}
}
pub(crate) fn init_with_reader<R>(&mut self, reader: &mut R) -> io::Result<()>
where R: Read + ?Sized {
reader.read_exact(bytemuck::cast_slice_mut(&mut self.buffer[..SEED_SIZE]))?;
for x in self.buffer[..SEED_SIZE].iter_mut() {
*x = u32::from_be(*x);
}
self.position = 0;
self.init();
Ok(())
}
pub(crate) fn forward(&mut self) {
for i in 0..LFG_J {
self.buffer[i] ^= self.buffer[i + LFG_K - LFG_J];
}
for i in LFG_J..LFG_K {
self.buffer[i] ^= self.buffer[i - LFG_J];
}
}
pub(crate) fn skip(&mut self, n: usize) {
self.position += n;
while self.position >= LFG_K * 4 {
self.forward();
self.position -= LFG_K * 4;
}
}
#[inline]
fn bytes(&self) -> &[u8; LFG_K * 4] {
unsafe { &*(self.buffer.as_ptr() as *const [u8; LFG_K * 4]) }
}
pub(crate) fn fill(&mut self, mut buf: &mut [u8]) {
while !buf.is_empty() {
let len = min(buf.len(), LFG_K * 4 - self.position);
buf[..len].copy_from_slice(&self.bytes()[self.position..self.position + len]);
self.position += len;
buf = &mut buf[len..];
if self.position == LFG_K * 4 {
self.forward();
self.position = 0;
}
}
}
}

13
src/util/mod.rs Normal file
View File

@ -0,0 +1,13 @@
use std::ops::{Div, Rem};
pub(crate) mod lfg;
pub(crate) mod reader;
pub(crate) mod take_seek;
#[inline(always)]
pub(crate) fn div_rem<T>(x: T, y: T) -> (T, T)
where T: Div<Output = T> + Rem<Output = T> + Copy {
let quot = x / y;
let rem = x % y;
(quot, rem)
}

243
src/util/reader.rs Normal file
View File

@ -0,0 +1,243 @@
use std::{ffi::CString, io, io::Read};
use io::Write;
pub(crate) const DYNAMIC_SIZE: usize = 0;
pub(crate) const fn struct_size<const N: usize>(fields: [usize; N]) -> usize {
let mut result = 0;
let mut i = 0;
while i < N {
let size = fields[i];
if size == DYNAMIC_SIZE {
// Dynamically sized
return DYNAMIC_SIZE;
}
result += size;
i += 1;
}
result
}
pub(crate) fn skip_bytes<const N: usize, R>(reader: &mut R) -> io::Result<()>
where R: Read + ?Sized {
let mut buf = [0u8; N];
reader.read_exact(&mut buf)?;
Ok(())
}
pub(crate) trait FromReader: Sized {
type Args<'a>;
const STATIC_SIZE: usize;
fn from_reader_args<R>(reader: &mut R, args: Self::Args<'_>) -> io::Result<Self>
where R: Read + ?Sized;
fn from_reader<'a, R>(reader: &mut R) -> io::Result<Self>
where
R: Read + ?Sized,
Self::Args<'a>: Default,
{
Self::from_reader_args(reader, Default::default())
}
}
macro_rules! impl_from_reader {
($($t:ty),*) => {
$(
impl FromReader for $t {
type Args<'a> = ();
const STATIC_SIZE: usize = std::mem::size_of::<Self>();
fn from_reader_args<R>(reader: &mut R, _args: Self::Args<'_>) -> io::Result<Self> where R: Read + ?Sized{
let mut buf = [0u8; Self::STATIC_SIZE];
reader.read_exact(&mut buf)?;
Ok(Self::from_be_bytes(buf))
}
}
)*
};
}
impl_from_reader!(u8, u16, u32, u64, u128, i8, i16, i32, i64, i128);
#[repr(transparent)]
pub struct U24(pub u32);
impl FromReader for U24 {
type Args<'a> = ();
const STATIC_SIZE: usize = 3;
fn from_reader_args<R>(reader: &mut R, _args: Self::Args<'_>) -> io::Result<Self>
where R: Read + ?Sized {
let mut buf = [0u8; 4];
reader.read_exact(&mut buf[1..])?;
Ok(U24(u32::from_be_bytes(buf)))
}
}
impl<const N: usize> FromReader for [u8; N] {
type Args<'a> = ();
const STATIC_SIZE: usize = N;
fn from_reader_args<R>(reader: &mut R, _args: Self::Args<'_>) -> io::Result<Self>
where R: Read + ?Sized {
let mut buf = [0u8; N];
reader.read_exact(&mut buf)?;
Ok(buf)
}
}
impl<const N: usize> FromReader for [u32; N] {
type Args<'a> = ();
const STATIC_SIZE: usize = N * u32::STATIC_SIZE;
fn from_reader_args<R>(reader: &mut R, _args: Self::Args<'_>) -> io::Result<Self>
where R: Read + ?Sized {
let mut buf = [0u32; N];
reader.read_exact(unsafe {
std::slice::from_raw_parts_mut(buf.as_mut_ptr() as *mut u8, Self::STATIC_SIZE)
})?;
for x in buf.iter_mut() {
*x = u32::from_be(*x);
}
Ok(buf)
}
}
impl FromReader for CString {
type Args<'a> = ();
const STATIC_SIZE: usize = DYNAMIC_SIZE;
fn from_reader_args<R>(reader: &mut R, _args: Self::Args<'_>) -> io::Result<Self>
where R: Read + ?Sized {
let mut buf = Vec::new();
loop {
let mut byte = [0u8; 1];
reader.read_exact(&mut byte)?;
buf.push(byte[0]);
if byte[0] == 0 {
break;
}
}
Ok(unsafe { CString::from_vec_with_nul_unchecked(buf) })
}
}
pub(crate) fn read_bytes<R>(reader: &mut R, count: usize) -> io::Result<Vec<u8>>
where R: Read + ?Sized {
let mut buf = vec![0u8; count];
reader.read_exact(&mut buf)?;
Ok(buf)
}
pub(crate) fn read_vec<'a, T, R>(reader: &mut R, count: usize) -> io::Result<Vec<T>>
where
T: FromReader,
R: Read + ?Sized,
<T as FromReader>::Args<'a>: Default,
{
let mut vec = Vec::with_capacity(count);
if T::STATIC_SIZE != DYNAMIC_SIZE {
// Read the entire buffer at once
let buf = read_bytes(reader, T::STATIC_SIZE * count)?;
let mut slice = buf.as_slice();
for _ in 0..count {
vec.push(T::from_reader(&mut slice)?);
}
} else {
for _ in 0..count {
vec.push(T::from_reader(reader)?);
}
}
Ok(vec)
}
pub(crate) trait ToWriter: Sized {
fn to_writer<W>(&self, writer: &mut W) -> io::Result<()>
where W: Write + ?Sized;
fn to_bytes(&self) -> io::Result<Vec<u8>> {
let mut buf = vec![0u8; self.write_size()];
self.to_writer(&mut buf.as_mut_slice())?;
Ok(buf)
}
fn write_size(&self) -> usize;
}
macro_rules! impl_to_writer {
($($t:ty),*) => {
$(
impl ToWriter for $t {
fn to_writer<W>(&self, writer: &mut W) -> io::Result<()>
where W: Write + ?Sized {
writer.write_all(&self.to_be_bytes())
}
fn to_bytes(&self) -> io::Result<Vec<u8>> {
Ok(self.to_be_bytes().to_vec())
}
fn write_size(&self) -> usize {
std::mem::size_of::<Self>()
}
}
)*
};
}
impl_to_writer!(u8, u16, u32, u64, u128, i8, i16, i32, i64, i128);
impl ToWriter for U24 {
fn to_writer<W>(&self, writer: &mut W) -> io::Result<()>
where W: Write + ?Sized {
writer.write_all(&self.0.to_be_bytes()[1..])
}
fn write_size(&self) -> usize { 3 }
}
impl<const N: usize> ToWriter for [u8; N] {
fn to_writer<W>(&self, writer: &mut W) -> io::Result<()>
where W: Write + ?Sized {
writer.write_all(self)
}
fn write_size(&self) -> usize { N }
}
impl ToWriter for &[u8] {
fn to_writer<W>(&self, writer: &mut W) -> io::Result<()>
where W: Write + ?Sized {
writer.write_all(self)
}
fn write_size(&self) -> usize { self.len() }
}
impl ToWriter for Vec<u8> {
fn to_writer<W>(&self, writer: &mut W) -> io::Result<()>
where W: Write + ?Sized {
writer.write_all(self)
}
fn write_size(&self) -> usize { self.len() }
}
pub(crate) fn write_vec<T, W>(writer: &mut W, vec: &[T]) -> io::Result<()>
where
T: ToWriter,
W: Write + ?Sized,
{
for item in vec {
item.to_writer(writer)?;
}
Ok(())
}

127
src/util/take_seek.rs Normal file
View File

@ -0,0 +1,127 @@
// From https://github.com/jam1garner/binrw/blob/e96a1320287ec83d1f471525ffa380800ec9e124/binrw/src/io/take_seek.rs
// MIT License
//
// Copyright (c) jam1garner and other contributors
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#![allow(dead_code)]
//! Types for seekable reader adapters which limit the number of bytes read from
//! the underlying reader.
use std::io::{Read, Result, Seek, SeekFrom};
/// Read adapter which limits the bytes read from an underlying reader, with
/// seek support.
///
/// This struct is generally created by importing the [`TakeSeekExt`] extension
/// and calling [`take_seek`] on a reader.
///
/// [`take_seek`]: TakeSeekExt::take_seek
#[derive(Debug)]
pub struct TakeSeek<T> {
inner: T,
pos: u64,
end: u64,
}
impl<T> TakeSeek<T> {
/// Gets a reference to the underlying reader.
pub fn get_ref(&self) -> &T { &self.inner }
/// Gets a mutable reference to the underlying reader.
///
/// Care should be taken to avoid modifying the internal I/O state of the
/// underlying reader as doing so may corrupt the internal limit of this
/// `TakeSeek`.
pub fn get_mut(&mut self) -> &mut T { &mut self.inner }
/// Consumes this wrapper, returning the wrapped value.
pub fn into_inner(self) -> T { self.inner }
/// Returns the number of bytes that can be read before this instance will
/// return EOF.
///
/// # Note
///
/// This instance may reach EOF after reading fewer bytes than indicated by
/// this method if the underlying [`Read`] instance reaches EOF.
pub fn limit(&self) -> u64 { self.end.saturating_sub(self.pos) }
}
impl<T: Seek> TakeSeek<T> {
/// Sets the number of bytes that can be read before this instance will
/// return EOF. This is the same as constructing a new `TakeSeek` instance,
/// so the amount of bytes read and the previous limit value dont matter
/// when calling this method.
///
/// # Panics
///
/// Panics if the inner stream returns an error from `stream_position`.
pub fn set_limit(&mut self, limit: u64) {
let pos = self.inner.stream_position().expect("cannot get position for `set_limit`");
self.pos = pos;
self.end = pos + limit;
}
}
impl<T: Read> Read for TakeSeek<T> {
fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
let limit = self.limit();
// Don't call into inner reader at all at EOF because it may still block
if limit == 0 {
return Ok(0);
}
// Lint: It is impossible for this cast to truncate because the value
// being cast is the minimum of two values, and one of the value types
// is already `usize`.
#[allow(clippy::cast_possible_truncation)]
let max = (buf.len() as u64).min(limit) as usize;
let n = self.inner.read(&mut buf[0..max])?;
self.pos += n as u64;
Ok(n)
}
}
impl<T: Seek> Seek for TakeSeek<T> {
fn seek(&mut self, pos: SeekFrom) -> Result<u64> {
self.pos = self.inner.seek(pos)?;
Ok(self.pos)
}
fn stream_position(&mut self) -> Result<u64> { Ok(self.pos) }
}
/// An extension trait that implements `take_seek()` for compatible streams.
pub trait TakeSeekExt {
/// Creates an adapter which will read at most `limit` bytes from the
/// wrapped stream.
fn take_seek(self, limit: u64) -> TakeSeek<Self>
where Self: Sized;
}
impl<T: Read + Seek> TakeSeekExt for T {
fn take_seek(mut self, limit: u64) -> TakeSeek<Self>
where Self: Sized {
let pos = self.stream_position().expect("cannot get position for `take_seek`");
TakeSeek { inner: self, pos, end: pos + limit }
}
}