Skip to main content

hadris_cd/
layout.rs

1//! Sector layout and allocation for hybrid CD/DVD images
2//!
3//! This module handles the physical layout of data on disk, ensuring that
4//! both ISO 9660 and UDF filesystems can reference the same file data.
5//!
6//! ## Disk Layout for UDF Bridge Format
7//!
8//! ```text
9//! Sector 0-15:    System area (boot code, partition tables)
10//! Sector 16:      ISO Primary Volume Descriptor
11//! Sector 17:      UDF BEA01 (Beginning of Extended Area)
12//! Sector 18:      UDF NSR02/NSR03 (UDF identifier)
13//! Sector 19:      UDF TEA01 (Terminal Extended Area)
14//! Sector 20-...:  More ISO Volume Descriptors (Joliet SVD, etc.)
15//! Sector ..:      ISO Volume Descriptor Set Terminator
16//! Sector 256:     UDF Anchor Volume Descriptor Pointer
17//! Sector 257+:    UDF Volume Descriptor Sequence
18//! Sector ..:      UDF File Set Descriptor
19//! Sector ..:      File data (shared between ISO and UDF)
20//! Sector ..:      ISO directory records
21//! Sector ..:      UDF directory structures (File Entries, FIDs)
22//! Sector ..:      ISO path tables
23//! ```
24
25use crate::error::{Error, Result};
26use crate::options::OpticalImageOptions;
27use crate::tree::{Directory, FileExtent, FileTree};
28
29/// Handles sector allocation for the CD image
30#[derive(Debug)]
31pub struct LayoutManager {
32    /// Sector size (usually 2048)
33    sector_size: usize,
34    /// Next available sector for file data
35    next_file_sector: u32,
36    /// Next available sector within UDF partition
37    next_udf_block: u32,
38    /// Next unique ID for UDF
39    next_unique_id: u64,
40}
41
42impl LayoutManager {
43    /// Create a new layout manager
44    pub fn new(sector_size: usize) -> Self {
45        Self {
46            sector_size,
47            // File data starts after the system area, volume descriptors, and UDF structures
48            // We'll calculate this more precisely during layout
49            next_file_sector: 0,
50            next_udf_block: 0,
51            next_unique_id: 16, // UDF reserves IDs 0-15
52        }
53    }
54
55    /// Allocate sectors for file data and assign extents to all files
56    ///
57    /// This is the core layout function that determines where each file's
58    /// data will be stored on disk. Both ISO and UDF will reference these
59    /// same sectors.
60    pub fn layout_files(
61        &mut self,
62        tree: &mut FileTree,
63        options: &OpticalImageOptions,
64    ) -> Result<LayoutInfo> {
65        // Calculate starting positions based on what we need to write
66        let vds_end = self.calculate_vds_end(options);
67
68        // Main/reserve VDS occupy 257-288 and the LVID occupies 289.
69        let udf_partition_start = 290;
70
71        // Plan every UDF metadata object once, globally. Block 0 is the FSD;
72        // directory/file ICBs and exact-sized FID extents follow it.
73        let mut next_udf_block = 1;
74        let udf_root =
75            Self::plan_udf_directory(&tree.root, &mut next_udf_block, None, self.sector_size)?;
76        let udf_metadata_sectors = next_udf_block;
77
78        // File data starts after UDF metadata (within UDF partition)
79        self.next_udf_block = udf_metadata_sectors;
80        self.next_file_sector = udf_partition_start + udf_metadata_sectors;
81
82        // Assign extents to all files
83        self.assign_file_extents(&mut tree.root)?;
84
85        // Assign unique IDs to directories and files
86        self.assign_unique_ids(&mut tree.root);
87
88        let file_data_end = self.next_file_sector;
89
90        Ok(LayoutInfo {
91            vds_end,
92            udf_partition_start,
93            udf_metadata_sectors,
94            file_data_start: udf_partition_start + udf_metadata_sectors,
95            file_data_end,
96            total_sectors: file_data_end + 100, // Reserve space for ISO path tables etc.
97            udf_root,
98        })
99    }
100
101    /// Calculate where the Volume Descriptor Sequence ends
102    fn calculate_vds_end(&self, options: &OpticalImageOptions) -> u32 {
103        let mut sector = 16; // VDS starts at sector 16
104
105        // ISO Primary Volume Descriptor
106        if options.iso.enabled {
107            sector += 1;
108        }
109
110        // UDF VRS (BEA01, NSR02/03, TEA01) - actually at 16-18 with ISO
111        // In hybrid format, VRS is interleaved with ISO VD
112        // For simplicity, we'll place VRS at sectors 16-18
113
114        // Joliet SVD
115        if options.iso.joliet.is_some() {
116            sector += 1;
117        }
118
119        // ISO 9660:1999 EVD
120        if options.iso.long_filenames {
121            sector += 1;
122        }
123
124        // Boot record (El-Torito)
125        if options.boot.is_some() {
126            sector += 1;
127        }
128
129        // Volume Set Terminator
130        sector += 1;
131
132        sector
133    }
134
135    fn plan_udf_directory(
136        dir: &Directory,
137        next_block: &mut u32,
138        parent_icb: Option<u32>,
139        sector_size: usize,
140    ) -> Result<UdfDirectoryLayout> {
141        let icb_block = *next_block;
142        *next_block = next_block
143            .checked_add(1)
144            .ok_or_else(|| Error::InvalidConfig("UDF metadata block overflow".into()))?;
145
146        let mut fid_bytes = 40usize; // parent FID (38-byte base, padded to four)
147        for name in dir
148            .files
149            .iter()
150            .map(|file| file.name.as_str())
151            .chain(dir.subdirs.iter().map(|child| child.name.as_str()))
152        {
153            let encoded_len = cs0_filename_len(name)?;
154            fid_bytes = fid_bytes
155                .checked_add((38 + encoded_len + 3) & !3)
156                .ok_or_else(|| Error::InvalidConfig("UDF FID size overflow".into()))?;
157        }
158        let fid_sectors = fid_bytes.div_ceil(sector_size) as u32;
159        let fid_block = *next_block;
160        *next_block = next_block
161            .checked_add(fid_sectors)
162            .ok_or_else(|| Error::InvalidConfig("UDF metadata block overflow".into()))?;
163
164        let mut file_icb_blocks = Vec::with_capacity(dir.files.len());
165        for _ in &dir.files {
166            file_icb_blocks.push(*next_block);
167            *next_block = next_block
168                .checked_add(1)
169                .ok_or_else(|| Error::InvalidConfig("UDF metadata block overflow".into()))?;
170        }
171
172        let mut subdirs = Vec::with_capacity(dir.subdirs.len());
173        for child in &dir.subdirs {
174            subdirs.push(Self::plan_udf_directory(
175                child,
176                next_block,
177                Some(icb_block),
178                sector_size,
179            )?);
180        }
181
182        Ok(UdfDirectoryLayout {
183            icb_block,
184            parent_icb_block: parent_icb.unwrap_or(icb_block),
185            fid_block,
186            fid_bytes,
187            file_icb_blocks,
188            subdirs,
189        })
190    }
191
192    /// Recursively assign file extents
193    fn assign_file_extents(&mut self, dir: &mut Directory) -> Result<()> {
194        // Assign extents to files
195        for file in &mut dir.files {
196            let size = file
197                .size()
198                .map_err(|error| Error::Io(hadris_io::Error::from_source(error).erase()))?;
199
200            if size == 0 {
201                // Zero-size files have no extent (sector 0 per ISO spec)
202                file.extent = FileExtent::new(0, 0);
203            } else {
204                file.extent = FileExtent::new(self.next_file_sector, size);
205                let sectors = file.extent.sector_count(self.sector_size);
206                self.next_file_sector += sectors;
207            }
208        }
209
210        // Recursively handle subdirectories
211        for subdir in &mut dir.subdirs {
212            self.assign_file_extents(subdir)?;
213        }
214
215        Ok(())
216    }
217
218    /// Assign unique IDs to all directories and files
219    fn assign_unique_ids(&mut self, dir: &mut Directory) {
220        dir.unique_id = self.next_unique_id;
221        self.next_unique_id += 1;
222
223        for file in &mut dir.files {
224            file.unique_id = self.next_unique_id;
225            self.next_unique_id += 1;
226        }
227
228        for subdir in &mut dir.subdirs {
229            self.assign_unique_ids(subdir);
230        }
231    }
232
233    /// Allocate a single sector within the UDF partition
234    pub fn allocate_udf_block(&mut self) -> u32 {
235        let block = self.next_udf_block;
236        self.next_udf_block += 1;
237        block
238    }
239
240    /// Get the next available unique ID
241    pub fn next_unique_id(&mut self) -> u64 {
242        let id = self.next_unique_id;
243        self.next_unique_id += 1;
244        id
245    }
246}
247
248/// Information about the disk layout after planning
249#[derive(Debug, Clone)]
250pub struct LayoutInfo {
251    /// Sector where volume descriptor sequence ends
252    pub vds_end: u32,
253    /// Starting sector of UDF partition
254    pub udf_partition_start: u32,
255    /// Number of sectors reserved for UDF metadata
256    pub udf_metadata_sectors: u32,
257    /// Starting sector for file data
258    pub file_data_start: u32,
259    /// Ending sector for file data
260    pub file_data_end: u32,
261    /// Total sectors needed for the image
262    pub total_sectors: u32,
263    /// Complete collision-free UDF directory/ICB plan.
264    pub(crate) udf_root: UdfDirectoryLayout,
265}
266
267/// Planned UDF metadata blocks for one directory and its descendants.
268#[derive(Debug, Clone)]
269pub(crate) struct UdfDirectoryLayout {
270    pub(crate) icb_block: u32,
271    pub(crate) parent_icb_block: u32,
272    pub(crate) fid_block: u32,
273    pub(crate) fid_bytes: usize,
274    pub(crate) file_icb_blocks: Vec<u32>,
275    pub(crate) subdirs: Vec<UdfDirectoryLayout>,
276}
277
278fn cs0_filename_len(name: &str) -> Result<usize> {
279    let content_len = if name.chars().all(|ch| (ch as u32) <= 0xff) {
280        name.chars().count()
281    } else {
282        name.encode_utf16()
283            .count()
284            .checked_mul(2)
285            .ok_or_else(|| Error::InvalidConfig("UDF filename encoded length overflow".into()))?
286    };
287    let encoded_len = content_len + 1;
288    if encoded_len > u8::MAX as usize {
289        return Err(Error::InvalidPath(format!(
290            "UDF filename exceeds the 255-byte encoded limit: {name}"
291        )));
292    }
293    Ok(encoded_len)
294}
295
296impl core::fmt::Display for LayoutInfo {
297    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
298        write!(
299            f,
300            "layout: {} total sectors (files at sectors {}-{})",
301            self.total_sectors, self.file_data_start, self.file_data_end
302        )
303    }
304}
305
306impl LayoutInfo {
307    /// Get the UDF partition length in sectors
308    pub fn udf_partition_length(&self) -> u32 {
309        self.total_sectors - self.udf_partition_start
310    }
311}
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316    use crate::tree::FileEntry;
317
318    #[test]
319    fn test_layout_empty_tree() {
320        let mut tree = FileTree::new();
321        let options = OpticalImageOptions::default();
322        let mut layout = LayoutManager::new(2048);
323
324        let info = layout.layout_files(&mut tree, &options).unwrap();
325        assert!(info.file_data_end >= info.file_data_start);
326    }
327
328    #[test]
329    fn test_layout_with_files() {
330        let mut tree = FileTree::new();
331        tree.add_file(FileEntry::from_buffer("test.txt", vec![0u8; 4096]));
332        tree.add_file(FileEntry::from_buffer("small.txt", vec![0u8; 100]));
333
334        let options = OpticalImageOptions::default();
335        let mut layout = LayoutManager::new(2048);
336
337        layout.layout_files(&mut tree, &options).unwrap();
338
339        // First file should have a valid extent
340        let file1 = tree.root.files.first().unwrap();
341        assert!(file1.extent.sector > 0);
342        assert_eq!(file1.extent.length, 4096);
343
344        // Second file should come after first
345        let file2 = tree.root.files.get(1).unwrap();
346        assert!(file2.extent.sector > file1.extent.sector);
347    }
348
349    #[test]
350    fn test_layout_zero_size_file() {
351        let mut tree = FileTree::new();
352        tree.add_file(FileEntry::from_buffer("empty.txt", vec![]));
353
354        let options = OpticalImageOptions::default();
355        let mut layout = LayoutManager::new(2048);
356
357        layout.layout_files(&mut tree, &options).unwrap();
358
359        let file = tree.root.files.first().unwrap();
360        assert_eq!(file.extent.sector, 0);
361        assert_eq!(file.extent.length, 0);
362    }
363}