Add conversion support & large refactor

This commit is contained in:
Luke Street
2024-11-22 00:01:26 -07:00
parent 374c6950b2
commit 3848edfe7b
55 changed files with 9109 additions and 3065 deletions
+8 -8
View File
@@ -95,42 +95,42 @@ jobs:
target: x86_64-unknown-linux-musl
name: linux-x86_64
build: zigbuild
features: asm
features: openssl-vendored
- platform: ubuntu-latest
target: i686-unknown-linux-musl
name: linux-i686
build: zigbuild
features: asm
features: openssl-vendored
- platform: ubuntu-latest
target: aarch64-unknown-linux-musl
name: linux-aarch64
build: zigbuild
features: nightly
features: openssl-vendored
- platform: windows-latest
target: i686-pc-windows-msvc
name: windows-x86
build: build
features: default
features: openssl-vendored
- platform: windows-latest
target: x86_64-pc-windows-msvc
name: windows-x86_64
build: build
features: default
features: openssl-vendored
- platform: windows-latest
target: aarch64-pc-windows-msvc
name: windows-arm64
build: build
features: nightly
features: openssl-vendored
- platform: macos-latest
target: x86_64-apple-darwin
name: macos-x86_64
build: build
features: asm
features: openssl
- platform: macos-latest
target: aarch64-apple-darwin
name: macos-arm64
build: build
features: nightly
features: openssl
fail-fast: false
runs-on: ${{ matrix.platform }}
steps:
Generated
+586 -128
View File
File diff suppressed because it is too large Load Diff
+10
View File
@@ -2,6 +2,9 @@
members = ["nod", "nodtool"]
resolver = "2"
[profile.release]
debug = 1
[profile.release-lto]
inherits = "release"
lto = "fat"
@@ -16,3 +19,10 @@ authors = ["Luke Street <luke@street.dev>"]
license = "MIT OR Apache-2.0"
repository = "https://github.com/encounter/nod"
keywords = ["gamecube", "wii", "iso", "wbfs", "rvz"]
[workspace.dependencies]
digest = { version = "0.11.0-pre.9", default-features = false }
md-5 = { version = "0.11.0-pre.4", default-features = false }
sha1 = { version = "0.11.0-pre.4", default-features = false }
tracing = "0.1"
zerocopy = { version = "0.8", features = ["alloc", "derive"] }
+26 -11
View File
@@ -16,27 +16,42 @@ categories = ["command-line-utilities", "parser-implementations"]
[features]
default = ["compress-bzip2", "compress-lzma", "compress-zlib", "compress-zstd"]
asm = ["sha1/asm"]
compress-bzip2 = ["bzip2"]
compress-lzma = ["liblzma"]
compress-lzma = ["liblzma", "liblzma-sys"]
compress-zlib = ["adler", "miniz_oxide"]
compress-zstd = ["zstd"]
compress-zstd = ["zstd", "zstd-safe"]
openssl = ["dep:openssl"]
openssl-vendored = ["openssl", "openssl/vendored"]
[dependencies]
adler = { version = "1.0", optional = true }
aes = "0.8"
aes = "0.9.0-pre.2"
base16ct = "0.2"
bit-set = "0.8"
bytes = "1.8"
bzip2 = { version = "0.4", features = ["static"], optional = true }
cbc = "0.1"
digest = "0.10"
cbc = "0.2.0-pre.2"
crc32fast = "1.4"
crossbeam-channel = "0.5"
crossbeam-utils = "0.8"
digest = { workspace = true }
dyn-clone = "1.0"
encoding_rs = "0.8"
itertools = "0.13"
liblzma = { version = "0.3", features = ["static"], optional = true }
log = "0.4"
liblzma-sys = { version = "0.3", features = ["static"], optional = true }
lru = "0.12"
md-5 = { workspace = true }
memmap2 = "0.9"
miniz_oxide = { version = "0.8", optional = true }
openssl = { version = "0.10", optional = true }
rand = "0.8"
rayon = "1.10"
sha1 = "0.10"
thiserror = "1.0"
zerocopy = { version = "0.8", features = ["alloc", "derive"] }
zstd = { version = "0.13", optional = true }
sha1 = { workspace = true }
simple_moving_average = "1.0"
thiserror = "2.0"
tracing = { workspace = true }
xxhash-rust = { version = "0.8", features = ["xxh64"] }
zerocopy = { workspace = true }
zstd = { version = "0.13", optional = true, default-features = false }
zstd-safe = { version = "7.2", optional = true, default-features = false }
+827
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -0,0 +1,4 @@
//! Disc image building.
pub mod gc;
pub mod wii;
+1
View File
@@ -0,0 +1 @@
#![allow(missing_docs)] // TODO
+325
View File
@@ -0,0 +1,325 @@
//! Common types.
use std::{borrow::Cow, fmt, str::FromStr, sync::Arc};
use crate::{
disc::{wii::WiiPartitionHeader, DiscHeader, PartitionHeader, SECTOR_SIZE},
Error, Result,
};
/// SHA-1 hash bytes
pub type HashBytes = [u8; 20];
/// AES key bytes
pub type KeyBytes = [u8; 16];
/// Magic bytes
pub type MagicBytes = [u8; 4];
/// The disc file format.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Format {
/// ISO / GCM (GameCube master disc)
#[default]
Iso,
/// CISO (Compact ISO)
Ciso,
/// GCZ
Gcz,
/// NFS (Wii U VC)
Nfs,
/// RVZ
Rvz,
/// WBFS
Wbfs,
/// WIA
Wia,
/// TGC
Tgc,
}
impl Format {
/// Returns the default block size for the disc format, if any.
pub fn default_block_size(self) -> u32 {
match self {
Format::Ciso => crate::io::ciso::DEFAULT_BLOCK_SIZE,
#[cfg(feature = "compress-zlib")]
Format::Gcz => crate::io::gcz::DEFAULT_BLOCK_SIZE,
Format::Rvz => crate::io::wia::RVZ_DEFAULT_CHUNK_SIZE,
Format::Wbfs => crate::io::wbfs::DEFAULT_BLOCK_SIZE,
Format::Wia => crate::io::wia::WIA_DEFAULT_CHUNK_SIZE,
_ => 0,
}
}
/// Returns the default compression algorithm for the disc format.
pub fn default_compression(self) -> Compression {
match self {
#[cfg(feature = "compress-zlib")]
Format::Gcz => crate::io::gcz::DEFAULT_COMPRESSION,
Format::Rvz => crate::io::wia::RVZ_DEFAULT_COMPRESSION,
Format::Wia => crate::io::wia::WIA_DEFAULT_COMPRESSION,
_ => Compression::None,
}
}
}
impl fmt::Display for Format {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Format::Iso => write!(f, "ISO"),
Format::Ciso => write!(f, "CISO"),
Format::Gcz => write!(f, "GCZ"),
Format::Nfs => write!(f, "NFS"),
Format::Rvz => write!(f, "RVZ"),
Format::Wbfs => write!(f, "WBFS"),
Format::Wia => write!(f, "WIA"),
Format::Tgc => write!(f, "TGC"),
}
}
}
/// The disc file format's compression algorithm.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Compression {
/// No compression
#[default]
None,
/// BZIP2
Bzip2(u8),
/// Deflate (GCZ only)
Deflate(u8),
/// LZMA
Lzma(u8),
/// LZMA2
Lzma2(u8),
/// Zstandard
Zstandard(i8),
}
impl Compression {
/// Validates the compression level. Sets the default level if the level is 0.
pub fn validate_level(&mut self) -> Result<()> {
match self {
Compression::Bzip2(level) => {
if *level == 0 {
*level = 9;
}
if *level > 9 {
return Err(Error::Other(format!(
"Invalid BZIP2 compression level: {level} (expected 1-9)"
)));
}
}
Compression::Deflate(level) => {
if *level == 0 {
*level = 9;
}
if *level > 10 {
return Err(Error::Other(format!(
"Invalid Deflate compression level: {level} (expected 1-10)"
)));
}
}
Compression::Lzma(level) => {
if *level == 0 {
*level = 6;
}
if *level > 9 {
return Err(Error::Other(format!(
"Invalid LZMA compression level: {level} (expected 1-9)"
)));
}
}
Compression::Lzma2(level) => {
if *level == 0 {
*level = 6;
}
if *level > 9 {
return Err(Error::Other(format!(
"Invalid LZMA2 compression level: {level} (expected 1-9)"
)));
}
}
Compression::Zstandard(level) => {
if *level == 0 {
*level = 19;
}
if *level < -22 || *level > 22 {
return Err(Error::Other(format!(
"Invalid Zstandard compression level: {level} (expected -22 to 22)"
)));
}
}
_ => {}
}
Ok(())
}
}
impl fmt::Display for Compression {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Compression::None => write!(f, "None"),
Compression::Bzip2(level) => {
if *level == 0 {
write!(f, "BZIP2")
} else {
write!(f, "BZIP2 ({level})")
}
}
Compression::Deflate(level) => {
if *level == 0 {
write!(f, "Deflate")
} else {
write!(f, "Deflate ({level})")
}
}
Compression::Lzma(level) => {
if *level == 0 {
write!(f, "LZMA")
} else {
write!(f, "LZMA ({level})")
}
}
Compression::Lzma2(level) => {
if *level == 0 {
write!(f, "LZMA2")
} else {
write!(f, "LZMA2 ({level})")
}
}
Compression::Zstandard(level) => {
if *level == 0 {
write!(f, "Zstandard")
} else {
write!(f, "Zstandard ({level})")
}
}
}
}
}
impl FromStr for Compression {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (format, level) =
if let Some((format, level_str)) = s.split_once(':').or_else(|| s.split_once('.')) {
let level = level_str
.parse::<i32>()
.map_err(|_| format!("Failed to parse compression level: {level_str:?}"))?;
(format, level)
} else {
(s, 0)
};
match format.to_ascii_lowercase().as_str() {
"" | "none" => Ok(Compression::None),
"bz2" | "bzip2" => Ok(Compression::Bzip2(level as u8)),
"deflate" | "gz" | "gzip" => Ok(Compression::Deflate(level as u8)),
"lzma" => Ok(Compression::Lzma(level as u8)),
"lzma2" | "xz" => Ok(Compression::Lzma2(level as u8)),
"zst" | "zstd" | "zstandard" => Ok(Compression::Zstandard(level as i8)),
_ => Err(format!("Unknown compression type: {format:?}")),
}
}
}
/// The kind of disc partition.
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub enum PartitionKind {
/// Data partition.
Data,
/// Update partition.
Update,
/// Channel partition.
Channel,
/// Other partition kind.
Other(u32),
}
impl fmt::Display for PartitionKind {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Data => write!(f, "Data"),
Self::Update => write!(f, "Update"),
Self::Channel => write!(f, "Channel"),
Self::Other(v) => {
let bytes = v.to_be_bytes();
write!(f, "Other ({:08X}, {})", v, String::from_utf8_lossy(&bytes))
}
}
}
}
impl PartitionKind {
/// Returns the directory name for the partition kind.
#[inline]
pub fn dir_name(&self) -> Cow<str> {
match self {
Self::Data => Cow::Borrowed("DATA"),
Self::Update => Cow::Borrowed("UPDATE"),
Self::Channel => Cow::Borrowed("CHANNEL"),
Self::Other(v) => {
let bytes = v.to_be_bytes();
Cow::Owned(format!("P-{}", String::from_utf8_lossy(&bytes)))
}
}
}
}
impl From<u32> for PartitionKind {
#[inline]
fn from(v: u32) -> Self {
match v {
0 => Self::Data,
1 => Self::Update,
2 => Self::Channel,
v => Self::Other(v),
}
}
}
/// Wii partition information.
#[derive(Debug, Clone)]
pub struct PartitionInfo {
/// The partition index.
pub index: usize,
/// The kind of disc partition.
pub kind: PartitionKind,
/// The start sector of the partition.
pub start_sector: u32,
/// The start sector of the partition's (usually encrypted) data.
pub data_start_sector: u32,
/// The end sector of the partition's (usually encrypted) data.
pub data_end_sector: u32,
/// The AES key for the partition, also known as the "title key".
pub key: KeyBytes,
/// The Wii partition header.
pub header: Arc<WiiPartitionHeader>,
/// The disc header within the partition.
pub disc_header: Arc<DiscHeader>,
/// The partition header within the partition.
pub partition_header: Arc<PartitionHeader>,
/// Whether the partition data is encrypted
pub has_encryption: bool,
/// Whether the partition data hashes are present
pub has_hashes: bool,
}
impl PartitionInfo {
/// Returns the size of the partition's data region in bytes.
#[inline]
pub fn data_size(&self) -> u64 {
(self.data_end_sector as u64 - self.data_start_sector as u64) * SECTOR_SIZE as u64
}
/// Returns whether the given sector is within the partition's data region.
#[inline]
pub fn data_contains_sector(&self, sector: u32) -> bool {
sector >= self.data_start_sector && sector < self.data_end_sector
}
}
+124
View File
@@ -0,0 +1,124 @@
use std::{
io,
io::{BufRead, Seek, SeekFrom},
sync::Arc,
};
use zerocopy::FromZeros;
use crate::{
common::KeyBytes,
disc::{wii::SECTOR_DATA_SIZE, DiscHeader, SECTOR_SIZE},
io::block::{Block, BlockReader},
util::impl_read_for_bufread,
Result,
};
pub enum DirectDiscReaderMode {
Raw,
Partition { disc_header: Arc<DiscHeader>, data_start_sector: u32, key: KeyBytes },
}
/// Simplified disc reader that uses a block reader directly.
///
/// This is used to read disc and partition metadata before we can construct a full disc reader.
pub struct DirectDiscReader {
io: Box<dyn BlockReader>,
block: Block,
block_buf: Box<[u8]>,
block_decrypted: bool,
pos: u64,
mode: DirectDiscReaderMode,
}
impl DirectDiscReader {
pub fn new(inner: Box<dyn BlockReader>) -> Result<Box<Self>> {
let block_size = inner.block_size() as usize;
Ok(Box::new(Self {
io: inner,
block: Block::default(),
block_buf: <[u8]>::new_box_zeroed_with_elems(block_size)?,
block_decrypted: false,
pos: 0,
mode: DirectDiscReaderMode::Raw,
}))
}
pub fn reset(&mut self, mode: DirectDiscReaderMode) {
self.block = Block::default();
self.block_decrypted = false;
self.pos = 0;
self.mode = mode;
}
pub fn into_inner(self) -> Box<dyn BlockReader> { self.io }
}
impl BufRead for DirectDiscReader {
fn fill_buf(&mut self) -> io::Result<&[u8]> {
match &self.mode {
DirectDiscReaderMode::Raw => {
// Read new block if necessary
let sector = (self.pos / SECTOR_SIZE as u64) as u32;
if self.block_decrypted || !self.block.contains(sector) {
self.block = self.io.read_block(self.block_buf.as_mut(), sector)?;
self.block_decrypted = false;
}
self.block.data(self.block_buf.as_ref(), self.pos)
}
DirectDiscReaderMode::Partition { disc_header, data_start_sector, key } => {
let has_encryption = disc_header.has_partition_encryption();
let has_hashes = disc_header.has_partition_hashes();
let part_sector = if has_hashes {
(self.pos / SECTOR_DATA_SIZE as u64) as u32
} else {
(self.pos / SECTOR_SIZE as u64) as u32
};
// Read new block if necessary
let abs_sector = data_start_sector + part_sector;
if !self.block.contains(abs_sector) {
self.block = self.io.read_block(self.block_buf.as_mut(), abs_sector)?;
self.block_decrypted = false;
}
// Allow reusing the same block from raw mode, just decrypt it if necessary
if !self.block_decrypted {
self.block
.decrypt_block(self.block_buf.as_mut(), has_encryption.then_some(*key))?;
self.block_decrypted = true;
}
self.block.partition_data(
self.block_buf.as_ref(),
self.pos,
*data_start_sector,
has_hashes,
)
}
}
}
#[inline]
fn consume(&mut self, amt: usize) { self.pos += amt as u64; }
}
impl_read_for_bufread!(DirectDiscReader);
impl Seek for DirectDiscReader {
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,
"DirectDiscReader: SeekFrom::End is not supported",
));
}
SeekFrom::Current(v) => self.pos.saturating_add_signed(v),
};
Ok(self.pos)
}
fn stream_position(&mut self) -> io::Result<u64> { Ok(self.pos) }
}
+198 -15
View File
@@ -1,11 +1,15 @@
//! Disc file system types
//! File system table (FST) types.
use std::{borrow::Cow, ffi::CStr, mem::size_of};
use encoding_rs::SHIFT_JIS;
use zerocopy::{big_endian::*, FromBytes, Immutable, IntoBytes, KnownLayout};
use itertools::Itertools;
use zerocopy::{big_endian::*, FromBytes, FromZeros, Immutable, IntoBytes, KnownLayout};
use crate::{static_assert, Result};
use crate::{
util::{array_ref, static_assert},
Error, Result,
};
/// File system node kind.
#[derive(Clone, Debug, PartialEq)]
@@ -25,13 +29,31 @@ pub struct Node {
kind: u8,
// u24 big-endian
name_offset: [u8; 3],
pub(crate) offset: U32,
offset: U32,
length: U32,
}
static_assert!(size_of::<Node>() == 12);
impl Node {
/// Create a new node.
#[inline]
pub fn new(kind: NodeKind, name_offset: u32, offset: u64, length: u32, is_wii: bool) -> Self {
Self {
kind: match kind {
NodeKind::File => 0,
NodeKind::Directory => 1,
NodeKind::Invalid => u8::MAX,
},
name_offset: *array_ref![name_offset.to_be_bytes(), 1, 3],
offset: U32::new(match kind {
NodeKind::File if is_wii => (offset / 4) as u32,
_ => offset as u32,
}),
length: U32::new(length),
}
}
/// File system node kind.
#[inline]
pub fn kind(&self) -> NodeKind {
@@ -42,6 +64,16 @@ impl Node {
}
}
/// Set the node kind.
#[inline]
pub fn set_kind(&mut self, kind: NodeKind) {
self.kind = match kind {
NodeKind::File => 0,
NodeKind::Directory => 1,
NodeKind::Invalid => u8::MAX,
};
}
/// Whether the node is a file.
#[inline]
pub fn is_file(&self) -> bool { self.kind == 0 }
@@ -56,6 +88,12 @@ impl Node {
u32::from_be_bytes([0, self.name_offset[0], self.name_offset[1], self.name_offset[2]])
}
/// Set the name offset of the node.
#[inline]
pub fn set_name_offset(&mut self, name_offset: u32) {
self.name_offset = *array_ref![name_offset.to_be_bytes(), 1, 3];
}
/// For files, this is the partition offset of the file data. (Wii: >> 2)
///
/// For directories, this is the parent node index in the FST.
@@ -68,16 +106,27 @@ impl Node {
}
}
/// Set the offset of the node. See [`Node::offset`] for details.
#[inline]
pub fn set_offset(&mut self, offset: u64, is_wii: bool) {
self.offset.set(if is_wii && self.is_file() { (offset / 4) as u32 } else { offset as u32 });
}
/// For files, this is the byte size of the file.
///
/// For directories, this is the child end index in the FST.
///
/// Number of child files and directories recursively is `length - offset`.
#[inline]
pub fn length(&self) -> u64 { self.length.get() as u64 }
pub fn length(&self) -> u32 { self.length.get() }
/// Set the length of the node. See [`Node::length`] for details.
#[inline]
pub fn set_length(&mut self, length: u32) { self.length.set(length); }
}
/// A view into the file system table (FST).
#[derive(Clone)]
pub struct Fst<'a> {
/// The nodes in the FST.
pub nodes: &'a [Node],
@@ -87,14 +136,13 @@ pub struct Fst<'a> {
impl<'a> Fst<'a> {
/// Create a new FST view from a buffer.
#[allow(clippy::missing_inline_in_public_items)]
pub fn new(buf: &'a [u8]) -> Result<Self, &'static str> {
let Ok((root_node, _)) = Node::ref_from_prefix(buf) else {
return Err("FST root node not found");
};
// String table starts after the last node
let string_base = root_node.length() * size_of::<Node>() as u64;
if string_base >= buf.len() as u64 {
let string_base = root_node.length() * size_of::<Node>() as u32;
if string_base > buf.len() as u32 {
return Err("FST string table out of bounds");
}
let (node_buf, string_table) = buf.split_at(string_base as usize);
@@ -104,10 +152,9 @@ impl<'a> Fst<'a> {
/// Iterate over the nodes in the FST.
#[inline]
pub fn iter(&self) -> FstIter { FstIter { fst: self, idx: 1 } }
pub fn iter(&self) -> FstIter { FstIter { fst: self.clone(), idx: 1, segments: vec![] } }
/// Get the name of a node.
#[allow(clippy::missing_inline_in_public_items)]
pub fn get_name(&self, node: Node) -> Result<Cow<'a, str>, String> {
let name_buf = self.string_table.get(node.name_offset() as usize..).ok_or_else(|| {
format!(
@@ -126,7 +173,6 @@ impl<'a> Fst<'a> {
}
/// Finds a particular file or directory by path.
#[allow(clippy::missing_inline_in_public_items)]
pub fn find(&self, path: &str) -> Option<(usize, Node)> {
let mut split = path.trim_matches('/').split('/');
let mut current = next_non_empty(&mut split);
@@ -160,23 +206,46 @@ impl<'a> Fst<'a> {
}
None
}
/// Count the number of files in the FST.
pub fn num_files(&self) -> usize { self.nodes.iter().filter(|n| n.is_file()).count() }
}
/// Iterator over the nodes in an FST.
///
/// For each node, the iterator yields the node index, the node itself,
/// and the full path to the node (separated by `/`).
pub struct FstIter<'a> {
fst: &'a Fst<'a>,
fst: Fst<'a>,
idx: usize,
segments: Vec<(Cow<'a, str>, usize)>,
}
impl<'a> Iterator for FstIter<'a> {
type Item = (usize, Node, Result<Cow<'a, str>, String>);
type Item = (usize, Node, String);
fn next(&mut self) -> Option<Self::Item> {
let idx = self.idx;
let node = self.fst.nodes.get(idx).copied()?;
let name = self.fst.get_name(node);
let name = self.fst.get_name(node).unwrap_or("<invalid>".into());
self.idx += 1;
Some((idx, node, name))
// Remove ended path segments
let mut new_size = 0;
for (_, end) in self.segments.iter() {
if *end == idx {
break;
}
new_size += 1;
}
self.segments.truncate(new_size);
// Add the new path segment
let length = node.length() as u64;
let end = if node.is_dir() { length as usize } else { idx + 1 };
self.segments.push((name, end));
let path = self.segments.iter().map(|(name, _)| name.as_ref()).join("/");
Some((idx, node, path))
}
}
@@ -190,3 +259,117 @@ fn next_non_empty<'a>(iter: &mut impl Iterator<Item = &'a str>) -> &'a str {
}
}
}
/// A builder for creating a file system table (FST).
pub struct FstBuilder {
nodes: Vec<Node>,
string_table: Vec<u8>,
stack: Vec<(String, u32)>,
is_wii: bool,
}
impl FstBuilder {
/// Create a new FST builder.
pub fn new(is_wii: bool) -> Self {
let mut builder = Self { nodes: vec![], string_table: vec![], stack: vec![], is_wii };
builder.add_node(NodeKind::Directory, "<root>", 0, 0);
builder
}
/// Create a new FST builder with an existing string table. This allows matching the string
/// ordering of an existing FST.
pub fn new_with_string_table(is_wii: bool, string_table: Vec<u8>) -> Result<Self> {
if matches!(string_table.last(), Some(n) if *n != 0) {
return Err(Error::DiscFormat("String table must be null-terminated".to_string()));
}
let root_name = CStr::from_bytes_until_nul(&string_table)
.map_err(|_| {
Error::DiscFormat("String table root name not null-terminated".to_string())
})?
.to_str()
.unwrap_or("<root>")
.to_string();
let mut builder = Self { nodes: vec![], string_table, stack: vec![], is_wii };
builder.add_node(NodeKind::Directory, &root_name, 0, 0);
Ok(builder)
}
/// Add a file to the FST. All paths within a directory must be added sequentially,
/// otherwise the output FST will be invalid.
pub fn add_file(&mut self, path: &str, offset: u64, size: u32) {
let components = path.split('/').collect::<Vec<_>>();
for i in 0..components.len() - 1 {
if matches!(self.stack.get(i), Some((name, _)) if name != components[i]) {
// Pop directories
while self.stack.len() > i {
let (_, idx) = self.stack.pop().unwrap();
let length = self.nodes.len() as u32;
self.nodes[idx as usize].set_length(length);
}
}
while i >= self.stack.len() {
// Push a new directory node
let component_idx = self.stack.len();
let parent = if component_idx == 0 { 0 } else { self.stack[component_idx - 1].1 };
let node_idx =
self.add_node(NodeKind::Directory, components[component_idx], parent as u64, 0);
self.stack.push((components[i].to_string(), node_idx));
}
}
if components.len() == 1 {
// Pop all directories
while let Some((_, idx)) = self.stack.pop() {
let length = self.nodes.len() as u32;
self.nodes[idx as usize].set_length(length);
}
}
// Add file node
self.add_node(NodeKind::File, components.last().unwrap(), offset, size);
}
/// Get the byte size of the FST.
pub fn byte_size(&self) -> usize {
size_of_val(self.nodes.as_slice()) + self.string_table.len()
}
/// Finalize the FST and return the serialized data.
pub fn finalize(mut self) -> Box<[u8]> {
// Finalize directory lengths
let node_count = self.nodes.len() as u32;
while let Some((_, idx)) = self.stack.pop() {
self.nodes[idx as usize].set_length(node_count);
}
self.nodes[0].set_length(node_count);
// Serialize nodes and string table
let nodes_data = self.nodes.as_bytes();
let string_table_data = self.string_table.as_bytes();
let mut data =
<[u8]>::new_box_zeroed_with_elems(nodes_data.len() + string_table_data.len()).unwrap();
data[..nodes_data.len()].copy_from_slice(self.nodes.as_bytes());
data[nodes_data.len()..].copy_from_slice(self.string_table.as_bytes());
data
}
fn add_node(&mut self, kind: NodeKind, name: &str, offset: u64, length: u32) -> u32 {
let (bytes, _, _) = SHIFT_JIS.encode(name);
// Check if the name already exists in the string table
let mut name_offset = 0;
while name_offset < self.string_table.len() {
let string_buf = &self.string_table[name_offset..];
let existing = CStr::from_bytes_until_nul(string_buf).unwrap();
if existing.to_bytes() == bytes.as_ref() {
break;
}
name_offset += existing.to_bytes_with_nul().len();
}
// Otherwise, add the name to the string table
if name_offset == self.string_table.len() {
self.string_table.extend_from_slice(bytes.as_ref());
self.string_table.push(0);
}
let idx = self.nodes.len() as u32;
self.nodes.push(Node::new(kind, name_offset as u32, offset, length, self.is_wii));
idx
}
}
+146 -136
View File
@@ -2,190 +2,144 @@ use std::{
io,
io::{BufRead, Read, Seek, SeekFrom},
mem::size_of,
sync::Arc,
};
use zerocopy::{FromBytes, FromZeros};
use zerocopy::FromBytes;
use super::{
ApploaderHeader, DiscHeader, DolHeader, FileStream, Node, PartitionBase, PartitionHeader,
PartitionMeta, BI2_SIZE, BOOT_SIZE, SECTOR_SIZE,
};
use crate::{
disc::streams::OwnedFileStream,
io::block::{Block, BlockIO},
util::read::{read_box, read_box_slice, read_vec},
disc::{
preloader::{Preloader, SectorGroup, SectorGroupRequest},
ApploaderHeader, DiscHeader, DolHeader, PartitionHeader, BI2_SIZE, BOOT_SIZE,
SECTOR_GROUP_SIZE, SECTOR_SIZE,
},
io::block::BlockReader,
read::{PartitionEncryption, PartitionMeta, PartitionReader},
util::{
impl_read_for_bufread,
read::{read_arc, read_arc_slice, read_vec},
},
Result, ResultContext,
};
pub struct PartitionGC {
io: Box<dyn BlockIO>,
block: Block,
block_buf: Box<[u8]>,
block_idx: u32,
sector_buf: Box<[u8; SECTOR_SIZE]>,
sector: u32,
pub struct PartitionReaderGC {
io: Box<dyn BlockReader>,
preloader: Arc<Preloader>,
pos: u64,
disc_header: Box<DiscHeader>,
disc_size: u64,
sector_group: Option<SectorGroup>,
meta: Option<PartitionMeta>,
}
impl Clone for PartitionGC {
impl Clone for PartitionReaderGC {
fn clone(&self) -> Self {
Self {
io: self.io.clone(),
block: Block::default(),
block_buf: <[u8]>::new_box_zeroed_with_elems(self.block_buf.len()).unwrap(),
block_idx: u32::MAX,
sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed().unwrap(),
sector: u32::MAX,
preloader: self.preloader.clone(),
pos: 0,
disc_header: self.disc_header.clone(),
disc_size: self.disc_size,
sector_group: None,
meta: self.meta.clone(),
}
}
}
impl PartitionGC {
pub fn new(inner: Box<dyn BlockIO>, disc_header: Box<DiscHeader>) -> Result<Box<Self>> {
let block_size = inner.block_size();
impl PartitionReaderGC {
pub fn new(
inner: Box<dyn BlockReader>,
preloader: Arc<Preloader>,
disc_size: u64,
) -> Result<Box<Self>> {
Ok(Box::new(Self {
io: inner,
block: Block::default(),
block_buf: <[u8]>::new_box_zeroed_with_elems(block_size as usize).unwrap(),
block_idx: u32::MAX,
sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed().unwrap(),
sector: u32::MAX,
preloader,
pos: 0,
disc_header,
disc_size,
sector_group: None,
meta: None,
}))
}
pub fn into_inner(self) -> Box<dyn BlockIO> { self.io }
}
impl BufRead for PartitionGC {
impl BufRead for PartitionReaderGC {
fn fill_buf(&mut self) -> io::Result<&[u8]> {
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;
if self.pos >= self.disc_size {
return Ok(&[]);
}
// Copy sector if necessary
if sector != self.sector {
self.block.copy_raw(
self.sector_buf.as_mut(),
self.block_buf.as_ref(),
sector,
&self.disc_header,
)?;
self.sector = sector;
}
let abs_sector = (self.pos / SECTOR_SIZE as u64) as u32;
let group_idx = abs_sector / 64;
let abs_group_sector = group_idx * 64;
let max_groups = self.disc_size.div_ceil(SECTOR_GROUP_SIZE as u64) as u32;
let request = SectorGroupRequest {
group_idx,
partition_idx: None,
mode: PartitionEncryption::Original,
};
let offset = (self.pos % SECTOR_SIZE as u64) as usize;
Ok(&self.sector_buf[offset..])
let sector_group = if matches!(&self.sector_group, Some(sector_group) if sector_group.request == request)
{
// We can improve this in Rust 2024 with `if_let_rescope`
// https://github.com/rust-lang/rust/issues/124085
self.sector_group.as_ref().unwrap()
} else {
self.sector_group.insert(self.preloader.fetch(request, max_groups)?)
};
// Calculate the number of consecutive sectors in the group
let group_sector = abs_sector - abs_group_sector;
let consecutive_sectors = sector_group.consecutive_sectors(group_sector);
if consecutive_sectors == 0 {
return Ok(&[]);
}
let num_sectors = group_sector + consecutive_sectors;
// Read from sector group buffer
let group_start = abs_group_sector as u64 * SECTOR_SIZE as u64;
let offset = (self.pos - group_start) as usize;
let end =
(num_sectors as u64 * SECTOR_SIZE as u64).min(self.disc_size - group_start) as usize;
Ok(&sector_group.data[offset..end])
}
#[inline]
fn consume(&mut self, amt: usize) { self.pos += amt as u64; }
}
impl Read for PartitionGC {
#[inline]
fn read(&mut self, out: &mut [u8]) -> io::Result<usize> {
let buf = self.fill_buf()?;
let len = buf.len().min(out.len());
out[..len].copy_from_slice(&buf[..len]);
self.consume(len);
Ok(len)
}
}
impl_read_for_bufread!(PartitionReaderGC);
impl Seek for PartitionGC {
impl Seek for PartitionReaderGC {
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::End(v) => self.disc_size.saturating_add_signed(v),
SeekFrom::Current(v) => self.pos.saturating_add_signed(v),
};
Ok(self.pos)
}
fn stream_position(&mut self) -> io::Result<u64> { 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)
}
impl PartitionReader for PartitionReaderGC {
fn is_wii(&self) -> bool { false }
fn open_file(&mut self, node: Node) -> io::Result<FileStream> {
if !node.is_file() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Node is not a file".to_string(),
));
fn meta(&mut self) -> Result<PartitionMeta> {
if let Some(meta) = &self.meta {
Ok(meta.clone())
} else {
let meta = read_part_meta(self, false)?;
self.meta = Some(meta.clone());
Ok(meta)
}
FileStream::new(self, node.offset(false), node.length())
}
fn into_open_file(self: Box<Self>, node: Node) -> io::Result<OwnedFileStream> {
if !node.is_file() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Node is not a file".to_string(),
));
}
OwnedFileStream::new(self, node.offset(false), node.length())
}
}
pub(crate) fn read_part_meta(
reader: &mut dyn PartitionBase,
pub(crate) fn read_dol(
reader: &mut dyn PartitionReader,
partition_header: &PartitionHeader,
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_bytes(&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_bytes(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")?;
let raw_apploader = raw_apploader.into_boxed_slice();
// fst.bin
reader
.seek(SeekFrom::Start(partition_header.fst_offset(is_wii)))
.context("Seeking to FST offset")?;
let raw_fst: Box<[u8]> = read_box_slice(reader, partition_header.fst_size(is_wii) as usize)
.with_context(|| {
format!(
"Reading partition FST (offset {}, size {})",
partition_header.fst_offset(is_wii),
partition_header.fst_size(is_wii)
)
})?;
// main.dol
) -> Result<Arc<[u8]>> {
reader
.seek(SeekFrom::Start(partition_header.dol_offset(is_wii)))
.context("Seeking to DOL offset")?;
@@ -208,9 +162,65 @@ pub(crate) fn read_part_meta(
.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")?;
let raw_dol = raw_dol.into_boxed_slice();
Ok(Arc::from(raw_dol.as_slice()))
}
Ok(Box::new(PartitionMeta {
pub(crate) fn read_fst<R>(
reader: &mut R,
partition_header: &PartitionHeader,
is_wii: bool,
) -> Result<Arc<[u8]>>
where
R: Read + Seek + ?Sized,
{
reader
.seek(SeekFrom::Start(partition_header.fst_offset(is_wii)))
.context("Seeking to FST offset")?;
let raw_fst: Arc<[u8]> = read_arc_slice(reader, partition_header.fst_size(is_wii) as usize)
.with_context(|| {
format!(
"Reading partition FST (offset {}, size {})",
partition_header.fst_offset(is_wii),
partition_header.fst_size(is_wii)
)
})?;
Ok(raw_fst)
}
pub(crate) fn read_part_meta(
reader: &mut dyn PartitionReader,
is_wii: bool,
) -> Result<PartitionMeta> {
// boot.bin
let raw_boot: Arc<[u8; BOOT_SIZE]> = read_arc(reader).context("Reading boot.bin")?;
let partition_header =
PartitionHeader::ref_from_bytes(&raw_boot[size_of::<DiscHeader>()..]).unwrap();
// bi2.bin
let raw_bi2: Arc<[u8; BI2_SIZE]> = read_arc(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_bytes(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")?;
let raw_apploader = Arc::from(raw_apploader.as_slice());
// fst.bin
let raw_fst = read_fst(reader, partition_header, is_wii)?;
// main.dol
let raw_dol = read_dol(reader, partition_header, is_wii)?;
Ok(PartitionMeta {
raw_boot,
raw_bi2,
raw_apploader,
@@ -220,5 +230,5 @@ pub(crate) fn read_part_meta(
raw_tmd: None,
raw_cert_chain: None,
raw_h3_table: None,
}))
})
}
+67 -177
View File
@@ -1,202 +1,92 @@
use std::{
io::{Read, Seek, SeekFrom},
sync::{Arc, Mutex},
time::Instant,
};
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use sha1::{Digest, Sha1};
use zerocopy::FromZeros;
use tracing::instrument;
use zerocopy::{FromZeros, IntoBytes};
use crate::{
array_ref, array_ref_mut,
common::HashBytes,
disc::{
reader::DiscReader,
wii::{HASHES_SIZE, SECTOR_DATA_SIZE},
SECTOR_GROUP_SIZE, SECTOR_SIZE,
},
io::HashBytes,
util::read::read_box_slice,
PartitionOptions, Result, ResultContext, SECTOR_SIZE,
util::{array_ref, array_ref_mut},
};
/// In a sector, following the 0x400 byte block of hashes, each 0x400 bytes of decrypted data is
/// hashed, yielding 31 H0 hashes.
/// Then, 8 sectors are aggregated into a subgroup, and the 31 H0 hashes for each sector are hashed,
/// yielding 8 H1 hashes.
/// Then, 8 subgroups are aggregated into a group, and the 8 H1 hashes for each subgroup are hashed,
/// yielding 8 H2 hashes.
/// Finally, the 8 H2 hashes for each group are hashed, yielding 1 H3 hash.
/// The H3 hashes for each group are stored in the partition's H3 table.
#[derive(Clone, Debug)]
pub struct HashTable {
/// SHA-1 hash of each 0x400 byte block of decrypted data.
pub h0_hashes: Box<[HashBytes]>,
/// SHA-1 hash of the 31 H0 hashes for each sector.
pub h1_hashes: Box<[HashBytes]>,
/// SHA-1 hash of the 8 H1 hashes for each subgroup.
pub h2_hashes: Box<[HashBytes]>,
/// SHA-1 hash of the 8 H2 hashes for each group.
pub h3_hashes: Box<[HashBytes]>,
}
/// Hashes for a single sector group (64 sectors).
#[derive(Clone, FromZeros)]
struct HashResult {
h0_hashes: [HashBytes; 1984],
h1_hashes: [HashBytes; 64],
h2_hashes: [HashBytes; 8],
h3_hash: HashBytes,
pub struct GroupHashes {
pub h3_hash: HashBytes,
pub h2_hashes: [HashBytes; 8],
pub h1_hashes: [HashBytes; 64],
pub h0_hashes: [HashBytes; 1984],
}
impl HashTable {
fn new(num_sectors: u32) -> Self {
let num_sectors = num_sectors.next_multiple_of(64) as usize;
let num_data_hashes = num_sectors * 31;
let num_subgroups = num_sectors / 8;
let num_groups = num_subgroups / 8;
Self {
h0_hashes: <[HashBytes]>::new_box_zeroed_with_elems(num_data_hashes).unwrap(),
h1_hashes: <[HashBytes]>::new_box_zeroed_with_elems(num_sectors).unwrap(),
h2_hashes: <[HashBytes]>::new_box_zeroed_with_elems(num_subgroups).unwrap(),
h3_hashes: <[HashBytes]>::new_box_zeroed_with_elems(num_groups).unwrap(),
impl GroupHashes {
#[inline]
pub fn hashes_for_sector(
&self,
sector: usize,
) -> (&[HashBytes; 31], &[HashBytes; 8], &[HashBytes; 8]) {
let h1_hashes = array_ref![self.h1_hashes, sector & !7, 8];
let h0_hashes = array_ref![self.h0_hashes, sector * 31, 31];
(h0_hashes, h1_hashes, &self.h2_hashes)
}
#[inline]
pub fn apply(&self, sector_data: &mut [u8; SECTOR_SIZE], sector: usize) {
let (h0_hashes, h1_hashes, h2_hashes) = self.hashes_for_sector(sector);
array_ref_mut![sector_data, 0, 0x26C].copy_from_slice(h0_hashes.as_bytes());
array_ref_mut![sector_data, 0x280, 0xA0].copy_from_slice(h1_hashes.as_bytes());
array_ref_mut![sector_data, 0x340, 0xA0].copy_from_slice(h2_hashes.as_bytes());
}
}
fn extend(&mut self, group_index: usize, result: &HashResult) {
*array_ref_mut![self.h0_hashes, group_index * 1984, 1984] = result.h0_hashes;
*array_ref_mut![self.h1_hashes, group_index * 64, 64] = result.h1_hashes;
*array_ref_mut![self.h2_hashes, group_index * 8, 8] = result.h2_hashes;
self.h3_hashes[group_index] = result.h3_hash;
}
}
pub const NUM_H0_HASHES: usize = SECTOR_DATA_SIZE / HASHES_SIZE;
pub fn rebuild_hashes(reader: &mut DiscReader) -> Result<()> {
const NUM_H0_HASHES: usize = SECTOR_DATA_SIZE / HASHES_SIZE;
log::info!(
"Rebuilding hashes for Wii partition data (using {} threads)",
rayon::current_num_threads()
);
let start = Instant::now();
// Precompute hashes for zeroed sectors.
const ZERO_H0_BYTES: &[u8] = &[0u8; HASHES_SIZE];
let zero_h0_hash = sha1_hash(ZERO_H0_BYTES);
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!(
"Rebuilding hashes: {} sectors, {} subgroups, {} groups",
hash_table.h1_hashes.len(),
hash_table.h2_hashes.len(),
hash_table.h3_hashes.len()
);
let group_count = hash_table.h3_hashes.len();
let mutex = Arc::new(Mutex::new(hash_table));
let partition_options = PartitionOptions { validate_hashes: false };
(0..group_count).into_par_iter().try_for_each_with(
(reader.open_partition(part.index, &partition_options)?, mutex.clone()),
|(stream, mutex), h3_index| -> Result<()> {
let mut result = HashResult::new_box_zeroed()?;
let mut data_buf = <[u8]>::new_box_zeroed_with_elems(SECTOR_DATA_SIZE)?;
let mut h3_hasher = Sha1::new();
for h2_index in 0..8 {
let mut h2_hasher = Sha1::new();
for h1_index in 0..8 {
#[instrument(skip_all)]
pub fn hash_sector_group(sector_group: &[u8; SECTOR_GROUP_SIZE]) -> Box<GroupHashes> {
let mut result = GroupHashes::new_box_zeroed().unwrap();
for (h2_index, h2_hash) in result.h2_hashes.iter_mut().enumerate() {
let out_h1_hashes = array_ref_mut![result.h1_hashes, h2_index * 8, 8];
for (h1_index, h1_hash) in out_h1_hashes.iter_mut().enumerate() {
let sector = h1_index + h2_index * 8;
let part_sector = sector as u32 + h3_index as u32 * 64;
let mut h1_hasher = Sha1::new();
if part_sector >= part_sectors {
for h0_index in 0..NUM_H0_HASHES {
result.h0_hashes[h0_index + sector * 31] = zero_h0_hash;
h1_hasher.update(zero_h0_hash);
}
let out_h0_hashes =
array_ref_mut![result.h0_hashes, sector * NUM_H0_HASHES, NUM_H0_HASHES];
if array_ref![sector_group, sector * SECTOR_SIZE, 20].iter().any(|&v| v != 0) {
// Hash block already present, use it
out_h0_hashes.as_mut_bytes().copy_from_slice(array_ref![
sector_group,
sector * SECTOR_SIZE,
0x26C
]);
} else {
stream
.seek(SeekFrom::Start(part_sector as u64 * SECTOR_DATA_SIZE as u64))
.with_context(|| format!("Seeking to sector {}", part_sector))?;
stream
.read_exact(&mut data_buf)
.with_context(|| format!("Reading sector {}", part_sector))?;
for h0_index in 0..NUM_H0_HASHES {
let h0_hash = sha1_hash(array_ref![
data_buf,
h0_index * HASHES_SIZE,
for (h0_index, h0_hash) in out_h0_hashes.iter_mut().enumerate() {
*h0_hash = sha1_hash(array_ref![
sector_group,
sector * SECTOR_SIZE + HASHES_SIZE + h0_index * HASHES_SIZE,
HASHES_SIZE
]);
result.h0_hashes[h0_index + sector * 31] = h0_hash;
h1_hasher.update(h0_hash);
}
};
let h1_hash = h1_hasher.finalize().into();
result.h1_hashes[sector] = h1_hash;
h2_hasher.update(h1_hash);
}
let h2_hash = h2_hasher.finalize().into();
result.h2_hashes[h2_index] = h2_hash;
h3_hasher.update(h2_hash);
}
result.h3_hash = h3_hasher.finalize().into();
let mut hash_table = mutex.lock().map_err(|_| "Failed to lock mutex")?;
hash_table.extend(h3_index, &result);
Ok(())
},
)?;
let hash_table = Arc::try_unwrap(mutex)
.map_err(|_| "Failed to unwrap Arc")?
.into_inner()
.map_err(|_| "Failed to lock mutex")?;
hash_tables.push(hash_table);
}
// Verify against H3 table
for (part, hash_table) in reader.partitions.clone().iter().zip(hash_tables.iter()) {
log::debug!(
"Verifying H3 table for partition {} (count {})",
part.index,
hash_table.h3_hashes.len()
);
reader
.seek(SeekFrom::Start(
part.start_sector as u64 * SECTOR_SIZE as u64 + part.header.h3_table_off(),
))
.context("Seeking to H3 table")?;
let h3_table: Box<[HashBytes]> =
read_box_slice(reader, hash_table.h3_hashes.len()).context("Reading H3 table")?;
let mut mismatches = 0;
for (idx, (expected_hash, h3_hash)) in
h3_table.iter().zip(hash_table.h3_hashes.iter()).enumerate()
{
if expected_hash != h3_hash {
let mut got_bytes = [0u8; 40];
let got = base16ct::lower::encode_str(h3_hash, &mut got_bytes).unwrap();
let mut expected_bytes = [0u8; 40];
let expected =
base16ct::lower::encode_str(expected_hash, &mut expected_bytes).unwrap();
log::debug!(
"Partition {} H3 table does not match:\n\tindex {}\n\texpected: {}\n\tgot: {}",
part.index, idx, expected, got
);
mismatches += 1;
}
}
if mismatches > 0 {
log::warn!("Partition {} H3 table has {} hash mismatches", part.index, mismatches);
*h1_hash = sha1_hash(out_h0_hashes.as_bytes());
}
*h2_hash = sha1_hash(out_h1_hashes.as_bytes());
}
for (part, hash_table) in reader.partitions.iter_mut().zip(hash_tables) {
part.hash_table = Some(hash_table);
}
log::info!("Rebuilt hashes in {:?}", start.elapsed());
Ok(())
result.h3_hash = sha1_hash(result.h2_hashes.as_bytes());
result
}
/// Hashes a byte slice with SHA-1.
#[inline]
pub fn sha1_hash(buf: &[u8]) -> HashBytes { HashBytes::from(Sha1::digest(buf)) }
#[instrument(skip_all)]
pub fn sha1_hash(buf: &[u8]) -> HashBytes {
#[cfg(feature = "openssl")]
{
// The one-shot openssl::sha::sha1 ends up being much slower
let mut hasher = openssl::sha::Sha1::new();
hasher.update(buf);
hasher.finish()
}
#[cfg(not(feature = "openssl"))]
{
use sha1::Digest;
HashBytes::from(sha1::Sha1::digest(buf))
}
}
+75 -243
View File
@@ -1,40 +1,54 @@
//! Disc type related logic (GameCube, Wii)
//! GameCube/Wii disc format types.
use std::{
borrow::Cow,
ffi::CStr,
fmt::{Debug, Display, Formatter},
io,
io::{BufRead, Seek},
mem::size_of,
str::from_utf8,
};
use std::{ffi::CStr, str::from_utf8};
use dyn_clone::DynClone;
use zerocopy::{big_endian::*, FromBytes, Immutable, IntoBytes, KnownLayout};
use crate::{io::MagicBytes, static_assert, Result};
use crate::{common::MagicBytes, util::static_assert};
pub(crate) mod fst;
pub(crate) mod direct;
pub mod fst;
pub(crate) mod gcn;
pub(crate) mod hashes;
pub(crate) mod preloader;
pub(crate) mod reader;
pub(crate) mod streams;
pub(crate) mod wii;
pub use fst::{Fst, Node, NodeKind};
pub use streams::{FileStream, OwnedFileStream, WindowedStream};
pub use wii::{ContentMetadata, SignedHeader, Ticket, TicketLimit, TmdHeader, REGION_SIZE};
pub mod wii;
pub(crate) mod writer;
/// Size in bytes of a disc sector. (32 KiB)
pub const SECTOR_SIZE: usize = 0x8000;
/// Size in bytes of a Wii partition sector group. (32 KiB * 64, 2 MiB)
pub const SECTOR_GROUP_SIZE: usize = SECTOR_SIZE * 64;
/// Magic bytes for Wii discs. Located at offset 0x18.
pub const WII_MAGIC: MagicBytes = [0x5D, 0x1C, 0x9E, 0xA3];
/// Magic bytes for GameCube discs. Located at offset 0x1C.
pub const GCN_MAGIC: MagicBytes = [0xC2, 0x33, 0x9F, 0x3D];
/// Size in bytes of the disc header and partition header. (boot.bin)
pub const BOOT_SIZE: usize = size_of::<DiscHeader>() + size_of::<PartitionHeader>();
/// Size in bytes of the debug and region information. (bi2.bin)
pub const BI2_SIZE: usize = 0x2000;
/// The size of a single-layer MiniDVD. (1.4 GB)
///
/// GameCube games and some third-party Wii discs (Datel) use this format.
pub const MINI_DVD_SIZE: u64 = 1_459_978_240;
/// The size of a single-layer DVD. (4.7 GB)
///
/// The vast majority of Wii games use this format.
pub const SL_DVD_SIZE: u64 = 4_699_979_776;
/// The size of a dual-layer DVD. (8.5 GB)
///
/// A few larger Wii games use this format.
/// (Super Smash Bros. Brawl, Metroid Prime Trilogy, etc.)
pub const DL_DVD_SIZE: u64 = 8_511_160_320;
/// Shared GameCube & Wii disc header.
///
/// This header is always at the start of the disc image and within each Wii partition.
@@ -53,7 +67,7 @@ pub struct DiscHeader {
pub audio_stream_buf_size: u8,
/// Padding
_pad1: [u8; 14],
/// If this is a Wii disc, this will be 0x5D1C9EA3
/// If this is a Wii disc, this will bPartitionKinde 0x5D1C9EA3
pub wii_magic: MagicBytes,
/// If this is a GameCube disc, this will be 0xC2339F3D
pub gcn_magic: MagicBytes,
@@ -112,7 +126,7 @@ pub struct PartitionHeader {
pub debug_mon_offset: U32,
/// Debug monitor load address
pub debug_load_address: U32,
/// Padding
/// PaddingPartitionKind
_pad1: [u8; 0x18],
/// Offset to main DOL (Wii: >> 2)
pub dol_offset: U32,
@@ -145,6 +159,16 @@ impl PartitionHeader {
}
}
/// Set the offset within the partition to the main DOL.
#[inline]
pub fn set_dol_offset(&mut self, offset: u64, is_wii: bool) {
if is_wii {
self.dol_offset.set((offset / 4) as u32);
} else {
self.dol_offset.set(offset as u32);
}
}
/// Offset within the partition to the file system table (FST).
#[inline]
pub fn fst_offset(&self, is_wii: bool) -> u64 {
@@ -155,6 +179,16 @@ impl PartitionHeader {
}
}
/// Set the offset within the partition to the file system table (FST).
#[inline]
pub fn set_fst_offset(&mut self, offset: u64, is_wii: bool) {
if is_wii {
self.fst_offset.set((offset / 4) as u32);
} else {
self.fst_offset.set(offset as u32);
}
}
/// Size of the file system table (FST).
#[inline]
pub fn fst_size(&self, is_wii: bool) -> u64 {
@@ -165,6 +199,16 @@ impl PartitionHeader {
}
}
/// Set the size of the file system table (FST).
#[inline]
pub fn set_fst_size(&mut self, size: u64, is_wii: bool) {
if is_wii {
self.fst_size.set((size / 4) as u32);
} else {
self.fst_size.set(size as u32);
}
}
/// Maximum size of the file system table (FST) across multi-disc games.
#[inline]
pub fn fst_max_size(&self, is_wii: bool) -> u64 {
@@ -174,6 +218,16 @@ impl PartitionHeader {
self.fst_max_size.get() as u64
}
}
/// Set the maximum size of the file system table (FST) across multi-disc games.
#[inline]
pub fn set_fst_max_size(&mut self, size: u64, is_wii: bool) {
if is_wii {
self.fst_max_size.set((size / 4) as u32);
} else {
self.fst_max_size.set(size as u32);
}
}
}
/// Apploader header.
@@ -231,225 +285,3 @@ pub struct DolHeader {
}
static_assert!(size_of::<DolHeader>() == 0x100);
/// The kind of disc partition.
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub enum PartitionKind {
/// Data partition.
Data,
/// Update partition.
Update,
/// Channel partition.
Channel,
/// Other partition kind.
Other(u32),
}
impl Display for PartitionKind {
#[inline]
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Data => write!(f, "Data"),
Self::Update => write!(f, "Update"),
Self::Channel => write!(f, "Channel"),
Self::Other(v) => {
let bytes = v.to_be_bytes();
write!(f, "Other ({:08X}, {})", v, String::from_utf8_lossy(&bytes))
}
}
}
}
impl PartitionKind {
/// Returns the directory name for the partition kind.
#[inline]
pub fn dir_name(&self) -> Cow<str> {
match self {
Self::Data => Cow::Borrowed("DATA"),
Self::Update => Cow::Borrowed("UPDATE"),
Self::Channel => Cow::Borrowed("CHANNEL"),
Self::Other(v) => {
let bytes = v.to_be_bytes();
Cow::Owned(format!("P-{}", String::from_utf8_lossy(&bytes)))
}
}
}
}
impl From<u32> for PartitionKind {
#[inline]
fn from(v: u32) -> Self {
match v {
0 => Self::Data,
1 => Self::Update,
2 => Self::Channel,
v => Self::Other(v),
}
}
}
/// An open disc partition.
pub trait PartitionBase: DynClone + BufRead + Seek + Send + Sync {
/// Reads the partition header and file system table.
fn meta(&mut self) -> Result<Box<PartitionMeta>>;
/// Seeks the partition stream to the specified file system node
/// and returns a windowed stream.
///
/// # Examples
///
/// Basic usage:
/// ```no_run
/// use std::io::Read;
///
/// use nod::{Disc, PartitionKind};
///
/// fn main() -> nod::Result<()> {
/// let disc = Disc::new("path/to/file.iso")?;
/// let mut partition = disc.open_partition_kind(PartitionKind::Data)?;
/// let meta = partition.meta()?;
/// let fst = meta.fst()?;
/// if let Some((_, node)) = fst.find("/MP3/Worlds.txt") {
/// let mut s = String::new();
/// partition
/// .open_file(node)
/// .expect("Failed to open file stream")
/// .read_to_string(&mut s)
/// .expect("Failed to read file");
/// println!("{}", s);
/// }
/// Ok(())
/// }
/// ```
fn open_file(&mut self, node: Node) -> io::Result<FileStream>;
/// Consumes the partition instance and returns a windowed stream.
///
/// # Examples
///
/// ```no_run
/// use std::io::Read;
///
/// use nod::{Disc, PartitionKind, OwnedFileStream};
///
/// fn main() -> nod::Result<()> {
/// let disc = Disc::new("path/to/file.iso")?;
/// let mut partition = disc.open_partition_kind(PartitionKind::Data)?;
/// let meta = partition.meta()?;
/// let fst = meta.fst()?;
/// if let Some((_, node)) = fst.find("/disc.tgc") {
/// let file: OwnedFileStream = partition
/// .clone() // Clone the Box<dyn PartitionBase>
/// .into_open_file(node) // Get an OwnedFileStream
/// .expect("Failed to open file stream");
/// // Open the inner disc image using the owned stream
/// let inner_disc = Disc::new_stream(Box::new(file))
/// .expect("Failed to open inner disc");
/// // ...
/// }
/// Ok(())
/// }
/// ```
fn into_open_file(self: Box<Self>, node: Node) -> io::Result<OwnedFileStream>;
}
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)
pub const BI2_SIZE: usize = 0x2000;
/// Extra disc partition data. (DOL, FST, etc.)
#[derive(Clone, Debug)]
pub struct PartitionMeta {
/// Disc and partition header (boot.bin)
pub raw_boot: Box<[u8; BOOT_SIZE]>,
/// Debug and region information (bi2.bin)
pub raw_bi2: Box<[u8; BI2_SIZE]>,
/// Apploader (apploader.bin)
pub raw_apploader: Box<[u8]>,
/// Main binary (main.dol)
pub raw_dol: Box<[u8]>,
/// File system table (fst.bin)
pub raw_fst: Box<[u8]>,
/// Ticket (ticket.bin, Wii only)
pub raw_ticket: Option<Box<[u8]>>,
/// TMD (tmd.bin, Wii only)
pub raw_tmd: Option<Box<[u8]>>,
/// Certificate chain (cert.bin, Wii only)
pub raw_cert_chain: Option<Box<[u8]>>,
/// H3 hash table (h3.bin, Wii only)
pub raw_h3_table: Option<Box<[u8]>>,
}
impl PartitionMeta {
/// A view into the disc header.
#[inline]
pub fn header(&self) -> &DiscHeader {
DiscHeader::ref_from_bytes(&self.raw_boot[..size_of::<DiscHeader>()])
.expect("Invalid header alignment")
}
/// A view into the partition header.
#[inline]
pub fn partition_header(&self) -> &PartitionHeader {
PartitionHeader::ref_from_bytes(&self.raw_boot[size_of::<DiscHeader>()..])
.expect("Invalid partition header alignment")
}
/// A view into the apploader header.
#[inline]
pub fn apploader_header(&self) -> &ApploaderHeader {
ApploaderHeader::ref_from_prefix(&self.raw_apploader)
.expect("Invalid apploader alignment")
.0
}
/// A view into the file system table (FST).
#[inline]
pub fn fst(&self) -> Result<Fst, &'static str> { Fst::new(&self.raw_fst) }
/// A view into the DOL header.
#[inline]
pub fn dol_header(&self) -> &DolHeader {
DolHeader::ref_from_prefix(&self.raw_dol).expect("Invalid DOL alignment").0
}
/// A view into the ticket. (Wii only)
#[inline]
pub fn ticket(&self) -> Option<&Ticket> {
let raw_ticket = self.raw_ticket.as_deref()?;
Some(Ticket::ref_from_bytes(raw_ticket).expect("Invalid ticket alignment"))
}
/// A view into the TMD. (Wii only)
#[inline]
pub fn tmd_header(&self) -> Option<&TmdHeader> {
let raw_tmd = self.raw_tmd.as_deref()?;
Some(TmdHeader::ref_from_prefix(raw_tmd).expect("Invalid TMD alignment").0)
}
/// A view into the TMD content metadata. (Wii only)
#[inline]
pub fn content_metadata(&self) -> Option<&[ContentMetadata]> {
let raw_cmd = &self.raw_tmd.as_deref()?[size_of::<TmdHeader>()..];
Some(<[ContentMetadata]>::ref_from_bytes(raw_cmd).expect("Invalid CMD alignment"))
}
}
/// The size of a single-layer MiniDVD. (1.4 GB)
///
/// GameCube games and some third-party Wii discs (Datel) use this format.
pub const MINI_DVD_SIZE: u64 = 1_459_978_240;
/// The size of a single-layer DVD. (4.7 GB)
///
/// The vast majority of Wii games use this format.
pub const SL_DVD_SIZE: u64 = 4_699_979_776;
/// The size of a dual-layer DVD. (8.5 GB)
///
/// A few larger Wii games use this format.
/// (Super Smash Bros. Brawl, Metroid Prime Trilogy, etc.)
pub const DL_DVD_SIZE: u64 = 8_511_160_320;
File diff suppressed because it is too large Load Diff
+292 -201
View File
File diff suppressed because it is too large Load Diff
-101
View File
@@ -1,101 +0,0 @@
//! Partition file read stream.
use std::{
io,
io::{BufRead, Read, Seek, SeekFrom},
};
use super::PartitionBase;
/// A file read stream borrowing a [`PartitionBase`].
pub type FileStream<'a> = WindowedStream<&'a mut dyn PartitionBase>;
/// A file read stream owning a [`PartitionBase`].
pub type OwnedFileStream = WindowedStream<Box<dyn PartitionBase>>;
/// A read stream with a fixed window.
#[derive(Clone)]
pub struct WindowedStream<T>
where T: BufRead + Seek
{
base: T,
pos: u64,
begin: u64,
end: u64,
}
impl<T> WindowedStream<T>
where T: BufRead + Seek
{
/// Creates a new windowed stream with offset and size.
///
/// Seeks underlying stream immediately.
#[inline]
pub fn new(mut base: T, offset: u64, size: u64) -> io::Result<Self> {
base.seek(SeekFrom::Start(offset))?;
Ok(Self { base, pos: offset, begin: offset, end: offset + size })
}
/// Returns the length of the window.
#[inline]
#[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> u64 { self.end - self.begin }
}
impl<T> Read for WindowedStream<T>
where T: BufRead + Seek
{
#[inline]
fn read(&mut self, out: &mut [u8]) -> io::Result<usize> {
let buf = self.fill_buf()?;
let len = buf.len().min(out.len());
out[..len].copy_from_slice(&buf[..len]);
self.consume(len);
Ok(len)
}
}
impl<T> BufRead for WindowedStream<T>
where T: BufRead + Seek
{
#[inline]
fn fill_buf(&mut self) -> io::Result<&[u8]> {
let limit = self.end.saturating_sub(self.pos);
if limit == 0 {
return Ok(&[]);
}
let buf = self.base.fill_buf()?;
let max = (buf.len() as u64).min(limit) as usize;
Ok(&buf[..max])
}
#[inline]
fn consume(&mut self, amt: usize) {
self.base.consume(amt);
self.pos += amt as u64;
}
}
impl<T> Seek for WindowedStream<T>
where T: BufRead + Seek
{
#[inline]
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
let mut pos = match pos {
SeekFrom::Start(p) => self.begin + p,
SeekFrom::End(p) => self.end.saturating_add_signed(p),
SeekFrom::Current(p) => self.pos.saturating_add_signed(p),
};
if pos < self.begin {
pos = self.begin;
} else if pos > self.end {
pos = self.end;
}
let result = self.base.seek(SeekFrom::Start(pos))?;
self.pos = result;
Ok(result - self.begin)
}
#[inline]
fn stream_position(&mut self) -> io::Result<u64> { Ok(self.pos) }
}
+187 -210
View File
File diff suppressed because it is too large Load Diff
+273
View File
@@ -0,0 +1,273 @@
use std::{
io,
io::{BufRead, Read},
};
use bytes::{Bytes, BytesMut};
use dyn_clone::DynClone;
use rayon::prelude::*;
use crate::{
common::PartitionInfo,
disc::{
reader::DiscReader,
wii::{HASHES_SIZE, SECTOR_DATA_SIZE},
SECTOR_SIZE,
},
util::{aes::decrypt_sector_b2b, array_ref, array_ref_mut, lfg::LaggedFibonacci},
write::{DiscFinalization, DiscWriterWeight, ProcessOptions},
Error, Result, ResultContext,
};
/// A callback for writing disc data.
///
/// The callback should write all data to the output stream before returning, or return an error if
/// writing fails. The second and third arguments are the current bytes processed and the total
/// bytes to process, respectively. For most formats, this has no relation to the written disc size,
/// but can be used to display progress.
pub type DataCallback<'a> = dyn FnMut(Bytes, u64, u64) -> io::Result<()> + Send + 'a;
/// A trait for writing disc images.
pub trait DiscWriter: DynClone {
/// Processes the disc writer to completion.
///
/// The data callback will be called, in order, for each block of data to write to the output
/// file. The callback should write all data before returning, or return an error if writing
/// fails.
fn process(
&self,
data_callback: &mut DataCallback,
options: &ProcessOptions,
) -> Result<DiscFinalization>;
/// Returns the progress upper bound for the disc writer.
///
/// For most formats, this has no relation to the written disc size, but can be used to display
/// progress.
fn progress_bound(&self) -> u64;
/// Returns the weight of the disc writer.
///
/// This can help determine the number of threads to dedicate for output processing, and may
/// differ based on the format's configuration, such as whether compression is enabled.
fn weight(&self) -> DiscWriterWeight;
}
dyn_clone::clone_trait_object!(DiscWriter);
#[derive(Default)]
pub struct BlockResult<T> {
/// Input block index
pub block_idx: u32,
/// Input disc data (before processing)
pub disc_data: Bytes,
/// Output block data (after processing). If None, the disc data is used.
pub block_data: Bytes,
/// Output metadata
pub meta: T,
}
pub trait BlockProcessor: Clone + Send + Sync {
type BlockMeta;
fn process_block(&mut self, block_idx: u32) -> io::Result<BlockResult<Self::BlockMeta>>;
}
pub fn read_block(reader: &mut DiscReader, block_size: usize) -> io::Result<(Bytes, Bytes)> {
let initial_block = reader.fill_buf_internal()?;
if initial_block.len() >= block_size {
// Happy path: we have a full block that we can cheaply slice
let data = initial_block.slice(0..block_size);
reader.consume(block_size);
return Ok((data.clone(), data));
} else if initial_block.is_empty() {
return Err(io::Error::from(io::ErrorKind::UnexpectedEof));
}
reader.consume(initial_block.len());
// Combine smaller blocks into a new buffer
let mut buf = BytesMut::zeroed(block_size);
let mut len = initial_block.len();
buf[..len].copy_from_slice(initial_block.as_ref());
drop(initial_block);
while len < block_size {
let read = reader.read(&mut buf[len..])?;
if read == 0 {
break;
}
len += read;
}
// The block data is full size, padded with zeroes
let block_data = buf.freeze();
// The disc data is the actual data read, without padding
let disc_data = block_data.slice(0..len);
Ok((block_data, disc_data))
}
/// Process blocks in parallel, ensuring that they are written in order.
pub(crate) fn par_process<P, T>(
create_processor: impl Fn() -> P + Sync,
block_count: u32,
num_threads: usize,
mut callback: impl FnMut(BlockResult<T>) -> Result<()> + Send,
) -> Result<()>
where
T: Send,
P: BlockProcessor<BlockMeta = T>,
{
if num_threads == 0 {
// Fall back to single-threaded processing
let mut processor = create_processor();
for block_idx in 0..block_count {
let block = processor
.process_block(block_idx)
.with_context(|| format!("Failed to process block {block_idx}"))?;
callback(block)?;
}
return Ok(());
}
let (block_tx, block_rx) = crossbeam_channel::bounded(block_count as usize);
for block_idx in 0..block_count {
block_tx.send(block_idx).unwrap();
}
drop(block_tx); // Disconnect channel
let (result_tx, result_rx) = crossbeam_channel::bounded(0);
let mut process_error = None;
let mut write_error = None;
rayon::join(
|| {
if let Err(e) = (0..num_threads).into_par_iter().try_for_each_init(
|| (block_rx.clone(), result_tx.clone(), create_processor()),
|(receiver, block_tx, processor), _| {
while let Ok(block_idx) = receiver.recv() {
let block = processor
.process_block(block_idx)
.with_context(|| format!("Failed to process block {block_idx}"))?;
if block_tx.send(block).is_err() {
break;
}
}
Ok::<_, Error>(())
},
) {
process_error = Some(e);
}
drop(result_tx); // Disconnect channel
},
|| {
let mut current_block = 0;
let mut out_of_order = Vec::<BlockResult<T>>::new();
'outer: while let Ok(result) = result_rx.recv() {
if result.block_idx == current_block {
if let Err(e) = callback(result) {
write_error = Some(e);
break;
}
current_block += 1;
// Check if any out of order blocks can be written
while out_of_order.first().is_some_and(|r| r.block_idx == current_block) {
let result = out_of_order.remove(0);
if let Err(e) = callback(result) {
write_error = Some(e);
break 'outer;
}
current_block += 1;
}
} else {
out_of_order.push(result);
out_of_order.sort_unstable_by_key(|r| r.block_idx);
}
}
},
);
if let Some(e) = process_error {
return Err(e);
}
if let Some(e) = write_error {
return Err(e);
}
Ok(())
}
/// The determined block type.
pub enum CheckBlockResult {
Normal,
Zeroed,
Junk,
}
/// Check if a block is zeroed or junk data.
pub(crate) fn check_block(
buf: &[u8],
decrypted_block: &mut [u8],
input_position: u64,
partition_info: &[PartitionInfo],
lfg: &mut LaggedFibonacci,
disc_id: [u8; 4],
disc_num: u8,
) -> io::Result<CheckBlockResult> {
let start_sector = (input_position / SECTOR_SIZE as u64) as u32;
let end_sector = ((input_position + buf.len() as u64) / SECTOR_SIZE as u64) as u32;
if let Some(partition) = partition_info.iter().find(|p| {
p.has_hashes && start_sector >= p.data_start_sector && end_sector < p.data_end_sector
}) {
if input_position % SECTOR_SIZE as u64 != 0 {
return Err(io::Error::new(
io::ErrorKind::Other,
"Partition block not aligned to sector boundary",
));
}
if buf.len() % SECTOR_SIZE != 0 {
return Err(io::Error::new(
io::ErrorKind::Other,
"Partition block not a multiple of sector size",
));
}
let block = if partition.has_encryption {
if decrypted_block.len() < buf.len() {
return Err(io::Error::new(
io::ErrorKind::Other,
"Decrypted block buffer too small",
));
}
for i in 0..buf.len() / SECTOR_SIZE {
decrypt_sector_b2b(
array_ref![buf, SECTOR_SIZE * i, SECTOR_SIZE],
array_ref_mut![decrypted_block, SECTOR_SIZE * i, SECTOR_SIZE],
&partition.key,
);
}
&decrypted_block[..buf.len()]
} else {
buf
};
if sector_data_iter(block).all(|sector_data| sector_data.iter().all(|&b| b == 0)) {
return Ok(CheckBlockResult::Zeroed);
}
let partition_start = partition.data_start_sector as u64 * SECTOR_SIZE as u64;
let partition_offset =
((input_position - partition_start) / SECTOR_SIZE as u64) * SECTOR_DATA_SIZE as u64;
if sector_data_iter(block).enumerate().all(|(i, sector_data)| {
let sector_offset = partition_offset + i as u64 * SECTOR_DATA_SIZE as u64;
lfg.check_sector_chunked(sector_data, disc_id, disc_num, sector_offset)
}) {
return Ok(CheckBlockResult::Junk);
}
} else {
if buf.iter().all(|&b| b == 0) {
return Ok(CheckBlockResult::Zeroed);
}
if lfg.check_sector_chunked(buf, disc_id, disc_num, input_position) {
return Ok(CheckBlockResult::Junk);
}
}
Ok(CheckBlockResult::Normal)
}
#[inline]
fn sector_data_iter(buf: &[u8]) -> impl Iterator<Item = &[u8; SECTOR_DATA_SIZE]> {
buf.chunks_exact(SECTOR_SIZE).map(|chunk| (&chunk[HASHES_SIZE..]).try_into().unwrap())
}
+271 -290
View File
File diff suppressed because it is too large Load Diff
+228 -35
View File
@@ -2,20 +2,36 @@ use std::{
io,
io::{Read, Seek, SeekFrom},
mem::size_of,
sync::Arc,
};
use zerocopy::{little_endian::*, FromBytes, Immutable, IntoBytes, KnownLayout};
use bytes::{BufMut, Bytes, BytesMut};
use zerocopy::{little_endian::*, FromBytes, FromZeros, Immutable, IntoBytes, KnownLayout};
use crate::{
disc::SECTOR_SIZE,
io::{
block::{Block, BlockIO, DiscStream, PartitionInfo, CISO_MAGIC},
nkit::NKitHeader,
Format, MagicBytes,
common::{Compression, Format, MagicBytes},
disc::{
reader::DiscReader,
writer::{
check_block, par_process, read_block, BlockProcessor, BlockResult, CheckBlockResult,
DataCallback, DiscWriter,
},
SECTOR_SIZE,
},
io::{
block::{Block, BlockKind, BlockReader, CISO_MAGIC},
nkit::{JunkBits, NKitHeader},
},
read::{DiscMeta, DiscStream},
util::{
array_ref,
digest::DigestManager,
lfg::LaggedFibonacci,
read::{box_to_bytes, read_arc},
static_assert,
util::read::read_from,
DiscMeta, Error, Result, ResultContext,
},
write::{DiscFinalization, DiscWriterWeight, FormatOptions, ProcessOptions},
Error, Result, ResultContext,
};
pub const CISO_MAP_SIZE: usize = SECTOR_SIZE - 8;
@@ -32,18 +48,18 @@ struct CISOHeader {
static_assert!(size_of::<CISOHeader>() == SECTOR_SIZE);
#[derive(Clone)]
pub struct DiscIOCISO {
pub struct BlockReaderCISO {
inner: Box<dyn DiscStream>,
header: CISOHeader,
block_map: [u16; CISO_MAP_SIZE],
header: Arc<CISOHeader>,
block_map: Arc<[u16; CISO_MAP_SIZE]>,
nkit_header: Option<NKitHeader>,
}
impl DiscIOCISO {
impl BlockReaderCISO {
pub fn new(mut inner: Box<dyn DiscStream>) -> Result<Box<Self>> {
// Read header
inner.seek(SeekFrom::Start(0)).context("Seeking to start")?;
let header: CISOHeader = read_from(inner.as_mut()).context("Reading CISO header")?;
let header: Arc<CISOHeader> = read_arc(inner.as_mut()).context("Reading CISO header")?;
if header.magic != CISO_MAGIC {
return Err(Error::DiscFormat("Invalid CISO magic".to_string()));
}
@@ -69,54 +85,47 @@ impl DiscIOCISO {
}
// Read NKit header if present (after CISO data)
let nkit_header = if len > file_size + 4 {
let nkit_header = if len > file_size + 12 {
inner.seek(SeekFrom::Start(file_size)).context("Seeking to NKit header")?;
NKitHeader::try_read_from(inner.as_mut(), header.block_size.get(), true)
} else {
None
};
Ok(Box::new(Self { inner, header, block_map, nkit_header }))
Ok(Box::new(Self { inner, header, block_map: Arc::new(block_map), nkit_header }))
}
}
impl BlockIO for DiscIOCISO {
fn read_block_internal(
&mut self,
out: &mut [u8],
block: u32,
partition: Option<&PartitionInfo>,
) -> io::Result<Block> {
if block >= CISO_MAP_SIZE as u32 {
impl BlockReader for BlockReaderCISO {
fn read_block(&mut self, out: &mut [u8], sector: u32) -> io::Result<Block> {
let block_size = self.header.block_size.get();
let block_idx = ((sector as u64 * SECTOR_SIZE as u64) / block_size as u64) as u32;
if block_idx >= CISO_MAP_SIZE as u32 {
// Out of bounds
return Ok(Block::Zero);
return Ok(Block::new(block_idx, block_size, BlockKind::None));
}
// Find the block in the map
let phys_block = self.block_map[block as usize];
let phys_block = self.block_map[block_idx as usize];
if phys_block == u16::MAX {
// Check if block is junk data
if self.nkit_header.as_ref().and_then(|h| h.is_junk_block(block)).unwrap_or(false) {
return Ok(Block::Junk);
if self.nkit_header.as_ref().and_then(|h| h.is_junk_block(block_idx)).unwrap_or(false) {
return Ok(Block::new(block_idx, block_size, BlockKind::Junk));
};
// Otherwise, read zeroes
return Ok(Block::Zero);
return Ok(Block::new(block_idx, block_size, BlockKind::Zero));
}
// Read block
let file_offset = size_of::<CISOHeader>() as u64
+ phys_block as u64 * self.header.block_size.get() as u64;
let file_offset = size_of::<CISOHeader>() as u64 + phys_block as u64 * block_size as u64;
self.inner.seek(SeekFrom::Start(file_offset))?;
self.inner.read_exact(out)?;
match partition {
Some(partition) if partition.has_encryption => Ok(Block::PartEncrypted),
_ => Ok(Block::Raw),
}
Ok(Block::new(block_idx, block_size, BlockKind::Raw))
}
fn block_size_internal(&self) -> u32 { self.header.block_size.get() }
fn block_size(&self) -> u32 { self.header.block_size.get() }
fn meta(&self) -> DiscMeta {
let mut result = DiscMeta {
@@ -130,3 +139,187 @@ impl BlockIO for DiscIOCISO {
result
}
}
struct BlockProcessorCISO {
inner: DiscReader,
block_size: u32,
decrypted_block: Box<[u8]>,
lfg: LaggedFibonacci,
disc_id: [u8; 4],
disc_num: u8,
}
impl Clone for BlockProcessorCISO {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
block_size: self.block_size,
decrypted_block: <[u8]>::new_box_zeroed_with_elems(self.block_size as usize).unwrap(),
lfg: LaggedFibonacci::default(),
disc_id: self.disc_id,
disc_num: self.disc_num,
}
}
}
impl BlockProcessor for BlockProcessorCISO {
type BlockMeta = CheckBlockResult;
fn process_block(&mut self, block_idx: u32) -> io::Result<BlockResult<Self::BlockMeta>> {
let block_size = self.block_size as usize;
let input_position = block_idx as u64 * block_size as u64;
self.inner.seek(SeekFrom::Start(input_position))?;
let (block_data, disc_data) = read_block(&mut self.inner, block_size)?;
// Check if block is zeroed or junk
let result = match check_block(
disc_data.as_ref(),
&mut self.decrypted_block,
input_position,
self.inner.partitions(),
&mut self.lfg,
self.disc_id,
self.disc_num,
)? {
CheckBlockResult::Normal => {
BlockResult { block_idx, disc_data, block_data, meta: CheckBlockResult::Normal }
}
CheckBlockResult::Zeroed => BlockResult {
block_idx,
disc_data,
block_data: Bytes::new(),
meta: CheckBlockResult::Zeroed,
},
CheckBlockResult::Junk => BlockResult {
block_idx,
disc_data,
block_data: Bytes::new(),
meta: CheckBlockResult::Junk,
},
};
Ok(result)
}
}
#[derive(Clone)]
pub struct DiscWriterCISO {
inner: DiscReader,
block_size: u32,
block_count: u32,
disc_size: u64,
}
pub const DEFAULT_BLOCK_SIZE: u32 = 0x200000; // 2 MiB
impl DiscWriterCISO {
pub fn new(inner: DiscReader, options: &FormatOptions) -> Result<Box<dyn DiscWriter>> {
if options.format != Format::Ciso {
return Err(Error::DiscFormat("Invalid format for CISO writer".to_string()));
}
if options.compression != Compression::None {
return Err(Error::DiscFormat("CISO does not support compression".to_string()));
}
let block_size = DEFAULT_BLOCK_SIZE;
let disc_size = inner.disc_size();
let block_count = disc_size.div_ceil(block_size as u64) as u32;
if block_count > CISO_MAP_SIZE as u32 {
return Err(Error::DiscFormat(format!(
"CISO block count exceeds maximum: {} > {}",
block_count, CISO_MAP_SIZE
)));
}
Ok(Box::new(Self { inner, block_size, block_count, disc_size }))
}
}
impl DiscWriter for DiscWriterCISO {
fn process(
&self,
data_callback: &mut DataCallback,
options: &ProcessOptions,
) -> Result<DiscFinalization> {
data_callback(BytesMut::zeroed(SECTOR_SIZE).freeze(), 0, self.disc_size)
.context("Failed to write header")?;
// Determine junk data values
let disc_header = self.inner.header();
let disc_id = *array_ref![disc_header.game_id, 0, 4];
let disc_num = disc_header.disc_num;
// Create hashers
let digest = DigestManager::new(options);
let block_size = self.block_size;
let mut junk_bits = JunkBits::new(block_size);
let mut input_position = 0;
let mut block_count = 0;
let mut header = CISOHeader::new_box_zeroed()?;
header.magic = CISO_MAGIC;
header.block_size = block_size.into();
par_process(
|| BlockProcessorCISO {
inner: self.inner.clone(),
block_size,
decrypted_block: <[u8]>::new_box_zeroed_with_elems(block_size as usize).unwrap(),
lfg: LaggedFibonacci::default(),
disc_id,
disc_num,
},
self.block_count,
options.processor_threads,
|block| -> Result<()> {
// Update hashers
let disc_data_len = block.disc_data.len() as u64;
digest.send(block.disc_data);
// Check if block is zeroed or junk
match block.meta {
CheckBlockResult::Normal => {
header.block_present[block.block_idx as usize] = 1;
block_count += 1;
}
CheckBlockResult::Zeroed => {}
CheckBlockResult::Junk => {
junk_bits.set(block.block_idx, true);
}
}
input_position += disc_data_len;
data_callback(block.block_data, input_position, self.disc_size)
.with_context(|| format!("Failed to write block {}", block.block_idx))?;
Ok(())
},
)?;
// Collect hash results
let digest_results = digest.finish();
let mut nkit_header = NKitHeader {
version: 2,
size: Some(self.disc_size),
crc32: None,
md5: None,
sha1: None,
xxh64: None,
junk_bits: Some(junk_bits),
encrypted: true,
};
nkit_header.apply_digests(&digest_results);
// Write NKit header after data
let mut buffer = BytesMut::new().writer();
nkit_header.write_to(&mut buffer).context("Writing NKit header")?;
data_callback(buffer.into_inner().freeze(), self.disc_size, self.disc_size)
.context("Failed to write NKit header")?;
let header = Bytes::from(box_to_bytes(header));
let mut finalization = DiscFinalization { header, ..Default::default() };
finalization.apply_digests(&digest_results);
Ok(finalization)
}
fn progress_bound(&self) -> u64 { self.disc_size }
fn weight(&self) -> DiscWriterWeight { DiscWriterWeight::Medium }
}

Some files were not shown because too many files have changed in this diff Show More