Skip to main content

hadris_iso/read/
mod.rs

1use super::directory::DirectoryRef;
2use super::io::{self, IsoCursor, LogicalSector, Read, Seek};
3use super::path::{PathTableEntry, PathTableInfo, PathTableRef};
4use super::volume::{PrimaryVolumeDescriptor, VolumeDescriptorList};
5use crate::file::EntryType;
6use crate::joliet::JolietLevel;
7use hadris_common::types::endian::Endian;
8#[cfg(not(feature = "alloc"))]
9use hadris_common::types::no_alloc::ArrayVec;
10use hadris_path::{Component, Separators, VPath};
11pub use volume::VolumeDescriptorIter;
12
13mod boot;
14mod directory;
15pub use boot::*;
16pub use directory::{DirEntry, Extent, IsoDir};
17sync_only! {
18    pub use directory::{IsoDirIter, RawDirIter};
19}
20use spin::Mutex;
21
22mod rrip;
23pub(crate) use rrip::SuspInfo;
24pub use rrip::*;
25
26mod volume;
27
28/// Identifies a FilenameType value.
29pub enum FilenameType {
30    /// The `Builtin` variant.
31    Builtin,
32    /// The `Joliet` variant.
33    Joliet,
34}
35
36#[derive(Debug)]
37/// Represents IsoImageInfo.
38pub struct IsoImageInfo {
39    block_size: usize,
40    sector_size: usize,
41    root_dirs: RootDirs,
42    boot_catalog: Option<u32>,
43    path_table: PathTableRef,
44    pub(crate) susp_info: SuspInfo,
45    /// True if an ISO 9660:1999 Enhanced Volume Descriptor (EVD) was found.
46    has_evd: bool,
47    /// Cached path table entries, parsed once during open() to avoid
48    /// repeated seeks for directory hierarchy traversal.
49    #[cfg(feature = "alloc")]
50    path_table_cache: alloc::vec::Vec<PathTableEntry>,
51}
52
53impl IsoImageInfo {
54    /// Returns the logical block size declared by the selected volume descriptor.
55    pub fn block_size(&self) -> usize {
56        self.block_size
57    }
58
59    /// Returns the physical sector size used by the image reader.
60    pub fn sector_size(&self) -> usize {
61        self.sector_size
62    }
63}
64
65#[derive(Debug)]
66/// Represents RootDirs.
67pub struct RootDirs {
68    #[cfg(not(feature = "alloc"))]
69    dirs: ArrayVec<RootDir, 8>,
70    #[cfg(feature = "alloc")]
71    dirs: alloc::vec::Vec<RootDir>,
72}
73
74impl RootDirs {
75    fn new() -> Self {
76        Self {
77            #[cfg(not(feature = "alloc"))]
78            dirs: ArrayVec::new(),
79            #[cfg(feature = "alloc")]
80            dirs: alloc::vec::Vec::new(),
81        }
82    }
83
84    /// Returns the number of directory-tree namespaces in the image.
85    pub fn len(&self) -> usize {
86        self.dirs.len()
87    }
88
89    /// Returns whether the image contains no directory-tree namespaces.
90    pub fn is_empty(&self) -> bool {
91        self.dirs.is_empty()
92    }
93
94    /// Iterates over every directory-tree namespace in descriptor order.
95    pub fn iter(&self) -> core::slice::Iter<'_, RootDir> {
96        self.dirs.iter()
97    }
98
99    /// Finds the root whose entry type exactly matches `ty`.
100    pub fn get(&self, ty: EntryType) -> Option<RootDir> {
101        self.dirs.iter().copied().find(|root| root.ty == ty)
102    }
103
104    /// Selects the most useful directory-tree namespace, if one exists.
105    pub fn try_best_choice(&self) -> Option<RootDir> {
106        if self.dirs.is_empty() {
107            return None;
108        }
109        let mut best = (0, EntryType::default());
110        for (idx, dir) in self.dirs.iter().enumerate() {
111            if dir.ty > best.1 {
112                best = (idx, dir.ty);
113            }
114        }
115        Some(self.dirs[best.0])
116    }
117
118    /// Selects the most useful directory-tree namespace.
119    ///
120    /// # Panics
121    ///
122    /// Panics if the ISO image contains no directory trees. Use
123    /// [`Self::try_best_choice`] when handling a potentially empty collection.
124    pub fn best_choice(&self) -> RootDir {
125        self.try_best_choice()
126            .expect("ISO image contains no directory trees!")
127    }
128}
129
130impl<'a> IntoIterator for &'a RootDirs {
131    type Item = &'a RootDir;
132    type IntoIter = core::slice::Iter<'a, RootDir>;
133
134    fn into_iter(self) -> Self::IntoIter {
135        self.iter()
136    }
137}
138
139#[derive(Debug, Default, Clone, Copy)]
140/// Represents RootDir.
141pub struct RootDir {
142    ty: EntryType,
143    dir_ref: DirectoryRef,
144}
145
146impl RootDir {
147    /// Returns the filename namespace represented by this root.
148    pub fn entry_type(&self) -> EntryType {
149        self.ty
150    }
151
152    /// Performs the `iter` operation.
153    pub fn iter<'a, DATA: Read + Seek>(&self, iso: &'a IsoImage<DATA>) -> IsoDir<'a, DATA> {
154        IsoDir {
155            image: iso,
156            directory: self.dir_ref,
157        }
158    }
159
160    /// Returns the underlying `DirectoryRef` for this root directory.
161    pub fn dir_ref(&self) -> DirectoryRef {
162        self.dir_ref
163    }
164}
165
166#[repr(u8)]
167#[derive(Debug, Clone, Copy, PartialEq, Eq)]
168/// Identifies a PathSeparator value.
169pub enum PathSeparator {
170    /// The `ForwardSlash` variant.
171    ForwardSlash = b'/',
172    /// The `Backslash` variant.
173    Backslash = b'\\',
174}
175
176impl PathSeparator {
177    /// Performs the `as_char` operation.
178    pub fn as_char(self) -> char {
179        self as u8 as char
180    }
181}
182
183/// A struct representing an open ISO9660 Image
184///
185/// This struct is interior mutable.
186#[derive(Debug)]
187pub struct IsoImage<DATA: Seek> {
188    pub(crate) data: Mutex<IsoCursor<DATA>>,
189    pub(crate) info: IsoImageInfo,
190}
191
192impl<DATA: Seek> IsoImage<DATA> {
193    /// Consumes the image handle and returns its underlying data source.
194    pub fn into_inner(self) -> DATA {
195        self.data.into_inner().data
196    }
197}
198
199io_transform! {
200impl<DATA: Read + Seek> IsoImage<DATA> {
201    /// Opens a ISO9660 Image
202    pub async fn open(data: DATA) -> io::Result<Self> {
203        let sector_size = 2048;
204        let mut data = IsoCursor::new(data, sector_size);
205        data.seek_sector(LogicalSector(16)).await?;
206        let mut root_dirs = RootDirs::new();
207        let volume_descriptors = VolumeDescriptorList::parse(&mut data).await?;
208        let pvd = volume_descriptors.try_primary().ok_or_else(|| {
209            io::Error::new(
210                io::ErrorKind::InvalidData,
211                "volume descriptor sequence has no primary descriptor",
212            )
213        })?;
214        if !pvd.volume_space_size.is_consistent()
215            || !pvd.volume_set_size.is_consistent()
216            || !pvd.volume_sequence_number.is_consistent()
217            || !pvd.logical_block_size.is_consistent()
218            || !pvd.path_table_size.is_consistent()
219            || !pvd.dir_record.header.extent.is_consistent()
220            || !pvd.dir_record.header.data_len.is_consistent()
221            || !pvd
222                .dir_record
223                .header
224                .volume_sequence_number
225                .is_consistent()
226        {
227            return Err(io::Error::new(
228                io::ErrorKind::InvalidData,
229                "primary volume descriptor has inconsistent redundant endian fields",
230            ));
231        }
232        let block_size = pvd.logical_block_size.read() as usize;
233        // IsoImage's extent→byte math assumes a 2048-byte logical block. Rather
234        // than silently misread an image that declares a different block size,
235        // reject it. (The allocation-free `IsoReader` honors the PVD block size;
236        // full non-2048 support in `IsoImage` is future work.)
237        if block_size != 2048 {
238            return Err(io::Error::new(
239                io::ErrorKind::Unsupported,
240                "ISO logical block size other than 2048 is not supported by IsoImage",
241            ));
242        }
243        let root_extent = LogicalSector(
244            pvd.dir_record.header.extent.read() as usize
245                + pvd.dir_record.header.extended_attr_record as usize,
246        );
247        let root_size = pvd.dir_record.header.data_len.read() as usize;
248        let root_dir = DirectoryRef {
249            extent: root_extent,
250            size: root_size,
251        };
252
253        // Detect SUSP/RRIP from root directory's "." entry
254        let susp_info = rrip::detect_susp_rrip(&mut data, root_extent).await?;
255        let supports_rrip = susp_info.rrip_detected;
256
257        root_dirs.dirs.push(RootDir {
258            ty: EntryType::Level1 {
259                supports_lowercase: false,
260                supports_rrip,
261            },
262            dir_ref: root_dir,
263        });
264
265        let path_table = PathTableRef {
266            lpt: LogicalSector(pvd.type_l_path_table.get() as usize),
267            mpt: LogicalSector(pvd.type_m_path_table.get() as usize),
268            size: pvd.path_table_size.read() as u64,
269        };
270
271        // Parse and cache path table entries
272        #[cfg(feature = "alloc")]
273        let path_table_cache = {
274            use crate::types::EndianType;
275            let pt_start = if cfg!(target_endian = "little") {
276                path_table.lpt
277            } else {
278                path_table.mpt
279            };
280            let start_byte = pt_start.0 as u64 * sector_size as u64;
281            let end_byte = start_byte + path_table.size;
282            data.seek(super::io::SeekFrom::Start(start_byte))
283                .await
284                .map_err(super::io::Error::erase)?;
285            let mut entries = alloc::vec::Vec::new();
286            let mut pos = start_byte;
287            while pos < end_byte {
288                let entry = PathTableEntry::parse(&mut data, EndianType::NativeEndian).await?;
289                pos += entry.size() as u64;
290                entries.push(entry);
291            }
292            entries
293        };
294
295        let mut info = IsoImageInfo {
296            block_size,
297            sector_size,
298            root_dirs,
299            boot_catalog: None,
300            path_table,
301            susp_info,
302            has_evd: false,
303            #[cfg(feature = "alloc")]
304            path_table_cache,
305        };
306
307        for svd in volume_descriptors.supplementary() {
308            if svd.header.version == 1 {
309                // Joliet Check
310                for &level in JolietLevel::all() {
311                    if svd.escape_sequences == level.escape_sequence() {
312                        info.root_dirs.dirs.push(RootDir {
313                            ty: EntryType::Joliet {
314                                level,
315                                supports_rrip: false,
316                            },
317                            dir_ref: DirectoryRef {
318                                extent: LogicalSector(
319                                    svd.dir_record.header.extent.read() as usize
320                                        + svd.dir_record.header.extended_attr_record as usize,
321                                ),
322                                size: svd.dir_record.header.data_len.read() as usize,
323                            },
324                        });
325                    }
326                }
327            } else if svd.file_structure_version == 2 {
328                // ISO 9660:1999 enhanced namespace (ECMA-119, 3rd edition).
329                info.has_evd = true;
330                info.root_dirs.dirs.push(RootDir {
331                    ty: EntryType::Level3 {
332                        supports_lowercase: true,
333                        supports_rrip: false,
334                    },
335                    dir_ref: DirectoryRef {
336                        extent: LogicalSector(
337                            svd.dir_record.header.extent.read() as usize
338                                + svd.dir_record.header.extended_attr_record as usize,
339                        ),
340                        size: svd.dir_record.header.data_len.read() as usize,
341                    },
342                });
343            }
344        }
345
346        if let Some(boot_record) = volume_descriptors.boot_record() {
347            info.boot_catalog = Some(boot_record.catalog_ptr.get());
348        }
349
350        Ok(Self {
351            data: Mutex::new(data),
352            info,
353        })
354    }
355
356    /// Read raw bytes from an absolute byte position in the image.
357    pub async fn read_bytes_at(&self, byte_offset: u64, buf: &mut [u8]) -> io::Result<()> {
358        let mut data = self.data.lock();
359        data.seek(super::io::SeekFrom::Start(byte_offset))
360            .await
361            .map_err(super::io::Error::erase)?;
362        data.read_exact(buf).await?;
363        Ok(())
364    }
365
366    /// Finds an entry by a slash- or backslash-delimited path.
367    ///
368    /// Leading and repeated separators and `.` components are ignored. The
369    /// root itself has no directory entry, so an empty or root-only path
370    /// returns `None`. Parent (`..`) components are rejected.
371    pub async fn find_path(&self, path: &str) -> io::Result<Option<DirEntry>> {
372        let mut components = VPath::with_separators(path, Separators::SlashOrBackslash)
373            .components()
374            .filter_map(|component| match component {
375                Component::Root | Component::Current => None,
376                Component::Parent => Some(Err(io::Error::other(
377                    "parent path components are not supported",
378                ))),
379                Component::Normal(component) => Some(Ok(component)),
380            })
381            .peekable();
382        let mut directory = self.open_dir(self.root_dir().dir_ref());
383
384        while let Some(component) = components.next() {
385            let component = component?;
386            let Some(entry) = directory.find(component).await? else {
387                return Ok(None);
388            };
389            if components.peek().is_none() {
390                return Ok(Some(entry));
391            }
392            if !entry.is_directory() {
393                return Ok(None);
394            }
395            directory = self.open_dir(entry.as_dir_ref(self).await?);
396        }
397        Ok(None)
398    }
399
400    /// Read the complete contents of a file, handling multi-extent files.
401    ///
402    /// For single-extent files, this reads from the entry's extent.
403    /// For multi-extent files (using `NOT_FINAL` flag), this reads and
404    /// concatenates all extents in order.
405    #[cfg(feature = "alloc")]
406    pub async fn read_file(&self, entry: &directory::DirEntry) -> io::Result<alloc::vec::Vec<u8>> {
407        if entry.header().file_unit_size != 0 || entry.header().interleave_gap_size != 0 {
408            return Err(io::Error::new(
409                io::ErrorKind::Unsupported,
410                "interleaved ISO files are not supported",
411            ));
412        }
413        let total = entry.total_size();
414        // `total` comes from on-disk directory-record data-length fields (u32 each,
415        // summed across extents) and is untrusted. Bound it against the actual
416        // image size before allocating, otherwise a tiny image whose record claims
417        // ~4 GiB would force that allocation up front — a DoS that aborts the
418        // process on no-overcommit / embedded targets.
419        let image_len = {
420            let mut data = self.data.lock();
421            data.seek(super::io::SeekFrom::End(0))
422                .await
423                .map_err(super::io::Error::erase)?
424        };
425        if total > image_len {
426            return Err(io::Error::new(
427                io::ErrorKind::InvalidData,
428                "directory entry claims more data than the image contains",
429            ));
430        }
431        let mut buf = alloc::vec![0u8; total as usize];
432
433        if entry.is_multi_extent() {
434            let mut offset = 0usize;
435            for extent in entry.extents() {
436                let len = extent.length as usize;
437                let byte_offset = extent.sector.0 as u64 * 2048;
438                self.read_bytes_at(byte_offset, &mut buf[offset..offset + len]).await?;
439                offset += len;
440            }
441        } else {
442            let header = entry.header();
443            let byte_offset = (header.extent.read() as u64
444                + header.extended_attr_record as u64)
445                * 2048;
446            let len = header.data_len.read() as usize;
447            self.read_bytes_at(byte_offset, &mut buf[..len]).await?;
448        }
449
450        Ok(buf)
451    }
452}
453} // io_transform!
454
455impl<DATA: Read + Seek> IsoImage<DATA> {
456    /// Performs the `root_dir` operation.
457    pub fn root_dir(&self) -> RootDir {
458        self.root_dirs().best_choice()
459    }
460
461    /// Performs the `root_dirs` operation.
462    pub fn root_dirs(&self) -> &RootDirs {
463        &self.info.root_dirs
464    }
465
466    /// Returns whether RRIP (Rock Ridge) extensions were detected in the image.
467    pub fn supports_rrip(&self) -> bool {
468        self.info.susp_info.rrip_detected
469    }
470
471    /// Returns whether an ISO 9660:1999 Enhanced Volume Descriptor was found.
472    pub fn has_evd(&self) -> bool {
473        self.info.has_evd
474    }
475
476    /// Open a directory by its `DirectoryRef`, enabling navigation into subdirectories.
477    pub fn open_dir(&self, dir_ref: DirectoryRef) -> IsoDir<'_, DATA> {
478        IsoDir {
479            image: self,
480            directory: dir_ref,
481        }
482    }
483
484    /// Returns the path table information for this image.
485    pub fn path_table(&self) -> PathTableInfo {
486        PathTableInfo {
487            path_table: self.info.path_table,
488        }
489    }
490
491    /// Creates a volume-descriptor cursor starting at logical sector 16.
492    ///
493    /// Async callers use [`VolumeDescriptorIter::next_descriptor`]. In sync
494    /// builds the cursor also implements [`Iterator`].
495    pub fn read_volume_descriptors(&self) -> VolumeDescriptorIter<'_, DATA> {
496        VolumeDescriptorIter {
497            data: &self.data,
498            current_sector: LogicalSector(16),
499            done: false,
500        }
501    }
502
503    /// Returns the cached path table entries, parsed once during `open()`.
504    ///
505    /// Each entry contains a directory name, its LBA, and its parent index,
506    /// enabling fast directory hierarchy traversal without repeated seeks.
507    #[cfg(feature = "alloc")]
508    pub fn path_table_entries(&self) -> &[PathTableEntry] {
509        &self.info.path_table_cache
510    }
511}
512
513io_transform! {
514impl<DATA: Read + Seek> IsoImage<DATA> {
515    /// Reads the primary volume descriptor.
516    ///
517    /// Returns an I/O error if the descriptor sequence is malformed, truncated,
518    /// or contains no primary descriptor.
519    pub async fn read_pvd(&self) -> io::Result<PrimaryVolumeDescriptor> {
520        let mut descriptors = self.read_volume_descriptors();
521        while let Some(descriptor) = descriptors.next_descriptor().await? {
522            if let super::volume::VolumeDescriptor::Primary(pvd) = descriptor {
523                return Ok(pvd);
524            }
525        }
526        Err(io::Error::new(
527            io::ErrorKind::InvalidData,
528            "primary volume descriptor not found",
529        ))
530    }
531}
532} // io_transform!