Skip to main content

ext4fs/ondisk/
journal.rs

1#![forbid(unsafe_code)]
2use crate::error::{Ext4Error, Result};
3
4pub const JOURNAL_MAGIC: u32 = 0xC03B_3998;
5
6// JBD2 feature incompat flags
7pub const JBD2_FEATURE_INCOMPAT_64BIT: u32 = 0x0000_0002;
8pub const JBD2_FEATURE_INCOMPAT_CSUM_V2: u32 = 0x0000_0008;
9pub const JBD2_FEATURE_INCOMPAT_CSUM_V3: u32 = 0x0000_0010;
10
11// ── helpers ──────────────────────────────────────────────────────────────────
12
13#[inline]
14fn be32(buf: &[u8], off: usize) -> u32 {
15    u32::from_be_bytes(buf[off..off + 4].try_into().unwrap())
16}
17
18#[inline]
19fn be64(buf: &[u8], off: usize) -> u64 {
20    u64::from_be_bytes(buf[off..off + 8].try_into().unwrap())
21}
22
23fn check_len(buf: &[u8], need: usize, structure: &'static str) -> Result<()> {
24    if buf.len() < need {
25        Err(Ext4Error::TooShort {
26            structure,
27            expected: need,
28            found: buf.len(),
29        })
30    } else {
31        Ok(())
32    }
33}
34
35// ── JournalBlockType ─────────────────────────────────────────────────────────
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum JournalBlockType {
39    Descriptor,
40    Commit,
41    SuperblockV1,
42    SuperblockV2,
43    Revoke,
44    Unknown(u32),
45}
46
47impl From<u32> for JournalBlockType {
48    fn from(v: u32) -> Self {
49        match v {
50            1 => Self::Descriptor,
51            2 => Self::Commit,
52            3 => Self::SuperblockV1,
53            4 => Self::SuperblockV2,
54            5 => Self::Revoke,
55            other => Self::Unknown(other),
56        }
57    }
58}
59
60// ── JournalHeader ─────────────────────────────────────────────────────────────
61
62/// Common 12-byte header present at the start of every JBD2 block.
63#[derive(Debug, Clone)]
64pub struct JournalHeader {
65    pub magic: u32,
66    pub block_type: JournalBlockType,
67    pub sequence: u32,
68}
69
70impl JournalHeader {
71    pub fn parse(buf: &[u8]) -> Result<Self> {
72        check_len(buf, 12, "JournalHeader")?;
73        let magic = be32(buf, 0);
74        if magic != JOURNAL_MAGIC {
75            return Err(Ext4Error::JournalCorrupt(format!(
76                "bad journal magic: 0x{magic:08X}"
77            )));
78        }
79        Ok(Self {
80            magic,
81            block_type: JournalBlockType::from(be32(buf, 4)),
82            sequence: be32(buf, 8),
83        })
84    }
85}
86
87// ── JournalSuperblock ─────────────────────────────────────────────────────────
88
89/// JBD2 superblock (journal block 0).
90///
91/// Offsets (all big-endian):
92///   0x00 header (12 bytes)
93///   0x0C block_size
94///   0x10 max_len
95///   0x14 first
96///   0x18 sequence
97///   0x1C start
98///   0x20 errno
99///   0x24 feature_compat
100///   0x28 feature_incompat
101///   0x2C feature_ro_compat
102///   0x30 uuid (16 bytes)
103///   0x40 nr_users
104///   0x44 dynsuper
105///   0x48 max_transaction
106///   0x4C max_trans_data
107///   0x50 checksum_type (1 byte)
108///   0xFC checksum (u32)
109#[derive(Debug, Clone)]
110pub struct JournalSuperblock {
111    pub header: JournalHeader,
112    pub block_size: u32,
113    pub max_len: u32,
114    pub first: u32,
115    pub sequence: u32,
116    pub start: u32,
117    pub errno: u32,
118    pub feature_compat: u32,
119    pub feature_incompat: u32,
120    pub feature_ro_compat: u32,
121    pub uuid: [u8; 16],
122    pub nr_users: u32,
123    pub checksum_type: u8,
124    pub checksum: u32,
125}
126
127impl JournalSuperblock {
128    pub fn parse(buf: &[u8]) -> Result<Self> {
129        check_len(buf, 0x100, "JournalSuperblock")?;
130        let header = JournalHeader::parse(buf)?;
131        let mut uuid = [0u8; 16];
132        uuid.copy_from_slice(&buf[0x30..0x40]);
133        Ok(Self {
134            header,
135            block_size: be32(buf, 0x0C),
136            max_len: be32(buf, 0x10),
137            first: be32(buf, 0x14),
138            sequence: be32(buf, 0x18),
139            start: be32(buf, 0x1C),
140            errno: be32(buf, 0x20),
141            feature_compat: be32(buf, 0x24),
142            feature_incompat: be32(buf, 0x28),
143            feature_ro_compat: be32(buf, 0x2C),
144            uuid,
145            nr_users: be32(buf, 0x40),
146            checksum_type: buf[0x50],
147            checksum: be32(buf, 0xFC),
148        })
149    }
150
151    pub fn is_64bit(&self) -> bool {
152        self.feature_incompat & JBD2_FEATURE_INCOMPAT_64BIT != 0
153    }
154
155    pub fn has_csum_v3(&self) -> bool {
156        self.feature_incompat & JBD2_FEATURE_INCOMPAT_CSUM_V3 != 0
157    }
158
159    pub fn has_csum_v2(&self) -> bool {
160        self.feature_incompat & JBD2_FEATURE_INCOMPAT_CSUM_V2 != 0
161    }
162}
163
164// ── JournalBlockTag ───────────────────────────────────────────────────────────
165
166/// A block tag inside a descriptor block.
167///
168/// v3 layout (16 bytes, same_uuid=true) or 32 bytes (same_uuid=false):
169///   0x00  blocknr_lo (u32)
170///   0x04  flags (u32)  bit0=escaped, bit1=same_uuid, bit3=last_tag
171///   0x08  blocknr_hi (u32, only meaningful when is_64bit)
172///   0x0C  checksum (u32)
173///   0x10  uuid (16 bytes, present only when same_uuid=false)
174#[derive(Debug, Clone)]
175pub struct JournalBlockTag {
176    pub blocknr: u64,
177    pub checksum: u32,
178    pub escaped: bool,
179    pub same_uuid: bool,
180    pub last_tag: bool,
181    /// Byte size consumed by this tag in the descriptor block.
182    pub tag_size: usize,
183}
184
185impl JournalBlockTag {
186    pub fn parse_v3(buf: &[u8], is_64bit: bool) -> Self {
187        let blocknr_lo = u64::from(be32(buf, 0x00));
188        let flags = be32(buf, 0x04);
189        let escaped = flags & 0x01 != 0;
190        let same_uuid = flags & 0x02 != 0;
191        let last_tag = flags & 0x08 != 0;
192        let blocknr_hi = if is_64bit {
193            u64::from(be32(buf, 0x08))
194        } else {
195            0
196        };
197        let blocknr = (blocknr_hi << 32) | blocknr_lo;
198        let checksum = be32(buf, 0x0C);
199        // 16-byte base + 16-byte UUID when uuid is not shared
200        let tag_size = if same_uuid { 16 } else { 32 };
201        Self {
202            blocknr,
203            checksum,
204            escaped,
205            same_uuid,
206            last_tag,
207            tag_size,
208        }
209    }
210}
211
212// ── JournalCommit ─────────────────────────────────────────────────────────────
213
214/// Commit block — marks end of a complete transaction.
215///
216/// Offsets (beyond the 12-byte header):
217///   0x0C  checksum_type (u8)
218///   0x0D  checksum_size (u8)
219///   0x30  commit_seconds (i64 BE)
220///   0x38  commit_nanoseconds (u32 BE)
221#[derive(Debug, Clone)]
222pub struct JournalCommit {
223    pub sequence: u32,
224    pub commit_seconds: i64,
225    pub commit_nanoseconds: u32,
226}
227
228impl JournalCommit {
229    pub fn parse(buf: &[u8]) -> Result<Self> {
230        check_len(buf, 0x3C, "JournalCommit")?;
231        let header = JournalHeader::parse(buf)?;
232        Ok(Self {
233            sequence: header.sequence,
234            commit_seconds: be64(buf, 0x30) as i64,
235            commit_nanoseconds: be32(buf, 0x38),
236        })
237    }
238}
239
240// ── JournalRevoke ─────────────────────────────────────────────────────────────
241
242/// Revoke block — lists blocks whose old versions must not be replayed.
243///
244/// Layout:
245///   0x00  header (12 bytes)
246///   0x0C  count (u32) — total byte length of revoke data (including header)
247///   0x10  revoked block numbers (u32 or u64 each, depending on 64bit flag)
248#[derive(Debug, Clone)]
249pub struct JournalRevoke {
250    pub sequence: u32,
251    pub revoked_blocks: Vec<u64>,
252}
253
254impl JournalRevoke {
255    pub fn parse(buf: &[u8], is_64bit: bool) -> Result<Self> {
256        check_len(buf, 16, "JournalRevoke")?;
257        let header = JournalHeader::parse(buf)?;
258        let byte_count = be32(buf, 12) as usize;
259        check_len(buf, byte_count, "JournalRevoke data")?;
260        let entry_size = if is_64bit { 8usize } else { 4 };
261        let data = &buf[16..byte_count];
262        let mut revoked_blocks = Vec::with_capacity(data.len() / entry_size);
263        let mut off = 0;
264        while off + entry_size <= data.len() {
265            let blk = if is_64bit {
266                be64(data, off)
267            } else {
268                u64::from(be32(data, off))
269            };
270            revoked_blocks.push(blk);
271            off += entry_size;
272        }
273        Ok(Self {
274            sequence: header.sequence,
275            revoked_blocks,
276        })
277    }
278}
279
280// ── Tests ─────────────────────────────────────────────────────────────────────
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285
286    #[test]
287    fn parse_journal_header() {
288        let mut buf = vec![0u8; 12];
289        buf[0..4].copy_from_slice(&0xC03B_3998u32.to_be_bytes());
290        buf[4..8].copy_from_slice(&1u32.to_be_bytes()); // descriptor
291        buf[8..12].copy_from_slice(&42u32.to_be_bytes());
292        let hdr = JournalHeader::parse(&buf).unwrap();
293        assert_eq!(hdr.magic, JOURNAL_MAGIC);
294        assert_eq!(hdr.block_type, JournalBlockType::Descriptor);
295        assert_eq!(hdr.sequence, 42);
296    }
297
298    #[test]
299    fn reject_bad_journal_magic() {
300        let buf = vec![0u8; 12];
301        let err = JournalHeader::parse(&buf).unwrap_err();
302        assert!(matches!(err, crate::error::Ext4Error::JournalCorrupt(_)));
303    }
304
305    #[test]
306    fn parse_journal_superblock() {
307        let mut buf = vec![0u8; 1024];
308        buf[0..4].copy_from_slice(&JOURNAL_MAGIC.to_be_bytes());
309        buf[4..8].copy_from_slice(&4u32.to_be_bytes()); // sb_v2
310        buf[8..12].copy_from_slice(&1u32.to_be_bytes());
311        buf[0x0C..0x10].copy_from_slice(&4096u32.to_be_bytes()); // block_size
312        buf[0x10..0x14].copy_from_slice(&1024u32.to_be_bytes()); // max_len
313        buf[0x14..0x18].copy_from_slice(&1u32.to_be_bytes()); // first
314        let jsb = JournalSuperblock::parse(&buf).unwrap();
315        assert_eq!(jsb.block_size, 4096);
316        assert_eq!(jsb.max_len, 1024);
317        assert_eq!(jsb.first, 1);
318    }
319
320    #[test]
321    fn parse_block_tag_v3() {
322        let mut buf = vec![0u8; 16];
323        buf[0..4].copy_from_slice(&500u32.to_be_bytes()); // blocknr
324        buf[4..8].copy_from_slice(&0x08u32.to_be_bytes()); // flags: LAST_TAG
325        buf[12..16].copy_from_slice(&0xDEAD_BEEFu32.to_be_bytes());
326        let tag = JournalBlockTag::parse_v3(&buf, false);
327        assert_eq!(tag.blocknr, 500);
328        assert!(tag.last_tag);
329        assert!(!tag.escaped);
330    }
331
332    #[test]
333    fn journal_superblock_feature_flags() {
334        let mut buf = vec![0u8; 1024];
335        buf[0..4].copy_from_slice(&0xC03B_3998u32.to_be_bytes());
336        buf[4..8].copy_from_slice(&4u32.to_be_bytes()); // SuperblockV2
337        buf[8..12].copy_from_slice(&1u32.to_be_bytes()); // sequence
338        buf[0x0C..0x10].copy_from_slice(&1024u32.to_be_bytes()); // block_size
339        buf[0x10..0x14].copy_from_slice(&100u32.to_be_bytes()); // max_len
340        buf[0x14..0x18].copy_from_slice(&1u32.to_be_bytes()); // first_block
341                                                              // Set feature_incompat with 64BIT and CSUM_V3
342        let features: u32 = JBD2_FEATURE_INCOMPAT_64BIT | JBD2_FEATURE_INCOMPAT_CSUM_V3;
343        buf[0x28..0x2C].copy_from_slice(&features.to_be_bytes());
344
345        let jsb = JournalSuperblock::parse(&buf).unwrap();
346        assert!(jsb.is_64bit());
347        assert!(jsb.has_csum_v3());
348        // CSUM_V2 flag is NOT set
349        assert!(!jsb.has_csum_v2());
350    }
351
352    #[test]
353    fn journal_superblock_csum_v2_only() {
354        let mut buf = vec![0u8; 1024];
355        buf[0..4].copy_from_slice(&0xC03B_3998u32.to_be_bytes());
356        buf[4..8].copy_from_slice(&4u32.to_be_bytes()); // SuperblockV2
357        buf[8..12].copy_from_slice(&1u32.to_be_bytes());
358        buf[0x0C..0x10].copy_from_slice(&1024u32.to_be_bytes());
359        buf[0x10..0x14].copy_from_slice(&100u32.to_be_bytes());
360        buf[0x14..0x18].copy_from_slice(&1u32.to_be_bytes());
361        let features: u32 = JBD2_FEATURE_INCOMPAT_CSUM_V2; // CSUM_V2 only
362        buf[0x28..0x2C].copy_from_slice(&features.to_be_bytes());
363
364        let jsb = JournalSuperblock::parse(&buf).unwrap();
365        assert!(!jsb.is_64bit());
366        assert!(!jsb.has_csum_v3());
367        assert!(jsb.has_csum_v2());
368    }
369
370    #[test]
371    fn parse_commit_block() {
372        let mut buf = vec![0u8; 64];
373        buf[0..4].copy_from_slice(&JOURNAL_MAGIC.to_be_bytes());
374        buf[4..8].copy_from_slice(&2u32.to_be_bytes()); // commit
375        buf[8..12].copy_from_slice(&99u32.to_be_bytes());
376        buf[0x30..0x38].copy_from_slice(&1_700_000_000u64.to_be_bytes());
377        buf[0x38..0x3C].copy_from_slice(&500_000_000u32.to_be_bytes());
378        let commit = JournalCommit::parse(&buf).unwrap();
379        assert_eq!(commit.sequence, 99);
380        assert_eq!(commit.commit_seconds, 1_700_000_000);
381        assert_eq!(commit.commit_nanoseconds, 500_000_000);
382    }
383}