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;
53use crate::write::{InputEntryKind, InputMetadata};
54
55/// Operations that can be performed on an ISO image.
56#[derive(Debug, Clone)]
57pub enum ModifyOp {
58    /// Add a new file to the image.
59    AppendFile {
60        /// Path within the ISO (e.g., "docs/readme.txt")
61        path: String,
62        /// File contents
63        data: FileData,
64    },
65    /// Create a new directory.
66    CreateDir {
67        /// Path of the directory to create
68        path: String,
69    },
70    /// Mark a file as deleted (hidden from new session).
71    Delete {
72        /// Path of the file to delete
73        path: String,
74    },
75    /// Replace a file's content.
76    Replace {
77        /// Path of the file to replace
78        path: String,
79        /// New file contents
80        data: FileData,
81    },
82}
83
84/// File data for modification operations.
85#[derive(Debug, Clone)]
86pub enum FileData {
87    /// In-memory buffer.
88    Buffer(Vec<u8>),
89    /// Path to a file on the filesystem.
90    #[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    /// Returns the size of the file data.
115    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    /// Reads the file data into a buffer.
128    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/// Error type for ISO modification operations.
140#[derive(Debug, thiserror::Error)]
141pub enum IsoModifyError {
142    /// I/O error.
143    #[error(transparent)]
144    Io(#[from] io::Error),
145    /// File not found.
146    #[error("file not found: {0}")]
147    FileNotFound(String),
148    /// Path already exists.
149    #[error("path already exists: {0}")]
150    PathExists(String),
151    /// Not enough space.
152    #[error("not enough space to allocate {0} bytes")]
153    NotEnoughSpace(u64),
154    /// Invalid path.
155    #[error("invalid path: {0}")]
156    InvalidPath(String),
157}
158
159/// Canonical error for ISO modification operations.
160pub type Error = IsoModifyError;
161/// Canonical result for ISO modification operations.
162pub type Result<T> = core::result::Result<T, Error>;
163
164/// Modifier for ISO 9660 images.
165///
166/// This struct provides methods to append files, create directories,
167/// and mark files for deletion in an existing ISO image. Changes are
168/// committed as a new session at the end of the image.
169pub struct IsoModifier<RW: Read + Write + Seek> {
170    /// The underlying reader/writer.
171    inner: IsoCursor<RW>,
172    /// Parsed from existing image.
173    existing_layout: DirectoryLayout,
174    /// Pending operations.
175    pending_ops: Vec<ModifyOp>,
176    /// Entry types from the existing image.
177    entry_types: Vec<EntryType>,
178    /// Sector size.
179    sector_size: usize,
180    /// Current end of the image.
181    end_sector: LogicalSector,
182}
183
184io_transform! {
185impl<RW: Read + Write + Seek> IsoModifier<RW> {
186    /// Opens an existing ISO image for modification.
187    pub async fn open(inner: RW) -> Result<Self> {
188        // Parse existing image
189        let sector_size = 2048;
190        let mut cursor = IsoCursor::new(inner, sector_size);
191
192        // Read volume descriptors to get image info
193        cursor.seek_sector(LogicalSector(16)).await?;
194        let volume_descriptors = VolumeDescriptorList::parse(&mut cursor).await?;
195
196        // Get primary volume descriptor info
197        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        // Build entry types from volume descriptors
206        let mut entry_types = Vec::new();
207        entry_types.push(EntryType::Level1 {
208            supports_lowercase: false,
209            supports_rrip: false,
210        });
211
212        // Check for Joliet
213        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        // Build directory layout from existing image
227        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    /// Builds a DirectoryLayout from an existing ISO directory structure.
246    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        // Reserve system area and volume descriptors (sectors 0-16)
255        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    /// Recursively reads a directory and its contents.
270    #[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        // Directory extents come from untrusted on-disk records; a child
288        // pointing at an already-visited extent would recurse forever.
289        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        // Mark directory extent as used
300        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            // Skip . and .. entries
314            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            // Decode name
323            let name_str = String::from_utf8_lossy(name).to_string();
324            // Remove version suffix (;1)
325            let clean_name = if let Some(idx) = name_str.rfind(';') {
326                name_str[..idx].to_string()
327            } else {
328                name_str
329            };
330
331            // An identifier that is only a version suffix (e.g. ";1") decodes
332            // to an empty name; such entries cannot be represented in the new
333            // session, so drop them.
334            if clean_name.is_empty() {
335                offset += record.header().len as usize;
336                continue;
337            }
338
339            if record.is_directory() {
340                // Recurse into subdirectory
341                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                // Save current position
350                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                // Restore position
365                cursor
366                    .seek(SeekFrom::Start(current_pos))
367                    .await
368                    .map_err(io::Error::erase)?;
369
370                layout.add_subdir(subdir);
371            } else {
372                // Mark file extent as used (if non-empty)
373                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    /// Queues a modification operation.
388    pub fn queue(&mut self, op: ModifyOp) {
389        self.pending_ops.push(op);
390    }
391
392    /// Appends a file to the image (convenience method).
393    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    /// Creates a directory (convenience method).
401    pub fn create_dir(&mut self, path: &str) {
402        self.queue(ModifyOp::CreateDir {
403            path: path.to_string(),
404        });
405    }
406
407    /// Marks a file for deletion (convenience method).
408    pub fn delete(&mut self, path: &str) {
409        self.queue(ModifyOp::Delete {
410            path: path.to_string(),
411        });
412    }
413
414    /// Replaces a file's content (convenience method).
415    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    /// Returns the current layout.
423    pub fn layout(&self) -> &DirectoryLayout {
424        &self.existing_layout
425    }
426
427    /// Finishes all pending changes and returns the underlying image target.
428    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        // 1. Apply pending ops to layout
434        let new_layout = self.apply_ops()?;
435
436        // 2. Write new file data and allocate sectors
437        let written_files = self.write_new_data(&new_layout).await?;
438
439        // 3. Write new session metadata
440        self.write_new_session(&new_layout, written_files).await?;
441
442        Ok(self.inner.into_inner())
443    }
444
445    /// Applies pending operations to create a new layout.
446    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                    // Check if file already exists
453                    if layout.find_file(path).is_some() {
454                        return Err(IsoModifyError::PathExists(path.clone()));
455                    }
456
457                    // Split path into directory and filename
458                    let (dir_path, filename) = Self::split_path(path)?;
459
460                    // Get or create parent directory
461                    let dir = if dir_path.is_empty() {
462                        &mut layout
463                    } else {
464                        layout.get_or_create_dir(&dir_path)
465                    };
466
467                    // Create file layout with temporary extent (will be set during write)
468                    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                    // Get or create the directory
475                    layout.get_or_create_dir(path);
476                }
477                ModifyOp::Delete { path } => {
478                    // Remove file from layout
479                    if layout.remove_file(path).is_none() {
480                        return Err(IsoModifyError::FileNotFound(path.clone()));
481                    }
482                }
483                ModifyOp::Replace { path, data } => {
484                    // Find the file and update its extent
485                    let file = layout
486                        .find_file_mut(path)
487                        .ok_or_else(|| IsoModifyError::FileNotFound(path.clone()))?;
488
489                    // Update size (extent sector will be set during write)
490                    let size = data.size()?;
491                    file.extent = Extent::new(0, size);
492                }
493            }
494        }
495
496        Ok(layout)
497    }
498
499    /// Writes new file data and returns a map of paths to extents.
500    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        // Start writing after current end
508        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                        // Empty files use sector 0
516                        file_extents.insert(path.clone(), Extent::new(0, 0));
517                        continue;
518                    }
519
520                    // Allocate space
521                    let extent = Extent::new(current_sector, size);
522                    file_extents.insert(path.clone(), extent);
523
524                    // Write data
525                    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                    // Update current sector
531                    current_sector += extent.sector_count(sector_size);
532                }
533                _ => {}
534            }
535        }
536
537        // Pad to sector boundary
538        self.inner.pad_align_sector().await?;
539        self.end_sector = LogicalSector(current_sector as usize);
540
541        Ok(file_extents)
542    }
543
544    /// Writes the new session metadata.
545    async fn write_new_session(
546        &mut self,
547        layout: &DirectoryLayout,
548        file_extents: BTreeMap<String, Extent>,
549    ) -> Result<()> {
550        // Build WrittenFiles structure from layout
551        let mut written_files = WrittenFiles::new();
552        Self::build_written_files(layout, &file_extents, &mut written_files, "")?;
553
554        // Write directory records for all entry types
555        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        // Write path tables
566        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        // Update volume descriptors with new root directories and path tables
590        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    /// Recursively builds the `WrittenFiles` tree from a `DirectoryLayout`.
597    ///
598    /// Populates file and directory entries with their assigned extents.
599    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            // Look up extent from our written data or existing layout
609            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            // Add subdirectory to written files
628            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            // Recurse
636            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    /// Writes a directory (static version for use in new session).
651    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        // Current Directory Entry
659        DirectoryRecord::new(b"\x00", &[], DirectoryRef::default(), FileFlags::DIRECTORY)
660            .write(&mut *data).await?;
661
662        // Parent Directory Entry
663        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    /// 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 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            // 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}