use crate::Result;
use crate::block::BlockDevice;
pub const BBSIZE: u64 = 512;
pub const XLOG_HEADER_MAGIC_NUM: u32 = 0xFEED_BABE;
pub const XLOG_VERSION_2: u32 = 2;
pub const XLOG_FMT_LINUX_LE: u32 = 1;
pub const XLOG_DEFAULT_H_SIZE: u32 = 32 * 1024;
pub const XLOG_UNMOUNT_TYPE: u16 = 0x556e;
pub const XFS_LOG_CLIENTID: u8 = 0xAA;
pub const XLOG_OP_UNMOUNT_FLAGS: u8 = 0x20;
pub const DEFAULT_LOG_BLOCKS: u32 = 512;
pub fn write_empty_log(
dev: &mut dyn BlockDevice,
log_byte_off: u64,
log_bytes: u64,
uuid: &[u8; 16],
) -> Result<()> {
if log_bytes < 2 * BBSIZE {
return Err(crate::Error::InvalidArgument(format!(
"xfs: log {log_bytes} bytes too small (need at least 1024)"
)));
}
let mut hdr_bb = vec![0u8; BBSIZE as usize];
let mut op_bb = vec![0u8; BBSIZE as usize];
hdr_bb[0..4].copy_from_slice(&XLOG_HEADER_MAGIC_NUM.to_be_bytes());
hdr_bb[4..8].copy_from_slice(&1u32.to_be_bytes()); hdr_bb[8..12].copy_from_slice(&XLOG_VERSION_2.to_be_bytes());
hdr_bb[12..16].copy_from_slice(&(BBSIZE as u32).to_be_bytes());
let h_lsn = 1u64 << 32;
hdr_bb[16..24].copy_from_slice(&h_lsn.to_be_bytes());
hdr_bb[24..32].copy_from_slice(&h_lsn.to_be_bytes());
hdr_bb[36..40].copy_from_slice(&u32::MAX.to_be_bytes()); hdr_bb[40..44].copy_from_slice(&1u32.to_be_bytes()); hdr_bb[44..48].copy_from_slice(&1u32.to_be_bytes());
hdr_bb[300..304].copy_from_slice(&XLOG_FMT_LINUX_LE.to_be_bytes());
hdr_bb[304..320].copy_from_slice(uuid);
hdr_bb[320..324].copy_from_slice(&XLOG_DEFAULT_H_SIZE.to_be_bytes());
dev.write_at(log_byte_off, &hdr_bb)?;
op_bb[0..4].copy_from_slice(&1u32.to_be_bytes()); op_bb[4..8].copy_from_slice(&8u32.to_be_bytes()); op_bb[8] = XFS_LOG_CLIENTID;
op_bb[9] = XLOG_OP_UNMOUNT_FLAGS;
op_bb[10..12].copy_from_slice(&0u16.to_be_bytes()); op_bb[12..14].copy_from_slice(&XLOG_UNMOUNT_TYPE.to_le_bytes());
dev.write_at(log_byte_off + BBSIZE, &op_bb)?;
Ok(())
}
pub const XFS_TRANSACTION_CLIENTID: u8 = 0x69;
pub const XLOG_START_TRANS: u8 = 0x01;
pub const XLOG_COMMIT_TRANS: u8 = 0x02;
pub const XFS_TRANS_HEADER_MAGIC: u32 = 0x5452_414e;
pub const XFS_LI_INODE: u16 = 0x123b;
pub const XFS_ILOG_CORE: u32 = 0x0001;
pub const XFS_ILOG_DEXT: u32 = 0x0004;
const OP_HDR_BYTES: usize = 12;
const TRANS_HDR_BYTES: usize = 16;
const INODE_LOG_FORMAT_BYTES: usize = 56;
#[allow(clippy::too_many_arguments)]
pub fn write_inode_update_transaction(
dev: &mut dyn BlockDevice,
log_byte_off: u64,
log_bytes: u64,
tid: u32,
cycle: u32,
inode_ino: u64,
inode_disk_byte: u64,
inode_bytes: &[u8],
uuid: &[u8; 16],
) -> Result<()> {
let payload_len =
4 * OP_HDR_BYTES + TRANS_HDR_BYTES + INODE_LOG_FORMAT_BYTES + inode_bytes.len();
let payload_bbs = (payload_len as u64).div_ceil(BBSIZE) as usize;
let payload_capacity = payload_bbs * BBSIZE as usize;
let log_bb_capacity = log_bytes / BBSIZE;
if log_bb_capacity < (1 + payload_bbs as u64) {
return Err(crate::Error::InvalidArgument(format!(
"xfs: log {log_bytes} bytes too small for transaction ({payload_bbs}+1 BBs needed)"
)));
}
if payload_bbs > 64 {
return Err(crate::Error::Unsupported(format!(
"xfs: log transaction payload {payload_bbs} BBs exceeds h_cycle_data[] slot count"
)));
}
let mut payload = vec![0u8; payload_capacity];
let mut cursor: usize = 0;
write_op_header(
&mut payload[cursor..cursor + OP_HDR_BYTES],
tid,
TRANS_HDR_BYTES as u32,
XLOG_START_TRANS,
);
cursor += OP_HDR_BYTES;
payload[cursor..cursor + 4].copy_from_slice(&XFS_TRANS_HEADER_MAGIC.to_le_bytes());
payload[cursor + 4..cursor + 8].copy_from_slice(&39u32.to_le_bytes());
payload[cursor + 8..cursor + 12].copy_from_slice(&tid.to_le_bytes());
payload[cursor + 12..cursor + 16].copy_from_slice(&1u32.to_le_bytes());
cursor += TRANS_HDR_BYTES;
write_op_header(
&mut payload[cursor..cursor + OP_HDR_BYTES],
tid,
INODE_LOG_FORMAT_BYTES as u32,
0,
);
cursor += OP_HDR_BYTES;
payload[cursor..cursor + 2].copy_from_slice(&XFS_LI_INODE.to_le_bytes());
payload[cursor + 2..cursor + 4].copy_from_slice(&2u16.to_le_bytes());
payload[cursor + 4..cursor + 8].copy_from_slice(&(XFS_ILOG_CORE | XFS_ILOG_DEXT).to_le_bytes());
payload[cursor + 8..cursor + 10].copy_from_slice(&0u16.to_le_bytes());
payload[cursor + 10..cursor + 12].copy_from_slice(&0u16.to_le_bytes());
payload[cursor + 12..cursor + 16].copy_from_slice(&0u32.to_le_bytes());
payload[cursor + 16..cursor + 24].copy_from_slice(&inode_ino.to_le_bytes());
payload[cursor + 24..cursor + 32].copy_from_slice(&inode_disk_byte.to_le_bytes());
payload[cursor + 32..cursor + 40].copy_from_slice(&0u64.to_le_bytes());
let blkno = (inode_disk_byte / 512) as i64;
payload[cursor + 40..cursor + 48].copy_from_slice(&blkno.to_le_bytes());
let len = inode_bytes.len().div_ceil(512) as i32;
payload[cursor + 48..cursor + 52].copy_from_slice(&len.to_le_bytes());
let boffset = (inode_disk_byte % 512) as i32;
payload[cursor + 52..cursor + 56].copy_from_slice(&boffset.to_le_bytes());
cursor += INODE_LOG_FORMAT_BYTES;
write_op_header(
&mut payload[cursor..cursor + OP_HDR_BYTES],
tid,
inode_bytes.len() as u32,
0,
);
cursor += OP_HDR_BYTES;
payload[cursor..cursor + inode_bytes.len()].copy_from_slice(inode_bytes);
cursor += inode_bytes.len();
write_op_header(
&mut payload[cursor..cursor + OP_HDR_BYTES],
tid,
0,
XLOG_COMMIT_TRANS,
);
cursor += OP_HDR_BYTES;
debug_assert_eq!(cursor, payload_len);
let mut h_cycle_data = [0u32; 64];
for (i, slot) in h_cycle_data.iter_mut().take(payload_bbs).enumerate() {
let off = i * BBSIZE as usize;
*slot = u32::from_le_bytes(payload[off..off + 4].try_into().unwrap());
payload[off..off + 4].copy_from_slice(&cycle.to_be_bytes());
}
let mut hdr_bb = vec![0u8; BBSIZE as usize];
hdr_bb[0..4].copy_from_slice(&XLOG_HEADER_MAGIC_NUM.to_be_bytes());
hdr_bb[4..8].copy_from_slice(&cycle.to_be_bytes());
hdr_bb[8..12].copy_from_slice(&XLOG_VERSION_2.to_be_bytes());
hdr_bb[12..16].copy_from_slice(&(payload_capacity as u32).to_be_bytes());
let h_lsn = (cycle as u64) << 32;
hdr_bb[16..24].copy_from_slice(&h_lsn.to_be_bytes());
hdr_bb[24..32].copy_from_slice(&h_lsn.to_be_bytes());
hdr_bb[36..40].copy_from_slice(&u32::MAX.to_be_bytes()); hdr_bb[40..44].copy_from_slice(&4u32.to_be_bytes()); for (i, v) in h_cycle_data.iter().enumerate() {
let off = 44 + i * 4;
hdr_bb[off..off + 4].copy_from_slice(&v.to_be_bytes());
}
hdr_bb[300..304].copy_from_slice(&XLOG_FMT_LINUX_LE.to_be_bytes());
hdr_bb[304..320].copy_from_slice(uuid);
hdr_bb[320..324].copy_from_slice(&XLOG_DEFAULT_H_SIZE.to_be_bytes());
dev.write_at(log_byte_off, &hdr_bb)?;
dev.write_at(log_byte_off + BBSIZE, &payload)?;
Ok(())
}
fn write_op_header(buf: &mut [u8], oh_tid: u32, oh_len: u32, oh_flags: u8) {
buf[0..4].copy_from_slice(&oh_tid.to_be_bytes());
buf[4..8].copy_from_slice(&oh_len.to_be_bytes());
buf[8] = XFS_TRANSACTION_CLIENTID;
buf[9] = oh_flags;
buf[10..12].copy_from_slice(&0u16.to_be_bytes());
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReplayOutcome {
AlreadyClean,
Replayed,
PartialDiscarded,
}
pub fn replay_log(
dev: &mut dyn BlockDevice,
log_byte_off: u64,
log_bytes: u64,
uuid: &[u8; 16],
) -> Result<ReplayOutcome> {
if log_bytes < 2 * BBSIZE {
return Err(crate::Error::InvalidArgument(format!(
"xfs: log {log_bytes} bytes too small for replay scan"
)));
}
let mut hdr = vec![0u8; BBSIZE as usize];
dev.read_at(log_byte_off, &mut hdr)?;
let magic = u32::from_be_bytes(hdr[0..4].try_into().unwrap());
if magic != XLOG_HEADER_MAGIC_NUM {
return Err(crate::Error::InvalidImage(format!(
"xfs: log header magic {magic:#010x} != {XLOG_HEADER_MAGIC_NUM:#010x}"
)));
}
let num_logops = u32::from_be_bytes(hdr[40..44].try_into().unwrap());
let h_len = u32::from_be_bytes(hdr[12..16].try_into().unwrap());
if num_logops == 1 {
let mut bb1 = vec![0u8; BBSIZE as usize];
dev.read_at(log_byte_off + BBSIZE, &mut bb1)?;
if bb1[8] == XFS_LOG_CLIENTID && (bb1[9] & XLOG_OP_UNMOUNT_FLAGS) != 0 {
return Ok(ReplayOutcome::AlreadyClean);
}
return Err(crate::Error::InvalidImage(
"xfs: log num_logops==1 but op is not unmount".into(),
));
}
if num_logops != 4 {
return Err(crate::Error::Unsupported(format!(
"xfs: log replay only handles 4-op inode-update records (found {num_logops})"
)));
}
let h_len = h_len as usize;
if h_len % BBSIZE as usize != 0 || h_len == 0 {
return Err(crate::Error::Unsupported(format!(
"xfs: log replay only handles BB-aligned records (h_len = {h_len})"
)));
}
let payload_bbs = h_len / BBSIZE as usize;
if payload_bbs > 64 {
return Err(crate::Error::Unsupported(format!(
"xfs: log replay h_len {h_len} exceeds h_cycle_data slot count"
)));
}
if log_bytes / BBSIZE < (1 + payload_bbs as u64) {
return Err(crate::Error::InvalidImage(format!(
"xfs: log {log_bytes} bytes too small for record ({payload_bbs}+1 BBs)"
)));
}
let mut payload = vec![0u8; h_len];
dev.read_at(log_byte_off + BBSIZE, &mut payload)?;
for i in 0..payload_bbs {
let off = i * BBSIZE as usize;
let saved = u32::from_be_bytes(hdr[44 + i * 4..48 + i * 4].try_into().unwrap());
payload[off..off + 4].copy_from_slice(&saved.to_le_bytes());
}
let mut cur: usize = 0;
let (oh_len1, oh_flags1) = read_op_header(&payload[cur..])?;
if (oh_flags1 & XLOG_START_TRANS) == 0 {
return Err(crate::Error::InvalidImage(format!(
"xfs: log op 1 missing XLOG_START_TRANS flag (flags={oh_flags1:#x})"
)));
}
if oh_len1 as usize != TRANS_HDR_BYTES {
return Err(crate::Error::InvalidImage(format!(
"xfs: log op 1 has oh_len {oh_len1} != {TRANS_HDR_BYTES}"
)));
}
let trans_payload = &payload[cur + OP_HDR_BYTES..cur + OP_HDR_BYTES + TRANS_HDR_BYTES];
let th_magic = u32::from_le_bytes(trans_payload[0..4].try_into().unwrap());
if th_magic != XFS_TRANS_HEADER_MAGIC {
return Err(crate::Error::InvalidImage(format!(
"xfs: log trans header magic {th_magic:#010x} != {XFS_TRANS_HEADER_MAGIC:#010x}"
)));
}
cur += OP_HDR_BYTES + TRANS_HDR_BYTES;
let (oh_len2, _oh_flags2) = read_op_header(&payload[cur..])?;
if oh_len2 as usize != INODE_LOG_FORMAT_BYTES {
return Err(crate::Error::InvalidImage(format!(
"xfs: log op 2 has oh_len {oh_len2} != {INODE_LOG_FORMAT_BYTES}"
)));
}
let ilfmt = &payload[cur + OP_HDR_BYTES..cur + OP_HDR_BYTES + INODE_LOG_FORMAT_BYTES];
let ilf_type = u16::from_le_bytes(ilfmt[0..2].try_into().unwrap());
if ilf_type != XFS_LI_INODE {
return Err(crate::Error::InvalidImage(format!(
"xfs: log inode item magic {ilf_type:#06x} != {XFS_LI_INODE:#06x}"
)));
}
let inode_disk_byte = u64::from_le_bytes(ilfmt[24..32].try_into().unwrap());
cur += OP_HDR_BYTES + INODE_LOG_FORMAT_BYTES;
let (oh_len3, _oh_flags3) = read_op_header(&payload[cur..])?;
let inode_len = oh_len3 as usize;
if cur + OP_HDR_BYTES + inode_len > payload.len() {
return Err(crate::Error::InvalidImage(
"xfs: log inode payload runs past record end".into(),
));
}
let inode_bytes = payload[cur + OP_HDR_BYTES..cur + OP_HDR_BYTES + inode_len].to_vec();
cur += OP_HDR_BYTES + inode_len;
let (oh_len4, oh_flags4) = read_op_header(&payload[cur..])?;
if (oh_flags4 & XLOG_COMMIT_TRANS) == 0 {
write_empty_log(dev, log_byte_off, log_bytes, uuid)?;
return Ok(ReplayOutcome::PartialDiscarded);
}
if oh_len4 != 0 {
return Err(crate::Error::InvalidImage(format!(
"xfs: log commit op has oh_len {oh_len4} != 0"
)));
}
dev.write_at(inode_disk_byte, &inode_bytes)?;
write_empty_log(dev, log_byte_off, log_bytes, uuid)?;
Ok(ReplayOutcome::Replayed)
}
fn read_op_header(buf: &[u8]) -> Result<(u32, u8)> {
if buf.len() < OP_HDR_BYTES {
return Err(crate::Error::InvalidImage(
"xfs: log op header truncated".into(),
));
}
let oh_len = u32::from_be_bytes(buf[4..8].try_into().unwrap());
let oh_flags = buf[9];
Ok((oh_len, oh_flags))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::block::MemoryBackend;
#[test]
fn write_empty_log_smoke() {
let mut dev = MemoryBackend::new(1024 * 1024);
let uuid = [0xCDu8; 16];
write_empty_log(&mut dev, 4096, 256 * 1024, &uuid).unwrap();
let mut bb0 = vec![0u8; 512];
dev.read_at(4096, &mut bb0).unwrap();
assert_eq!(
u32::from_be_bytes(bb0[0..4].try_into().unwrap()),
XLOG_HEADER_MAGIC_NUM
);
assert_eq!(u32::from_be_bytes(bb0[4..8].try_into().unwrap()), 1);
assert_eq!(u32::from_be_bytes(bb0[8..12].try_into().unwrap()), 2);
assert_eq!(&bb0[304..320], &uuid);
}
#[test]
fn write_empty_log_rejects_tiny() {
let mut dev = MemoryBackend::new(4096);
let r = write_empty_log(&mut dev, 0, BBSIZE, &[0u8; 16]);
assert!(r.is_err());
}
}