mirror of
https://github.com/encounter/nod.git
synced 2026-07-10 12:18:48 -07:00
Finish WIA/RVZ, add WBFS, CISO & more
Generally a complete overhaul.
This commit is contained in:
+22
-11
@@ -11,8 +11,9 @@ readme = "README.md"
|
||||
description = """
|
||||
Rust library and CLI tool for reading GameCube and Wii disc images.
|
||||
"""
|
||||
keywords = ["gamecube", "wii", "iso", "nfs", "gcm"]
|
||||
keywords = ["gamecube", "wii", "iso", "nfs", "rvz"]
|
||||
categories = ["command-line-utilities", "parser-implementations"]
|
||||
build = "build.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "nodtool"
|
||||
@@ -24,27 +25,37 @@ lto = "thin"
|
||||
strip = "debuginfo"
|
||||
|
||||
[features]
|
||||
default = ["compress-bzip2", "compress-zstd"] #, "compress-lzma"
|
||||
default = ["compress-bzip2", "compress-lzma", "compress-zstd"]
|
||||
asm = ["md-5/asm", "sha1/asm"]
|
||||
compress-bzip2 = ["bzip2"]
|
||||
compress-lzma = ["liblzma"]
|
||||
compress-zstd = ["zstd"]
|
||||
#compress-lzma = ["xz2"]
|
||||
nightly = ["crc32fast/nightly"]
|
||||
|
||||
[dependencies]
|
||||
aes = "0.8.3"
|
||||
argh = "0.1.12"
|
||||
aes = "0.8.4"
|
||||
argh_derive = "0.1.12"
|
||||
argp = "0.3.0"
|
||||
base16ct = "0.2.0"
|
||||
binrw = "0.13.3"
|
||||
bytemuck = "1.14.1"
|
||||
bzip2 = { version = "0.4.4", optional = true }
|
||||
bzip2 = { version = "0.4.4", features = ["static"], optional = true }
|
||||
cbc = "0.1.2"
|
||||
crc32fast = "1.3.2"
|
||||
crc32fast = "1.4.0"
|
||||
digest = "0.10.7"
|
||||
enable-ansi-support = "0.2.1"
|
||||
encoding_rs = "0.8.33"
|
||||
file-size = "1.0.3"
|
||||
indicatif = "0.17.8"
|
||||
itertools = "0.12.1"
|
||||
liblzma = { git = "https://github.com/encounter/liblzma-rs.git", rev = "ce29b22", features = ["static"], optional = true }
|
||||
log = "0.4.20"
|
||||
md-5 = "0.10.6"
|
||||
rayon = "1.8.1"
|
||||
sha1 = "0.10.6"
|
||||
thiserror = "1.0.56"
|
||||
xz2 = { version = "0.1.7", optional = true }
|
||||
supports-color = "3.0.0"
|
||||
thiserror = "1.0.57"
|
||||
tracing = "0.1.40"
|
||||
tracing-attributes = "0.1.27"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
xxhash-rust = { version = "0.8.8", features = ["xxh64"] }
|
||||
zerocopy = { version = "0.7.32", features = ["alloc", "derive"] }
|
||||
zstd = { version = "0.13.0", optional = true }
|
||||
|
||||
@@ -10,52 +10,68 @@
|
||||
|
||||
Library for traversing & reading GameCube and Wii disc images.
|
||||
|
||||
Based on the C++ library [nod](https://github.com/AxioDL/nod),
|
||||
Originally based on the C++ library [nod](https://github.com/AxioDL/nod),
|
||||
but does not currently support authoring.
|
||||
|
||||
Currently supported file formats:
|
||||
- ISO (GCM)
|
||||
- WIA / RVZ
|
||||
- WBFS
|
||||
- NFS (Wii U VC files, e.g. `hif_000000.nfs`)
|
||||
- CISO
|
||||
- NFS (Wii U VC)
|
||||
|
||||
### CLI tool
|
||||
## CLI tool
|
||||
|
||||
This crate includes a CLI tool `nodtool`, which can be used to extract disc images to a specified directory:
|
||||
This crate includes a command-line tool called `nodtool`.
|
||||
|
||||
### info
|
||||
|
||||
Displays information about a disc image.
|
||||
|
||||
```shell
|
||||
nodtool info /path/to/game.iso
|
||||
```
|
||||
|
||||
### extract
|
||||
|
||||
Extracts the contents of a disc image to a directory.
|
||||
|
||||
```shell
|
||||
nodtool extract /path/to/game.iso [outdir]
|
||||
```
|
||||
|
||||
For Wii U VC titles, use `content/hif_*.nfs`:
|
||||
For Wii U VC titles, use `content/hif_000000.nfs`:
|
||||
|
||||
```shell
|
||||
nodtool extract /path/to/game/content/hif_000000.nfs [outdir]
|
||||
```
|
||||
|
||||
### Library example
|
||||
### convert
|
||||
|
||||
Converts any supported format to raw ISO.
|
||||
|
||||
```shell
|
||||
nodtool convert /path/to/game.wia /path/to/game.iso
|
||||
```
|
||||
|
||||
## Library example
|
||||
|
||||
Opening a disc image and reading a file:
|
||||
|
||||
```rust
|
||||
use std::io::Read;
|
||||
|
||||
use nod::{
|
||||
disc::{new_disc_base, PartHeader},
|
||||
fst::NodeType,
|
||||
io::{new_disc_io, DiscIOOptions},
|
||||
};
|
||||
use nod::{Disc, PartitionKind};
|
||||
|
||||
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 disc = Disc::new("path/to/file.iso")?;
|
||||
let mut partition = disc.open_partition_kind(PartitionKind::Data)?;
|
||||
let meta = partition.meta()?;
|
||||
let fst = meta.fst()?;
|
||||
if let Some((_, node)) = fst.find("/MP3/Worlds.txt") {
|
||||
let mut s = String::new();
|
||||
partition
|
||||
.begin_file_stream(node)
|
||||
.open_file(node)
|
||||
.expect("Failed to open file stream")
|
||||
.read_to_string(&mut s)
|
||||
.expect("Failed to read file");
|
||||
@@ -65,7 +81,7 @@ fn main() -> nod::Result<()> {
|
||||
}
|
||||
```
|
||||
|
||||
### License
|
||||
## License
|
||||
|
||||
Licensed under either of
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
fn main() {
|
||||
let output = std::process::Command::new("git")
|
||||
.args(["rev-parse", "HEAD"])
|
||||
.output()
|
||||
.expect("Failed to execute git");
|
||||
let rev = String::from_utf8(output.stdout).expect("Failed to parse git output");
|
||||
println!("cargo:rustc-env=GIT_COMMIT_SHA={rev}");
|
||||
println!("cargo:rustc-rerun-if-changed=.git/HEAD");
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// Originally from https://gist.github.com/suluke/e0c672492126be0a4f3b4f0e1115d77c
|
||||
//! Extend `argp` to be better integrated with the `cargo` ecosystem
|
||||
//!
|
||||
//! For now, this only adds a --version/-V option which causes early-exit.
|
||||
use std::ffi::OsStr;
|
||||
|
||||
use argp::{parser::ParseGlobalOptions, EarlyExit, FromArgs, TopLevelCommand};
|
||||
|
||||
struct ArgsOrVersion<T>(T)
|
||||
where T: FromArgs;
|
||||
|
||||
impl<T> TopLevelCommand for ArgsOrVersion<T> where T: FromArgs {}
|
||||
|
||||
impl<T> FromArgs for ArgsOrVersion<T>
|
||||
where T: FromArgs
|
||||
{
|
||||
fn _from_args(
|
||||
command_name: &[&str],
|
||||
args: &[&OsStr],
|
||||
parent: Option<&mut dyn ParseGlobalOptions>,
|
||||
) -> Result<Self, EarlyExit> {
|
||||
/// Also use argp for catching `--version`-only invocations
|
||||
#[derive(FromArgs)]
|
||||
struct Version {
|
||||
/// Print version information and exit.
|
||||
#[argp(switch, short = 'V')]
|
||||
pub version: bool,
|
||||
}
|
||||
|
||||
match Version::from_args(command_name, args) {
|
||||
Ok(v) => {
|
||||
if v.version {
|
||||
println!(
|
||||
"{} {} {}",
|
||||
command_name.first().unwrap_or(&""),
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
env!("GIT_COMMIT_SHA"),
|
||||
);
|
||||
std::process::exit(0);
|
||||
} else {
|
||||
// Pass through empty arguments
|
||||
T::_from_args(command_name, args, parent).map(Self)
|
||||
}
|
||||
}
|
||||
Err(exit) => match exit {
|
||||
EarlyExit::Help(_help) => {
|
||||
// TODO: Chain help info from Version
|
||||
// For now, we just put the switch on T as well
|
||||
T::from_args(command_name, &["--help"]).map(Self)
|
||||
}
|
||||
EarlyExit::Err(_) => T::_from_args(command_name, args, parent).map(Self),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a `FromArgs` type from the current process’s `env::args`.
|
||||
///
|
||||
/// This function will exit early from the current process if argument parsing was unsuccessful or if information like `--help` was requested.
|
||||
/// Error messages will be printed to stderr, and `--help` output to stdout.
|
||||
pub fn from_env<T>() -> T
|
||||
where T: TopLevelCommand {
|
||||
argp::parse_args_or_exit::<ArgsOrVersion<T>>(argp::DEFAULT).0
|
||||
}
|
||||
+488
-118
File diff suppressed because it is too large
Load Diff
+118
-130
@@ -1,77 +1,109 @@
|
||||
use std::{
|
||||
io,
|
||||
io::{Cursor, Read, Seek, SeekFrom},
|
||||
io::{Read, Seek, SeekFrom},
|
||||
mem::size_of,
|
||||
};
|
||||
|
||||
use zerocopy::FromBytes;
|
||||
|
||||
use crate::{
|
||||
array_ref,
|
||||
disc::{
|
||||
AppLoaderHeader, DiscBase, DiscIO, DolHeader, Header, PartHeader, PartReadStream,
|
||||
PartitionHeader, PartitionType, SECTOR_SIZE,
|
||||
AppLoaderHeader, DiscBase, DiscHeader, DiscIO, DolHeader, PartitionBase, PartitionHeader,
|
||||
PartitionInfo, PartitionKind, PartitionMeta, BI2_SIZE, BOOT_SIZE, MINI_DVD_SIZE,
|
||||
SECTOR_SIZE,
|
||||
},
|
||||
fst::{find_node, read_fst, Node, NodeKind, NodeType},
|
||||
fst::{Node, NodeKind},
|
||||
streams::{ReadStream, SharedWindowedReadStream},
|
||||
util::{
|
||||
div_rem,
|
||||
reader::{read_bytes, FromReader},
|
||||
reader::{read_from, read_vec},
|
||||
},
|
||||
Error, Result, ResultContext,
|
||||
Error, OpenOptions, Result, ResultContext,
|
||||
};
|
||||
|
||||
pub(crate) struct DiscGCN {
|
||||
pub(crate) header: Header,
|
||||
pub(crate) header: DiscHeader,
|
||||
pub(crate) disc_size: u64,
|
||||
// pub(crate) junk_start: u64,
|
||||
}
|
||||
|
||||
impl DiscGCN {
|
||||
pub(crate) fn new(header: Header) -> Result<DiscGCN> { Ok(DiscGCN { header }) }
|
||||
pub(crate) fn new(
|
||||
_stream: &mut dyn ReadStream,
|
||||
header: DiscHeader,
|
||||
disc_size: Option<u64>,
|
||||
) -> Result<DiscGCN> {
|
||||
// stream.seek(SeekFrom::Start(size_of::<DiscHeader>() as u64)).context("Seeking to partition header")?;
|
||||
// let partition_header: PartitionHeader = read_from(stream).context("Reading partition header")?;
|
||||
// let junk_start = partition_header.fst_off(false) + partition_header.fst_sz(false);
|
||||
Ok(DiscGCN { header, disc_size: disc_size.unwrap_or(MINI_DVD_SIZE) /*, junk_start*/ })
|
||||
}
|
||||
}
|
||||
|
||||
fn open_partition<'a>(disc_io: &'a dyn DiscIO) -> Result<Box<dyn PartitionBase + 'a>> {
|
||||
let stream = disc_io.open()?;
|
||||
Ok(Box::new(PartitionGC { stream, offset: 0, cur_block: u32::MAX, buf: [0; SECTOR_SIZE] }))
|
||||
}
|
||||
|
||||
impl DiscBase for DiscGCN {
|
||||
fn get_header(&self) -> &Header { &self.header }
|
||||
fn header(&self) -> &DiscHeader { &self.header }
|
||||
|
||||
fn get_data_partition<'a>(
|
||||
&self,
|
||||
disc_io: &'a mut dyn DiscIO,
|
||||
_validate_hashes: bool,
|
||||
) -> Result<Box<dyn PartReadStream + 'a>> {
|
||||
let stream = disc_io.begin_read_stream(0).context("Opening data partition stream")?;
|
||||
Ok(Box::from(GCPartReadStream {
|
||||
stream,
|
||||
offset: 0,
|
||||
cur_block: u32::MAX,
|
||||
buf: [0; SECTOR_SIZE],
|
||||
}))
|
||||
fn partitions(&self) -> Vec<PartitionInfo> {
|
||||
vec![PartitionInfo {
|
||||
group_index: 0,
|
||||
part_index: 0,
|
||||
part_offset: 0,
|
||||
kind: PartitionKind::Data,
|
||||
data_offset: 0,
|
||||
data_size: self.disc_size,
|
||||
header: None,
|
||||
lfg_seed: *array_ref!(self.header.game_id, 0, 4),
|
||||
// junk_start: self.junk_start,
|
||||
}]
|
||||
}
|
||||
|
||||
fn get_partition<'a>(
|
||||
fn open_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!(
|
||||
disc_io: &'a dyn DiscIO,
|
||||
index: usize,
|
||||
_options: &OpenOptions,
|
||||
) -> Result<Box<dyn PartitionBase + 'a>> {
|
||||
if index != 0 {
|
||||
return Err(Error::DiscFormat(format!(
|
||||
"Invalid partition index {} for GameCube disc",
|
||||
index
|
||||
)));
|
||||
}
|
||||
open_partition(disc_io)
|
||||
}
|
||||
|
||||
fn open_partition_kind<'a>(
|
||||
&self,
|
||||
disc_io: &'a dyn DiscIO,
|
||||
part_type: PartitionKind,
|
||||
_options: &OpenOptions,
|
||||
) -> Result<Box<dyn PartitionBase + 'a>> {
|
||||
if part_type != PartitionKind::Data {
|
||||
return Err(Error::DiscFormat(format!(
|
||||
"Invalid partition type {:?} for GameCube disc",
|
||||
part_type
|
||||
)))
|
||||
)));
|
||||
}
|
||||
open_partition(disc_io)
|
||||
}
|
||||
|
||||
fn disc_size(&self) -> u64 { self.disc_size }
|
||||
}
|
||||
|
||||
struct GCPartReadStream<'a> {
|
||||
struct PartitionGC<'a> {
|
||||
stream: Box<dyn ReadStream + 'a>,
|
||||
offset: u64,
|
||||
cur_block: u32,
|
||||
buf: [u8; SECTOR_SIZE],
|
||||
}
|
||||
|
||||
impl<'a> Read for GCPartReadStream<'a> {
|
||||
impl<'a> Read for PartitionGC<'a> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let (block, block_offset) = div_rem(self.offset, SECTOR_SIZE as u64);
|
||||
let mut block = block as u32;
|
||||
@@ -104,12 +136,12 @@ impl<'a> Read for GCPartReadStream<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Seek for GCPartReadStream<'a> {
|
||||
impl<'a> Seek for PartitionGC<'a> {
|
||||
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
||||
self.offset = match pos {
|
||||
SeekFrom::Start(v) => v,
|
||||
SeekFrom::End(v) => (self.stable_stream_len()? as i64 + v) as u64,
|
||||
SeekFrom::Current(v) => (self.offset as i64 + v) as u64,
|
||||
SeekFrom::End(v) => self.stable_stream_len()?.saturating_add_signed(v),
|
||||
SeekFrom::Current(v) => self.offset.saturating_add_signed(v),
|
||||
};
|
||||
let block = self.offset / SECTOR_SIZE as u64;
|
||||
if block as u32 != self.cur_block {
|
||||
@@ -122,138 +154,94 @@ impl<'a> Seek for GCPartReadStream<'a> {
|
||||
fn stream_position(&mut self) -> io::Result<u64> { Ok(self.offset) }
|
||||
}
|
||||
|
||||
impl<'a> ReadStream for GCPartReadStream<'a> {
|
||||
impl<'a> ReadStream for PartitionGC<'a> {
|
||||
fn stable_stream_len(&mut self) -> io::Result<u64> { self.stream.stable_stream_len() }
|
||||
|
||||
fn as_dyn(&mut self) -> &mut dyn ReadStream { self }
|
||||
}
|
||||
|
||||
impl<'a> PartReadStream for GCPartReadStream<'a> {
|
||||
fn begin_file_stream(&mut self, node: &Node) -> io::Result<SharedWindowedReadStream> {
|
||||
assert_eq!(node.kind, NodeKind::File);
|
||||
self.new_window(node.offset as u64, node.length as u64)
|
||||
impl<'a> PartitionBase for PartitionGC<'a> {
|
||||
fn meta(&mut self) -> Result<Box<PartitionMeta>> {
|
||||
self.seek(SeekFrom::Start(0)).context("Seeking to partition header")?;
|
||||
read_part_header(self, false)
|
||||
}
|
||||
|
||||
fn read_header(&mut self) -> Result<Box<dyn PartHeader>> {
|
||||
self.seek(SeekFrom::Start(0)).context("Seeking to partition header")?;
|
||||
Ok(Box::from(read_part_header(self)?))
|
||||
fn open_file(&mut self, node: &Node) -> io::Result<SharedWindowedReadStream> {
|
||||
assert_eq!(node.kind(), NodeKind::File);
|
||||
self.new_window(node.offset(false), node.length(false))
|
||||
}
|
||||
|
||||
fn ideal_buffer_size(&self) -> usize { SECTOR_SIZE }
|
||||
}
|
||||
|
||||
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,
|
||||
partition_header: PartitionHeader,
|
||||
apploader_header: AppLoaderHeader,
|
||||
root_node: NodeType,
|
||||
dol_header: DolHeader,
|
||||
}
|
||||
|
||||
fn read_part_header<R>(reader: &mut R) -> Result<GCPartition>
|
||||
pub(crate) fn read_part_header<R>(reader: &mut R, is_wii: bool) -> Result<Box<PartitionMeta>>
|
||||
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");
|
||||
let raw_boot: [u8; BOOT_SIZE] = read_from(reader).context("Reading boot.bin")?;
|
||||
let partition_header = PartitionHeader::ref_from(&raw_boot[size_of::<DiscHeader>()..]).unwrap();
|
||||
|
||||
// bi2.bin
|
||||
let raw_bi2 = <[u8; BI2_SIZE]>::from_reader(reader).context("Reading bi2.bin")?;
|
||||
let raw_bi2: [u8; BI2_SIZE] = read_from(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")?;
|
||||
let mut raw_apploader: Vec<u8> =
|
||||
read_vec(reader, size_of::<AppLoaderHeader>()).context("Reading apploader header")?;
|
||||
let apploader_header = AppLoaderHeader::ref_from(raw_apploader.as_slice()).unwrap();
|
||||
raw_apploader.resize(
|
||||
AppLoaderHeader::STATIC_SIZE
|
||||
+ apploader_header.size as usize
|
||||
+ apploader_header.trailer_size as usize,
|
||||
size_of::<AppLoaderHeader>()
|
||||
+ apploader_header.size.get() as usize
|
||||
+ apploader_header.trailer_size.get() as usize,
|
||||
0,
|
||||
);
|
||||
reader
|
||||
.read_exact(&mut raw_apploader[AppLoaderHeader::STATIC_SIZE..])
|
||||
.read_exact(&mut raw_apploader[size_of::<AppLoaderHeader>()..])
|
||||
.context("Reading apploader")?;
|
||||
|
||||
// fst.bin
|
||||
reader
|
||||
.seek(SeekFrom::Start(partition_header.fst_off as u64))
|
||||
.seek(SeekFrom::Start(partition_header.fst_off(is_wii)))
|
||||
.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))?;
|
||||
let raw_fst: Vec<u8> = read_vec(reader, partition_header.fst_sz(is_wii) as usize)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Reading partition FST (offset {}, size {})",
|
||||
partition_header.fst_off, partition_header.fst_sz
|
||||
)
|
||||
})?;
|
||||
|
||||
// main.dol
|
||||
reader
|
||||
.seek(SeekFrom::Start(partition_header.dol_off as u64))
|
||||
.seek(SeekFrom::Start(partition_header.dol_off(is_wii)))
|
||||
.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 mut raw_dol: Vec<u8> =
|
||||
read_vec(reader, size_of::<DolHeader>()).context("Reading DOL header")?;
|
||||
let dol_header = DolHeader::ref_from(raw_dol.as_slice()).unwrap();
|
||||
let dol_size = dol_header
|
||||
.text_offs
|
||||
.iter()
|
||||
.zip(&dol_header.text_sizes)
|
||||
.map(|(offs, size)| offs + size)
|
||||
.map(|(offs, size)| offs.get() + size.get())
|
||||
.chain(
|
||||
dol_header.data_offs.iter().zip(&dol_header.data_sizes).map(|(offs, size)| offs + size),
|
||||
dol_header
|
||||
.data_offs
|
||||
.iter()
|
||||
.zip(&dol_header.data_sizes)
|
||||
.map(|(offs, size)| offs.get() + size.get()),
|
||||
)
|
||||
.max()
|
||||
.unwrap_or(DolHeader::STATIC_SIZE as u32);
|
||||
.unwrap_or(size_of::<DolHeader>() as u32);
|
||||
raw_dol.resize(dol_size as usize, 0);
|
||||
reader.read_exact(&mut raw_dol[DolHeader::STATIC_SIZE..]).context("Reading DOL")?;
|
||||
reader.read_exact(&mut raw_dol[size_of::<DolHeader>()..]).context("Reading DOL")?;
|
||||
|
||||
Ok(GCPartition {
|
||||
Ok(Box::new(PartitionMeta {
|
||||
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 }
|
||||
raw_ticket: None,
|
||||
raw_tmd: None,
|
||||
raw_cert_chain: None,
|
||||
raw_h3_table: None,
|
||||
}))
|
||||
}
|
||||
|
||||
+299
-290
File diff suppressed because it is too large
Load Diff
+372
-396
File diff suppressed because it is too large
Load Diff
+136
-161
@@ -1,17 +1,11 @@
|
||||
//! Disc file system types
|
||||
|
||||
use std::{
|
||||
ffi::CString,
|
||||
io,
|
||||
io::{Read, Seek, SeekFrom},
|
||||
};
|
||||
use std::{borrow::Cow, ffi::CStr, mem::size_of};
|
||||
|
||||
use encoding_rs::SHIFT_JIS;
|
||||
use zerocopy::{big_endian::*, AsBytes, FromBytes, FromZeroes};
|
||||
|
||||
use crate::{
|
||||
util::reader::{struct_size, FromReader, DYNAMIC_SIZE, U24},
|
||||
Result, ResultContext,
|
||||
};
|
||||
use crate::{static_assert, Result};
|
||||
|
||||
/// File system node kind.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -20,180 +14,161 @@ pub enum NodeKind {
|
||||
File,
|
||||
/// Node is a directory.
|
||||
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")),
|
||||
}
|
||||
}
|
||||
/// Invalid node kind. (Should not normally occur)
|
||||
Invalid,
|
||||
}
|
||||
|
||||
/// An individual file system node.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[derive(Clone, Debug, PartialEq, FromBytes, FromZeroes, AsBytes)]
|
||||
#[repr(C, align(4))]
|
||||
pub struct Node {
|
||||
kind: u8,
|
||||
// u24 big-endian
|
||||
name_offset: [u8; 3],
|
||||
offset: U32,
|
||||
length: U32,
|
||||
}
|
||||
|
||||
static_assert!(size_of::<Node>() == 12);
|
||||
|
||||
impl Node {
|
||||
/// File system node type.
|
||||
pub kind: NodeKind,
|
||||
pub fn kind(&self) -> NodeKind {
|
||||
match self.kind {
|
||||
0 => NodeKind::File,
|
||||
1 => NodeKind::Directory,
|
||||
_ => NodeKind::Invalid,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the node is a file.
|
||||
pub fn is_file(&self) -> bool { self.kind == 0 }
|
||||
|
||||
/// Whether the node is a directory.
|
||||
pub fn is_dir(&self) -> bool { self.kind == 1 }
|
||||
|
||||
/// Offset in the string table to the filename.
|
||||
pub name_offset: u32,
|
||||
pub fn name_offset(&self) -> u32 {
|
||||
u32::from_be_bytes([0, self.name_offset[0], self.name_offset[1], self.name_offset[2]])
|
||||
}
|
||||
|
||||
/// For files, this is the partition offset of the file data. (Wii: >> 2)
|
||||
///
|
||||
/// For directories, this is the children start offset in the FST.
|
||||
pub offset: u32,
|
||||
/// For directories, this is the parent node index in the FST.
|
||||
pub fn offset(&self, is_wii: bool) -> u64 {
|
||||
if is_wii && self.kind == 0 {
|
||||
self.offset.get() as u64 * 4
|
||||
} else {
|
||||
self.offset.get() as u64
|
||||
}
|
||||
}
|
||||
|
||||
/// For files, this is the byte size of the file.
|
||||
/// For files, this is the byte size of the file. (Wii: >> 2)
|
||||
///
|
||||
/// For directories, this is the children end offset in the FST.
|
||||
/// For directories, this is the child end index in the FST.
|
||||
///
|
||||
/// Number of child files and directories recursively is `length - offset`.
|
||||
pub length: u32,
|
||||
|
||||
/// The node name.
|
||||
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.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum NodeType {
|
||||
/// A single file node.
|
||||
File(Node),
|
||||
/// A directory node with children.
|
||||
Directory(Node, Vec<NodeType>),
|
||||
}
|
||||
|
||||
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)
|
||||
pub fn length(&self, is_wii: bool) -> u64 {
|
||||
if is_wii && self.kind == 0 {
|
||||
self.length.get() as u64 * 4
|
||||
} else {
|
||||
NodeType::File(node)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn read_node_name<R>(
|
||||
reader: &mut R,
|
||||
string_base: u64,
|
||||
node: &mut NodeType,
|
||||
root: bool,
|
||||
) -> io::Result<()>
|
||||
where
|
||||
R: Read + Seek + ?Sized,
|
||||
{
|
||||
let mut decode_name = |v: &mut Node| -> io::Result<()> {
|
||||
if !root {
|
||||
let offset = string_base + v.name_offset as u64;
|
||||
reader.seek(SeekFrom::Start(offset))?;
|
||||
|
||||
let c_string = CString::from_reader(reader)?;
|
||||
let (decoded, _, errors) = SHIFT_JIS.decode(c_string.as_bytes());
|
||||
if errors {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid shift-jis"));
|
||||
}
|
||||
v.name = decoded.into_owned();
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
match node {
|
||||
NodeType::File(inner) => {
|
||||
decode_name(inner)?;
|
||||
}
|
||||
NodeType::Directory(inner, children) => {
|
||||
decode_name(inner)?;
|
||||
for child in children {
|
||||
read_node_name(reader, string_base, child, false)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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_str().eq_ignore_ascii_case(name),
|
||||
NodeType::Directory(v, _) => {
|
||||
v.name.is_empty() /* root */ || v.name.as_str().eq_ignore_ascii_case(name)
|
||||
self.length.get() as u64
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn find_node<'a>(mut node: &'a NodeType, path: &str) -> Option<&'a NodeType> {
|
||||
let mut split = path.split('/');
|
||||
let mut current = split.next();
|
||||
while current.is_some() {
|
||||
if matches_name(node, current.unwrap()) {
|
||||
match node {
|
||||
NodeType::File(_) => {
|
||||
return if split.next().is_none() { Some(node) } else { None };
|
||||
/// A view into the file system tree (FST).
|
||||
pub struct Fst<'a> {
|
||||
pub nodes: &'a [Node],
|
||||
pub string_table: &'a [u8],
|
||||
}
|
||||
|
||||
impl<'a> Fst<'a> {
|
||||
/// Create a new FST view from a buffer.
|
||||
pub fn new(buf: &'a [u8]) -> Result<Self, &'static str> {
|
||||
let Some(root_node) = Node::ref_from_prefix(buf) else {
|
||||
return Err("FST root node not found");
|
||||
};
|
||||
// String table starts after the last node
|
||||
let string_base = root_node.length(false) * size_of::<Node>() as u64;
|
||||
if string_base >= buf.len() as u64 {
|
||||
return Err("FST string table out of bounds");
|
||||
}
|
||||
let (node_buf, string_table) = buf.split_at(string_base as usize);
|
||||
let nodes = Node::slice_from(node_buf).unwrap();
|
||||
Ok(Self { nodes, string_table })
|
||||
}
|
||||
|
||||
/// Iterate over the nodes in the FST.
|
||||
pub fn iter(&self) -> FstIter { FstIter { fst: self, idx: 1 } }
|
||||
|
||||
/// Get the name of a node.
|
||||
pub fn get_name(&self, node: &Node) -> Result<Cow<str>, String> {
|
||||
let name_buf = self.string_table.get(node.name_offset() as usize..).ok_or_else(|| {
|
||||
format!(
|
||||
"FST: name offset {} out of bounds (string table size: {})",
|
||||
node.name_offset(),
|
||||
self.string_table.len()
|
||||
)
|
||||
})?;
|
||||
let c_string = CStr::from_bytes_until_nul(name_buf).map_err(|_| {
|
||||
format!("FST: name at offset {} not null-terminated", node.name_offset())
|
||||
})?;
|
||||
let (decoded, _, errors) = SHIFT_JIS.decode(c_string.to_bytes());
|
||||
if errors {
|
||||
return Err(format!("FST: Failed to decode name at offset {}", node.name_offset()));
|
||||
}
|
||||
Ok(decoded)
|
||||
}
|
||||
|
||||
/// Finds a particular file or directory by path.
|
||||
pub fn find(&self, path: &str) -> Option<(usize, &Node)> {
|
||||
let mut split = path.trim_matches('/').split('/');
|
||||
let mut current = split.next()?;
|
||||
let mut idx = 1;
|
||||
let mut stop_at = None;
|
||||
while let Some(node) = self.nodes.get(idx) {
|
||||
if self.get_name(node).as_ref().map_or(false, |name| name.eq_ignore_ascii_case(current))
|
||||
{
|
||||
if let Some(next) = split.next() {
|
||||
current = next;
|
||||
} else {
|
||||
return Some((idx, node));
|
||||
}
|
||||
NodeType::Directory(v, c) => {
|
||||
// Find child
|
||||
if !v.name.is_empty() || current.unwrap().is_empty() {
|
||||
current = split.next();
|
||||
}
|
||||
if current.is_none() || current.unwrap().is_empty() {
|
||||
return if split.next().is_none() { Some(node) } else { None };
|
||||
}
|
||||
for x in c {
|
||||
if matches_name(x, current.unwrap()) {
|
||||
node = x;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Descend into directory
|
||||
idx += 1;
|
||||
stop_at = Some(node.length(false) as usize + idx);
|
||||
} else if node.is_dir() {
|
||||
// Skip directory
|
||||
idx = node.length(false) as usize;
|
||||
} else {
|
||||
// Skip file
|
||||
idx += 1;
|
||||
}
|
||||
if let Some(stop) = stop_at {
|
||||
if idx >= stop {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator over the nodes in an FST.
|
||||
pub struct FstIter<'a> {
|
||||
fst: &'a Fst<'a>,
|
||||
idx: usize,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for FstIter<'a> {
|
||||
type Item = (usize, &'a Node, Result<Cow<'a, str>, String>);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let idx = self.idx;
|
||||
let node = self.fst.nodes.get(idx)?;
|
||||
let name = self.fst.get_name(node);
|
||||
self.idx += 1;
|
||||
Some((idx, node, name))
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
+267
@@ -0,0 +1,267 @@
|
||||
use std::{
|
||||
cmp::min,
|
||||
io,
|
||||
io::{BufReader, Read, Seek, SeekFrom},
|
||||
mem::size_of,
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use zerocopy::{little_endian::*, AsBytes, FromBytes, FromZeroes};
|
||||
|
||||
use crate::{
|
||||
disc::{gcn::DiscGCN, wii::DiscWii, DiscBase, DL_DVD_SIZE, SECTOR_SIZE},
|
||||
io::{nkit::NKitHeader, split::SplitFileReader, DiscIO, MagicBytes},
|
||||
static_assert,
|
||||
util::{
|
||||
lfg::LaggedFibonacci,
|
||||
reader::{read_box_slice, read_from},
|
||||
},
|
||||
DiscHeader, DiscMeta, Error, PartitionInfo, ReadStream, Result, ResultContext,
|
||||
};
|
||||
|
||||
pub const CISO_MAGIC: MagicBytes = *b"CISO";
|
||||
pub const CISO_MAP_SIZE: usize = SECTOR_SIZE - 8;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, FromBytes, FromZeroes, AsBytes)]
|
||||
#[repr(C, align(4))]
|
||||
struct CISOHeader {
|
||||
magic: MagicBytes,
|
||||
// little endian
|
||||
block_size: U32,
|
||||
block_present: [u8; CISO_MAP_SIZE],
|
||||
}
|
||||
|
||||
static_assert!(size_of::<CISOHeader>() == SECTOR_SIZE);
|
||||
|
||||
pub struct DiscIOCISO {
|
||||
inner: SplitFileReader,
|
||||
header: CISOHeader,
|
||||
block_map: [u16; CISO_MAP_SIZE],
|
||||
nkit_header: Option<NKitHeader>,
|
||||
junk_blocks: Option<Box<[u8]>>,
|
||||
partitions: Vec<PartitionInfo>,
|
||||
disc_num: u8,
|
||||
}
|
||||
|
||||
impl DiscIOCISO {
|
||||
pub fn new(filename: &Path) -> Result<Self> {
|
||||
let mut inner = BufReader::new(SplitFileReader::new(filename)?);
|
||||
|
||||
// Read header
|
||||
let header: CISOHeader = read_from(&mut inner).context("Reading CISO header")?;
|
||||
if header.magic != CISO_MAGIC {
|
||||
return Err(Error::DiscFormat("Invalid CISO magic".to_string()));
|
||||
}
|
||||
|
||||
// Build block map
|
||||
let mut block_map = [0u16; CISO_MAP_SIZE];
|
||||
let mut block = 0u16;
|
||||
for (presence, out) in header.block_present.iter().zip(block_map.iter_mut()) {
|
||||
if *presence == 1 {
|
||||
*out = block;
|
||||
block += 1;
|
||||
} else {
|
||||
*out = u16::MAX;
|
||||
}
|
||||
}
|
||||
let file_size = SECTOR_SIZE as u64 + block as u64 * header.block_size.get() as u64;
|
||||
if file_size > inner.get_ref().len() {
|
||||
return Err(Error::DiscFormat(format!(
|
||||
"CISO file size mismatch: expected at least {} bytes, got {}",
|
||||
file_size,
|
||||
inner.get_ref().len()
|
||||
)));
|
||||
}
|
||||
|
||||
// Read NKit header if present (after CISO data)
|
||||
let nkit_header = if inner.get_ref().len() > file_size + 4 {
|
||||
inner.seek(SeekFrom::Start(file_size)).context("Seeking to NKit header")?;
|
||||
NKitHeader::try_read_from(&mut inner)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Read junk data bitstream if present (after NKit header)
|
||||
let junk_blocks = if nkit_header.is_some() {
|
||||
let n = 1 + DL_DVD_SIZE / header.block_size.get() as u64 / 8;
|
||||
Some(read_box_slice(&mut inner, n as usize).context("Reading NKit bitstream")?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let (partitions, disc_num) = if junk_blocks.is_some() {
|
||||
let mut stream: Box<dyn ReadStream> = Box::new(CISOReadStream {
|
||||
inner: BufReader::new(inner.get_ref().clone()),
|
||||
block_size: header.block_size.get(),
|
||||
block_map,
|
||||
cur_block: u16::MAX,
|
||||
pos: 0,
|
||||
junk_blocks: None,
|
||||
partitions: vec![],
|
||||
disc_num: 0,
|
||||
});
|
||||
let header: DiscHeader = read_from(stream.as_mut()).context("Reading disc header")?;
|
||||
let disc_num = header.disc_num;
|
||||
let disc_base: Box<dyn DiscBase> = if header.is_wii() {
|
||||
Box::new(DiscWii::new(stream.as_mut(), header, None)?)
|
||||
} else if header.is_gamecube() {
|
||||
Box::new(DiscGCN::new(stream.as_mut(), header, None)?)
|
||||
} else {
|
||||
return Err(Error::DiscFormat(format!(
|
||||
"Invalid GC/Wii magic: {:#010X}/{:#010X}",
|
||||
header.gcn_magic.get(),
|
||||
header.wii_magic.get()
|
||||
)));
|
||||
};
|
||||
(disc_base.partitions(), disc_num)
|
||||
} else {
|
||||
(vec![], 0)
|
||||
};
|
||||
|
||||
// Reset reader
|
||||
let mut inner = inner.into_inner();
|
||||
inner.reset();
|
||||
Ok(Self { inner, header, block_map, nkit_header, junk_blocks, partitions, disc_num })
|
||||
}
|
||||
}
|
||||
|
||||
impl DiscIO for DiscIOCISO {
|
||||
fn open(&self) -> Result<Box<dyn ReadStream>> {
|
||||
Ok(Box::new(CISOReadStream {
|
||||
inner: BufReader::new(self.inner.clone()),
|
||||
block_size: self.header.block_size.get(),
|
||||
block_map: self.block_map,
|
||||
cur_block: u16::MAX,
|
||||
pos: 0,
|
||||
junk_blocks: self.junk_blocks.clone(),
|
||||
partitions: self.partitions.clone(),
|
||||
disc_num: self.disc_num,
|
||||
}))
|
||||
}
|
||||
|
||||
fn meta(&self) -> Result<DiscMeta> {
|
||||
Ok(self.nkit_header.as_ref().map(DiscMeta::from).unwrap_or_default())
|
||||
}
|
||||
|
||||
fn disc_size(&self) -> Option<u64> { self.nkit_header.as_ref().and_then(|h| h.size) }
|
||||
}
|
||||
|
||||
struct CISOReadStream {
|
||||
inner: BufReader<SplitFileReader>,
|
||||
block_size: u32,
|
||||
block_map: [u16; CISO_MAP_SIZE],
|
||||
cur_block: u16,
|
||||
pos: u64,
|
||||
|
||||
// Data for recreating junk data
|
||||
junk_blocks: Option<Box<[u8]>>,
|
||||
partitions: Vec<PartitionInfo>,
|
||||
disc_num: u8,
|
||||
}
|
||||
|
||||
impl CISOReadStream {
|
||||
fn read_junk_data(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let Some(junk_blocks) = self.junk_blocks.as_deref() else {
|
||||
return Ok(0);
|
||||
};
|
||||
let block_size = self.block_size as u64;
|
||||
let block = (self.pos / block_size) as u16;
|
||||
if junk_blocks[(block / 8) as usize] & (1 << (7 - (block & 7))) == 0 {
|
||||
return Ok(0);
|
||||
}
|
||||
let Some(partition) = self.partitions.iter().find(|p| {
|
||||
let start = p.part_offset + p.data_offset;
|
||||
start <= self.pos && self.pos < start + p.data_size
|
||||
}) else {
|
||||
log::warn!("No partition found for junk data at offset {:#x}", self.pos);
|
||||
return Ok(0);
|
||||
};
|
||||
let offset = self.pos - (partition.part_offset + partition.data_offset);
|
||||
let to_read = min(
|
||||
buf.len(),
|
||||
// The LFG is only valid for a single sector
|
||||
SECTOR_SIZE - (offset % SECTOR_SIZE as u64) as usize,
|
||||
);
|
||||
let mut lfg = LaggedFibonacci::default();
|
||||
lfg.init_with_seed(partition.lfg_seed, self.disc_num, offset);
|
||||
lfg.fill(&mut buf[..to_read]);
|
||||
self.pos += to_read as u64;
|
||||
Ok(to_read)
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for CISOReadStream {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let block_size = self.block_size as u64;
|
||||
let block = (self.pos / block_size) as u16;
|
||||
let block_offset = self.pos & (block_size - 1);
|
||||
if block != self.cur_block {
|
||||
if block >= CISO_MAP_SIZE as u16 {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
// Find the block in the map
|
||||
let phys_block = self.block_map[block as usize];
|
||||
if phys_block == u16::MAX {
|
||||
// Try to recreate junk data
|
||||
let read = self.read_junk_data(buf)?;
|
||||
if read > 0 {
|
||||
return Ok(read);
|
||||
}
|
||||
|
||||
// Otherwise, read zeroes
|
||||
let to_read = min(buf.len(), (block_size - block_offset) as usize);
|
||||
buf[..to_read].fill(0);
|
||||
self.pos += to_read as u64;
|
||||
return Ok(to_read);
|
||||
}
|
||||
|
||||
// Seek to the new block
|
||||
let file_offset =
|
||||
size_of::<CISOHeader>() as u64 + phys_block as u64 * block_size + block_offset;
|
||||
self.inner.seek(SeekFrom::Start(file_offset))?;
|
||||
self.cur_block = block;
|
||||
}
|
||||
|
||||
let to_read = min(buf.len(), (block_size - block_offset) as usize);
|
||||
let read = self.inner.read(&mut buf[..to_read])?;
|
||||
self.pos += read as u64;
|
||||
Ok(read)
|
||||
}
|
||||
}
|
||||
|
||||
impl Seek for CISOReadStream {
|
||||
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
||||
let new_pos = match pos {
|
||||
SeekFrom::Start(v) => v,
|
||||
SeekFrom::End(_) => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Unsupported,
|
||||
"CISOReadStream: SeekFrom::End is not supported",
|
||||
));
|
||||
}
|
||||
SeekFrom::Current(v) => self.pos.saturating_add_signed(v),
|
||||
};
|
||||
|
||||
let block_size = self.block_size as u64;
|
||||
let new_block = (self.pos / block_size) as u16;
|
||||
if new_block == self.cur_block {
|
||||
// Seek within the same block
|
||||
self.inner.seek(SeekFrom::Current(new_pos as i64 - self.pos as i64))?;
|
||||
} else {
|
||||
// Seek to a different block, handled by next read
|
||||
self.cur_block = u16::MAX;
|
||||
}
|
||||
|
||||
self.pos = new_pos;
|
||||
Ok(new_pos)
|
||||
}
|
||||
}
|
||||
|
||||
impl ReadStream for CISOReadStream {
|
||||
fn stable_stream_len(&mut self) -> io::Result<u64> {
|
||||
Ok(self.block_size as u64 * CISO_MAP_SIZE as u64)
|
||||
}
|
||||
|
||||
fn as_dyn(&mut self) -> &mut dyn ReadStream { self }
|
||||
}
|
||||
+13
-37
@@ -1,49 +1,25 @@
|
||||
use std::{
|
||||
fs::File,
|
||||
io,
|
||||
io::{Seek, SeekFrom},
|
||||
path::{Path, PathBuf},
|
||||
use std::{io::BufReader, path::Path};
|
||||
|
||||
use crate::{
|
||||
io::{split::SplitFileReader, DiscIO},
|
||||
streams::ReadStream,
|
||||
Result,
|
||||
};
|
||||
|
||||
use crate::{io::DiscIO, streams::ReadStream, Result};
|
||||
|
||||
pub(crate) struct DiscIOISO {
|
||||
pub(crate) filename: PathBuf,
|
||||
pub struct DiscIOISO {
|
||||
pub inner: SplitFileReader,
|
||||
}
|
||||
|
||||
impl DiscIOISO {
|
||||
pub(crate) fn new(filename: &Path) -> Result<DiscIOISO> {
|
||||
Ok(DiscIOISO { filename: filename.to_owned() })
|
||||
pub fn new(filename: &Path) -> Result<Self> {
|
||||
Ok(Self { inner: SplitFileReader::new(filename)? })
|
||||
}
|
||||
}
|
||||
|
||||
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))?;
|
||||
Ok(Box::from(file))
|
||||
fn open(&self) -> Result<Box<dyn ReadStream>> {
|
||||
Ok(Box::new(BufReader::new(self.inner.clone())))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct DiscIOISOStream<T>
|
||||
where T: ReadStream + Sized
|
||||
{
|
||||
pub(crate) stream: T,
|
||||
}
|
||||
|
||||
impl<T> DiscIOISOStream<T>
|
||||
where T: ReadStream + Sized
|
||||
{
|
||||
pub(crate) fn new(stream: T) -> Result<DiscIOISOStream<T>> { Ok(DiscIOISOStream { stream }) }
|
||||
}
|
||||
|
||||
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)?;
|
||||
stream.seek(SeekFrom::Start(offset))?;
|
||||
Ok(Box::from(stream))
|
||||
}
|
||||
fn disc_size(&self) -> Option<u64> { Some(self.inner.len()) }
|
||||
}
|
||||
|
||||
+58
-86
@@ -1,52 +1,52 @@
|
||||
//! Disc file format related logic (ISO, NFS, etc)
|
||||
//! Disc file format related logic (CISO, NFS, WBFS, WIA, etc.)
|
||||
|
||||
use std::{fs, io, path::Path};
|
||||
use std::{fs, fs::File, path::Path};
|
||||
|
||||
use crate::{
|
||||
io::{
|
||||
iso::{DiscIOISO, DiscIOISOStream},
|
||||
nfs::DiscIONFS,
|
||||
wia::DiscIOWIA,
|
||||
},
|
||||
streams::{ByteReadStream, ReadStream},
|
||||
Error, Result,
|
||||
streams::ReadStream, util::reader::read_from, Error, OpenOptions, Result, ResultContext,
|
||||
};
|
||||
|
||||
pub(crate) mod ciso;
|
||||
pub(crate) mod iso;
|
||||
pub(crate) mod nfs;
|
||||
pub(crate) mod nkit;
|
||||
pub(crate) mod split;
|
||||
pub(crate) mod wbfs;
|
||||
pub(crate) mod wia;
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct DiscIOOptions {
|
||||
/// Rebuild hashes for the disc image.
|
||||
pub rebuild_hashes: bool,
|
||||
}
|
||||
/// SHA-1 hash bytes
|
||||
pub(crate) type HashBytes = [u8; 20];
|
||||
|
||||
/// Abstraction over supported disc file types.
|
||||
/// AES key bytes
|
||||
pub(crate) type KeyBytes = [u8; 16];
|
||||
|
||||
/// Magic bytes
|
||||
pub(crate) type MagicBytes = [u8; 4];
|
||||
|
||||
/// Abstraction over supported disc file formats.
|
||||
pub trait DiscIO: Send + Sync {
|
||||
/// Opens a new read stream for the disc file(s).
|
||||
/// Generally does _not_ need to be used directly.
|
||||
fn begin_read_stream(&mut self, offset: u64) -> io::Result<Box<dyn ReadStream + '_>>;
|
||||
fn open(&self) -> Result<Box<dyn ReadStream + '_>>;
|
||||
|
||||
/// If false, the file format does not use standard Wii partition encryption. (e.g. NFS)
|
||||
fn has_wii_crypto(&self) -> bool { true }
|
||||
/// Returns extra metadata included in the disc file format, if any.
|
||||
fn meta(&self) -> Result<DiscMeta> { Ok(DiscMeta::default()) }
|
||||
|
||||
/// If None, the file format does not store the original disc size. (e.g. WBFS, NFS)
|
||||
fn disc_size(&self) -> Option<u64>;
|
||||
}
|
||||
|
||||
/// Extra metadata included in some disc file formats.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct DiscMeta {
|
||||
pub crc32: Option<u32>,
|
||||
pub md5: Option<[u8; 16]>,
|
||||
pub sha1: Option<[u8; 20]>,
|
||||
pub xxhash64: Option<u64>,
|
||||
}
|
||||
|
||||
/// Creates a new [`DiscIO`] instance.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Basic usage:
|
||||
/// ```no_run
|
||||
/// use nod::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)?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn new_disc_io(filename: &Path, options: &DiscIOOptions) -> Result<Box<dyn DiscIO>> {
|
||||
pub fn open(filename: &Path, options: &OpenOptions) -> Result<Box<dyn DiscIO>> {
|
||||
let path_result = fs::canonicalize(filename);
|
||||
if let Err(err) = path_result {
|
||||
return Err(Error::Io(format!("Failed to open {}", filename.display()), err));
|
||||
@@ -59,66 +59,38 @@ pub fn new_disc_io(filename: &Path, options: &DiscIOOptions) -> Result<Box<dyn D
|
||||
if !meta.unwrap().is_file() {
|
||||
return Err(Error::DiscFormat(format!("Input is not a file: {}", filename.display())));
|
||||
}
|
||||
if has_extension(path, "iso") {
|
||||
Ok(Box::from(DiscIOISO::new(path)?))
|
||||
} else if has_extension(path, "nfs") {
|
||||
match path.parent() {
|
||||
let magic: MagicBytes = {
|
||||
let mut file =
|
||||
File::open(path).with_context(|| format!("Opening file {}", filename.display()))?;
|
||||
read_from(&mut file)
|
||||
.with_context(|| format!("Reading magic bytes from {}", filename.display()))?
|
||||
};
|
||||
match magic {
|
||||
ciso::CISO_MAGIC => Ok(Box::new(ciso::DiscIOCISO::new(path)?)),
|
||||
nfs::NFS_MAGIC => match path.parent() {
|
||||
Some(parent) if parent.is_dir() => {
|
||||
Ok(Box::from(DiscIONFS::new(path.parent().unwrap())?))
|
||||
Ok(Box::new(nfs::DiscIONFS::new(path.parent().unwrap(), options)?))
|
||||
}
|
||||
_ => 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 {
|
||||
Err(Error::DiscFormat("Unknown file type".to_string()))
|
||||
},
|
||||
wbfs::WBFS_MAGIC => Ok(Box::new(wbfs::DiscIOWBFS::new(path)?)),
|
||||
wia::WIA_MAGIC | wia::RVZ_MAGIC => Ok(Box::new(wia::DiscIOWIA::new(path, options)?)),
|
||||
_ => Ok(Box::new(iso::DiscIOISO::new(path)?)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new [`DiscIO`] instance from a byte slice.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Basic usage:
|
||||
/// ```no_run
|
||||
/// use nod::io::new_disc_io_from_buf;
|
||||
///
|
||||
/// # 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 })
|
||||
/// Encrypts data in-place using AES-128-CBC with the given key and IV.
|
||||
pub(crate) fn aes_encrypt(key: &KeyBytes, iv: KeyBytes, data: &mut [u8]) {
|
||||
use aes::cipher::{block_padding::NoPadding, BlockEncryptMut, KeyIvInit};
|
||||
<cbc::Encryptor<aes::Aes128>>::new(key.into(), &aes::Block::from(iv))
|
||||
.encrypt_padded_mut::<NoPadding>(data, data.len())
|
||||
.unwrap(); // Safe: using NoPadding
|
||||
}
|
||||
|
||||
/// Creates a new [`DiscIO`] instance from an existing [`ReadStream`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Basic usage:
|
||||
/// ```no_run
|
||||
/// use nod::{io::new_disc_io_from_stream, streams::ByteReadStream};
|
||||
///
|
||||
/// # 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,
|
||||
) -> Result<Box<dyn DiscIO + 'a>> {
|
||||
Ok(Box::from(DiscIOISOStream::new(stream)?))
|
||||
}
|
||||
|
||||
/// Helper function for checking a file extension.
|
||||
#[inline(always)]
|
||||
pub fn has_extension(filename: &Path, extension: &str) -> bool {
|
||||
match filename.extension() {
|
||||
Some(ext) => ext.eq_ignore_ascii_case(extension),
|
||||
None => false,
|
||||
}
|
||||
/// Decrypts data in-place using AES-128-CBC with the given key and IV.
|
||||
pub(crate) fn aes_decrypt(key: &KeyBytes, iv: KeyBytes, data: &mut [u8]) {
|
||||
use aes::cipher::{block_padding::NoPadding, BlockDecryptMut, KeyIvInit};
|
||||
<cbc::Decryptor<aes::Aes128>>::new(key.into(), &aes::Block::from(iv))
|
||||
.decrypt_padded_mut::<NoPadding>(data)
|
||||
.unwrap(); // Safe: using NoPadding
|
||||
}
|
||||
|
||||
+258
-266
File diff suppressed because it is too large
Load Diff
+146
@@ -0,0 +1,146 @@
|
||||
use std::{
|
||||
io,
|
||||
io::{Read, Seek, SeekFrom},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
io::MagicBytes,
|
||||
util::reader::{read_from, read_u16_be, read_u32_be, read_u64_be, read_vec},
|
||||
DiscMeta,
|
||||
};
|
||||
|
||||
#[allow(unused)]
|
||||
#[repr(u16)]
|
||||
enum NKitHeaderFlags {
|
||||
Size = 0x1,
|
||||
Crc32 = 0x2,
|
||||
Md5 = 0x4,
|
||||
Sha1 = 0x8,
|
||||
Xxhash64 = 0x10,
|
||||
Key = 0x20,
|
||||
Encrypted = 0x40,
|
||||
ExtraData = 0x80,
|
||||
IndexFile = 0x100,
|
||||
}
|
||||
|
||||
const NKIT_HEADER_V1_FLAGS: u16 = NKitHeaderFlags::Crc32 as u16
|
||||
| NKitHeaderFlags::Md5 as u16
|
||||
| NKitHeaderFlags::Sha1 as u16
|
||||
| NKitHeaderFlags::Xxhash64 as u16;
|
||||
|
||||
const fn calc_header_size(version: u8, flags: u16, key_len: u32) -> usize {
|
||||
let mut size = 8;
|
||||
if version >= 2 {
|
||||
// header size + flags
|
||||
size += 4;
|
||||
}
|
||||
if flags & NKitHeaderFlags::Size as u16 != 0 {
|
||||
size += 8;
|
||||
}
|
||||
if flags & NKitHeaderFlags::Crc32 as u16 != 0 {
|
||||
size += 4;
|
||||
}
|
||||
if flags & NKitHeaderFlags::Md5 as u16 != 0 {
|
||||
size += 16;
|
||||
}
|
||||
if flags & NKitHeaderFlags::Sha1 as u16 != 0 {
|
||||
size += 20;
|
||||
}
|
||||
if flags & NKitHeaderFlags::Xxhash64 as u16 != 0 {
|
||||
size += 8;
|
||||
}
|
||||
if flags & NKitHeaderFlags::Key as u16 != 0 {
|
||||
size += key_len as usize + 2;
|
||||
}
|
||||
size
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NKitHeader {
|
||||
pub version: u8,
|
||||
pub flags: u16,
|
||||
pub size: Option<u64>,
|
||||
pub crc32: Option<u32>,
|
||||
pub md5: Option<[u8; 16]>,
|
||||
pub sha1: Option<[u8; 20]>,
|
||||
pub xxhash64: Option<u64>,
|
||||
}
|
||||
|
||||
const VERSION_PREFIX: [u8; 7] = *b"NKIT v";
|
||||
|
||||
impl NKitHeader {
|
||||
pub fn try_read_from<R>(reader: &mut R) -> Option<Self>
|
||||
where R: Read + Seek + ?Sized {
|
||||
let magic: MagicBytes = read_from(reader).ok()?;
|
||||
if magic == *b"NKIT" {
|
||||
reader.seek(SeekFrom::Current(-4)).ok()?;
|
||||
match NKitHeader::read_from(reader) {
|
||||
Ok(header) => Some(header),
|
||||
Err(e) => {
|
||||
log::warn!("Failed to read NKit header: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_from<R>(reader: &mut R) -> io::Result<Self>
|
||||
where R: Read + ?Sized {
|
||||
let version_string: [u8; 8] = read_from(reader)?;
|
||||
if version_string[0..7] != VERSION_PREFIX
|
||||
|| version_string[7] < b'1'
|
||||
|| version_string[7] > b'9'
|
||||
{
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"Invalid NKit header version string",
|
||||
));
|
||||
}
|
||||
let version = version_string[7] - b'0';
|
||||
let header_size = match version {
|
||||
1 => calc_header_size(version, NKIT_HEADER_V1_FLAGS, 0) as u16,
|
||||
2 => read_u16_be(reader)?,
|
||||
_ => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("Unsupported NKit header version: {}", version),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let mut remaining_header_size = header_size as usize - 8;
|
||||
if version >= 2 {
|
||||
// We read the header size already
|
||||
remaining_header_size -= 2;
|
||||
}
|
||||
let header_bytes = read_vec(reader, remaining_header_size)?;
|
||||
let mut reader = &header_bytes[..];
|
||||
|
||||
let flags = if version == 1 { NKIT_HEADER_V1_FLAGS } else { read_u16_be(&mut reader)? };
|
||||
let size = (flags & NKitHeaderFlags::Size as u16 != 0)
|
||||
.then(|| read_u64_be(&mut reader))
|
||||
.transpose()?;
|
||||
let crc32 = (flags & NKitHeaderFlags::Crc32 as u16 != 0)
|
||||
.then(|| read_u32_be(&mut reader))
|
||||
.transpose()?;
|
||||
let md5 = (flags & NKitHeaderFlags::Md5 as u16 != 0)
|
||||
.then(|| read_from::<[u8; 16], _>(&mut reader))
|
||||
.transpose()?;
|
||||
let sha1 = (flags & NKitHeaderFlags::Sha1 as u16 != 0)
|
||||
.then(|| read_from::<[u8; 20], _>(&mut reader))
|
||||
.transpose()?;
|
||||
let xxhash64 = (flags & NKitHeaderFlags::Xxhash64 as u16 != 0)
|
||||
.then(|| read_u64_be(&mut reader))
|
||||
.transpose()?;
|
||||
Ok(Self { version, flags, size, crc32, md5, sha1, xxhash64 })
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&NKitHeader> for DiscMeta {
|
||||
fn from(value: &NKitHeader) -> Self {
|
||||
Self { crc32: value.crc32, md5: value.md5, sha1: value.sha1, xxhash64: value.xxhash64 }
|
||||
}
|
||||
}
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
use std::{
|
||||
fs::File,
|
||||
io,
|
||||
io::{Read, Seek, SeekFrom},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use crate::{ErrorContext, ReadStream, Result, ResultContext};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SplitFileReader {
|
||||
files: Vec<Split<PathBuf>>,
|
||||
open_file: Option<Split<File>>,
|
||||
pos: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Split<T> {
|
||||
inner: T,
|
||||
begin: u64,
|
||||
size: u64,
|
||||
}
|
||||
|
||||
impl<T> Split<T> {
|
||||
fn contains(&self, pos: u64) -> bool { self.begin <= pos && pos < self.begin + self.size }
|
||||
}
|
||||
|
||||
// .iso.1, .iso.2, etc.
|
||||
fn split_path_1(input: &Path, index: u32) -> PathBuf {
|
||||
let input_str = input.to_str().unwrap_or("[INVALID]");
|
||||
let mut out = input_str.to_string();
|
||||
out.push('.');
|
||||
out.push(char::from_digit(index, 10).unwrap());
|
||||
PathBuf::from(out)
|
||||
}
|
||||
|
||||
// .part1.iso, .part2.iso, etc.
|
||||
fn split_path_2(input: &Path, index: u32) -> PathBuf {
|
||||
let extension = input.extension().and_then(|s| s.to_str()).unwrap_or("iso");
|
||||
let input_without_ext = input.with_extension("");
|
||||
let input_str = input_without_ext.to_str().unwrap_or("[INVALID]");
|
||||
let mut out = input_str.to_string();
|
||||
out.push_str(".part");
|
||||
out.push(char::from_digit(index, 10).unwrap());
|
||||
out.push('.');
|
||||
out.push_str(extension);
|
||||
PathBuf::from(out)
|
||||
}
|
||||
|
||||
// .wbf1, .wbf2, etc.
|
||||
fn split_path_3(input: &Path, index: u32) -> PathBuf {
|
||||
let input_str = input.to_str().unwrap_or("[INVALID]");
|
||||
let mut chars = input_str.chars();
|
||||
chars.next_back();
|
||||
let mut out = chars.as_str().to_string();
|
||||
out.push(char::from_digit(index, 10).unwrap());
|
||||
PathBuf::from(out)
|
||||
}
|
||||
|
||||
impl SplitFileReader {
|
||||
pub fn empty() -> Self { Self { files: Vec::new(), open_file: None, pos: 0 } }
|
||||
|
||||
pub fn new(path: &Path) -> Result<Self> {
|
||||
let mut files = vec![];
|
||||
let mut begin = 0;
|
||||
match path.metadata() {
|
||||
Ok(metadata) => {
|
||||
files.push(Split { inner: path.to_path_buf(), begin, size: metadata.len() });
|
||||
begin += metadata.len();
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(e.context(format!("Failed to stat file {}", path.display())));
|
||||
}
|
||||
}
|
||||
for path_fn in [split_path_1, split_path_2, split_path_3] {
|
||||
let mut index = 1;
|
||||
loop {
|
||||
let path = path_fn(path, index);
|
||||
if let Ok(metadata) = path.metadata() {
|
||||
files.push(Split { inner: path, begin, size: metadata.len() });
|
||||
begin += metadata.len();
|
||||
index += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if index > 1 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(Self { files, open_file: None, pos: 0 })
|
||||
}
|
||||
|
||||
pub fn add(&mut self, path: &Path) -> Result<()> {
|
||||
let begin = self.len();
|
||||
let metadata =
|
||||
path.metadata().context(format!("Failed to stat file {}", path.display()))?;
|
||||
self.files.push(Split { inner: path.to_path_buf(), begin, size: metadata.len() });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.open_file = None;
|
||||
self.pos = 0;
|
||||
}
|
||||
|
||||
pub fn len(&self) -> u64 { self.files.last().map_or(0, |f| f.begin + f.size) }
|
||||
}
|
||||
|
||||
impl Read for SplitFileReader {
|
||||
fn read(&mut self, mut buf: &mut [u8]) -> io::Result<usize> {
|
||||
let mut total = 0;
|
||||
while !buf.is_empty() {
|
||||
if let Some(split) = &mut self.open_file {
|
||||
let n = buf.len().min((split.begin + split.size - self.pos) as usize);
|
||||
if n == 0 {
|
||||
self.open_file = None;
|
||||
continue;
|
||||
}
|
||||
split.inner.read_exact(&mut buf[..n])?;
|
||||
total += n;
|
||||
self.pos += n as u64;
|
||||
buf = &mut buf[n..];
|
||||
} else if let Some(split) = self.files.iter().find(|f| f.contains(self.pos)) {
|
||||
let mut file = File::open(&split.inner)?;
|
||||
if self.pos > split.begin {
|
||||
file.seek(SeekFrom::Start(self.pos - split.begin))?;
|
||||
}
|
||||
self.open_file = Some(Split { inner: file, begin: split.begin, size: split.size });
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(total)
|
||||
}
|
||||
}
|
||||
|
||||
impl Seek for SplitFileReader {
|
||||
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
||||
self.pos = match pos {
|
||||
SeekFrom::Start(pos) => pos,
|
||||
SeekFrom::Current(offset) => self.pos.saturating_add_signed(offset),
|
||||
SeekFrom::End(offset) => self.len().saturating_add_signed(offset),
|
||||
};
|
||||
if let Some(split) = &mut self.open_file {
|
||||
if split.contains(self.pos) {
|
||||
// Seek within the open file
|
||||
split.inner.seek(SeekFrom::Start(self.pos - split.begin))?;
|
||||
} else {
|
||||
self.open_file = None;
|
||||
}
|
||||
}
|
||||
Ok(self.pos)
|
||||
}
|
||||
}
|
||||
|
||||
impl ReadStream for SplitFileReader {
|
||||
fn stable_stream_len(&mut self) -> io::Result<u64> { Ok(self.len()) }
|
||||
|
||||
fn as_dyn(&mut self) -> &mut dyn ReadStream { self }
|
||||
}
|
||||
|
||||
impl Clone for SplitFileReader {
|
||||
fn clone(&self) -> Self { Self { files: self.files.clone(), open_file: None, pos: self.pos } }
|
||||
}
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
use std::{
|
||||
cmp::min,
|
||||
io,
|
||||
io::{BufReader, Read, Seek, SeekFrom},
|
||||
mem::size_of,
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use zerocopy::{big_endian::*, AsBytes, FromBytes, FromZeroes};
|
||||
|
||||
use crate::{
|
||||
disc::SECTOR_SIZE,
|
||||
io::{nkit::NKitHeader, split::SplitFileReader, DiscIO, DiscMeta, MagicBytes},
|
||||
util::reader::{read_from, read_vec},
|
||||
Error, ReadStream, Result, ResultContext,
|
||||
};
|
||||
|
||||
pub const WBFS_MAGIC: MagicBytes = *b"WBFS";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, FromBytes, FromZeroes, AsBytes)]
|
||||
#[repr(C, align(4))]
|
||||
struct WBFSHeader {
|
||||
magic: MagicBytes,
|
||||
num_sectors: U32,
|
||||
sector_size_shift: u8,
|
||||
wbfs_sector_size_shift: u8,
|
||||
_pad: [u8; 2],
|
||||
}
|
||||
|
||||
impl WBFSHeader {
|
||||
fn sector_size(&self) -> u32 { 1 << self.sector_size_shift }
|
||||
|
||||
fn wbfs_sector_size(&self) -> u32 { 1 << self.wbfs_sector_size_shift }
|
||||
|
||||
// fn align_lba(&self, x: u32) -> u32 { (x + self.sector_size() - 1) & !(self.sector_size() - 1) }
|
||||
//
|
||||
// fn num_wii_sectors(&self) -> u32 {
|
||||
// (self.num_sectors.get() / SECTOR_SIZE as u32) * self.sector_size()
|
||||
// }
|
||||
//
|
||||
// fn max_wii_sectors(&self) -> u32 { NUM_WII_SECTORS }
|
||||
//
|
||||
// fn num_wbfs_sectors(&self) -> u32 {
|
||||
// self.num_wii_sectors() >> (self.wbfs_sector_size_shift - 15)
|
||||
// }
|
||||
|
||||
fn max_wbfs_sectors(&self) -> u32 { NUM_WII_SECTORS >> (self.wbfs_sector_size_shift - 15) }
|
||||
}
|
||||
|
||||
const DISC_HEADER_SIZE: usize = 0x100;
|
||||
const NUM_WII_SECTORS: u32 = 143432 * 2; // Double layer discs
|
||||
|
||||
pub struct DiscIOWBFS {
|
||||
pub inner: SplitFileReader,
|
||||
/// WBFS header
|
||||
header: WBFSHeader,
|
||||
/// Map of Wii LBAs to WBFS LBAs
|
||||
wlba_table: Vec<U16>,
|
||||
/// Optional NKit header
|
||||
nkit_header: Option<NKitHeader>,
|
||||
}
|
||||
|
||||
impl DiscIOWBFS {
|
||||
pub fn new(filename: &Path) -> Result<Self> {
|
||||
let mut inner = BufReader::new(SplitFileReader::new(filename)?);
|
||||
|
||||
let header: WBFSHeader = read_from(&mut inner).context("Reading WBFS header")?;
|
||||
if header.magic != WBFS_MAGIC {
|
||||
return Err(Error::DiscFormat("Invalid WBFS magic".to_string()));
|
||||
}
|
||||
// log::debug!("{:?}", header);
|
||||
// log::debug!("sector_size: {}", header.sector_size());
|
||||
// log::debug!("wbfs_sector_size: {}", header.wbfs_sector_size());
|
||||
let file_len = inner.stable_stream_len().context("Getting WBFS file size")?;
|
||||
let expected_file_len = header.num_sectors.get() as u64 * header.sector_size() as u64;
|
||||
if file_len != expected_file_len {
|
||||
return Err(Error::DiscFormat(format!(
|
||||
"Invalid WBFS file size: {}, expected {}",
|
||||
file_len, expected_file_len
|
||||
)));
|
||||
}
|
||||
|
||||
let disc_table: Vec<u8> =
|
||||
read_vec(&mut inner, header.sector_size() as usize - size_of::<WBFSHeader>())
|
||||
.context("Reading WBFS disc table")?;
|
||||
if disc_table[0] != 1 {
|
||||
return Err(Error::DiscFormat("WBFS doesn't contain a disc".to_string()));
|
||||
}
|
||||
if disc_table[1../*max_disc as usize*/].iter().any(|&x| x != 0) {
|
||||
return Err(Error::DiscFormat("Only single WBFS discs are supported".to_string()));
|
||||
}
|
||||
|
||||
// Read WBFS LBA table
|
||||
inner
|
||||
.seek(SeekFrom::Start(header.sector_size() as u64 + DISC_HEADER_SIZE as u64))
|
||||
.context("Seeking to WBFS LBA table")?; // Skip header
|
||||
let wlba_table: Vec<U16> = read_vec(&mut inner, header.max_wbfs_sectors() as usize)
|
||||
.context("Reading WBFS LBA table")?;
|
||||
|
||||
// Read NKit header if present (always at 0x10000)
|
||||
inner.seek(SeekFrom::Start(0x10000)).context("Seeking to NKit header")?;
|
||||
let nkit_header = NKitHeader::try_read_from(&mut inner);
|
||||
|
||||
// Reset reader
|
||||
let mut inner = inner.into_inner();
|
||||
inner.reset();
|
||||
Ok(Self { inner, header, wlba_table, nkit_header })
|
||||
}
|
||||
}
|
||||
|
||||
impl DiscIO for DiscIOWBFS {
|
||||
fn open(&self) -> Result<Box<dyn ReadStream>> {
|
||||
Ok(Box::new(WBFSReadStream {
|
||||
inner: BufReader::new(self.inner.clone()),
|
||||
header: self.header.clone(),
|
||||
wlba_table: self.wlba_table.clone(),
|
||||
wlba: u32::MAX,
|
||||
pos: 0,
|
||||
disc_size: self.nkit_header.as_ref().and_then(|h| h.size),
|
||||
}))
|
||||
}
|
||||
|
||||
fn meta(&self) -> Result<DiscMeta> {
|
||||
Ok(self.nkit_header.as_ref().map(DiscMeta::from).unwrap_or_default())
|
||||
}
|
||||
|
||||
fn disc_size(&self) -> Option<u64> { self.nkit_header.as_ref().and_then(|h| h.size) }
|
||||
}
|
||||
|
||||
struct WBFSReadStream {
|
||||
/// File reader
|
||||
inner: BufReader<SplitFileReader>,
|
||||
/// WBFS header
|
||||
header: WBFSHeader,
|
||||
/// Map of Wii LBAs to WBFS LBAs
|
||||
wlba_table: Vec<U16>,
|
||||
/// Current WBFS LBA
|
||||
wlba: u32,
|
||||
/// Current stream offset
|
||||
pos: u64,
|
||||
/// Optional known size
|
||||
disc_size: Option<u64>,
|
||||
}
|
||||
|
||||
impl WBFSReadStream {
|
||||
fn disc_size(&self) -> u64 {
|
||||
self.disc_size.unwrap_or(NUM_WII_SECTORS as u64 * SECTOR_SIZE as u64)
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for WBFSReadStream {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let wlba = (self.pos >> self.header.wbfs_sector_size_shift) as u32;
|
||||
let wlba_size = self.header.wbfs_sector_size() as u64;
|
||||
let wlba_offset = self.pos & (wlba_size - 1);
|
||||
if wlba != self.wlba {
|
||||
if self.pos >= self.disc_size() || wlba >= self.header.max_wbfs_sectors() {
|
||||
return Ok(0);
|
||||
}
|
||||
let wlba_start = wlba_size * self.wlba_table[wlba as usize].get() as u64;
|
||||
self.inner.seek(SeekFrom::Start(wlba_start + wlba_offset))?;
|
||||
self.wlba = wlba;
|
||||
}
|
||||
|
||||
let to_read = min(buf.len(), (wlba_size - wlba_offset) as usize);
|
||||
let read = self.inner.read(&mut buf[..to_read])?;
|
||||
self.pos += read as u64;
|
||||
Ok(read)
|
||||
}
|
||||
}
|
||||
|
||||
impl Seek for WBFSReadStream {
|
||||
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
||||
let new_pos = match pos {
|
||||
SeekFrom::Start(v) => v,
|
||||
SeekFrom::End(_) => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Unsupported,
|
||||
"WBFSReadStream: SeekFrom::End is not supported",
|
||||
));
|
||||
}
|
||||
SeekFrom::Current(v) => self.pos.saturating_add_signed(v),
|
||||
};
|
||||
|
||||
let new_wlba = (self.pos >> self.header.wbfs_sector_size_shift) as u32;
|
||||
if new_wlba == self.wlba {
|
||||
// Seek within the same WBFS LBA
|
||||
self.inner.seek(SeekFrom::Current(new_pos as i64 - self.pos as i64))?;
|
||||
} else {
|
||||
// Seek to a different WBFS LBA, handled by next read
|
||||
self.wlba = u32::MAX;
|
||||
}
|
||||
|
||||
self.pos = new_pos;
|
||||
Ok(new_pos)
|
||||
}
|
||||
}
|
||||
|
||||
impl ReadStream for WBFSReadStream {
|
||||
fn stable_stream_len(&mut self) -> io::Result<u64> { Ok(self.disc_size()) }
|
||||
|
||||
fn as_dyn(&mut self) -> &mut dyn ReadStream { self }
|
||||
}
|
||||
+400
-829
File diff suppressed because it is too large
Load Diff
+106
-26
@@ -1,4 +1,4 @@
|
||||
#![warn(missing_docs, 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),
|
||||
@@ -16,22 +16,17 @@
|
||||
//! ```no_run
|
||||
//! use std::io::Read;
|
||||
//!
|
||||
//! use nod::{
|
||||
//! disc::{new_disc_base, PartHeader},
|
||||
//! fst::NodeType,
|
||||
//! io::{new_disc_io, DiscIOOptions},
|
||||
//! };
|
||||
//! use nod::{Disc, PartitionKind};
|
||||
//!
|
||||
//! 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 disc = Disc::new("path/to/file.iso")?;
|
||||
//! let mut partition = disc.open_partition_kind(PartitionKind::Data)?;
|
||||
//! let meta = partition.meta()?;
|
||||
//! let fst = meta.fst()?;
|
||||
//! if let Some((_, node)) = fst.find("/MP3/Worlds.txt") {
|
||||
//! let mut s = String::new();
|
||||
//! partition
|
||||
//! .begin_file_stream(node)
|
||||
//! .open_file(node)
|
||||
//! .expect("Failed to open file stream")
|
||||
//! .read_to_string(&mut s)
|
||||
//! .expect("Failed to read file");
|
||||
@@ -40,11 +35,24 @@
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
pub mod disc;
|
||||
pub mod fst;
|
||||
pub mod io;
|
||||
pub mod streams;
|
||||
pub mod util;
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use disc::DiscBase;
|
||||
pub use disc::{
|
||||
AppLoaderHeader, DiscHeader, DolHeader, PartitionBase, PartitionHeader, PartitionInfo,
|
||||
PartitionKind, PartitionMeta, BI2_SIZE, BOOT_SIZE,
|
||||
};
|
||||
pub use fst::{Fst, Node, NodeKind};
|
||||
use io::DiscIO;
|
||||
pub use io::DiscMeta;
|
||||
pub use streams::ReadStream;
|
||||
|
||||
mod disc;
|
||||
mod fst;
|
||||
mod io;
|
||||
mod streams;
|
||||
mod util;
|
||||
|
||||
/// Error types for nod.
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
@@ -55,19 +63,22 @@ pub enum Error {
|
||||
/// A general I/O error.
|
||||
#[error("I/O error: {0}")]
|
||||
Io(String, #[source] std::io::Error),
|
||||
/// An unknown error.
|
||||
#[error("error: {0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl From<&str> for Error {
|
||||
fn from(s: &str) -> Error { Error::Other(s.to_string()) }
|
||||
}
|
||||
|
||||
impl From<String> for Error {
|
||||
fn from(s: String) -> Error { Error::Other(s) }
|
||||
}
|
||||
|
||||
/// Helper result type for [`Error`].
|
||||
pub type Result<T, E = Error> = core::result::Result<T, E>;
|
||||
|
||||
impl From<aes::cipher::block_padding::UnpadError> for Error {
|
||||
fn from(_: aes::cipher::block_padding::UnpadError) -> Self { unreachable!() }
|
||||
}
|
||||
|
||||
impl From<base16ct::Error> for Error {
|
||||
fn from(_: base16ct::Error) -> Self { unreachable!() }
|
||||
}
|
||||
|
||||
pub trait ErrorContext {
|
||||
fn context(self, context: impl Into<String>) -> Error;
|
||||
}
|
||||
@@ -95,3 +106,72 @@ where E: ErrorContext
|
||||
self.map_err(|e| e.context(f()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct OpenOptions {
|
||||
/// Wii: Validate partition data hashes while reading the disc image if present.
|
||||
pub validate_hashes: bool,
|
||||
/// Wii: Rebuild partition data hashes for the disc image if the underlying format
|
||||
/// does not store them. (e.g. WIA/RVZ)
|
||||
pub rebuild_hashes: bool,
|
||||
/// Wii: Rebuild partition data encryption if the underlying format stores data decrypted.
|
||||
/// (e.g. WIA/RVZ, NFS)
|
||||
///
|
||||
/// Unnecessary if only opening a disc partition stream, which will already provide a decrypted
|
||||
/// stream. In this case, this will cause unnecessary processing.
|
||||
///
|
||||
/// Only valid in combination with `rebuild_hashes`, as the data encryption is derived from the
|
||||
/// partition data hashes.
|
||||
pub rebuild_encryption: bool,
|
||||
}
|
||||
|
||||
pub struct Disc {
|
||||
io: Box<dyn DiscIO>,
|
||||
base: Box<dyn DiscBase>,
|
||||
options: OpenOptions,
|
||||
}
|
||||
|
||||
impl Disc {
|
||||
/// Opens a disc image from a file path.
|
||||
pub fn new<P: AsRef<Path>>(path: P) -> Result<Disc> {
|
||||
Disc::new_with_options(path, &OpenOptions::default())
|
||||
}
|
||||
|
||||
/// Opens a disc image from a file path with custom options.
|
||||
pub fn new_with_options<P: AsRef<Path>>(path: P, options: &OpenOptions) -> Result<Disc> {
|
||||
let mut io = io::open(path.as_ref(), options)?;
|
||||
let base = disc::new(io.as_mut())?;
|
||||
Ok(Disc { io, base, options: options.clone() })
|
||||
}
|
||||
|
||||
/// The disc's header.
|
||||
pub fn header(&self) -> &DiscHeader { self.base.header() }
|
||||
|
||||
/// Returns extra metadata included in the disc file format, if any.
|
||||
pub fn meta(&self) -> Result<DiscMeta> { self.io.meta() }
|
||||
|
||||
/// The disc's size in bytes or an estimate if not stored by the format.
|
||||
pub fn disc_size(&self) -> u64 { self.base.disc_size() }
|
||||
|
||||
/// A list of partitions on the disc.
|
||||
///
|
||||
/// For GameCube discs, this will return a single data partition spanning the entire disc.
|
||||
pub fn partitions(&self) -> Vec<PartitionInfo> { self.base.partitions() }
|
||||
|
||||
/// Opens a new read stream for the base disc image.
|
||||
///
|
||||
/// Generally does _not_ need to be used directly. Opening a partition will provide a
|
||||
/// decrypted stream instead.
|
||||
pub fn open(&self) -> Result<Box<dyn ReadStream + '_>> { self.io.open() }
|
||||
|
||||
/// Opens a new, decrypted partition read stream for the specified partition index.
|
||||
pub fn open_partition(&self, index: usize) -> Result<Box<dyn PartitionBase + '_>> {
|
||||
self.base.open_partition(self.io.as_ref(), index, &self.options)
|
||||
}
|
||||
|
||||
/// Opens a new partition read stream for the first partition matching
|
||||
/// the specified type.
|
||||
pub fn open_partition_kind(&self, kind: PartitionKind) -> Result<Box<dyn PartitionBase + '_>> {
|
||||
self.base.open_partition_kind(self.io.as_ref(), kind, &self.options)
|
||||
}
|
||||
}
|
||||
|
||||
+23
-8
@@ -3,8 +3,7 @@
|
||||
use std::{
|
||||
fs::File,
|
||||
io,
|
||||
io::{Read, Seek, SeekFrom},
|
||||
ops::DerefMut,
|
||||
io::{BufReader, Read, Seek, SeekFrom},
|
||||
};
|
||||
|
||||
/// Creates a fixed-size array reference from a slice.
|
||||
@@ -31,6 +30,14 @@ macro_rules! array_ref_mut {
|
||||
}};
|
||||
}
|
||||
|
||||
/// Compile-time assertion.
|
||||
#[macro_export]
|
||||
macro_rules! static_assert {
|
||||
($condition:expr) => {
|
||||
const _: () = core::assert!($condition);
|
||||
};
|
||||
}
|
||||
|
||||
/// A helper trait for seekable read streams.
|
||||
pub trait ReadStream: Read + Seek {
|
||||
/// Replace with [`Read.stream_len`] when stabilized.
|
||||
@@ -65,12 +72,20 @@ impl ReadStream for File {
|
||||
fn as_dyn(&mut self) -> &mut dyn ReadStream { self }
|
||||
}
|
||||
|
||||
impl<T> ReadStream for BufReader<T>
|
||||
where T: ReadStream
|
||||
{
|
||||
fn stable_stream_len(&mut self) -> io::Result<u64> { self.get_mut().stable_stream_len() }
|
||||
|
||||
fn as_dyn(&mut self) -> &mut dyn ReadStream { self }
|
||||
}
|
||||
|
||||
trait WindowedReadStream: ReadStream {
|
||||
fn base_stream(&mut self) -> &mut dyn ReadStream;
|
||||
fn window(&self) -> (u64, u64);
|
||||
}
|
||||
|
||||
/// An window into an existing [`ReadStream`], with ownership of the underlying stream.
|
||||
/// A window into an existing [`ReadStream`], with ownership of the underlying stream.
|
||||
pub struct OwningWindowedReadStream<'a> {
|
||||
/// The base stream.
|
||||
pub base: Box<dyn ReadStream + 'a>,
|
||||
@@ -111,7 +126,7 @@ impl<'a> SharedWindowedReadStream<'a> {
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn windowed_read(stream: &mut dyn WindowedReadStream, buf: &mut [u8]) -> io::Result<usize> {
|
||||
fn windowed_read(stream: &mut impl WindowedReadStream, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let pos = stream.stream_position()?;
|
||||
let size = stream.stable_stream_len()?;
|
||||
if pos == size {
|
||||
@@ -125,7 +140,7 @@ fn windowed_read(stream: &mut dyn WindowedReadStream, buf: &mut [u8]) -> io::Res
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn windowed_seek(stream: &mut dyn WindowedReadStream, pos: SeekFrom) -> io::Result<u64> {
|
||||
fn windowed_seek(stream: &mut impl WindowedReadStream, pos: SeekFrom) -> io::Result<u64> {
|
||||
let (begin, end) = stream.window();
|
||||
let result = stream.base_stream().seek(match pos {
|
||||
SeekFrom::Start(p) => SeekFrom::Start(begin + p),
|
||||
@@ -158,7 +173,7 @@ impl<'a> ReadStream for OwningWindowedReadStream<'a> {
|
||||
}
|
||||
|
||||
impl<'a> WindowedReadStream for OwningWindowedReadStream<'a> {
|
||||
fn base_stream(&mut self) -> &mut dyn ReadStream { self.base.deref_mut() }
|
||||
fn base_stream(&mut self) -> &mut dyn ReadStream { self.base.as_dyn() }
|
||||
|
||||
fn window(&self) -> (u64, u64) { (self.begin, self.end) }
|
||||
}
|
||||
@@ -219,8 +234,8 @@ impl Seek for ByteReadStream<'_> {
|
||||
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
||||
let new_pos = match pos {
|
||||
SeekFrom::Start(v) => v,
|
||||
SeekFrom::End(v) => (self.bytes.len() as i64 + v) as u64,
|
||||
SeekFrom::Current(v) => (self.position as i64 + v) as u64,
|
||||
SeekFrom::End(v) => (self.bytes.len() as u64).saturating_add_signed(v),
|
||||
SeekFrom::Current(v) => self.position.saturating_add_signed(v),
|
||||
};
|
||||
if new_pos > self.bytes.len() as u64 {
|
||||
Err(io::Error::from(io::ErrorKind::UnexpectedEof))
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
use std::{io, io::Read};
|
||||
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// Decodes the LZMA Properties byte (lc/lp/pb).
|
||||
/// See `lzma_lzma_lclppb_decode` in `liblzma/lzma/lzma_decoder.c`.
|
||||
#[cfg(feature = "compress-lzma")]
|
||||
pub fn lzma_lclppb_decode(options: &mut liblzma::stream::LzmaOptions, byte: u8) -> Result<()> {
|
||||
let mut d = byte as u32;
|
||||
if d >= (9 * 5 * 5) {
|
||||
return Err(Error::DiscFormat(format!("Invalid LZMA props byte: {}", d)));
|
||||
}
|
||||
options.literal_context_bits(d % 9);
|
||||
d /= 9;
|
||||
options.position_bits(d / 5);
|
||||
options.literal_position_bits(d % 5);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Decodes LZMA properties.
|
||||
/// See `lzma_lzma_props_decode` in `liblzma/lzma/lzma_decoder.c`.
|
||||
#[cfg(feature = "compress-lzma")]
|
||||
pub fn lzma_props_decode(props: &[u8]) -> Result<liblzma::stream::LzmaOptions> {
|
||||
use crate::array_ref;
|
||||
if props.len() != 5 {
|
||||
return Err(Error::DiscFormat(format!("Invalid LZMA props length: {}", props.len())));
|
||||
}
|
||||
let mut options = liblzma::stream::LzmaOptions::new();
|
||||
lzma_lclppb_decode(&mut options, props[0])?;
|
||||
options.dict_size(u32::from_le_bytes(*array_ref!(props, 1, 4)));
|
||||
Ok(options)
|
||||
}
|
||||
|
||||
/// Decodes LZMA2 properties.
|
||||
/// See `lzma_lzma2_props_decode` in `liblzma/lzma/lzma2_decoder.c`.
|
||||
#[cfg(feature = "compress-lzma")]
|
||||
pub fn lzma2_props_decode(props: &[u8]) -> Result<liblzma::stream::LzmaOptions> {
|
||||
use std::cmp::Ordering;
|
||||
if props.len() != 1 {
|
||||
return Err(Error::DiscFormat(format!("Invalid LZMA2 props length: {}", props.len())));
|
||||
}
|
||||
let d = props[0] as u32;
|
||||
let mut options = liblzma::stream::LzmaOptions::new();
|
||||
options.dict_size(match d.cmp(&40) {
|
||||
Ordering::Greater => {
|
||||
return Err(Error::DiscFormat(format!("Invalid LZMA2 props byte: {}", d)));
|
||||
}
|
||||
Ordering::Equal => u32::MAX,
|
||||
Ordering::Less => (2 | (d & 1)) << (d / 2 + 11),
|
||||
});
|
||||
Ok(options)
|
||||
}
|
||||
|
||||
/// Creates a new raw LZMA decoder with the given options.
|
||||
#[cfg(feature = "compress-lzma")]
|
||||
pub fn new_lzma_decoder<R>(
|
||||
reader: R,
|
||||
options: &liblzma::stream::LzmaOptions,
|
||||
) -> io::Result<liblzma::read::XzDecoder<R>>
|
||||
where
|
||||
R: Read,
|
||||
{
|
||||
let mut filters = liblzma::stream::Filters::new();
|
||||
filters.lzma1(options);
|
||||
let stream = liblzma::stream::Stream::new_raw_decoder(&filters).map_err(io::Error::from)?;
|
||||
Ok(liblzma::read::XzDecoder::new_stream(reader, stream))
|
||||
}
|
||||
|
||||
/// Creates a new raw LZMA2 decoder with the given options.
|
||||
#[cfg(feature = "compress-lzma")]
|
||||
pub fn new_lzma2_decoder<R>(
|
||||
reader: R,
|
||||
options: &liblzma::stream::LzmaOptions,
|
||||
) -> io::Result<liblzma::read::XzDecoder<R>>
|
||||
where
|
||||
R: Read,
|
||||
{
|
||||
let mut filters = liblzma::stream::Filters::new();
|
||||
filters.lzma2(options);
|
||||
let stream = liblzma::stream::Stream::new_raw_decoder(&filters).map_err(io::Error::from)?;
|
||||
Ok(liblzma::read::XzDecoder::new_stream(reader, stream))
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user