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 const V5_DIR_CRC_OFFSET: usize = 4;
pub const V5_DATA_HDR_SIZE: usize = 64;
pub fn stamp_v5_dir_block_crc(block: &mut [u8]) {
block[V5_DIR_CRC_OFFSET..V5_DIR_CRC_OFFSET + 4].copy_from_slice(&[0u8; 4]);
let crc = crc32c::crc32c(block);
block[V5_DIR_CRC_OFFSET..V5_DIR_CRC_OFFSET + 4].copy_from_slice(&crc.to_le_bytes());
}
pub fn encode_v5_block_dir(
dir_block_size: usize,
owner: u64,
parent: u64,
entries: &[(String, u64, u8)],
uuid: &[u8; 16],
block_basic_blkno: u64,
) -> Result<Vec<u8>> {
if dir_block_size < V5_DATA_HDR_SIZE + 32 {
return Err(crate::Error::InvalidArgument(format!(
"xfs: dir block size {dir_block_size} too small for v5 header"
)));
}
let mut block = vec![0u8; dir_block_size];
block[0..4].copy_from_slice(&XFS_DIR3_BLOCK_MAGIC.to_be_bytes());
block[8..16].copy_from_slice(&block_basic_blkno.to_be_bytes());
block[24..40].copy_from_slice(uuid);
block[40..48].copy_from_slice(&owner.to_be_bytes());
let mut all: Vec<(String, u64, u8)> = Vec::with_capacity(entries.len() + 2);
all.push((".".to_string(), owner, XFS_DIR3_FT_DIR));
all.push(("..".to_string(), parent, XFS_DIR3_FT_DIR));
all.extend(entries.iter().cloned());
if all.len() > u32::MAX as usize {
return Err(crate::Error::InvalidArgument(
"xfs: block dir has too many entries".into(),
));
}
let leaf_count = all.len() as u32;
let leaf_bytes = (leaf_count as usize) * 8;
let tail_off = dir_block_size - 8 - leaf_bytes;
let mut pos = V5_DATA_HDR_SIZE;
let mut leaf_pairs: Vec<(u32, u32)> = Vec::with_capacity(all.len());
for (name, inum, ft) in &all {
let namelen = name.len();
let raw_len = 8 + 1 + namelen + 1 + 2;
let padded = (raw_len + 7) & !7;
if pos + padded > tail_off {
return Err(crate::Error::InvalidArgument(
"xfs: block dir overflowed available space".into(),
));
}
block[pos..pos + 8].copy_from_slice(&inum.to_be_bytes());
block[pos + 8] = namelen as u8;
block[pos + 9..pos + 9 + namelen].copy_from_slice(name.as_bytes());
block[pos + 9 + namelen] = *ft;
let tag = (pos as u16).to_be_bytes();
block[pos + padded - 2..pos + padded].copy_from_slice(&tag);
let hashval = dahashname(name.as_bytes());
let address = (pos / 8) as u32;
leaf_pairs.push((hashval, address));
pos += padded;
}
if tail_off > pos {
let slack = tail_off - pos;
if slack < 8 {
return Err(crate::Error::InvalidArgument(format!(
"xfs: block dir slack {slack} < 8 bytes"
)));
}
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 tag_off = pos + slack - 2;
block[tag_off..tag_off + 2].copy_from_slice(&(pos as u16).to_be_bytes());
block[48..50].copy_from_slice(&(pos as u16).to_be_bytes());
block[50..52].copy_from_slice(&(slack as u16).to_be_bytes());
}
leaf_pairs.sort_by_key(|p| p.0);
for (i, (h, a)) in leaf_pairs.iter().enumerate() {
let off = tail_off + i * 8;
block[off..off + 4].copy_from_slice(&h.to_be_bytes());
block[off + 4..off + 8].copy_from_slice(&a.to_be_bytes());
}
block[dir_block_size - 8..dir_block_size - 4].copy_from_slice(&leaf_count.to_be_bytes());
block[dir_block_size - 4..dir_block_size].copy_from_slice(&0u32.to_be_bytes());
stamp_v5_dir_block_crc(&mut block);
Ok(block)
}
pub fn dahashname(name: &[u8]) -> u32 {
let mut hash: u32 = 0;
let mut i = 0;
while i + 4 <= name.len() {
let n0 = name[i] as u32;
let n1 = name[i + 1] as u32;
let n2 = name[i + 2] as u32;
let n3 = name[i + 3] as u32;
hash = (n0 << 21) ^ (n1 << 14) ^ (n2 << 7) ^ n3 ^ hash.rotate_left(28);
i += 4;
}
let remaining = name.len() - i;
match remaining {
3 => {
let n0 = name[i] as u32;
let n1 = name[i + 1] as u32;
let n2 = name[i + 2] as u32;
(n0 << 14) ^ (n1 << 7) ^ n2 ^ hash.rotate_left(21)
}
2 => {
let n0 = name[i] as u32;
let n1 = name[i + 1] as u32;
(n0 << 7) ^ n1 ^ hash.rotate_left(14)
}
1 => {
let n0 = name[i] as u32;
n0 ^ hash.rotate_left(7)
}
_ => hash,
}
}
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());
}
}