1use alloc::{collections::BTreeMap, sync::Arc};
2
3mod types;
4mod utils;
5
6pub mod estimator;
7pub 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
46pub mod options;
48use options::IsoFormatOptions;
49
50#[derive(Debug, thiserror::Error)]
51pub enum FileConversionError {
53 #[error("I/O error: {0}")]
54 Io(#[from] std::io::Error),
56 #[error("Path {0:?} is not a valid UTF-8 string")]
57 InvalidUtf8Path(std::path::PathBuf),
59 #[error("Unsupported filesystem entry type at {0:?}")]
60 UnsupportedFileType(std::path::PathBuf),
62}
63
64#[derive(Debug, thiserror::Error)]
65pub enum IsoCreationError {
67 #[error(transparent)]
68 Io(#[from] io::Error),
70}
71
72pub type Error = IsoCreationError;
74pub type Result<T> = core::result::Result<T, Error>;
76
77pub 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 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 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 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 #[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 async fn write_partition_tables(&mut self, end_sector: LogicalSector) -> io::Result<()> {
1074 match self.ops.partition_scheme() {
1075 None | Some(PartitionScheme::None) => {
1076 }
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 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 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 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 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 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 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 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 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 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} #[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); 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); 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 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 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], }],
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]), 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 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 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 assert_eq!(partition[0], 0x80, "Partition should be bootable");
2050
2051 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], }],
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 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 let mut header = [0u8; 92];
2100 output.seek(SeekFrom::Start(512)).expect("Failed to seek"); output
2102 .read_exact(&mut header)
2103 .expect("Failed to read GPT header");
2104
2105 assert_eq!(
2107 &header[0..8],
2108 b"EFI PART",
2109 "GPT header signature should be 'EFI PART'"
2110 );
2111
2112 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], },
2129 File::File {
2130 name: Arc::new("efi.img".into()),
2131 contents: vec![0xAA; 2048], },
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 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 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 if partition[4] == 0xEE {
2186 continue; }
2188
2189 if partition[4] == 0x17 {
2191 found_iso = true;
2192 if partition[0] == 0x80 {
2193 found_bootable = true;
2194 }
2195 }
2196
2197 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 let mut header = [0u8; 92];
2209 output.seek(SeekFrom::Start(512)).expect("Failed to seek"); 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 assert_eq!(
2221 header[8..12],
2222 [0x00, 0x00, 0x01, 0x00],
2223 "GPT revision should be 1.0"
2224 );
2225
2226 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 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 for i in 0..4 {
2244 let offset = 1024 + (i * 128); 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 if entry[0..16] == [0u8; 16] {
2255 continue; }
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}