mirror of
https://github.com/encounter/nod.git
synced 2026-07-10 12:18:48 -07:00
Add conversion support & large refactor
This commit is contained in:
@@ -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
File diff suppressed because it is too large
Load Diff
+10
@@ -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
@@ -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 }
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,4 @@
|
||||
//! Disc image building.
|
||||
|
||||
pub mod gc;
|
||||
pub mod wii;
|
||||
@@ -0,0 +1 @@
|
||||
#![allow(missing_docs)] // TODO
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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(§or_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
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
File diff suppressed because it is too large
Load Diff
+228
-35
@@ -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
Reference in New Issue
Block a user