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        let inode = u32::from_le_bytes(buf[0..4].try_into().unwrap());
65        let rec_len = u16::from_le_bytes(buf[4..6].try_into().unwrap());
66        let name_len = buf[6] as usize;
67        let file_type = DirEntryType::from(buf[7]);
68
69        let needed = HEADER_LEN + name_len;
70        if buf.len() < needed {
71            return Err(Ext4Error::TooShort {
72                structure: "DirEntry name",
73                expected: needed,
74                found: buf.len(),
75            });
76        }
77
78        let name = buf[HEADER_LEN..HEADER_LEN + name_len].to_vec();
79
80        Ok(Self {
81            inode,
82            rec_len,
83            name,
84            file_type,
85        })
86    }
87
88    /// Return the entry name as a lossy UTF-8 string.
89    pub fn name_str(&self) -> String {
90        String::from_utf8_lossy(&self.name).into_owned()
91    }
92
93    /// An entry with inode == 0 has been deleted (the space is reused by rec_len).
94    pub fn is_deleted(&self) -> bool {
95        self.inode == 0
96    }
97}
98
99/// Iterate through every directory entry in a directory data block.
100///
101/// Stops at the end of the block or if `rec_len` is zero (would loop forever).
102/// Entries with `inode == 0` are included — callers decide whether to skip them.
103pub fn parse_dir_block(block: &[u8]) -> Vec<DirEntry> {
104    let mut entries = Vec::new();
105    let mut offset = 0usize;
106
107    while offset < block.len() {
108        let remaining = &block[offset..];
109        match DirEntry::parse(remaining) {
110            Ok(entry) => {
111                let rec_len = entry.rec_len as usize;
112                entries.push(entry);
113                if rec_len == 0 {
114                    break; // guard against infinite loop on corrupt data
115                }
116                offset += rec_len;
117            }
118            Err(_) => break,
119        }
120    }
121
122    entries
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[test]
130    fn parse_dir_entry() {
131        let mut buf = vec![0u8; 20];
132        buf[0] = 12; // inode
133        buf[4] = 20; // rec_len
134        buf[6] = 5; // name_len
135        buf[7] = 1; // file_type (regular)
136        buf[8..13].copy_from_slice(b"hello");
137        let entry = DirEntry::parse(&buf).unwrap();
138        assert_eq!(entry.inode, 12);
139        assert_eq!(entry.rec_len, 20);
140        assert_eq!(entry.name, b"hello");
141        assert_eq!(entry.file_type, DirEntryType::RegularFile);
142    }
143
144    #[test]
145    fn parse_dot_entries() {
146        let mut buf = vec![0u8; 12];
147        buf[0] = 2;
148        buf[4] = 12;
149        buf[6] = 1;
150        buf[7] = 2;
151        buf[8] = b'.';
152        let entry = DirEntry::parse(&buf).unwrap();
153        assert_eq!(entry.name, b".");
154        assert_eq!(entry.file_type, DirEntryType::Directory);
155    }
156
157    #[test]
158    fn skip_deleted_entry() {
159        let mut buf = vec![0u8; 12];
160        buf[0] = 0;
161        buf[4] = 12;
162        buf[6] = 3;
163        buf[7] = 1;
164        buf[8..11].copy_from_slice(b"foo");
165        let entry = DirEntry::parse(&buf).unwrap();
166        assert_eq!(entry.inode, 0);
167    }
168
169    #[test]
170    fn reject_too_short() {
171        let buf = vec![0u8; 4];
172        let err = DirEntry::parse(&buf).unwrap_err();
173        assert!(matches!(err, crate::error::Ext4Error::TooShort { .. }));
174    }
175
176    #[test]
177    fn is_deleted_flag() {
178        let mut buf = vec![0u8; 12];
179        buf[0] = 0;
180        buf[4] = 12;
181        buf[6] = 3;
182        buf[7] = 1;
183        buf[8..11].copy_from_slice(b"del");
184        let entry = DirEntry::parse(&buf).unwrap();
185        assert!(entry.is_deleted());
186    }
187
188    #[test]
189    fn name_str_utf8() {
190        let mut buf = vec![0u8; 13];
191        buf[0] = 5;
192        buf[4] = 13;
193        buf[6] = 5;
194        buf[7] = 1;
195        buf[8..13].copy_from_slice(b"world");
196        let entry = DirEntry::parse(&buf).unwrap();
197        assert_eq!(entry.name_str(), "world");
198    }
199
200    #[test]
201    fn parse_dir_block_multiple_entries() {
202        // Two back-to-back 12-byte entries.
203        let mut block = vec![0u8; 24];
204        // Entry 1: inode=1, rec_len=12, name_len=1, type=2 (dir), name="."
205        block[0] = 1;
206        block[4] = 12;
207        block[6] = 1;
208        block[7] = 2;
209        block[8] = b'.';
210        // Entry 2: inode=2, rec_len=12, name_len=2, type=2 (dir), name=".."
211        block[12] = 2;
212        block[16] = 12;
213        block[18] = 2;
214        block[19] = 2;
215        block[20] = b'.';
216        block[21] = b'.';
217        let entries = parse_dir_block(&block);
218        assert_eq!(entries.len(), 2);
219        assert_eq!(entries[0].name, b".");
220        assert_eq!(entries[1].name, b"..");
221    }
222
223    #[test]
224    fn dir_entry_type_from_u8() {
225        assert_eq!(DirEntryType::from(0), DirEntryType::Unknown);
226        assert_eq!(DirEntryType::from(1), DirEntryType::RegularFile);
227        assert_eq!(DirEntryType::from(2), DirEntryType::Directory);
228        assert_eq!(DirEntryType::from(3), DirEntryType::CharDevice);
229        assert_eq!(DirEntryType::from(4), DirEntryType::BlockDevice);
230        assert_eq!(DirEntryType::from(5), DirEntryType::Fifo);
231        assert_eq!(DirEntryType::from(6), DirEntryType::Socket);
232        assert_eq!(DirEntryType::from(7), DirEntryType::Symlink);
233        assert_eq!(DirEntryType::from(255), DirEntryType::Unknown);
234    }
235}