1use crate::bytes::Endian;
36use crate::error::UfsError;
37use crate::inode::{read_inode, Inode};
38use crate::superblock::{Superblock, UFS_ROOTINO};
39
40pub const DIRBLKSIZ: usize = 512;
43
44pub const DIR_ROUNDUP: usize = 4;
47
48const DIRECT_HEAD: usize = 8;
51
52const OFF_INO: usize = 0;
54const OFF_RECLEN: usize = 4;
55const OFF_TYPE: usize = 6;
56const OFF_NAMLEN: usize = 7;
57const OFF_NAME: usize = 8;
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66#[non_exhaustive]
67pub enum DirEntryType {
68 Unknown,
70 Fifo,
72 CharDevice,
74 Directory,
76 BlockDevice,
78 Regular,
80 Symlink,
82 Socket,
84 Whiteout,
86 Other(u8),
89}
90
91impl DirEntryType {
92 #[must_use]
94 pub fn from_d_type(d_type: u8) -> Self {
95 match d_type {
96 0 => DirEntryType::Unknown,
97 1 => DirEntryType::Fifo,
98 2 => DirEntryType::CharDevice,
99 4 => DirEntryType::Directory,
100 6 => DirEntryType::BlockDevice,
101 8 => DirEntryType::Regular,
102 10 => DirEntryType::Symlink,
103 12 => DirEntryType::Socket,
104 14 => DirEntryType::Whiteout,
105 other => DirEntryType::Other(other),
106 }
107 }
108}
109
110#[derive(Debug, Clone, PartialEq, Eq)]
114#[non_exhaustive]
115pub struct DirEntry {
116 pub name: Vec<u8>,
119 pub ino: u64,
122 pub file_type: DirEntryType,
124 pub deleted: bool,
127}
128
129pub fn read_block<'a>(
146 partition: &'a [u8],
147 sb: &Superblock,
148 addr: u64,
149 len: usize,
150) -> Result<&'a [u8], UfsError> {
151 if sb.fsize <= 0 {
152 return Err(UfsError::ImpossibleGeometry {
153 field: "fs_fsize",
154 value: sb.fsize as u64,
155 limit: i64::MAX as u64,
156 });
157 }
158 let fsize = sb.fsize as u64;
159 let start = usize::try_from(addr.saturating_mul(fsize)).unwrap_or(usize::MAX);
160 let end = start.saturating_add(len).min(partition.len());
161 Ok(partition.get(start..end.max(start)).unwrap_or(&[]))
162}
163
164pub fn list_dir(
179 partition: &[u8],
180 sb: &Superblock,
181 dir_ino: u64,
182) -> Result<Vec<DirEntry>, UfsError> {
183 Ok(list_dir_all(partition, sb, dir_ino)?
184 .into_iter()
185 .filter(|e| !e.deleted)
186 .collect())
187}
188
189pub fn list_dir_all(
198 partition: &[u8],
199 sb: &Superblock,
200 dir_ino: u64,
201) -> Result<Vec<DirEntry>, UfsError> {
202 let inode = read_inode(partition, sb, dir_ino)?;
203 Ok(list_dir_entries(partition, sb, &inode))
204}
205
206fn list_dir_entries(partition: &[u8], sb: &Superblock, inode: &Inode) -> Vec<DirEntry> {
216 let mut entries = Vec::new();
217 if sb.fsize <= 0 {
218 return entries; }
220 let bsize = if sb.bsize > 0 {
221 sb.bsize as u64
222 } else {
223 DIRBLKSIZ as u64 };
225 let mut remaining = inode.size;
226
227 for &addr in &inode.direct {
228 if remaining == 0 {
229 break;
230 }
231 if addr == 0 {
232 remaining = remaining.saturating_sub(bsize);
235 continue;
236 }
237 let want = usize::try_from(remaining.min(bsize)).unwrap_or(usize::MAX);
238 let Ok(block) = read_block(partition, sb, addr, want) else {
239 break; };
241 walk_block(block, sb.endian, &mut entries);
242 remaining = remaining.saturating_sub(bsize);
243 }
244 entries
245}
246
247fn walk_block(block: &[u8], endian: Endian, out: &mut Vec<DirEntry>) {
251 let mut off = 0usize;
252 while off + DIRECT_HEAD <= block.len() {
253 let ino = u64::from(endian.u32(block, off + OFF_INO));
254 let reclen = endian.u16(block, off + OFF_RECLEN) as usize;
255 let d_type = crate::bytes::u8_at(block, off + OFF_TYPE);
256 let namlen = crate::bytes::u8_at(block, off + OFF_NAMLEN) as usize;
257
258 if reclen < DIRECT_HEAD {
261 break;
262 }
263
264 let name_start = off + OFF_NAME;
267 let name_cap = reclen.saturating_sub(OFF_NAME);
268 let take = namlen.min(name_cap);
269 let name_end = name_start.saturating_add(take).min(block.len());
270 let name = block
271 .get(name_start..name_end)
272 .map(<[u8]>::to_vec)
273 .unwrap_or_default();
274
275 out.push(DirEntry {
276 name,
277 ino,
278 file_type: DirEntryType::from_d_type(d_type),
279 deleted: ino == 0,
280 });
281
282 off = off.saturating_add(reclen);
283 }
284}
285
286pub fn read_by_path(
300 partition: &[u8],
301 sb: &Superblock,
302 path: &str,
303) -> Result<Option<(u64, Inode)>, UfsError> {
304 let root = read_inode(partition, sb, UFS_ROOTINO)?;
305 let mut cur_ino = UFS_ROOTINO;
306 let mut cur = root;
307
308 for comp in path.split('/') {
309 if comp.is_empty() {
310 continue; }
312 if !cur.is_dir() {
313 return Ok(None); }
315 let entries = list_dir_entries(partition, sb, &cur);
316 let Some(hit) = entries
317 .iter()
318 .find(|e| !e.deleted && e.name == comp.as_bytes())
319 else {
320 return Ok(None);
321 };
322 cur_ino = hit.ino;
323 cur = read_inode(partition, sb, cur_ino)?;
324 }
325 Ok(Some((cur_ino, cur)))
326}
327
328#[cfg(test)]
329#[allow(clippy::unreadable_literal)]
330mod tests {
331 use super::*;
332 use crate::superblock::{UfsVersion, FS_UFS2_MAGIC, SBLOCK_UFS2};
333
334 #[test]
335 fn d_type_classifies_all_dt_values() {
336 assert_eq!(DirEntryType::from_d_type(0), DirEntryType::Unknown);
337 assert_eq!(DirEntryType::from_d_type(1), DirEntryType::Fifo);
338 assert_eq!(DirEntryType::from_d_type(2), DirEntryType::CharDevice);
339 assert_eq!(DirEntryType::from_d_type(4), DirEntryType::Directory);
340 assert_eq!(DirEntryType::from_d_type(6), DirEntryType::BlockDevice);
341 assert_eq!(DirEntryType::from_d_type(8), DirEntryType::Regular);
342 assert_eq!(DirEntryType::from_d_type(10), DirEntryType::Symlink);
343 assert_eq!(DirEntryType::from_d_type(12), DirEntryType::Socket);
344 assert_eq!(DirEntryType::from_d_type(14), DirEntryType::Whiteout);
345 assert_eq!(DirEntryType::from_d_type(9), DirEntryType::Other(9));
346 }
347
348 fn direct(ino: u32, reclen: u16, d_type: u8, name: &[u8]) -> Vec<u8> {
350 let mut e = vec![0u8; reclen as usize];
351 e[OFF_INO..OFF_INO + 4].copy_from_slice(&ino.to_le_bytes());
352 e[OFF_RECLEN..OFF_RECLEN + 2].copy_from_slice(&reclen.to_le_bytes());
353 e[OFF_TYPE] = d_type;
354 e[OFF_NAMLEN] = name.len() as u8;
355 e[OFF_NAME..OFF_NAME + name.len()].copy_from_slice(name);
356 e
357 }
358
359 fn real_root_block() -> Vec<u8> {
363 let mut b = Vec::new();
364 b.extend(direct(2, 12, 4, b"."));
365 b.extend(direct(2, 12, 4, b".."));
366 b.extend(direct(3, 16, 4, b".snap"));
367 b.extend(direct(128, 20, 4, b"a_directory"));
368 b.extend(direct(4, 24, 8, b"passwords.txt"));
369 b.extend(direct(5, 428, 10, b"a_link"));
370 assert_eq!(b.len(), DIRBLKSIZ, "root block is one DIRBLKSIZ");
371 b
372 }
373
374 fn walk(block: &[u8]) -> Vec<DirEntry> {
375 let mut out = Vec::new();
376 walk_block(block, Endian::Little, &mut out);
377 out
378 }
379
380 #[test]
381 fn walk_block_decodes_real_root_layout() {
382 let entries = walk(&real_root_block());
383 let names: Vec<&[u8]> = entries.iter().map(|e| e.name.as_slice()).collect();
384 assert_eq!(
385 names,
386 vec![
387 &b"."[..],
388 &b".."[..],
389 &b".snap"[..],
390 &b"a_directory"[..],
391 &b"passwords.txt"[..],
392 &b"a_link"[..],
393 ]
394 );
395 let inos: Vec<u64> = entries.iter().map(|e| e.ino).collect();
396 assert_eq!(inos, vec![2, 2, 3, 128, 4, 5]);
397 assert_eq!(entries[3].file_type, DirEntryType::Directory);
398 assert_eq!(entries[4].file_type, DirEntryType::Regular);
399 assert_eq!(entries[5].file_type, DirEntryType::Symlink);
400 assert!(entries.iter().all(|e| !e.deleted));
401 }
402
403 #[test]
404 fn walk_block_surfaces_deleted_slot() {
405 let mut b = Vec::new();
407 b.extend(direct(7, 16, 8, b"live"));
408 b.extend(direct(0, 16, 8, b"ghost")); let entries = walk(&b);
410 assert_eq!(entries.len(), 2);
411 assert!(!entries[0].deleted);
412 assert_eq!(entries[0].name, b"live");
413 assert!(entries[1].deleted, "d_ino==0 is a deleted slot");
414 assert_eq!(entries[1].ino, 0);
415 assert_eq!(entries[1].name, b"ghost", "residual name preserved");
416 }
417
418 #[test]
419 fn lying_zero_reclen_does_not_loop_forever() {
420 let mut b = direct(9, 16, 8, b"ok");
422 let mut bad = vec![0u8; DIRECT_HEAD];
424 bad[OFF_INO..OFF_INO + 4].copy_from_slice(&5u32.to_le_bytes());
425 b.extend(bad);
427 let entries = walk(&b);
428 assert_eq!(entries.len(), 1, "walk stops at the zero-reclen entry");
429 assert_eq!(entries[0].name, b"ok");
430 }
431
432 #[test]
433 fn over_long_namlen_is_clamped_not_overread() {
434 let mut e = vec![0u8; 16];
436 e[OFF_INO..OFF_INO + 4].copy_from_slice(&3u32.to_le_bytes());
437 e[OFF_RECLEN..OFF_RECLEN + 2].copy_from_slice(&16u16.to_le_bytes());
438 e[OFF_TYPE] = 8;
439 e[OFF_NAMLEN] = 200; e[OFF_NAME..OFF_NAME + 4].copy_from_slice(b"abcd");
441 let entries = walk(&e);
442 assert_eq!(entries.len(), 1);
443 assert!(entries[0].name.len() <= 16 - OFF_NAME);
446 }
447
448 #[test]
449 fn reclen_below_head_ends_block() {
450 let mut e = vec![0u8; 8];
453 e[OFF_INO..OFF_INO + 4].copy_from_slice(&1u32.to_le_bytes());
454 e[OFF_RECLEN..OFF_RECLEN + 2].copy_from_slice(&4u16.to_le_bytes()); let entries = walk(&e);
456 assert!(entries.is_empty());
457 }
458
459 #[test]
460 fn walk_empty_or_short_block_is_safe() {
461 assert!(walk(&[]).is_empty());
462 assert!(walk(&[0u8; 3]).is_empty()); }
464
465 fn tiny_sb() -> Superblock {
468 let mut d = vec![0u8; 1376];
470 let wr32 = |d: &mut [u8], off: usize, v: i32| {
471 d[off..off + 4].copy_from_slice(&v.to_le_bytes());
472 };
473 let wr64 = |d: &mut [u8], off: usize, v: i64| {
474 d[off..off + 8].copy_from_slice(&v.to_le_bytes());
475 };
476 wr32(&mut d, 8, 24); wr32(&mut d, 12, 32); wr32(&mut d, 16, 40); wr32(&mut d, 20, 48); wr32(&mut d, 44, 4); wr32(&mut d, 48, 32768); wr32(&mut d, 52, 4096); wr32(&mut d, 56, 8); wr32(&mut d, 184, 128); wr32(&mut d, 188, 256); wr32(&mut d, 1320, 120); wr64(&mut d, 1080, 1022); wr64(&mut d, 1000, SBLOCK_UFS2 as i64);
489 d[1372..1376].copy_from_slice(&FS_UFS2_MAGIC.to_le_bytes());
490 Superblock::parse(&d).unwrap()
491 }
492
493 #[test]
494 fn read_block_offsets_by_fragment_size() {
495 let sb = tiny_sb();
496 let mut part = vec![0u8; 8192 + 16];
498 part[8192..8192 + 4].copy_from_slice(b"HERE");
499 let block = read_block(&part, &sb, 2, 4).unwrap();
500 assert_eq!(block, b"HERE");
501 }
502
503 #[test]
504 fn read_block_clamps_past_end_without_error() {
505 let sb = tiny_sb();
506 let part = vec![0u8; 100];
507 let block = read_block(&part, &sb, 1, 512).unwrap();
509 assert!(block.is_empty());
510 }
511
512 #[test]
513 fn read_block_rejects_zero_fsize() {
514 let mut sb = tiny_sb();
515 sb.fsize = 0;
516 assert!(matches!(
517 read_block(&[0u8; 16], &sb, 0, 4),
518 Err(UfsError::ImpossibleGeometry {
519 field: "fs_fsize",
520 ..
521 })
522 ));
523 }
524
525 fn synthetic_fs() -> (Vec<u8>, Superblock) {
532 let sb = tiny_sb();
533 let fsize = 4096usize;
534 let iblkno = 40usize;
535 let fpg = 256usize;
536 let ipg = 128usize;
537 let inode_size = 256usize;
538
539 let root_dir_frag = 60u64;
542 let adir_frag = 61u64;
543
544 let ino_byte = |ino: usize| -> usize {
547 let c = ino / ipg;
548 let within = ino % ipg;
549 (c * fpg + iblkno) * fsize + within * inode_size
550 };
551
552 let max_byte = [
555 SBLOCK_UFS2 + 1376,
556 ino_byte(130) + inode_size,
557 (root_dir_frag as usize + 1) * fsize,
558 (adir_frag as usize + 1) * fsize,
559 ]
560 .into_iter()
561 .max()
562 .unwrap();
563 let mut part = vec![0u8; max_byte + 16];
564
565 let sb_bytes = {
567 let mut d = vec![0u8; 1376];
568 let wr32 = |d: &mut [u8], off: usize, v: i32| {
569 d[off..off + 4].copy_from_slice(&v.to_le_bytes());
570 };
571 let wr64 = |d: &mut [u8], off: usize, v: i64| {
572 d[off..off + 8].copy_from_slice(&v.to_le_bytes());
573 };
574 wr32(&mut d, 8, 24);
575 wr32(&mut d, 12, 32);
576 wr32(&mut d, 16, iblkno as i32);
577 wr32(&mut d, 20, 48);
578 wr32(&mut d, 44, 4);
579 wr32(&mut d, 48, 32768);
580 wr32(&mut d, 52, fsize as i32);
581 wr32(&mut d, 56, 8);
582 wr32(&mut d, 184, ipg as i32);
583 wr32(&mut d, 188, fpg as i32);
584 wr32(&mut d, 1320, 120);
585 wr64(&mut d, 1080, 1022);
586 wr64(&mut d, 1000, SBLOCK_UFS2 as i64);
587 d[1372..1376].copy_from_slice(&FS_UFS2_MAGIC.to_le_bytes());
588 d
589 };
590 part[SBLOCK_UFS2..SBLOCK_UFS2 + 1376].copy_from_slice(&sb_bytes);
591
592 let dir_inode = |frag: u64, size: u64, mode: u16| -> Vec<u8> {
594 let mut d = vec![0u8; inode_size];
595 d[0..2].copy_from_slice(&mode.to_le_bytes()); d[2..4].copy_from_slice(&1u16.to_le_bytes()); d[16..24].copy_from_slice(&size.to_le_bytes()); d[112..120].copy_from_slice(&frag.to_le_bytes()); d
600 };
601 part[ino_byte(2)..ino_byte(2) + inode_size].copy_from_slice(&dir_inode(
603 root_dir_frag,
604 512,
605 0o040755,
606 ));
607 part[ino_byte(128)..ino_byte(128) + inode_size]
609 .copy_from_slice(&dir_inode(adir_frag, 512, 0o040755));
610 part[ino_byte(4)..ino_byte(4) + inode_size].copy_from_slice(&dir_inode(0, 116, 0o100644));
612 part[ino_byte(129)..ino_byte(129) + inode_size]
614 .copy_from_slice(&dir_inode(0, 116, 0o100644));
615
616 let root_block = real_root_block();
618 let rb = root_dir_frag as usize * fsize;
619 part[rb..rb + root_block.len()].copy_from_slice(&root_block);
620
621 let mut adir = Vec::new();
623 adir.extend(direct(128, 12, 4, b"."));
624 adir.extend(direct(2, 12, 4, b".."));
625 adir.extend(direct(129, 488, 8, b"a_file"));
626 let ab = adir_frag as usize * fsize;
627 part[ab..ab + adir.len()].copy_from_slice(&adir);
628
629 (part, sb)
630 }
631
632 #[test]
633 fn list_dir_returns_live_root_entries() {
634 let (part, sb) = synthetic_fs();
635 let entries = list_dir(&part, &sb, 2).unwrap();
636 let names: Vec<&[u8]> = entries.iter().map(|e| e.name.as_slice()).collect();
637 assert_eq!(
638 names,
639 vec![
640 &b"."[..],
641 &b".."[..],
642 &b".snap"[..],
643 &b"a_directory"[..],
644 &b"passwords.txt"[..],
645 &b"a_link"[..],
646 ]
647 );
648 let pw = entries.iter().find(|e| e.name == b"passwords.txt").unwrap();
650 assert_eq!(pw.ino, 4);
651 assert_eq!(pw.file_type, DirEntryType::Regular);
652 }
653
654 #[test]
655 fn list_dir_skips_hole_in_direct_pointers() {
656 let sb = tiny_sb();
661 let fsize = sb.fsize as u64;
662 let bsize = sb.bsize as u64; let frag0 = 60u64;
664
665 let mut dino = vec![0u8; 256];
668 dino[0..2].copy_from_slice(&0o040755u16.to_le_bytes()); dino[2..4].copy_from_slice(&1u16.to_le_bytes());
670 dino[16..24].copy_from_slice(&(2 * bsize).to_le_bytes()); dino[112..120].copy_from_slice(&frag0.to_le_bytes()); let inode = Inode::parse(&dino, UfsVersion::Ufs2, Endian::Little).unwrap();
673 assert_eq!(inode.direct[1], 0, "second pointer is a hole");
674
675 let mut block = Vec::new();
677 block.extend(direct(9, 12, 8, b"x"));
678 block.extend(direct(10, DIRBLKSIZ as u16 - 12, 8, b"y"));
679 let start = (frag0 * fsize) as usize;
680 let mut part = vec![0u8; start + block.len()];
681 part[start..start + block.len()].copy_from_slice(&block);
682
683 let entries = list_dir_entries(&part, &sb, &inode);
684 let names: Vec<&[u8]> = entries.iter().map(|e| e.name.as_slice()).collect();
685 assert_eq!(
686 names,
687 vec![&b"x"[..], &b"y"[..]],
688 "first block walked, hole skipped"
689 );
690 }
691
692 #[test]
693 fn read_by_path_root_resolves_to_inode2() {
694 let (part, sb) = synthetic_fs();
695 let (ino, inode) = read_by_path(&part, &sb, "/").unwrap().unwrap();
696 assert_eq!(ino, 2);
697 assert!(inode.is_dir());
698 }
699
700 #[test]
701 fn read_by_path_resolves_known_file_inode4() {
702 let (part, sb) = synthetic_fs();
703 let (ino, inode) = read_by_path(&part, &sb, "/passwords.txt").unwrap().unwrap();
704 assert_eq!(ino, 4);
705 assert_eq!(inode.size, 116, "P1 metadata: passwords.txt is 116 bytes");
706 }
707
708 #[test]
709 fn read_by_path_descends_nested_directory() {
710 let (part, sb) = synthetic_fs();
711 let (ino, inode) = read_by_path(&part, &sb, "/a_directory/a_file")
712 .unwrap()
713 .unwrap();
714 assert_eq!(ino, 129);
715 assert!(inode.is_regular());
716 }
717
718 #[test]
719 fn read_by_path_missing_component_is_none() {
720 let (part, sb) = synthetic_fs();
721 assert!(read_by_path(&part, &sb, "/nope").unwrap().is_none());
722 assert!(read_by_path(&part, &sb, "/a_directory/missing")
723 .unwrap()
724 .is_none());
725 }
726
727 #[test]
728 fn read_by_path_through_non_directory_is_none() {
729 let (part, sb) = synthetic_fs();
730 assert!(read_by_path(&part, &sb, "/passwords.txt/x")
732 .unwrap()
733 .is_none());
734 }
735}