1use 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
24pub 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 pub fn new(writer: W, options: OpticalImageOptions) -> Self {
35 Self { writer, options }
36 }
37
38 pub async fn create(writer: W, tree: FileTree, options: OpticalImageOptions) -> Result<W> {
40 Self::new(writer, options).finish(tree).await
41 }
42
43 pub fn into_inner(self) -> W {
45 self.writer
46 }
47
48 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 tree.sort();
58
59 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 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 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 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; }
181
182 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 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 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 for subdir in &dir.subdirs {
213 self.write_directory_file_data(subdir).await?;
214 }
215
216 Ok(())
217 }
218
219 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 let iso_files = Self::tree_to_iso_files(&tree.root)?;
226
227 let input_files = InputTree::new(PathSeparator::ForwardSlash, iso_files);
228
229 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 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 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 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 udf_writer.write_vrs_at(layout_info.vds_end)?;
331
332 let vds_start = 257u32;
335 let vds_length = 16u32;
336
337 let reserve_vds_start = vds_start + vds_length;
339
340 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 udf_writer.write_avdp_at(trailing_anchor, main_vds, reserve_vds)?;
353
354 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 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 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 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 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 udf_writer.write_lvid(lvid_location, true)?;
397
398 udf_writer.write_fsd(fsd_block, root_icb)?;
400
401 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 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 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 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 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![] };
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} #[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]; let cursor = Cursor::new(buffer);
522
523 let options = OpticalImageOptions::default().volume_id("TEST");
524 let writer = OpticalImageWriter::new(cursor, options);
525
526 let output = writer.finish(tree).unwrap();
529 assert!(!output.get_ref().is_empty());
530 }
531}