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)]
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,
}
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());
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,
})
}
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);
}
#[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)
}
}
}