use crate::error::{Error, Result};
use crate::jbd2::{JBD2_COMMIT_BLOCK, JBD2_DESCRIPTOR_BLOCK, JBD2_MAGIC_NUMBER, JBD2_REVOKE_BLOCK};
use crate::journal::{TAG_LAST, TAG_SAME_UUID};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct JournaledBlock {
pub fs_block: u64,
pub bytes: Vec<u8>,
}
#[derive(Debug, Clone)]
pub struct Transaction {
pub sequence: u32,
pub block_size: u32,
pub uses_64bit: bool,
pub uses_csum_v3: bool,
pub writes: Vec<JournaledBlock>,
pub revokes: Vec<u64>,
}
impl Transaction {
pub fn begin(sequence: u32, block_size: u32, uses_64bit: bool, uses_csum_v3: bool) -> Self {
Self {
sequence,
block_size,
uses_64bit,
uses_csum_v3,
writes: Vec::new(),
revokes: Vec::new(),
}
}
pub fn add_write(&mut self, fs_block: u64, bytes: Vec<u8>) -> Result<()> {
if bytes.len() != self.block_size as usize {
return Err(Error::Corrupt("add_write: bytes size != block_size"));
}
self.writes.push(JournaledBlock { fs_block, bytes });
Ok(())
}
pub fn add_revoke(&mut self, fs_block: u64) {
self.revokes.push(fs_block);
}
pub fn commit(&self) -> Result<Vec<Vec<u8>>> {
let mut out = Vec::new();
if !self.writes.is_empty() {
out.push(self.build_descriptor_block()?);
for w in &self.writes {
out.push(w.bytes.clone());
}
}
if !self.revokes.is_empty() {
out.push(self.build_revoke_block()?);
}
out.push(self.build_commit_block()?);
Ok(out)
}
fn build_descriptor_block(&self) -> Result<Vec<u8>> {
let mut blk = vec![0u8; self.block_size as usize];
self.write_header(&mut blk, JBD2_DESCRIPTOR_BLOCK);
let tag_size = if self.uses_csum_v3 {
if self.uses_64bit {
16
} else {
12
}
} else if self.uses_64bit {
12
} else {
8
};
let mut pos = 12usize;
let last_idx = self.writes.len().saturating_sub(1);
for (i, w) in self.writes.iter().enumerate() {
let mut flags: u32 = if i == last_idx { TAG_LAST } else { 0 };
flags |= TAG_SAME_UUID;
if pos + tag_size > blk.len() {
return Err(Error::Corrupt("descriptor block overflow (too many tags)"));
}
let blocknr_lo = (w.fs_block & 0xFFFF_FFFF) as u32;
blk[pos..pos + 4].copy_from_slice(&blocknr_lo.to_be_bytes());
if self.uses_csum_v3 {
blk[pos + 4..pos + 8].copy_from_slice(&flags.to_be_bytes());
if self.uses_64bit {
let hi = (w.fs_block >> 32) as u32;
blk[pos + 8..pos + 12].copy_from_slice(&hi.to_be_bytes());
}
} else {
blk[pos + 4..pos + 6].copy_from_slice(&0u16.to_be_bytes());
blk[pos + 6..pos + 8].copy_from_slice(&(flags as u16).to_be_bytes());
if self.uses_64bit {
let hi = (w.fs_block >> 32) as u32;
blk[pos + 8..pos + 12].copy_from_slice(&hi.to_be_bytes());
}
}
pos += tag_size;
}
Ok(blk)
}
fn build_revoke_block(&self) -> Result<Vec<u8>> {
let mut blk = vec![0u8; self.block_size as usize];
self.write_header(&mut blk, JBD2_REVOKE_BLOCK);
let record_size = if self.uses_64bit { 8 } else { 4 };
let records_bytes = self.revokes.len() * record_size;
let total_bytes = 16 + records_bytes; if total_bytes > blk.len() {
return Err(Error::Corrupt("revoke block overflow"));
}
blk[12..16].copy_from_slice(&(total_bytes as u32).to_be_bytes());
let mut pos = 16usize;
for &b in &self.revokes {
if self.uses_64bit {
blk[pos..pos + 8].copy_from_slice(&b.to_be_bytes());
} else {
blk[pos..pos + 4].copy_from_slice(&(b as u32).to_be_bytes());
}
pos += record_size;
}
Ok(blk)
}
fn build_commit_block(&self) -> Result<Vec<u8>> {
let mut blk = vec![0u8; self.block_size as usize];
self.write_header(&mut blk, JBD2_COMMIT_BLOCK);
Ok(blk)
}
fn write_header(&self, blk: &mut [u8], block_type: u32) {
blk[0..4].copy_from_slice(&JBD2_MAGIC_NUMBER.to_be_bytes());
blk[4..8].copy_from_slice(&block_type.to_be_bytes());
blk[8..12].copy_from_slice(&self.sequence.to_be_bytes());
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::jbd2::{JbdIncompat, JournalSuperblock, JBD2_SUPERBLOCK_V2};
use crate::journal::{ReplayEntry, ReplayPlan, RevokeEntry};
fn mk_tx(seq: u32) -> Transaction {
Transaction::begin(seq, 4096, false, false)
}
#[test]
fn empty_transaction_yields_only_commit() {
let tx = mk_tx(1);
let blocks = tx.commit().unwrap();
assert_eq!(blocks.len(), 1);
let magic = u32::from_be_bytes(blocks[0][0..4].try_into().unwrap());
let bt = u32::from_be_bytes(blocks[0][4..8].try_into().unwrap());
assert_eq!(magic, JBD2_MAGIC_NUMBER);
assert_eq!(bt, JBD2_COMMIT_BLOCK);
}
#[test]
fn two_block_txn_layout_is_desc_then_data_then_commit() {
let mut tx = mk_tx(42);
tx.add_write(100, vec![0xAA; 4096]).unwrap();
tx.add_write(200, vec![0xBB; 4096]).unwrap();
let blocks = tx.commit().unwrap();
assert_eq!(blocks.len(), 4); let bt0 = u32::from_be_bytes(blocks[0][4..8].try_into().unwrap());
assert_eq!(bt0, JBD2_DESCRIPTOR_BLOCK);
assert_eq!(blocks[1][0], 0xAA);
assert_eq!(blocks[2][0], 0xBB);
let bt3 = u32::from_be_bytes(blocks[3][4..8].try_into().unwrap());
assert_eq!(bt3, JBD2_COMMIT_BLOCK);
}
#[test]
fn descriptor_tags_are_big_endian_and_have_last_flag() {
let mut tx = mk_tx(7);
tx.add_write(0x1234_5678, vec![0u8; 4096]).unwrap();
let blocks = tx.commit().unwrap();
let desc = &blocks[0];
let blocknr_lo = u32::from_be_bytes(desc[12..16].try_into().unwrap());
assert_eq!(blocknr_lo, 0x1234_5678);
let flags_u16 = u16::from_be_bytes(desc[18..20].try_into().unwrap()) as u32;
assert_eq!(flags_u16 & TAG_LAST, TAG_LAST, "last tag must set TAG_LAST");
assert_eq!(
flags_u16 & TAG_SAME_UUID,
TAG_SAME_UUID,
"same_uuid expected"
);
}
#[test]
fn revoke_block_serialises_count_and_records() {
let mut tx = mk_tx(3);
tx.add_revoke(1000);
tx.add_revoke(2000);
let blocks = tx.commit().unwrap();
assert_eq!(blocks.len(), 2); let rev = &blocks[0];
let bt = u32::from_be_bytes(rev[4..8].try_into().unwrap());
assert_eq!(bt, JBD2_REVOKE_BLOCK);
let r_count = u32::from_be_bytes(rev[12..16].try_into().unwrap());
assert_eq!(r_count, 16 + 2 * 4);
let r0 = u32::from_be_bytes(rev[16..20].try_into().unwrap());
let r1 = u32::from_be_bytes(rev[20..24].try_into().unwrap());
assert_eq!(r0, 1000);
assert_eq!(r1, 2000);
}
#[test]
fn round_trip_through_walk_style_parsing() {
let mut tx = mk_tx(100);
tx.add_write(500, vec![0x11; 4096]).unwrap();
tx.add_write(501, vec![0x22; 4096]).unwrap();
tx.add_write(900, vec![0x33; 4096]).unwrap();
let blocks = tx.commit().unwrap();
let desc = &blocks[0];
assert_eq!(
u32::from_be_bytes(desc[4..8].try_into().unwrap()),
JBD2_DESCRIPTOR_BLOCK
);
let mut pos = 12usize;
let mut seen = Vec::new();
for _ in 0..3 {
let blocknr = u32::from_be_bytes(desc[pos..pos + 4].try_into().unwrap()) as u64;
let flags = u16::from_be_bytes(desc[pos + 6..pos + 8].try_into().unwrap()) as u32;
seen.push((blocknr, flags));
pos += 8;
}
assert_eq!(seen[0].0, 500);
assert_eq!(seen[1].0, 501);
assert_eq!(seen[2].0, 900);
assert_eq!(seen[0].1 & TAG_LAST, 0);
assert_eq!(seen[1].1 & TAG_LAST, 0);
assert_eq!(seen[2].1 & TAG_LAST, TAG_LAST);
}
#[test]
fn v3_tag_layout_32bit_flags() {
let mut tx = Transaction::begin(50, 4096, false, true);
tx.add_write(0xABCD_1234, vec![0u8; 4096]).unwrap();
let blocks = tx.commit().unwrap();
let desc = &blocks[0];
let blocknr = u32::from_be_bytes(desc[12..16].try_into().unwrap());
assert_eq!(blocknr, 0xABCD_1234);
let flags = u32::from_be_bytes(desc[16..20].try_into().unwrap());
assert_eq!(flags & TAG_LAST, TAG_LAST);
assert_eq!(flags & TAG_SAME_UUID, TAG_SAME_UUID);
}
#[test]
fn wrong_block_size_payload_is_rejected() {
let mut tx = mk_tx(1);
let err = tx.add_write(100, vec![0u8; 2048]).unwrap_err();
match err {
Error::Corrupt(msg) => assert!(msg.contains("size")),
_ => panic!(),
}
}
fn parse_descriptor_classical_no64(desc: &[u8], seq: u32) -> ReplayPlan {
let mut plan = ReplayPlan::default();
let mut pos = 12usize;
let mut tag_idx = 0u64;
loop {
if pos + 8 > desc.len() {
break;
}
let blocknr = u32::from_be_bytes(desc[pos..pos + 4].try_into().unwrap()) as u64;
let flags = u16::from_be_bytes(desc[pos + 6..pos + 8].try_into().unwrap()) as u32;
if blocknr == 0 && flags == 0 && tag_idx > 0 {
break;
}
plan.writes.push(ReplayEntry {
transaction: seq,
fs_block: blocknr,
journal_block: tag_idx + 1,
flags,
});
tag_idx += 1;
pos += 8;
if flags & TAG_LAST != 0 {
break;
}
}
plan
}
#[test]
fn parsed_descriptor_matches_original_tags() {
let mut tx = mk_tx(77);
tx.add_write(10, vec![0u8; 4096]).unwrap();
tx.add_write(20, vec![0u8; 4096]).unwrap();
tx.add_write(30, vec![0u8; 4096]).unwrap();
let blocks = tx.commit().unwrap();
let plan = parse_descriptor_classical_no64(&blocks[0], 77);
assert_eq!(plan.writes.len(), 3);
assert_eq!(plan.writes[0].fs_block, 10);
assert_eq!(plan.writes[1].fs_block, 20);
assert_eq!(plan.writes[2].fs_block, 30);
}
#[test]
fn jsb_constants_available() {
let _ = (
JournalSuperblock {
block_type: JBD2_SUPERBLOCK_V2,
header_sequence: 0,
block_size: 4096,
max_len: 1024,
first: 1,
sequence: 1,
start: 0,
errno: 0,
feature_compat: 0,
feature_incompat: JbdIncompat::REVOKE.bits(),
feature_ro_compat: 0,
uuid: [0; 16],
nr_users: 1,
checksum_type: 0,
num_fc_blocks: 0,
checksum: 0,
},
RevokeEntry {
transaction: 1,
fs_block: 0,
},
);
}
}