mirror of https://github.com/encounter/nod-rs.git
Restore all functionality, split lib/bin & integrate redump validation
This commit is contained in:
parent
7f97dac399
commit
07bb8ccc1d
|
@ -1,3 +1,2 @@
|
|||
/target
|
||||
Cargo.lock
|
||||
.idea
|
||||
|
|
File diff suppressed because it is too large
Load Diff
63
Cargo.toml
63
Cargo.toml
|
@ -1,65 +1,8 @@
|
|||
[package]
|
||||
name = "nod"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.59.0"
|
||||
authors = ["Luke Street <luke@street.dev>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/encounter/nod-rs"
|
||||
documentation = "https://docs.rs/nod"
|
||||
readme = "README.md"
|
||||
description = """
|
||||
Rust library and CLI tool for reading GameCube and Wii disc images.
|
||||
"""
|
||||
keywords = ["gamecube", "wii", "iso", "nfs", "rvz"]
|
||||
categories = ["command-line-utilities", "parser-implementations"]
|
||||
build = "build.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "nodtool"
|
||||
path = "src/bin.rs"
|
||||
|
||||
[profile.release]
|
||||
debug = true
|
||||
[workspace]
|
||||
members = ["nod", "nodtool"]
|
||||
resolver = "2"
|
||||
|
||||
[profile.release-lto]
|
||||
inherits = "release"
|
||||
lto = "thin"
|
||||
strip = "debuginfo"
|
||||
|
||||
[features]
|
||||
default = ["compress-bzip2", "compress-lzma", "compress-zstd"]
|
||||
asm = ["md-5/asm", "sha1/asm"]
|
||||
compress-bzip2 = ["bzip2"]
|
||||
compress-lzma = ["liblzma"]
|
||||
compress-zstd = ["zstd"]
|
||||
nightly = ["crc32fast/nightly"]
|
||||
|
||||
[dependencies]
|
||||
aes = "0.8.4"
|
||||
argh_derive = "0.1.12"
|
||||
argp = "0.3.0"
|
||||
base16ct = "0.2.0"
|
||||
bzip2 = { version = "0.4.4", features = ["static"], optional = true }
|
||||
cbc = "0.1.2"
|
||||
crc32fast = "1.4.0"
|
||||
digest = "0.10.7"
|
||||
dyn-clone = "1.0.16"
|
||||
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"
|
||||
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 }
|
||||
|
|
9
build.rs
9
build.rs
|
@ -1,9 +0,0 @@
|
|||
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");
|
||||
}
|
|
@ -74,6 +74,8 @@ allow = [
|
|||
"Apache-2.0",
|
||||
"BSD-3-Clause",
|
||||
"Unicode-DFS-2016",
|
||||
"BSL-1.0",
|
||||
"ISC",
|
||||
]
|
||||
# List of explictly disallowed licenses
|
||||
# See https://spdx.org/licenses/ for list of possible licenses
|
||||
|
@ -197,7 +199,7 @@ allow-git = []
|
|||
|
||||
[sources.allow-org]
|
||||
# 1 or more github.com organizations to allow git sources for
|
||||
#github = [""]
|
||||
github = ["encounter"]
|
||||
# 1 or more gitlab.com organizations to allow git sources for
|
||||
#gitlab = [""]
|
||||
# 1 or more bitbucket.org organizations to allow git sources for
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
[package]
|
||||
name = "nod"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.59.0"
|
||||
authors = ["Luke Street <luke@street.dev>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/encounter/nod-rs"
|
||||
documentation = "https://docs.rs/nod"
|
||||
readme = "../README.md"
|
||||
description = """
|
||||
Library for reading GameCube and Wii disc images.
|
||||
"""
|
||||
keywords = ["gamecube", "wii", "iso", "wbfs", "rvz"]
|
||||
categories = ["command-line-utilities", "parser-implementations"]
|
||||
|
||||
[features]
|
||||
default = ["compress-bzip2", "compress-lzma", "compress-zstd"]
|
||||
asm = ["sha1/asm"]
|
||||
compress-bzip2 = ["bzip2"]
|
||||
compress-lzma = ["liblzma"]
|
||||
compress-zstd = ["zstd"]
|
||||
|
||||
[dependencies]
|
||||
aes = "0.8.4"
|
||||
base16ct = "0.2.0"
|
||||
bzip2 = { version = "0.4.4", features = ["static"], optional = true }
|
||||
cbc = "0.1.2"
|
||||
digest = "0.10.7"
|
||||
dyn-clone = "1.0.16"
|
||||
encoding_rs = "0.8.33"
|
||||
itertools = "0.12.1"
|
||||
liblzma = { version = "0.2.3", features = ["static"], optional = true }
|
||||
log = "0.4.20"
|
||||
rayon = "1.8.1"
|
||||
sha1 = "0.10.6"
|
||||
thiserror = "1.0.57"
|
||||
zerocopy = { version = "0.7.32", features = ["alloc", "derive"] }
|
||||
zstd = { version = "0.13.0", optional = true }
|
|
@ -0,0 +1,202 @@
|
|||
use std::{
|
||||
cmp::min,
|
||||
io,
|
||||
io::{Read, Seek, SeekFrom},
|
||||
mem::size_of,
|
||||
};
|
||||
|
||||
use zerocopy::{FromBytes, FromZeroes};
|
||||
|
||||
use crate::{
|
||||
disc::{
|
||||
AppLoaderHeader, DiscHeader, DolHeader, PartitionBase, PartitionHeader, PartitionMeta,
|
||||
BI2_SIZE, BOOT_SIZE, SECTOR_SIZE,
|
||||
},
|
||||
fst::{Node, NodeKind},
|
||||
io::block::{Block, BlockIO},
|
||||
streams::{ReadStream, SharedWindowedReadStream},
|
||||
util::read::{read_box, read_box_slice, read_vec},
|
||||
Result, ResultContext,
|
||||
};
|
||||
|
||||
pub struct PartitionGC {
|
||||
io: Box<dyn BlockIO>,
|
||||
block: Option<Block>,
|
||||
block_buf: Box<[u8]>,
|
||||
block_idx: u32,
|
||||
sector_buf: Box<[u8; SECTOR_SIZE]>,
|
||||
sector: u32,
|
||||
pos: u64,
|
||||
disc_header: Box<DiscHeader>,
|
||||
}
|
||||
|
||||
impl Clone for PartitionGC {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
io: self.io.clone(),
|
||||
block: None,
|
||||
block_buf: <u8>::new_box_slice_zeroed(self.block_buf.len()),
|
||||
block_idx: u32::MAX,
|
||||
sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed(),
|
||||
sector: u32::MAX,
|
||||
pos: 0,
|
||||
disc_header: self.disc_header.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartitionGC {
|
||||
pub fn new(inner: Box<dyn BlockIO>, disc_header: Box<DiscHeader>) -> Result<Box<Self>> {
|
||||
let block_size = inner.block_size();
|
||||
Ok(Box::new(Self {
|
||||
io: inner,
|
||||
block: None,
|
||||
block_buf: <u8>::new_box_slice_zeroed(block_size as usize),
|
||||
block_idx: u32::MAX,
|
||||
sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed(),
|
||||
sector: u32::MAX,
|
||||
pos: 0,
|
||||
disc_header,
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> Box<dyn BlockIO> { self.io }
|
||||
}
|
||||
|
||||
impl Read for PartitionGC {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let sector = (self.pos / SECTOR_SIZE as u64) as u32;
|
||||
let block_idx = (sector as u64 * SECTOR_SIZE as u64 / self.block_buf.len() as u64) as u32;
|
||||
|
||||
// Read new block if necessary
|
||||
if block_idx != self.block_idx {
|
||||
self.block = self.io.read_block(self.block_buf.as_mut(), block_idx, None)?;
|
||||
self.block_idx = block_idx;
|
||||
}
|
||||
|
||||
// Copy sector if necessary
|
||||
if sector != self.sector {
|
||||
let Some(block) = &self.block else {
|
||||
return Ok(0);
|
||||
};
|
||||
block.copy_raw(
|
||||
self.sector_buf.as_mut(),
|
||||
self.block_buf.as_ref(),
|
||||
block_idx,
|
||||
sector,
|
||||
&self.disc_header,
|
||||
)?;
|
||||
self.sector = sector;
|
||||
}
|
||||
|
||||
let offset = (self.pos % SECTOR_SIZE as u64) as usize;
|
||||
let len = min(buf.len(), SECTOR_SIZE - offset);
|
||||
buf[..len].copy_from_slice(&self.sector_buf[offset..offset + len]);
|
||||
self.pos += len as u64;
|
||||
Ok(len)
|
||||
}
|
||||
}
|
||||
|
||||
impl Seek for PartitionGC {
|
||||
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
||||
self.pos = match pos {
|
||||
SeekFrom::Start(v) => v,
|
||||
SeekFrom::End(_) => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Unsupported,
|
||||
"GCPartitionReader: SeekFrom::End is not supported".to_string(),
|
||||
));
|
||||
}
|
||||
SeekFrom::Current(v) => self.pos.saturating_add_signed(v),
|
||||
};
|
||||
Ok(self.pos)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartitionBase for PartitionGC {
|
||||
fn meta(&mut self) -> Result<Box<PartitionMeta>> {
|
||||
self.seek(SeekFrom::Start(0)).context("Seeking to partition metadata")?;
|
||||
read_part_meta(self, false)
|
||||
}
|
||||
|
||||
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 }
|
||||
}
|
||||
|
||||
pub(crate) fn read_part_meta(
|
||||
reader: &mut dyn ReadStream,
|
||||
is_wii: bool,
|
||||
) -> Result<Box<PartitionMeta>> {
|
||||
// boot.bin
|
||||
let raw_boot: Box<[u8; BOOT_SIZE]> = read_box(reader).context("Reading boot.bin")?;
|
||||
let partition_header = PartitionHeader::ref_from(&raw_boot[size_of::<DiscHeader>()..]).unwrap();
|
||||
|
||||
// bi2.bin
|
||||
let raw_bi2: Box<[u8; BI2_SIZE]> = read_box(reader).context("Reading bi2.bin")?;
|
||||
|
||||
// apploader.bin
|
||||
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(
|
||||
size_of::<AppLoaderHeader>()
|
||||
+ apploader_header.size.get() as usize
|
||||
+ apploader_header.trailer_size.get() as usize,
|
||||
0,
|
||||
);
|
||||
reader
|
||||
.read_exact(&mut raw_apploader[size_of::<AppLoaderHeader>()..])
|
||||
.context("Reading apploader")?;
|
||||
|
||||
// fst.bin
|
||||
reader
|
||||
.seek(SeekFrom::Start(partition_header.fst_off(is_wii)))
|
||||
.context("Seeking to FST offset")?;
|
||||
let raw_fst: Box<[u8]> = read_box_slice(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(is_wii)))
|
||||
.context("Seeking to DOL offset")?;
|
||||
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.get() + size.get())
|
||||
.chain(
|
||||
dol_header
|
||||
.data_offs
|
||||
.iter()
|
||||
.zip(&dol_header.data_sizes)
|
||||
.map(|(offs, size)| offs.get() + size.get()),
|
||||
)
|
||||
.max()
|
||||
.unwrap_or(size_of::<DolHeader>() as u32);
|
||||
raw_dol.resize(dol_size as usize, 0);
|
||||
reader.read_exact(&mut raw_dol[size_of::<DolHeader>()..]).context("Reading DOL")?;
|
||||
|
||||
Ok(Box::new(PartitionMeta {
|
||||
raw_boot,
|
||||
raw_bi2,
|
||||
raw_apploader: raw_apploader.into_boxed_slice(),
|
||||
raw_fst,
|
||||
raw_dol: raw_dol.into_boxed_slice(),
|
||||
raw_ticket: None,
|
||||
raw_tmd: None,
|
||||
raw_cert_chain: None,
|
||||
raw_h3_table: None,
|
||||
}))
|
||||
}
|
|
@ -11,13 +11,12 @@ use zerocopy::FromZeroes;
|
|||
use crate::{
|
||||
array_ref, array_ref_mut,
|
||||
disc::{
|
||||
partition::PartitionReader,
|
||||
reader::DiscReader,
|
||||
wii::{HASHES_SIZE, SECTOR_DATA_SIZE},
|
||||
},
|
||||
io::HashBytes,
|
||||
util::read::read_box_slice,
|
||||
Result, ResultContext, SECTOR_SIZE,
|
||||
OpenOptions, Result, ResultContext, SECTOR_SIZE,
|
||||
};
|
||||
|
||||
/// In a sector, following the 0x400 byte block of hashes, each 0x400 bytes of decrypted data is
|
||||
|
@ -88,8 +87,9 @@ pub fn rebuild_hashes(reader: &mut DiscReader) -> Result<()> {
|
|||
zero_h1_hash.update(zero_h0_hash);
|
||||
}
|
||||
|
||||
let mut hash_tables = Vec::with_capacity(reader.partitions.len());
|
||||
for part in &reader.partitions {
|
||||
let partitions = reader.partitions();
|
||||
let mut hash_tables = Vec::with_capacity(partitions.len());
|
||||
for part in partitions {
|
||||
let part_sectors = part.data_end_sector - part.data_start_sector;
|
||||
let hash_table = HashTable::new(part_sectors);
|
||||
log::debug!(
|
||||
|
@ -102,7 +102,7 @@ pub fn rebuild_hashes(reader: &mut DiscReader) -> Result<()> {
|
|||
let group_count = hash_table.h3_hashes.len();
|
||||
let mutex = Arc::new(Mutex::new(hash_table));
|
||||
(0..group_count).into_par_iter().try_for_each_with(
|
||||
(PartitionReader::new(reader.io.clone(), part)?, mutex.clone()),
|
||||
(reader.open_partition(part.index, &OpenOptions::default())?, mutex.clone()),
|
||||
|(stream, mutex), h3_index| -> Result<()> {
|
||||
let mut result = HashResult::new_box_zeroed();
|
||||
let mut data_buf = <u8>::new_box_slice_zeroed(SECTOR_DATA_SIZE);
|
|
@ -9,25 +9,20 @@ use std::{
|
|||
str::from_utf8,
|
||||
};
|
||||
|
||||
use dyn_clone::DynClone;
|
||||
use zerocopy::{big_endian::*, AsBytes, FromBytes, FromZeroes};
|
||||
|
||||
use crate::{
|
||||
disc::{
|
||||
gcn::DiscGCN,
|
||||
wii::{DiscWii, Ticket, TmdHeader, WiiPartitionHeader},
|
||||
},
|
||||
disc::wii::{Ticket, TmdHeader},
|
||||
fst::Node,
|
||||
io::DiscIO,
|
||||
static_assert,
|
||||
streams::{ReadStream, SharedWindowedReadStream},
|
||||
util::read::read_from,
|
||||
Error, Fst, OpenOptions, Result, ResultContext,
|
||||
Fst, Result,
|
||||
};
|
||||
|
||||
pub(crate) mod gcn;
|
||||
pub(crate) mod hashes;
|
||||
pub mod partition;
|
||||
pub mod reader;
|
||||
pub(crate) mod reader;
|
||||
pub(crate) mod wii;
|
||||
|
||||
pub const SECTOR_SIZE: usize = 0x8000;
|
||||
|
@ -251,82 +246,8 @@ impl From<u32> for PartitionKind {
|
|||
}
|
||||
}
|
||||
|
||||
/// Information about a GameCube or Wii disc partition.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PartitionInfo {
|
||||
/// Partition group index
|
||||
pub group_index: u32,
|
||||
/// Partition index within the group
|
||||
pub part_index: u32,
|
||||
/// Partition offset within disc
|
||||
pub part_offset: u64,
|
||||
/// Partition kind
|
||||
pub kind: PartitionKind,
|
||||
/// Data offset within partition
|
||||
pub data_offset: u64,
|
||||
/// Data size
|
||||
pub data_size: u64,
|
||||
/// Raw Wii partition header
|
||||
pub header: Option<WiiPartitionHeader>,
|
||||
/// Lagged Fibonacci generator seed (for junk data)
|
||||
pub lfg_seed: [u8; 4],
|
||||
// /// Junk data start offset
|
||||
// pub junk_start: u64,
|
||||
}
|
||||
|
||||
/// Contains a disc's header & partition information.
|
||||
pub trait DiscBase: Send + Sync {
|
||||
/// Retrieves the disc's header.
|
||||
fn header(&self) -> &DiscHeader;
|
||||
|
||||
/// A list of partitions on the disc.
|
||||
fn partitions(&self) -> Vec<PartitionInfo>;
|
||||
|
||||
/// Opens a new, decrypted partition read stream for the specified partition index.
|
||||
///
|
||||
/// `validate_hashes`: Validate Wii disc hashes while reading (slow!)
|
||||
fn open_partition<'a>(
|
||||
&self,
|
||||
disc_io: &'a dyn DiscIO,
|
||||
index: usize,
|
||||
options: &OpenOptions,
|
||||
) -> Result<Box<dyn PartitionBase + 'a>>;
|
||||
|
||||
/// Opens a new partition read stream for the first partition matching
|
||||
/// the specified type.
|
||||
///
|
||||
/// `validate_hashes`: Validate Wii disc hashes while reading (slow!)
|
||||
fn open_partition_kind<'a>(
|
||||
&self,
|
||||
disc_io: &'a dyn DiscIO,
|
||||
part_type: PartitionKind,
|
||||
options: &OpenOptions,
|
||||
) -> Result<Box<dyn PartitionBase + 'a>>;
|
||||
|
||||
/// The disc's size in bytes, or an estimate if not stored by the format.
|
||||
fn disc_size(&self) -> u64;
|
||||
}
|
||||
|
||||
/// Creates a new [`DiscBase`] instance.
|
||||
pub fn new(disc_io: &mut dyn DiscIO) -> Result<Box<dyn DiscBase>> {
|
||||
let disc_size = disc_io.disc_size();
|
||||
let mut stream = disc_io.open()?;
|
||||
let header: DiscHeader = read_from(stream.as_mut()).context("Reading disc header")?;
|
||||
if header.is_wii() {
|
||||
Ok(Box::new(DiscWii::new(stream.as_mut(), header, disc_size)?))
|
||||
} else if header.is_gamecube() {
|
||||
Ok(Box::new(DiscGCN::new(stream.as_mut(), header, disc_size)?))
|
||||
} else {
|
||||
Err(Error::DiscFormat(format!(
|
||||
"Invalid GC/Wii magic: {:#010X}/{:#010X}",
|
||||
header.gcn_magic.get(),
|
||||
header.wii_magic.get()
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
/// An open read stream for a disc partition.
|
||||
pub trait PartitionBase: ReadStream {
|
||||
pub trait PartitionBase: DynClone + ReadStream + Send + Sync {
|
||||
/// Reads the partition header and file system table.
|
||||
fn meta(&mut self) -> Result<Box<PartitionMeta>>;
|
||||
|
||||
|
@ -366,6 +287,8 @@ pub trait PartitionBase: ReadStream {
|
|||
fn ideal_buffer_size(&self) -> usize;
|
||||
}
|
||||
|
||||
dyn_clone::clone_trait_object!(PartitionBase);
|
||||
|
||||
/// Size of the disc header and partition header (boot.bin)
|
||||
pub const BOOT_SIZE: usize = size_of::<DiscHeader>() + size_of::<PartitionHeader>();
|
||||
/// Size of the debug and region information (bi2.bin)
|
|
@ -8,14 +8,15 @@ use zerocopy::FromZeroes;
|
|||
|
||||
use crate::{
|
||||
disc::{
|
||||
gcn::PartitionGC,
|
||||
hashes::{rebuild_hashes, HashTable},
|
||||
partition::PartitionReader,
|
||||
wii::{WiiPartEntry, WiiPartGroup, WiiPartitionHeader, WII_PART_GROUP_OFF},
|
||||
wii::{PartitionWii, WiiPartEntry, WiiPartGroup, WiiPartitionHeader, WII_PART_GROUP_OFF},
|
||||
DL_DVD_SIZE, MINI_DVD_SIZE, SL_DVD_SIZE,
|
||||
},
|
||||
io::block::{BPartitionInfo, Block, BlockIO},
|
||||
io::block::{Block, BlockIO, PartitionInfo},
|
||||
util::read::{read_box, read_from, read_vec},
|
||||
DiscHeader, Error, PartitionHeader, PartitionKind, Result, ResultContext, SECTOR_SIZE,
|
||||
DiscHeader, DiscMeta, Error, OpenOptions, PartitionBase, PartitionHeader, PartitionKind,
|
||||
Result, ResultContext, SECTOR_SIZE,
|
||||
};
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
|
||||
|
@ -25,7 +26,7 @@ pub enum EncryptionMode {
|
|||
}
|
||||
|
||||
pub struct DiscReader {
|
||||
pub(crate) io: Box<dyn BlockIO>,
|
||||
io: Box<dyn BlockIO>,
|
||||
block: Option<Block>,
|
||||
block_buf: Box<[u8]>,
|
||||
block_idx: u32,
|
||||
|
@ -34,7 +35,7 @@ pub struct DiscReader {
|
|||
pos: u64,
|
||||
mode: EncryptionMode,
|
||||
disc_header: Box<DiscHeader>,
|
||||
pub(crate) partitions: Vec<BPartitionInfo>,
|
||||
pub(crate) partitions: Vec<PartitionInfo>,
|
||||
hash_tables: Vec<HashTable>,
|
||||
}
|
||||
|
||||
|
@ -57,9 +58,9 @@ impl Clone for DiscReader {
|
|||
}
|
||||
|
||||
impl DiscReader {
|
||||
pub fn new(inner: Box<dyn BlockIO>, mode: EncryptionMode) -> Result<Self> {
|
||||
pub fn new(inner: Box<dyn BlockIO>, options: &OpenOptions) -> Result<Self> {
|
||||
let block_size = inner.block_size();
|
||||
let meta = inner.meta()?;
|
||||
let meta = inner.meta();
|
||||
let mut reader = Self {
|
||||
io: inner,
|
||||
block: None,
|
||||
|
@ -68,7 +69,11 @@ impl DiscReader {
|
|||
sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed(),
|
||||
sector_idx: u32::MAX,
|
||||
pos: 0,
|
||||
mode,
|
||||
mode: if options.rebuild_encryption {
|
||||
EncryptionMode::Encrypted
|
||||
} else {
|
||||
EncryptionMode::Decrypted
|
||||
},
|
||||
disc_header: DiscHeader::new_box_zeroed(),
|
||||
partitions: vec![],
|
||||
hash_tables: vec![],
|
||||
|
@ -78,7 +83,7 @@ impl DiscReader {
|
|||
if reader.disc_header.is_wii() {
|
||||
reader.partitions = read_partition_info(&mut reader)?;
|
||||
// Rebuild hashes if the format requires it
|
||||
if mode == EncryptionMode::Encrypted && meta.needs_hash_recovery {
|
||||
if options.rebuild_encryption && meta.needs_hash_recovery {
|
||||
rebuild_hashes(&mut reader)?;
|
||||
}
|
||||
}
|
||||
|
@ -96,16 +101,53 @@ impl DiscReader {
|
|||
}
|
||||
|
||||
pub fn disc_size(&self) -> u64 {
|
||||
self.io
|
||||
.meta()
|
||||
.ok()
|
||||
.and_then(|m| m.disc_size)
|
||||
.unwrap_or_else(|| guess_disc_size(&self.partitions))
|
||||
self.io.meta().disc_size.unwrap_or_else(|| guess_disc_size(&self.partitions))
|
||||
}
|
||||
|
||||
pub fn header(&self) -> &DiscHeader { &self.disc_header }
|
||||
|
||||
pub fn partitions(&self) -> &[BPartitionInfo] { &self.partitions }
|
||||
pub fn partitions(&self) -> &[PartitionInfo] { &self.partitions }
|
||||
|
||||
pub fn meta(&self) -> DiscMeta { self.io.meta() }
|
||||
|
||||
/// Opens a new, decrypted partition read stream for the specified partition index.
|
||||
pub fn open_partition(
|
||||
&self,
|
||||
index: usize,
|
||||
options: &OpenOptions,
|
||||
) -> Result<Box<dyn PartitionBase>> {
|
||||
if self.disc_header.is_gamecube() {
|
||||
if index == 0 {
|
||||
Ok(PartitionGC::new(self.io.clone(), self.disc_header.clone())?)
|
||||
} else {
|
||||
Err(Error::DiscFormat("GameCube discs only have one partition".to_string()))
|
||||
}
|
||||
} else if let Some(part) = self.partitions.get(index) {
|
||||
Ok(PartitionWii::new(self.io.clone(), self.disc_header.clone(), part, options)?)
|
||||
} else {
|
||||
Err(Error::DiscFormat(format!("Partition {index} not found")))
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens a new, decrypted partition read stream for the first partition matching
|
||||
/// the specified type.
|
||||
pub fn open_partition_kind(
|
||||
&self,
|
||||
part_type: PartitionKind,
|
||||
options: &OpenOptions,
|
||||
) -> Result<Box<dyn PartitionBase>> {
|
||||
if self.disc_header.is_gamecube() {
|
||||
if part_type == PartitionKind::Data {
|
||||
Ok(PartitionGC::new(self.io.clone(), self.disc_header.clone())?)
|
||||
} else {
|
||||
Err(Error::DiscFormat("GameCube discs only have a data partition".to_string()))
|
||||
}
|
||||
} else if let Some(part) = self.partitions.iter().find(|v| v.kind == part_type) {
|
||||
Ok(PartitionWii::new(self.io.clone(), self.disc_header.clone(), part, options)?)
|
||||
} else {
|
||||
Err(Error::DiscFormat(format!("Partition type {part_type} not found")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for DiscReader {
|
||||
|
@ -135,14 +177,14 @@ impl Read for DiscReader {
|
|||
if let Some(partition) = partition {
|
||||
match self.mode {
|
||||
EncryptionMode::Decrypted => block.decrypt(
|
||||
&mut self.sector_buf,
|
||||
self.sector_buf.as_mut(),
|
||||
self.block_buf.as_ref(),
|
||||
block_idx,
|
||||
abs_sector,
|
||||
partition,
|
||||
)?,
|
||||
EncryptionMode::Encrypted => block.encrypt(
|
||||
&mut self.sector_buf,
|
||||
self.sector_buf.as_mut(),
|
||||
self.block_buf.as_ref(),
|
||||
block_idx,
|
||||
abs_sector,
|
||||
|
@ -151,7 +193,7 @@ impl Read for DiscReader {
|
|||
}
|
||||
} else {
|
||||
block.copy_raw(
|
||||
&mut self.sector_buf,
|
||||
self.sector_buf.as_mut(),
|
||||
self.block_buf.as_ref(),
|
||||
block_idx,
|
||||
abs_sector,
|
||||
|
@ -186,26 +228,26 @@ impl Seek for DiscReader {
|
|||
}
|
||||
}
|
||||
|
||||
fn read_partition_info(stream: &mut DiscReader) -> crate::Result<Vec<BPartitionInfo>> {
|
||||
stream.seek(SeekFrom::Start(WII_PART_GROUP_OFF)).context("Seeking to partition groups")?;
|
||||
let part_groups: [WiiPartGroup; 4] = read_from(stream).context("Reading partition groups")?;
|
||||
fn read_partition_info(reader: &mut DiscReader) -> crate::Result<Vec<PartitionInfo>> {
|
||||
reader.seek(SeekFrom::Start(WII_PART_GROUP_OFF)).context("Seeking to partition groups")?;
|
||||
let part_groups: [WiiPartGroup; 4] = read_from(reader).context("Reading partition groups")?;
|
||||
let mut part_info = Vec::new();
|
||||
for (group_idx, group) in part_groups.iter().enumerate() {
|
||||
let part_count = group.part_count.get();
|
||||
if part_count == 0 {
|
||||
continue;
|
||||
}
|
||||
stream
|
||||
reader
|
||||
.seek(SeekFrom::Start(group.part_entry_off()))
|
||||
.with_context(|| format!("Seeking to partition group {group_idx}"))?;
|
||||
let entries: Vec<WiiPartEntry> = read_vec(stream, part_count as usize)
|
||||
let entries: Vec<WiiPartEntry> = read_vec(reader, part_count as usize)
|
||||
.with_context(|| format!("Reading partition group {group_idx}"))?;
|
||||
for (part_idx, entry) in entries.iter().enumerate() {
|
||||
let offset = entry.offset();
|
||||
stream
|
||||
reader
|
||||
.seek(SeekFrom::Start(offset))
|
||||
.with_context(|| format!("Seeking to partition data {group_idx}:{part_idx}"))?;
|
||||
let header: Box<WiiPartitionHeader> = read_box(stream)
|
||||
let header: Box<WiiPartitionHeader> = read_box(reader)
|
||||
.with_context(|| format!("Reading partition header {group_idx}:{part_idx}"))?;
|
||||
|
||||
let key = header.ticket.decrypt_title_key()?;
|
||||
|
@ -224,8 +266,8 @@ fn read_partition_info(stream: &mut DiscReader) -> crate::Result<Vec<BPartitionI
|
|||
"Partition {group_idx}:{part_idx} data is not sector aligned",
|
||||
)));
|
||||
}
|
||||
let mut info = BPartitionInfo {
|
||||
index: part_info.len() as u32,
|
||||
let mut info = PartitionInfo {
|
||||
index: part_info.len(),
|
||||
kind: entry.kind.get().into(),
|
||||
start_sector: (start_offset / SECTOR_SIZE as u64) as u32,
|
||||
data_start_sector: (data_start_offset / SECTOR_SIZE as u64) as u32,
|
||||
|
@ -237,7 +279,12 @@ fn read_partition_info(stream: &mut DiscReader) -> crate::Result<Vec<BPartitionI
|
|||
hash_table: None,
|
||||
};
|
||||
|
||||
let mut partition_reader = PartitionReader::new(stream.io.clone(), &info)?;
|
||||
let mut partition_reader = PartitionWii::new(
|
||||
reader.io.clone(),
|
||||
reader.disc_header.clone(),
|
||||
&info,
|
||||
&OpenOptions::default(),
|
||||
)?;
|
||||
info.disc_header = read_box(&mut partition_reader).context("Reading disc header")?;
|
||||
info.partition_header =
|
||||
read_box(&mut partition_reader).context("Reading partition header")?;
|
||||
|
@ -248,7 +295,7 @@ fn read_partition_info(stream: &mut DiscReader) -> crate::Result<Vec<BPartitionI
|
|||
Ok(part_info)
|
||||
}
|
||||
|
||||
fn guess_disc_size(part_info: &[BPartitionInfo]) -> u64 {
|
||||
fn guess_disc_size(part_info: &[PartitionInfo]) -> u64 {
|
||||
let max_offset = part_info
|
||||
.iter()
|
||||
.flat_map(|v| {
|
|
@ -0,0 +1,447 @@
|
|||
use std::{
|
||||
cmp::min,
|
||||
ffi::CStr,
|
||||
io,
|
||||
io::{Read, Seek, SeekFrom},
|
||||
mem::size_of,
|
||||
};
|
||||
|
||||
use sha1::{digest, Digest, Sha1};
|
||||
use zerocopy::{big_endian::*, AsBytes, FromBytes, FromZeroes};
|
||||
|
||||
use crate::{
|
||||
array_ref,
|
||||
disc::{
|
||||
gcn::{read_part_meta, PartitionGC},
|
||||
PartitionBase, PartitionKind, PartitionMeta, SECTOR_SIZE,
|
||||
},
|
||||
fst::{Node, NodeKind},
|
||||
io::{
|
||||
aes_decrypt,
|
||||
block::{Block, BlockIO, PartitionInfo},
|
||||
KeyBytes,
|
||||
},
|
||||
static_assert,
|
||||
streams::{ReadStream, SharedWindowedReadStream},
|
||||
util::{div_rem, read::read_box_slice},
|
||||
DiscHeader, Error, OpenOptions, Result, ResultContext,
|
||||
};
|
||||
|
||||
pub(crate) const HASHES_SIZE: usize = 0x400;
|
||||
pub(crate) const SECTOR_DATA_SIZE: usize = SECTOR_SIZE - HASHES_SIZE; // 0x7C00
|
||||
|
||||
// ppki (Retail)
|
||||
const RVL_CERT_ISSUER_PPKI_TICKET: &str = "Root-CA00000001-XS00000003";
|
||||
#[rustfmt::skip]
|
||||
const RETAIL_COMMON_KEYS: [KeyBytes; 3] = [
|
||||
/* RVL_KEY_RETAIL */
|
||||
[0xeb, 0xe4, 0x2a, 0x22, 0x5e, 0x85, 0x93, 0xe4, 0x48, 0xd9, 0xc5, 0x45, 0x73, 0x81, 0xaa, 0xf7],
|
||||
/* RVL_KEY_KOREAN */
|
||||
[0x63, 0xb8, 0x2b, 0xb4, 0xf4, 0x61, 0x4e, 0x2e, 0x13, 0xf2, 0xfe, 0xfb, 0xba, 0x4c, 0x9b, 0x7e],
|
||||
/* vWii_KEY_RETAIL */
|
||||
[0x30, 0xbf, 0xc7, 0x6e, 0x7c, 0x19, 0xaf, 0xbb, 0x23, 0x16, 0x33, 0x30, 0xce, 0xd7, 0xc2, 0x8d],
|
||||
];
|
||||
|
||||
// dpki (Debug)
|
||||
const RVL_CERT_ISSUER_DPKI_TICKET: &str = "Root-CA00000002-XS00000006";
|
||||
#[rustfmt::skip]
|
||||
const DEBUG_COMMON_KEYS: [KeyBytes; 3] = [
|
||||
/* RVL_KEY_DEBUG */
|
||||
[0xa1, 0x60, 0x4a, 0x6a, 0x71, 0x23, 0xb5, 0x29, 0xae, 0x8b, 0xec, 0x32, 0xc8, 0x16, 0xfc, 0xaa],
|
||||
/* RVL_KEY_KOREAN_DEBUG */
|
||||
[0x67, 0x45, 0x8b, 0x6b, 0xc6, 0x23, 0x7b, 0x32, 0x69, 0x98, 0x3c, 0x64, 0x73, 0x48, 0x33, 0x66],
|
||||
/* vWii_KEY_DEBUG */
|
||||
[0x2f, 0x5c, 0x1b, 0x29, 0x44, 0xe7, 0xfd, 0x6f, 0xc3, 0x97, 0x96, 0x4b, 0x05, 0x76, 0x91, 0xfa],
|
||||
];
|
||||
|
||||
#[derive(Debug, PartialEq, FromBytes, FromZeroes, AsBytes)]
|
||||
#[repr(C, align(4))]
|
||||
pub(crate) struct WiiPartEntry {
|
||||
pub(crate) offset: U32,
|
||||
pub(crate) kind: U32,
|
||||
}
|
||||
|
||||
static_assert!(size_of::<WiiPartEntry>() == 8);
|
||||
|
||||
impl WiiPartEntry {
|
||||
pub(crate) fn offset(&self) -> u64 { (self.offset.get() as u64) << 2 }
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) struct WiiPartInfo {
|
||||
pub(crate) group_idx: u32,
|
||||
pub(crate) part_idx: u32,
|
||||
pub(crate) offset: u64,
|
||||
pub(crate) kind: PartitionKind,
|
||||
pub(crate) header: WiiPartitionHeader,
|
||||
pub(crate) junk_id: [u8; 4],
|
||||
pub(crate) junk_start: u64,
|
||||
pub(crate) title_key: KeyBytes,
|
||||
}
|
||||
|
||||
pub(crate) const WII_PART_GROUP_OFF: u64 = 0x40000;
|
||||
|
||||
#[derive(Debug, PartialEq, FromBytes, FromZeroes, AsBytes)]
|
||||
#[repr(C, align(4))]
|
||||
pub(crate) struct WiiPartGroup {
|
||||
pub(crate) part_count: U32,
|
||||
pub(crate) part_entry_off: U32,
|
||||
}
|
||||
|
||||
static_assert!(size_of::<WiiPartGroup>() == 8);
|
||||
|
||||
impl WiiPartGroup {
|
||||
pub(crate) fn part_entry_off(&self) -> u64 { (self.part_entry_off.get() as u64) << 2 }
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, FromBytes, FromZeroes, AsBytes)]
|
||||
#[repr(C, align(4))]
|
||||
pub struct SignedHeader {
|
||||
/// Signature type, always 0x00010001 (RSA-2048)
|
||||
pub sig_type: U32,
|
||||
/// RSA-2048 signature
|
||||
pub sig: [u8; 256],
|
||||
_pad: [u8; 60],
|
||||
}
|
||||
|
||||
static_assert!(size_of::<SignedHeader>() == 0x140);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Default, FromBytes, FromZeroes, AsBytes)]
|
||||
#[repr(C, align(4))]
|
||||
pub struct TicketTimeLimit {
|
||||
pub enable_time_limit: U32,
|
||||
pub time_limit: U32,
|
||||
}
|
||||
|
||||
static_assert!(size_of::<TicketTimeLimit>() == 8);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, FromBytes, FromZeroes, AsBytes)]
|
||||
#[repr(C, align(4))]
|
||||
pub struct Ticket {
|
||||
pub header: SignedHeader,
|
||||
pub sig_issuer: [u8; 64],
|
||||
pub ecdh: [u8; 60],
|
||||
pub version: u8,
|
||||
_pad1: U16,
|
||||
pub title_key: KeyBytes,
|
||||
_pad2: u8,
|
||||
pub ticket_id: [u8; 8],
|
||||
pub console_id: [u8; 4],
|
||||
pub title_id: [u8; 8],
|
||||
_pad3: U16,
|
||||
pub ticket_title_version: U16,
|
||||
pub permitted_titles_mask: U32,
|
||||
pub permit_mask: U32,
|
||||
pub title_export_allowed: u8,
|
||||
pub common_key_idx: u8,
|
||||
_pad4: [u8; 48],
|
||||
pub content_access_permissions: [u8; 64],
|
||||
_pad5: [u8; 2],
|
||||
pub time_limits: [TicketTimeLimit; 8],
|
||||
}
|
||||
|
||||
static_assert!(size_of::<Ticket>() == 0x2A4);
|
||||
|
||||
impl Ticket {
|
||||
pub fn decrypt_title_key(&self) -> Result<KeyBytes> {
|
||||
let mut iv: KeyBytes = [0; 16];
|
||||
iv[..8].copy_from_slice(&self.title_id);
|
||||
let cert_issuer_ticket =
|
||||
CStr::from_bytes_until_nul(&self.sig_issuer).ok().and_then(|c| c.to_str().ok());
|
||||
let common_keys = match cert_issuer_ticket {
|
||||
Some(RVL_CERT_ISSUER_PPKI_TICKET) => &RETAIL_COMMON_KEYS,
|
||||
Some(RVL_CERT_ISSUER_DPKI_TICKET) => &DEBUG_COMMON_KEYS,
|
||||
Some(v) => {
|
||||
return Err(Error::DiscFormat(format!("unknown certificate issuer {:?}", v)));
|
||||
}
|
||||
None => {
|
||||
return Err(Error::DiscFormat("failed to parse certificate issuer".to_string()));
|
||||
}
|
||||
};
|
||||
let common_key = common_keys.get(self.common_key_idx as usize).ok_or(Error::DiscFormat(
|
||||
format!("unknown common key index {}", self.common_key_idx),
|
||||
))?;
|
||||
let mut title_key = self.title_key;
|
||||
aes_decrypt(common_key, iv, &mut title_key);
|
||||
Ok(title_key)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, FromBytes, FromZeroes, AsBytes)]
|
||||
#[repr(C, align(4))]
|
||||
pub struct TmdHeader {
|
||||
pub header: SignedHeader,
|
||||
pub sig_issuer: [u8; 64],
|
||||
pub version: u8,
|
||||
pub ca_crl_version: u8,
|
||||
pub signer_crl_version: u8,
|
||||
pub is_vwii: u8,
|
||||
pub ios_id: [u8; 8],
|
||||
pub title_id: [u8; 8],
|
||||
pub title_type: u32,
|
||||
pub group_id: U16,
|
||||
_pad1: [u8; 2],
|
||||
pub region: U16,
|
||||
pub ratings: KeyBytes,
|
||||
_pad2: [u8; 12],
|
||||
pub ipc_mask: [u8; 12],
|
||||
_pad3: [u8; 18],
|
||||
pub access_flags: U32,
|
||||
pub title_version: U16,
|
||||
pub num_contents: U16,
|
||||
pub boot_idx: U16,
|
||||
pub minor_version: U16,
|
||||
}
|
||||
|
||||
static_assert!(size_of::<TmdHeader>() == 0x1E4);
|
||||
|
||||
pub const H3_TABLE_SIZE: usize = 0x18000;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, FromBytes, FromZeroes, AsBytes)]
|
||||
#[repr(C, align(4))]
|
||||
pub struct WiiPartitionHeader {
|
||||
pub ticket: Ticket,
|
||||
tmd_size: U32,
|
||||
tmd_off: U32,
|
||||
cert_chain_size: U32,
|
||||
cert_chain_off: U32,
|
||||
h3_table_off: U32,
|
||||
data_off: U32,
|
||||
data_size: U32,
|
||||
}
|
||||
|
||||
static_assert!(size_of::<WiiPartitionHeader>() == 0x2C0);
|
||||
|
||||
impl WiiPartitionHeader {
|
||||
pub fn tmd_size(&self) -> u64 { self.tmd_size.get() as u64 }
|
||||
|
||||
pub fn tmd_off(&self) -> u64 { (self.tmd_off.get() as u64) << 2 }
|
||||
|
||||
pub fn cert_chain_size(&self) -> u64 { self.cert_chain_size.get() as u64 }
|
||||
|
||||
pub fn cert_chain_off(&self) -> u64 { (self.cert_chain_off.get() as u64) << 2 }
|
||||
|
||||
pub fn h3_table_off(&self) -> u64 { (self.h3_table_off.get() as u64) << 2 }
|
||||
|
||||
pub fn h3_table_size(&self) -> u64 { H3_TABLE_SIZE as u64 }
|
||||
|
||||
pub fn data_off(&self) -> u64 { (self.data_off.get() as u64) << 2 }
|
||||
|
||||
pub fn data_size(&self) -> u64 { (self.data_size.get() as u64) << 2 }
|
||||
}
|
||||
|
||||
pub struct PartitionWii {
|
||||
io: Box<dyn BlockIO>,
|
||||
partition: PartitionInfo,
|
||||
block: Option<Block>,
|
||||
block_buf: Box<[u8]>,
|
||||
block_idx: u32,
|
||||
sector_buf: Box<[u8; SECTOR_SIZE]>,
|
||||
sector: u32,
|
||||
pos: u64,
|
||||
verify: bool,
|
||||
raw_tmd: Box<[u8]>,
|
||||
raw_cert_chain: Box<[u8]>,
|
||||
raw_h3_table: Box<[u8]>,
|
||||
}
|
||||
|
||||
impl Clone for PartitionWii {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
io: self.io.clone(),
|
||||
partition: self.partition.clone(),
|
||||
block: None,
|
||||
block_buf: <u8>::new_box_slice_zeroed(self.block_buf.len()),
|
||||
block_idx: u32::MAX,
|
||||
sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed(),
|
||||
sector: u32::MAX,
|
||||
pos: 0,
|
||||
verify: self.verify,
|
||||
raw_tmd: self.raw_tmd.clone(),
|
||||
raw_cert_chain: self.raw_cert_chain.clone(),
|
||||
raw_h3_table: self.raw_h3_table.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartitionWii {
|
||||
pub fn new(
|
||||
inner: Box<dyn BlockIO>,
|
||||
disc_header: Box<DiscHeader>,
|
||||
partition: &PartitionInfo,
|
||||
options: &OpenOptions,
|
||||
) -> Result<Box<Self>> {
|
||||
let block_size = inner.block_size();
|
||||
let mut reader = PartitionGC::new(inner, disc_header)?;
|
||||
|
||||
// Read TMD, cert chain, and H3 table
|
||||
let offset = partition.start_sector as u64 * SECTOR_SIZE as u64;
|
||||
reader
|
||||
.seek(SeekFrom::Start(offset + partition.header.tmd_off()))
|
||||
.context("Seeking to TMD offset")?;
|
||||
let raw_tmd: Box<[u8]> = read_box_slice(&mut reader, partition.header.tmd_size() as usize)
|
||||
.context("Reading TMD")?;
|
||||
reader
|
||||
.seek(SeekFrom::Start(offset + partition.header.cert_chain_off()))
|
||||
.context("Seeking to cert chain offset")?;
|
||||
let raw_cert_chain: Box<[u8]> =
|
||||
read_box_slice(&mut reader, partition.header.cert_chain_size() as usize)
|
||||
.context("Reading cert chain")?;
|
||||
reader
|
||||
.seek(SeekFrom::Start(offset + partition.header.h3_table_off()))
|
||||
.context("Seeking to H3 table offset")?;
|
||||
let raw_h3_table: Box<[u8]> =
|
||||
read_box_slice(&mut reader, H3_TABLE_SIZE).context("Reading H3 table")?;
|
||||
|
||||
Ok(Box::new(Self {
|
||||
io: reader.into_inner(),
|
||||
partition: partition.clone(),
|
||||
block: None,
|
||||
block_buf: <u8>::new_box_slice_zeroed(block_size as usize),
|
||||
block_idx: u32::MAX,
|
||||
sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed(),
|
||||
sector: u32::MAX,
|
||||
pos: 0,
|
||||
verify: options.validate_hashes,
|
||||
raw_tmd,
|
||||
raw_cert_chain,
|
||||
raw_h3_table,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for PartitionWii {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let partition_sector = (self.pos / SECTOR_DATA_SIZE as u64) as u32;
|
||||
let sector = self.partition.data_start_sector + partition_sector;
|
||||
if sector >= self.partition.data_end_sector {
|
||||
return Ok(0);
|
||||
}
|
||||
let block_idx = (sector as u64 * SECTOR_SIZE as u64 / self.block_buf.len() as u64) as u32;
|
||||
|
||||
// Read new block if necessary
|
||||
if block_idx != self.block_idx {
|
||||
self.block =
|
||||
self.io.read_block(self.block_buf.as_mut(), block_idx, Some(&self.partition))?;
|
||||
self.block_idx = block_idx;
|
||||
}
|
||||
|
||||
// Decrypt sector if necessary
|
||||
if sector != self.sector {
|
||||
let Some(block) = &self.block else {
|
||||
return Ok(0);
|
||||
};
|
||||
block.decrypt(
|
||||
self.sector_buf.as_mut(),
|
||||
self.block_buf.as_ref(),
|
||||
block_idx,
|
||||
sector,
|
||||
&self.partition,
|
||||
)?;
|
||||
if self.verify {
|
||||
verify_hashes(&self.sector_buf, sector)?;
|
||||
}
|
||||
self.sector = sector;
|
||||
}
|
||||
|
||||
let offset = (self.pos % SECTOR_DATA_SIZE as u64) as usize;
|
||||
let len = min(buf.len(), SECTOR_DATA_SIZE - offset);
|
||||
buf[..len]
|
||||
.copy_from_slice(&self.sector_buf[HASHES_SIZE + offset..HASHES_SIZE + offset + len]);
|
||||
self.pos += len as u64;
|
||||
Ok(len)
|
||||
}
|
||||
}
|
||||
|
||||
impl Seek for PartitionWii {
|
||||
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
||||
self.pos = match pos {
|
||||
SeekFrom::Start(v) => v,
|
||||
SeekFrom::End(_) => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Unsupported,
|
||||
"WiiPartitionReader: SeekFrom::End is not supported".to_string(),
|
||||
));
|
||||
}
|
||||
SeekFrom::Current(v) => self.pos.saturating_add_signed(v),
|
||||
};
|
||||
Ok(self.pos)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn as_digest(slice: &[u8; 20]) -> digest::Output<Sha1> { (*slice).into() }
|
||||
|
||||
fn verify_hashes(buf: &[u8; SECTOR_SIZE], sector: u32) -> io::Result<()> {
|
||||
let (mut group, sub_group) = div_rem(sector as usize, 8);
|
||||
group %= 8;
|
||||
|
||||
// H0 hashes
|
||||
for i in 0..31 {
|
||||
let mut hash = Sha1::new();
|
||||
hash.update(array_ref![buf, (i + 1) * 0x400, 0x400]);
|
||||
let expected = as_digest(array_ref![buf, i * 20, 20]);
|
||||
let output = hash.finalize();
|
||||
if output != expected {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("Invalid H0 hash! (block {:?}) {:x}\n\texpected {:x}", i, output, expected),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// H1 hash
|
||||
{
|
||||
let mut hash = Sha1::new();
|
||||
hash.update(array_ref![buf, 0, 0x26C]);
|
||||
let expected = as_digest(array_ref![buf, 0x280 + sub_group * 20, 20]);
|
||||
let output = hash.finalize();
|
||||
if output != expected {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"Invalid H1 hash! (subgroup {:?}) {:x}\n\texpected {:x}",
|
||||
sub_group, output, expected
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// H2 hash
|
||||
{
|
||||
let mut hash = Sha1::new();
|
||||
hash.update(array_ref![buf, 0x280, 0xA0]);
|
||||
let expected = as_digest(array_ref![buf, 0x340 + group * 20, 20]);
|
||||
let output = hash.finalize();
|
||||
if output != expected {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"Invalid H2 hash! (group {:?}) {:x}\n\texpected {:x}",
|
||||
group, output, expected
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
// TODO H3 hash
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl PartitionBase for PartitionWii {
|
||||
fn meta(&mut self) -> Result<Box<PartitionMeta>> {
|
||||
self.seek(SeekFrom::Start(0)).context("Seeking to partition header")?;
|
||||
let mut meta = read_part_meta(self, true)?;
|
||||
meta.raw_ticket = Some(Box::from(self.partition.header.ticket.as_bytes()));
|
||||
meta.raw_tmd = Some(self.raw_tmd.clone());
|
||||
meta.raw_cert_chain = Some(self.raw_cert_chain.clone());
|
||||
meta.raw_h3_table = Some(self.raw_h3_table.clone());
|
||||
Ok(meta)
|
||||
}
|
||||
|
||||
fn open_file(&mut self, node: &Node) -> io::Result<SharedWindowedReadStream> {
|
||||
assert_eq!(node.kind(), NodeKind::File);
|
||||
self.new_window(node.offset(true), node.length(true))
|
||||
}
|
||||
|
||||
fn ideal_buffer_size(&self) -> usize { SECTOR_DATA_SIZE }
|
||||
}
|
|
@ -12,8 +12,7 @@ use crate::{
|
|||
},
|
||||
io::{aes_decrypt, aes_encrypt, ciso, iso, nfs, wbfs, wia, KeyBytes, MagicBytes},
|
||||
util::{lfg::LaggedFibonacci, read::read_from},
|
||||
DiscHeader, DiscMeta, Error, OpenOptions, PartitionHeader, PartitionKind, Result,
|
||||
ResultContext,
|
||||
DiscHeader, DiscMeta, Error, PartitionHeader, PartitionKind, Result, ResultContext,
|
||||
};
|
||||
|
||||
/// Block I/O trait for reading disc images.
|
||||
|
@ -23,20 +22,20 @@ pub trait BlockIO: DynClone + Send + Sync {
|
|||
&mut self,
|
||||
out: &mut [u8],
|
||||
block: u32,
|
||||
partition: Option<&BPartitionInfo>,
|
||||
partition: Option<&PartitionInfo>,
|
||||
) -> io::Result<Option<Block>>;
|
||||
|
||||
/// The format's block size in bytes. Must be a multiple of the sector size (0x8000).
|
||||
fn block_size(&self) -> u32;
|
||||
|
||||
/// Returns extra metadata included in the disc file format, if any.
|
||||
fn meta(&self) -> Result<DiscMeta>;
|
||||
fn meta(&self) -> DiscMeta;
|
||||
}
|
||||
|
||||
dyn_clone::clone_trait_object!(BlockIO);
|
||||
|
||||
/// Creates a new [`BlockIO`] instance.
|
||||
pub fn open(filename: &Path, options: &OpenOptions) -> Result<Box<dyn BlockIO>> {
|
||||
pub fn open(filename: &Path) -> Result<Box<dyn BlockIO>> {
|
||||
let path_result = fs::canonicalize(filename);
|
||||
if let Err(err) = path_result {
|
||||
return Err(Error::Io(format!("Failed to open {}", filename.display()), err));
|
||||
|
@ -58,20 +57,18 @@ pub fn open(filename: &Path, options: &OpenOptions) -> Result<Box<dyn BlockIO>>
|
|||
match magic {
|
||||
ciso::CISO_MAGIC => Ok(ciso::DiscIOCISO::new(path)?),
|
||||
nfs::NFS_MAGIC => match path.parent() {
|
||||
Some(parent) if parent.is_dir() => {
|
||||
Ok(nfs::DiscIONFS::new(path.parent().unwrap(), options)?)
|
||||
}
|
||||
Some(parent) if parent.is_dir() => Ok(nfs::DiscIONFS::new(path.parent().unwrap())?),
|
||||
_ => Err(Error::DiscFormat("Failed to locate NFS parent directory".to_string())),
|
||||
},
|
||||
wbfs::WBFS_MAGIC => Ok(wbfs::DiscIOWBFS::new(path)?),
|
||||
wia::WIA_MAGIC | wia::RVZ_MAGIC => Ok(wia::DiscIOWIA::new(path, options)?),
|
||||
wia::WIA_MAGIC | wia::RVZ_MAGIC => Ok(wia::DiscIOWIA::new(path)?),
|
||||
_ => Ok(iso::DiscIOISO::new(path)?),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BPartitionInfo {
|
||||
pub index: u32,
|
||||
pub struct PartitionInfo {
|
||||
pub index: usize,
|
||||
pub kind: PartitionKind,
|
||||
pub start_sector: u32,
|
||||
pub data_start_sector: u32,
|
||||
|
@ -99,13 +96,14 @@ pub enum Block {
|
|||
}
|
||||
|
||||
impl Block {
|
||||
/// Decrypts the block's data (if necessary) and writes it to the output buffer.
|
||||
pub(crate) fn decrypt(
|
||||
self,
|
||||
out: &mut [u8; SECTOR_SIZE],
|
||||
data: &[u8],
|
||||
block_idx: u32,
|
||||
abs_sector: u32,
|
||||
partition: &BPartitionInfo,
|
||||
partition: &PartitionInfo,
|
||||
) -> io::Result<()> {
|
||||
let rel_sector = abs_sector - self.start_sector(block_idx, data.len());
|
||||
match self {
|
||||
|
@ -131,13 +129,14 @@ impl Block {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Encrypts the block's data (if necessary) and writes it to the output buffer.
|
||||
pub(crate) fn encrypt(
|
||||
self,
|
||||
out: &mut [u8; SECTOR_SIZE],
|
||||
data: &[u8],
|
||||
block_idx: u32,
|
||||
abs_sector: u32,
|
||||
partition: &BPartitionInfo,
|
||||
partition: &PartitionInfo,
|
||||
) -> io::Result<()> {
|
||||
let rel_sector = abs_sector - self.start_sector(block_idx, data.len());
|
||||
match self {
|
||||
|
@ -165,6 +164,7 @@ impl Block {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Copies the block's raw data to the output buffer.
|
||||
pub(crate) fn copy_raw(
|
||||
self,
|
||||
out: &mut [u8; SECTOR_SIZE],
|
||||
|
@ -225,7 +225,7 @@ fn block_sector<const N: usize>(data: &[u8], sector_idx: u32) -> io::Result<&[u8
|
|||
fn generate_junk(
|
||||
out: &mut [u8; SECTOR_SIZE],
|
||||
sector: u32,
|
||||
partition: Option<&BPartitionInfo>,
|
||||
partition: Option<&PartitionInfo>,
|
||||
disc_header: &DiscHeader,
|
||||