1use alloc::collections::BTreeMap;
33use alloc::string::{String, ToString};
34use alloc::sync::Arc;
35use alloc::vec::Vec;
36
37use super::io::{self, Read, Seek, SeekFrom, Write};
38use hadris_common::types::endian::Endian;
39use hadris_common::types::extent::{Extent, FileType};
40use hadris_common::types::layout::{DirectoryLayout, FileLayout};
41use hadris_path::split_path;
42
43use super::directory::{DirectoryRecord, DirectoryRef, FileFlags};
44use super::io::{IsoCursor, LogicalSector};
45use super::path::PathTableRef;
46use super::volume::{
47 PrimaryVolumeDescriptor, SupplementaryVolumeDescriptor, VolumeDescriptorHeader,
48 VolumeDescriptorList, VolumeDescriptorType,
49};
50use super::write::writer::{PathTableWriter, WrittenDirectory, WrittenFile, WrittenFiles};
51use crate::file::EntryType;
52use crate::joliet::JolietLevel;
53
54#[derive(Debug, Clone)]
56pub enum ModifyOp {
57 AppendFile {
59 path: String,
61 data: FileData,
63 },
64 CreateDir {
66 path: String,
68 },
69 Delete {
71 path: String,
73 },
74 Replace {
76 path: String,
78 data: FileData,
80 },
81}
82
83#[derive(Debug, Clone)]
85pub enum FileData {
86 Buffer(Vec<u8>),
88 #[cfg(feature = "std")]
90 Path(std::path::PathBuf),
91}
92
93impl From<Vec<u8>> for FileData {
94 fn from(data: Vec<u8>) -> Self {
95 FileData::Buffer(data)
96 }
97}
98
99impl From<&[u8]> for FileData {
100 fn from(data: &[u8]) -> Self {
101 FileData::Buffer(data.to_vec())
102 }
103}
104
105#[cfg(feature = "std")]
106impl From<std::path::PathBuf> for FileData {
107 fn from(path: std::path::PathBuf) -> Self {
108 FileData::Path(path)
109 }
110}
111
112impl FileData {
113 pub fn size(&self) -> io::Result<u64> {
115 match self {
116 FileData::Buffer(data) => Ok(data.len() as u64),
117 #[cfg(feature = "std")]
118 FileData::Path(path) => {
119 let metadata = std::fs::metadata(path)
120 .map_err(|error| io::Error::from_source(error).erase())?;
121 Ok(metadata.len())
122 }
123 }
124 }
125
126 pub fn read_all(&self) -> io::Result<Vec<u8>> {
128 match self {
129 FileData::Buffer(data) => Ok(data.clone()),
130 #[cfg(feature = "std")]
131 FileData::Path(path) => {
132 std::fs::read(path).map_err(|error| io::Error::from_source(error).erase())
133 }
134 }
135 }
136}
137
138#[derive(Debug, thiserror::Error)]
140pub enum IsoModifyError {
141 #[error(transparent)]
143 Io(#[from] io::Error),
144 #[error("file not found: {0}")]
146 FileNotFound(String),
147 #[error("path already exists: {0}")]
149 PathExists(String),
150 #[error("not enough space to allocate {0} bytes")]
152 NotEnoughSpace(u64),
153 #[error("invalid path: {0}")]
155 InvalidPath(String),
156}
157
158pub type Error = IsoModifyError;
160pub type Result<T> = core::result::Result<T, Error>;
162
163pub struct IsoModifier<RW: Read + Write + Seek> {
169 inner: IsoCursor<RW>,
171 existing_layout: DirectoryLayout,
173 pending_ops: Vec<ModifyOp>,
175 entry_types: Vec<EntryType>,
177 sector_size: usize,
179 end_sector: LogicalSector,
181}
182
183io_transform! {
184impl<RW: Read + Write + Seek> IsoModifier<RW> {
185 pub async fn open(inner: RW) -> Result<Self> {
187 let sector_size = 2048;
189 let mut cursor = IsoCursor::new(inner, sector_size);
190
191 cursor.seek_sector(LogicalSector(16)).await?;
193 let volume_descriptors = VolumeDescriptorList::parse(&mut cursor).await?;
194
195 let pvd = volume_descriptors.try_primary().ok_or_else(|| {
197 io::Error::new(
198 io::ErrorKind::InvalidData,
199 "volume descriptor sequence has no primary descriptor",
200 )
201 })?;
202 let end_sector = pvd.volume_space_size.read() as usize;
203
204 let mut entry_types = Vec::new();
206 entry_types.push(EntryType::Level1 {
207 supports_lowercase: false,
208 supports_rrip: false,
209 });
210
211 for svd in volume_descriptors.supplementary() {
213 if svd.header.version == 1 {
214 for &level in JolietLevel::all() {
215 if svd.escape_sequences == level.escape_sequence() {
216 entry_types.push(EntryType::Joliet {
217 level,
218 supports_rrip: false,
219 });
220 }
221 }
222 }
223 }
224
225 let root_ref = DirectoryRef {
227 extent: LogicalSector(pvd.dir_record.header.extent.read() as usize),
228 size: pvd.dir_record.header.data_len.read() as usize,
229 };
230
231 let (existing_layout, _used_extents) =
232 Self::build_layout_from_directory(&mut cursor, root_ref, sector_size).await?;
233
234 Ok(Self {
235 inner: cursor,
236 existing_layout,
237 pending_ops: Vec::new(),
238 entry_types,
239 sector_size,
240 end_sector: LogicalSector(end_sector),
241 })
242 }
243
244 async fn build_layout_from_directory(
246 cursor: &mut IsoCursor<RW>,
247 root_ref: DirectoryRef,
248 sector_size: usize,
249 ) -> Result<(DirectoryLayout, Vec<Extent>)> {
250 let mut layout = DirectoryLayout::root();
251 let mut used_extents = Vec::new();
252
253 used_extents.push(Extent::new(0, 16 * sector_size as u64));
255
256 Self::read_directory_recursive(
257 cursor,
258 root_ref,
259 &mut layout,
260 &mut used_extents,
261 sector_size,
262 0,
263 ).await?;
264
265 Ok((layout, used_extents))
266 }
267
268 #[allow(clippy::only_used_in_recursion)]
270 async fn read_directory_recursive(
271 cursor: &mut IsoCursor<RW>,
272 dir_ref: DirectoryRef,
273 layout: &mut DirectoryLayout,
274 used_extents: &mut Vec<Extent>,
275 sector_size: usize,
276 depth: usize,
277 ) -> Result<()> {
278 const MAX_DIRECTORY_DEPTH: usize = 64;
279 if depth > MAX_DIRECTORY_DEPTH {
280 return Err(io::Error::new(
281 io::ErrorKind::InvalidData,
282 "directory nesting exceeds depth limit",
283 )
284 .into());
285 }
286 if used_extents
289 .iter()
290 .any(|extent| extent.sector == dir_ref.extent.0 as u32)
291 {
292 return Err(io::Error::new(
293 io::ErrorKind::InvalidData,
294 "cyclic directory extent reference",
295 )
296 .into());
297 }
298 used_extents.push(Extent::new(dir_ref.extent.0 as u32, dir_ref.size as u64));
300
301 cursor.seek_sector(dir_ref.extent).await?;
302 let mut offset = 0;
303
304 while offset < dir_ref.size {
305 let record = DirectoryRecord::parse(&mut *cursor).await?;
306 if record.header().len == 0 {
307 break;
308 }
309
310 let name = record.name();
311
312 if name == b"\x00" || name == b"\x01" {
314 offset += record.header().len as usize;
315 continue;
316 }
317
318 let header = record.header();
319 let extent = Extent::new(header.extent.read(), header.data_len.read() as u64);
320
321 let name_str = String::from_utf8_lossy(name).to_string();
323 let clean_name = if let Some(idx) = name_str.rfind(';') {
325 name_str[..idx].to_string()
326 } else {
327 name_str
328 };
329
330 if clean_name.is_empty() {
334 offset += record.header().len as usize;
335 continue;
336 }
337
338 if record.is_directory() {
339 let sub_ref = DirectoryRef {
341 extent: LogicalSector(header.extent.read() as usize),
342 size: header.data_len.read() as usize,
343 };
344
345 let mut subdir = DirectoryLayout::new(&clean_name);
346 subdir.extent = Some(extent);
347
348 let current_pos = cursor
350 .stream_position()
351 .await
352 .map_err(io::Error::erase)?;
353
354 Self::read_directory_recursive(
355 cursor,
356 sub_ref,
357 &mut subdir,
358 used_extents,
359 sector_size,
360 depth + 1,
361 ).await?;
362
363 cursor
365 .seek(SeekFrom::Start(current_pos))
366 .await
367 .map_err(io::Error::erase)?;
368
369 layout.add_subdir(subdir);
370 } else {
371 if extent.length > 0 {
373 used_extents.push(extent);
374 }
375
376 let file = FileLayout::new(&clean_name, extent).with_type(FileType::RegularFile);
377 layout.add_file(file);
378 }
379
380 offset += record.header().len as usize;
381 }
382
383 Ok(())
384 }
385
386 pub fn queue(&mut self, op: ModifyOp) {
388 self.pending_ops.push(op);
389 }
390
391 pub fn append_file(&mut self, path: &str, data: impl Into<FileData>) {
393 self.queue(ModifyOp::AppendFile {
394 path: path.to_string(),
395 data: data.into(),
396 });
397 }
398
399 pub fn create_dir(&mut self, path: &str) {
401 self.queue(ModifyOp::CreateDir {
402 path: path.to_string(),
403 });
404 }
405
406 pub fn delete(&mut self, path: &str) {
408 self.queue(ModifyOp::Delete {
409 path: path.to_string(),
410 });
411 }
412
413 pub fn replace(&mut self, path: &str, data: impl Into<FileData>) {
415 self.queue(ModifyOp::Replace {
416 path: path.to_string(),
417 data: data.into(),
418 });
419 }
420
421 pub fn layout(&self) -> &DirectoryLayout {
423 &self.existing_layout
424 }
425
426 pub async fn finish(mut self) -> Result<RW> {
428 if self.pending_ops.is_empty() {
429 return Ok(self.inner.into_inner());
430 }
431
432 let new_layout = self.apply_ops()?;
434
435 let written_files = self.write_new_data(&new_layout).await?;
437
438 self.write_new_session(&new_layout, written_files).await?;
440
441 Ok(self.inner.into_inner())
442 }
443
444 fn apply_ops(&mut self) -> Result<DirectoryLayout> {
446 let mut layout = self.existing_layout.clone();
447
448 for op in &self.pending_ops {
449 match op {
450 ModifyOp::AppendFile { path, data } => {
451 if layout.find_file(path).is_some() {
453 return Err(IsoModifyError::PathExists(path.clone()));
454 }
455
456 let (dir_path, filename) = Self::split_path(path)?;
458
459 let dir = if dir_path.is_empty() {
461 &mut layout
462 } else {
463 layout.get_or_create_dir(&dir_path)
464 };
465
466 let size = data.size()?;
468 let file = FileLayout::new(filename, Extent::new(0, size))
469 .with_type(FileType::RegularFile);
470 dir.add_file(file);
471 }
472 ModifyOp::CreateDir { path } => {
473 layout.get_or_create_dir(path);
475 }
476 ModifyOp::Delete { path } => {
477 if layout.remove_file(path).is_none() {
479 return Err(IsoModifyError::FileNotFound(path.clone()));
480 }
481 }
482 ModifyOp::Replace { path, data } => {
483 let file = layout
485 .find_file_mut(path)
486 .ok_or_else(|| IsoModifyError::FileNotFound(path.clone()))?;
487
488 let size = data.size()?;
490 file.extent = Extent::new(0, size);
491 }
492 }
493 }
494
495 Ok(layout)
496 }
497
498 async fn write_new_data(
500 &mut self,
501 _layout: &DirectoryLayout,
502 ) -> Result<BTreeMap<String, Extent>> {
503 let mut file_extents = BTreeMap::new();
504 let sector_size = self.sector_size as u32;
505
506 let mut current_sector = self.end_sector.0 as u32;
508
509 for op in &self.pending_ops {
510 match op {
511 ModifyOp::AppendFile { path, data } | ModifyOp::Replace { path, data } => {
512 let size = data.size()?;
513 if size == 0 {
514 file_extents.insert(path.clone(), Extent::new(0, 0));
516 continue;
517 }
518
519 let extent = Extent::new(current_sector, size);
521 file_extents.insert(path.clone(), extent);
522
523 self.inner
525 .seek_sector(LogicalSector(current_sector as usize)).await?;
526 let content = data.read_all()?;
527 self.inner.write_all(&content).await?;
528
529 current_sector += extent.sector_count(sector_size);
531 }
532 _ => {}
533 }
534 }
535
536 self.inner.pad_align_sector().await?;
538 self.end_sector = LogicalSector(current_sector as usize);
539
540 Ok(file_extents)
541 }
542
543 async fn write_new_session(
545 &mut self,
546 layout: &DirectoryLayout,
547 file_extents: BTreeMap<String, Extent>,
548 ) -> Result<()> {
549 let mut written_files = WrittenFiles::new();
551 Self::build_written_files(layout, &file_extents, &mut written_files, "")?;
552
553 let mut root_dirs = BTreeMap::new();
555 for &ty in &self.entry_types {
556 let root_id = written_files.root_dir();
557 let dir = written_files.get_mut(&root_id);
558 Self::write_directory_static(&mut self.inner, ty, dir).await?;
559 if let Some(dir_ref) = dir.entries.get(&ty) {
560 root_dirs.insert(ty, *dir_ref);
561 }
562 }
563
564 let mut path_tables = BTreeMap::new();
566 let entry_types = self.entry_types.clone();
567 for ty in entry_types {
568 let l_ref = self.write_path_table(
569 ty,
570 hadris_common::types::endian::EndianType::LittleEndian,
571 &mut written_files,
572 ).await?;
573 let m_ref = self.write_path_table(
574 ty,
575 hadris_common::types::endian::EndianType::BigEndian,
576 &mut written_files,
577 ).await?;
578 path_tables.insert(
579 ty,
580 PathTableRef {
581 lpt: l_ref.extent,
582 mpt: m_ref.extent,
583 size: l_ref.size as u64,
584 },
585 );
586 }
587
588 let end_sector = self.inner.pad_align_sector().await?;
590 self.update_volume_descriptors(&root_dirs, &path_tables, end_sector).await?;
591
592 Ok(())
593 }
594
595 fn build_written_files(
597 layout: &DirectoryLayout,
598 file_extents: &BTreeMap<String, Extent>,
599 written_files: &mut WrittenFiles,
600 path_prefix: &str,
601 ) -> Result<()> {
602 for file in &layout.files {
603 let full_path = if path_prefix.is_empty() {
604 file.name.clone()
605 } else {
606 alloc::format!("{}/{}", path_prefix, file.name)
607 };
608
609 let extent = file_extents.get(&full_path).copied().unwrap_or(file.extent);
611
612 let dir_ref = DirectoryRef {
613 extent: LogicalSector(extent.sector as usize),
614 size: extent.length as usize,
615 };
616
617 let root_id = written_files.root_dir();
618 let dir = written_files.get_mut(&root_id);
619 dir.files.push(WrittenFile {
620 name: Arc::new(file.name.clone()),
621 entry: dir_ref,
622 kind: crate::write::InputEntryKind::File(Vec::new()),
623 metadata: crate::write::InputMetadata::default(),
624 });
625 }
626
627 for subdir in &layout.subdirs {
628 let full_path = if path_prefix.is_empty() {
629 subdir.name.clone()
630 } else {
631 alloc::format!("{}/{}", path_prefix, subdir.name)
632 };
633
634 let root_id = written_files.root_dir();
636 let dir = written_files.get_mut(&root_id);
637 let _subdir_idx = dir.push_dir(
638 Arc::new(subdir.name.clone()),
639 crate::write::InputMetadata::default(),
640 );
641
642 Self::build_written_files(subdir, file_extents, written_files, &full_path)?;
644 }
645
646 Ok(())
647 }
648
649 async fn write_directory_static<W: Read + Write + Seek>(
651 data: &mut IsoCursor<W>,
652 ty: EntryType,
653 dir: &mut WrittenDirectory,
654 ) -> io::Result<()> {
655 let start = data.pad_align_sector().await?;
656
657 DirectoryRecord::new(b"\x00", &[], DirectoryRef::default(), FileFlags::DIRECTORY)
659 .write(&mut *data).await?;
660
661 DirectoryRecord::new(b"\x01", &[], DirectoryRef::default(), FileFlags::DIRECTORY)
663 .write(&mut *data).await?;
664
665 for directory in &dir.dirs {
666 let WrittenDirectory { name, entries, .. } = directory;
667 let flags = FileFlags::DIRECTORY;
668 let converted_name = ty.convert_directory_name(name);
669 let record = DirectoryRecord::new(
670 converted_name.as_bytes(),
671 &[],
672 *entries.get(&ty).unwrap_or(&DirectoryRef::default()),
673 flags,
674 );
675 record.write(&mut *data).await?;
676 }
677
678 for file in &dir.files {
679 let WrittenFile { name, entry, .. } = file;
680 let flags = FileFlags::empty();
681 let converted_name = ty.convert_name(name);
682 let record = DirectoryRecord::new(converted_name.as_bytes(), &[], *entry, flags);
683 record.write(&mut *data).await?;
684 }
685
686 let end = data.pad_align_sector().await?;
687 let size = (end.0 - start.0) * data.sector_size;
688 dir.entries.insert(
689 ty,
690 DirectoryRef {
691 extent: start,
692 size,
693 },
694 );
695 Ok(())
696 }
697
698 async fn write_path_table(
700 &mut self,
701 ty: EntryType,
702 endian: hadris_common::types::endian::EndianType,
703 written_files: &mut WrittenFiles,
704 ) -> io::Result<DirectoryRef> {
705 let start = self.inner.pad_align_sector().await?;
706 PathTableWriter {
707 written_files,
708 ty,
709 endian,
710 }
711 .write(&mut self.inner).await?;
712 let size = self
713 .inner
714 .stream_position()
715 .await
716 .map_err(io::Error::erase)? as usize
717 - (start.0 * self.sector_size);
718 let _end = self.inner.pad_align_sector().await?;
719 Ok(DirectoryRef {
720 extent: start,
721 size,
722 })
723 }
724
725 async fn update_volume_descriptors(
727 &mut self,
728 root_dirs: &BTreeMap<EntryType, DirectoryRef>,
729 path_tables: &BTreeMap<EntryType, PathTableRef>,
730 end_sector: LogicalSector,
731 ) -> io::Result<()> {
732 self.inner.seek_sector(LogicalSector(16)).await?;
733
734 let mut buffer = [0u8; 2048];
735 loop {
736 self.inner.read_exact(&mut buffer).await?;
737 let header = VolumeDescriptorHeader::from_bytes(&buffer[0..7]);
738 let ty = VolumeDescriptorType::from_u8(header.descriptor_type);
739
740 if let VolumeDescriptorType::VolumeSetTerminator = ty {
741 break;
742 }
743
744 match ty {
745 VolumeDescriptorType::PrimaryVolumeDescriptor => {
746 let base_type = self
747 .entry_types
748 .iter()
749 .find(|e| matches!(e, EntryType::Level1 { .. } | EntryType::Level2 { .. }))
750 .expect("no base level found");
751
752 if let Some(root_dir) = root_dirs.get(base_type)
753 && let Some(pt) = path_tables.get(base_type)
754 {
755 let pvd = bytemuck::from_bytes_mut::<PrimaryVolumeDescriptor>(&mut buffer);
756 pvd.dir_record.header.extent.write(root_dir.extent.0 as u32);
757 pvd.dir_record.header.data_len.write(root_dir.size as u32);
758 pvd.type_l_path_table.set(pt.lpt.0 as u32);
759 pvd.type_m_path_table.set(pt.mpt.0 as u32);
760 pvd.path_table_size.write(pt.size as u32);
761 pvd.volume_space_size.write(end_sector.0 as u32);
762 }
763 }
764 VolumeDescriptorType::SupplementaryVolumeDescriptor => {
765 let svd =
766 bytemuck::from_bytes_mut::<SupplementaryVolumeDescriptor>(&mut buffer);
767 if svd.header.version == 1 {
768 for &level in JolietLevel::all() {
770 if svd.escape_sequences == level.escape_sequence() {
771 let joliet = self
772 .entry_types
773 .iter()
774 .find(|e| matches!(e, EntryType::Joliet { level: jl, .. } if *jl == level));
775
776 if let Some(joliet) = joliet
777 && let Some(root_dir) = root_dirs.get(joliet)
778 && let Some(pt) = path_tables.get(joliet)
779 {
780 svd.dir_record.header.extent.write(root_dir.extent.0 as u32);
781 svd.dir_record.header.data_len.write(root_dir.size as u32);
782 svd.type_l_path_table.set(pt.lpt.0 as u32);
783 svd.type_m_path_table.set(pt.mpt.0 as u32);
784 svd.path_table_size.write(pt.size as u32);
785 svd.volume_space_size.write(end_sector.0 as u32);
786 }
787 }
788 }
789 }
790 }
791 _ => continue,
792 }
793
794 self.inner
796 .seek_relative(-(buffer.len() as i64))
797 .await
798 .map_err(io::Error::erase)?;
799 self.inner.write_all(&buffer).await?;
800 }
801
802 Ok(())
803 }
804
805 fn split_path(path: &str) -> Result<(String, String)> {
807 split_path(path)
808 .ok_or_else(|| IsoModifyError::InvalidPath(path.to_string()))
809 }
810}
811} #[cfg(test)]
814mod tests {
815 use super::*;
816 use alloc::vec;
817
818 #[test]
819 fn test_split_path() {
820 let (dir, file) = IsoModifier::<std::io::Cursor<Vec<u8>>>::split_path("test.txt").unwrap();
821 assert_eq!(dir, "");
822 assert_eq!(file, "test.txt");
823
824 let (dir, file) =
825 IsoModifier::<std::io::Cursor<Vec<u8>>>::split_path("docs/readme.txt").unwrap();
826 assert_eq!(dir, "docs");
827 assert_eq!(file, "readme.txt");
828
829 let (dir, file) =
830 IsoModifier::<std::io::Cursor<Vec<u8>>>::split_path("a/b/c/d.txt").unwrap();
831 assert_eq!(dir, "a/b/c");
832 assert_eq!(file, "d.txt");
833 }
834
835 #[test]
836 fn test_file_data() {
837 let data = FileData::from(vec![1, 2, 3, 4]);
838 assert_eq!(data.size().unwrap(), 4);
839 assert_eq!(data.read_all().unwrap(), vec![1, 2, 3, 4]);
840
841 let data = FileData::from(&[5, 6, 7][..]);
842 assert_eq!(data.size().unwrap(), 3);
843 }
844}