use crate::error::{Error, Result};
use crate::fs::Filesystem;
use crate::inode::Inode;
use crate::jbd2::{self, JournalSuperblock};
use crate::journal::ReplayPlan;
pub(crate) fn set_needs_recovery_in(sb: &mut [u8], on: bool) {
let recover = crate::features::Incompat::RECOVER.bits();
let incompat = u32::from_le_bytes(sb[0x60..0x64].try_into().unwrap());
let incompat = if on {
incompat | recover
} else {
incompat & !recover
};
sb[0x60..0x64].copy_from_slice(&incompat.to_le_bytes());
let ro_compat = u32::from_le_bytes(sb[0x64..0x68].try_into().unwrap());
if ro_compat & crate::features::RoCompat::METADATA_CSUM.bits() != 0 {
let csum = crate::checksum::linux_crc32c(!0, &sb[..0x3FC]);
sb[0x3FC..0x400].copy_from_slice(&csum.to_le_bytes());
}
}
pub(crate) fn write_needs_recovery(dev: &dyn crate::block_io::BlockDevice, on: bool) -> Result<()> {
let at = crate::superblock::SUPERBLOCK_OFFSET;
let mut sb = vec![0u8; 1024];
dev.read_at(at, &mut sb)?;
set_needs_recovery_in(&mut sb, on);
dev.write_at(at, &sb)
}
fn mark_journal_clean(fs: &Filesystem, jsb: &JournalSuperblock, plan: &ReplayPlan) -> Result<()> {
let raw = fs.read_inode_raw(fs.sb.journal_inode)?;
let jinode = Inode::parse(&raw)?;
let phys = jbd2::journal_block_to_physical(fs, &jinode, 0)?
.ok_or(Error::Corrupt("journal_apply: journal superblock unmapped"))?;
let at = byte_offset_of(fs, phys)?;
let mut buf = vec![0u8; 1024];
fs.dev.read_at(at, &mut buf)?;
if u32::from_be_bytes(buf[0..4].try_into().unwrap()) != jbd2::JBD2_MAGIC_NUMBER {
return Err(Error::Corrupt(
"journal_apply: journal superblock lost its magic during replay",
));
}
let sequence = plan.next_sequence.unwrap_or(jsb.sequence).wrapping_add(1);
buf[0x18..0x1C].copy_from_slice(&sequence.to_be_bytes());
buf[0x1C..0x20].copy_from_slice(&0u32.to_be_bytes());
if jsb.uses_csum_v2_or_v3() {
buf[0xFC..0x100].fill(0);
let csum = crate::checksum::linux_crc32c(!0, &buf);
buf[0xFC..0x100].copy_from_slice(&csum.to_be_bytes());
}
fs.dev.write_at(at, &buf)?;
fs.dev.flush()
}
pub(crate) fn byte_offset_of(fs: &Filesystem, block: u64) -> Result<u64> {
byte_offset_in(
block,
fs.sb.blocks_count,
fs.sb.block_size(),
fs.dev.size_bytes(),
)
}
pub(crate) fn byte_offset_in(
block: u64,
blocks_count: u64,
block_size: u32,
device_bytes: u64,
) -> Result<u64> {
if block >= blocks_count {
return Err(Error::Corrupt(
"journal: names a block past the end of the filesystem",
));
}
let block_size = block_size as u64;
let offset = block
.checked_mul(block_size)
.ok_or(Error::Corrupt("journal: block offset overflows"))?;
let end = offset
.checked_add(block_size)
.ok_or(Error::Corrupt("journal: block offset overflows"))?;
if end > device_bytes {
return Err(Error::Corrupt(
"journal: names a block past the end of the device",
));
}
Ok(offset)
}
pub fn apply(fs: &Filesystem, plan: &ReplayPlan) -> Result<usize> {
if plan.writes.is_empty() {
return Ok(0);
}
if !fs.dev.is_writable() {
return Err(Error::Corrupt(
"journal_apply: device is not writable; cannot replay",
));
}
let block_size = fs.sb.block_size() as u64;
let resolved = resolve(fs, plan)?;
let mut applied = 0usize;
for (w, &(source, destination)) in plan.writes.iter().zip(&resolved) {
let mut buf = vec![0u8; block_size as usize];
fs.dev.read_at(source, &mut buf)?;
if w.flags & crate::journal::TAG_ESCAPED != 0 {
buf[0..4].copy_from_slice(&crate::jbd2::JBD2_MAGIC_NUMBER.to_be_bytes());
}
fs.dev.write_at(destination, &buf)?;
applied += 1;
}
fs.dev.flush()?;
Ok(applied)
}
pub fn replay_into_cache(fs: &Filesystem) -> Result<usize> {
let Some(jsb) = jbd2::read_superblock(fs)? else {
return Ok(0);
};
if jsb.is_clean() {
return Ok(0);
}
let plan = crate::journal::walk(fs, &jsb)?;
if plan.writes.is_empty() {
return Ok(0);
}
let block_size = fs.sb.block_size() as u64;
let resolved = resolve(fs, &plan)?;
let mut blocks = Vec::with_capacity(plan.writes.len());
for (w, &(source, _)) in plan.writes.iter().zip(&resolved) {
let mut buf = vec![0u8; block_size as usize];
fs.dev.read_at(source, &mut buf)?;
if w.flags & crate::journal::TAG_ESCAPED != 0 {
buf[0..4].copy_from_slice(&crate::jbd2::JBD2_MAGIC_NUMBER.to_be_bytes());
}
blocks.push((w.fs_block, buf));
}
let n = blocks.len();
for (block, bytes) in blocks {
fs.dev.populate_cache(block, bytes);
}
Ok(n)
}
fn resolve(fs: &Filesystem, plan: &ReplayPlan) -> Result<Vec<(u64, u64)>> {
let raw = fs.read_inode_raw(fs.sb.journal_inode)?;
let jinode = Inode::parse(&raw)?;
plan.writes
.iter()
.map(|w| {
let phys = jbd2::journal_block_to_physical(fs, &jinode, w.journal_block)?
.ok_or(Error::Corrupt("journal_apply: journal block unmapped"))?;
Ok((byte_offset_of(fs, phys)?, byte_offset_of(fs, w.fs_block)?))
})
.collect()
}
pub fn replay_if_dirty(fs: &Filesystem) -> Result<usize> {
if !fs.dev.is_writable() {
return Ok(0);
}
let Some(jsb) = jbd2::read_superblock(fs)? else {
return Ok(0); };
if jsb.is_clean() {
if fs.sb.feature_incompat & crate::features::Incompat::RECOVER.bits() != 0 {
write_needs_recovery(fs.dev.as_ref(), false)?;
fs.dev.flush()?;
}
return Ok(0);
}
let write_breaking = crate::features::write_breaking_incompat(fs.sb.feature_incompat);
if write_breaking != 0 {
return Err(crate::Error::UnsupportedIncompat(write_breaking));
}
let plan = crate::journal::walk(fs, &jsb)?;
let applied = apply(fs, &plan)?;
mark_journal_clean(fs, &jsb, &plan)?;
write_needs_recovery(fs.dev.as_ref(), false)?;
fs.dev.flush()?;
Ok(applied)
}
#[derive(Debug, Clone)]
pub struct ReplaySummary {
pub clean: bool,
pub sequence: u32,
pub start: u32,
pub max_len: u32,
pub has_revoke: bool,
pub uses_64bit: bool,
pub uses_csum_v2_or_v3: bool,
}
impl From<&JournalSuperblock> for ReplaySummary {
fn from(jsb: &JournalSuperblock) -> Self {
Self {
clean: jsb.is_clean(),
sequence: jsb.sequence,
start: jsb.start,
max_len: jsb.max_len,
has_revoke: jsb.has_revoke(),
uses_64bit: jsb.uses_64bit(),
uses_csum_v2_or_v3: jsb.uses_csum_v2_or_v3(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::journal::{ReplayEntry, ReplayPlan};
#[test]
fn empty_plan_applies_zero_blocks() {
let plan = ReplayPlan::default();
assert!(plan.writes.is_empty());
}
#[test]
fn byte_offset_in_refuses_a_block_whose_offset_wraps() {
let block_size = 4096u32;
let wraps_to_zero = u64::MAX / block_size as u64 + 1;
assert_eq!(wraps_to_zero.wrapping_mul(block_size as u64), 0);
match byte_offset_in(wraps_to_zero, u64::MAX, block_size, u64::MAX) {
Err(Error::Corrupt(msg)) => assert_eq!(msg, "journal: block offset overflows"),
other => panic!("block {wraps_to_zero} was not refused as an overflow: {other:?}"),
}
}
#[test]
fn replay_summary_mirrors_jsb() {
let jsb = JournalSuperblock {
block_type: crate::jbd2::JBD2_SUPERBLOCK_V2,
header_sequence: 1,
block_size: 4096,
max_len: 8192,
first: 1,
sequence: 42,
start: 100,
errno: 0,
feature_compat: 0,
feature_incompat: crate::jbd2::JbdIncompat::REVOKE.bits()
| crate::jbd2::JbdIncompat::BIT64.bits()
| crate::jbd2::JbdIncompat::CSUM_V3.bits(),
feature_ro_compat: 0,
uuid: [0; 16],
nr_users: 1,
checksum_type: 4,
num_fc_blocks: 0,
checksum: 0,
};
let s = ReplaySummary::from(&jsb);
assert!(!s.clean);
assert_eq!(s.sequence, 42);
assert_eq!(s.start, 100);
assert!(s.has_revoke);
assert!(s.uses_64bit);
assert!(s.uses_csum_v2_or_v3);
}
#[test]
fn clean_summary() {
let mut jsb = JournalSuperblock {
block_type: crate::jbd2::JBD2_SUPERBLOCK_V2,
header_sequence: 1,
block_size: 4096,
max_len: 8192,
first: 1,
sequence: 1,
start: 0,
errno: 0,
feature_compat: 0,
feature_incompat: 0,
feature_ro_compat: 0,
uuid: [0; 16],
nr_users: 1,
checksum_type: 0,
num_fc_blocks: 0,
checksum: 0,
};
jsb.start = 0;
let s = ReplaySummary::from(&jsb);
assert!(s.clean);
}
#[test]
fn replay_entry_structure_stable() {
let e = ReplayEntry {
transaction: 1,
fs_block: 100,
journal_block: 5,
flags: 0,
};
assert_eq!(e.transaction, 1);
assert_eq!(e.fs_block, 100);
assert_eq!(e.journal_block, 5);
}
}