use crate::error::{OpticaldiscsError, Result};
use crate::sector_reader::SectorReader;
pub const PVD_SECTOR: u64 = 16;
pub const PVD_OFFSET: u64 = PVD_SECTOR * crate::sector_reader::SECTOR_SIZE;
const PVD_TYPE: u8 = 0x01;
const VD_SET_TERMINATOR_TYPE: u8 = 0xFF;
const ISO9660_ID: &[u8; 5] = b"CD001";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PvdDateTime {
pub year: u16,
pub month: u8,
pub day: u8,
pub hour: u8,
pub minute: u8,
pub second: u8,
pub hundredths: u8,
pub gmt_offset_quarter_hours: i8,
}
impl PvdDateTime {
pub fn parse(bytes: &[u8]) -> Option<Self> {
if bytes.len() < 17 {
return None;
}
if bytes[..17].iter().all(|&b| b == 0) {
return None;
}
if bytes[..16].iter().all(|&b| b == b'0') && bytes[16] == 0 {
return None;
}
let year = parse_digits(&bytes[0..4])? as u16;
let month = parse_digits(&bytes[4..6])? as u8;
let day = parse_digits(&bytes[6..8])? as u8;
let hour = parse_digits(&bytes[8..10])? as u8;
let minute = parse_digits(&bytes[10..12])? as u8;
let second = parse_digits(&bytes[12..14])? as u8;
let hundredths = parse_digits(&bytes[14..16])? as u8;
let gmt_offset_quarter_hours = bytes[16] as i8;
Some(Self {
year,
month,
day,
hour,
minute,
second,
hundredths,
gmt_offset_quarter_hours,
})
}
pub fn to_iso8601(&self) -> String {
let total_minutes = self.gmt_offset_quarter_hours as i32 * 15;
let sign = if total_minutes < 0 { '-' } else { '+' };
let abs = total_minutes.abs();
format!(
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:02}{}{:02}:{:02}",
self.year,
self.month,
self.day,
self.hour,
self.minute,
self.second,
self.hundredths,
sign,
abs / 60,
abs % 60,
)
}
}
fn parse_digits(bytes: &[u8]) -> Option<u32> {
let mut value = 0u32;
for &b in bytes {
if !b.is_ascii_digit() {
return None;
}
value = value * 10 + (b - b'0') as u32;
}
Some(value)
}
#[derive(Debug, Clone)]
pub struct PrimaryVolumeDescriptor {
pub volume_id: String,
pub system_id: String,
pub volume_set_id: String,
pub publisher_id: String,
pub application_id: String,
pub volume_space_size: u32,
pub logical_block_size: u16,
pub root_directory_lba: u32,
pub root_directory_size: u32,
pub creation_date: Option<PvdDateTime>,
pub modification_date: Option<PvdDateTime>,
pub expiration_date: Option<PvdDateTime>,
pub effective_date: Option<PvdDateTime>,
}
impl PrimaryVolumeDescriptor {
pub fn read_from(reader: &mut dyn SectorReader) -> Result<Self> {
let sector = reader.read_sector(PVD_SECTOR)?;
Self::parse(§or)
}
pub fn parse(sector: &[u8]) -> Result<Self> {
if sector.len() < crate::sector_reader::SECTOR_SIZE as usize {
return Err(OpticaldiscsError::Parse(format!(
"sector too small: {} bytes",
sector.len()
)));
}
match sector[0] {
PVD_TYPE => {}
VD_SET_TERMINATOR_TYPE => {
return Err(OpticaldiscsError::Parse(
"reached Volume Descriptor Set Terminator before PVD".into(),
))
}
t => {
return Err(OpticaldiscsError::Parse(format!(
"unexpected volume descriptor type 0x{t:02X} (expected 0x01)"
)))
}
}
if §or[1..6] != ISO9660_ID {
return Err(OpticaldiscsError::Parse(
"missing ISO 9660 identifier 'CD001'".into(),
));
}
if sector[6] != 1 {
return Err(OpticaldiscsError::Parse(format!(
"unsupported PVD version {}",
sector[6]
)));
}
let system_id = Self::extract_str(§or[8..40]);
let volume_id = Self::extract_str(§or[40..72]);
let volume_space_size = u32::from_le_bytes(sector[80..84].try_into().unwrap());
let logical_block_size = u16::from_le_bytes(sector[128..130].try_into().unwrap());
let volume_set_id = Self::extract_str(§or[190..318]);
let publisher_id = Self::extract_str(§or[318..446]);
let application_id = Self::extract_str(§or[574..702]);
let rdr = §or[156..190];
let root_directory_lba = u32::from_le_bytes(rdr[2..6].try_into().unwrap());
let root_directory_size = u32::from_le_bytes(rdr[10..14].try_into().unwrap());
let creation_date = PvdDateTime::parse(§or[813..830]);
let modification_date = PvdDateTime::parse(§or[830..847]);
let expiration_date = PvdDateTime::parse(§or[847..864]);
let effective_date = PvdDateTime::parse(§or[864..881]);
Ok(Self {
volume_id,
system_id,
volume_set_id,
publisher_id,
application_id,
volume_space_size,
logical_block_size,
root_directory_lba,
root_directory_size,
creation_date,
modification_date,
expiration_date,
effective_date,
})
}
fn extract_str(bytes: &[u8]) -> String {
String::from_utf8_lossy(bytes)
.trim_end_matches([' ', '\0'])
.to_string()
}
}
#[doc(hidden)]
pub fn build_test_pvd_sector(volume_id: &str, root_lba: u32, root_size: u32) -> Vec<u8> {
let mut s = vec![0u8; 2048];
s[0] = PVD_TYPE;
s[1..6].copy_from_slice(ISO9660_ID);
s[6] = 1;
let sys = b"CDROM ";
s[8..40].copy_from_slice(sys);
let mut vol = [b' '; 32];
let src = volume_id.as_bytes();
let len = src.len().min(32);
vol[..len].copy_from_slice(&src[..len]);
s[40..72].copy_from_slice(&vol);
let total = 100u32;
s[80..84].copy_from_slice(&total.to_le_bytes());
s[84..88].copy_from_slice(&total.to_be_bytes());
s[128..130].copy_from_slice(&2048u16.to_le_bytes());
s[130..132].copy_from_slice(&2048u16.to_be_bytes());
let rdr = &mut s[156..190];
rdr[0] = 34;
rdr[2..6].copy_from_slice(&root_lba.to_le_bytes());
rdr[6..10].copy_from_slice(&root_lba.to_be_bytes());
rdr[10..14].copy_from_slice(&root_size.to_le_bytes());
rdr[14..18].copy_from_slice(&root_size.to_be_bytes());
rdr[25] = 0x02;
rdr[32] = 1;
rdr[33] = 0x00;
s
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_valid_pvd() {
let sector = build_test_pvd_sector("MY_DISC", 20, 2048);
let pvd = PrimaryVolumeDescriptor::parse(§or).unwrap();
assert_eq!(pvd.volume_id, "MY_DISC");
assert_eq!(pvd.system_id, "CDROM");
assert_eq!(pvd.logical_block_size, 2048);
assert_eq!(pvd.root_directory_lba, 20);
assert_eq!(pvd.root_directory_size, 2048);
}
fn write_datetime(sector: &mut [u8], offset: usize, ascii16: &[u8; 16], gmt: i8) {
sector[offset..offset + 16].copy_from_slice(ascii16);
sector[offset + 16] = gmt as u8;
}
#[test]
fn parse_creation_date() {
let mut sector = build_test_pvd_sector("DATED_DISC", 20, 2048);
write_datetime(&mut sector, 813, b"1997031816454700", 0);
let pvd = PrimaryVolumeDescriptor::parse(§or).unwrap();
let dt = pvd.creation_date.expect("creation date present");
assert_eq!(dt.year, 1997);
assert_eq!(dt.month, 3);
assert_eq!(dt.day, 18);
assert_eq!(dt.hour, 16);
assert_eq!(dt.minute, 45);
assert_eq!(dt.second, 47);
assert_eq!(dt.hundredths, 0);
assert_eq!(dt.gmt_offset_quarter_hours, 0);
assert_eq!(dt.to_iso8601(), "1997-03-18T16:45:47.00+00:00");
}
#[test]
fn parse_date_not_specified() {
let mut sector = build_test_pvd_sector("X", 20, 2048);
write_datetime(&mut sector, 813, b"0000000000000000", 0);
let pvd = PrimaryVolumeDescriptor::parse(§or).unwrap();
assert_eq!(pvd.creation_date, None);
let sector = build_test_pvd_sector("X", 20, 2048);
let pvd = PrimaryVolumeDescriptor::parse(§or).unwrap();
assert_eq!(pvd.creation_date, None);
assert_eq!(pvd.modification_date, None);
assert_eq!(pvd.expiration_date, None);
assert_eq!(pvd.effective_date, None);
}
#[test]
fn parse_negative_gmt_offset() {
let mut sector = build_test_pvd_sector("X", 20, 2048);
write_datetime(&mut sector, 813, b"1997031816454700", -28);
let pvd = PrimaryVolumeDescriptor::parse(§or).unwrap();
let dt = pvd.creation_date.unwrap();
assert_eq!(dt.gmt_offset_quarter_hours, -28);
assert_eq!(dt.to_iso8601(), "1997-03-18T16:45:47.00-07:00");
}
#[test]
fn parse_date_rejects_non_digits() {
let mut sector = build_test_pvd_sector("X", 20, 2048);
write_datetime(&mut sector, 813, b"19X7031816454700", 0);
let pvd = PrimaryVolumeDescriptor::parse(§or).unwrap();
assert_eq!(pvd.creation_date, None);
}
#[test]
fn parse_rejects_wrong_magic() {
let mut sector = build_test_pvd_sector("X", 20, 2048);
sector[1..6].copy_from_slice(b"XXXXX");
assert!(PrimaryVolumeDescriptor::parse(§or).is_err());
}
#[test]
fn parse_rejects_wrong_type() {
let mut sector = build_test_pvd_sector("X", 20, 2048);
sector[0] = 0x02; assert!(PrimaryVolumeDescriptor::parse(§or).is_err());
}
#[test]
fn parse_rejects_terminator() {
let mut sector = build_test_pvd_sector("X", 20, 2048);
sector[0] = 0xFF;
assert!(PrimaryVolumeDescriptor::parse(§or).is_err());
}
#[test]
fn extract_str_trims_spaces_and_nulls() {
let bytes = b"HELLO \0\0";
assert_eq!(PrimaryVolumeDescriptor::extract_str(bytes), "HELLO");
}
#[test]
fn read_from_sector_reader() {
use crate::sector_reader::SECTOR_SIZE;
use std::io::Cursor;
let total = (PVD_SECTOR + 1) * SECTOR_SIZE;
let mut img = vec![0u8; total as usize];
let pvd_bytes = build_test_pvd_sector("READER_TEST", 18, 2048);
let start = (PVD_SECTOR * SECTOR_SIZE) as usize;
img[start..start + 2048].copy_from_slice(&pvd_bytes);
let mut reader = CursorSectorReader(Cursor::new(img));
let pvd = PrimaryVolumeDescriptor::read_from(&mut reader).unwrap();
assert_eq!(pvd.volume_id, "READER_TEST");
}
struct CursorSectorReader(std::io::Cursor<Vec<u8>>);
impl SectorReader for CursorSectorReader {
fn read_sector(&mut self, lba: u64) -> Result<Vec<u8>> {
use std::io::{Read, Seek, SeekFrom};
self.0
.seek(SeekFrom::Start(lba * crate::sector_reader::SECTOR_SIZE))
.map_err(OpticaldiscsError::Io)?;
let mut buf = vec![0u8; crate::sector_reader::SECTOR_SIZE as usize];
self.0.read_exact(&mut buf).map_err(OpticaldiscsError::Io)?;
Ok(buf)
}
}
}