Skip to main content

hadris_iso/
modify.rs

1//! ISO 9660 image modification support.
2//!
3//! This module provides the ability to append files to existing ISO images
4//! and mark files for deletion. It uses a multi-session approach where
5//! each modification creates a new session with updated metadata.
6//!
7//! # Multi-Session Approach
8//!
9//! ISO 9660 is fundamentally an immutable format. Modifications are achieved
10//! through multi-session writing:
11//!
12//! - Each "session" has its own complete Volume Descriptor Set
13//! - New sessions are appended to the end of the image
14//! - The latest session's directory records reference all visible files
15//! - "Deletion" means hiding files from the new session's directory listing
16//!
17//! # Example
18//!
19//! ```rust,ignore
20//! use hadris_iso::modify::IsoModifier;
21//!
22//! let file = std::fs::OpenOptions::new()
23//!     .read(true).write(true)
24//!     .open("image.iso")?;
25//!
26//! let mut modifier = IsoModifier::open(file)?;
27//! modifier.append_file("new_file.txt", b"Hello, world!".to_vec());
28//! modifier.delete("old_file.txt");
29//! modifier.finish()?;
30//! ```
31
32use 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/// Operations that can be performed on an ISO image.
55#[derive(Debug, Clone)]
56pub enum ModifyOp {
57    /// Add a new file to the image.
58    AppendFile {
59        /// Path within the ISO (e.g., "docs/readme.txt")
60        path: String,
61        /// File contents
62        data: FileData,
63    },
64    /// Create a new directory.
65    CreateDir {
66        /// Path of the directory to create
67        path: String,
68    },
69    /// Mark a file as deleted (hidden from new session).
70    Delete {
71        /// Path of the file to delete
72        path: String,
73    },
74    /// Replace a file's content.
75    Replace {
76        /// Path of the file to replace
77        path: String,
78        /// New file contents
79        data: FileData,
80    },
81}
82
83/// File data for modification operations.
84#[derive(Debug, Clone)]
85pub enum FileData {
86    /// In-memory buffer.
87    Buffer(Vec<u8>),
88    /// Path to a file on the filesystem.
89    #[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    /// Returns the size of the file data.
114    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    /// Reads the file data into a buffer.
127    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/// Error type for ISO modification operations.
139#[derive(Debug, thiserror::Error)]
140pub enum IsoModifyError {
141    /// I/O error.
142    #[error(transparent)]
143    Io(#[from] io::Error),
144    /// File not found.
145    #[error("file not found: {0}")]
146    FileNotFound(String),
147    /// Path already exists.
148    #[error("path already exists: {0}")]
149    PathExists(String),
150    /// Not enough space.
151    #[error("not enough space to allocate {0} bytes")]
152    NotEnoughSpace(u64),
153    /// Invalid path.
154    #[error("invalid path: {0}")]
155    InvalidPath(String),
156}
157
158/// Canonical error for ISO modification operations.
159pub type Error = IsoModifyError;
160/// Canonical result for ISO modification operations.
161pub type Result<T> = core::result::Result<T, Error>;
162
163/// Modifier for ISO 9660 images.
164///
165/// This struct provides methods to append files, create directories,
166/// and mark files for deletion in an existing ISO image. Changes are
167/// committed as a new session at the end of the image.
168pub struct IsoModifier<RW: Read + Write + Seek> {
169    /// The underlying reader/writer.
170    inner: IsoCursor<RW>,
171    /// Parsed from existing image.
172    existing_layout: DirectoryLayout,
173    /// Pending operations.
174    pending_ops: Vec<ModifyOp>,
175    /// Entry types from the existing image.
176    entry_types: Vec<EntryType>,
177    /// Sector size.
178    sector_size: usize,
179    /// Current end of the image.
180    end_sector: LogicalSector,
181}
182
183io_transform! {
184impl<RW: Read + Write + Seek> IsoModifier<RW> {
185    /// Opens an existing ISO image for modification.
186    pub async fn open(inner: RW) -> Result<Self> {
187        // Parse existing image
188        let sector_size = 2048;
189        let mut cursor = IsoCursor::new(inner, sector_size);
190
191        // Read volume descriptors to get image info
192        cursor.seek_sector(LogicalSector(16)).await?;
193        let volume_descriptors = VolumeDescriptorList::parse(&mut cursor).await?;
194
195        // Get primary volume descriptor info
196        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        // Build entry types from volume descriptors
205        let mut entry_types = Vec::new();
206        entry_types.push(EntryType::Level1 {
207            supports_lowercase: false,
208            supports_rrip: false,
209        });
210
211        // Check for Joliet
212        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        // Build directory layout from existing image
226        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    /// Builds a DirectoryLayout from an existing ISO directory structure.
245    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        // Reserve system area and volume descriptors (sectors 0-16)
254        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    /// Recursively reads a directory and its contents.
269    #[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        // Directory extents come from untrusted on-disk records; a child
287        // pointing at an already-visited extent would recurse forever.
288        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        // Mark directory extent as used
299        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            // Skip . and .. entries
313            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            // Decode name
322            let name_str = String::from_utf8_lossy(name).to_string();
323            // Remove version suffix (;1)
324            let clean_name = if let Some(idx) = name_str.rfind(';') {
325                name_str[..idx].to_string()
326            } else {
327                name_str
328            };
329
330            // An identifier that is only a version suffix (e.g. ";1") decodes
331            // to an empty name; such entries cannot be represented in the new
332            // session, so drop them.
333            if clean_name.is_empty() {
334                offset += record.header().len as usize;
335                continue;
336            }
337
338            if record.is_directory() {
339                // Recurse into subdirectory
340                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                // Save current position
349                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                // Restore position
364                cursor
365                    .seek(SeekFrom::Start(current_pos))
366                    .await
367                    .map_err(io::Error::erase)?;
368
369                layout.add_subdir(subdir);
370            } else {
371                // Mark file extent as used (if non-empty)
372                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    /// Queues a modification operation.
387    pub fn queue(&mut self, op: ModifyOp) {
388        self.pending_ops.push(op);
389    }
390
391    /// Appends a file to the image (convenience method).
392    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    /// Creates a directory (convenience method).
400    pub fn create_dir(&mut self, path: &str) {
401        self.queue(ModifyOp::CreateDir {
402            path: path.to_string(),
403        });
404    }
405
406    /// Marks a file for deletion (convenience method).
407    pub fn delete(&mut self, path: &str) {
408        self.queue(ModifyOp::Delete {
409            path: path.to_string(),
410        });
411    }
412
413    /// Replaces a file's content (convenience method).
414    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    /// Returns the current layout.
422    pub fn layout(&self) -> &DirectoryLayout {
423        &self.existing_layout
424    }
425
426    /// Finishes all pending changes and returns the underlying image target.
427    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        // 1. Apply pending ops to layout
433        let new_layout = self.apply_ops()?;
434
435        // 2. Write new file data and allocate sectors
436        let written_files = self.write_new_data(&new_layout).await?;
437
438        // 3. Write new session metadata
439        self.write_new_session(&new_layout, written_files).await?;
440
441        Ok(self.inner.into_inner())
442    }
443
444    /// Applies pending operations to create a new layout.
445    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                    // Check if file already exists
452                    if layout.find_file(path).is_some() {
453                        return Err(IsoModifyError::PathExists(path.clone()));
454                    }
455
456                    // Split path into directory and filename
457                    let (dir_path, filename) = Self::split_path(path)?;
458
459                    // Get or create parent directory
460                    let dir = if dir_path.is_empty() {
461                        &mut layout
462                    } else {
463                        layout.get_or_create_dir(&dir_path)
464                    };
465
466                    // Create file layout with temporary extent (will be set during write)
467                    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                    // Get or create the directory
474                    layout.get_or_create_dir(path);
475                }
476                ModifyOp::Delete { path } => {
477                    // Remove file from layout
478                    if layout.remove_file(path).is_none() {
479                        return Err(IsoModifyError::FileNotFound(path.clone()));
480                    }
481                }
482                ModifyOp::Replace { path, data } => {
483                    // Find the file and update its extent
484                    let file = layout
485                        .find_file_mut(path)
486                        .ok_or_else(|| IsoModifyError::FileNotFound(path.clone()))?;
487
488                    // Update size (extent sector will be set during write)
489                    let size = data.size()?;
490                    file.extent = Extent::new(0, size);
491                }
492            }
493        }
494
495        Ok(layout)
496    }
497
498    /// Writes new file data and returns a map of paths to extents.
499    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        // Start writing after current end
507        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                        // Empty files use sector 0
515                        file_extents.insert(path.clone(), Extent::new(0, 0));
516                        continue;
517                    }
518
519                    // Allocate space
520                    let extent = Extent::new(current_sector, size);
521                    file_extents.insert(path.clone(), extent);
522
523                    // Write data
524                    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                    // Update current sector
530                    current_sector += extent.sector_count(sector_size);
531                }
532                _ => {}
533            }
534        }
535
536        // Pad to sector boundary
537        self.inner.pad_align_sector().await?;
538        self.end_sector = LogicalSector(current_sector as usize);
539
540        Ok(file_extents)
541    }
542
543    /// Writes the new session metadata.
544    async fn write_new_session(
545        &mut self,
546        layout: &DirectoryLayout,
547        file_extents: BTreeMap<String, Extent>,
548    ) -> Result<()> {
549        // Build WrittenFiles structure from layout
550        let mut written_files = WrittenFiles::new();
551        Self::build_written_files(layout, &file_extents, &mut written_files, "")?;
552
553        // Write directory records for all entry types
554        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        // Write path tables
565        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        // Update volume descriptors with new root directories and path tables
589        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    /// Builds WrittenFiles from DirectoryLayout.
596    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            // Look up extent from our written data or existing layout
610            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            // Add subdirectory to written files
635            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            // Recurse
643            Self::build_written_files(subdir, file_extents, written_files, &full_path)?;
644        }
645
646        Ok(())
647    }
648
649    /// Writes a directory (static version for use in new session).
650    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        // Current Directory Entry
658        DirectoryRecord::new(b"\x00", &[], DirectoryRef::default(), FileFlags::DIRECTORY)
659            .write(&mut *data).await?;
660
661        // Parent Directory Entry
662        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    /// Writes a path table.
699    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    /// Updates volume descriptors with new root directories and path tables.
726    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                        // Joliet
769                        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            // Write back the modified descriptor
795            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    /// Splits a path into (directory, filename).
806    fn split_path(path: &str) -> Result<(String, String)> {
807        split_path(path)
808            .ok_or_else(|| IsoModifyError::InvalidPath(path.to_string()))
809    }
810}
811} // io_transform!
812
813#[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}