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::{CdError, CdResult};
26use crate::options::CdOptions;
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: &CdOptions,
64    ) -> CdResult<LayoutInfo> {
65        // Calculate starting positions based on what we need to write
66        let vds_end = self.calculate_vds_end(options);
67
68        // UDF partition starts after AVDP at sector 256
69        let udf_partition_start = 257;
70
71        // Reserve space for UDF structures (FSD, directory entries)
72        // We'll use a conservative estimate and may adjust later
73        let udf_metadata_sectors = self.estimate_udf_metadata_sectors(tree);
74
75        // File data starts after UDF metadata (within UDF partition)
76        self.next_udf_block = udf_metadata_sectors;
77        self.next_file_sector = udf_partition_start + udf_metadata_sectors;
78
79        // Assign extents to all files
80        self.assign_file_extents(&mut tree.root)?;
81
82        // Assign unique IDs to directories and files
83        self.assign_unique_ids(&mut tree.root);
84
85        let file_data_end = self.next_file_sector;
86
87        Ok(LayoutInfo {
88            vds_end,
89            udf_partition_start,
90            udf_metadata_sectors,
91            file_data_start: udf_partition_start + udf_metadata_sectors,
92            file_data_end,
93            total_sectors: file_data_end + 100, // Reserve space for ISO path tables etc.
94        })
95    }
96
97    /// Calculate where the Volume Descriptor Sequence ends
98    fn calculate_vds_end(&self, options: &CdOptions) -> u32 {
99        let mut sector = 16; // VDS starts at sector 16
100
101        // ISO Primary Volume Descriptor
102        if options.iso.enabled {
103            sector += 1;
104        }
105
106        // UDF VRS (BEA01, NSR02/03, TEA01) - actually at 16-18 with ISO
107        // In hybrid format, VRS is interleaved with ISO VD
108        // For simplicity, we'll place VRS at sectors 16-18
109
110        // Joliet SVD
111        if options.iso.joliet.is_some() {
112            sector += 1;
113        }
114
115        // ISO 9660:1999 EVD
116        if options.iso.long_filenames {
117            sector += 1;
118        }
119
120        // Boot record (El-Torito)
121        if options.boot.is_some() {
122            sector += 1;
123        }
124
125        // Volume Set Terminator
126        sector += 1;
127
128        sector
129    }
130
131    /// Estimate how many sectors we need for UDF metadata
132    fn estimate_udf_metadata_sectors(&self, tree: &FileTree) -> u32 {
133        // File Set Descriptor: 1 sector
134        // Root directory File Entry: 1 sector
135        // Root directory FIDs: ceil(entry_count * ~40 bytes / sector_size)
136        // For each subdirectory: File Entry + FIDs
137
138        let total_dirs = tree.total_dirs();
139        let total_files = tree.total_files();
140
141        // Each directory needs at least 2 sectors (File Entry + FIDs)
142        // Plus some buffer for larger directories
143        let estimated = (total_dirs * 2 + total_files / 50 + 10) as u32;
144
145        // Round up to be safe
146        estimated.max(20)
147    }
148
149    /// Recursively assign file extents
150    fn assign_file_extents(&mut self, dir: &mut Directory) -> CdResult<()> {
151        // Assign extents to files
152        for file in &mut dir.files {
153            let size = file.size().map_err(CdError::Io)?;
154
155            if size == 0 {
156                // Zero-size files have no extent (sector 0 per ISO spec)
157                file.extent = FileExtent::new(0, 0);
158            } else {
159                file.extent = FileExtent::new(self.next_file_sector, size);
160                let sectors = file.extent.sector_count(self.sector_size);
161                self.next_file_sector += sectors;
162            }
163        }
164
165        // Recursively handle subdirectories
166        for subdir in &mut dir.subdirs {
167            self.assign_file_extents(subdir)?;
168        }
169
170        Ok(())
171    }
172
173    /// Assign unique IDs to all directories and files
174    fn assign_unique_ids(&mut self, dir: &mut Directory) {
175        dir.unique_id = self.next_unique_id;
176        self.next_unique_id += 1;
177
178        for file in &mut dir.files {
179            file.unique_id = self.next_unique_id;
180            self.next_unique_id += 1;
181        }
182
183        for subdir in &mut dir.subdirs {
184            self.assign_unique_ids(subdir);
185        }
186    }
187
188    /// Allocate a single sector within the UDF partition
189    pub fn allocate_udf_block(&mut self) -> u32 {
190        let block = self.next_udf_block;
191        self.next_udf_block += 1;
192        block
193    }
194
195    /// Get the next available unique ID
196    pub fn next_unique_id(&mut self) -> u64 {
197        let id = self.next_unique_id;
198        self.next_unique_id += 1;
199        id
200    }
201}
202
203/// Information about the disk layout after planning
204#[derive(Debug, Clone)]
205pub struct LayoutInfo {
206    /// Sector where volume descriptor sequence ends
207    pub vds_end: u32,
208    /// Starting sector of UDF partition
209    pub udf_partition_start: u32,
210    /// Number of sectors reserved for UDF metadata
211    pub udf_metadata_sectors: u32,
212    /// Starting sector for file data
213    pub file_data_start: u32,
214    /// Ending sector for file data
215    pub file_data_end: u32,
216    /// Total sectors needed for the image
217    pub total_sectors: u32,
218}
219
220impl core::fmt::Display for LayoutInfo {
221    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
222        write!(
223            f,
224            "layout: {} total sectors (files at sectors {}-{})",
225            self.total_sectors, self.file_data_start, self.file_data_end
226        )
227    }
228}
229
230impl LayoutInfo {
231    /// Get the UDF partition length in sectors
232    pub fn udf_partition_length(&self) -> u32 {
233        self.total_sectors - self.udf_partition_start
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240    use crate::tree::FileEntry;
241
242    #[test]
243    fn test_layout_empty_tree() {
244        let mut tree = FileTree::new();
245        let options = CdOptions::default();
246        let mut layout = LayoutManager::new(2048);
247
248        let info = layout.layout_files(&mut tree, &options).unwrap();
249        assert!(info.file_data_end >= info.file_data_start);
250    }
251
252    #[test]
253    fn test_layout_with_files() {
254        let mut tree = FileTree::new();
255        tree.add_file(FileEntry::from_buffer("test.txt", vec![0u8; 4096]));
256        tree.add_file(FileEntry::from_buffer("small.txt", vec![0u8; 100]));
257
258        let options = CdOptions::default();
259        let mut layout = LayoutManager::new(2048);
260
261        let info = layout.layout_files(&mut tree, &options).unwrap();
262
263        // First file should have a valid extent
264        let file1 = tree.root.files.get(0).unwrap();
265        assert!(file1.extent.sector > 0);
266        assert_eq!(file1.extent.length, 4096);
267
268        // Second file should come after first
269        let file2 = tree.root.files.get(1).unwrap();
270        assert!(file2.extent.sector > file1.extent.sector);
271    }
272
273    #[test]
274    fn test_layout_zero_size_file() {
275        let mut tree = FileTree::new();
276        tree.add_file(FileEntry::from_buffer("empty.txt", vec![]));
277
278        let options = CdOptions::default();
279        let mut layout = LayoutManager::new(2048);
280
281        layout.layout_files(&mut tree, &options).unwrap();
282
283        let file = tree.root.files.get(0).unwrap();
284        assert_eq!(file.extent.sector, 0);
285        assert_eq!(file.extent.length, 0);
286    }
287}