use std::fs::File;
use std::io::{BufReader, Read, Seek, SeekFrom};
use std::path::Path;
use crate::error::{OpticaldiscsError, Result};
use crate::sector_reader::{SectorReader, SECTOR_SIZE};
pub const CSO_MAGIC: &[u8; 4] = b"CISO";
const CSO_NOT_COMPRESSED: u32 = 0x8000_0000;
pub struct CsoSectorReader {
file: BufReader<File>,
block_size: u32,
align: u8,
index: Vec<u32>,
total_size: u64,
num_blocks: u64,
}
impl CsoSectorReader {
pub fn open(path: impl AsRef<Path>) -> Result<Self> {
let mut file = BufReader::new(File::open(path.as_ref()).map_err(OpticaldiscsError::Io)?);
let mut head = [0u8; 24];
file.read_exact(&mut head).map_err(OpticaldiscsError::Io)?;
if &head[0..4] != CSO_MAGIC {
return Err(OpticaldiscsError::Parse("not a CISO (.cso) image".into()));
}
let total_size = u64::from_le_bytes(head[8..16].try_into().unwrap());
let block_size = u32::from_le_bytes(head[16..20].try_into().unwrap());
let version = head[20];
let align = head[21];
if version != 1 {
return Err(OpticaldiscsError::Parse(format!(
"unsupported CSO version {version} (only CISOv1 is supported)"
)));
}
if block_size as u64 != SECTOR_SIZE {
return Err(OpticaldiscsError::Parse(format!(
"unsupported CSO block size {block_size} (expected {SECTOR_SIZE})"
)));
}
if total_size == 0 || total_size % block_size as u64 != 0 {
return Err(OpticaldiscsError::Parse(
"invalid CSO total size (not a whole number of blocks)".into(),
));
}
let num_blocks = total_size / block_size as u64;
let index_entries = (num_blocks + 1) as usize;
let mut raw = vec![0u8; index_entries * 4];
file.seek(SeekFrom::Start(24))
.map_err(OpticaldiscsError::Io)?;
file.read_exact(&mut raw).map_err(OpticaldiscsError::Io)?;
let index: Vec<u32> = raw
.chunks_exact(4)
.map(|c| u32::from_le_bytes(c.try_into().unwrap()))
.collect();
Ok(Self {
file,
block_size,
align,
index,
total_size,
num_blocks,
})
}
pub fn total_size(&self) -> u64 {
self.total_size
}
fn read_block(&mut self, i: u64) -> Result<Vec<u8>> {
let bs = self.block_size as usize;
if i >= self.num_blocks {
return Ok(vec![0u8; bs]);
}
let entry = self.index[i as usize];
let next = self.index[i as usize + 1];
let plain = entry & CSO_NOT_COMPRESSED != 0;
let off = ((entry & !CSO_NOT_COMPRESSED) as u64) << self.align;
let next_off = ((next & !CSO_NOT_COMPRESSED) as u64) << self.align;
let comp_len = next_off.saturating_sub(off) as usize;
if comp_len == 0 {
return Ok(vec![0u8; bs]);
}
self.file
.seek(SeekFrom::Start(off))
.map_err(OpticaldiscsError::Io)?;
let mut buf = vec![0u8; comp_len];
self.file
.read_exact(&mut buf)
.map_err(OpticaldiscsError::Io)?;
if plain {
buf.truncate(bs);
if buf.len() < bs {
buf.resize(bs, 0);
}
return Ok(buf);
}
let mut out = vec![0u8; bs];
let mut dec = flate2::Decompress::new(false);
dec.decompress(&buf, &mut out, flate2::FlushDecompress::Finish)
.map_err(|e| OpticaldiscsError::Parse(format!("CSO block {i} inflate: {e}")))?;
Ok(out)
}
}
impl SectorReader for CsoSectorReader {
fn read_sector(&mut self, lba: u64) -> Result<Vec<u8>> {
self.read_block(lba)
}
}
#[cfg(test)]
mod tests {
use super::*;
use flate2::write::DeflateEncoder;
use flate2::Compression;
use std::io::Write;
fn build_cso(sectors: &[[u8; 2048]]) -> Vec<u8> {
let bs = 2048u32;
let n = sectors.len() as u64;
let total = n * bs as u64;
let mut header = Vec::new();
header.extend_from_slice(CSO_MAGIC);
header.extend_from_slice(&0u32.to_le_bytes()); header.extend_from_slice(&total.to_le_bytes()); header.extend_from_slice(&bs.to_le_bytes()); header.push(1); header.push(0); header.extend_from_slice(&0u16.to_le_bytes());
let index_bytes = ((n + 1) * 4) as u64;
let body_start = 24 + index_bytes;
let mut body = Vec::new();
let mut index: Vec<u32> = Vec::new();
for (i, s) in sectors.iter().enumerate() {
let off = body_start + body.len() as u64;
if i % 2 == 0 {
let mut enc = DeflateEncoder::new(Vec::new(), Compression::best());
enc.write_all(s).unwrap();
let comp = enc.finish().unwrap();
index.push(off as u32); body.extend_from_slice(&comp);
} else {
index.push(off as u32 | CSO_NOT_COMPRESSED);
body.extend_from_slice(s);
}
}
index.push((body_start + body.len() as u64) as u32);
let mut out = header;
for e in &index {
out.extend_from_slice(&e.to_le_bytes());
}
out.extend_from_slice(&body);
out
}
fn write_tmp(bytes: &[u8]) -> tempfile::NamedTempFile {
let mut f = tempfile::Builder::new().suffix(".cso").tempfile().unwrap();
f.write_all(bytes).unwrap();
f.flush().unwrap();
f
}
#[test]
fn roundtrips_compressed_and_plain_blocks() {
let mut s0 = [0u8; 2048];
let mut s1 = [0u8; 2048];
let mut s2 = [0u8; 2048];
for (i, b) in s0.iter_mut().enumerate() {
*b = (i % 251) as u8; }
s1.fill(0xAB); s2[..5].copy_from_slice(b"CD001");
let cso = build_cso(&[s0, s1, s2]);
let f = write_tmp(&cso);
let mut r = CsoSectorReader::open(f.path()).unwrap();
assert_eq!(r.total_size(), 3 * 2048);
assert_eq!(r.read_sector(0).unwrap(), s0);
assert_eq!(r.read_sector(1).unwrap(), s1);
assert_eq!(&r.read_sector(2).unwrap()[..5], b"CD001");
assert_eq!(r.read_sector(99).unwrap(), vec![0u8; 2048]);
}
#[test]
fn rejects_non_cso() {
let f = write_tmp(b"not a cso file at all, padding padding padding padding");
assert!(CsoSectorReader::open(f.path()).is_err());
}
}