use crate::dir::{read_block, read_by_path};
use crate::error::UfsError;
use crate::inode::{read_inode, Inode, UFS_NDADDR};
use crate::superblock::{Superblock, UfsVersion};
pub fn read_file(partition: &[u8], sb: &Superblock, ino: u64) -> Result<Vec<u8>, UfsError> {
let inode = read_inode(partition, sb, ino)?;
read_inode_file(partition, sb, &inode)
}
pub fn read_inode_file(
partition: &[u8],
sb: &Superblock,
inode: &Inode,
) -> Result<Vec<u8>, UfsError> {
if sb.bsize <= 0 {
return Err(UfsError::ImpossibleGeometry {
field: "fs_bsize",
value: sb.bsize as u64,
limit: i64::MAX as u64,
});
}
if sb.fsize <= 0 {
return Err(UfsError::ImpossibleGeometry {
field: "fs_fsize",
value: sb.fsize as u64,
limit: i64::MAX as u64,
});
}
let size = inode.size;
let part_len = partition.len() as u64;
if size > part_len {
return Err(UfsError::ImpossibleGeometry {
field: "di_size",
value: size,
limit: part_len,
});
}
let bsize = sb.bsize as u64;
let size_usize = usize::try_from(size).unwrap_or(usize::MAX);
let mut out = vec![0u8; size_usize];
if size == 0 {
return Ok(out);
}
let n_blocks = size.div_ceil(bsize);
let nindir = if sb.nindir > 0 { sb.nindir as u64 } else { 0 };
let mut remaining = size;
let mut written = 0usize;
let mut bi: u64 = 0;
while bi < n_blocks {
let this_len = usize::try_from(remaining.min(bsize)).unwrap_or(usize::MAX);
let addr = resolve_block(partition, sb, inode, bi, nindir);
if addr != 0 {
let block = read_block(partition, sb, addr, this_len)?;
let take = block.len().min(this_len);
if let Some(dst) = out.get_mut(written..written + take) {
dst.copy_from_slice(&block[..take]);
}
}
written = written.saturating_add(this_len);
remaining = remaining.saturating_sub(bsize);
bi += 1;
}
Ok(out)
}
fn resolve_block(partition: &[u8], sb: &Superblock, inode: &Inode, bi: u64, nindir: u64) -> u64 {
let ndaddr = UFS_NDADDR as u64;
if bi < ndaddr {
return inode.direct[bi as usize];
}
if nindir == 0 {
return 0; }
let mut i = bi - ndaddr;
if i < nindir {
return indirect_ptr(partition, sb, inode.indirect[0], i);
}
i -= nindir;
let nindir2 = nindir.saturating_mul(nindir);
if i < nindir2 {
let sib = indirect_ptr(partition, sb, inode.indirect[1], i / nindir);
return indirect_ptr(partition, sb, sib, i % nindir);
}
i -= nindir2;
let nindir3 = nindir2.saturating_mul(nindir);
if i >= nindir3 {
return 0; }
let dib = indirect_ptr(partition, sb, inode.indirect[2], i / nindir2);
let rem = i % nindir2;
let sib = indirect_ptr(partition, sb, dib, rem / nindir);
indirect_ptr(partition, sb, sib, rem % nindir)
}
fn indirect_ptr(partition: &[u8], sb: &Superblock, ind_addr: u64, idx: u64) -> u64 {
if ind_addr == 0 {
return 0;
}
let bsize = if sb.bsize > 0 { sb.bsize as usize } else { 0 };
let Ok(block) = read_block(partition, sb, ind_addr, bsize) else {
return 0; };
let ptr_size = match sb.version {
UfsVersion::Ufs2 => 8usize,
UfsVersion::Ufs1 => 4usize,
};
let off = usize::try_from(idx.saturating_mul(ptr_size as u64)).unwrap_or(usize::MAX);
match sb.version {
UfsVersion::Ufs2 => sb.endian.u64(block, off),
UfsVersion::Ufs1 => u64::from(sb.endian.u32(block, off)),
}
}
pub fn read_symlink_target(
partition: &[u8],
sb: &Superblock,
inode: &Inode,
) -> Result<Vec<u8>, UfsError> {
if let Some(inline) = inode.symlink_target() {
return Ok(inline.to_vec());
}
read_inode_file(partition, sb, inode)
}
pub fn read_path_content(
partition: &[u8],
sb: &Superblock,
path: &str,
) -> Result<Option<Vec<u8>>, UfsError> {
let Some((_ino, inode)) = read_by_path(partition, sb, path)? else {
return Ok(None);
};
Ok(Some(read_inode_file(partition, sb, &inode)?))
}
#[cfg(test)]
#[allow(clippy::unreadable_literal)]
mod tests {
use super::*;
use crate::superblock::FS_UFS2_MAGIC;
fn tiny_sb(bsize: i32, fsize: i32, nindir: i32) -> 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, 1); wr32(&mut d, 12, 2); wr32(&mut d, 16, 4); wr32(&mut d, 20, 8); wr32(&mut d, 44, 1); wr32(&mut d, 48, bsize); wr32(&mut d, 52, fsize); wr32(&mut d, 56, 1); wr32(&mut d, 116, nindir); wr32(&mut d, 120, bsize / 256); wr32(&mut d, 184, 128); wr32(&mut d, 188, 4096); wr32(&mut d, 1320, 120); wr64(&mut d, 1080, 65536); d[1372..1376].copy_from_slice(&FS_UFS2_MAGIC.to_le_bytes());
Superblock::parse(&d).unwrap()
}
fn inode_with(size: u64, direct: &[u64], ib: [u64; 3]) -> Inode {
let mut d = vec![0u8; 256];
d[0..2].copy_from_slice(&0o100644u16.to_le_bytes()); d[2..4].copy_from_slice(&1u16.to_le_bytes()); d[16..24].copy_from_slice(&size.to_le_bytes()); for (i, &a) in direct.iter().enumerate() {
d[112 + i * 8..112 + i * 8 + 8].copy_from_slice(&a.to_le_bytes());
}
for (i, &a) in ib.iter().enumerate() {
d[208 + i * 8..208 + i * 8 + 8].copy_from_slice(&a.to_le_bytes());
}
Inode::parse(&d, UfsVersion::Ufs2, crate::Endian::Little).unwrap()
}
#[test]
fn read_file_direct_only_single_block() {
let sb = tiny_sb(512, 512, 64);
let mut part = vec![0u8; 512 * 32];
let payload: Vec<u8> = (0..100u16).map(|i| (i & 0xff) as u8).collect();
let frag = 10usize;
part[frag * 512..frag * 512 + payload.len()].copy_from_slice(&payload);
let inode = inode_with(payload.len() as u64, &[frag as u64], [0, 0, 0]);
let got = read_inode_file(&part, &sb, &inode).unwrap();
assert_eq!(got, payload);
}
#[test]
fn read_file_zero_size_is_empty() {
let sb = tiny_sb(512, 512, 64);
let part = vec![0u8; 512 * 4];
let inode = inode_with(0, &[0; 12], [0, 0, 0]);
assert!(read_inode_file(&part, &sb, &inode).unwrap().is_empty());
}
#[test]
fn read_file_single_indirect_block() {
let sb = tiny_sb(512, 512, 64);
let mut part = vec![0u8; 512 * 64];
let mut direct = [0u64; 12];
for (i, slot) in direct.iter_mut().enumerate() {
let f = 20 + i as u64;
*slot = f;
part[f as usize * 512] = (i + 1) as u8; }
let sib = 40usize;
let data13 = 41u64;
part[sib * 512..sib * 512 + 8].copy_from_slice(&data13.to_le_bytes());
part[data13 as usize * 512] = 0xAB;
let size = 13 * 512u64;
let inode = inode_with(size, &direct, [sib as u64, 0, 0]);
let got = read_inode_file(&part, &sb, &inode).unwrap();
assert_eq!(got.len() as u64, size);
assert_eq!(got[0], 1, "first direct block marker");
assert_eq!(got[12 * 512], 0xAB, "block 12 came via single-indirect");
}
#[test]
fn read_file_double_and_triple_indirect_with_nindir2() {
let sb = tiny_sb(512, 512, 2);
let bsize = 512usize;
let mut part = vec![0u8; bsize * 128];
let put_ptr = |p: &mut [u8], frag: usize, idx: usize, target: u64| {
let off = frag * bsize + idx * 8;
p[off..off + 8].copy_from_slice(&target.to_le_bytes());
};
let mark = |p: &mut [u8], frag: usize, m: u8| p[frag * bsize] = m;
let mut direct = [0u64; 12];
for (i, slot) in direct.iter_mut().enumerate() {
let f = 20 + i;
*slot = f as u64;
mark(&mut part, f, i as u8 + 1);
}
put_ptr(&mut part, 40, 0, 50);
put_ptr(&mut part, 40, 1, 51);
mark(&mut part, 50, 100);
mark(&mut part, 51, 101);
put_ptr(&mut part, 41, 0, 42);
put_ptr(&mut part, 41, 1, 43);
put_ptr(&mut part, 42, 0, 52); put_ptr(&mut part, 42, 1, 53); put_ptr(&mut part, 43, 0, 54); put_ptr(&mut part, 43, 1, 55); for (blk, f) in [(14, 52), (15, 53), (16, 54), (17, 55)] {
mark(&mut part, f, blk as u8);
}
put_ptr(&mut part, 44, 0, 45);
put_ptr(&mut part, 45, 0, 46);
put_ptr(&mut part, 46, 0, 56); mark(&mut part, 56, 200);
let n_blocks = 19u64; let size = n_blocks * bsize as u64;
let inode = inode_with(size, &direct, [40, 41, 44]);
let got = read_inode_file(&part, &sb, &inode).unwrap();
assert_eq!(got.len() as u64, size);
assert_eq!(got[0], 1, "direct block 0");
assert_eq!(got[12 * bsize], 100, "block 12 via single-indirect");
assert_eq!(got[14 * bsize], 14, "block 14 via double-indirect");
assert_eq!(got[17 * bsize], 17, "block 17 via double-indirect");
assert_eq!(got[18 * bsize], 200, "block 18 via triple-indirect");
}
#[test]
fn read_file_ufs1_single_indirect_uses_4byte_pointers() {
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());
};
wr32(&mut d, 8, 1);
wr32(&mut d, 12, 2);
wr32(&mut d, 16, 4);
wr32(&mut d, 20, 8);
wr32(&mut d, 44, 1);
wr32(&mut d, 48, 512); wr32(&mut d, 52, 512); wr32(&mut d, 56, 1); wr32(&mut d, 116, 128); wr32(&mut d, 120, 4); wr32(&mut d, 184, 128);
wr32(&mut d, 188, 4096);
wr32(&mut d, 1320, 60);
wr32(&mut d, 36, 65536); d[1372..1376].copy_from_slice(&crate::superblock::FS_UFS1_MAGIC.to_le_bytes());
let sb = Superblock::parse(&d).unwrap();
assert_eq!(sb.version, UfsVersion::Ufs1);
let bsize = 512usize;
let mut part = vec![0u8; bsize * 64];
let mut dn = vec![0u8; 128];
dn[0..2].copy_from_slice(&0o100644u16.to_le_bytes());
dn[8..16].copy_from_slice(&(13u64 * bsize as u64).to_le_bytes());
for i in 0..12u32 {
let f = 20 + i;
dn[40 + i as usize * 4..40 + i as usize * 4 + 4].copy_from_slice(&f.to_le_bytes());
part[f as usize * bsize] = i as u8 + 1;
}
let sib = 40u32;
dn[88..92].copy_from_slice(&sib.to_le_bytes());
part[sib as usize * bsize..sib as usize * bsize + 4].copy_from_slice(&41u32.to_le_bytes());
part[41 * bsize] = 0xCD;
let inode = Inode::parse(&dn, UfsVersion::Ufs1, crate::Endian::Little).unwrap();
let got = read_inode_file(&part, &sb, &inode).unwrap();
assert_eq!(got.len(), 13 * bsize);
assert_eq!(
got[12 * bsize],
0xCD,
"UFS1 4-byte indirect pointer resolved"
);
}
#[test]
fn read_file_by_inode_number_locates_then_reads() {
let (part, sb) = minimal_fs();
let got = read_file(&part, &sb, 4).unwrap();
assert_eq!(got, b"twenty-byte-content!");
}
#[test]
fn read_file_hole_zero_fills() {
let sb = tiny_sb(512, 512, 64);
let mut part = vec![0u8; 512 * 16];
let f1 = 9u64;
part[f1 as usize * 512] = 0x77;
let inode = inode_with(2 * 512, &[0, f1], [0, 0, 0]);
let got = read_inode_file(&part, &sb, &inode).unwrap();
assert!(got[..512].iter().all(|&b| b == 0), "hole zero-filled");
assert_eq!(got[512], 0x77);
}
#[test]
fn read_file_rejects_allocation_bomb() {
let sb = tiny_sb(512, 512, 64);
let part = vec![0u8; 512 * 4];
let inode = inode_with(u64::MAX, &[0; 12], [0, 0, 0]);
let err = read_inode_file(&part, &sb, &inode).unwrap_err();
assert!(matches!(
err,
UfsError::ImpossibleGeometry {
field: "di_size",
..
}
));
}
#[test]
fn read_file_rejects_zero_bsize() {
let mut sb = tiny_sb(512, 512, 64);
sb.bsize = 0;
let part = vec![0u8; 512 * 4];
let inode = inode_with(100, &[1], [0, 0, 0]);
let err = read_inode_file(&part, &sb, &inode).unwrap_err();
assert!(matches!(
err,
UfsError::ImpossibleGeometry {
field: "fs_bsize",
..
}
));
}
#[test]
fn read_file_rejects_zero_fsize() {
let mut sb = tiny_sb(512, 512, 64);
sb.fsize = 0;
let part = vec![0u8; 512 * 4];
let inode = inode_with(100, &[1], [0, 0, 0]);
let err = read_inode_file(&part, &sb, &inode).unwrap_err();
assert!(matches!(
err,
UfsError::ImpossibleGeometry {
field: "fs_fsize",
..
}
));
}
#[test]
fn read_file_truncated_partition_no_panic() {
let sb = tiny_sb(512, 512, 64);
let part = vec![0u8; 512 * 3];
let inode = inode_with(2 * 512, &[2, 100], [0, 0, 0]); let got = read_inode_file(&part, &sb, &inode).unwrap();
assert_eq!(got.len(), 2 * 512);
}
#[test]
fn resolve_block_zero_nindir_collapses_to_direct_only() {
let sb = tiny_sb(512, 512, 0); let inode = inode_with(20 * 512, &[5; 12], [40, 0, 0]);
assert_eq!(resolve_block(&[], &sb, &inode, 12, 0), 0);
assert_eq!(resolve_block(&[], &sb, &inode, 0, 0), 5);
}
#[test]
fn indirect_ptr_hole_addr_is_zero() {
let sb = tiny_sb(512, 512, 64);
assert_eq!(indirect_ptr(&[0u8; 512], &sb, 0, 0), 0);
}
#[test]
fn read_symlink_target_fast_inline() {
let sb = tiny_sb(512, 512, 64);
let mut d = vec![0u8; 256];
d[0..2].copy_from_slice(&0o120755u16.to_le_bytes());
d[16..24].copy_from_slice(&5u64.to_le_bytes());
d[112..117].copy_from_slice(b"a/b/c");
let inode = Inode::parse(&d, UfsVersion::Ufs2, crate::Endian::Little).unwrap();
let target = read_symlink_target(&[], &sb, &inode).unwrap();
assert_eq!(target, b"a/b/c");
}
#[test]
fn read_symlink_target_slow_reads_data_block() {
let sb = tiny_sb(512, 512, 64);
let target = b"x".repeat(130);
let mut part = vec![0u8; 512 * 16];
let frag = 7u64;
part[frag as usize * 512..frag as usize * 512 + 130].copy_from_slice(&target);
let mut d = vec![0u8; 256];
d[0..2].copy_from_slice(&0o120755u16.to_le_bytes());
d[16..24].copy_from_slice(&130u64.to_le_bytes());
d[112..120].copy_from_slice(&frag.to_le_bytes());
let inode = Inode::parse(&d, UfsVersion::Ufs2, crate::Endian::Little).unwrap();
assert!(
inode.symlink_target().is_none(),
"slow symlink is not inline"
);
let got = read_symlink_target(&part, &sb, &inode).unwrap();
assert_eq!(got, target);
}
fn minimal_fs() -> (Vec<u8>, Superblock) {
let sb = tiny_sb(512, 512, 64);
let fsize = 512usize;
let iblkno = 4usize;
let fpg = 4096usize;
let ipg = 128usize;
let isz = 256usize;
let ino_byte = |ino: usize| (ino / ipg * fpg + iblkno) * fsize + (ino % ipg) * isz;
let root_frag = 300u64;
let file_frag = 301u64;
let max = [ino_byte(5), (file_frag as usize + 1) * fsize]
.into_iter()
.max()
.unwrap();
let mut part = vec![0u8; max + 16];
let mut rdi = vec![0u8; isz];
rdi[0..2].copy_from_slice(&0o040755u16.to_le_bytes());
rdi[16..24].copy_from_slice(&512u64.to_le_bytes());
rdi[112..120].copy_from_slice(&root_frag.to_le_bytes());
part[ino_byte(2)..ino_byte(2) + isz].copy_from_slice(&rdi);
let payload = b"twenty-byte-content!";
let mut fi = vec![0u8; isz];
fi[0..2].copy_from_slice(&0o100644u16.to_le_bytes());
fi[16..24].copy_from_slice(&(payload.len() as u64).to_le_bytes());
fi[112..120].copy_from_slice(&file_frag.to_le_bytes());
part[ino_byte(4)..ino_byte(4) + isz].copy_from_slice(&fi);
let fb = file_frag as usize * fsize;
part[fb..fb + payload.len()].copy_from_slice(payload);
let mut rb = Vec::new();
let direct = |ino: u32, reclen: u16, name: &[u8]| -> Vec<u8> {
let mut e = vec![0u8; reclen as usize];
e[0..4].copy_from_slice(&ino.to_le_bytes());
e[4..6].copy_from_slice(&reclen.to_le_bytes());
e[6] = 8; e[7] = name.len() as u8;
e[8..8 + name.len()].copy_from_slice(name);
e
};
rb.extend(direct(2, 12, b"."));
rb.extend(direct(2, 12, b".."));
rb.extend(direct(4, 512 - 24, b"f"));
let rbo = root_frag as usize * fsize;
part[rbo..rbo + rb.len()].copy_from_slice(&rb);
(part, sb)
}
#[test]
fn read_path_content_reads_a_file_and_missing_is_none() {
let (part, sb) = minimal_fs();
let got = read_path_content(&part, &sb, "/f").unwrap().expect("found");
assert_eq!(got, b"twenty-byte-content!");
assert!(read_path_content(&part, &sb, "/does-not-exist")
.unwrap()
.is_none());
}
}