Skip to main content

fs_ext4/
jbd2.rs

1//! JBD2 journal superblock parser.
2//!
3//! Spec: kernel.org/doc/html/latest/filesystems/ext4/journal.html.
4//!
5//! JBD2 writes journal metadata in **big-endian**, unlike the ext4 filesystem
6//! body which is little-endian. Every block in the journal begins with a
7//! 12-byte `journal_header_t` whose magic is `0xc03b3998`. Block-type 3 or 4
8//! identifies the superblock (v1 / v2). The superblock is at journal block 0.
9//!
10//! Locating the journal: `sb.journal_inode` (usually 8) names an inode whose
11//! extent tree maps the journal file. Journal block N = logical block N of
12//! that inode; convert to a physical fs block via the extent tree.
13//!
14//! This module parses the journal superblock. Walking transactions is
15//! [`crate::journal`], replaying them [`crate::journal_apply`], and writing
16//! them [`crate::journal_writer`].
17//!
18//! Layout summary:
19//! ```text
20//!   0x0000 journal_header_t (12 bytes)
21//!            __be32 h_magic       = 0xc03b3998
22//!            __be32 h_blocktype   = 3 (V1) or 4 (V2)
23//!            __be32 h_sequence
24//!   0x000C __be32 s_blocksize   (journal block size)
25//!   0x0010 __be32 s_maxlen      (total blocks in journal)
26//!   0x0014 __be32 s_first       (first block of log information)
27//!   0x0018 __be32 s_sequence    (first commit ID expected on next replay)
28//!   0x001C __be32 s_start       (block number of the log start; 0 = clean)
29//!   0x0020 __be32 s_errno
30//!   --- V2 fields begin (0x0024+) ---
31//!   0x0024 __be32 s_feature_compat
32//!   0x0028 __be32 s_feature_incompat
33//!   0x002C __be32 s_feature_ro_compat
34//!   0x0030 u8[16] s_uuid
35//!   0x0040 __be32 s_nr_users
36//!   0x0044 __be32 s_dynsuper
37//!   0x0048 __be32 s_max_transaction
38//!   0x004C __be32 s_max_trans_data
39//!   0x0050 u8     s_checksum_type
40//!   0x0051 u8[3]  padding
41//!   0x0054 __be32 s_num_fc_blks
42//!   0x0058 __be32 s_head
43//!   0x005C u8[160] padding
44//!   0x00FC __be32 s_checksum
45//!   0x0100 u8[16*48] s_users
46//!   0x0400 end
47//! ```
48
49use crate::error::{Error, Result};
50use crate::fs::Filesystem;
51use crate::inode::Inode;
52
53/// JBD2 header magic — big-endian `0xc0 0x3b 0x39 0x98` on disk.
54pub const JBD2_MAGIC_NUMBER: u32 = 0xc03b_3998;
55
56/// Header block-type values.
57pub const JBD2_DESCRIPTOR_BLOCK: u32 = 1;
58pub const JBD2_COMMIT_BLOCK: u32 = 2;
59pub const JBD2_SUPERBLOCK_V1: u32 = 3;
60pub const JBD2_SUPERBLOCK_V2: u32 = 4;
61pub const JBD2_REVOKE_BLOCK: u32 = 5;
62
63bitflags::bitflags! {
64    /// `s_feature_incompat` — kernel must refuse to replay if any unknown bit set.
65    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
66    pub struct JbdIncompat: u32 {
67        const REVOKE        = 0x0000_0001;
68        const BIT64         = 0x0000_0002;
69        const ASYNC_COMMIT  = 0x0000_0004;
70        const CSUM_V2       = 0x0000_0008;
71        const CSUM_V3       = 0x0000_0010;
72        const FAST_COMMIT   = 0x0000_0020;
73    }
74}
75
76/// Parsed JBD2 superblock (read-only view).
77#[derive(Debug, Clone)]
78pub struct JournalSuperblock {
79    /// Header block-type (`JBD2_SUPERBLOCK_V1` or `_V2`).
80    pub block_type: u32,
81    /// Next expected sequence number from the header (not s_sequence).
82    pub header_sequence: u32,
83    /// Journal block size in bytes.
84    pub block_size: u32,
85    /// Total blocks in the journal.
86    pub max_len: u32,
87    /// First block of log information (typically 1 — block 0 is the sb).
88    pub first: u32,
89    /// First commit ID expected when replay starts.
90    pub sequence: u32,
91    /// Start block of the log. **0 means clean unmount** (nothing to replay).
92    pub start: u32,
93    /// `s_errno`: the error an aborted journal records, 0 if healthy.
94    pub errno: u32,
95    /// V2-only feature flags (zero on V1 superblocks).
96    pub feature_compat: u32,
97    pub feature_incompat: u32,
98    pub feature_ro_compat: u32,
99    /// UUID identifying this journal (V2 only).
100    pub uuid: [u8; 16],
101    /// Number of filesystems sharing this journal (V2 only; always 1 for
102    /// internal ext4 journals).
103    pub nr_users: u32,
104    /// Checksum algorithm id (V2 only; 1 = crc32, 4 = crc32c).
105    pub checksum_type: u8,
106    /// Total fast-commit blocks (V2, INCOMPAT_FAST_COMMIT only).
107    pub num_fc_blocks: u32,
108    /// Superblock checksum as stored on disk (V2 only).
109    pub checksum: u32,
110}
111
112impl JournalSuperblock {
113    /// The journal format supported by the checked recovery/write lifecycle.
114    /// Feature refusal is a dependency capability boundary, not filesystem damage.
115    pub fn validate_plain_recovery(&self, block_size: u32, blocks_count: u64) -> Result<()> {
116        let supported = JbdIncompat::REVOKE.bits() | JbdIncompat::BIT64.bits();
117        if self.feature_compat != 0
118            || self.feature_ro_compat != 0
119            || self.feature_incompat & !supported != 0
120        {
121            return Err(Error::Unsupported("checked lifecycle supports plain JBD2 only; checksummed/async/fast-commit journals need separate qualification"));
122        }
123        if self.block_type != JBD2_SUPERBLOCK_V2
124            || self.block_size != block_size
125            || self.first != 1
126            || self.max_len <= self.first
127            || u64::from(self.max_len) > blocks_count
128            || (self.start != 0 && (self.start < self.first || self.start >= self.max_len))
129            || self.nr_users != 1
130        {
131            return Err(Error::Corrupt("unsupported internal journal geometry"));
132        }
133        if self.errno != 0 {
134            return Err(Error::Corrupt("journal records an outstanding error"));
135        }
136        Ok(())
137    }
138
139    /// True when the log has no outstanding transactions and replay is a no-op.
140    pub fn is_clean(&self) -> bool {
141        self.start == 0
142    }
143
144    /// Returns true if the journal uses v2 or v3 checksum tags.
145    /// Whether this journal uses the v3 tag layout.
146    ///
147    /// Distinct from [`Self::uses_csum_v2_or_v3`], which says whether it
148    /// is checksummed at all: the tag layout changes with V3 alone, and
149    /// confusing the two lays tags neither this crate's reader nor the
150    /// kernel can parse.
151    pub fn uses_csum_v3(&self) -> bool {
152        self.feature_incompat & JbdIncompat::CSUM_V3.bits() != 0
153    }
154
155    pub fn uses_csum_v2_or_v3(&self) -> bool {
156        self.feature_incompat & (JbdIncompat::CSUM_V2.bits() | JbdIncompat::CSUM_V3.bits()) != 0
157    }
158
159    /// Returns true if the journal uses 64-bit block numbers (tag.t_blocknr_high).
160    pub fn uses_64bit(&self) -> bool {
161        self.feature_incompat & JbdIncompat::BIT64.bits() != 0
162    }
163
164    /// Returns true if revoke blocks are used.
165    pub fn has_revoke(&self) -> bool {
166        self.feature_incompat & JbdIncompat::REVOKE.bits() != 0
167    }
168
169    /// The `s_feature_incompat` bits this driver does not handle. Replay
170    /// and writing both refuse a journal carrying any (#81): the field's
171    /// own rule. `ASYNC_COMMIT` makes a transaction's completeness rest on
172    /// checksums alone, and `FAST_COMMIT` keeps committed updates in an
173    /// area the walker never visits.
174    pub fn unsupported_incompat(&self) -> u32 {
175        self.feature_incompat & !SUPPORTED_JBD_INCOMPAT
176    }
177
178    /// Why this journal's checksum declaration is one that cannot be
179    /// honoured, or `None` when it names a single, usable scheme.
180    ///
181    /// A journal declares at most one way of checksumming its blocks: the
182    /// v1 commit-block checksum (compat bit 0x1), or the per-block v2
183    /// (incompat 0x8) or v3 (incompat 0x10) scheme, whose checksums are
184    /// crc32c (`s_checksum_type` 4). journal.html lists the bits and the
185    /// type codes; which combinations are refused was observed in the
186    /// harness VM by patching the superblock of a clean journal (tests/
187    /// jbd2_layouts_oracle.rs): the kernel will not load, and `e2fsck -fn`
188    /// calls "Journal superblock is corrupt", exactly the three cases below.
189    /// A journal without v2 or v3 is accepted whatever its type byte says.
190    ///
191    /// Replay and the writer each support v2 and v3 one at a time, so a
192    /// journal naming both would otherwise pass the incompat check and be
193    /// read with a layout its declaration does not name.
194    pub fn checksum_declaration_error(&self) -> Option<&'static str> {
195        let has = |bit: JbdIncompat| self.feature_incompat & bit.bits() != 0;
196        let per_block = [has(JbdIncompat::CSUM_V2), has(JbdIncompat::CSUM_V3)];
197        let commit_only = self.feature_compat & JBD2_FEATURE_COMPAT_CHECKSUM != 0;
198        match per_block {
199            [false, false] => None,
200            [true, true] => Some("journal declares both CSUM_V2 and CSUM_V3"),
201            _ if commit_only => Some("journal declares v1 checksums alongside CSUM_V2/V3"),
202            _ if self.checksum_type != JBD2_CRC32C_CHKSUM => {
203                Some("journal checksum type is not crc32c")
204            }
205            _ => None,
206        }
207    }
208
209    /// The journal checksum seed: the crc32c of the journal's UUID, from
210    /// `~0`. Every transaction-block checksum starts from it.
211    pub fn csum_seed(&self) -> u32 {
212        crate::checksum::linux_crc32c(!0, &self.uuid)
213    }
214
215    /// Bytes per descriptor-block tag, not counting the 16-byte UUID that
216    /// follows a tag without the "same UUID" flag.
217    ///
218    /// journal.html describes two tag records: the v3 one (block number,
219    /// flags, high block number, 32-bit checksum -- always all four
220    /// words) and the older one (block number, 16-bit checksum, 16-bit
221    /// flags, and the high block number only under `64BIT`). It does not
222    /// say the older record's checksum slot exists only under v2; that was
223    /// measured on journals `debugfs jo`/`jw` wrote in the harness VM
224    /// (tests/jbd2_layouts_oracle.rs), where the distance between
225    /// consecutive tags is exactly this table.
226    pub fn tag_bytes(&self) -> usize {
227        let per_block_scheme = if self.uses_csum_v3() {
228            TagChecksum::V3
229        } else if self.feature_incompat & JbdIncompat::CSUM_V2.bits() != 0 {
230            TagChecksum::V2
231        } else {
232            TagChecksum::None
233        };
234        match (per_block_scheme, self.uses_64bit()) {
235            (TagChecksum::None, false) => 8,
236            (TagChecksum::None, true) => 12,
237            (TagChecksum::V2, false) => 10,
238            (TagChecksum::V2, true) => 14,
239            (TagChecksum::V3, _) => 16,
240        }
241    }
242}
243
244/// Which per-block checksum a descriptor tag carries, for [`JournalSuperblock::tag_bytes`].
245#[derive(Clone, Copy)]
246enum TagChecksum {
247    None,
248    V2,
249    V3,
250}
251
252/// `JBD2_FEATURE_COMPAT_CHECKSUM`: the v1 commit-block checksum.
253pub const JBD2_FEATURE_COMPAT_CHECKSUM: u32 = 0x1;
254
255/// `JBD2_CRC32C_CHKSUM`, the one `s_checksum_type` CSUM_V2/V3 allow.
256pub const JBD2_CRC32C_CHKSUM: u8 = 4;
257
258/// Journal incompat bits this driver replays and writes.
259pub const SUPPORTED_JBD_INCOMPAT: u32 = JbdIncompat::REVOKE.bits()
260    | JbdIncompat::BIT64.bits()
261    | JbdIncompat::CSUM_V2.bits()
262    | JbdIncompat::CSUM_V3.bits();
263
264/// Bytes a checksummed journal reserves at the end of a descriptor or
265/// revoke block for its `t_checksum` / `r_checksum` tail.
266pub const BLOCK_TAIL_BYTES: usize = 4;
267
268/// Offset of `h_chksum[0]` in a commit block (after the 12-byte header,
269/// `h_chksum_type`, `h_chksum_size` and two bytes of padding).
270pub const COMMIT_CHECKSUM_AT: usize = 16;
271
272/// A tag's checksum: crc32c of the big-endian transaction sequence then
273/// the journal copy of the block, from the journal's seed (the tag's
274/// `t_checksum` in the format documentation). CSUM_V3 stores all 32 bits;
275/// CSUM_V2 the
276/// low 16.
277pub fn tag_checksum(seed: u32, sequence: u32, data: &[u8]) -> u32 {
278    let seq = crate::checksum::linux_crc32c(seed, &sequence.to_be_bytes());
279    crate::checksum::linux_crc32c(seq, data)
280}
281
282/// A descriptor or revoke block's tail checksum: crc32c of the whole
283/// block with the last four bytes taken as zero (the format
284/// documentation's `jbd2_journal_block_tail` and `r_checksum`).
285pub fn block_tail_checksum(seed: u32, block: &[u8]) -> u32 {
286    let body = block.len().saturating_sub(BLOCK_TAIL_BYTES);
287    let crc = crate::checksum::linux_crc32c(seed, &block[..body]);
288    crate::checksum::linux_crc32c(crc, &[0u8; BLOCK_TAIL_BYTES])
289}
290
291/// A commit block's checksum: crc32c of the whole block with `h_chksum[0]`
292/// taken as zero (the format documentation's `h_chksum`).
293pub fn commit_block_checksum(seed: u32, block: &[u8]) -> u32 {
294    let at = COMMIT_CHECKSUM_AT;
295    let crc = crate::checksum::linux_crc32c(seed, &block[..at]);
296    let crc = crate::checksum::linux_crc32c(crc, &[0u8; 4]);
297    crate::checksum::linux_crc32c(crc, &block[at + 4..])
298}
299
300impl JournalSuperblock {
301    /// Parse a journal superblock from its 1024-byte on-disk representation.
302    /// `raw` should be at least 1024 bytes (block-size bytes in practice); we
303    /// only read up to offset 0x100.
304    pub fn parse(raw: &[u8]) -> Result<Self> {
305        if raw.len() < 0x100 {
306            return Err(Error::Corrupt("jbd2 superblock buffer too small"));
307        }
308
309        let h_magic = u32::from_be_bytes(raw[0x00..0x04].try_into().unwrap());
310        if h_magic != JBD2_MAGIC_NUMBER {
311            return Err(Error::Corrupt("jbd2 superblock magic mismatch"));
312        }
313        let block_type = u32::from_be_bytes(raw[0x04..0x08].try_into().unwrap());
314        if block_type != JBD2_SUPERBLOCK_V1 && block_type != JBD2_SUPERBLOCK_V2 {
315            return Err(Error::Corrupt("jbd2 header is not a superblock block type"));
316        }
317        let header_sequence = u32::from_be_bytes(raw[0x08..0x0C].try_into().unwrap());
318
319        let block_size = u32::from_be_bytes(raw[0x0C..0x10].try_into().unwrap());
320        let max_len = u32::from_be_bytes(raw[0x10..0x14].try_into().unwrap());
321        let first = u32::from_be_bytes(raw[0x14..0x18].try_into().unwrap());
322        let sequence = u32::from_be_bytes(raw[0x18..0x1C].try_into().unwrap());
323        let start = u32::from_be_bytes(raw[0x1C..0x20].try_into().unwrap());
324        let errno = u32::from_be_bytes(raw[0x20..0x24].try_into().unwrap());
325
326        let mut out = Self {
327            block_type,
328            header_sequence,
329            block_size,
330            max_len,
331            first,
332            sequence,
333            start,
334            errno,
335            feature_compat: 0,
336            feature_incompat: 0,
337            feature_ro_compat: 0,
338            uuid: [0; 16],
339            nr_users: 0,
340            checksum_type: 0,
341            num_fc_blocks: 0,
342            checksum: 0,
343        };
344
345        // V2 extends with the dynamic-journal-features section.
346        if block_type == JBD2_SUPERBLOCK_V2 {
347            out.feature_compat = u32::from_be_bytes(raw[0x24..0x28].try_into().unwrap());
348            out.feature_incompat = u32::from_be_bytes(raw[0x28..0x2C].try_into().unwrap());
349            out.feature_ro_compat = u32::from_be_bytes(raw[0x2C..0x30].try_into().unwrap());
350            out.uuid.copy_from_slice(&raw[0x30..0x40]);
351            out.nr_users = u32::from_be_bytes(raw[0x40..0x44].try_into().unwrap());
352            // 0x44 s_dynsuper ignored (never used by upstream)
353            // 0x48 s_max_transaction / 0x4C s_max_trans_data ignored (hints only)
354            out.checksum_type = raw[0x50];
355            // 0x51..0x54 padding
356            out.num_fc_blocks = u32::from_be_bytes(raw[0x54..0x58].try_into().unwrap());
357            // 0x58 s_head / padding
358            out.checksum = u32::from_be_bytes(raw[0xFC..0x100].try_into().unwrap());
359        }
360
361        Ok(out)
362    }
363}
364
365/// Map a journal-relative block number to its physical fs block. Journal
366/// block 0 contains the superblock.
367///
368/// Flavor-aware via [`crate::indirect::map_logical_any`]: ext4 journals
369/// (whose journal inode carries `EXT4_EXTENTS_FL`) traverse the extent
370/// tree; ext3 journals (legacy direct/indirect block pointers) walk the
371/// indirect tree. Both schemes return `Ok(Some(physical))` for mapped
372/// blocks, `Ok(None)` for sparse holes (which a healthy journal should
373/// never have, but we don't enforce that here).
374pub fn journal_block_to_physical(
375    fs: &Filesystem,
376    journal_inode: &Inode,
377    journal_block: u64,
378) -> Result<Option<u64>> {
379    crate::indirect::map_logical_any(
380        &journal_inode.block,
381        journal_inode.flags,
382        fs.dev.as_ref(),
383        fs.sb.block_size(),
384        journal_block,
385    )
386}
387
388/// Read and parse the JBD2 superblock at block 0 of the journal.
389///
390/// Flow:
391///   1. Look up `sb.journal_inode` (typically ino 8).
392///   2. Map journal logical block 0 → physical fs block via its extent tree.
393///   3. Read the block (one fs block; the JBD2 sb fits within it).
394///   4. Parse the first 256 bytes as a `JournalSuperblock`.
395///
396/// Returns `Ok(None)` if `sb.journal_inode == 0` (filesystem lacks a journal).
397pub fn read_superblock(fs: &Filesystem) -> Result<Option<JournalSuperblock>> {
398    let jino_num = fs.sb.journal_inode;
399    if jino_num == 0 {
400        return Ok(None);
401    }
402
403    let raw = fs.read_inode_raw(jino_num)?;
404    let jinode = Inode::parse(&raw)?;
405
406    let phys = journal_block_to_physical(fs, &jinode, 0)?
407        .ok_or(Error::Corrupt("journal block 0 unmapped (sparse journal?)"))?;
408
409    let block_size = fs.sb.block_size() as u64;
410    let mut buf = vec![0u8; block_size as usize];
411    fs.dev.read_at(phys * block_size, &mut buf)?;
412    Ok(Some(JournalSuperblock::parse(&buf)?))
413}
414
415#[cfg(test)]
416mod tests {
417    use super::*;
418
419    fn jsb_with(incompat: u32) -> JournalSuperblock {
420        let mut jsb = JournalSuperblock::parse(&make_v2_sb()).unwrap();
421        jsb.feature_incompat = incompat;
422        jsb
423    }
424
425    /// The tag sizes measured on debugfs-written journals: the distance
426    /// between the second and third tag of a descriptor block, for each
427    /// scheme with and without 64BIT.
428    #[test]
429    fn tag_sizes_are_the_measured_table() {
430        let b64 = JbdIncompat::BIT64.bits();
431        let v2 = JbdIncompat::CSUM_V2.bits();
432        let v3 = JbdIncompat::CSUM_V3.bits();
433        for (incompat, bytes) in [
434            (0, 8),
435            (b64, 12),
436            (v2, 10),
437            (v2 | b64, 14),
438            (v3, 16),
439            (v3 | b64, 16),
440        ] {
441            assert_eq!(
442                jsb_with(incompat).tag_bytes(),
443                bytes,
444                "incompat {incompat:#x}"
445            );
446        }
447    }
448
449    /// The accept/refuse table the kernel and e2fsck gave for patched
450    /// journal superblocks (tests/jbd2_layouts_oracle.rs runs it live).
451    #[test]
452    fn checksum_declarations_follow_the_observed_table() {
453        let v2 = JbdIncompat::CSUM_V2.bits();
454        let v3 = JbdIncompat::CSUM_V3.bits();
455        let v1 = JBD2_FEATURE_COMPAT_CHECKSUM;
456        let with = |compat: u32, incompat: u32, kind: u8| {
457            let mut jsb = jsb_with(incompat);
458            jsb.feature_compat = compat;
459            jsb.checksum_type = kind;
460            jsb.checksum_declaration_error().is_none()
461        };
462        let accepted = [
463            (0, 0, 0),
464            (0, 0, 4),
465            (v1, 0, 0),
466            (v1, 0, 1),
467            (v1, 0, 4),
468            (0, v2, 4),
469            (0, v3, 4),
470        ];
471        let refused = [
472            (0, v2 | v3, 4),
473            (v1, v2, 4),
474            (v1, v3, 4),
475            (0, v2, 1),
476            (0, v3, 1),
477            (0, v3, 2),
478            (0, v3, 0),
479        ];
480        for (compat, incompat, kind) in accepted {
481            assert!(
482                with(compat, incompat, kind),
483                "{compat:#x}/{incompat:#x}/{kind}"
484            );
485        }
486        for (compat, incompat, kind) in refused {
487            assert!(
488                !with(compat, incompat, kind),
489                "{compat:#x}/{incompat:#x}/{kind}"
490            );
491        }
492    }
493
494    #[test]
495    fn async_commit_and_fast_commit_are_unsupported() {
496        let known =
497            JbdIncompat::REVOKE.bits() | JbdIncompat::BIT64.bits() | JbdIncompat::CSUM_V3.bits();
498        assert_eq!(jsb_with(known).unsupported_incompat(), 0);
499        for bit in [
500            JbdIncompat::ASYNC_COMMIT.bits(),
501            JbdIncompat::FAST_COMMIT.bits(),
502            0x8000_0000,
503        ] {
504            assert_eq!(jsb_with(known | bit).unsupported_incompat(), bit);
505        }
506    }
507
508    /// The tail and commit checksums treat their own field as zero, so what
509    /// is stored there does not change them, and every other byte does.
510    #[test]
511    fn block_checksums_skip_only_their_own_field() {
512        let seed = jsb_with(0).csum_seed();
513        let mut block = vec![0x5Au8; 4096];
514        let tail = block_tail_checksum(seed, &block);
515        block[4092..].fill(0xFF);
516        assert_eq!(block_tail_checksum(seed, &block), tail);
517        let commit = commit_block_checksum(seed, &block);
518        block[COMMIT_CHECKSUM_AT..COMMIT_CHECKSUM_AT + 4].fill(0x00);
519        assert_eq!(commit_block_checksum(seed, &block), commit);
520        block[4091] ^= 1;
521        assert_ne!(block_tail_checksum(seed, &block), tail);
522        assert_ne!(commit_block_checksum(seed, &block), commit);
523        assert_ne!(tag_checksum(seed, 1, &block), tag_checksum(seed, 2, &block));
524    }
525
526    /// Build a synthetic V2 superblock buffer for parser unit-testing.
527    fn make_v2_sb() -> Vec<u8> {
528        let mut buf = vec![0u8; 1024];
529        buf[0x00..0x04].copy_from_slice(&JBD2_MAGIC_NUMBER.to_be_bytes());
530        buf[0x04..0x08].copy_from_slice(&JBD2_SUPERBLOCK_V2.to_be_bytes());
531        buf[0x08..0x0C].copy_from_slice(&1u32.to_be_bytes()); // header seq
532        buf[0x0C..0x10].copy_from_slice(&4096u32.to_be_bytes()); // block_size
533        buf[0x10..0x14].copy_from_slice(&8192u32.to_be_bytes()); // max_len
534        buf[0x14..0x18].copy_from_slice(&1u32.to_be_bytes()); // first
535        buf[0x18..0x1C].copy_from_slice(&42u32.to_be_bytes()); // sequence
536        buf[0x1C..0x20].copy_from_slice(&0u32.to_be_bytes()); // start = 0 (clean)
537        buf[0x20..0x24].copy_from_slice(&0u32.to_be_bytes()); // errno
538        buf[0x24..0x28].copy_from_slice(&0u32.to_be_bytes()); // feat_compat
539        buf[0x28..0x2C].copy_from_slice(
540            &(JbdIncompat::REVOKE.bits() | JbdIncompat::CSUM_V3.bits()).to_be_bytes(),
541        );
542        buf[0x2C..0x30].copy_from_slice(&0u32.to_be_bytes()); // feat_ro
543        buf[0x30..0x40].copy_from_slice(&[0xAA; 16]);
544        buf[0x40..0x44].copy_from_slice(&1u32.to_be_bytes()); // nr_users
545        buf[0x50] = 4; // crc32c
546        buf[0xFC..0x100].copy_from_slice(&0xDEADBEEFu32.to_be_bytes());
547        buf
548    }
549
550    #[test]
551    fn parses_v2_superblock() {
552        let buf = make_v2_sb();
553        let sb = JournalSuperblock::parse(&buf).unwrap();
554        assert_eq!(sb.block_type, JBD2_SUPERBLOCK_V2);
555        assert_eq!(sb.block_size, 4096);
556        assert_eq!(sb.max_len, 8192);
557        assert_eq!(sb.first, 1);
558        assert_eq!(sb.sequence, 42);
559        assert!(sb.is_clean());
560        assert!(sb.has_revoke());
561        assert!(sb.uses_csum_v2_or_v3());
562        assert!(!sb.uses_64bit());
563        assert_eq!(sb.checksum_type, 4);
564        assert_eq!(sb.checksum, 0xDEADBEEF);
565    }
566
567    #[test]
568    fn rejects_bad_magic() {
569        let mut buf = make_v2_sb();
570        buf[0] = 0;
571        assert!(JournalSuperblock::parse(&buf).is_err());
572    }
573
574    #[test]
575    fn rejects_non_superblock_block_type() {
576        let mut buf = make_v2_sb();
577        buf[0x04..0x08].copy_from_slice(&JBD2_DESCRIPTOR_BLOCK.to_be_bytes());
578        assert!(JournalSuperblock::parse(&buf).is_err());
579    }
580
581    #[test]
582    fn v1_leaves_feature_fields_zero() {
583        let mut buf = make_v2_sb();
584        buf[0x04..0x08].copy_from_slice(&JBD2_SUPERBLOCK_V1.to_be_bytes());
585        // Even though we wrote feature bytes, parse skips them on V1.
586        let sb = JournalSuperblock::parse(&buf).unwrap();
587        assert_eq!(sb.feature_incompat, 0);
588        assert_eq!(sb.uuid, [0; 16]);
589        assert_eq!(sb.checksum, 0);
590    }
591
592    #[test]
593    fn dirty_sb_is_not_clean() {
594        let mut buf = make_v2_sb();
595        buf[0x1C..0x20].copy_from_slice(&100u32.to_be_bytes()); // start != 0
596        let sb = JournalSuperblock::parse(&buf).unwrap();
597        assert!(!sb.is_clean());
598        assert_eq!(sb.start, 100);
599    }
600}