mirror of https://github.com/encounter/nod-rs.git
Add -h (validate Wii disc hashes); complete documentation
This commit is contained in:
parent
3e78aad790
commit
97c726c209
|
@ -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
|
||||
|
|
28
src/bin.rs
28
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)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)?,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
||||
|
|
|
@ -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>> {
|
||||
|
|
12
src/lib.rs
12
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<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) }
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue