Compare commits

...

2 Commits

Author SHA1 Message Date
Luke Street 1e44f23aba Add RVZ packing support 2024-11-30 16:17:19 -07:00
Luke Street 55b0d3f29e Move sha1_hash to util/digest 2024-11-24 01:22:28 -07:00
15 changed files with 934 additions and 394 deletions

View File

@ -2,8 +2,12 @@
use std::{borrow::Cow, fmt, str::FromStr, sync::Arc};
use zerocopy::FromBytes;
use crate::{
disc::{wii::WiiPartitionHeader, DiscHeader, PartitionHeader, SECTOR_SIZE},
disc::{
fst::Fst, wii::WiiPartitionHeader, DiscHeader, PartitionHeader, BOOT_SIZE, SECTOR_SIZE,
},
Error, Result,
};
@ -300,14 +304,14 @@ pub struct PartitionInfo {
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,
/// Disc and partition header (boot.bin)
pub raw_boot: Arc<[u8; BOOT_SIZE]>,
/// File system table (fst.bin), or `None` if partition is invalid
pub raw_fst: Option<Arc<[u8]>>,
}
impl PartitionInfo {
@ -322,4 +326,25 @@ impl PartitionInfo {
pub fn data_contains_sector(&self, sector: u32) -> bool {
sector >= self.data_start_sector && sector < self.data_end_sector
}
/// A view into the disc header.
#[inline]
pub fn disc_header(&self) -> &DiscHeader {
DiscHeader::ref_from_bytes(&self.raw_boot[..size_of::<DiscHeader>()])
.expect("Invalid disc 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 file system table (FST).
#[inline]
pub fn fst(&self) -> Option<Fst> {
// FST has already been parsed, so we can safely unwrap
Some(Fst::new(self.raw_fst.as_deref()?).unwrap())
}
}

View File

@ -5,7 +5,7 @@ use std::{
sync::Arc,
};
use zerocopy::FromBytes;
use zerocopy::{FromBytes, FromZeros, IntoBytes};
use crate::{
disc::{
@ -17,7 +17,7 @@ use crate::{
read::{PartitionEncryption, PartitionMeta, PartitionReader},
util::{
impl_read_for_bufread,
read::{read_arc, read_arc_slice, read_vec},
read::{read_arc, read_arc_slice, read_from},
},
Result, ResultContext,
};
@ -138,17 +138,16 @@ pub(crate) fn read_dol(
reader
.seek(SeekFrom::Start(partition_header.dol_offset(is_wii)))
.context("Seeking to DOL offset")?;
let mut raw_dol: Vec<u8> =
read_vec(reader, size_of::<DolHeader>()).context("Reading DOL header")?;
let dol_header = DolHeader::ref_from_bytes(raw_dol.as_slice()).unwrap();
let dol_header: DolHeader = read_from(reader).context("Reading DOL header")?;
let dol_size = (dol_header.text_offs.iter().zip(&dol_header.text_sizes))
.chain(dol_header.data_offs.iter().zip(&dol_header.data_sizes))
.map(|(offs, size)| offs.get() + size.get())
.max()
.unwrap_or(size_of::<DolHeader>() as u32);
raw_dol.resize(dol_size as usize, 0);
let mut raw_dol = <[u8]>::new_box_zeroed_with_elems(dol_size as usize)?;
raw_dol[..size_of::<DolHeader>()].copy_from_slice(dol_header.as_bytes());
reader.read_exact(&mut raw_dol[size_of::<DolHeader>()..]).context("Reading DOL")?;
Ok(Arc::from(raw_dol.as_slice()))
Ok(Arc::from(raw_dol))
}
pub(crate) fn read_fst<R>(
@ -173,6 +172,24 @@ where
Ok(raw_fst)
}
pub(crate) fn read_apploader<R>(reader: &mut R) -> Result<Arc<[u8]>>
where R: Read + Seek + ?Sized {
reader
.seek(SeekFrom::Start(BOOT_SIZE as u64 + BI2_SIZE as u64))
.context("Seeking to apploader offset")?;
let apploader_header: ApploaderHeader =
read_from(reader).context("Reading apploader header")?;
let apploader_size = size_of::<ApploaderHeader>()
+ apploader_header.size.get() as usize
+ apploader_header.trailer_size.get() as usize;
let mut raw_apploader = <[u8]>::new_box_zeroed_with_elems(apploader_size)?;
raw_apploader[..size_of::<ApploaderHeader>()].copy_from_slice(apploader_header.as_bytes());
reader
.read_exact(&mut raw_apploader[size_of::<ApploaderHeader>()..])
.context("Reading apploader")?;
Ok(Arc::from(raw_apploader))
}
pub(crate) fn read_part_meta(
reader: &mut dyn PartitionReader,
is_wii: bool,
@ -186,17 +203,7 @@ pub(crate) fn read_part_meta(
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();
let apploader_size = size_of::<ApploaderHeader>()
+ apploader_header.size.get() as usize
+ apploader_header.trailer_size.get() as usize;
raw_apploader.resize(apploader_size, 0);
reader
.read_exact(&mut raw_apploader[size_of::<ApploaderHeader>()..])
.context("Reading apploader")?;
let raw_apploader = Arc::from(raw_apploader.as_slice());
let raw_apploader = read_apploader(reader)?;
// fst.bin
let raw_fst = read_fst(reader, partition_header, is_wii)?;

View File

@ -7,7 +7,7 @@ use crate::{
wii::{HASHES_SIZE, SECTOR_DATA_SIZE},
SECTOR_GROUP_SIZE, SECTOR_SIZE,
},
util::{array_ref, array_ref_mut},
util::{array_ref, array_ref_mut, digest::sha1_hash},
};
/// Hashes for a single sector group (64 sectors).
@ -73,20 +73,3 @@ pub fn hash_sector_group(sector_group: &[u8; SECTOR_GROUP_SIZE]) -> Box<GroupHas
result.h3_hash = sha1_hash(result.h2_hashes.as_bytes());
result
}
/// Hashes a byte slice with SHA-1.
#[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))
}
}

View File

@ -260,7 +260,7 @@ pub const DOL_MAX_TEXT_SECTIONS: usize = 7;
pub const DOL_MAX_DATA_SECTIONS: usize = 11;
/// Dolphin executable (DOL) header.
#[derive(Debug, Clone, FromBytes, Immutable, KnownLayout)]
#[derive(Debug, Clone, FromBytes, IntoBytes, Immutable, KnownLayout)]
pub struct DolHeader {
/// Text section offsets
pub text_offs: [U32; DOL_MAX_TEXT_SECTIONS],

View File

@ -415,7 +415,7 @@ impl SectorGroupLoader {
sector_data,
self.block_buf.as_mut(),
abs_sector,
&partition.disc_header,
partition.disc_header(),
Some(partition),
)?;
if !encrypted {

View File

@ -6,7 +6,7 @@ use std::{
use bytes::Bytes;
use tracing::warn;
use zerocopy::IntoBytes;
use zerocopy::{FromBytes, IntoBytes};
use crate::{
common::{PartitionInfo, PartitionKind},
@ -21,7 +21,8 @@ use crate::{
PartitionReaderWii, WiiPartEntry, WiiPartGroup, WiiPartitionHeader, REGION_OFFSET,
REGION_SIZE, WII_PART_GROUP_OFF,
},
DiscHeader, DL_DVD_SIZE, MINI_DVD_SIZE, SECTOR_GROUP_SIZE, SECTOR_SIZE, SL_DVD_SIZE,
DiscHeader, PartitionHeader, BOOT_SIZE, DL_DVD_SIZE, MINI_DVD_SIZE, SECTOR_GROUP_SIZE,
SECTOR_SIZE, SL_DVD_SIZE,
},
io::block::BlockReader,
read::{DiscMeta, DiscOptions, PartitionEncryption, PartitionOptions, PartitionReader},
@ -38,12 +39,22 @@ pub struct DiscReader {
pos: u64,
size: u64,
mode: PartitionEncryption,
disc_header: Arc<DiscHeader>,
partitions: Arc<[PartitionInfo]>,
region: Option<[u8; REGION_SIZE]>,
sector_group: Option<SectorGroup>,
raw_boot: Arc<[u8; BOOT_SIZE]>,
alt_disc_header: Option<Arc<DiscHeader>>,
alt_partitions: Option<Arc<[PartitionInfo]>>,
disc_data: DiscReaderData,
sector_group: Option<SectorGroup>,
}
#[derive(Clone)]
enum DiscReaderData {
GameCube {
raw_fst: Option<Arc<[u8]>>,
},
Wii {
partitions: Arc<[PartitionInfo]>,
alt_partitions: Option<Arc<[PartitionInfo]>>,
region: [u8; REGION_SIZE],
},
}
impl Clone for DiscReader {
@ -54,12 +65,10 @@ impl Clone for DiscReader {
pos: 0,
size: self.size,
mode: self.mode,
disc_header: self.disc_header.clone(),
partitions: self.partitions.clone(),
region: self.region,
sector_group: None,
raw_boot: self.raw_boot.clone(),
alt_disc_header: self.alt_disc_header.clone(),
alt_partitions: self.alt_partitions.clone(),
disc_data: self.disc_data.clone(),
sector_group: None,
}
}
}
@ -68,12 +77,14 @@ impl DiscReader {
pub fn new(inner: Box<dyn BlockReader>, options: &DiscOptions) -> Result<Self> {
let mut reader = DirectDiscReader::new(inner)?;
let disc_header: Arc<DiscHeader> = read_arc(&mut reader).context("Reading disc header")?;
let raw_boot: Arc<[u8; BOOT_SIZE]> =
read_arc(reader.as_mut()).context("Reading disc headers")?;
let disc_header = DiscHeader::ref_from_bytes(&raw_boot[..size_of::<DiscHeader>()])
.expect("Invalid disc header alignment");
let disc_header_arc = Arc::from(disc_header.clone());
let mut alt_disc_header = None;
let mut region = None;
let mut partitions = Arc::<[PartitionInfo]>::default();
let mut alt_partitions = None;
if disc_header.is_wii() {
let disc_data = if disc_header.is_wii() {
// Sanity check
if disc_header.has_partition_encryption() && !disc_header.has_partition_hashes() {
return Err(Error::DiscFormat(
@ -90,17 +101,22 @@ impl DiscReader {
// Read region info
reader.seek(SeekFrom::Start(REGION_OFFSET)).context("Seeking to region info")?;
region = Some(read_from(&mut reader).context("Reading region info")?);
let region: [u8; REGION_SIZE] =
read_from(&mut reader).context("Reading region info")?;
// Read partition info
partitions = Arc::from(read_partition_info(&mut reader, disc_header.clone())?);
let partitions = Arc::<[PartitionInfo]>::from(read_partition_info(
&mut reader,
disc_header_arc.clone(),
)?);
let mut alt_partitions = None;
// Update disc header with encryption mode
if matches!(
options.partition_encryption,
PartitionEncryption::ForceDecrypted | PartitionEncryption::ForceEncrypted
) {
let mut disc_header = Box::new(disc_header.as_ref().clone());
let mut disc_header = Box::new(disc_header.clone());
let mut partitions = Box::<[PartitionInfo]>::from(partitions.as_ref());
disc_header.no_partition_encryption = match options.partition_encryption {
PartitionEncryption::ForceDecrypted => 1,
@ -113,15 +129,23 @@ impl DiscReader {
alt_disc_header = Some(Arc::from(disc_header));
alt_partitions = Some(Arc::from(partitions));
}
} else if !disc_header.is_gamecube() {
DiscReaderData::Wii { partitions, alt_partitions, region }
} else if disc_header.is_gamecube() {
DiscReaderData::GameCube { raw_fst: None }
} else {
return Err(Error::DiscFormat("Invalid disc header".to_string()));
}
};
// Calculate disc size
let io = reader.into_inner();
let size = io.meta().disc_size.unwrap_or_else(|| guess_disc_size(&partitions));
let partitions = match &disc_data {
DiscReaderData::Wii { partitions, .. } => partitions,
_ => &Arc::default(),
};
let size = io.meta().disc_size.unwrap_or_else(|| guess_disc_size(partitions));
let preloader = Preloader::new(
SectorGroupLoader::new(io.clone(), disc_header.clone(), partitions.clone()),
SectorGroupLoader::new(io.clone(), disc_header_arc, partitions.clone()),
options.preloader_threads,
);
Ok(Self {
@ -130,12 +154,10 @@ impl DiscReader {
pos: 0,
size,
mode: options.partition_encryption,
disc_header,
partitions,
region,
raw_boot,
disc_data,
sector_group: None,
alt_disc_header,
alt_partitions,
})
}
@ -150,15 +172,68 @@ impl DiscReader {
#[inline]
pub fn header(&self) -> &DiscHeader {
self.alt_disc_header.as_ref().unwrap_or(&self.disc_header)
self.alt_disc_header.as_deref().unwrap_or_else(|| {
DiscHeader::ref_from_bytes(&self.raw_boot[..size_of::<DiscHeader>()])
.expect("Invalid disc header alignment")
})
}
// #[inline]
// pub fn orig_header(&self) -> &DiscHeader {
// DiscHeader::ref_from_bytes(&self.raw_boot[..size_of::<DiscHeader>()])
// .expect("Invalid disc header alignment")
// }
#[inline]
pub fn region(&self) -> Option<&[u8; REGION_SIZE]> {
match &self.disc_data {
DiscReaderData::Wii { region, .. } => Some(region),
_ => None,
}
}
#[inline]
pub fn region(&self) -> Option<&[u8; REGION_SIZE]> { self.region.as_ref() }
pub fn partitions(&self) -> &[PartitionInfo] {
match &self.disc_data {
DiscReaderData::Wii { partitions, alt_partitions, .. } => {
alt_partitions.as_deref().unwrap_or(partitions)
}
_ => &[],
}
}
#[inline]
pub fn partitions(&self) -> &[PartitionInfo] {
self.alt_partitions.as_deref().unwrap_or(&self.partitions)
pub fn orig_partitions(&self) -> &[PartitionInfo] {
match &self.disc_data {
DiscReaderData::Wii { partitions, .. } => partitions,
_ => &[],
}
}
#[inline]
pub fn partition_header(&self) -> Option<&PartitionHeader> {
match &self.disc_data {
DiscReaderData::GameCube { .. } => Some(
PartitionHeader::ref_from_bytes(
&self.raw_boot[size_of::<DiscHeader>()
..size_of::<DiscHeader>() + size_of::<PartitionHeader>()],
)
.expect("Invalid partition header alignment"),
),
_ => None,
}
}
/// A reference to the raw FST for GameCube discs.
/// For Wii discs, use the FST from the appropriate [PartitionInfo].
#[inline]
pub fn fst(&self) -> Option<Fst> {
match &self.disc_data {
DiscReaderData::GameCube { raw_fst } => {
raw_fst.as_deref().and_then(|v| Fst::new(v).ok())
}
_ => None,
}
}
#[inline]
@ -170,20 +245,30 @@ impl DiscReader {
index: usize,
options: &PartitionOptions,
) -> Result<Box<dyn PartitionReader>> {
if self.disc_header.is_gamecube() {
if index == 0 {
Ok(PartitionReaderGC::new(
self.io.clone(),
self.preloader.clone(),
self.disc_size(),
)?)
} else {
Err(Error::DiscFormat("GameCube discs only have one partition".to_string()))
match &self.disc_data {
DiscReaderData::GameCube { .. } => {
if index == 0 {
Ok(PartitionReaderGC::new(
self.io.clone(),
self.preloader.clone(),
self.disc_size(),
)?)
} else {
Err(Error::DiscFormat("GameCube discs only have one partition".to_string()))
}
}
DiscReaderData::Wii { partitions, .. } => {
if let Some(part) = partitions.get(index) {
Ok(PartitionReaderWii::new(
self.io.clone(),
self.preloader.clone(),
part,
options,
)?)
} else {
Err(Error::DiscFormat(format!("Partition {index} not found")))
}
}
} else if let Some(part) = self.partitions.get(index) {
Ok(PartitionReaderWii::new(self.io.clone(), self.preloader.clone(), part, options)?)
} else {
Err(Error::DiscFormat(format!("Partition {index} not found")))
}
}
@ -194,20 +279,30 @@ impl DiscReader {
kind: PartitionKind,
options: &PartitionOptions,
) -> Result<Box<dyn PartitionReader>> {
if self.disc_header.is_gamecube() {
if kind == PartitionKind::Data {
Ok(PartitionReaderGC::new(
self.io.clone(),
self.preloader.clone(),
self.disc_size(),
)?)
} else {
Err(Error::DiscFormat("GameCube discs only have a data partition".to_string()))
match &self.disc_data {
DiscReaderData::GameCube { .. } => {
if kind == PartitionKind::Data {
Ok(PartitionReaderGC::new(
self.io.clone(),
self.preloader.clone(),
self.disc_size(),
)?)
} else {
Err(Error::DiscFormat("GameCube discs only have a data partition".to_string()))
}
}
DiscReaderData::Wii { partitions, .. } => {
if let Some(part) = partitions.iter().find(|v| v.kind == kind) {
Ok(PartitionReaderWii::new(
self.io.clone(),
self.preloader.clone(),
part,
options,
)?)
} else {
Err(Error::DiscFormat(format!("Partition type {kind} not found")))
}
}
} else if let Some(part) = self.partitions.iter().find(|v| v.kind == kind) {
Ok(PartitionReaderWii::new(self.io.clone(), self.preloader.clone(), part, options)?)
} else {
Err(Error::DiscFormat(format!("Partition type {kind} not found")))
}
}
@ -228,7 +323,7 @@ impl DiscReader {
// Build sector group request
let abs_sector = (self.pos / SECTOR_SIZE as u64) as u32;
let (request, abs_group_sector, max_groups) = if let Some(partition) =
self.partitions.iter().find(|part| part.data_contains_sector(abs_sector))
self.orig_partitions().iter().find(|part| part.data_contains_sector(abs_sector))
{
let group_idx = (abs_sector - partition.data_start_sector) / 64;
let abs_group_sector = partition.data_start_sector + group_idx * 64;
@ -283,7 +378,7 @@ impl BufRead for DiscReader {
// Build sector group request
let abs_sector = (self.pos / SECTOR_SIZE as u64) as u32;
let (request, abs_group_sector, max_groups) = if let Some(partition) =
self.partitions.iter().find(|part| part.data_contains_sector(abs_sector))
self.orig_partitions().iter().find(|part| part.data_contains_sector(abs_sector))
{
let group_idx = (abs_sector - partition.data_start_sector) / 64;
let abs_group_sector = partition.data_start_sector + group_idx * 64;
@ -395,11 +490,16 @@ fn read_partition_info(
data_start_sector,
key,
});
let partition_disc_header: Arc<DiscHeader> =
read_arc(reader).context("Reading partition disc header")?;
let partition_header = read_arc(reader).context("Reading partition header")?;
if partition_disc_header.is_wii() {
let raw_fst = read_fst(reader, &partition_header, true)?;
let raw_boot: Arc<[u8; BOOT_SIZE]> =
read_arc(reader).context("Reading partition headers")?;
let partition_disc_header =
DiscHeader::ref_from_bytes(&raw_boot[..size_of::<DiscHeader>()])
.expect("Invalid disc header alignment");
let partition_header =
PartitionHeader::ref_from_bytes(&raw_boot[size_of::<DiscHeader>()..])
.expect("Invalid partition header alignment");
let raw_fst = if partition_disc_header.is_wii() {
let raw_fst = read_fst(reader, partition_header, true)?;
let fst = Fst::new(&raw_fst)?;
let max_fst_offset = fst
.nodes
@ -420,9 +520,11 @@ fn read_partition_info(
)));
}
}
Some(raw_fst)
} else {
warn!("Partition {group_idx}:{part_idx} is not valid");
}
None
};
reader.reset(DirectDiscReaderMode::Raw);
part_info.push(PartitionInfo {
@ -433,10 +535,10 @@ fn read_partition_info(
data_end_sector,
key,
header,
disc_header: partition_disc_header,
partition_header,
has_encryption: disc_header.has_partition_encryption(),
has_hashes: disc_header.has_partition_hashes(),
raw_boot,
raw_fst,
});
}
}

View File

@ -14,7 +14,6 @@ use crate::{
common::{HashBytes, KeyBytes, PartitionInfo},
disc::{
gcn::{read_part_meta, PartitionReaderGC},
hashes::sha1_hash,
preloader::{fetch_sector_group, Preloader, SectorGroup, SectorGroupRequest},
SECTOR_GROUP_SIZE, SECTOR_SIZE,
},
@ -22,7 +21,9 @@ use crate::{
read::{PartitionEncryption, PartitionMeta, PartitionOptions, PartitionReader},
util::{
aes::aes_cbc_decrypt,
array_ref, div_rem, impl_read_for_bufread,
array_ref,
digest::sha1_hash,
div_rem, impl_read_for_bufread,
read::{read_arc, read_arc_slice},
static_assert,
},

View File

@ -253,6 +253,7 @@ pub(crate) fn check_block(
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)
== sector_data.len()
}) {
return Ok(CheckBlockResult::Junk);
}
@ -260,7 +261,7 @@ pub(crate) fn check_block(
if buf.iter().all(|&b| b == 0) {
return Ok(CheckBlockResult::Zeroed);
}
if lfg.check_sector_chunked(buf, disc_id, disc_num, input_position) {
if lfg.check_sector_chunked(buf, disc_id, disc_num, input_position) == buf.len() {
return Ok(CheckBlockResult::Junk);
}
}

View File

@ -324,19 +324,15 @@ impl DiscWriter for DiscWriterGCZ {
options.processor_threads,
|block| {
// Update hashers
let disc_data_len = block.disc_data.len() as u64;
input_position += block.disc_data.len() as u64;
digest.send(block.disc_data);
// Update block map and hash
if block.meta.is_compressed {
block_map[block.block_idx as usize] = data_position.into();
} else {
block_map[block.block_idx as usize] = (data_position | (1 << 63)).into();
}
let uncompressed_bit = (!block.meta.is_compressed as u64) << 63;
block_map[block.block_idx as usize] = (data_position | uncompressed_bit).into();
block_hashes[block.block_idx as usize] = block.meta.block_hash.into();
// Write block data
input_position += disc_data_len;
data_position += block.block_data.len() as u64;
data_callback(block.block_data, input_position, disc_size)
.with_context(|| format!("Failed to write block {}", block.block_idx))?;

View File

@ -103,7 +103,6 @@ impl SplitFileReader {
pub fn len(&self) -> u64 { self.files.last().map_or(0, |f| f.begin + f.size) }
#[instrument(name = "SplitFileReader::check_open_file", skip_all)]
fn check_open_file(&mut self) -> io::Result<Option<&mut Split<BufReader<File>>>> {
if self.open_file.is_none() || !self.open_file.as_ref().unwrap().contains(self.pos) {
self.open_file = if let Some(split) = self.files.iter().find(|f| f.contains(self.pos)) {

File diff suppressed because it is too large Load Diff

View File

@ -6,10 +6,33 @@ use digest::Digest;
use tracing::instrument;
use crate::{
common::HashBytes,
io::nkit::NKitHeader,
write::{DiscFinalization, ProcessOptions},
};
/// Hashes a byte slice with SHA-1.
#[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))
}
}
/// Hashes a byte slice with XXH64.
#[allow(unused_braces)] // https://github.com/rust-lang/rust/issues/116347
#[instrument(skip_all)]
pub fn xxh64_hash(buf: &[u8]) -> u64 { xxhash_rust::xxh64::xxh64(buf, 0) }
pub type DigestThread = (Sender<Bytes>, JoinHandle<DigestResult>);
pub fn digest_thread<H>() -> DigestThread
@ -40,13 +63,13 @@ impl DigestManager {
}
if options.digest_md5 {
#[cfg(feature = "openssl")]
threads.push(digest_thread::<ossl::HasherMD5>());
threads.push(digest_thread::<openssl_util::HasherMD5>());
#[cfg(not(feature = "openssl"))]
threads.push(digest_thread::<md5::Md5>());
}
if options.digest_sha1 {
#[cfg(feature = "openssl")]
threads.push(digest_thread::<ossl::HasherSHA1>());
threads.push(digest_thread::<openssl_util::HasherSHA1>());
#[cfg(not(feature = "openssl"))]
threads.push(digest_thread::<sha1::Sha1>());
}
@ -156,7 +179,7 @@ impl Hasher for xxhash_rust::xxh64::Xxh64 {
}
#[cfg(feature = "openssl")]
mod ossl {
mod openssl_util {
use tracing::instrument;
use super::{DigestResult, Hasher};
@ -208,7 +231,7 @@ mod ossl {
}
#[allow(unused_braces)] // https://github.com/rust-lang/rust/issues/116347
#[instrument(name = "ossl::HasherMD5::update", skip_all)]
#[instrument(name = "openssl_util::HasherMD5::update", skip_all)]
fn update(&mut self, data: &[u8]) { self.hasher.update(data).unwrap() }
}
@ -222,7 +245,7 @@ mod ossl {
}
#[allow(unused_braces)] // https://github.com/rust-lang/rust/issues/116347
#[instrument(name = "ossl::HasherSHA1::update", skip_all)]
#[instrument(name = "openssl_util::HasherSHA1::update", skip_all)]
fn update(&mut self, data: &[u8]) { self.hasher.update(data).unwrap() }
}
}

View File

@ -1,25 +1,31 @@
//! Lagged Fibonacci generator for GC / Wii partition junk data.
use std::{
cmp::min,
io,
io::{Read, Write},
};
use bytes::Buf;
use tracing::instrument;
use zerocopy::{transmute_ref, IntoBytes};
use crate::disc::SECTOR_SIZE;
use crate::{disc::SECTOR_SIZE, util::array_ref_mut};
/// Value of `k` for the LFG.
pub const LFG_K: usize = 521;
/// Value of `k` for the LFG in bytes.
pub const LFG_K_BYTES: usize = LFG_K * 4;
/// Value of `j` for the LFG.
pub const LFG_J: usize = 32;
/// Number of 32-bit words in the seed.
pub const SEED_SIZE: usize = 17;
/// Size of the seed in bytes.
pub const SEED_SIZE_BYTES: usize = SEED_SIZE * 4;
/// Lagged Fibonacci generator for GC / Wii partition junk data.
///
/// References (license CC0-1.0):
@ -38,48 +44,68 @@ impl Default for LaggedFibonacci {
impl LaggedFibonacci {
fn init(&mut self) {
for i in SEED_SIZE..LFG_K {
self.buffer[i] =
(self.buffer[i - 17] << 23) ^ (self.buffer[i - 16] >> 9) ^ self.buffer[i - 1];
self.buffer[i] = (self.buffer[i - SEED_SIZE] << 23)
^ (self.buffer[i - SEED_SIZE + 1] >> 9)
^ self.buffer[i - 1];
}
// Instead of doing the "shift by 18 instead of 16" oddity when actually outputting the data,
// we can do the shifting (and byteswapping) at this point to make the output code simpler.
for x in self.buffer.iter_mut() {
*x = ((*x & 0xFF00FFFF) | (*x >> 2 & 0x00FF0000)).swap_bytes();
*x = ((*x & 0xFF00FFFF) | (*x >> 2 & 0x00FF0000)).to_be();
}
for _ in 0..4 {
self.forward();
}
}
/// Initializes the LFG with the standard seed for a given disc ID, disc number, and sector.
/// The partition offset is used to determine the sector and how many bytes to skip within the
/// sector.
pub fn init_with_seed(&mut self, disc_id: [u8; 4], disc_num: u8, partition_offset: u64) {
/// Generates the seed for GC / Wii partition junk data using the disc ID, disc number, and sector.
pub fn generate_seed(out: &mut [u32; SEED_SIZE], disc_id: [u8; 4], disc_num: u8, sector: u32) {
let seed = u32::from_be_bytes([
disc_id[2],
disc_id[1],
disc_id[3].wrapping_add(disc_id[2]),
disc_id[0].wrapping_add(disc_id[1]),
]) ^ disc_num as u32;
let sector = (partition_offset / SECTOR_SIZE as u64) as u32;
let sector_offset = partition_offset % SECTOR_SIZE as u64;
let mut n = seed.wrapping_mul(0x260BCD5) ^ sector.wrapping_mul(0x1EF29123);
for i in 0..SEED_SIZE {
let mut v = 0u32;
for v in &mut *out {
*v = 0u32;
for _ in 0..LFG_J {
n = n.wrapping_mul(0x5D588B65).wrapping_add(1);
v = (v >> 1) | (n & 0x80000000);
*v = (*v >> 1) | (n & 0x80000000);
}
self.buffer[i] = v;
}
self.buffer[16] ^= self.buffer[0] >> 9 ^ self.buffer[16] << 23;
out[16] ^= out[0] >> 9 ^ out[16] << 23;
}
/// Same as [`generate_seed`], but ensures the resulting seed is big-endian.
pub fn generate_seed_be(
out: &mut [u32; SEED_SIZE],
disc_id: [u8; 4],
disc_num: u8,
sector: u32,
) {
Self::generate_seed(out, disc_id, disc_num, sector);
for x in out.iter_mut() {
*x = x.to_be();
}
}
/// Initializes the LFG with the standard seed for a given disc ID, disc number, and sector.
/// The partition offset is used to determine the sector and how many bytes to skip within the
/// sector.
#[instrument(name = "LaggedFibonacci::init_with_seed", skip_all)]
pub fn init_with_seed(&mut self, disc_id: [u8; 4], disc_num: u8, partition_offset: u64) {
let sector = (partition_offset / SECTOR_SIZE as u64) as u32;
let sector_offset = (partition_offset % SECTOR_SIZE as u64) as usize;
Self::generate_seed(array_ref_mut![self.buffer, 0, SEED_SIZE], disc_id, disc_num, sector);
self.position = 0;
self.init();
self.skip(sector_offset as usize);
self.skip(sector_offset);
}
/// Initializes the LFG with the seed read from a reader. The seed is assumed to be big-endian.
/// This is used for rebuilding junk data in WIA/RVZ files.
#[instrument(name = "LaggedFibonacci::init_with_reader", skip_all)]
pub fn init_with_reader<R>(&mut self, reader: &mut R) -> io::Result<()>
where R: Read + ?Sized {
reader.read_exact(self.buffer[..SEED_SIZE].as_mut_bytes())?;
@ -93,6 +119,7 @@ impl LaggedFibonacci {
/// Initializes the LFG with the seed read from a [`Buf`]. The seed is assumed to be big-endian.
/// This is used for rebuilding junk data in WIA/RVZ files.
#[instrument(name = "LaggedFibonacci::init_with_buf", skip_all)]
pub fn init_with_buf(&mut self, reader: &mut impl Buf) -> io::Result<()> {
let out = self.buffer[..SEED_SIZE].as_mut_bytes();
if reader.remaining() < out.len() {
@ -108,6 +135,9 @@ impl LaggedFibonacci {
}
/// Advances the LFG by one step.
// This gets vectorized and aggressively inlined, so it's better to
// keep it separate for code size and instruction cache pressure.
#[inline(never)]
fn forward(&mut self) {
for i in 0..LFG_J {
self.buffer[i] ^= self.buffer[i + LFG_K - LFG_J];
@ -120,61 +150,49 @@ impl LaggedFibonacci {
/// Skips `n` bytes of junk data.
pub fn skip(&mut self, n: usize) {
self.position += n;
while self.position >= LFG_K * 4 {
while self.position >= LFG_K_BYTES {
self.forward();
self.position -= LFG_K * 4;
self.position -= LFG_K_BYTES;
}
}
// pub fn backward(&mut self) {
// for i in (LFG_J..LFG_K).rev() {
// self.buffer[i] ^= self.buffer[i - LFG_J];
// }
// for i in (0..LFG_J).rev() {
// self.buffer[i] ^= self.buffer[i + LFG_K - LFG_J];
// }
// }
// pub fn get_seed(&mut self, seed: &mut [u8; SEED_SIZE]) {
// for i in 0..SEED_SIZE {
// seed[i] = self.buffer[i].to_be_bytes()[3];
// }
// }
/// Fills the buffer with junk data.
#[instrument(name = "LaggedFibonacci::fill", skip_all)]
pub fn fill(&mut self, mut buf: &mut [u8]) {
while !buf.is_empty() {
let len = min(buf.len(), LFG_K * 4 - self.position);
let bytes: &[u8; LFG_K * 4] = transmute_ref!(&self.buffer);
while self.position >= LFG_K_BYTES {
self.forward();
self.position -= LFG_K_BYTES;
}
let bytes: &[u8; LFG_K_BYTES] = transmute_ref!(&self.buffer);
let len = buf.len().min(LFG_K_BYTES - self.position);
buf[..len].copy_from_slice(&bytes[self.position..self.position + len]);
self.position += len;
buf = &mut buf[len..];
if self.position == LFG_K * 4 {
self.forward();
self.position = 0;
}
}
}
/// Writes junk data to the output stream.
#[instrument(name = "LaggedFibonacci::write", skip_all)]
pub fn write<W>(&mut self, w: &mut W, mut len: u64) -> io::Result<()>
where W: Write + ?Sized {
while len > 0 {
let write_len = min(len, LFG_K as u64 * 4 - self.position as u64) as usize;
let bytes: &[u8; LFG_K * 4] = transmute_ref!(&self.buffer);
while self.position >= LFG_K_BYTES {
self.forward();
self.position -= LFG_K_BYTES;
}
let bytes: &[u8; LFG_K_BYTES] = transmute_ref!(&self.buffer);
let write_len = len.min((LFG_K_BYTES - self.position) as u64) as usize;
w.write_all(&bytes[self.position..self.position + write_len])?;
self.position += write_len;
len -= write_len as u64;
if self.position == LFG_K * 4 {
self.forward();
self.position = 0;
}
}
Ok(())
}
/// The junk data on GC / Wii discs is reinitialized every 32KB. This functions handles the
/// wrapping logic and reinitializes the LFG at sector boundaries.
#[instrument(name = "LaggedFibonacci::fill_sector_chunked", skip_all)]
pub fn fill_sector_chunked(
&mut self,
mut buf: &mut [u8],
@ -194,6 +212,7 @@ impl LaggedFibonacci {
/// The junk data on GC / Wii discs is reinitialized every 32KB. This functions handles the
/// wrapping logic and reinitializes the LFG at sector boundaries.
#[instrument(name = "LaggedFibonacci::write_sector_chunked", skip_all)]
pub fn write_sector_chunked<W>(
&mut self,
w: &mut W,
@ -215,31 +234,50 @@ impl LaggedFibonacci {
Ok(())
}
/// Checks if the data matches the junk data generated by the LFG, up to the first sector
/// boundary.
#[instrument(name = "LaggedFibonacci::check", skip_all)]
pub fn check(
&mut self,
buf: &[u8],
disc_id: [u8; 4],
disc_num: u8,
partition_offset: u64,
) -> usize {
let mut lfg_buf = [0u8; SECTOR_SIZE];
self.init_with_seed(disc_id, disc_num, partition_offset);
let len = (SECTOR_SIZE - (partition_offset % SECTOR_SIZE as u64) as usize).min(buf.len());
self.fill(&mut lfg_buf[..len]);
buf[..len].iter().zip(&lfg_buf[..len]).take_while(|(a, b)| a == b).count()
}
/// Checks if the data matches the junk data generated by the LFG. This function handles the
/// wrapping logic and reinitializes the LFG at sector boundaries.
#[instrument(name = "LaggedFibonacci::check_sector_chunked", skip_all)]
pub fn check_sector_chunked(
&mut self,
mut buf: &[u8],
disc_id: [u8; 4],
disc_num: u8,
mut partition_offset: u64,
) -> bool {
if buf.is_empty() {
return false;
}
) -> usize {
let mut lfg_buf = [0u8; SECTOR_SIZE];
let mut total_num_matching = 0;
while !buf.is_empty() {
self.init_with_seed(disc_id, disc_num, partition_offset);
let len =
(SECTOR_SIZE - (partition_offset % SECTOR_SIZE as u64) as usize).min(buf.len());
self.fill(&mut lfg_buf[..len]);
if buf[..len] != lfg_buf[..len] {
return false;
let num_matching =
buf[..len].iter().zip(&lfg_buf[..len]).take_while(|(a, b)| a == b).count();
total_num_matching += num_matching;
if num_matching != len {
break;
}
buf = &buf[len..];
partition_offset += len as u64;
}
true
total_num_matching
}
}

View File

@ -673,7 +673,7 @@ fn check_junk_data(
.fill_buf()
.with_context(|| format!("Failed to read disc file at offset {}", offset))?;
let read_len = (file_buf.len() as u64).min(remaining) as usize;
if !lfg.check_sector_chunked(&file_buf[..read_len], junk_id, disc_num, pos) {
if lfg.check_sector_chunked(&file_buf[..read_len], junk_id, disc_num, pos) != read_len {
return Ok(false);
}

View File

@ -92,12 +92,13 @@ fn info_file(path: &Path) -> nod::Result<()> {
} else {
"N/A".to_string()
};
println!("\tTitle: {}", info.disc_header.game_title_str());
println!("\tGame ID: {} ({})", info.disc_header.game_id_str(), title_id_str);
let part_disc_header = info.disc_header();
println!("\tTitle: {}", part_disc_header.game_title_str());
println!("\tGame ID: {} ({})", part_disc_header.game_id_str(), title_id_str);
println!(
"\tDisc {}, Revision {}",
info.disc_header.disc_num + 1,
info.disc_header.disc_version
part_disc_header.disc_num + 1,
part_disc_header.disc_version
);
}
} else if header.is_gamecube() {