use crate::error::Error;
use crate::region::Region;
use crate::region::RegionType;
use std::cmp;
use std::fmt;
use std::io;
use std::io::Cursor;
use std::io::Read;
use std::io::Seek;
use std::io::SeekFrom;
use std::io::Write;
use std::sync::Arc;
use byteorder::BigEndian;
use byteorder::ReadBytesExt;
use bzip2::read::BzDecoder;
use zstd::Decoder;
use sha1::Digest;
use sha1::Sha1;
use lru::LruCache;
pub struct Rvz<T: Seek + Read> {
pub metadata: Metadata,
io: Io<T>,
}
#[derive(Debug)]
pub struct Metadata {
pub header: Header,
pub disc: Disc,
pub partitions: Vec<Partition>,
pub raw_data: Vec<RawData>,
pub groups: Vec<Group>,
pub regions: Vec<Region>,
pub hashes: Vec<Hashes>,
}
#[derive(Debug)]
pub struct Header {
pub magic: [u8; 4],
pub version: u32,
pub version_compatible: u32,
pub disc_size: u32,
pub disc_hash: [u8; 20],
pub iso_file_size: u64,
pub rvz_file_size: u64,
pub file_head_hash: [u8; 20],
}
#[derive(Debug)]
pub struct Disc {
pub disc_type: DiscType,
pub compression: Compression,
pub compression_level: i32,
pub chunk_size: u32,
pub disc_head: [u8; 0x80],
pub partition_count: u32,
pub partition_size: u32,
pub partitions_offset: u64,
pub partitions_hash: [u8; 20],
pub raw_data_count: u32,
pub raw_data_offset: u64,
pub raw_data_size: u32,
pub group_count: u32,
pub group_offset: u64,
pub group_size: u32,
pub compressor_data_length: u8,
pub compressor_data: [u8; 7],
}
#[derive(Debug, Clone, Copy)]
pub enum DiscType {
Unknown,
GameCube,
Wii,
}
#[derive(Debug, Copy, Clone)]
pub struct Partition {
pub partition_key: [u8; 16],
pub partition_data: [PartitionData; 2],
}
#[derive(Debug, Copy, Clone)]
pub struct PartitionData {
pub first_sector: u32,
pub sector_count: u32,
pub group_index: u32,
pub group_count: u32,
}
#[derive(Debug, Default, Clone)]
pub struct Hashes {
pub h0: Vec<[[u8; 20]; 31]>,
pub h1: Vec<[[u8; 20]; 8]>,
pub h2: Vec<[[u8; 20]; 8]>,
pub h3: Vec<[u8; 20]>,
}
#[derive(Debug, Copy, Clone)]
pub struct RawData {
pub raw_data_offset: u64,
pub raw_data_size: u64,
pub group_index: u32,
pub group_count: u32,
}
#[derive(Debug)]
pub struct Group {
pub data_offset_div4: u32,
pub data_size: u32,
pub packed_size: u32,
}
#[derive(Debug)]
pub struct ExceptionList(pub Vec<Exception>);
#[derive(Debug)]
pub struct Exception {
pub offset: u16,
pub hash: [u8; 20],
}
#[derive(Debug, Clone, Copy)]
pub enum Compression {
None,
Bzip2,
Lzma,
Lzma2,
Zstd,
}
pub trait HeaderRead {
fn has_rvz_magic(&mut self) -> bool;
fn read_rvz_header(&mut self) -> Result<Header, Error>;
fn read_rvz_disc(&mut self) -> Result<Disc, Error>;
fn read_rvz_partitions(&mut self, disc: &Disc) -> Result<Vec<Partition>, Error>;
fn read_rvz_raw_datas(&mut self, disc: &Disc) -> Result<Vec<RawData>, Error>;
fn read_rvz_groups(&mut self, disc: &Disc) -> Result<Vec<Group>, Error>;
}
pub trait GroupExceptionRead {
fn read_rvz_exceptions(&mut self) -> io::Result<ExceptionList>;
}
struct Io<T: Seek + Read> {
io: T,
position: u64,
group_cache: LruCache<u32, Arc<[u8]>>,
}
fn next_start(position: u64, regions: &[Region]) -> Option<u64> {
let mut result = None;
for region in regions {
if position < region.start() && !region.contains(position) {
result = Some(region.start());
}
}
result
}
impl<T: Read + Seek> Seek for Rvz<T> {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
Ok(self.io.seek_io(pos, &self.metadata.header))
}
}
impl<T: Read + Seek> Rvz<T> {
pub fn compute_hashes(&mut self) -> Result<&Vec<Hashes>, Error> {
let position = self.io.position;
self.metadata.hashes = self.io.compute_hashes(&self.metadata)?;
self.seek(SeekFrom::Start(position))?;
Ok(&self.metadata.hashes)
}
}
const H1_ZERO_HASH: [u8; 20] = [
0x26, 0x94, 0x62, 0xc3, 0xc0, 0x85, 0xad, 0x49, 0x3d, 0x26, 0xca, 0x70, 0xa0, 0x0c, 0xb7, 0x26,
0x8c, 0x7b, 0xa0, 0x4c,
];
const H2_ZERO_HASH: [u8; 20] = [
0x9e, 0x69, 0xb0, 0xac, 0x67, 0x72, 0x95, 0xa0, 0xb4, 0x57, 0x14, 0xdd, 0x84, 0xd5, 0xfd, 0x4d,
0x24, 0x63, 0x93, 0x66,
];
impl<T: Read + Seek> Io<T> {
pub const fn seek_io(&mut self, pos: SeekFrom, header: &Header) -> u64 {
self.position = match pos {
SeekFrom::Start(offset) => offset,
SeekFrom::End(offset) => header.iso_file_size.wrapping_add_signed(offset),
SeekFrom::Current(offset) => self.position.wrapping_add_signed(offset),
};
self.position
}
pub fn read_io(&mut self, buf: &mut [u8], metadata: &Metadata) -> io::Result<usize> {
let mut left = buf.len();
let mut buf = Cursor::new(buf);
let total = left;
let mut position = self.position;
let header = &metadata.header;
let disc = &metadata.disc;
let regions = &metadata.regions;
while left != 0 {
if position >= header.iso_file_size {
self.position = position;
return Ok(total - left);
}
let region = regions.iter().find(|r| r.contains(position));
if let Some(region) = region {
let amount = if position < 0x80 {
let amount = cmp::min(0x80 - usize::try_from(position).unwrap(), left);
let tmp_position = usize::try_from(position).unwrap();
buf.write_all(&disc.disc_head[tmp_position..tmp_position + amount])?;
u64::try_from(amount).unwrap()
} else {
let buf_pos = buf.position();
self.read_from_region(
region,
metadata,
position,
&mut buf.get_mut()[buf_pos as usize..],
)?
};
left -= usize::try_from(amount).unwrap();
position += amount;
} else {
let end = next_start(position, regions);
let end = end.unwrap_or(header.iso_file_size);
let amount = cmp::min(end - position, u64::try_from(left).unwrap());
let mut null = io::repeat(0).take(amount);
let amount = io::copy(&mut null, &mut buf)?;
left -= usize::try_from(amount).unwrap();
position += amount;
}
}
self.position = position;
Ok(total)
}
fn read_from_region(
&mut self,
region: &Region,
metadata: &Metadata,
position: u64,
buf: &mut [u8],
) -> io::Result<u64> {
let mut position = position;
let mut amount_read = 0u64;
let mut buf_position = 0usize;
loop {
let Some((index, _)) = region.group_of_position(metadata, position) else {
return Err(io::Error::from(io::ErrorKind::InvalidInput));
};
let data = if let Some(group) = self.group_cache.get_mut(&index) {
group.clone()
} else {
let data = region.read_group(&mut self.io, metadata, position)?;
let data = Arc::<[u8]>::from(&*data);
self.group_cache.put(index, data.clone());
data
};
#[allow(clippy::cast_possible_truncation)]
let group_offset =
(region.region_offset(position) % u64::from(metadata.disc.chunk_size)) as usize;
let to_read = cmp::min(data.len(), buf.len() - buf_position);
let to_read = cmp::min(to_read, data.len() - group_offset);
buf[buf_position..buf_position + to_read]
.copy_from_slice(&data[group_offset..group_offset + to_read]);
amount_read += to_read as u64;
position += to_read as u64;
buf_position += to_read;
if buf.len() == buf_position || position >= region.end() {
return Ok(amount_read);
}
let last_index = region.group_index() + region.group_count() - 1;
if (to_read == 0) && (index == last_index) {
panic!("huh, we finally hit this?");
}
}
}
fn compute_h0(
&mut self,
metadata: &Metadata,
partition: &Partition,
) -> io::Result<Vec<[[u8; 20]; 31]>> {
let mut h0_table = vec![];
for data in partition.partition_data {
for sector in data.first_sector..data.first_sector + data.sector_count {
let offset = u64::from(sector) * 0x8000 + 0x400;
self.seek_io(SeekFrom::Start(offset), &metadata.header);
let mut data = [0u8; 0x7C00];
self.read_io(&mut data, metadata)?;
let mut input_cursor = Cursor::new(&data);
let mut h0 = [[0u8; 20]; 31];
for hash in &mut h0 {
let mut sha1 = Sha1::new();
io::copy(&mut (&mut input_cursor).take(0x400), &mut sha1)?;
hash.clone_from_slice(sha1.finalize().as_slice());
}
h0_table.push(h0);
}
}
Ok(h0_table)
}
fn compute_h1(partition: &Partition, h0_table: &[[[u8; 20]; 31]]) -> Vec<[[u8; 20]; 8]> {
let mut h1_table = vec![];
let sector_count =
partition.partition_data[0].sector_count + partition.partition_data[1].sector_count;
let subgroup_count = usize::try_from(sector_count / 8).unwrap();
let partial_subgroup = !sector_count.is_multiple_of(8);
for i in 0..subgroup_count {
let mut h1_hash = [[0u8; 20]; 8];
for j in 0..8 {
let mut h1_sha1 = Sha1::new();
for hash in &h0_table[j + i * 8] {
h1_sha1.update(hash);
}
h1_hash[j] = h1_sha1.finalize().into();
}
h1_table.push(h1_hash);
}
if partial_subgroup {
let mut h1_hash = [[0u8; 20]; 8];
let have = (sector_count % 8) as usize;
let missing = 8 - have;
for j in 0..have {
let mut h1_sha1 = Sha1::new();
for hash in &h0_table[subgroup_count * 8 + j] {
h1_sha1.update(hash);
}
h1_hash[j] = h1_sha1.finalize().into();
}
for j in 0..missing {
h1_hash[have + j] = H1_ZERO_HASH;
}
h1_table.push(h1_hash);
}
h1_table
}
fn compute_h2(h1_table: &[[[u8; 20]; 8]]) -> Vec<[[u8; 20]; 8]> {
let mut h2_table = vec![];
let group_count = h1_table.len() / 8;
let subgroup_leftover = h1_table.len() % 8;
let partial_group = !subgroup_leftover.is_multiple_of(8);
for i in 0..group_count {
let mut h2_hash = [[0u8; 20]; 8];
for j in 0..8 {
let mut h2_sha1 = Sha1::new();
for hash in &h1_table[j + i * 8] {
h2_sha1.update(hash);
}
h2_hash[j] = h2_sha1.finalize().into();
}
h2_table.push(h2_hash);
}
if partial_group {
let mut h2_hash = [[0u8; 20]; 8];
let have = subgroup_leftover;
let missing = 8 - have;
for j in 0..have {
let mut h2_sha1 = Sha1::new();
for hash in &h1_table[group_count * 8 + j] {
h2_sha1.update(hash);
}
h2_hash[j] = h2_sha1.finalize().into();
}
for j in 0..missing {
h2_hash[have + j] = H2_ZERO_HASH;
}
h2_table.push(h2_hash);
}
h2_table
}
fn compute_h3(h2_table: &[[[u8; 20]; 8]]) -> Vec<[u8; 20]> {
let mut result = vec![];
for group in h2_table {
let mut h3_sha1 = Sha1::new();
for hash in group {
h3_sha1.update(hash);
}
result.push(h3_sha1.finalize().into());
}
result
}
pub fn compute_hashes(&mut self, metadata: &Metadata) -> Result<Vec<Hashes>, Error> {
let mut hashes = vec![];
for partition in &metadata.partitions {
let h0_table = self.compute_h0(metadata, partition)?;
let h1_table = Self::compute_h1(partition, &h0_table);
let h2_table = Self::compute_h2(&h1_table);
let h3_table = Self::compute_h3(&h2_table);
hashes.push(Hashes {
h0: h0_table,
h1: h1_table,
h2: h2_table,
h3: h3_table,
});
self.group_cache.clear();
}
Ok(hashes)
}
}
impl<T: Read + Seek> Read for Rvz<T> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.io.read_io(buf, &self.metadata)
}
}
impl Disc {
pub fn compression_io<'io, T: Read + 'io>(&self, io: T) -> io::Result<Box<dyn Read + 'io>> {
let io: Box<dyn Read> = match self.compression {
Compression::None => Box::new(io),
Compression::Zstd => Box::new(Decoder::new(io)?),
Compression::Bzip2 => Box::new(BzDecoder::new(io)),
Compression::Lzma => {
panic!("LZMA/2 not implemented");
let properties = self.compressor_data[0];
let dict_size = u32::from_le_bytes(self.compressor_data[1..5].try_into().unwrap());
let lc = properties % 9;
let pb = properties / 9 / 5;
let lp = (properties / 9) % 5;
}
Compression::Lzma2 => {
panic!("LZMA/2 not implemented");
let properties: u32 = self.compressor_data[0].into();
let size = (2 | properties & 1) << (properties / 2 + 11);
if properties > 40 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"invalid LZMA2 dict size",
));
}
let size = if properties == 40 { 0xFFFF_FFFF } else { size };
}
};
Ok(io)
}
}
impl<T: Read + Seek> HeaderRead for T {
fn has_rvz_magic(&mut self) -> bool {
let Ok(_) = self.seek(SeekFrom::Start(0)) else {
return false;
};
let Ok(magic) = self.read_u32::<BigEndian>() else {
return false;
};
magic == 0x5256_5a01
}
fn read_rvz_header(&mut self) -> Result<Header, Error> {
self.seek(SeekFrom::Start(0))?;
let mut magic = [0u8; 4];
self.read_exact(&mut magic)?;
let version = self.read_u32::<BigEndian>()?;
let version_compatible = self.read_u32::<BigEndian>()?;
let disc_size = self.read_u32::<BigEndian>()?;
let mut disc_hash = [0u8; 20];
self.read_exact(&mut disc_hash)?;
let iso_file_size = self.read_u64::<BigEndian>()?;
let rvz_file_size = self.read_u64::<BigEndian>()?;
let mut file_head_hash = [0u8; 20];
self.read_exact(&mut file_head_hash)?;
Ok(Header {
magic,
version,
version_compatible,
disc_size,
disc_hash,
iso_file_size,
rvz_file_size,
file_head_hash,
})
}
fn read_rvz_disc(&mut self) -> Result<Disc, Error> {
self.seek(SeekFrom::Start(0x48))?;
let disc_type = self.read_u32::<BigEndian>()?.try_into()?;
let compression = self.read_u32::<BigEndian>()?.try_into()?;
let compression_level = self.read_i32::<BigEndian>()?;
let chunk_size = self.read_u32::<BigEndian>()?;
let mut disc_head = [0u8; 0x80];
self.read_exact(&mut disc_head)?;
let partition_count = self.read_u32::<BigEndian>()?;
let partition_size = self.read_u32::<BigEndian>()?;
let partitions_offset = self.read_u64::<BigEndian>()?;
let mut partitions_hash = [0u8; 20];
self.read_exact(&mut partitions_hash)?;
let raw_data_count = self.read_u32::<BigEndian>()?;
let raw_data_offset = self.read_u64::<BigEndian>()?;
let raw_data_size = self.read_u32::<BigEndian>()?;
let group_count = self.read_u32::<BigEndian>()?;
let group_offset = self.read_u64::<BigEndian>()?;
let group_size = self.read_u32::<BigEndian>()?;
let compressor_data_length = self.read_u8()?;
let mut compressor_data = [0u8; 7];
self.read_exact(&mut compressor_data)?;
Ok(Disc {
disc_type,
compression,
compression_level,
chunk_size,
disc_head,
partition_count,
partition_size,
partitions_offset,
partitions_hash,
raw_data_count,
raw_data_offset,
raw_data_size,
group_count,
group_offset,
group_size,
compressor_data_length,
compressor_data,
})
}
fn read_rvz_partitions(&mut self, disc: &Disc) -> Result<Vec<Partition>, Error> {
self.seek(SeekFrom::Start(disc.partitions_offset))?;
let mut partitions = vec![];
for _ in 0..disc.partition_count {
let mut partition_key = [0u8; 16];
self.read_exact(&mut partition_key)?;
let partition_data = [
PartitionData {
first_sector: self.read_u32::<BigEndian>()?,
sector_count: self.read_u32::<BigEndian>()?,
group_index: self.read_u32::<BigEndian>()?,
group_count: self.read_u32::<BigEndian>()?,
},
PartitionData {
first_sector: self.read_u32::<BigEndian>()?,
sector_count: self.read_u32::<BigEndian>()?,
group_index: self.read_u32::<BigEndian>()?,
group_count: self.read_u32::<BigEndian>()?,
},
];
partitions.push(Partition {
partition_key,
partition_data,
});
}
Ok(partitions)
}
fn read_rvz_raw_datas(&mut self, disc: &Disc) -> Result<Vec<RawData>, Error> {
self.seek(SeekFrom::Start(disc.raw_data_offset))?;
let mut io = self.take(disc.raw_data_size.into());
let mut decompressor = disc.compression_io(&mut io)?;
let mut result = vec![];
for _ in 0..disc.raw_data_count {
let raw_data_offset = decompressor.read_u64::<BigEndian>()?;
let raw_data_size = decompressor.read_u64::<BigEndian>()?;
let group_index = decompressor.read_u32::<BigEndian>()?;
let group_count = decompressor.read_u32::<BigEndian>()?;
result.push(RawData {
raw_data_offset,
raw_data_size,
group_index,
group_count,
});
}
Ok(result)
}
fn read_rvz_groups(&mut self, disc: &Disc) -> Result<Vec<Group>, Error> {
self.seek(SeekFrom::Start(disc.group_offset))?;
let mut io = self.take(disc.group_size.into());
let mut decompressor = disc.compression_io(&mut io)?;
let mut result = vec![];
for _ in 0..disc.group_count {
let data_offset_div4 = decompressor.read_u32::<BigEndian>()?;
let data_size = decompressor.read_u32::<BigEndian>()?;
let packed_size = decompressor.read_u32::<BigEndian>()?;
result.push(Group {
data_offset_div4,
data_size,
packed_size,
});
}
Ok(result)
}
}
impl<T: Read> GroupExceptionRead for T {
fn read_rvz_exceptions(&mut self) -> Result<ExceptionList, io::Error> {
let exception_count = self.read_u16::<BigEndian>()?;
let mut exception_list = vec![];
for _ in 0..exception_count {
let offset = self.read_u16::<BigEndian>()?;
let mut hash = [0u8; 20];
self.read_exact(&mut hash)?;
exception_list.push(Exception { offset, hash });
}
Ok(ExceptionList(exception_list))
}
}
impl<T: Read + Seek> Rvz<T> {
pub fn new(mut io: T) -> Result<Self, Error> {
let header = io.read_rvz_header()?;
let disc = io.read_rvz_disc()?;
if disc.partition_size != 48 {
return Err(Error::Parse("unexpected RVZ disc partition size".into()));
}
let partitions = io.read_rvz_partitions(&disc)?;
let raw_data = io.read_rvz_raw_datas(&disc)?;
let groups = io.read_rvz_groups(&disc)?;
let mut regions = vec![];
for raw in &raw_data {
let start = raw.raw_data_offset & !0x7FFF;
let extra = raw.raw_data_offset & 0x7FFF;
let size = raw.raw_data_size + extra;
let end = start + size;
regions.push(Region {
range: (start..end).into(),
type_: RegionType::Raw(*raw),
});
}
for partition in &partitions {
for data in &partition.partition_data {
let start = u64::from(data.first_sector) * 0x8000;
let end = start + u64::from(data.sector_count) * 0x8000;
regions.push(Region {
range: (start..end).into(),
type_: RegionType::Partition(*partition, *data),
});
}
}
let metadata = Metadata {
header,
disc,
partitions,
raw_data,
groups,
regions,
hashes: Vec::default(),
};
#[allow(clippy::missing_panics_doc)]
let io = Io {
io,
position: 0,
group_cache: LruCache::new(32.try_into().unwrap()),
};
Ok(Self { metadata, io })
}
}
impl fmt::Display for DiscType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Unknown => write!(f, "unknown"),
Self::GameCube => write!(f, "GameCube"),
Self::Wii => write!(f, "Wii"),
}
}
}
impl fmt::Display for Compression {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::None => write!(f, "none"),
Self::Bzip2 => write!(f, "BZIP2"),
Self::Lzma => write!(f, "LZMA"),
Self::Lzma2 => write!(f, "LZMA2"),
Self::Zstd => write!(f, "ZSTD"),
}
}
}
impl TryFrom<u32> for Compression {
type Error = Error;
fn try_from(val: u32) -> Result<Self, Error> {
match val {
0 => Ok(Self::None),
2 => Ok(Self::Bzip2),
3 => Ok(Self::Lzma),
4 => Ok(Self::Lzma2),
5 => Ok(Self::Zstd),
_ => Err(Error::Parse("unknown compression found".into())),
}
}
}
impl TryFrom<u32> for DiscType {
type Error = Error;
fn try_from(val: u32) -> Result<Self, Error> {
match val {
0 => Ok(Self::Unknown),
1 => Ok(Self::GameCube),
2 => Ok(Self::Wii),
_ => Err(Error::Parse("unknown disc type found".into())),
}
}
}