use crate::error::DiscError;
use crate::mds::{MdsDisc, MDS_HEADER_MIN_SIZE, MDS_MAGIC};
use crate::types::{CdSubcodeType, CdToc, CdTrack, CdTrackType, DiscType};
use aes::cipher::{BlockDecrypt, KeyInit};
use aes::Aes256;
use chdlady_core::ChdError;
use flate2::read::ZlibDecoder;
use flate2::Decompress;
use ripemd::Ripemd160;
use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use std::path::Path;
const EDC_CRCTABLE: [u32; 256] = [
0x00000000, 0x90910101, 0x91210201, 0x01B00300, 0x92410401, 0x02D00500, 0x03600600, 0x93F10701,
0x94810801, 0x04100900, 0x05A00A00, 0x95310B01, 0x06C00C00, 0x96510D01, 0x97E10E01, 0x07700F00,
0x99011001, 0x09901100, 0x08201200, 0x98B11301, 0x0B401400, 0x9BD11501, 0x9A611601, 0x0AF01700,
0x0D801800, 0x9D111901, 0x9CA11A01, 0x0C301B00, 0x9FC11C01, 0x0F501D00, 0x0EE01E00, 0x9E711F01,
0x82012001, 0x12902100, 0x13202200, 0x83B12301, 0x10402400, 0x80D12501, 0x81612601, 0x11F02700,
0x16802800, 0x86112901, 0x87A12A01, 0x17302B00, 0x84C12C01, 0x14502D00, 0x15E02E00, 0x85712F01,
0x1B003000, 0x8B913101, 0x8A213201, 0x1AB03300, 0x89413401, 0x19D03500, 0x18603600, 0x88F13701,
0x8F813801, 0x1F103900, 0x1EA03A00, 0x8E313B01, 0x1DC03C00, 0x8D513D01, 0x8CE13E01, 0x1C703F00,
0xB4014001, 0x24904100, 0x25204200, 0xB5B14301, 0x26404400, 0xB6D14501, 0xB7614601, 0x27F04700,
0x20804800, 0xB0114901, 0xB1A14A01, 0x21304B00, 0xB2C14C01, 0x22504D00, 0x23E04E00, 0xB3714F01,
0x2D005000, 0xBD915101, 0xBC215201, 0x2CB05300, 0xBF415401, 0x2FD05500, 0x2E605600, 0xBEF15701,
0xB9815801, 0x29105900, 0x28A05A00, 0xB8315B01, 0x2BC05C00, 0xBB515D01, 0xBAE15E01, 0x2A705F00,
0x36006000, 0xA6916101, 0xA7216201, 0x37B06300, 0xA4416401, 0x34D06500, 0x35606600, 0xA5F16701,
0xA2816801, 0x32106900, 0x33A06A00, 0xA3316B01, 0x30C06C00, 0xA0516D01, 0xA1E16E01, 0x31706F00,
0xAF017001, 0x3F907100, 0x3E207200, 0xAEB17301, 0x3D407400, 0xADD17501, 0xAC617601, 0x3CF07700,
0x3B807800, 0xAB117901, 0xAAA17A01, 0x3A307B00, 0xA9C17C01, 0x39507D00, 0x38E07E00, 0xA8717F01,
0xD8018001, 0x48908100, 0x49208200, 0xD9B18301, 0x4A408400, 0xDAD18501, 0xDB618601, 0x4BF08700,
0x4C808800, 0xDC118901, 0xDDA18A01, 0x4D308B00, 0xDEC18C01, 0x4E508D00, 0x4FE08E00, 0xDF718F01,
0x41009000, 0xD1919101, 0xD0219201, 0x40B09300, 0xD3419401, 0x43D09500, 0x42609600, 0xD2F19701,
0xD5819801, 0x45109900, 0x44A09A00, 0xD4319B01, 0x47C09C00, 0xD7519D01, 0xD6E19E01, 0x46709F00,
0x5A00A000, 0xCA91A101, 0xCB21A201, 0x5BB0A300, 0xC841A401, 0x58D0A500, 0x5960A600, 0xC9F1A701,
0xCE81A801, 0x5E10A900, 0x5FA0AA00, 0xCF31AB01, 0x5CC0AC00, 0xCC51AD01, 0xCDE1AE01, 0x5D70AF00,
0xC301B001, 0x5390B100, 0x5220B200, 0xC2B1B301, 0x5140B400, 0xC1D1B501, 0xC061B601, 0x50F0B700,
0x5780B800, 0xC711B901, 0xC6A1BA01, 0x5630BB00, 0xC5C1BC01, 0x5550BD00, 0x54E0BE00, 0xC471BF01,
0x6C00C000, 0xFC91C101, 0xFD21C201, 0x6DB0C300, 0xFE41C401, 0x6ED0C500, 0x6F60C600, 0xFFF1C701,
0xF881C801, 0x6810C900, 0x69A0CA00, 0xF931CB01, 0x6AC0CC00, 0xFA51CD01, 0xFBE1CE01, 0x6B70CF00,
0xF501D001, 0x6590D100, 0x6420D200, 0xF4B1D301, 0x6740D400, 0xF7D1D501, 0xF661D601, 0x66F0D700,
0x6180D800, 0xF111D901, 0xF0A1DA01, 0x6030DB00, 0xF3C1DC01, 0x6350DD00, 0x62E0DE00, 0xF271DF01,
0xEE01E001, 0x7E90E100, 0x7F20E200, 0xEFB1E301, 0x7C40E400, 0xECD1E501, 0xED61E601, 0x7DF0E700,
0x7A80E800, 0xEA11E901, 0xEBA1EA01, 0x7B30EB00, 0xE8C1EC01, 0x7850ED00, 0x79E0EE00, 0xE971EF01,
0x7700F000, 0xE791F101, 0xE621F201, 0x76B0F300, 0xE541F401, 0x75D0F500, 0x7460F600, 0xE4F1F701,
0xE381F801, 0x7310F900, 0x72A0FA00, 0xE231FB01, 0x71C0FC00, 0xE151FD01, 0xE0E1FE01, 0x7070FF00,
];
fn get_edc(data: &[u8]) -> u32 {
let mut res = 0u32;
for &b in data {
res = EDC_CRCTABLE[((res ^ (b as u32)) & 0xff) as usize] ^ (res >> 8);
}
res
}
fn derive_password(salt: &[u8; 64]) -> [u8; 64] {
let mut pwd = *salt;
let mut modifier = get_edc(&pwd) ^ 0x5673_72ff;
for i in (0..64).step_by(4) {
modifier = modifier.wrapping_mul(0x35e8_5a6d).wrapping_add(0x1548_dce9);
let val = u32::from_le_bytes(pwd[i..i + 4].try_into().expect("fixed-size slice"));
let transformed = val ^ modifier ^ 0xec56_4717;
pwd[i..i + 4].copy_from_slice(&transformed.to_le_bytes().map(|b| if b == 0 { b'_' } else { b }));
}
pwd
}
fn cbc_dewhiten_decrypt(
data: &[u8],
key: &[u8; 32],
iv_bytes: &[u8; 16],
) -> Result<Vec<u8>, DiscError> {
if !data.len().is_multiple_of(16) {
return Err(DiscError::InvalidMds(
"data length must be a multiple of 16 for AES decryption".to_string(),
));
}
let cipher = Aes256::new_from_slice(key)
.map_err(|e| DiscError::InvalidMds(format!("AES cipher error: {e}")))?;
let iv0 = u64::from_le_bytes(iv_bytes[0..8].try_into().expect("fixed-size slice"));
let iv1 = u64::from_le_bytes(iv_bytes[8..16].try_into().expect("fixed-size slice"));
let mut buf_iv0 = iv0;
let mut buf_iv1 = iv1;
let mut out = vec![0u8; data.len()];
for i in (0..data.len()).step_by(16) {
let d0 = u64::from_le_bytes(data[i..i + 8].try_into().expect("fixed-size slice")) ^ iv1;
let d1 = u64::from_le_bytes(data[i + 8..i + 16].try_into().expect("fixed-size slice")) ^ iv1;
let mut block = aes::Block::default();
block[0..8].copy_from_slice(&d0.to_le_bytes());
block[8..16].copy_from_slice(&d1.to_le_bytes());
cipher.decrypt_block(&mut block);
let dec0 = u64::from_le_bytes(block[0..8].try_into().expect("fixed-size slice")) ^ buf_iv0;
let dec1 = u64::from_le_bytes(block[8..16].try_into().expect("fixed-size slice")) ^ buf_iv1;
buf_iv0 = d0;
buf_iv1 = d1;
out[i..i + 8].copy_from_slice(&dec0.to_le_bytes());
out[i + 8..i + 16].copy_from_slice(&dec1.to_le_bytes());
}
Ok(out)
}
fn decrypt_and_decompress_v2_descriptor<R: Read + Seek>(
reader: &mut R,
mdx_footer_offset: u64,
mdx_footer_length: u64,
file_header_18: &[u8; 18],
) -> Result<MdsDisc, DiscError> {
if mdx_footer_length < 64 {
return Err(DiscError::InvalidMds(
"invalid MDX footer length".to_string(),
));
}
let total_file_len = reader.seek(SeekFrom::End(0))?;
let enc_hdr_offset = mdx_footer_offset
.checked_add(mdx_footer_length)
.and_then(|sum| sum.checked_sub(64))
.ok_or_else(|| DiscError::InvalidMds("encryption header offset overflow".to_string()))?;
if enc_hdr_offset.saturating_add(512) > total_file_len {
return Err(DiscError::InvalidMds(
"encryption header exceeds container file length".to_string(),
));
}
reader.seek(SeekFrom::Start(enc_hdr_offset))?;
let mut enc_hdr_raw = [0u8; 512];
reader.read_exact(&mut enc_hdr_raw)?;
let mut salt = [0u8; 64];
salt.copy_from_slice(&enc_hdr_raw[..64]);
let enc_part = &enc_hdr_raw[64..];
let pwd = derive_password(&salt);
let mut master_key = [0u8; 152];
pbkdf2::pbkdf2_hmac::<Ripemd160>(&pwd, &salt, 2000, &mut master_key);
let mut iv_bytes = [0u8; 16];
iv_bytes.copy_from_slice(&master_key[0..16]);
let mut key_bytes = [0u8; 32];
key_bytes.copy_from_slice(&master_key[32..64]);
let dec_payload = cbc_dewhiten_decrypt(enc_part, &key_bytes, &iv_bytes)?;
let key_data_checksum = u32::from_le_bytes(dec_payload[0..4].try_into().expect("fixed-size slice"));
let magic = u32::from_le_bytes(dec_payload[4..8].try_into().expect("fixed-size slice"));
if magic != 0x5452_5545 {
return Err(DiscError::InvalidMds(format!(
"invalid MDX encryption header magic: {:#010x}",
magic
)));
}
let key_data = &dec_payload[16..16 + 256];
let mut crc = flate2::Crc::new();
crc.update(key_data);
if crc.sum() != key_data_checksum {
return Err(DiscError::InvalidMds(
"MDX key data checksum mismatch".to_string(),
));
}
let comp_sz = u32::from_le_bytes(dec_payload[272..276].try_into().expect("fixed-size slice")) as usize;
let descriptor_offset = mdx_footer_offset;
let descriptor_size_u64 = mdx_footer_length - 64;
if descriptor_size_u64 > 16 * 1024 * 1024 || descriptor_size_u64 > total_file_len {
return Err(DiscError::InvalidMds(
"descriptor size exceeds allowable limit".to_string(),
));
}
if !descriptor_size_u64.is_multiple_of(16) {
return Err(DiscError::InvalidMds(
"descriptor size must be a multiple of 16 for decryption".to_string(),
));
}
let descriptor_size = descriptor_size_u64 as usize;
let mut desc_raw = vec![0u8; descriptor_size];
reader.seek(SeekFrom::Start(descriptor_offset))?;
reader.read_exact(&mut desc_raw)?;
let mut desc_iv = [0u8; 16];
desc_iv.copy_from_slice(&key_data[0..16]);
let mut desc_key = [0u8; 32];
desc_key.copy_from_slice(&key_data[32..64]);
let mut dec_desc = Vec::with_capacity(desc_raw.len());
for chunk in desc_raw.chunks(512) {
let dec_chunk = cbc_dewhiten_decrypt(chunk, &desc_key, &desc_iv)?;
dec_desc.extend_from_slice(&dec_chunk);
}
if comp_sz > dec_desc.len() {
return Err(DiscError::InvalidMds(
"compressed descriptor exceeds decrypted buffer".to_string(),
));
}
let mut decompressed = Vec::new();
ZlibDecoder::new(&dec_desc[..comp_sz]).read_to_end(&mut decompressed)?;
let mut full_mds = Vec::with_capacity(18 + decompressed.len());
full_mds.extend_from_slice(file_header_18);
full_mds.extend_from_slice(&decompressed);
MdsDisc::parse(&full_mds)
}
#[derive(Debug, Clone, Copy)]
pub struct BatEntry {
pub file_offset: u64,
pub comp_size: u16,
pub is_rle: bool,
pub rle_byte: u8,
}
#[derive(Debug)]
pub struct MdxCompressedReader<R: Read + Seek> {
inner: R,
bat: Vec<BatEntry>,
group_size: usize,
total_bytes: u64,
current_pos: u64,
cached_group: Option<usize>,
cache_buf: Vec<u8>,
comp_buf: Vec<u8>,
}
impl<R: Read + Seek> Seek for MdxCompressedReader<R> {
fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
let new_pos = match pos {
SeekFrom::Start(offset) => offset as i64,
SeekFrom::Current(offset) => (self.current_pos as i64) + offset,
SeekFrom::End(offset) => (self.total_bytes as i64) + offset,
};
if new_pos < 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"cannot seek to negative position in MDX compressed reader",
));
}
self.current_pos = (new_pos as u64).min(self.total_bytes);
Ok(self.current_pos)
}
}
impl<R: Read + Seek> Read for MdxCompressedReader<R> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
if buf.is_empty() || self.current_pos >= self.total_bytes {
return Ok(0);
}
let mut bytes_read = 0;
while bytes_read < buf.len() && self.current_pos < self.total_bytes {
let group_idx = (self.current_pos / self.group_size as u64) as usize;
let offset_in_group = (self.current_pos % self.group_size as u64) as usize;
if self.cached_group != Some(group_idx) {
if group_idx >= self.bat.len() {
break;
}
let entry = &self.bat[group_idx];
self.cache_buf.resize(self.group_size, 0);
if entry.comp_size == 0 && !entry.is_rle {
self.inner.seek(SeekFrom::Start(entry.file_offset))?;
self.inner.read_exact(&mut self.cache_buf)?;
} else if entry.is_rle {
self.cache_buf.fill(entry.rle_byte);
} else {
self.comp_buf.resize(entry.comp_size as usize, 0);
self.inner.seek(SeekFrom::Start(entry.file_offset))?;
self.inner.read_exact(&mut self.comp_buf)?;
let mut decompressor = Decompress::new(false);
if let Err(e) = decompressor.decompress(
&self.comp_buf,
&mut self.cache_buf,
flate2::FlushDecompress::Finish,
) {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("MDX raw deflate decompression error: {e}"),
));
}
}
self.cached_group = Some(group_idx);
}
let available_in_group = self.group_size - offset_in_group;
let remaining_in_stream = (self.total_bytes - self.current_pos) as usize;
let want = buf.len() - bytes_read;
let to_copy = want.min(available_in_group).min(remaining_in_stream);
buf[bytes_read..bytes_read + to_copy]
.copy_from_slice(&self.cache_buf[offset_in_group..offset_in_group + to_copy]);
bytes_read += to_copy;
self.current_pos += to_copy as u64;
}
Ok(bytes_read)
}
}
#[derive(Debug)]
pub enum MdxSectorStream<R: Read + Seek> {
Raw {
inner: R,
data_offset: u64,
data_len: u64,
},
Compressed(MdxCompressedReader<R>),
}
impl<R: Read + Seek> MdxSectorStream<R> {
pub fn new(inner: R, data_offset: u64, data_len: u64) -> Self {
Self::Raw {
inner,
data_offset,
data_len,
}
}
#[inline]
pub fn data_offset(&self) -> u64 {
match self {
Self::Raw { data_offset, .. } => *data_offset,
Self::Compressed(_) => 0,
}
}
#[inline]
pub fn data_len(&self) -> u64 {
match self {
Self::Raw { data_len, .. } => *data_len,
Self::Compressed(s) => s.total_bytes,
}
}
pub fn into_inner(self) -> R {
match self {
Self::Raw { inner, .. } => inner,
Self::Compressed(s) => s.inner,
}
}
pub fn open(reader: R) -> Result<Self, DiscError> {
let (_, stream) = MdxDisc::parse(reader)?;
Ok(stream)
}
}
impl<R: Read + Seek> Read for MdxSectorStream<R> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
match self {
Self::Raw { inner, .. } => inner.read(buf),
Self::Compressed(s) => s.read(buf),
}
}
}
impl<R: Read + Seek> Seek for MdxSectorStream<R> {
fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
match self {
Self::Raw {
inner,
data_offset,
data_len,
} => {
let adjusted_pos = match pos {
SeekFrom::Start(offset) => {
let target = if offset >= *data_offset {
offset
} else {
data_offset.saturating_add(offset)
};
SeekFrom::Start(target)
}
SeekFrom::Current(offset) => SeekFrom::Current(offset),
SeekFrom::End(offset) => {
let abs_end = data_offset.saturating_add(*data_len);
let target = (abs_end as i64 + offset).max(*data_offset as i64) as u64;
SeekFrom::Start(target)
}
};
let new_inner_pos = inner.seek(adjusted_pos)?;
Ok(new_inner_pos.saturating_sub(*data_offset))
}
Self::Compressed(s) => s.seek(pos),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MdxDisc {
pub mds: MdsDisc,
pub data_offset: u64,
pub data_len: u64,
}
impl MdxDisc {
pub fn parse<R: Read + Seek>(mut reader: R) -> Result<(Self, MdxSectorStream<R>), DiscError> {
let total_file_len = reader.seek(SeekFrom::End(0))?;
if total_file_len < MDS_HEADER_MIN_SIZE as u64 {
return Err(DiscError::InvalidMds(format!(
"file too short for MDX: {} bytes (minimum {})",
total_file_len, MDS_HEADER_MIN_SIZE
)));
}
reader.seek(SeekFrom::Start(0))?;
let mut magic = [0u8; 16];
reader.read_exact(&mut magic)?;
if &magic != MDS_MAGIC {
return Err(DiscError::InvalidMds(
"invalid signature: missing 'MEDIA DESCRIPTOR' magic".to_string(),
));
}
reader.seek(SeekFrom::Start(0))?;
let mut initial_hdr = [0u8; 0x60];
let bytes_read = reader.read(&mut initial_hdr)?;
let is_v2_encrypted = if bytes_read >= 0x40 {
let version_major = initial_hdr[16];
let offset_check = u32::from_le_bytes([
initial_hdr[0x2c],
initial_hdr[0x2d],
initial_hdr[0x2e],
initial_hdr[0x2f],
]);
version_major >= 2 && offset_check == 0xffff_ffff
} else {
false
};
let mds = if is_v2_encrypted {
let mdx_footer_offset = u64::from_le_bytes(initial_hdr[0x30..0x38].try_into().expect("fixed-size slice"));
let mdx_footer_length = u64::from_le_bytes(initial_hdr[0x38..0x40].try_into().expect("fixed-size slice"));
let mut file_hdr_18 = [0u8; 18];
file_hdr_18.copy_from_slice(&initial_hdr[0..18]);
decrypt_and_decompress_v2_descriptor(
&mut reader,
mdx_footer_offset,
mdx_footer_length,
&file_hdr_18,
)?
} else {
if bytes_read >= 0x5c {
let enc_offset = u32::from_le_bytes([
initial_hdr[0x58],
initial_hdr[0x59],
initial_hdr[0x5a],
initial_hdr[0x5b],
]);
if enc_offset > 0 {
return Err(DiscError::InvalidMds(
"password-protected or encrypted MDX images are not supported".to_string(),
));
}
}
let descriptor_read_limit = (total_file_len as usize).min(64 * 1024);
let mut descriptor_buf = vec![0u8; descriptor_read_limit];
reader.seek(SeekFrom::Start(0))?;
reader.read_exact(&mut descriptor_buf)?;
MdsDisc::parse(&descriptor_buf)?
};
let compressed_track = mds.sessions.iter().flat_map(|s| &s.tracks).find(|t| {
t.footer
.as_ref()
.is_some_and(|f| f.is_compressed() && f.blocks_in_compression_group > 0)
});
if let Some(trk) = compressed_track {
let footer = match &trk.footer {
Some(f) => f,
None => return Err(DiscError::InvalidMds("missing compressed track footer".to_string())),
};
let blocks_in_group = footer.blocks_in_compression_group as usize;
let sector_size = trk.sector_size as usize;
if blocks_in_group == 0 || sector_size == 0 {
return Err(DiscError::InvalidMds(
"invalid zero sector size or compression group size".to_string(),
));
}
let group_size = blocks_in_group
.checked_mul(sector_size)
.ok_or_else(|| DiscError::InvalidMds("compression group size overflow".to_string()))?;
let total_sectors = footer.track_data_length;
let total_bytes = total_sectors
.checked_mul(sector_size as u64)
.ok_or_else(|| DiscError::InvalidMds("total track bytes overflow".to_string()))?;
let num_entries = (total_sectors as usize).div_ceil(blocks_in_group);
let bat_rel_offset = footer.compression_table_offset;
let bat_abs_offset = trk
.file_offset
.checked_add(bat_rel_offset)
.ok_or_else(|| DiscError::InvalidMds("BAT offset overflow".to_string()))?;
if bat_abs_offset > total_file_len {
return Err(DiscError::InvalidMds(
"BAT offset exceeds container file length".to_string(),
));
}
let mdx_footer_offset = if is_v2_encrypted {
u64::from_le_bytes(initial_hdr[0x30..0x38].try_into().expect("fixed-size slice"))
} else {
total_file_len
};
if mdx_footer_offset < bat_abs_offset {
return Err(DiscError::InvalidMds(
"invalid MDX footer offset preceding BAT".to_string(),
));
}
let bat_comp_len_u64 = mdx_footer_offset - bat_abs_offset;
if bat_comp_len_u64 > 16 * 1024 * 1024 || bat_comp_len_u64 > total_file_len {
return Err(DiscError::InvalidMds(
"compressed BAT exceeds allowable size limit".to_string(),
));
}
let bat_comp_len = bat_comp_len_u64 as usize;
reader.seek(SeekFrom::Start(bat_abs_offset))?;
let mut bat_comp = vec![0u8; bat_comp_len];
reader.read_exact(&mut bat_comp)?;
let mut decomp_bat = Vec::with_capacity(num_entries * 2);
ZlibDecoder::new(&bat_comp[..]).read_to_end(&mut decomp_bat)?;
let mut bat = Vec::with_capacity(num_entries);
let mut cum_file_offset = trk.file_offset;
for chunk in decomp_bat.as_chunks::<2>().0.iter().take(num_entries) {
let val = u16::from_le_bytes(*chunk);
let (comp_size, is_rle, rle_byte, advance) = if val == 0 {
(0, false, 0, group_size as u64)
} else if (val & 0x8000) != 0 {
(0, true, (val & 0xff) as u8, 0)
} else {
(val, false, 0, val as u64)
};
bat.push(BatEntry {
file_offset: cum_file_offset,
comp_size,
is_rle,
rle_byte,
});
cum_file_offset += advance;
}
let comp_reader = MdxCompressedReader {
inner: reader,
bat,
group_size,
total_bytes,
current_pos: 0,
cached_group: None,
cache_buf: Vec::new(),
comp_buf: Vec::new(),
};
return Ok((
Self {
mds,
data_offset: 0,
data_len: total_bytes,
},
MdxSectorStream::Compressed(comp_reader),
));
}
let mut data_offset: u64 = 0;
for session in &mds.sessions {
for track in &session.tracks {
if track.point == 1 && track.file_offset > 0 {
data_offset = track.file_offset;
break;
}
}
if data_offset > 0 {
break;
}
}
if data_offset == 0 {
let mut max_extent: u64 = mds.header.sessions_offset as u64
+ (mds.header.num_sessions as u64) * 24;
for session in &mds.sessions {
let track_extent = session.track_data_offset as u64
+ (session.num_all_tracks as u64) * 80;
if track_extent > max_extent {
max_extent = track_extent;
}
for track in &session.tracks {
if track.point >= 1 && track.point <= 99 {
let extra_end = track_extent + 24;
if extra_end > max_extent {
max_extent = extra_end;
}
}
}
}
let estimated_size = MdsDisc::calculate_descriptor_size(
&mds.to_toc(Path::new("dummy.mdx"))?,
"*.mdx\0",
) as u64;
data_offset = max_extent.max(estimated_size);
}
if total_file_len < data_offset {
return Err(DiscError::InvalidMds(
"MDX file is truncated before sector payload".to_string(),
));
}
let data_len = total_file_len.saturating_sub(data_offset);
reader.seek(SeekFrom::Start(data_offset))?;
let sector_stream = MdxSectorStream::new(reader, data_offset, data_len);
Ok((
Self {
mds,
data_offset,
data_len,
},
sector_stream,
))
}
pub fn parse_file<P: AsRef<Path>>(path: P) -> Result<CdToc, DiscError> {
let p = path.as_ref();
let mut file = File::open(p)?;
let (disc, _) = Self::parse(&mut file)?;
disc.to_toc(p)
}
pub fn to_toc(&self, mdx_path: &Path) -> Result<CdToc, DiscError> {
let mut user_tracks = Vec::new();
for session in &self.mds.sessions {
for track in &session.tracks {
if track.point >= 1 && track.point <= 99 && track.track_num != 0xaa {
user_tracks.push(track.clone());
}
}
}
if user_tracks.is_empty() {
return Err(DiscError::InvalidMds(
"no valid user tracks found in MDX".to_string(),
));
}
user_tracks.sort_by_key(|t| t.point);
let mdx_file_name = mdx_path
.file_name()
.and_then(|f| f.to_str())
.unwrap_or("disc.mdx")
.to_string();
let is_any_compressed = user_tracks.iter().any(|t| {
t.footer
.as_ref()
.is_some_and(|f| f.is_compressed() && f.blocks_in_compression_group > 0)
});
let mut cd_tracks = Vec::with_capacity(user_tracks.len());
let total_tracks = user_tracks.len();
let mut cumulative_file_offset: u64 = self.data_offset;
for (i, trk) in user_tracks.iter().enumerate() {
let track_num = trk.point as u32;
let track_type = if (trk.adr_control & 0x04) == 0 || trk.mode == 0 {
CdTrackType::Audio
} else if trk.sector_size == 2048 {
CdTrackType::Mode1
} else if trk.mode == 2 {
CdTrackType::Mode2Raw
} else {
CdTrackType::Mode1Raw
};
let subcode_type = if trk.subchannel_mode == 0x08
|| trk.sector_size == 2448
|| trk.sector_size == 2144
{
CdSubcodeType::RwRaw
} else {
CdSubcodeType::None
};
let frames = if let Some(footer) = trk.footer.as_ref().filter(|f| f.track_data_length > 0) {
footer.track_data_length as u32
} else if trk.sector_count > 0 {
trk.sector_count
} else if i + 1 < total_tracks && user_tracks[i + 1].start_lba > trk.start_lba {
(user_tracks[i + 1].start_lba - trk.start_lba) as u32
} else if let Some(first_sess) = self.mds.sessions.first() {
(first_sess.session_end_lba - trk.start_lba).max(0) as u32
} else {
0
};
let pregap_frames = if trk.pregap_sectors > 0 {
trk.pregap_sectors
} else if trk.start_lba > trk.pregap_lba && trk.pregap_lba >= 0 {
(trk.start_lba - trk.pregap_lba) as u32
} else {
0
};
let track_offset = if is_any_compressed {
cumulative_file_offset
} else if trk.file_offset > 0 {
trk.file_offset
} else {
cumulative_file_offset
};
let mut cd_track = CdTrack::new(track_num, track_type, frames);
cd_track.subcode_type = subcode_type;
cd_track.pregap_frames = pregap_frames;
cd_track.file_name = Some(mdx_file_name.clone());
cd_track.file_offset = track_offset;
let track_byte_size = (frames as u64) * (trk.sector_size as u64);
cumulative_file_offset = track_offset + track_byte_size;
cd_tracks.push(cd_track);
}
if let Some(last_track) = cd_tracks.last_mut() {
if last_track.frames == 0 {
let sec_size = if last_track.subcode_type == CdSubcodeType::RwRaw {
2448
} else {
last_track.track_type.sector_size() as u64
};
let remaining_bytes = (self.data_offset + self.data_len).saturating_sub(last_track.file_offset);
last_track.frames = (remaining_bytes / sec_size) as u32;
}
}
Ok(CdToc {
disc_type: DiscType::Cd,
tracks: cd_tracks,
})
}
pub fn format_header(toc: &CdToc) -> Vec<u8> {
let descriptor_size = MdsDisc::calculate_descriptor_size(toc, "*.mdx\0");
MdsDisc::format_with_base_offset(toc, descriptor_size as u64, "*.mdx\0")
}
pub fn format(toc: &CdToc, payload: &[u8]) -> Vec<u8> {
let mut header = Self::format_header(toc);
header.extend_from_slice(payload);
header
}
}
pub fn parse_mdx<R: Read + Seek>(
reader: R,
) -> Result<(CdToc, MdxSectorStream<R>), ChdError> {
let (disc, stream) = MdxDisc::parse(reader)
.map_err(|e| ChdError::InvalidParameter(e.to_string()))?;
let toc = disc
.to_toc(Path::new("disc.mdx"))
.map_err(|e| ChdError::InvalidParameter(e.to_string()))?;
Ok((toc, stream))
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
#[test]
fn test_mdx_sector_stream_read_and_seek() {
let mut raw_data = vec![0u8; 1024];
for (i, b) in raw_data.iter_mut().enumerate() {
*b = (i % 256) as u8;
}
let data_offset = 256u64;
let data_len = 512u64;
let cursor = Cursor::new(raw_data);
let mut stream = MdxSectorStream::new(cursor, data_offset, data_len);
assert_eq!(stream.data_offset(), 256);
assert_eq!(stream.data_len(), 512);
let pos = stream.seek(SeekFrom::Start(10)).expect("seek start");
assert_eq!(pos, 10);
let mut buf = [0u8; 4];
stream.read_exact(&mut buf).expect("read");
assert_eq!(buf, [(266 % 256) as u8, (267 % 256) as u8, (268 % 256) as u8, (269 % 256) as u8]);
let pos = stream.seek(SeekFrom::Current(2)).expect("seek current");
assert_eq!(pos, 16);
let pos = stream.seek(SeekFrom::End(-10)).expect("seek end");
assert_eq!(pos, 502);
}
#[test]
fn test_mdx_format_and_parse_roundtrip() {
let track1 = CdTrack::new(1, CdTrackType::Mode1, 100);
let mut track2 = CdTrack::new(2, CdTrackType::Audio, 50);
track2.pregap_frames = 150;
let toc = CdToc {
disc_type: DiscType::Cd,
tracks: vec![track1, track2],
};
let dummy_payload = vec![0x55u8; 100 * 2048 + 50 * 2352];
let mdx_bytes = MdxDisc::format(&toc, &dummy_payload);
let cursor = Cursor::new(mdx_bytes);
let (disc, stream) = MdxDisc::parse(cursor).expect("parse mdx");
assert_eq!(stream.data_len(), dummy_payload.len() as u64);
assert_eq!(disc.data_offset, stream.data_offset());
let parsed_toc = disc.to_toc(Path::new("test.mdx")).expect("to_toc");
assert_eq!(parsed_toc.tracks.len(), 2);
assert_eq!(parsed_toc.tracks[0].track_num, 1);
assert_eq!(parsed_toc.tracks[0].track_type, CdTrackType::Mode1);
assert_eq!(parsed_toc.tracks[0].frames, 100);
assert_eq!(parsed_toc.tracks[0].file_offset, disc.data_offset);
assert_eq!(parsed_toc.tracks[1].track_num, 2);
assert_eq!(parsed_toc.tracks[1].track_type, CdTrackType::Audio);
assert_eq!(parsed_toc.tracks[1].frames, 50);
assert_eq!(parsed_toc.tracks[1].file_offset, disc.data_offset + 100 * 2048);
}
#[test]
fn test_parse_mdx_helper_function() {
let track = CdTrack::new(1, CdTrackType::Mode1, 20);
let toc = CdToc {
disc_type: DiscType::Cd,
tracks: vec![track],
};
let payload = vec![0xaau8; 20 * 2048];
let mdx_bytes = MdxDisc::format(&toc, &payload);
let (parsed_toc, mut stream) = parse_mdx(Cursor::new(mdx_bytes)).expect("parse_mdx");
assert_eq!(parsed_toc.tracks.len(), 1);
assert_eq!(parsed_toc.tracks[0].frames, 20);
let mut read_buf = vec![0u8; 2048];
stream.read_exact(&mut read_buf).expect("read stream");
assert!(read_buf.iter().all(|&b| b == 0xaa));
}
#[test]
fn test_crypto_api() {
let salt = [0x42u8; 64];
let pwd = derive_password(&salt);
assert_eq!(pwd.len(), 64);
assert!(!pwd.contains(&0));
let key = [0x55u8; 32];
let iv = [0x11u8; 16];
let ct = vec![0x33u8; 32];
let pt = cbc_dewhiten_decrypt(&ct, &key, &iv).expect("decrypt block");
assert_eq!(pt.len(), 32);
}
#[test]
fn test_parse_mdx_invalid_signature() {
let data = vec![0u8; 256];
let err = MdxDisc::parse(Cursor::new(data)).unwrap_err();
assert!(matches!(err, DiscError::InvalidMds(_)));
}
}