Skip to main content

hadris_iso/
directory.rs

1use super::io::{self, Read, Write};
2use bytemuck::Zeroable;
3
4use super::io::LogicalSector;
5use crate::types::{U16LsbMsb, U32LsbMsb};
6
7/// The header of a directory record, because the identifier is variable length
8/// (ECMA-119 9.1 fixed fields).
9///
10/// @hadris-spec ECMA-119:9.1
11/// @hadris-compliance partial
12/// @hadris-note Fixed fields round-trip, but all identifier, flag, and semantic constraints are not yet validated.
13/// @hadris-tests directory::tests::directory_record_parse_roundtrip
14/// @hadris-fuzz iso_read
15#[repr(C)]
16#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
17pub struct DirectoryRecordHeader {
18    /// The `len` field.
19    pub len: u8,
20    /// The `extended_attr_record` field.
21    pub extended_attr_record: u8,
22    /// The LBA of the record
23    pub extent: U32LsbMsb,
24    /// The length of the data in bytes
25    pub data_len: U32LsbMsb,
26    /// The `date_time` field.
27    pub date_time: DirDateTime,
28    /// The `flags` field.
29    pub flags: u8,
30    /// The `file_unit_size` field.
31    pub file_unit_size: u8,
32    /// The `interleave_gap_size` field.
33    pub interleave_gap_size: u8,
34    /// The `volume_sequence_number` field.
35    pub volume_sequence_number: U16LsbMsb,
36    /// The `file_identifier_len` field.
37    pub file_identifier_len: u8,
38}
39
40impl Default for DirectoryRecordHeader {
41    fn default() -> Self {
42        Self {
43            len: 0,
44            extended_attr_record: 0,
45            extent: U32LsbMsb::new(0),
46            data_len: U32LsbMsb::new(0),
47            date_time: DirDateTime::now(),
48            flags: 0,
49            file_unit_size: 0,
50            interleave_gap_size: 0,
51            volume_sequence_number: U16LsbMsb::new(0),
52            file_identifier_len: 0,
53        }
54    }
55}
56
57impl DirectoryRecordHeader {
58    /// Performs the `from_bytes` operation.
59    pub fn from_bytes(bytes: &[u8]) -> &Self {
60        bytemuck::from_bytes(bytes)
61    }
62
63    /// Performs the `to_bytes` operation.
64    pub fn to_bytes(&self) -> &[u8] {
65        bytemuck::bytes_of(self)
66    }
67
68    /// Performs the `is_directory` operation.
69    pub fn is_directory(&self) -> bool {
70        FileFlags::from_bits_retain(self.flags).contains(FileFlags::DIRECTORY)
71    }
72}
73
74/// Directory Record (ECMA-119 9.1) — header plus variable identifier / system use.
75///
76/// @hadris-spec ECMA-119:9.1
77/// @hadris-compliance partial
78/// @hadris-tests directory::tests::directory_record_parse_roundtrip
79/// @hadris-fuzz iso_read
80/// @hadris-note Joliet+RRIP coexistence on read may hide one namespace; see crate Known Limitations
81#[repr(transparent)]
82#[derive(Debug, Clone, Copy, bytemuck::Zeroable, bytemuck::Pod)]
83pub struct DirectoryRecord {
84    data: [u8; 256],
85}
86
87impl Default for DirectoryRecord {
88    fn default() -> Self {
89        bytemuck::Zeroable::zeroed()
90    }
91}
92
93/// Error returned when trying to access a file as a directory
94#[derive(Debug, Clone, Copy)]
95pub struct NotADirectoryError;
96
97impl core::fmt::Display for NotADirectoryError {
98    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
99        write!(f, "not a directory")
100    }
101}
102
103#[cfg(feature = "std")]
104impl std::error::Error for NotADirectoryError {}
105
106impl DirectoryRecord {
107    const DATA_START: usize = size_of::<DirectoryRecordHeader>();
108
109    #[inline]
110    /// Returns the directory record header.
111    pub fn header(&self) -> &DirectoryRecordHeader {
112        bytemuck::from_bytes(&self.data[0..Self::DATA_START])
113    }
114
115    #[inline]
116    /// Returns the total length of this directory record.
117    #[allow(dead_code)]
118    pub(crate) fn len(&self) -> u8 {
119        let header = self.header();
120        header.len
121    }
122
123    /// Returns whether this is a sector-padding record.
124    #[allow(dead_code)]
125    pub(crate) fn is_empty(&self) -> bool {
126        self.len() == 0
127    }
128
129    #[inline]
130    /// Returns the file flags (directory, hidden, etc.).
131    #[allow(dead_code)]
132    pub(crate) fn flags(&self) -> FileFlags {
133        let header = self.header();
134        FileFlags::from_bits_truncate(header.flags)
135    }
136
137    #[inline]
138    /// Returns a mutable reference to the header.
139    pub fn header_mut(&mut self) -> &mut DirectoryRecordHeader {
140        bytemuck::from_bytes_mut(&mut self.data[0..size_of::<DirectoryRecordHeader>()])
141    }
142
143    #[inline]
144    /// Returns the file or directory name bytes.
145    pub fn name(&self) -> &[u8] {
146        let len = self.header().file_identifier_len as usize;
147        &self.data[Self::DATA_START..Self::DATA_START + len]
148    }
149
150    /// Get the filename decoded from Joliet (UTF-16 BE) encoding
151    ///
152    /// This is useful when reading from a Joliet supplementary volume descriptor.
153    /// Returns the decoded Unicode string.
154    #[cfg(feature = "alloc")]
155    pub fn joliet_name(&self) -> alloc::string::String {
156        crate::joliet::decode_joliet_name(self.name())
157    }
158
159    /// Check if this entry's name appears to be Joliet-encoded (UTF-16 BE)
160    #[cfg(feature = "alloc")]
161    pub fn is_joliet_name(&self) -> bool {
162        crate::joliet::is_likely_joliet_name(self.name())
163    }
164
165    #[inline]
166    /// Performs the `system_use` operation.
167    pub fn system_use(&self) -> &[u8] {
168        let header = self.header();
169        // ISO 9660 requires a padding byte after the file identifier when its
170        // length is even, so the system use area always starts at an even offset.
171        let su_start = (Self::DATA_START + header.file_identifier_len as usize + 1) & !1;
172        if su_start >= header.len as usize {
173            return &[];
174        }
175        &self.data[su_start..header.len as usize]
176    }
177
178    sync_only! {
179        /// Returns the mutable system-use area needed by the synchronous writer.
180        #[cfg(feature = "write")]
181        pub(crate) fn system_use_mut(&mut self) -> &mut [u8] {
182            let header = self.header();
183            let name_len = header.file_identifier_len as usize;
184            let start = (Self::DATA_START + name_len + 1) & !1;
185            let end = header.len as usize;
186            &mut self.data[start..end]
187        }
188    }
189
190    #[inline]
191    /// Performs the `is_special` operation.
192    pub fn is_special(&self) -> bool {
193        self.name() == b"\x00" || self.name() == b"\x01"
194    }
195
196    #[inline]
197    /// Performs the `is_directory` operation.
198    pub fn is_directory(&self) -> bool {
199        self.header().is_directory()
200    }
201
202    /// Performs the `is_file` operation.
203    pub fn is_file(&self) -> bool {
204        !self.header().is_directory()
205    }
206
207    /// Performs the `as_dir_ref` operation.
208    pub fn as_dir_ref(&self) -> Result<DirectoryRef, NotADirectoryError> {
209        if !self.is_directory() {
210            return Err(NotADirectoryError);
211        }
212
213        let header = self.header();
214        Ok(DirectoryRef {
215            extent: LogicalSector(header.extent.read() as usize),
216            size: header.data_len.read() as usize,
217        })
218    }
219
220    /// Performs the `size` operation.
221    pub fn size(&self) -> usize {
222        self.header().len as usize
223    }
224
225    /// Performs the `to_bytes` operation.
226    pub fn to_bytes(&self) -> &[u8] {
227        &self.data[0..self.size()]
228    }
229
230    /// Performs the `new` operation.
231    pub fn new(name: &[u8], system_use: &[u8], directory: DirectoryRef, flags: FileFlags) -> Self {
232        let mut sel = Self::zeroed();
233        assert!(
234            !name.is_empty() && name.len() <= u8::MAX as usize,
235            "ISO directory-record identifier must contain 1..=255 bytes"
236        );
237        // ISO 9660 (ECMA-119 9.1): a padding byte follows the file identifier
238        // when its length is even, so the system use area starts at an even offset.
239        let su_start = (Self::DATA_START + name.len() + 1) & !1;
240        let total = su_start + system_use.len();
241        // Record length must be even (ECMA-119 7.1.1).
242        let record_len = (total + 1) & !1;
243        assert!(
244            record_len <= 255,
245            "DirectoryRecord too large: {} bytes (name={}, su={})",
246            record_len,
247            name.len(),
248            system_use.len()
249        );
250        *sel.header_mut() = DirectoryRecordHeader {
251            len: record_len as u8,
252            extended_attr_record: 0,
253            extent: U32LsbMsb::new(directory.extent.0 as u32),
254            data_len: U32LsbMsb::new(directory.size as u32),
255            date_time: DirDateTime::now(),
256            flags: flags.bits(),
257            file_unit_size: 0,
258            interleave_gap_size: 0,
259            volume_sequence_number: U16LsbMsb::new(1),
260            file_identifier_len: name.len() as u8,
261        };
262        sel.data[Self::DATA_START..Self::DATA_START + name.len()].copy_from_slice(name);
263        // Padding byte (if any) is already zero from zeroed().
264        sel.data[su_start..su_start + system_use.len()].copy_from_slice(system_use);
265        sel
266    }
267
268    /// Performs the `with_len` operation.
269    pub fn with_len(name_len: usize, su_len: usize) -> Self {
270        let mut sel = Self::zeroed();
271        assert!(
272            (1..=u8::MAX as usize).contains(&name_len),
273            "ISO directory-record identifier must contain 1..=255 bytes"
274        );
275        let su_start = (Self::DATA_START + name_len + 1) & !1;
276        let total = su_start + su_len;
277        let record_len = (total + 1) & !1;
278        assert!(record_len <= u8::MAX as usize, "DirectoryRecord too large");
279        sel.header_mut().len = record_len as u8;
280        sel
281    }
282}
283
284io_transform! {
285impl DirectoryRecord {
286    /// Performs the `parse` operation.
287    pub async fn parse<R: Read>(reader: &mut R) -> io::Result<Self> {
288        let mut sel = Self::zeroed();
289        reader.read_exact(&mut sel.data[0..Self::DATA_START]).await?;
290        let size = sel.size();
291        if size == 0 {
292            return Ok(sel);
293        }
294        let name_len = sel.header().file_identifier_len as usize;
295        if size < Self::DATA_START + 1
296            || !size.is_multiple_of(2)
297            || Self::DATA_START + name_len > size
298            || sel.header().flags & 0b0110_0000 != 0
299            || !sel.header().extent.is_consistent()
300            || !sel.header().data_len.is_consistent()
301            || !sel.header().volume_sequence_number.is_consistent()
302        {
303            return Err(io::Error::new(
304                io::ErrorKind::InvalidData,
305                "invalid ISO directory record",
306            ));
307        }
308        if size > Self::DATA_START {
309            reader.read_exact(&mut sel.data[Self::DATA_START..size]).await?;
310        }
311        if name_len.is_multiple_of(2) {
312            let padding = Self::DATA_START + name_len;
313            if padding >= size || sel.data[padding] != 0 {
314                return Err(io::Error::new(
315                    io::ErrorKind::InvalidData,
316                    "invalid ISO directory-record identifier padding",
317                ));
318            }
319        }
320        Ok(sel)
321    }
322
323    /// Performs the `write` operation.
324    pub async fn write<W: Write>(&self, writer: &mut W) -> io::Result<usize> {
325        let size = self.size();
326        writer.write_all(&self.data[0..size]).await?;
327        Ok(size)
328    }
329}
330} // io_transform!
331
332/// The root directory entry
333#[repr(C)]
334#[derive(Default, Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
335pub struct RootDirectoryEntry {
336    /// The `header` field.
337    pub header: DirectoryRecordHeader,
338    /// There is no name on the root directory, so this is always empty
339    pub padding: u8,
340}
341
342#[repr(C)]
343#[derive(Default, Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
344/// Represents DirDateTime.
345pub struct DirDateTime {
346    /// Number of years since 1900
347    year: u8,
348    month: u8,
349    day: u8,
350    hour: u8,
351    minute: u8,
352    second: u8,
353    offset: u8,
354}
355
356impl DirDateTime {
357    #[cfg(feature = "std")]
358    /// Performs the `now` operation.
359    pub fn now() -> Self {
360        use chrono::{Datelike, Timelike, Utc};
361        let now = Utc::now();
362        Self {
363            year: (now.year() - 1900) as u8,
364            month: now.month() as u8,
365            day: now.day() as u8,
366            hour: now.hour() as u8,
367            minute: now.minute() as u8,
368            second: now.second() as u8,
369            // UTC offset is always 0
370            offset: 0,
371        }
372    }
373
374    /// Creates a zeroed datetime for no-std environments
375    #[cfg(not(feature = "std"))]
376    pub fn now() -> Self {
377        Self::default()
378    }
379}
380
381/// Reference to a file or directory location on the ISO image.
382///
383/// Points to where the data starts (extent) and how large it is.
384#[derive(Default, Debug, Clone, Copy)]
385pub struct DirectoryRef {
386    /// The `extent` field.
387    pub extent: LogicalSector,
388    /// The `size` field.
389    pub size: usize,
390}
391
392impl DirectoryRef {
393    /// Creates a new directory reference.
394    #[allow(dead_code)]
395    pub(crate) const fn new(extent: usize, size: usize) -> Self {
396        Self {
397            extent: LogicalSector(extent),
398            size,
399        }
400    }
401}
402
403sync_only! {
404#[cfg(test)]
405mod tests {
406    use super::*;
407    use alloc::vec;
408    use alloc::vec::Vec;
409    use std::io::Cursor;
410
411    /// Vertical slice for ECMA-119:9.1 — parse `.` / `..` records from a sector.
412    #[test]
413    fn directory_record_parse_roundtrip() {
414        // Minimal root directory sector (same layout as comprehensive_iso helper)
415        let mut dir = vec![0u8; 2048];
416        let mut offset = 0usize;
417        for (id, flags) in [(0x00u8, 0x02u8), (0x01u8, 0x02u8)] {
418            dir[offset] = 34;
419            dir[offset + 2..offset + 6].copy_from_slice(&20u32.to_le_bytes());
420            dir[offset + 6..offset + 10].copy_from_slice(&20u32.to_be_bytes());
421            dir[offset + 10..offset + 14].copy_from_slice(&2048u32.to_le_bytes());
422            dir[offset + 14..offset + 18].copy_from_slice(&2048u32.to_be_bytes());
423            dir[offset + 25] = flags;
424            dir[offset + 28..offset + 30].copy_from_slice(&1u16.to_le_bytes());
425            dir[offset + 30..offset + 32].copy_from_slice(&1u16.to_be_bytes());
426            dir[offset + 32] = 1;
427            dir[offset + 33] = id;
428            offset += 34;
429        }
430
431        let mut cursor = Cursor::new(&dir[..]);
432        let dot = DirectoryRecord::parse(&mut cursor).expect("parse .");
433        assert_eq!(dot.size(), 34);
434        assert_eq!(core::mem::size_of::<DirectoryRecordHeader>(), 33);
435        assert!(dot.is_directory());
436        assert_eq!(dot.name(), b"\x00");
437        assert_eq!(dot.header().extent.read(), 20);
438        assert_eq!(dot.header().data_len.read(), 2048);
439
440        let dotdot = DirectoryRecord::parse(&mut cursor).expect("parse ..");
441        assert_eq!(dotdot.name(), b"\x01");
442        assert!(dotdot.is_special());
443
444        // new() + write/parse roundtrip for a file identifier
445        let made = DirectoryRecord::new(
446            b"README.;1",
447            &[],
448            DirectoryRef {
449                extent: LogicalSector(42),
450                size: 100,
451            },
452            FileFlags::empty(),
453        );
454        assert!(made.size() >= 33 + b"README.;1".len());
455        assert!(!made.is_directory());
456        assert_eq!(made.name(), b"README.;1");
457        assert_eq!(made.header().extent.read(), 42);
458
459        let mut out = Vec::new();
460        made.write(&mut out).expect("write");
461        let mut round = Cursor::new(&out[..]);
462        let parsed = DirectoryRecord::parse(&mut round).expect("re-parse");
463        assert_eq!(parsed.name(), made.name());
464        assert_eq!(parsed.size(), made.size());
465        assert_eq!(parsed.header().extent.read(), 42);
466    }
467
468    #[test]
469    fn directory_record_rejects_invalid_bounds_and_endian_copy() {
470        let mut short = vec![0_u8; 33];
471        short[0] = 32;
472        short[32] = 1;
473        assert_eq!(
474            DirectoryRecord::parse(&mut Cursor::new(short))
475                .unwrap_err()
476                .kind(),
477            io::ErrorKind::InvalidData
478        );
479
480        let mut mismatched = vec![0_u8; 34];
481        mismatched[0] = 34;
482        mismatched[2..6].copy_from_slice(&20_u32.to_le_bytes());
483        mismatched[6..10].copy_from_slice(&21_u32.to_be_bytes());
484        mismatched[28..30].copy_from_slice(&1_u16.to_le_bytes());
485        mismatched[30..32].copy_from_slice(&1_u16.to_be_bytes());
486        mismatched[32] = 1;
487        assert_eq!(
488            DirectoryRecord::parse(&mut Cursor::new(mismatched))
489                .unwrap_err()
490                .kind(),
491            io::ErrorKind::InvalidData
492        );
493    }
494}
495}
496
497bitflags::bitflags! {
498    /// Flags stored in an ISO 9660 directory record.
499    #[derive(Debug, Clone, Copy)]
500    pub struct FileFlags: u8 {
501        /// Entry is hidden from ordinary directory listings.
502        const HIDDEN = 0b0000_0001;
503        /// Entry identifies a directory.
504        const DIRECTORY = 0b0000_0010;
505        /// Entry is an associated file.
506        const ASSOCIATED_FILE = 0b0000_0100;
507        /// Entry includes an extended-attribute record.
508        const EXTENDED_ATTRIBUTES = 0b0000_1000;
509        /// Entry has extended permission information.
510        const EXTENDED_PERMISSIONS = 0b0001_0000;
511        /// Entry continues in another directory record.
512        const NOT_FINAL = 0b1000_0000;
513    }
514}