Skip to main content

hadris_iso/read/
directory.rs

1use alloc::borrow::Cow;
2use alloc::string::String;
3use alloc::vec::Vec;
4
5use super::super::directory::FileFlags;
6use super::super::directory::{DirectoryRecord, DirectoryRecordHeader, DirectoryRef};
7use super::super::io::{self, LogicalSector, Read, Seek};
8
9use super::IsoImage;
10use super::rrip::{self, RripMetadata};
11
12sync_only! {
13use core::ops::DerefMut;
14use super::super::io::{IsoCursor, SeekFrom};
15use super::rrip::collect_su_entries;
16use spin::Mutex;
17}
18
19/// A single extent of a file (sector location + byte length).
20#[derive(Debug, Clone, Copy)]
21pub struct Extent {
22    /// The `sector` field.
23    pub sector: LogicalSector,
24    /// The `length` field.
25    pub length: u32,
26}
27
28// ── IsoDir ──
29
30/// Represents IsoDir.
31pub struct IsoDir<'a, T: Read + Seek> {
32    pub(crate) image: &'a IsoImage<T>,
33    pub(crate) directory: DirectoryRef,
34}
35
36sync_only! {
37impl<'a, T: Read + Seek> IsoDir<'a, T> {
38    /// Iterate directory entries with automatic RRIP enrichment when detected.
39    pub fn entries(&self) -> IsoDirIter<'_, T> {
40        let rrip_skip = if self.image.info.susp_info.rrip_detected {
41            Some(self.image.info.susp_info.bytes_skipped)
42        } else {
43            None
44        };
45        IsoDirIter {
46            image: self.image,
47            directory: self.directory,
48            offset: 0,
49            rrip_skip,
50            pending_associated: None,
51        }
52    }
53
54    /// Iterate directory entries as raw `DirectoryRecord` without RRIP processing.
55    pub fn raw_entries(&self) -> RawDirIter<'_, T> {
56        RawDirIter {
57            reader: &self.image.data,
58            directory: self.directory,
59            offset: 0,
60        }
61    }
62}
63} // sync_only!
64
65// ── DirEntry ──
66
67/// A directory entry that may be enriched with RRIP metadata.
68///
69/// For multi-extent files (using the `NOT_FINAL` flag for files >4 GiB),
70/// `additional_extents` contains the extents beyond the first one stored
71/// in the primary `record`.
72#[derive(Debug, Clone)]
73pub struct DirEntry {
74    /// The `record` field.
75    pub record: DirectoryRecord,
76    /// The `rrip` field.
77    pub rrip: Option<RripMetadata>,
78    /// Additional extents for multi-extent files. Empty for single-extent files.
79    pub additional_extents: Vec<Extent>,
80    /// Associated file record, if one precedes this entry (ASSOCIATED_FILE flag).
81    ///
82    /// ISO 9660 allows an "associated file" record with the same identifier
83    /// to appear before the primary record. This is commonly used for
84    /// resource forks or metadata streams.
85    pub associated_file: Option<Extent>,
86}
87
88impl DirEntry {
89    #[inline]
90    /// Performs the `name` operation.
91    pub fn name(&self) -> &[u8] {
92        self.record.name()
93    }
94
95    /// Returns the display name: RRIP alternate name if available, else decoded raw name.
96    pub fn display_name(&self) -> Cow<'_, str> {
97        if let Some(ref rrip) = self.rrip
98            && let Some(ref nm) = rrip.alternate_name
99        {
100            return Cow::Borrowed(nm.as_str());
101        }
102        String::from_utf8_lossy(self.record.name())
103    }
104
105    /// Returns whether this entry matches a path component.
106    ///
107    /// Rock Ridge alternate names are matched exactly. Plain ISO 9660 names
108    /// are matched case-insensitively after removing trailing NUL bytes and a
109    /// numeric version suffix such as `;1`.
110    pub fn matches_name(&self, name: &str) -> bool {
111        if let Some(rrip_name) = self
112            .rrip
113            .as_ref()
114            .and_then(|metadata| metadata.alternate_name.as_deref())
115        {
116            return rrip_name == name;
117        }
118
119        let raw_name = self.record.name();
120        if raw_name.len().is_multiple_of(2) {
121            let units: Vec<u16> = raw_name
122                .chunks_exact(2)
123                .map(|bytes| u16::from_be_bytes([bytes[0], bytes[1]]))
124                .collect();
125            if let Ok(joliet_name) = String::from_utf16(&units) {
126                let joliet_name = match joliet_name.rsplit_once(';') {
127                    Some((base, version))
128                        if !version.is_empty()
129                            && version.bytes().all(|byte| byte.is_ascii_digit()) =>
130                    {
131                        base
132                    }
133                    _ => &joliet_name,
134                };
135                if joliet_name == name {
136                    return true;
137                }
138            }
139        }
140
141        let display_name: String = self
142            .display_name()
143            .chars()
144            .filter(|character| *character != '\0')
145            .collect();
146        let display_name = match display_name.rsplit_once(';') {
147            Some((base, version))
148                if !version.is_empty() && version.bytes().all(|b| b.is_ascii_digit()) =>
149            {
150                base
151            }
152            _ => &display_name,
153        };
154        display_name.eq_ignore_ascii_case(name)
155    }
156
157    #[inline]
158    /// Performs the `header` operation.
159    pub fn header(&self) -> &DirectoryRecordHeader {
160        self.record.header()
161    }
162
163    /// Returns true if this entry represents a directory.
164    /// CL-aware: a child link means the entry points to a directory.
165    #[inline]
166    pub fn is_directory(&self) -> bool {
167        if let Some(ref rrip) = self.rrip
168            && rrip.child_link.is_some()
169        {
170            return true;
171        }
172        self.record.is_directory()
173    }
174
175    #[inline]
176    /// Performs the `is_special` operation.
177    pub fn is_special(&self) -> bool {
178        self.record.is_special()
179    }
180
181    #[inline]
182    /// Performs the `is_file` operation.
183    pub fn is_file(&self) -> bool {
184        !self.is_directory()
185    }
186
187    /// Returns the record size (directory record length), NOT the file data size.
188    /// For the file's total data size, use [`total_size`](Self::total_size).
189    #[inline]
190    pub fn size(&self) -> usize {
191        self.record.size()
192    }
193
194    /// Returns the total file data size across all extents.
195    ///
196    /// For single-extent files this is the same as `header().data_len.read()`.
197    /// For multi-extent files this sums all extent lengths.
198    pub fn total_size(&self) -> u64 {
199        let first = self.record.header().data_len.read() as u64;
200        let rest: u64 = self
201            .additional_extents
202            .iter()
203            .map(|e| e.length as u64)
204            .sum();
205        first + rest
206    }
207
208    /// Returns true if this is a multi-extent file.
209    pub fn is_multi_extent(&self) -> bool {
210        !self.additional_extents.is_empty()
211    }
212
213    /// Returns an iterator over all extents of this file.
214    ///
215    /// The first extent comes from the primary record; additional extents
216    /// follow in order.
217    pub fn extents(&self) -> impl Iterator<Item = Extent> + '_ {
218        let header = self.record.header();
219        let first = Extent {
220            sector: LogicalSector(
221                header.extent.read() as usize + header.extended_attr_record as usize,
222            ),
223            length: header.data_len.read(),
224        };
225        core::iter::once(first).chain(self.additional_extents.iter().copied())
226    }
227
228    /// Returns true if this entry has an associated file (e.g., resource fork).
229    pub fn has_associated_file(&self) -> bool {
230        self.associated_file.is_some()
231    }
232
233    /// Returns the length of the extended attribute record in logical sectors,
234    /// or `None` if no extended attributes are present.
235    ///
236    /// When present (`extended_attr_record > 0`), the XA record is located
237    /// at the start of the file's extent (before the file data) and occupies
238    /// this many logical sectors.
239    pub fn extended_attr_len(&self) -> Option<u8> {
240        let len = self.record.header().extended_attr_record;
241        if len > 0 { Some(len) } else { None }
242    }
243
244    #[inline]
245    /// Performs the `system_use` operation.
246    pub fn system_use(&self) -> &[u8] {
247        self.record.system_use()
248    }
249}
250
251io_transform! {
252impl DirEntry {
253    /// Get a `DirectoryRef` for navigating into this directory.
254    ///
255    /// For CL entries, this follows the child link to the relocated directory.
256    /// For regular directories, it uses the ISO 9660 extent/size.
257    pub async fn as_dir_ref<DATA: Read + Seek>(
258        &self,
259        image: &IsoImage<DATA>,
260    ) -> io::Result<DirectoryRef> {
261        if let Some(ref rrip) = self.rrip
262            && let Some(cl_sector) = rrip.child_link
263        {
264            return rrip::read_dir_size(image, LogicalSector(cl_sector as usize)).await;
265        }
266        let mut directory = self
267            .record
268            .as_dir_ref()
269            .map_err(|_| io::Error::other("not a directory"))?;
270        directory.extent += self.record.header().extended_attr_record as usize;
271        Ok(directory)
272    }
273}
274} // io_transform!
275
276io_transform! {
277impl<T: Read + Seek> IsoDir<'_, T> {
278    /// Reads all entries in this directory.
279    ///
280    /// Unlike the synchronous `entries` iterator, this collection-oriented operation is
281    /// available with both synchronous and asynchronous I/O.
282    pub async fn read_entries(&self) -> io::Result<Vec<DirEntry>> {
283        const SECTOR_SIZE: usize = 2048;
284        // `directory.size` comes from an on-disk data-length field and is
285        // untrusted. Bound it against the actual image size before allocating,
286        // like `IsoImage::read_file` does.
287        let image_len = {
288            let mut data = self.image.data.lock();
289            data.seek(io::SeekFrom::End(0))
290                .await
291                .map_err(io::Error::erase)?
292        };
293        if self.directory.size as u64 > image_len {
294            return Err(io::Error::new(
295                io::ErrorKind::InvalidData,
296                "directory claims more data than the image contains",
297            ));
298        }
299        let mut bytes = alloc::vec![0_u8; self.directory.size];
300        self.image
301            .read_bytes_at(self.directory.extent.0 as u64 * SECTOR_SIZE as u64, &mut bytes)
302            .await?;
303        let mut cursor = hadris_io::Cursor::new(bytes.as_slice());
304        let mut offset = 0_usize;
305        let mut entries = Vec::new();
306        let mut associated_file = None;
307
308        while offset < bytes.len() {
309            if bytes[offset] == 0 {
310                offset = (offset / SECTOR_SIZE + 1) * SECTOR_SIZE;
311                continue;
312            }
313            cursor.seek(io::SeekFrom::Start(offset as u64)).await?;
314            let record = DirectoryRecord::parse(&mut cursor).await?;
315            offset += record.size();
316            let flags = FileFlags::from_bits_retain(record.header().flags);
317            if record.header().file_unit_size != 0 || record.header().interleave_gap_size != 0 {
318                return Err(io::Error::new(
319                    io::ErrorKind::Unsupported,
320                    "interleaved ISO entries are not supported",
321                ));
322            }
323            if flags.contains(FileFlags::ASSOCIATED_FILE) {
324                associated_file = Some(Extent {
325                    sector: LogicalSector(
326                        record.header().extent.read() as usize
327                            + record.header().extended_attr_record as usize,
328                    ),
329                    length: record.header().data_len.read(),
330                });
331                continue;
332            }
333
334            let mut additional_extents = Vec::new();
335            if flags.contains(FileFlags::NOT_FINAL) {
336                const MAX_EXTENTS: usize = 4096;
337                while additional_extents.len() < MAX_EXTENTS && offset < bytes.len() {
338                    if bytes[offset] == 0 {
339                        offset = (offset / SECTOR_SIZE + 1) * SECTOR_SIZE;
340                        continue;
341                    }
342                    cursor.seek(io::SeekFrom::Start(offset as u64)).await?;
343                    let continuation = DirectoryRecord::parse(&mut cursor).await?;
344                    offset += continuation.size();
345                    let header = continuation.header();
346                    if continuation.name() != record.name()
347                        || (header.flags ^ record.header().flags)
348                            & !FileFlags::NOT_FINAL.bits()
349                            != 0
350                        || header.volume_sequence_number.read()
351                            != record.header().volume_sequence_number.read()
352                        || header.file_unit_size != 0
353                        || header.interleave_gap_size != 0
354                    {
355                        return Err(io::Error::new(
356                            io::ErrorKind::InvalidData,
357                            "invalid ISO multi-extent continuation",
358                        ));
359                    }
360                    additional_extents.push(Extent {
361                        sector: LogicalSector(
362                            header.extent.read() as usize + header.extended_attr_record as usize,
363                        ),
364                        length: header.data_len.read(),
365                    });
366                    if !FileFlags::from_bits_retain(header.flags)
367                        .contains(FileFlags::NOT_FINAL)
368                    {
369                        break;
370                    }
371                }
372            }
373
374            let rrip = if self.image.info.susp_info.rrip_detected {
375                let fields = rrip::collect_su_entries(
376                    &record,
377                    self.image,
378                    self.image.info.susp_info.bytes_skipped,
379                )
380                .await?;
381                let metadata = RripMetadata::from_fields(&fields);
382                if metadata.is_relocated {
383                    continue;
384                }
385                Some(metadata)
386            } else {
387                None
388            };
389            entries.push(DirEntry {
390                record,
391                rrip,
392                additional_extents,
393                associated_file: associated_file.take(),
394            });
395        }
396        Ok(entries)
397    }
398
399    /// Finds an entry in this directory by its displayed name.
400    pub async fn find(&self, name: &str) -> io::Result<Option<DirEntry>> {
401        Ok(self
402            .read_entries()
403            .await?
404            .into_iter()
405            .find(|entry| entry.matches_name(name)))
406    }
407}
408} // io_transform!
409
410// ── IsoDirIter (RRIP-aware) ──
411
412sync_only! {
413
414/// Iterator over directory entries with automatic RRIP enrichment.
415///
416/// When RRIP is detected (`rrip_skip` is `Some`), each entry is enriched with
417/// parsed RRIP metadata and RE-marked entries are skipped.
418/// When RRIP is not detected, entries have `rrip: None` (zero overhead).
419pub struct IsoDirIter<'a, T: Read + Seek> {
420    image: &'a IsoImage<T>,
421    directory: DirectoryRef,
422    offset: usize,
423    rrip_skip: Option<u8>,
424    /// Pending associated file from a previous record with ASSOCIATED_FILE flag.
425    pending_associated: Option<Extent>,
426}
427
428impl<T: Read + Seek> IsoDirIter<'_, T> {
429    /// Performs the `offset` operation.
430    pub fn offset(&self) -> usize {
431        self.offset
432    }
433
434    /// After reading a record with `NOT_FINAL` set, consume subsequent
435    /// continuation records (same file identifier) until the final extent.
436    /// Returns the additional extents (not including the first/primary record).
437    fn collect_additional_extents(
438        &mut self,
439        first: &DirectoryRecord,
440    ) -> io::Result<Vec<Extent>> {
441        let mut extents = Vec::new();
442        // Depth limit to prevent infinite loops on malformed images
443        const MAX_EXTENTS: usize = 4096;
444
445        loop {
446            if extents.len() >= MAX_EXTENTS {
447                break;
448            }
449
450            let record = match self.next_raw_record() {
451                Some(Ok(r)) => r,
452                Some(Err(e)) => return Err(e),
453                None => break,
454            };
455
456            let header = record.header();
457            if record.name() != first.name()
458                || (header.flags ^ first.header().flags) & !FileFlags::NOT_FINAL.bits() != 0
459                || header.volume_sequence_number.read()
460                    != first.header().volume_sequence_number.read()
461                || header.file_unit_size != 0
462                || header.interleave_gap_size != 0
463            {
464                return Err(io::Error::new(
465                    io::ErrorKind::InvalidData,
466                    "invalid ISO multi-extent continuation",
467                ));
468            }
469            extents.push(Extent {
470                sector: LogicalSector(
471                    header.extent.read() as usize + header.extended_attr_record as usize,
472                ),
473                length: header.data_len.read(),
474            });
475
476            // If this record does NOT have NOT_FINAL, it's the last extent
477            if !FileFlags::from_bits_retain(header.flags).contains(FileFlags::NOT_FINAL) {
478                break;
479            }
480        }
481
482        Ok(extents)
483    }
484
485    /// Read the next raw DirectoryRecord from the directory data.
486    fn next_raw_record(&mut self) -> Option<io::Result<DirectoryRecord>> {
487        use super::super::io::try_io_result_option;
488        let reader = &self.image.data;
489        let mut reader = reader.lock();
490
491        const SECTOR_SIZE: usize = 2048;
492
493        loop {
494            if self.offset >= self.directory.size {
495                return None;
496            }
497
498            try_io_result_option!(reader.seek(SeekFrom::Start(
499                (self.directory.extent.0 as u64) * SECTOR_SIZE as u64 + (self.offset as u64),
500            )));
501
502            let mut len_byte = [0u8; 1];
503            try_io_result_option!(reader.read_exact(&mut len_byte));
504
505            if len_byte[0] == 0 {
506                let current_sector_offset = self.offset % SECTOR_SIZE;
507                if current_sector_offset == 0 {
508                    return None;
509                }
510                let bytes_to_skip = SECTOR_SIZE - current_sector_offset;
511                self.offset += bytes_to_skip;
512                continue;
513            }
514
515            try_io_result_option!(reader.seek(SeekFrom::Start(
516                (self.directory.extent.0 as u64) * SECTOR_SIZE as u64 + (self.offset as u64),
517            )));
518
519            let record = try_io_result_option!(DirectoryRecord::parse(reader.deref_mut()));
520            self.offset += record.size();
521
522            return Some(Ok(record));
523        }
524    }
525}
526
527impl<T: Read + Seek> Iterator for IsoDirIter<'_, T> {
528    type Item = io::Result<DirEntry>;
529
530    fn next(&mut self) -> Option<Self::Item> {
531        loop {
532            let record = match self.next_raw_record()? {
533                Ok(r) => r,
534                Err(e) => return Some(Err(e)),
535            };
536
537            let flags = FileFlags::from_bits_retain(record.header().flags);
538            if record.header().file_unit_size != 0 || record.header().interleave_gap_size != 0 {
539                return Some(Err(io::Error::new(
540                    io::ErrorKind::Unsupported,
541                    "interleaved ISO entries are not supported",
542                )));
543            }
544
545            // If this is an associated file record, save it and continue
546            // to the next record (the primary file entry).
547            if flags.contains(FileFlags::ASSOCIATED_FILE) {
548                let header = record.header();
549                self.pending_associated = Some(Extent {
550                    sector: LogicalSector(
551                        header.extent.read() as usize + header.extended_attr_record as usize,
552                    ),
553                    length: header.data_len.read(),
554                });
555                continue;
556            }
557
558            // Check for multi-extent: if NOT_FINAL is set, collect additional extents
559            let additional_extents = if flags.contains(FileFlags::NOT_FINAL) {
560                match self.collect_additional_extents(&record) {
561                    Ok(extents) => extents,
562                    Err(e) => return Some(Err(e)),
563                }
564            } else {
565                Vec::new()
566            };
567
568            let associated_file = self.pending_associated.take();
569
570            if let Some(bytes_to_skip) = self.rrip_skip {
571                // RRIP mode: enrich with metadata, skip RE entries
572                let fields = match collect_su_entries(&record, self.image, bytes_to_skip) {
573                    Ok(f) => f,
574                    Err(e) => return Some(Err(e)),
575                };
576                let rrip = RripMetadata::from_fields(&fields);
577
578                // Skip RE-marked entries (relocated directory placeholders)
579                if rrip.is_relocated {
580                    continue;
581                }
582
583                return Some(Ok(DirEntry {
584                    record,
585                    rrip: Some(rrip),
586                    additional_extents,
587                    associated_file,
588                }));
589            } else {
590                // No RRIP: return plain entry
591                return Some(Ok(DirEntry {
592                    record,
593                    rrip: None,
594                    additional_extents,
595                    associated_file,
596                }));
597            }
598        }
599    }
600}
601
602// ── RawDirIter ──
603
604/// Iterator over raw directory records without RRIP processing.
605pub struct RawDirIter<'a, T: Read + Seek> {
606    pub(crate) reader: &'a Mutex<IsoCursor<T>>,
607    pub(crate) directory: DirectoryRef,
608    pub(crate) offset: usize,
609}
610
611impl<T: Read + Seek> RawDirIter<'_, T> {
612    /// Performs the `offset` operation.
613    pub fn offset(&self) -> usize {
614        self.offset
615    }
616}
617
618impl<T: Read + Seek> Iterator for RawDirIter<'_, T> {
619    type Item = io::Result<DirectoryRecord>;
620    fn next(&mut self) -> Option<Self::Item> {
621        use super::super::io::try_io_result_option;
622        let mut reader = self.reader.lock();
623
624        const SECTOR_SIZE: usize = 2048;
625
626        loop {
627            if self.offset >= self.directory.size {
628                return None;
629            }
630
631            try_io_result_option!(reader.seek(SeekFrom::Start(
632                (self.directory.extent.0 as u64) * SECTOR_SIZE as u64 + (self.offset as u64),
633            )));
634
635            let mut len_byte = [0u8; 1];
636            try_io_result_option!(reader.read_exact(&mut len_byte));
637
638            if len_byte[0] == 0 {
639                let current_sector_offset = self.offset % SECTOR_SIZE;
640                if current_sector_offset == 0 {
641                    return None;
642                }
643                let bytes_to_skip = SECTOR_SIZE - current_sector_offset;
644                self.offset += bytes_to_skip;
645                continue;
646            }
647
648            try_io_result_option!(reader.seek(SeekFrom::Start(
649                (self.directory.extent.0 as u64) * SECTOR_SIZE as u64 + (self.offset as u64),
650            )));
651
652            let record = try_io_result_option!(DirectoryRecord::parse(reader.deref_mut()));
653            self.offset += record.size();
654
655            return Some(Ok(record));
656        }
657    }
658}
659
660} // sync_only!