1use crate::checksum::Checksummer;
17use crate::error::{Error, Result};
18
19#[repr(u8)]
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum DirEntryType {
23 Unknown = 0,
24 RegFile = 1,
25 Directory = 2,
26 CharDev = 3,
27 BlockDev = 4,
28 Fifo = 5,
29 Socket = 6,
30 Symlink = 7,
31}
32
33impl DirEntryType {
34 pub fn from_mode(mode: u16) -> Self {
41 use crate::inode::{
42 S_IFBLK, S_IFCHR, S_IFDIR, S_IFIFO, S_IFLNK, S_IFMT, S_IFREG, S_IFSOCK,
43 };
44 match mode & S_IFMT {
45 S_IFREG => Self::RegFile,
46 S_IFDIR => Self::Directory,
47 S_IFCHR => Self::CharDev,
48 S_IFBLK => Self::BlockDev,
49 S_IFIFO => Self::Fifo,
50 S_IFSOCK => Self::Socket,
51 S_IFLNK => Self::Symlink,
52 _ => Self::Unknown,
53 }
54 }
55
56 pub fn from_u8(b: u8) -> Self {
57 match b {
58 1 => Self::RegFile,
59 2 => Self::Directory,
60 3 => Self::CharDev,
61 4 => Self::BlockDev,
62 5 => Self::Fifo,
63 6 => Self::Socket,
64 7 => Self::Symlink,
65 _ => Self::Unknown,
66 }
67 }
68}
69
70#[derive(Debug, Clone)]
72pub struct DirEntry {
73 pub inode: u32,
74 pub name: Vec<u8>,
75 pub file_type: DirEntryType,
76}
77
78pub struct DirBlockIter<'a> {
83 buf: &'a [u8],
84 offset: usize,
85 has_file_type: bool,
88}
89
90impl<'a> DirBlockIter<'a> {
91 pub fn new(buf: &'a [u8], has_file_type: bool) -> Self {
92 Self {
93 buf,
94 offset: 0,
95 has_file_type,
96 }
97 }
98}
99
100impl<'a> Iterator for DirBlockIter<'a> {
101 type Item = Result<DirEntry>;
102
103 fn next(&mut self) -> Option<Self::Item> {
104 loop {
105 if self.offset + 8 > self.buf.len() {
106 return None;
107 }
108
109 let inode_bytes = &self.buf[self.offset..self.offset + 4];
110 let inode = u32::from_le_bytes(inode_bytes.try_into().unwrap());
111 let rec_len = u16::from_le_bytes(
112 self.buf[self.offset + 4..self.offset + 6]
113 .try_into()
114 .unwrap(),
115 );
116
117 if rec_len < 8
119 || (rec_len as usize) > self.buf.len() - self.offset
120 || (rec_len % 4) != 0
121 {
122 return Some(Err(Error::CorruptDirEntry("bad rec_len")));
123 }
124
125 let name_len_lo = self.buf[self.offset + 6];
126 let type_or_namehi = self.buf[self.offset + 7];
127
128 let is_csum_tail =
130 inode == 0 && rec_len == 12 && name_len_lo == 0 && type_or_namehi == 0xDE;
131
132 let entry_end = self.offset + rec_len as usize;
133
134 if inode == 0 || is_csum_tail {
136 self.offset = entry_end;
137 if is_csum_tail {
138 return None; }
140 continue;
141 }
142
143 let name_len = if self.has_file_type {
144 name_len_lo as usize
145 } else {
146 ((type_or_namehi as usize) << 8) | name_len_lo as usize
147 };
148
149 if 8 + name_len > rec_len as usize {
150 return Some(Err(Error::CorruptDirEntry("name overflows rec_len")));
151 }
152
153 let name_start = self.offset + 8;
154 let name = self.buf[name_start..name_start + name_len].to_vec();
155 let file_type = if self.has_file_type {
156 DirEntryType::from_u8(type_or_namehi)
157 } else {
158 DirEntryType::Unknown
159 };
160
161 self.offset = entry_end;
162 return Some(Ok(DirEntry {
163 inode,
164 name,
165 file_type,
166 }));
167 }
168 }
169}
170
171pub fn parse_block(buf: &[u8], has_file_type: bool) -> Result<Vec<DirEntry>> {
173 let mut out = Vec::new();
174 for entry in DirBlockIter::new(buf, has_file_type) {
175 out.push(entry?);
176 }
177 Ok(out)
178}
179
180pub fn has_csum_tail(buf: &[u8]) -> bool {
183 if buf.len() < 12 {
184 return false;
185 }
186 let off = buf.len() - 12;
187 let inode = u32::from_le_bytes(buf[off..off + 4].try_into().unwrap());
188 let rec_len = u16::from_le_bytes(buf[off + 4..off + 6].try_into().unwrap());
189 inode == 0 && rec_len == 12 && buf[off + 6] == 0 && buf[off + 7] == 0xDE
190}
191
192pub fn parse_block_verified(
202 buf: &[u8],
203 has_file_type: bool,
204 ino: u32,
205 generation: u32,
206 csum: &Checksummer,
207) -> Result<Vec<DirEntry>> {
208 if csum.enabled && has_csum_tail(buf) && !csum.verify_dir_entry_tail(ino, generation, buf) {
209 return Err(Error::BadChecksum {
210 what: "directory block",
211 });
212 }
213 parse_block(buf, has_file_type)
214}
215
216#[inline]
223pub const fn entry_rec_len(name_len: usize) -> usize {
224 8 + ((name_len + 3) & !3)
225}
226
227fn write_entry(
230 buf: &mut [u8],
231 off: usize,
232 inode: u32,
233 rec_len: u16,
234 name: &[u8],
235 file_type: DirEntryType,
236 has_file_type: bool,
237) {
238 buf[off..off + 4].copy_from_slice(&inode.to_le_bytes());
239 buf[off + 4..off + 6].copy_from_slice(&rec_len.to_le_bytes());
240 let name_len = name.len();
241 if has_file_type {
242 buf[off + 6] = name_len as u8;
243 buf[off + 7] = file_type as u8;
244 } else {
245 buf[off + 6] = (name_len & 0xFF) as u8;
246 buf[off + 7] = ((name_len >> 8) & 0xFF) as u8;
247 }
248 buf[off + 8..off + 8 + name_len].copy_from_slice(name);
249 let pad_start = off + 8 + name_len;
252 let pad_end = off + rec_len as usize;
253 if pad_end > pad_start {
254 for b in &mut buf[pad_start..pad_end] {
255 *b = 0;
256 }
257 }
258}
259
260pub fn add_entry_to_block(
272 buf: &mut [u8],
273 inode: u32,
274 name: &[u8],
275 file_type: DirEntryType,
276 has_file_type: bool,
277 reserved_tail: usize,
278) -> Result<()> {
279 if inode == 0 {
280 return Err(Error::CorruptDirEntry(
281 "refuse to insert entry with inode 0",
282 ));
283 }
284 if name.is_empty() || name.len() > 255 {
285 return Err(Error::CorruptDirEntry("name length out of range"));
286 }
287 let usable = buf
288 .len()
289 .checked_sub(reserved_tail)
290 .ok_or(Error::OutOfBounds)?;
291 let needed = entry_rec_len(name.len());
292 if needed > usable {
293 return Err(Error::OutOfBounds);
294 }
295
296 let mut off = 0usize;
297 while off + 8 <= usable {
298 let cur_inode = u32::from_le_bytes(buf[off..off + 4].try_into().unwrap());
299 let rec_len = u16::from_le_bytes(buf[off + 4..off + 6].try_into().unwrap()) as usize;
300 if rec_len < 8 || !rec_len.is_multiple_of(4) || off + rec_len > usable {
301 return Err(Error::CorruptDirEntry("bad rec_len during add"));
302 }
303
304 if cur_inode == 0 {
305 if rec_len >= needed {
307 write_entry(
308 buf,
309 off,
310 inode,
311 rec_len as u16,
312 name,
313 file_type,
314 has_file_type,
315 );
316 return Ok(());
317 }
318 } else {
319 let cur_name_lo = buf[off + 6];
320 let cur_type_or_hi = buf[off + 7];
321 let cur_name_len = if has_file_type {
322 cur_name_lo as usize
323 } else {
324 ((cur_type_or_hi as usize) << 8) | cur_name_lo as usize
325 };
326 let cur_actual = entry_rec_len(cur_name_len);
327 if rec_len >= cur_actual + needed {
328 buf[off + 4..off + 6].copy_from_slice(&(cur_actual as u16).to_le_bytes());
330 let new_off = off + cur_actual;
331 let new_rec_len = rec_len - cur_actual;
332 write_entry(
333 buf,
334 new_off,
335 inode,
336 new_rec_len as u16,
337 name,
338 file_type,
339 has_file_type,
340 );
341 return Ok(());
342 }
343 }
344
345 off += rec_len;
346 }
347
348 Err(Error::OutOfBounds)
349}
350
351pub fn remove_entry_from_block(
360 buf: &mut [u8],
361 name: &[u8],
362 has_file_type: bool,
363 reserved_tail: usize,
364) -> Result<bool> {
365 let usable = buf
366 .len()
367 .checked_sub(reserved_tail)
368 .ok_or(Error::OutOfBounds)?;
369 let mut off = 0usize;
370 let mut prev_off: Option<usize> = None;
371
372 while off + 8 <= usable {
373 let cur_inode = u32::from_le_bytes(buf[off..off + 4].try_into().unwrap());
374 let rec_len = u16::from_le_bytes(buf[off + 4..off + 6].try_into().unwrap()) as usize;
375 if rec_len < 8 || !rec_len.is_multiple_of(4) || off + rec_len > usable {
376 return Err(Error::CorruptDirEntry("bad rec_len during remove"));
377 }
378
379 if cur_inode != 0 {
380 let cur_name_lo = buf[off + 6];
381 let cur_type_or_hi = buf[off + 7];
382 let cur_name_len = if has_file_type {
383 cur_name_lo as usize
384 } else {
385 ((cur_type_or_hi as usize) << 8) | cur_name_lo as usize
386 };
387 if 8 + cur_name_len <= rec_len && &buf[off + 8..off + 8 + cur_name_len] == name {
388 if let Some(prev) = prev_off {
389 let prev_rec_len =
391 u16::from_le_bytes(buf[prev + 4..prev + 6].try_into().unwrap()) as usize;
392 let merged = prev_rec_len + rec_len;
393 if merged > u16::MAX as usize {
394 return Err(Error::CorruptDirEntry("merged rec_len overflows u16"));
395 }
396 buf[prev + 4..prev + 6].copy_from_slice(&(merged as u16).to_le_bytes());
397 for b in &mut buf[off..off + 8] {
399 *b = 0;
400 }
401 } else {
402 buf[off..off + 4].copy_from_slice(&0u32.to_le_bytes());
404 }
405 return Ok(true);
406 }
407 }
408
409 prev_off = Some(off);
410 off += rec_len;
411 }
412
413 Ok(false)
414}
415
416#[cfg(test)]
417mod tests {
418 use super::*;
419
420 #[test]
423 fn from_mode_knows_every_file_type() {
424 use crate::inode::{S_IFBLK, S_IFCHR, S_IFDIR, S_IFIFO, S_IFLNK, S_IFREG, S_IFSOCK};
425 for (mode, want) in [
426 (S_IFREG, DirEntryType::RegFile),
427 (S_IFDIR, DirEntryType::Directory),
428 (S_IFCHR, DirEntryType::CharDev),
429 (S_IFBLK, DirEntryType::BlockDev),
430 (S_IFIFO, DirEntryType::Fifo),
431 (S_IFSOCK, DirEntryType::Socket),
432 (S_IFLNK, DirEntryType::Symlink),
433 (0, DirEntryType::Unknown),
434 ] {
435 let got = DirEntryType::from_mode(mode | 0o7777);
436 assert_eq!(got, want, "mode {mode:#o}");
437 assert_eq!(DirEntryType::from_u8(got as u8), want);
438 }
439 }
440
441 fn synth_block() -> Vec<u8> {
443 let block_size = 4096;
444 let mut buf = vec![0u8; block_size];
445
446 buf[0..4].copy_from_slice(&2u32.to_le_bytes());
448 buf[4..6].copy_from_slice(&12u16.to_le_bytes());
449 buf[6] = 1;
450 buf[7] = 2;
451 buf[8] = b'.';
452
453 buf[12..16].copy_from_slice(&2u32.to_le_bytes());
455 buf[16..18].copy_from_slice(&12u16.to_le_bytes());
456 buf[18] = 2;
457 buf[19] = 2;
458 buf[20] = b'.';
459 buf[21] = b'.';
460
461 buf[24..28].copy_from_slice(&12u32.to_le_bytes());
464 buf[28..30].copy_from_slice(&((block_size - 24) as u16).to_le_bytes());
465 buf[30] = 5;
466 buf[31] = 1;
467 buf[32..37].copy_from_slice(b"hello");
468
469 buf
470 }
471
472 #[test]
473 fn parses_dot_dotdot_hello() {
474 let buf = synth_block();
475 let entries = parse_block(&buf, true).expect("parse");
476
477 assert_eq!(entries.len(), 3);
478 assert_eq!(entries[0].name, b".");
479 assert_eq!(entries[0].inode, 2);
480 assert_eq!(entries[0].file_type, DirEntryType::Directory);
481
482 assert_eq!(entries[1].name, b"..");
483 assert_eq!(entries[1].file_type, DirEntryType::Directory);
484
485 assert_eq!(entries[2].name, b"hello");
486 assert_eq!(entries[2].inode, 12);
487 assert_eq!(entries[2].file_type, DirEntryType::RegFile);
488 }
489
490 #[test]
491 fn rejects_bad_rec_len() {
492 let mut buf = vec![0u8; 64];
493 buf[0..4].copy_from_slice(&2u32.to_le_bytes());
494 buf[4..6].copy_from_slice(&3u16.to_le_bytes());
496 buf[6] = 1;
497 buf[7] = 2;
498 buf[8] = b'.';
499
500 let result = parse_block(&buf, true);
501 assert!(matches!(result, Err(Error::CorruptDirEntry(_))));
502 }
503
504 #[test]
507 fn rec_len_matches_kernel_layout() {
508 assert_eq!(entry_rec_len(1), 12); assert_eq!(entry_rec_len(2), 12); assert_eq!(entry_rec_len(5), 16); assert_eq!(entry_rec_len(8), 16); assert_eq!(entry_rec_len(9), 20);
513 }
514
515 #[test]
516 fn add_entry_splits_terminal_record() {
517 let mut buf = synth_block();
518 add_entry_to_block(&mut buf, 99, b"world", DirEntryType::RegFile, true, 0)
519 .expect("add_entry");
520
521 let entries = parse_block(&buf, true).expect("parse");
522 assert_eq!(entries.len(), 4);
523 assert_eq!(entries[3].name, b"world");
524 assert_eq!(entries[3].inode, 99);
525 assert_eq!(entries[3].file_type, DirEntryType::RegFile);
526
527 let hello_rec_len = u16::from_le_bytes(buf[28..30].try_into().unwrap());
530 assert_eq!(hello_rec_len, 16, "hello rec_len shrinks to actual size");
531 }
532
533 #[test]
534 fn add_entry_enospc_when_no_room() {
535 let block_size = 32;
537 let mut buf = vec![0u8; block_size];
538 buf[0..4].copy_from_slice(&2u32.to_le_bytes());
540 buf[4..6].copy_from_slice(&12u16.to_le_bytes());
541 buf[6] = 1;
542 buf[7] = 2;
543 buf[8] = b'.';
544 buf[12..16].copy_from_slice(&2u32.to_le_bytes());
546 buf[16..18].copy_from_slice(&12u16.to_le_bytes());
547 buf[18] = 2;
548 buf[19] = 2;
549 buf[20] = b'.';
550 buf[21] = b'.';
551 buf[24..28].copy_from_slice(&12u32.to_le_bytes());
553 buf[28..30].copy_from_slice(&8u16.to_le_bytes());
554 buf[30] = 1;
555 buf[31] = 1;
556
557 let err = add_entry_to_block(&mut buf, 42, b"xx", DirEntryType::RegFile, true, 0);
558 assert!(matches!(err, Err(Error::OutOfBounds)));
559 }
560
561 #[test]
562 fn add_entry_respects_reserved_tail() {
563 let mut buf = vec![0u8; 64];
567 buf[0..4].copy_from_slice(&2u32.to_le_bytes());
568 buf[4..6].copy_from_slice(&12u16.to_le_bytes());
569 buf[6] = 1;
570 buf[7] = 2;
571 buf[8] = b'.';
572 buf[12..16].copy_from_slice(&2u32.to_le_bytes());
573 buf[16..18].copy_from_slice(&40u16.to_le_bytes());
576 buf[18] = 2;
577 buf[19] = 2;
578 buf[20] = b'.';
579 buf[21] = b'.';
580
581 buf[52..56].copy_from_slice(&0u32.to_le_bytes());
583 buf[56..58].copy_from_slice(&12u16.to_le_bytes());
584 buf[58] = 0;
585 buf[59] = 0xDE;
586
587 let too_big = b"aaaaaaaaaaaaaaaaaaaaa"; assert!(matches!(
590 add_entry_to_block(&mut buf, 10, too_big, DirEntryType::RegFile, true, 12),
591 Err(Error::OutOfBounds)
592 ));
593 let ok = b"aaaaaaaaaaaaa"; add_entry_to_block(&mut buf, 10, ok, DirEntryType::RegFile, true, 12)
596 .expect("add should succeed within reserved tail");
597
598 assert_eq!(buf[58], 0);
600 assert_eq!(buf[59], 0xDE);
601 }
602
603 #[test]
604 fn remove_entry_coalesces_into_previous() {
605 let mut buf = synth_block();
606 let ok = remove_entry_from_block(&mut buf, b"hello", true, 0).expect("remove");
607 assert!(ok);
608
609 let entries = parse_block(&buf, true).expect("parse");
610 assert_eq!(entries.len(), 2);
611 assert_eq!(entries[0].name, b".");
612 assert_eq!(entries[1].name, b"..");
613
614 let dotdot_rec_len = u16::from_le_bytes(buf[16..18].try_into().unwrap());
616 assert_eq!(dotdot_rec_len, 4084);
617 }
618
619 #[test]
620 fn remove_entry_tombstones_first_slot() {
621 let mut buf = synth_block();
622 buf[0..4].copy_from_slice(&17u32.to_le_bytes());
624 buf[6] = 4; buf[7] = 1; buf[8..12].copy_from_slice(b"root");
627
628 let ok = remove_entry_from_block(&mut buf, b"root", true, 0).expect("remove");
629 assert!(ok);
630 let first_inode = u32::from_le_bytes(buf[0..4].try_into().unwrap());
632 assert_eq!(first_inode, 0);
633 let first_rec = u16::from_le_bytes(buf[4..6].try_into().unwrap());
634 assert_eq!(first_rec, 12);
635
636 let entries = parse_block(&buf, true).expect("parse");
638 assert_eq!(entries.len(), 2);
639 }
640
641 #[test]
642 fn remove_entry_reports_missing() {
643 let mut buf = synth_block();
644 let ok = remove_entry_from_block(&mut buf, b"nope", true, 0).expect("remove");
645 assert!(!ok);
646
647 let entries = parse_block(&buf, true).expect("parse");
648 assert_eq!(entries.len(), 3);
649 }
650
651 #[test]
652 fn add_then_remove_round_trips() {
653 let mut buf = synth_block();
654 add_entry_to_block(&mut buf, 77, b"fresh", DirEntryType::RegFile, true, 0).unwrap();
655 assert_eq!(parse_block(&buf, true).unwrap().len(), 4);
656
657 assert!(remove_entry_from_block(&mut buf, b"fresh", true, 0).unwrap());
658 let entries = parse_block(&buf, true).unwrap();
659 assert_eq!(entries.len(), 3);
660 assert!(!entries.iter().any(|e| e.name == b"fresh"));
661 }
662
663 #[test]
664 fn add_entry_reuses_tombstone_in_middle() {
665 let block_size = 4096;
667 let mut buf = vec![0u8; block_size];
668 buf[0..4].copy_from_slice(&2u32.to_le_bytes());
669 buf[4..6].copy_from_slice(&12u16.to_le_bytes());
670 buf[6] = 1;
671 buf[7] = 2;
672 buf[8] = b'.';
673 buf[12..14].copy_from_slice(&0u16.to_le_bytes());
675 buf[14..16].copy_from_slice(&0u16.to_le_bytes());
676 buf[16..18].copy_from_slice(&16u16.to_le_bytes());
677 let tail_off = 28;
679 let tail_rec_len = (block_size - tail_off) as u16;
680 buf[tail_off..tail_off + 4].copy_from_slice(&5u32.to_le_bytes());
681 buf[tail_off + 4..tail_off + 6].copy_from_slice(&tail_rec_len.to_le_bytes());
682 buf[tail_off + 6] = 4;
683 buf[tail_off + 7] = 1;
684 buf[tail_off + 8..tail_off + 12].copy_from_slice(b"tail");
685
686 add_entry_to_block(&mut buf, 99, b"x", DirEntryType::RegFile, true, 0).unwrap();
687 let new_inode = u32::from_le_bytes(buf[12..16].try_into().unwrap());
689 assert_eq!(new_inode, 99);
690 let new_rec_len = u16::from_le_bytes(buf[16..18].try_into().unwrap());
691 assert_eq!(new_rec_len, 16, "tombstone slot rec_len preserved");
692 assert_eq!(&buf[20..21], b"x");
693
694 let entries = parse_block(&buf, true).unwrap();
695 assert_eq!(entries.len(), 3);
696 assert_eq!(entries[1].name, b"x");
697 assert_eq!(entries[2].name, b"tail");
698 }
699}