use crate::Result;
use crate::block::BlockDevice;
pub const BMBT_REC_SIZE: usize = 16;
pub const BMBT_KEY_SIZE: usize = 8;
pub const BMBT_PTR_SIZE: usize = 8;
pub const XFS_BMAP_MAGIC: u32 = 0x424D_4150;
pub const XFS_BMAP_CRC_MAGIC: u32 = 0x424D_4133;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Extent {
pub offset: u64,
pub startblock: u64,
pub blockcount: u32,
pub unwritten: bool,
}
impl Extent {
pub fn decode(buf: &[u8]) -> Result<Self> {
if buf.len() < BMBT_REC_SIZE {
return Err(crate::Error::InvalidImage(
"xfs: bmbt record buffer too small".into(),
));
}
let hi = u64::from_be_bytes(buf[0..8].try_into().unwrap());
let lo = u64::from_be_bytes(buf[8..16].try_into().unwrap());
let unwritten = (hi >> 63) & 1 == 1;
let offset = (hi >> 9) & ((1u64 << 54) - 1);
let sb_hi = hi & ((1u64 << 9) - 1);
let sb_lo = lo >> 21;
let startblock = (sb_hi << 43) | sb_lo;
let blockcount = (lo & ((1u64 << 21) - 1)) as u32;
if blockcount == 0 {
return Err(crate::Error::InvalidImage(
"xfs: bmbt record has blockcount=0".into(),
));
}
Ok(Self {
offset,
startblock,
blockcount,
unwritten,
})
}
#[cfg(test)]
pub fn encode(&self) -> [u8; BMBT_REC_SIZE] {
let unwritten = if self.unwritten { 1u64 } else { 0 };
let hi = (unwritten << 63)
| ((self.offset & ((1 << 54) - 1)) << 9)
| ((self.startblock >> 43) & ((1 << 9) - 1));
let lo = ((self.startblock & ((1 << 43) - 1)) << 21)
| (self.blockcount as u64 & ((1 << 21) - 1));
let mut out = [0u8; 16];
out[0..8].copy_from_slice(&hi.to_be_bytes());
out[8..16].copy_from_slice(&lo.to_be_bytes());
out
}
}
pub fn decode_extents(buf: &[u8], n: u32) -> Result<Vec<Extent>> {
let need = (n as usize)
.checked_mul(BMBT_REC_SIZE)
.ok_or_else(|| crate::Error::InvalidImage("xfs: extent count overflows".into()))?;
if buf.len() < need {
return Err(crate::Error::InvalidImage(format!(
"xfs: extent array truncated: need {need} bytes, have {}",
buf.len()
)));
}
let mut out = Vec::with_capacity(n as usize);
for i in 0..n as usize {
let start = i * BMBT_REC_SIZE;
out.push(Extent::decode(&buf[start..start + BMBT_REC_SIZE])?);
}
Ok(out)
}
fn fsb_to_byte(agblklog: u8, blocksize: u32, agblocks: u32, fsb: u64) -> u64 {
let ag = fsb >> agblklog as u32;
let agblk = fsb & ((1u64 << agblklog as u32) - 1);
ag * (agblocks as u64) * (blocksize as u64) + agblk * (blocksize as u64)
}
#[derive(Debug, Clone, Copy)]
pub struct BmbtLayout {
pub blocksize: u32,
pub agblocks: u32,
pub agblklog: u8,
pub is_v5: bool,
}
impl BmbtLayout {
pub fn node_header_bytes(&self) -> usize {
if self.is_v5 { 72 } else { 24 }
}
}
pub fn decode_root(lit: &[u8]) -> Result<(u16, u16, Vec<u64>, Vec<u64>)> {
if lit.len() < 4 {
return Err(crate::Error::InvalidImage(
"xfs: bmbt root truncated".into(),
));
}
let level = u16::from_be_bytes(lit[0..2].try_into().unwrap());
let numrecs = u16::from_be_bytes(lit[2..4].try_into().unwrap());
let nrec = numrecs as usize;
let keys_bytes = nrec
.checked_mul(BMBT_KEY_SIZE)
.ok_or_else(|| crate::Error::InvalidImage("xfs: bmbt root keys length overflows".into()))?;
let ptrs_bytes = nrec
.checked_mul(BMBT_PTR_SIZE)
.ok_or_else(|| crate::Error::InvalidImage("xfs: bmbt root ptrs length overflows".into()))?;
if 4 + keys_bytes + ptrs_bytes > lit.len() {
return Err(crate::Error::InvalidImage(format!(
"xfs: bmbt root needs {} bytes but literal area is {}",
4 + keys_bytes + ptrs_bytes,
lit.len()
)));
}
let keys_start = 4;
let keys_end = keys_start + keys_bytes;
let mut keys = Vec::with_capacity(nrec);
for i in 0..nrec {
let off = keys_start + i * BMBT_KEY_SIZE;
keys.push(u64::from_be_bytes(lit[off..off + 8].try_into().unwrap()));
}
let ptrs_start_packed = keys_end;
let ptrs_start_tail = lit.len() - ptrs_bytes;
let ptrs_start = if ptrs_start_packed == ptrs_start_tail {
ptrs_start_packed
} else {
ptrs_start_tail
};
let mut ptrs = Vec::with_capacity(nrec);
for i in 0..nrec {
let off = ptrs_start + i * BMBT_PTR_SIZE;
ptrs.push(u64::from_be_bytes(lit[off..off + 8].try_into().unwrap()));
}
Ok((level, numrecs, keys, ptrs))
}
pub struct BmbtNodeRead {
pub level: u16,
pub numrecs: u16,
pub keys: Vec<u64>,
pub ptrs: Vec<u64>,
pub recs: Vec<Extent>,
}
pub fn read_node_block(
dev: &mut dyn BlockDevice,
layout: &BmbtLayout,
fsb: u64,
) -> Result<BmbtNodeRead> {
let byte_off = fsb_to_byte(layout.agblklog, layout.blocksize, layout.agblocks, fsb);
let mut block = vec![0u8; layout.blocksize as usize];
dev.read_at(byte_off, &mut block)?;
if block.len() < layout.node_header_bytes() {
return Err(crate::Error::InvalidImage(
"xfs: bmbt node block smaller than its header".into(),
));
}
let magic = u32::from_be_bytes(block[0..4].try_into().unwrap());
let want = if layout.is_v5 {
XFS_BMAP_CRC_MAGIC
} else {
XFS_BMAP_MAGIC
};
if magic != want {
return Err(crate::Error::InvalidImage(format!(
"xfs: bmbt block at fsb {fsb} has magic {magic:#010x}, want {want:#010x}"
)));
}
let level = u16::from_be_bytes(block[4..6].try_into().unwrap());
let numrecs = u16::from_be_bytes(block[6..8].try_into().unwrap());
let nrec = numrecs as usize;
let hdr = layout.node_header_bytes();
if level == 0 {
let need = nrec
.checked_mul(BMBT_REC_SIZE)
.ok_or_else(|| crate::Error::InvalidImage("xfs: bmbt leaf nrec overflows".into()))?;
if hdr + need > block.len() {
return Err(crate::Error::InvalidImage(format!(
"xfs: bmbt leaf needs {} bytes but block is {}",
hdr + need,
block.len()
)));
}
let mut recs = Vec::with_capacity(nrec);
for i in 0..nrec {
let off = hdr + i * BMBT_REC_SIZE;
recs.push(Extent::decode(&block[off..off + BMBT_REC_SIZE])?);
}
Ok(BmbtNodeRead {
level,
numrecs,
keys: Vec::new(),
ptrs: Vec::new(),
recs,
})
} else {
let max_recs = (layout.blocksize as usize - hdr) / (BMBT_KEY_SIZE + BMBT_PTR_SIZE);
if nrec > max_recs {
return Err(crate::Error::InvalidImage(format!(
"xfs: bmbt internal numrecs {nrec} > max {max_recs}"
)));
}
let keys_start = hdr;
let ptrs_start = hdr + max_recs * BMBT_KEY_SIZE;
let mut keys = Vec::with_capacity(nrec);
let mut ptrs = Vec::with_capacity(nrec);
for i in 0..nrec {
let ko = keys_start + i * BMBT_KEY_SIZE;
let po = ptrs_start + i * BMBT_PTR_SIZE;
if po + 8 > block.len() {
return Err(crate::Error::InvalidImage(
"xfs: bmbt internal ptr offset out of block".into(),
));
}
keys.push(u64::from_be_bytes(block[ko..ko + 8].try_into().unwrap()));
ptrs.push(u64::from_be_bytes(block[po..po + 8].try_into().unwrap()));
}
Ok(BmbtNodeRead {
level,
numrecs,
keys,
ptrs,
recs: Vec::new(),
})
}
}
pub fn walk_btree(
dev: &mut dyn BlockDevice,
layout: &BmbtLayout,
lit: &[u8],
) -> Result<Vec<Extent>> {
let (root_level, root_numrecs, _root_keys, root_ptrs) = decode_root(lit)?;
if root_level == 0 {
return decode_extents(&lit[4..], root_numrecs as u32);
}
let mut out = Vec::new();
fn walk(
dev: &mut dyn BlockDevice,
layout: &BmbtLayout,
fsb: u64,
sink: &mut Vec<Extent>,
) -> Result<()> {
let node = read_node_block(dev, layout, fsb)?;
if node.level == 0 {
sink.extend(node.recs);
} else {
for child in node.ptrs {
walk(dev, layout, child, sink)?;
}
}
Ok(())
}
for child in root_ptrs {
walk(dev, layout, child, &mut out)?;
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::block::MemoryBackend;
#[test]
fn extent_roundtrip() {
let cases = [
Extent {
offset: 0,
startblock: 100,
blockcount: 1,
unwritten: false,
},
Extent {
offset: 4,
startblock: 200,
blockcount: 8,
unwritten: false,
},
Extent {
offset: 1 << 30,
startblock: (1u64 << 50) | 0x12_3456,
blockcount: 0x1FFFFF,
unwritten: true,
},
];
for c in cases {
let bytes = c.encode();
let back = Extent::decode(&bytes).unwrap();
assert_eq!(back, c);
}
}
#[test]
fn extent_rejects_zero_blockcount() {
let mut bytes = [0u8; 16];
let e = Extent::decode(&bytes);
assert!(e.is_err());
bytes[0] = 0x00;
let e = Extent::decode(&bytes);
assert!(e.is_err());
}
#[test]
fn known_pattern() {
let offset: u64 = 4;
let startblock: u64 = 200;
let blockcount: u64 = 8;
let hi = (offset << 9) | ((startblock >> 43) & ((1 << 9) - 1));
let lo = ((startblock & ((1 << 43) - 1)) << 21) | blockcount;
let mut buf = [0u8; 16];
buf[0..8].copy_from_slice(&hi.to_be_bytes());
buf[8..16].copy_from_slice(&lo.to_be_bytes());
let e = Extent::decode(&buf).unwrap();
assert_eq!(e.offset, 4);
assert_eq!(e.startblock, 200);
assert_eq!(e.blockcount, 8);
assert!(!e.unwritten);
}
#[test]
fn walk_two_level_v5_btree() {
let blocksize = 4096u32;
let agblocks = 256u32;
let agblklog = 8u8;
let layout = BmbtLayout {
blocksize,
agblocks,
agblklog,
is_v5: true,
};
let total = (agblocks as u64) * 4 * blocksize as u64;
let mut dev = MemoryBackend::new(total);
let mut leaf0 = vec![0u8; blocksize as usize];
leaf0[0..4].copy_from_slice(&XFS_BMAP_CRC_MAGIC.to_be_bytes());
leaf0[4..6].copy_from_slice(&0u16.to_be_bytes()); leaf0[6..8].copy_from_slice(&2u16.to_be_bytes()); let e0 = Extent {
offset: 0,
startblock: 100,
blockcount: 4,
unwritten: false,
};
let e1 = Extent {
offset: 4,
startblock: 200,
blockcount: 8,
unwritten: false,
};
leaf0[72..72 + 16].copy_from_slice(&e0.encode());
leaf0[72 + 16..72 + 32].copy_from_slice(&e1.encode());
dev.write_at(fsb_to_byte(agblklog, blocksize, agblocks, 10), &leaf0)
.unwrap();
let mut leaf1 = vec![0u8; blocksize as usize];
leaf1[0..4].copy_from_slice(&XFS_BMAP_CRC_MAGIC.to_be_bytes());
leaf1[6..8].copy_from_slice(&2u16.to_be_bytes());
let e2 = Extent {
offset: 12,
startblock: 300,
blockcount: 2,
unwritten: false,
};
let e3 = Extent {
offset: 20,
startblock: 400,
blockcount: 1,
unwritten: false,
};
leaf1[72..72 + 16].copy_from_slice(&e2.encode());
leaf1[72 + 16..72 + 32].copy_from_slice(&e3.encode());
dev.write_at(fsb_to_byte(agblklog, blocksize, agblocks, 11), &leaf1)
.unwrap();
let mut root = vec![0u8; 64];
root[0..2].copy_from_slice(&1u16.to_be_bytes()); root[2..4].copy_from_slice(&2u16.to_be_bytes()); root[4..12].copy_from_slice(&0u64.to_be_bytes()); root[12..20].copy_from_slice(&12u64.to_be_bytes()); root[48..56].copy_from_slice(&10u64.to_be_bytes()); root[56..64].copy_from_slice(&11u64.to_be_bytes());
let extents = walk_btree(&mut dev, &layout, &root).unwrap();
assert_eq!(extents.len(), 4);
assert_eq!(extents[0], e0);
assert_eq!(extents[1], e1);
assert_eq!(extents[2], e2);
assert_eq!(extents[3], e3);
}
#[test]
fn fsb_to_byte_matches_inode_math() {
let b = fsb_to_byte(3, 4096, 8, 9);
assert_eq!(b, 32768 + 4096);
}
}