From 97c726c2093caad288112eb9b15a02eedfe2ed98 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Thu, 3 Feb 2022 20:54:05 -0500 Subject: [PATCH] Add -h (validate Wii disc hashes); complete documentation --- .github/workflows/build.yaml | 2 ++ src/bin.rs | 28 +++++++++++++++++++--------- src/disc/gcn.rs | 1 + src/disc/mod.rs | 19 ++++++++++++++++--- src/disc/wii.rs | 3 ++- src/fst.rs | 4 ++++ src/io/mod.rs | 26 +++++++++++++++++++++++++- src/lib.rs | 12 ++++++++++-- src/streams.rs | 14 ++++++++++++++ 9 files changed, 93 insertions(+), 16 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 315fa0a..3320223 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -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 diff --git a/src/bin.rs b/src/bin.rs index c751d5c..c010b4d 100644 --- a/src/bin.rs +++ b/src/bin.rs @@ -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)?; } } } diff --git a/src/disc/gcn.rs b/src/disc/gcn.rs index 082ba26..27ac840 100644 --- a/src/disc/gcn.rs +++ b/src/disc/gcn.rs @@ -27,6 +27,7 @@ impl DiscBase for DiscGCN { fn get_data_partition<'a>( &self, disc_io: &'a mut dyn DiscIO, + _validate_hashes: bool, ) -> Result> { Result::Ok(Box::from(GCPartReadStream { stream: disc_io.begin_read_stream(0)?, diff --git a/src/disc/mod.rs b/src/disc/mod.rs index 0805e89..6dd4176 100644 --- a/src/disc/mod.rs +++ b/src/disc/mod.rs @@ -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>; } @@ -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); diff --git a/src/disc/wii.rs b/src/disc/wii.rs index a8015f1..8dd7114 100644 --- a/src/disc/wii.rs +++ b/src/disc/wii.rs @@ -220,6 +220,7 @@ impl DiscBase for DiscWii { fn get_data_partition<'a>( &self, disc_io: &'a mut dyn DiscIO, + validate_hashes: bool, ) -> Result> { 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) } diff --git a/src/fst.rs b/src/fst.rs index 37a6f13..c5ce0f3 100644 --- a/src/fst.rs +++ b/src/fst.rs @@ -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, diff --git a/src/io/mod.rs b/src/io/mod.rs index 0cc0fe1..9cd5617 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -70,10 +70,34 @@ pub fn new_disc_io(filename: &Path) -> Result> { } } +/// 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> { - 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> { diff --git a/src/lib.rs b/src/lib.rs index 1368948..93822f7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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 = anyhow::Result; +/// Helper result type for [`enum@Error`]. +pub type Result = core::result::Result; impl From for Error { fn from(v: std::io::Error) -> Self { Error::Io("I/O error".to_string(), v) } diff --git a/src/streams.rs b/src/streams.rs index 59a2d81..7618892 100644 --- a/src/streams.rs +++ b/src/streams.rs @@ -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, + /// 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, }