use std::{
io,
io::{Read, Seek, SeekFrom},
mem::size_of,
};
use zerocopy::{big_endian::*, FromBytes, Immutable, IntoBytes, KnownLayout};
use crate::{
disc::{
hashes::hash_bytes,
wii::{HASHES_SIZE, SECTOR_DATA_SIZE},
SECTOR_SIZE,
},
io::{
block::{Block, BlockIO, DiscStream, PartitionInfo, RVZ_MAGIC, WIA_MAGIC},
nkit::NKitHeader,
Compression, Format, HashBytes, KeyBytes, MagicBytes,
},
static_assert,
util::{
lfg::LaggedFibonacci,
read::{read_box_slice, read_from, read_u16_be, read_vec},
take_seek::TakeSeekExt,
},
DiscMeta, Error, Result, ResultContext,
};
#[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(4))]
pub struct WIAFileHeader {
pub magic: MagicBytes,
pub version: U32,
pub version_compatible: U32,
pub disc_size: U32,
pub disc_hash: HashBytes,
pub iso_file_size: U64,
pub wia_file_size: U64,
pub file_head_hash: HashBytes,
}
static_assert!(size_of::<WIAFileHeader>() == 0x48);
impl WIAFileHeader {
pub fn validate(&self) -> Result<()> {
if self.magic != WIA_MAGIC && self.magic != RVZ_MAGIC {
return Err(Error::DiscFormat(format!("Invalid WIA/RVZ magic: {:#X?}", self.magic)));
}
let bytes = self.as_bytes();
verify_hash(&bytes[..bytes.len() - size_of::<HashBytes>()], &self.file_head_hash)?;
if self.version_compatible.get() < 0x30000 {
return Err(Error::DiscFormat(format!(
"WIA/RVZ version {:#X} is not supported",
self.version_compatible
)));
}
Ok(())
}
pub fn is_rvz(&self) -> bool { self.magic == RVZ_MAGIC }
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DiscKind {
GameCube,
Wii,
}
impl TryFrom<u32> for DiscKind {
type Error = Error;
fn try_from(value: u32) -> Result<Self> {
match value {
1 => Ok(Self::GameCube),
2 => Ok(Self::Wii),
v => Err(Error::DiscFormat(format!("Invalid disc type {}", v))),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum WIACompression {
None,
Purge,
Bzip2,
Lzma,
Lzma2,
Zstandard,
}
impl TryFrom<u32> for WIACompression {
type Error = Error;
fn try_from(value: u32) -> Result<Self> {
match value {
0 => Ok(Self::None),
1 => Ok(Self::Purge),
2 => Ok(Self::Bzip2),
3 => Ok(Self::Lzma),
4 => Ok(Self::Lzma2),
5 => Ok(Self::Zstandard),
v => Err(Error::DiscFormat(format!("Invalid compression type {}", v))),
}
}
}
const DISC_HEAD_SIZE: usize = 0x80;
#[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(4))]
pub struct WIADisc {
pub disc_type: U32,
pub compression: U32,
pub compression_level: I32,
pub chunk_size: U32,
pub disc_head: [u8; DISC_HEAD_SIZE],
pub num_partitions: U32,
pub partition_type_size: U32,
pub partition_offset: U64,
pub partition_hash: HashBytes,
pub num_raw_data: U32,
pub raw_data_offset: U64,
pub raw_data_size: U32,
pub num_groups: U32,
pub group_offset: U64,
pub group_size: U32,
pub compr_data_len: u8,
pub compr_data: [u8; 7],
}
static_assert!(size_of::<WIADisc>() == 0xDC);
impl WIADisc {
pub fn validate(&self) -> Result<()> {
DiscKind::try_from(self.disc_type.get())?;
WIACompression::try_from(self.compression.get())?;
if self.partition_type_size.get() != size_of::<WIAPartition>() as u32 {
return Err(Error::DiscFormat(format!(
"WIA/RVZ partition type size is {}, expected {}",
self.partition_type_size.get(),
size_of::<WIAPartition>()
)));
}
Ok(())
}
pub fn compression(&self) -> WIACompression {
WIACompression::try_from(self.compression.get()).unwrap()
}
}
#[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(4))]
pub struct WIAPartitionData {
pub first_sector: U32,
pub num_sectors: U32,
pub group_index: U32,
pub num_groups: U32,
}
static_assert!(size_of::<WIAPartitionData>() == 0x10);
impl WIAPartitionData {
pub fn contains(&self, sector: u32) -> bool {
let start = self.first_sector.get();
sector >= start && sector < start + self.num_sectors.get()
}
}
#[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(4))]
pub struct WIAPartition {
pub partition_key: KeyBytes,
pub partition_data: [WIAPartitionData; 2],
}
static_assert!(size_of::<WIAPartition>() == 0x30);
#[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(4))]
pub struct WIARawData {
pub raw_data_offset: U64,
pub raw_data_size: U64,
pub group_index: U32,
pub num_groups: U32,
}
impl WIARawData {
pub fn start_offset(&self) -> u64 { self.raw_data_offset.get() & !(SECTOR_SIZE as u64 - 1) }
pub fn start_sector(&self) -> u32 { (self.start_offset() / SECTOR_SIZE as u64) as u32 }
pub fn end_offset(&self) -> u64 { self.raw_data_offset.get() + self.raw_data_size.get() }
pub fn end_sector(&self) -> u32 { (self.end_offset() / SECTOR_SIZE as u64) as u32 }
pub fn contains(&self, sector: u32) -> bool {
sector >= self.start_sector() && sector < self.end_sector()
}
}
#[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(4))]
pub struct WIAGroup {
pub data_offset: U32,
pub data_size: U32,
}
#[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(4))]
pub struct RVZGroup {
pub data_offset: U32,
pub data_size_and_flag: U32,
pub rvz_packed_size: U32,
}
impl RVZGroup {
pub fn data_size(&self) -> u32 { self.data_size_and_flag.get() & 0x7FFFFFFF }
pub fn is_compressed(&self) -> bool { self.data_size_and_flag.get() & 0x80000000 != 0 }
}
impl From<&WIAGroup> for RVZGroup {
fn from(value: &WIAGroup) -> Self {
Self {
data_offset: value.data_offset,
data_size_and_flag: U32::new(value.data_size.get() | 0x80000000),
rvz_packed_size: U32::new(0),
}
}
}
#[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(2))]
pub struct WIAException {
pub offset: U16,
pub hash: HashBytes,
}
type WIAExceptionList = Box<[WIAException]>;
#[derive(Clone)]
pub enum Decompressor {
None,
#[cfg(feature = "compress-bzip2")]
Bzip2,
#[cfg(feature = "compress-lzma")]
Lzma(Box<[u8]>),
#[cfg(feature = "compress-lzma")]
Lzma2(Box<[u8]>),
#[cfg(feature = "compress-zstd")]
Zstandard,
}
impl Decompressor {
pub fn new(disc: &WIADisc) -> Result<Self> {
let _data = &disc.compr_data[..disc.compr_data_len as usize];
match disc.compression() {
WIACompression::None => Ok(Self::None),
#[cfg(feature = "compress-bzip2")]
WIACompression::Bzip2 => Ok(Self::Bzip2),
#[cfg(feature = "compress-lzma")]
WIACompression::Lzma => Ok(Self::Lzma(Box::from(_data))),
#[cfg(feature = "compress-lzma")]
WIACompression::Lzma2 => Ok(Self::Lzma2(Box::from(_data))),
#[cfg(feature = "compress-zstd")]
WIACompression::Zstandard => Ok(Self::Zstandard),
comp => Err(Error::DiscFormat(format!("Unsupported WIA/RVZ compression: {:?}", comp))),
}
}
pub fn wrap<'a, R>(&mut self, reader: R) -> io::Result<Box<dyn Read + 'a>>
where R: Read + 'a {
Ok(match self {
Decompressor::None => Box::new(reader),
#[cfg(feature = "compress-bzip2")]
Decompressor::Bzip2 => Box::new(bzip2::read::BzDecoder::new(reader)),
#[cfg(feature = "compress-lzma")]
Decompressor::Lzma(data) => {
use crate::util::compress::{lzma_props_decode, new_lzma_decoder};
let options = lzma_props_decode(data)?;
Box::new(new_lzma_decoder(reader, &options)?)
}
#[cfg(feature = "compress-lzma")]
Decompressor::Lzma2(data) => {
use crate::util::compress::{lzma2_props_decode, new_lzma2_decoder};
let options = lzma2_props_decode(data)?;
Box::new(new_lzma2_decoder(reader, &options)?)
}
#[cfg(feature = "compress-zstd")]
Decompressor::Zstandard => Box::new(zstd::stream::Decoder::new(reader)?),
})
}
}
pub struct DiscIOWIA {
inner: Box<dyn DiscStream>,
header: WIAFileHeader,
disc: WIADisc,
partitions: Box<[WIAPartition]>,
raw_data: Box<[WIARawData]>,
groups: Box<[RVZGroup]>,
nkit_header: Option<NKitHeader>,
decompressor: Decompressor,
group: u32,
group_data: Vec<u8>,
exception_lists: Vec<WIAExceptionList>,
}
impl Clone for DiscIOWIA {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
header: self.header.clone(),
disc: self.disc.clone(),
partitions: self.partitions.clone(),
raw_data: self.raw_data.clone(),
groups: self.groups.clone(),
nkit_header: self.nkit_header.clone(),
decompressor: self.decompressor.clone(),
group: u32::MAX,
group_data: Vec::new(),
exception_lists: Vec::new(),
}
}
}
fn verify_hash(buf: &[u8], expected: &HashBytes) -> Result<()> {
let out = hash_bytes(buf);
if out != *expected {
let mut got_bytes = [0u8; 40];
let got = base16ct::lower::encode_str(&out, &mut got_bytes).unwrap(); let mut expected_bytes = [0u8; 40];
let expected = base16ct::lower::encode_str(expected, &mut expected_bytes).unwrap(); return Err(Error::DiscFormat(format!(
"WIA/RVZ hash mismatch: {}, expected {}",
got, expected
)));
}
Ok(())
}
impl DiscIOWIA {
pub fn new(mut inner: Box<dyn DiscStream>) -> Result<Box<Self>> {
inner.seek(SeekFrom::Start(0)).context("Seeking to start")?;
let header: WIAFileHeader =
read_from(inner.as_mut()).context("Reading WIA/RVZ file header")?;
header.validate()?;
let is_rvz = header.is_rvz();
let mut disc_buf: Vec<u8> = read_vec(inner.as_mut(), header.disc_size.get() as usize)
.context("Reading WIA/RVZ disc header")?;
verify_hash(&disc_buf, &header.disc_hash)?;
disc_buf.resize(size_of::<WIADisc>(), 0);
let disc = WIADisc::read_from_bytes(disc_buf.as_slice()).unwrap();
disc.validate()?;
let nkit_header = NKitHeader::try_read_from(inner.as_mut(), disc.chunk_size.get(), false);
inner
.seek(SeekFrom::Start(disc.partition_offset.get()))
.context("Seeking to WIA/RVZ partition headers")?;
let partitions: Box<[WIAPartition]> =
read_box_slice(inner.as_mut(), disc.num_partitions.get() as usize)
.context("Reading WIA/RVZ partition headers")?;
verify_hash(partitions.as_ref().as_bytes(), &disc.partition_hash)?;
let mut decompressor = Decompressor::new(&disc)?;
let raw_data: Box<[WIARawData]> = {
inner
.seek(SeekFrom::Start(disc.raw_data_offset.get()))
.context("Seeking to WIA/RVZ raw data headers")?;
let mut reader = decompressor
.wrap(inner.as_mut().take(disc.raw_data_size.get() as u64))
.context("Creating WIA/RVZ decompressor")?;
read_box_slice(&mut reader, disc.num_raw_data.get() as usize)
.context("Reading WIA/RVZ raw data headers")?
};
for (idx, rd) in raw_data.iter().enumerate() {
let start_offset = rd.start_offset();
let end_offset = rd.end_offset();
if (start_offset % SECTOR_SIZE as u64) != 0 || (end_offset % SECTOR_SIZE as u64) != 0 {
return Err(Error::DiscFormat(format!(
"WIA/RVZ raw data {} not aligned to sector: {:#X}..{:#X}",
idx, start_offset, end_offset
)));
}
}
let groups = {
inner
.seek(SeekFrom::Start(disc.group_offset.get()))
.context("Seeking to WIA/RVZ group headers")?;
let mut reader = decompressor
.wrap(inner.as_mut().take(disc.group_size.get() as u64))
.context("Creating WIA/RVZ decompressor")?;
if is_rvz {
read_box_slice(&mut reader, disc.num_groups.get() as usize)
.context("Reading WIA/RVZ group headers")?
} else {
let wia_groups: Box<[WIAGroup]> =
read_box_slice(&mut reader, disc.num_groups.get() as usize)
.context("Reading WIA/RVZ group headers")?;
wia_groups.iter().map(RVZGroup::from).collect()
}
};
Ok(Box::new(Self {
header,
disc,
partitions,
raw_data,
groups,
inner,
nkit_header,
decompressor,
group: u32::MAX,
group_data: vec![],
exception_lists: vec![],
}))
}
}
fn read_exception_lists<R>(
reader: &mut R,
in_partition: bool,
chunk_size: u32,
) -> io::Result<Vec<WIAExceptionList>>
where
R: Read + ?Sized,
{
if !in_partition {
return Ok(vec![]);
}
let num_exception_list = (chunk_size as usize).div_ceil(0x200000);
let mut exception_lists = Vec::with_capacity(num_exception_list);
for i in 0..num_exception_list {
let num_exceptions = read_u16_be(reader)?;
let exceptions: Box<[WIAException]> = read_box_slice(reader, num_exceptions as usize)?;
if !exceptions.is_empty() {
log::debug!("Exception list {}: {:?}", i, exceptions);
}
exception_lists.push(exceptions);
}
Ok(exception_lists)
}
impl BlockIO for DiscIOWIA {
fn read_block_internal(
&mut self,
out: &mut [u8],
sector: u32,
partition: Option<&PartitionInfo>,
) -> io::Result<Block> {
let chunk_size = self.disc.chunk_size.get();
let sectors_per_chunk = chunk_size / SECTOR_SIZE as u32;
let (group_index, group_sector, partition_offset) = if let Some(partition) = partition {
let Some(wia_part) = self.partitions.get(partition.index) else {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("Couldn't find WIA/RVZ partition index {}", partition.index),
));
};
let wia_part_start = wia_part.partition_data[0].first_sector.get();
let wia_part_end = wia_part.partition_data[1].first_sector.get()
+ wia_part.partition_data[1].num_sectors.get();
if partition.data_start_sector != wia_part_start
|| partition.data_end_sector != wia_part_end
{
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"WIA/RVZ partition sector mismatch: {}..{} != {}..{}",
wia_part_start,
wia_part_end,
partition.data_start_sector,
partition.data_end_sector
),
));
}
let Some(pd) = wia_part.partition_data.iter().find(|pd| pd.contains(sector)) else {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("Couldn't find WIA/RVZ partition data for sector {}", sector),
));
};
let part_data_sector = sector - pd.first_sector.get();
let part_group_index = part_data_sector / sectors_per_chunk;
let part_group_sector = part_data_sector % sectors_per_chunk;
if part_group_index >= pd.num_groups.get() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"WIA/RVZ partition group index out of range: {} >= {}",
part_group_index,
pd.num_groups.get()
),
));
}
let part_group_offset =
(((part_group_index * sectors_per_chunk) + pd.first_sector.get())
- wia_part.partition_data[0].first_sector.get()) as u64
* SECTOR_DATA_SIZE as u64;
(pd.group_index.get() + part_group_index, part_group_sector, part_group_offset)
} else {
let Some(rd) = self.raw_data.iter().find(|d| d.contains(sector)) else {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("Couldn't find WIA/RVZ raw data for sector {}", sector),
));
};
let data_sector = sector - (rd.raw_data_offset.get() / SECTOR_SIZE as u64) as u32;
let group_index = data_sector / sectors_per_chunk;
let group_sector = data_sector % sectors_per_chunk;
if group_index >= rd.num_groups.get() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"WIA/RVZ raw data group index out of range: {} >= {}",
group_index,
rd.num_groups.get()
),
));
}
(rd.group_index.get() + group_index, group_sector, 0)
};
let Some(group) = self.groups.get(group_index as usize) else {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("Couldn't find WIA/RVZ group index {}", group_index),
));
};
if group.data_size() == 0 {
self.exception_lists.clear();
return Ok(Block::Zero);
}
if group_index != self.group {
let group_data_size = if partition.is_some() {
(sectors_per_chunk * SECTOR_DATA_SIZE as u32) as usize
} else {
chunk_size as usize
};
self.group_data = Vec::with_capacity(group_data_size);
let group_data_start = group.data_offset.get() as u64 * 4;
self.inner.seek(SeekFrom::Start(group_data_start))?;
let mut reader = (&mut self.inner).take_seek(group.data_size() as u64);
let uncompressed_exception_lists =
matches!(self.disc.compression(), WIACompression::None | WIACompression::Purge)
|| !group.is_compressed();
if uncompressed_exception_lists {
self.exception_lists = read_exception_lists(
&mut reader,
partition.is_some(),
self.disc.chunk_size.get(),
)?;
let rem = reader.stream_position()? % 4;
if rem != 0 {
reader.seek(SeekFrom::Current((4 - rem) as i64))?;
}
}
let mut reader: Box<dyn Read> = if group.is_compressed() {
self.decompressor.wrap(reader)?
} else {
Box::new(reader)
};
if !uncompressed_exception_lists {
self.exception_lists = read_exception_lists(
reader.as_mut(),
partition.is_some(),
self.disc.chunk_size.get(),
)?;
}
if group.rvz_packed_size.get() > 0 {
let mut lfg = LaggedFibonacci::default();
loop {
let mut size_bytes = [0u8; 4];
match reader.read_exact(&mut size_bytes) {
Ok(_) => {}
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => break,
Err(e) => {
return Err(io::Error::new(e.kind(), "Failed to read RVZ packed size"));
}
}
let size = u32::from_be_bytes(size_bytes);
let cur_data_len = self.group_data.len();
if size & 0x80000000 != 0 {
let size = size & 0x7FFFFFFF;
lfg.init_with_reader(reader.as_mut())?;
lfg.skip(
((partition_offset + cur_data_len as u64) % SECTOR_SIZE as u64)
as usize,
);
self.group_data.resize(cur_data_len + size as usize, 0);
lfg.fill(&mut self.group_data[cur_data_len..]);
} else {
self.group_data.resize(cur_data_len + size as usize, 0);
reader.read_exact(&mut self.group_data[cur_data_len..])?;
}
}
} else {
reader.read_to_end(&mut self.group_data)?;
}
self.group = group_index;
}
if partition.is_some() {
let sector_data_start = group_sector as usize * SECTOR_DATA_SIZE;
out[..HASHES_SIZE].fill(0);
out[HASHES_SIZE..SECTOR_SIZE].copy_from_slice(
&self.group_data[sector_data_start..sector_data_start + SECTOR_DATA_SIZE],
);
Ok(Block::PartDecrypted { has_hashes: false })
} else {
let sector_data_start = group_sector as usize * SECTOR_SIZE;
out.copy_from_slice(
&self.group_data[sector_data_start..sector_data_start + SECTOR_SIZE],
);
Ok(Block::Raw)
}
}
fn block_size_internal(&self) -> u32 {
SECTOR_SIZE as u32
}
fn meta(&self) -> DiscMeta {
let mut result = DiscMeta {
format: if self.header.is_rvz() { Format::Rvz } else { Format::Wia },
block_size: Some(self.disc.chunk_size.get()),
compression: match self.disc.compression() {
WIACompression::None => Compression::None,
WIACompression::Purge => Compression::Purge,
WIACompression::Bzip2 => Compression::Bzip2,
WIACompression::Lzma => Compression::Lzma,
WIACompression::Lzma2 => Compression::Lzma2,
WIACompression::Zstandard => Compression::Zstandard,
},
decrypted: true,
needs_hash_recovery: true,
lossless: true,
disc_size: Some(self.header.iso_file_size.get()),
..Default::default()
};
if let Some(nkit_header) = &self.nkit_header {
nkit_header.apply(&mut result);
}
result
}
}