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