Skip to main content

hadris_common/types/
extent.rs

1//! Basic extent and file metadata types for filesystem operations.
2//!
3//! These types are no-std compatible and don't require allocation.
4
5use core::fmt;
6
7/// A contiguous region on disk, identified by a starting sector and byte length.
8///
9/// # Example
10///
11/// ```rust
12/// use hadris_common::types::extent::Extent;
13///
14/// let extent = Extent::new(100, 4096);
15/// assert_eq!(extent.sector, 100);
16/// assert_eq!(extent.length, 4096);
17/// assert_eq!(extent.sector_count(2048), 2);
18/// assert_eq!(extent.end_sector(2048), 102);
19/// assert!(!extent.is_empty());
20/// ```
21#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
22#[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))]
23#[repr(C)]
24pub struct Extent {
25    /// Starting logical sector (LBA).
26    pub sector: u32,
27    /// Padding for alignment.
28    _padding: u32,
29    /// Size in bytes.
30    pub length: u64,
31}
32
33impl Extent {
34    /// Creates a new extent.
35    #[inline]
36    pub const fn new(sector: u32, length: u64) -> Self {
37        Self {
38            sector,
39            _padding: 0,
40            length,
41        }
42    }
43
44    /// Returns the end sector (exclusive) based on the given sector size.
45    #[inline]
46    pub const fn end_sector(&self, sector_size: u32) -> u32 {
47        let sectors = self.length.div_ceil(sector_size as u64);
48        self.sector + sectors as u32
49    }
50
51    /// Returns the number of sectors this extent spans.
52    #[inline]
53    pub const fn sector_count(&self, sector_size: u32) -> u32 {
54        self.length.div_ceil(sector_size as u64) as u32
55    }
56
57    /// Checks if this extent overlaps with another.
58    #[inline]
59    pub const fn overlaps(&self, other: &Extent, sector_size: u32) -> bool {
60        let self_end = self.end_sector(sector_size);
61        let other_end = other.end_sector(sector_size);
62        self.sector < other_end && other.sector < self_end
63    }
64
65    /// Checks if this extent is empty (zero length).
66    #[inline]
67    pub const fn is_empty(&self) -> bool {
68        self.length == 0
69    }
70}
71
72impl fmt::Display for Extent {
73    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74        write!(f, "sector {} ({} bytes)", self.sector, self.length)
75    }
76}
77
78/// File type classification.
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
80#[repr(u8)]
81pub enum FileType {
82    /// A regular file containing data.
83    #[default]
84    RegularFile = 0,
85    /// A directory containing other entries.
86    Directory = 1,
87    /// A symbolic link to another file or directory.
88    Symlink = 2,
89}
90
91impl FileType {
92    /// Returns true if this is a directory.
93    #[inline]
94    pub const fn is_directory(&self) -> bool {
95        matches!(self, FileType::Directory)
96    }
97
98    /// Returns true if this is a regular file.
99    #[inline]
100    pub const fn is_file(&self) -> bool {
101        matches!(self, FileType::RegularFile)
102    }
103
104    /// Returns true if this is a symbolic link.
105    #[inline]
106    pub const fn is_symlink(&self) -> bool {
107        matches!(self, FileType::Symlink)
108    }
109}
110
111impl fmt::Display for FileType {
112    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113        match self {
114            FileType::RegularFile => write!(f, "file"),
115            FileType::Directory => write!(f, "directory"),
116            FileType::Symlink => write!(f, "symlink"),
117        }
118    }
119}
120
121/// Generic timestamps using Unix epoch (seconds since 1970-01-01 00:00:00 UTC).
122///
123/// This representation is no-std compatible and can be converted to
124/// platform-specific types when needed.
125#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
126#[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))]
127#[repr(C)]
128pub struct Timestamps {
129    /// Creation time (seconds since Unix epoch).
130    pub created: u64,
131    /// Last modification time (seconds since Unix epoch).
132    pub modified: u64,
133    /// Last access time (seconds since Unix epoch).
134    pub accessed: u64,
135}
136
137impl Timestamps {
138    /// Creates new timestamps with all fields set to the same value.
139    #[inline]
140    pub const fn new(time: u64) -> Self {
141        Self {
142            created: time,
143            modified: time,
144            accessed: time,
145        }
146    }
147
148    /// Creates timestamps with explicit values for each field.
149    #[inline]
150    pub const fn with_times(created: u64, modified: u64, accessed: u64) -> Self {
151        Self {
152            created,
153            modified,
154            accessed,
155        }
156    }
157
158    /// Returns the most recent timestamp.
159    #[inline]
160    pub const fn most_recent(&self) -> u64 {
161        let max = if self.created > self.modified {
162            self.created
163        } else {
164            self.modified
165        };
166        if self.accessed > max {
167            self.accessed
168        } else {
169            max
170        }
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    #[test]
179    fn test_extent_basics() {
180        let extent = Extent::new(100, 4096);
181        assert_eq!(extent.sector, 100);
182        assert_eq!(extent.length, 4096);
183        assert!(!extent.is_empty());
184
185        // With 2048-byte sectors, 4096 bytes = 2 sectors
186        assert_eq!(extent.sector_count(2048), 2);
187        assert_eq!(extent.end_sector(2048), 102);
188    }
189
190    #[test]
191    fn test_extent_overlap() {
192        let a = Extent::new(100, 4096); // sectors 100-101
193        let b = Extent::new(101, 2048); // sector 101
194        let c = Extent::new(102, 2048); // sector 102
195
196        assert!(a.overlaps(&b, 2048));
197        assert!(!a.overlaps(&c, 2048));
198        assert!(!b.overlaps(&c, 2048));
199    }
200
201    #[test]
202    fn test_file_type() {
203        assert!(FileType::Directory.is_directory());
204        assert!(FileType::RegularFile.is_file());
205        assert!(FileType::Symlink.is_symlink());
206    }
207
208    #[test]
209    fn test_timestamps() {
210        let ts = Timestamps::new(1000);
211        assert_eq!(ts.created, 1000);
212        assert_eq!(ts.modified, 1000);
213        assert_eq!(ts.accessed, 1000);
214
215        let ts2 = Timestamps::with_times(100, 200, 150);
216        assert_eq!(ts2.most_recent(), 200);
217    }
218}