use std::io::Read;
use crate::Result;
use crate::block::BlockDevice;
use super::Xfs;
use super::bmbt::Extent;
use super::dir::{
DataEntry, XFS_DIR3_FT_BLKDEV, XFS_DIR3_FT_CHRDEV, XFS_DIR3_FT_DIR, XFS_DIR3_FT_FIFO,
XFS_DIR3_FT_REG_FILE, XFS_DIR3_FT_SOCK, XFS_DIR3_FT_SYMLINK, encode_v5_block_dir,
stamp_v5_dir_block_crc,
};
use super::format::{
AG0_METADATA_BLOCKS, LOG_AGBLOCK, ROOT_CHUNK_AGBLOCK, XFS_ABTB_CRC_MAGIC, XFS_ABTC_CRC_MAGIC,
XFS_BLOCKSIZE, XFS_BTREE_SBLOCK_V5_SIZE, XFS_IBT_CRC_MAGIC, XFS_INODES_PER_CHUNK,
XFS_INODESIZE, XFS_INOPBLOCK, stamp_v5_btree_block_crc,
};
use super::inode::{
S_IFBLK, S_IFCHR, S_IFDIR, S_IFIFO, S_IFLNK, S_IFREG, S_IFSOCK, V3DinodeBuilder, XfsTimestamp,
stamp_v3_inode_crc,
};
use super::journal::DEFAULT_LOG_BLOCKS;
use super::symlink::XFS_SYMLINK_HDR_SIZE;
pub const SCRATCH_SIZE: usize = 64 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DeviceKind {
Char,
Block,
Fifo,
Socket,
}
impl DeviceKind {
fn s_ifmt(self) -> u16 {
match self {
Self::Char => S_IFCHR,
Self::Block => S_IFBLK,
Self::Fifo => S_IFIFO,
Self::Socket => S_IFSOCK,
}
}
fn ftype(self) -> u8 {
match self {
Self::Char => XFS_DIR3_FT_CHRDEV,
Self::Block => XFS_DIR3_FT_BLKDEV,
Self::Fifo => XFS_DIR3_FT_FIFO,
Self::Socket => XFS_DIR3_FT_SOCK,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct EntryMeta {
pub mode: u16,
pub uid: u32,
pub gid: u32,
pub mtime: u32,
pub atime: u32,
pub ctime: u32,
}
impl Default for EntryMeta {
fn default() -> Self {
Self {
mode: 0o644,
uid: 0,
gid: 0,
mtime: 0,
atime: 0,
ctime: 0,
}
}
}
impl EntryMeta {
fn ts(&self) -> (XfsTimestamp, XfsTimestamp, XfsTimestamp) {
(
XfsTimestamp {
sec: self.atime,
nsec: 0,
},
XfsTimestamp {
sec: self.mtime,
nsec: 0,
},
XfsTimestamp {
sec: self.ctime,
nsec: 0,
},
)
}
}
#[derive(Debug, Clone)]
struct InodeChunk {
startino_ag: u32,
ir_free: u64,
#[allow(dead_code)]
agblock: u32,
}
impl InodeChunk {
fn alloc(&mut self) -> Option<u32> {
for i in 0..64 {
let mask = 1u64 << i;
if (self.ir_free & mask) != 0 {
self.ir_free &= !mask;
return Some(self.startino_ag + i);
}
}
None
}
fn freecount(&self) -> u32 {
self.ir_free.count_ones()
}
fn free_slot(&mut self, slot: u32) -> bool {
if slot >= 64 {
return false;
}
let mask = 1u64 << slot;
if self.ir_free & mask != 0 {
return false; }
self.ir_free |= mask;
true
}
}
#[derive(Debug, Clone)]
struct AgState {
next_agblock: u32,
chunks: Vec<InodeChunk>,
freed_extents: Vec<(u32, u32)>,
}
impl AgState {
fn freecount_inodes(&self) -> u32 {
self.chunks.iter().map(|c| c.freecount()).sum()
}
fn usedcount_inodes(&self) -> u32 {
self.chunks.iter().map(|c| 64 - c.freecount()).sum()
}
}
#[derive(Debug, Clone)]
pub struct WriteState {
ags: Vec<AgState>,
next_inode_ag: u32,
next_block_ag: u32,
#[allow(dead_code)]
uuid: [u8; 16],
inodes_used: u64,
inodes_free: u64,
}
impl WriteState {
pub fn initial(uuid: [u8; 16], agcount: u32) -> Self {
let log_blocks = DEFAULT_LOG_BLOCKS;
let mut ags = Vec::with_capacity(agcount as usize);
for ag in 0..agcount {
let (next_agblock, chunks) = if ag == 0 {
(
LOG_AGBLOCK + log_blocks,
vec![InodeChunk {
startino_ag: ROOT_CHUNK_AGBLOCK * (XFS_INOPBLOCK as u32),
ir_free: !0b111u64,
agblock: ROOT_CHUNK_AGBLOCK,
}],
)
} else {
(AG0_METADATA_BLOCKS, Vec::new())
};
ags.push(AgState {
next_agblock,
chunks,
freed_extents: Vec::new(),
});
}
Self {
ags,
next_inode_ag: 0,
next_block_ag: 0,
uuid,
inodes_used: 3,
inodes_free: 61,
}
}
pub fn initial_single_ag(uuid: [u8; 16]) -> Self {
Self::initial(uuid, 1)
}
}
impl Xfs {
pub fn begin_writes(&mut self, uuid: [u8; 16]) {
let agcount = self.sb.agcount.max(1);
self.write_state = Some(WriteState::initial(uuid, agcount));
}
fn ws_mut(&mut self) -> Result<&mut WriteState> {
self.write_state.as_mut().ok_or_else(|| {
crate::Error::InvalidArgument(
"xfs: write methods called before begin_writes() / format()".into(),
)
})
}
fn alloc_blocks_in_any_ag(&mut self, n: u32) -> Result<(u32, u32)> {
let agblocks = self.sb.agblocks;
let ws = self.ws_mut()?;
let agcount = ws.ags.len() as u32;
for offset in 0..agcount {
let ag = (ws.next_block_ag + offset) % agcount;
let ag_state = &mut ws.ags[ag as usize];
if let Some(idx) = ag_state.freed_extents.iter().position(|(_s, c)| *c == n) {
let (start, _) = ag_state.freed_extents.swap_remove(idx);
ws.next_block_ag = (ag + 1) % agcount;
return Ok((ag, start));
}
let next = ag_state.next_agblock;
if next.checked_add(n).is_some_and(|end| end <= agblocks) {
let start = next;
ag_state.next_agblock = next + n;
ws.next_block_ag = (ag + 1) % agcount;
return Ok((ag, start));
}
}
Err(crate::Error::InvalidArgument(format!(
"xfs: out of space across all {agcount} AGs (requested {n} blocks)"
)))
}
fn alloc_blocks_fsb(&mut self, n: u32) -> Result<u64> {
let (ag, agblk) = self.alloc_blocks_in_any_ag(n)?;
Ok(((ag as u64) << self.sb.agblklog as u32) | (agblk as u64))
}
fn alloc_inode(&mut self) -> Result<u64> {
let inopblog = self.sb.inopblog as u32;
let agblklog = self.sb.agblklog as u32;
let agcount = {
let ws = self.ws_mut()?;
ws.ags.len() as u32
};
for offset in 0..agcount {
let try_result = {
let ws = self.ws_mut()?;
let ag = (ws.next_inode_ag + offset) % agcount;
let ag_state = &mut ws.ags[ag as usize];
let mut found = None;
for chunk in &mut ag_state.chunks {
if let Some(rel) = chunk.alloc() {
found = Some(rel);
break;
}
}
found.map(|rel| (ag, rel))
};
if let Some((ag, rel)) = try_result {
let ws = self.ws_mut()?;
ws.inodes_used += 1;
ws.inodes_free -= 1;
ws.next_inode_ag = (ag + 1) % agcount;
return Ok(((ag as u64) << (inopblog + agblklog)) | (rel as u64));
}
}
let chunk_blocks = XFS_INODES_PER_CHUNK / (XFS_INOPBLOCK as u32);
let (ag, agblk) = self.alloc_blocks_in_any_ag(chunk_blocks)?;
let startino_ag = agblk * (XFS_INOPBLOCK as u32);
let mut chunk = InodeChunk {
startino_ag,
ir_free: !1u64,
agblock: agblk,
};
let rel = chunk.alloc().expect("fresh chunk has 64 free inodes");
let ws = self.ws_mut()?;
ws.ags[ag as usize].chunks.push(chunk);
ws.inodes_used += 1;
ws.inodes_free += 63;
ws.next_inode_ag = (ag + 1) % (ws.ags.len() as u32);
Ok(((ag as u64) << (inopblog + agblklog)) | (rel as u64))
}
fn free_blocks_fsb(&mut self, start_fsb: u64, n: u32) -> Result<()> {
let agblklog = self.sb.agblklog as u32;
let ag = (start_fsb >> agblklog) as u32;
let agblk = (start_fsb & ((1u64 << agblklog) - 1)) as u32;
let ws = self.ws_mut()?;
if (ag as usize) >= ws.ags.len() {
return Err(crate::Error::InvalidImage(format!(
"xfs: free_blocks_fsb: ag {ag} out of range"
)));
}
ws.ags[ag as usize].freed_extents.push((agblk, n));
Ok(())
}
fn free_inode(&mut self, ino: u64) -> Result<(u32, u32)> {
let inopblog = self.sb.inopblog as u32;
let agblklog = self.sb.agblklog as u32;
let slot_mask = (1u64 << inopblog) - 1;
let agblk_mask = (1u64 << agblklog) - 1;
let slot = (ino & slot_mask) as u32;
let agrel_ino = ino & ((1u64 << (inopblog + agblklog)) - 1);
let agrel_ino = agrel_ino as u32;
let ag = (ino >> (inopblog + agblklog)) as u32;
let _ = agblk_mask;
let ws = self.ws_mut()?;
if (ag as usize) >= ws.ags.len() {
return Err(crate::Error::InvalidImage(format!(
"xfs: free_inode: ag {ag} out of range"
)));
}
let ag_state = &mut ws.ags[ag as usize];
for chunk in &mut ag_state.chunks {
if agrel_ino >= chunk.startino_ag && agrel_ino < chunk.startino_ag + 64 {
let slot_in_chunk = agrel_ino - chunk.startino_ag;
if chunk.free_slot(slot_in_chunk) {
ws.inodes_used -= 1;
ws.inodes_free += 1;
return Ok((ag, slot));
}
return Err(crate::Error::InvalidArgument(format!(
"xfs: free_inode {ino}: slot already free"
)));
}
}
Err(crate::Error::InvalidImage(format!(
"xfs: free_inode {ino}: no chunk owns this inode"
)))
}
fn write_inode(
&mut self,
dev: &mut dyn BlockDevice,
ino: u64,
builder: V3DinodeBuilder,
literal: &[u8],
) -> Result<()> {
let off = self.ino_byte_offset(ino)?;
let mut buf = builder.build();
let lit_max = (XFS_INODESIZE as usize) - 176;
if literal.len() > lit_max {
return Err(crate::Error::InvalidArgument(format!(
"xfs: literal area {} > {lit_max}",
literal.len()
)));
}
buf[176..176 + literal.len()].copy_from_slice(literal);
stamp_v3_inode_crc(&mut buf);
dev.write_at(off, &buf)?;
Ok(())
}
fn append_dir_entry(
&mut self,
dev: &mut dyn BlockDevice,
parent_ino: u64,
name: &str,
child_ino: u64,
ftype: u8,
) -> Result<u64> {
let (parent_buf, parent_core) = self.read_inode(dev, parent_ino)?;
if !parent_core.is_dir() {
return Err(crate::Error::InvalidArgument(format!(
"xfs: parent inode {parent_ino} is not a directory"
)));
}
let extents = self.read_extent_list(dev, &parent_buf, &parent_core)?;
if extents.len() != 1 || extents[0].blockcount != 1 {
return Err(crate::Error::Unsupported(
"xfs: write path only supports block-format (single-extent) directories".into(),
));
}
let parent_self_extent = extents[0];
let dir_block_size = self.sb.dir_block_size() as usize;
let mut block = vec![0u8; dir_block_size];
let phys_byte = self.fsb_to_byte(parent_self_extent.startblock);
dev.read_at(phys_byte, &mut block)?;
let existing = super::dir::decode_block_dir(&block, self.sb.is_v5())?;
let mut all: Vec<(String, u64, u8)> = existing
.into_iter()
.filter(|e| e.name != "." && e.name != "..")
.map(|e: DataEntry| (e.name, e.inumber, e.ftype))
.collect();
all.push((name.to_string(), child_ino, ftype));
let parent_parent = existing_parent(&block, self.sb.is_v5())?;
let basic_blkno = phys_byte / 512;
let uuid = self.uuid_for_writes();
let new_block = encode_v5_block_dir(
dir_block_size,
parent_ino,
parent_parent,
&all,
&uuid,
basic_blkno,
)?;
dev.write_at(phys_byte, &new_block)?;
let new_size = dir_block_size as u64;
let (atime, mtime, ctime) = (parent_core.atime, parent_core.mtime, parent_core.ctime);
let crtime = mtime;
let nlink = if ftype == XFS_DIR3_FT_DIR {
parent_core.nlink + 1 } else {
parent_core.nlink
};
let builder = V3DinodeBuilder {
inodesize: XFS_INODESIZE as usize,
mode: parent_core.mode,
format: 2,
uid: parent_core.uid,
gid: parent_core.gid,
nlink,
atime,
mtime,
ctime,
crtime,
size: new_size,
nblocks: 1,
extsize: 0,
nextents: 1,
forkoff: 0,
aformat: 2,
flags: parent_core.flags,
generation: parent_core.generation,
di_ino: parent_ino,
uuid,
};
let mut lit = Vec::with_capacity(16);
lit.extend_from_slice(&parent_self_extent.encode());
self.write_inode(dev, parent_ino, builder, &lit)?;
Ok(new_size)
}
fn uuid_for_writes(&self) -> [u8; 16] {
self.sb.uuid
}
pub fn add_file<R: Read>(
&mut self,
dev: &mut dyn BlockDevice,
parent_ino: u64,
name: &str,
meta: EntryMeta,
size: u64,
src: &mut R,
) -> Result<u64> {
let bs = self.sb.blocksize as u64;
let nblocks = if size == 0 { 0 } else { size.div_ceil(bs) } as u32;
let startblock = if nblocks > 0 {
self.alloc_blocks_fsb(nblocks)?
} else {
0
};
if nblocks > 0 {
let mut scratch = [0u8; SCRATCH_SIZE];
let mut remaining = size;
let mut dev_offset = self.fsb_to_byte(startblock);
while remaining > 0 {
let want = (remaining.min(SCRATCH_SIZE as u64)) as usize;
let n = read_exact_or_eof(src, &mut scratch[..want])?;
if n == 0 {
return Err(crate::Error::InvalidArgument(format!(
"xfs: source for {name:?} returned EOF before {size} bytes (short by {remaining})"
)));
}
dev.write_at(dev_offset, &scratch[..n])?;
dev_offset += n as u64;
remaining -= n as u64;
}
let tail = (size % bs) as usize;
if tail != 0 {
let pad = (bs as usize) - tail;
let zero = [0u8; SCRATCH_SIZE];
let n = pad.min(SCRATCH_SIZE);
dev.write_at(dev_offset, &zero[..n])?;
}
}
let ino = self.alloc_inode()?;
let (atime, mtime, ctime) = meta.ts();
let builder = V3DinodeBuilder {
inodesize: XFS_INODESIZE as usize,
mode: S_IFREG | (meta.mode & 0o7777),
format: 2, uid: meta.uid,
gid: meta.gid,
nlink: 1,
atime,
mtime,
ctime,
crtime: mtime,
size,
nblocks: nblocks as u64,
extsize: 0,
nextents: if nblocks > 0 { 1 } else { 0 },
forkoff: 0,
aformat: 2,
flags: 0,
generation: 1,
di_ino: ino,
uuid: self.uuid_for_writes(),
};
let lit = if nblocks > 0 {
let ext = Extent {
offset: 0,
startblock,
blockcount: nblocks,
unwritten: false,
};
ext.encode().to_vec()
} else {
Vec::new()
};
self.write_inode(dev, ino, builder, &lit)?;
self.append_dir_entry(dev, parent_ino, name, ino, XFS_DIR3_FT_REG_FILE)?;
Ok(ino)
}
pub fn add_dir(
&mut self,
dev: &mut dyn BlockDevice,
parent_ino: u64,
name: &str,
meta: EntryMeta,
) -> Result<u64> {
let dir_block_size = self.sb.dir_block_size() as usize;
let dir_block_fsb = self.alloc_blocks_fsb(1)?;
let ino = self.alloc_inode()?;
let uuid = self.uuid_for_writes();
let phys_byte = self.fsb_to_byte(dir_block_fsb);
let basic_blkno = phys_byte / 512;
let block = encode_v5_block_dir(dir_block_size, ino, parent_ino, &[], &uuid, basic_blkno)?;
dev.write_at(phys_byte, &block)?;
let (atime, mtime, ctime) = meta.ts();
let builder = V3DinodeBuilder {
inodesize: XFS_INODESIZE as usize,
mode: S_IFDIR | (meta.mode & 0o7777),
format: 2, uid: meta.uid,
gid: meta.gid,
nlink: 2,
atime,
mtime,
ctime,
crtime: mtime,
size: dir_block_size as u64,
nblocks: 1,
extsize: 0,
nextents: 1,
forkoff: 0,
aformat: 2,
flags: 0,
generation: 1,
di_ino: ino,
uuid,
};
let ext = Extent {
offset: 0,
startblock: dir_block_fsb,
blockcount: 1,
unwritten: false,
};
self.write_inode(dev, ino, builder, &ext.encode())?;
self.append_dir_entry(dev, parent_ino, name, ino, XFS_DIR3_FT_DIR)?;
Ok(ino)
}
pub fn add_symlink(
&mut self,
dev: &mut dyn BlockDevice,
parent_ino: u64,
name: &str,
target: &str,
meta: EntryMeta,
) -> Result<u64> {
let lit_max = (XFS_INODESIZE as usize) - 176;
let target_bytes = target.as_bytes();
let bs = self.sb.blocksize as usize;
let max_remote = bs - XFS_SYMLINK_HDR_SIZE;
let ino = self.alloc_inode()?;
let uuid = self.uuid_for_writes();
let (atime, mtime, ctime) = meta.ts();
if target_bytes.len() <= lit_max {
let builder = V3DinodeBuilder {
inodesize: XFS_INODESIZE as usize,
mode: S_IFLNK | 0o777,
format: 1, uid: meta.uid,
gid: meta.gid,
nlink: 1,
atime,
mtime,
ctime,
crtime: mtime,
size: target_bytes.len() as u64,
nblocks: 0,
extsize: 0,
nextents: 0,
forkoff: 0,
aformat: 2,
flags: 0,
generation: 1,
di_ino: ino,
uuid,
};
self.write_inode(dev, ino, builder, target_bytes)?;
} else if target_bytes.len() <= max_remote {
let blk_fsb = self.alloc_blocks_fsb(1)?;
let blk_byte = self.fsb_to_byte(blk_fsb);
let mut blkbuf = vec![0u8; bs];
blkbuf[0..4].copy_from_slice(&super::symlink::XFS_SYMLINK_MAGIC.to_be_bytes());
blkbuf[16..24].copy_from_slice(&ino.to_be_bytes());
let basic_blkno = blk_byte / 512;
blkbuf[24..32].copy_from_slice(&basic_blkno.to_be_bytes());
blkbuf[40..56].copy_from_slice(&uuid);
blkbuf[XFS_SYMLINK_HDR_SIZE..XFS_SYMLINK_HDR_SIZE + target_bytes.len()]
.copy_from_slice(target_bytes);
stamp_v5_dir_block_crc(&mut blkbuf);
dev.write_at(blk_byte, &blkbuf)?;
let builder = V3DinodeBuilder {
inodesize: XFS_INODESIZE as usize,
mode: S_IFLNK | 0o777,
format: 2, uid: meta.uid,
gid: meta.gid,
nlink: 1,
atime,
mtime,
ctime,
crtime: mtime,
size: target_bytes.len() as u64,
nblocks: 1,
extsize: 0,
nextents: 1,
forkoff: 0,
aformat: 2,
flags: 0,
generation: 1,
di_ino: ino,
uuid,
};
let ext = Extent {
offset: 0,
startblock: blk_fsb,
blockcount: 1,
unwritten: false,
};
self.write_inode(dev, ino, builder, &ext.encode())?;
} else {
return Err(crate::Error::Unsupported(format!(
"xfs: symlink target {} bytes > one-block remote limit ({max_remote})",
target_bytes.len()
)));
}
self.append_dir_entry(dev, parent_ino, name, ino, XFS_DIR3_FT_SYMLINK)?;
Ok(ino)
}
#[allow(clippy::too_many_arguments)]
pub fn add_device(
&mut self,
dev: &mut dyn BlockDevice,
parent_ino: u64,
name: &str,
kind: DeviceKind,
major: u32,
minor: u32,
meta: EntryMeta,
) -> Result<u64> {
let ino = self.alloc_inode()?;
let uuid = self.uuid_for_writes();
let (atime, mtime, ctime) = meta.ts();
let packed = ((major as u64) << 20) | (minor as u64 & 0xFFFFF);
let mut lit = [0u8; 8];
lit.copy_from_slice(&packed.to_be_bytes());
let builder = V3DinodeBuilder {
inodesize: XFS_INODESIZE as usize,
mode: kind.s_ifmt() | (meta.mode & 0o7777),
format: 0, uid: meta.uid,
gid: meta.gid,
nlink: 1,
atime,
mtime,
ctime,
crtime: mtime,
size: 0,
nblocks: 0,
extsize: 0,
nextents: 0,
forkoff: 0,
aformat: 2,
flags: 0,
generation: 1,
di_ino: ino,
uuid,
};
self.write_inode(dev, ino, builder, &lit)?;
self.append_dir_entry(dev, parent_ino, name, ino, kind.ftype())?;
Ok(ino)
}
pub fn remove(
&mut self,
dev: &mut dyn BlockDevice,
parent_ino: u64,
name: &str,
) -> Result<u64> {
if name == "." || name == ".." || name.is_empty() {
return Err(crate::Error::InvalidArgument(format!(
"xfs: cannot remove {name:?}"
)));
}
let (parent_buf, parent_core) = self.read_inode(dev, parent_ino)?;
if !parent_core.is_dir() {
return Err(crate::Error::InvalidArgument(format!(
"xfs: parent inode {parent_ino} is not a directory"
)));
}
let extents = self.read_extent_list(dev, &parent_buf, &parent_core)?;
if extents.len() != 1 || extents[0].blockcount != 1 {
return Err(crate::Error::Unsupported(
"xfs: remove only supports block-format (single-extent) parents".into(),
));
}
let parent_self_extent = extents[0];
let dir_block_size = self.sb.dir_block_size() as usize;
let phys_byte = self.fsb_to_byte(parent_self_extent.startblock);
let mut block = vec![0u8; dir_block_size];
dev.read_at(phys_byte, &mut block)?;
let existing = super::dir::decode_block_dir(&block, self.sb.is_v5())?;
let target = existing.iter().find(|e| e.name == name).ok_or_else(|| {
crate::Error::InvalidArgument(format!(
"xfs: {name:?} not present in directory inode {parent_ino}"
))
})?;
let target_ino = target.inumber;
let target_ftype = target.ftype;
let (target_buf, target_core) = self.read_inode(dev, target_ino)?;
if target_core.is_dir() {
let child_entries = self.read_dir_entries(dev, &target_buf, &target_core)?;
let user_entries: Vec<_> = child_entries
.iter()
.filter(|e| e.name != "." && e.name != "..")
.collect();
if !user_entries.is_empty() {
return Err(crate::Error::InvalidArgument(format!(
"xfs: directory {name:?} not empty (contains {} entries)",
user_entries.len()
)));
}
}
if matches!(
target_core.format,
super::inode::DiFormat::Extents | super::inode::DiFormat::Btree
) {
let target_extents = self.read_extent_list(dev, &target_buf, &target_core)?;
for ext in &target_extents {
self.free_blocks_fsb(ext.startblock, ext.blockcount)?;
}
}
self.free_inode(target_ino)?;
let ino_off = self.ino_byte_offset(target_ino)?;
let zero = vec![0u8; XFS_INODESIZE as usize];
dev.write_at(ino_off, &zero)?;
let new_entries: Vec<(String, u64, u8)> = existing
.into_iter()
.filter(|e| e.name != "." && e.name != ".." && e.name != name)
.map(|e| (e.name, e.inumber, e.ftype))
.collect();
let parent_parent = existing_parent(&block, self.sb.is_v5())?;
let uuid = self.uuid_for_writes();
let basic_blkno = phys_byte / 512;
let new_block = encode_v5_block_dir(
dir_block_size,
parent_ino,
parent_parent,
&new_entries,
&uuid,
basic_blkno,
)?;
dev.write_at(phys_byte, &new_block)?;
let new_nlink = if target_ftype == XFS_DIR3_FT_DIR {
parent_core.nlink.saturating_sub(1)
} else {
parent_core.nlink
};
let (atime, mtime, ctime) = (parent_core.atime, parent_core.mtime, parent_core.ctime);
let builder = V3DinodeBuilder {
inodesize: XFS_INODESIZE as usize,
mode: parent_core.mode,
format: 2, uid: parent_core.uid,
gid: parent_core.gid,
nlink: new_nlink,
atime,
mtime,
ctime,
crtime: mtime,
size: dir_block_size as u64,
nblocks: 1,
extsize: 0,
nextents: 1,
forkoff: 0,
aformat: 2,
flags: parent_core.flags,
generation: parent_core.generation,
di_ino: parent_ino,
uuid,
};
let mut lit = Vec::with_capacity(16);
lit.extend_from_slice(&parent_self_extent.encode());
self.write_inode(dev, parent_ino, builder, &lit)?;
Ok(target_ino)
}
pub fn add_xattr(
&mut self,
dev: &mut dyn BlockDevice,
ino: u64,
name: &str,
value: &[u8],
) -> Result<()> {
if name.is_empty() {
return Err(crate::Error::InvalidArgument(
"xfs: empty xattr name".into(),
));
}
let (ino_buf, core) = self.read_inode(dev, ino)?;
let mut current: Vec<(String, Vec<u8>)> = self
.read_xattrs_from_core(&ino_buf, &core)?
.into_iter()
.collect();
if let Some(slot) = current.iter_mut().find(|(n, _)| n == name) {
slot.1 = value.to_vec();
} else {
current.push((name.to_string(), value.to_vec()));
}
let encoded = super::xattr::encode_shortform(¤t)?;
let lit_size = (XFS_INODESIZE as usize) - 176;
let data_fork_bytes = match core.format {
super::inode::DiFormat::Local => core.size as usize,
super::inode::DiFormat::Dev => 8,
super::inode::DiFormat::Extents => (core.nextents as usize) * 16,
super::inode::DiFormat::Btree => {
return Err(crate::Error::Unsupported(
"xfs: add_xattr to BTREE-format inode not supported".into(),
));
}
super::inode::DiFormat::Unknown(b) => {
return Err(crate::Error::Unsupported(format!(
"xfs: add_xattr to inode with unknown di_format {b}"
)));
}
};
let data_fork_words = data_fork_bytes.div_ceil(8);
let attr_words = encoded.len().div_ceil(8);
let min_forkoff = data_fork_words.max(1);
let max_forkoff = (lit_size / 8).saturating_sub(attr_words);
if min_forkoff > max_forkoff {
return Err(crate::Error::Unsupported(format!(
"xfs: shortform xattrs ({} bytes) don't fit in inode literal area",
encoded.len()
)));
}
let forkoff = min_forkoff as u8;
let attr_off = (forkoff as usize) * 8;
if attr_off + encoded.len() > lit_size {
return Err(crate::Error::Unsupported(format!(
"xfs: shortform xattrs at forkoff={forkoff} overrun literal area"
)));
}
let data_fork_slice = &ino_buf[176..176 + data_fork_bytes];
let builder = V3DinodeBuilder {
inodesize: XFS_INODESIZE as usize,
mode: core.mode,
format: match core.format {
super::inode::DiFormat::Local => 1,
super::inode::DiFormat::Dev => 0,
super::inode::DiFormat::Extents => 2,
super::inode::DiFormat::Btree => 3,
super::inode::DiFormat::Unknown(b) => b,
},
uid: core.uid,
gid: core.gid,
nlink: core.nlink,
atime: core.atime,
mtime: core.mtime,
ctime: core.ctime,
crtime: core.mtime,
size: core.size,
nblocks: core.nblocks,
extsize: 0,
nextents: core.nextents,
forkoff,
aformat: 1, flags: core.flags,
generation: core.generation,
di_ino: ino,
uuid: self.uuid_for_writes(),
};
let mut buf = builder.build();
buf[176..176 + data_fork_bytes].copy_from_slice(data_fork_slice);
buf[176 + attr_off..176 + attr_off + encoded.len()].copy_from_slice(&encoded);
stamp_v3_inode_crc(&mut buf);
let ino_off = self.ino_byte_offset(ino)?;
dev.write_at(ino_off, &buf)?;
Ok(())
}
pub fn read_xattrs(
&self,
dev: &mut dyn BlockDevice,
ino: u64,
) -> Result<std::collections::HashMap<String, Vec<u8>>> {
let (buf, core) = self.read_inode(dev, ino)?;
self.read_xattrs_from_core(&buf, &core)
}
fn read_xattrs_from_core(
&self,
ino_buf: &[u8],
core: &super::inode::DinodeCore,
) -> Result<std::collections::HashMap<String, Vec<u8>>> {
if core.forkoff == 0 {
return Ok(std::collections::HashMap::new());
}
let attr_off = (core.forkoff as usize) * 8;
let lit_start = core.literal_offset;
let attr_start = lit_start + attr_off;
let inodesize = self.sb.inodesize as usize;
if attr_start >= inodesize {
return Ok(std::collections::HashMap::new());
}
let attr_buf = &ino_buf[attr_start..inodesize.min(ino_buf.len())];
let aformat = if ino_buf.len() >= 84 { ino_buf[83] } else { 0 };
match aformat {
1 => super::xattr::decode_shortform(attr_buf),
0 => Ok(std::collections::HashMap::new()),
2 => Err(crate::Error::Unsupported(
"xfs: extents-format attribute fork not supported on read".into(),
)),
3 => Err(crate::Error::Unsupported(
"xfs: btree-format attribute fork not supported on read".into(),
)),
other => Err(crate::Error::Unsupported(format!(
"xfs: unknown attribute-fork format {other}"
))),
}
}
pub fn flush_writes(&mut self, dev: &mut dyn BlockDevice) -> Result<()> {
let agblocks = self.sb.agblocks;
let total_blocks = self.sb.dblocks as u32;
let uuid = self.uuid_for_writes();
let ws = self
.write_state
.as_ref()
.ok_or_else(|| {
crate::Error::InvalidArgument(
"xfs: flush_writes called before begin_writes()".into(),
)
})?
.clone();
let bs = XFS_BLOCKSIZE as u64;
let agcount = ws.ags.len() as u32;
let mut total_free_blocks_u64: u64 = 0;
for (ag_idx, ag_state) in ws.ags.iter().enumerate() {
let ag = ag_idx as u32;
let ag_byte = (ag as u64) * (agblocks as u64) * bs;
let this_ag_blocks = if ag == agcount - 1 {
total_blocks.saturating_sub(ag * agblocks).max(1)
} else {
agblocks
};
let mut extents: Vec<(u32, u32)> = ag_state.freed_extents.clone();
let tail_free = this_ag_blocks.saturating_sub(ag_state.next_agblock);
if tail_free > 0 {
extents.push((ag_state.next_agblock, tail_free));
}
extents.sort_by_key(|(s, _)| *s);
let mut coalesced: Vec<(u32, u32)> = Vec::with_capacity(extents.len());
for (s, c) in extents {
if let Some((ls, lc)) = coalesced.last_mut() {
if *ls + *lc == s {
*lc += c;
continue;
}
}
coalesced.push((s, c));
}
let total_free_in_ag: u32 = coalesced.iter().map(|(_, c)| *c).sum();
let longest = coalesced.iter().map(|(_, c)| *c).max().unwrap_or(0);
total_free_blocks_u64 += total_free_in_ag as u64;
let mut bno = vec![0u8; XFS_BLOCKSIZE as usize];
write_btree_header_for_ag(
&mut bno,
XFS_ABTB_CRC_MAGIC,
0,
coalesced.len() as u16,
&uuid,
ag,
agblocks,
4,
);
for (i, (s, c)) in coalesced.iter().enumerate() {
let off = XFS_BTREE_SBLOCK_V5_SIZE + i * 8;
if off + 8 > bno.len() {
return Err(crate::Error::Unsupported(
"xfs: too many free-space extents for a single-leaf BNO/CNT".into(),
));
}
bno[off..off + 4].copy_from_slice(&s.to_be_bytes());
bno[off + 4..off + 8].copy_from_slice(&c.to_be_bytes());
}
stamp_v5_btree_block_crc(&mut bno);
dev.write_at(ag_byte + 4 * bs, &bno)?;
let mut cnt_sorted = coalesced.clone();
cnt_sorted.sort_by_key(|(_, c)| *c);
let mut cnt = vec![0u8; XFS_BLOCKSIZE as usize];
write_btree_header_for_ag(
&mut cnt,
XFS_ABTC_CRC_MAGIC,
0,
cnt_sorted.len() as u16,
&uuid,
ag,
agblocks,
5,
);
for (i, (s, c)) in cnt_sorted.iter().enumerate() {
let off = XFS_BTREE_SBLOCK_V5_SIZE + i * 8;
cnt[off..off + 4].copy_from_slice(&s.to_be_bytes());
cnt[off + 4..off + 8].copy_from_slice(&c.to_be_bytes());
}
stamp_v5_btree_block_crc(&mut cnt);
dev.write_at(ag_byte + 5 * bs, &cnt)?;
let mut inobt = vec![0u8; XFS_BLOCKSIZE as usize];
write_btree_header_for_ag(
&mut inobt,
XFS_IBT_CRC_MAGIC,
0,
ag_state.chunks.len() as u16,
&uuid,
ag,
agblocks,
6,
);
for (i, chunk) in ag_state.chunks.iter().enumerate() {
let off = XFS_BTREE_SBLOCK_V5_SIZE + i * 16;
if off + 16 > inobt.len() {
return Err(crate::Error::Unsupported(
"xfs: too many inode chunks for a single-leaf INOBT".into(),
));
}
inobt[off..off + 4].copy_from_slice(&chunk.startino_ag.to_be_bytes());
inobt[off + 4..off + 8].copy_from_slice(&chunk.freecount().to_be_bytes());
inobt[off + 8..off + 16].copy_from_slice(&chunk.ir_free.to_be_bytes());
}
stamp_v5_btree_block_crc(&mut inobt);
dev.write_at(ag_byte + 6 * bs, &inobt)?;
let sect = super::format::XFS_SECTSIZE as u64;
let mut agf = vec![0u8; sect as usize];
agf[0..4].copy_from_slice(&super::format::XFS_AGF_MAGIC.to_be_bytes());
agf[4..8].copy_from_slice(&super::format::XFS_AGF_VERSION.to_be_bytes());
agf[8..12].copy_from_slice(&ag.to_be_bytes());
agf[12..16].copy_from_slice(&this_ag_blocks.to_be_bytes());
agf[16..20].copy_from_slice(&4u32.to_be_bytes()); agf[20..24].copy_from_slice(&5u32.to_be_bytes()); agf[24..28].copy_from_slice(&0u32.to_be_bytes()); agf[28..32].copy_from_slice(&1u32.to_be_bytes());
agf[32..36].copy_from_slice(&1u32.to_be_bytes());
agf[36..40].copy_from_slice(&0u32.to_be_bytes()); agf[40..44].copy_from_slice(&0u32.to_be_bytes());
agf[44..48].copy_from_slice(&0u32.to_be_bytes());
agf[48..52].copy_from_slice(&0u32.to_be_bytes());
agf[52..56].copy_from_slice(&total_free_in_ag.to_be_bytes());
agf[56..60].copy_from_slice(&longest.to_be_bytes());
agf[60..64].copy_from_slice(&0u32.to_be_bytes());
agf[64..80].copy_from_slice(&uuid);
super::format::stamp_v5_agf_crc(&mut agf);
dev.write_at(ag_byte + sect, &agf)?;
let mut agi = vec![0u8; sect as usize];
agi[0..4].copy_from_slice(&super::format::XFS_AGI_MAGIC.to_be_bytes());
agi[4..8].copy_from_slice(&super::format::XFS_AGI_VERSION.to_be_bytes());
agi[8..12].copy_from_slice(&ag.to_be_bytes());
agi[12..16].copy_from_slice(&this_ag_blocks.to_be_bytes());
let agi_count = ag_state.usedcount_inodes() + ag_state.freecount_inodes();
agi[16..20].copy_from_slice(&agi_count.to_be_bytes());
agi[20..24].copy_from_slice(&6u32.to_be_bytes()); agi[24..28].copy_from_slice(&1u32.to_be_bytes()); agi[28..32].copy_from_slice(&ag_state.freecount_inodes().to_be_bytes());
if let Some(last) = ag_state.chunks.last() {
agi[32..36].copy_from_slice(&last.startino_ag.to_be_bytes());
} else {
agi[32..36].copy_from_slice(&u32::MAX.to_be_bytes());
}
agi[36..40].copy_from_slice(&u32::MAX.to_be_bytes()); for i in 0..64 {
let off = 40 + i * 4;
agi[off..off + 4].copy_from_slice(&u32::MAX.to_be_bytes());
}
agi[296..312].copy_from_slice(&uuid);
agi[328..332].copy_from_slice(&0u32.to_be_bytes());
agi[332..336].copy_from_slice(&0u32.to_be_bytes());
super::format::stamp_v5_agi_crc(&mut agi);
dev.write_at(ag_byte + 2 * sect, &agi)?;
}
let mut sb = vec![0u8; XFS_BLOCKSIZE as usize];
dev.read_at(0, &mut sb)?;
let chunk_total: u64 = ws.ags.iter().map(|a| (a.chunks.len() as u64) * 64).sum();
sb[128..136].copy_from_slice(&chunk_total.to_be_bytes());
sb[136..144].copy_from_slice(&ws.inodes_free.to_be_bytes());
sb[144..152].copy_from_slice(&total_free_blocks_u64.to_be_bytes());
super::format::stamp_v5_superblock_crc(&mut sb);
dev.write_at(0, &sb)?;
dev.sync()?;
Ok(())
}
pub fn add_file_path<R: Read>(
&mut self,
dev: &mut dyn BlockDevice,
path: &str,
meta: EntryMeta,
size: u64,
src: &mut R,
) -> Result<u64> {
let (parent_ino, name) = self.split_path_for_create(dev, path)?;
self.add_file(dev, parent_ino, &name, meta, size, src)
}
pub fn add_dir_path(
&mut self,
dev: &mut dyn BlockDevice,
path: &str,
meta: EntryMeta,
) -> Result<u64> {
let (parent_ino, name) = self.split_path_for_create(dev, path)?;
self.add_dir(dev, parent_ino, &name, meta)
}
pub fn add_symlink_path(
&mut self,
dev: &mut dyn BlockDevice,
path: &str,
target: &str,
meta: EntryMeta,
) -> Result<u64> {
let (parent_ino, name) = self.split_path_for_create(dev, path)?;
self.add_symlink(dev, parent_ino, &name, target, meta)
}
pub fn add_device_path(
&mut self,
dev: &mut dyn BlockDevice,
path: &str,
kind: DeviceKind,
major: u32,
minor: u32,
meta: EntryMeta,
) -> Result<u64> {
let (parent_ino, name) = self.split_path_for_create(dev, path)?;
self.add_device(dev, parent_ino, &name, kind, major, minor, meta)
}
pub fn remove_path(&mut self, dev: &mut dyn BlockDevice, path: &str) -> Result<u64> {
let (parent_ino, name) = self.split_path_for_create(dev, path)?;
self.remove(dev, parent_ino, &name)
}
fn split_path_for_create(
&self,
dev: &mut dyn BlockDevice,
path: &str,
) -> Result<(u64, String)> {
let trimmed = path.trim_start_matches('/');
if trimmed.is_empty() {
return Err(crate::Error::InvalidArgument(
"xfs: cannot create entry at root".into(),
));
}
let (parent, leaf) = match trimmed.rfind('/') {
None => ("/", trimmed),
Some(i) => (&path[..=i], &trimmed[i + 1..]),
};
let parent_ino = self.lookup_path_ino(dev, parent)?;
Ok((parent_ino, leaf.to_string()))
}
pub fn lookup_path_ino(&self, dev: &mut dyn BlockDevice, path: &str) -> Result<u64> {
let (ino, _, _) = self.resolve_path(dev, path)?;
Ok(ino)
}
}
#[allow(dead_code)]
fn write_btree_header(
buf: &mut [u8],
magic: u32,
level: u16,
numrecs: u16,
uuid: &[u8; 16],
blkno_ag: u32,
) {
write_btree_header_for_ag(buf, magic, level, numrecs, uuid, 0, 0, blkno_ag)
}
#[allow(clippy::too_many_arguments)]
fn write_btree_header_for_ag(
buf: &mut [u8],
magic: u32,
level: u16,
numrecs: u16,
uuid: &[u8; 16],
ag: u32,
agblocks: u32,
blkno_ag: u32,
) {
buf[0..4].copy_from_slice(&magic.to_be_bytes());
buf[4..6].copy_from_slice(&level.to_be_bytes());
buf[6..8].copy_from_slice(&numrecs.to_be_bytes());
buf[8..12].copy_from_slice(&u32::MAX.to_be_bytes());
buf[12..16].copy_from_slice(&u32::MAX.to_be_bytes());
let basic = ((ag as u64) * (agblocks as u64) + blkno_ag as u64) * (XFS_BLOCKSIZE as u64 / 512);
buf[16..24].copy_from_slice(&basic.to_be_bytes());
buf[32..48].copy_from_slice(uuid);
buf[48..52].copy_from_slice(&ag.to_be_bytes()); }
fn existing_parent(block: &[u8], is_v5: bool) -> Result<u64> {
let entries = super::dir::decode_block_dir(block, is_v5)?;
for e in &entries {
if e.name == ".." {
return Ok(e.inumber);
}
}
Err(crate::Error::InvalidImage(
"xfs: directory missing \"..\" entry".into(),
))
}
fn read_exact_or_eof<R: Read>(r: &mut R, buf: &mut [u8]) -> std::io::Result<usize> {
let mut total = 0;
while total < buf.len() {
let n = r.read(&mut buf[total..])?;
if n == 0 {
break;
}
total += n;
}
Ok(total).map_err(|e: std::convert::Infallible| match e {})
}
#[allow(dead_code)]
const _AG0_META: u32 = AG0_METADATA_BLOCKS;
#[cfg(test)]
mod tests {
use super::*;
use crate::block::MemoryBackend;
#[test]
fn write_state_initial_has_root_only() {
let ws = WriteState::initial_single_ag([0u8; 16]);
assert_eq!(ws.ags.len(), 1);
assert_eq!(ws.ags[0].chunks.len(), 1);
assert_eq!(ws.inodes_used, 3);
assert_eq!(ws.inodes_free, 61);
}
#[test]
fn add_file_then_list() {
let mut dev = MemoryBackend::new(64 * 1024 * 1024);
let opts = super::super::FormatOpts::default();
let mut xfs = super::super::format(&mut dev, &opts).unwrap();
xfs.begin_writes([0u8; 16]);
let rootino = xfs.superblock().rootino;
let mut src = std::io::Cursor::new(b"hello world".to_vec());
let _ino = xfs
.add_file(
&mut dev,
rootino,
"greet",
EntryMeta::default(),
11,
&mut src,
)
.unwrap();
xfs.flush_writes(&mut dev).unwrap();
let entries = xfs.list_path(&mut dev, "/").unwrap();
let user: Vec<&crate::fs::DirEntry> = entries
.iter()
.filter(|e| e.name != "." && e.name != "..")
.collect();
assert_eq!(user.len(), 1);
assert_eq!(user[0].name, "greet");
assert_eq!(user[0].kind, crate::fs::EntryKind::Regular);
let mut r = xfs.open_file_reader(&mut dev, "/greet").unwrap();
let mut out = Vec::new();
std::io::Read::read_to_end(&mut r, &mut out).unwrap();
assert_eq!(&out, b"hello world");
}
#[test]
fn add_dir_and_file_in_subdir() {
let mut dev = MemoryBackend::new(64 * 1024 * 1024);
let opts = super::super::FormatOpts::default();
let mut xfs = super::super::format(&mut dev, &opts).unwrap();
xfs.begin_writes([0u8; 16]);
let rootino = xfs.superblock().rootino;
let sub = xfs
.add_dir(&mut dev, rootino, "sub", EntryMeta::default())
.unwrap();
let mut src = std::io::Cursor::new(b"x".to_vec());
xfs.add_file(&mut dev, sub, "f", EntryMeta::default(), 1, &mut src)
.unwrap();
xfs.flush_writes(&mut dev).unwrap();
let entries = xfs.list_path(&mut dev, "/sub").unwrap();
let user: Vec<&crate::fs::DirEntry> = entries
.iter()
.filter(|e| e.name != "." && e.name != "..")
.collect();
assert_eq!(user.len(), 1);
assert_eq!(user[0].name, "f");
}
#[test]
fn add_symlink_inline() {
let mut dev = MemoryBackend::new(64 * 1024 * 1024);
let opts = super::super::FormatOpts::default();
let mut xfs = super::super::format(&mut dev, &opts).unwrap();
xfs.begin_writes([0u8; 16]);
let rootino = xfs.superblock().rootino;
xfs.add_symlink(
&mut dev,
rootino,
"lnk",
"/etc/hostname",
EntryMeta::default(),
)
.unwrap();
xfs.flush_writes(&mut dev).unwrap();
let target = xfs.read_symlink(&mut dev, "/lnk").unwrap();
assert_eq!(target, "/etc/hostname");
}
#[test]
fn remove_regular_file() {
let mut dev = MemoryBackend::new(64 * 1024 * 1024);
let opts = super::super::FormatOpts::default();
let mut xfs = super::super::format(&mut dev, &opts).unwrap();
xfs.begin_writes([0u8; 16]);
let rootino = xfs.superblock().rootino;
let mut src = std::io::Cursor::new(b"some data".to_vec());
let ino = xfs
.add_file(&mut dev, rootino, "tmp", EntryMeta::default(), 9, &mut src)
.unwrap();
let removed = xfs.remove(&mut dev, rootino, "tmp").unwrap();
assert_eq!(removed, ino);
xfs.flush_writes(&mut dev).unwrap();
let entries = xfs.list_path(&mut dev, "/").unwrap();
let user: Vec<&crate::fs::DirEntry> = entries
.iter()
.filter(|e| e.name != "." && e.name != "..")
.collect();
assert!(user.is_empty(), "expected dir to be empty, got {user:?}");
}
#[test]
fn remove_empty_dir() {
let mut dev = MemoryBackend::new(64 * 1024 * 1024);
let opts = super::super::FormatOpts::default();
let mut xfs = super::super::format(&mut dev, &opts).unwrap();
xfs.begin_writes([0u8; 16]);
let rootino = xfs.superblock().rootino;
xfs.add_dir(&mut dev, rootino, "sub", EntryMeta::default())
.unwrap();
xfs.remove(&mut dev, rootino, "sub").unwrap();
xfs.flush_writes(&mut dev).unwrap();
let entries = xfs.list_path(&mut dev, "/").unwrap();
let user: Vec<&crate::fs::DirEntry> = entries
.iter()
.filter(|e| e.name != "." && e.name != "..")
.collect();
assert!(user.is_empty());
}
#[test]
fn remove_nonempty_dir_fails() {
let mut dev = MemoryBackend::new(64 * 1024 * 1024);
let opts = super::super::FormatOpts::default();
let mut xfs = super::super::format(&mut dev, &opts).unwrap();
xfs.begin_writes([0u8; 16]);
let rootino = xfs.superblock().rootino;
let sub = xfs
.add_dir(&mut dev, rootino, "sub", EntryMeta::default())
.unwrap();
let mut src = std::io::Cursor::new(b"x".to_vec());
xfs.add_file(&mut dev, sub, "f", EntryMeta::default(), 1, &mut src)
.unwrap();
let r = xfs.remove(&mut dev, rootino, "sub");
assert!(matches!(r, Err(crate::Error::InvalidArgument(_))));
}
#[test]
fn add_xattr_round_trip() {
let mut dev = MemoryBackend::new(64 * 1024 * 1024);
let opts = super::super::FormatOpts::default();
let mut xfs = super::super::format(&mut dev, &opts).unwrap();
xfs.begin_writes([0u8; 16]);
let rootino = xfs.superblock().rootino;
let mut src = std::io::Cursor::new(b"hello".to_vec());
let ino = xfs
.add_file(&mut dev, rootino, "f", EntryMeta::default(), 5, &mut src)
.unwrap();
xfs.add_xattr(&mut dev, ino, "user.mime_type", b"text/plain")
.unwrap();
xfs.add_xattr(&mut dev, ino, "trusted.foo", b"bar").unwrap();
let attrs = xfs.read_xattrs(&mut dev, ino).unwrap();
assert_eq!(attrs.get("user.mime_type"), Some(&b"text/plain".to_vec()));
assert_eq!(attrs.get("trusted.foo"), Some(&b"bar".to_vec()));
let mut reader = xfs.open_file_reader(&mut dev, "/f").unwrap();
let mut out = Vec::new();
std::io::Read::read_to_end(&mut reader, &mut out).unwrap();
assert_eq!(out, b"hello");
}
#[test]
fn xattr_overwrite_replaces() {
let mut dev = MemoryBackend::new(64 * 1024 * 1024);
let opts = super::super::FormatOpts::default();
let mut xfs = super::super::format(&mut dev, &opts).unwrap();
xfs.begin_writes([0u8; 16]);
let rootino = xfs.superblock().rootino;
let mut src = std::io::Cursor::new(b"x".to_vec());
let ino = xfs
.add_file(&mut dev, rootino, "g", EntryMeta::default(), 1, &mut src)
.unwrap();
xfs.add_xattr(&mut dev, ino, "user.k", b"v1").unwrap();
xfs.add_xattr(&mut dev, ino, "user.k", b"v2").unwrap();
let attrs = xfs.read_xattrs(&mut dev, ino).unwrap();
assert_eq!(attrs.get("user.k"), Some(&b"v2".to_vec()));
assert_eq!(attrs.len(), 1);
}
#[test]
#[ignore]
fn xfs_writes_image_for_external_tools() {
use crate::block::FileBackend;
let path = "/tmp/xfs_test.img";
let _ = std::fs::remove_file(path);
let f = std::fs::File::create(path).unwrap();
f.set_len(768 * 1024 * 1024).unwrap();
drop(f);
let mut dev = FileBackend::open(std::path::Path::new(path)).unwrap();
let opts = super::super::FormatOpts {
uuid: [0x11u8; 16],
..Default::default()
};
let mut xfs = super::super::format(&mut dev, &opts).unwrap();
xfs.begin_writes([0x11u8; 16]);
let rootino = xfs.superblock().rootino;
let mut src = std::io::Cursor::new(b"hello world".to_vec());
xfs.add_file(
&mut dev,
rootino,
"greet",
EntryMeta::default(),
11,
&mut src,
)
.unwrap();
let sub = xfs
.add_dir(&mut dev, rootino, "sub", EntryMeta::default())
.unwrap();
let mut src2 = std::io::Cursor::new(b"hi".to_vec());
let fino = xfs
.add_file(&mut dev, sub, "leaf", EntryMeta::default(), 2, &mut src2)
.unwrap();
xfs.add_xattr(&mut dev, fino, "user.k", b"v").unwrap();
xfs.flush_writes(&mut dev).unwrap();
}
#[test]
fn multi_ag_format_works() {
let size = 512 * 1024 * 1024;
let mut dev = MemoryBackend::new(size);
let opts = super::super::FormatOpts::default();
let xfs = super::super::format(&mut dev, &opts).unwrap();
assert!(
xfs.ag_count() >= 2,
"expected ≥2 AGs, got {}",
xfs.ag_count()
);
let mut xfs2 = super::super::Xfs::open(&mut dev).unwrap();
let entries = xfs2.list_path(&mut dev, "/").unwrap();
let user: Vec<_> = entries
.iter()
.filter(|e| e.name != "." && e.name != "..")
.collect();
assert!(user.is_empty());
xfs2.begin_writes([0u8; 16]);
let rootino = xfs2.superblock().rootino;
let mut src = std::io::Cursor::new(b"hi".to_vec());
xfs2.add_file(&mut dev, rootino, "ma", EntryMeta::default(), 2, &mut src)
.unwrap();
xfs2.flush_writes(&mut dev).unwrap();
let entries = xfs2.list_path(&mut dev, "/").unwrap();
assert!(entries.iter().any(|e| e.name == "ma"));
}
}