mirror of https://github.com/encounter/nod-rs.git
268 lines
8.9 KiB
Rust
268 lines
8.9 KiB
Rust
use std::{
|
|
cmp::min,
|
|
io,
|
|
io::{BufReader, Read, Seek, SeekFrom},
|
|
mem::size_of,
|
|
path::Path,
|
|
};
|
|
|
|
use zerocopy::{little_endian::*, AsBytes, FromBytes, FromZeroes};
|
|
|
|
use crate::{
|
|
disc::{gcn::DiscGCN, wii::DiscWii, DiscBase, DL_DVD_SIZE, SECTOR_SIZE},
|
|
io::{nkit::NKitHeader, split::SplitFileReader, DiscIO, MagicBytes},
|
|
static_assert,
|
|
util::{
|
|
lfg::LaggedFibonacci,
|
|
reader::{read_box_slice, read_from},
|
|
},
|
|
DiscHeader, DiscMeta, Error, PartitionInfo, ReadStream, Result, ResultContext,
|
|
};
|
|
|
|
pub const CISO_MAGIC: MagicBytes = *b"CISO";
|
|
pub const CISO_MAP_SIZE: usize = SECTOR_SIZE - 8;
|
|
|
|
#[derive(Clone, Debug, PartialEq, FromBytes, FromZeroes, AsBytes)]
|
|
#[repr(C, align(4))]
|
|
struct CISOHeader {
|
|
magic: MagicBytes,
|
|
// little endian
|
|
block_size: U32,
|
|
block_present: [u8; CISO_MAP_SIZE],
|
|
}
|
|
|
|
static_assert!(size_of::<CISOHeader>() == SECTOR_SIZE);
|
|
|
|
pub struct DiscIOCISO {
|
|
inner: SplitFileReader,
|
|
header: CISOHeader,
|
|
block_map: [u16; CISO_MAP_SIZE],
|
|
nkit_header: Option<NKitHeader>,
|
|
junk_blocks: Option<Box<[u8]>>,
|
|
partitions: Vec<PartitionInfo>,
|
|
disc_num: u8,
|
|
}
|
|
|
|
impl DiscIOCISO {
|
|
pub fn new(filename: &Path) -> Result<Self> {
|
|
let mut inner = BufReader::new(SplitFileReader::new(filename)?);
|
|
|
|
// Read header
|
|
let header: CISOHeader = read_from(&mut inner).context("Reading CISO header")?;
|
|
if header.magic != CISO_MAGIC {
|
|
return Err(Error::DiscFormat("Invalid CISO magic".to_string()));
|
|
}
|
|
|
|
// Build block map
|
|
let mut block_map = [0u16; CISO_MAP_SIZE];
|
|
let mut block = 0u16;
|
|
for (presence, out) in header.block_present.iter().zip(block_map.iter_mut()) {
|
|
if *presence == 1 {
|
|
*out = block;
|
|
block += 1;
|
|
} else {
|
|
*out = u16::MAX;
|
|
}
|
|
}
|
|
let file_size = SECTOR_SIZE as u64 + block as u64 * header.block_size.get() as u64;
|
|
if file_size > inner.get_ref().len() {
|
|
return Err(Error::DiscFormat(format!(
|
|
"CISO file size mismatch: expected at least {} bytes, got {}",
|
|
file_size,
|
|
inner.get_ref().len()
|
|
)));
|
|
}
|
|
|
|
// Read NKit header if present (after CISO data)
|
|
let nkit_header = if inner.get_ref().len() > file_size + 4 {
|
|
inner.seek(SeekFrom::Start(file_size)).context("Seeking to NKit header")?;
|
|
NKitHeader::try_read_from(&mut inner)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
// Read junk data bitstream if present (after NKit header)
|
|
let junk_blocks = if nkit_header.is_some() {
|
|
let n = 1 + DL_DVD_SIZE / header.block_size.get() as u64 / 8;
|
|
Some(read_box_slice(&mut inner, n as usize).context("Reading NKit bitstream")?)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let (partitions, disc_num) = if junk_blocks.is_some() {
|
|
let mut stream: Box<dyn ReadStream> = Box::new(CISOReadStream {
|
|
inner: BufReader::new(inner.get_ref().clone()),
|
|
block_size: header.block_size.get(),
|
|
block_map,
|
|
cur_block: u16::MAX,
|
|
pos: 0,
|
|
junk_blocks: None,
|
|
partitions: vec![],
|
|
disc_num: 0,
|
|
});
|
|
let header: DiscHeader = read_from(stream.as_mut()).context("Reading disc header")?;
|
|
let disc_num = header.disc_num;
|
|
let disc_base: Box<dyn DiscBase> = if header.is_wii() {
|
|
Box::new(DiscWii::new(stream.as_mut(), header, None)?)
|
|
} else if header.is_gamecube() {
|
|
Box::new(DiscGCN::new(stream.as_mut(), header, None)?)
|
|
} else {
|
|
return Err(Error::DiscFormat(format!(
|
|
"Invalid GC/Wii magic: {:#010X}/{:#010X}",
|
|
header.gcn_magic.get(),
|
|
header.wii_magic.get()
|
|
)));
|
|
};
|
|
(disc_base.partitions(), disc_num)
|
|
} else {
|
|
(vec![], 0)
|
|
};
|
|
|
|
// Reset reader
|
|
let mut inner = inner.into_inner();
|
|
inner.reset();
|
|
Ok(Self { inner, header, block_map, nkit_header, junk_blocks, partitions, disc_num })
|
|
}
|
|
}
|
|
|
|
impl DiscIO for DiscIOCISO {
|
|
fn open(&self) -> Result<Box<dyn ReadStream>> {
|
|
Ok(Box::new(CISOReadStream {
|
|
inner: BufReader::new(self.inner.clone()),
|
|
block_size: self.header.block_size.get(),
|
|
block_map: self.block_map,
|
|
cur_block: u16::MAX,
|
|
pos: 0,
|
|
junk_blocks: self.junk_blocks.clone(),
|
|
partitions: self.partitions.clone(),
|
|
disc_num: self.disc_num,
|
|
}))
|
|
}
|
|
|
|
fn meta(&self) -> Result<DiscMeta> {
|
|
Ok(self.nkit_header.as_ref().map(DiscMeta::from).unwrap_or_default())
|
|
}
|
|
|
|
fn disc_size(&self) -> Option<u64> { self.nkit_header.as_ref().and_then(|h| h.size) }
|
|
}
|
|
|
|
struct CISOReadStream {
|
|
inner: BufReader<SplitFileReader>,
|
|
block_size: u32,
|
|
block_map: [u16; CISO_MAP_SIZE],
|
|
cur_block: u16,
|
|
pos: u64,
|
|
|
|
// Data for recreating junk data
|
|
junk_blocks: Option<Box<[u8]>>,
|
|
partitions: Vec<PartitionInfo>,
|
|
disc_num: u8,
|
|
}
|
|
|
|
impl CISOReadStream {
|
|
fn read_junk_data(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
|
let Some(junk_blocks) = self.junk_blocks.as_deref() else {
|
|
return Ok(0);
|
|
};
|
|
let block_size = self.block_size as u64;
|
|
let block = (self.pos / block_size) as u16;
|
|
if junk_blocks[(block / 8) as usize] & (1 << (7 - (block & 7))) == 0 {
|
|
return Ok(0);
|
|
}
|
|
let Some(partition) = self.partitions.iter().find(|p| {
|
|
let start = p.part_offset + p.data_offset;
|
|
start <= self.pos && self.pos < start + p.data_size
|
|
}) else {
|
|
log::warn!("No partition found for junk data at offset {:#x}", self.pos);
|
|
return Ok(0);
|
|
};
|
|
let offset = self.pos - (partition.part_offset + partition.data_offset);
|
|
let to_read = min(
|
|
buf.len(),
|
|
// The LFG is only valid for a single sector
|
|
SECTOR_SIZE - (offset % SECTOR_SIZE as u64) as usize,
|
|
);
|
|
let mut lfg = LaggedFibonacci::default();
|
|
lfg.init_with_seed(partition.lfg_seed, self.disc_num, offset);
|
|
lfg.fill(&mut buf[..to_read]);
|
|
self.pos += to_read as u64;
|
|
Ok(to_read)
|
|
}
|
|
}
|
|
|
|
impl Read for CISOReadStream {
|
|
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
|
let block_size = self.block_size as u64;
|
|
let block = (self.pos / block_size) as u16;
|
|
let block_offset = self.pos & (block_size - 1);
|
|
if block != self.cur_block {
|
|
if block >= CISO_MAP_SIZE as u16 {
|
|
return Ok(0);
|
|
}
|
|
|
|
// Find the block in the map
|
|
let phys_block = self.block_map[block as usize];
|
|
if phys_block == u16::MAX {
|
|
// Try to recreate junk data
|
|
let read = self.read_junk_data(buf)?;
|
|
if read > 0 {
|
|
return Ok(read);
|
|
}
|
|
|
|
// Otherwise, read zeroes
|
|
let to_read = min(buf.len(), (block_size - block_offset) as usize);
|
|
buf[..to_read].fill(0);
|
|
self.pos += to_read as u64;
|
|
return Ok(to_read);
|
|
}
|
|
|
|
// Seek to the new block
|
|
let file_offset =
|
|
size_of::<CISOHeader>() as u64 + phys_block as u64 * block_size + block_offset;
|
|
self.inner.seek(SeekFrom::Start(file_offset))?;
|
|
self.cur_block = block;
|
|
}
|
|
|
|
let to_read = min(buf.len(), (block_size - block_offset) as usize);
|
|
let read = self.inner.read(&mut buf[..to_read])?;
|
|
self.pos += read as u64;
|
|
Ok(read)
|
|
}
|
|
}
|
|
|
|
impl Seek for CISOReadStream {
|
|
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
|
let new_pos = match pos {
|
|
SeekFrom::Start(v) => v,
|
|
SeekFrom::End(_) => {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::Unsupported,
|
|
"CISOReadStream: SeekFrom::End is not supported",
|
|
));
|
|
}
|
|
SeekFrom::Current(v) => self.pos.saturating_add_signed(v),
|
|
};
|
|
|
|
let block_size = self.block_size as u64;
|
|
let new_block = (self.pos / block_size) as u16;
|
|
if new_block == self.cur_block {
|
|
// Seek within the same block
|
|
self.inner.seek(SeekFrom::Current(new_pos as i64 - self.pos as i64))?;
|
|
} else {
|
|
// Seek to a different block, handled by next read
|
|
self.cur_block = u16::MAX;
|
|
}
|
|
|
|
self.pos = new_pos;
|
|
Ok(new_pos)
|
|
}
|
|
}
|
|
|
|
impl ReadStream for CISOReadStream {
|
|
fn stable_stream_len(&mut self) -> io::Result<u64> {
|
|
Ok(self.block_size as u64 * CISO_MAP_SIZE as u64)
|
|
}
|
|
|
|
fn as_dyn(&mut self) -> &mut dyn ReadStream { self }
|
|
}
|