use crate::IsoError;
pub const PVD_TYPE: u8 = 0x01;
pub const SVD_TYPE: u8 = 0x02; pub const TERMINATOR_TYPE: u8 = 0xFF;
pub const BOOT_RECORD_TYPE: u8 = 0x00;
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct IsoDateTime {
pub year: u16,
pub month: u8,
pub day: u8,
pub hour: u8,
pub minute: u8,
pub second: u8,
pub centisecond: u8,
pub tz_offset_15min: i8,
}
pub(crate) fn parse_iso_datetime(b: &[u8]) -> Option<IsoDateTime> {
if b.len() < 17 {
return None;
}
if b[..16].iter().all(|&x| x == b'0' || x == 0) {
return None;
}
let d = |i: usize| (b[i].wrapping_sub(b'0')) as u16;
let year = d(0) * 1000 + d(1) * 100 + d(2) * 10 + d(3);
if year == 0 {
return None;
}
let month = (d(4) * 10 + d(5)) as u8;
let day = (d(6) * 10 + d(7)) as u8;
let hour = (d(8) * 10 + d(9)) as u8;
let minute = (d(10) * 10 + d(11)) as u8;
let second = (d(12) * 10 + d(13)) as u8;
let centisecond = (d(14) * 10 + d(15)) as u8;
let tz_offset_15min = b[16] as i8;
Some(IsoDateTime { year, month, day, hour, minute, second, centisecond, tz_offset_15min })
}
fn trim_field(bytes: &[u8]) -> String {
let s = std::str::from_utf8(bytes).unwrap_or("");
s.trim_end_matches(['\0', ' ']).to_string()
}
#[derive(Debug, Clone, Default)]
pub struct PrimaryVolumeDescriptor {
pub volume_label: String,
pub root_dir_lba: u32,
pub root_dir_size: u32,
pub volume_space_size: u32,
pub system_id: String,
pub volume_set_id: String,
pub publisher_id: String,
pub data_preparer_id: String,
pub application_id: String,
pub copyright_file_id: String,
pub abstract_file_id: String,
pub bibliographic_file_id: String,
pub volume_creation_time: Option<IsoDateTime>,
pub volume_modification_time: Option<IsoDateTime>,
pub volume_expiration_time: Option<IsoDateTime>,
pub volume_effective_time: Option<IsoDateTime>,
pub logical_block_size: u16,
pub path_table_size: u32,
pub l_path_table_lba: u32,
pub m_path_table_lba: u32,
}
impl PrimaryVolumeDescriptor {
pub fn parse(sector: &[u8]) -> Result<Self, IsoError> {
if sector.len() < 883 {
return Err(IsoError::BadDescriptor("sector too short for PVD".into()));
}
if §or[1..6] != b"CD001" {
return Err(IsoError::BadDescriptor("missing CD001 signature".into()));
}
if sector[0] != PVD_TYPE {
return Err(IsoError::BadDescriptor(format!(
"expected type 0x01, got 0x{:02x}",
sector[0]
)));
}
if sector[6] != 0x01 {
return Err(IsoError::BadDescriptor(format!(
"expected version 0x01, got 0x{:02x}",
sector[6]
)));
}
let le16 = |i: usize| u16::from_le_bytes(sector[i..i + 2].try_into().unwrap());
let le32 = |i: usize| u32::from_le_bytes(sector[i..i + 4].try_into().unwrap());
let be32 = |i: usize| u32::from_be_bytes(sector[i..i + 4].try_into().unwrap());
let volume_label = trim_field(§or[40..72]);
let volume_space_size = le32(80);
let root_dir_lba = le32(158); let root_dir_size = le32(166);
Ok(Self {
volume_label,
root_dir_lba,
root_dir_size,
volume_space_size,
system_id: trim_field(§or[8..40]),
volume_set_id: trim_field(§or[190..318]),
publisher_id: trim_field(§or[318..446]),
data_preparer_id: trim_field(§or[446..574]),
application_id: trim_field(§or[574..702]),
copyright_file_id: trim_field(§or[702..739]),
abstract_file_id: trim_field(§or[739..775]),
bibliographic_file_id: trim_field(§or[775..812]),
volume_creation_time: parse_iso_datetime(§or[813..830]),
volume_modification_time: parse_iso_datetime(§or[830..847]),
volume_expiration_time: parse_iso_datetime(§or[847..864]),
volume_effective_time: parse_iso_datetime(§or[864..881]),
logical_block_size: le16(128),
path_table_size: le32(132),
l_path_table_lba: le32(140),
m_path_table_lba: be32(148),
})
}
}
#[derive(Debug, Clone)]
pub struct SupplementaryVolumeDescriptor {
pub version: u8,
pub is_joliet: bool,
pub volume_label: String,
pub root_dir_lba: u32,
pub root_dir_size: u32,
pub l_path_table_lba: u32,
pub m_path_table_lba: u32,
pub path_table_size: u32,
}
impl SupplementaryVolumeDescriptor {
#[must_use]
pub fn is_enhanced(&self) -> bool {
self.version >= 2 && !self.is_joliet
}
pub fn parse(sector: &[u8]) -> Result<Self, IsoError> {
if sector.len() < 190 {
return Err(IsoError::BadDescriptor("SVD sector too short".into()));
}
if §or[1..6] != b"CD001" || sector[0] != SVD_TYPE {
return Err(IsoError::BadDescriptor("not a Supplementary VD".into()));
}
let esc = §or[88..120];
let is_joliet =
esc.windows(3).any(|w| w == b"%/@" || w == b"%/B" || w == b"%/C" || w == b"%/E");
let volume_label = if is_joliet {
decode_ucs2be(§or[40..72])
} else {
std::str::from_utf8(§or[40..72]).unwrap_or("").trim_end().to_string()
};
let root = §or[156..190];
let root_dir_lba = u32::from_le_bytes(root[2..6].try_into().unwrap());
let root_dir_size = u32::from_le_bytes(root[10..14].try_into().unwrap());
let path_table_size = u32::from_le_bytes(sector[132..136].try_into().unwrap());
let l_path_table_lba = u32::from_le_bytes(sector[140..144].try_into().unwrap());
let m_path_table_lba = u32::from_be_bytes(sector[148..152].try_into().unwrap());
Ok(Self {
version: sector[6], is_joliet,
volume_label,
root_dir_lba,
root_dir_size,
l_path_table_lba,
m_path_table_lba,
path_table_size,
})
}
}
pub(crate) fn decode_ucs2be(bytes: &[u8]) -> String {
bytes
.chunks_exact(2)
.map_while(|w| {
let cp = u16::from_be_bytes([w[0], w[1]]);
if cp == 0 {
None
} else {
char::from_u32(u32::from(cp))
}
})
.collect::<String>()
.trim_end()
.to_string()
}