pub mod bmbt;
pub mod dir;
pub mod format;
pub mod inode;
pub mod journal;
pub mod superblock;
pub mod symlink;
pub mod write;
pub mod xattr;
pub use format::{FormatOpts, format};
pub use write::{DeviceKind, EntryMeta, WriteState};
use crate::Result;
use crate::block::BlockDevice;
use self::bmbt::{BmbtLayout, Extent};
use self::dir::DataEntry;
use self::inode::{DiFormat, DinodeCore};
use self::superblock::Superblock;
pub struct Xfs {
pub(crate) sb: Superblock,
pub(crate) write_state: Option<WriteState>,
}
impl Xfs {
pub fn open(dev: &mut dyn BlockDevice) -> Result<Self> {
if dev.total_size() < 512 {
return Err(crate::Error::InvalidImage(
"xfs: device too small to hold a superblock".into(),
));
}
let mut buf = [0u8; 512];
dev.read_at(0, &mut buf)?;
let sb = Superblock::decode(&buf)?;
if sb.rblocks != 0 {
return Err(crate::Error::Unsupported(
"xfs: realtime subvolume not supported".into(),
));
}
Ok(Self {
sb,
write_state: None,
})
}
pub fn total_bytes(&self) -> u64 {
self.sb.total_bytes()
}
pub fn block_size(&self) -> u32 {
self.sb.blocksize
}
pub fn inode_size(&self) -> u32 {
self.sb.inodesize as u32
}
pub fn ag_count(&self) -> u32 {
self.sb.agcount
}
pub fn superblock(&self) -> &Superblock {
&self.sb
}
fn split_ino(&self, ino: u64) -> (u64, u64, u64) {
let inopblog = self.sb.inopblog as u32;
let agblklog = self.sb.agblklog as u32;
let slot_mask = (1u64 << inopblog) - 1;
let blk_mask = (1u64 << agblklog) - 1;
let slot = ino & slot_mask;
let blk = (ino >> inopblog) & blk_mask;
let ag = ino >> (inopblog + agblklog);
(ag, blk, slot)
}
fn ino_byte_offset(&self, ino: u64) -> Result<u64> {
let (ag, blk, slot) = self.split_ino(ino);
if ag >= self.sb.agcount as u64 {
return Err(crate::Error::InvalidImage(format!(
"xfs: inode {ino} references ag {ag} but agcount = {}",
self.sb.agcount
)));
}
let ag_bytes = ag * (self.sb.agblocks as u64) * (self.sb.blocksize as u64);
let blk_bytes = blk * (self.sb.blocksize as u64);
let slot_bytes = slot * (self.sb.inodesize as u64);
Ok(ag_bytes + blk_bytes + slot_bytes)
}
fn read_inode(&self, dev: &mut dyn BlockDevice, ino: u64) -> Result<(Vec<u8>, DinodeCore)> {
let off = self.ino_byte_offset(ino)?;
let mut buf = vec![0u8; self.sb.inodesize as usize];
dev.read_at(off, &mut buf)?;
let core = DinodeCore::decode(&buf)?;
if let Some(self_ino) = core.di_ino
&& self_ino != ino
{
return Err(crate::Error::InvalidImage(format!(
"xfs: inode {ino}: di_ino self-reference is {self_ino}"
)));
}
Ok((buf, core))
}
fn resolve_path(
&self,
dev: &mut dyn BlockDevice,
path: &str,
) -> Result<(u64, Vec<u8>, DinodeCore)> {
let mut cur_ino = self.sb.rootino;
let (buf, core) = self.read_inode(dev, cur_ino)?;
if !core.is_dir() {
return Err(crate::Error::InvalidImage(
"xfs: root inode is not a directory".into(),
));
}
let mut cur_buf = buf;
let mut cur_core = core;
for part in split_path(path) {
let dir_entries = self.read_dir_entries(dev, &cur_buf, &cur_core)?;
let found = dir_entries.iter().find(|e| e.name == part).ok_or_else(|| {
crate::Error::InvalidArgument(format!("xfs: no such entry {part:?} under {path:?}"))
})?;
cur_ino = found.inumber;
let (b, c) = self.read_inode(dev, cur_ino)?;
cur_buf = b;
cur_core = c;
}
Ok((cur_ino, cur_buf, cur_core))
}
fn bmbt_layout(&self) -> BmbtLayout {
BmbtLayout {
blocksize: self.sb.blocksize,
agblocks: self.sb.agblocks,
agblklog: self.sb.agblklog,
is_v5: self.sb.is_v5(),
}
}
fn fsb_to_byte(&self, fsb: u64) -> u64 {
let ag = fsb >> self.sb.agblklog as u32;
let agblk = fsb & ((1u64 << self.sb.agblklog as u32) - 1);
ag * (self.sb.agblocks as u64) * (self.sb.blocksize as u64)
+ agblk * (self.sb.blocksize as u64)
}
fn read_extent_list(
&self,
dev: &mut dyn BlockDevice,
ino_buf: &[u8],
core: &DinodeCore,
) -> Result<Vec<Extent>> {
let lit = core.literal_area(ino_buf, self.sb.inodesize as usize);
match core.format {
DiFormat::Extents => bmbt::decode_extents(lit, core.nextents),
DiFormat::Btree => {
let layout = self.bmbt_layout();
bmbt::walk_btree(dev, &layout, lit)
}
other => Err(crate::Error::InvalidArgument(format!(
"xfs: extent list requested for non-extent inode (format={other:?})"
))),
}
}
fn read_extent_dir_entries(
&self,
dev: &mut dyn BlockDevice,
ino_buf: &[u8],
core: &DinodeCore,
) -> Result<Vec<DataEntry>> {
let extents = self.read_extent_list(dev, ino_buf, core)?;
if extents.is_empty() {
return Ok(Vec::new());
}
let dir_block_size = self.sb.dir_block_size() as u64;
let fs_blocks_per_dir_block = (dir_block_size / self.sb.blocksize as u64).max(1);
let leaf_dir_block_addr_fsblk = dir::XFS_DIR2_LEAF_FIRSTDB_BYTES / self.sb.blocksize as u64;
let first = self.read_dir_block_at_logical(dev, &extents, 0, dir_block_size as usize)?;
let is_v5 = self.sb.is_v5();
if dir::is_block_format(&first)? {
return dir::decode_block_dir(&first, is_v5);
}
let mut out = Vec::new();
let max_covered = extents
.iter()
.map(|e| e.offset + e.blockcount as u64)
.max()
.unwrap_or(0);
let upper = max_covered.min(leaf_dir_block_addr_fsblk);
let mut lblk = 0u64;
while lblk < upper {
let covered = extents
.iter()
.any(|e| lblk >= e.offset && lblk < e.offset + e.blockcount as u64);
if covered {
let block =
self.read_dir_block_at_logical(dev, &extents, lblk, dir_block_size as usize)?;
if block.len() >= 4 {
let magic = u32::from_be_bytes(block[0..4].try_into().unwrap());
if magic == dir::XFS_DIR3_DATA_MAGIC || magic == dir::XFS_DIR2_DATA_MAGIC {
let mut entries = dir::decode_data_block(&block, is_v5)?;
out.append(&mut entries);
}
}
}
lblk += fs_blocks_per_dir_block;
}
Ok(out)
}
fn read_dir_block_at_logical(
&self,
dev: &mut dyn BlockDevice,
extents: &[Extent],
lblk: u64,
dir_block_size: usize,
) -> Result<Vec<u8>> {
let bs = self.sb.blocksize as u64;
let fs_blocks_per_dir_block = (dir_block_size as u64 / bs).max(1);
let mut out = vec![0u8; dir_block_size];
for i in 0..fs_blocks_per_dir_block {
let target = lblk + i;
let ext = extents
.iter()
.find(|e| target >= e.offset && target < e.offset + e.blockcount as u64)
.ok_or_else(|| {
crate::Error::InvalidImage(format!(
"xfs: dir logical block {target} not covered by extent list"
))
})?;
let phys_fsb = ext.startblock + (target - ext.offset);
let phys_byte = self.fsb_to_byte(phys_fsb);
let off = (i * bs) as usize;
dev.read_at(phys_byte, &mut out[off..off + bs as usize])?;
}
Ok(out)
}
fn read_dir_entries(
&self,
dev: &mut dyn BlockDevice,
ino_buf: &[u8],
core: &DinodeCore,
) -> Result<Vec<DataEntry>> {
if !core.is_dir() {
return Err(crate::Error::InvalidArgument(
"xfs: target is not a directory".into(),
));
}
match core.format {
DiFormat::Local => {
let lit = core.literal_area(ino_buf, self.sb.inodesize as usize);
let has_ftype = core.version >= 3;
let (_parent, entries) = dir::decode_shortform(lit, has_ftype)?;
Ok(entries
.into_iter()
.map(|e| DataEntry {
name: e.name,
inumber: e.inumber,
ftype: e.ftype,
})
.collect())
}
DiFormat::Extents => self.read_extent_dir_entries(dev, ino_buf, core),
DiFormat::Btree => self.read_extent_dir_entries(dev, ino_buf, core),
DiFormat::Dev => Err(crate::Error::InvalidImage(
"xfs: directory inode has di_format=dev".into(),
)),
DiFormat::Unknown(b) => Err(crate::Error::Unsupported(format!(
"xfs: directory inode has unknown di_format {b}"
))),
}
}
pub fn list_path(
&self,
dev: &mut dyn BlockDevice,
path: &str,
) -> Result<Vec<crate::fs::DirEntry>> {
let (_ino, buf, core) = self.resolve_path(dev, path)?;
let entries = self.read_dir_entries(dev, &buf, &core)?;
Ok(dir::data_entries_to_generic(&entries))
}
pub fn read_symlink(&self, dev: &mut dyn BlockDevice, path: &str) -> Result<String> {
let (_ino, buf, core) = self.resolve_path(dev, path)?;
if !core.is_symlink() {
return Err(crate::Error::InvalidArgument(format!(
"xfs: {path:?} is not a symlink"
)));
}
match core.format {
DiFormat::Local => {
let lit = core.literal_area(&buf, self.sb.inodesize as usize);
symlink::decode_local(lit, core.size)
}
DiFormat::Extents => {
let extents = self.read_extent_list(dev, &buf, &core)?;
let layout = self.bmbt_layout();
symlink::decode_remote(dev, &layout, &extents, core.size)
}
DiFormat::Btree => Err(crate::Error::Unsupported(
"xfs: B-tree (di_format=BTREE) symlinks not implemented".into(),
)),
DiFormat::Dev => Err(crate::Error::InvalidArgument(
"xfs: symlink inode with di_format=dev".into(),
)),
DiFormat::Unknown(b) => Err(crate::Error::Unsupported(format!(
"xfs: symlink inode with unknown di_format {b}"
))),
}
}
pub fn open_file_reader<'a>(
&self,
dev: &'a mut dyn BlockDevice,
path: &str,
) -> Result<XfsFileReader<'a>> {
let (_ino, buf, core) = self.resolve_path(dev, path)?;
if !core.is_reg() {
return Err(crate::Error::InvalidArgument(format!(
"xfs: {path:?} is not a regular file"
)));
}
match core.format {
DiFormat::Extents | DiFormat::Btree => {
let extents = self.read_extent_list(dev, &buf, &core)?;
for e in &extents {
if e.unwritten {
return Err(crate::Error::Unsupported(
"xfs: unwritten extents not supported in read path".into(),
));
}
}
let layout = self.bmbt_layout();
Ok(XfsFileReader {
dev,
extents,
blocksize: self.sb.blocksize as u64,
agblocks: self.sb.agblocks as u64,
agblklog: self.sb.agblklog,
size: core.size,
pos: 0,
inline: None,
_layout: layout,
})
}
DiFormat::Local => {
let lit = core.literal_area(&buf, self.sb.inodesize as usize);
if (core.size as usize) > lit.len() {
return Err(crate::Error::InvalidImage(format!(
"xfs: local-format file claims size {} > literal area {}",
core.size,
lit.len()
)));
}
let data = lit[..core.size as usize].to_vec();
Ok(XfsFileReader {
dev,
extents: Vec::new(),
blocksize: self.sb.blocksize as u64,
agblocks: self.sb.agblocks as u64,
agblklog: self.sb.agblklog,
size: core.size,
pos: 0,
inline: Some(data),
_layout: self.bmbt_layout(),
})
}
DiFormat::Dev => Err(crate::Error::InvalidArgument(
"xfs: device-special inode has no file data".into(),
)),
DiFormat::Unknown(b) => Err(crate::Error::Unsupported(format!(
"xfs: file inode has unknown di_format {b}"
))),
}
}
}
pub struct XfsFileReader<'a> {
dev: &'a mut dyn BlockDevice,
extents: Vec<Extent>,
blocksize: u64,
agblocks: u64,
agblklog: u8,
size: u64,
pos: u64,
inline: Option<Vec<u8>>,
_layout: BmbtLayout,
}
impl<'a> XfsFileReader<'a> {
fn fsb_to_byte(&self, fsb: u64) -> u64 {
let ag = fsb >> self.agblklog as u32;
let agblk = fsb & ((1u64 << self.agblklog as u32) - 1);
ag * self.agblocks * self.blocksize + agblk * self.blocksize
}
}
impl<'a> std::io::Read for XfsFileReader<'a> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
if self.pos >= self.size || buf.is_empty() {
return Ok(0);
}
let want = buf.len() as u64;
let avail = self.size - self.pos;
let n = want.min(avail) as usize;
if let Some(inline) = &self.inline {
let start = self.pos as usize;
let end = start + n;
buf[..n].copy_from_slice(&inline[start..end]);
self.pos += n as u64;
return Ok(n);
}
let pos_blk = self.pos / self.blocksize;
let pos_off = self.pos % self.blocksize;
let extent = self
.extents
.iter()
.find(|e| pos_blk >= e.offset && pos_blk < e.offset + e.blockcount as u64);
let n_done = match extent {
Some(e) => {
let extent_blocks_left = e.offset + e.blockcount as u64 - pos_blk;
let extent_bytes_left = extent_blocks_left * self.blocksize - pos_off;
let to_read = (n as u64).min(extent_bytes_left) as usize;
let phys_fsb = e.startblock + (pos_blk - e.offset);
let phys_byte = self.fsb_to_byte(phys_fsb) + pos_off;
self.dev
.read_at(phys_byte, &mut buf[..to_read])
.map_err(std::io::Error::other)?;
to_read
}
None => {
let next_extent_block = self
.extents
.iter()
.filter(|e| e.offset > pos_blk)
.map(|e| e.offset)
.min();
let next_byte = next_extent_block
.map(|b| b * self.blocksize)
.unwrap_or(self.size);
let hole_bytes = next_byte.saturating_sub(self.pos);
let to_zero = (n as u64).min(hole_bytes) as usize;
buf[..to_zero].fill(0);
to_zero
}
};
if n_done == 0 {
return Ok(0);
}
self.pos += n_done as u64;
Ok(n_done)
}
}
pub fn probe(dev: &mut dyn BlockDevice) -> Result<bool> {
if dev.total_size() < 512 {
return Ok(false);
}
let mut head = [0u8; 4];
dev.read_at(0, &mut head)?;
Ok(&head == b"XFSB")
}
fn split_path(path: &str) -> Vec<&str> {
path.split('/')
.filter(|p| !p.is_empty() && *p != ".")
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::block::MemoryBackend;
fn minimal_image() -> MemoryBackend {
let blocksize = 4096u32;
let agblocks = 8u32;
let agcount = 4u32;
let inodesize = 256u16;
let inopblock = (blocksize as u16) / inodesize;
let dblocks = (agblocks as u64) * (agcount as u64);
let rootino = 128u64;
let buf = superblock::synth_sb_for_tests(
blocksize,
dblocks,
agblocks,
agcount,
inodesize,
inopblock,
rootino,
superblock::XFS_SB_VERSION_5,
);
let total = dblocks * (blocksize as u64);
let mut dev = MemoryBackend::new(total);
dev.write_at(0, &buf).unwrap();
dev
}
#[test]
fn open_reads_superblock() {
let mut dev = minimal_image();
let xfs = Xfs::open(&mut dev).unwrap();
assert_eq!(xfs.block_size(), 4096);
assert_eq!(xfs.inode_size(), 256);
assert_eq!(xfs.ag_count(), 4);
assert_eq!(xfs.total_bytes(), 32 * 4096);
}
#[test]
fn probe_returns_true_for_xfs() {
let mut dev = minimal_image();
assert!(probe(&mut dev).unwrap());
}
#[test]
fn probe_returns_false_for_garbage() {
let mut dev = MemoryBackend::new(4096);
assert!(!probe(&mut dev).unwrap());
}
#[test]
fn ino_split_matches_manual_math() {
let mut dev = minimal_image();
let xfs = Xfs::open(&mut dev).unwrap();
assert_eq!(xfs.sb.inopblog, 4);
assert_eq!(xfs.sb.agblklog, 3);
let (ag, blk, slot) = xfs.split_ino(128);
assert_eq!((ag, blk, slot), (1, 0, 0));
let (ag, blk, slot) = xfs.split_ino(0);
assert_eq!((ag, blk, slot), (0, 0, 0));
let (ag, blk, slot) = xfs.split_ino(309);
assert_eq!((ag, blk, slot), (2, 3, 5));
}
#[test]
fn ino_byte_offset_rejects_out_of_range_ag() {
let mut dev = minimal_image();
let xfs = Xfs::open(&mut dev).unwrap();
let bad_ino = 4u64 * 128;
assert!(matches!(
xfs.ino_byte_offset(bad_ino),
Err(crate::Error::InvalidImage(_))
));
}
#[test]
fn ino_byte_offset_arithmetic() {
let mut dev = minimal_image();
let xfs = Xfs::open(&mut dev).unwrap();
let off = xfs.ino_byte_offset(128).unwrap();
assert_eq!(off, 8 * 4096);
let off = xfs.ino_byte_offset(309).unwrap();
assert_eq!(off, 2 * 8 * 4096 + 3 * 4096 + 5 * 256);
}
#[test]
fn split_path_handles_edge_cases() {
assert!(split_path("/").is_empty());
assert!(split_path("").is_empty());
assert!(split_path("/.").is_empty());
assert_eq!(split_path("/a/b"), vec!["a", "b"]);
assert_eq!(split_path("a//b///c"), vec!["a", "b", "c"]);
}
#[test]
fn list_root_shortform() {
let mut dev = minimal_image();
let xfs = Xfs::open(&mut dev).unwrap();
let rootino = xfs.sb.rootino;
let off = xfs.ino_byte_offset(rootino).unwrap();
let mut ino_buf = vec![0u8; 256];
ino_buf[0..2].copy_from_slice(&inode::XFS_DINODE_MAGIC.to_be_bytes());
ino_buf[2..4].copy_from_slice(&(inode::S_IFDIR | 0o755).to_be_bytes());
ino_buf[4] = 3; ino_buf[5] = 1; ino_buf[16..20].copy_from_slice(&2u32.to_be_bytes()); ino_buf[152..160].copy_from_slice(&rootino.to_be_bytes()); let lit_off = 176;
ino_buf[lit_off] = 1; ino_buf[lit_off + 1] = 0; ino_buf[lit_off + 2..lit_off + 6].copy_from_slice(&(rootino as u32).to_be_bytes());
let entry_off = lit_off + 6;
ino_buf[entry_off] = 2; ino_buf[entry_off + 1] = 0;
ino_buf[entry_off + 2] = 0; ino_buf[entry_off + 3..entry_off + 5].copy_from_slice(b"hi");
ino_buf[entry_off + 5] = dir::XFS_DIR3_FT_REG_FILE;
ino_buf[entry_off + 6..entry_off + 10].copy_from_slice(&200u32.to_be_bytes());
dev.write_at(off, &ino_buf).unwrap();
let xfs = Xfs::open(&mut dev).unwrap();
let entries = xfs.list_path(&mut dev, "/").unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].name, "hi");
assert_eq!(entries[0].inode, 200);
assert_eq!(entries[0].kind, crate::fs::EntryKind::Regular);
}
#[test]
fn read_inline_symlink_end_to_end() {
let mut dev = minimal_image();
let xfs = Xfs::open(&mut dev).unwrap();
let rootino = xfs.sb.rootino;
let root_off = xfs.ino_byte_offset(rootino).unwrap();
let mut root_buf = vec![0u8; 256];
root_buf[0..2].copy_from_slice(&inode::XFS_DINODE_MAGIC.to_be_bytes());
root_buf[2..4].copy_from_slice(&(inode::S_IFDIR | 0o755).to_be_bytes());
root_buf[4] = 3; root_buf[5] = 1; root_buf[16..20].copy_from_slice(&2u32.to_be_bytes()); root_buf[152..160].copy_from_slice(&rootino.to_be_bytes()); let lit_off = 176;
root_buf[lit_off] = 1; root_buf[lit_off + 1] = 0; root_buf[lit_off + 2..lit_off + 6].copy_from_slice(&(rootino as u32).to_be_bytes());
let e = lit_off + 6;
root_buf[e] = 3; root_buf[e + 1] = 0;
root_buf[e + 2] = 0;
root_buf[e + 3..e + 6].copy_from_slice(b"lnk");
root_buf[e + 6] = dir::XFS_DIR3_FT_SYMLINK;
root_buf[e + 7..e + 11].copy_from_slice(&200u32.to_be_bytes());
dev.write_at(root_off, &root_buf).unwrap();
let off200 = xfs.ino_byte_offset(200).unwrap();
let mut buf = vec![0u8; 256];
buf[0..2].copy_from_slice(&inode::XFS_DINODE_MAGIC.to_be_bytes());
buf[2..4].copy_from_slice(&(inode::S_IFLNK | 0o777).to_be_bytes());
buf[4] = 3; buf[5] = 1; buf[16..20].copy_from_slice(&1u32.to_be_bytes()); let target = "/etc/hostname";
buf[56..64].copy_from_slice(&(target.len() as u64).to_be_bytes());
buf[152..160].copy_from_slice(&200u64.to_be_bytes()); let lit = 176;
buf[lit..lit + target.len()].copy_from_slice(target.as_bytes());
dev.write_at(off200, &buf).unwrap();
let xfs = Xfs::open(&mut dev).unwrap();
let got = xfs.read_symlink(&mut dev, "/lnk").unwrap();
assert_eq!(got, target);
}
}