use super::io::{self, Read, Write};
use bytemuck::Zeroable;
use super::io::LogicalSector;
use crate::types::{U16LsbMsb, U32LsbMsb};
#[repr(C)]
#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
pub struct DirectoryRecordHeader {
pub len: u8,
pub extended_attr_record: u8,
pub extent: U32LsbMsb,
pub data_len: U32LsbMsb,
pub date_time: DirDateTime,
pub flags: u8,
pub file_unit_size: u8,
pub interleave_gap_size: u8,
pub volume_sequence_number: U16LsbMsb,
pub file_identifier_len: u8,
}
impl Default for DirectoryRecordHeader {
fn default() -> Self {
Self {
len: 0,
extended_attr_record: 0,
extent: U32LsbMsb::new(0),
data_len: U32LsbMsb::new(0),
date_time: DirDateTime::now(),
flags: 0,
file_unit_size: 0,
interleave_gap_size: 0,
volume_sequence_number: U16LsbMsb::new(0),
file_identifier_len: 0,
}
}
}
impl DirectoryRecordHeader {
pub fn from_bytes(bytes: &[u8]) -> &Self {
bytemuck::from_bytes(bytes)
}
pub fn to_bytes(&self) -> &[u8] {
bytemuck::bytes_of(self)
}
pub fn is_directory(&self) -> bool {
FileFlags::from_bits_retain(self.flags).contains(FileFlags::DIRECTORY)
}
}
#[repr(transparent)]
#[derive(Debug, Clone, Copy, bytemuck::Zeroable, bytemuck::Pod)]
pub struct DirectoryRecord {
data: [u8; 256],
}
impl Default for DirectoryRecord {
fn default() -> Self {
bytemuck::Zeroable::zeroed()
}
}
#[derive(Debug, Clone, Copy)]
pub struct NotADirectoryError;
impl core::fmt::Display for NotADirectoryError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "not a directory")
}
}
#[cfg(feature = "std")]
impl std::error::Error for NotADirectoryError {}
impl DirectoryRecord {
const DATA_START: usize = size_of::<DirectoryRecordHeader>();
#[inline]
pub fn header(&self) -> &DirectoryRecordHeader {
bytemuck::from_bytes(&self.data[0..Self::DATA_START])
}
#[inline]
pub fn header_mut(&mut self) -> &mut DirectoryRecordHeader {
bytemuck::from_bytes_mut(&mut self.data[0..size_of::<DirectoryRecordHeader>()])
}
#[inline]
pub fn name(&self) -> &[u8] {
let len = self.header().file_identifier_len as usize;
&self.data[Self::DATA_START..Self::DATA_START + len]
}
#[cfg(feature = "alloc")]
pub fn joliet_name(&self) -> alloc::string::String {
crate::joliet::decode_joliet_name(self.name())
}
#[cfg(feature = "alloc")]
pub fn is_joliet_name(&self) -> bool {
crate::joliet::is_likely_joliet_name(self.name())
}
#[inline]
pub fn system_use(&self) -> &[u8] {
let header = self.header();
let su_start = (Self::DATA_START + header.file_identifier_len as usize + 1) & !1;
if su_start >= header.len as usize {
return &[];
}
&self.data[su_start..header.len as usize]
}
sync_only! {
#[cfg(feature = "write")]
pub(crate) fn system_use_mut(&mut self) -> &mut [u8] {
let name_len = self.header().file_identifier_len as usize;
let start = (Self::DATA_START + name_len + 1) & !1;
let end = self.header().len as usize;
&mut self.data[start..end]
}
}
#[inline]
pub fn is_special(&self) -> bool {
self.name() == b"\x00" || self.name() == b"\x01"
}
#[inline]
pub fn is_directory(&self) -> bool {
self.header().is_directory()
}
pub fn is_file(&self) -> bool {
!self.header().is_directory()
}
pub fn as_dir_ref(&self) -> Result<DirectoryRef, NotADirectoryError> {
if !self.is_directory() {
return Err(NotADirectoryError);
}
let header = self.header();
Ok(DirectoryRef {
extent: LogicalSector(header.extent.read() as usize),
size: header.data_len.read() as usize,
})
}
pub fn size(&self) -> usize {
self.header().len as usize
}
pub fn to_bytes(&self) -> &[u8] {
&self.data[0..self.size()]
}
pub fn new(name: &[u8], system_use: &[u8], directory: DirectoryRef, flags: FileFlags) -> Self {
let mut sel = Self::zeroed();
assert!(
!name.is_empty() && name.len() <= u8::MAX as usize,
"ISO directory-record identifier must contain 1..=255 bytes"
);
let su_start = (Self::DATA_START + name.len() + 1) & !1;
let total = su_start + system_use.len();
let record_len = (total + 1) & !1;
assert!(
record_len <= 255,
"DirectoryRecord too large: {} bytes (name={}, su={})",
record_len,
name.len(),
system_use.len()
);
*sel.header_mut() = DirectoryRecordHeader {
len: record_len as u8,
extended_attr_record: 0,
extent: U32LsbMsb::new(directory.extent.0 as u32),
data_len: U32LsbMsb::new(directory.size as u32),
date_time: DirDateTime::now(),
flags: flags.bits(),
file_unit_size: 0,
interleave_gap_size: 0,
volume_sequence_number: U16LsbMsb::new(1),
file_identifier_len: name.len() as u8,
};
sel.data[Self::DATA_START..Self::DATA_START + name.len()].copy_from_slice(name);
sel.data[su_start..su_start + system_use.len()].copy_from_slice(system_use);
sel
}
pub fn with_len(name_len: usize, su_len: usize) -> Self {
let mut sel = Self::zeroed();
assert!(
(1..=u8::MAX as usize).contains(&name_len),
"ISO directory-record identifier must contain 1..=255 bytes"
);
let su_start = (Self::DATA_START + name_len + 1) & !1;
let total = su_start + su_len;
let record_len = (total + 1) & !1;
assert!(record_len <= u8::MAX as usize, "DirectoryRecord too large");
sel.header_mut().len = record_len as u8;
sel
}
}
io_transform! {
impl DirectoryRecord {
pub async fn parse<R: Read>(reader: &mut R) -> io::Result<Self> {
let mut sel = Self::zeroed();
reader.read_exact(&mut sel.data[0..Self::DATA_START]).await?;
let size = sel.size();
if size == 0 {
return Ok(sel);
}
let name_len = sel.header().file_identifier_len as usize;
if size < Self::DATA_START + 1
|| !size.is_multiple_of(2)
|| Self::DATA_START + name_len > size
|| sel.header().flags & 0b0110_0000 != 0
|| !sel.header().extent.is_consistent()
|| !sel.header().data_len.is_consistent()
|| !sel.header().volume_sequence_number.is_consistent()
{
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"invalid ISO directory record",
));
}
if size > Self::DATA_START {
reader.read_exact(&mut sel.data[Self::DATA_START..size]).await?;
}
if name_len.is_multiple_of(2) {
let padding = Self::DATA_START + name_len;
if padding >= size || sel.data[padding] != 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"invalid ISO directory-record identifier padding",
));
}
}
Ok(sel)
}
pub async fn write<W: Write>(&self, writer: &mut W) -> io::Result<usize> {
let size = self.size();
writer.write_all(&self.data[0..size]).await?;
Ok(size)
}
}
}
#[repr(C)]
#[derive(Default, Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
pub struct RootDirectoryEntry {
pub header: DirectoryRecordHeader,
pub padding: u8,
}
#[repr(C)]
#[derive(Default, Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
pub struct DirDateTime {
year: u8,
month: u8,
day: u8,
hour: u8,
minute: u8,
second: u8,
offset: u8,
}
impl DirDateTime {
#[cfg(feature = "std")]
pub fn now() -> Self {
use chrono::{Datelike, Timelike, Utc};
let now = Utc::now();
Self {
year: (now.year() - 1900) as u8,
month: now.month() as u8,
day: now.day() as u8,
hour: now.hour() as u8,
minute: now.minute() as u8,
second: now.second() as u8,
offset: 0,
}
}
#[cfg(not(feature = "std"))]
pub fn now() -> Self {
Self::default()
}
}
#[derive(Default, Debug, Clone, Copy)]
pub struct DirectoryRef {
pub extent: LogicalSector,
pub size: usize,
}
sync_only! {
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
use alloc::vec::Vec;
use std::io::Cursor;
#[test]
fn directory_record_parse_roundtrip() {
let mut dir = vec![0u8; 2048];
let mut offset = 0usize;
for (id, flags) in [(0x00u8, 0x02u8), (0x01u8, 0x02u8)] {
dir[offset] = 34;
dir[offset + 2..offset + 6].copy_from_slice(&20u32.to_le_bytes());
dir[offset + 6..offset + 10].copy_from_slice(&20u32.to_be_bytes());
dir[offset + 10..offset + 14].copy_from_slice(&2048u32.to_le_bytes());
dir[offset + 14..offset + 18].copy_from_slice(&2048u32.to_be_bytes());
dir[offset + 25] = flags;
dir[offset + 28..offset + 30].copy_from_slice(&1u16.to_le_bytes());
dir[offset + 30..offset + 32].copy_from_slice(&1u16.to_be_bytes());
dir[offset + 32] = 1;
dir[offset + 33] = id;
offset += 34;
}
let mut cursor = Cursor::new(&dir[..]);
let dot = DirectoryRecord::parse(&mut cursor).expect("parse .");
assert_eq!(dot.size(), 34);
assert_eq!(core::mem::size_of::<DirectoryRecordHeader>(), 33);
assert!(dot.is_directory());
assert_eq!(dot.name(), b"\x00");
assert_eq!(dot.header().extent.read(), 20);
assert_eq!(dot.header().data_len.read(), 2048);
let dotdot = DirectoryRecord::parse(&mut cursor).expect("parse ..");
assert_eq!(dotdot.name(), b"\x01");
assert!(dotdot.is_special());
let made = DirectoryRecord::new(
b"README.;1",
&[],
DirectoryRef {
extent: LogicalSector(42),
size: 100,
},
FileFlags::empty(),
);
assert!(made.size() >= 33 + b"README.;1".len());
assert!(!made.is_directory());
assert_eq!(made.name(), b"README.;1");
assert_eq!(made.header().extent.read(), 42);
let mut out = Vec::new();
made.write(&mut out).expect("write");
let mut round = Cursor::new(&out[..]);
let parsed = DirectoryRecord::parse(&mut round).expect("re-parse");
assert_eq!(parsed.name(), made.name());
assert_eq!(parsed.size(), made.size());
assert_eq!(parsed.header().extent.read(), 42);
}
#[test]
fn directory_record_rejects_invalid_bounds_and_endian_copy() {
let mut short = vec![0_u8; 33];
short[0] = 32;
short[32] = 1;
assert_eq!(
DirectoryRecord::parse(&mut Cursor::new(short))
.unwrap_err()
.kind(),
io::ErrorKind::InvalidData
);
let mut mismatched = vec![0_u8; 34];
mismatched[0] = 34;
mismatched[2..6].copy_from_slice(&20_u32.to_le_bytes());
mismatched[6..10].copy_from_slice(&21_u32.to_be_bytes());
mismatched[28..30].copy_from_slice(&1_u16.to_le_bytes());
mismatched[30..32].copy_from_slice(&1_u16.to_be_bytes());
mismatched[32] = 1;
assert_eq!(
DirectoryRecord::parse(&mut Cursor::new(mismatched))
.unwrap_err()
.kind(),
io::ErrorKind::InvalidData
);
}
}
}
bitflags::bitflags! {
#[derive(Clone, Copy)]
pub struct FileFlags: u8 {
const HIDDEN = 0b0000_0001;
const DIRECTORY = 0b0000_0010;
const ASSOCIATED_FILE = 0b0000_0100;
const EXTENDED_ATTRIBUTES = 0b0000_1000;
const EXTENDED_PERMISSIONS = 0b0001_0000;
const NOT_FINAL = 0b1000_0000;
}
}