Skip to main content

hadris_iso/read/
basic.rs

1//! Allocation-free ISO 9660 reader.
2
3use core::char::decode_utf16;
4
5use super::directory::{DirectoryRecord, DirectoryRecordHeader, FileFlags};
6use super::io::{self, Read, Seek, SeekFrom};
7use super::volume::{
8    PrimaryVolumeDescriptor, SupplementaryVolumeDescriptor, VolumeDescriptor,
9    VolumeDescriptorHeader, VolumeDescriptorType,
10};
11use crate::joliet::JolietLevel;
12
13const DESCRIPTOR_SIZE: u64 = 2048;
14const FIRST_DESCRIPTOR: u64 = 16;
15const MIN_RECORD_SIZE: usize = core::mem::size_of::<DirectoryRecordHeader>() + 1;
16
17/// A filename namespace exposed by an ISO image.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum IsoNamespace {
20    /// The primary ISO 9660 directory tree.
21    Primary,
22    /// A Joliet supplementary directory tree.
23    Joliet(JolietLevel),
24    /// The ISO 9660:1999 enhanced directory tree.
25    Enhanced,
26}
27
28/// A root directory and the namespace it represents.
29#[derive(Debug, Clone, Copy)]
30pub struct IsoRoot {
31    namespace: IsoNamespace,
32    extent: u32,
33    size: u32,
34    block_size: u16,
35}
36
37impl IsoRoot {
38    /// Returns the root's filename namespace.
39    pub const fn namespace(self) -> IsoNamespace {
40        self.namespace
41    }
42
43    /// Returns the root directory's byte length.
44    pub const fn size(self) -> u32 {
45        self.size
46    }
47
48    /// Returns the image logical-block size used by this tree.
49    pub const fn block_size(self) -> u16 {
50        self.block_size
51    }
52}
53
54/// An error produced while decoding an entry name.
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum NameError {
57    /// The on-disk name is not valid for its namespace.
58    InvalidEncoding,
59    /// The caller-provided UTF-8 buffer is too small.
60    BufferTooSmall,
61}
62
63impl core::fmt::Display for NameError {
64    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
65        match self {
66            Self::InvalidEncoding => f.write_str("invalid ISO filename encoding"),
67            Self::BufferTooSmall => f.write_str("filename output buffer is too small"),
68        }
69    }
70}
71
72impl core::error::Error for NameError {}
73
74/// A borrowed, allocation-free view of an ISO directory-entry name.
75#[derive(Debug, Clone, Copy)]
76pub struct IsoName<'a> {
77    raw: &'a [u8],
78    namespace: IsoNamespace,
79}
80
81impl<'a> IsoName<'a> {
82    /// Returns the name exactly as stored in the directory record.
83    pub const fn raw(self) -> &'a [u8] {
84        self.raw
85    }
86
87    /// Returns the name's namespace and encoding.
88    pub const fn namespace(self) -> IsoNamespace {
89        self.namespace
90    }
91
92    /// Compares this name with a UTF-8 path component without allocating.
93    pub fn matches(self, candidate: &str) -> bool {
94        if self.raw == [0] {
95            return candidate == ".";
96        }
97        if self.raw == [1] {
98            return candidate == "..";
99        }
100        match self.namespace {
101            IsoNamespace::Primary => {
102                strip_primary_version(self.raw).eq_ignore_ascii_case(candidate.as_bytes())
103            }
104            IsoNamespace::Joliet(_) => joliet_matches(strip_joliet_version(self.raw), candidate),
105            IsoNamespace::Enhanced => {
106                strip_primary_version(self.raw).eq_ignore_ascii_case(candidate.as_bytes())
107            }
108        }
109    }
110
111    /// Decodes the name as UTF-8 into `output` and returns the initialized text.
112    pub fn decode_into(self, output: &mut [u8]) -> Result<&str, NameError> {
113        if self.raw == [0] {
114            return write_special(output, b".");
115        }
116        if self.raw == [1] {
117            return write_special(output, b"..");
118        }
119        match self.namespace {
120            IsoNamespace::Primary => {
121                let raw = strip_primary_version(self.raw);
122                if !raw.is_ascii() {
123                    return Err(NameError::InvalidEncoding);
124                }
125                if raw.len() > output.len() {
126                    return Err(NameError::BufferTooSmall);
127                }
128                output[..raw.len()].copy_from_slice(raw);
129                core::str::from_utf8(&output[..raw.len()]).map_err(|_| NameError::InvalidEncoding)
130            }
131            IsoNamespace::Joliet(_) => decode_joliet_into(strip_joliet_version(self.raw), output),
132            IsoNamespace::Enhanced => {
133                let raw = strip_primary_version(self.raw);
134                core::str::from_utf8(raw).map_err(|_| NameError::InvalidEncoding)?;
135                if raw.len() > output.len() {
136                    return Err(NameError::BufferTooSmall);
137                }
138                output[..raw.len()].copy_from_slice(raw);
139                core::str::from_utf8(&output[..raw.len()]).map_err(|_| NameError::InvalidEncoding)
140            }
141        }
142    }
143}
144
145fn write_special<'a>(output: &'a mut [u8], value: &[u8]) -> Result<&'a str, NameError> {
146    if value.len() > output.len() {
147        return Err(NameError::BufferTooSmall);
148    }
149    output[..value.len()].copy_from_slice(value);
150    core::str::from_utf8(&output[..value.len()]).map_err(|_| NameError::InvalidEncoding)
151}
152
153fn strip_primary_version(raw: &[u8]) -> &[u8] {
154    if let Some(pos) = raw.iter().rposition(|byte| *byte == b';')
155        && pos + 1 < raw.len()
156        && raw[pos + 1..].iter().all(u8::is_ascii_digit)
157    {
158        &raw[..pos]
159    } else {
160        raw
161    }
162}
163
164fn strip_joliet_version(raw: &[u8]) -> &[u8] {
165    if raw.len() % 2 != 0 {
166        return raw;
167    }
168    let mut pos = raw.len();
169    while pos >= 2 && raw[pos - 2] == 0 && raw[pos - 1].is_ascii_digit() {
170        pos -= 2;
171    }
172    if pos + 2 <= raw.len() && pos >= 2 && raw[pos - 2..pos] == [0, b';'] {
173        &raw[..pos - 2]
174    } else {
175        raw
176    }
177}
178
179fn joliet_matches(raw: &[u8], candidate: &str) -> bool {
180    if raw.len() % 2 != 0 {
181        return false;
182    }
183    let units = raw
184        .chunks_exact(2)
185        .map(|pair| u16::from_be_bytes([pair[0], pair[1]]));
186    let mut expected = candidate.chars();
187    for decoded in decode_utf16(units) {
188        let Ok(ch) = decoded else {
189            return false;
190        };
191        if expected.next() != Some(ch) {
192            return false;
193        }
194    }
195    expected.next().is_none()
196}
197
198/// Copies the Rock Ridge alternate name (SUSP `NM` entries) out of an inline
199/// system-use area into `out`, concatenating a `CONTINUE`-flagged run. Returns
200/// the number of bytes written, or `None` when there is no usable `NM` entry.
201///
202/// Only the inline system-use area is parsed; a name continued into a SUSP `CE`
203/// continuation area is not followed, which keeps this allocation- and I/O-free.
204/// The `CURRENT`/`PARENT` NM aliases (the "." and ".." names) yield `None`.
205fn rrip_nm_into(su: &[u8], out: &mut [u8]) -> Option<usize> {
206    const NM_CONTINUE: u8 = 0x01;
207    const NM_CURRENT: u8 = 0x02;
208    const NM_PARENT: u8 = 0x04;
209
210    let mut written = 0usize;
211    let mut found = false;
212    let mut i = 0usize;
213    // Each SUSP entry is [SIG0, SIG1, LEN, VER, ...]; LEN covers the whole entry.
214    while i + 4 <= su.len() {
215        let len = su[i + 2] as usize;
216        if len < 4 || i + len > su.len() {
217            break;
218        }
219        if &su[i..i + 2] == b"ST" {
220            break; // SUSP terminator
221        }
222        if &su[i..i + 2] == b"NM" && len >= 5 {
223            let flags = su[i + 4];
224            if flags & (NM_CURRENT | NM_PARENT) != 0 {
225                return None;
226            }
227            let content = &su[i + 5..i + len];
228            if written + content.len() > out.len() {
229                return None; // caller buffer too small for the full name
230            }
231            out[written..written + content.len()].copy_from_slice(content);
232            written += content.len();
233            found = true;
234            if flags & NM_CONTINUE == 0 {
235                break;
236            }
237        }
238        i += len;
239    }
240
241    found.then_some(written)
242}
243
244fn decode_joliet_into<'a>(raw: &[u8], output: &'a mut [u8]) -> Result<&'a str, NameError> {
245    if raw.len() % 2 != 0 {
246        return Err(NameError::InvalidEncoding);
247    }
248    let units = raw
249        .chunks_exact(2)
250        .map(|pair| u16::from_be_bytes([pair[0], pair[1]]));
251    let mut written = 0;
252    for decoded in decode_utf16(units) {
253        let ch = decoded.map_err(|_| NameError::InvalidEncoding)?;
254        let needed = ch.len_utf8();
255        if written + needed > output.len() {
256            return Err(NameError::BufferTooSmall);
257        }
258        ch.encode_utf8(&mut output[written..written + needed]);
259        written += needed;
260    }
261    core::str::from_utf8(&output[..written]).map_err(|_| NameError::InvalidEncoding)
262}
263
264#[derive(Debug, Clone, Copy)]
265struct DirectoryLocation {
266    extent: u32,
267    size: u32,
268    block_size: u16,
269    namespace: IsoNamespace,
270}
271
272impl From<IsoRoot> for DirectoryLocation {
273    fn from(root: IsoRoot) -> Self {
274        Self {
275            extent: root.extent,
276            size: root.size,
277            block_size: root.block_size,
278            namespace: root.namespace,
279        }
280    }
281}
282
283/// An owned, fixed-size directory entry returned by the allocation-free reader.
284#[derive(Debug, Clone, Copy)]
285pub struct IsoDirEntry {
286    record: DirectoryRecord,
287    directory: DirectoryLocation,
288    continuation_offset: Option<u32>,
289    total_size: u64,
290}
291
292impl IsoDirEntry {
293    /// Returns the underlying ISO directory record.
294    pub const fn record(&self) -> &DirectoryRecord {
295        &self.record
296    }
297
298    /// Returns an allocation-free view of the filename.
299    pub fn name(&self) -> IsoName<'_> {
300        IsoName {
301            raw: self.record.name(),
302            namespace: self.directory.namespace,
303        }
304    }
305
306    /// Returns whether this entry is a directory.
307    pub fn is_directory(&self) -> bool {
308        self.record.is_directory()
309    }
310
311    /// Returns whether this entry is a regular file.
312    pub fn is_file(&self) -> bool {
313        !self.is_directory()
314    }
315
316    /// Returns whether the file uses multiple extents.
317    pub const fn is_multi_extent(&self) -> bool {
318        self.continuation_offset.is_some()
319    }
320
321    /// Returns the total data length across all file extents.
322    pub const fn total_size(&self) -> u64 {
323        self.total_size
324    }
325
326    /// Returns the raw system-use area.
327    pub fn system_use(&self) -> &[u8] {
328        self.record.system_use()
329    }
330
331    /// Resolves the Rock Ridge alternate name (SUSP `NM`) into `output`, without
332    /// allocating.
333    ///
334    /// Returns `None` when the entry carries no usable `NM` field — a plain
335    /// ISO 9660/Joliet image, or a "."/".." alias — in which case
336    /// [`Self::name`] is the name to use. Otherwise the RRIP name is validated
337    /// as UTF-8 and written into `output`. Only the inline system-use area is
338    /// parsed; a name continued into a SUSP `CE` area is not followed.
339    pub fn rrip_name_into<'b>(&self, output: &'b mut [u8]) -> Option<Result<&'b str, NameError>> {
340        let len = rrip_nm_into(self.system_use(), output)?;
341        Some(core::str::from_utf8(&output[..len]).map_err(|_| NameError::InvalidEncoding))
342    }
343
344    /// Returns `true` if this entry's Rock Ridge name equals `candidate`.
345    ///
346    /// Returns `false` when the entry has no `NM` field or the name does not
347    /// fit POSIX `NAME_MAX` (255 bytes). Allocation-free.
348    pub fn rrip_name_matches(&self, candidate: &str) -> bool {
349        let mut buffer = [0u8; 255];
350        match rrip_nm_into(self.system_use(), &mut buffer) {
351            Some(len) => &buffer[..len] == candidate.as_bytes(),
352            None => false,
353        }
354    }
355}
356
357/// Allocation-free ISO 9660 image reader.
358#[derive(Debug)]
359pub struct IsoReader<R> {
360    source: R,
361    primary: IsoRoot,
362    joliet: Option<IsoRoot>,
363    enhanced: Option<IsoRoot>,
364}
365
366impl<R> IsoReader<R> {
367    /// Consumes the reader and returns its underlying data source.
368    pub fn into_inner(self) -> R {
369        self.source
370    }
371
372    /// Returns the primary ISO 9660 root.
373    pub const fn primary_root(&self) -> IsoRoot {
374        self.primary
375    }
376
377    /// Returns the highest-level recognized Joliet root, when present.
378    pub const fn joliet_root(&self) -> Option<IsoRoot> {
379        self.joliet
380    }
381
382    /// Returns the ISO 9660:1999 enhanced root, when present.
383    pub const fn enhanced_root(&self) -> Option<IsoRoot> {
384        self.enhanced
385    }
386
387    /// Returns the preferred root, choosing Joliet, then enhanced, then primary.
388    pub const fn preferred_root(&self) -> IsoRoot {
389        match (self.joliet, self.enhanced) {
390            (Some(root), _) => root,
391            (None, Some(root)) => root,
392            (None, None) => self.primary,
393        }
394    }
395
396    /// Iterates over the primary, enhanced, and Joliet roots.
397    pub fn roots(&self) -> impl Iterator<Item = IsoRoot> {
398        [Some(self.primary), self.enhanced, self.joliet]
399            .into_iter()
400            .flatten()
401    }
402
403    /// Selects a root by namespace.
404    pub fn root(&self, namespace: IsoNamespace) -> Option<IsoRoot> {
405        match namespace {
406            IsoNamespace::Primary => Some(self.primary),
407            IsoNamespace::Joliet(level) => self
408                .joliet
409                .filter(|root| root.namespace == IsoNamespace::Joliet(level)),
410            IsoNamespace::Enhanced => self.enhanced,
411        }
412    }
413}
414
415io_transform! {
416impl<R: Read + Seek> IsoReader<R> {
417    /// Opens an ISO image without allocating.
418    pub async fn open(mut source: R) -> io::Result<Self> {
419        let mut primary = None;
420        let mut joliet = None;
421        let mut enhanced = None;
422        let mut sector = FIRST_DESCRIPTOR;
423        loop {
424            source.seek(SeekFrom::Start(sector.checked_mul(DESCRIPTOR_SIZE).ok_or_else(overflow)?))
425                .await.map_err(io::Error::erase)?;
426            let mut bytes = [0_u8; DESCRIPTOR_SIZE as usize];
427            source.read_exact(&mut bytes).await?;
428            let header = VolumeDescriptorHeader::from_bytes(&bytes[..7]);
429            if !header.is_valid() {
430                return Err(invalid("invalid volume descriptor header"));
431            }
432            let ty = VolumeDescriptorType::from_u8(header.descriptor_type);
433            if ty == VolumeDescriptorType::VolumeSetTerminator { break; }
434            match VolumeDescriptor::new(bytes) {
435                VolumeDescriptor::Primary(descriptor) => primary = Some(root_from_primary(&descriptor)?),
436                VolumeDescriptor::Supplementary(descriptor) => {
437                    if descriptor.header.version == 2 && descriptor.file_structure_version == 2 {
438                        enhanced = Some(root_from_enhanced(&descriptor)?);
439                    } else if let Some(root) = root_from_joliet(&descriptor)?
440                        && joliet.map(|old: IsoRoot| joliet_level(old) < joliet_level(root)).unwrap_or(true)
441                    { joliet = Some(root); }
442                }
443                _ => {}
444            }
445            sector = sector.checked_add(1).ok_or_else(overflow)?;
446        }
447        let primary = primary.ok_or_else(|| invalid("primary volume descriptor not found"))?;
448        Ok(Self { source, primary, joliet, enhanced })
449    }
450
451    /// Opens an allocation-free directory reader.
452    pub fn open_dir(&mut self, root: IsoRoot) -> IsoDirReader<'_, R> {
453        IsoDirReader { image: self, directory: root.into(), offset: 0 }
454    }
455
456    /// Finds an entry using the preferred namespace.
457    pub async fn find_path(&mut self, path: &str) -> io::Result<Option<IsoDirEntry>> {
458        self.find_path_in(self.preferred_root(), path).await
459    }
460
461    /// Finds an entry in an explicitly selected directory tree.
462    pub async fn find_path_in(&mut self, root: IsoRoot, path: &str) -> io::Result<Option<IsoDirEntry>> {
463        let mut location = DirectoryLocation::from(root);
464        let mut components = hadris_path::VPath::with_separators(
465            path, hadris_path::Separators::SlashOrBackslash
466        ).components().filter_map(|component| match component {
467            hadris_path::Component::Root | hadris_path::Component::Current => None,
468            other => Some(other),
469        }).peekable();
470
471        while let Some(component) = components.next() {
472            let hadris_path::Component::Normal(name) = component else {
473                return Err(io::Error::new(io::ErrorKind::InvalidInput, "parent path components are not supported"));
474            };
475            let mut offset = 0;
476            let mut found = None;
477            while let Some(entry) = self.next_entry(location, &mut offset).await? {
478                let flags = FileFlags::from_bits_retain(entry.record.header().flags);
479                if !entry.record.is_special()
480                    && !flags.contains(FileFlags::ASSOCIATED_FILE)
481                    && entry.name().matches(name)
482                { found = Some(entry); break; }
483            }
484            let Some(entry) = found else { return Ok(None); };
485            if components.peek().is_none() { return Ok(Some(entry)); }
486            if !entry.is_directory() { return Ok(None); }
487            location = entry_directory(&entry)?;
488        }
489        Ok(None)
490    }
491
492    /// Opens a caller-buffered stream for a file entry.
493    pub fn open_file<'a>(&'a mut self, entry: &IsoDirEntry) -> io::Result<IsoFileReader<'a, R>> {
494        validate_file(entry)?;
495        Ok(IsoFileReader {
496            image: self,
497            entry: *entry,
498            current_record: entry.record,
499            current_extent_start: 0,
500            continuation_cursor: entry.continuation_offset,
501            position: 0,
502        })
503    }
504
505    async fn next_entry(&mut self, directory: DirectoryLocation, offset: &mut u32) -> io::Result<Option<IsoDirEntry>> {
506        let Some(record) = self.next_raw(directory, offset).await? else { return Ok(None); };
507        let first_flags = FileFlags::from_bits_retain(record.header().flags);
508        let continuation_offset = first_flags.contains(FileFlags::NOT_FINAL).then_some(*offset);
509        let mut total_size = record.header().data_len.read() as u64;
510        if continuation_offset.is_some() {
511            loop {
512                let continuation = self.next_raw(directory, offset).await?
513                    .ok_or_else(|| invalid("truncated multi-extent file"))?;
514                validate_continuation(&record, &continuation)?;
515                total_size = total_size.checked_add(continuation.header().data_len.read() as u64)
516                    .ok_or_else(overflow)?;
517                if !FileFlags::from_bits_retain(continuation.header().flags).contains(FileFlags::NOT_FINAL) { break; }
518            }
519        }
520        Ok(Some(IsoDirEntry { record, directory, continuation_offset, total_size }))
521    }
522
523    async fn next_raw(&mut self, directory: DirectoryLocation, offset: &mut u32) -> io::Result<Option<DirectoryRecord>> {
524        let block = directory.block_size as u32;
525        while *offset < directory.size {
526            let base = byte_offset(directory.extent, directory.block_size)?;
527            let absolute = base.checked_add(*offset as u64).ok_or_else(overflow)?;
528            self.source.seek(SeekFrom::Start(absolute)).await.map_err(io::Error::erase)?;
529            let mut len = [0_u8; 1];
530            self.source.read_exact(&mut len).await?;
531            if len[0] == 0 {
532                let next = ((*offset / block) + 1).checked_mul(block).ok_or_else(overflow)?;
533                if next <= *offset { return Err(invalid("directory offset did not advance")); }
534                *offset = next.min(directory.size);
535                continue;
536            }
537            let len = len[0] as usize;
538            if len < MIN_RECORD_SIZE || (*offset % block) as usize + len > block as usize {
539                return Err(invalid("invalid directory record length"));
540            }
541            let mut header_bytes = [0_u8; core::mem::size_of::<DirectoryRecordHeader>()];
542            header_bytes[0] = len as u8;
543            self.source.read_exact(&mut header_bytes[1..]).await?;
544            let header: DirectoryRecordHeader = bytemuck::pod_read_unaligned(&header_bytes);
545            let name_end = MIN_RECORD_SIZE - 1 + header.file_identifier_len as usize;
546            if name_end > len { return Err(invalid("directory identifier exceeds record")); }
547            self.source.seek(SeekFrom::Start(absolute)).await.map_err(io::Error::erase)?;
548            let record = DirectoryRecord::parse(&mut self.source).await?;
549            *offset = offset.checked_add(len as u32).ok_or_else(overflow)?;
550            return Ok(Some(record));
551        }
552        Ok(None)
553    }
554
555}
556} // io_transform!
557
558/// Streaming allocation-free directory reader.
559pub struct IsoDirReader<'a, R> {
560    image: &'a mut IsoReader<R>,
561    directory: DirectoryLocation,
562    offset: u32,
563}
564
565io_transform! {
566impl<R: Read + Seek> IsoDirReader<'_, R> {
567    /// Reads the next logical directory entry.
568    pub async fn next_entry(&mut self) -> io::Result<Option<IsoDirEntry>> {
569        self.image.next_entry(self.directory, &mut self.offset).await
570    }
571}
572} // io_transform!
573
574sync_only! {
575impl<R: Read + Seek> Iterator for IsoDirReader<'_, R> {
576    type Item = io::Result<IsoDirEntry>;
577    fn next(&mut self) -> Option<Self::Item> { self.next_entry().transpose() }
578}
579}
580
581/// Caller-buffered file stream backed by an [`IsoReader`].
582pub struct IsoFileReader<'a, R> {
583    image: &'a mut IsoReader<R>,
584    entry: IsoDirEntry,
585    current_record: DirectoryRecord,
586    current_extent_start: u64,
587    continuation_cursor: Option<u32>,
588    position: u64,
589}
590
591impl<R> IsoFileReader<'_, R> {
592    /// Returns the complete logical file length.
593    pub const fn len(&self) -> u64 {
594        self.entry.total_size
595    }
596
597    /// Returns whether the file contains no bytes.
598    pub const fn is_empty(&self) -> bool {
599        self.entry.total_size == 0
600    }
601
602    /// Returns the current logical read position.
603    pub const fn position(&self) -> u64 {
604        self.position
605    }
606}
607
608io_transform! {
609impl<R: Read + Seek> IsoFileReader<'_, R> {
610    /// Reads the next chunk into a caller-provided buffer.
611    pub async fn read_chunk(&mut self, output: &mut [u8]) -> io::Result<usize> {
612        if self.position >= self.entry.total_size || output.is_empty() {
613            return Ok(0);
614        }
615
616        let wanted = output
617            .len()
618            .min((self.entry.total_size - self.position) as usize);
619        let mut written = 0;
620        while written < wanted {
621            let extent_len = self.current_record.header().data_len.read() as u64;
622            let within = self.position.checked_sub(self.current_extent_start)
623                .ok_or_else(|| invalid("file reader extent state is invalid"))?;
624            if within < extent_len {
625                let available = (extent_len - within) as usize;
626                let take = available.min(wanted - written);
627                let header = self.current_record.header();
628                let xar = header.extended_attr_record as u64
629                    * self.entry.directory.block_size as u64;
630                let absolute = byte_offset(header.extent.read(), self.entry.directory.block_size)?
631                    .checked_add(xar)
632                    .and_then(|value| value.checked_add(within))
633                    .ok_or_else(overflow)?;
634                self.image.source.seek(SeekFrom::Start(absolute)).await
635                    .map_err(io::Error::erase)?;
636                self.image.source.read_exact(&mut output[written..written + take]).await?;
637                written += take;
638                self.position += take as u64;
639            }
640
641            if written == wanted {
642                break;
643            }
644            self.advance_extent(extent_len).await?;
645        }
646        Ok(written)
647    }
648
649    async fn advance_extent(&mut self, extent_len: u64) -> io::Result<()> {
650        self.current_extent_start = self.current_extent_start
651            .checked_add(extent_len)
652            .ok_or_else(overflow)?;
653        let Some(ref mut cursor) = self.continuation_cursor else {
654            return Err(invalid("file extent chain ended before its declared size"));
655        };
656        let next = self.image.next_raw(self.entry.directory, cursor).await?
657            .ok_or_else(|| invalid("truncated multi-extent file"))?;
658        validate_continuation(&self.entry.record, &next)?;
659        if !FileFlags::from_bits_retain(next.header().flags).contains(FileFlags::NOT_FINAL) {
660            self.continuation_cursor = None;
661        }
662        self.current_record = next;
663        Ok(())
664    }
665}
666} // io_transform!
667
668fn root_from_primary(descriptor: &PrimaryVolumeDescriptor) -> io::Result<IsoRoot> {
669    make_root(
670        IsoNamespace::Primary,
671        descriptor.logical_block_size.read(),
672        descriptor.dir_record.header.extent.read(),
673        descriptor.dir_record.header.data_len.read(),
674        descriptor.dir_record.header.extended_attr_record,
675    )
676}
677
678fn root_from_joliet(descriptor: &SupplementaryVolumeDescriptor) -> io::Result<Option<IsoRoot>> {
679    let Some(level) = JolietLevel::from_escape_sequence(&descriptor.escape_sequences) else {
680        return Ok(None);
681    };
682    Ok(Some(make_root(
683        IsoNamespace::Joliet(level),
684        descriptor.logical_block_size.read(),
685        descriptor.dir_record.header.extent.read(),
686        descriptor.dir_record.header.data_len.read(),
687        descriptor.dir_record.header.extended_attr_record,
688    )?))
689}
690
691fn root_from_enhanced(descriptor: &SupplementaryVolumeDescriptor) -> io::Result<IsoRoot> {
692    make_root(
693        IsoNamespace::Enhanced,
694        descriptor.logical_block_size.read(),
695        descriptor.dir_record.header.extent.read(),
696        descriptor.dir_record.header.data_len.read(),
697        descriptor.dir_record.header.extended_attr_record,
698    )
699}
700
701fn joliet_level(root: IsoRoot) -> JolietLevel {
702    match root.namespace {
703        IsoNamespace::Joliet(level) => level,
704        IsoNamespace::Primary | IsoNamespace::Enhanced => {
705            unreachable!("non-Joliet root used as Joliet root")
706        }
707    }
708}
709
710fn make_root(
711    namespace: IsoNamespace,
712    block_size: u16,
713    extent: u32,
714    size: u32,
715    xar_blocks: u8,
716) -> io::Result<IsoRoot> {
717    if block_size < 512 || !block_size.is_power_of_two() {
718        return Err(invalid("invalid logical block size"));
719    }
720    let extent = extent.checked_add(xar_blocks as u32).ok_or_else(overflow)?;
721    byte_offset(extent, block_size)?;
722    Ok(IsoRoot {
723        namespace,
724        extent,
725        size,
726        block_size,
727    })
728}
729
730fn entry_directory(entry: &IsoDirEntry) -> io::Result<DirectoryLocation> {
731    if !entry.is_directory() {
732        return Err(invalid("entry is not a directory"));
733    }
734    Ok(DirectoryLocation {
735        extent: entry
736            .record
737            .header()
738            .extent
739            .read()
740            .checked_add(entry.record.header().extended_attr_record as u32)
741            .ok_or_else(overflow)?,
742        size: entry.record.header().data_len.read(),
743        block_size: entry.directory.block_size,
744        namespace: entry.directory.namespace,
745    })
746}
747
748fn validate_file(entry: &IsoDirEntry) -> io::Result<()> {
749    if entry.is_directory() {
750        return Err(io::Error::new(
751            io::ErrorKind::InvalidInput,
752            "entry is a directory",
753        ));
754    }
755    let header = entry.record.header();
756    if header.file_unit_size != 0 || header.interleave_gap_size != 0 {
757        return Err(io::Error::new(
758            io::ErrorKind::Unsupported,
759            "interleaved files are unsupported",
760        ));
761    }
762    if header.volume_sequence_number.read() != 1 {
763        return Err(io::Error::new(
764            io::ErrorKind::Unsupported,
765            "multi-volume files are unsupported",
766        ));
767    }
768    Ok(())
769}
770
771fn validate_continuation(first: &DirectoryRecord, next: &DirectoryRecord) -> io::Result<()> {
772    if first.name() != next.name()
773        || next.is_directory()
774        || FileFlags::from_bits_retain(next.header().flags).contains(FileFlags::ASSOCIATED_FILE)
775    {
776        return Err(invalid("invalid multi-extent continuation"));
777    }
778    Ok(())
779}
780
781fn byte_offset(extent: u32, block_size: u16) -> io::Result<u64> {
782    (extent as u64)
783        .checked_mul(block_size as u64)
784        .ok_or_else(overflow)
785}
786
787fn invalid(message: &'static str) -> io::Error {
788    io::Error::new(io::ErrorKind::InvalidData, message)
789}
790fn overflow() -> io::Error {
791    invalid("ISO offset overflow")
792}