Skip to main content

arcbox_ext4/
dir.rs

1// Directory entry writing (Formatter) and parsing (Reader).
2//
3// On-disk directory entries are variable-length records packed into blocks.
4// Each entry has an 8-byte header (DirectoryEntry) followed by the name bytes
5// and padding to a 4-byte boundary.
6
7use crate::constants::*;
8use crate::types::*;
9use std::io::{self, Write};
10
11// ---------------------------------------------------------------------------
12// Helpers
13// ---------------------------------------------------------------------------
14
15/// Round `n` up to the next multiple of `align`.
16#[inline]
17fn align_up(n: usize, align: usize) -> usize {
18    (n + align - 1) & !(align - 1)
19}
20
21/// Determine the directory-entry file type from an inode mode.
22fn dir_file_type(mode: u16) -> u8 {
23    FileType::from_mode(mode) as u8
24}
25
26// ---------------------------------------------------------------------------
27// Write path (Formatter)
28// ---------------------------------------------------------------------------
29
30/// Write a single directory entry.
31///
32/// `left` tracks the number of remaining bytes in the current directory block.
33/// If there is not enough room for both this entry and a trailing terminator
34/// (12 bytes minimum), the current block is finished first with a zero-inode
35/// terminator before writing this entry at the start of a new block.
36///
37/// When `link_inode` is `Some`, the entry's inode number is taken from
38/// `link_inode` and the file type is derived from `link_mode` (the target
39/// inode's mode).  Otherwise, `inode` and `mode` are used directly.
40pub fn write_dir_entry<W: Write>(
41    writer: &mut W,
42    name: &str,
43    inode: u32,
44    mode: u16,
45    link_inode: Option<u32>,
46    link_mode: Option<u16>,
47    block_size: u32,
48    left: &mut i32,
49) -> io::Result<()> {
50    let name_bytes = name.as_bytes();
51    let entry_size = align_up(DirectoryEntry::SIZE + name_bytes.len(), 4);
52
53    // Minimum trailing entry is 12 bytes (header 8 + 4 for alignment).
54    let min_trailing = 12;
55
56    // If the current block does not have room for this entry plus a trailing
57    // terminator, finish the block first.
58    if (*left as usize) < entry_size + min_trailing {
59        finish_dir_entry_block(writer, left, block_size)?;
60    }
61
62    // Resolve inode number and file type for hard links.
63    let actual_inode = link_inode.unwrap_or(inode);
64    let actual_mode = link_mode.unwrap_or(mode);
65
66    let entry = DirectoryEntry {
67        inode: actual_inode,
68        rec_len: entry_size as u16,
69        name_len: name_bytes.len() as u8,
70        file_type: dir_file_type(actual_mode),
71    };
72
73    let mut header_buf = [0u8; DirectoryEntry::SIZE];
74    entry.write_to(&mut header_buf);
75    writer.write_all(&header_buf)?;
76
77    // Write name bytes.
78    writer.write_all(name_bytes)?;
79
80    // Write padding zeros to align to 4 bytes.
81    let padding = entry_size - DirectoryEntry::SIZE - name_bytes.len();
82    if padding > 0 {
83        let zeros = [0u8; 4];
84        writer.write_all(&zeros[..padding])?;
85    }
86
87    *left -= entry_size as i32;
88
89    Ok(())
90}
91
92/// Finish the current directory entry block by writing a zero-inode terminator
93/// entry that consumes all remaining space.
94///
95/// If `left` is already <= 0 (block boundary was exactly reached), reset
96/// `left` to `block_size` and return without writing.
97pub fn finish_dir_entry_block<W: Write>(
98    writer: &mut W,
99    left: &mut i32,
100    block_size: u32,
101) -> io::Result<()> {
102    if *left <= 0 {
103        *left = block_size as i32;
104        return Ok(());
105    }
106
107    let remaining = *left as usize;
108
109    // Write a terminator entry: inode=0, rec_len=remaining, name_len=0, file_type=0.
110    let term = DirectoryEntry {
111        inode: 0,
112        rec_len: remaining as u16,
113        name_len: 0,
114        file_type: 0,
115    };
116
117    let mut header_buf = [0u8; DirectoryEntry::SIZE];
118    term.write_to(&mut header_buf);
119    writer.write_all(&header_buf)?;
120
121    // Fill remaining bytes with zeros.
122    let fill = remaining - DirectoryEntry::SIZE;
123    if fill > 0 {
124        let zeros = vec![0u8; fill];
125        writer.write_all(&zeros)?;
126    }
127
128    *left = block_size as i32;
129
130    Ok(())
131}
132
133// ---------------------------------------------------------------------------
134// Read path (Reader)
135// ---------------------------------------------------------------------------
136
137/// Parse directory entries from a block of raw data.
138///
139/// Returns a vector of `(name, inode_number)` pairs.  Deleted entries
140/// (inode == 0) and entries with zero-length names are skipped.
141pub fn parse_dir_entries(data: &[u8]) -> Vec<(String, u32)> {
142    let mut entries = Vec::new();
143    let mut offset = 0;
144
145    while offset + DirectoryEntry::SIZE <= data.len() {
146        let entry = DirectoryEntry::read_from(&data[offset..]);
147
148        // rec_len of 0 means we'd loop forever.
149        if entry.rec_len == 0 {
150            break;
151        }
152
153        if entry.inode != 0 && entry.name_len > 0 {
154            let name_start = offset + DirectoryEntry::SIZE;
155            let name_end = name_start + entry.name_len as usize;
156
157            if name_end <= data.len() {
158                let name = String::from_utf8_lossy(&data[name_start..name_end]).into_owned();
159                entries.push((name, entry.inode));
160            }
161        }
162
163        offset += entry.rec_len as usize;
164    }
165
166    entries
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    #[test]
174    fn test_align_up() {
175        assert_eq!(align_up(0, 4), 0);
176        assert_eq!(align_up(1, 4), 4);
177        assert_eq!(align_up(4, 4), 4);
178        assert_eq!(align_up(5, 4), 8);
179        assert_eq!(align_up(8, 4), 8);
180        assert_eq!(align_up(9, 4), 12);
181    }
182
183    #[test]
184    fn test_write_and_parse_dir_entries() {
185        let block_size = 4096u32;
186        let mut buf = Vec::new();
187        let mut left = block_size as i32;
188
189        // Write "." entry.
190        write_dir_entry(
191            &mut buf, ".", 2, file_mode::S_IFDIR | 0o755,
192            None, None, block_size, &mut left,
193        ).unwrap();
194
195        // Write ".." entry.
196        write_dir_entry(
197            &mut buf, "..", 2, file_mode::S_IFDIR | 0o755,
198            None, None, block_size, &mut left,
199        ).unwrap();
200
201        // Write a regular file entry.
202        write_dir_entry(
203            &mut buf, "hello.txt", 11, file_mode::S_IFREG | 0o644,
204            None, None, block_size, &mut left,
205        ).unwrap();
206
207        // Finish the block.
208        finish_dir_entry_block(&mut buf, &mut left, block_size).unwrap();
209
210        assert_eq!(buf.len(), block_size as usize);
211        assert_eq!(left, block_size as i32);
212
213        // Parse back.
214        let entries = parse_dir_entries(&buf);
215        assert_eq!(entries.len(), 3);
216        assert_eq!(entries[0], (".".to_string(), 2));
217        assert_eq!(entries[1], ("..".to_string(), 2));
218        assert_eq!(entries[2], ("hello.txt".to_string(), 11));
219    }
220
221    #[test]
222    fn test_finish_dir_entry_block_at_boundary() {
223        let block_size = 4096u32;
224        let mut buf = Vec::new();
225        let mut left = 0i32;
226
227        // Already at a block boundary -- should just reset `left`.
228        finish_dir_entry_block(&mut buf, &mut left, block_size).unwrap();
229        assert_eq!(buf.len(), 0);
230        assert_eq!(left, block_size as i32);
231    }
232
233    #[test]
234    fn test_hard_link_entry() {
235        let block_size = 4096u32;
236        let mut buf = Vec::new();
237        let mut left = block_size as i32;
238
239        // Write a hard link entry: display name "link.txt", but pointing to inode 42
240        // which is a regular file.
241        write_dir_entry(
242            &mut buf, "link.txt", 99, file_mode::S_IFREG | 0o644,
243            Some(42), Some(file_mode::S_IFREG | 0o644),
244            block_size, &mut left,
245        ).unwrap();
246
247        finish_dir_entry_block(&mut buf, &mut left, block_size).unwrap();
248
249        let entries = parse_dir_entries(&buf);
250        assert_eq!(entries.len(), 1);
251        // The inode number should be the link target (42), not the original (99).
252        assert_eq!(entries[0], ("link.txt".to_string(), 42));
253    }
254
255    #[test]
256    fn test_parse_empty_block() {
257        let data = vec![0u8; 4096];
258        let entries = parse_dir_entries(&data);
259        // All-zero block has rec_len=0 in the first entry, so parsing stops immediately.
260        assert!(entries.is_empty());
261    }
262}