use crate::Result;
use crate::block::BlockDevice;
pub const JBD2_MAGIC: u32 = 0xC03B_3998;
pub const JBD2_DESCRIPTOR_BLOCK: u32 = 1;
pub const JBD2_COMMIT_BLOCK: u32 = 2;
pub const JBD2_SUPERBLOCK_V1: u32 = 3;
pub const JBD2_SUPERBLOCK_V2: u32 = 4;
pub const JBD2_REVOKE_BLOCK: u32 = 5;
pub const JBD2_FLAG_ESCAPE: u16 = 0x1;
pub const JBD2_FLAG_SAME_UUID: u16 = 0x2;
pub const JBD2_FLAG_LAST_TAG: u16 = 0x8;
pub const JSB_OFF_BLOCKSIZE: usize = 12;
pub const JSB_OFF_MAXLEN: usize = 16;
pub const JSB_OFF_FIRST: usize = 20;
pub const JSB_OFF_SEQUENCE: usize = 24;
pub const JSB_OFF_START: usize = 28;
pub const JSB_OFF_FEATURE_INCOMPAT: usize = 40;
pub const JSB_OFF_UUID: usize = 48;
pub const JBD2_FEATURE_INCOMPAT_64BIT: u32 = 0x0000_0002;
pub const JBD2_FEATURE_INCOMPAT_CSUM_V2: u32 = 0x0000_0008;
pub const JBD2_FEATURE_INCOMPAT_CSUM_V3: u32 = 0x0000_0010;
#[derive(Debug, Clone, Copy)]
pub struct JournalSuperblock {
pub blocksize: u32,
pub maxlen: u32,
pub first: u32,
pub sequence: u32,
pub start: u32,
pub feature_incompat: u32,
pub uuid: [u8; 16],
}
impl JournalSuperblock {
pub fn decode(buf: &[u8]) -> Result<Self> {
if buf.len() < 64 {
return Err(crate::Error::InvalidImage(
"ext: journal SB block shorter than 64 bytes".into(),
));
}
let magic = u32::from_be_bytes(buf[0..4].try_into().unwrap());
if magic != JBD2_MAGIC {
return Err(crate::Error::InvalidImage(format!(
"ext: bad JBD2 magic {magic:#010x} on journal SB block"
)));
}
let blocktype = u32::from_be_bytes(buf[4..8].try_into().unwrap());
if blocktype != JBD2_SUPERBLOCK_V1 && blocktype != JBD2_SUPERBLOCK_V2 {
return Err(crate::Error::InvalidImage(format!(
"ext: journal SB block has blocktype {blocktype} (expected v1=3 or v2=4)"
)));
}
let mut uuid = [0u8; 16];
uuid.copy_from_slice(&buf[JSB_OFF_UUID..JSB_OFF_UUID + 16]);
Ok(Self {
blocksize: u32::from_be_bytes(
buf[JSB_OFF_BLOCKSIZE..JSB_OFF_BLOCKSIZE + 4]
.try_into()
.unwrap(),
),
maxlen: u32::from_be_bytes(buf[JSB_OFF_MAXLEN..JSB_OFF_MAXLEN + 4].try_into().unwrap()),
first: u32::from_be_bytes(buf[JSB_OFF_FIRST..JSB_OFF_FIRST + 4].try_into().unwrap()),
sequence: u32::from_be_bytes(
buf[JSB_OFF_SEQUENCE..JSB_OFF_SEQUENCE + 4]
.try_into()
.unwrap(),
),
start: u32::from_be_bytes(buf[JSB_OFF_START..JSB_OFF_START + 4].try_into().unwrap()),
feature_incompat: u32::from_be_bytes(
buf[JSB_OFF_FEATURE_INCOMPAT..JSB_OFF_FEATURE_INCOMPAT + 4]
.try_into()
.unwrap(),
),
uuid,
})
}
}
pub fn encode_header(blocktype: u32, sequence: u32) -> [u8; 12] {
let mut out = [0u8; 12];
out[0..4].copy_from_slice(&JBD2_MAGIC.to_be_bytes());
out[4..8].copy_from_slice(&blocktype.to_be_bytes());
out[8..12].copy_from_slice(&sequence.to_be_bytes());
out
}
#[derive(Debug, Clone)]
pub struct JournalBlock {
pub fs_block: u32,
pub bytes: Vec<u8>,
}
pub fn encode_descriptor_block(
block_size: u32,
sequence: u32,
blocks: &[JournalBlock],
uuid: &[u8; 16],
is_first_descriptor: bool,
is_last_descriptor: bool,
) -> Vec<u8> {
let mut out = vec![0u8; block_size as usize];
out[..12].copy_from_slice(&encode_header(JBD2_DESCRIPTOR_BLOCK, sequence));
let mut off = 12usize;
for (i, jb) in blocks.iter().enumerate() {
let is_very_first_tag = is_first_descriptor && i == 0;
let is_very_last_tag = is_last_descriptor && i + 1 == blocks.len();
let mut flags: u16 = 0;
if !is_very_first_tag {
flags |= JBD2_FLAG_SAME_UUID;
}
if is_very_last_tag {
flags |= JBD2_FLAG_LAST_TAG;
}
out[off..off + 4].copy_from_slice(&jb.fs_block.to_be_bytes());
out[off + 4..off + 6].copy_from_slice(&0u16.to_be_bytes());
out[off + 6..off + 8].copy_from_slice(&flags.to_be_bytes());
off += 8;
if is_very_first_tag {
out[off..off + 16].copy_from_slice(uuid);
off += 16;
}
}
out
}
pub fn descriptor_tag_capacity(block_size: u32, is_first_descriptor: bool) -> usize {
let header = 12usize;
let uuid_overhead = if is_first_descriptor { 16 } else { 0 };
(block_size as usize - header - uuid_overhead) / 8
}
pub fn encode_commit_block(
block_size: u32,
sequence: u32,
commit_sec: u64,
commit_nsec: u32,
) -> Vec<u8> {
let mut out = vec![0u8; block_size as usize];
out[..12].copy_from_slice(&encode_header(JBD2_COMMIT_BLOCK, sequence));
out[48..56].copy_from_slice(&commit_sec.to_be_bytes());
out[56..60].copy_from_slice(&commit_nsec.to_be_bytes());
out
}
pub fn set_sequence(buf: &mut [u8], sequence: u32) {
buf[JSB_OFF_SEQUENCE..JSB_OFF_SEQUENCE + 4].copy_from_slice(&sequence.to_be_bytes());
}
pub fn set_start(buf: &mut [u8], start: u32) {
buf[JSB_OFF_START..JSB_OFF_START + 4].copy_from_slice(&start.to_be_bytes());
}
pub(crate) fn journal_tag_bytes(feature_incompat: u32) -> usize {
if feature_incompat & JBD2_FEATURE_INCOMPAT_CSUM_V3 != 0 {
return 16;
}
let csum_v2 = feature_incompat & JBD2_FEATURE_INCOMPAT_CSUM_V2 != 0;
let is_64bit = feature_incompat & JBD2_FEATURE_INCOMPAT_64BIT != 0;
8 + usize::from(csum_v2) * 2 + usize::from(is_64bit) * 4
}
pub(crate) fn descriptor_payload_len(block_size: u32, feature_incompat: u32) -> usize {
let tail = if feature_incompat & (JBD2_FEATURE_INCOMPAT_CSUM_V2 | JBD2_FEATURE_INCOMPAT_CSUM_V3)
!= 0
{
4
} else {
0
};
(block_size as usize).saturating_sub(tail)
}
pub fn decode_tag(buf: &[u8], feature_incompat: u32) -> Result<(u64, u16, usize)> {
let csum_v3 = feature_incompat & JBD2_FEATURE_INCOMPAT_CSUM_V3 != 0;
let is_64bit = feature_incompat & JBD2_FEATURE_INCOMPAT_64BIT != 0;
let tag_bytes = journal_tag_bytes(feature_incompat);
if buf.len() < tag_bytes {
return Err(crate::Error::InvalidImage(
"ext: journal descriptor tag past end of block".into(),
));
}
let block_lo = u32::from_be_bytes(buf[0..4].try_into().unwrap()) as u64;
let flags = if csum_v3 {
u32::from_be_bytes(buf[4..8].try_into().unwrap()) as u16
} else {
u16::from_be_bytes(buf[6..8].try_into().unwrap())
};
let block_hi = if is_64bit {
u32::from_be_bytes(buf[8..12].try_into().unwrap()) as u64
} else {
0
};
let size = tag_bytes
+ if flags & JBD2_FLAG_SAME_UUID == 0 {
16
} else {
0
};
if buf.len() < size {
return Err(crate::Error::InvalidImage(
"ext: journal descriptor tag uuid past end of block".into(),
));
}
Ok(((block_hi << 32) | block_lo, flags, size))
}
pub(crate) fn read_journal_block(
ext: &super::Ext,
dev: &mut dyn BlockDevice,
journal_inode: &super::Inode,
idx: u32,
) -> Result<Vec<u8>> {
let phys = ext.file_block(dev, journal_inode, idx)?;
if phys == 0 {
return Err(crate::Error::InvalidImage(format!(
"ext: journal block {idx} unmapped"
)));
}
let bs = ext.layout.block_size as usize;
let mut buf = vec![0u8; bs];
dev.read_at(phys as u64 * bs as u64, &mut buf)?;
Ok(buf)
}
pub(crate) fn write_journal_block(
ext: &super::Ext,
dev: &mut dyn BlockDevice,
journal_inode: &super::Inode,
idx: u32,
bytes: &[u8],
) -> Result<()> {
let phys = ext.file_block(dev, journal_inode, idx)?;
if phys == 0 {
return Err(crate::Error::InvalidImage(format!(
"ext: journal block {idx} unmapped"
)));
}
let bs = ext.layout.block_size as u64;
dev.write_at(phys as u64 * bs, bytes)?;
Ok(())
}
fn tid_geq(a: u32, b: u32) -> bool {
(a.wrapping_sub(b) as i32) >= 0
}
struct StagedBlock {
fs_block: u64,
journal_idx: u32,
flags: u16,
}
struct StagedTransaction {
tid: u32,
blocks: Vec<StagedBlock>,
}
pub(crate) fn replay_journal(ext: &super::Ext, dev: &mut dyn BlockDevice) -> Result<bool> {
let jino = ext.sb.journal_inum;
if jino == 0 {
return Ok(false);
}
let journal_inode = ext.read_inode(dev, jino)?;
let bs = ext.layout.block_size;
let jsb_buf = read_journal_block(ext, dev, &journal_inode, 0)?;
let jsb = JournalSuperblock::decode(&jsb_buf)?;
if jsb.start == 0 {
return Ok(false);
}
if jsb.blocksize != bs {
return Err(crate::Error::InvalidImage(format!(
"ext: journal blocksize {} != FS blocksize {bs}",
jsb.blocksize
)));
}
let mut idx = jsb.start;
let mut expected_tid = jsb.sequence;
let mut revoke_table: std::collections::HashMap<u64, u32> = std::collections::HashMap::new();
let mut staged: Vec<StagedTransaction> = Vec::new();
let ring_size = jsb.maxlen.saturating_sub(jsb.first).max(1) as u64;
let mut blocks_visited: u64 = 0;
'transactions: loop {
let tid = expected_tid;
let mut pending: Vec<StagedBlock> = Vec::new();
let mut pending_revokes: Vec<u64> = Vec::new();
loop {
blocks_visited += 1;
if blocks_visited > ring_size {
return Err(crate::Error::InvalidImage(
"ext4: journal replay exceeded ring size".into(),
));
}
let block = read_journal_block(ext, dev, &journal_inode, idx)?;
let magic = u32::from_be_bytes(block[0..4].try_into().unwrap());
if magic != JBD2_MAGIC {
break 'transactions;
}
let blocktype = u32::from_be_bytes(block[4..8].try_into().unwrap());
let sequence = u32::from_be_bytes(block[8..12].try_into().unwrap());
if sequence != tid {
break 'transactions;
}
idx = ring_next(idx, &jsb);
match blocktype {
JBD2_DESCRIPTOR_BLOCK => {
let (tags, _) = parse_descriptor_tags(&block, bs, jsb.feature_incompat)?;
for tag in tags {
blocks_visited += 1;
if blocks_visited > ring_size {
return Err(crate::Error::InvalidImage(
"ext4: journal replay exceeded ring size".into(),
));
}
pending.push(StagedBlock {
fs_block: tag.fs_block,
journal_idx: idx,
flags: tag.flags,
});
idx = ring_next(idx, &jsb);
}
}
JBD2_REVOKE_BLOCK => {
pending_revokes.extend(parse_revoke_records(&block, bs, jsb.feature_incompat)?);
}
JBD2_COMMIT_BLOCK => {
for b in pending_revokes {
revoke_table
.entry(b)
.and_modify(|t| {
if tid_geq(tid, *t) {
*t = tid;
}
})
.or_insert(tid);
}
staged.push(StagedTransaction {
tid,
blocks: pending,
});
expected_tid = expected_tid.wrapping_add(1);
break;
}
_ => break 'transactions,
}
}
}
let replayed = !staged.is_empty();
for txn in &staged {
for b in &txn.blocks {
if let Some(&revoked_at) = revoke_table.get(&b.fs_block)
&& tid_geq(revoked_at, txn.tid)
{
continue;
}
let mut payload = read_journal_block(ext, dev, &journal_inode, b.journal_idx)?;
if b.flags & JBD2_FLAG_ESCAPE != 0 {
payload[0..4].copy_from_slice(&JBD2_MAGIC.to_be_bytes());
}
dev.write_at(b.fs_block * bs as u64, &payload)?;
}
}
if replayed {
let mut jsb_new = jsb_buf.clone();
set_start(&mut jsb_new, 0);
set_sequence(&mut jsb_new, expected_tid);
write_journal_block(ext, dev, &journal_inode, 0, &jsb_new)?;
}
Ok(replayed)
}
pub(crate) fn ring_next(idx: u32, jsb: &JournalSuperblock) -> u32 {
let next = idx + 1;
if next >= jsb.maxlen { jsb.first } else { next }
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct ParsedTag {
pub fs_block: u64,
pub flags: u16,
}
pub(crate) fn parse_descriptor_tags(
buf: &[u8],
block_size: u32,
feature_incompat: u32,
) -> Result<(Vec<ParsedTag>, usize)> {
let mut out = Vec::new();
let mut off = 12usize;
let mut first = true;
let limit = descriptor_payload_len(block_size, feature_incompat).min(buf.len());
let tag_bytes = journal_tag_bytes(feature_incompat);
while off + tag_bytes <= limit {
let (fs_block, flags, sz) = decode_tag(&buf[off..], feature_incompat)?;
if fs_block == 0 && flags == 0 && first {
break;
}
out.push(ParsedTag { fs_block, flags });
off += sz;
first = false;
if flags & JBD2_FLAG_LAST_TAG != 0 {
break;
}
}
let count = out.len();
Ok((out, count))
}
pub(crate) fn parse_revoke_records(
buf: &[u8],
block_size: u32,
feature_incompat: u32,
) -> Result<Vec<u64>> {
if buf.len() < 16 {
return Err(crate::Error::InvalidImage(
"ext: journal revoke block shorter than header".into(),
));
}
let count = u32::from_be_bytes(buf[12..16].try_into().unwrap()) as usize;
let checksum_tail = if feature_incompat
& (JBD2_FEATURE_INCOMPAT_CSUM_V2 | JBD2_FEATURE_INCOMPAT_CSUM_V3)
!= 0
{
4
} else {
0
};
let limit = block_size as usize - checksum_tail;
if count < 16 || count > limit || count > buf.len() {
return Err(crate::Error::InvalidImage(format!(
"ext: journal revoke byte count {count} is out of bounds"
)));
}
let record_size = if feature_incompat & JBD2_FEATURE_INCOMPAT_64BIT != 0 {
8
} else {
4
};
if !(count - 16).is_multiple_of(record_size) {
return Err(crate::Error::InvalidImage(
"ext: journal revoke records are misaligned".into(),
));
}
let mut records = Vec::with_capacity((count - 16) / record_size);
for record in buf[16..count].chunks_exact(record_size) {
records.push(if record_size == 8 {
u64::from_be_bytes(record.try_into().unwrap())
} else {
u32::from_be_bytes(record.try_into().unwrap()) as u64
});
}
Ok(records)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn write_transaction(
ext: &super::Ext,
dev: &mut dyn BlockDevice,
journal_inode: &super::Inode,
jsb_buf: &mut [u8],
jsb: &JournalSuperblock,
start_idx: u32,
tid: u32,
blocks: &[JournalBlock],
commit_sec: u64,
commit_nsec: u32,
) -> Result<u32> {
let bs = ext.layout.block_size;
let first_cap = descriptor_tag_capacity(bs, true);
let next_cap = descriptor_tag_capacity(bs, false);
let n_descs = if blocks.len() <= first_cap {
1
} else {
1 + (blocks.len() - first_cap).div_ceil(next_cap)
};
let need = (n_descs + blocks.len() + 1) as u32;
let avail = jsb.maxlen.saturating_sub(jsb.first);
if need > avail {
return Err(crate::Error::Unsupported(format!(
"ext: journal too small ({} blocks, transaction needs {need})",
jsb.maxlen
)));
}
let mut idx = start_idx;
let mut chunk_start = 0usize;
let mut is_first_desc = true;
while chunk_start < blocks.len().max(1) {
let cap = if is_first_desc { first_cap } else { next_cap };
let chunk_end = (chunk_start + cap).min(blocks.len());
let chunk = if blocks.is_empty() {
&[][..]
} else {
&blocks[chunk_start..chunk_end]
};
let is_last_desc = chunk_end == blocks.len();
let desc = encode_descriptor_block(bs, tid, chunk, &jsb.uuid, is_first_desc, is_last_desc);
write_journal_block(ext, dev, journal_inode, idx, &desc)?;
idx = ring_next(idx, jsb);
for jb in chunk {
debug_assert_eq!(jb.bytes.len(), bs as usize, "journal payload wrong size");
write_journal_block(ext, dev, journal_inode, idx, &jb.bytes)?;
idx = ring_next(idx, jsb);
}
chunk_start = chunk_end;
is_first_desc = false;
if blocks.is_empty() {
break;
}
}
let commit = encode_commit_block(bs, tid, commit_sec, commit_nsec);
write_journal_block(ext, dev, journal_inode, idx, &commit)?;
let after = ring_next(idx, jsb);
set_start(jsb_buf, start_idx);
set_sequence(jsb_buf, tid);
Ok(after)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn header_round_trip() {
let h = encode_header(JBD2_COMMIT_BLOCK, 0x1234_5678);
assert_eq!(u32::from_be_bytes(h[0..4].try_into().unwrap()), JBD2_MAGIC);
assert_eq!(
u32::from_be_bytes(h[4..8].try_into().unwrap()),
JBD2_COMMIT_BLOCK
);
assert_eq!(
u32::from_be_bytes(h[8..12].try_into().unwrap()),
0x1234_5678
);
}
#[test]
fn descriptor_layout() {
let blocks = vec![
JournalBlock {
fs_block: 100,
bytes: vec![0; 1024],
},
JournalBlock {
fs_block: 200,
bytes: vec![0; 1024],
},
];
let uuid = [0xAA; 16];
let buf = encode_descriptor_block(1024, 7, &blocks, &uuid, true, true);
assert_eq!(
u32::from_be_bytes(buf[0..4].try_into().unwrap()),
JBD2_MAGIC
);
assert_eq!(
u32::from_be_bytes(buf[4..8].try_into().unwrap()),
JBD2_DESCRIPTOR_BLOCK
);
assert_eq!(u32::from_be_bytes(buf[8..12].try_into().unwrap()), 7);
assert_eq!(u32::from_be_bytes(buf[12..16].try_into().unwrap()), 100);
let flags0 = u16::from_be_bytes(buf[18..20].try_into().unwrap());
assert_eq!(flags0 & JBD2_FLAG_SAME_UUID, 0);
assert_eq!(flags0 & JBD2_FLAG_LAST_TAG, 0);
assert_eq!(&buf[20..36], &uuid);
assert_eq!(u32::from_be_bytes(buf[36..40].try_into().unwrap()), 200);
let flags1 = u16::from_be_bytes(buf[42..44].try_into().unwrap());
assert!(flags1 & JBD2_FLAG_SAME_UUID != 0);
assert!(flags1 & JBD2_FLAG_LAST_TAG != 0);
}
#[test]
fn descriptor_round_trip_parses() {
let blocks = vec![
JournalBlock {
fs_block: 100,
bytes: vec![0; 1024],
},
JournalBlock {
fs_block: 200,
bytes: vec![0; 1024],
},
JournalBlock {
fs_block: 300,
bytes: vec![0; 1024],
},
];
let uuid = [0x42; 16];
let buf = encode_descriptor_block(1024, 9, &blocks, &uuid, true, true);
let (tags, n) = parse_descriptor_tags(&buf, 1024, 0).unwrap();
assert_eq!(n, 3);
assert_eq!(tags[0].fs_block, 100);
assert_eq!(tags[1].fs_block, 200);
assert_eq!(tags[2].fs_block, 300);
assert!(tags[2].flags & JBD2_FLAG_LAST_TAG != 0);
}
#[test]
fn continuation_descriptor_first_tag_reuses_uuid() {
let blocks = [JournalBlock {
fs_block: 400,
bytes: vec![0; 1024],
}];
let uuid = [0x42; 16];
let buf = encode_descriptor_block(1024, 9, &blocks, &uuid, false, true);
let (tags, n) = parse_descriptor_tags(&buf, 1024, 0).unwrap();
assert_eq!(n, 1);
assert_eq!(tags[0].fs_block, 400);
assert!(tags[0].flags & JBD2_FLAG_SAME_UUID != 0);
assert!(tags[0].flags & JBD2_FLAG_LAST_TAG != 0);
}
#[test]
fn descriptor_without_same_uuid_carries_uuid_after_first_tag() {
let mut buf = vec![0u8; 1024];
buf[..12].copy_from_slice(&encode_header(JBD2_DESCRIPTOR_BLOCK, 9));
buf[12..16].copy_from_slice(&100u32.to_be_bytes());
buf[18..20].copy_from_slice(&0u16.to_be_bytes());
buf[20..36].fill(0x11);
buf[36..40].copy_from_slice(&200u32.to_be_bytes());
buf[42..44].copy_from_slice(&JBD2_FLAG_LAST_TAG.to_be_bytes());
buf[44..60].fill(0x22);
let (tags, n) = parse_descriptor_tags(&buf, 1024, 0).unwrap();
assert_eq!(n, 2);
assert_eq!(tags[0].fs_block, 100);
assert_eq!(tags[1].fs_block, 200);
}
#[test]
fn decodes_kernel_64bit_descriptor_tags() {
let mut buf = vec![0_u8; 1024];
buf[..12].copy_from_slice(&encode_header(JBD2_DESCRIPTOR_BLOCK, 9));
buf[12..16].copy_from_slice(&0x0050_0001_u32.to_be_bytes());
buf[16..18].copy_from_slice(&0_u16.to_be_bytes());
buf[18..20].copy_from_slice(&(JBD2_FLAG_SAME_UUID | JBD2_FLAG_LAST_TAG).to_be_bytes());
buf[20..24].copy_from_slice(&0_u32.to_be_bytes());
let (tags, n) = parse_descriptor_tags(&buf, 1024, JBD2_FEATURE_INCOMPAT_64BIT).unwrap();
assert_eq!(n, 1);
assert_eq!(tags[0].fs_block, 0x0050_0001);
assert_eq!(tags[0].flags, JBD2_FLAG_SAME_UUID | JBD2_FLAG_LAST_TAG);
}
#[test]
fn commit_layout() {
let buf = encode_commit_block(1024, 42, 1_234_567, 890);
assert_eq!(
u32::from_be_bytes(buf[0..4].try_into().unwrap()),
JBD2_MAGIC
);
assert_eq!(
u32::from_be_bytes(buf[4..8].try_into().unwrap()),
JBD2_COMMIT_BLOCK
);
assert_eq!(u32::from_be_bytes(buf[8..12].try_into().unwrap()), 42);
assert_eq!(
u64::from_be_bytes(buf[48..56].try_into().unwrap()),
1_234_567
);
assert_eq!(u32::from_be_bytes(buf[56..60].try_into().unwrap()), 890);
}
#[test]
fn decodes_64bit_revoke_records() {
let mut buf = vec![0_u8; 1024];
buf[..12].copy_from_slice(&encode_header(JBD2_REVOKE_BLOCK, 9));
buf[12..16].copy_from_slice(&32_u32.to_be_bytes());
buf[16..24].copy_from_slice(&0x0000_0001_0050_0001_u64.to_be_bytes());
buf[24..32].copy_from_slice(&0x0000_0000_0000_0042_u64.to_be_bytes());
let records = parse_revoke_records(&buf, 1024, JBD2_FEATURE_INCOMPAT_64BIT).unwrap();
assert_eq!(records, [0x0000_0001_0050_0001, 0x42]);
}
#[test]
fn ring_next_wraps() {
let jsb = JournalSuperblock {
blocksize: 1024,
maxlen: 10,
first: 1,
sequence: 1,
start: 0,
feature_incompat: 0,
uuid: [0; 16],
};
assert_eq!(ring_next(1, &jsb), 2);
assert_eq!(ring_next(8, &jsb), 9);
assert_eq!(ring_next(9, &jsb), 1);
}
}
#[cfg(test)]
mod revoke_tests {
use super::*;
use crate::block::MemoryBackend;
use crate::fs::ext::{Ext, FormatOpts, FsKind};
fn encode_revoke_block(block_size: u32, sequence: u32, blocks: &[u32]) -> Vec<u8> {
let mut out = vec![0u8; block_size as usize];
out[..12].copy_from_slice(&encode_header(JBD2_REVOKE_BLOCK, sequence));
let count = 16 + blocks.len() * 4;
out[12..16].copy_from_slice(&(count as u32).to_be_bytes());
for (i, &b) in blocks.iter().enumerate() {
let at = 16 + i * 4;
out[at..at + 4].copy_from_slice(&b.to_be_bytes());
}
out
}
struct Harness {
dev: MemoryBackend,
ext: Ext,
jsb_buf: Vec<u8>,
jsb: JournalSuperblock,
journal_inode: crate::fs::ext::Inode,
bs: u32,
}
fn harness() -> Harness {
let opts = FormatOpts {
kind: FsKind::Ext4,
block_size: 1024,
blocks_count: 4096,
inodes_count: 128,
journal_blocks: 64,
..FormatOpts::default()
};
let total = opts.blocks_count as u64 * opts.block_size as u64;
let mut dev = MemoryBackend::new(total);
let mut ext = Ext::format_with(&mut dev, &opts).unwrap();
ext.flush(&mut dev).unwrap();
let journal_inode = ext.read_inode(&mut dev, ext.sb.journal_inum).unwrap();
let jsb_buf = read_journal_block(&ext, &mut dev, &journal_inode, 0).unwrap();
let jsb = JournalSuperblock::decode(&jsb_buf).unwrap();
let bs = ext.layout.block_size;
Harness {
dev,
ext,
jsb_buf,
jsb,
journal_inode,
bs,
}
}
impl Harness {
fn put(&mut self, idx: u32, bytes: &[u8]) {
write_journal_block(&self.ext, &mut self.dev, &self.journal_inode, idx, bytes).unwrap();
}
fn arm(&mut self, start: u32, seq: u32) {
let mut sb = self.jsb_buf.clone();
set_start(&mut sb, start);
set_sequence(&mut sb, seq);
write_journal_block(&self.ext, &mut self.dev, &self.journal_inode, 0, &sb).unwrap();
}
fn fs_block(&mut self, blk: u64) -> Vec<u8> {
let mut buf = vec![0u8; self.bs as usize];
self.dev.read_at(blk * self.bs as u64, &mut buf).unwrap();
buf
}
fn set_fs_block(&mut self, blk: u64, fill: u8) {
let buf = vec![fill; self.bs as usize];
self.dev.write_at(blk * self.bs as u64, &buf).unwrap();
}
}
const TARGET: u64 = 3000;
const UNTOUCHED: u64 = 3001;
#[test]
fn revoke_in_a_later_transaction_suppresses_an_earlier_write() {
let mut h = harness();
let bs = h.bs;
let uuid = h.jsb.uuid;
let first = h.jsb.first;
h.set_fs_block(TARGET, 0x55);
h.set_fs_block(UNTOUCHED, 0x55);
let blocks = [
JournalBlock {
fs_block: TARGET as u32,
bytes: vec![0xAA; bs as usize],
},
JournalBlock {
fs_block: UNTOUCHED as u32,
bytes: vec![0xBB; bs as usize],
},
];
let mut idx = first;
h.put(
idx,
&encode_descriptor_block(bs, 100, &blocks, &uuid, true, true),
);
idx = ring_next(idx, &h.jsb);
h.put(idx, &vec![0xAA; bs as usize]);
idx = ring_next(idx, &h.jsb);
h.put(idx, &vec![0xBB; bs as usize]);
idx = ring_next(idx, &h.jsb);
h.put(idx, &encode_commit_block(bs, 100, 0, 0));
idx = ring_next(idx, &h.jsb);
h.put(idx, &encode_revoke_block(bs, 101, &[TARGET as u32]));
idx = ring_next(idx, &h.jsb);
h.put(idx, &encode_commit_block(bs, 101, 0, 0));
h.arm(first, 100);
let mut dev = std::mem::replace(&mut h.dev, MemoryBackend::new(0));
let replayed = replay_journal(&h.ext, &mut dev).unwrap();
h.dev = dev;
assert!(replayed);
assert_eq!(
h.fs_block(TARGET)[0],
0x55,
"a block revoked by a later transaction must not be replayed"
);
assert_eq!(
h.fs_block(UNTOUCHED)[0],
0xBB,
"an unrevoked block from the same transaction must still replay"
);
}
#[test]
fn revoke_before_the_write_does_not_suppress_it() {
let mut h = harness();
let bs = h.bs;
let uuid = h.jsb.uuid;
let first = h.jsb.first;
h.set_fs_block(TARGET, 0x55);
let mut idx = first;
h.put(idx, &encode_revoke_block(bs, 100, &[TARGET as u32]));
idx = ring_next(idx, &h.jsb);
h.put(idx, &encode_commit_block(bs, 100, 0, 0));
idx = ring_next(idx, &h.jsb);
let blocks = [JournalBlock {
fs_block: TARGET as u32,
bytes: vec![0xCC; bs as usize],
}];
h.put(
idx,
&encode_descriptor_block(bs, 101, &blocks, &uuid, true, true),
);
idx = ring_next(idx, &h.jsb);
h.put(idx, &vec![0xCC; bs as usize]);
idx = ring_next(idx, &h.jsb);
h.put(idx, &encode_commit_block(bs, 101, 0, 0));
h.arm(first, 100);
let mut dev = std::mem::replace(&mut h.dev, MemoryBackend::new(0));
replay_journal(&h.ext, &mut dev).unwrap();
h.dev = dev;
assert_eq!(
h.fs_block(TARGET)[0],
0xCC,
"a revoke from an earlier transaction must not block a later write"
);
}
#[test]
fn uncommitted_tail_transaction_is_dropped_entirely() {
let mut h = harness();
let bs = h.bs;
let uuid = h.jsb.uuid;
let first = h.jsb.first;
h.set_fs_block(TARGET, 0x55);
h.set_fs_block(UNTOUCHED, 0x55);
let committed = [JournalBlock {
fs_block: UNTOUCHED as u32,
bytes: vec![0xBB; bs as usize],
}];
let mut idx = first;
h.put(
idx,
&encode_descriptor_block(bs, 100, &committed, &uuid, true, true),
);
idx = ring_next(idx, &h.jsb);
h.put(idx, &vec![0xBB; bs as usize]);
idx = ring_next(idx, &h.jsb);
h.put(idx, &encode_commit_block(bs, 100, 0, 0));
idx = ring_next(idx, &h.jsb);
let torn = [JournalBlock {
fs_block: TARGET as u32,
bytes: vec![0xDD; bs as usize],
}];
h.put(idx, &encode_revoke_block(bs, 101, &[UNTOUCHED as u32]));
idx = ring_next(idx, &h.jsb);
h.put(
idx,
&encode_descriptor_block(bs, 101, &torn, &uuid, true, true),
);
idx = ring_next(idx, &h.jsb);
h.put(idx, &vec![0xDD; bs as usize]);
h.arm(first, 100);
let mut dev = std::mem::replace(&mut h.dev, MemoryBackend::new(0));
replay_journal(&h.ext, &mut dev).unwrap();
h.dev = dev;
assert_eq!(
h.fs_block(TARGET)[0],
0x55,
"an uncommitted transaction must not be replayed"
);
assert_eq!(
h.fs_block(UNTOUCHED)[0],
0xBB,
"an uncommitted transaction's revoke must not suppress a committed write"
);
}
#[test]
fn tid_geq_handles_the_32_bit_wrap() {
assert!(tid_geq(5, 5));
assert!(tid_geq(6, 5));
assert!(!tid_geq(5, 6));
assert!(tid_geq(1, u32::MAX));
assert!(!tid_geq(u32::MAX, 1));
}
}