Skip to main content

hadris_cd/
tree.rs

1//! Shared directory tree representation for hybrid ISO+UDF images
2//!
3//! This module provides a file tree structure that can be used by both
4//! ISO 9660 and UDF filesystem writers. The key insight is that both
5//! filesystems point to the same physical file data on disk.
6
7use std::path::PathBuf;
8use std::sync::Arc;
9
10/// Represents where a file's data lives on disk
11#[derive(Debug, Clone, Copy, Default)]
12pub struct FileExtent {
13    /// Starting sector (logical block address)
14    pub sector: u32,
15    /// Length of the file data in bytes
16    pub length: u64,
17}
18
19impl core::fmt::Display for FileExtent {
20    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
21        if self.is_empty() {
22            write!(f, "empty")
23        } else {
24            write!(f, "sector {} ({} bytes)", self.sector, self.length)
25        }
26    }
27}
28
29impl FileExtent {
30    /// Create a new file extent
31    pub fn new(sector: u32, length: u64) -> Self {
32        Self { sector, length }
33    }
34
35    /// Check if this extent is empty (zero-size file)
36    pub fn is_empty(&self) -> bool {
37        self.length == 0
38    }
39
40    /// Calculate the number of sectors needed for this extent
41    pub fn sector_count(&self, sector_size: usize) -> u32 {
42        if self.length == 0 {
43            0
44        } else {
45            self.length.div_ceil(sector_size as u64) as u32
46        }
47    }
48}
49
50/// Source of file content
51pub enum FileData {
52    /// In-memory buffer
53    Buffer(Vec<u8>),
54    /// File on disk to read from
55    Path(PathBuf),
56}
57
58impl std::fmt::Debug for FileData {
59    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60        match self {
61            Self::Buffer(b) => write!(f, "Buffer({} bytes)", b.len()),
62            Self::Path(p) => write!(f, "Path({:?})", p),
63        }
64    }
65}
66
67impl core::fmt::Display for FileData {
68    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
69        match self {
70            Self::Buffer(b) => write!(f, "buffer ({} bytes)", b.len()),
71            Self::Path(p) => write!(f, "path ({})", p.display()),
72        }
73    }
74}
75
76impl FileData {
77    /// Get the size of the file data
78    pub fn size(&self) -> std::io::Result<u64> {
79        match self {
80            Self::Buffer(b) => Ok(b.len() as u64),
81            Self::Path(p) => Ok(std::fs::metadata(p)?.len()),
82        }
83    }
84
85    /// Read the file content into a buffer
86    pub fn read_all(&self) -> std::io::Result<Vec<u8>> {
87        match self {
88            Self::Buffer(b) => Ok(b.clone()),
89            Self::Path(p) => std::fs::read(p),
90        }
91    }
92}
93
94/// A file entry in the directory tree
95#[derive(Debug)]
96pub struct FileEntry {
97    /// File name
98    pub name: Arc<String>,
99    /// Physical location on disk (filled during layout phase)
100    pub extent: FileExtent,
101    /// Source of file content
102    pub data: FileData,
103    /// Unique ID for this file (used by UDF)
104    pub unique_id: u64,
105}
106
107impl core::fmt::Display for FileEntry {
108    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
109        write!(f, "{}", self.name)
110    }
111}
112
113impl FileEntry {
114    /// Create a new file entry from in-memory data
115    pub fn from_buffer(name: impl Into<String>, data: Vec<u8>) -> Self {
116        Self {
117            name: Arc::new(name.into()),
118            extent: FileExtent::default(),
119            data: FileData::Buffer(data),
120            unique_id: 0,
121        }
122    }
123
124    /// Create a new file entry from a filesystem path
125    pub fn from_path(name: impl Into<String>, path: PathBuf) -> Self {
126        Self {
127            name: Arc::new(name.into()),
128            extent: FileExtent::default(),
129            data: FileData::Path(path),
130            unique_id: 0,
131        }
132    }
133
134    /// Get the file size
135    pub fn size(&self) -> std::io::Result<u64> {
136        self.data.size()
137    }
138}
139
140/// A directory in the file tree
141#[derive(Debug)]
142pub struct Directory {
143    /// Directory name (empty for root)
144    pub name: Arc<String>,
145    /// Files in this directory
146    pub files: Vec<FileEntry>,
147    /// Subdirectories
148    pub subdirs: Vec<Directory>,
149    /// Unique ID for this directory (used by UDF)
150    pub unique_id: u64,
151    /// ICB location for UDF (logical block within partition)
152    pub udf_icb_location: u32,
153    /// Directory extent for ISO (sector and size)
154    pub iso_extent: FileExtent,
155}
156
157impl core::fmt::Display for Directory {
158    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
159        let name = if self.name.is_empty() {
160            "/"
161        } else {
162            &self.name
163        };
164        write!(
165            f,
166            "{} ({} files, {} subdirs)",
167            name,
168            self.files.len(),
169            self.subdirs.len()
170        )
171    }
172}
173
174impl Directory {
175    /// Create a new empty directory
176    pub fn new(name: impl Into<String>) -> Self {
177        Self {
178            name: Arc::new(name.into()),
179            files: Vec::new(),
180            subdirs: Vec::new(),
181            unique_id: 0,
182            udf_icb_location: 0,
183            iso_extent: FileExtent::default(),
184        }
185    }
186
187    /// Create an empty root directory
188    pub fn root() -> Self {
189        Self::new("")
190    }
191
192    /// Add a file to this directory
193    pub fn add_file(&mut self, file: FileEntry) {
194        self.files.push(file);
195    }
196
197    /// Add a subdirectory
198    pub fn add_subdir(&mut self, dir: Directory) {
199        self.subdirs.push(dir);
200    }
201
202    /// Find a file by name in this directory (not recursive)
203    pub fn find_file(&self, name: &str) -> Option<&FileEntry> {
204        self.files.iter().find(|f| f.name.as_str() == name)
205    }
206
207    /// Find a file by name in this directory (not recursive, mutable)
208    pub fn find_file_mut(&mut self, name: &str) -> Option<&mut FileEntry> {
209        self.files.iter_mut().find(|f| f.name.as_str() == name)
210    }
211
212    /// Find a subdirectory by name
213    pub fn find_subdir(&self, name: &str) -> Option<&Directory> {
214        self.subdirs.iter().find(|d| d.name.as_str() == name)
215    }
216
217    /// Find a subdirectory by name (mutable)
218    pub fn find_subdir_mut(&mut self, name: &str) -> Option<&mut Directory> {
219        self.subdirs.iter_mut().find(|d| d.name.as_str() == name)
220    }
221
222    /// Get the total number of files (recursive)
223    pub fn total_files(&self) -> usize {
224        self.files.len() + self.subdirs.iter().map(|d| d.total_files()).sum::<usize>()
225    }
226
227    /// Get the total number of directories (recursive, including self)
228    pub fn total_dirs(&self) -> usize {
229        1 + self.subdirs.iter().map(|d| d.total_dirs()).sum::<usize>()
230    }
231
232    /// Iterate over all files recursively
233    pub fn iter_files(&self) -> Vec<&FileEntry> {
234        let mut result: Vec<&FileEntry> = self.files.iter().collect();
235        for subdir in &self.subdirs {
236            result.extend(subdir.iter_files());
237        }
238        result
239    }
240
241    /// Sort files and directories by name
242    pub fn sort(&mut self) {
243        self.files.sort_by(|a, b| a.name.cmp(&b.name));
244        self.subdirs.sort_by(|a, b| a.name.cmp(&b.name));
245        for subdir in &mut self.subdirs {
246            subdir.sort();
247        }
248    }
249}
250
251/// The complete file tree for a CD/DVD image
252#[derive(Debug)]
253pub struct FileTree {
254    /// Root directory
255    pub root: Directory,
256}
257
258impl core::fmt::Display for FileTree {
259    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
260        write!(
261            f,
262            "{} files, {} directories",
263            self.total_files(),
264            self.total_dirs()
265        )
266    }
267}
268
269impl Default for FileTree {
270    fn default() -> Self {
271        Self::new()
272    }
273}
274
275impl FileTree {
276    /// Create a new empty file tree
277    pub fn new() -> Self {
278        Self {
279            root: Directory::root(),
280        }
281    }
282
283    /// Add a file to the root directory
284    pub fn add_file(&mut self, file: FileEntry) {
285        self.root.add_file(file);
286    }
287
288    /// Add a directory to the root
289    pub fn add_dir(&mut self, dir: Directory) {
290        self.root.add_subdir(dir);
291    }
292
293    /// Find a file by path (e.g., "dir/subdir/file.txt")
294    pub fn find_file(&self, path: &str) -> Option<&FileEntry> {
295        let parts: Vec<&str> = path.split('/').collect();
296        if parts.is_empty() {
297            return None;
298        }
299
300        let mut current = &self.root;
301        for (i, part) in parts.iter().enumerate() {
302            if i == parts.len() - 1 {
303                // Last part is the file name
304                return current.find_file(part);
305            } else {
306                // Navigate to subdirectory
307                current = current.find_subdir(part)?;
308            }
309        }
310        None
311    }
312
313    /// Get the total number of files
314    pub fn total_files(&self) -> usize {
315        self.root.total_files()
316    }
317
318    /// Get the total number of directories (including root)
319    pub fn total_dirs(&self) -> usize {
320        self.root.total_dirs()
321    }
322
323    /// Sort all files and directories by name
324    pub fn sort(&mut self) {
325        self.root.sort();
326    }
327
328    /// Create a file tree from a filesystem directory
329    pub fn from_fs(path: &std::path::Path) -> std::io::Result<Self> {
330        let mut tree = Self::new();
331        tree.root = Self::read_dir_recursive(path)?;
332        tree.root.name = Arc::new(String::new()); // Root has empty name
333        Ok(tree)
334    }
335
336    fn read_dir_recursive(path: &std::path::Path) -> std::io::Result<Directory> {
337        let name = path
338            .file_name()
339            .and_then(|n| n.to_str())
340            .unwrap_or("")
341            .to_string();
342
343        let mut dir = Directory::new(name);
344
345        for entry in std::fs::read_dir(path)? {
346            let entry = entry?;
347            let file_type = entry.file_type()?;
348            let entry_name = entry.file_name().to_string_lossy().to_string();
349
350            if file_type.is_file() {
351                dir.add_file(FileEntry::from_path(entry_name, entry.path()));
352            } else if file_type.is_dir() {
353                dir.add_subdir(Self::read_dir_recursive(&entry.path())?);
354            }
355            // Skip symlinks and other file types for now
356        }
357
358        // Sort for consistent ordering
359        dir.sort();
360        Ok(dir)
361    }
362}
363
364#[cfg(test)]
365mod tests {
366    use super::*;
367
368    #[test]
369    fn test_file_extent() {
370        let extent = FileExtent::new(100, 4096);
371        assert_eq!(extent.sector, 100);
372        assert_eq!(extent.length, 4096);
373        assert_eq!(extent.sector_count(2048), 2);
374
375        let empty = FileExtent::default();
376        assert!(empty.is_empty());
377        assert_eq!(empty.sector_count(2048), 0);
378    }
379
380    #[test]
381    fn test_directory_tree() {
382        let mut tree = FileTree::new();
383
384        // Add a file to root
385        tree.add_file(FileEntry::from_buffer("readme.txt", b"Hello".to_vec()));
386
387        // Add a subdirectory with a file
388        let mut subdir = Directory::new("docs");
389        subdir.add_file(FileEntry::from_buffer(
390            "guide.txt",
391            b"Guide content".to_vec(),
392        ));
393        tree.add_dir(subdir);
394
395        assert_eq!(tree.total_files(), 2);
396        assert_eq!(tree.total_dirs(), 2);
397
398        // Find files
399        assert!(tree.find_file("readme.txt").is_some());
400        assert!(tree.find_file("docs/guide.txt").is_some());
401        assert!(tree.find_file("nonexistent.txt").is_none());
402    }
403}