Skip to main content

ext4fs/ondisk/
dir_entry.rs

1#![forbid(unsafe_code)]
2use crate::error::{Ext4Error, Result};
3
4/// File type stored in the `dir_entry` `file_type` byte (ext2 `dir_entry_2` format).
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum DirEntryType {
7    Unknown,
8    RegularFile,
9    Directory,
10    CharDevice,
11    BlockDevice,
12    Fifo,
13    Socket,
14    Symlink,
15}
16
17impl From<u8> for DirEntryType {
18    fn from(v: u8) -> Self {
19        match v {
20            1 => Self::RegularFile,
21            2 => Self::Directory,
22            3 => Self::CharDevice,
23            4 => Self::BlockDevice,
24            5 => Self::Fifo,
25            6 => Self::Socket,
26            7 => Self::Symlink,
27            _ => Self::Unknown,
28        }
29    }
30}
31
32/// A single directory entry parsed from an ext4 directory data block.
33///
34/// Layout (all little-endian):
35///   0..4  inode    u32
36///   4..6  `rec_len`  u16
37///   6     `name_len` u8
38///   7     `file_type` u8
39///   `8..8+name_len`  name bytes
40#[derive(Debug, Clone)]
41pub struct DirEntry {
42    pub inode: u32,
43    pub rec_len: u16,
44    pub name: Vec<u8>,
45    pub file_type: DirEntryType,
46}
47
48const HEADER_LEN: usize = 8;
49
50impl DirEntry {
51    /// Parse a single directory entry from the start of `buf`.
52    ///
53    /// Returns `Err(TooShort)` if the buffer is shorter than the 8-byte header
54    /// or shorter than the full entry (header + name).
55    pub fn parse(buf: &[u8]) -> Result<Self> {
56        if buf.len() < HEADER_LEN {
57            return Err(Ext4Error::TooShort {
58                structure: "DirEntry",
59                expected: HEADER_LEN,
60                found: buf.len(),
61            });
62        }
63
64        // In-bounds by the `buf.len() < HEADER_LEN` (8) guard above: offsets
65        // 0..4 and 4..6 lie within the first 8 bytes, so safe_read yields the
66        // real value (never the out-of-range 0). Behavior-preserving.
67        let inode = safe_read::le_u32(buf, 0);
68        let rec_len = safe_read::le_u16(buf, 4);
69        let name_len = buf[6] as usize;
70        let file_type = DirEntryType::from(buf[7]);
71
72        let needed = HEADER_LEN + name_len;
73        if buf.len() < needed {
74            return Err(Ext4Error::TooShort {
75                structure: "DirEntry name",
76                expected: needed,
77                found: buf.len(),
78            });
79        }
80
81        let name = buf[HEADER_LEN..HEADER_LEN + name_len].to_vec();
82
83        Ok(Self {
84            inode,
85            rec_len,
86            name,
87            file_type,
88        })
89    }
90
91    /// Return the entry name as a lossy UTF-8 string.
92    pub fn name_str(&self) -> String {
93        String::from_utf8_lossy(&self.name).into_owned()
94    }
95
96    /// An entry with inode == 0 has been deleted (the space is reused by `rec_len`).
97    pub fn is_deleted(&self) -> bool {
98        self.inode == 0
99    }
100}
101
102/// Iterate through every directory entry in a directory data block.
103///
104/// Stops at the end of the block or if `rec_len` is zero (would loop forever).
105/// Entries with `inode == 0` are included — callers decide whether to skip them.
106pub fn parse_dir_block(block: &[u8]) -> Vec<DirEntry> {
107    let mut entries = Vec::new();
108    let mut offset = 0usize;
109
110    while offset < block.len() {
111        let remaining = &block[offset..];
112        match DirEntry::parse(remaining) {
113            Ok(entry) => {
114                let rec_len = entry.rec_len as usize;
115                entries.push(entry);
116                if rec_len == 0 {
117                    break; // guard against infinite loop on corrupt data
118                }
119                offset += rec_len;
120            }
121            Err(_) => break,
122        }
123    }
124
125    entries
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn parse_dir_entry() {
134        let mut buf = vec![0u8; 20];
135        buf[0] = 12; // inode
136        buf[4] = 20; // rec_len
137        buf[6] = 5; // name_len
138        buf[7] = 1; // file_type (regular)
139        buf[8..13].copy_from_slice(b"hello");
140        let entry = DirEntry::parse(&buf).unwrap();
141        assert_eq!(entry.inode, 12);
142        assert_eq!(entry.rec_len, 20);
143        assert_eq!(entry.name, b"hello");
144        assert_eq!(entry.file_type, DirEntryType::RegularFile);
145    }
146
147    #[test]
148    fn parse_dot_entries() {
149        let mut buf = vec![0u8; 12];
150        buf[0] = 2;
151        buf[4] = 12;
152        buf[6] = 1;
153        buf[7] = 2;
154        buf[8] = b'.';
155        let entry = DirEntry::parse(&buf).unwrap();
156        assert_eq!(entry.name, b".");
157        assert_eq!(entry.file_type, DirEntryType::Directory);
158    }
159
160    #[test]
161    fn skip_deleted_entry() {
162        let mut buf = vec![0u8; 12];
163        buf[0] = 0;
164        buf[4] = 12;
165        buf[6] = 3;
166        buf[7] = 1;
167        buf[8..11].copy_from_slice(b"foo");
168        let entry = DirEntry::parse(&buf).unwrap();
169        assert_eq!(entry.inode, 0);
170    }
171
172    #[test]
173    fn reject_too_short() {
174        let buf = vec![0u8; 4];
175        let err = DirEntry::parse(&buf).unwrap_err();
176        assert!(matches!(err, crate::error::Ext4Error::TooShort { .. }));
177    }
178
179    #[test]
180    fn is_deleted_flag() {
181        let mut buf = vec![0u8; 12];
182        buf[0] = 0;
183        buf[4] = 12;
184        buf[6] = 3;
185        buf[7] = 1;
186        buf[8..11].copy_from_slice(b"del");
187        let entry = DirEntry::parse(&buf).unwrap();
188        assert!(entry.is_deleted());
189    }
190
191    #[test]
192    fn name_str_utf8() {
193        let mut buf = vec![0u8; 13];
194        buf[0] = 5;
195        buf[4] = 13;
196        buf[6] = 5;
197        buf[7] = 1;
198        buf[8..13].copy_from_slice(b"world");
199        let entry = DirEntry::parse(&buf).unwrap();
200        assert_eq!(entry.name_str(), "world");
201    }
202
203    #[test]
204    fn parse_dir_block_multiple_entries() {
205        // Two back-to-back 12-byte entries.
206        let mut block = vec![0u8; 24];
207        // Entry 1: inode=1, rec_len=12, name_len=1, type=2 (dir), name="."
208        block[0] = 1;
209        block[4] = 12;
210        block[6] = 1;
211        block[7] = 2;
212        block[8] = b'.';
213        // Entry 2: inode=2, rec_len=12, name_len=2, type=2 (dir), name=".."
214        block[12] = 2;
215        block[16] = 12;
216        block[18] = 2;
217        block[19] = 2;
218        block[20] = b'.';
219        block[21] = b'.';
220        let entries = parse_dir_block(&block);
221        assert_eq!(entries.len(), 2);
222        assert_eq!(entries[0].name, b".");
223        assert_eq!(entries[1].name, b"..");
224    }
225
226    #[test]
227    fn dir_entry_type_from_u8() {
228        assert_eq!(DirEntryType::from(0), DirEntryType::Unknown);
229        assert_eq!(DirEntryType::from(1), DirEntryType::RegularFile);
230        assert_eq!(DirEntryType::from(2), DirEntryType::Directory);
231        assert_eq!(DirEntryType::from(3), DirEntryType::CharDevice);
232        assert_eq!(DirEntryType::from(4), DirEntryType::BlockDevice);
233        assert_eq!(DirEntryType::from(5), DirEntryType::Fifo);
234        assert_eq!(DirEntryType::from(6), DirEntryType::Socket);
235        assert_eq!(DirEntryType::from(7), DirEntryType::Symlink);
236        assert_eq!(DirEntryType::from(255), DirEntryType::Unknown);
237    }
238}