1#[cfg(feature = "unstable-streaming")]
2use alloc::boxed::Box;
3use core::{fmt, ops::Deref};
4use std::{fs::FileType, path::PathBuf};
5
6use alloc::{
7 collections::{BTreeMap, BTreeSet, VecDeque},
8 string::{String, ToString},
9 sync::Arc,
10 vec::Vec,
11};
12
13use super::FileConversionError;
14use crate::{
15 directory::{DirectoryRef, FileFlags},
16 file::{ConvertedName, EntryType},
17 io::{self},
18 read::PathSeparator,
19 rrip::RripOptions,
20 susp::SplitSu,
21 write::{utils::*, writer::*},
22};
23
24pub type FileOrder = (DirectoryId, usize);
26
27pub type RelocationMap = BTreeMap<(usize, EntryType), DirectoryRef>;
29
30pub type RripTime = [u8; 7];
32
33pub struct InputFiles {
39 pub path_separator: PathSeparator,
41 pub files: Vec<File>,
43}
44
45#[derive(Clone, PartialEq, Eq)]
46pub enum File {
48 File {
50 name: Arc<String>,
52 contents: Vec<u8>,
54 },
55 Directory {
57 name: Arc<String>,
59 children: Vec<File>,
61 },
62}
63
64impl core::fmt::Debug for File {
65 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66 let mut dbg = f.debug_struct("File");
67 match self {
68 Self::Directory { name, children } => {
69 dbg.field("name", name);
70 dbg.field("children", children);
71 }
72 Self::File { name, contents } => {
73 dbg.field("name", name);
74 dbg.field("data_len", &contents.len());
75 }
76 }
77 dbg.finish()
78 }
79}
80
81impl File {
82 pub fn name(&self) -> Arc<String> {
84 match self {
85 File::File { name, .. } => name.clone(),
86 File::Directory { name, .. } => name.clone(),
87 }
88 }
89}
90
91#[derive(Debug, Clone, PartialEq, Eq)]
93pub struct InputTree {
94 pub path_separator: PathSeparator,
96 pub entries: Vec<InputEntry>,
98}
99
100impl InputTree {
101 pub(crate) fn validate(&self, rrip: Option<&RripOptions>) -> crate::io::Result<()> {
110 Self::visit(&self.entries, rrip, 1, 0)
111 }
112
113 fn visit(
114 entries: &[InputEntry],
115 rrip: Option<&RripOptions>,
116 depth: usize,
117 path_len: usize,
118 ) -> io::Result<()> {
119 for entry in entries {
120 match &entry.kind {
121 InputEntryKind::Directory(children) => {
122 let child_path_len = if path_len == 0 {
123 entry.name.len()
124 } else {
125 path_len + 1 + entry.name.len()
126 };
127 if (depth >= 8 || child_path_len > 255)
128 && !rrip
129 .is_some_and(|options| options.enabled && options.relocate_deep_dirs)
130 {
131 return Err(io::Error::new(
132 io::ErrorKind::InvalidInput,
133 "directory depth or path length exceeds ISO 9660 limits and RRIP relocation is disabled",
134 ));
135 }
136
137 Self::visit(children, rrip, depth + 1, child_path_len)?;
138 }
139 InputEntryKind::Symlink(_) => {
140 if !rrip.is_some_and(|options| options.enabled && options.preserve_symlinks) {
141 return Err(io::Error::new(
142 io::ErrorKind::InvalidInput,
143 "symbolic links require RRIP preserve_symlinks",
144 ));
145 }
146 }
147 InputEntryKind::CharacterDevice { .. } | InputEntryKind::BlockDevice { .. } => {
148 if !rrip.is_some_and(|options| options.enabled && options.preserve_devices) {
149 return Err(io::Error::new(
150 io::ErrorKind::InvalidInput,
151 "device entries require RRIP preserve_devices",
152 ));
153 }
154 }
155 InputEntryKind::File(_contents) => {
156 }
164 #[cfg(feature = "unstable-streaming")]
165 InputEntryKind::Source(_) => {}
166 #[cfg(test)]
167 InputEntryKind::TestFile { .. } => {}
168 }
169 }
170 Ok(())
171 }
172}
173
174#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
176pub struct InputMetadata {
177 pub mode: Option<u32>,
179 pub uid: Option<u32>,
181 pub gid: Option<u32>,
183 pub created: Option<i64>,
185 pub modified: Option<i64>,
187 pub accessed: Option<i64>,
189}
190
191impl InputMetadata {
192 pub(crate) fn from_fs(fs_metadata: std::fs::Metadata) -> Self {
194 #[allow(unused_mut)]
195 let mut metadata = Self {
196 created: system_time_seconds(fs_metadata.created()),
197 modified: system_time_seconds(fs_metadata.modified()),
198 accessed: system_time_seconds(fs_metadata.accessed()),
199 ..Self::default()
200 };
201
202 #[cfg(unix)]
203 {
204 use std::os::unix::fs::MetadataExt;
205 metadata.mode = Some(fs_metadata.mode() & 0o7777);
206 metadata.uid = Some(fs_metadata.uid());
207 metadata.gid = Some(fs_metadata.gid());
208 }
209
210 metadata
211 }
212}
213
214#[cfg(feature = "unstable-streaming")]
232#[cfg_attr(docsrs, doc(cfg(feature = "unstable-streaming")))]
233#[derive(Clone)]
234pub struct FileSource {
235 len: u64,
236 open:
237 alloc::sync::Arc<dyn Fn() -> std::io::Result<Box<dyn std::io::Read + Send>> + Send + Sync>,
238}
239
240#[cfg(feature = "unstable-streaming")]
241impl FileSource {
242 pub fn new<F>(len: u64, open: F) -> Self
244 where
245 F: Fn() -> std::io::Result<Box<dyn std::io::Read + Send>> + Send + Sync + 'static,
246 {
247 Self {
248 len,
249 open: alloc::sync::Arc::new(open),
250 }
251 }
252
253 pub fn from_path(path: impl Into<PathBuf>) -> std::io::Result<Self> {
274 let path = path.into();
275 let len = std::fs::metadata(&path)?.len();
276 Ok(Self::new(len, move || {
277 std::fs::File::open(&path).map(|file| Box::new(file) as Box<dyn std::io::Read + Send>)
278 }))
279 }
280
281 pub const fn len(&self) -> u64 {
283 self.len
284 }
285
286 pub const fn is_empty(&self) -> bool {
288 self.len == 0
289 }
290
291 pub fn open(&self) -> std::io::Result<Box<dyn std::io::Read + Send>> {
293 (self.open)()
294 }
295}
296
297#[cfg(feature = "unstable-streaming")]
298impl fmt::Debug for FileSource {
299 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
300 f.debug_struct("FileSource")
301 .field("len", &self.len)
302 .finish_non_exhaustive()
303 }
304}
305
306#[cfg(feature = "unstable-streaming")]
307impl PartialEq for FileSource {
308 fn eq(&self, other: &Self) -> bool {
310 self.len == other.len && alloc::sync::Arc::ptr_eq(&self.open, &other.open)
311 }
312}
313
314#[cfg(feature = "unstable-streaming")]
315impl Eq for FileSource {}
316
317#[derive(Debug, Clone, PartialEq, Eq)]
323pub enum InputEntryKind {
324 File(Vec<u8>),
326 #[cfg(feature = "unstable-streaming")]
331 #[cfg_attr(docsrs, doc(cfg(feature = "unstable-streaming")))]
332 Source(FileSource),
333 #[cfg(test)]
335 TestFile {
336 size: u64,
338 },
339 Directory(Vec<InputEntry>),
341 Symlink(String),
343 CharacterDevice {
345 major: u32,
347 minor: u32,
349 },
350 BlockDevice {
352 major: u32,
354 minor: u32,
356 },
357}
358
359impl InputEntryKind {
360 pub(crate) fn file_len(&self) -> Option<u64> {
361 match self {
362 Self::File(contents) => Some(contents.len() as u64),
363 #[cfg(feature = "unstable-streaming")]
364 Self::Source(source) => Some(source.len()),
365 #[cfg(test)]
366 Self::TestFile { size } => Some(*size),
367 _ => None,
368 }
369 }
370
371 pub(crate) fn new(
379 file_type: FileType,
380 path: PathBuf,
381 ) -> core::result::Result<Self, FileConversionError> {
382 if file_type.is_file() {
383 let content = std::fs::read(&path)?;
384 Ok(InputEntryKind::File(content))
385 } else if file_type.is_dir() {
386 let dir = read_input_directory_recursively(&path)?;
387 Ok(InputEntryKind::Directory(dir))
388 } else if file_type.is_symlink() {
389 let target = std::fs::read_link(&path)?;
390 let symlink = target
391 .to_str()
392 .ok_or_else(|| FileConversionError::InvalidUtf8Path(target.clone()))?
393 .to_string();
394 Ok(InputEntryKind::Symlink(symlink))
395 } else {
396 #[cfg(unix)]
397 {
398 use std::os::unix::fs::{FileTypeExt, MetadataExt};
399 const DEV_MAJOR_MASK_LOW: u64 = 0xfff;
400 const DEV_MAJOR_MASK_HIGH: u64 = 0xfffff000;
401 const DEV_MINOR_MASK_LOW: u64 = 0xff;
402 const DEV_MINOR_MASK_HIGH: u64 = 0xffffff00;
403
404 let fs_metadata = std::fs::symlink_metadata(&path)?;
405 let device = fs_metadata.rdev();
406 let major =
407 ((device >> 8) & DEV_MAJOR_MASK_LOW) | ((device >> 32) & DEV_MAJOR_MASK_HIGH);
408 let minor = (device & DEV_MINOR_MASK_LOW) | ((device >> 12) & DEV_MINOR_MASK_HIGH);
409 if file_type.is_char_device() {
410 Ok(InputEntryKind::CharacterDevice {
411 major: major as u32,
412 minor: minor as u32,
413 })
414 } else if file_type.is_block_device() {
415 Ok(InputEntryKind::BlockDevice {
416 major: major as u32,
417 minor: minor as u32,
418 })
419 } else {
420 Err(FileConversionError::UnsupportedFileType(path))
421 }
422 }
423 #[cfg(not(unix))]
424 return Err(FileConversionError::UnsupportedFileType(path));
425 }
426 }
427}
428
429#[derive(Debug, Clone, PartialEq, Eq)]
434pub struct InputEntry {
435 pub name: Arc<String>,
437 pub kind: InputEntryKind,
439 pub metadata: InputMetadata,
441}
442
443impl InputEntry {
444 pub fn file(name: impl Into<String>, contents: impl Into<Vec<u8>>) -> Self {
446 Self::new(name, InputEntryKind::File(contents.into()))
447 }
448
449 pub fn directory(name: impl Into<String>, children: Vec<Self>) -> Self {
451 Self::new(name, InputEntryKind::Directory(children))
452 }
453
454 pub fn symlink(name: impl Into<String>, target: impl Into<String>) -> Self {
456 Self::new(name, InputEntryKind::Symlink(target.into()))
457 }
458
459 pub fn character_device(name: impl Into<String>, major: u32, minor: u32) -> Self {
461 Self::new(name, InputEntryKind::CharacterDevice { major, minor })
462 }
463
464 pub fn block_device(name: impl Into<String>, major: u32, minor: u32) -> Self {
466 Self::new(name, InputEntryKind::BlockDevice { major, minor })
467 }
468
469 pub fn with_metadata(mut self, metadata: InputMetadata) -> Self {
471 self.metadata = metadata;
472 self
473 }
474
475 pub fn name(&self) -> Arc<String> {
477 self.name.clone()
478 }
479
480 fn new(name: impl Into<String>, kind: InputEntryKind) -> Self {
481 Self {
482 name: Arc::new(name.into()),
483 kind,
484 metadata: InputMetadata::default(),
485 }
486 }
487}
488
489impl InputTree {
490 pub fn new(path_separator: PathSeparator, entries: Vec<InputEntry>) -> Self {
492 Self {
493 path_separator,
494 entries,
495 }
496 }
497
498 pub fn from_fs(
500 root_path: &std::path::Path,
501 path_separator: PathSeparator,
502 ) -> core::result::Result<Self, FileConversionError> {
503 if !root_path.is_dir() {
504 return Err(FileConversionError::Io(std::io::Error::new(
505 std::io::ErrorKind::InvalidInput,
506 alloc::format!("Root path '{root_path:?}' is not a directory"),
507 )));
508 }
509 Ok(Self::new(
510 path_separator,
511 read_input_directory_recursively(root_path)?,
512 ))
513 }
514}
515
516impl From<InputFiles> for InputTree {
517 fn from(value: InputFiles) -> Self {
518 fn convert(file: File) -> InputEntry {
519 match file {
520 File::File { name, contents } => InputEntry {
521 name,
522 kind: InputEntryKind::File(contents),
523 metadata: InputMetadata::default(),
524 },
525 File::Directory { name, children } => InputEntry {
526 name,
527 kind: InputEntryKind::Directory(children.into_iter().map(convert).collect()),
528 metadata: InputMetadata::default(),
529 },
530 }
531 }
532 Self::new(
533 value.path_separator,
534 value.files.into_iter().map(convert).collect(),
535 )
536 }
537}
538
539pub struct FileTreeWalker<'a>(VecDeque<StackFrame<'a>>);
541
542enum StackFrame<'a> {
544 Node(&'a InputEntry),
545 DirExit(&'a InputEntry),
546}
547
548#[derive(Debug, PartialEq, Eq)]
550pub enum TreeWalkerItem<'a> {
551 EnterDirectory(&'a InputEntry),
553 File(&'a InputEntry),
555 ExitDirectory(&'a InputEntry),
557}
558
559impl<'a> FileTreeWalker<'a> {
560 pub fn new(input: &'a InputTree) -> Self {
562 let mut stack = VecDeque::new();
563 for file in input.entries.iter().rev() {
564 stack.push_back(StackFrame::Node(file));
565 }
566 FileTreeWalker(stack)
567 }
568
569 pub fn walk(self, written_files: &mut WrittenFiles) {
571 let mut next_directory_id = 1usize;
572 let mut current_dir = written_files.root_dir();
573
574 for file in self {
575 match file {
576 TreeWalkerItem::EnterDirectory(dir) => {
577 let name = dir.name();
578 let metadata = dir.metadata;
579 let written_dir = written_files.get_mut(¤t_dir);
580 let index = written_dir.push_dir(name, metadata);
581 written_dir.dirs[index].id = next_directory_id;
582 next_directory_id += 1;
583 current_dir.push(index);
584 }
585 TreeWalkerItem::ExitDirectory(_dir) => {
586 current_dir.pop();
587 }
588 TreeWalkerItem::File(file) => {
589 let dir = written_files.get_mut(¤t_dir);
593 dir.push_file(WrittenFile::new(
594 file.name.clone(),
595 file.kind.clone(),
596 file.metadata,
597 ));
598 }
599 };
600 }
601 }
602}
603
604impl<'a> Iterator for FileTreeWalker<'a> {
605 type Item = TreeWalkerItem<'a>;
606
607 fn next(&mut self) -> Option<Self::Item> {
608 let frame = self.0.pop_back()?;
609 match frame {
610 StackFrame::Node(file) => match &file.kind {
611 InputEntryKind::Directory(children) => {
612 let current_dir = file;
614
615 self.0.push_back(StackFrame::DirExit(current_dir));
617
618 for child in children.iter().rev() {
620 self.0.push_back(StackFrame::Node(child));
621 }
622
623 Some(TreeWalkerItem::EnterDirectory(current_dir))
624 }
625 _ => Some(TreeWalkerItem::File(file)),
626 },
627 StackFrame::DirExit(dir) => Some(TreeWalkerItem::ExitDirectory(dir)),
628 }
629 }
630}
631
632pub enum RripEntryKind<'a> {
634 RootDot {
636 metadata: InputMetadata,
638 nlink: u32,
640 },
641 RootDotDot {
643 metadata: InputMetadata,
645 nlink: u32,
647 },
648 Dot {
650 metadata: InputMetadata,
652 nlink: u32,
654 },
655 DotDot {
657 metadata: InputMetadata,
659 nlink: u32,
661 },
662 Directory {
664 original_name: &'a str,
666 metadata: InputMetadata,
668 nlink: u32,
670 },
671 Entry {
673 original_name: &'a str,
675 metadata: InputMetadata,
677 kind: &'a InputEntryKind,
679 },
680}
681
682pub struct PendingRecords(Vec<PendingRecord>);
712
713impl PendingRecords {
714 #[allow(clippy::too_many_arguments)]
721 pub fn new(
722 ty: EntryType,
723 dir: &WrittenDirectory,
724 is_root: bool,
725 inode_counter: &mut u32,
726 rrip_options: Option<&RripOptions>,
727 fallback_time: &RripTime,
728 relocation_refs: &BTreeMap<(usize, EntryType), DirectoryRef>,
729 sector_size: u64,
730 ) -> io::Result<Self> {
731 let rrip_options = rrip_options.filter(|options| options.enabled);
732 let has_rrip = ty.supports_rrip() && rrip_options.is_some();
733 let options = rrip_options.copied().unwrap_or_else(RripOptions::disabled);
734 let directory_nlink = 2 + dir.dirs.len() as u32;
735
736 let mut records: Vec<PendingRecord> = Vec::new();
737
738 records.push(Self::build_dot_record(
739 has_rrip,
740 is_root,
741 &dir.metadata,
742 directory_nlink,
743 &options,
744 fallback_time,
745 ));
746 records.push(Self::build_dotdot_record(
747 has_rrip,
748 is_root,
749 dir,
750 ty,
751 &options,
752 fallback_time,
753 relocation_refs,
754 )?);
755
756 Self::build_directory_records(
757 &mut records,
758 &dir.dirs,
759 ty,
760 has_rrip,
761 &options,
762 inode_counter,
763 fallback_time,
764 relocation_refs,
765 )?;
766
767 Self::build_file_records(
768 &mut records,
769 &dir.files,
770 ty,
771 has_rrip,
772 &options,
773 inode_counter,
774 fallback_time,
775 sector_size,
776 )?;
777
778 Self::deduplicate_names(&mut records, ty);
779 Self::sort_records(&mut records);
780
781 Ok(Self(records))
782 }
783
784 fn build_dot_record(
785 has_rrip: bool,
786 is_root: bool,
787 metadata: &InputMetadata,
788 nlink: u32,
789 options: &RripOptions,
790 fallback_time: &RripTime,
791 ) -> PendingRecord {
792 let split = if has_rrip {
793 let kind = if is_root {
794 RripEntryKind::RootDot {
795 metadata: *metadata,
796 nlink,
797 }
798 } else {
799 RripEntryKind::Dot {
800 metadata: *metadata,
801 nlink,
802 }
803 };
804 let max = available_su_space(1); build_rrip_entries(kind, 0, options, fallback_time).build_split(max)
806 } else {
807 SplitSu::empty()
808 };
809 PendingRecord::current_dir(split)
810 }
811
812 fn build_dotdot_record(
813 has_rrip: bool,
814 is_root: bool,
815 dir: &WrittenDirectory,
816 ty: EntryType,
817 options: &RripOptions,
818 fallback_time: &RripTime,
819 relocation_refs: &BTreeMap<(usize, EntryType), DirectoryRef>,
820 ) -> io::Result<PendingRecord> {
821 let split = if has_rrip {
822 let kind = if is_root {
823 RripEntryKind::RootDotDot {
824 metadata: dir.metadata,
825 nlink: 2 + dir.dirs.len() as u32,
826 }
827 } else {
828 RripEntryKind::DotDot {
829 metadata: dir.metadata,
830 nlink: 2 + dir.dirs.len() as u32,
831 }
832 };
833 let max = available_su_space(1); let mut builder = build_rrip_entries(kind, 0, options, fallback_time);
835 if let DirectoryRelocation::Moved { logical_parent, .. } = dir.relocation {
836 let parent = relocation_refs
837 .get(&(logical_parent, ty))
838 .copied()
839 .unwrap_or_default();
840 builder.add_pl(parent.extent.0 as u32);
841 }
842 builder.build_split(max)
843 } else {
844 SplitSu::empty()
845 };
846 Ok(PendingRecord::parent_dir(split))
847 }
848
849 #[allow(clippy::too_many_arguments)]
850 fn build_directory_records(
851 records: &mut Vec<PendingRecord>,
852 dirs: &[WrittenDirectory],
853 ty: EntryType,
854 has_rrip: bool,
855 options: &RripOptions,
856 inode_counter: &mut u32,
857 fallback_time: &RripTime,
858 relocation_refs: &BTreeMap<(usize, EntryType), DirectoryRef>,
859 ) -> io::Result<()> {
860 for directory in dirs {
861 let converted_name = ty.convert_directory_name(&directory.name);
862 let split = if has_rrip {
863 let inode = *inode_counter;
864 *inode_counter += 1;
865 let max = available_su_space(converted_name.as_bytes().len());
866 let mut builder = build_rrip_entries(
867 RripEntryKind::Directory {
868 original_name: &directory.rrip_name,
869 metadata: directory.metadata,
870 nlink: 2 + directory.dirs.len() as u32,
871 },
872 inode,
873 options,
874 fallback_time,
875 );
876 match directory.relocation {
877 DirectoryRelocation::Placeholder { target } => {
878 let target = relocation_refs.get(&(target, ty)).ok_or_else(|| {
879 io::Error::new(
880 io::ErrorKind::InvalidData,
881 "relocated directory extent was not written",
882 )
883 })?;
884 builder.add_cl(target.extent.0 as u32);
885 }
886 DirectoryRelocation::Moved { .. } => {
887 builder.add_re();
888 }
889 DirectoryRelocation::None => {}
890 }
891 builder.build_split(max)
892 } else {
893 SplitSu::empty()
894 };
895
896 let dir_ref = directory.get_dir_ref(ty, relocation_refs);
897 let flags = if has_rrip
898 && matches!(
899 directory.relocation,
900 DirectoryRelocation::Placeholder { .. }
901 ) {
902 FileFlags::empty()
903 } else {
904 FileFlags::DIRECTORY
905 };
906 let record = PendingRecord::new(&converted_name, split, dir_ref, flags);
907
908 records.push(record);
909 }
910 Ok(())
911 }
912
913 #[allow(clippy::too_many_arguments)]
914 fn build_file_records(
915 records: &mut Vec<PendingRecord>,
916 files: &[WrittenFile],
917 ty: EntryType,
918 has_rrip: bool,
919 options: &RripOptions,
920 inode_counter: &mut u32,
921 fallback_time: &RripTime,
922 sector_size: u64,
923 ) -> io::Result<()> {
924 for file in files {
925 let converted_name = ty.convert_name(&file.name);
926
927 let split = if has_rrip {
929 let inode = *inode_counter;
930 *inode_counter += 1;
931 let max = available_su_space(converted_name.as_bytes().len());
932 let kind = RripEntryKind::Entry {
933 original_name: &file.name,
934 metadata: file.metadata,
935 kind: &file.kind,
936 };
937 build_rrip_entries(kind, inode, options, fallback_time).build_split(max)
938 } else {
939 SplitSu::empty()
940 };
941
942 let len = file.kind.file_len().unwrap_or(file.entry.size as u64);
943 let cursor = file.entry.extent.0 as u64 * sector_size;
944 let mut additional_extents =
945 ExtentIter::new(len, cursor, sector_size).skip(1).peekable();
946 let first_flags = if additional_extents.peek().is_none() {
947 FileFlags::empty()
948 } else {
949 FileFlags::NOT_FINAL
950 };
951
952 let first = PendingRecord::new(&converted_name, split, file.entry, first_flags);
953 records.push(first);
954
955 while let Some((dir_ref, _)) = additional_extents.next() {
956 let flags = if additional_extents.peek().is_none() {
957 FileFlags::empty()
958 } else {
959 FileFlags::NOT_FINAL
960 };
961
962 let record = PendingRecord::new(&converted_name, SplitSu::empty(), dir_ref, flags);
963 records.push(record);
964 }
965 }
966 Ok(())
967 }
968
969 fn deduplicate_names(records: &mut [PendingRecord], ty: EntryType) {
970 let mut seen: BTreeSet<PendingRecordName> = BTreeSet::new();
971 let mut start = 0;
972
973 while start < records.len() {
974 if records[start].name.len() == 1
975 && (records[start].name[0] == 0x00 || records[start].name[0] == 0x01)
976 {
977 start += 1;
978 continue;
979 }
980
981 let mut end = start + 1;
982 while end < records.len() && records[end - 1].flags.contains(FileFlags::NOT_FINAL) {
983 end += 1;
984 }
985
986 let original_name = records[start].name.clone();
987 let unique_name = if seen.contains(&original_name) {
988 let mut suffix = 1;
989 loop {
990 let candidate = apply_dedup_suffix(&original_name, suffix, ty).into();
991 suffix += 1;
992 if !seen.contains(&candidate) {
993 break candidate;
994 }
995 }
996 } else {
997 original_name.clone()
998 };
999
1000 seen.insert(unique_name.clone());
1001
1002 for record in &mut records[start..end] {
1003 record.name = unique_name.clone();
1004 }
1005
1006 start = end;
1007 }
1008 }
1009
1010 fn sort_records(records: &mut [PendingRecord]) {
1011 records.sort_by(|a, b| {
1012 let rank = |name: &[u8]| match name {
1013 [0x00] => 0,
1014 [0x01] => 1,
1015 _ => 2,
1016 };
1017 rank(&a.name)
1018 .cmp(&rank(&b.name))
1019 .then_with(|| a.name.cmp(&b.name))
1020 });
1021 }
1022
1023 pub fn overflow_len(&self) -> u64 {
1025 self.0
1026 .iter()
1027 .filter(|r| r.split.has_overflow())
1028 .map(|r| r.split.overflow.len() as u64)
1029 .sum()
1030 }
1031
1032 pub fn has_overflow(&self) -> bool {
1034 self.0.iter().any(|r| r.split.has_overflow())
1035 }
1036
1037 pub fn iter(&self) -> impl Iterator<Item = &PendingRecord> {
1039 self.0.iter()
1040 }
1041
1042 pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut PendingRecord> {
1044 self.0.iter_mut()
1045 }
1046}
1047
1048#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
1054pub struct PendingRecordName(Arc<[u8]>);
1055
1056impl From<Vec<u8>> for PendingRecordName {
1057 fn from(value: Vec<u8>) -> Self {
1058 Self(value.into())
1059 }
1060}
1061
1062impl Deref for PendingRecordName {
1063 type Target = [u8];
1064
1065 fn deref(&self) -> &Self::Target {
1066 &self.0
1067 }
1068}
1069
1070#[derive(Debug, Clone)]
1072pub struct PendingRecord {
1073 pub name: PendingRecordName,
1075 pub split: SplitSu,
1077 pub dir_ref: DirectoryRef,
1079 pub flags: FileFlags,
1081}
1082
1083impl PendingRecord {
1084 pub fn new(
1086 name: &ConvertedName,
1087 split: SplitSu,
1088 dir_ref: DirectoryRef,
1089 flags: FileFlags,
1090 ) -> Self {
1091 Self {
1092 name: PendingRecordName(name.as_bytes().into()),
1093 split,
1094 dir_ref,
1095 flags,
1096 }
1097 }
1098
1099 pub fn current_dir(dot_split: SplitSu) -> Self {
1101 Self {
1102 name: PendingRecordName([0x00].into()),
1103 split: dot_split,
1104 dir_ref: DirectoryRef::default(),
1105 flags: FileFlags::DIRECTORY,
1106 }
1107 }
1108
1109 pub fn parent_dir(dotdot_split: SplitSu) -> Self {
1111 Self {
1112 name: PendingRecordName([0x01].into()),
1113 split: dotdot_split,
1114 dir_ref: DirectoryRef::default(),
1115 flags: FileFlags::DIRECTORY,
1116 }
1117 }
1118}
1119
1120pub struct ExtentIter {
1122 remaining: u64,
1123 cursor: u64,
1124 sector_size: u64,
1125}
1126
1127impl ExtentIter {
1128 pub fn new(len: u64, cursor: u64, sector_size: u64) -> Self {
1130 Self {
1131 remaining: len,
1132 cursor,
1133 sector_size,
1134 }
1135 }
1136
1137 fn max_extent_size(&self) -> u64 {
1139 (u32::MAX as u64 / self.sector_size) * self.sector_size
1140 }
1141
1142 #[cfg(test)]
1144 pub fn collect_with_cursor(self) -> (Vec<DirectoryRef>, u64) {
1145 let mut extents = Vec::new();
1146 let mut cursor = self.cursor;
1147
1148 for (extent, new_cursor) in self {
1149 extents.push(extent);
1150 cursor = new_cursor;
1151 }
1152
1153 (extents, cursor)
1154 }
1155}
1156
1157impl Iterator for ExtentIter {
1158 type Item = (DirectoryRef, u64);
1159
1160 fn next(&mut self) -> Option<Self::Item> {
1161 if self.remaining == 0 {
1162 return None;
1163 }
1164
1165 let chunk = self.remaining.min(self.max_extent_size());
1166
1167 let aligned = (self.cursor + self.sector_size - 1) & !(self.sector_size - 1);
1168 let extent = DirectoryRef::new((aligned / self.sector_size) as usize, chunk as usize);
1169
1170 self.cursor = aligned + chunk;
1171 self.remaining -= chunk;
1172
1173 Some((extent, self.cursor))
1174 }
1175}
1176
1177#[cfg(test)]
1178mod tests {
1179 use super::*;
1180
1181 #[test]
1182 fn extent_iter_splits_correctly() {
1183 let sector_size = 2048;
1184 let cursor = 0;
1185
1186 let len = u32::MAX as u64 + 1;
1187 let iter = ExtentIter::new(len, cursor, sector_size);
1188 let (extents, final_cursor) = iter.collect_with_cursor();
1189
1190 assert_eq!(extents.len(), 2);
1191
1192 assert_eq!(extents[0].size, 4_294_965_248);
1193 assert_eq!(extents[0].extent.0, 0);
1194
1195 assert_eq!(extents[1].size, 2048);
1196 let expected_sector: u64 = 4_294_965_248 / 2048;
1197 assert_eq!(extents[1].extent.0, expected_sector as usize);
1198
1199 assert_eq!(final_cursor, len);
1200 }
1201
1202 #[test]
1203 fn extent_iter_aligns_non_final_extents_to_sector_size() {
1204 let sector_size = 4096;
1205 let len = u32::MAX as u64 + 1;
1206 let (extents, final_cursor) = ExtentIter::new(len, 0, sector_size).collect_with_cursor();
1207
1208 assert_eq!(extents.len(), 2);
1209 assert_eq!(extents[0].size as u64, 4_294_963_200);
1210 assert_eq!(extents[0].size as u64 % sector_size, 0);
1211 assert_eq!(extents[1].size as u64, len - 4_294_963_200);
1212 assert_eq!(extents[1].extent.0 as u64, 4_294_963_200 / sector_size);
1213 assert_eq!(final_cursor, len);
1214 }
1215
1216 #[test]
1217 fn extent_iter_small_file() {
1218 let sector_size = 2048;
1219 let len = 1024; let iter = ExtentIter::new(len, 0, sector_size);
1221 let (extents, final_cursor) = iter.collect_with_cursor();
1222
1223 assert_eq!(extents.len(), 1);
1224 assert_eq!(extents[0].size, 1024);
1225 assert_eq!(extents[0].extent.0, 0);
1226 assert_eq!(final_cursor, 1024);
1227 }
1228
1229 #[test]
1230 fn deduplicates_logical_files_without_splitting_extent_names() {
1231 let record = |flags| PendingRecord {
1232 name: b"README.TXT;1".to_vec().into(),
1233 split: SplitSu::empty(),
1234 dir_ref: DirectoryRef::default(),
1235 flags,
1236 };
1237 let mut records = vec![
1238 record(FileFlags::NOT_FINAL),
1239 record(FileFlags::empty()),
1240 record(FileFlags::empty()),
1241 ];
1242
1243 PendingRecords::deduplicate_names(&mut records, EntryType::default());
1244
1245 assert_eq!(records[0].name, records[1].name);
1246 assert_ne!(records[1].name, records[2].name);
1247 }
1248}