Finish WIA/RVZ, add WBFS, CISO & more

Generally a complete overhaul.
This commit is contained in:
Luke Street
2024-02-16 22:53:37 -07:00
parent fff7b350b1
commit ce9fbbf822
23 changed files with 3388 additions and 2626 deletions
+22 -11
View File
@@ -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 }
+35 -19
View File
@@ -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
+9
View File
@@ -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");
}
+64
View File
@@ -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 processs `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
View File
File diff suppressed because it is too large Load Diff
+118 -130
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+372 -396
View File
File diff suppressed because it is too large Load Diff
+136 -161
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+146
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+106 -26
View File
@@ -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
View File
@@ -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))
+82
View File
@@ -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