use crate::bytes::Endian;
use crate::error::UfsError;
use crate::superblock::{Superblock, UfsVersion};
pub const UFS_NDADDR: usize = 12;
pub const UFS_NIADDR: usize = 3;
pub const UFS2_DINODE_SIZE: usize = 256;
pub const UFS1_DINODE_SIZE: usize = 128;
const U2_MODE: usize = 0;
const U2_NLINK: usize = 2;
const U2_UID: usize = 4;
const U2_GID: usize = 8;
const U2_SIZE: usize = 16;
const U2_BLOCKS: usize = 24;
const U2_ATIME: usize = 32;
const U2_MTIME: usize = 40;
const U2_CTIME: usize = 48;
const U2_BIRTHTIME: usize = 56;
const U2_MTIMENSEC: usize = 64;
const U2_ATIMENSEC: usize = 68;
const U2_CTIMENSEC: usize = 72;
const U2_BIRTHNSEC: usize = 76;
const U2_DB: usize = 112;
const U2_IB: usize = 208;
const U1_MODE: usize = 0;
const U1_NLINK: usize = 2;
const U1_SIZE: usize = 8;
const U1_ATIME: usize = 16;
const U1_ATIMENSEC: usize = 20;
const U1_MTIME: usize = 24;
const U1_MTIMENSEC: usize = 28;
const U1_CTIME: usize = 32;
const U1_CTIMENSEC: usize = 36;
const U1_DB: usize = 40;
const U1_IB: usize = 88;
const U1_BLOCKS: usize = 104;
const U1_UID: usize = 112;
const U1_GID: usize = 116;
const IFMT: u16 = 0o170_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Timespec {
pub sec: i64,
pub nsec: i32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum FileType {
Fifo,
CharDevice,
Directory,
BlockDevice,
Regular,
Symlink,
Socket,
Whiteout,
Other(u16),
}
impl FileType {
#[must_use]
pub fn from_mode(mode: u16) -> Self {
match mode & IFMT {
0o010_000 => FileType::Fifo,
0o020_000 => FileType::CharDevice,
0o040_000 => FileType::Directory,
0o060_000 => FileType::BlockDevice,
0o100_000 => FileType::Regular,
0o120_000 => FileType::Symlink,
0o140_000 => FileType::Socket,
0o160_000 => FileType::Whiteout,
other => FileType::Other(other),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Inode {
pub version: UfsVersion,
pub mode: u16,
pub file_type: FileType,
pub nlink: u16,
pub uid: u32,
pub gid: u32,
pub size: u64,
pub blocks: u64,
pub atime: Timespec,
pub mtime: Timespec,
pub ctime: Timespec,
pub birthtime: Option<Timespec>,
pub direct: [u64; UFS_NDADDR],
pub indirect: [u64; UFS_NIADDR],
fast_symlink: Option<Vec<u8>>,
}
impl Inode {
pub fn parse(data: &[u8], version: UfsVersion, endian: Endian) -> Result<Self, UfsError> {
Self::parse_with_maxsymlink(data, version, endian, DEFAULT_MAXSYMLINKLEN)
}
pub fn parse_with_maxsymlink(
data: &[u8],
version: UfsVersion,
endian: Endian,
maxsymlinklen: i32,
) -> Result<Self, UfsError> {
let need = match version {
UfsVersion::Ufs1 => UFS1_DINODE_SIZE,
UfsVersion::Ufs2 => UFS2_DINODE_SIZE,
};
if data.len() < need {
return Err(UfsError::Truncated {
structure: "dinode",
need,
have: data.len(),
});
}
let (mode, nlink, uid, gid, size, blocks, atime, mtime, ctime, birthtime, db_off, ib_off) =
match version {
UfsVersion::Ufs2 => (
endian.u16(data, U2_MODE),
endian.u16(data, U2_NLINK),
endian.u32(data, U2_UID),
endian.u32(data, U2_GID),
endian.u64(data, U2_SIZE),
endian.u64(data, U2_BLOCKS),
Timespec {
sec: endian.i64(data, U2_ATIME),
nsec: endian.i32(data, U2_ATIMENSEC),
},
Timespec {
sec: endian.i64(data, U2_MTIME),
nsec: endian.i32(data, U2_MTIMENSEC),
},
Timespec {
sec: endian.i64(data, U2_CTIME),
nsec: endian.i32(data, U2_CTIMENSEC),
},
Some(Timespec {
sec: endian.i64(data, U2_BIRTHTIME),
nsec: endian.i32(data, U2_BIRTHNSEC),
}),
U2_DB,
U2_IB,
),
UfsVersion::Ufs1 => (
endian.u16(data, U1_MODE),
endian.u16(data, U1_NLINK),
endian.u32(data, U1_UID),
endian.u32(data, U1_GID),
endian.u64(data, U1_SIZE),
u64::from(endian.u32(data, U1_BLOCKS)),
Timespec {
sec: i64::from(endian.i32(data, U1_ATIME)),
nsec: endian.i32(data, U1_ATIMENSEC),
},
Timespec {
sec: i64::from(endian.i32(data, U1_MTIME)),
nsec: endian.i32(data, U1_MTIMENSEC),
},
Timespec {
sec: i64::from(endian.i32(data, U1_CTIME)),
nsec: endian.i32(data, U1_CTIMENSEC),
},
None,
U1_DB,
U1_IB,
),
};
let ptr_size = match version {
UfsVersion::Ufs1 => 4usize,
UfsVersion::Ufs2 => 8usize,
};
let read_ptr = |off: usize| -> u64 {
match version {
UfsVersion::Ufs1 => u64::from(endian.u32(data, off)),
UfsVersion::Ufs2 => endian.u64(data, off),
}
};
let mut direct = [0u64; UFS_NDADDR];
for (i, slot) in direct.iter_mut().enumerate() {
*slot = read_ptr(db_off + i * ptr_size);
}
let mut indirect = [0u64; UFS_NIADDR];
for (i, slot) in indirect.iter_mut().enumerate() {
*slot = read_ptr(ib_off + i * ptr_size);
}
let file_type = FileType::from_mode(mode);
let fast_symlink = if file_type == FileType::Symlink
&& maxsymlinklen > 0
&& size <= maxsymlinklen as u64
{
let region_len = (UFS_NDADDR + UFS_NIADDR) * ptr_size;
let take = (size as usize).min(region_len);
data.get(db_off..db_off + take).map(<[u8]>::to_vec)
} else {
None
};
Ok(Self {
version,
mode,
file_type,
nlink,
uid,
gid,
size,
blocks,
atime,
mtime,
ctime,
birthtime,
direct,
indirect,
fast_symlink,
})
}
#[must_use]
pub fn is_dir(&self) -> bool {
self.file_type == FileType::Directory
}
#[must_use]
pub fn is_regular(&self) -> bool {
self.file_type == FileType::Regular
}
#[must_use]
pub fn is_symlink(&self) -> bool {
self.file_type == FileType::Symlink
}
#[must_use]
pub fn symlink_target(&self) -> Option<&[u8]> {
self.fast_symlink.as_deref()
}
}
const DEFAULT_MAXSYMLINKLEN: i32 = ((UFS_NDADDR + UFS_NIADDR) * 8) as i32;
pub fn read_inode(partition: &[u8], sb: &Superblock, ino: u64) -> Result<Inode, UfsError> {
if sb.ipg <= 0 {
return Err(UfsError::ImpossibleGeometry {
field: "fs_ipg",
value: sb.ipg as u64,
limit: i64::MAX as u64,
});
}
if sb.fpg <= 0 {
return Err(UfsError::ImpossibleGeometry {
field: "fs_fpg",
value: sb.fpg 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,
});
}
if sb.iblkno < 0 {
return Err(UfsError::ImpossibleGeometry {
field: "fs_iblkno",
value: sb.iblkno as u64,
limit: i64::MAX as u64,
});
}
let ipg = sb.ipg as u64;
let fpg = sb.fpg as u64;
let fsize = sb.fsize as u64;
let iblkno = sb.iblkno as u64;
let inode_size = u64::from(sb.inode_size());
let count = ipg.saturating_mul(u64::from(sb.ncg));
if ino >= count {
return Err(UfsError::InodeOutOfRange { ino, count });
}
let cg = ino / ipg;
let within = ino % ipg;
let cgimin = cg.saturating_mul(fpg).saturating_add(iblkno);
let byte = cgimin
.saturating_mul(fsize)
.saturating_add(within.saturating_mul(inode_size));
let start = usize::try_from(byte).unwrap_or(usize::MAX);
let end = start.saturating_add(inode_size as usize);
let Some(slice) = partition.get(start..end) else {
return Err(UfsError::Truncated {
structure: "dinode (located)",
need: end,
have: partition.len(),
});
};
Inode::parse_with_maxsymlink(slice, sb.version, sb.endian, sb.maxsymlinklen)
}
#[cfg(test)]
#[allow(clippy::unreadable_literal)]
mod tests {
use super::*;
fn ufs2_dinode(mode: u16, size: u64, db0: u64) -> Vec<u8> {
let mut d = vec![0u8; UFS2_DINODE_SIZE];
d[U2_MODE..U2_MODE + 2].copy_from_slice(&mode.to_le_bytes());
d[U2_NLINK..U2_NLINK + 2].copy_from_slice(&1u16.to_le_bytes());
d[U2_UID..U2_UID + 4].copy_from_slice(&1000u32.to_le_bytes());
d[U2_GID..U2_GID + 4].copy_from_slice(&1000u32.to_le_bytes());
d[U2_SIZE..U2_SIZE + 8].copy_from_slice(&size.to_le_bytes());
d[U2_BLOCKS..U2_BLOCKS + 8].copy_from_slice(&8u64.to_le_bytes());
d[U2_MTIME..U2_MTIME + 8].copy_from_slice(&0x1122_3344i64.to_le_bytes());
d[U2_MTIMENSEC..U2_MTIMENSEC + 4].copy_from_slice(&500i32.to_le_bytes());
d[U2_BIRTHTIME..U2_BIRTHTIME + 8].copy_from_slice(&0x2233i64.to_le_bytes());
d[U2_DB..U2_DB + 8].copy_from_slice(&db0.to_le_bytes());
d
}
fn ufs1_dinode(mode: u16, size: u64, db0: u32) -> Vec<u8> {
let mut d = vec![0u8; UFS1_DINODE_SIZE];
d[U1_MODE..U1_MODE + 2].copy_from_slice(&mode.to_le_bytes());
d[U1_NLINK..U1_NLINK + 2].copy_from_slice(&2u16.to_le_bytes());
d[U1_SIZE..U1_SIZE + 8].copy_from_slice(&size.to_le_bytes());
d[U1_MTIME..U1_MTIME + 4].copy_from_slice(&0x0055_6677u32.to_le_bytes());
d[U1_MTIMENSEC..U1_MTIMENSEC + 4].copy_from_slice(&7i32.to_le_bytes());
d[U1_BLOCKS..U1_BLOCKS + 4].copy_from_slice(&4u32.to_le_bytes());
d[U1_UID..U1_UID + 4].copy_from_slice(&501u32.to_le_bytes());
d[U1_GID..U1_GID + 4].copy_from_slice(&20u32.to_le_bytes());
d[U1_DB..U1_DB + 4].copy_from_slice(&db0.to_le_bytes());
d
}
#[test]
fn file_type_from_mode_classifies_all_ifmt() {
assert_eq!(FileType::from_mode(0o040755), FileType::Directory);
assert_eq!(FileType::from_mode(0o100644), FileType::Regular);
assert_eq!(FileType::from_mode(0o120777), FileType::Symlink);
assert_eq!(FileType::from_mode(0o010000), FileType::Fifo);
assert_eq!(FileType::from_mode(0o020000), FileType::CharDevice);
assert_eq!(FileType::from_mode(0o060000), FileType::BlockDevice);
assert_eq!(FileType::from_mode(0o140000), FileType::Socket);
assert_eq!(FileType::from_mode(0o160000), FileType::Whiteout);
assert_eq!(FileType::from_mode(0o050000), FileType::Other(0o050000));
}
#[test]
fn decodes_ufs2_regular_file() {
let d = ufs2_dinode(0o100644, 116, 57);
let ino = Inode::parse(&d, UfsVersion::Ufs2, Endian::Little).unwrap();
assert_eq!(ino.version, UfsVersion::Ufs2);
assert_eq!(ino.file_type, FileType::Regular);
assert!(ino.is_regular());
assert!(!ino.is_dir());
assert_eq!(ino.mode & 0o7777, 0o644);
assert_eq!(ino.nlink, 1);
assert_eq!(ino.uid, 1000);
assert_eq!(ino.gid, 1000);
assert_eq!(ino.size, 116);
assert_eq!(ino.blocks, 8);
assert_eq!(ino.mtime.sec, 0x1122_3344);
assert_eq!(ino.mtime.nsec, 500);
assert_eq!(
ino.birthtime,
Some(Timespec {
sec: 0x2233,
nsec: 0
})
);
assert_eq!(ino.direct[0], 57);
assert!(ino.direct[1..].iter().all(|&b| b == 0));
assert!(ino.symlink_target().is_none());
}
#[test]
fn decodes_ufs2_directory() {
let d = ufs2_dinode(0o040755, 512, 56);
let ino = Inode::parse(&d, UfsVersion::Ufs2, Endian::Little).unwrap();
assert!(ino.is_dir());
assert_eq!(ino.direct[0], 56);
}
#[test]
fn decodes_ufs2_fast_symlink_inline_target() {
let target = b"a_directory/another_file";
let mut d = ufs2_dinode(0o120755, target.len() as u64, 0);
d[U2_DB..U2_DB + target.len()].copy_from_slice(target);
let ino = Inode::parse(&d, UfsVersion::Ufs2, Endian::Little).unwrap();
assert_eq!(ino.file_type, FileType::Symlink);
assert!(ino.is_symlink());
assert_eq!(ino.symlink_target(), Some(&target[..]));
}
#[test]
fn slow_symlink_over_threshold_has_no_inline_target() {
let d = ufs2_dinode(0o120755, 200, 57);
let ino = Inode::parse_with_maxsymlink(&d, UfsVersion::Ufs2, Endian::Little, 120).unwrap();
assert_eq!(ino.file_type, FileType::Symlink);
assert!(ino.symlink_target().is_none());
assert_eq!(ino.direct[0], 57);
}
#[test]
fn decodes_ufs2_big_endian() {
let mut d = vec![0u8; UFS2_DINODE_SIZE];
d[U2_MODE..U2_MODE + 2].copy_from_slice(&0o100644u16.to_be_bytes());
d[U2_SIZE..U2_SIZE + 8].copy_from_slice(&999u64.to_be_bytes());
d[U2_DB..U2_DB + 8].copy_from_slice(&123u64.to_be_bytes());
let ino = Inode::parse(&d, UfsVersion::Ufs2, Endian::Big).unwrap();
assert_eq!(ino.file_type, FileType::Regular);
assert_eq!(ino.size, 999);
assert_eq!(ino.direct[0], 123);
}
#[test]
fn decodes_ufs1_dinode_32bit_layout() {
let d = ufs1_dinode(0o100600, 4096, 0xdead);
let ino = Inode::parse(&d, UfsVersion::Ufs1, Endian::Little).unwrap();
assert_eq!(ino.version, UfsVersion::Ufs1);
assert_eq!(ino.file_type, FileType::Regular);
assert_eq!(ino.mode & 0o7777, 0o600);
assert_eq!(ino.nlink, 2);
assert_eq!(ino.uid, 501);
assert_eq!(ino.gid, 20);
assert_eq!(ino.size, 4096);
assert_eq!(ino.blocks, 4);
assert_eq!(ino.mtime.sec, 0x0055_6677);
assert_eq!(ino.mtime.nsec, 7);
assert_eq!(ino.birthtime, None, "UFS1 has no birthtime");
assert_eq!(ino.direct[0], 0xdead);
}
#[test]
fn decodes_ufs1_fast_symlink() {
let target = b"../elsewhere";
let mut d = ufs1_dinode(0o120777, target.len() as u64, 0);
d[U1_DB..U1_DB + target.len()].copy_from_slice(target);
let ino = Inode::parse_with_maxsymlink(&d, UfsVersion::Ufs1, Endian::Little, 60).unwrap();
assert_eq!(ino.symlink_target(), Some(&target[..]));
}
#[test]
fn truncated_dinode_fails_loud_not_panic() {
let d = vec![0u8; UFS2_DINODE_SIZE - 1];
let err = Inode::parse(&d, UfsVersion::Ufs2, Endian::Little).unwrap_err();
assert!(matches!(
err,
UfsError::Truncated {
structure: "dinode",
need: UFS2_DINODE_SIZE,
..
}
));
}
#[test]
fn empty_dinode_buffer_does_not_panic() {
assert!(matches!(
Inode::parse(&[], UfsVersion::Ufs2, Endian::Little),
Err(UfsError::Truncated { .. })
));
}
fn synthetic_partition(ino_to_place: u64, dinode: &[u8]) -> (Vec<u8>, Superblock) {
use crate::superblock::{FS_UFS2_MAGIC, SBLOCK_UFS2};
let iblkno = 40u64;
let fsize = 4096u64;
let ipg = 128u64;
let fpg = 256u64;
let ncg = 4u32;
let inode_size = 256u64;
let cg = ino_to_place / ipg;
let within = ino_to_place % ipg;
let cgimin = cg * fpg + iblkno;
let byte = (cgimin * fsize + within * inode_size) as usize;
let sboff = SBLOCK_UFS2;
let total = (byte + dinode.len()).max(sboff + 1376) + 16;
let mut part = vec![0u8; total];
part[byte..byte + dinode.len()].copy_from_slice(dinode);
let wr32 = |p: &mut [u8], off: usize, v: i32| {
p[off..off + 4].copy_from_slice(&v.to_le_bytes());
};
let wr64 = |p: &mut [u8], off: usize, v: i64| {
p[off..off + 8].copy_from_slice(&v.to_le_bytes());
};
wr32(&mut part, sboff + 8, 24); wr32(&mut part, sboff + 12, 32); wr32(&mut part, sboff + 16, iblkno as i32); wr32(&mut part, sboff + 20, 48); wr32(&mut part, sboff + 44, ncg as i32); wr32(&mut part, sboff + 48, 32768); wr32(&mut part, sboff + 52, fsize as i32); wr32(&mut part, sboff + 56, 8); wr32(&mut part, sboff + 80, 15); wr32(&mut part, sboff + 84, 12); wr32(&mut part, sboff + 120, 128); wr32(&mut part, sboff + 184, ipg as i32); wr32(&mut part, sboff + 188, fpg as i32); wr32(&mut part, sboff + 1320, 120); wr64(&mut part, sboff + 1080, 1022); wr64(&mut part, sboff + 1088, 901); wr64(&mut part, sboff + 1000, SBLOCK_UFS2 as i64); part[sboff + 1372..sboff + 1376].copy_from_slice(&FS_UFS2_MAGIC.to_le_bytes());
let sb = Superblock::parse(&part[sboff..]).unwrap();
(part, sb)
}
#[test]
fn read_inode_locates_and_decodes() {
let dinode = ufs2_dinode(0o100644, 116, 57);
let (part, sb) = synthetic_partition(4, &dinode);
let ino = read_inode(&part, &sb, 4).unwrap();
assert_eq!(ino.file_type, FileType::Regular);
assert_eq!(ino.size, 116);
assert_eq!(ino.direct[0], 57);
}
#[test]
fn read_inode_rejects_out_of_range() {
let (part, sb) = synthetic_partition(4, &ufs2_dinode(0o100644, 1, 1));
let err = read_inode(&part, &sb, 512).unwrap_err();
assert!(matches!(
err,
UfsError::InodeOutOfRange {
ino: 512,
count: 512
}
));
}
#[test]
fn read_inode_truncated_partition_fails_loud() {
let dinode = ufs2_dinode(0o100644, 1, 1);
let (mut part, sb) = synthetic_partition(4, &dinode);
part.truncate(180_000);
let err = read_inode(&part, &sb, 200).unwrap_err();
assert!(matches!(err, UfsError::Truncated { .. }));
}
#[test]
fn read_inode_rejects_zero_ipg_geometry() {
let dinode = ufs2_dinode(0o100644, 1, 1);
let (part, mut sb) = synthetic_partition(4, &dinode);
sb.ipg = 0;
let err = read_inode(&part, &sb, 4).unwrap_err();
assert!(matches!(
err,
UfsError::ImpossibleGeometry {
field: "fs_ipg",
..
}
));
}
#[test]
fn read_inode_rejects_zero_fpg_geometry() {
let (part, mut sb) = synthetic_partition(4, &ufs2_dinode(0o100644, 1, 1));
sb.fpg = 0;
let err = read_inode(&part, &sb, 4).unwrap_err();
assert!(matches!(
err,
UfsError::ImpossibleGeometry {
field: "fs_fpg",
..
}
));
}
#[test]
fn read_inode_rejects_zero_fsize_geometry() {
let (part, mut sb) = synthetic_partition(4, &ufs2_dinode(0o100644, 1, 1));
sb.fsize = 0;
let err = read_inode(&part, &sb, 4).unwrap_err();
assert!(matches!(
err,
UfsError::ImpossibleGeometry {
field: "fs_fsize",
..
}
));
}
#[test]
fn read_inode_rejects_negative_iblkno_geometry() {
let (part, mut sb) = synthetic_partition(4, &ufs2_dinode(0o100644, 1, 1));
sb.iblkno = -1;
let err = read_inode(&part, &sb, 4).unwrap_err();
assert!(matches!(
err,
UfsError::ImpossibleGeometry {
field: "fs_iblkno",
..
}
));
}
}