use std::{fs, io, io::Read, path::Path};
use dyn_clone::DynClone;
use crate::{
Error, Result, ResultContext,
common::{Format, KeyBytes, MagicBytes, PartitionInfo},
disc::{
DiscHeader, GCN_MAGIC, SECTOR_SIZE, WII_MAGIC,
wii::{HASHES_SIZE, SECTOR_DATA_SIZE},
},
io::{
split::SplitFileReader,
wia::{WIAException, WIAExceptionList},
},
read::{DiscMeta, DiscStream},
util::{
aes::decrypt_sector,
array_ref, array_ref_mut,
lfg::LaggedFibonacci,
read::{read_at, read_from},
},
};
pub trait BlockReader: DynClone + Send {
fn read_block(&mut self, out: &mut [u8], sector: u32) -> io::Result<Block>;
fn block_size(&self) -> u32;
fn meta(&self) -> DiscMeta;
}
dyn_clone::clone_trait_object!(BlockReader);
pub fn new(mut stream: Box<dyn DiscStream>) -> Result<Box<dyn BlockReader>> {
let io: Box<dyn BlockReader> =
match detect_stream(stream.as_mut()).context("Detecting file type")? {
Some(Format::Iso) => crate::io::iso::BlockReaderISO::new(stream)?,
Some(Format::Ciso) => crate::io::ciso::BlockReaderCISO::new(stream)?,
Some(Format::Gcz) => {
#[cfg(feature = "compress-zlib")]
{
crate::io::gcz::BlockReaderGCZ::new(stream)?
}
#[cfg(not(feature = "compress-zlib"))]
return Err(Error::DiscFormat("GCZ support is disabled".to_string()));
}
Some(Format::Nfs) => {
return Err(Error::DiscFormat("NFS requires a filesystem path".to_string()));
}
Some(Format::Wbfs) => crate::io::wbfs::BlockReaderWBFS::new(stream)?,
Some(Format::Wia | Format::Rvz) => crate::io::wia::BlockReaderWIA::new(stream)?,
Some(Format::Tgc) => crate::io::tgc::BlockReaderTGC::new(stream)?,
None => return Err(Error::DiscFormat("Unknown disc format".to_string())),
};
check_block_size(io.as_ref())?;
Ok(io)
}
pub fn open(filename: &Path) -> Result<Box<dyn BlockReader>> {
let path_result = fs::canonicalize(filename);
if let Err(err) = path_result {
return Err(Error::Io(format!("Failed to open {}", filename.display()), err));
}
let path = path_result.as_ref().unwrap();
let meta = fs::metadata(path);
if let Err(err) = meta {
return Err(Error::Io(format!("Failed to open {}", filename.display()), err));
}
if !meta.unwrap().is_file() {
return Err(Error::DiscFormat(format!("Input is not a file: {}", filename.display())));
}
let mut stream = Box::new(SplitFileReader::new(filename)?);
let io: Box<dyn BlockReader> = match detect_stream(stream.as_mut())
.context("Detecting file type")?
{
Some(Format::Iso) => crate::io::iso::BlockReaderISO::new(stream)?,
Some(Format::Ciso) => crate::io::ciso::BlockReaderCISO::new(stream)?,
Some(Format::Gcz) => {
#[cfg(feature = "compress-zlib")]
{
crate::io::gcz::BlockReaderGCZ::new(stream)?
}
#[cfg(not(feature = "compress-zlib"))]
return Err(Error::DiscFormat("GCZ support is disabled".to_string()));
}
Some(Format::Nfs) => match path.parent() {
Some(parent) if parent.is_dir() => {
crate::io::nfs::BlockReaderNFS::new(path.parent().unwrap())?
}
_ => {
return Err(Error::DiscFormat("Failed to locate NFS parent directory".to_string()));
}
},
Some(Format::Tgc) => crate::io::tgc::BlockReaderTGC::new(stream)?,
Some(Format::Wbfs) => crate::io::wbfs::BlockReaderWBFS::new(stream)?,
Some(Format::Wia | Format::Rvz) => crate::io::wia::BlockReaderWIA::new(stream)?,
None => return Err(Error::DiscFormat("Unknown disc format".to_string())),
};
check_block_size(io.as_ref())?;
Ok(io)
}
pub const CISO_MAGIC: MagicBytes = *b"CISO";
pub const GCZ_MAGIC: MagicBytes = [0x01, 0xC0, 0x0B, 0xB1];
pub const NFS_MAGIC: MagicBytes = *b"EGGS";
pub const TGC_MAGIC: MagicBytes = [0xAE, 0x0F, 0x38, 0xA2];
pub const WBFS_MAGIC: MagicBytes = *b"WBFS";
pub const WIA_MAGIC: MagicBytes = *b"WIA\x01";
pub const RVZ_MAGIC: MagicBytes = *b"RVZ\x01";
pub fn detect<R>(stream: &mut R) -> io::Result<Option<Format>>
where R: Read + ?Sized {
match read_from(stream) {
Ok(ref magic) => Ok(detect_internal(magic)),
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => Ok(None),
Err(e) => Err(e),
}
}
fn detect_stream(stream: &mut dyn DiscStream) -> io::Result<Option<Format>> {
match read_at(stream, 0) {
Ok(ref magic) => Ok(detect_internal(magic)),
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => Ok(None),
Err(e) => Err(e),
}
}
fn detect_internal(data: &[u8; 0x20]) -> Option<Format> {
match *array_ref!(data, 0, 4) {
CISO_MAGIC => Some(Format::Ciso),
GCZ_MAGIC => Some(Format::Gcz),
NFS_MAGIC => Some(Format::Nfs),
TGC_MAGIC => Some(Format::Tgc),
WBFS_MAGIC => Some(Format::Wbfs),
WIA_MAGIC => Some(Format::Wia),
RVZ_MAGIC => Some(Format::Rvz),
_ if *array_ref!(data, 0x18, 4) == WII_MAGIC || *array_ref!(data, 0x1C, 4) == GCN_MAGIC => {
Some(Format::Iso)
}
_ => None,
}
}
fn check_block_size(io: &dyn BlockReader) -> Result<()> {
if io.block_size() % SECTOR_SIZE as u32 != 0 {
return Err(Error::DiscFormat(format!(
"Block size {} is not a multiple of sector size {}",
io.block_size(),
SECTOR_SIZE
)));
}
Ok(())
}
#[derive(Debug, Clone, Default)]
pub struct Block {
pub sector: u32,
pub count: u32,
pub kind: BlockKind,
pub hash_exceptions: Box<[WIAExceptionList]>,
pub io_duration: Option<std::time::Duration>,
}
impl Block {
#[inline]
pub fn new(block_idx: u32, block_size: u32, kind: BlockKind) -> Self {
let sectors_per_block = block_size / SECTOR_SIZE as u32;
Self {
sector: block_idx * sectors_per_block,
count: sectors_per_block,
kind,
hash_exceptions: Default::default(),
io_duration: None,
}
}
#[inline]
pub fn sector(sector: u32, kind: BlockKind) -> Self {
Self { sector, count: 1, kind, hash_exceptions: Default::default(), io_duration: None }
}
#[inline]
pub fn sectors(sector: u32, count: u32, kind: BlockKind) -> Self {
Self { sector, count, kind, hash_exceptions: Default::default(), io_duration: None }
}
#[inline]
pub fn contains(&self, sector: u32) -> bool {
sector >= self.sector && sector < self.sector + self.count
}
pub fn ensure_contains(&self, sector: u32) -> io::Result<()> {
if !self.contains(sector) {
return Err(io::Error::other(format!(
"Sector {} not in block range {}-{}",
sector,
self.sector,
self.sector + self.count
)));
}
Ok(())
}
pub(crate) fn decrypt_block(&self, data: &mut [u8], key: Option<KeyBytes>) -> io::Result<()> {
match self.kind {
BlockKind::None => {}
BlockKind::Raw => {
if let Some(key) = key {
for i in 0..self.count as usize {
decrypt_sector(array_ref_mut![data, i * SECTOR_SIZE, SECTOR_SIZE], &key);
}
}
}
BlockKind::PartDecrypted { .. } => {
}
BlockKind::Junk => {
data.fill(0);
}
BlockKind::Zero => data.fill(0),
}
Ok(())
}
pub(crate) fn copy_sector(
&self,
out: &mut [u8; SECTOR_SIZE],
data: &[u8],
abs_sector: u32,
disc_header: &DiscHeader,
partition: Option<&PartitionInfo>,
) -> io::Result<(bool, bool)> {
let mut encrypted = false;
let mut has_hashes = false;
match self.kind {
BlockKind::None => {}
BlockKind::Raw => {
*out = *self.sector_buf(data, abs_sector)?;
if partition.is_some_and(|p| p.has_encryption) {
encrypted = true;
}
if partition.is_some_and(|p| p.has_hashes) {
has_hashes = true;
}
}
BlockKind::PartDecrypted { hash_block } => {
if hash_block {
*out = *self.sector_buf(data, abs_sector)?;
has_hashes = partition.is_some_and(|p| p.has_hashes);
} else {
*array_ref_mut![out, HASHES_SIZE, SECTOR_DATA_SIZE] =
*self.sector_data_buf(data, abs_sector)?;
}
}
BlockKind::Junk => generate_junk_sector(out, abs_sector, partition, disc_header),
BlockKind::Zero => out.fill(0),
}
Ok((encrypted, has_hashes))
}
pub(crate) fn sector_buf<'a>(
&self,
data: &'a [u8],
abs_sector: u32,
) -> io::Result<&'a [u8; SECTOR_SIZE]> {
self.ensure_contains(abs_sector)?;
let block_offset = ((abs_sector - self.sector) * SECTOR_SIZE as u32) as usize;
Ok(array_ref!(data, block_offset, SECTOR_SIZE))
}
pub(crate) fn sector_data_buf<'a>(
&self,
data: &'a [u8],
abs_sector: u32,
) -> io::Result<&'a [u8; SECTOR_DATA_SIZE]> {
self.ensure_contains(abs_sector)?;
let block_offset = ((abs_sector - self.sector) * SECTOR_DATA_SIZE as u32) as usize;
Ok(array_ref!(data, block_offset, SECTOR_DATA_SIZE))
}
pub(crate) fn data<'a>(&self, data: &'a [u8], pos: u64) -> io::Result<&'a [u8]> {
if self.kind == BlockKind::None {
return Ok(&[]);
}
self.ensure_contains((pos / SECTOR_SIZE as u64) as u32)?;
let offset = (pos - self.sector as u64 * SECTOR_SIZE as u64) as usize;
let end = self.count as usize * SECTOR_SIZE;
Ok(&data[offset..end])
}
pub(crate) fn partition_data<'a>(
&self,
data: &'a [u8],
pos: u64,
data_start_sector: u32,
partition_has_hashes: bool,
) -> io::Result<&'a [u8]> {
let block_has_hashes = match self.kind {
BlockKind::Raw => partition_has_hashes,
BlockKind::PartDecrypted { hash_block, .. } => hash_block && partition_has_hashes,
BlockKind::Junk | BlockKind::Zero => false,
BlockKind::None => return Ok(&[]),
};
let (part_sector, sector_offset) = if partition_has_hashes {
((pos / SECTOR_DATA_SIZE as u64) as u32, (pos % SECTOR_DATA_SIZE as u64) as usize)
} else {
((pos / SECTOR_SIZE as u64) as u32, (pos % SECTOR_SIZE as u64) as usize)
};
let abs_sector = part_sector + data_start_sector;
self.ensure_contains(abs_sector)?;
let block_sector = (abs_sector - self.sector) as usize;
if block_has_hashes {
let offset = block_sector * SECTOR_SIZE + HASHES_SIZE + sector_offset;
let end = (block_sector + 1) * SECTOR_SIZE; Ok(&data[offset..end])
} else if partition_has_hashes {
let offset = block_sector * SECTOR_DATA_SIZE + sector_offset;
let end = self.count as usize * SECTOR_DATA_SIZE; Ok(&data[offset..end])
} else {
let offset = block_sector * SECTOR_SIZE + sector_offset;
let end = self.count as usize * SECTOR_SIZE; Ok(&data[offset..end])
}
}
pub(crate) fn append_hash_exceptions(
&self,
abs_sector: u32,
group_sector: u32,
out: &mut Vec<WIAException>,
) -> io::Result<()> {
self.ensure_contains(abs_sector)?;
let block_sector = abs_sector - self.sector;
let group = (block_sector / 64) as usize;
let base_offset = ((block_sector % 64) as usize * HASHES_SIZE) as u16;
let new_base_offset = (group_sector * HASHES_SIZE as u32) as u16;
out.extend(self.hash_exceptions.get(group).iter().flat_map(|list| {
list.iter().filter_map(|exception| {
let offset = exception.offset.get();
if offset >= base_offset && offset < base_offset + HASHES_SIZE as u16 {
let new_offset = (offset - base_offset) + new_base_offset;
Some(WIAException { offset: new_offset.into(), hash: exception.hash })
} else {
None
}
})
}));
Ok(())
}
}
#[derive(Debug, Copy, Clone, PartialEq, Default)]
pub enum BlockKind {
#[default]
None,
Raw,
PartDecrypted {
hash_block: bool,
},
Junk,
Zero,
}
pub fn generate_junk_sector(
out: &mut [u8; SECTOR_SIZE],
abs_sector: u32,
partition: Option<&PartitionInfo>,
disc_header: &DiscHeader,
) {
let (pos, offset) = if partition.is_some_and(|p| p.has_hashes) {
let sector = abs_sector - partition.unwrap().data_start_sector;
(sector as u64 * SECTOR_DATA_SIZE as u64, HASHES_SIZE)
} else {
(abs_sector as u64 * SECTOR_SIZE as u64, 0)
};
out[..offset].fill(0);
let mut lfg = LaggedFibonacci::default();
lfg.fill_sector_chunked(
&mut out[offset..],
*array_ref![disc_header.game_id, 0, 4],
disc_header.disc_num,
pos,
);
}