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_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;
#[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],
) -> 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 mut flags: u16 = 0;
if i != 0 {
flags |= JBD2_FLAG_SAME_UUID;
}
if i + 1 == blocks.len() {
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 i == 0 {
out[off..off + 16].copy_from_slice(uuid);
off += 16;
}
}
out
}
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 fn decode_tag(buf: &[u8], is_first: bool) -> Result<(u32, u16, usize)> {
if buf.len() < 8 {
return Err(crate::Error::InvalidImage(
"ext: journal descriptor tag past end of block".into(),
));
}
let blocknr = u32::from_be_bytes(buf[0..4].try_into().unwrap());
let flags = u16::from_be_bytes(buf[6..8].try_into().unwrap());
let has_uuid = is_first || (flags & JBD2_FLAG_SAME_UUID) == 0;
let size = if has_uuid { 24 } else { 8 };
if buf.len() < size {
return Err(crate::Error::InvalidImage(
"ext: journal descriptor tag uuid past end of block".into(),
));
}
Ok((blocknr, 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(())
}
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 replayed = false;
loop {
let blk = read_journal_block(ext, dev, &journal_inode, idx)?;
let magic = u32::from_be_bytes(blk[0..4].try_into().unwrap());
if magic != JBD2_MAGIC {
break;
}
let blocktype = u32::from_be_bytes(blk[4..8].try_into().unwrap());
let tid = u32::from_be_bytes(blk[8..12].try_into().unwrap());
if tid != expected_tid {
break;
}
if blocktype != JBD2_DESCRIPTOR_BLOCK {
break;
}
let (data_targets, payload_count) = parse_descriptor_tags(&blk, bs)?;
idx = ring_next(idx, &jsb);
let mut commit_seen = false;
for tag in &data_targets {
let mut payload = read_journal_block(ext, dev, &journal_inode, idx)?;
if tag.flags & JBD2_FLAG_ESCAPE != 0 {
payload[0..4].copy_from_slice(&JBD2_MAGIC.to_be_bytes());
}
dev.write_at(tag.fs_block as u64 * bs as u64, &payload)?;
idx = ring_next(idx, &jsb);
}
let _ = payload_count;
let commit_buf = read_journal_block(ext, dev, &journal_inode, idx)?;
let cmagic = u32::from_be_bytes(commit_buf[0..4].try_into().unwrap());
let ctype = u32::from_be_bytes(commit_buf[4..8].try_into().unwrap());
let ctid = u32::from_be_bytes(commit_buf[8..12].try_into().unwrap());
if cmagic == JBD2_MAGIC && ctype == JBD2_COMMIT_BLOCK && ctid == tid {
commit_seen = true;
}
idx = ring_next(idx, &jsb);
if !commit_seen {
break;
}
replayed = true;
expected_tid = expected_tid.wrapping_add(1);
}
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: u32,
pub flags: u16,
}
pub(crate) fn parse_descriptor_tags(
buf: &[u8],
block_size: u32,
) -> Result<(Vec<ParsedTag>, usize)> {
let mut out = Vec::new();
let mut off = 12usize;
let mut first = true;
while off + 8 <= block_size as usize {
let (fs_block, flags, sz) = decode_tag(&buf[off..], first)?;
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))
}
#[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 need = 2 + blocks.len() 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 desc = encode_descriptor_block(bs, tid, blocks, &jsb.uuid);
write_journal_block(ext, dev, journal_inode, start_idx, &desc)?;
let mut idx = ring_next(start_idx, jsb);
for jb in blocks {
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);
}
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);
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);
let (tags, n) = parse_descriptor_tags(&buf, 1024).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 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 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);
}
}