use crate::bytes::{u8_at, Endian};
use crate::error::UfsError;
pub const FS_UFS1_MAGIC: u32 = 0x0001_1954;
pub const FS_UFS2_MAGIC: u32 = 0x1954_0119;
pub const SBLOCK_UFS1: usize = 8192;
pub const SBLOCK_UFS2: usize = 65536;
pub const UFS_ROOTINO: u64 = 2;
const FS_MAGIC_OFF: usize = 1372;
const SB_MIN_LEN: usize = 1376;
const OFF_SBLKNO: usize = 8;
const OFF_CBLKNO: usize = 12;
const OFF_IBLKNO: usize = 16;
const OFF_DBLKNO: usize = 20;
const OFF_OLD_TIME: usize = 32;
const OFF_OLD_SIZE: usize = 36;
const OFF_OLD_DSIZE: usize = 40;
const OFF_NCG: usize = 44;
const OFF_BSIZE: usize = 48;
const OFF_FSIZE: usize = 52;
const OFF_FRAG: usize = 56;
const OFF_BSHIFT: usize = 80;
const OFF_FSHIFT: usize = 84;
const OFF_FRAGSHIFT: usize = 96;
const OFF_FSBTODB: usize = 100;
const OFF_SBSIZE: usize = 104;
const OFF_NINDIR: usize = 116;
const OFF_INOPB: usize = 120;
const OFF_IPG: usize = 184;
const OFF_FPG: usize = 188;
const OFF_SIZE: usize = 1080;
const OFF_DSIZE: usize = 1088;
const OFF_CSADDR: usize = 1096;
const OFF_SBLOCKLOC: usize = 1000;
const OFF_MAXSYMLINKLEN: usize = 1320;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UfsVersion {
Ufs1,
Ufs2,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Superblock {
pub version: UfsVersion,
pub endian: Endian,
pub sblkno: i32,
pub cblkno: i32,
pub iblkno: i32,
pub dblkno: i32,
pub ncg: u32,
pub bsize: i32,
pub fsize: i32,
pub frag: i32,
pub bshift: i32,
pub fshift: i32,
pub fragshift: i32,
pub fsbtodb: i32,
pub sbsize: i32,
pub nindir: i32,
pub inopb: u32,
pub ipg: i32,
pub fpg: i32,
pub size: i64,
pub dsize: i64,
pub csaddr: i64,
pub sblockloc: i64,
pub maxsymlinklen: i32,
}
impl Superblock {
pub fn parse(data: &[u8]) -> Result<Self, UfsError> {
if data.len() < SB_MIN_LEN {
return Err(UfsError::Truncated {
structure: "superblock",
need: SB_MIN_LEN,
have: data.len(),
});
}
let bytes = [
u8_at(data, FS_MAGIC_OFF),
u8_at(data, FS_MAGIC_OFF + 1),
u8_at(data, FS_MAGIC_OFF + 2),
u8_at(data, FS_MAGIC_OFF + 3),
];
let le = u32::from_le_bytes(bytes);
let be = u32::from_be_bytes(bytes);
let (version, endian) = match (le, be) {
(FS_UFS1_MAGIC, _) => (UfsVersion::Ufs1, Endian::Little),
(FS_UFS2_MAGIC, _) => (UfsVersion::Ufs2, Endian::Little),
(_, FS_UFS1_MAGIC) => (UfsVersion::Ufs1, Endian::Big),
(_, FS_UFS2_MAGIC) => (UfsVersion::Ufs2, Endian::Big),
_ => {
return Err(UfsError::BadMagic {
offset: FS_MAGIC_OFF,
bytes,
le,
be,
})
}
};
let (size, dsize) = match version {
UfsVersion::Ufs1 => (
i64::from(endian.i32(data, OFF_OLD_SIZE)),
i64::from(endian.i32(data, OFF_OLD_DSIZE)),
),
UfsVersion::Ufs2 => (endian.i64(data, OFF_SIZE), endian.i64(data, OFF_DSIZE)),
};
let sblockloc = match version {
UfsVersion::Ufs1 => 0,
UfsVersion::Ufs2 => endian.i64(data, OFF_SBLOCKLOC),
};
let _ = (OFF_OLD_TIME, OFF_CSADDR);
let sb = Self {
version,
endian,
sblkno: endian.i32(data, OFF_SBLKNO),
cblkno: endian.i32(data, OFF_CBLKNO),
iblkno: endian.i32(data, OFF_IBLKNO),
dblkno: endian.i32(data, OFF_DBLKNO),
ncg: endian.u32(data, OFF_NCG),
bsize: endian.i32(data, OFF_BSIZE),
fsize: endian.i32(data, OFF_FSIZE),
frag: endian.i32(data, OFF_FRAG),
bshift: endian.i32(data, OFF_BSHIFT),
fshift: endian.i32(data, OFF_FSHIFT),
fragshift: endian.i32(data, OFF_FRAGSHIFT),
fsbtodb: endian.i32(data, OFF_FSBTODB),
sbsize: endian.i32(data, OFF_SBSIZE),
nindir: endian.i32(data, OFF_NINDIR),
inopb: endian.u32(data, OFF_INOPB),
ipg: endian.i32(data, OFF_IPG),
fpg: endian.i32(data, OFF_FPG),
size,
dsize,
csaddr: endian.i64(data, OFF_CSADDR),
sblockloc,
maxsymlinklen: endian.i32(data, OFF_MAXSYMLINKLEN),
};
sb.validate_geometry()?;
Ok(sb)
}
fn validate_geometry(&self) -> Result<(), UfsError> {
if self.bsize <= 0 || self.bsize > 65536 {
return Err(UfsError::ImpossibleGeometry {
field: "fs_bsize",
value: self.bsize as u64,
limit: 65536,
});
}
if self.fsize <= 0 || self.fsize > self.bsize {
return Err(UfsError::ImpossibleGeometry {
field: "fs_fsize",
value: self.fsize as u64,
limit: self.bsize as u64,
});
}
const MAX_NCG: u64 = 1 << 24;
if u64::from(self.ncg) > MAX_NCG {
return Err(UfsError::ImpossibleGeometry {
field: "fs_ncg",
value: u64::from(self.ncg),
limit: MAX_NCG,
});
}
Ok(())
}
#[must_use]
pub fn inode_size(&self) -> u32 {
match self.version {
UfsVersion::Ufs1 => 128,
UfsVersion::Ufs2 => 256,
}
}
#[must_use]
pub fn primary_offset(&self) -> usize {
match self.version {
UfsVersion::Ufs1 => SBLOCK_UFS1,
UfsVersion::Ufs2 => SBLOCK_UFS2,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn synthetic(magic: u32, endian: Endian, ufs2: bool) -> Vec<u8> {
let mut d = vec![0u8; SB_MIN_LEN];
let wr32 = |d: &mut [u8], off: usize, v: i32| {
let b = match endian {
Endian::Little => v.to_le_bytes(),
Endian::Big => v.to_be_bytes(),
};
d[off..off + 4].copy_from_slice(&b);
};
let wr64 = |d: &mut [u8], off: usize, v: i64| {
let b = match endian {
Endian::Little => v.to_le_bytes(),
Endian::Big => v.to_be_bytes(),
};
d[off..off + 8].copy_from_slice(&b);
};
let mb = match endian {
Endian::Little => magic.to_le_bytes(),
Endian::Big => magic.to_be_bytes(),
};
d[FS_MAGIC_OFF..FS_MAGIC_OFF + 4].copy_from_slice(&mb);
wr32(&mut d, OFF_SBLKNO, 24);
wr32(&mut d, OFF_CBLKNO, 32);
wr32(&mut d, OFF_IBLKNO, 40);
wr32(&mut d, OFF_DBLKNO, 48);
wr32(&mut d, OFF_NCG, 4);
wr32(&mut d, OFF_BSIZE, 32768);
wr32(&mut d, OFF_FSIZE, 4096);
wr32(&mut d, OFF_FRAG, 8);
wr32(&mut d, OFF_BSHIFT, 15);
wr32(&mut d, OFF_FSHIFT, 12);
wr32(&mut d, OFF_INOPB, 128);
wr32(&mut d, OFF_IPG, 128);
wr32(&mut d, OFF_FPG, 256);
wr32(&mut d, OFF_MAXSYMLINKLEN, 120);
if ufs2 {
wr64(&mut d, OFF_SIZE, 1022);
wr64(&mut d, OFF_DSIZE, 901);
wr64(&mut d, OFF_SBLOCKLOC, SBLOCK_UFS2 as i64);
} else {
wr32(&mut d, OFF_OLD_SIZE, 1000);
wr32(&mut d, OFF_OLD_DSIZE, 900);
}
d
}
#[test]
fn detects_ufs2_little_endian() {
let d = synthetic(FS_UFS2_MAGIC, Endian::Little, true);
let sb = Superblock::parse(&d).unwrap();
assert_eq!(sb.version, UfsVersion::Ufs2);
assert_eq!(sb.endian, Endian::Little);
assert_eq!(sb.bsize, 32768);
assert_eq!(sb.ncg, 4);
assert_eq!(sb.size, 1022);
assert_eq!(sb.sblockloc, SBLOCK_UFS2 as i64);
assert_eq!(sb.inode_size(), 256);
assert_eq!(sb.primary_offset(), SBLOCK_UFS2);
}
#[test]
fn detects_ufs2_big_endian() {
let d = synthetic(FS_UFS2_MAGIC, Endian::Big, true);
let sb = Superblock::parse(&d).unwrap();
assert_eq!(sb.version, UfsVersion::Ufs2);
assert_eq!(sb.endian, Endian::Big);
assert_eq!(sb.bsize, 32768);
assert_eq!(sb.fpg, 256);
}
#[test]
fn detects_ufs1_and_uses_old_size_fields() {
let d = synthetic(FS_UFS1_MAGIC, Endian::Little, false);
let sb = Superblock::parse(&d).unwrap();
assert_eq!(sb.version, UfsVersion::Ufs1);
assert_eq!(sb.size, 1000, "UFS1 uses fs_old_size@36");
assert_eq!(sb.dsize, 900, "UFS1 uses fs_old_dsize@40");
assert_eq!(sb.sblockloc, 0, "UFS1 has no fs_sblockloc");
assert_eq!(sb.inode_size(), 128);
assert_eq!(sb.primary_offset(), SBLOCK_UFS1);
}
#[test]
fn detects_ufs1_big_endian() {
let d = synthetic(FS_UFS1_MAGIC, Endian::Big, false);
let sb = Superblock::parse(&d).unwrap();
assert_eq!(sb.version, UfsVersion::Ufs1);
assert_eq!(sb.endian, Endian::Big);
assert_eq!(sb.size, 1000, "UFS1 old_size decoded big-endian");
assert_eq!(sb.inode_size(), 128);
}
#[test]
fn bad_magic_fails_loud_with_bytes() {
let mut d = vec![0u8; SB_MIN_LEN];
d[FS_MAGIC_OFF..FS_MAGIC_OFF + 4].copy_from_slice(&0xdead_beef_u32.to_le_bytes());
let err = Superblock::parse(&d).unwrap_err();
assert!(
matches!(
&err,
UfsError::BadMagic { offset, bytes, le, .. }
if *offset == FS_MAGIC_OFF
&& *le == 0xdead_beef
&& *bytes == 0xdead_beef_u32.to_le_bytes()
),
"expected BadMagic with offending bytes, got {err:?}"
);
}
#[test]
fn truncated_buffer_fails_loud_not_panic() {
let d = vec![0u8; SB_MIN_LEN - 1];
let err = Superblock::parse(&d).unwrap_err();
assert!(
matches!(
&err,
UfsError::Truncated { structure, need, .. }
if *structure == "superblock" && *need == SB_MIN_LEN
),
"expected Truncated superblock, got {err:?}"
);
}
#[test]
fn empty_buffer_reports_truncated_not_panic() {
assert!(matches!(
Superblock::parse(&[]),
Err(UfsError::Truncated { .. })
));
}
#[test]
fn impossible_block_size_rejected() {
let mut d = synthetic(FS_UFS2_MAGIC, Endian::Little, true);
d[OFF_BSIZE..OFF_BSIZE + 4].copy_from_slice(&(1_048_576_i32).to_le_bytes());
assert!(matches!(
Superblock::parse(&d),
Err(UfsError::ImpossibleGeometry {
field: "fs_bsize",
..
})
));
}
#[test]
fn impossible_fragment_size_rejected() {
let mut d = synthetic(FS_UFS2_MAGIC, Endian::Little, true);
d[OFF_FSIZE..OFF_FSIZE + 4].copy_from_slice(&(65536_i32).to_le_bytes());
assert!(matches!(
Superblock::parse(&d),
Err(UfsError::ImpossibleGeometry {
field: "fs_fsize",
..
})
));
}
#[test]
fn absurd_cg_count_rejected() {
let mut d = synthetic(FS_UFS2_MAGIC, Endian::Little, true);
d[OFF_NCG..OFF_NCG + 4].copy_from_slice(&0x7fff_ffff_u32.to_le_bytes());
assert!(matches!(
Superblock::parse(&d),
Err(UfsError::ImpossibleGeometry {
field: "fs_ncg",
..
})
));
}
}