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: strategy:
matrix: matrix:
toolchain: [ stable, 1.51.0, nightly ] toolchain: [ stable, 1.51.0, nightly ]
env:
RUSTFLAGS: -D warnings
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1 - 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 FILE: +required "Path to disc image (ISO or NFS)")
(@arg DIR: "Output directory (optional)") (@arg DIR: "Output directory (optional)")
(@arg quiet: -q "Quiet output") (@arg quiet: -q "Quiet output")
(@arg validate: -h "Validate disc hashes (Wii only)")
) )
) )
.get_matches(); .get_matches();
@ -57,9 +58,15 @@ Phillip Stephens (Antidote)")
} }
let mut disc_io = new_disc_io(file.as_path())?; let mut disc_io = new_disc_io(file.as_path())?;
let disc_base = new_disc_base(disc_io.as_mut())?; 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()?; 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(()) Result::Ok(())
} }
@ -68,16 +75,19 @@ fn extract_node(
node: &NodeType, node: &NodeType,
partition: &mut dyn PartReadStream, partition: &mut dyn PartReadStream,
base_path: &Path, base_path: &Path,
quiet: bool,
) -> io::Result<()> { ) -> io::Result<()> {
match node { match node {
NodeType::File(v) => { NodeType::File(v) => {
let mut file_path = base_path.to_owned(); let mut file_path = base_path.to_owned();
file_path.push(v.name.as_ref()); file_path.push(v.name.as_ref());
println!( if !quiet {
"Extracting {} (size: {})", println!(
file_path.to_string_lossy(), "Extracting {} (size: {})",
file_size::fit_4(v.length as u64) file_path.to_string_lossy(),
); file_size::fit_4(v.length as u64)
);
}
let file = fs::File::create(file_path)?; let file = fs::File::create(file_path)?;
let mut buf_writer = BufWriter::with_capacity(partition.ideal_buffer_size(), file); let mut buf_writer = BufWriter::with_capacity(partition.ideal_buffer_size(), file);
io::copy(&mut partition.begin_file_stream(v)?, &mut buf_writer)?; io::copy(&mut partition.begin_file_stream(v)?, &mut buf_writer)?;
@ -86,14 +96,14 @@ fn extract_node(
if v.name.is_empty() { if v.name.is_empty() {
fs::create_dir_all(base_path)?; fs::create_dir_all(base_path)?;
for x in c { for x in c {
extract_node(x, partition, base_path)?; extract_node(x, partition, base_path, quiet)?;
} }
} else { } else {
let mut new_base = base_path.to_owned(); let mut new_base = base_path.to_owned();
new_base.push(v.name.as_ref()); new_base.push(v.name.as_ref());
fs::create_dir_all(&new_base)?; fs::create_dir_all(&new_base)?;
for x in c { 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>( fn get_data_partition<'a>(
&self, &self,
disc_io: &'a mut dyn DiscIO, disc_io: &'a mut dyn DiscIO,
_validate_hashes: bool,
) -> Result<Box<dyn PartReadStream + 'a>> { ) -> Result<Box<dyn PartReadStream + 'a>> {
Result::Ok(Box::from(GCPartReadStream { Result::Ok(Box::from(GCPartReadStream {
stream: disc_io.begin_read_stream(0)?, stream: disc_io.begin_read_stream(0)?,

View File

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

View File

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

View File

@ -8,7 +8,9 @@ use encoding_rs::SHIFT_JIS;
/// File system node kind. /// File system node kind.
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub enum NodeKind { pub enum NodeKind {
/// Node is a file.
File, File,
/// Node is a directory.
Directory, Directory,
} }
@ -18,6 +20,8 @@ pub enum NodeKind {
pub struct Node { pub struct Node {
#[br(temp)] #[br(temp)]
type_and_name_offset: u32, type_and_name_offset: u32,
/// File system node type.
#[br(calc = if (type_and_name_offset >> 24) != 0 { NodeKind::Directory } else { NodeKind::File })] #[br(calc = if (type_and_name_offset >> 24) != 0 { NodeKind::Directory } else { NodeKind::File })]
pub kind: NodeKind, 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 + '_>> { 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>( pub fn new_disc_io_from_stream<'a, T: 'a + ReadStream + Sized + Send + Sync>(
stream: T, stream: T,
) -> Result<Box<dyn DiscIO + 'a>> { ) -> 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. //! Library for traversing & reading GameCube and Wii disc images.
//! //!
//! Based on the C++ library [nod](https://github.com/AxioDL/nod), //! 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 mut disc_io = new_disc_io("path/to/file".as_ref())?;
//! let disc_base = new_disc_base(disc_io.as_mut())?; //! 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()?; //! let header = partition.read_header()?;
//! if let Some(NodeType::File(node)) = header.find_node("/MP3/Worlds.txt") { //! if let Some(NodeType::File(node)) = header.find_node("/MP3/Worlds.txt") {
//! let mut s = String::new(); //! let mut s = String::new();
@ -34,19 +36,25 @@ pub mod fst;
pub mod io; pub mod io;
pub mod streams; pub mod streams;
/// Error types for nod.
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum Error { pub enum Error {
/// An error during binary format parsing.
#[error("binary format")] #[error("binary format")]
BinaryFormat(#[from] binrw::Error), BinaryFormat(#[from] binrw::Error),
/// An error during Wii disc decryption.
#[error("encryption")] #[error("encryption")]
Encryption(#[from] block_modes::BlockModeError), Encryption(#[from] block_modes::BlockModeError),
/// A general I/O error.
#[error("io error: `{0}`")] #[error("io error: `{0}`")]
Io(String, #[source] std::io::Error), Io(String, #[source] std::io::Error),
/// An error for disc format related issues.
#[error("disc format error: `{0}`")] #[error("disc format error: `{0}`")]
DiscFormat(String), 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 { impl From<std::io::Error> for Error {
fn from(v: std::io::Error) -> Self { Error::Io("I/O error".to_string(), v) } 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 { pub trait ReadStream: Read + Seek {
/// Replace with [`Read.stream_len`] when stabilized. /// 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; fn as_dyn(&mut self) -> &mut dyn ReadStream;
} }
@ -72,9 +74,13 @@ trait WindowedReadStream: ReadStream {
fn window(&self) -> (u64, u64); fn window(&self) -> (u64, u64);
} }
/// An window into an existing [`ReadStream`], with ownership of the underlying stream.
pub struct OwningWindowedReadStream<'a> { pub struct OwningWindowedReadStream<'a> {
/// The base stream.
pub base: Box<dyn ReadStream + 'a>, pub base: Box<dyn ReadStream + 'a>,
/// The beginning of the window in bytes.
pub begin: u64, pub begin: u64,
/// The end of the window in bytes.
pub end: u64, pub end: u64,
} }
@ -88,13 +94,18 @@ pub fn wrap_windowed<'a>(
io::Result::Ok(OwningWindowedReadStream { base, begin: offset, end: offset + size }) io::Result::Ok(OwningWindowedReadStream { base, begin: offset, end: offset + size })
} }
/// A non-owning window into an existing [`ReadStream`].
pub struct SharedWindowedReadStream<'a> { pub struct SharedWindowedReadStream<'a> {
/// A reference to the base stream.
pub base: &'a mut dyn ReadStream, pub base: &'a mut dyn ReadStream,
/// The beginning of the window in bytes.
pub begin: u64, pub begin: u64,
/// The end of the window in bytes.
pub end: u64, pub end: u64,
} }
impl<'a> SharedWindowedReadStream<'a> { 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<()> { pub fn set_window(&mut self, begin: u64, end: u64) -> io::Result<()> {
self.base.seek(SeekFrom::Start(begin))?; self.base.seek(SeekFrom::Start(begin))?;
self.begin = begin; self.begin = begin;
@ -180,8 +191,11 @@ impl<'a> WindowedReadStream for SharedWindowedReadStream<'a> {
fn window(&self) -> (u64, u64) { (self.begin, self.end) } fn window(&self) -> (u64, u64) { (self.begin, self.end) }
} }
/// A non-owning [`ReadStream`] wrapping a byte slice reference.
pub struct ByteReadStream<'a> { pub struct ByteReadStream<'a> {
/// A reference to the underlying byte slice.
pub bytes: &'a [u8], pub bytes: &'a [u8],
/// The current position in the stream.
pub position: u64, pub position: u64,
} }