Skip to main content

hadris_iso/write/
mod.rs

1use alloc::{collections::BTreeMap, sync::Arc};
2use core::fmt;
3
4pub mod estimator;
5/// APIs for writer.
6pub mod writer;
7
8use super::super::boot::{
9    BootCatalog, BootInfoTable, BootSectionEntry, ElToritoWriter, Grub2BootInfoTable, PlatformId,
10};
11use super::super::directory::{DirectoryRecord, DirectoryRef, FileFlags};
12use super::super::io::{self, Read, Seek, SeekFrom, Write};
13use super::super::io::{IsoCursor, LogicalSector};
14use super::super::path::PathTableRef;
15use super::super::read::PathSeparator;
16use super::super::rrip::{RripBuilder, RripOptions};
17use super::super::susp::SplitSu;
18use super::super::volume::{
19    BootRecordVolumeDescriptor, PrimaryVolumeDescriptor, SupplementaryVolumeDescriptor,
20    VolumeDescriptor, VolumeDescriptorHeader, VolumeDescriptorList, VolumeDescriptorType,
21};
22use crate::file::EntryType;
23use crate::joliet::JolietLevel;
24use crate::types::{Charset, IsoStr};
25use hadris_common::types::{
26    endian::{Endian, EndianType},
27    number::U32,
28};
29use hadris_part::{
30    GptDisk, GptDiskWriteExt, Le,
31    gpt::{GptPartitionEntry, Guid},
32    hybrid::HybridMbrBuilder,
33    mbr::{Chs, MasterBootRecord, MbrPartition, MbrPartitionType},
34};
35use options::PartitionScheme;
36use writer::{DirectoryRelocation, PathTableWriter, WrittenDirectory, WrittenFile, WrittenFiles};
37
38use alloc::{collections::VecDeque, string::String, vec, vec::Vec};
39
40/// APIs for options.
41pub mod options;
42use options::IsoFormatOptions;
43
44#[derive(Debug, thiserror::Error)]
45/// Identifies a FileConversionError value.
46pub enum FileConversionError {
47    #[error("I/O error: {0}")]
48    /// The `Io` variant.
49    Io(#[from] std::io::Error),
50    #[error("Path {0:?} is not a valid UTF-8 string")]
51    /// The `InvalidUtf8Path` variant.
52    InvalidUtf8Path(std::path::PathBuf),
53    #[error("Unsupported filesystem entry type at {0:?}")]
54    /// The `UnsupportedFileType` variant.
55    UnsupportedFileType(std::path::PathBuf),
56}
57
58/// A compact input tree for callers that do not need per-entry metadata.
59///
60/// [`InputTree`] is the richer model for Rock Ridge metadata and host
61/// filesystem imports. Both models are accepted by [`IsoImageWriter::create`].
62pub struct InputFiles {
63    /// Separator used by paths referenced from writer options.
64    pub path_separator: PathSeparator,
65    /// Root-level files and directories.
66    pub files: Vec<File>,
67}
68
69#[derive(Clone, PartialEq, Eq)]
70/// A file or directory in the compact [`InputFiles`] model.
71pub enum File {
72    /// The `File` variant.
73    File {
74        /// The `name` field.
75        name: Arc<String>,
76        /// The `contents` field.
77        contents: Vec<u8>,
78    },
79    /// The `Directory` variant.
80    Directory {
81        /// The `name` field.
82        name: Arc<String>,
83        /// The `children` field.
84        children: Vec<File>,
85    },
86}
87
88impl core::fmt::Debug for File {
89    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90        let mut dbg = f.debug_struct("File");
91        match self {
92            Self::Directory { name, children } => {
93                dbg.field("name", name);
94                dbg.field("children", children);
95            }
96            Self::File { name, contents } => {
97                dbg.field("name", name);
98                dbg.field("data_len", &contents.len());
99            }
100        }
101        dbg.finish()
102    }
103}
104
105impl File {
106    /// Performs the `name` operation.
107    pub fn name(&self) -> Arc<String> {
108        match self {
109            File::File { name, .. } => name.clone(),
110            File::Directory { name, .. } => name.clone(),
111        }
112    }
113}
114
115/// A metadata-aware tree used to create an ISO image.
116#[derive(Debug, Clone, PartialEq, Eq)]
117pub struct InputTree {
118    /// The `path_separator` field.
119    pub path_separator: PathSeparator,
120    /// The `entries` field.
121    pub entries: Vec<InputEntry>,
122}
123
124/// Optional POSIX metadata for an input entry.
125#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
126pub struct InputMetadata {
127    /// The `mode` field.
128    pub mode: Option<u32>,
129    /// The `uid` field.
130    pub uid: Option<u32>,
131    /// The `gid` field.
132    pub gid: Option<u32>,
133    /// Creation time as seconds since the Unix epoch.
134    pub created: Option<i64>,
135    /// Modification time as seconds since the Unix epoch.
136    pub modified: Option<i64>,
137    /// Access time as seconds since the Unix epoch.
138    pub accessed: Option<i64>,
139}
140
141/// The data represented by an [`InputEntry`].
142#[derive(Debug, Clone, PartialEq, Eq)]
143pub enum InputEntryKind {
144    /// The `File` variant.
145    File(Vec<u8>),
146    /// The `Directory` variant.
147    Directory(Vec<InputEntry>),
148    /// The `Symlink` variant.
149    Symlink(String),
150    /// A POSIX character device.
151    CharacterDevice {
152        /// Device-class identifier.
153        major: u32,
154        /// Device identifier within the class.
155        minor: u32,
156    },
157    /// A POSIX block device.
158    BlockDevice {
159        /// Device-class identifier.
160        major: u32,
161        /// Device identifier within the class.
162        minor: u32,
163    },
164}
165
166/// A named input entry and its optional host metadata.
167#[derive(Debug, Clone, PartialEq, Eq)]
168pub struct InputEntry {
169    /// The `name` field.
170    pub name: Arc<String>,
171    /// The `kind` field.
172    pub kind: InputEntryKind,
173    /// The `metadata` field.
174    pub metadata: InputMetadata,
175}
176
177impl InputEntry {
178    /// Performs the `file` operation.
179    pub fn file(name: impl Into<String>, contents: impl Into<Vec<u8>>) -> Self {
180        Self::new(name, InputEntryKind::File(contents.into()))
181    }
182
183    /// Performs the `directory` operation.
184    pub fn directory(name: impl Into<String>, children: Vec<Self>) -> Self {
185        Self::new(name, InputEntryKind::Directory(children))
186    }
187
188    /// Performs the `symlink` operation.
189    pub fn symlink(name: impl Into<String>, target: impl Into<String>) -> Self {
190        Self::new(name, InputEntryKind::Symlink(target.into()))
191    }
192
193    /// Performs the `character_device` operation.
194    pub fn character_device(name: impl Into<String>, major: u32, minor: u32) -> Self {
195        Self::new(name, InputEntryKind::CharacterDevice { major, minor })
196    }
197
198    /// Performs the `block_device` operation.
199    pub fn block_device(name: impl Into<String>, major: u32, minor: u32) -> Self {
200        Self::new(name, InputEntryKind::BlockDevice { major, minor })
201    }
202
203    /// Performs the `with_metadata` operation.
204    pub fn with_metadata(mut self, metadata: InputMetadata) -> Self {
205        self.metadata = metadata;
206        self
207    }
208
209    /// Performs the `name` operation.
210    pub fn name(&self) -> Arc<String> {
211        self.name.clone()
212    }
213
214    fn new(name: impl Into<String>, kind: InputEntryKind) -> Self {
215        Self {
216            name: Arc::new(name.into()),
217            kind,
218            metadata: InputMetadata::default(),
219        }
220    }
221}
222
223impl InputTree {
224    /// Performs the `new` operation.
225    pub fn new(path_separator: PathSeparator, entries: Vec<InputEntry>) -> Self {
226        Self {
227            path_separator,
228            entries,
229        }
230    }
231
232    /// Performs the `from_fs` operation.
233    pub fn from_fs(
234        root_path: &std::path::Path,
235        path_separator: PathSeparator,
236    ) -> core::result::Result<Self, FileConversionError> {
237        if !root_path.is_dir() {
238            return Err(FileConversionError::Io(std::io::Error::new(
239                std::io::ErrorKind::InvalidInput,
240                alloc::format!("Root path '{root_path:?}' is not a directory"),
241            )));
242        }
243        Ok(Self::new(
244            path_separator,
245            read_input_directory_recursively(root_path)?,
246        ))
247    }
248}
249
250impl From<InputFiles> for InputTree {
251    fn from(value: InputFiles) -> Self {
252        fn convert(file: File) -> InputEntry {
253            match file {
254                File::File { name, contents } => InputEntry {
255                    name,
256                    kind: InputEntryKind::File(contents),
257                    metadata: InputMetadata::default(),
258                },
259                File::Directory { name, children } => InputEntry {
260                    name,
261                    kind: InputEntryKind::Directory(children.into_iter().map(convert).collect()),
262                    metadata: InputMetadata::default(),
263                },
264            }
265        }
266        Self::new(
267            value.path_separator,
268            value.files.into_iter().map(convert).collect(),
269        )
270    }
271}
272
273fn system_time_seconds(value: std::io::Result<std::time::SystemTime>) -> Option<i64> {
274    value
275        .ok()?
276        .duration_since(std::time::UNIX_EPOCH)
277        .ok()
278        .and_then(|duration| i64::try_from(duration.as_secs()).ok())
279}
280
281fn read_input_directory_recursively(
282    current_path: &std::path::Path,
283) -> core::result::Result<Vec<InputEntry>, FileConversionError> {
284    use alloc::string::ToString;
285    let mut children = Vec::new();
286    for entry in std::fs::read_dir(current_path)? {
287        let entry = entry?;
288        let path = entry.path();
289        let name = path
290            .file_name()
291            .and_then(|value| value.to_str())
292            .ok_or_else(|| FileConversionError::InvalidUtf8Path(path.clone()))?
293            .to_string();
294        let fs_metadata = std::fs::symlink_metadata(&path)?;
295        let file_type = fs_metadata.file_type();
296        let mut metadata = InputMetadata {
297            created: system_time_seconds(fs_metadata.created()),
298            modified: system_time_seconds(fs_metadata.modified()),
299            accessed: system_time_seconds(fs_metadata.accessed()),
300            ..InputMetadata::default()
301        };
302        #[cfg(unix)]
303        {
304            use std::os::unix::fs::MetadataExt;
305            metadata.mode = Some(fs_metadata.mode() & 0o7777);
306            metadata.uid = Some(fs_metadata.uid());
307            metadata.gid = Some(fs_metadata.gid());
308        }
309        let kind = if file_type.is_file() {
310            InputEntryKind::File(std::fs::read(&path)?)
311        } else if file_type.is_dir() {
312            InputEntryKind::Directory(read_input_directory_recursively(&path)?)
313        } else if file_type.is_symlink() {
314            let target = std::fs::read_link(&path)?;
315            InputEntryKind::Symlink(
316                target
317                    .to_str()
318                    .ok_or_else(|| FileConversionError::InvalidUtf8Path(target.clone()))?
319                    .to_string(),
320            )
321        } else {
322            #[cfg(unix)]
323            {
324                use std::os::unix::fs::{FileTypeExt, MetadataExt};
325                let device = fs_metadata.rdev();
326                let major = ((device >> 8) & 0xfff) | ((device >> 32) & 0xfffff000);
327                let minor = (device & 0xff) | ((device >> 12) & 0xffffff00);
328                if file_type.is_char_device() {
329                    InputEntryKind::CharacterDevice {
330                        major: major as u32,
331                        minor: minor as u32,
332                    }
333                } else if file_type.is_block_device() {
334                    InputEntryKind::BlockDevice {
335                        major: major as u32,
336                        minor: minor as u32,
337                    }
338                } else {
339                    return Err(FileConversionError::UnsupportedFileType(path));
340                }
341            }
342            #[cfg(not(unix))]
343            return Err(FileConversionError::UnsupportedFileType(path));
344        };
345        children.push(InputEntry {
346            name: Arc::new(name),
347            kind,
348            metadata,
349        });
350    }
351    children.sort_by_key(|entry| entry.name.to_ascii_lowercase());
352    Ok(children)
353}
354
355fn validate_input_tree(tree: &InputTree, rrip: Option<&RripOptions>) -> io::Result<()> {
356    fn visit(
357        entries: &[InputEntry],
358        rrip: Option<&RripOptions>,
359        depth: usize,
360        path_len: usize,
361    ) -> io::Result<()> {
362        for entry in entries {
363            match &entry.kind {
364                InputEntryKind::Directory(children) => {
365                    let child_path_len = if path_len == 0 {
366                        entry.name.len()
367                    } else {
368                        path_len + 1 + entry.name.len()
369                    };
370                    if (depth >= 8 || child_path_len > 255)
371                        && !rrip
372                            .is_some_and(|options| options.enabled && options.relocate_deep_dirs)
373                    {
374                        return Err(io::Error::new(
375                            io::ErrorKind::InvalidInput,
376                            "directory depth or path length exceeds ISO 9660 limits and RRIP relocation is disabled",
377                        ));
378                    }
379                    visit(children, rrip, depth + 1, child_path_len)?;
380                }
381                InputEntryKind::Symlink(_) => {
382                    if !rrip.is_some_and(|options| options.enabled && options.preserve_symlinks) {
383                        return Err(io::Error::new(
384                            io::ErrorKind::InvalidInput,
385                            "symbolic links require RRIP preserve_symlinks",
386                        ));
387                    }
388                }
389                InputEntryKind::CharacterDevice { .. } | InputEntryKind::BlockDevice { .. } => {
390                    if !rrip.is_some_and(|options| options.enabled && options.preserve_devices) {
391                        return Err(io::Error::new(
392                            io::ErrorKind::InvalidInput,
393                            "device entries require RRIP preserve_devices",
394                        ));
395                    }
396                }
397                InputEntryKind::File(contents) => {
398                    if contents.len() as u64 > MAX_SINGLE_EXTENT_FILE_LEN {
399                        return Err(io::Error::new(
400                            io::ErrorKind::InvalidInput,
401                            "file exceeds 4 GiB; the ISO writer stores each file in a single \
402                             extent and cannot yet emit multi-extent records",
403                        ));
404                    }
405                }
406            }
407        }
408        Ok(())
409    }
410    visit(&tree.entries, rrip, 1, 0)
411}
412
413/// Largest file size a single ISO 9660 (ECMA-119 9.1) directory record can
414/// address through its 32-bit `data_len` field. Larger files require
415/// multi-extent records, which the writer does not yet emit — attempting to
416/// write one is rejected up front rather than silently truncated to `u32`.
417pub(crate) const MAX_SINGLE_EXTENT_FILE_LEN: u64 = u32::MAX as u64;
418
419fn relocate_deep_directories(files: &mut WrittenFiles) {
420    fn visit(
421        dir: &mut WrittenDirectory,
422        physical_depth: usize,
423        physical_path_len: usize,
424        moved: &mut Vec<WrittenDirectory>,
425        internal_id: &mut usize,
426    ) {
427        let mut retained = Vec::with_capacity(dir.dirs.len());
428        for mut child in core::mem::take(&mut dir.dirs) {
429            let child_path_len = if physical_path_len == 0 {
430                child.name.len()
431            } else {
432                physical_path_len + 1 + child.name.len()
433            };
434            if physical_depth + 1 > 8 || child_path_len > 255 {
435                let target = child.id;
436                let logical_parent = dir.id;
437                let original_name = child.rrip_name.clone();
438                child.name = Arc::new(alloc::format!("RRD{:06}", *internal_id));
439                *internal_id += 1;
440                child.relocation = DirectoryRelocation::Moved {
441                    id: target,
442                    logical_parent,
443                };
444                let relocated_path_len = "RR_MOVED".len() + 1 + child.name.len();
445                visit(&mut child, 3, relocated_path_len, moved, internal_id);
446                moved.push(child);
447
448                let mut placeholder = WrittenDirectory::new(original_name);
449                placeholder.relocation = DirectoryRelocation::Placeholder { target };
450                retained.push(placeholder);
451            } else {
452                visit(
453                    &mut child,
454                    physical_depth + 1,
455                    child_path_len,
456                    moved,
457                    internal_id,
458                );
459                retained.push(child);
460            }
461        }
462        dir.dirs = retained;
463    }
464
465    let root = files.get_mut(&files.root_dir());
466    let mut moved = Vec::new();
467    let mut internal_id = 1;
468    visit(root, 1, 0, &mut moved, &mut internal_id);
469    if moved.is_empty() {
470        return;
471    }
472
473    let occupied = root
474        .dirs
475        .iter()
476        .map(|directory| directory.name.as_str())
477        .collect::<std::collections::HashSet<_>>();
478    let mut relocation_name = String::from("RR_MOVED");
479    let mut suffix = 1;
480    while occupied.contains(relocation_name.as_str()) {
481        relocation_name = alloc::format!("RR_MOVED_{suffix}");
482        suffix += 1;
483    }
484    let mut relocation_dir = WrittenDirectory::new(Arc::new(relocation_name));
485    relocation_dir.id = usize::MAX;
486    relocation_dir.dirs = moved;
487    root.dirs.insert(0, relocation_dir);
488}
489
490#[derive(Debug, thiserror::Error)]
491/// Identifies a IsoCreationError value.
492pub enum IsoCreationError {
493    #[error(transparent)]
494    /// The `Io` variant.
495    Io(#[from] io::Error),
496}
497
498/// Canonical error for ISO creation operations.
499pub type Error = IsoCreationError;
500/// Canonical result for ISO creation operations.
501pub type Result<T> = core::result::Result<T, Error>;
502
503/// Represents IsoImageWriter.
504pub struct IsoImageWriter<DATA: Read + Write + Seek> {
505    data: IsoCursor<DATA>,
506    entry_types: Vec<EntryType>,
507    ops: IsoFormatOptions,
508    written_files: WrittenFiles,
509    path_tables: BTreeMap<EntryType, PathTableRef>,
510    inode_counter: u32,
511    rrip_time: [u8; 7],
512}
513
514/// The kind of directory entry, used to select which RRIP entries to emit.
515enum RripEntryKind<'a> {
516    /// Root directory's "." entry — needs SP + ER + PX + NM(CURRENT)
517    RootDot { metadata: InputMetadata, nlink: u32 },
518    /// Root directory's ".." entry — needs PX + NM(PARENT)
519    RootDotDot { metadata: InputMetadata, nlink: u32 },
520    /// Non-root "." entry — needs PX + NM(CURRENT)
521    Dot { metadata: InputMetadata, nlink: u32 },
522    /// Non-root ".." entry — needs PX + NM(PARENT)
523    DotDot { metadata: InputMetadata, nlink: u32 },
524    /// A named directory entry
525    Directory {
526        original_name: &'a str,
527        metadata: InputMetadata,
528        nlink: u32,
529    },
530    /// A named file entry
531    Entry {
532        original_name: &'a str,
533        metadata: InputMetadata,
534        kind: &'a InputEntryKind,
535    },
536}
537
538/// Compute the available system use space in a DirectoryRecord given
539/// the ISO name length. The record is 256 bytes max; the fixed header
540/// is 33 bytes, followed by the name (padded to even).
541fn available_su_space(iso_name_len: usize) -> usize {
542    let used = (33 + iso_name_len + 1) & !1; // pad to even boundary
543    256usize.saturating_sub(used)
544}
545
546/// Build complete RRIP entries for a directory record.
547///
548/// Entries are ordered by priority (most important first, largest last),
549/// so that `build_split` keeps the important ones inline and overflows
550/// the rest via a CE pointer.
551fn rrip_datetime(timestamp: Option<i64>, fallback: &[u8; 7]) -> [u8; 7] {
552    use chrono::{Datelike, Timelike};
553    let Some(timestamp) = timestamp.and_then(|value| chrono::DateTime::from_timestamp(value, 0))
554    else {
555        return *fallback;
556    };
557    [
558        (timestamp.year() - 1900).clamp(0, 255) as u8,
559        timestamp.month() as u8,
560        timestamp.day() as u8,
561        timestamp.hour() as u8,
562        timestamp.minute() as u8,
563        timestamp.second() as u8,
564        0,
565    ]
566}
567
568fn build_rrip_entries(
569    kind: RripEntryKind<'_>,
570    inode: u32,
571    options: &RripOptions,
572    fallback_time: &[u8; 7],
573) -> RripBuilder {
574    let mut builder = RripBuilder::new();
575    let add_common = |builder: &mut RripBuilder,
576                      metadata: InputMetadata,
577                      type_mode: u32,
578                      default_permissions: u32,
579                      nlink: u32| {
580        let permissions = if options.preserve_permissions {
581            metadata.mode.unwrap_or(default_permissions)
582        } else {
583            default_permissions
584        };
585        let (uid, gid) = if options.preserve_ownership {
586            (metadata.uid.unwrap_or(0), metadata.gid.unwrap_or(0))
587        } else {
588            (0, 0)
589        };
590        builder.add_px(type_mode | permissions, nlink, uid, gid, inode);
591        if options.preserve_timestamps {
592            let modified = rrip_datetime(metadata.modified, fallback_time);
593            let accessed = rrip_datetime(metadata.accessed, fallback_time);
594            // Emit the creation timestamp when the input carries one; in-memory
595            // entries without a creation time keep the modify/access-only form.
596            let created = metadata
597                .created
598                .map(|created| rrip_datetime(Some(created), fallback_time));
599            builder.add_tf(created.as_ref(), &modified, &accessed);
600        }
601    };
602
603    match &kind {
604        RripEntryKind::RootDot { metadata, nlink } => {
605            builder.add_sp(0);
606            add_common(&mut builder, *metadata, 0o040000, 0o755, *nlink);
607            builder.add_nm_current();
608            builder.add_rrip_er(); // full ER, last (largest)
609        }
610        RripEntryKind::RootDotDot { metadata, nlink } => {
611            add_common(&mut builder, *metadata, 0o040000, 0o755, *nlink);
612            builder.add_nm_parent();
613        }
614        RripEntryKind::Dot { metadata, nlink } => {
615            add_common(&mut builder, *metadata, 0o040000, 0o755, *nlink);
616            builder.add_nm_current();
617        }
618        RripEntryKind::DotDot { metadata, nlink } => {
619            add_common(&mut builder, *metadata, 0o040000, 0o755, *nlink);
620            builder.add_nm_parent();
621        }
622        RripEntryKind::Directory {
623            original_name,
624            metadata,
625            nlink,
626        } => {
627            add_common(&mut builder, *metadata, 0o040000, 0o755, *nlink);
628            builder.add_nm(original_name.as_bytes());
629        }
630        RripEntryKind::Entry {
631            original_name,
632            metadata,
633            kind,
634        } => {
635            let (type_mode, default_permissions) = match kind {
636                InputEntryKind::File(_) => (0o100000, 0o644),
637                InputEntryKind::Symlink(_) => (0o120000, 0o777),
638                InputEntryKind::CharacterDevice { .. } => (0o020000, 0o600),
639                InputEntryKind::BlockDevice { .. } => (0o060000, 0o600),
640                InputEntryKind::Directory(_) => unreachable!(),
641            };
642            add_common(&mut builder, *metadata, type_mode, default_permissions, 1);
643            builder.add_nm(original_name.as_bytes());
644            match kind {
645                InputEntryKind::Symlink(target) => {
646                    builder.add_sl(target);
647                }
648                InputEntryKind::CharacterDevice { major, minor }
649                | InputEntryKind::BlockDevice { major, minor } => {
650                    builder.add_pn(*major, *minor);
651                }
652                _ => {}
653            }
654        }
655    }
656
657    builder
658}
659
660/// Apply a deduplication suffix to a name, producing e.g. `READM_1.TXT;1`.
661///
662/// The suffix `_N` is inserted before the extension (and before any `;1` version
663/// suffix). The basename is truncated if needed to stay within format limits.
664fn apply_dedup_suffix(name: &[u8], n: usize, ty: EntryType) -> Vec<u8> {
665    let suffix = alloc::format!("_{n}");
666    let suffix_bytes = suffix.as_bytes();
667
668    match ty {
669        EntryType::Joliet { .. } => {
670            // Joliet: UTF-16 BE, find the dot (0x00 0x2E) or end
671            let mut dot_pos = None;
672            let mut i = 0;
673            while i + 1 < name.len() {
674                if name[i] == 0x00 && name[i + 1] == 0x2E {
675                    dot_pos = Some(i);
676                }
677                i += 2;
678            }
679            let (basename, ext) = match dot_pos {
680                Some(pos) => (&name[..pos], &name[pos..]),
681                None => (name, &[][..]),
682            };
683            // Convert suffix to UTF-16 BE
684            let suffix_u16: Vec<u8> = suffix
685                .encode_utf16()
686                .flat_map(|c| c.to_be_bytes())
687                .collect();
688            // Max 206 bytes (103 code units) for Joliet
689            let max_basename = 206usize.saturating_sub(ext.len() + suffix_u16.len());
690            let trunc_basename = &basename[..basename.len().min(max_basename) & !1];
691            let mut result =
692                Vec::with_capacity(trunc_basename.len() + suffix_u16.len() + ext.len());
693            result.extend_from_slice(trunc_basename);
694            result.extend_from_slice(&suffix_u16);
695            result.extend_from_slice(ext);
696            result
697        }
698        _ => {
699            // ASCII-based names (L1, L2, L3)
700            // Strip ";1" version suffix if present
701            let (base_name, version) = if name.ends_with(b";1") {
702                (&name[..name.len() - 2], &b";1"[..])
703            } else {
704                (name, &[][..])
705            };
706            // Find the dot separator
707            let dot_pos = base_name.iter().rposition(|&b| b == b'.');
708            let (basename, ext) = match dot_pos {
709                Some(pos) => (&base_name[..pos], &base_name[pos..]),
710                None => (base_name, &[][..]),
711            };
712            // Determine max basename length based on level
713            let max_total = match ty {
714                EntryType::Level1 { .. } => 8,
715                EntryType::Level2 { .. } => 30usize.saturating_sub(ext.len()),
716                _ => 207usize.saturating_sub(ext.len() + version.len()),
717            };
718            let max_basename = max_total.saturating_sub(suffix_bytes.len());
719            let trunc_basename = &basename[..basename.len().min(max_basename)];
720            let mut result = Vec::with_capacity(
721                trunc_basename.len() + suffix_bytes.len() + ext.len() + version.len(),
722            );
723            result.extend_from_slice(trunc_basename);
724            result.extend_from_slice(suffix_bytes);
725            result.extend_from_slice(ext);
726            result.extend_from_slice(version);
727            result
728        }
729    }
730}
731
732/// A pending directory record, built in phase 1 and written in phases 2-3.
733struct PendingRecord {
734    name: Vec<u8>,
735    split: SplitSu,
736    dir_ref: DirectoryRef,
737    flags: FileFlags,
738}
739
740io_transform! {
741impl<DATA: Read + Write + Seek> IsoImageWriter<DATA> {
742    /// Creates a complete ISO image and returns its output target.
743    pub async fn create<T: Into<InputTree>>(
744        data: DATA,
745        files: T,
746        ops: IsoFormatOptions,
747    ) -> Result<DATA> {
748        Self::create_with_allocation_floor(data, files, ops, None).await
749    }
750
751    /// Creates an ISO image while keeping file and directory allocations at or
752    /// after an optional logical-sector floor.
753    ///
754    /// This is useful when composing ISO 9660 with another on-disc format whose
755    /// metadata occupies sectors after the ISO volume descriptor sequence. A
756    /// floor below the end of the descriptors has no effect. Volume descriptors
757    /// and system-area structures retain their specification-defined locations.
758    pub async fn create_with_allocation_floor<T: Into<InputTree>>(
759        data: DATA,
760        files: T,
761        ops: IsoFormatOptions,
762        allocation_floor: Option<u32>,
763    ) -> Result<DATA> {
764        let mut files = files.into();
765        if ops.sector_size != 2048 {
766            return Err(io::Error::new(
767                io::ErrorKind::InvalidInput,
768                "ISO creation currently requires 2048-byte logical sectors",
769            )
770            .into());
771        }
772        validate_input_tree(&files, ops.features.rock_ridge.as_ref())?;
773        let mut writer = Self::new(data, ops);
774        writer.write_volume_descriptors(&mut files).await?;
775        if let Some(sector) = allocation_floor {
776            let current = writer
777                .data
778                .stream_position()
779                .await
780                .map_err(io::Error::erase)?;
781            let floor = u64::from(sector)
782                .checked_mul(writer.ops.sector_size as u64)
783                .ok_or_else(|| {
784                    io::Error::new(io::ErrorKind::InvalidInput, "allocation floor overflow")
785                })?;
786            if floor > current {
787                writer
788                    .data
789                    .seek(SeekFrom::Start(floor))
790                    .await
791                    .map_err(io::Error::erase)?;
792            }
793        }
794        let root_dirs = writer.write_files(&files).await?;
795        writer.write_path_tables().await?;
796        writer.finalize_volume_descriptors(root_dirs).await?;
797        Ok(writer.into_inner())
798    }
799
800    /// Returns the output target.
801    pub fn into_inner(self) -> DATA {
802        self.data.into_inner()
803    }
804
805    fn new(data: DATA, ops: IsoFormatOptions) -> Self {
806        let now = super::super::directory::DirDateTime::now();
807        let rrip_time = *<&[u8; 7]>::try_from(bytemuck::bytes_of(&now)).unwrap();
808        let mut entry_types = Vec::new();
809        // The base (PVD) entry type inherits supports_rrip from the filenames config
810        entry_types.push(ops.features.filenames.into());
811        if ops.features.long_filenames {
812            entry_types.push(EntryType::Level3 {
813                supports_lowercase: true,
814                supports_rrip: false,
815            });
816        }
817        if let Some(joliet) = ops.features.joliet {
818            entry_types.push(joliet.into());
819        }
820
821        Self {
822            data: IsoCursor::new(data, ops.sector_size),
823            ops,
824            entry_types,
825            written_files: WrittenFiles::new(),
826            path_tables: BTreeMap::new(),
827            inode_counter: 1,
828            rrip_time,
829        }
830    }
831
832    const VOLUME_DESCRIPTOR_SET_START: LogicalSector = LogicalSector(16);
833
834    fn parse_iso_str<C: Charset, const N: usize>(
835        &self,
836        s: &str,
837        field_name: &'static str,
838    ) -> io::Result<IsoStr<C, N>> {
839        if self.ops.strict_charset {
840            IsoStr::from_str_lossy(s)
841        } else {
842            IsoStr::from_str_unchecked(s)
843        }
844        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, field_name))
845    }
846
847    async fn write_volume_descriptors(&mut self, files: &mut InputTree) -> io::Result<()> {
848        self.data.seek_sector(Self::VOLUME_DESCRIPTOR_SET_START).await?;
849        let mut volume_descriptors = VolumeDescriptorList::empty();
850        for &entry in &self.entry_types {
851            match entry {
852                EntryType::Level1 { .. } | EntryType::Level2 { .. } => {
853                    let mut pvd = PrimaryVolumeDescriptor::new(&self.ops.volume_name, 0);
854                    pvd.volume_identifier = self.parse_iso_str(&self.ops.volume_name, "volume name")?;
855                    pvd.dir_record.header.len = 34;
856                    pvd.dir_record.header.flags = FileFlags::DIRECTORY.bits();
857                    pvd.dir_record.header.file_identifier_len = 1;
858                    pvd.dir_record.header.volume_sequence_number.write(1);
859                    pvd.volume_sequence_number.write(1);
860                    if let Some(s) = &self.ops.system_id {
861                        pvd.system_identifier = self.parse_iso_str(s, "system identifier")?;
862                    }
863                    if let Some(s) = &self.ops.volume_set_id {
864                        pvd.volume_set_identifier = self.parse_iso_str(s, "volume set identifier")?;
865                    }
866                    if let Some(s) = &self.ops.publisher_id {
867                        pvd.publisher_identifier = self.parse_iso_str(s, "publisher identifier")?;
868                    }
869                    if let Some(s) = &self.ops.preparer_id {
870                        pvd.preparer_identifier = self.parse_iso_str(s, "preparer identifier")?;
871                    }
872                    if let Some(s) = &self.ops.application_id {
873                        pvd.application_identifier = self.parse_iso_str(s, "application identifier")?;
874                    }
875                    volume_descriptors.push(VolumeDescriptor::Primary(pvd));
876                }
877                EntryType::Level3 { .. } => {
878                    // Version 2 for EVD
879                    let mut evd = SupplementaryVolumeDescriptor::new_evd(&self.ops.volume_name, 0);
880                    evd.volume_identifier = self.parse_iso_str(&self.ops.volume_name, "volume name")?;
881                    evd.dir_record.header.len = 34;
882                    evd.dir_record.header.flags = FileFlags::DIRECTORY.bits();
883                    evd.dir_record.header.file_identifier_len = 1;
884                    evd.dir_record.header.volume_sequence_number.write(1);
885                    evd.volume_sequence_number.write(1);
886                    volume_descriptors.push(VolumeDescriptor::Supplementary(evd));
887                }
888                EntryType::Joliet { level, .. } => {
889                    let mut svd = SupplementaryVolumeDescriptor::new_svd(
890                        &self.ops.volume_name,
891                        0,
892                        level.escape_sequence(),
893                    );
894                    svd.dir_record.header.len = 34;
895                    svd.dir_record.header.flags = FileFlags::DIRECTORY.bits();
896                    svd.dir_record.header.file_identifier_len = 1;
897                    svd.dir_record.header.volume_sequence_number.write(1);
898                    svd.volume_sequence_number.write(1);
899                    if let Some(s) = &self.ops.system_id {
900                        svd.system_identifier = SupplementaryVolumeDescriptor::utf16be_str(s);
901                    }
902                    if let Some(s) = &self.ops.volume_set_id {
903                        svd.volume_set_identifier = SupplementaryVolumeDescriptor::utf16be_str(s);
904                    }
905                    if let Some(s) = &self.ops.publisher_id {
906                        svd.publisher_identifier = SupplementaryVolumeDescriptor::utf16be_str(s);
907                    }
908                    if let Some(s) = &self.ops.preparer_id {
909                        svd.preparer_identifier = SupplementaryVolumeDescriptor::utf16be_str(s);
910                    }
911                    if let Some(s) = &self.ops.application_id {
912                        svd.application_identifier = SupplementaryVolumeDescriptor::utf16be_str(s);
913                    }
914                    volume_descriptors.push(VolumeDescriptor::Supplementary(svd));
915                }
916            }
917        }
918
919        if let Some(boot) = &self.ops.features.el_torito {
920            let boot_record = ElToritoWriter::create_descriptor(boot, files);
921            volume_descriptors.insert(1, VolumeDescriptor::BootRecord(boot_record));
922        }
923
924        volume_descriptors.write(&mut self.data).await?;
925        Ok(())
926    }
927
928    async fn finalize_volume_descriptors(
929        &mut self,
930        root_dirs: BTreeMap<EntryType, DirectoryRef>,
931    ) -> io::Result<()> {
932        // Write boot catalog
933        let catalog_ptr = if let Some(boot) = &self.ops.features.el_torito {
934            let mut catalog = BootCatalog::default();
935            let current_sector = self.data.pad_align_sector().await?;
936
937            for (section, entry) in boot.sections() {
938                let dir_ref = self
939                    .written_files
940                    .find_file(&entry.boot_image_path, self.ops.path_separator)
941                    .ok_or_else(|| {
942                        io::Error::new(
943                            io::ErrorKind::NotFound,
944                            "boot image file not found",
945                        )
946                    })?;
947                let load_size = entry.load_size.map(core::num::NonZeroU16::get).unwrap_or_else(
948                    || {
949                        // Emulated media load a single virtual boot sector; the
950                        // firmware then services the emulated disk. No-emulation
951                        // entries load the whole image as 512-byte sectors.
952                        if entry.emulation.is_emulated() {
953                            1
954                        } else {
955                            dir_ref.size.div_ceil(512) as u16
956                        }
957                    },
958                );
959                let boot_image_lba = dir_ref.extent.0 as u32;
960                let boot_entry =
961                    BootSectionEntry::new(entry.emulation, 0, load_size, boot_image_lba);
962                if let Some(section) = section {
963                    catalog.add_section(section.platform, vec![boot_entry]);
964                } else {
965                    catalog.set_default_entry(boot_entry);
966                }
967
968                // Handle boot info table (standard El-Torito) or GRUB2 boot info
969                // Both use similar format at offset 8 in the boot image
970                if entry.boot_info_table || entry.grub2_boot_info {
971                    // Boot info table requires at least 64 bytes in the boot image
972                    // (header is at offset 8-56/64, checksum covers bytes 64+)
973                    if dir_ref.size < 64 {
974                        return Err(io::Error::new(
975                            io::ErrorKind::InvalidInput,
976                            "boot image too small for boot info table (minimum 64 bytes)",
977                        ));
978                    }
979
980                    let mut checksum = 0u32;
981                    let mut buffer = [0u8; 4];
982                    let byte_offset = (boot_image_lba as u64) * self.ops.sector_size as u64;
983                    self.data
984                        .seek(SeekFrom::Start(byte_offset + 64))
985                        .await
986                        .map_err(io::Error::erase)?;
987                    // Calculate checksum for all 4-byte chunks from offset 64 to end
988                    let checksum_bytes = dir_ref.size - 64;
989                    for _ in 0..(checksum_bytes / 4) {
990                        self.data.read_exact(&mut buffer).await?;
991                        checksum = checksum.wrapping_add(u32::from_le_bytes(buffer));
992                    }
993
994                    const TABLE_OFFSET: u64 = 8;
995                    self.data
996                        .seek(SeekFrom::Start(byte_offset + TABLE_OFFSET))
997                        .await
998                        .map_err(io::Error::erase)?;
999
1000                    if entry.grub2_boot_info {
1001                        // GRUB2/ISOLINUX uses extended 56-byte format with reserved bytes
1002                        let table = Grub2BootInfoTable {
1003                            pvd_lba: U32::new(16),
1004                            file_lba: U32::new(dir_ref.extent.0 as u32),
1005                            file_len: U32::new(dir_ref.size as u32),
1006                            checksum: U32::new(checksum),
1007                            reserved: [0u8; 40],
1008                        };
1009                        self.data.write_all(bytemuck::bytes_of(&table)).await?;
1010                    } else {
1011                        // Standard El-Torito 16-byte format
1012                        let table = BootInfoTable {
1013                            iso_start: U32::new(16),
1014                            file_lba: U32::new(dir_ref.extent.0 as u32),
1015                            file_len: U32::new(dir_ref.size as u32),
1016                            checksum: U32::new(checksum),
1017                        };
1018                        self.data.write_all(bytemuck::bytes_of(&table)).await?;
1019                    }
1020                }
1021            }
1022
1023            if boot.write_boot_catalog {
1024                let dir_ref = self
1025                    .written_files
1026                    .find_file("boot.catalog", self.ops.path_separator)
1027                    .ok_or_else(|| {
1028                        io::Error::new(
1029                            io::ErrorKind::NotFound,
1030                            "boot.catalog file not found in written files",
1031                        )
1032                    })?;
1033                self.data.seek_sector(dir_ref.extent).await?;
1034                if dir_ref.size < catalog.size() {
1035                    return Err(io::Error::new(
1036                        io::ErrorKind::InvalidData,
1037                        "boot.catalog file too small",
1038                    ));
1039                }
1040                catalog.write(&mut self.data).await?;
1041                self.data.seek_sector(current_sector).await?;
1042
1043                Some(dir_ref.extent.0 as u32)
1044            } else {
1045                self.data.seek_sector(current_sector).await?;
1046                catalog.write(&mut self.data).await?;
1047                self.data.pad_align_sector().await?;
1048                Some(current_sector.0 as u32)
1049            }
1050        } else {
1051            None
1052        };
1053
1054        let end_position = self
1055            .data
1056            .stream_position()
1057            .await
1058            .map_err(io::Error::erase)?;
1059        let end_sector = self.data.pad_align_sector().await?;
1060        let volume_space = self.volume_space_sectors(end_sector);
1061        let image_len = end_sector.0 as u64 * self.ops.sector_size as u64;
1062        if alignment_requires_materialization(end_position, image_len) {
1063            self.data
1064                .seek(SeekFrom::Start(image_len - 1))
1065                .await
1066                .map_err(io::Error::erase)?;
1067            self.data.write_all(&[0]).await?;
1068        }
1069        self.data.seek_sector(Self::VOLUME_DESCRIPTOR_SET_START).await?;
1070
1071        let mut buffer = vec![0u8; self.ops.sector_size];
1072        loop {
1073            self.data.read_exact(&mut buffer).await?;
1074            let header = VolumeDescriptorHeader::from_bytes(&buffer[0..7]);
1075            let ty = VolumeDescriptorType::from_u8(header.descriptor_type);
1076            if let VolumeDescriptorType::VolumeSetTerminator = ty {
1077                break;
1078            }
1079            if !header.is_valid() {
1080                return Err(io::Error::new(
1081                    io::ErrorKind::InvalidData,
1082                    "invalid volume descriptor header during finalization",
1083                ));
1084            }
1085
1086            match ty {
1087                VolumeDescriptorType::PrimaryVolumeDescriptor => {
1088                    let base_type = self
1089                        .entry_types
1090                        .iter()
1091                        .find(|e| matches!(e, EntryType::Level1 { .. } | EntryType::Level2 { .. }))
1092                        .ok_or_else(|| {
1093                            io::Error::new(
1094                                io::ErrorKind::InvalidData,
1095                                "no base Level entry type found for PVD",
1096                            )
1097                        })?;
1098                    let root_dir = root_dirs.get(base_type).ok_or_else(|| {
1099                        io::Error::new(
1100                            io::ErrorKind::InvalidData,
1101                            "root directory not found for PVD entry type",
1102                        )
1103                    })?;
1104                    let pt = self.path_tables.get(base_type).ok_or_else(|| {
1105                        io::Error::new(
1106                            io::ErrorKind::InvalidData,
1107                            "path table not found for PVD entry type",
1108                        )
1109                    })?;
1110                    let pvd = bytemuck::from_bytes_mut::<PrimaryVolumeDescriptor>(&mut buffer);
1111                    pvd.dir_record.header.extent.write(root_dir.extent.0 as u32);
1112                    pvd.dir_record.header.data_len.write(root_dir.size as u32);
1113                    pvd.type_l_path_table.set(pt.lpt.0 as u32);
1114                    pvd.type_m_path_table.set(pt.mpt.0 as u32);
1115                    pvd.path_table_size.write(pt.size as u32);
1116                    pvd.volume_space_size.write(volume_space);
1117                }
1118                VolumeDescriptorType::SupplementaryVolumeDescriptor => {
1119                    let svd =
1120                        bytemuck::from_bytes_mut::<SupplementaryVolumeDescriptor>(&mut buffer);
1121                    match svd.header.version {
1122                        1 => {
1123                            for &level in JolietLevel::all() {
1124                                if svd.escape_sequences == level.escape_sequence() {
1125                                    let Some(joliet) = self
1126                                        .entry_types
1127                                        .iter()
1128                                        .find(
1129                                            |e| matches!(e, EntryType::Joliet{ level: jl, ..} if *jl == level),
1130                                        )
1131                                    else {
1132                                        continue;
1133                                    };
1134                                    let Some(root_dir) = root_dirs.get(joliet) else {
1135                                        continue;
1136                                    };
1137                                    let Some(pt) = self.path_tables.get(joliet) else {
1138                                        continue;
1139                                    };
1140
1141                                    svd.dir_record.header.extent.write(root_dir.extent.0 as u32);
1142                                    svd.dir_record.header.data_len.write(root_dir.size as u32);
1143                                    svd.type_l_path_table.set(pt.lpt.0 as u32);
1144                                    svd.type_m_path_table.set(pt.mpt.0 as u32);
1145                                    svd.path_table_size.write(pt.size as u32);
1146                                    svd.volume_space_size.write(volume_space);
1147                                }
1148                            }
1149                        }
1150                        2 => {
1151                            if svd.escape_sequences != [b' '; 32] {
1152                                // We don't recognize this EVD
1153                                continue;
1154                            }
1155
1156                            let Some(l3) = self
1157                                .entry_types
1158                                .iter()
1159                                .find(|e| matches!(e, EntryType::Level3 { .. }))
1160                            else {
1161                                continue;
1162                            };
1163                            let Some(root_dir) = root_dirs.get(l3) else {
1164                                continue;
1165                            };
1166                            let Some(pt) = self.path_tables.get(l3) else {
1167                                continue;
1168                            };
1169                            svd.dir_record.header.extent.write(root_dir.extent.0 as u32);
1170                            svd.dir_record.header.data_len.write(root_dir.size as u32);
1171                            svd.type_l_path_table.set(pt.lpt.0 as u32);
1172                            svd.type_m_path_table.set(pt.mpt.0 as u32);
1173                            svd.path_table_size.write(pt.size as u32);
1174                            svd.volume_space_size.write(volume_space);
1175                        }
1176
1177                        // Unknown version
1178                        _ => {}
1179                    }
1180                }
1181                VolumeDescriptorType::BootRecord => {
1182                    let Some(catalog_ptr) = catalog_ptr else {
1183                        return Err(io::Error::new(
1184                            io::ErrorKind::InvalidData,
1185                            "boot record found but no boot catalog was written",
1186                        ));
1187                    };
1188                    let boot_record =
1189                        bytemuck::from_bytes_mut::<BootRecordVolumeDescriptor>(&mut buffer);
1190                    boot_record.catalog_ptr.set(catalog_ptr);
1191                }
1192                // We don't do anything
1193                _ => continue,
1194            }
1195
1196            // Write the new data
1197            self.data
1198                .seek_relative(-(buffer.len() as i64))
1199                .await
1200                .map_err(io::Error::erase)?;
1201            self.data.write_all(&buffer).await?;
1202        }
1203
1204        // Now we finalize the partition tables based on hybrid boot options
1205        self.write_partition_tables(end_sector).await?;
1206
1207        Ok(())
1208    }
1209
1210    async fn write_files(&mut self, files: &InputTree) -> io::Result<BTreeMap<EntryType, DirectoryRef>> {
1211        let mut next_directory_id = 1usize;
1212        {
1213            let walker = FileTreeWalker::new(files);
1214            let mut current_dir = self.written_files.root_dir();
1215            for file in walker {
1216                match file {
1217                    TreeWalkerItem::EnterDirectory(dir) => {
1218                        let name = dir.name();
1219                        let metadata = dir.metadata;
1220                        let written_dir = self.written_files.get_mut(&current_dir);
1221                        let index = written_dir.push_dir(name, metadata);
1222                        written_dir.dirs[index].id = next_directory_id;
1223                        next_directory_id += 1;
1224                        current_dir.push(index);
1225                    }
1226                    TreeWalkerItem::ExitDirectory(_dir) => {
1227                        current_dir.pop();
1228                    }
1229                    TreeWalkerItem::File(file) => {
1230                        // Extents are assigned in the planning pass below.
1231                        // Empty files keep extent 0 (per ISO 9660 they have no
1232                        // data to reference).
1233                        let dir = self.written_files.get_mut(&current_dir);
1234                        dir.files.push(WrittenFile {
1235                            name: file.name.clone(),
1236                            entry: DirectoryRef {
1237                                extent: LogicalSector(0),
1238                                size: 0,
1239                            },
1240                            kind: file.kind.clone(),
1241                            metadata: file.metadata,
1242                        });
1243                    }
1244                };
1245            }
1246        }
1247
1248        if self
1249            .ops
1250            .features
1251            .rock_ridge
1252            .is_some_and(|options| options.enabled && options.relocate_deep_dirs)
1253        {
1254            relocate_deep_directories(&mut self.written_files);
1255        }
1256
1257        // Pre-order: parents before children. libarchive (bsdtar) scans
1258        // directories as a stream and ignores a directory whose extent is
1259        // lower than its current position, so extents must ascend from
1260        // parents to children.
1261        fn collect_preorder(
1262            files: &WrittenFiles,
1263            id: &writer::DirectoryId,
1264            output: &mut Vec<writer::DirectoryId>,
1265        ) {
1266            output.push(id.clone());
1267            let dir = files.get(id);
1268            for (index, child) in dir.dirs.iter().enumerate() {
1269                if matches!(child.relocation, DirectoryRelocation::Placeholder { .. }) {
1270                    continue;
1271                }
1272                let mut child_id = id.clone();
1273                child_id.push(index);
1274                collect_preorder(files, &child_id, output);
1275            }
1276        }
1277
1278        let root_id = self.written_files.root_dir();
1279        let mut order = Vec::new();
1280        collect_preorder(&self.written_files, &root_id, &mut order);
1281
1282        let mut file_order = Vec::new();
1283        for directory_id in &order {
1284            let dir = self.written_files.get(directory_id);
1285            for (index, file) in dir.files.iter().enumerate() {
1286                if matches!(&file.kind, InputEntryKind::File(contents) if !contents.is_empty()) {
1287                    file_order.push((directory_id.clone(), index));
1288                }
1289            }
1290        }
1291
1292        let sector_size = self.ops.sector_size as u64;
1293        let rrip_options = self.ops.features.rock_ridge;
1294        let rrip_time = self.rrip_time;
1295        let entry_types = self.entry_types.clone();
1296        let mut inode_counter = self.inode_counter;
1297
1298        // ── Planning pass ──
1299        //
1300        // Build every directory's records once to measure its size, then
1301        // assign directory extents in pre-order and file data extents after
1302        // the whole directory region. Record sizes never depend on extent
1303        // *values*, so the write pass below reproduces this layout exactly.
1304        // libarchive requires continuation areas to precede the data extents
1305        // of regular files carrying CE entries, which file-data-last gives.
1306        let mut cursor = self
1307            .data
1308            .stream_position()
1309            .await
1310            .map_err(io::Error::erase)?;
1311        let mut relocation_refs: BTreeMap<(usize, EntryType), DirectoryRef> = BTreeMap::new();
1312        // Default refs tolerate forward references (unassigned child extents,
1313        // relocation targets) while sizing; the write pass rebuilds the
1314        // records with the real values.
1315        let mut default_refs: BTreeMap<(usize, EntryType), DirectoryRef> = BTreeMap::new();
1316        for directory_id in &order {
1317            let dir = self.written_files.get(directory_id);
1318            for ty in &entry_types {
1319                default_refs.insert((dir.id, *ty), DirectoryRef::default());
1320                if let DirectoryRelocation::Moved { id, .. } = dir.relocation {
1321                    default_refs.insert((id, *ty), DirectoryRef::default());
1322                }
1323            }
1324            let dir = self.written_files.get_mut(directory_id);
1325            for ty in &entry_types {
1326                dir.entries.entry(*ty).or_default();
1327            }
1328        }
1329        for directory_id in &order {
1330            let is_root = directory_id == &root_id;
1331            for ty in &entry_types {
1332                let dir = self.written_files.get(directory_id);
1333                let records = Self::build_directory_records(
1334                    *ty,
1335                    dir,
1336                    is_root,
1337                    &mut inode_counter,
1338                    rrip_options.as_ref(),
1339                    &rrip_time,
1340                    &default_refs,
1341                )?;
1342                let (extent, size_sectors) =
1343                    Self::layout_directory_records(cursor, sector_size, &records);
1344                let ca_len = records
1345                    .iter()
1346                    .filter(|r| r.split.has_overflow())
1347                    .map(|r| r.split.overflow.len() as u64)
1348                    .sum::<u64>();
1349                let reference = DirectoryRef {
1350                    extent: LogicalSector(extent as usize),
1351                    size: (size_sectors * sector_size) as usize,
1352                };
1353                let dir = self.written_files.get_mut(directory_id);
1354                dir.entries.insert(*ty, reference);
1355                relocation_refs.insert((dir.id, *ty), reference);
1356                if let DirectoryRelocation::Moved { id, .. } = dir.relocation {
1357                    relocation_refs.insert((id, *ty), reference);
1358                }
1359                cursor = (extent + size_sectors) * sector_size + ca_len;
1360            }
1361        }
1362        for (directory_id, index) in &file_order {
1363            let aligned = (cursor + sector_size - 1) & !(sector_size - 1);
1364            let dir = self.written_files.get_mut(directory_id);
1365            let file = &mut dir.files[*index];
1366            let len = match &file.kind {
1367                InputEntryKind::File(contents) => contents.len() as u64,
1368                _ => 0,
1369            };
1370            file.entry = DirectoryRef {
1371                extent: LogicalSector((aligned / sector_size) as usize),
1372                size: len as usize,
1373            };
1374            cursor = aligned + len;
1375        }
1376
1377        // ── Write pass ──
1378        //
1379        // Rebuild the records now that every extent is known and write
1380        // directories in pre-order, each followed by its continuation area,
1381        // then all file contents.
1382        for directory_id in &order {
1383            let is_root = directory_id == &root_id;
1384            for ty in &entry_types {
1385                let dir = self.written_files.get(directory_id);
1386                let expected = dir.entries.get(ty).copied().unwrap_or_default();
1387                let mut records = Self::build_directory_records(
1388                    *ty,
1389                    dir,
1390                    is_root,
1391                    &mut inode_counter,
1392                    rrip_options.as_ref(),
1393                    &rrip_time,
1394                    &relocation_refs,
1395                )?;
1396                Self::write_directory_records(&mut self.data, sector_size, expected, &mut records)
1397                    .await?;
1398            }
1399        }
1400        self.inode_counter = inode_counter;
1401
1402        for (directory_id, index) in &file_order {
1403            let expected = {
1404                let dir = self.written_files.get(directory_id);
1405                dir.files[*index].entry
1406            };
1407            let start = self.data.pad_align_sector().await?;
1408            if start != expected.extent {
1409                return Err(io::Error::new(
1410                    io::ErrorKind::InvalidData,
1411                    "file extent prediction did not match the written layout",
1412                ));
1413            }
1414            let dir = self.written_files.get(directory_id);
1415            if let InputEntryKind::File(contents) = &dir.files[*index].kind {
1416                self.data.write_all(contents).await?;
1417            }
1418        }
1419
1420        fn collect_moved(
1421            directory: &WrittenDirectory,
1422            output: &mut Vec<(usize, usize, BTreeMap<EntryType, DirectoryRef>)>,
1423        ) {
1424            if let DirectoryRelocation::Moved { id, logical_parent } = directory.relocation {
1425                output.push((id, logical_parent, directory.entries.clone()));
1426            }
1427            for child in &directory.dirs {
1428                collect_moved(child, output);
1429            }
1430        }
1431        let mut moved = Vec::new();
1432        collect_moved(self.written_files.get(&root_id), &mut moved);
1433        let directory_end = self
1434            .data
1435            .stream_position()
1436            .await
1437            .map_err(io::Error::erase)?;
1438        for (_id, logical_parent, entries) in moved {
1439            for (ty, directory) in entries {
1440                if !ty.supports_rrip() {
1441                    continue;
1442                }
1443                let parent = relocation_refs
1444                    .get(&(logical_parent, ty))
1445                    .copied()
1446                    .ok_or_else(|| {
1447                        io::Error::new(
1448                            io::ErrorKind::InvalidData,
1449                            "logical parent extent was not written",
1450                        )
1451                    })?;
1452                self.patch_parent_link(directory, parent).await?;
1453            }
1454        }
1455        self.data
1456            .seek(SeekFrom::Start(directory_end))
1457            .await
1458            .map_err(io::Error::erase)?;
1459        let roots = self.written_files.root_refs().clone();
1460
1461        let pos = self
1462            .data
1463            .stream_position()
1464            .await
1465            .map_err(io::Error::erase)?;
1466        for root in roots.values() {
1467            self.update_directory(*root, *root).await?;
1468        }
1469        // We need to seek back to this position
1470        self.data
1471            .seek(SeekFrom::Start(pos))
1472            .await
1473            .map_err(io::Error::erase)?;
1474
1475        Ok(roots)
1476    }
1477
1478    async fn write_path_tables(&mut self) -> io::Result<()> {
1479        for i in 0..self.entry_types.len() {
1480            let ty = self.entry_types[i];
1481            let l_ref = self.write_path_table(ty, EndianType::LittleEndian).await?;
1482            let m_ref = self.write_path_table(ty, EndianType::BigEndian).await?;
1483            assert_eq!(l_ref.size, m_ref.size);
1484            self.path_tables.insert(
1485                ty,
1486                PathTableRef {
1487                    lpt: l_ref.extent,
1488                    mpt: m_ref.extent,
1489                    size: l_ref.size as u64,
1490                },
1491            );
1492        }
1493        Ok(())
1494    }
1495
1496    async fn write_path_table(&mut self, ty: EntryType, endian: EndianType) -> io::Result<DirectoryRef> {
1497        let start = self.data.pad_align_sector().await?;
1498        PathTableWriter {
1499            written_files: &self.written_files,
1500            ty,
1501            endian,
1502        }
1503        .write(&mut self.data).await?;
1504        let size = self
1505            .data
1506            .stream_position()
1507            .await
1508            .map_err(io::Error::erase)? as usize
1509            - (start.0 * self.data.sector_size);
1510        let _end = self.data.pad_align_sector().await?;
1511        Ok(DirectoryRef {
1512            extent: start,
1513            size,
1514        })
1515    }
1516
1517    /// Writes the partition tables (MBR, GPT, or Hybrid) based on configuration.
1518    ///
1519    /// The GPT and Hybrid schemes append 33 sectors of 512 bytes (backup
1520    /// entry array plus backup header) after the ISO data, padded so that
1521    /// the image length stays a multiple of the logical sector size and the
1522    /// backup header occupies the last 512-byte sector. The appended region
1523    /// lies outside the ISO file-data area but is still counted in
1524    /// `volume_space_size`, so tools that copy `volume_space_size` logical
1525    /// sectors preserve the backup GPT, matching xorriso's appended-GPT
1526    /// practice.
1527    async fn write_partition_tables(&mut self, end_sector: LogicalSector) -> io::Result<()> {
1528        match self
1529            .ops
1530            .features
1531            .hybrid_boot
1532            .as_ref()
1533            .map(|h| h.partition_scheme)
1534        {
1535            None | Some(PartitionScheme::None) => {
1536                // No partition table requested - leave the system area empty.
1537                // Writing an MBR here would cause the kernel to detect a
1538                // partition table and prevent the ISO from being mounted.
1539            }
1540            Some(PartitionScheme::Mbr) => {
1541                self.write_mbr_boot(end_sector).await?;
1542            }
1543            Some(PartitionScheme::Gpt) => {
1544                self.write_gpt_boot(end_sector).await?;
1545            }
1546            Some(PartitionScheme::Hybrid) => {
1547                self.write_hybrid_boot(end_sector).await?;
1548            }
1549        }
1550
1551        Ok(())
1552    }
1553
1554    /// Writes an MBR partition table for BIOS USB boot (isohybrid-style).
1555    async fn write_mbr_boot(&mut self, end_sector: LogicalSector) -> io::Result<()> {
1556        let end_block = (end_sector.0 * (self.data.sector_size / 512)) as u32;
1557
1558        let hybrid_opts = self.ops.features.hybrid_boot.as_ref();
1559        let bootable = hybrid_opts.map(|h| h.bootable).unwrap_or(true);
1560
1561        let mut mbr = MasterBootRecord::default();
1562        mbr.with_partition_table(|pt| {
1563            // Create a partition covering the entire ISO
1564            // Type 0x17 is ISO9660/Hidden NTFS which is commonly used for hybrid ISOs
1565            pt[0] = MbrPartition {
1566                boot_indicator: if bootable { 0x80 } else { 0x00 },
1567                start_chs: Chs::new(0),
1568                part_type: MbrPartitionType::Iso9660.to_u8(),
1569                end_chs: Chs::new(end_block.saturating_sub(1)),
1570                start_lba: Le::<u32>::from_ne(0),
1571                sector_count: Le::<u32>::from_ne(end_block),
1572            };
1573        });
1574
1575        // Inject bootstrap code if provided
1576        if let Some(ref hybrid_opts) = self.ops.features.hybrid_boot
1577            && let Some(ref bootstrap) = hybrid_opts.mbr_bootstrap
1578        {
1579            let len = bootstrap.len().min(446);
1580            mbr.bootstrap[..len].copy_from_slice(&bootstrap[..len]);
1581        }
1582
1583        self.data
1584            .seek(SeekFrom::Start(0))
1585            .await
1586            .map_err(io::Error::erase)?;
1587        self.data.write_all(bytemuck::bytes_of(&mbr)).await?;
1588
1589        Ok(())
1590    }
1591
1592    /// Resolves the ISO path of the El Torito UEFI image to expose as a GPT
1593    /// EFI System Partition.
1594    ///
1595    /// Uses `HybridBootOptions::efi_boot_partition` when set; otherwise falls
1596    /// back to the boot image of the single `PlatformId::UEFI` El Torito
1597    /// section entry, if there is exactly one.
1598    fn efi_boot_partition_path(&self) -> Option<String> {
1599        let hybrid = self.ops.features.hybrid_boot.as_ref()?;
1600        if let Some(path) = &hybrid.efi_boot_partition {
1601            return Some(path.clone());
1602        }
1603        let boot = self.ops.features.el_torito.as_ref()?;
1604        let mut uefi_entries = boot
1605            .entries
1606            .iter()
1607            .filter(|(section, _)| section.platform == PlatformId::UEFI)
1608            .map(|(_, entry)| &entry.boot_image_path);
1609        let first = uefi_entries.next()?;
1610        if uefi_entries.next().is_some() {
1611            return None;
1612        }
1613        Some(first.clone())
1614    }
1615
1616    /// Builds the GPT structures for the image.
1617    ///
1618    /// The disk spans the ISO data plus an appended backup-GPT region; the
1619    /// backup header sits in the last 512-byte sector of the padded image.
1620    /// The layout is non-overlapping (overlap is rejected by
1621    /// [`GptDisk::validate`]): a basic-data partition covers the ISO area
1622    /// before the EFI System Partition, the ESP covers the El Torito UEFI
1623    /// image exactly, and a second basic-data partition covers any remaining
1624    /// ISO data after it. Partitions start no earlier than 512-byte LBA 64,
1625    /// where the ISO volume descriptors begin: tools that convert ISOHYBRID
1626    /// GPT images to MBR (e.g. `limine bios-install`) embed boot code in
1627    /// sectors 1..63 and reject partitions starting below sector 63.
1628    ///
1629    /// Returns the disk together with the entry indices of the leading
1630    /// ISO9660 partition and the ESP, for hybrid MBR mirroring.
1631    /// Total image size in 512-byte sectors for GPT/Hybrid images: the ISO
1632    /// data plus the appended backup-GPT region, padded to a logical-sector
1633    /// boundary.
1634    fn gpt_total_512(&self, end_sector: LogicalSector) -> u64 {
1635        const BACKUP_GPT_SECTORS: u64 = 33;
1636        let blocks_per_sector = (self.ops.sector_size / 512) as u64;
1637        let iso_512 = end_sector.0 as u64 * blocks_per_sector;
1638        (iso_512 + BACKUP_GPT_SECTORS).div_ceil(blocks_per_sector) * blocks_per_sector
1639    }
1640
1641    /// The `volume_space_size` to declare: for GPT/Hybrid images it covers the
1642    /// whole image including the appended backup-GPT region, so tools that
1643    /// copy `volume_space_size` logical sectors preserve the backup GPT.
1644    fn volume_space_sectors(&self, end_sector: LogicalSector) -> u32 {
1645        match self
1646            .ops
1647            .features
1648            .hybrid_boot
1649            .as_ref()
1650            .map(|h| h.partition_scheme)
1651        {
1652            Some(PartitionScheme::Gpt) | Some(PartitionScheme::Hybrid) => {
1653                let blocks_per_sector = (self.ops.sector_size / 512) as u64;
1654                (self.gpt_total_512(end_sector) / blocks_per_sector) as u32
1655            }
1656            _ => end_sector.0 as u32,
1657        }
1658    }
1659
1660    fn build_gpt_disk(
1661        &self,
1662        end_sector: LogicalSector,
1663    ) -> io::Result<(GptDisk, Option<usize>, Option<usize>)> {
1664        let blocks_per_sector = (self.data.sector_size / 512) as u64;
1665        let iso_512 = end_sector.0 as u64 * blocks_per_sector;
1666        let total_512 = self.gpt_total_512(end_sector);
1667
1668        let mut gpt = GptDisk::new(total_512, 512);
1669        let disk_guid =
1670            Self::generate_guid_from_string(&alloc::format!("disk-{}", self.ops.volume_name));
1671        gpt.primary_header.disk_guid = disk_guid;
1672        gpt.backup_header.disk_guid = disk_guid;
1673
1674        const ISO_DATA_START_512: u64 = 64;
1675
1676        let first_usable = gpt.primary_header.first_usable_lba.to_ne();
1677        let iso_part_start = first_usable.max(ISO_DATA_START_512);
1678        let iso_end = iso_512.saturating_sub(1);
1679        if iso_end <= iso_part_start {
1680            return Err(io::Error::new(
1681                io::ErrorKind::InvalidData,
1682                "image too small for a GPT partition table",
1683            ));
1684        }
1685
1686        let map_part_err = |_| io::Error::new(io::ErrorKind::InvalidData, "invalid GPT layout");
1687
1688        let esp = match self.efi_boot_partition_path() {
1689            Some(path) => {
1690                let dir_ref = self
1691                    .written_files
1692                    .find_file(&path, self.ops.path_separator)
1693                    .ok_or_else(|| {
1694                        io::Error::new(
1695                            io::ErrorKind::NotFound,
1696                            "EFI boot partition image not found in the ISO tree",
1697                        )
1698                    })?;
1699                let start = dir_ref.extent.0 as u64 * blocks_per_sector;
1700                let sectors = (dir_ref.size as u64).div_ceil(512).max(1);
1701                let end = start + sectors - 1;
1702                if start < iso_part_start || end > iso_end {
1703                    return Err(io::Error::new(
1704                        io::ErrorKind::InvalidData,
1705                        "EFI boot partition lies outside the ISO data area",
1706                    ));
1707                }
1708                Some((start, end))
1709            }
1710            None => None,
1711        };
1712
1713        let mut iso_index = None;
1714        let mut esp_index = None;
1715        match esp {
1716            Some((esp_start, esp_end)) => {
1717                if esp_start > iso_part_start {
1718                    let mut data = GptPartitionEntry::new(
1719                        Guid::BASIC_DATA,
1720                        Self::generate_guid_from_string(&self.ops.volume_name),
1721                        iso_part_start,
1722                        esp_start - 1,
1723                    );
1724                    data.set_name_ascii(b"ISO9660");
1725                    iso_index = Some(gpt.add_partition(data).map_err(map_part_err)?);
1726                }
1727                let mut esp_entry = GptPartitionEntry::new(
1728                    Guid::EFI_SYSTEM,
1729                    Self::generate_guid_from_string(&alloc::format!(
1730                        "esp-{}",
1731                        self.ops.volume_name
1732                    )),
1733                    esp_start,
1734                    esp_end,
1735                );
1736                esp_entry.set_name_ascii(b"EFI System Partition");
1737                esp_index = Some(gpt.add_partition(esp_entry).map_err(map_part_err)?);
1738                if esp_end < iso_end {
1739                    let mut tail = GptPartitionEntry::new(
1740                        Guid::BASIC_DATA,
1741                        Self::generate_guid_from_string(&alloc::format!(
1742                            "data-{}",
1743                            self.ops.volume_name
1744                        )),
1745                        esp_end + 1,
1746                        iso_end,
1747                    );
1748                    tail.set_name_ascii(b"ISO9660");
1749                    gpt.add_partition(tail).map_err(map_part_err)?;
1750                }
1751            }
1752            None => {
1753                let mut data = GptPartitionEntry::new(
1754                    Guid::BASIC_DATA,
1755                    Self::generate_guid_from_string(&self.ops.volume_name),
1756                    iso_part_start,
1757                    iso_end,
1758                );
1759                data.set_name_ascii(b"ISO9660");
1760                iso_index = Some(gpt.add_partition(data).map_err(map_part_err)?);
1761            }
1762        }
1763
1764        gpt.update_crcs();
1765        gpt.validate().map_err(map_part_err)?;
1766        Ok((gpt, iso_index, esp_index))
1767    }
1768
1769    /// Writes a GPT partition table (protective MBR, primary and backup GPT)
1770    /// for UEFI boot. See [`Self::write_partition_tables`] for the appended
1771    /// backup region and [`Self::build_gpt_disk`] for the partition layout.
1772    async fn write_gpt_boot(&mut self, end_sector: LogicalSector) -> io::Result<()> {
1773        let (gpt, _, _) = self.build_gpt_disk(end_sector)?;
1774        gpt.write_to(&mut self.data).await.map_err(part_io_error)?;
1775        Ok(())
1776    }
1777
1778    /// Writes a Hybrid MBR + GPT for dual BIOS/UEFI boot.
1779    ///
1780    /// The MBR carries a protective entry over the pre-partition gap, mirrors
1781    /// the leading ISO9660 basic-data partition as type 0x17, and mirrors the
1782    /// EFI System Partition as type 0xEF.
1783    async fn write_hybrid_boot(&mut self, end_sector: LogicalSector) -> io::Result<()> {
1784        let hybrid_opts = self.ops.features.hybrid_boot.as_ref();
1785        let bootable = hybrid_opts.map(|h| h.bootable).unwrap_or(true);
1786
1787        let (gpt, iso_index, esp_index) = self.build_gpt_disk(end_sector)?;
1788        let total_512 = gpt.backup_header.my_lba.to_ne() + 1;
1789
1790        let mut builder = HybridMbrBuilder::new(total_512).protective_slot(0);
1791        if let Some(iso_index) = iso_index {
1792            builder = builder.mirror_partition(iso_index as u32, MbrPartitionType::Iso9660, bootable);
1793        }
1794        if let Some(esp_index) = esp_index {
1795            builder = builder.mirror_partition(
1796                esp_index as u32,
1797                MbrPartitionType::EfiSystemPartition,
1798                false,
1799            );
1800        }
1801        let mut mbr = builder
1802            .build(&gpt.entries)
1803            .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid hybrid MBR"))?;
1804
1805        if let Some(ref hybrid_opts) = self.ops.features.hybrid_boot
1806            && let Some(ref bootstrap) = hybrid_opts.mbr_bootstrap
1807        {
1808            let len = bootstrap.len().min(446);
1809            mbr.bootstrap[..len].copy_from_slice(&bootstrap[..len]);
1810        }
1811
1812        gpt.write_to_with_mbr(&mut self.data, &mbr)
1813            .await
1814            .map_err(part_io_error)?;
1815
1816        Ok(())
1817    }
1818
1819    /// Generates a deterministic GUID from a string (simple hash-based).
1820    fn generate_guid_from_string(s: &str) -> Guid {
1821        // Simple FNV-1a hash to generate a deterministic GUID
1822        let mut hash1: u64 = 0xcbf29ce484222325;
1823        let mut hash2: u64 = 0x100000001b3;
1824
1825        for byte in s.bytes() {
1826            hash1 ^= byte as u64;
1827            hash1 = hash1.wrapping_mul(0x100000001b3);
1828            hash2 ^= byte as u64;
1829            hash2 = hash2.wrapping_mul(0xcbf29ce484222325);
1830        }
1831
1832        let mut bytes = [0u8; 16];
1833        bytes[0..8].copy_from_slice(&hash1.to_le_bytes());
1834        bytes[8..16].copy_from_slice(&hash2.to_le_bytes());
1835
1836        // Set version 4 (random) and variant bits
1837        bytes[6] = (bytes[6] & 0x0f) | 0x40; // Version 4
1838        bytes[8] = (bytes[8] & 0x3f) | 0x80; // Variant 1
1839
1840        Guid::from_bytes(bytes)
1841    }
1842
1843    async fn update_directory(
1844        &mut self,
1845        parent: DirectoryRef,
1846        directory: DirectoryRef,
1847    ) -> io::Result<()> {
1848        let start = self.data.seek_sector(directory.extent).await?;
1849        let mut offset = 0;
1850        loop {
1851            if offset >= directory.size as u64 {
1852                break;
1853            }
1854            self.data
1855                .seek(SeekFrom::Start(start + offset))
1856                .await
1857                .map_err(io::Error::erase)?;
1858            let mut record = DirectoryRecord::parse(&mut self.data).await?;
1859            if record.header().len == 0 {
1860                break;
1861            }
1862
1863            if record.name() == b"\x00" || record.name() == b"\x01" {
1864                let dir_ref = [directory, parent][record.name()[0] as usize];
1865                let header = record.header_mut();
1866                header.extent.write(dir_ref.extent.0 as u32);
1867                header.data_len.write(dir_ref.size as u32);
1868                self.data
1869                    .seek(SeekFrom::Start(start + offset))
1870                    .await
1871                    .map_err(io::Error::erase)?;
1872                record.write(&mut self.data).await?;
1873                offset += record.header().len as u64;
1874                continue;
1875            }
1876            offset += record.header().len as u64;
1877
1878            if FileFlags::from_bits_truncate(record.header().flags).contains(FileFlags::DIRECTORY) {
1879                let record = DirectoryRef {
1880                    extent: LogicalSector(record.header().extent.read() as usize),
1881                    size: record.header().data_len.read() as usize,
1882                };
1883                self.update_directory(directory, record).await?;
1884            }
1885        }
1886
1887        Ok(())
1888    }
1889
1890    async fn patch_parent_link(
1891        &mut self,
1892        directory: DirectoryRef,
1893        parent: DirectoryRef,
1894    ) -> io::Result<()> {
1895        let start = self.data.seek_sector(directory.extent).await?;
1896        self.data
1897            .seek(SeekFrom::Start(start))
1898            .await
1899            .map_err(io::Error::erase)?;
1900        let dot = DirectoryRecord::parse(&mut self.data).await?;
1901        self.data
1902            .seek(SeekFrom::Start(start + dot.header().len as u64))
1903            .await
1904            .map_err(io::Error::erase)?;
1905        let mut dotdot = DirectoryRecord::parse(&mut self.data).await?;
1906        let system_use = dotdot.system_use_mut();
1907        let mut offset = 0;
1908        while offset + 4 <= system_use.len() {
1909            let length = system_use[offset + 2] as usize;
1910            if length < 4 || offset + length > system_use.len() {
1911                break;
1912            }
1913            if &system_use[offset..offset + 2] == b"PL" && length >= 12 {
1914                let value = crate::types::U32LsbMsb::new(parent.extent.0 as u32);
1915                system_use[offset + 4..offset + 12]
1916                    .copy_from_slice(bytemuck::bytes_of(&value));
1917                self.data
1918                    .seek(SeekFrom::Start(start + dot.header().len as u64))
1919                    .await
1920                    .map_err(io::Error::erase)?;
1921                dotdot.write(&mut self.data).await?;
1922                return Ok(());
1923            }
1924            offset += length;
1925        }
1926        Err(io::Error::new(
1927            io::ErrorKind::InvalidData,
1928            "relocated directory is missing its RRIP PL entry",
1929        ))
1930    }
1931
1932    /// Write a directory using a three-phase approach:
1933    ///
1934    /// 1. Build all RRIP entries and split each against available inline space
1935    /// 2. If any have overflow: write a shared continuation area, patch CE entries
1936    /// 3. Write directory records with the inline SU bytes
1937    #[allow(clippy::too_many_arguments)]
1938    /// Build the pending records for one directory: dot/dotdot, child
1939    /// directories, and files, with RRIP system-use areas split against the
1940    /// inline budget, names deduplicated, and records ordered by File
1941    /// Identifier (ECMA-119 9.3). Sizes never depend on extent values, so
1942    /// the planning pass calls this with placeholder refs and the write pass
1943    /// rebuilds with the real ones.
1944    fn build_directory_records(
1945        ty: EntryType,
1946        dir: &WrittenDirectory,
1947        is_root: bool,
1948        inode_counter: &mut u32,
1949        rrip_options: Option<&RripOptions>,
1950        fallback_time: &[u8; 7],
1951        relocation_refs: &BTreeMap<(usize, EntryType), DirectoryRef>,
1952    ) -> io::Result<Vec<PendingRecord>> {
1953        let rrip_options = rrip_options.filter(|options| options.enabled);
1954        let has_rrip = ty.supports_rrip() && rrip_options.is_some();
1955        let options = rrip_options.copied().unwrap_or_else(RripOptions::disabled);
1956        let directory_nlink = 2 + dir.dirs.len() as u32;
1957
1958        let mut records: Vec<PendingRecord> = Vec::new();
1959
1960        // Dot entry (".")
1961        let dot_split = if has_rrip {
1962            let kind = if is_root {
1963                RripEntryKind::RootDot {
1964                    metadata: dir.metadata,
1965                    nlink: directory_nlink,
1966                }
1967            } else {
1968                RripEntryKind::Dot {
1969                    metadata: dir.metadata,
1970                    nlink: directory_nlink,
1971                }
1972            };
1973            let max = available_su_space(1); // name is b"\x00"
1974            build_rrip_entries(kind, 0, &options, fallback_time).build_split(max)
1975        } else {
1976            SplitSu::empty()
1977        };
1978        records.push(PendingRecord {
1979            name: vec![0x00],
1980            split: dot_split,
1981            dir_ref: DirectoryRef::default(),
1982            flags: FileFlags::DIRECTORY,
1983        });
1984
1985        // Dotdot entry ("..")
1986        let dotdot_split = if has_rrip {
1987            let kind = if is_root {
1988                RripEntryKind::RootDotDot {
1989                    metadata: dir.metadata,
1990                    nlink: directory_nlink,
1991                }
1992            } else {
1993                RripEntryKind::DotDot {
1994                    metadata: dir.metadata,
1995                    nlink: directory_nlink,
1996                }
1997            };
1998            let max = available_su_space(1); // name is b"\x01"
1999            let mut builder = build_rrip_entries(kind, 0, &options, fallback_time);
2000            if let DirectoryRelocation::Moved { logical_parent, .. } = dir.relocation {
2001                let parent = relocation_refs
2002                    .get(&(logical_parent, ty))
2003                    .copied()
2004                    .unwrap_or_default();
2005                builder.add_pl(parent.extent.0 as u32);
2006            }
2007            builder.build_split(max)
2008        } else {
2009            SplitSu::empty()
2010        };
2011        records.push(PendingRecord {
2012            name: vec![0x01],
2013            split: dotdot_split,
2014            dir_ref: DirectoryRef::default(),
2015            flags: FileFlags::DIRECTORY,
2016        });
2017
2018        // Directory entries
2019        for directory in &dir.dirs {
2020            let WrittenDirectory {
2021                name,
2022                rrip_name,
2023                entries,
2024                metadata,
2025                dirs,
2026                relocation,
2027                ..
2028            } = directory;
2029            let converted_name = ty.convert_directory_name(name);
2030            let split = if has_rrip {
2031                let inode = *inode_counter;
2032                *inode_counter += 1;
2033                let max = available_su_space(converted_name.as_bytes().len());
2034                let mut builder = build_rrip_entries(
2035                    RripEntryKind::Directory {
2036                        original_name: rrip_name,
2037                        metadata: *metadata,
2038                        nlink: 2 + dirs.len() as u32,
2039                    },
2040                    inode,
2041                    &options,
2042                    fallback_time,
2043                );
2044                match relocation {
2045                    DirectoryRelocation::Placeholder { target } => {
2046                        let target = relocation_refs.get(&(*target, ty)).ok_or_else(|| {
2047                            io::Error::new(
2048                                io::ErrorKind::InvalidData,
2049                                "relocated directory extent was not written",
2050                            )
2051                        })?;
2052                        builder.add_cl(target.extent.0 as u32);
2053                    }
2054                    DirectoryRelocation::Moved { .. } => {
2055                        builder.add_re();
2056                    }
2057                    DirectoryRelocation::None => {}
2058                }
2059                builder.build_split(max)
2060            } else {
2061                SplitSu::empty()
2062            };
2063            records.push(PendingRecord {
2064                name: converted_name.as_bytes().to_vec(),
2065                split,
2066                dir_ref: match relocation {
2067                    DirectoryRelocation::Placeholder { target } => relocation_refs
2068                        .get(&(*target, ty))
2069                        .copied()
2070                        .unwrap_or_default(),
2071                    _ => *entries.get(&ty).unwrap(),
2072                },
2073                flags: FileFlags::DIRECTORY,
2074            });
2075        }
2076
2077        // File entries
2078        for file in &dir.files {
2079            let WrittenFile {
2080                name,
2081                entry,
2082                kind,
2083                metadata,
2084            } = file;
2085            let converted_name = ty.convert_name(name);
2086            let split = if has_rrip {
2087                let inode = *inode_counter;
2088                *inode_counter += 1;
2089                let max = available_su_space(converted_name.as_bytes().len());
2090                build_rrip_entries(
2091                    RripEntryKind::Entry {
2092                        original_name: name,
2093                        metadata: *metadata,
2094                        kind,
2095                    },
2096                    inode,
2097                    &options,
2098                    fallback_time,
2099                )
2100                .build_split(max)
2101            } else {
2102                SplitSu::empty()
2103            };
2104            records.push(PendingRecord {
2105                name: converted_name.as_bytes().to_vec(),
2106                split,
2107                dir_ref: *entry,
2108                flags: FileFlags::empty(),
2109            });
2110        }
2111
2112        // ── Phase 1.5: Deduplicate names ──
2113        // Name mangling can map different original names to the same ISO name.
2114        // e.g., "readme.txt" and "README.txt" both become "README.TXT;1".
2115        // Resolve collisions with underscore-based suffixes.
2116        {
2117            use std::collections::HashSet;
2118            let mut seen: HashSet<Vec<u8>> = HashSet::new();
2119            for record in &mut records {
2120                // Skip dot/dotdot entries
2121                if record.name.len() == 1 && (record.name[0] == 0x00 || record.name[0] == 0x01) {
2122                    continue;
2123                }
2124                if seen.insert(record.name.clone()) {
2125                    continue;
2126                }
2127                let original = record.name.clone();
2128                let mut suffix = 1;
2129                loop {
2130                    let candidate = apply_dedup_suffix(&original, suffix, ty);
2131                    suffix += 1;
2132                    if seen.insert(candidate.clone()) {
2133                        record.name = candidate;
2134                        break;
2135                    }
2136                }
2137            }
2138        }
2139
2140        // ── Phase 1.75: Order records by File Identifier (ECMA-119 9.3) ──
2141        // Directory records must be written in ascending File Identifier order.
2142        // "." and ".." must be the first two records in that order. A plain
2143        // byte-wise sort is insufficient for Joliet because every UTF-16BE ASCII
2144        // name begins with 0x00 and would therefore sort before the 0x01 ".."
2145        // identifier. Sort ordinary entries byte-wise only after ranking the two
2146        // special records explicitly.
2147        records.sort_by(|a, b| {
2148            let rank = |name: &[u8]| match name {
2149                [0x00] => 0,
2150                [0x01] => 1,
2151                _ => 2,
2152            };
2153            rank(&a.name)
2154                .cmp(&rank(&b.name))
2155                .then_with(|| a.name.cmp(&b.name))
2156        });
2157
2158        Ok(records)
2159    }
2160
2161    /// Compute the sector span `records` will occupy when written at byte
2162    /// position `pos`: returns (start sector, size in sectors). Mirrors the
2163    /// write logic in [`Self::write_directory_records`] exactly.
2164    fn layout_directory_records(
2165        pos: u64,
2166        sector_size: u64,
2167        records: &[PendingRecord],
2168    ) -> (u64, u64) {
2169        let align = |pos: u64| (pos + sector_size - 1) & !(sector_size - 1);
2170        let start = align(pos);
2171        let mut pos = start;
2172        for record in records {
2173            let record_size = DirectoryRecord::new(
2174                &record.name,
2175                &record.split.inline,
2176                record.dir_ref,
2177                record.flags,
2178            )
2179            .size() as u64;
2180            let remaining = sector_size - pos % sector_size;
2181            if record_size > remaining {
2182                pos += remaining;
2183            }
2184            pos += record_size;
2185        }
2186        let end = align(pos);
2187        (start / sector_size, (end - start) / sector_size)
2188    }
2189
2190    /// Write records at the extent the planning pass assigned, followed by
2191    /// the continuation area their CE entries are patched to point at.
2192    /// Fails if the written layout does not match the plan.
2193    async fn write_directory_records(
2194        data: &mut IsoCursor<DATA>,
2195        sector_size: u64,
2196        expected: DirectoryRef,
2197        records: &mut [PendingRecord],
2198    ) -> io::Result<()> {
2199        let has_overflow = records.iter().any(|r| r.split.has_overflow());
2200        if has_overflow {
2201            let ca_sector = expected.extent.0 as u64 + expected.size as u64 / sector_size;
2202            let mut offset = 0u32;
2203            for record in records.iter_mut() {
2204                if record.split.has_overflow() {
2205                    record.split.patch_ce(ca_sector as u32, offset);
2206                    offset += record.split.overflow.len() as u32;
2207                }
2208            }
2209        }
2210
2211        let start = data.pad_align_sector().await?;
2212        if start != expected.extent {
2213            return Err(io::Error::new(
2214                io::ErrorKind::InvalidData,
2215                "directory extent prediction did not match the written layout",
2216            ));
2217        }
2218        for record in records.iter() {
2219            let directory_record = DirectoryRecord::new(
2220                &record.name,
2221                &record.split.inline,
2222                record.dir_ref,
2223                record.flags,
2224            );
2225            let position = data.stream_position().await.map_err(io::Error::erase)? as usize;
2226            let sector_offset = position % data.sector_size;
2227            let remaining = data.sector_size - sector_offset;
2228            if directory_record.size() > remaining {
2229                let padding = vec![0_u8; remaining];
2230                data.write_all(&padding).await?;
2231            }
2232            directory_record.write(&mut *data).await?;
2233        }
2234        let end = data.pad_align_sector().await?;
2235        let size = (end.0 - start.0) * data.sector_size;
2236        if size != expected.size {
2237            return Err(io::Error::new(
2238                io::ErrorKind::InvalidData,
2239                "directory size prediction did not match the written layout",
2240            ));
2241        }
2242
2243        if has_overflow {
2244            for record in records.iter() {
2245                if record.split.has_overflow() {
2246                    data.write_all(&record.split.overflow).await?;
2247                }
2248            }
2249        }
2250        Ok(())
2251    }
2252}
2253} // io_transform!
2254
2255fn alignment_requires_materialization(current_position: u64, aligned_position: u64) -> bool {
2256    aligned_position > current_position
2257}
2258
2259fn part_io_error(err: hadris_part::Error) -> io::Error {
2260    match err {
2261        hadris_part::Error::Io(err) => err,
2262        _ => io::Error::new(
2263            io::ErrorKind::InvalidData,
2264            "failed to write partition table",
2265        ),
2266    }
2267}
2268
2269struct FileTreeWalker<'a> {
2270    stack: VecDeque<StackFrame<'a>>,
2271}
2272
2273enum StackFrame<'a> {
2274    Node(&'a InputEntry),
2275    DirExit(&'a InputEntry),
2276}
2277
2278#[derive(Debug, PartialEq, Eq)]
2279enum TreeWalkerItem<'a> {
2280    EnterDirectory(&'a InputEntry),
2281    File(&'a InputEntry),
2282    ExitDirectory(&'a InputEntry),
2283}
2284
2285impl<'a> FileTreeWalker<'a> {
2286    pub fn new(input: &'a InputTree) -> Self {
2287        let mut stack = VecDeque::new();
2288        for file in input.entries.iter().rev() {
2289            stack.push_back(StackFrame::Node(file));
2290        }
2291        FileTreeWalker { stack }
2292    }
2293}
2294
2295impl<'a> Iterator for FileTreeWalker<'a> {
2296    type Item = TreeWalkerItem<'a>;
2297
2298    fn next(&mut self) -> Option<Self::Item> {
2299        let frame = self.stack.pop_back()?;
2300        match frame {
2301            StackFrame::Node(file) => match &file.kind {
2302                InputEntryKind::Directory(children) => {
2303                    // Yield that we are entering this directory (pre-order event)
2304                    let current_dir = file;
2305
2306                    // Push an Exit frame to signal leaving this directory later
2307                    self.stack.push_back(StackFrame::DirExit(current_dir));
2308
2309                    // Push children in reverse order for DFS
2310                    for child in children.iter().rev() {
2311                        self.stack.push_back(StackFrame::Node(child));
2312                    }
2313
2314                    Some(TreeWalkerItem::EnterDirectory(current_dir))
2315                }
2316                _ => Some(TreeWalkerItem::File(file)),
2317            },
2318            StackFrame::DirExit(dir) => Some(TreeWalkerItem::ExitDirectory(dir)),
2319        }
2320    }
2321}
2322
2323#[cfg(test)]
2324mod tests {
2325    use super::*; // Import items from the outer module
2326    use alloc::vec;
2327
2328    /// The single-extent ceiling is exactly the 32-bit `data_len` range, and
2329    /// ordinary files validate. The >4 GiB rejection path itself cannot be
2330    /// exercised in a unit test without allocating a >4 GiB buffer (files are
2331    /// held in memory as `Vec<u8>`); it is guarded by inspection and the
2332    /// constant below.
2333    #[test]
2334    fn single_extent_ceiling_and_normal_files_validate() {
2335        assert_eq!(MAX_SINGLE_EXTENT_FILE_LEN, u32::MAX as u64);
2336
2337        let tree = InputTree::new(
2338            PathSeparator::ForwardSlash,
2339            vec![InputEntry::file("hello.txt", vec![0u8; 4096])],
2340        );
2341        assert!(validate_input_tree(&tree, None).is_ok());
2342    }
2343
2344    #[test]
2345    fn alignment_only_materializes_new_padding() {
2346        assert!(!alignment_requires_materialization(2048, 2048));
2347        assert!(alignment_requires_materialization(2047, 2048));
2348    }
2349
2350    #[test]
2351    fn test_depth_first_tree_walk_iterator() {
2352        // Define a test file hierarchy
2353        let file_a = InputEntry::file("root/dir1/fileA.txt", Vec::new());
2354        let file_b = InputEntry::file("root/dir1/fileB.txt", Vec::new());
2355        let file_c = InputEntry::file("root/fileC.txt", Vec::new());
2356        let file_d = InputEntry::file("root/dir2/fileD.txt", Vec::new());
2357        let file_e = InputEntry::file("root/dir2/subdir/fileE.txt", Vec::new());
2358
2359        let subdir_node = InputEntry::directory("root/dir2/subdir", vec![file_e.clone()]);
2360
2361        let dir1_node = InputEntry::directory("root/dir1", vec![file_a.clone(), file_b.clone()]);
2362
2363        let dir2_node = InputEntry::directory(
2364            "root/dir2",
2365            vec![
2366                file_d.clone(),
2367                subdir_node.clone(), // Subdirectory
2368            ],
2369        );
2370
2371        let root_level_files = vec![dir1_node.clone(), file_c.clone(), dir2_node.clone()];
2372
2373        let input_tree = InputTree::new(PathSeparator::ForwardSlash, root_level_files);
2374
2375        // Create the iterator
2376        let walker = FileTreeWalker::new(&input_tree);
2377
2378        // Define the expected sequence of events (depth-first, pre-order for Enter, post-order for Exit)
2379        let expected_sequence = vec![
2380            TreeWalkerItem::EnterDirectory(&dir1_node),   // Enter dir1
2381            TreeWalkerItem::File(&file_a),                // Process fileA
2382            TreeWalkerItem::File(&file_b),                // Process fileB
2383            TreeWalkerItem::ExitDirectory(&dir1_node),    // Exit dir1
2384            TreeWalkerItem::File(&file_c),                // Process fileC
2385            TreeWalkerItem::EnterDirectory(&dir2_node),   // Enter dir2
2386            TreeWalkerItem::File(&file_d),                // Process fileD
2387            TreeWalkerItem::EnterDirectory(&subdir_node), // Enter subdir
2388            TreeWalkerItem::File(&file_e),                // Process fileE
2389            TreeWalkerItem::ExitDirectory(&subdir_node),  // Exit subdir
2390            TreeWalkerItem::ExitDirectory(&dir2_node),    // Exit dir2
2391        ];
2392
2393        // Collect all items from the iterator
2394        let actual_sequence: Vec<TreeWalkerItem> = walker.collect();
2395
2396        // Assert that the actual sequence matches the expected sequence
2397        assert_eq!(actual_sequence, expected_sequence);
2398    }
2399
2400    #[test]
2401    fn test_dedup_suffix_l1_with_ext() {
2402        let ty = EntryType::Level1 {
2403            supports_lowercase: false,
2404            supports_rrip: false,
2405        };
2406        let result = apply_dedup_suffix(b"README.TXT;1", 1, ty);
2407        assert_eq!(result, b"README_1.TXT;1");
2408    }
2409
2410    #[test]
2411    fn test_dedup_suffix_l1_no_ext() {
2412        let ty = EntryType::Level1 {
2413            supports_lowercase: false,
2414            supports_rrip: false,
2415        };
2416        let result = apply_dedup_suffix(b"FILENAME;1", 1, ty);
2417        assert_eq!(result, b"FILENA_1;1");
2418    }
2419
2420    #[test]
2421    fn test_dedup_suffix_l2() {
2422        let ty = EntryType::Level2 {
2423            supports_lowercase: false,
2424            supports_rrip: false,
2425        };
2426        let result = apply_dedup_suffix(b"LONGFILENAME.EXT;1", 2, ty);
2427        assert_eq!(result, b"LONGFILENAME_2.EXT;1");
2428    }
2429
2430    #[test]
2431    fn test_dedup_suffix_l3_no_version() {
2432        let ty = EntryType::Level3 {
2433            supports_lowercase: false,
2434            supports_rrip: false,
2435        };
2436        let result = apply_dedup_suffix(b"README.TXT", 1, ty);
2437        assert_eq!(result, b"README_1.TXT");
2438    }
2439
2440    #[test]
2441    fn test_dedup_suffix_distinct() {
2442        let ty = EntryType::Level1 {
2443            supports_lowercase: false,
2444            supports_rrip: false,
2445        };
2446        let r1 = apply_dedup_suffix(b"README.TXT;1", 1, ty);
2447        let r2 = apply_dedup_suffix(b"README.TXT;1", 2, ty);
2448        let r3 = apply_dedup_suffix(b"README.TXT;1", 3, ty);
2449        assert_ne!(r1, r2);
2450        assert_ne!(r2, r3);
2451        assert_ne!(r1, r3);
2452    }
2453}