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;
53use crate::write::{InputEntryKind, InputMetadata};
54
55#[derive(Debug, Clone)]
57pub enum ModifyOp {
58 AppendFile {
60 path: String,
62 data: FileData,
64 },
65 CreateDir {
67 path: String,
69 },
70 Delete {
72 path: String,
74 },
75 Replace {
77 path: String,
79 data: FileData,
81 },
82}
83
84#[derive(Debug, Clone)]
86pub enum FileData {
87 Buffer(Vec<u8>),
89 #[cfg(feature = "std")]
91 Path(std::path::PathBuf),
92}
93
94impl From<Vec<u8>> for FileData {
95 fn from(data: Vec<u8>) -> Self {
96 FileData::Buffer(data)
97 }
98}
99
100impl From<&[u8]> for FileData {
101 fn from(data: &[u8]) -> Self {
102 FileData::Buffer(data.to_vec())
103 }
104}
105
106#[cfg(feature = "std")]
107impl From<std::path::PathBuf> for FileData {
108 fn from(path: std::path::PathBuf) -> Self {
109 FileData::Path(path)
110 }
111}
112
113impl FileData {
114 pub fn size(&self) -> io::Result<u64> {
116 match self {
117 FileData::Buffer(data) => Ok(data.len() as u64),
118 #[cfg(feature = "std")]
119 FileData::Path(path) => {
120 let metadata = std::fs::metadata(path)
121 .map_err(|error| io::Error::from_source(error).erase())?;
122 Ok(metadata.len())
123 }
124 }
125 }
126
127 pub fn read_all(&self) -> io::Result<Vec<u8>> {
129 match self {
130 FileData::Buffer(data) => Ok(data.clone()),
131 #[cfg(feature = "std")]
132 FileData::Path(path) => {
133 std::fs::read(path).map_err(|error| io::Error::from_source(error).erase())
134 }
135 }
136 }
137}
138
139#[derive(Debug, thiserror::Error)]
141pub enum IsoModifyError {
142 #[error(transparent)]
144 Io(#[from] io::Error),
145 #[error("file not found: {0}")]
147 FileNotFound(String),
148 #[error("path already exists: {0}")]
150 PathExists(String),
151 #[error("not enough space to allocate {0} bytes")]
153 NotEnoughSpace(u64),
154 #[error("invalid path: {0}")]
156 InvalidPath(String),
157}
158
159pub type Error = IsoModifyError;
161pub type Result<T> = core::result::Result<T, Error>;
163
164pub struct IsoModifier<RW: Read + Write + Seek> {
170 inner: IsoCursor<RW>,
172 existing_layout: DirectoryLayout,
174 pending_ops: Vec<ModifyOp>,
176 entry_types: Vec<EntryType>,
178 sector_size: usize,
180 end_sector: LogicalSector,
182}
183
184io_transform! {
185impl<RW: Read + Write + Seek> IsoModifier<RW> {
186 pub async fn open(inner: RW) -> Result<Self> {
188 let sector_size = 2048;
190 let mut cursor = IsoCursor::new(inner, sector_size);
191
192 cursor.seek_sector(LogicalSector(16)).await?;
194 let volume_descriptors = VolumeDescriptorList::parse(&mut cursor).await?;
195
196 let pvd = volume_descriptors.try_primary().ok_or_else(|| {
198 io::Error::new(
199 io::ErrorKind::InvalidData,
200 "volume descriptor sequence has no primary descriptor",
201 )
202 })?;
203 let end_sector = pvd.volume_space_size.read() as usize;
204
205 let mut entry_types = Vec::new();
207 entry_types.push(EntryType::Level1 {
208 supports_lowercase: false,
209 supports_rrip: false,
210 });
211
212 for svd in volume_descriptors.supplementary() {
214 if svd.header.version == 1 {
215 for &level in JolietLevel::all() {
216 if JolietLevel::from_escape_sequence(&svd.escape_sequences) == Some(level) {
217 entry_types.push(EntryType::Joliet {
218 level,
219 supports_rrip: false,
220 });
221 }
222 }
223 }
224 }
225
226 let root_ref = DirectoryRef {
228 extent: LogicalSector(pvd.dir_record.header.extent.read() as usize),
229 size: pvd.dir_record.header.data_len.read() as usize,
230 };
231
232 let (existing_layout, _used_extents) =
233 Self::build_layout_from_directory(&mut cursor, root_ref, sector_size).await?;
234
235 Ok(Self {
236 inner: cursor,
237 existing_layout,
238 pending_ops: Vec::new(),
239 entry_types,
240 sector_size,
241 end_sector: LogicalSector(end_sector),
242 })
243 }
244
245 async fn build_layout_from_directory(
247 cursor: &mut IsoCursor<RW>,
248 root_ref: DirectoryRef,
249 sector_size: usize,
250 ) -> Result<(DirectoryLayout, Vec<Extent>)> {
251 let mut layout = DirectoryLayout::root();
252 let mut used_extents = Vec::new();
253
254 used_extents.push(Extent::new(0, 16 * sector_size as u64));
256
257 Self::read_directory_recursive(
258 cursor,
259 root_ref,
260 &mut layout,
261 &mut used_extents,
262 sector_size,
263 0,
264 ).await?;
265
266 Ok((layout, used_extents))
267 }
268
269 #[allow(clippy::only_used_in_recursion)]
271 async fn read_directory_recursive(
272 cursor: &mut IsoCursor<RW>,
273 dir_ref: DirectoryRef,
274 layout: &mut DirectoryLayout,
275 used_extents: &mut Vec<Extent>,
276 sector_size: usize,
277 depth: usize,
278 ) -> Result<()> {
279 const MAX_DIRECTORY_DEPTH: usize = 64;
280 if depth > MAX_DIRECTORY_DEPTH {
281 return Err(io::Error::new(
282 io::ErrorKind::InvalidData,
283 "directory nesting exceeds depth limit",
284 )
285 .into());
286 }
287 if used_extents
290 .iter()
291 .any(|extent| extent.sector == dir_ref.extent.0 as u32)
292 {
293 return Err(io::Error::new(
294 io::ErrorKind::InvalidData,
295 "cyclic directory extent reference",
296 )
297 .into());
298 }
299 used_extents.push(Extent::new(dir_ref.extent.0 as u32, dir_ref.size as u64));
301
302 cursor.seek_sector(dir_ref.extent).await?;
303 let mut offset = 0;
304
305 while offset < dir_ref.size {
306 let record = DirectoryRecord::parse(&mut *cursor).await?;
307 if record.header().len == 0 {
308 break;
309 }
310
311 let name = record.name();
312
313 if name == b"\x00" || name == b"\x01" {
315 offset += record.header().len as usize;
316 continue;
317 }
318
319 let header = record.header();
320 let extent = Extent::new(header.extent.read(), header.data_len.read() as u64);
321
322 let name_str = String::from_utf8_lossy(name).to_string();
324 let clean_name = if let Some(idx) = name_str.rfind(';') {
326 name_str[..idx].to_string()
327 } else {
328 name_str
329 };
330
331 if clean_name.is_empty() {
335 offset += record.header().len as usize;
336 continue;
337 }
338
339 if record.is_directory() {
340 let sub_ref = DirectoryRef {
342 extent: LogicalSector(header.extent.read() as usize),
343 size: header.data_len.read() as usize,
344 };
345
346 let mut subdir = DirectoryLayout::new(&clean_name);
347 subdir.extent = Some(extent);
348
349 let current_pos = cursor
351 .stream_position()
352 .await
353 .map_err(io::Error::erase)?;
354
355 Self::read_directory_recursive(
356 cursor,
357 sub_ref,
358 &mut subdir,
359 used_extents,
360 sector_size,
361 depth + 1,
362 ).await?;
363
364 cursor
366 .seek(SeekFrom::Start(current_pos))
367 .await
368 .map_err(io::Error::erase)?;
369
370 layout.add_subdir(subdir);
371 } else {
372 if extent.length > 0 {
374 used_extents.push(extent);
375 }
376
377 let file = FileLayout::new(&clean_name, extent).with_type(FileType::RegularFile);
378 layout.add_file(file);
379 }
380
381 offset += record.header().len as usize;
382 }
383
384 Ok(())
385 }
386
387 pub fn queue(&mut self, op: ModifyOp) {
389 self.pending_ops.push(op);
390 }
391
392 pub fn append_file(&mut self, path: &str, data: impl Into<FileData>) {
394 self.queue(ModifyOp::AppendFile {
395 path: path.to_string(),
396 data: data.into(),
397 });
398 }
399
400 pub fn create_dir(&mut self, path: &str) {
402 self.queue(ModifyOp::CreateDir {
403 path: path.to_string(),
404 });
405 }
406
407 pub fn delete(&mut self, path: &str) {
409 self.queue(ModifyOp::Delete {
410 path: path.to_string(),
411 });
412 }
413
414 pub fn replace(&mut self, path: &str, data: impl Into<FileData>) {
416 self.queue(ModifyOp::Replace {
417 path: path.to_string(),
418 data: data.into(),
419 });
420 }
421
422 pub fn layout(&self) -> &DirectoryLayout {
424 &self.existing_layout
425 }
426
427 pub async fn finish(mut self) -> Result<RW> {
429 if self.pending_ops.is_empty() {
430 return Ok(self.inner.into_inner());
431 }
432
433 let new_layout = self.apply_ops()?;
435
436 let written_files = self.write_new_data(&new_layout).await?;
438
439 self.write_new_session(&new_layout, written_files).await?;
441
442 Ok(self.inner.into_inner())
443 }
444
445 fn apply_ops(&mut self) -> Result<DirectoryLayout> {
447 let mut layout = self.existing_layout.clone();
448
449 for op in &self.pending_ops {
450 match op {
451 ModifyOp::AppendFile { path, data } => {
452 if layout.find_file(path).is_some() {
454 return Err(IsoModifyError::PathExists(path.clone()));
455 }
456
457 let (dir_path, filename) = Self::split_path(path)?;
459
460 let dir = if dir_path.is_empty() {
462 &mut layout
463 } else {
464 layout.get_or_create_dir(&dir_path)
465 };
466
467 let size = data.size()?;
469 let file = FileLayout::new(filename, Extent::new(0, size))
470 .with_type(FileType::RegularFile);
471 dir.add_file(file);
472 }
473 ModifyOp::CreateDir { path } => {
474 layout.get_or_create_dir(path);
476 }
477 ModifyOp::Delete { path } => {
478 if layout.remove_file(path).is_none() {
480 return Err(IsoModifyError::FileNotFound(path.clone()));
481 }
482 }
483 ModifyOp::Replace { path, data } => {
484 let file = layout
486 .find_file_mut(path)
487 .ok_or_else(|| IsoModifyError::FileNotFound(path.clone()))?;
488
489 let size = data.size()?;
491 file.extent = Extent::new(0, size);
492 }
493 }
494 }
495
496 Ok(layout)
497 }
498
499 async fn write_new_data(
501 &mut self,
502 _layout: &DirectoryLayout,
503 ) -> Result<BTreeMap<String, Extent>> {
504 let mut file_extents = BTreeMap::new();
505 let sector_size = self.sector_size as u32;
506
507 let mut current_sector = self.end_sector.0 as u32;
509
510 for op in &self.pending_ops {
511 match op {
512 ModifyOp::AppendFile { path, data } | ModifyOp::Replace { path, data } => {
513 let size = data.size()?;
514 if size == 0 {
515 file_extents.insert(path.clone(), Extent::new(0, 0));
517 continue;
518 }
519
520 let extent = Extent::new(current_sector, size);
522 file_extents.insert(path.clone(), extent);
523
524 self.inner
526 .seek_sector(LogicalSector(current_sector as usize)).await?;
527 let content = data.read_all()?;
528 self.inner.write_all(&content).await?;
529
530 current_sector += extent.sector_count(sector_size);
532 }
533 _ => {}
534 }
535 }
536
537 self.inner.pad_align_sector().await?;
539 self.end_sector = LogicalSector(current_sector as usize);
540
541 Ok(file_extents)
542 }
543
544 async fn write_new_session(
546 &mut self,
547 layout: &DirectoryLayout,
548 file_extents: BTreeMap<String, Extent>,
549 ) -> Result<()> {
550 let mut written_files = WrittenFiles::new();
552 Self::build_written_files(layout, &file_extents, &mut written_files, "")?;
553
554 let mut root_dirs = BTreeMap::new();
556 for &ty in &self.entry_types {
557 let root_id = written_files.root_dir();
558 let dir = written_files.get_mut(&root_id);
559 Self::write_directory_static(&mut self.inner, ty, dir).await?;
560 if let Some(dir_ref) = dir.entries.get(&ty) {
561 root_dirs.insert(ty, *dir_ref);
562 }
563 }
564
565 let mut path_tables = BTreeMap::new();
567 let entry_types = self.entry_types.clone();
568 for ty in entry_types {
569 let l_ref = self.write_path_table(
570 ty,
571 hadris_common::types::endian::EndianType::LittleEndian,
572 &mut written_files,
573 ).await?;
574 let m_ref = self.write_path_table(
575 ty,
576 hadris_common::types::endian::EndianType::BigEndian,
577 &mut written_files,
578 ).await?;
579 path_tables.insert(
580 ty,
581 PathTableRef {
582 lpt: l_ref.extent,
583 mpt: m_ref.extent,
584 size: l_ref.size as u64,
585 },
586 );
587 }
588
589 let end_sector = self.inner.pad_align_sector().await?;
591 self.update_volume_descriptors(&root_dirs, &path_tables, end_sector).await?;
592
593 Ok(())
594 }
595
596 fn build_written_files(
600 layout: &DirectoryLayout,
601 file_extents: &BTreeMap<String, Extent>,
602 written_files: &mut WrittenFiles,
603 path_prefix: &str,
604 ) -> Result<()> {
605 for file in &layout.files {
606 let full_path = Self::join_path(path_prefix, &file.name);
607
608 let extent = file_extents.get(&full_path).copied().unwrap_or(file.extent);
610 let dir_ref = DirectoryRef::new(extent.sector as usize, extent.length as usize);
611
612 let root_id = written_files.root_dir();
613 let dir = written_files.get_mut(&root_id);
614 let written = WrittenFile::with_extent(
615 Arc::new(file.name.clone()),
616 InputEntryKind::File(Vec::new()),
617 InputMetadata::default(),
618 dir_ref
619 );
620
621 dir.push_file(written);
622 }
623
624 for subdir in &layout.subdirs {
625 let full_path = Self::join_path(path_prefix, &subdir.name);
626
627 let root_id = written_files.root_dir();
629 let dir = written_files.get_mut(&root_id);
630 let _subdir_idx = dir.push_dir(
631 Arc::new(subdir.name.clone()),
632 crate::write::InputMetadata::default(),
633 );
634
635 Self::build_written_files(subdir, file_extents, written_files, &full_path)?;
637 }
638
639 Ok(())
640 }
641
642 fn join_path(path_prefix: &str, name: &str) -> String {
643 if path_prefix.is_empty() {
644 name.to_string()
645 } else {
646 alloc::format!("{path_prefix}/{name}")
647 }
648 }
649
650 async fn write_directory_static<W: Read + Write + Seek>(
652 data: &mut IsoCursor<W>,
653 ty: EntryType,
654 dir: &mut WrittenDirectory,
655 ) -> io::Result<()> {
656 let start = data.pad_align_sector().await?;
657
658 DirectoryRecord::new(b"\x00", &[], DirectoryRef::default(), FileFlags::DIRECTORY)
660 .write(&mut *data).await?;
661
662 DirectoryRecord::new(b"\x01", &[], DirectoryRef::default(), FileFlags::DIRECTORY)
664 .write(&mut *data).await?;
665
666 for directory in &dir.dirs {
667 let WrittenDirectory { name, entries, .. } = directory;
668 let flags = FileFlags::DIRECTORY;
669 let converted_name = ty.convert_directory_name(name);
670 let record = DirectoryRecord::new(
671 converted_name.as_bytes(),
672 &[],
673 *entries.get(&ty).unwrap_or(&DirectoryRef::default()),
674 flags,
675 );
676 record.write(&mut *data).await?;
677 }
678
679 for WrittenFile { name, entry, .. } in &dir.files {
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 JolietLevel::from_escape_sequence(&svd.escape_sequences) == Some(level) {
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}