use crate::pvd::{decode_ucs2be, IsoDateTime};
use crate::IsoError;
pub const FILE_FLAG_DIRECTORY: u8 = 0x02;
pub const FILE_FLAG_ASSOCIATED: u8 = 0x04;
pub const FILE_FLAG_MULTI_EXTENT: u8 = 0x80;
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DirRecord {
pub lba: u32,
pub size: u32,
pub name_bytes: Vec<u8>,
pub flags: u8,
pub recorded: Option<IsoDateTime>,
pub system_use: Vec<u8>,
pub extra_extents: Vec<(u32, u32)>,
}
impl DirRecord {
pub fn parse(data: &[u8], offset: usize) -> Result<Option<(Self, usize)>, IsoError> {
if offset >= data.len() {
return Ok(None);
}
let len = data[offset] as usize;
if len == 0 {
return Ok(None); }
if offset + len > data.len() || len < 33 {
return Err(IsoError::BadDirRecord(format!(
"record at offset {offset} claims length {len} but only {} bytes remain",
data.len() - offset
)));
}
let rec = &data[offset..offset + len];
let lba = u32::from_le_bytes(rec[2..6].try_into().unwrap());
let size = u32::from_le_bytes(rec[10..14].try_into().unwrap());
let recorded = parse_recording_datetime(&rec[18..25]);
let flags = rec[25];
let name_len = rec[32] as usize;
if 33 + name_len > len {
return Err(IsoError::BadDirRecord("name extends past record".into()));
}
let name_bytes = rec[33..33 + name_len].to_vec();
let su_start = 33 + name_len + (if name_len % 2 == 0 { 1 } else { 0 });
let system_use = if su_start < len { rec[su_start..len].to_vec() } else { Vec::new() };
Ok(Some((
DirRecord {
lba,
size,
name_bytes,
flags,
recorded,
system_use,
extra_extents: Vec::new(),
},
len,
)))
}
pub fn is_dir(&self) -> bool {
self.flags & FILE_FLAG_DIRECTORY != 0
}
pub fn is_multi_extent(&self) -> bool {
self.flags & FILE_FLAG_MULTI_EXTENT != 0
}
pub fn is_dot(&self) -> bool {
self.name_bytes == [0x00] || self.name_bytes == [0x01]
}
pub fn iso_name(&self) -> String {
let raw = std::str::from_utf8(&self.name_bytes).unwrap_or("").trim_end_matches('\0');
if let Some(pos) = raw.rfind(';') {
raw[..pos].to_string()
} else {
raw.to_string()
}
}
pub fn joliet_name(&self) -> String {
decode_ucs2be(&self.name_bytes)
}
}
fn parse_recording_datetime(b: &[u8]) -> Option<IsoDateTime> {
if b.len() < 7 || b[..6].iter().all(|&x| x == 0) {
return None;
}
Some(IsoDateTime {
year: 1900 + u16::from(b[0]),
month: b[1],
day: b[2],
hour: b[3],
minute: b[4],
second: b[5],
centisecond: 0,
tz_offset_15min: b[6] as i8,
})
}
pub fn parse_dir_records(data: &[u8]) -> Result<Vec<DirRecord>, IsoError> {
let mut records = Vec::new();
let mut offset = 0;
while offset < data.len() {
if data[offset] == 0 {
offset = (offset + 2047) & !2047;
continue;
}
match DirRecord::parse(data, offset)? {
Some((rec, advance)) => {
if !rec.is_dot() {
records.push(rec);
}
offset += advance;
if advance == 0 {
break;
}
}
None => break,
}
}
Ok(records)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_recording_datetime() {
let mut rec = vec![0u8; 34];
rec[0] = 34;
rec[32] = 1; rec[18] = 126; rec[19] = 6;
rec[20] = 15;
rec[21] = 12;
rec[22] = 30;
rec[23] = 0;
rec[24] = 0; let (r, _) = DirRecord::parse(&rec, 0).unwrap().unwrap();
let dt = r.recorded.expect("recording datetime");
assert_eq!((dt.year, dt.month, dt.day, dt.hour, dt.minute), (2026, 6, 15, 12, 30));
}
#[test]
fn zero_recording_datetime_is_none() {
let mut rec = vec![0u8; 34];
rec[0] = 34;
rec[32] = 1;
let (r, _) = DirRecord::parse(&rec, 0).unwrap().unwrap();
assert!(r.recorded.is_none());
}
}