Skip to main content

hadris_cd/
writer.rs

1//! Main writer for creating hybrid ISO+UDF images
2//!
3//! The `CdWriter` 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::{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::CdResult;
20use crate::layout::{LayoutInfo, LayoutManager};
21use crate::options::CdOptions;
22use crate::tree::{Directory, FileData, FileTree};
23
24/// Writer for creating hybrid ISO+UDF CD/DVD images
25pub struct CdWriter<W: Read + Write + Seek> {
26    writer: W,
27    options: CdOptions,
28}
29
30io_transform! {
31
32impl<W: Read + Write + Seek> CdWriter<W> {
33    /// Create a new CD writer
34    pub fn new(writer: W, options: CdOptions) -> Self {
35        Self { writer, options }
36    }
37
38    /// Create a hybrid image from a file tree
39    pub async fn write(mut self, mut tree: FileTree) -> CdResult<()> {
40        // Sort the tree for consistent output
41        tree.sort();
42
43        // Phase 1: Layout - determine where all files will be placed
44        let mut layout_manager = LayoutManager::new(self.options.sector_size);
45        let layout_info = layout_manager.layout_files(&mut tree, &self.options)?;
46
47        // Phase 2: Write file data to their assigned sectors
48        self.write_file_data(&tree, &layout_info).await?;
49
50        // Phase 3: Write ISO 9660 structures (if enabled)
51        if self.options.iso.enabled {
52            self.write_iso_structures(&tree, &layout_info).await?;
53        }
54
55        // Phase 4: Write UDF structures (if enabled)
56        if self.options.udf.enabled {
57            self.write_udf_structures(&tree, &layout_info).await?;
58        }
59
60        Ok(())
61    }
62
63    /// Write all file data to their pre-assigned sectors
64    async fn write_file_data(&mut self, tree: &FileTree, _layout_info: &LayoutInfo) -> CdResult<()> {
65        self.write_directory_file_data(&tree.root).await?;
66        Ok(())
67    }
68
69    async fn write_directory_file_data(&mut self, dir: &Directory) -> CdResult<()> {
70        for file in &dir.files {
71            if file.extent.length == 0 {
72                continue; // Skip zero-size files
73            }
74
75            // Seek to the file's assigned sector
76            let offset = (file.extent.sector as u64) * self.options.sector_size as u64;
77            self.writer.seek(SeekFrom::Start(offset)).await?;
78
79            // Write the file data
80            match &file.data {
81                FileData::Buffer(data) => {
82                    self.writer.write_all(data).await?;
83                }
84                FileData::Path(path) => {
85                    let data = std::fs::read(path)?;
86                    self.writer.write_all(&data).await?;
87                }
88            }
89
90            // Pad to sector boundary
91            let written = file.extent.length as usize;
92            let padded = written.div_ceil(self.options.sector_size)
93                * self.options.sector_size;
94            if padded > written {
95                let padding = vec![0u8; padded - written];
96                self.writer.write_all(&padding).await?;
97            }
98        }
99
100        // Recursively write subdirectory files
101        for subdir in &dir.subdirs {
102            self.write_directory_file_data(subdir).await?;
103        }
104
105        Ok(())
106    }
107
108    /// Write ISO 9660 structures
109    async fn write_iso_structures(&mut self, tree: &FileTree, _layout_info: &LayoutInfo) -> CdResult<()> {
110        use hadris_iso::write::options::{CreationFeatures, FormatOptions};
111        use hadris_iso::write::{InputFiles, IsoImageWriter};
112
113        // Convert our tree to ISO's InputFiles format
114        let iso_files = self.tree_to_iso_files(&tree.root);
115
116        let input_files = InputFiles {
117            path_separator: PathSeparator::ForwardSlash,
118            files: iso_files,
119        };
120
121        // Build ISO format options from our options
122        let features = CreationFeatures {
123            filenames: self.options.iso.level,
124            long_filenames: self.options.iso.long_filenames,
125            joliet: self.options.iso.joliet,
126            rock_ridge: self.options.iso.rock_ridge,
127            el_torito: self.options.boot.clone(),
128            hybrid_boot: self.options.hybrid_boot.clone(),
129        };
130
131        let format_options = FormatOptions {
132            volume_name: self.options.volume_id.clone(),
133            system_id: None,
134            volume_set_id: None,
135            publisher_id: None,
136            preparer_id: None,
137            application_id: None,
138            sector_size: self.options.sector_size,
139            features,
140            path_separator: PathSeparator::ForwardSlash,
141        };
142
143        // Reset position and write ISO
144        self.writer.seek(SeekFrom::Start(0)).await?;
145        IsoImageWriter::format_new(&mut self.writer, input_files, format_options)?;
146
147        Ok(())
148    }
149
150    /// Convert our tree to ISO's file format
151    fn tree_to_iso_files(&self, dir: &Directory) -> Vec<hadris_iso::write::File> {
152        let mut files = Vec::new();
153
154        for file in &dir.files {
155            let data = match &file.data {
156                FileData::Buffer(b) => b.clone(),
157                FileData::Path(p) => std::fs::read(p).unwrap_or_default(),
158            };
159            files.push(hadris_iso::write::File::File {
160                name: file.name.clone(),
161                contents: data,
162            });
163        }
164
165        for subdir in &dir.subdirs {
166            files.push(hadris_iso::write::File::Directory {
167                name: subdir.name.clone(),
168                children: self.tree_to_iso_files(subdir),
169            });
170        }
171
172        files
173    }
174
175    /// Write UDF structures
176    async fn write_udf_structures(&mut self, tree: &FileTree, layout_info: &LayoutInfo) -> CdResult<()> {
177        let udf_options = UdfWriteOptions {
178            volume_id: self.options.volume_id.clone(),
179            revision: self.options.udf.revision,
180            partition_start: layout_info.udf_partition_start,
181            partition_length: layout_info.udf_partition_length(),
182        };
183
184        let mut udf_writer = UdfWriter::new(&mut self.writer, udf_options);
185
186        // Write Volume Recognition Sequence
187        udf_writer.write_vrs()?;
188
189        // VDS at sectors 257-262
190        let vds_start = 257u32;
191        let vds_length = 6u32; // 6 descriptors
192
193        // Reserve VDS extent
194        let reserve_vds_start = 263u32;
195
196        // Write Anchor Volume Descriptor Pointer
197        let main_vds = ExtentDescriptor {
198            length: vds_length * UDF_SECTOR_SIZE as u32,
199            location: vds_start,
200        };
201        let reserve_vds = ExtentDescriptor {
202            length: vds_length * UDF_SECTOR_SIZE as u32,
203            location: reserve_vds_start,
204        };
205        udf_writer.write_avdp(main_vds, reserve_vds)?;
206
207        // File Set Descriptor location (first block in partition)
208        let fsd_block = 0u32;
209        let fsd_icb = LongAllocationDescriptor {
210            extent_length: UDF_SECTOR_SIZE as u32,
211            logical_block_num: fsd_block,
212            partition_ref_num: 0,
213            impl_use: [0; 6],
214        };
215
216        // Root directory ICB location
217        let root_icb_block = 1u32;
218        let root_icb = LongAllocationDescriptor {
219            extent_length: UDF_SECTOR_SIZE as u32,
220            logical_block_num: root_icb_block,
221            partition_ref_num: 0,
222            impl_use: [0; 6],
223        };
224
225        // LVID location
226        let lvid_location = reserve_vds_start + vds_length;
227        let integrity_extent = ExtentDescriptor {
228            length: UDF_SECTOR_SIZE as u32,
229            location: lvid_location,
230        };
231
232        // Write Volume Descriptor Sequence
233        udf_writer.write_pvd(vds_start, 0)?;
234        udf_writer.write_iuvd(vds_start + 1, 1)?;
235        udf_writer.write_partition_descriptor(vds_start + 2, 2)?;
236        udf_writer.write_lvd(vds_start + 3, 3, fsd_icb, integrity_extent)?;
237        udf_writer.write_usd(vds_start + 4, 4)?;
238        udf_writer.write_terminating_descriptor(vds_start + 5)?;
239
240        // Write reserve VDS (copy of main VDS)
241        udf_writer.write_pvd(reserve_vds_start, 0)?;
242        udf_writer.write_iuvd(reserve_vds_start + 1, 1)?;
243        udf_writer.write_partition_descriptor(reserve_vds_start + 2, 2)?;
244        udf_writer.write_lvd(reserve_vds_start + 3, 3, fsd_icb, integrity_extent)?;
245        udf_writer.write_usd(reserve_vds_start + 4, 4)?;
246        udf_writer.write_terminating_descriptor(reserve_vds_start + 5)?;
247
248        // Write Logical Volume Integrity Descriptor
249        udf_writer.write_lvid(lvid_location, true)?;
250
251        // Write File Set Descriptor
252        udf_writer.write_fsd(fsd_block, root_icb)?;
253
254        // Write root directory
255        Self::write_udf_directory_static(&mut udf_writer, &tree.root, root_icb_block, layout_info)?;
256
257        Ok(())
258    }
259
260    /// Write UDF directory structure (File Entry + FIDs) - static method to avoid borrow issues
261    fn write_udf_directory_static<WR: Write + Seek>(
262        udf_writer: &mut UdfWriter<WR>,
263        dir: &Directory,
264        icb_block: u32,
265        layout_info: &LayoutInfo,
266    ) -> CdResult<()> {
267        // Collect child entries for FIDs
268        let mut entries: Vec<(String, LongAllocationDescriptor, bool)> = Vec::new();
269        let mut next_icb = icb_block + 2; // After this dir's File Entry and FIDs
270
271        // Process files
272        for file in &dir.files {
273            let file_icb_block = next_icb;
274            next_icb += 1;
275
276            let file_icb = LongAllocationDescriptor {
277                extent_length: UDF_SECTOR_SIZE as u32,
278                logical_block_num: file_icb_block,
279                partition_ref_num: 0,
280                impl_use: [0; 6],
281            };
282
283            entries.push((file.name.to_string(), file_icb, false));
284        }
285
286        // Process subdirectories (we'll write them recursively)
287        for subdir in &dir.subdirs {
288            let subdir_icb_block = next_icb;
289            // Reserve space for subdir's File Entry and FIDs
290            let subdir_entries = subdir.files.len() + subdir.subdirs.len() + 1; // +1 for parent
291            let fid_sectors = (subdir_entries * 50).div_ceil(UDF_SECTOR_SIZE);
292            next_icb += 1 + fid_sectors as u32;
293
294            let subdir_icb = LongAllocationDescriptor {
295                extent_length: UDF_SECTOR_SIZE as u32,
296                logical_block_num: subdir_icb_block,
297                partition_ref_num: 0,
298                impl_use: [0; 6],
299            };
300
301            entries.push((subdir.name.to_string(), subdir_icb, true));
302        }
303
304        // Calculate directory data size (FIDs)
305        let total_entries = entries.len() + 1; // +1 for parent entry
306        let estimated_fid_size = total_entries * 50; // Rough estimate
307        let dir_data_sectors =
308            estimated_fid_size.div_ceil(UDF_SECTOR_SIZE) as u32;
309        let dir_data_size = (dir_data_sectors as usize) * UDF_SECTOR_SIZE;
310
311        // Write directory File Entry
312        let dir_alloc = vec![ShortAllocationDescriptor {
313            extent_length: dir_data_size as u32,
314            extent_position: icb_block + 1, // FIDs follow File Entry
315        }];
316        udf_writer.write_file_entry(
317            icb_block,
318            FileType::Directory,
319            dir_data_size as u64,
320            &dir_alloc,
321            dir.unique_id,
322        )?;
323
324        // Write FIDs (parent + children)
325        let parent_icb = LongAllocationDescriptor {
326            extent_length: UDF_SECTOR_SIZE as u32,
327            logical_block_num: icb_block, // Self for root, or actual parent
328            partition_ref_num: 0,
329            impl_use: [0; 6],
330        };
331        udf_writer.write_fids(icb_block + 1, parent_icb, &entries)?;
332
333        // Write file File Entries
334        let mut file_icb = icb_block + 2;
335        for file in &dir.files {
336            let file_alloc = if file.extent.length > 0 {
337                // Convert absolute sector to logical block within partition
338                let logical_block = file.extent.sector - layout_info.udf_partition_start;
339                vec![ShortAllocationDescriptor {
340                    extent_length: file.extent.length as u32,
341                    extent_position: logical_block,
342                }]
343            } else {
344                vec![] // Empty file
345            };
346
347            udf_writer.write_file_entry(
348                file_icb,
349                FileType::RegularFile,
350                file.extent.length,
351                &file_alloc,
352                file.unique_id,
353            )?;
354            file_icb += 1;
355        }
356
357        // Recursively write subdirectories
358        let mut subdir_icb = file_icb;
359        for subdir in &dir.subdirs {
360            Self::write_udf_directory_static(udf_writer, subdir, subdir_icb, layout_info)?;
361            // Calculate next subdir's ICB position
362            let subdir_entries = subdir.files.len() + subdir.subdirs.len() + 1;
363            let fid_sectors = (subdir_entries * 50).div_ceil(UDF_SECTOR_SIZE);
364            subdir_icb += 1 + fid_sectors as u32 + subdir.files.len() as u32;
365        }
366
367        Ok(())
368    }
369}
370
371} // io_transform!
372
373#[cfg(test)]
374mod tests {
375    use super::*;
376    use crate::tree::FileEntry;
377    use std::io::Cursor;
378
379    #[test]
380    fn test_basic_writer() {
381        let mut tree = FileTree::new();
382        tree.add_file(FileEntry::from_buffer(
383            "test.txt",
384            b"Hello, World!".to_vec(),
385        ));
386
387        let buffer = vec![0u8; 1024 * 1024]; // 1MB buffer
388        let cursor = Cursor::new(buffer);
389
390        let options = CdOptions::with_volume_id("TEST");
391        let writer = CdWriter::new(cursor, options);
392
393        // This will test the basic flow
394        // Note: Full verification would require mounting the resulting image
395        let result = writer.write(tree);
396        assert!(result.is_ok());
397    }
398}