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 + sector_size as u64 - 1) / 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() { "/" } else { &self.name };
160        write!(f, "{} ({} files, {} subdirs)", name, self.files.len(), self.subdirs.len())
161    }
162}
163
164impl Directory {
165    /// Create a new empty directory
166    pub fn new(name: impl Into<String>) -> Self {
167        Self {
168            name: Arc::new(name.into()),
169            files: Vec::new(),
170            subdirs: Vec::new(),
171            unique_id: 0,
172            udf_icb_location: 0,
173            iso_extent: FileExtent::default(),
174        }
175    }
176
177    /// Create an empty root directory
178    pub fn root() -> Self {
179        Self::new("")
180    }
181
182    /// Add a file to this directory
183    pub fn add_file(&mut self, file: FileEntry) {
184        self.files.push(file);
185    }
186
187    /// Add a subdirectory
188    pub fn add_subdir(&mut self, dir: Directory) {
189        self.subdirs.push(dir);
190    }
191
192    /// Find a file by name in this directory (not recursive)
193    pub fn find_file(&self, name: &str) -> Option<&FileEntry> {
194        self.files.iter().find(|f| f.name.as_str() == name)
195    }
196
197    /// Find a file by name in this directory (not recursive, mutable)
198    pub fn find_file_mut(&mut self, name: &str) -> Option<&mut FileEntry> {
199        self.files.iter_mut().find(|f| f.name.as_str() == name)
200    }
201
202    /// Find a subdirectory by name
203    pub fn find_subdir(&self, name: &str) -> Option<&Directory> {
204        self.subdirs.iter().find(|d| d.name.as_str() == name)
205    }
206
207    /// Find a subdirectory by name (mutable)
208    pub fn find_subdir_mut(&mut self, name: &str) -> Option<&mut Directory> {
209        self.subdirs.iter_mut().find(|d| d.name.as_str() == name)
210    }
211
212    /// Get the total number of files (recursive)
213    pub fn total_files(&self) -> usize {
214        self.files.len() + self.subdirs.iter().map(|d| d.total_files()).sum::<usize>()
215    }
216
217    /// Get the total number of directories (recursive, including self)
218    pub fn total_dirs(&self) -> usize {
219        1 + self.subdirs.iter().map(|d| d.total_dirs()).sum::<usize>()
220    }
221
222    /// Iterate over all files recursively
223    pub fn iter_files(&self) -> Vec<&FileEntry> {
224        let mut result: Vec<&FileEntry> = self.files.iter().collect();
225        for subdir in &self.subdirs {
226            result.extend(subdir.iter_files());
227        }
228        result
229    }
230
231    /// Sort files and directories by name
232    pub fn sort(&mut self) {
233        self.files.sort_by(|a, b| a.name.cmp(&b.name));
234        self.subdirs.sort_by(|a, b| a.name.cmp(&b.name));
235        for subdir in &mut self.subdirs {
236            subdir.sort();
237        }
238    }
239}
240
241/// The complete file tree for a CD/DVD image
242#[derive(Debug)]
243pub struct FileTree {
244    /// Root directory
245    pub root: Directory,
246}
247
248impl core::fmt::Display for FileTree {
249    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
250        write!(f, "{} files, {} directories", self.total_files(), self.total_dirs())
251    }
252}
253
254impl Default for FileTree {
255    fn default() -> Self {
256        Self::new()
257    }
258}
259
260impl FileTree {
261    /// Create a new empty file tree
262    pub fn new() -> Self {
263        Self {
264            root: Directory::root(),
265        }
266    }
267
268    /// Add a file to the root directory
269    pub fn add_file(&mut self, file: FileEntry) {
270        self.root.add_file(file);
271    }
272
273    /// Add a directory to the root
274    pub fn add_dir(&mut self, dir: Directory) {
275        self.root.add_subdir(dir);
276    }
277
278    /// Find a file by path (e.g., "dir/subdir/file.txt")
279    pub fn find_file(&self, path: &str) -> Option<&FileEntry> {
280        let parts: Vec<&str> = path.split('/').collect();
281        if parts.is_empty() {
282            return None;
283        }
284
285        let mut current = &self.root;
286        for (i, part) in parts.iter().enumerate() {
287            if i == parts.len() - 1 {
288                // Last part is the file name
289                return current.find_file(part);
290            } else {
291                // Navigate to subdirectory
292                current = current.find_subdir(part)?;
293            }
294        }
295        None
296    }
297
298    /// Get the total number of files
299    pub fn total_files(&self) -> usize {
300        self.root.total_files()
301    }
302
303    /// Get the total number of directories (including root)
304    pub fn total_dirs(&self) -> usize {
305        self.root.total_dirs()
306    }
307
308    /// Sort all files and directories by name
309    pub fn sort(&mut self) {
310        self.root.sort();
311    }
312
313    /// Create a file tree from a filesystem directory
314    pub fn from_fs(path: &std::path::Path) -> std::io::Result<Self> {
315        let mut tree = Self::new();
316        tree.root = Self::read_dir_recursive(path)?;
317        tree.root.name = Arc::new(String::new()); // Root has empty name
318        Ok(tree)
319    }
320
321    fn read_dir_recursive(path: &std::path::Path) -> std::io::Result<Directory> {
322        let name = path
323            .file_name()
324            .and_then(|n| n.to_str())
325            .unwrap_or("")
326            .to_string();
327
328        let mut dir = Directory::new(name);
329
330        for entry in std::fs::read_dir(path)? {
331            let entry = entry?;
332            let file_type = entry.file_type()?;
333            let entry_name = entry.file_name().to_string_lossy().to_string();
334
335            if file_type.is_file() {
336                dir.add_file(FileEntry::from_path(entry_name, entry.path()));
337            } else if file_type.is_dir() {
338                dir.add_subdir(Self::read_dir_recursive(&entry.path())?);
339            }
340            // Skip symlinks and other file types for now
341        }
342
343        // Sort for consistent ordering
344        dir.sort();
345        Ok(dir)
346    }
347}
348
349#[cfg(test)]
350mod tests {
351    use super::*;
352
353    #[test]
354    fn test_file_extent() {
355        let extent = FileExtent::new(100, 4096);
356        assert_eq!(extent.sector, 100);
357        assert_eq!(extent.length, 4096);
358        assert_eq!(extent.sector_count(2048), 2);
359
360        let empty = FileExtent::default();
361        assert!(empty.is_empty());
362        assert_eq!(empty.sector_count(2048), 0);
363    }
364
365    #[test]
366    fn test_directory_tree() {
367        let mut tree = FileTree::new();
368
369        // Add a file to root
370        tree.add_file(FileEntry::from_buffer("readme.txt", b"Hello".to_vec()));
371
372        // Add a subdirectory with a file
373        let mut subdir = Directory::new("docs");
374        subdir.add_file(FileEntry::from_buffer(
375            "guide.txt",
376            b"Guide content".to_vec(),
377        ));
378        tree.add_dir(subdir);
379
380        assert_eq!(tree.total_files(), 2);
381        assert_eq!(tree.total_dirs(), 2);
382
383        // Find files
384        assert!(tree.find_file("readme.txt").is_some());
385        assert!(tree.find_file("docs/guide.txt").is_some());
386        assert!(tree.find_file("nonexistent.txt").is_none());
387    }
388}