Skip to main content

hadris_iso/write/
types.rs

1#[cfg(feature = "unstable-streaming")]
2use alloc::boxed::Box;
3use core::{fmt, ops::Deref};
4use std::{fs::FileType, path::PathBuf};
5
6use alloc::{
7    collections::{BTreeMap, BTreeSet, VecDeque},
8    string::{String, ToString},
9    sync::Arc,
10    vec::Vec,
11};
12
13use super::FileConversionError;
14use crate::{
15    directory::{DirectoryRef, FileFlags},
16    file::{ConvertedName, EntryType},
17    io::{self},
18    read::PathSeparator,
19    rrip::RripOptions,
20    susp::SplitSu,
21    write::{utils::*, writer::*},
22};
23
24/// Represents a file in the write order: (directory ID, file index).
25pub type FileOrder = (DirectoryId, usize);
26
27/// Maps (directory ID, entry type) to directory reference for relocation.
28pub type RelocationMap = BTreeMap<(usize, EntryType), DirectoryRef>;
29
30/// RRIP timestamp (7 bytes: year, month, day, hour, minute, second, offset).
31pub type RripTime = [u8; 7];
32
33/// A compact input tree for callers that do not need per-entry metadata.
34///
35/// [`InputTree`] is the richer model for Rock Ridge metadata and host
36/// filesystem imports. Both models are accepted by
37/// [`IsoImageWriter::create`](crate::write::IsoImageWriter::create).
38pub struct InputFiles {
39    /// Separator used by paths referenced from writer options.
40    pub path_separator: PathSeparator,
41    /// Root-level files and directories.
42    pub files: Vec<File>,
43}
44
45#[derive(Clone, PartialEq, Eq)]
46/// A file or directory in the compact [`InputFiles`] model.
47pub enum File {
48    /// The `File` variant.
49    File {
50        /// The `name` field.
51        name: Arc<String>,
52        /// The `contents` field.
53        contents: Vec<u8>,
54    },
55    /// The `Directory` variant.
56    Directory {
57        /// The `name` field.
58        name: Arc<String>,
59        /// The `children` field.
60        children: Vec<File>,
61    },
62}
63
64impl core::fmt::Debug for File {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        let mut dbg = f.debug_struct("File");
67        match self {
68            Self::Directory { name, children } => {
69                dbg.field("name", name);
70                dbg.field("children", children);
71            }
72            Self::File { name, contents } => {
73                dbg.field("name", name);
74                dbg.field("data_len", &contents.len());
75            }
76        }
77        dbg.finish()
78    }
79}
80
81impl File {
82    /// Performs the `name` operation.
83    pub fn name(&self) -> Arc<String> {
84        match self {
85            File::File { name, .. } => name.clone(),
86            File::Directory { name, .. } => name.clone(),
87        }
88    }
89}
90
91/// A metadata-aware tree used to create an ISO image.
92#[derive(Debug, Clone, PartialEq, Eq)]
93pub struct InputTree {
94    /// The `path_separator` field.
95    pub path_separator: PathSeparator,
96    /// The `entries` field.
97    pub entries: Vec<InputEntry>,
98}
99
100impl InputTree {
101    /// Recursive validation.
102    ///
103    /// Checks:
104    /// - Depth: max 8 directories (unless RRIP deep dirs enabled)
105    /// - Path length: max 255 bytes (unless RRIP deep dirs enabled)
106    /// - Symlinks: require RRIP preserve_symlinks
107    /// - Devices: require RRIP preserve_devices
108    /// - File size: max 4 GiB
109    pub(crate) fn validate(&self, rrip: Option<&RripOptions>) -> crate::io::Result<()> {
110        Self::visit(&self.entries, rrip, 1, 0)
111    }
112
113    fn visit(
114        entries: &[InputEntry],
115        rrip: Option<&RripOptions>,
116        depth: usize,
117        path_len: usize,
118    ) -> io::Result<()> {
119        for entry in entries {
120            match &entry.kind {
121                InputEntryKind::Directory(children) => {
122                    let child_path_len = if path_len == 0 {
123                        entry.name.len()
124                    } else {
125                        path_len + 1 + entry.name.len()
126                    };
127                    if (depth >= 8 || child_path_len > 255)
128                        && !rrip
129                            .is_some_and(|options| options.enabled && options.relocate_deep_dirs)
130                    {
131                        return Err(io::Error::new(
132                            io::ErrorKind::InvalidInput,
133                            "directory depth or path length exceeds ISO 9660 limits and RRIP relocation is disabled",
134                        ));
135                    }
136
137                    Self::visit(children, rrip, depth + 1, child_path_len)?;
138                }
139                InputEntryKind::Symlink(_) => {
140                    if !rrip.is_some_and(|options| options.enabled && options.preserve_symlinks) {
141                        return Err(io::Error::new(
142                            io::ErrorKind::InvalidInput,
143                            "symbolic links require RRIP preserve_symlinks",
144                        ));
145                    }
146                }
147                InputEntryKind::CharacterDevice { .. } | InputEntryKind::BlockDevice { .. } => {
148                    if !rrip.is_some_and(|options| options.enabled && options.preserve_devices) {
149                        return Err(io::Error::new(
150                            io::ErrorKind::InvalidInput,
151                            "device entries require RRIP preserve_devices",
152                        ));
153                    }
154                }
155                InputEntryKind::File(_contents) => {
156                    // if contents.len() as u64 > Self::MAX_SINGLE_EXTENT_FILE_LEN {
157                    //     return Err(io::Error::new(
158                    //         io::ErrorKind::InvalidInput,
159                    //         "file exceeds 4 GiB; the ISO writer stores each file in a single \
160                    //         extent and cannot yet emit multi-extent records",
161                    //     ));
162                    // }
163                }
164                #[cfg(feature = "unstable-streaming")]
165                InputEntryKind::Source(_) => {}
166                #[cfg(test)]
167                InputEntryKind::TestFile { .. } => {}
168            }
169        }
170        Ok(())
171    }
172}
173
174/// Optional POSIX metadata for an input entry.
175#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
176pub struct InputMetadata {
177    /// The `mode` field.
178    pub mode: Option<u32>,
179    /// The `uid` field.
180    pub uid: Option<u32>,
181    /// The `gid` field.
182    pub gid: Option<u32>,
183    /// Creation time as seconds since the Unix epoch.
184    pub created: Option<i64>,
185    /// Modification time as seconds since the Unix epoch.
186    pub modified: Option<i64>,
187    /// Access time as seconds since the Unix epoch.
188    pub accessed: Option<i64>,
189}
190
191impl InputMetadata {
192    /// Creates metadata from filesystem metadata.
193    pub(crate) fn from_fs(fs_metadata: std::fs::Metadata) -> Self {
194        #[allow(unused_mut)]
195        let mut metadata = Self {
196            created: system_time_seconds(fs_metadata.created()),
197            modified: system_time_seconds(fs_metadata.modified()),
198            accessed: system_time_seconds(fs_metadata.accessed()),
199            ..Self::default()
200        };
201
202        #[cfg(unix)]
203        {
204            use std::os::unix::fs::MetadataExt;
205            metadata.mode = Some(fs_metadata.mode() & 0o7777);
206            metadata.uid = Some(fs_metadata.uid());
207            metadata.gid = Some(fs_metadata.gid());
208        }
209
210        metadata
211    }
212}
213
214/// File contents that are read from a reader while the image is written.
215///
216/// An [`InputEntryKind::File`] holds its contents in memory. A `FileSource` holds
217/// only the length and a way to open a reader, so a large file is streamed into the
218/// image in fixed-size chunks and never has to fit in memory. The writer opens the
219/// reader once per file, in image order, and reads exactly `len` bytes from it: a
220/// reader that ends early fails the write with [`ErrorKind::UnexpectedEof`], and
221/// bytes beyond `len` are never read.
222///
223/// The reader is a blocking [`std::io::Read`] in both the sync and the async writer;
224/// the async writer awaits only its output.
225///
226/// Requires the `unstable-streaming` feature, which is outside the V2 API
227/// stability promise: this type and [`InputEntryKind::Source`] may change in any
228/// release.
229///
230/// [`ErrorKind::UnexpectedEof`]: crate::io::ErrorKind::UnexpectedEof
231#[cfg(feature = "unstable-streaming")]
232#[cfg_attr(docsrs, doc(cfg(feature = "unstable-streaming")))]
233#[derive(Clone)]
234pub struct FileSource {
235    len: u64,
236    open:
237        alloc::sync::Arc<dyn Fn() -> std::io::Result<Box<dyn std::io::Read + Send>> + Send + Sync>,
238}
239
240#[cfg(feature = "unstable-streaming")]
241impl FileSource {
242    /// A source of `len` bytes that `open` produces a reader for.
243    pub fn new<F>(len: u64, open: F) -> Self
244    where
245        F: Fn() -> std::io::Result<Box<dyn std::io::Read + Send>> + Send + Sync + 'static,
246    {
247        Self {
248            len,
249            open: alloc::sync::Arc::new(open),
250        }
251    }
252
253    /// A source that streams the file at `path`, whose length is taken now.
254    ///
255    /// The file is opened when the image is written; its length must not change
256    /// in between.
257    ///
258    /// ```
259    /// # #[cfg(feature = "unstable-streaming")] {
260    /// use hadris_iso::write::FileSource;
261    ///
262    /// let path = std::env::temp_dir().join("hadris-iso-file-source-doc.bin");
263    /// std::fs::write(&path, b"streamed")?;
264    /// let source = FileSource::from_path(&path)?;
265    /// assert_eq!(source.len(), 8);
266    /// let mut contents = Vec::new();
267    /// source.open()?.read_to_end(&mut contents)?;
268    /// assert_eq!(contents, b"streamed");
269    /// # std::fs::remove_file(&path)?;
270    /// # }
271    /// # Ok::<(), std::io::Error>(())
272    /// ```
273    pub fn from_path(path: impl Into<PathBuf>) -> std::io::Result<Self> {
274        let path = path.into();
275        let len = std::fs::metadata(&path)?.len();
276        Ok(Self::new(len, move || {
277            std::fs::File::open(&path).map(|file| Box::new(file) as Box<dyn std::io::Read + Send>)
278        }))
279    }
280
281    /// The number of bytes the source provides.
282    pub const fn len(&self) -> u64 {
283        self.len
284    }
285
286    /// Whether the source provides no bytes.
287    pub const fn is_empty(&self) -> bool {
288        self.len == 0
289    }
290
291    /// Opens a fresh reader positioned at the start of the contents.
292    pub fn open(&self) -> std::io::Result<Box<dyn std::io::Read + Send>> {
293        (self.open)()
294    }
295}
296
297#[cfg(feature = "unstable-streaming")]
298impl fmt::Debug for FileSource {
299    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
300        f.debug_struct("FileSource")
301            .field("len", &self.len)
302            .finish_non_exhaustive()
303    }
304}
305
306#[cfg(feature = "unstable-streaming")]
307impl PartialEq for FileSource {
308    /// Two sources are equal when they share the same opener and length.
309    fn eq(&self, other: &Self) -> bool {
310        self.len == other.len && alloc::sync::Arc::ptr_eq(&self.open, &other.open)
311    }
312}
313
314#[cfg(feature = "unstable-streaming")]
315impl Eq for FileSource {}
316
317/// The data represented by an [`InputEntry`].
318///
319/// With the `unstable-streaming` feature enabled this enum gains the
320/// `Source` variant, so exhaustive matches on it must account for
321/// that variant when the feature is on.
322#[derive(Debug, Clone, PartialEq, Eq)]
323pub enum InputEntryKind {
324    /// The `File` variant.
325    File(Vec<u8>),
326    /// A file whose contents are streamed from a reader while the image is written.
327    ///
328    /// Requires the `unstable-streaming` feature and is outside the V2 API
329    /// stability promise.
330    #[cfg(feature = "unstable-streaming")]
331    #[cfg_attr(docsrs, doc(cfg(feature = "unstable-streaming")))]
332    Source(FileSource),
333    /// A virtual sparse file used by large-file regression tests.
334    #[cfg(test)]
335    TestFile {
336        /// Virtual file size in bytes.
337        size: u64,
338    },
339    /// The `Directory` variant.
340    Directory(Vec<InputEntry>),
341    /// The `Symlink` variant.
342    Symlink(String),
343    /// A POSIX character device.
344    CharacterDevice {
345        /// Device-class identifier.
346        major: u32,
347        /// Device identifier within the class.
348        minor: u32,
349    },
350    /// A POSIX block device.
351    BlockDevice {
352        /// Device-class identifier.
353        major: u32,
354        /// Device identifier within the class.
355        minor: u32,
356    },
357}
358
359impl InputEntryKind {
360    pub(crate) fn file_len(&self) -> Option<u64> {
361        match self {
362            Self::File(contents) => Some(contents.len() as u64),
363            #[cfg(feature = "unstable-streaming")]
364            Self::Source(source) => Some(source.len()),
365            #[cfg(test)]
366            Self::TestFile { size } => Some(*size),
367            _ => None,
368        }
369    }
370
371    /// Creates an `InputEntryKind` from a filesystem file type and path.
372    ///
373    /// This function inspects the file type and constructs the appropriate variant:
374    /// - Regular files → `File` (contents are read into memory).
375    /// - Directories → `Directory` (recursively reads all children).
376    /// - Symlinks → `Symlink` (reads the link target as a string).
377    /// - Character/block devices → `CharacterDevice` or `BlockDevice` (Unix only).
378    pub(crate) fn new(
379        file_type: FileType,
380        path: PathBuf,
381    ) -> core::result::Result<Self, FileConversionError> {
382        if file_type.is_file() {
383            let content = std::fs::read(&path)?;
384            Ok(InputEntryKind::File(content))
385        } else if file_type.is_dir() {
386            let dir = read_input_directory_recursively(&path)?;
387            Ok(InputEntryKind::Directory(dir))
388        } else if file_type.is_symlink() {
389            let target = std::fs::read_link(&path)?;
390            let symlink = target
391                .to_str()
392                .ok_or_else(|| FileConversionError::InvalidUtf8Path(target.clone()))?
393                .to_string();
394            Ok(InputEntryKind::Symlink(symlink))
395        } else {
396            #[cfg(unix)]
397            {
398                use std::os::unix::fs::{FileTypeExt, MetadataExt};
399                const DEV_MAJOR_MASK_LOW: u64 = 0xfff;
400                const DEV_MAJOR_MASK_HIGH: u64 = 0xfffff000;
401                const DEV_MINOR_MASK_LOW: u64 = 0xff;
402                const DEV_MINOR_MASK_HIGH: u64 = 0xffffff00;
403
404                let fs_metadata = std::fs::symlink_metadata(&path)?;
405                let device = fs_metadata.rdev();
406                let major =
407                    ((device >> 8) & DEV_MAJOR_MASK_LOW) | ((device >> 32) & DEV_MAJOR_MASK_HIGH);
408                let minor = (device & DEV_MINOR_MASK_LOW) | ((device >> 12) & DEV_MINOR_MASK_HIGH);
409                if file_type.is_char_device() {
410                    Ok(InputEntryKind::CharacterDevice {
411                        major: major as u32,
412                        minor: minor as u32,
413                    })
414                } else if file_type.is_block_device() {
415                    Ok(InputEntryKind::BlockDevice {
416                        major: major as u32,
417                        minor: minor as u32,
418                    })
419                } else {
420                    Err(FileConversionError::UnsupportedFileType(path))
421                }
422            }
423            #[cfg(not(unix))]
424            return Err(FileConversionError::UnsupportedFileType(path));
425        }
426    }
427}
428
429/// A named entry in the input tree, representing a file, directory, symlink, or device.
430///
431/// This struct is the building block of [`InputTree`]. Each entry has a name,
432/// a kind (file, directory, symlink, or device), and optional POSIX metadata.
433#[derive(Debug, Clone, PartialEq, Eq)]
434pub struct InputEntry {
435    /// The name of the entry (e.g., `"README.txt"` or `"src"`).
436    pub name: Arc<String>,
437    /// The kind of entry (file, directory, symlink, or device).
438    pub kind: InputEntryKind,
439    /// Optional POSIX metadata (permissions, ownership, timestamps).
440    pub metadata: InputMetadata,
441}
442
443impl InputEntry {
444    /// Creates a regular file entry with content.
445    pub fn file(name: impl Into<String>, contents: impl Into<Vec<u8>>) -> Self {
446        Self::new(name, InputEntryKind::File(contents.into()))
447    }
448
449    /// Creates a directory entry with children.
450    pub fn directory(name: impl Into<String>, children: Vec<Self>) -> Self {
451        Self::new(name, InputEntryKind::Directory(children))
452    }
453
454    /// Creates a symbolic link entry.
455    pub fn symlink(name: impl Into<String>, target: impl Into<String>) -> Self {
456        Self::new(name, InputEntryKind::Symlink(target.into()))
457    }
458
459    /// Creates a character device entry (Unix only).
460    pub fn character_device(name: impl Into<String>, major: u32, minor: u32) -> Self {
461        Self::new(name, InputEntryKind::CharacterDevice { major, minor })
462    }
463
464    /// Creates a block device entry (Unix only).
465    pub fn block_device(name: impl Into<String>, major: u32, minor: u32) -> Self {
466        Self::new(name, InputEntryKind::BlockDevice { major, minor })
467    }
468
469    /// Sets the metadata for this entry.
470    pub fn with_metadata(mut self, metadata: InputMetadata) -> Self {
471        self.metadata = metadata;
472        self
473    }
474
475    /// Returns the name of the entry.
476    pub fn name(&self) -> Arc<String> {
477        self.name.clone()
478    }
479
480    fn new(name: impl Into<String>, kind: InputEntryKind) -> Self {
481        Self {
482            name: Arc::new(name.into()),
483            kind,
484            metadata: InputMetadata::default(),
485        }
486    }
487}
488
489impl InputTree {
490    /// Performs the `new` operation.
491    pub fn new(path_separator: PathSeparator, entries: Vec<InputEntry>) -> Self {
492        Self {
493            path_separator,
494            entries,
495        }
496    }
497
498    /// Performs the `from_fs` operation.
499    pub fn from_fs(
500        root_path: &std::path::Path,
501        path_separator: PathSeparator,
502    ) -> core::result::Result<Self, FileConversionError> {
503        if !root_path.is_dir() {
504            return Err(FileConversionError::Io(std::io::Error::new(
505                std::io::ErrorKind::InvalidInput,
506                alloc::format!("Root path '{root_path:?}' is not a directory"),
507            )));
508        }
509        Ok(Self::new(
510            path_separator,
511            read_input_directory_recursively(root_path)?,
512        ))
513    }
514}
515
516impl From<InputFiles> for InputTree {
517    fn from(value: InputFiles) -> Self {
518        fn convert(file: File) -> InputEntry {
519            match file {
520                File::File { name, contents } => InputEntry {
521                    name,
522                    kind: InputEntryKind::File(contents),
523                    metadata: InputMetadata::default(),
524                },
525                File::Directory { name, children } => InputEntry {
526                    name,
527                    kind: InputEntryKind::Directory(children.into_iter().map(convert).collect()),
528                    metadata: InputMetadata::default(),
529                },
530            }
531        }
532        Self::new(
533            value.path_separator,
534            value.files.into_iter().map(convert).collect(),
535        )
536    }
537}
538
539/// Depth-first tree walker.
540pub struct FileTreeWalker<'a>(VecDeque<StackFrame<'a>>);
541
542/// Walker internal state.
543enum StackFrame<'a> {
544    Node(&'a InputEntry),
545    DirExit(&'a InputEntry),
546}
547
548/// Walker events.
549#[derive(Debug, PartialEq, Eq)]
550pub enum TreeWalkerItem<'a> {
551    /// Entering a directory.
552    EnterDirectory(&'a InputEntry),
553    /// A file.
554    File(&'a InputEntry),
555    /// Exiting a directory.
556    ExitDirectory(&'a InputEntry),
557}
558
559impl<'a> FileTreeWalker<'a> {
560    /// New walker.
561    pub fn new(input: &'a InputTree) -> Self {
562        let mut stack = VecDeque::new();
563        for file in input.entries.iter().rev() {
564            stack.push_back(StackFrame::Node(file));
565        }
566        FileTreeWalker(stack)
567    }
568
569    /// Walk the tree and build WrittenFiles.
570    pub fn walk(self, written_files: &mut WrittenFiles) {
571        let mut next_directory_id = 1usize;
572        let mut current_dir = written_files.root_dir();
573
574        for file in self {
575            match file {
576                TreeWalkerItem::EnterDirectory(dir) => {
577                    let name = dir.name();
578                    let metadata = dir.metadata;
579                    let written_dir = written_files.get_mut(&current_dir);
580                    let index = written_dir.push_dir(name, metadata);
581                    written_dir.dirs[index].id = next_directory_id;
582                    next_directory_id += 1;
583                    current_dir.push(index);
584                }
585                TreeWalkerItem::ExitDirectory(_dir) => {
586                    current_dir.pop();
587                }
588                TreeWalkerItem::File(file) => {
589                    // Extents are assigned in the planning pass below.
590                    // Empty files keep extent 0 (per ISO 9660 they have no
591                    // data to reference).
592                    let dir = written_files.get_mut(&current_dir);
593                    dir.push_file(WrittenFile::new(
594                        file.name.clone(),
595                        file.kind.clone(),
596                        file.metadata,
597                    ));
598                }
599            };
600        }
601    }
602}
603
604impl<'a> Iterator for FileTreeWalker<'a> {
605    type Item = TreeWalkerItem<'a>;
606
607    fn next(&mut self) -> Option<Self::Item> {
608        let frame = self.0.pop_back()?;
609        match frame {
610            StackFrame::Node(file) => match &file.kind {
611                InputEntryKind::Directory(children) => {
612                    // Yield that we are entering this directory (pre-order event)
613                    let current_dir = file;
614
615                    // Push an Exit frame to signal leaving this directory later
616                    self.0.push_back(StackFrame::DirExit(current_dir));
617
618                    // Push children in reverse order for DFS
619                    for child in children.iter().rev() {
620                        self.0.push_back(StackFrame::Node(child));
621                    }
622
623                    Some(TreeWalkerItem::EnterDirectory(current_dir))
624                }
625                _ => Some(TreeWalkerItem::File(file)),
626            },
627            StackFrame::DirExit(dir) => Some(TreeWalkerItem::ExitDirectory(dir)),
628        }
629    }
630}
631
632/// Selects which RRIP fields to emit for a directory entry.
633pub enum RripEntryKind<'a> {
634    /// Root "." entry: SP + ER + PX + NM(CURRENT)
635    RootDot {
636        /// File metadata.
637        metadata: InputMetadata,
638        /// Number of hard links.
639        nlink: u32,
640    },
641    /// Root ".." entry: PX + NM(PARENT)
642    RootDotDot {
643        /// File metadata.
644        metadata: InputMetadata,
645        /// Number of hard links.
646        nlink: u32,
647    },
648    /// Non-root "." entry: PX + NM(CURRENT)
649    Dot {
650        /// File metadata.
651        metadata: InputMetadata,
652        /// Number of hard links.
653        nlink: u32,
654    },
655    /// Non-root ".." entry: PX + NM(PARENT)
656    DotDot {
657        /// File metadata.
658        metadata: InputMetadata,
659        /// Number of hard links.
660        nlink: u32,
661    },
662    /// Named directory entry.
663    Directory {
664        /// Original filename.
665        original_name: &'a str,
666        /// File metadata.
667        metadata: InputMetadata,
668        /// Number of hard links.
669        nlink: u32,
670    },
671    /// Named file entry.
672    Entry {
673        /// Original filename.
674        original_name: &'a str,
675        /// File metadata.
676        metadata: InputMetadata,
677        /// File contents.
678        kind: &'a InputEntryKind,
679    },
680}
681
682/// Pending directory records ready for layout planning and writing.
683///
684/// # Overview
685/// These records represent a single directory's contents, including:
686/// - "." and ".." entries
687/// - Child directories
688/// - Files (including multi-extent files)
689///
690/// # RRIP System Use
691/// Each record can have inline System Use data (RRIP extensions).
692/// If the data exceeds the available inline space, overflow is tracked
693/// and written to a separate continuation area.
694///
695/// # Multi-Extent Files
696/// Files larger than 4 GiB are split into multiple extents.
697/// Each extent gets its own directory record with the same name.
698/// The first record contains the RRIP data; subsequent extents
699/// have empty System Use.
700///
701/// # Processing Phases
702/// 1. Build all records with RRIP entries split against inline space
703/// 2. If any record has overflow: allocate continuation area, patch CE entries
704/// 3. Write directory records with inline SU bytes
705///
706/// # Ordering
707/// Records are ordered by File Identifier (ECMA-119 9.3):
708/// - "." (0x00) first
709/// - ".." (0x01) second
710/// - All other entries sorted by name
711pub struct PendingRecords(Vec<PendingRecord>);
712
713impl PendingRecords {
714    /// Build the pending records for one directory: dot/dotdot, child
715    /// directories, and files, with RRIP system-use areas split against the
716    /// inline budget, names deduplicated, and records ordered by File
717    /// Identifier (ECMA-119 9.3). Sizes never depend on extent values, so
718    /// the planning pass calls this with placeholder refs and the write pass
719    /// rebuilds with the real ones.
720    #[allow(clippy::too_many_arguments)]
721    pub fn new(
722        ty: EntryType,
723        dir: &WrittenDirectory,
724        is_root: bool,
725        inode_counter: &mut u32,
726        rrip_options: Option<&RripOptions>,
727        fallback_time: &RripTime,
728        relocation_refs: &BTreeMap<(usize, EntryType), DirectoryRef>,
729        sector_size: u64,
730    ) -> io::Result<Self> {
731        let rrip_options = rrip_options.filter(|options| options.enabled);
732        let has_rrip = ty.supports_rrip() && rrip_options.is_some();
733        let options = rrip_options.copied().unwrap_or_else(RripOptions::disabled);
734        let directory_nlink = 2 + dir.dirs.len() as u32;
735
736        let mut records: Vec<PendingRecord> = Vec::new();
737
738        records.push(Self::build_dot_record(
739            has_rrip,
740            is_root,
741            &dir.metadata,
742            directory_nlink,
743            &options,
744            fallback_time,
745        ));
746        records.push(Self::build_dotdot_record(
747            has_rrip,
748            is_root,
749            dir,
750            ty,
751            &options,
752            fallback_time,
753            relocation_refs,
754        )?);
755
756        Self::build_directory_records(
757            &mut records,
758            &dir.dirs,
759            ty,
760            has_rrip,
761            &options,
762            inode_counter,
763            fallback_time,
764            relocation_refs,
765        )?;
766
767        Self::build_file_records(
768            &mut records,
769            &dir.files,
770            ty,
771            has_rrip,
772            &options,
773            inode_counter,
774            fallback_time,
775            sector_size,
776        )?;
777
778        Self::deduplicate_names(&mut records, ty);
779        Self::sort_records(&mut records);
780
781        Ok(Self(records))
782    }
783
784    fn build_dot_record(
785        has_rrip: bool,
786        is_root: bool,
787        metadata: &InputMetadata,
788        nlink: u32,
789        options: &RripOptions,
790        fallback_time: &RripTime,
791    ) -> PendingRecord {
792        let split = if has_rrip {
793            let kind = if is_root {
794                RripEntryKind::RootDot {
795                    metadata: *metadata,
796                    nlink,
797                }
798            } else {
799                RripEntryKind::Dot {
800                    metadata: *metadata,
801                    nlink,
802                }
803            };
804            let max = available_su_space(1); // name is b"\x00"
805            build_rrip_entries(kind, 0, options, fallback_time).build_split(max)
806        } else {
807            SplitSu::empty()
808        };
809        PendingRecord::current_dir(split)
810    }
811
812    fn build_dotdot_record(
813        has_rrip: bool,
814        is_root: bool,
815        dir: &WrittenDirectory,
816        ty: EntryType,
817        options: &RripOptions,
818        fallback_time: &RripTime,
819        relocation_refs: &BTreeMap<(usize, EntryType), DirectoryRef>,
820    ) -> io::Result<PendingRecord> {
821        let split = if has_rrip {
822            let kind = if is_root {
823                RripEntryKind::RootDotDot {
824                    metadata: dir.metadata,
825                    nlink: 2 + dir.dirs.len() as u32,
826                }
827            } else {
828                RripEntryKind::DotDot {
829                    metadata: dir.metadata,
830                    nlink: 2 + dir.dirs.len() as u32,
831                }
832            };
833            let max = available_su_space(1); // name is b"\x01"
834            let mut builder = build_rrip_entries(kind, 0, options, fallback_time);
835            if let DirectoryRelocation::Moved { logical_parent, .. } = dir.relocation {
836                let parent = relocation_refs
837                    .get(&(logical_parent, ty))
838                    .copied()
839                    .unwrap_or_default();
840                builder.add_pl(parent.extent.0 as u32);
841            }
842            builder.build_split(max)
843        } else {
844            SplitSu::empty()
845        };
846        Ok(PendingRecord::parent_dir(split))
847    }
848
849    #[allow(clippy::too_many_arguments)]
850    fn build_directory_records(
851        records: &mut Vec<PendingRecord>,
852        dirs: &[WrittenDirectory],
853        ty: EntryType,
854        has_rrip: bool,
855        options: &RripOptions,
856        inode_counter: &mut u32,
857        fallback_time: &RripTime,
858        relocation_refs: &BTreeMap<(usize, EntryType), DirectoryRef>,
859    ) -> io::Result<()> {
860        for directory in dirs {
861            let converted_name = ty.convert_directory_name(&directory.name);
862            let split = if has_rrip {
863                let inode = *inode_counter;
864                *inode_counter += 1;
865                let max = available_su_space(converted_name.as_bytes().len());
866                let mut builder = build_rrip_entries(
867                    RripEntryKind::Directory {
868                        original_name: &directory.rrip_name,
869                        metadata: directory.metadata,
870                        nlink: 2 + directory.dirs.len() as u32,
871                    },
872                    inode,
873                    options,
874                    fallback_time,
875                );
876                match directory.relocation {
877                    DirectoryRelocation::Placeholder { target } => {
878                        let target = relocation_refs.get(&(target, ty)).ok_or_else(|| {
879                            io::Error::new(
880                                io::ErrorKind::InvalidData,
881                                "relocated directory extent was not written",
882                            )
883                        })?;
884                        builder.add_cl(target.extent.0 as u32);
885                    }
886                    DirectoryRelocation::Moved { .. } => {
887                        builder.add_re();
888                    }
889                    DirectoryRelocation::None => {}
890                }
891                builder.build_split(max)
892            } else {
893                SplitSu::empty()
894            };
895
896            let dir_ref = directory.get_dir_ref(ty, relocation_refs);
897            let flags = if has_rrip
898                && matches!(
899                    directory.relocation,
900                    DirectoryRelocation::Placeholder { .. }
901                ) {
902                FileFlags::empty()
903            } else {
904                FileFlags::DIRECTORY
905            };
906            let record = PendingRecord::new(&converted_name, split, dir_ref, flags);
907
908            records.push(record);
909        }
910        Ok(())
911    }
912
913    #[allow(clippy::too_many_arguments)]
914    fn build_file_records(
915        records: &mut Vec<PendingRecord>,
916        files: &[WrittenFile],
917        ty: EntryType,
918        has_rrip: bool,
919        options: &RripOptions,
920        inode_counter: &mut u32,
921        fallback_time: &RripTime,
922        sector_size: u64,
923    ) -> io::Result<()> {
924        for file in files {
925            let converted_name = ty.convert_name(&file.name);
926
927            // Main file entry (with RRIP if enabled)
928            let split = if has_rrip {
929                let inode = *inode_counter;
930                *inode_counter += 1;
931                let max = available_su_space(converted_name.as_bytes().len());
932                let kind = RripEntryKind::Entry {
933                    original_name: &file.name,
934                    metadata: file.metadata,
935                    kind: &file.kind,
936                };
937                build_rrip_entries(kind, inode, options, fallback_time).build_split(max)
938            } else {
939                SplitSu::empty()
940            };
941
942            let len = file.kind.file_len().unwrap_or(file.entry.size as u64);
943            let cursor = file.entry.extent.0 as u64 * sector_size;
944            let mut additional_extents =
945                ExtentIter::new(len, cursor, sector_size).skip(1).peekable();
946            let first_flags = if additional_extents.peek().is_none() {
947                FileFlags::empty()
948            } else {
949                FileFlags::NOT_FINAL
950            };
951
952            let first = PendingRecord::new(&converted_name, split, file.entry, first_flags);
953            records.push(first);
954
955            while let Some((dir_ref, _)) = additional_extents.next() {
956                let flags = if additional_extents.peek().is_none() {
957                    FileFlags::empty()
958                } else {
959                    FileFlags::NOT_FINAL
960                };
961
962                let record = PendingRecord::new(&converted_name, SplitSu::empty(), dir_ref, flags);
963                records.push(record);
964            }
965        }
966        Ok(())
967    }
968
969    fn deduplicate_names(records: &mut [PendingRecord], ty: EntryType) {
970        let mut seen: BTreeSet<PendingRecordName> = BTreeSet::new();
971        let mut start = 0;
972
973        while start < records.len() {
974            if records[start].name.len() == 1
975                && (records[start].name[0] == 0x00 || records[start].name[0] == 0x01)
976            {
977                start += 1;
978                continue;
979            }
980
981            let mut end = start + 1;
982            while end < records.len() && records[end - 1].flags.contains(FileFlags::NOT_FINAL) {
983                end += 1;
984            }
985
986            let original_name = records[start].name.clone();
987            let unique_name = if seen.contains(&original_name) {
988                let mut suffix = 1;
989                loop {
990                    let candidate = apply_dedup_suffix(&original_name, suffix, ty).into();
991                    suffix += 1;
992                    if !seen.contains(&candidate) {
993                        break candidate;
994                    }
995                }
996            } else {
997                original_name.clone()
998            };
999
1000            seen.insert(unique_name.clone());
1001
1002            for record in &mut records[start..end] {
1003                record.name = unique_name.clone();
1004            }
1005
1006            start = end;
1007        }
1008    }
1009
1010    fn sort_records(records: &mut [PendingRecord]) {
1011        records.sort_by(|a, b| {
1012            let rank = |name: &[u8]| match name {
1013                [0x00] => 0,
1014                [0x01] => 1,
1015                _ => 2,
1016            };
1017            rank(&a.name)
1018                .cmp(&rank(&b.name))
1019                .then_with(|| a.name.cmp(&b.name))
1020        });
1021    }
1022
1023    /// Total length of all overflow (continuation area) data.
1024    pub fn overflow_len(&self) -> u64 {
1025        self.0
1026            .iter()
1027            .filter(|r| r.split.has_overflow())
1028            .map(|r| r.split.overflow.len() as u64)
1029            .sum()
1030    }
1031
1032    /// Returns true if any record has RRIP overflow (needs continuation area).
1033    pub fn has_overflow(&self) -> bool {
1034        self.0.iter().any(|r| r.split.has_overflow())
1035    }
1036
1037    /// Iterates over pending records.
1038    pub fn iter(&self) -> impl Iterator<Item = &PendingRecord> {
1039        self.0.iter()
1040    }
1041
1042    /// Mutably iterates over pending records.
1043    pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut PendingRecord> {
1044        self.0.iter_mut()
1045    }
1046}
1047
1048/// A reference-counted, cloneable name for a pending directory record.
1049///
1050/// This type wraps an `Arc<[u8]>`, allowing efficient sharing of the name
1051/// across multiple records (e.g., multi-extent files where all extents share
1052/// the same name). It is cheap to clone and can be compared and hashed.
1053#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
1054pub struct PendingRecordName(Arc<[u8]>);
1055
1056impl From<Vec<u8>> for PendingRecordName {
1057    fn from(value: Vec<u8>) -> Self {
1058        Self(value.into())
1059    }
1060}
1061
1062impl Deref for PendingRecordName {
1063    type Target = [u8];
1064
1065    fn deref(&self) -> &Self::Target {
1066        &self.0
1067    }
1068}
1069
1070/// A pending directory record, built in phase 1 and written in phases 2-3.
1071#[derive(Debug, Clone)]
1072pub struct PendingRecord {
1073    /// File or directory name.
1074    pub name: PendingRecordName,
1075    /// System Use data (split into inline and overflow).
1076    pub split: SplitSu,
1077    /// Location and size of the file/dir.
1078    pub dir_ref: DirectoryRef,
1079    /// File flags (directory, hidden, etc.).
1080    pub flags: FileFlags,
1081}
1082
1083impl PendingRecord {
1084    /// Creates a new pending directory record.
1085    pub fn new(
1086        name: &ConvertedName,
1087        split: SplitSu,
1088        dir_ref: DirectoryRef,
1089        flags: FileFlags,
1090    ) -> Self {
1091        Self {
1092            name: PendingRecordName(name.as_bytes().into()),
1093            split,
1094            dir_ref,
1095            flags,
1096        }
1097    }
1098
1099    /// Current directory "." entry.
1100    pub fn current_dir(dot_split: SplitSu) -> Self {
1101        Self {
1102            name: PendingRecordName([0x00].into()),
1103            split: dot_split,
1104            dir_ref: DirectoryRef::default(),
1105            flags: FileFlags::DIRECTORY,
1106        }
1107    }
1108
1109    /// Parent directory ".." entry.
1110    pub fn parent_dir(dotdot_split: SplitSu) -> Self {
1111        Self {
1112            name: PendingRecordName([0x01].into()),
1113            split: dotdot_split,
1114            dir_ref: DirectoryRef::default(),
1115            flags: FileFlags::DIRECTORY,
1116        }
1117    }
1118}
1119
1120/// An iterator that splits a file into ISO 9660 extents.
1121pub struct ExtentIter {
1122    remaining: u64,
1123    cursor: u64,
1124    sector_size: u64,
1125}
1126
1127impl ExtentIter {
1128    /// Creates a new extent iterator.
1129    pub fn new(len: u64, cursor: u64, sector_size: u64) -> Self {
1130        Self {
1131            remaining: len,
1132            cursor,
1133            sector_size,
1134        }
1135    }
1136
1137    // ECMA-119 6.5.4: Non-final extents must be multiples of the logical block size.
1138    fn max_extent_size(&self) -> u64 {
1139        (u32::MAX as u64 / self.sector_size) * self.sector_size
1140    }
1141
1142    /// Collects all extents into a `Vec<DirectoryRef>` and returns the final cursor.
1143    #[cfg(test)]
1144    pub fn collect_with_cursor(self) -> (Vec<DirectoryRef>, u64) {
1145        let mut extents = Vec::new();
1146        let mut cursor = self.cursor;
1147
1148        for (extent, new_cursor) in self {
1149            extents.push(extent);
1150            cursor = new_cursor;
1151        }
1152
1153        (extents, cursor)
1154    }
1155}
1156
1157impl Iterator for ExtentIter {
1158    type Item = (DirectoryRef, u64);
1159
1160    fn next(&mut self) -> Option<Self::Item> {
1161        if self.remaining == 0 {
1162            return None;
1163        }
1164
1165        let chunk = self.remaining.min(self.max_extent_size());
1166
1167        let aligned = (self.cursor + self.sector_size - 1) & !(self.sector_size - 1);
1168        let extent = DirectoryRef::new((aligned / self.sector_size) as usize, chunk as usize);
1169
1170        self.cursor = aligned + chunk;
1171        self.remaining -= chunk;
1172
1173        Some((extent, self.cursor))
1174    }
1175}
1176
1177#[cfg(test)]
1178mod tests {
1179    use super::*;
1180
1181    #[test]
1182    fn extent_iter_splits_correctly() {
1183        let sector_size = 2048;
1184        let cursor = 0;
1185
1186        let len = u32::MAX as u64 + 1;
1187        let iter = ExtentIter::new(len, cursor, sector_size);
1188        let (extents, final_cursor) = iter.collect_with_cursor();
1189
1190        assert_eq!(extents.len(), 2);
1191
1192        assert_eq!(extents[0].size, 4_294_965_248);
1193        assert_eq!(extents[0].extent.0, 0);
1194
1195        assert_eq!(extents[1].size, 2048);
1196        let expected_sector: u64 = 4_294_965_248 / 2048;
1197        assert_eq!(extents[1].extent.0, expected_sector as usize);
1198
1199        assert_eq!(final_cursor, len);
1200    }
1201
1202    #[test]
1203    fn extent_iter_aligns_non_final_extents_to_sector_size() {
1204        let sector_size = 4096;
1205        let len = u32::MAX as u64 + 1;
1206        let (extents, final_cursor) = ExtentIter::new(len, 0, sector_size).collect_with_cursor();
1207
1208        assert_eq!(extents.len(), 2);
1209        assert_eq!(extents[0].size as u64, 4_294_963_200);
1210        assert_eq!(extents[0].size as u64 % sector_size, 0);
1211        assert_eq!(extents[1].size as u64, len - 4_294_963_200);
1212        assert_eq!(extents[1].extent.0 as u64, 4_294_963_200 / sector_size);
1213        assert_eq!(final_cursor, len);
1214    }
1215
1216    #[test]
1217    fn extent_iter_small_file() {
1218        let sector_size = 2048;
1219        let len = 1024; // < 4 GiB
1220        let iter = ExtentIter::new(len, 0, sector_size);
1221        let (extents, final_cursor) = iter.collect_with_cursor();
1222
1223        assert_eq!(extents.len(), 1);
1224        assert_eq!(extents[0].size, 1024);
1225        assert_eq!(extents[0].extent.0, 0);
1226        assert_eq!(final_cursor, 1024);
1227    }
1228
1229    #[test]
1230    fn deduplicates_logical_files_without_splitting_extent_names() {
1231        let record = |flags| PendingRecord {
1232            name: b"README.TXT;1".to_vec().into(),
1233            split: SplitSu::empty(),
1234            dir_ref: DirectoryRef::default(),
1235            flags,
1236        };
1237        let mut records = vec![
1238            record(FileFlags::NOT_FINAL),
1239            record(FileFlags::empty()),
1240            record(FileFlags::empty()),
1241        ];
1242
1243        PendingRecords::deduplicate_names(&mut records, EntryType::default());
1244
1245        assert_eq!(records[0].name, records[1].name);
1246        assert_ne!(records[1].name, records[2].name);
1247    }
1248}