#![forbid(unsafe_code)]
use crate::error::{Ext4Error, Result};
pub const JOURNAL_MAGIC: u32 = 0xC03B_3998;
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;
#[inline]
fn be32(buf: &[u8], off: usize) -> u32 {
u32::from_be_bytes(buf[off..off + 4].try_into().unwrap())
}
#[inline]
fn be64(buf: &[u8], off: usize) -> u64 {
u64::from_be_bytes(buf[off..off + 8].try_into().unwrap())
}
fn check_len(buf: &[u8], need: usize, structure: &'static str) -> Result<()> {
if buf.len() < need {
Err(Ext4Error::TooShort {
structure,
expected: need,
found: buf.len(),
})
} else {
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JournalBlockType {
Descriptor,
Commit,
SuperblockV1,
SuperblockV2,
Revoke,
Unknown(u32),
}
impl From<u32> for JournalBlockType {
fn from(v: u32) -> Self {
match v {
1 => Self::Descriptor,
2 => Self::Commit,
3 => Self::SuperblockV1,
4 => Self::SuperblockV2,
5 => Self::Revoke,
other => Self::Unknown(other),
}
}
}
#[derive(Debug, Clone)]
pub struct JournalHeader {
pub magic: u32,
pub block_type: JournalBlockType,
pub sequence: u32,
}
impl JournalHeader {
pub fn parse(buf: &[u8]) -> Result<Self> {
check_len(buf, 12, "JournalHeader")?;
let magic = be32(buf, 0);
if magic != JOURNAL_MAGIC {
return Err(Ext4Error::JournalCorrupt(format!(
"bad journal magic: 0x{magic:08X}"
)));
}
Ok(Self {
magic,
block_type: JournalBlockType::from(be32(buf, 4)),
sequence: be32(buf, 8),
})
}
}
#[derive(Debug, Clone)]
pub struct JournalSuperblock {
pub header: JournalHeader,
pub block_size: u32,
pub max_len: u32,
pub first: u32,
pub sequence: u32,
pub start: u32,
pub errno: u32,
pub feature_compat: u32,
pub feature_incompat: u32,
pub feature_ro_compat: u32,
pub uuid: [u8; 16],
pub nr_users: u32,
pub checksum_type: u8,
pub checksum: u32,
}
impl JournalSuperblock {
pub fn parse(buf: &[u8]) -> Result<Self> {
check_len(buf, 0x100, "JournalSuperblock")?;
let header = JournalHeader::parse(buf)?;
let mut uuid = [0u8; 16];
uuid.copy_from_slice(&buf[0x30..0x40]);
Ok(Self {
header,
block_size: be32(buf, 0x0C),
max_len: be32(buf, 0x10),
first: be32(buf, 0x14),
sequence: be32(buf, 0x18),
start: be32(buf, 0x1C),
errno: be32(buf, 0x20),
feature_compat: be32(buf, 0x24),
feature_incompat: be32(buf, 0x28),
feature_ro_compat: be32(buf, 0x2C),
uuid,
nr_users: be32(buf, 0x40),
checksum_type: buf[0x50],
checksum: be32(buf, 0xFC),
})
}
pub fn is_64bit(&self) -> bool {
self.feature_incompat & JBD2_FEATURE_INCOMPAT_64BIT != 0
}
pub fn has_csum_v3(&self) -> bool {
self.feature_incompat & JBD2_FEATURE_INCOMPAT_CSUM_V3 != 0
}
pub fn has_csum_v2(&self) -> bool {
self.feature_incompat & JBD2_FEATURE_INCOMPAT_CSUM_V2 != 0
}
}
#[derive(Debug, Clone)]
pub struct JournalBlockTag {
pub blocknr: u64,
pub checksum: u32,
pub escaped: bool,
pub same_uuid: bool,
pub last_tag: bool,
pub tag_size: usize,
}
impl JournalBlockTag {
pub fn parse_v3(buf: &[u8], is_64bit: bool) -> Self {
let blocknr_lo = u64::from(be32(buf, 0x00));
let flags = be32(buf, 0x04);
let escaped = flags & 0x01 != 0;
let same_uuid = flags & 0x02 != 0;
let last_tag = flags & 0x08 != 0;
let blocknr_hi = if is_64bit {
u64::from(be32(buf, 0x08))
} else {
0
};
let blocknr = (blocknr_hi << 32) | blocknr_lo;
let checksum = be32(buf, 0x0C);
let tag_size = if same_uuid { 16 } else { 32 };
Self {
blocknr,
checksum,
escaped,
same_uuid,
last_tag,
tag_size,
}
}
}
#[derive(Debug, Clone)]
pub struct JournalCommit {
pub sequence: u32,
pub commit_seconds: i64,
pub commit_nanoseconds: u32,
}
impl JournalCommit {
pub fn parse(buf: &[u8]) -> Result<Self> {
check_len(buf, 0x3C, "JournalCommit")?;
let header = JournalHeader::parse(buf)?;
Ok(Self {
sequence: header.sequence,
commit_seconds: be64(buf, 0x30) as i64,
commit_nanoseconds: be32(buf, 0x38),
})
}
}
#[derive(Debug, Clone)]
pub struct JournalRevoke {
pub sequence: u32,
pub revoked_blocks: Vec<u64>,
}
impl JournalRevoke {
pub fn parse(buf: &[u8], is_64bit: bool) -> Result<Self> {
check_len(buf, 16, "JournalRevoke")?;
let header = JournalHeader::parse(buf)?;
let byte_count = be32(buf, 12) as usize;
check_len(buf, byte_count, "JournalRevoke data")?;
let entry_size = if is_64bit { 8usize } else { 4 };
let data = &buf[16..byte_count];
let mut revoked_blocks = Vec::with_capacity(data.len() / entry_size);
let mut off = 0;
while off + entry_size <= data.len() {
let blk = if is_64bit {
be64(data, off)
} else {
u64::from(be32(data, off))
};
revoked_blocks.push(blk);
off += entry_size;
}
Ok(Self {
sequence: header.sequence,
revoked_blocks,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_journal_header() {
let mut buf = vec![0u8; 12];
buf[0..4].copy_from_slice(&0xC03B_3998u32.to_be_bytes());
buf[4..8].copy_from_slice(&1u32.to_be_bytes()); buf[8..12].copy_from_slice(&42u32.to_be_bytes());
let hdr = JournalHeader::parse(&buf).unwrap();
assert_eq!(hdr.magic, JOURNAL_MAGIC);
assert_eq!(hdr.block_type, JournalBlockType::Descriptor);
assert_eq!(hdr.sequence, 42);
}
#[test]
fn reject_bad_journal_magic() {
let buf = vec![0u8; 12];
let err = JournalHeader::parse(&buf).unwrap_err();
assert!(matches!(err, crate::error::Ext4Error::JournalCorrupt(_)));
}
#[test]
fn parse_journal_superblock() {
let mut buf = vec![0u8; 1024];
buf[0..4].copy_from_slice(&JOURNAL_MAGIC.to_be_bytes());
buf[4..8].copy_from_slice(&4u32.to_be_bytes()); buf[8..12].copy_from_slice(&1u32.to_be_bytes());
buf[0x0C..0x10].copy_from_slice(&4096u32.to_be_bytes()); buf[0x10..0x14].copy_from_slice(&1024u32.to_be_bytes()); buf[0x14..0x18].copy_from_slice(&1u32.to_be_bytes()); let jsb = JournalSuperblock::parse(&buf).unwrap();
assert_eq!(jsb.block_size, 4096);
assert_eq!(jsb.max_len, 1024);
assert_eq!(jsb.first, 1);
}
#[test]
fn parse_block_tag_v3() {
let mut buf = vec![0u8; 16];
buf[0..4].copy_from_slice(&500u32.to_be_bytes()); buf[4..8].copy_from_slice(&0x08u32.to_be_bytes()); buf[12..16].copy_from_slice(&0xDEAD_BEEFu32.to_be_bytes());
let tag = JournalBlockTag::parse_v3(&buf, false);
assert_eq!(tag.blocknr, 500);
assert!(tag.last_tag);
assert!(!tag.escaped);
}
#[test]
fn journal_superblock_feature_flags() {
let mut buf = vec![0u8; 1024];
buf[0..4].copy_from_slice(&0xC03B_3998u32.to_be_bytes());
buf[4..8].copy_from_slice(&4u32.to_be_bytes()); buf[8..12].copy_from_slice(&1u32.to_be_bytes()); buf[0x0C..0x10].copy_from_slice(&1024u32.to_be_bytes()); buf[0x10..0x14].copy_from_slice(&100u32.to_be_bytes()); buf[0x14..0x18].copy_from_slice(&1u32.to_be_bytes()); let features: u32 = JBD2_FEATURE_INCOMPAT_64BIT | JBD2_FEATURE_INCOMPAT_CSUM_V3;
buf[0x28..0x2C].copy_from_slice(&features.to_be_bytes());
let jsb = JournalSuperblock::parse(&buf).unwrap();
assert!(jsb.is_64bit());
assert!(jsb.has_csum_v3());
assert!(!jsb.has_csum_v2());
}
#[test]
fn journal_superblock_csum_v2_only() {
let mut buf = vec![0u8; 1024];
buf[0..4].copy_from_slice(&0xC03B_3998u32.to_be_bytes());
buf[4..8].copy_from_slice(&4u32.to_be_bytes()); buf[8..12].copy_from_slice(&1u32.to_be_bytes());
buf[0x0C..0x10].copy_from_slice(&1024u32.to_be_bytes());
buf[0x10..0x14].copy_from_slice(&100u32.to_be_bytes());
buf[0x14..0x18].copy_from_slice(&1u32.to_be_bytes());
let features: u32 = JBD2_FEATURE_INCOMPAT_CSUM_V2; buf[0x28..0x2C].copy_from_slice(&features.to_be_bytes());
let jsb = JournalSuperblock::parse(&buf).unwrap();
assert!(!jsb.is_64bit());
assert!(!jsb.has_csum_v3());
assert!(jsb.has_csum_v2());
}
#[test]
fn parse_commit_block() {
let mut buf = vec![0u8; 64];
buf[0..4].copy_from_slice(&JOURNAL_MAGIC.to_be_bytes());
buf[4..8].copy_from_slice(&2u32.to_be_bytes()); buf[8..12].copy_from_slice(&99u32.to_be_bytes());
buf[0x30..0x38].copy_from_slice(&1_700_000_000u64.to_be_bytes());
buf[0x38..0x3C].copy_from_slice(&500_000_000u32.to_be_bytes());
let commit = JournalCommit::parse(&buf).unwrap();
assert_eq!(commit.sequence, 99);
assert_eq!(commit.commit_seconds, 1_700_000_000);
assert_eq!(commit.commit_nanoseconds, 500_000_000);
}
}