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