Skip to main content

hadris_cd/
writer.rs

1//! Main writer for creating hybrid ISO+UDF images
2//!
3//! The `OpticalImageWriter` orchestrates the creation of a hybrid image by:
4//! 1. Building a shared file tree
5//! 2. Laying out file data (shared between both filesystems)
6//! 3. Writing ISO 9660 metadata
7//! 4. Writing UDF metadata
8//! 5. Finalizing the image
9
10use super::super::{Borrowed, Read, Seek, SeekFrom, Write};
11
12use hadris_iso::read::PathSeparator;
13use hadris_udf::descriptor::{
14    ExtentDescriptor, LongAllocationDescriptor, ShortAllocationDescriptor,
15};
16use hadris_udf::write::{UdfWriteOptions, UdfWriter};
17use hadris_udf::{FileType, SECTOR_SIZE as UDF_SECTOR_SIZE};
18
19use crate::error::Result;
20use crate::layout::{LayoutInfo, LayoutManager, UdfDirectoryLayout};
21use crate::options::OpticalImageOptions;
22use crate::tree::{Directory, FileData, FileTree};
23
24/// Writer for creating hybrid ISO+UDF CD/DVD images
25pub struct OpticalImageWriter<W: Read + Write + Seek> {
26    writer: W,
27    options: OpticalImageOptions,
28}
29
30io_transform! {
31
32impl<W: Read + Write + Seek> OpticalImageWriter<W> {
33    /// Create a new CD writer
34    pub fn new(writer: W, options: OpticalImageOptions) -> Self {
35        Self { writer, options }
36    }
37
38    /// Creates an optical image and returns its output target.
39    pub async fn create(writer: W, tree: FileTree, options: OpticalImageOptions) -> Result<W> {
40        Self::new(writer, options).finish(tree).await
41    }
42
43    /// Returns the output target without writing an image.
44    pub fn into_inner(self) -> W {
45        self.writer
46    }
47
48    /// Finishes the image and returns its output target.
49    pub async fn finish(mut self, mut tree: FileTree) -> Result<W> {
50        if self.options.udf.enabled && self.options.sector_size != UDF_SECTOR_SIZE {
51            return Err(crate::error::Error::InvalidConfig(format!(
52                "UDF bridge images require {UDF_SECTOR_SIZE}-byte logical sectors"
53            )));
54        }
55
56        // Sort the tree for consistent output
57        tree.sort();
58
59        // Phase 1: Layout - determine where all files will be placed
60        let mut layout_manager = LayoutManager::new(self.options.sector_size);
61        let mut layout_info = layout_manager.layout_files(&mut tree, &self.options)?;
62
63        // ISO creation writes payloads as well as directory structures. Use its
64        // actual payload extents for the UDF allocation descriptors: the
65        // provisional layout does not account for ISO directory data placed at
66        // the allocation floor before those payloads.
67        if self.options.iso.enabled {
68            self.write_iso_structures(&tree, &layout_info).await?;
69            self.sync_iso_file_extents(&mut tree, &mut layout_info).await?;
70        } else {
71            self.write_file_data(&tree, &layout_info).await?;
72        }
73
74        // UDF metadata points at the already-written ISO payloads.
75        if self.options.udf.enabled {
76            self.write_udf_structures(&tree, &layout_info).await?;
77        }
78
79        Ok(self.writer)
80    }
81
82    async fn sync_iso_file_extents(
83        &mut self,
84        tree: &mut FileTree,
85        layout: &mut LayoutInfo,
86    ) -> Result<()> {
87        use hadris_iso::read::IsoImage;
88
89        let mut paths = Vec::new();
90        Self::collect_file_paths(&tree.root, "", &mut paths);
91        let mut extents = std::collections::BTreeMap::new();
92        {
93            let image = IsoImage::open(Borrowed::new(&mut self.writer))?;
94            for path in paths {
95                let entry = image.find_path(&path)?.ok_or_else(|| {
96                    crate::error::Error::InvalidPath(format!(
97                        "ISO writer did not produce the planned file: {path}"
98                    ))
99                })?;
100                extents.insert(
101                    path,
102                    (
103                        entry.header().extent.read(),
104                        u64::from(entry.header().data_len.read()),
105                    ),
106                );
107            }
108        }
109        Self::apply_iso_file_extents(&mut tree.root, "", &extents)?;
110
111        let end = extents
112            .values()
113            .filter(|(_, len)| *len != 0)
114            .map(|(sector, len)| {
115                *sector + len.div_ceil(self.options.sector_size as u64) as u32
116            })
117            .max()
118            .unwrap_or(layout.file_data_start);
119        layout.file_data_end = end;
120        layout.total_sectors = end.saturating_add(100);
121        Ok(())
122    }
123
124    fn collect_file_paths(dir: &Directory, prefix: &str, output: &mut Vec<String>) {
125        for file in &dir.files {
126            output.push(if prefix.is_empty() {
127                file.name.to_string()
128            } else {
129                format!("{prefix}/{}", file.name)
130            });
131        }
132        for child in &dir.subdirs {
133            let child_prefix = if prefix.is_empty() {
134                child.name.to_string()
135            } else {
136                format!("{prefix}/{}", child.name)
137            };
138            Self::collect_file_paths(child, &child_prefix, output);
139        }
140    }
141
142    fn apply_iso_file_extents(
143        dir: &mut Directory,
144        prefix: &str,
145        extents: &std::collections::BTreeMap<String, (u32, u64)>,
146    ) -> Result<()> {
147        for file in &mut dir.files {
148            let path = if prefix.is_empty() {
149                file.name.to_string()
150            } else {
151                format!("{prefix}/{}", file.name)
152            };
153            let &(sector, length) = extents.get(&path).ok_or_else(|| {
154                crate::error::Error::InvalidPath(format!("missing ISO extent for {path}"))
155            })?;
156            file.extent.sector = sector;
157            file.extent.length = length;
158        }
159        for child in &mut dir.subdirs {
160            let child_prefix = if prefix.is_empty() {
161                child.name.to_string()
162            } else {
163                format!("{prefix}/{}", child.name)
164            };
165            Self::apply_iso_file_extents(child, &child_prefix, extents)?;
166        }
167        Ok(())
168    }
169
170    /// Write all file data to their pre-assigned sectors
171    async fn write_file_data(&mut self, tree: &FileTree, _layout_info: &LayoutInfo) -> Result<()> {
172        self.write_directory_file_data(&tree.root).await?;
173        Ok(())
174    }
175
176    async fn write_directory_file_data(&mut self, dir: &Directory) -> Result<()> {
177        for file in &dir.files {
178            if file.extent.length == 0 {
179                continue; // Skip zero-size files
180            }
181
182            // Seek to the file's assigned sector
183            let offset = (file.extent.sector as u64) * self.options.sector_size as u64;
184            self.writer
185                .seek(SeekFrom::Start(offset))
186                .await
187                .map_err(hadris_io::Error::erase)?;
188
189            // Write the file data
190            match &file.data {
191                FileData::Buffer(data) => {
192                    self.writer.write_all(data).await?;
193                }
194                FileData::Path(path) => {
195                    let data = std::fs::read(path)
196                        .map_err(|error| hadris_io::Error::from_source(error).erase())?;
197                    self.writer.write_all(&data).await?;
198                }
199            }
200
201            // Pad to sector boundary
202            let written = file.extent.length as usize;
203            let padded = written.div_ceil(self.options.sector_size)
204                * self.options.sector_size;
205            if padded > written {
206                let padding = vec![0u8; padded - written];
207                self.writer.write_all(&padding).await?;
208            }
209        }
210
211        // Recursively write subdirectory files
212        for subdir in &dir.subdirs {
213            self.write_directory_file_data(subdir).await?;
214        }
215
216        Ok(())
217    }
218
219    /// Write ISO 9660 structures
220    async fn write_iso_structures(&mut self, tree: &FileTree, layout_info: &LayoutInfo) -> Result<()> {
221        use hadris_iso::write::options::{CreationFeatures, IsoFormatOptions};
222        use hadris_iso::write::{InputTree, IsoImageWriter};
223
224        // Convert our tree to ISO's InputFiles format
225        let iso_files = Self::tree_to_iso_files(&tree.root)?;
226
227        let input_files = InputTree::new(PathSeparator::ForwardSlash, iso_files);
228
229        // Build ISO format options from our options
230        let features = CreationFeatures {
231            filenames: self.options.iso.level,
232            long_filenames: self.options.iso.long_filenames,
233            joliet: self.options.iso.joliet,
234            rock_ridge: self.options.iso.rock_ridge,
235            el_torito: self.options.boot.clone(),
236            hybrid_boot: self.options.hybrid_boot.clone(),
237        };
238
239        let format_options = IsoFormatOptions {
240            volume_name: self.options.volume_id.clone(),
241            system_id: None,
242            volume_set_id: None,
243            publisher_id: None,
244            preparer_id: None,
245            application_id: None,
246            sector_size: self.options.sector_size,
247            features,
248            path_separator: PathSeparator::ForwardSlash,
249            strict_charset: false,
250        };
251
252        // Reset position and write ISO
253        self.writer
254            .seek(SeekFrom::Start(0))
255            .await
256            .map_err(hadris_io::Error::erase)?;
257        IsoImageWriter::create_with_allocation_floor(
258            Borrowed::new(&mut self.writer),
259            input_files,
260            format_options,
261            self.options
262                .udf
263                .enabled
264                .then_some(layout_info.file_data_start),
265        )?;
266
267        Ok(())
268    }
269
270    /// Convert our tree to ISO's file format
271    fn tree_to_iso_files(dir: &Directory) -> Result<Vec<hadris_iso::write::InputEntry>> {
272        let mut files = Vec::new();
273
274        for file in &dir.files {
275            let data = match &file.data {
276                FileData::Buffer(b) => b.clone(),
277                FileData::Path(p) => std::fs::read(p)
278                    .map_err(|error| hadris_io::Error::from_source(error).erase())?,
279            };
280            files.push(hadris_iso::write::InputEntry::file(
281                file.name.as_ref().clone(),
282                data,
283            ));
284        }
285
286        for subdir in &dir.subdirs {
287            files.push(hadris_iso::write::InputEntry::directory(
288                subdir.name.as_ref().clone(),
289                Self::tree_to_iso_files(subdir)?,
290            ));
291        }
292
293        Ok(files)
294    }
295
296    /// Write UDF structures
297    async fn write_udf_structures(&mut self, tree: &FileTree, layout_info: &LayoutInfo) -> Result<()> {
298        let image_bytes = self
299            .writer
300            .seek(SeekFrom::End(0))
301            .await
302            .map_err(hadris_io::Error::erase)?;
303        let image_sectors = image_bytes.div_ceil(self.options.sector_size as u64);
304        let required_sectors = u64::from(layout_info.total_sectors)
305            .checked_add(257)
306            .ok_or_else(|| crate::error::Error::InvalidConfig("image is too large".into()))?;
307        let final_sector_count = image_sectors.max(required_sectors);
308        let last_sector = u32::try_from(final_sector_count - 1)
309            .map_err(|_| crate::error::Error::InvalidConfig("image has too many sectors".into()))?;
310        let trailing_anchor = last_sector - 256;
311        let partition_length = trailing_anchor
312            .checked_sub(layout_info.udf_partition_start)
313            .ok_or_else(|| {
314                crate::error::Error::InvalidConfig(
315                    "UDF partition overlaps the trailing anchor".into(),
316                )
317            })?;
318
319        let udf_options = UdfWriteOptions {
320            volume_id: self.options.volume_id.clone(),
321            revision: self.options.udf.revision,
322            partition_start: layout_info.udf_partition_start,
323            partition_length,
324        };
325
326        let mut udf_writer = UdfWriter::new(Borrowed::new(&mut self.writer), udf_options);
327
328        // Keep the UDF VRS after ISO's descriptor terminator so both descriptor
329        // streams remain independently parseable.
330        udf_writer.write_vrs_at(layout_info.vds_end)?;
331
332        // Each VDS extent occupies sixteen sectors. The six descriptors are
333        // followed by reserved sectors within the declared extent.
334        let vds_start = 257u32;
335        let vds_length = 16u32;
336
337        // Reserve VDS extent
338        let reserve_vds_start = vds_start + vds_length;
339
340        // Write Anchor Volume Descriptor Pointer
341        let main_vds = ExtentDescriptor {
342            length: vds_length * UDF_SECTOR_SIZE as u32,
343            location: vds_start,
344        };
345        let reserve_vds = ExtentDescriptor {
346            length: vds_length * UDF_SECTOR_SIZE as u32,
347            location: reserve_vds_start,
348        };
349        udf_writer.write_avdp(main_vds, reserve_vds)?;
350        // The partition ends before this anchor, and the final 256 sectors are
351        // reserved so its N-256 position cannot overlap ISO or UDF content.
352        udf_writer.write_avdp_at(trailing_anchor, main_vds, reserve_vds)?;
353
354        // File Set Descriptor location (first block in partition)
355        let fsd_block = 0u32;
356        let fsd_icb = LongAllocationDescriptor {
357            extent_length: UDF_SECTOR_SIZE as u32,
358            logical_block_num: fsd_block,
359            partition_ref_num: 0,
360            impl_use: [0; 6],
361        };
362
363        // Root directory ICB location
364        let root_icb_block = layout_info.udf_root.icb_block;
365        let root_icb = LongAllocationDescriptor {
366            extent_length: UDF_SECTOR_SIZE as u32,
367            logical_block_num: root_icb_block,
368            partition_ref_num: 0,
369            impl_use: [0; 6],
370        };
371
372        // LVID location
373        let lvid_location = reserve_vds_start + vds_length;
374        let integrity_extent = ExtentDescriptor {
375            length: UDF_SECTOR_SIZE as u32,
376            location: lvid_location,
377        };
378
379        // Write Volume Descriptor Sequence
380        udf_writer.write_pvd(vds_start, 0)?;
381        udf_writer.write_iuvd(vds_start + 1, 1)?;
382        udf_writer.write_partition_descriptor(vds_start + 2, 2)?;
383        udf_writer.write_lvd(vds_start + 3, 3, fsd_icb, integrity_extent)?;
384        udf_writer.write_usd(vds_start + 4, 4)?;
385        udf_writer.write_terminating_descriptor(vds_start + 5)?;
386
387        // Write reserve VDS (copy of main VDS)
388        udf_writer.write_pvd(reserve_vds_start, 0)?;
389        udf_writer.write_iuvd(reserve_vds_start + 1, 1)?;
390        udf_writer.write_partition_descriptor(reserve_vds_start + 2, 2)?;
391        udf_writer.write_lvd(reserve_vds_start + 3, 3, fsd_icb, integrity_extent)?;
392        udf_writer.write_usd(reserve_vds_start + 4, 4)?;
393        udf_writer.write_terminating_descriptor(reserve_vds_start + 5)?;
394
395        // Write Logical Volume Integrity Descriptor
396        udf_writer.write_lvid(lvid_location, true)?;
397
398        // Write File Set Descriptor
399        udf_writer.write_fsd(fsd_block, root_icb)?;
400
401        // Write root directory
402        Self::write_udf_directory_static(
403            &mut udf_writer,
404            &tree.root,
405            &layout_info.udf_root,
406            layout_info,
407        )?;
408        drop(udf_writer);
409
410        if final_sector_count > image_sectors {
411            self.writer
412                .seek(SeekFrom::Start(
413                    u64::from(last_sector) * self.options.sector_size as u64,
414                ))
415                .await
416                .map_err(hadris_io::Error::erase)?;
417            self.writer
418                .write_all(&vec![0_u8; self.options.sector_size])
419                .await?;
420        }
421
422        Ok(())
423    }
424
425    /// Write UDF directory structure (File Entry + FIDs) - static method to avoid borrow issues
426    fn write_udf_directory_static<WR: Write + Seek>(
427        udf_writer: &mut UdfWriter<WR>,
428        dir: &Directory,
429        plan: &UdfDirectoryLayout,
430        layout_info: &LayoutInfo,
431    ) -> Result<()> {
432        let mut entries: Vec<(String, LongAllocationDescriptor, bool)> = Vec::new();
433        for (file, &file_icb_block) in dir.files.iter().zip(&plan.file_icb_blocks) {
434            let file_icb = LongAllocationDescriptor {
435                extent_length: UDF_SECTOR_SIZE as u32,
436                logical_block_num: file_icb_block,
437                partition_ref_num: 0,
438                impl_use: [0; 6],
439            };
440
441            entries.push((file.name.to_string(), file_icb, false));
442        }
443        for (subdir, subdir_plan) in dir.subdirs.iter().zip(&plan.subdirs) {
444            let subdir_icb = LongAllocationDescriptor {
445                extent_length: UDF_SECTOR_SIZE as u32,
446                logical_block_num: subdir_plan.icb_block,
447                partition_ref_num: 0,
448                impl_use: [0; 6],
449            };
450            entries.push((subdir.name.to_string(), subdir_icb, true));
451        }
452
453        // Write directory File Entry
454        let dir_alloc = vec![ShortAllocationDescriptor {
455            extent_length: plan.fid_bytes as u32,
456            extent_position: plan.fid_block,
457        }];
458        udf_writer.write_file_entry(
459            plan.icb_block,
460            FileType::Directory,
461            plan.fid_bytes as u64,
462            &dir_alloc,
463            dir.unique_id,
464        )?;
465
466        // Write FIDs (parent + children)
467        let parent_icb = LongAllocationDescriptor {
468            extent_length: UDF_SECTOR_SIZE as u32,
469            logical_block_num: plan.parent_icb_block,
470            partition_ref_num: 0,
471            impl_use: [0; 6],
472        };
473        udf_writer.write_fids(plan.fid_block, parent_icb, &entries)?;
474
475        for (file, &file_icb) in dir.files.iter().zip(&plan.file_icb_blocks) {
476            let file_alloc = if file.extent.length > 0 {
477                // Convert absolute sector to logical block within partition
478                let logical_block = file.extent.sector - layout_info.udf_partition_start;
479                vec![ShortAllocationDescriptor {
480                    extent_length: file.extent.length as u32,
481                    extent_position: logical_block,
482                }]
483            } else {
484                vec![] // Empty file
485            };
486
487            udf_writer.write_file_entry(
488                file_icb,
489                FileType::RegularFile,
490                file.extent.length,
491                &file_alloc,
492                file.unique_id,
493            )?;
494        }
495
496        for (subdir, subdir_plan) in dir.subdirs.iter().zip(&plan.subdirs) {
497            Self::write_udf_directory_static(udf_writer, subdir, subdir_plan, layout_info)?;
498        }
499
500        Ok(())
501    }
502}
503
504} // io_transform!
505
506#[cfg(test)]
507mod tests {
508    use super::*;
509    use crate::tree::FileEntry;
510    use std::io::Cursor;
511
512    #[test]
513    fn test_basic_writer() {
514        let mut tree = FileTree::new();
515        tree.add_file(FileEntry::from_buffer(
516            "test.txt",
517            b"Hello, World!".to_vec(),
518        ));
519
520        let buffer = vec![0u8; 1024 * 1024]; // 1MB buffer
521        let cursor = Cursor::new(buffer);
522
523        let options = OpticalImageOptions::default().volume_id("TEST");
524        let writer = OpticalImageWriter::new(cursor, options);
525
526        // This will test the basic flow
527        // Note: Full verification would require mounting the resulting image
528        let output = writer.finish(tree).unwrap();
529        assert!(!output.get_ref().is_empty());
530    }
531}