use crate::Result;
use crate::fs::{DirEntry, EntryKind};
pub const XFS_DIR3_FT_UNKNOWN: u8 = 0;
pub const XFS_DIR3_FT_REG_FILE: u8 = 1;
pub const XFS_DIR3_FT_DIR: u8 = 2;
pub const XFS_DIR3_FT_CHRDEV: u8 = 3;
pub const XFS_DIR3_FT_BLKDEV: u8 = 4;
pub const XFS_DIR3_FT_FIFO: u8 = 5;
pub const XFS_DIR3_FT_SOCK: u8 = 6;
pub const XFS_DIR3_FT_SYMLINK: u8 = 7;
pub const XFS_DIR2_BLOCK_MAGIC: u32 = 0x5844_3242; pub const XFS_DIR2_DATA_MAGIC: u32 = 0x5844_3244; pub const XFS_DIR3_BLOCK_MAGIC: u32 = 0x5844_4233; pub const XFS_DIR3_DATA_MAGIC: u32 = 0x5844_4433;
pub const XFS_DIR2_DATA_FREE_TAG: u16 = 0xFFFF;
pub const XFS_DIR2_LEAF_FIRSTDB_BYTES: u64 = 32 * 1024 * 1024 * 1024;
pub fn ftype_to_kind(ft: u8) -> EntryKind {
match ft {
XFS_DIR3_FT_REG_FILE => EntryKind::Regular,
XFS_DIR3_FT_DIR => EntryKind::Dir,
XFS_DIR3_FT_CHRDEV => EntryKind::Char,
XFS_DIR3_FT_BLKDEV => EntryKind::Block,
XFS_DIR3_FT_FIFO => EntryKind::Fifo,
XFS_DIR3_FT_SOCK => EntryKind::Socket,
XFS_DIR3_FT_SYMLINK => EntryKind::Symlink,
_ => EntryKind::Unknown,
}
}
#[derive(Debug, Clone)]
pub struct ShortformEntry {
pub name: String,
pub inumber: u64,
pub ftype: u8,
}
#[derive(Debug, Clone)]
pub struct DataEntry {
pub name: String,
pub inumber: u64,
pub ftype: u8,
}
pub fn decode_shortform(lit: &[u8], has_ftype: bool) -> Result<(u64, Vec<ShortformEntry>)> {
if lit.len() < 2 {
return Err(crate::Error::InvalidImage(
"xfs: shortform dir header truncated".into(),
));
}
let count = lit[0] as usize;
let i8count = lit[1];
let parent_len = if i8count == 0 { 4 } else { 8 };
if lit.len() < 2 + parent_len {
return Err(crate::Error::InvalidImage(
"xfs: shortform dir parent truncated".into(),
));
}
let parent = if parent_len == 4 {
u32::from_be_bytes(lit[2..6].try_into().unwrap()) as u64
} else {
u64::from_be_bytes(lit[2..10].try_into().unwrap())
};
let inum_len = parent_len;
let mut pos = 2 + parent_len;
let mut entries = Vec::with_capacity(count);
for _ in 0..count {
if pos + 3 > lit.len() {
return Err(crate::Error::InvalidImage(
"xfs: shortform dir entry truncated".into(),
));
}
let namelen = lit[pos] as usize;
let name_start = pos + 3;
let name_end = name_start + namelen;
if name_end > lit.len() {
return Err(crate::Error::InvalidImage(format!(
"xfs: shortform dir entry name truncated (need {name_end}, have {})",
lit.len()
)));
}
let name = std::str::from_utf8(&lit[name_start..name_end])
.map_err(|_| crate::Error::InvalidImage("xfs: non-UTF-8 shortform dir name".into()))?
.to_string();
let mut cur = name_end;
let ftype = if has_ftype {
if cur >= lit.len() {
return Err(crate::Error::InvalidImage(
"xfs: shortform dir entry missing ftype byte".into(),
));
}
let f = lit[cur];
cur += 1;
f
} else {
XFS_DIR3_FT_UNKNOWN
};
if cur + inum_len > lit.len() {
return Err(crate::Error::InvalidImage(
"xfs: shortform dir entry missing inum".into(),
));
}
let inumber = if inum_len == 4 {
u32::from_be_bytes(lit[cur..cur + 4].try_into().unwrap()) as u64
} else {
u64::from_be_bytes(lit[cur..cur + 8].try_into().unwrap())
};
cur += inum_len;
pos = cur;
entries.push(ShortformEntry {
name,
inumber,
ftype,
});
}
Ok((parent, entries))
}
pub fn shortform_to_generic(entries: &[ShortformEntry]) -> Vec<DirEntry> {
entries
.iter()
.map(|e| DirEntry {
name: e.name.clone(),
inode: e.inumber as u32,
kind: ftype_to_kind(e.ftype),
})
.collect()
}
pub fn data_entries_to_generic(entries: &[DataEntry]) -> Vec<DirEntry> {
entries
.iter()
.map(|e| DirEntry {
name: e.name.clone(),
inode: e.inumber as u32,
kind: ftype_to_kind(e.ftype),
})
.collect()
}
fn parse_data_records(
block: &[u8],
entries_start: usize,
entries_end: usize,
has_ftype: bool,
) -> Result<Vec<DataEntry>> {
if entries_start > entries_end || entries_end > block.len() {
return Err(crate::Error::InvalidImage(format!(
"xfs: dir data record range out of bounds: [{entries_start}..{entries_end}] vs buf {}",
block.len()
)));
}
let mut out = Vec::new();
let mut pos = entries_start;
while pos + 4 <= entries_end {
let head = u16::from_be_bytes(block[pos..pos + 2].try_into().unwrap());
if head == XFS_DIR2_DATA_FREE_TAG {
let length = u16::from_be_bytes(block[pos + 2..pos + 4].try_into().unwrap()) as usize;
if length == 0 {
return Err(crate::Error::InvalidImage(
"xfs: dir data_unused with length 0".into(),
));
}
if pos + length > entries_end {
return Err(crate::Error::InvalidImage(format!(
"xfs: dir data_unused length {length} overshoots end at {entries_end}"
)));
}
pos += length;
continue;
}
if pos + 8 + 1 > entries_end {
return Err(crate::Error::InvalidImage(
"xfs: dir data_entry header truncated".into(),
));
}
let inumber = u64::from_be_bytes(block[pos..pos + 8].try_into().unwrap());
let namelen = block[pos + 8] as usize;
if namelen == 0 {
return Err(crate::Error::InvalidImage(
"xfs: dir data_entry has namelen=0".into(),
));
}
let name_start = pos + 9;
let name_end = name_start + namelen;
if name_end > entries_end {
return Err(crate::Error::InvalidImage(format!(
"xfs: dir data_entry name truncated (need {name_end}, have {entries_end})"
)));
}
let name = std::str::from_utf8(&block[name_start..name_end])
.map_err(|_| {
crate::Error::InvalidImage("xfs: non-UTF-8 dir name in data block".into())
})?
.to_string();
let mut after_name = name_end;
let ftype = if has_ftype {
if after_name >= entries_end {
return Err(crate::Error::InvalidImage(
"xfs: dir data_entry missing ftype byte".into(),
));
}
let f = block[after_name];
after_name += 1;
f
} else {
XFS_DIR3_FT_UNKNOWN
};
let raw_len = 8 + 1 + namelen + (if has_ftype { 1 } else { 0 }) + 2;
let padded_len = (raw_len + 7) & !7;
if pos + padded_len > entries_end {
return Err(crate::Error::InvalidImage(format!(
"xfs: dir data_entry padded length {padded_len} overshoots end {entries_end}"
)));
}
let _ = after_name;
pos += padded_len;
out.push(DataEntry {
name,
inumber,
ftype,
});
}
Ok(out)
}
fn decode_data_header(block: &[u8], is_v5: bool) -> Result<(usize, u32)> {
if block.len() < 4 {
return Err(crate::Error::InvalidImage(
"xfs: dir data block too small".into(),
));
}
let magic = u32::from_be_bytes(block[0..4].try_into().unwrap());
if is_v5 {
match magic {
XFS_DIR3_BLOCK_MAGIC | XFS_DIR3_DATA_MAGIC => {}
_ => {
return Err(crate::Error::InvalidImage(format!(
"xfs: bad v5 dir block magic {magic:#010x}"
)));
}
}
if block.len() < 64 {
return Err(crate::Error::InvalidImage(
"xfs: v5 dir block shorter than header".into(),
));
}
Ok((64, magic))
} else {
match magic {
XFS_DIR2_BLOCK_MAGIC | XFS_DIR2_DATA_MAGIC => {}
_ => {
return Err(crate::Error::InvalidImage(format!(
"xfs: bad v4 dir block magic {magic:#010x}"
)));
}
}
if block.len() < 16 {
return Err(crate::Error::InvalidImage(
"xfs: v4 dir block shorter than header".into(),
));
}
Ok((16, magic))
}
}
fn block_dir_entries_end(block: &[u8]) -> Result<usize> {
let blen = block.len();
if blen < 8 {
return Err(crate::Error::InvalidImage(
"xfs: block dir too short for tail".into(),
));
}
let count = u32::from_be_bytes(block[blen - 8..blen - 4].try_into().unwrap()) as usize;
let leaf_bytes = count
.checked_mul(8)
.ok_or_else(|| crate::Error::InvalidImage("xfs: block dir leaf count overflows".into()))?;
let tail = 8 + leaf_bytes;
if tail > blen {
return Err(crate::Error::InvalidImage(format!(
"xfs: block dir tail (8 + 8*{count}) > block size {blen}"
)));
}
Ok(blen - tail)
}
pub fn decode_block_dir(block: &[u8], is_v5: bool) -> Result<Vec<DataEntry>> {
let (entries_start, magic) = decode_data_header(block, is_v5)?;
let want_magic = if is_v5 {
XFS_DIR3_BLOCK_MAGIC
} else {
XFS_DIR2_BLOCK_MAGIC
};
if magic != want_magic {
return Err(crate::Error::InvalidImage(format!(
"xfs: block dir expected magic {want_magic:#010x}, got {magic:#010x}"
)));
}
let entries_end = block_dir_entries_end(block)?;
parse_data_records(block, entries_start, entries_end, is_v5)
}
pub fn decode_data_block(block: &[u8], is_v5: bool) -> Result<Vec<DataEntry>> {
let (entries_start, magic) = decode_data_header(block, is_v5)?;
let want_magic = if is_v5 {
XFS_DIR3_DATA_MAGIC
} else {
XFS_DIR2_DATA_MAGIC
};
if magic != want_magic {
return Err(crate::Error::InvalidImage(format!(
"xfs: data dir expected magic {want_magic:#010x}, got {magic:#010x}"
)));
}
parse_data_records(block, entries_start, block.len(), is_v5)
}
pub fn is_block_format(block: &[u8]) -> Result<bool> {
if block.len() < 4 {
return Err(crate::Error::InvalidImage(
"xfs: dir first block too small".into(),
));
}
let magic = u32::from_be_bytes(block[0..4].try_into().unwrap());
Ok(matches!(magic, XFS_DIR2_BLOCK_MAGIC | XFS_DIR3_BLOCK_MAGIC))
}
#[cfg(test)]
mod tests {
use super::*;
fn synth(parent: u32, entries: &[(&str, u32, u8)], has_ftype: bool) -> Vec<u8> {
let mut buf = Vec::new();
buf.push(entries.len() as u8);
buf.push(0); buf.extend_from_slice(&parent.to_be_bytes());
for (name, ino, ft) in entries {
buf.push(name.len() as u8);
buf.extend_from_slice(&[0, 0]); buf.extend_from_slice(name.as_bytes());
if has_ftype {
buf.push(*ft);
}
buf.extend_from_slice(&ino.to_be_bytes());
}
buf
}
#[test]
fn decode_two_entries_with_ftype() {
let buf = synth(
128,
&[
("a", 200, XFS_DIR3_FT_REG_FILE),
("dir", 201, XFS_DIR3_FT_DIR),
],
true,
);
let (parent, entries) = decode_shortform(&buf, true).unwrap();
assert_eq!(parent, 128);
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].name, "a");
assert_eq!(entries[0].inumber, 200);
assert_eq!(entries[0].ftype, XFS_DIR3_FT_REG_FILE);
assert_eq!(entries[1].name, "dir");
assert_eq!(entries[1].inumber, 201);
assert_eq!(entries[1].ftype, XFS_DIR3_FT_DIR);
}
#[test]
fn decode_empty() {
let buf = synth(128, &[], true);
let (parent, entries) = decode_shortform(&buf, true).unwrap();
assert_eq!(parent, 128);
assert!(entries.is_empty());
}
#[test]
fn decode_without_ftype_v2() {
let buf = synth(128, &[("x", 7, 0)], false);
let (parent, entries) = decode_shortform(&buf, false).unwrap();
assert_eq!(parent, 128);
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].name, "x");
assert_eq!(entries[0].inumber, 7);
assert_eq!(entries[0].ftype, XFS_DIR3_FT_UNKNOWN);
}
#[test]
fn decode_rejects_truncation() {
let mut buf = synth(128, &[("name", 1, XFS_DIR3_FT_REG_FILE)], true);
buf.truncate(buf.len() - 2);
assert!(decode_shortform(&buf, true).is_err());
}
#[test]
fn ftype_mapping() {
assert_eq!(ftype_to_kind(XFS_DIR3_FT_REG_FILE), EntryKind::Regular);
assert_eq!(ftype_to_kind(XFS_DIR3_FT_DIR), EntryKind::Dir);
assert_eq!(ftype_to_kind(XFS_DIR3_FT_SYMLINK), EntryKind::Symlink);
assert_eq!(ftype_to_kind(XFS_DIR3_FT_UNKNOWN), EntryKind::Unknown);
}
#[test]
fn to_generic_preserves_data() {
let entries = vec![ShortformEntry {
name: "x".into(),
inumber: 7,
ftype: XFS_DIR3_FT_REG_FILE,
}];
let g = shortform_to_generic(&entries);
assert_eq!(g.len(), 1);
assert_eq!(g[0].name, "x");
assert_eq!(g[0].inode, 7);
assert_eq!(g[0].kind, EntryKind::Regular);
}
fn build_v5_entry(inumber: u64, name: &str, ftype: u8) -> Vec<u8> {
let raw_len = 8 + 1 + name.len() + 1 + 2;
let padded = (raw_len + 7) & !7;
let mut e = vec![0u8; padded];
e[0..8].copy_from_slice(&inumber.to_be_bytes());
e[8] = name.len() as u8;
e[9..9 + name.len()].copy_from_slice(name.as_bytes());
e[9 + name.len()] = ftype;
e
}
fn build_v4_entry(inumber: u64, name: &str) -> Vec<u8> {
let raw_len = 8 + 1 + name.len() + 2;
let padded = (raw_len + 7) & !7;
let mut e = vec![0u8; padded];
e[0..8].copy_from_slice(&inumber.to_be_bytes());
e[8] = name.len() as u8;
e[9..9 + name.len()].copy_from_slice(name.as_bytes());
e
}
fn build_v5_block_dir(entries: &[(u64, &str, u8)], dirsize: usize) -> Vec<u8> {
let mut block = vec![0u8; dirsize];
block[0..4].copy_from_slice(&XFS_DIR3_BLOCK_MAGIC.to_be_bytes());
let mut pos = 64usize;
for (ino, name, ft) in entries {
let rec = build_v5_entry(*ino, name, *ft);
block[pos..pos + rec.len()].copy_from_slice(&rec);
pos += rec.len();
}
let count = entries.len() as u32;
block[dirsize - 8..dirsize - 4].copy_from_slice(&count.to_be_bytes());
let entries_end = dirsize - 8 - (count as usize) * 8;
if entries_end > pos {
let slack = entries_end - pos;
block[pos..pos + 2].copy_from_slice(&XFS_DIR2_DATA_FREE_TAG.to_be_bytes());
block[pos + 2..pos + 4].copy_from_slice(&(slack as u16).to_be_bytes());
}
block
}
#[test]
fn decode_v5_block_dir_smoke() {
let block = build_v5_block_dir(
&[
(200, "hello", XFS_DIR3_FT_REG_FILE),
(300, "subdir", XFS_DIR3_FT_DIR),
],
4096,
);
let entries = decode_block_dir(&block, true).unwrap();
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].name, "hello");
assert_eq!(entries[0].inumber, 200);
assert_eq!(entries[0].ftype, XFS_DIR3_FT_REG_FILE);
assert_eq!(entries[1].name, "subdir");
assert_eq!(entries[1].inumber, 300);
assert_eq!(entries[1].ftype, XFS_DIR3_FT_DIR);
}
#[test]
fn block_dir_handles_unused_region() {
let dirsize = 4096usize;
let mut block = vec![0u8; dirsize];
block[0..4].copy_from_slice(&XFS_DIR3_BLOCK_MAGIC.to_be_bytes());
let mut pos = 64;
let rec1 = build_v5_entry(100, "a", XFS_DIR3_FT_REG_FILE);
block[pos..pos + rec1.len()].copy_from_slice(&rec1);
pos += rec1.len();
block[pos..pos + 2].copy_from_slice(&XFS_DIR2_DATA_FREE_TAG.to_be_bytes());
block[pos + 2..pos + 4].copy_from_slice(&16u16.to_be_bytes());
pos += 16;
let rec2 = build_v5_entry(101, "bee", XFS_DIR3_FT_DIR);
block[pos..pos + rec2.len()].copy_from_slice(&rec2);
pos += rec2.len();
let count = 2u32;
block[dirsize - 8..dirsize - 4].copy_from_slice(&count.to_be_bytes());
let entries_end = dirsize - 8 - (count as usize) * 8;
if entries_end > pos {
let slack = entries_end - pos;
block[pos..pos + 2].copy_from_slice(&XFS_DIR2_DATA_FREE_TAG.to_be_bytes());
block[pos + 2..pos + 4].copy_from_slice(&(slack as u16).to_be_bytes());
}
let entries = decode_block_dir(&block, true).unwrap();
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].name, "a");
assert_eq!(entries[1].name, "bee");
}
#[test]
fn decode_v5_data_block_smoke() {
let mut block = vec![0u8; 4096];
block[0..4].copy_from_slice(&XFS_DIR3_DATA_MAGIC.to_be_bytes());
let mut pos = 64;
for (ino, name, ft) in &[
(10u64, "f1", XFS_DIR3_FT_REG_FILE),
(11, "f2", XFS_DIR3_FT_REG_FILE),
(12, "f3", XFS_DIR3_FT_REG_FILE),
] {
let rec = build_v5_entry(*ino, name, *ft);
block[pos..pos + rec.len()].copy_from_slice(&rec);
pos += rec.len();
}
block[pos..pos + 2].copy_from_slice(&XFS_DIR2_DATA_FREE_TAG.to_be_bytes());
block[pos + 2..pos + 4].copy_from_slice(&((4096 - pos) as u16).to_be_bytes());
let entries = decode_data_block(&block, true).unwrap();
assert_eq!(entries.len(), 3);
assert_eq!(entries[0].name, "f1");
assert_eq!(entries[2].name, "f3");
}
#[test]
fn decode_v4_block_dir_no_ftype() {
let dirsize = 4096usize;
let mut block = vec![0u8; dirsize];
block[0..4].copy_from_slice(&XFS_DIR2_BLOCK_MAGIC.to_be_bytes());
let mut pos = 16; let rec = build_v4_entry(42, "abc");
block[pos..pos + rec.len()].copy_from_slice(&rec);
pos += rec.len();
let count = 1u32;
block[dirsize - 8..dirsize - 4].copy_from_slice(&count.to_be_bytes());
let entries_end = dirsize - 8 - (count as usize) * 8;
if entries_end > pos {
let slack = entries_end - pos;
block[pos..pos + 2].copy_from_slice(&XFS_DIR2_DATA_FREE_TAG.to_be_bytes());
block[pos + 2..pos + 4].copy_from_slice(&(slack as u16).to_be_bytes());
}
let entries = decode_block_dir(&block, false).unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].name, "abc");
assert_eq!(entries[0].inumber, 42);
assert_eq!(entries[0].ftype, XFS_DIR3_FT_UNKNOWN);
}
#[test]
fn rejects_bad_magic() {
let block = vec![0u8; 4096];
let r = decode_block_dir(&block, true);
assert!(matches!(r, Err(crate::Error::InvalidImage(_))));
}
#[test]
fn is_block_format_detects() {
let mut block = vec![0u8; 4096];
block[0..4].copy_from_slice(&XFS_DIR3_BLOCK_MAGIC.to_be_bytes());
assert!(is_block_format(&block).unwrap());
block[0..4].copy_from_slice(&XFS_DIR3_DATA_MAGIC.to_be_bytes());
assert!(!is_block_format(&block).unwrap());
block[0..4].copy_from_slice(&XFS_DIR2_BLOCK_MAGIC.to_be_bytes());
assert!(is_block_format(&block).unwrap());
}
}