use crate::bytes::Endian;
use crate::error::UfsError;
use crate::inode::{read_inode, Inode};
use crate::superblock::{Superblock, UFS_ROOTINO};
pub const DIRBLKSIZ: usize = 512;
pub const DIR_ROUNDUP: usize = 4;
const DIRECT_HEAD: usize = 8;
const OFF_INO: usize = 0;
const OFF_RECLEN: usize = 4;
const OFF_TYPE: usize = 6;
const OFF_NAMLEN: usize = 7;
const OFF_NAME: usize = 8;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum DirEntryType {
Unknown,
Fifo,
CharDevice,
Directory,
BlockDevice,
Regular,
Symlink,
Socket,
Whiteout,
Other(u8),
}
impl DirEntryType {
#[must_use]
pub fn from_d_type(d_type: u8) -> Self {
match d_type {
0 => DirEntryType::Unknown,
1 => DirEntryType::Fifo,
2 => DirEntryType::CharDevice,
4 => DirEntryType::Directory,
6 => DirEntryType::BlockDevice,
8 => DirEntryType::Regular,
10 => DirEntryType::Symlink,
12 => DirEntryType::Socket,
14 => DirEntryType::Whiteout,
other => DirEntryType::Other(other),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct DirEntry {
pub name: Vec<u8>,
pub ino: u64,
pub file_type: DirEntryType,
pub deleted: bool,
}
pub fn read_block<'a>(
partition: &'a [u8],
sb: &Superblock,
addr: u64,
len: usize,
) -> Result<&'a [u8], UfsError> {
if sb.fsize <= 0 {
return Err(UfsError::ImpossibleGeometry {
field: "fs_fsize",
value: sb.fsize as u64,
limit: i64::MAX as u64,
});
}
let fsize = sb.fsize as u64;
let start = usize::try_from(addr.saturating_mul(fsize)).unwrap_or(usize::MAX);
let end = start.saturating_add(len).min(partition.len());
Ok(partition.get(start..end.max(start)).unwrap_or(&[]))
}
pub fn list_dir(
partition: &[u8],
sb: &Superblock,
dir_ino: u64,
) -> Result<Vec<DirEntry>, UfsError> {
Ok(list_dir_all(partition, sb, dir_ino)?
.into_iter()
.filter(|e| !e.deleted)
.collect())
}
pub fn list_dir_all(
partition: &[u8],
sb: &Superblock,
dir_ino: u64,
) -> Result<Vec<DirEntry>, UfsError> {
let inode = read_inode(partition, sb, dir_ino)?;
Ok(list_dir_entries(partition, sb, &inode))
}
fn list_dir_entries(partition: &[u8], sb: &Superblock, inode: &Inode) -> Vec<DirEntry> {
let mut entries = Vec::new();
if sb.fsize <= 0 {
return entries; }
let bsize = if sb.bsize > 0 {
sb.bsize as u64
} else {
DIRBLKSIZ as u64 };
let mut remaining = inode.size;
for &addr in &inode.direct {
if remaining == 0 {
break;
}
if addr == 0 {
remaining = remaining.saturating_sub(bsize);
continue;
}
let want = usize::try_from(remaining.min(bsize)).unwrap_or(usize::MAX);
let Ok(block) = read_block(partition, sb, addr, want) else {
break; };
walk_block(block, sb.endian, &mut entries);
remaining = remaining.saturating_sub(bsize);
}
entries
}
fn walk_block(block: &[u8], endian: Endian, out: &mut Vec<DirEntry>) {
let mut off = 0usize;
while off + DIRECT_HEAD <= block.len() {
let ino = u64::from(endian.u32(block, off + OFF_INO));
let reclen = endian.u16(block, off + OFF_RECLEN) as usize;
let d_type = crate::bytes::u8_at(block, off + OFF_TYPE);
let namlen = crate::bytes::u8_at(block, off + OFF_NAMLEN) as usize;
if reclen < DIRECT_HEAD {
break;
}
let name_start = off + OFF_NAME;
let name_cap = reclen.saturating_sub(OFF_NAME);
let take = namlen.min(name_cap);
let name_end = name_start.saturating_add(take).min(block.len());
let name = block
.get(name_start..name_end)
.map(<[u8]>::to_vec)
.unwrap_or_default();
out.push(DirEntry {
name,
ino,
file_type: DirEntryType::from_d_type(d_type),
deleted: ino == 0,
});
off = off.saturating_add(reclen);
}
}
pub fn read_by_path(
partition: &[u8],
sb: &Superblock,
path: &str,
) -> Result<Option<(u64, Inode)>, UfsError> {
let root = read_inode(partition, sb, UFS_ROOTINO)?;
let mut cur_ino = UFS_ROOTINO;
let mut cur = root;
for comp in path.split('/') {
if comp.is_empty() {
continue; }
if !cur.is_dir() {
return Ok(None); }
let entries = list_dir_entries(partition, sb, &cur);
let Some(hit) = entries
.iter()
.find(|e| !e.deleted && e.name == comp.as_bytes())
else {
return Ok(None);
};
cur_ino = hit.ino;
cur = read_inode(partition, sb, cur_ino)?;
}
Ok(Some((cur_ino, cur)))
}
#[cfg(test)]
#[allow(clippy::unreadable_literal)]
mod tests {
use super::*;
use crate::superblock::{UfsVersion, FS_UFS2_MAGIC, SBLOCK_UFS2};
#[test]
fn d_type_classifies_all_dt_values() {
assert_eq!(DirEntryType::from_d_type(0), DirEntryType::Unknown);
assert_eq!(DirEntryType::from_d_type(1), DirEntryType::Fifo);
assert_eq!(DirEntryType::from_d_type(2), DirEntryType::CharDevice);
assert_eq!(DirEntryType::from_d_type(4), DirEntryType::Directory);
assert_eq!(DirEntryType::from_d_type(6), DirEntryType::BlockDevice);
assert_eq!(DirEntryType::from_d_type(8), DirEntryType::Regular);
assert_eq!(DirEntryType::from_d_type(10), DirEntryType::Symlink);
assert_eq!(DirEntryType::from_d_type(12), DirEntryType::Socket);
assert_eq!(DirEntryType::from_d_type(14), DirEntryType::Whiteout);
assert_eq!(DirEntryType::from_d_type(9), DirEntryType::Other(9));
}
fn direct(ino: u32, reclen: u16, d_type: u8, name: &[u8]) -> Vec<u8> {
let mut e = vec![0u8; reclen as usize];
e[OFF_INO..OFF_INO + 4].copy_from_slice(&ino.to_le_bytes());
e[OFF_RECLEN..OFF_RECLEN + 2].copy_from_slice(&reclen.to_le_bytes());
e[OFF_TYPE] = d_type;
e[OFF_NAMLEN] = name.len() as u8;
e[OFF_NAME..OFF_NAME + name.len()].copy_from_slice(name);
e
}
fn real_root_block() -> Vec<u8> {
let mut b = Vec::new();
b.extend(direct(2, 12, 4, b"."));
b.extend(direct(2, 12, 4, b".."));
b.extend(direct(3, 16, 4, b".snap"));
b.extend(direct(128, 20, 4, b"a_directory"));
b.extend(direct(4, 24, 8, b"passwords.txt"));
b.extend(direct(5, 428, 10, b"a_link"));
assert_eq!(b.len(), DIRBLKSIZ, "root block is one DIRBLKSIZ");
b
}
fn walk(block: &[u8]) -> Vec<DirEntry> {
let mut out = Vec::new();
walk_block(block, Endian::Little, &mut out);
out
}
#[test]
fn walk_block_decodes_real_root_layout() {
let entries = walk(&real_root_block());
let names: Vec<&[u8]> = entries.iter().map(|e| e.name.as_slice()).collect();
assert_eq!(
names,
vec![
&b"."[..],
&b".."[..],
&b".snap"[..],
&b"a_directory"[..],
&b"passwords.txt"[..],
&b"a_link"[..],
]
);
let inos: Vec<u64> = entries.iter().map(|e| e.ino).collect();
assert_eq!(inos, vec![2, 2, 3, 128, 4, 5]);
assert_eq!(entries[3].file_type, DirEntryType::Directory);
assert_eq!(entries[4].file_type, DirEntryType::Regular);
assert_eq!(entries[5].file_type, DirEntryType::Symlink);
assert!(entries.iter().all(|e| !e.deleted));
}
#[test]
fn walk_block_surfaces_deleted_slot() {
let mut b = Vec::new();
b.extend(direct(7, 16, 8, b"live"));
b.extend(direct(0, 16, 8, b"ghost")); let entries = walk(&b);
assert_eq!(entries.len(), 2);
assert!(!entries[0].deleted);
assert_eq!(entries[0].name, b"live");
assert!(entries[1].deleted, "d_ino==0 is a deleted slot");
assert_eq!(entries[1].ino, 0);
assert_eq!(entries[1].name, b"ghost", "residual name preserved");
}
#[test]
fn lying_zero_reclen_does_not_loop_forever() {
let mut b = direct(9, 16, 8, b"ok");
let mut bad = vec![0u8; DIRECT_HEAD];
bad[OFF_INO..OFF_INO + 4].copy_from_slice(&5u32.to_le_bytes());
b.extend(bad);
let entries = walk(&b);
assert_eq!(entries.len(), 1, "walk stops at the zero-reclen entry");
assert_eq!(entries[0].name, b"ok");
}
#[test]
fn over_long_namlen_is_clamped_not_overread() {
let mut e = vec![0u8; 16];
e[OFF_INO..OFF_INO + 4].copy_from_slice(&3u32.to_le_bytes());
e[OFF_RECLEN..OFF_RECLEN + 2].copy_from_slice(&16u16.to_le_bytes());
e[OFF_TYPE] = 8;
e[OFF_NAMLEN] = 200; e[OFF_NAME..OFF_NAME + 4].copy_from_slice(b"abcd");
let entries = walk(&e);
assert_eq!(entries.len(), 1);
assert!(entries[0].name.len() <= 16 - OFF_NAME);
}
#[test]
fn reclen_below_head_ends_block() {
let mut e = vec![0u8; 8];
e[OFF_INO..OFF_INO + 4].copy_from_slice(&1u32.to_le_bytes());
e[OFF_RECLEN..OFF_RECLEN + 2].copy_from_slice(&4u16.to_le_bytes()); let entries = walk(&e);
assert!(entries.is_empty());
}
#[test]
fn walk_empty_or_short_block_is_safe() {
assert!(walk(&[]).is_empty());
assert!(walk(&[0u8; 3]).is_empty()); }
fn tiny_sb() -> Superblock {
let mut d = vec![0u8; 1376];
let wr32 = |d: &mut [u8], off: usize, v: i32| {
d[off..off + 4].copy_from_slice(&v.to_le_bytes());
};
let wr64 = |d: &mut [u8], off: usize, v: i64| {
d[off..off + 8].copy_from_slice(&v.to_le_bytes());
};
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);
d[1372..1376].copy_from_slice(&FS_UFS2_MAGIC.to_le_bytes());
Superblock::parse(&d).unwrap()
}
#[test]
fn read_block_offsets_by_fragment_size() {
let sb = tiny_sb();
let mut part = vec![0u8; 8192 + 16];
part[8192..8192 + 4].copy_from_slice(b"HERE");
let block = read_block(&part, &sb, 2, 4).unwrap();
assert_eq!(block, b"HERE");
}
#[test]
fn read_block_clamps_past_end_without_error() {
let sb = tiny_sb();
let part = vec![0u8; 100];
let block = read_block(&part, &sb, 1, 512).unwrap();
assert!(block.is_empty());
}
#[test]
fn read_block_rejects_zero_fsize() {
let mut sb = tiny_sb();
sb.fsize = 0;
assert!(matches!(
read_block(&[0u8; 16], &sb, 0, 4),
Err(UfsError::ImpossibleGeometry {
field: "fs_fsize",
..
})
));
}
fn synthetic_fs() -> (Vec<u8>, Superblock) {
let sb = tiny_sb();
let fsize = 4096usize;
let iblkno = 40usize;
let fpg = 256usize;
let ipg = 128usize;
let inode_size = 256usize;
let root_dir_frag = 60u64;
let adir_frag = 61u64;
let ino_byte = |ino: usize| -> usize {
let c = ino / ipg;
let within = ino % ipg;
(c * fpg + iblkno) * fsize + within * inode_size
};
let max_byte = [
SBLOCK_UFS2 + 1376,
ino_byte(130) + inode_size,
(root_dir_frag as usize + 1) * fsize,
(adir_frag as usize + 1) * fsize,
]
.into_iter()
.max()
.unwrap();
let mut part = vec![0u8; max_byte + 16];
let sb_bytes = {
let mut d = vec![0u8; 1376];
let wr32 = |d: &mut [u8], off: usize, v: i32| {
d[off..off + 4].copy_from_slice(&v.to_le_bytes());
};
let wr64 = |d: &mut [u8], off: usize, v: i64| {
d[off..off + 8].copy_from_slice(&v.to_le_bytes());
};
wr32(&mut d, 8, 24);
wr32(&mut d, 12, 32);
wr32(&mut d, 16, iblkno as i32);
wr32(&mut d, 20, 48);
wr32(&mut d, 44, 4);
wr32(&mut d, 48, 32768);
wr32(&mut d, 52, fsize as i32);
wr32(&mut d, 56, 8);
wr32(&mut d, 184, ipg as i32);
wr32(&mut d, 188, fpg as i32);
wr32(&mut d, 1320, 120);
wr64(&mut d, 1080, 1022);
wr64(&mut d, 1000, SBLOCK_UFS2 as i64);
d[1372..1376].copy_from_slice(&FS_UFS2_MAGIC.to_le_bytes());
d
};
part[SBLOCK_UFS2..SBLOCK_UFS2 + 1376].copy_from_slice(&sb_bytes);
let dir_inode = |frag: u64, size: u64, mode: u16| -> Vec<u8> {
let mut d = vec![0u8; inode_size];
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
};
part[ino_byte(2)..ino_byte(2) + inode_size].copy_from_slice(&dir_inode(
root_dir_frag,
512,
0o040755,
));
part[ino_byte(128)..ino_byte(128) + inode_size]
.copy_from_slice(&dir_inode(adir_frag, 512, 0o040755));
part[ino_byte(4)..ino_byte(4) + inode_size].copy_from_slice(&dir_inode(0, 116, 0o100644));
part[ino_byte(129)..ino_byte(129) + inode_size]
.copy_from_slice(&dir_inode(0, 116, 0o100644));
let root_block = real_root_block();
let rb = root_dir_frag as usize * fsize;
part[rb..rb + root_block.len()].copy_from_slice(&root_block);
let mut adir = Vec::new();
adir.extend(direct(128, 12, 4, b"."));
adir.extend(direct(2, 12, 4, b".."));
adir.extend(direct(129, 488, 8, b"a_file"));
let ab = adir_frag as usize * fsize;
part[ab..ab + adir.len()].copy_from_slice(&adir);
(part, sb)
}
#[test]
fn list_dir_returns_live_root_entries() {
let (part, sb) = synthetic_fs();
let entries = list_dir(&part, &sb, 2).unwrap();
let names: Vec<&[u8]> = entries.iter().map(|e| e.name.as_slice()).collect();
assert_eq!(
names,
vec![
&b"."[..],
&b".."[..],
&b".snap"[..],
&b"a_directory"[..],
&b"passwords.txt"[..],
&b"a_link"[..],
]
);
let pw = entries.iter().find(|e| e.name == b"passwords.txt").unwrap();
assert_eq!(pw.ino, 4);
assert_eq!(pw.file_type, DirEntryType::Regular);
}
#[test]
fn list_dir_skips_hole_in_direct_pointers() {
let sb = tiny_sb();
let fsize = sb.fsize as u64;
let bsize = sb.bsize as u64; let frag0 = 60u64;
let mut dino = vec![0u8; 256];
dino[0..2].copy_from_slice(&0o040755u16.to_le_bytes()); dino[2..4].copy_from_slice(&1u16.to_le_bytes());
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();
assert_eq!(inode.direct[1], 0, "second pointer is a hole");
let mut block = Vec::new();
block.extend(direct(9, 12, 8, b"x"));
block.extend(direct(10, DIRBLKSIZ as u16 - 12, 8, b"y"));
let start = (frag0 * fsize) as usize;
let mut part = vec![0u8; start + block.len()];
part[start..start + block.len()].copy_from_slice(&block);
let entries = list_dir_entries(&part, &sb, &inode);
let names: Vec<&[u8]> = entries.iter().map(|e| e.name.as_slice()).collect();
assert_eq!(
names,
vec![&b"x"[..], &b"y"[..]],
"first block walked, hole skipped"
);
}
#[test]
fn read_by_path_root_resolves_to_inode2() {
let (part, sb) = synthetic_fs();
let (ino, inode) = read_by_path(&part, &sb, "/").unwrap().unwrap();
assert_eq!(ino, 2);
assert!(inode.is_dir());
}
#[test]
fn read_by_path_resolves_known_file_inode4() {
let (part, sb) = synthetic_fs();
let (ino, inode) = read_by_path(&part, &sb, "/passwords.txt").unwrap().unwrap();
assert_eq!(ino, 4);
assert_eq!(inode.size, 116, "P1 metadata: passwords.txt is 116 bytes");
}
#[test]
fn read_by_path_descends_nested_directory() {
let (part, sb) = synthetic_fs();
let (ino, inode) = read_by_path(&part, &sb, "/a_directory/a_file")
.unwrap()
.unwrap();
assert_eq!(ino, 129);
assert!(inode.is_regular());
}
#[test]
fn read_by_path_missing_component_is_none() {
let (part, sb) = synthetic_fs();
assert!(read_by_path(&part, &sb, "/nope").unwrap().is_none());
assert!(read_by_path(&part, &sb, "/a_directory/missing")
.unwrap()
.is_none());
}
#[test]
fn read_by_path_through_non_directory_is_none() {
let (part, sb) = synthetic_fs();
assert!(read_by_path(&part, &sb, "/passwords.txt/x")
.unwrap()
.is_none());
}
}