use crate::block_io::BlockDevice;
use crate::error::{Error, Result};
use crate::fs::Filesystem;
use crate::inode::Inode;
use crate::jbd2::{self, JournalSuperblock, JBD2_MAGIC_NUMBER};
use crate::transaction::Transaction;
pub struct JournalWriter {
physical_map: Vec<u64>,
block_size: u32,
jsb: JournalSuperblock,
}
impl JournalWriter {
pub fn open(fs: &Filesystem) -> Result<Option<Self>> {
let Some(jsb) = jbd2::read_superblock(fs)? else {
return Ok(None);
};
let raw = fs.read_inode_raw(fs.sb.journal_inode)?;
let jinode = Inode::parse(&raw)?;
let bs = fs.sb.block_size();
let mut physical_map = Vec::with_capacity(jsb.max_len as usize);
for logical in 0..jsb.max_len as u64 {
let phys = crate::indirect::map_logical_any(
&jinode.block,
jinode.flags,
fs.dev.as_ref(),
bs,
logical,
)?
.ok_or(Error::Corrupt(
"journal_writer: journal inode has unmapped logical block",
))?;
physical_map.push(phys);
}
Ok(Some(Self {
physical_map,
block_size: bs,
jsb,
}))
}
pub fn begin(&self) -> Transaction {
Transaction::begin(
self.jsb.sequence,
self.block_size,
self.jsb.uses_64bit(),
self.jsb.uses_csum_v2_or_v3(),
)
}
pub fn max_blocks_per_transaction(&self) -> usize {
(self.jsb.max_len as usize).saturating_sub(1)
}
pub fn commit(&mut self, dev: &dyn BlockDevice, tx: &Transaction) -> Result<()> {
if !dev.is_writable() {
return Err(Error::ReadOnly);
}
if tx.sequence != self.jsb.sequence {
return Err(Error::Corrupt(
"journal_writer: transaction sequence does not match writer state",
));
}
let blocks = tx.commit()?;
if blocks.is_empty() {
return Ok(()); }
if blocks.len() > self.max_blocks_per_transaction() {
return Err(Error::Corrupt(
"journal_writer: transaction too large for journal",
));
}
let bs_u64 = self.block_size as u64;
let txn_first_jblock = 1usize;
for (i, block) in blocks.iter().enumerate() {
let jblock_idx = txn_first_jblock + i;
let phys = self.physical_map[jblock_idx];
dev.write_at(phys * bs_u64, block)?;
}
dev.flush()?;
self.jsb.start = txn_first_jblock as u32;
self.write_jsb(dev)?;
dev.flush()?;
for w in &tx.writes {
dev.write_at(w.fs_block * bs_u64, &w.bytes)?;
}
dev.flush()?;
self.jsb.start = 0;
self.jsb.sequence = self.jsb.sequence.wrapping_add(1);
self.write_jsb(dev)?;
dev.flush()?;
Ok(())
}
fn write_jsb(&self, dev: &dyn BlockDevice) -> Result<()> {
let bs_u64 = self.block_size as u64;
let phys = self.physical_map[0];
let mut buf = vec![0u8; self.block_size as usize];
dev.read_at(phys * bs_u64, &mut buf)?;
let magic = u32::from_be_bytes(buf[0x00..0x04].try_into().unwrap());
if magic != JBD2_MAGIC_NUMBER {
return Err(Error::Corrupt(
"journal_writer: jsb block lost its magic between mount and commit",
));
}
let block_type = u32::from_be_bytes(buf[0x04..0x08].try_into().unwrap());
if block_type != self.jsb.block_type {
return Err(Error::Corrupt(
"journal_writer: jsb block_type changed since mount",
));
}
buf[0x08..0x0C].copy_from_slice(&self.jsb.sequence.to_be_bytes());
buf[0x18..0x1C].copy_from_slice(&self.jsb.sequence.to_be_bytes());
buf[0x1C..0x20].copy_from_slice(&self.jsb.start.to_be_bytes());
if self.jsb.uses_csum_v2_or_v3() {
const JSB_LEN: usize = 1024; buf[0xFC..0x100].copy_from_slice(&0u32.to_be_bytes());
let csum = crate::checksum::linux_crc32c(!0, &buf[..JSB_LEN]);
buf[0xFC..0x100].copy_from_slice(&csum.to_be_bytes());
}
dev.write_at(phys * bs_u64, &buf)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::block_io::FileDevice;
use std::fs;
use std::sync::Arc;
fn copy_to_tmp(name: &str, tag: &str) -> Option<String> {
use std::sync::atomic::{AtomicU32, Ordering};
static COUNTER: AtomicU32 = AtomicU32::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let src = format!("{}/test-disks/{}", env!("CARGO_MANIFEST_DIR"), name);
if !std::path::Path::new(&src).exists() {
return None;
}
let dst = format!("/tmp/fs_ext4_jw_{}_{tag}_{n}.img", std::process::id());
fs::copy(&src, &dst).ok()?;
Some(dst)
}
#[test]
fn open_returns_none_when_no_journal() {
let Some(path) = copy_to_tmp("ext4-no-csum.img", "no_journal") else {
return;
};
let dev = FileDevice::open(&path).expect("open ro");
let fs = Filesystem::mount(Arc::new(dev)).expect("mount");
let _ = JournalWriter::open(&fs).expect("open journal_writer");
fs::remove_file(path).ok();
}
#[test]
fn empty_transaction_is_no_op() {
let Some(path) = copy_to_tmp("ext4-basic.img", "empty_tx") else {
return;
};
let dev = FileDevice::open_rw(&path).expect("open rw");
let fs = Filesystem::mount(Arc::new(dev)).expect("mount");
let Some(mut jw) = JournalWriter::open(&fs).expect("open writer") else {
return; };
let initial_seq = jw.jsb.sequence;
let tx = jw.begin();
jw.commit(fs.dev.as_ref(), &tx).expect("commit");
assert_eq!(
jw.jsb.sequence,
initial_seq.wrapping_add(1),
"sequence should advance even for a no-write commit"
);
assert_eq!(jw.jsb.start, 0, "should be clean after commit");
fs::remove_file(path).ok();
}
}