use crate::Result;
pub const XFS_DINODE_MAGIC: u16 = 0x494e;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiFormat {
Dev,
Local,
Extents,
Btree,
Unknown(u8),
}
impl DiFormat {
fn from_byte(b: u8) -> Self {
match b {
0 => Self::Dev,
1 => Self::Local,
2 => Self::Extents,
3 => Self::Btree,
other => Self::Unknown(other),
}
}
}
pub const S_IFMT: u16 = 0o170_000;
pub const S_IFIFO: u16 = 0o010_000;
pub const S_IFCHR: u16 = 0o020_000;
pub const S_IFDIR: u16 = 0o040_000;
pub const S_IFBLK: u16 = 0o060_000;
pub const S_IFREG: u16 = 0o100_000;
pub const S_IFLNK: u16 = 0o120_000;
pub const S_IFSOCK: u16 = 0o140_000;
#[derive(Debug, Clone, Copy, Default)]
pub struct XfsTimestamp {
pub sec: u32,
pub nsec: u32,
}
impl XfsTimestamp {
pub fn decode(buf: &[u8]) -> Self {
Self {
sec: u32::from_be_bytes(buf[0..4].try_into().unwrap()),
nsec: u32::from_be_bytes(buf[4..8].try_into().unwrap()),
}
}
}
#[derive(Debug, Clone)]
pub struct DinodeCore {
pub magic: u16,
pub mode: u16,
pub version: u8,
pub format: DiFormat,
pub uid: u32,
pub gid: u32,
pub nlink: u32,
pub atime: XfsTimestamp,
pub mtime: XfsTimestamp,
pub ctime: XfsTimestamp,
pub size: u64,
pub nblocks: u64,
pub nextents: u32,
pub forkoff: u8,
pub flags: u16,
pub generation: u32,
pub di_ino: Option<u64>,
pub literal_offset: usize,
}
impl DinodeCore {
pub fn decode(buf: &[u8]) -> Result<Self> {
if buf.len() < 96 {
return Err(crate::Error::InvalidImage(
"xfs: inode buffer too small".into(),
));
}
let magic = u16::from_be_bytes(buf[0..2].try_into().unwrap());
if magic != XFS_DINODE_MAGIC {
return Err(crate::Error::InvalidImage(format!(
"xfs: bad inode magic {magic:#06x} (expected IN)"
)));
}
let mode = u16::from_be_bytes(buf[2..4].try_into().unwrap());
let version = buf[4];
let format = DiFormat::from_byte(buf[5]);
let uid = u32::from_be_bytes(buf[8..12].try_into().unwrap());
let gid = u32::from_be_bytes(buf[12..16].try_into().unwrap());
let nlink = u32::from_be_bytes(buf[16..20].try_into().unwrap());
let atime = XfsTimestamp::decode(&buf[32..40]);
let mtime = XfsTimestamp::decode(&buf[40..48]);
let ctime = XfsTimestamp::decode(&buf[48..56]);
let size = u64::from_be_bytes(buf[56..64].try_into().unwrap());
let nblocks = u64::from_be_bytes(buf[64..72].try_into().unwrap());
let nextents = u32::from_be_bytes(buf[76..80].try_into().unwrap());
let forkoff = buf[82];
let flags = u16::from_be_bytes(buf[90..92].try_into().unwrap());
let generation = u32::from_be_bytes(buf[92..96].try_into().unwrap());
let (literal_offset, di_ino) = if version >= 3 {
if buf.len() < 176 {
return Err(crate::Error::InvalidImage(
"xfs: v3 inode buffer too small for core".into(),
));
}
let ino = u64::from_be_bytes(buf[152..160].try_into().unwrap());
(176, Some(ino))
} else {
(96, None)
};
if let DiFormat::Unknown(b) = format {
return Err(crate::Error::Unsupported(format!(
"xfs: unknown di_format {b}"
)));
}
Ok(Self {
magic,
mode,
version,
format,
uid,
gid,
nlink,
atime,
mtime,
ctime,
size,
nblocks,
nextents,
forkoff,
flags,
generation,
di_ino,
literal_offset,
})
}
pub fn literal_area<'a>(&self, buf: &'a [u8], inodesize: usize) -> &'a [u8] {
let end = if self.forkoff == 0 {
inodesize
} else {
self.literal_offset + (self.forkoff as usize) * 8
};
&buf[self.literal_offset..end.min(buf.len())]
}
pub fn is_dir(&self) -> bool {
(self.mode & S_IFMT) == S_IFDIR
}
pub fn is_reg(&self) -> bool {
(self.mode & S_IFMT) == S_IFREG
}
pub fn is_symlink(&self) -> bool {
(self.mode & S_IFMT) == S_IFLNK
}
}
#[cfg(test)]
mod tests {
use super::*;
fn synth_v3_inode(size: u64, format: u8, mode: u16, lit_payload: &[u8]) -> Vec<u8> {
let mut buf = vec![0u8; 512];
buf[0..2].copy_from_slice(&XFS_DINODE_MAGIC.to_be_bytes());
buf[2..4].copy_from_slice(&mode.to_be_bytes());
buf[4] = 3; buf[5] = format;
buf[16..20].copy_from_slice(&1u32.to_be_bytes()); buf[56..64].copy_from_slice(&size.to_be_bytes());
buf[152..160].copy_from_slice(&128u64.to_be_bytes());
let n = lit_payload.len().min(512 - 176);
buf[176..176 + n].copy_from_slice(&lit_payload[..n]);
buf
}
#[test]
fn decode_v3_local_dir() {
let buf = synth_v3_inode(0, 1, S_IFDIR | 0o755, &[0xAA; 64]);
let core = DinodeCore::decode(&buf).unwrap();
assert_eq!(core.magic, XFS_DINODE_MAGIC);
assert_eq!(core.version, 3);
assert_eq!(core.format, DiFormat::Local);
assert!(core.is_dir());
assert_eq!(core.literal_offset, 176);
assert_eq!(core.di_ino, Some(128));
let lit = core.literal_area(&buf, 512);
assert_eq!(lit.len(), 512 - 176);
assert!(lit.iter().take(64).all(|&b| b == 0xAA));
}
#[test]
fn decode_rejects_bad_magic() {
let mut buf = synth_v3_inode(0, 1, S_IFDIR | 0o755, &[]);
buf[0] = 0;
assert!(matches!(
DinodeCore::decode(&buf),
Err(crate::Error::InvalidImage(_))
));
}
#[test]
fn forkoff_caps_literal_area() {
let mut buf = synth_v3_inode(0, 1, S_IFREG | 0o644, &[0; 8]);
buf[82] = 4;
let core = DinodeCore::decode(&buf).unwrap();
let lit = core.literal_area(&buf, 512);
assert_eq!(lit.len(), 32);
}
}