Add -h (validate Wii disc hashes); complete documentation

This commit is contained in:
Luke Street 2022-02-03 20:54:05 -05:00
parent 3e78aad790
commit 97c726c209
9 changed files with 93 additions and 16 deletions

View File

@ -9,6 +9,8 @@ jobs:
strategy:
matrix:
toolchain: [ stable, 1.51.0, nightly ]
env:
RUSTFLAGS: -D warnings
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1

View File

@ -37,6 +37,7 @@ Phillip Stephens (Antidote)")
(@arg FILE: +required "Path to disc image (ISO or NFS)")
(@arg DIR: "Output directory (optional)")
(@arg quiet: -q "Quiet output")
(@arg validate: -h "Validate disc hashes (Wii only)")
)
)
.get_matches();
@ -57,9 +58,15 @@ Phillip Stephens (Antidote)")
}
let mut disc_io = new_disc_io(file.as_path())?;
let disc_base = new_disc_base(disc_io.as_mut())?;
let mut partition = disc_base.get_data_partition(disc_io.as_mut())?;
let mut partition =
disc_base.get_data_partition(disc_io.as_mut(), matches.is_present("validate"))?;
let header = partition.read_header()?;
extract_node(header.root_node(), partition.as_mut(), output_dir.as_path())?;
extract_node(
header.root_node(),
partition.as_mut(),
output_dir.as_path(),
matches.is_present("quiet"),
)?;
}
Result::Ok(())
}
@ -68,16 +75,19 @@ fn extract_node(
node: &NodeType,
partition: &mut dyn PartReadStream,
base_path: &Path,
quiet: bool,
) -> io::Result<()> {
match node {
NodeType::File(v) => {
let mut file_path = base_path.to_owned();
file_path.push(v.name.as_ref());
println!(
"Extracting {} (size: {})",
file_path.to_string_lossy(),
file_size::fit_4(v.length as u64)
);
if !quiet {
println!(
"Extracting {} (size: {})",
file_path.to_string_lossy(),
file_size::fit_4(v.length as u64)
);
}
let file = fs::File::create(file_path)?;
let mut buf_writer = BufWriter::with_capacity(partition.ideal_buffer_size(), file);
io::copy(&mut partition.begin_file_stream(v)?, &mut buf_writer)?;
@ -86,14 +96,14 @@ fn extract_node(
if v.name.is_empty() {
fs::create_dir_all(base_path)?;
for x in c {
extract_node(x, partition, base_path)?;
extract_node(x, partition, base_path, quiet)?;
}
} else {
let mut new_base = base_path.to_owned();
new_base.push(v.name.as_ref());
fs::create_dir_all(&new_base)?;
for x in c {
extract_node(x, partition, new_base.as_path())?;
extract_node(x, partition, new_base.as_path(), quiet)?;
}
}
}

View File

@ -27,6 +27,7 @@ impl DiscBase for DiscGCN {
fn get_data_partition<'a>(
&self,
disc_io: &'a mut dyn DiscIO,
_validate_hashes: bool,
) -> Result<Box<dyn PartReadStream + 'a>> {
Result::Ok(Box::from(GCPartReadStream {
stream: disc_io.begin_read_stream(0)?,

View File

@ -18,25 +18,32 @@ pub(crate) mod wii;
/// Shared GameCube & Wii disc header
#[derive(Clone, Debug, PartialEq, BinRead)]
pub struct Header {
/// Game ID (e.g. GM8E01 for Metroid Prime)
pub game_id: [u8; 6],
/// Used in multi-disc games
pub disc_num: u8,
/// Disc version
pub disc_version: u8,
/// Audio streaming enabled (bool)
pub audio_streaming: u8,
/// Audio streaming buffer size
pub audio_stream_buf_size: u8,
#[br(pad_before(14))]
/// If this is a Wii disc, this will be 0x5D1C9EA3
pub wii_magic: u32,
/// If this is a GameCube disc, this will be 0xC2339F3D
pub gcn_magic: u32,
/// Game title
#[br(pad_size_to(64), map = NullString::into_string)]
pub game_title: String,
/// Disable hash verification
pub disable_hash_verification: u8,
/// Disable disc encryption and H3 hash table loading and verification
pub disable_disc_enc: u8,
/// Debug monitor offset
#[br(pad_before(0x39e))]
pub debug_mon_off: u32,
/// Debug monitor load address
pub debug_load_addr: u32,
#[br(pad_before(0x18))]
/// Offset to main DOL (Wii: >> 2)
@ -47,8 +54,11 @@ pub struct Header {
pub fst_sz: u32,
/// File system max size
pub fst_max_sz: u32,
/// File system table load address
pub fst_memory_address: u32,
/// User position
pub user_position: u32,
/// User size
#[br(pad_after(4))]
pub user_sz: u32,
}
@ -79,6 +89,8 @@ pub trait DiscBase: Send + Sync {
/// Opens a new partition read stream for the first data partition.
///
/// `validate_hashes`: Validate Wii disc hashes while reading (slow!)
///
/// # Examples
///
/// Basic usage:
@ -88,12 +100,13 @@ pub trait DiscBase: Send + Sync {
///
/// let mut disc_io = new_disc_io("path/to/file".as_ref())?;
/// let disc_base = new_disc_base(disc_io.as_mut())?;
/// let mut partition = disc_base.get_data_partition(disc_io.as_mut())?;
/// let mut partition = disc_base.get_data_partition(disc_io.as_mut(), false)?;
/// # Ok::<(), nod::Error>(())
/// ```
fn get_data_partition<'a>(
&self,
disc_io: &'a mut dyn DiscIO,
validate_hashes: bool,
) -> Result<Box<dyn PartReadStream + 'a>>;
}
@ -139,7 +152,7 @@ pub trait PartReadStream: ReadStream {
///
/// let mut disc_io = new_disc_io("path/to/file".as_ref())?;
/// let disc_base = new_disc_base(disc_io.as_mut())?;
/// let mut partition = disc_base.get_data_partition(disc_io.as_mut())?;
/// let mut partition = disc_base.get_data_partition(disc_io.as_mut(), false)?;
/// let header = partition.read_header()?;
/// if let Some(NodeType::File(node)) = header.find_node("/MP3/Worlds.txt") {
/// let mut s = String::new();
@ -176,7 +189,7 @@ pub trait PartHeader: Debug + Send + Sync {
///
/// let mut disc_io = new_disc_io("path/to/file".as_ref())?;
/// let disc_base = new_disc_base(disc_io.as_mut())?;
/// let mut partition = disc_base.get_data_partition(disc_io.as_mut())?;
/// let mut partition = disc_base.get_data_partition(disc_io.as_mut(), false)?;
/// let header = partition.read_header()?;
/// if let Some(NodeType::File(node)) = header.find_node("/MP1/Metroid1.pak") {
/// println!("{}", node.name);

View File

@ -220,6 +220,7 @@ impl DiscBase for DiscWii {
fn get_data_partition<'a>(
&self,
disc_io: &'a mut dyn DiscIO,
validate_hashes: bool,
) -> Result<Box<dyn PartReadStream + 'a>> {
let part = self
.part_info
@ -243,7 +244,7 @@ impl DiscBase for DiscWii {
offset: 0,
cur_block: u64::MAX,
buf: [0; 0x8000],
validate_hashes: false,
validate_hashes,
});
Result::Ok(result)
}

View File

@ -8,7 +8,9 @@ use encoding_rs::SHIFT_JIS;
/// File system node kind.
#[derive(Clone, Debug, PartialEq)]
pub enum NodeKind {
/// Node is a file.
File,
/// Node is a directory.
Directory,
}
@ -18,6 +20,8 @@ pub enum NodeKind {
pub struct Node {
#[br(temp)]
type_and_name_offset: u32,
/// File system node type.
#[br(calc = if (type_and_name_offset >> 24) != 0 { NodeKind::Directory } else { NodeKind::File })]
pub kind: NodeKind,

View File

@ -70,10 +70,34 @@ pub fn new_disc_io(filename: &Path) -> Result<Box<dyn DiscIO>> {
}
}
/// Creates a new [`DiscIO`] instance from a byte slice.
///
/// # Examples
///
/// Basic usage:
/// ```no_run
/// use nod::io::new_disc_io_from_buf;
///
/// # #[allow(non_upper_case_globals)] const buf: [u8; 0] = [];
/// let mut disc_io = new_disc_io_from_buf(&buf)?;
/// # Ok::<(), nod::Error>(())
/// ```
pub fn new_disc_io_from_buf(buf: &[u8]) -> Result<Box<dyn DiscIO + '_>> {
Ok(Box::from(DiscIOISOStream::new(ByteReadStream { bytes: buf, position: 0 })?))
new_disc_io_from_stream(ByteReadStream { bytes: buf, position: 0 })
}
/// Creates a new [`DiscIO`] instance from an existing [`ReadStream`].
///
/// # Examples
///
/// Basic usage:
/// ```no_run
/// use nod::io::new_disc_io_from_buf;
///
/// # #[allow(non_upper_case_globals)] const buf: [u8; 0] = [];
/// let mut disc_io = new_disc_io_from_buf(&buf)?;
/// # Ok::<(), nod::Error>(())
/// ```
pub fn new_disc_io_from_stream<'a, T: 'a + ReadStream + Sized + Send + Sync>(
stream: T,
) -> Result<Box<dyn DiscIO + 'a>> {

View File

@ -1,3 +1,5 @@
#![warn(missing_docs)]
#![warn(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),
@ -18,7 +20,7 @@
//!
//! let mut disc_io = new_disc_io("path/to/file".as_ref())?;
//! let disc_base = new_disc_base(disc_io.as_mut())?;
//! let mut partition = disc_base.get_data_partition(disc_io.as_mut())?;
//! let mut partition = disc_base.get_data_partition(disc_io.as_mut(), false)?;
//! let header = partition.read_header()?;
//! if let Some(NodeType::File(node)) = header.find_node("/MP3/Worlds.txt") {
//! let mut s = String::new();
@ -34,19 +36,25 @@ pub mod fst;
pub mod io;
pub mod streams;
/// Error types for nod.
#[derive(Error, Debug)]
pub enum Error {
/// An error during binary format parsing.
#[error("binary format")]
BinaryFormat(#[from] binrw::Error),
/// An error during Wii disc decryption.
#[error("encryption")]
Encryption(#[from] block_modes::BlockModeError),
/// A general I/O error.
#[error("io error: `{0}`")]
Io(String, #[source] std::io::Error),
/// An error for disc format related issues.
#[error("disc format error: `{0}`")]
DiscFormat(String),
}
pub type Result<T> = anyhow::Result<T, Error>;
/// Helper result type for [`enum@Error`].
pub type Result<T, E = Error> = core::result::Result<T, E>;
impl From<std::io::Error> for Error {
fn from(v: std::io::Error) -> Self { Error::Io("I/O error".to_string(), v) }

View File

@ -31,6 +31,7 @@ macro_rules! array_ref_mut {
}};
}
/// A helper trait for seekable read streams.
pub trait ReadStream: Read + Seek {
/// Replace with [`Read.stream_len`] when stabilized.
///
@ -49,6 +50,7 @@ pub trait ReadStream: Read + Seek {
})
}
/// Retrieves a type-erased reference to the stream.
fn as_dyn(&mut self) -> &mut dyn ReadStream;
}
@ -72,9 +74,13 @@ trait WindowedReadStream: ReadStream {
fn window(&self) -> (u64, u64);
}
/// An window into an existing [`ReadStream`], with ownership of the underlying stream.
pub struct OwningWindowedReadStream<'a> {
/// The base stream.
pub base: Box<dyn ReadStream + 'a>,
/// The beginning of the window in bytes.
pub begin: u64,
/// The end of the window in bytes.
pub end: u64,
}
@ -88,13 +94,18 @@ pub fn wrap_windowed<'a>(
io::Result::Ok(OwningWindowedReadStream { base, begin: offset, end: offset + size })
}
/// A non-owning window into an existing [`ReadStream`].
pub struct SharedWindowedReadStream<'a> {
/// A reference to the base stream.
pub base: &'a mut dyn ReadStream,
/// The beginning of the window in bytes.
pub begin: u64,
/// The end of the window in bytes.
pub end: u64,
}
impl<'a> SharedWindowedReadStream<'a> {
/// Modifies the current window & seeks to the beginning of the window.
pub fn set_window(&mut self, begin: u64, end: u64) -> io::Result<()> {
self.base.seek(SeekFrom::Start(begin))?;
self.begin = begin;
@ -180,8 +191,11 @@ impl<'a> WindowedReadStream for SharedWindowedReadStream<'a> {
fn window(&self) -> (u64, u64) { (self.begin, self.end) }
}
/// A non-owning [`ReadStream`] wrapping a byte slice reference.
pub struct ByteReadStream<'a> {
/// A reference to the underlying byte slice.
pub bytes: &'a [u8],
/// The current position in the stream.
pub position: u64,
}