Skip to main content

hadris_common/types/
layout.rs

1//! File and directory layout types for metadata-only writing.
2//!
3//! These types require the `alloc` feature for heap allocation.
4
5extern crate alloc;
6
7use alloc::string::String;
8use alloc::vec;
9use alloc::vec::Vec;
10use hadris_path::{Component, VPath};
11
12use super::extent::{Extent, FileType, Timestamps};
13
14fn path_parts(path: &str) -> Option<Vec<&str>> {
15    let mut parts = Vec::new();
16    for component in VPath::new(path).components() {
17        match component {
18            Component::Root | Component::Current => {}
19            Component::Parent => return None,
20            Component::Normal(component) => parts.push(component),
21        }
22    }
23    Some(parts)
24}
25
26/// File layout with pre-calculated extent (for metadata-only writing).
27///
28/// This structure describes where a file's data is located on disk
29/// without containing the actual data. Used for operations that only
30/// need to write/update filesystem metadata.
31#[derive(Debug, Clone)]
32pub struct FileLayout {
33    /// File name (without path).
34    pub name: String,
35    /// Location and size of file data on disk.
36    pub extent: Extent,
37    /// Type of the file entry.
38    pub file_type: FileType,
39    /// File timestamps.
40    pub timestamps: Timestamps,
41    /// Filesystem-specific attribute flags.
42    pub attributes: u32,
43    /// Optional symlink target (only valid if file_type is Symlink).
44    pub symlink_target: Option<String>,
45}
46
47impl FileLayout {
48    /// Creates a new file layout.
49    pub fn new(name: impl Into<String>, extent: Extent) -> Self {
50        Self {
51            name: name.into(),
52            extent,
53            file_type: FileType::RegularFile,
54            timestamps: Timestamps::default(),
55            attributes: 0,
56            symlink_target: None,
57        }
58    }
59
60    /// Sets the file type.
61    pub fn with_type(mut self, file_type: FileType) -> Self {
62        self.file_type = file_type;
63        self
64    }
65
66    /// Sets the timestamps.
67    pub fn with_timestamps(mut self, timestamps: Timestamps) -> Self {
68        self.timestamps = timestamps;
69        self
70    }
71
72    /// Sets the attributes.
73    pub fn with_attributes(mut self, attributes: u32) -> Self {
74        self.attributes = attributes;
75        self
76    }
77
78    /// Sets the symlink target (only meaningful for symlinks).
79    pub fn with_symlink_target(mut self, target: impl Into<String>) -> Self {
80        self.symlink_target = Some(target.into());
81        self
82    }
83
84    /// Returns the file size in bytes.
85    #[inline]
86    pub fn size(&self) -> u64 {
87        self.extent.length
88    }
89}
90
91/// Directory layout (tree of files).
92///
93/// Represents a complete directory tree with pre-calculated extents
94/// for all files. Used for metadata-only writing operations.
95#[derive(Debug, Clone, Default)]
96pub struct DirectoryLayout {
97    /// Directory name (empty string for root).
98    pub name: String,
99    /// Files in this directory.
100    pub files: Vec<FileLayout>,
101    /// Subdirectories.
102    pub subdirs: Vec<DirectoryLayout>,
103    /// Directory timestamps.
104    pub timestamps: Timestamps,
105    /// Filesystem-specific attribute flags.
106    pub attributes: u32,
107    /// Extent for the directory entry itself (if applicable).
108    pub extent: Option<Extent>,
109}
110
111impl DirectoryLayout {
112    /// Creates a new empty directory layout.
113    pub fn new(name: impl Into<String>) -> Self {
114        Self {
115            name: name.into(),
116            files: Vec::new(),
117            subdirs: Vec::new(),
118            timestamps: Timestamps::default(),
119            attributes: 0,
120            extent: None,
121        }
122    }
123
124    /// Creates a root directory layout (empty name).
125    pub fn root() -> Self {
126        Self::new("")
127    }
128
129    /// Adds a file to this directory.
130    pub fn add_file(&mut self, file: FileLayout) {
131        self.files.push(file);
132    }
133
134    /// Adds a subdirectory to this directory.
135    pub fn add_subdir(&mut self, subdir: DirectoryLayout) {
136        self.subdirs.push(subdir);
137    }
138
139    /// Sets the timestamps.
140    pub fn with_timestamps(mut self, timestamps: Timestamps) -> Self {
141        self.timestamps = timestamps;
142        self
143    }
144
145    /// Sets the extent for this directory entry.
146    pub fn with_extent(mut self, extent: Extent) -> Self {
147        self.extent = Some(extent);
148        self
149    }
150
151    /// Returns the total number of files in this directory (not recursive).
152    #[inline]
153    pub fn file_count(&self) -> usize {
154        self.files.len()
155    }
156
157    /// Returns the total number of subdirectories in this directory (not recursive).
158    #[inline]
159    pub fn subdir_count(&self) -> usize {
160        self.subdirs.len()
161    }
162
163    /// Returns the total number of entries (files + subdirs).
164    #[inline]
165    pub fn entry_count(&self) -> usize {
166        self.files.len() + self.subdirs.len()
167    }
168
169    /// Returns an iterator over all files (recursive, depth-first).
170    pub fn iter_files(&self) -> impl Iterator<Item = (&str, &FileLayout)> {
171        FileIterator::new(self)
172    }
173
174    /// Finds a file by path (e.g., "docs/readme.txt").
175    pub fn find_file(&self, path: &str) -> Option<&FileLayout> {
176        let parts = path_parts(path)?;
177        self.find_file_parts(&parts)
178    }
179
180    /// Finds a file by path parts.
181    fn find_file_parts(&self, parts: &[&str]) -> Option<&FileLayout> {
182        if parts.is_empty() {
183            return None;
184        }
185
186        if parts.len() == 1 {
187            // Looking for a file in this directory
188            self.files.iter().find(|f| f.name == parts[0])
189        } else {
190            // Looking in a subdirectory
191            self.subdirs
192                .iter()
193                .find(|d| d.name == parts[0])
194                .and_then(|d| d.find_file_parts(&parts[1..]))
195        }
196    }
197
198    /// Finds a mutable file reference by path.
199    pub fn find_file_mut(&mut self, path: &str) -> Option<&mut FileLayout> {
200        let parts = path_parts(path)?;
201        self.find_file_parts_mut(&parts)
202    }
203
204    /// Finds a mutable file reference by path parts.
205    fn find_file_parts_mut(&mut self, parts: &[&str]) -> Option<&mut FileLayout> {
206        if parts.is_empty() {
207            return None;
208        }
209
210        if parts.len() == 1 {
211            self.files.iter_mut().find(|f| f.name == parts[0])
212        } else {
213            self.subdirs
214                .iter_mut()
215                .find(|d| d.name == parts[0])
216                .and_then(|d| d.find_file_parts_mut(&parts[1..]))
217        }
218    }
219
220    /// Finds or creates a subdirectory by path.
221    pub fn get_or_create_dir(&mut self, path: &str) -> &mut DirectoryLayout {
222        let parts = path_parts(path).unwrap_or_default();
223        self.get_or_create_dir_parts(&parts)
224    }
225
226    /// Finds or creates a subdirectory by path parts.
227    fn get_or_create_dir_parts(&mut self, parts: &[&str]) -> &mut DirectoryLayout {
228        if parts.is_empty() {
229            return self;
230        }
231
232        let name = parts[0];
233
234        // Find or create the subdirectory
235        let idx = self.subdirs.iter().position(|d| d.name == name);
236        let idx = match idx {
237            Some(i) => i,
238            None => {
239                self.subdirs.push(DirectoryLayout::new(name));
240                self.subdirs.len() - 1
241            }
242        };
243
244        self.subdirs[idx].get_or_create_dir_parts(&parts[1..])
245    }
246
247    /// Removes a file by path. Returns the removed file if found.
248    pub fn remove_file(&mut self, path: &str) -> Option<FileLayout> {
249        let parts = path_parts(path)?;
250        self.remove_file_parts(&parts)
251    }
252
253    /// Removes a file by path parts.
254    fn remove_file_parts(&mut self, parts: &[&str]) -> Option<FileLayout> {
255        if parts.is_empty() {
256            return None;
257        }
258
259        if parts.len() == 1 {
260            let idx = self.files.iter().position(|f| f.name == parts[0])?;
261            Some(self.files.remove(idx))
262        } else {
263            self.subdirs
264                .iter_mut()
265                .find(|d| d.name == parts[0])
266                .and_then(|d| d.remove_file_parts(&parts[1..]))
267        }
268    }
269}
270
271/// Iterator over all files in a directory tree (depth-first).
272struct FileIterator<'a> {
273    stack: Vec<(&'a str, &'a DirectoryLayout, usize, usize)>,
274    path_prefix: String,
275}
276
277impl<'a> FileIterator<'a> {
278    fn new(root: &'a DirectoryLayout) -> Self {
279        Self {
280            stack: vec![("", root, 0, 0)],
281            path_prefix: String::new(),
282        }
283    }
284}
285
286impl<'a> Iterator for FileIterator<'a> {
287    type Item = (&'a str, &'a FileLayout);
288
289    fn next(&mut self) -> Option<Self::Item> {
290        while let Some((name, dir, file_idx, subdir_idx)) = self.stack.pop() {
291            // Update path prefix when entering a new directory
292            if !name.is_empty() {
293                if !self.path_prefix.is_empty() {
294                    self.path_prefix.push('/');
295                }
296                self.path_prefix.push_str(name);
297            }
298
299            // Return files first
300            if file_idx < dir.files.len() {
301                // Push state for next file
302                self.stack.push((name, dir, file_idx + 1, subdir_idx));
303                let file = &dir.files[file_idx];
304                // We can't easily return the full path here without allocation
305                // so we return just the file name
306                return Some((&file.name, file));
307            }
308
309            // Then recurse into subdirectories
310            if subdir_idx < dir.subdirs.len() {
311                // Push state for next subdir
312                self.stack.push((name, dir, file_idx, subdir_idx + 1));
313                let subdir = &dir.subdirs[subdir_idx];
314                self.stack.push((&subdir.name, subdir, 0, 0));
315                continue;
316            }
317
318            // Pop path segment when leaving directory
319            if !name.is_empty() {
320                if let Some(idx) = self.path_prefix.rfind('/') {
321                    self.path_prefix.truncate(idx);
322                } else {
323                    self.path_prefix.clear();
324                }
325            }
326        }
327        None
328    }
329}
330
331/// Tracks which sectors are allocated in an image.
332///
333/// Uses a bitmap to efficiently track sector usage, supporting
334/// allocation and deallocation operations.
335#[derive(Debug, Clone)]
336pub struct AllocationMap {
337    /// Bitmap of used sectors (1 = used, 0 = free).
338    bitmap: Vec<u8>,
339    /// Total sectors in image.
340    total_sectors: u32,
341    /// Next free sector hint for faster allocation.
342    next_free: u32,
343}
344
345impl AllocationMap {
346    /// Creates a new allocation map for the given number of sectors.
347    pub fn new(total_sectors: u32) -> Self {
348        let bitmap_size = (total_sectors as usize).div_ceil(8);
349        Self {
350            bitmap: alloc::vec![0u8; bitmap_size],
351            total_sectors,
352            next_free: 0,
353        }
354    }
355
356    /// Creates an allocation map from a list of existing used extents.
357    pub fn from_existing(used_extents: &[Extent], total_sectors: u32, sector_size: u32) -> Self {
358        let mut map = Self::new(total_sectors);
359        for extent in used_extents {
360            map.mark_used(*extent, sector_size);
361        }
362        map
363    }
364
365    /// Allocates a contiguous region of the given size.
366    ///
367    /// Returns `None` if there isn't enough contiguous free space.
368    pub fn allocate(&mut self, size_bytes: u64, sector_size: u32) -> Option<Extent> {
369        if size_bytes == 0 {
370            return Some(Extent::new(self.next_free, 0));
371        }
372
373        let sectors_needed = size_bytes.div_ceil(sector_size as u64) as u32;
374
375        // Start searching from next_free hint
376        let mut start = self.next_free;
377        let mut consecutive = 0u32;
378        let mut found_start = start;
379
380        while start + consecutive < self.total_sectors {
381            let current = start + consecutive;
382            if self.is_free(current) {
383                if consecutive == 0 {
384                    found_start = current;
385                }
386                consecutive += 1;
387                if consecutive >= sectors_needed {
388                    // Found enough space
389                    let extent = Extent::new(found_start, size_bytes);
390                    self.mark_used(extent, sector_size);
391                    return Some(extent);
392                }
393            } else {
394                // Reset search
395                consecutive = 0;
396                start = current + 1;
397                found_start = start;
398            }
399        }
400
401        // Try from beginning if we started after 0
402        if self.next_free > 0 {
403            start = 0;
404            consecutive = 0;
405            found_start = 0;
406
407            while start + consecutive < self.next_free {
408                let current = start + consecutive;
409                if self.is_free(current) {
410                    if consecutive == 0 {
411                        found_start = current;
412                    }
413                    consecutive += 1;
414                    if consecutive >= sectors_needed {
415                        let extent = Extent::new(found_start, size_bytes);
416                        self.mark_used(extent, sector_size);
417                        return Some(extent);
418                    }
419                } else {
420                    consecutive = 0;
421                    start = current + 1;
422                    found_start = start;
423                }
424            }
425        }
426
427        None
428    }
429
430    /// Marks the given extent as used.
431    pub fn mark_used(&mut self, extent: Extent, sector_size: u32) {
432        let end = extent.end_sector(sector_size);
433        for sector in extent.sector..end {
434            self.set_bit(sector, true);
435        }
436        // Update next_free hint
437        if extent.sector == self.next_free {
438            self.next_free = end;
439            // Skip any used sectors
440            while self.next_free < self.total_sectors && !self.is_free(self.next_free) {
441                self.next_free += 1;
442            }
443        }
444    }
445
446    /// Marks the given extent as free.
447    pub fn mark_free(&mut self, extent: Extent, sector_size: u32) {
448        let end = extent.end_sector(sector_size);
449        for sector in extent.sector..end {
450            self.set_bit(sector, false);
451        }
452        // Update next_free hint if this freed earlier sectors
453        if extent.sector < self.next_free {
454            self.next_free = extent.sector;
455        }
456    }
457
458    /// Checks if a sector is free.
459    #[inline]
460    pub fn is_free(&self, sector: u32) -> bool {
461        if sector >= self.total_sectors {
462            return false;
463        }
464        let byte_idx = sector as usize / 8;
465        let bit_idx = sector % 8;
466        (self.bitmap[byte_idx] & (1 << bit_idx)) == 0
467    }
468
469    /// Checks if a sector is used.
470    #[inline]
471    pub fn is_used(&self, sector: u32) -> bool {
472        !self.is_free(sector)
473    }
474
475    /// Returns the total number of sectors.
476    #[inline]
477    pub fn total_sectors(&self) -> u32 {
478        self.total_sectors
479    }
480
481    /// Returns the number of free sectors.
482    pub fn free_sectors(&self) -> u32 {
483        let mut count = 0u32;
484        for sector in 0..self.total_sectors {
485            if self.is_free(sector) {
486                count += 1;
487            }
488        }
489        count
490    }
491
492    /// Returns the number of used sectors.
493    #[inline]
494    pub fn used_sectors(&self) -> u32 {
495        self.total_sectors - self.free_sectors()
496    }
497
498    /// Sets or clears a bit in the bitmap.
499    #[inline]
500    fn set_bit(&mut self, sector: u32, used: bool) {
501        if sector >= self.total_sectors {
502            return;
503        }
504        let byte_idx = sector as usize / 8;
505        let bit_idx = sector % 8;
506        if used {
507            self.bitmap[byte_idx] |= 1 << bit_idx;
508        } else {
509            self.bitmap[byte_idx] &= !(1 << bit_idx);
510        }
511    }
512
513    /// Reserves the first N sectors (typically for metadata).
514    pub fn reserve_initial(&mut self, sectors: u32, sector_size: u32) {
515        let extent = Extent::new(0, sectors as u64 * sector_size as u64);
516        self.mark_used(extent, sector_size);
517    }
518}
519
520#[cfg(test)]
521mod tests {
522    use super::*;
523
524    #[test]
525    fn test_file_layout() {
526        let file = FileLayout::new("test.txt", Extent::new(100, 1024))
527            .with_type(FileType::RegularFile)
528            .with_attributes(0x20);
529
530        assert_eq!(file.name, "test.txt");
531        assert_eq!(file.size(), 1024);
532        assert_eq!(file.extent.sector, 100);
533    }
534
535    #[test]
536    fn test_directory_layout() {
537        let mut root = DirectoryLayout::root();
538        root.add_file(FileLayout::new("file1.txt", Extent::new(100, 1024)));
539
540        let mut subdir = DirectoryLayout::new("docs");
541        subdir.add_file(FileLayout::new("readme.md", Extent::new(200, 512)));
542        root.add_subdir(subdir);
543
544        assert_eq!(root.file_count(), 1);
545        assert_eq!(root.subdir_count(), 1);
546
547        let file = root.find_file("file1.txt");
548        assert!(file.is_some());
549        assert_eq!(file.unwrap().name, "file1.txt");
550
551        let nested = root.find_file("docs/readme.md");
552        assert!(nested.is_some());
553        assert_eq!(nested.unwrap().name, "readme.md");
554    }
555
556    #[test]
557    fn test_get_or_create_dir() {
558        let mut root = DirectoryLayout::root();
559        let dir = root.get_or_create_dir("docs/api/v1");
560
561        assert_eq!(dir.name, "v1");
562        assert_eq!(root.subdirs[0].name, "docs");
563        assert_eq!(root.subdirs[0].subdirs[0].name, "api");
564        assert_eq!(root.subdirs[0].subdirs[0].subdirs[0].name, "v1");
565    }
566
567    #[test]
568    fn test_remove_file() {
569        let mut root = DirectoryLayout::root();
570        root.add_file(FileLayout::new("test.txt", Extent::new(100, 1024)));
571
572        let removed = root.remove_file("test.txt");
573        assert!(removed.is_some());
574        assert_eq!(removed.unwrap().name, "test.txt");
575        assert_eq!(root.file_count(), 0);
576    }
577
578    #[test]
579    fn path_traversal_rejects_parent_escape() {
580        let mut root = DirectoryLayout::root();
581        assert!(root.find_file("../file.txt").is_none());
582        assert!(root.remove_file("../file.txt").is_none());
583        assert_eq!(root.get_or_create_dir("../docs").name, "");
584    }
585
586    #[test]
587    fn test_allocation_map() {
588        let mut map = AllocationMap::new(100);
589        assert_eq!(map.total_sectors(), 100);
590        assert_eq!(map.free_sectors(), 100);
591
592        // Allocate 10 sectors (20480 bytes with 2048-byte sectors)
593        let extent = map.allocate(20480, 2048).unwrap();
594        assert_eq!(extent.sector, 0);
595        assert_eq!(extent.sector_count(2048), 10);
596        assert_eq!(map.free_sectors(), 90);
597
598        // Allocate more
599        let extent2 = map.allocate(4096, 2048).unwrap();
600        assert_eq!(extent2.sector, 10);
601
602        // Free the first allocation
603        map.mark_free(extent, 2048);
604        assert_eq!(map.free_sectors(), 98);
605
606        // New allocation should reuse freed space
607        let extent3 = map.allocate(2048, 2048).unwrap();
608        assert_eq!(extent3.sector, 0);
609    }
610
611    #[test]
612    fn test_allocation_map_reserve() {
613        let mut map = AllocationMap::new(100);
614        map.reserve_initial(16, 2048); // Reserve sectors 0-15
615
616        let extent = map.allocate(2048, 2048).unwrap();
617        assert_eq!(extent.sector, 16); // Should start after reserved area
618    }
619}