use super::entry::{EntryType, FileEntry};
use super::filesystem::{Filesystem, FilesystemError};
use crate::sector_reader::{SectorReader, SECTOR_SIZE};
const VD_SECTOR: u64 = 16;
const ATTR_DIRECTORY: u16 = 0x8000;
pub fn detect_cdi(reader: &mut dyn SectorReader) -> bool {
match reader.read_sector(VD_SECTOR) {
Ok(vd) => vd.len() >= 6 && &vd[1..6] == b"CD-I ",
Err(_) => false,
}
}
pub fn read_volume_id(reader: &mut dyn SectorReader) -> Option<String> {
let vd = reader.read_sector(VD_SECTOR).ok()?;
if vd.len() < 72 || &vd[1..6] != b"CD-I " {
return None;
}
let s = ascii_field(&vd[40..72]);
if s.is_empty() {
None
} else {
Some(s)
}
}
pub struct CdiFilesystem {
reader: Box<dyn SectorReader>,
root_lba: u32,
root_size: u32,
volume_id: String,
}
impl CdiFilesystem {
pub fn new(mut reader: Box<dyn SectorReader>) -> Result<Self, FilesystemError> {
let vd = reader
.read_sector(VD_SECTOR)
.map_err(|e| FilesystemError::Io(io_of(e)))?;
if vd.len() < 152 || &vd[1..6] != b"CD-I " {
return Err(FilesystemError::Parse("not a CD-i volume".into()));
}
let volume_id = ascii_field(&vd[40..72]);
let m_path_lba = be32(&vd[148..152]);
let pt = reader
.read_sector(m_path_lba as u64)
.map_err(|e| FilesystemError::Io(io_of(e)))?;
if pt.len() < 8 || pt[0] == 0 {
return Err(FilesystemError::Parse("empty CD-i path table".into()));
}
let root_lba = be32(&pt[2..6]);
let root0 = reader
.read_sector(root_lba as u64)
.map_err(|e| FilesystemError::Io(io_of(e)))?;
let root_size = if root0.first().copied().unwrap_or(0) as usize >= 18 {
be32(&root0[14..18])
} else {
SECTOR_SIZE as u32
};
Ok(Self {
reader,
root_lba,
root_size,
volume_id,
})
}
fn read_dir(
&mut self,
lba: u32,
size: u32,
base_path: &str,
) -> Result<Vec<FileEntry>, FilesystemError> {
let bytes = self
.reader
.read_bytes(lba as u64 * SECTOR_SIZE, size as usize)
.map_err(|e| FilesystemError::Io(io_of(e)))?;
let mut out = Vec::new();
let mut pos = 0usize;
while pos < bytes.len() {
let len = bytes[pos] as usize;
if len == 0 {
let next = (pos / SECTOR_SIZE as usize + 1) * SECTOR_SIZE as usize;
if next <= pos || next >= bytes.len() {
break;
}
pos = next;
continue;
}
if len < 34 || pos + len > bytes.len() {
break;
}
let rec = &bytes[pos..pos + len];
let id_len = rec[32] as usize;
if 33 + id_len > rec.len() {
break;
}
let id = &rec[33..33 + id_len];
let is_special = id_len == 1 && (id[0] == 0x00 || id[0] == 0x01);
if !is_special {
let extent = be32(&rec[6..10]);
let data_len = be32(&rec[14..18]);
let attr = cdi_attributes(rec, id_len);
let is_dir = attr & ATTR_DIRECTORY != 0;
let name = clean_name(id);
let path = format!("{}/{}", base_path.trim_end_matches('/'), name);
out.push(make_entry(name, path, extent, data_len, is_dir));
}
pos += len;
}
Ok(out)
}
}
impl Filesystem for CdiFilesystem {
fn root(&mut self) -> Result<FileEntry, FilesystemError> {
Ok(make_entry(
String::new(),
"/".into(),
self.root_lba,
self.root_size,
true,
))
}
fn list_directory(&mut self, entry: &FileEntry) -> Result<Vec<FileEntry>, FilesystemError> {
if entry.entry_type != EntryType::Directory {
return Err(FilesystemError::NotADirectory(entry.path.clone()));
}
let path = entry.path.clone();
self.read_dir(entry.location as u32, entry.size as u32, &path)
}
fn read_file(&mut self, entry: &FileEntry) -> Result<Vec<u8>, FilesystemError> {
self.reader
.read_bytes(entry.location * SECTOR_SIZE, entry.size as usize)
.map_err(|e| FilesystemError::Io(io_of(e)))
}
fn read_file_range(
&mut self,
entry: &FileEntry,
offset: u64,
length: usize,
) -> Result<Vec<u8>, FilesystemError> {
let clamped = length.min((entry.size.saturating_sub(offset)) as usize);
self.reader
.read_bytes(entry.location * SECTOR_SIZE + offset, clamped)
.map_err(|e| FilesystemError::Io(io_of(e)))
}
fn read_resource_fork(
&mut self,
_entry: &FileEntry,
) -> Result<Option<Vec<u8>>, FilesystemError> {
Ok(None)
}
fn read_resource_fork_range(
&mut self,
_entry: &FileEntry,
_offset: u64,
_length: usize,
) -> Result<Option<Vec<u8>>, FilesystemError> {
Ok(None)
}
fn volume_name(&self) -> Option<&str> {
if self.volume_id.is_empty() {
None
} else {
Some(&self.volume_id)
}
}
}
fn cdi_attributes(rec: &[u8], id_len: usize) -> u16 {
let pad = (id_len + 1) & 1;
let attr_off = 33 + id_len + pad + 4;
if attr_off + 2 <= rec.len() {
be16(&rec[attr_off..attr_off + 2])
} else {
0
}
}
fn make_entry(name: String, path: String, lba: u32, size: u32, is_dir: bool) -> FileEntry {
FileEntry {
name,
path,
entry_type: if is_dir {
EntryType::Directory
} else {
EntryType::File
},
size: size as u64,
location: lba as u64,
children: None,
resource_fork_size: None,
type_code: None,
creator_code: None,
finder_flags: None,
symlink_target: None,
timestamps: None,
posix: None,
}
}
fn clean_name(id: &[u8]) -> String {
let stem = id.split(|&b| b == b';').next().unwrap_or(id);
ascii_field(stem)
}
fn ascii_field(bytes: &[u8]) -> String {
let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
String::from_utf8_lossy(&bytes[..end]).trim().to_string()
}
fn be16(b: &[u8]) -> u16 {
u16::from_be_bytes([b[0], b[1]])
}
fn be32(b: &[u8]) -> u32 {
u32::from_be_bytes([b[0], b[1], b[2], b[3]])
}
fn io_of(e: crate::error::OpticaldiscsError) -> std::io::Error {
match e {
crate::error::OpticaldiscsError::Io(io) => io,
other => std::io::Error::other(other.to_string()),
}
}