Skip to main content

hadris_iso/write/
mod.rs

1use alloc::{collections::BTreeMap, sync::Arc};
2
3mod types;
4mod utils;
5
6pub mod estimator;
7/// APIs for writer.
8pub mod writer;
9
10use super::super::boot::{
11    BootCatalog, BootInfoTable, BootSectionEntry, ElToritoWriter, Grub2BootInfoTable, PlatformId,
12};
13use super::super::directory::{DirectoryRecord, DirectoryRef, FileFlags};
14use super::super::io::{self, Read, Seek, SeekFrom, Write};
15use super::super::io::{IsoCursor, LogicalSector};
16use super::super::path::PathTableRef;
17use super::super::rrip::RripOptions;
18use super::super::volume::{
19    BootRecordVolumeDescriptor, PrimaryVolumeDescriptor, SupplementaryVolumeDescriptor,
20    VolumeDescriptor, VolumeDescriptorHeader, VolumeDescriptorList, VolumeDescriptorType,
21};
22use crate::boot::options::{BootEntryOptions, BootOptions, BootSectionOptions};
23use crate::directory::RootDirectoryEntry;
24use crate::file::EntryType;
25use crate::joliet::JolietLevel;
26use crate::types::{Charset, IsoStr};
27use crate::write::utils::*;
28use crate::write::writer::DirectoryId;
29use hadris_common::types::endian::{Endian, EndianType};
30use hadris_part::{
31    GptDisk, GptDiskWriteExt,
32    gpt::{GptPartitionEntry, Guid},
33    hybrid::HybridMbrBuilder,
34    mbr::{MasterBootRecord, MbrPartition, MbrPartitionType},
35};
36use options::PartitionScheme;
37use writer::{DirectoryRelocation, PathTableWriter, WrittenDirectory, WrittenFiles};
38
39use alloc::{string::String, vec, vec::Vec};
40
41#[cfg(feature = "unstable-streaming")]
42pub use types::FileSource;
43use types::*;
44pub use types::{File, InputEntry, InputEntryKind, InputFiles, InputMetadata, InputTree};
45
46/// APIs for options.
47pub mod options;
48use options::IsoFormatOptions;
49
50#[derive(Debug, thiserror::Error)]
51/// Identifies a FileConversionError value.
52pub enum FileConversionError {
53    #[error("I/O error: {0}")]
54    /// The `Io` variant.
55    Io(#[from] std::io::Error),
56    #[error("Path {0:?} is not a valid UTF-8 string")]
57    /// The `InvalidUtf8Path` variant.
58    InvalidUtf8Path(std::path::PathBuf),
59    #[error("Unsupported filesystem entry type at {0:?}")]
60    /// The `UnsupportedFileType` variant.
61    UnsupportedFileType(std::path::PathBuf),
62}
63
64#[derive(Debug, thiserror::Error)]
65/// Identifies a IsoCreationError value.
66pub enum IsoCreationError {
67    #[error(transparent)]
68    /// The `Io` variant.
69    Io(#[from] io::Error),
70}
71
72/// Canonical error for ISO creation operations.
73pub type Error = IsoCreationError;
74/// Canonical result for ISO creation operations.
75pub type Result<T> = core::result::Result<T, Error>;
76
77/// Writer for creating ISO 9660 images.
78///
79/// Supports:
80/// - ISO Level 1, 2, and 3
81/// - Joliet extensions (Unicode filenames)
82/// - Rock Ridge extensions (POSIX metadata, symlinks, devices)
83/// - El Torito bootable images
84/// - MBR, GPT, and Hybrid partition tables
85/// - Multi-extent files (>4 GiB)
86pub struct IsoImageWriter<DATA: Read + Write + Seek> {
87    data: IsoCursor<DATA>,
88    entry_types: Vec<EntryType>,
89    ops: IsoFormatOptions,
90    written_files: WrittenFiles,
91    path_tables: BTreeMap<EntryType, PathTableRef>,
92    inode_counter: u32,
93    rrip_time: RripTime,
94}
95
96io_transform! {
97impl<DATA: Read + Write + Seek> IsoImageWriter<DATA> {
98    const VOLUME_DESCRIPTOR_SET_START: LogicalSector = LogicalSector(16);
99    const GPT_PARTITION_NAME_ISO: &[u8] = b"ISO9660";
100    const GPT_PARTITION_NAME_ESP: &[u8] = b"EFI System Partition";
101    const BOOT_CATALOG_FILENAME: &str = "boot.catalog";
102
103    /// Creates a complete ISO image and returns its output target.
104    pub async fn create<T: Into<InputTree>>(
105        data: DATA,
106        files: T,
107        ops: IsoFormatOptions,
108    ) -> Result<DATA> {
109        Self::create_with_allocation_floor(data, files, ops, None).await
110    }
111
112    /// Creates an ISO image while keeping file and directory allocations at or
113    /// after an optional logical-sector floor.
114    ///
115    /// This is useful when composing ISO 9660 with another on-disc format whose
116    /// metadata occupies sectors after the ISO volume descriptor sequence. A
117    /// floor below the end of the descriptors has no effect. Volume descriptors
118    /// and system-area structures retain their specification-defined locations.
119    pub async fn create_with_allocation_floor<T: Into<InputTree>>(
120        data: DATA,
121        files: T,
122        ops: IsoFormatOptions,
123        allocation_floor: Option<u32>,
124    ) -> Result<DATA> {
125        let mut files = files.into();
126        if ops.sector_size != 2048 {
127            return Err(io::Error::new(
128                io::ErrorKind::InvalidInput,
129                "ISO creation currently requires 2048-byte logical sectors",
130            )
131            .into());
132        }
133
134        files.validate(ops.features.rock_ridge.as_ref())?;
135
136        let mut writer = Self::new(data, ops);
137
138        writer.write_volume_descriptors(&mut files).await?;
139        writer.try_apply_allocation_floor(allocation_floor)?;
140
141        let root_dirs = writer.write_files(&files).await?;
142        writer.write_path_tables().await?;
143        writer.finalize_volume_descriptors(root_dirs).await?;
144
145        Ok(writer.into_inner())
146    }
147
148    async fn try_apply_allocation_floor(&mut self, floor: Option<u32>) -> io::Result<()> {
149        let Some(sector) = floor else { return Ok(()) };
150
151        let current = self
152            .data
153            .stream_position()
154            .await
155            .map_err(io::Error::erase)?;
156
157        let target = u64::from(sector)
158            .checked_mul(self.ops.sector_size as u64)
159            .ok_or_else(|| {
160                io::Error::new(io::ErrorKind::InvalidInput, "allocation floor overflow")
161            })?;
162
163        if target > current {
164            self.data
165                .seek(SeekFrom::Start(target))
166                .await
167                .map_err(io::Error::erase)?;
168        }
169
170        Ok(())
171    }
172
173    /// Returns the output target.
174    pub fn into_inner(self) -> DATA {
175        self.data.into_inner()
176    }
177
178    fn new(data: DATA, ops: IsoFormatOptions) -> Self {
179        let now = super::super::directory::DirDateTime::now();
180        let rrip_time = *<&RripTime>::try_from(bytemuck::bytes_of(&now)).unwrap();
181        let entry_types = ops.entry_types();
182
183        Self {
184            data: IsoCursor::new(data, ops.sector_size),
185            ops,
186            entry_types,
187            written_files: WrittenFiles::new(),
188            path_tables: BTreeMap::new(),
189            inode_counter: 1,
190            rrip_time,
191        }
192    }
193
194    fn parse_iso_str<C: Charset, const N: usize>(
195        &self,
196        s: &str,
197        field_name: &'static str,
198    ) -> io::Result<IsoStr<C, N>> {
199        if self.ops.strict_charset {
200            IsoStr::from_str_lossy(s)
201        } else {
202            IsoStr::from_str_unchecked(s)
203        }
204        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, field_name))
205    }
206
207    async fn write_volume_descriptors(&mut self, files: &mut InputTree) -> io::Result<()> {
208        self.data.seek_sector(Self::VOLUME_DESCRIPTOR_SET_START).await?;
209        let mut volume_descriptors = VolumeDescriptorList::empty();
210
211        for &entry in &self.entry_types {
212            let descriptor = self.create_volume_descriptor(entry)?;
213            volume_descriptors.push(descriptor);
214        }
215
216        if let Some(boot) = &self.ops.features.el_torito {
217            let boot_record = ElToritoWriter::create_descriptor(boot, files);
218            volume_descriptors.insert(1, VolumeDescriptor::BootRecord(boot_record));
219        }
220
221        volume_descriptors.write(&mut self.data).await?;
222
223        Ok(())
224    }
225
226     fn create_volume_descriptor(&self, entry: EntryType) -> io::Result<VolumeDescriptor> {
227        match entry {
228            EntryType::Level1 { .. } | EntryType::Level2 { .. } => {
229                Ok(VolumeDescriptor::Primary(self.create_primary_volume_descriptor()?))
230            }
231            EntryType::Level3 { .. } => {
232                Ok(VolumeDescriptor::Supplementary(self.create_enhanced_volume_descriptor()?))
233            }
234            EntryType::Joliet { level, .. } => {
235                Ok(VolumeDescriptor::Supplementary(self.create_joliet_volume_descriptor(level)?))
236            }
237        }
238    }
239
240    fn configure_base_directory_record(&self, record: &mut RootDirectoryEntry) {
241        record.header.len = 34;
242        record.header.flags = FileFlags::DIRECTORY.bits();
243        record.header.file_identifier_len = 1;
244        record.header.volume_sequence_number.write(1);
245    }
246
247    fn create_primary_volume_descriptor(&self) -> io::Result<PrimaryVolumeDescriptor> {
248        let mut pvd = PrimaryVolumeDescriptor::new(&self.ops.volume_name, 0);
249        pvd.volume_identifier = self.parse_iso_str(&self.ops.volume_name, "volume name")?;
250        self.configure_base_directory_record(&mut pvd.dir_record);
251        pvd.volume_sequence_number.write(1);
252
253        if let Some(s) = &self.ops.system_id {
254            pvd.system_identifier = self.parse_iso_str(s, "system identifier")?;
255        }
256        if let Some(s) = &self.ops.volume_set_id {
257            pvd.volume_set_identifier = self.parse_iso_str(s, "volume set identifier")?;
258        }
259        if let Some(s) = &self.ops.publisher_id {
260            pvd.publisher_identifier = self.parse_iso_str(s, "publisher identifier")?;
261        }
262        if let Some(s) = &self.ops.preparer_id {
263            pvd.preparer_identifier = self.parse_iso_str(s, "preparer identifier")?;
264        }
265        if let Some(s) = &self.ops.application_id {
266            pvd.application_identifier = self.parse_iso_str(s, "application identifier")?;
267        }
268
269        Ok(pvd)
270    }
271
272    fn create_enhanced_volume_descriptor(&self) -> io::Result<SupplementaryVolumeDescriptor> {
273        let mut evd = SupplementaryVolumeDescriptor::new_evd(&self.ops.volume_name, 0);
274        evd.volume_identifier = self.parse_iso_str(&self.ops.volume_name, "volume name")?;
275        self.configure_base_directory_record(&mut evd.dir_record);
276        evd.volume_sequence_number.write(1);
277        Ok(evd)
278    }
279
280    fn create_joliet_volume_descriptor(&self, level: JolietLevel) -> io::Result<SupplementaryVolumeDescriptor> {
281        let mut svd = SupplementaryVolumeDescriptor::new_svd(
282            &self.ops.volume_name,
283            0,
284            level.escape_sequence(),
285        );
286        self.configure_base_directory_record(&mut svd.dir_record);
287        svd.volume_sequence_number.write(1);
288
289        if let Some(s) = &self.ops.system_id {
290            svd.system_identifier = SupplementaryVolumeDescriptor::utf16be_str(s);
291        }
292        if let Some(s) = &self.ops.volume_set_id {
293            svd.volume_set_identifier = SupplementaryVolumeDescriptor::utf16be_str(s);
294        }
295        if let Some(s) = &self.ops.publisher_id {
296            svd.publisher_identifier = SupplementaryVolumeDescriptor::utf16be_str(s);
297        }
298        if let Some(s) = &self.ops.preparer_id {
299            svd.preparer_identifier = SupplementaryVolumeDescriptor::utf16be_str(s);
300        }
301        if let Some(s) = &self.ops.application_id {
302            svd.application_identifier = SupplementaryVolumeDescriptor::utf16be_str(s);
303        }
304
305        Ok(svd)
306    }
307
308    async fn finalize_volume_descriptors(
309        &mut self,
310        root_dirs: BTreeMap<EntryType, DirectoryRef>,
311    ) -> io::Result<()> {
312        let catalog_ptr = self.write_boot_catalog().await?;
313        let end_sector = self.pad_and_get_end_sector().await?;
314        let volume_space = self.volume_space_sectors(end_sector)?;
315
316        self.patch_volume_descriptors(&root_dirs, catalog_ptr, volume_space).await?;
317
318        self.write_partition_tables(end_sector).await?;
319        Ok(())
320    }
321
322    async fn write_boot_section(
323        &mut self,
324        catalog: &mut BootCatalog,
325        section: Option<&BootSectionOptions>,
326        entry: &BootEntryOptions,
327    ) -> io::Result<()> {
328        let dir_ref = self.find_boot_image(&entry.boot_image_path)?;
329        let load_size = self.calculate_load_size(entry, &dir_ref);
330        let boot_entry = BootSectionEntry::new(entry.emulation, 0, load_size, dir_ref.extent.0 as u32);
331
332        if let Some(section) = section {
333            catalog.add_section(section.platform, vec![boot_entry]);
334        } else {
335            catalog.set_default_entry(boot_entry);
336        }
337
338        if entry.boot_info_table || entry.grub2_boot_info {
339            self.write_boot_info_table(entry, &dir_ref).await?;
340        }
341
342        Ok(())
343    }
344
345    fn find_boot_image(&self, path: &str) -> io::Result<DirectoryRef> {
346        let image = self.written_files
347            .find_file(path, self.ops.path_separator)
348            .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "boot image file not found"))?;
349        if image.size == 0 {
350            return Err(io::Error::new(io::ErrorKind::InvalidInput, "boot image file is empty"));
351        }
352        Ok(image)
353    }
354
355    fn calculate_load_size(&self, entry: &BootEntryOptions, dir_ref: &DirectoryRef) -> u16 {
356        entry.load_size.map(|s| s.get()).unwrap_or_else(|| {
357            if entry.emulation.is_emulated() {
358                1
359            } else {
360                dir_ref.size.div_ceil(512) as u16
361            }
362        })
363    }
364
365    async fn write_boot_catalog(&mut self) -> io::Result<Option<u32>> {
366        let Some(boot) = self.ops.features.el_torito.clone() else { return Ok(None) };
367
368        let mut catalog = BootCatalog::default();
369        let current_sector = self.data.pad_align_sector().await?;
370
371        for (section, entry) in boot.sections() {
372            self.write_boot_section(&mut catalog, section.as_ref(), &entry).await?;
373        }
374
375        self.write_catalog_to_disk(&boot, &mut catalog, current_sector).await
376    }
377
378    async fn write_boot_info_table(&mut self, entry: &BootEntryOptions, dir_ref: &DirectoryRef) -> io::Result<()> {
379        if dir_ref.size < 64 {
380            return Err(io::Error::new(
381                io::ErrorKind::InvalidInput,
382                "boot image too small for boot info table (minimum 64 bytes)",
383            ));
384        }
385
386        let checksum = self.calculate_boot_checksum(dir_ref).await?;
387        let byte_offset = (dir_ref.extent.0 as u64) * self.ops.sector_size as u64;
388
389        self.data
390            .seek(SeekFrom::Start(byte_offset + 8))
391            .await
392            .map_err(io::Error::erase)?;
393
394        if entry.grub2_boot_info {
395            let table = Grub2BootInfoTable::new(
396                dir_ref.extent.0 as u32,
397                dir_ref.size as u32,
398                checksum,
399            );
400            self.data.write_all(bytemuck::bytes_of(&table)).await?;
401        } else {
402            let table = BootInfoTable::new(
403                dir_ref.extent.0 as u32,
404                dir_ref.size as u32,
405                checksum,
406            );
407            self.data.write_all(bytemuck::bytes_of(&table)).await?;
408        }
409
410        Ok(())
411    }
412
413    async fn calculate_boot_checksum(&mut self, dir_ref: &DirectoryRef) -> io::Result<u32> {
414        let mut checksum = 0u32;
415        let mut buffer = [0u8; 4];
416        let byte_offset = (dir_ref.extent.0 as u64) * self.ops.sector_size as u64;
417
418        self.data
419            .seek(SeekFrom::Start(byte_offset + 64))
420            .await
421            .map_err(io::Error::erase)?;
422
423        let checksum_bytes = dir_ref.size - 64;
424        for _ in 0..(checksum_bytes / 4) {
425            self.data.read_exact(&mut buffer).await?;
426            checksum = checksum.wrapping_add(u32::from_le_bytes(buffer));
427        }
428
429        Ok(checksum)
430    }
431
432    async fn write_catalog_to_disk(
433        &mut self,
434        boot: &BootOptions,
435        catalog: &mut BootCatalog,
436        current_sector: LogicalSector,
437    ) -> io::Result<Option<u32>> {
438        if boot.write_boot_catalog {
439            let dir_ref = self.written_files
440                .find_file(Self::BOOT_CATALOG_FILENAME, self.ops.path_separator)
441                .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "boot.catalog not found"))?;
442
443            self.data.seek_sector(dir_ref.extent).await?;
444            if dir_ref.size < catalog.size() {
445                return Err(io::Error::new(io::ErrorKind::InvalidData, "boot.catalog too small"));
446            }
447            catalog.write(&mut self.data).await?;
448            self.data.seek_sector(current_sector).await?;
449            Ok(Some(dir_ref.extent.0 as u32))
450        } else {
451            self.data.seek_sector(current_sector).await?;
452            catalog.write(&mut self.data).await?;
453            self.data.pad_align_sector().await?;
454            Ok(Some(current_sector.0 as u32))
455        }
456    }
457
458    async fn pad_and_get_end_sector(&mut self) -> io::Result<LogicalSector> {
459        let end_position = self.data.stream_position().await.map_err(io::Error::erase)?;
460        let end_sector = self.data.pad_align_sector().await?;
461        let image_len = end_sector.0 as u64 * self.ops.sector_size as u64;
462
463        if alignment_requires_materialization(end_position, image_len) {
464            self.data
465                .seek(SeekFrom::Start(image_len - 1))
466                .await
467                .map_err(io::Error::erase)?;
468            self.data.write_all(&[0]).await?;
469        }
470
471        Ok(end_sector)
472    }
473
474    async fn patch_volume_descriptors(
475        &mut self,
476        root_dirs: &BTreeMap<EntryType, DirectoryRef>,
477        catalog_ptr: Option<u32>,
478        volume_space: u32,
479    ) -> io::Result<()> {
480        self.data.seek_sector(Self::VOLUME_DESCRIPTOR_SET_START).await?;
481
482        let mut buffer = vec![0u8; self.ops.sector_size];
483        loop {
484            self.data.read_exact(&mut buffer).await?;
485            let header = VolumeDescriptorHeader::from_bytes(&buffer[0..7]);
486            let ty = VolumeDescriptorType::from_u8(header.descriptor_type);
487
488            if matches!(ty, VolumeDescriptorType::VolumeSetTerminator) {
489                break;
490            }
491
492            if !header.is_valid() {
493                return Err(io::Error::new(
494                    io::ErrorKind::InvalidData,
495                    "invalid volume descriptor header during finalization",
496                ));
497            }
498
499            self.patch_single_descriptor(&mut buffer, ty, root_dirs, catalog_ptr, volume_space)?;
500
501            self.data
502                .seek_relative(-(buffer.len() as i64))
503                .await
504                .map_err(io::Error::erase)?;
505            self.data.write_all(&buffer).await?;
506        }
507
508        Ok(())
509    }
510
511    fn patch_single_descriptor(
512        &self,
513        buffer: &mut [u8],
514        ty: VolumeDescriptorType,
515        root_dirs: &BTreeMap<EntryType, DirectoryRef>,
516        catalog_ptr: Option<u32>,
517        volume_space: u32,
518    ) -> io::Result<()> {
519        match ty {
520            VolumeDescriptorType::PrimaryVolumeDescriptor => {
521                self.patch_primary_descriptor(buffer, root_dirs, volume_space)
522            }
523            VolumeDescriptorType::SupplementaryVolumeDescriptor => {
524                self.patch_supplementary_descriptor(buffer, root_dirs, volume_space)
525            }
526            VolumeDescriptorType::BootRecord => {
527                self.patch_boot_record(buffer, catalog_ptr)
528            }
529            _ => Ok(()),
530        }
531    }
532
533    fn patch_primary_descriptor(
534        &self,
535        buffer: &mut [u8],
536        root_dirs: &BTreeMap<EntryType, DirectoryRef>,
537        volume_space: u32,
538    ) -> io::Result<()> {
539        let base_type = self
540            .entry_types
541            .iter()
542            .find(|e| matches!(e, EntryType::Level1 { .. } | EntryType::Level2 { .. }))
543            .ok_or_else(|| {
544                io::Error::new(io::ErrorKind::InvalidData, "no base Level entry type found for PVD")
545            })?;
546
547        let root_dir = root_dirs.get(base_type)
548            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "root directory not found for PVD"))?;
549        let pt = self.path_tables.get(base_type)
550            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "path table not found for PVD"))?;
551
552        let pvd = bytemuck::from_bytes_mut::<PrimaryVolumeDescriptor>(buffer);
553        pvd.dir_record.header.extent.write(root_dir.extent.0 as u32);
554        pvd.dir_record.header.data_len.write(root_dir.size as u32);
555        pvd.type_l_path_table.set(pt.lpt.0 as u32);
556        pvd.type_m_path_table.set(pt.mpt.0 as u32);
557        pvd.path_table_size.write(pt.size as u32);
558        pvd.volume_space_size.write(volume_space);
559
560        Ok(())
561    }
562
563    fn patch_supplementary_descriptor(
564        &self,
565        buffer: &mut [u8],
566        root_dirs: &BTreeMap<EntryType, DirectoryRef>,
567        volume_space: u32,
568    ) -> io::Result<()> {
569        let svd = bytemuck::from_bytes_mut::<SupplementaryVolumeDescriptor>(buffer);
570
571        match svd.header.version {
572            1 => self.patch_joliet_descriptor(svd, root_dirs, volume_space),
573            2 => self.patch_enhanced_descriptor(svd, root_dirs, volume_space),
574            _ => Ok(()),
575        }
576    }
577
578    fn patch_joliet_descriptor(
579        &self,
580        svd: &mut SupplementaryVolumeDescriptor,
581        root_dirs: &BTreeMap<EntryType, DirectoryRef>,
582        volume_space: u32,
583    ) -> io::Result<()> {
584        for &level in JolietLevel::all() {
585            if JolietLevel::from_escape_sequence(&svd.escape_sequences) != Some(level) {
586                continue;
587            }
588
589            let joliet = self
590                .entry_types
591                .iter()
592                .find(|e| matches!(e, EntryType::Joliet{ level: jl, ..} if *jl == level))
593                .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Joliet entry type not found"))?;
594
595            let root_dir = root_dirs.get(joliet)
596                .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "root dir not found for Joliet"))?;
597            let pt = self.path_tables.get(joliet)
598                .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "path table not found for Joliet"))?;
599
600            svd.dir_record.header.extent.write(root_dir.extent.0 as u32);
601            svd.dir_record.header.data_len.write(root_dir.size as u32);
602            svd.type_l_path_table.set(pt.lpt.0 as u32);
603            svd.type_m_path_table.set(pt.mpt.0 as u32);
604            svd.path_table_size.write(pt.size as u32);
605            svd.volume_space_size.write(volume_space);
606            break;
607        }
608
609        Ok(())
610    }
611
612    fn patch_enhanced_descriptor(
613        &self,
614        svd: &mut SupplementaryVolumeDescriptor,
615        root_dirs: &BTreeMap<EntryType, DirectoryRef>,
616        volume_space: u32,
617    ) -> io::Result<()> {
618        if svd.header.version != 2 {
619            return Ok(());
620        }
621
622        let l3 = self
623            .entry_types
624            .iter()
625            .find(|e| matches!(e, EntryType::Level3 { .. }))
626            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Level3 entry type not found"))?;
627
628        let root_dir = root_dirs.get(l3)
629            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "root dir not found for Level3"))?;
630        let pt = self.path_tables.get(l3)
631            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "path table not found for Level3"))?;
632
633        svd.dir_record.header.extent.write(root_dir.extent.0 as u32);
634        svd.dir_record.header.data_len.write(root_dir.size as u32);
635        svd.type_l_path_table.set(pt.lpt.0 as u32);
636        svd.type_m_path_table.set(pt.mpt.0 as u32);
637        svd.path_table_size.write(pt.size as u32);
638        svd.volume_space_size.write(volume_space);
639
640        Ok(())
641    }
642
643    fn patch_boot_record(&self, buffer: &mut [u8], catalog_ptr: Option<u32>) -> io::Result<()> {
644        let Some(ptr) = catalog_ptr else {
645            return Err(io::Error::new(
646                io::ErrorKind::InvalidData,
647                "boot record found but no boot catalog was written",
648            ));
649        };
650
651        let boot_record = bytemuck::from_bytes_mut::<BootRecordVolumeDescriptor>(buffer);
652        boot_record.catalog_ptr.set(ptr);
653        Ok(())
654    }
655
656    async fn write_files(&mut self, files: &InputTree) -> io::Result<BTreeMap<EntryType, DirectoryRef>> {
657        let walker = FileTreeWalker::new(files);
658        walker.walk(&mut self.written_files);
659
660        if self.ops.has_rock_ridge_deep_dirs() {
661            relocate_deep_directories(&mut self.written_files, self.ops.features.filenames.into())?;
662        }
663
664        let root_id = self.written_files.root_dir();
665        let (order, file_order) = collect_preorder(&self.written_files, &root_id);
666
667        let rrip_options = self.ops.features.rock_ridge;
668        let rrip_time = self.rrip_time;
669        let entry_types = self.entry_types.clone();
670
671        let relocation_refs = self.plan_layout(
672            &order, &file_order, &entry_types,
673            rrip_options, &rrip_time, &root_id,
674        ).await?;
675
676        self.write_layout(
677            &order, &file_order, &entry_types,
678            rrip_options, &rrip_time, &relocation_refs, &root_id,
679        ).await?;
680
681        self.patch_moved_directories(&root_id, &relocation_refs).await?;
682
683        let roots = self.written_files.root_refs().clone();
684        let pos = self.data.stream_position().await.map_err(io::Error::erase)?;
685        for root in roots.values().cloned() {
686            self.update_directory(root, root).await?;
687        }
688        self.data.seek(SeekFrom::Start(pos)).await.map_err(io::Error::erase)?;
689
690        Ok(roots)
691    }
692
693    async fn plan_layout(
694        &mut self,
695        order: &[DirectoryId],
696        file_order: &[FileOrder],
697        entry_types: &[EntryType],
698        rrip_options: Option<RripOptions>,
699        rrip_time: &RripTime,
700        root_id: &DirectoryId,
701    ) -> io::Result<RelocationMap> {
702        let sector_size = self.ops.sector_size as u64;
703        let cursor = self.data.stream_position().await.map_err(io::Error::erase)?;
704        let inode_counter = self.inode_counter;
705
706        let default_refs = self.build_default_refs(order, entry_types);
707        let (relocation_refs, cursor, inode_counter) = self.plan_directories(
708            order,
709            entry_types,
710            &default_refs,
711            rrip_options,
712            rrip_time,
713            root_id,
714            cursor,
715            inode_counter,
716        ).await?;
717
718        let _ = self.plan_files(file_order, cursor, sector_size).await?;
719
720        self.inode_counter = inode_counter;
721        Ok(relocation_refs)
722    }
723
724    fn build_default_refs(
725        &mut self,
726        order: &[DirectoryId],
727        entry_types: &[EntryType],
728    ) -> RelocationMap {
729        let mut default_refs = BTreeMap::new();
730        for directory_id in order {
731            let dir = self.written_files.get(directory_id);
732            for ty in entry_types {
733                default_refs.insert((dir.id, *ty), DirectoryRef::default());
734                if let DirectoryRelocation::Moved { id, .. } = dir.relocation {
735                    default_refs.insert((id, *ty), DirectoryRef::default());
736                }
737            }
738            let dir = self.written_files.get_mut(directory_id);
739            for ty in entry_types {
740                dir.entries.entry(*ty).or_default();
741            }
742        }
743        default_refs
744    }
745
746    #[allow(clippy::too_many_arguments)]
747    async fn plan_directories(
748        &mut self,
749        order: &[DirectoryId],
750        entry_types: &[EntryType],
751        default_refs: &RelocationMap,
752        rrip_options: Option<RripOptions>,
753        rrip_time: &RripTime,
754        root_id: &DirectoryId,
755        mut cursor: u64,
756        mut inode_counter: u32,
757    ) -> io::Result<(RelocationMap, u64, u32)> {
758        let sector_size = self.ops.sector_size as u64;
759        let mut relocation_refs = BTreeMap::new();
760
761        for directory_id in order {
762            let is_root = directory_id == root_id;
763            for ty in entry_types {
764                let dir = self.written_files.get(directory_id);
765                let records = PendingRecords::new(
766                    *ty,
767                    dir,
768                    is_root,
769                    &mut inode_counter,
770                    rrip_options.as_ref(),
771                    rrip_time,
772                    default_refs,
773                    sector_size,
774                )?;
775
776                let (extent, size_sectors) = layout_directory_records(cursor, sector_size, &records);
777                let ca_len = records.overflow_len();
778                let reference = DirectoryRef::new(extent as _, (size_sectors * sector_size) as usize);
779
780                let dir = self.written_files.get_mut(directory_id);
781                dir.entries.insert(*ty, reference);
782                relocation_refs.insert((dir.id, *ty), reference);
783
784                if let DirectoryRelocation::Moved { id, .. } = dir.relocation {
785                    relocation_refs.insert((id, *ty), reference);
786                }
787
788                cursor = (extent + size_sectors) * sector_size + ca_len;
789            }
790        }
791
792        Ok((relocation_refs, cursor, inode_counter))
793    }
794
795    async fn plan_files(
796        &mut self,
797        file_order: &[FileOrder],
798        mut cursor: u64,
799        sector_size: u64,
800    ) -> io::Result<u64> {
801
802        for (directory_id, index) in file_order {
803            let dir = self.written_files.get_mut(directory_id);
804            let file = &mut dir.files[*index];
805
806            let len = file.kind.file_len().unwrap_or(0);
807
808            if len == 0 {
809                file.entry = DirectoryRef::default();
810                continue;
811            }
812
813            let mut extents = ExtentIter::new(len, cursor, sector_size);
814            if let Some((first, new_cursor)) = extents.next() {
815                file.entry = first;
816                cursor = new_cursor;
817            }
818            for (_, new_cursor) in extents {
819                cursor = new_cursor;
820            }
821        }
822
823        Ok(cursor)
824    }
825
826    #[allow(clippy::too_many_arguments)]
827    async fn write_layout(
828        &mut self,
829        order: &[DirectoryId],
830        file_order: &[FileOrder],
831        entry_types: &[EntryType],
832        rrip_options: Option<RripOptions>,
833        rrip_time: &[u8; 7],
834        relocation_refs: &RelocationMap,
835        root_id: &DirectoryId,
836    ) -> io::Result<()> {
837        let sector_size = self.ops.sector_size as u64;
838        let mut inode_counter = self.inode_counter;
839
840        self.write_directories(
841            order,
842            entry_types,
843            rrip_options,
844            rrip_time,
845            relocation_refs,
846            root_id,
847            sector_size,
848            &mut inode_counter,
849        ).await?;
850
851        self.inode_counter = inode_counter;
852        self.write_file_data(file_order).await?;
853
854        Ok(())
855    }
856
857    #[allow(clippy::too_many_arguments)]
858    async fn write_directories(
859        &mut self,
860        order: &[DirectoryId],
861        entry_types: &[EntryType],
862        rrip_options: Option<RripOptions>,
863        rrip_time: &RripTime,
864        relocation_refs: &RelocationMap,
865        root_id: &DirectoryId,
866        sector_size: u64,
867        inode_counter: &mut u32,
868    ) -> io::Result<()> {
869        for directory_id in order {
870            let is_root = directory_id == root_id;
871            for ty in entry_types {
872                let dir = self.written_files.get(directory_id);
873                let expected = dir.entries.get(ty).copied().unwrap_or_default();
874                let mut records = PendingRecords::new(
875                    *ty,
876                    dir,
877                    is_root,
878                    inode_counter,
879                    rrip_options.as_ref(),
880                    rrip_time,
881                    relocation_refs,
882                    sector_size,
883                )?;
884                Self::write_directory_records(&mut self.data, sector_size, expected, &mut records).await?;
885            }
886        }
887        Ok(())
888    }
889
890    async fn write_file_data(
891        &mut self,
892        file_order: &[FileOrder],
893    ) -> io::Result<()> {
894        let sector_size = self.data.sector_size as u64;
895
896        for (directory_id, index) in file_order {
897            let dir = self.written_files.get(directory_id);
898            let file = &dir.files[*index];
899            let len = file.kind.file_len().unwrap_or(0);
900            let cursor = file.entry.extent.0 as u64 * sector_size;
901            #[cfg(feature = "unstable-streaming")]
902            let mut reader = match &file.kind {
903                InputEntryKind::Source(source) => Some(source.open().map_err(source_error)?),
904                _ => None,
905            };
906
907            let mut offset = 0_u64;
908            for (extent, _) in ExtentIter::new(len, cursor, sector_size) {
909                let start = self.data.pad_align_sector().await?;
910
911                if start != extent.extent {
912                    return Err(io::Error::new(
913                        io::ErrorKind::InvalidData,
914                        "extent prediction did not match",
915                    ));
916                }
917
918                #[cfg(test)]
919                if matches!(file.kind, InputEntryKind::TestFile { .. }) {
920                    if extent.size > 0 {
921                        self.data
922                            .seek(SeekFrom::Current(extent.size as i64 - 1))
923                            .await
924                            .map_err(io::Error::erase)?;
925                        self.data.write_all(&[0]).await?;
926                    }
927                    offset += extent.size as u64;
928                    continue;
929                }
930
931                #[cfg(feature = "unstable-streaming")]
932                if let Some(reader) = reader.as_mut() {
933                    Self::copy_from_reader(&mut self.data, reader.as_mut(), extent.size as u64)
934                        .await?;
935                    offset += extent.size as u64;
936                    continue;
937                }
938
939                if let InputEntryKind::File(contents) = &file.kind {
940                    let chunk_end = (offset + extent.size as u64).min(contents.len() as u64);
941                    let range = usize::try_from(offset)
942                        .ok()
943                        .zip(usize::try_from(chunk_end).ok())
944                        .map(|(start, end)| start..end)
945                        .ok_or_else(|| {
946                            io::Error::new(
947                                io::ErrorKind::InvalidData,
948                                "file content range does not fit in memory",
949                            )
950                        })?;
951                    self.data.write_all(&contents[range]).await?;
952                    offset += extent.size as u64;
953                }
954            }
955        }
956
957        Ok(())
958    }
959
960    /// Copies exactly `len` bytes from `reader` to the image at the current position.
961    #[cfg(feature = "unstable-streaming")]
962    async fn copy_from_reader(
963        data: &mut IsoCursor<DATA>,
964        reader: &mut (dyn std::io::Read + Send),
965        len: u64,
966    ) -> io::Result<()> {
967        const CHUNK: usize = 64 * 1024;
968        let mut buffer = vec![0_u8; CHUNK];
969        let mut remaining = len;
970        while remaining > 0 {
971            let want = usize::try_from(remaining.min(CHUNK as u64)).unwrap_or(CHUNK);
972            let read = match reader.read(&mut buffer[..want]) {
973                Ok(0) => {
974                    return Err(io::Error::new(
975                        io::ErrorKind::UnexpectedEof,
976                        "file source ended before its declared length",
977                    ));
978                }
979                Ok(read) => read,
980                Err(error) if error.kind() == std::io::ErrorKind::Interrupted => continue,
981                Err(error) => return Err(source_error(error)),
982            };
983            data.write_all(&buffer[..read]).await?;
984            remaining -= read as u64;
985        }
986        Ok(())
987    }
988
989    async fn patch_moved_directories(
990        &mut self,
991        root_id: &DirectoryId,
992        relocation_refs: &RelocationMap,
993    ) -> io::Result<()> {
994        let moved = MovedDirectory::collect(self.written_files.get(root_id));
995        let directory_end = self.data.stream_position().await.map_err(io::Error::erase)?;
996
997        for moved_dir in moved.iter() {
998            for (ty, directory) in moved_dir.entries.iter() {
999
1000                if !ty.supports_rrip() {
1001                    continue;
1002                }
1003
1004                let parent = relocation_refs
1005                    .get(&(moved_dir.logical_parent, *ty))
1006                    .copied()
1007                    .ok_or_else(|| {
1008                        io::Error::new(
1009                            io::ErrorKind::InvalidData,
1010                            "logical parent extent was not written",
1011                        )
1012                    })?;
1013                self.patch_parent_link(*directory, parent).await?;
1014            }
1015        }
1016
1017        self.data
1018            .seek(SeekFrom::Start(directory_end))
1019            .await
1020            .map_err(io::Error::erase)?;
1021        Ok(())
1022    }
1023
1024    async fn write_path_tables(&mut self) -> io::Result<()> {
1025        for i in 0..self.entry_types.len() {
1026            let ty = self.entry_types[i];
1027            let l_ref = self.write_path_table(ty, EndianType::LittleEndian).await?;
1028            let m_ref = self.write_path_table(ty, EndianType::BigEndian).await?;
1029            assert_eq!(l_ref.size, m_ref.size);
1030            self.path_tables.insert(
1031                ty,
1032                PathTableRef {
1033                    lpt: l_ref.extent,
1034                    mpt: m_ref.extent,
1035                    size: l_ref.size as u64,
1036                },
1037            );
1038        }
1039        Ok(())
1040    }
1041
1042    async fn write_path_table(&mut self, ty: EntryType, endian: EndianType) -> io::Result<DirectoryRef> {
1043        let start = self.data.pad_align_sector().await?;
1044        PathTableWriter {
1045            written_files: &self.written_files,
1046            ty,
1047            endian,
1048        }
1049        .write(&mut self.data).await?;
1050        let size = self
1051            .data
1052            .stream_position()
1053            .await
1054            .map_err(io::Error::erase)? as usize
1055            - (start.0 * self.data.sector_size);
1056        let _end = self.data.pad_align_sector().await?;
1057        Ok(DirectoryRef {
1058            extent: start,
1059            size,
1060        })
1061    }
1062
1063    /// Writes the partition tables (MBR, GPT, or Hybrid) based on configuration.
1064    ///
1065    /// The GPT and Hybrid schemes append 33 sectors of 512 bytes (backup
1066    /// entry array plus backup header) after the ISO data, padded so that
1067    /// the image length stays a multiple of the logical sector size and the
1068    /// backup header occupies the last 512-byte sector. The appended region
1069    /// lies outside the ISO file-data area but is still counted in
1070    /// `volume_space_size`, so tools that copy `volume_space_size` logical
1071    /// sectors preserve the backup GPT, matching xorriso's appended-GPT
1072    /// practice.
1073    async fn write_partition_tables(&mut self, end_sector: LogicalSector) -> io::Result<()> {
1074        match self.ops.partition_scheme() {
1075            None | Some(PartitionScheme::None) => {
1076                // No partition table requested - leave the system area empty.
1077                // Writing an MBR here would cause the kernel to detect a
1078                // partition table and prevent the ISO from being mounted.
1079            }
1080            Some(PartitionScheme::Mbr) => {
1081                self.write_mbr_boot(end_sector).await?;
1082            }
1083            Some(PartitionScheme::Gpt) => {
1084                self.write_gpt_boot(end_sector).await?;
1085            }
1086            Some(PartitionScheme::Hybrid) => {
1087                self.write_hybrid_boot(end_sector).await?;
1088            }
1089        }
1090
1091        Ok(())
1092    }
1093
1094    /// Writes an MBR partition table for BIOS USB boot (isohybrid-style).
1095    async fn write_mbr_boot(&mut self, end_sector: LogicalSector) -> io::Result<()> {
1096        let sector_count = checked_sector_count(
1097            end_sector.0 as u64 * (self.data.sector_size as u64 / 512),
1098            "image exceeds the 2 TiB limit of an MBR partition entry",
1099        )?;
1100
1101        let hybrid_opts = self.ops.features.hybrid_boot.as_ref();
1102        let bootable = hybrid_opts.map(|h| h.bootable).unwrap_or(true);
1103
1104        let mut mbr = MasterBootRecord::default();
1105        mbr.with_partition_table(|pt| {
1106            pt[0] = MbrPartition::new_iso_partition(sector_count, bootable);
1107        });
1108
1109        // Inject bootstrap code if provided
1110        if let Some(ref hybrid_opts) = self.ops.features.hybrid_boot
1111            && let Some(ref bootstrap) = hybrid_opts.mbr_bootstrap
1112        {
1113            let len = bootstrap.len().min(446);
1114            mbr.bootstrap[..len].copy_from_slice(&bootstrap[..len]);
1115        }
1116
1117        self.data
1118            .seek(SeekFrom::Start(0))
1119            .await
1120            .map_err(io::Error::erase)?;
1121        self.data.write_all(bytemuck::bytes_of(&mbr)).await?;
1122
1123        Ok(())
1124    }
1125
1126    /// Resolves the ISO path of the El Torito UEFI image to expose as a GPT
1127    /// EFI System Partition.
1128    ///
1129    /// Uses `HybridBootOptions::efi_boot_partition` when set; otherwise falls
1130    /// back to the boot image of the single `PlatformId::UEFI` El Torito
1131    /// section entry, if there is exactly one.
1132    fn efi_boot_partition_path(&self) -> Option<String> {
1133        let hybrid = self.ops.features.hybrid_boot.as_ref()?;
1134        if let Some(path) = &hybrid.efi_boot_partition {
1135            return Some(path.clone());
1136        }
1137        let boot = self.ops.features.el_torito.as_ref()?;
1138        let mut uefi_entries = boot
1139            .entries
1140            .iter()
1141            .filter(|(section, _)| section.platform == PlatformId::UEFI)
1142            .map(|(_, entry)| &entry.boot_image_path);
1143        let first = uefi_entries.next()?;
1144        if uefi_entries.next().is_some() {
1145            return None;
1146        }
1147        Some(first.clone())
1148    }
1149
1150    /// Builds the GPT structures for the image.
1151    ///
1152    /// The disk spans the ISO data plus an appended backup-GPT region; the
1153    /// backup header sits in the last 512-byte sector of the padded image.
1154    /// The layout is non-overlapping (overlap is rejected by
1155    /// [`GptDisk::validate`]): a basic-data partition covers the ISO area
1156    /// before the EFI System Partition, the ESP covers the El Torito UEFI
1157    /// image exactly, and a second basic-data partition covers any remaining
1158    /// ISO data after it. Partitions start no earlier than 512-byte LBA 64,
1159    /// where the ISO volume descriptors begin: tools that convert ISOHYBRID
1160    /// GPT images to MBR (e.g. `limine bios-install`) embed boot code in
1161    /// sectors 1..63 and reject partitions starting below sector 63.
1162    ///
1163    /// Returns the disk together with the entry indices of the leading
1164    /// ISO9660 partition and the ESP, for hybrid MBR mirroring.
1165    /// Total image size in 512-byte sectors for GPT/Hybrid images: the ISO
1166    /// data plus the appended backup-GPT region, padded to a logical-sector
1167    /// boundary.
1168    fn gpt_total_512(&self, end_sector: LogicalSector) -> u64 {
1169        const BACKUP_GPT_SECTORS: u64 = 33;
1170        let blocks_per_sector = (self.ops.sector_size / 512) as u64;
1171        let iso_512 = end_sector.0 as u64 * blocks_per_sector;
1172        (iso_512 + BACKUP_GPT_SECTORS).div_ceil(blocks_per_sector) * blocks_per_sector
1173    }
1174
1175    /// The `volume_space_size` to declare: for GPT/Hybrid images it covers the
1176    /// whole image including the appended backup-GPT region, so tools that
1177    /// copy `volume_space_size` logical sectors preserve the backup GPT.
1178    fn volume_space_sectors(&self, end_sector: LogicalSector) -> io::Result<u32> {
1179        let sectors = match self
1180            .ops
1181            .features
1182            .hybrid_boot
1183            .as_ref()
1184            .map(|h| h.partition_scheme)
1185        {
1186            Some(PartitionScheme::Gpt) | Some(PartitionScheme::Hybrid) => {
1187                let blocks_per_sector = (self.ops.sector_size / 512) as u64;
1188                self.gpt_total_512(end_sector) / blocks_per_sector
1189            }
1190            _ => end_sector.0 as u64,
1191        };
1192        checked_sector_count(
1193            sectors,
1194            "image exceeds the 32-bit volume space size of ISO 9660",
1195        )
1196    }
1197
1198    fn build_gpt_disk(
1199        &self,
1200        end_sector: LogicalSector,
1201    ) -> io::Result<(GptDisk, Option<usize>, Option<usize>)> {
1202        let blocks_per_sector = (self.data.sector_size / 512) as u64;
1203        let iso_512 = end_sector.0 as u64 * blocks_per_sector;
1204        let total_512 = self.gpt_total_512(end_sector);
1205
1206        let mut gpt = GptDisk::new(total_512, 512);
1207        let disk_guid = generate_guid_from_string(&alloc::format!("disk-{}", self.ops.volume_name));
1208        gpt.primary_header.disk_guid = disk_guid;
1209        gpt.backup_header.disk_guid = disk_guid;
1210
1211        let iso_part_start = self.get_iso_partition_start(&gpt)?;
1212        let iso_end = iso_512.saturating_sub(1);
1213
1214        if iso_end <= iso_part_start {
1215            return Err(io::Error::new(
1216                io::ErrorKind::InvalidData,
1217                "image too small for a GPT partition table",
1218            ));
1219        }
1220
1221        let esp = self.find_esp_partition(blocks_per_sector, iso_part_start, iso_end)?;
1222        let (iso_index, esp_index) = self.add_gpt_partitions(&mut gpt, esp, iso_part_start, iso_end)?;
1223
1224        gpt.update_crcs();
1225        gpt.validate().map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid GPT layout"))?;
1226        Ok((gpt, iso_index, esp_index))
1227    }
1228
1229    fn get_iso_partition_start(&self, gpt: &GptDisk) -> io::Result<u64> {
1230        const ISO_DATA_START_512: u64 = 64;
1231        let first_usable = gpt.primary_header.first_usable_lba.to_ne();
1232        Ok(first_usable.max(ISO_DATA_START_512))
1233    }
1234
1235    fn find_esp_partition(
1236        &self,
1237        blocks_per_sector: u64,
1238        iso_part_start: u64,
1239        iso_end: u64,
1240    ) -> io::Result<Option<(u64, u64)>> {
1241        let Some(path) = self.efi_boot_partition_path() else { return Ok(None) };
1242
1243        let dir_ref = self
1244            .written_files
1245            .find_file(&path, self.ops.path_separator)
1246            .ok_or_else(|| {
1247                io::Error::new(
1248                    io::ErrorKind::NotFound,
1249                    "EFI boot partition image not found in the ISO tree",
1250                )
1251            })?;
1252
1253        let start = dir_ref.extent.0 as u64 * blocks_per_sector;
1254        let sectors = (dir_ref.size as u64).div_ceil(512).max(1);
1255        let end = start + sectors - 1;
1256
1257        if start < iso_part_start || end > iso_end {
1258            return Err(io::Error::new(
1259                io::ErrorKind::InvalidData,
1260                "EFI boot partition lies outside the ISO data area",
1261            ));
1262        }
1263
1264        Ok(Some((start, end)))
1265    }
1266
1267    fn add_gpt_partitions(
1268        &self,
1269        gpt: &mut GptDisk,
1270        esp: Option<(u64, u64)>,
1271        iso_part_start: u64,
1272        iso_end: u64,
1273    ) -> io::Result<(Option<usize>, Option<usize>)> {
1274        let map_err = |_| io::Error::new(io::ErrorKind::InvalidData, "invalid GPT layout");
1275        let mut iso_index = None;
1276        let mut esp_index = None;
1277
1278        match esp {
1279            Some((esp_start, esp_end)) => {
1280                if esp_start > iso_part_start {
1281                    let mut data = GptPartitionEntry::new(
1282                        Guid::BASIC_DATA,
1283                        generate_guid_from_string(&self.ops.volume_name),
1284                        iso_part_start,
1285                        esp_start - 1,
1286                    );
1287                    data.set_name_ascii(Self::GPT_PARTITION_NAME_ISO);
1288                    iso_index = Some(gpt.add_partition(data).map_err(map_err)?);
1289                }
1290
1291                let mut esp_entry = GptPartitionEntry::new(
1292                    Guid::EFI_SYSTEM,
1293                    generate_guid_from_string(&alloc::format!("esp-{}", self.ops.volume_name)),
1294                    esp_start,
1295                    esp_end,
1296                );
1297                esp_entry.set_name_ascii(Self::GPT_PARTITION_NAME_ESP);
1298                esp_index = Some(gpt.add_partition(esp_entry).map_err(map_err)?);
1299
1300                if esp_end < iso_end {
1301                    let mut tail = GptPartitionEntry::new(
1302                        Guid::BASIC_DATA,
1303                        generate_guid_from_string(&alloc::format!("data-{}", self.ops.volume_name)),
1304                        esp_end + 1,
1305                        iso_end,
1306                    );
1307                    tail.set_name_ascii(Self::GPT_PARTITION_NAME_ISO);
1308                    gpt.add_partition(tail).map_err(map_err)?;
1309                }
1310            }
1311            None => {
1312                let mut data = GptPartitionEntry::new(
1313                    Guid::BASIC_DATA,
1314                    generate_guid_from_string(&self.ops.volume_name),
1315                    iso_part_start,
1316                    iso_end,
1317                );
1318                data.set_name_ascii(b"ISO9660");
1319                iso_index = Some(gpt.add_partition(data).map_err(map_err)?);
1320            }
1321        }
1322
1323        Ok((iso_index, esp_index))
1324    }
1325
1326    /// Writes a GPT partition table (protective MBR, primary and backup GPT)
1327    /// for UEFI boot. See [`Self::write_partition_tables`] for the appended
1328    /// backup region and [`Self::build_gpt_disk`] for the partition layout.
1329    async fn write_gpt_boot(&mut self, end_sector: LogicalSector) -> io::Result<()> {
1330        let (gpt, _, _) = self.build_gpt_disk(end_sector)?;
1331        gpt.write_to(&mut self.data).await.map_err(part_io_error)?;
1332        Ok(())
1333    }
1334
1335    /// Writes a Hybrid MBR + GPT for dual BIOS/UEFI boot.
1336    ///
1337    /// The MBR carries a protective entry over the pre-partition gap, mirrors
1338    /// the leading ISO9660 basic-data partition as type 0x17, and mirrors the
1339    /// EFI System Partition as type 0xEF.
1340    async fn write_hybrid_boot(&mut self, end_sector: LogicalSector) -> io::Result<()> {
1341        let hybrid_opts = self.ops.features.hybrid_boot.as_ref();
1342        let bootable = hybrid_opts.map(|h| h.bootable).unwrap_or(true);
1343
1344        let (gpt, iso_index, esp_index) = self.build_gpt_disk(end_sector)?;
1345        let total_512 = gpt.backup_header.my_lba.to_ne() + 1;
1346
1347        let mut builder = HybridMbrBuilder::new(total_512).protective_slot(0);
1348        if let Some(iso_index) = iso_index {
1349            builder = builder.mirror_partition(iso_index as u32, MbrPartitionType::Iso9660, bootable);
1350        }
1351        if let Some(esp_index) = esp_index {
1352            builder = builder.mirror_partition(
1353                esp_index as u32,
1354                MbrPartitionType::EfiSystemPartition,
1355                false,
1356            );
1357        }
1358        let mut mbr = builder
1359            .build(&gpt.entries)
1360            .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid hybrid MBR"))?;
1361
1362        if let Some(ref hybrid_opts) = self.ops.features.hybrid_boot
1363            && let Some(ref bootstrap) = hybrid_opts.mbr_bootstrap
1364        {
1365            let len = bootstrap.len().min(446);
1366            mbr.bootstrap[..len].copy_from_slice(&bootstrap[..len]);
1367        }
1368
1369        gpt.write_to_with_mbr(&mut self.data, &mbr)
1370            .await
1371            .map_err(part_io_error)?;
1372
1373        Ok(())
1374    }
1375
1376    async fn update_directory(
1377        &mut self,
1378        parent: DirectoryRef,
1379        directory: DirectoryRef,
1380    ) -> io::Result<()> {
1381        let sector_size = self.data.sector_size as u64;
1382        let start = self.data.seek_sector(directory.extent).await?;
1383        let mut offset = 0;
1384        loop {
1385
1386            if offset >= directory.size as u64 {
1387                break;
1388            }
1389
1390            self.data
1391                .seek(SeekFrom::Start(start + offset))
1392                .await
1393                .map_err(io::Error::erase)?;
1394
1395            let mut record = DirectoryRecord::parse(&mut self.data).await?;
1396
1397            if record.is_empty() {
1398                let next_sector = (offset / sector_size + 1) * sector_size;
1399                if next_sector < directory.size as u64 {
1400                    offset = next_sector;
1401                    continue;
1402                }
1403                break;
1404            }
1405
1406            if matches!(record.name(), b"\x00" | b"\x01") {
1407                self.patch_dot_or_dotdot(&mut record, directory, parent, start, offset).await?;
1408                offset += record.len() as u64;
1409                continue;
1410            }
1411
1412            offset += record.len() as u64;
1413
1414            if record.flags().contains(FileFlags::DIRECTORY) {
1415                let record = DirectoryRef {
1416                    extent: LogicalSector(record.header().extent.read() as usize),
1417                    size: record.header().data_len.read() as usize,
1418                };
1419                self.update_directory(directory, record).await?;
1420            }
1421        }
1422
1423        Ok(())
1424    }
1425
1426    async fn patch_dot_or_dotdot(
1427        &mut self,
1428        record: &mut DirectoryRecord,
1429        directory: DirectoryRef,
1430        parent: DirectoryRef,
1431        start: u64,
1432        offset: u64,
1433    ) -> io::Result<()> {
1434        let dir_ref = [directory, parent][record.name()[0] as usize];
1435        let header = record.header_mut();
1436        header.extent.write(dir_ref.extent.0 as u32);
1437        header.data_len.write(dir_ref.size as u32);
1438
1439        self.data
1440            .seek(SeekFrom::Start(start + offset))
1441            .await
1442            .map_err(io::Error::erase)?;
1443        record.write(&mut self.data).await?;
1444        Ok(())
1445    }
1446
1447    async fn patch_parent_link(
1448        &mut self,
1449        directory: DirectoryRef,
1450        parent: DirectoryRef,
1451    ) -> io::Result<()> {
1452        let start = self.data.seek_sector(directory.extent).await?;
1453        let (dot, mut dotdot) = self.read_dot_and_dotdot(start).await?;
1454        let header_len = dot.header().len as u64;
1455
1456        self.find_and_patch_pl_entry(&mut dotdot, parent.extent.0 as u32)?;
1457        self.write_dotdot_at(start, header_len, &dotdot).await?;
1458
1459        Ok(())
1460    }
1461
1462    async fn read_dot_and_dotdot(
1463        &mut self,
1464        start: u64,
1465    ) -> io::Result<(DirectoryRecord, DirectoryRecord)> {
1466        self.data.seek(SeekFrom::Start(start)).await.map_err(io::Error::erase)?;
1467        let dot = DirectoryRecord::parse(&mut self.data).await?;
1468
1469        self.data
1470            .seek(SeekFrom::Start(start + dot.header().len as u64))
1471            .await
1472            .map_err(io::Error::erase)?;
1473        let dotdot = DirectoryRecord::parse(&mut self.data).await?;
1474
1475        Ok((dot, dotdot))
1476    }
1477
1478    /// Finds and patches the RRIP "PL" (Parent Location) entry in the ".." record.
1479    ///
1480    /// Updates the PL entry to point to the logical parent of a relocated directory.
1481    fn find_and_patch_pl_entry(&self, dotdot: &mut DirectoryRecord, parent_extent: u32) -> io::Result<()> {
1482        let system_use = dotdot.system_use_mut();
1483        let mut offset = 0;
1484
1485        while offset + 4 <= system_use.len() {
1486            let length = system_use[offset + 2] as usize;
1487            if length < 4 || offset + length > system_use.len() {
1488                break;
1489            }
1490
1491            if &system_use[offset..offset + 2] == b"PL" && length >= 12 {
1492                let value = crate::types::U32LsbMsb::new(parent_extent);
1493                system_use[offset + 4..offset + 12].copy_from_slice(bytemuck::bytes_of(&value));
1494                return Ok(());
1495            }
1496
1497            offset += length;
1498        }
1499
1500        Err(io::Error::new(
1501            io::ErrorKind::InvalidData,
1502            "relocated directory is missing its RRIP PL entry",
1503        ))
1504    }
1505
1506    async fn write_dotdot_at(
1507        &mut self,
1508        start: u64,
1509        header_len: u64,
1510        dotdot: &DirectoryRecord,
1511    ) -> io::Result<()> {
1512        self.data
1513            .seek(SeekFrom::Start(start + header_len))
1514            .await
1515            .map_err(io::Error::erase)?;
1516        dotdot.write(&mut self.data).await?;
1517        Ok(())
1518    }
1519
1520    /// Write records at the extent the planning pass assigned, followed by
1521    /// the continuation area their CE entries are patched to point at.
1522    /// Fails if the written layout does not match the plan.
1523    async fn write_directory_records(
1524        data: &mut IsoCursor<DATA>,
1525        sector_size: u64,
1526        expected: DirectoryRef,
1527        records: &mut PendingRecords,
1528    ) -> io::Result<()> {
1529        let has_overflow = records.has_overflow();
1530        if has_overflow {
1531            let ca_sector = expected.extent.0 as u64 + expected.size as u64 / sector_size;
1532            let mut offset = 0u32;
1533            for record in records.iter_mut() {
1534                if record.split.has_overflow() {
1535                    record.split.patch_ce(ca_sector as u32, offset);
1536                    offset += record.split.overflow.len() as u32;
1537                }
1538            }
1539        }
1540
1541        let start = data.pad_align_sector().await?;
1542
1543        if start != expected.extent {
1544            return Err(io::Error::new(
1545                io::ErrorKind::InvalidData,
1546                "directory extent prediction did not match the written layout",
1547            ));
1548        }
1549
1550        for record in records.iter() {
1551            let directory_record = DirectoryRecord::new(
1552                &record.name,
1553                &record.split.inline,
1554                record.dir_ref,
1555                record.flags,
1556            );
1557            let position = data.stream_position().await.map_err(io::Error::erase)? as usize;
1558            let sector_offset = position % data.sector_size;
1559            let remaining = data.sector_size - sector_offset;
1560            if directory_record.size() > remaining {
1561                let padding = vec![0_u8; remaining];
1562                data.write_all(&padding).await?;
1563            }
1564            directory_record.write(&mut *data).await?;
1565        }
1566
1567        let end = data.pad_align_sector().await?;
1568        let size = (end.0 - start.0) * data.sector_size;
1569
1570        if size != expected.size {
1571            return Err(io::Error::new(
1572                io::ErrorKind::InvalidData,
1573                "directory size prediction did not match the written layout",
1574            ));
1575        }
1576
1577        if has_overflow {
1578            for record in records.iter() {
1579                if record.split.has_overflow() {
1580                    data.write_all(&record.split.overflow).await?;
1581                }
1582            }
1583        }
1584
1585        Ok(())
1586    }
1587}
1588} // io_transform!
1589
1590/// A failure of a [`FileSource`] reader, as an image error.
1591#[cfg(feature = "unstable-streaming")]
1592fn source_error(error: std::io::Error) -> io::Error {
1593    io::Error::<std::io::Error>::from(error).erase()
1594}
1595
1596#[cfg(test)]
1597mod tests {
1598    use super::*;
1599    use crate::{
1600        IsoImage,
1601        read::PathSeparator,
1602        write::options::{CreationFeatures, HybridBootOptions},
1603    };
1604    use alloc::{format, string::ToString, vec};
1605    use core::assert_eq;
1606    use std::io::Cursor;
1607
1608    const DIR_NAME_DOT: &[u8] = b"\x00";
1609    const DIR_NAME_DOTDOT: &[u8] = b"\x01";
1610
1611    #[test]
1612    fn should_reject_sector_counts_that_do_not_fit_a_partition_entry() {
1613        assert_eq!(
1614            checked_sector_count(u64::from(u32::MAX), "too large").unwrap(),
1615            u32::MAX
1616        );
1617        let error = checked_sector_count(u64::from(u32::MAX) + 1, "too large").unwrap_err();
1618        assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
1619    }
1620
1621    #[test]
1622    fn should_create_empty_iso_no_boot() {
1623        let input = InputFiles {
1624            path_separator: PathSeparator::ForwardSlash,
1625            files: vec![],
1626        };
1627        let options = IsoFormatOptions {
1628            volume_name: "EMPTY".to_string(),
1629            system_id: Some("SYSTEM".to_string()),
1630            volume_set_id: Some("VOL_SET_ID".to_string()),
1631            publisher_id: Some("PUBLISHER_ID".to_string()),
1632            preparer_id: Some("PREPARER_ID".to_string()),
1633            application_id: Some("APP_ID".to_string()),
1634            sector_size: 2048,
1635            path_separator: PathSeparator::ForwardSlash,
1636            features: CreationFeatures {
1637                ..Default::default()
1638            },
1639            strict_charset: false,
1640        };
1641
1642        let cursor = Cursor::new(vec![0u8; 512]);
1643        let mut output = IsoImageWriter::create(cursor, input, options).unwrap();
1644
1645        output
1646            .seek(SeekFrom::Start(0))
1647            .expect("Failed to verify ISO image");
1648        let image = IsoImage::open(output).expect("Failed to parse ISO image");
1649
1650        let pvd = image.read_pvd().expect("Failed to parse ISO image");
1651        assert_eq!(pvd.volume_identifier.to_str(), "EMPTY");
1652        assert_eq!(pvd.system_identifier.to_str(), "SYSTEM");
1653        assert_eq!(pvd.volume_set_identifier.to_str(), "VOL_SET_ID");
1654        assert_eq!(pvd.preparer_identifier.to_str(), "PREPARER_ID");
1655        assert_eq!(pvd.application_identifier.to_str(), "APP_ID");
1656
1657        assert!(pvd.dir_record.header.is_directory());
1658        assert_eq!(pvd.dir_record.header.extent.read(), 18);
1659        assert_eq!(pvd.dir_record.header.data_len.read(), 2048);
1660        assert_eq!(pvd.dir_record.header.file_identifier_len, 1);
1661        assert_eq!(pvd.dir_record.header.len, 34);
1662        assert_eq!(pvd.dir_record.header.volume_sequence_number.read(), 1);
1663        assert_eq!(pvd.dir_record.header.file_unit_size, 0);
1664        assert_eq!(pvd.dir_record.header.extended_attr_record, 0);
1665
1666        assert_eq!(image.info.block_size(), 2048);
1667
1668        let path_table = image.path_table().path_table;
1669        assert_eq!(path_table.lpt.0, 19);
1670        assert_eq!(path_table.mpt.0, 20);
1671        assert_eq!(path_table.size, 10);
1672
1673        let susp_info = image.info.susp_info;
1674        assert_eq!(susp_info.bytes_skipped, 0);
1675        assert!(!susp_info.detected);
1676        assert!(!susp_info.rrip_detected);
1677
1678        assert_eq!(image.root_dirs().iter().count(), 1);
1679
1680        let root_dir = image.root_dir();
1681        assert_eq!(root_dir.dir_ref().extent.0, 18);
1682        assert_eq!(root_dir.dir_ref().size, 2048);
1683        assert_eq!(
1684            root_dir.entry_type(),
1685            EntryType::Level1 {
1686                supports_lowercase: false,
1687                supports_rrip: false
1688            }
1689        );
1690
1691        let iso_dir = root_dir.iter(&image);
1692
1693        assert_eq!(iso_dir.entries().count(), 2);
1694        let mut entries = iso_dir.entries();
1695        let current_dir = entries
1696            .next()
1697            .unwrap()
1698            .expect("Failed to parse current dir");
1699        assert_eq!(current_dir.name(), DIR_NAME_DOT);
1700        let parent_dir = entries.next().unwrap().expect("Failed to parse parent dir");
1701        assert_eq!(parent_dir.name(), DIR_NAME_DOTDOT);
1702
1703        let buffer = image.into_inner().into_inner();
1704        assert_eq!(
1705            buffer.len() % 2048,
1706            0,
1707            "Image size must be multiple of 2048"
1708        );
1709        let sectors_count = buffer.len() / 2048;
1710        assert_eq!(sectors_count, 21);
1711    }
1712
1713    #[test]
1714    fn should_create_iso_with_file() {
1715        let input = InputFiles {
1716            path_separator: PathSeparator::ForwardSlash,
1717            files: vec![File::File {
1718                name: Arc::new("TESTFILE".into()),
1719                contents: vec![0; 999],
1720            }],
1721        };
1722        let options = IsoFormatOptions {
1723            volume_name: "EMPTY".to_string(),
1724            system_id: Some("SYSTEM".to_string()),
1725            volume_set_id: Some("VOL_SET_ID".to_string()),
1726            publisher_id: Some("PUBLISHER_ID".to_string()),
1727            preparer_id: Some("PREPARER_ID".to_string()),
1728            application_id: Some("APP_ID".to_string()),
1729            sector_size: 2048,
1730            path_separator: PathSeparator::ForwardSlash,
1731            features: CreationFeatures {
1732                ..Default::default()
1733            },
1734            strict_charset: false,
1735        };
1736
1737        let cursor = Cursor::new(vec![0u8; 512]);
1738        let mut output = IsoImageWriter::create(cursor, input, options).unwrap();
1739
1740        output
1741            .seek(SeekFrom::Start(0))
1742            .expect("Failed to verify ISO image");
1743        let image = IsoImage::open(output).expect("Failed to parse ISO image");
1744
1745        let root_dir = image.root_dir();
1746        let iso_dir = root_dir.iter(&image);
1747
1748        assert_eq!(iso_dir.entries().count(), 3);
1749        let mut entries = iso_dir.entries();
1750        entries.next().unwrap().expect("Failed to parse iso dir");
1751        entries.next().unwrap().expect("Failed to parse iso dir");
1752        let file = entries.next().unwrap().expect("Failed to parse iso file");
1753
1754        assert!(!file.is_directory());
1755        assert_eq!(file.display_name(), "TESTFILE;1");
1756        assert_eq!(file.size(), 44); // 33 + 1 + 10 = 44
1757        assert_eq!(file.total_size(), 999);
1758        assert!(file.additional_extents.is_empty());
1759        assert!(file.rrip.is_none());
1760    }
1761
1762    #[test]
1763    fn should_create_iso_with_multi_extent_file() {
1764        const SIZE: u64 = 4_294_967_296;
1765
1766        let input = InputTree {
1767            path_separator: PathSeparator::ForwardSlash,
1768            entries: vec![InputEntry {
1769                name: Arc::new("TESTFILE".into()),
1770                kind: InputEntryKind::TestFile { size: SIZE },
1771                metadata: InputMetadata::default(),
1772            }],
1773        };
1774        let options = IsoFormatOptions {
1775            volume_name: "EMPTY".to_string(),
1776            system_id: Some("SYSTEM".to_string()),
1777            volume_set_id: Some("VOL_SET_ID".to_string()),
1778            publisher_id: Some("PUBLISHER_ID".to_string()),
1779            preparer_id: Some("PREPARER_ID".to_string()),
1780            application_id: Some("APP_ID".to_string()),
1781            sector_size: 2048,
1782            path_separator: PathSeparator::ForwardSlash,
1783            features: CreationFeatures {
1784                ..Default::default()
1785            },
1786            strict_charset: false,
1787        };
1788
1789        let output = tempfile::tempfile().unwrap();
1790        let mut output = IsoImageWriter::create(output, input, options).unwrap();
1791
1792        output
1793            .seek(SeekFrom::Start(0))
1794            .expect("Failed to verify ISO image");
1795        let image = IsoImage::open(output).expect("Failed to parse ISO image");
1796
1797        let root_dir = image.root_dir();
1798        let iso_dir = root_dir.iter(&image);
1799
1800        let mut entries = iso_dir.entries();
1801        entries.next().unwrap().expect("Failed to parse iso dir");
1802        entries.next().unwrap().expect("Failed to parse iso dir");
1803        let mut file = entries.next().unwrap().expect("Failed to parse iso file");
1804
1805        assert!(!file.is_directory());
1806        assert_eq!(file.display_name(), "TESTFILE;1");
1807        assert_eq!(file.size(), 44); // 33 + 1 + 10 = 44
1808        assert_eq!(file.total_size(), SIZE);
1809        assert_eq!(file.additional_extents.len(), 1);
1810
1811        let expected_sector =
1812            file.header().extent.read() as usize + (4_294_965_248_u64 / 2048) as usize;
1813        let extent = file.additional_extents.drain(..).next().unwrap();
1814
1815        assert_eq!(extent.length, 2048);
1816        assert_eq!(extent.sector.0, expected_sector);
1817
1818        assert!(file.rrip.is_none());
1819    }
1820
1821    #[cfg(feature = "unstable-streaming")]
1822    mod streaming {
1823        use super::*;
1824
1825        /// A deterministic byte pattern of a given length, produced without holding it.
1826        struct PatternReader {
1827            position: u64,
1828            len: u64,
1829        }
1830
1831        impl PatternReader {
1832            fn byte_at(position: u64) -> u8 {
1833                (position % 251) as u8
1834            }
1835        }
1836
1837        impl std::io::Read for PatternReader {
1838            fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
1839                let remaining = usize::try_from(self.len - self.position).unwrap_or(usize::MAX);
1840                // Short reads exercise the writer's refill loop.
1841                let count = buf.len().min(remaining).min(1000);
1842                for (index, byte) in buf[..count].iter_mut().enumerate() {
1843                    *byte = Self::byte_at(self.position + index as u64);
1844                }
1845                self.position += count as u64;
1846                Ok(count)
1847            }
1848        }
1849
1850        fn single_file_tree(name: &str, kind: InputEntryKind) -> InputTree {
1851            InputTree {
1852                path_separator: PathSeparator::ForwardSlash,
1853                entries: vec![InputEntry {
1854                    name: Arc::new(name.into()),
1855                    kind,
1856                    metadata: InputMetadata::default(),
1857                }],
1858            }
1859        }
1860
1861        fn plain_options(volume_name: &str) -> IsoFormatOptions {
1862            IsoFormatOptions {
1863                volume_name: volume_name.to_string(),
1864                system_id: None,
1865                volume_set_id: None,
1866                publisher_id: None,
1867                preparer_id: None,
1868                application_id: None,
1869                sector_size: 2048,
1870                path_separator: PathSeparator::ForwardSlash,
1871                features: CreationFeatures::default(),
1872                strict_charset: false,
1873            }
1874        }
1875
1876        #[test]
1877        fn should_stream_file_source_contents_opened_once() {
1878            const LEN: u64 = 3 * 1024 * 1024 + 1;
1879            let opens = Arc::new(core::sync::atomic::AtomicUsize::new(0));
1880            let counter = Arc::clone(&opens);
1881            let source = FileSource::new(LEN, move || {
1882                counter.fetch_add(1, core::sync::atomic::Ordering::SeqCst);
1883                Ok(Box::new(PatternReader {
1884                    position: 0,
1885                    len: LEN,
1886                }))
1887            });
1888            assert_eq!(source.len(), LEN);
1889            assert!(!source.is_empty());
1890            let input = single_file_tree("STREAMED", InputEntryKind::Source(source));
1891
1892            let output = tempfile::tempfile().unwrap();
1893            let mut output =
1894                IsoImageWriter::create(output, input, plain_options("STREAM")).unwrap();
1895            assert_eq!(opens.load(core::sync::atomic::Ordering::SeqCst), 1);
1896
1897            output.seek(SeekFrom::Start(0)).unwrap();
1898            let image = IsoImage::open(output).expect("Failed to parse ISO image");
1899            let root_dir = image.root_dir();
1900            let iso_dir = root_dir.iter(&image);
1901            let file = iso_dir
1902                .entries()
1903                .nth(2)
1904                .unwrap()
1905                .expect("Failed to parse iso file");
1906            assert_eq!(file.display_name(), "STREAMED;1");
1907            assert_eq!(file.total_size(), LEN);
1908
1909            let contents = image
1910                .read_file(&file)
1911                .expect("Failed to read streamed file");
1912            assert_eq!(contents.len() as u64, LEN);
1913            assert!(
1914                contents
1915                    .iter()
1916                    .enumerate()
1917                    .all(|(index, byte)| *byte == PatternReader::byte_at(index as u64))
1918            );
1919        }
1920
1921        #[test]
1922        fn should_fail_when_file_source_ends_early() {
1923            let source = FileSource::new(5000, || Ok(Box::new(Cursor::new(vec![7_u8; 100]))));
1924            let input = single_file_tree("SHORT", InputEntryKind::Source(source));
1925
1926            let output = tempfile::tempfile().unwrap();
1927            let error = IsoImageWriter::create(output, input, plain_options("SHORT"))
1928                .expect_err("a short source must fail the write");
1929            assert!(
1930                matches!(error, IsoCreationError::Io(ref io) if io.kind() == io::ErrorKind::UnexpectedEof),
1931                "{error:?}"
1932            );
1933        }
1934
1935        #[test]
1936        fn should_stream_file_source_from_path() {
1937            let contents: Vec<u8> = (0..70_000_u32).map(|value| (value % 199) as u8).collect();
1938            let file = tempfile::NamedTempFile::new().unwrap();
1939            std::fs::write(file.path(), &contents).unwrap();
1940            let source = FileSource::from_path(file.path()).unwrap();
1941            assert_eq!(source.len(), contents.len() as u64);
1942            let input = single_file_tree("ONDISK", InputEntryKind::Source(source));
1943
1944            let output = tempfile::tempfile().unwrap();
1945            let mut output =
1946                IsoImageWriter::create(output, input, plain_options("ONDISK")).unwrap();
1947            output.seek(SeekFrom::Start(0)).unwrap();
1948            let image = IsoImage::open(output).expect("Failed to parse ISO image");
1949            let root_dir = image.root_dir();
1950            let iso_dir = root_dir.iter(&image);
1951            let entry = iso_dir
1952                .entries()
1953                .nth(2)
1954                .unwrap()
1955                .expect("Failed to parse iso file");
1956            assert_eq!(image.read_file(&entry).unwrap(), contents);
1957        }
1958    }
1959
1960    #[test]
1961    fn should_plan_multi_extent_record_across_directory_sector_boundary() {
1962        const SIZE: u64 = 4_294_967_296;
1963
1964        let mut entries = (0..44)
1965            .map(|index| InputEntry {
1966                name: Arc::new(format!("F{index:07}")),
1967                kind: InputEntryKind::File(Vec::new()),
1968                metadata: InputMetadata::default(),
1969            })
1970            .collect::<Vec<_>>();
1971        entries.push(InputEntry {
1972            name: Arc::new("MULTIEXT".into()),
1973            kind: InputEntryKind::TestFile { size: SIZE },
1974            metadata: InputMetadata::default(),
1975        });
1976
1977        let input = InputTree {
1978            path_separator: PathSeparator::ForwardSlash,
1979            entries,
1980        };
1981        let options = IsoFormatOptions {
1982            volume_name: "BOUNDARY".to_string(),
1983            system_id: None,
1984            volume_set_id: None,
1985            publisher_id: None,
1986            preparer_id: None,
1987            application_id: None,
1988            sector_size: 2048,
1989            path_separator: PathSeparator::ForwardSlash,
1990            features: CreationFeatures::default(),
1991            strict_charset: false,
1992        };
1993
1994        let output = tempfile::tempfile().unwrap();
1995        IsoImageWriter::create(output, input, options).unwrap();
1996    }
1997
1998    #[test]
1999    fn should_create_iso_with_mbr_boot() {
2000        let input = InputFiles {
2001            path_separator: PathSeparator::ForwardSlash,
2002            files: vec![File::File {
2003                name: Arc::new("boot.bin".into()),
2004                contents: vec![0x55; 2048], // Boot image
2005            }],
2006        };
2007        let options = IsoFormatOptions {
2008            volume_name: "MBR_TEST".to_string(),
2009            system_id: Some("SYSTEM".to_string()),
2010            volume_set_id: Some("VOL_SET_ID".to_string()),
2011            publisher_id: Some("PUBLISHER_ID".to_string()),
2012            preparer_id: Some("PREPARER_ID".to_string()),
2013            application_id: Some("APP_ID".to_string()),
2014            sector_size: 2048,
2015            path_separator: PathSeparator::ForwardSlash,
2016            features: CreationFeatures {
2017                hybrid_boot: Some(HybridBootOptions {
2018                    partition_scheme: PartitionScheme::Mbr,
2019                    mbr_bootstrap: Some(vec![0x00; 446]), // Bootstrap code
2020                    bootable: true,
2021                    efi_boot_partition: None,
2022                }),
2023                ..Default::default()
2024            },
2025            strict_charset: false,
2026        };
2027
2028        let cursor = Cursor::new(vec![0u8; 512]);
2029        let mut output = IsoImageWriter::create(cursor, input, options).unwrap();
2030
2031        output.seek(SeekFrom::Start(0)).expect("Failed to seek");
2032
2033        // Read MBR signature at offset 510-511 (0x55AA)
2034        let mut sig = [0u8; 2];
2035        output.seek(SeekFrom::Start(510)).expect("Failed to seek");
2036        output
2037            .read_exact(&mut sig)
2038            .expect("Failed to read MBR signature");
2039        assert_eq!(sig, [0x55, 0xAA], "MBR signature should be 0x55AA");
2040
2041        // Read partition table entry at offset 446
2042        let mut partition = [0u8; 16];
2043        output.seek(SeekFrom::Start(446)).expect("Failed to seek");
2044        output
2045            .read_exact(&mut partition)
2046            .expect("Failed to read partition entry");
2047
2048        // Boot indicator should be 0x80 (bootable)
2049        assert_eq!(partition[0], 0x80, "Partition should be bootable");
2050
2051        // Partition type should be 0x17 (ISO9660/Hidden NTFS)
2052        assert_eq!(partition[4], 0x17, "Partition type should be 0x17");
2053    }
2054
2055    #[test]
2056    fn should_create_iso_with_gpt_boot() {
2057        let input = InputFiles {
2058            path_separator: PathSeparator::ForwardSlash,
2059            files: vec![File::File {
2060                name: Arc::new("efi.img".into()),
2061                contents: vec![0xAA; 2048], // EFI boot image
2062            }],
2063        };
2064        let options = IsoFormatOptions {
2065            volume_name: "GPT_TEST".to_string(),
2066            system_id: Some("SYSTEM".to_string()),
2067            volume_set_id: Some("VOL_SET_ID".to_string()),
2068            publisher_id: Some("PUBLISHER_ID".to_string()),
2069            preparer_id: Some("PREPARER_ID".to_string()),
2070            application_id: Some("APP_ID".to_string()),
2071            sector_size: 2048,
2072            path_separator: PathSeparator::ForwardSlash,
2073            features: CreationFeatures {
2074                hybrid_boot: Some(HybridBootOptions {
2075                    partition_scheme: PartitionScheme::Gpt,
2076                    mbr_bootstrap: None,
2077                    bootable: false,
2078                    efi_boot_partition: Some("efi.img".to_string()),
2079                }),
2080                ..Default::default()
2081            },
2082            strict_charset: false,
2083        };
2084
2085        let cursor = Cursor::new(vec![0u8; 512]);
2086        let mut output = IsoImageWriter::create(cursor, input, options).unwrap();
2087
2088        output.seek(SeekFrom::Start(0)).expect("Failed to seek");
2089
2090        // Read protective MBR signature at offset 510-511
2091        let mut sig = [0u8; 2];
2092        output.seek(SeekFrom::Start(510)).expect("Failed to seek");
2093        output
2094            .read_exact(&mut sig)
2095            .expect("Failed to read MBR signature");
2096        assert_eq!(sig, [0x55, 0xAA], "MBR signature should be 0x55AA");
2097
2098        // Read GPT header at sector 1 (LBA 1)
2099        let mut header = [0u8; 92];
2100        output.seek(SeekFrom::Start(512)).expect("Failed to seek"); // 512-byte sectors
2101        output
2102            .read_exact(&mut header)
2103            .expect("Failed to read GPT header");
2104
2105        // GPT signature is "EFI PART"
2106        assert_eq!(
2107            &header[0..8],
2108            b"EFI PART",
2109            "GPT header signature should be 'EFI PART'"
2110        );
2111
2112        // Check revision (1.0)
2113        assert_eq!(
2114            header[8..12],
2115            [0x00, 0x00, 0x01, 0x00],
2116            "GPT revision should be 1.0"
2117        );
2118    }
2119
2120    #[test]
2121    fn should_create_iso_with_hybrid_mbr_gpt_boot() {
2122        let input = InputFiles {
2123            path_separator: PathSeparator::ForwardSlash,
2124            files: vec![
2125                File::File {
2126                    name: Arc::new("boot.bin".into()),
2127                    contents: vec![0x55; 2048], // BIOS boot image
2128                },
2129                File::File {
2130                    name: Arc::new("efi.img".into()),
2131                    contents: vec![0xAA; 2048], // EFI boot image
2132                },
2133            ],
2134        };
2135        let options = IsoFormatOptions {
2136            volume_name: "HYBRID_TEST".to_string(),
2137            system_id: Some("SYSTEM".to_string()),
2138            volume_set_id: Some("VOL_SET_ID".to_string()),
2139            publisher_id: Some("PUBLISHER_ID".to_string()),
2140            preparer_id: Some("PREPARER_ID".to_string()),
2141            application_id: Some("APP_ID".to_string()),
2142            sector_size: 2048,
2143            path_separator: PathSeparator::ForwardSlash,
2144            features: CreationFeatures {
2145                hybrid_boot: Some(HybridBootOptions {
2146                    partition_scheme: PartitionScheme::Hybrid,
2147                    mbr_bootstrap: Some(vec![0x00; 446]),
2148                    bootable: true,
2149                    efi_boot_partition: Some("efi.img".to_string()),
2150                }),
2151                ..Default::default()
2152            },
2153            strict_charset: false,
2154        };
2155
2156        let cursor = Cursor::new(vec![0u8; 512]);
2157        let mut output = IsoImageWriter::create(cursor, input, options).unwrap();
2158
2159        output.seek(SeekFrom::Start(0)).expect("Failed to seek");
2160
2161        // 1. Check MBR signature
2162        let mut sig = [0u8; 2];
2163        output.seek(SeekFrom::Start(510)).expect("Failed to seek");
2164        output
2165            .read_exact(&mut sig)
2166            .expect("Failed to read MBR signature");
2167        assert_eq!(sig, [0x55, 0xAA], "MBR signature should be 0x55AA");
2168
2169        // 2. Check partition table entries in MBR
2170        let mut found_iso = false;
2171        let mut found_bootable = false;
2172        let mut found_esp = false;
2173
2174        for slot in 0..4 {
2175            let offset = 446 + (slot * 16);
2176            let mut partition = [0u8; 16];
2177            output
2178                .seek(SeekFrom::Start(offset))
2179                .expect("Failed to seek");
2180            output
2181                .read_exact(&mut partition)
2182                .expect("Failed to read partition");
2183
2184            // Slot 0 is usually protective MBR (type 0xEE)
2185            if partition[4] == 0xEE {
2186                continue; // Skip protective MBR
2187            }
2188
2189            // Check for ISO9660 partition (type 0x17)
2190            if partition[4] == 0x17 {
2191                found_iso = true;
2192                if partition[0] == 0x80 {
2193                    found_bootable = true;
2194                }
2195            }
2196
2197            // Check for EFI System Partition (type 0xEF)
2198            if partition[4] == 0xEF {
2199                found_esp = true;
2200            }
2201        }
2202
2203        assert!(found_iso, "ISO9660 partition not found in MBR");
2204        assert!(found_bootable, "ISO9660 partition should be bootable");
2205        assert!(found_esp, "EFI System Partition not found in MBR");
2206
2207        // 3. Check GPT header (dual-boot)
2208        let mut header = [0u8; 92];
2209        output.seek(SeekFrom::Start(512)).expect("Failed to seek"); // 512-byte sectors
2210        output
2211            .read_exact(&mut header)
2212            .expect("Failed to read GPT header");
2213        assert_eq!(
2214            &header[0..8],
2215            b"EFI PART",
2216            "GPT header signature should be 'EFI PART'"
2217        );
2218
2219        // Check revision (1.0)
2220        assert_eq!(
2221            header[8..12],
2222            [0x00, 0x00, 0x01, 0x00],
2223            "GPT revision should be 1.0"
2224        );
2225
2226        // 4. Check GPT partition entries
2227        // Partition type GUID for basic data (ISO9660)
2228        let basic_data_guid = [
2229            0xA2, 0xA0, 0xD0, 0xEB, 0xE5, 0xB9, 0x33, 0x44, 0x87, 0xC0, 0x68, 0xB6, 0xB7, 0x26,
2230            0x99, 0xC7,
2231        ];
2232
2233        // Partition type GUID for EFI System
2234        let esp_guid = [
2235            0x28, 0x73, 0x2A, 0xC1, 0x1F, 0xF8, 0xD2, 0x11, 0xBA, 0x4B, 0x00, 0xA0, 0xC9, 0x3E,
2236            0xC9, 0x3B,
2237        ];
2238
2239        let mut found_gpt_iso = false;
2240        let mut found_gpt_esp = false;
2241
2242        // GPT entries start at LBA 2 (1024 bytes offset in 512-byte sectors)
2243        for i in 0..4 {
2244            let offset = 1024 + (i * 128); // Each entry is 128 bytes
2245            let mut entry = [0u8; 128];
2246            output
2247                .seek(SeekFrom::Start(offset))
2248                .expect("Failed to seek");
2249            output
2250                .read_exact(&mut entry)
2251                .expect("Failed to read GPT partition");
2252
2253            // Check if entry is non-zero (has a GUID)
2254            if entry[0..16] == [0u8; 16] {
2255                continue; // Empty entry
2256            }
2257
2258            if entry[0..16] == basic_data_guid {
2259                found_gpt_iso = true;
2260            }
2261            if entry[0..16] == esp_guid {
2262                found_gpt_esp = true;
2263            }
2264        }
2265
2266        assert!(found_gpt_iso, "ISO9660 partition not found in GPT");
2267        assert!(found_gpt_esp, "EFI System Partition not found in GPT");
2268    }
2269}