Skip to main content

fs_ext4/
transaction.rs

1//! JBD2 transaction writer (E11).
2//!
3//! Takes the plan-layer outputs from E5 (bitmap), E7 (extent), E8 (dir), E9
4//! (htree), E10 (file) and serializes them into JBD2 journal blocks the
5//! caller writes to the journal inode. Produces, in this order:
6//!
7//!   1. One or more **descriptor groups**, when there are writes: a
8//!      descriptor block tagging as many dirty fs blocks as fit in it, then
9//!      a **data block** for each of its tags with the new content. A write
10//!      set larger than one descriptor holds spills into further groups, each
11//!      ending in its own LAST tag.
12//!   2. Zero or more **revoke blocks** for fs blocks whose earlier-journal
13//!      contents must NOT be replayed (typically: freed leaf/index blocks so
14//!      a crash during free doesn't restore stale pointers), as many as the
15//!      records need.
16//!   3. A **commit block** — the single marker that atomically makes the
17//!      transaction visible to recovery.
18//!
19//! `the_serialised_shape_is_descriptor_groups_then_revokes_then_commit`
20//! pins this shape.
21//!
22//! All JBD2 data is **big-endian** on disk (opposite of ext4 filesystem body).
23//!
24//! This module is the dual of [`crate::journal`] (the replay walker): given
25//! the same tag+data layout, `journal::walk` should reproduce the mutations
26//! this module serialised. The round-trip property is covered by tests.
27//!
28//! ### What we serialize vs. what we don't
29//!
30//! - **Serialize**: the journal blocks themselves (bytes you write to the
31//!   journal inode's logical block range).
32//! - **Do NOT serialize**: the actual final-location writes. The caller
33//!   sequences: write journal blocks → fsync → write final locations →
34//!   update `jsb.start` on next checkpoint. That write-ordering discipline
35//!   lives in the future E12+/capi write path; this module only produces
36//!   the journal-format bytes.
37//!
38//! ### Checksum scope
39//!
40//! A journal declaring CSUM_V2 or CSUM_V3 checksums three things, all seeded
41//! from the crc32c of its UUID: every tag (the sequence and the data block
42//! it names -- 32 bits in a v3 tag, the low 16 in a v2 one), the descriptor
43//! and revoke blocks (a four-byte tail over the rest of the block), and the
44//! commit block (`h_chksum[0]`). Linux's recovery takes a commit that fails
45//! its checksum for the end of the log, so a transaction written with zeros
46//! there is one only this crate would replay (#80).
47//!
48//! [`Transaction::commit_for`] lays the blocks out for a given journal and
49//! fills every checksum it declares. [`Transaction::commit`] knows only the
50//! flags the transaction was begun with and no seed: it lays out the same
51//! blocks with the checksum fields zero, which is right only for a journal
52//! without checksums.
53
54use crate::error::{Error, Result};
55use crate::jbd2::{
56    self, JbdIncompat, JournalSuperblock, BLOCK_TAIL_BYTES, COMMIT_CHECKSUM_AT, JBD2_COMMIT_BLOCK,
57    JBD2_DESCRIPTOR_BLOCK, JBD2_MAGIC_NUMBER, JBD2_REVOKE_BLOCK,
58};
59use crate::journal::{TAG_ESCAPED, TAG_LAST, TAG_SAME_UUID};
60
61/// One buffered write the transaction will journal.
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct JournaledBlock {
64    /// Target fs block (absolute — the same value that will end up in the
65    /// descriptor tag's `t_blocknr`).
66    pub fs_block: u64,
67    /// Full block contents (must be exactly `block_size` bytes).
68    pub bytes: Vec<u8>,
69}
70
71/// How a journal lays out and checksums its transaction blocks.
72#[derive(Debug, Clone, Copy)]
73struct Layout {
74    tag_bytes: usize,
75    uses_64bit: bool,
76    /// The tail reserved at the end of descriptor and revoke blocks.
77    tail_bytes: usize,
78    /// `Some(seed)` when the checksum fields are to be filled.
79    seed: Option<u32>,
80    /// CSUM_V3 (32-bit tag checksum) rather than CSUM_V2 (16-bit).
81    csum_v3: bool,
82}
83
84/// Transaction builder. Call [`Transaction::add_write`] to buffer a mutated
85/// fs block; call [`Transaction::add_revoke`] to mark an older block's
86/// contents as unreachable; call [`Transaction::commit`] to serialise.
87#[derive(Debug, Clone)]
88pub struct Transaction {
89    pub sequence: u32,
90    pub block_size: u32,
91    pub uses_64bit: bool,
92    pub uses_csum_v3: bool,
93    pub writes: Vec<JournaledBlock>,
94    pub revokes: Vec<u64>,
95}
96
97impl Transaction {
98    /// Begin a new transaction with the given sequence number (next value from
99    /// the JBD2 superblock).
100    pub fn begin(sequence: u32, block_size: u32, uses_64bit: bool, uses_csum_v3: bool) -> Self {
101        Self {
102            sequence,
103            block_size,
104            uses_64bit,
105            uses_csum_v3,
106            writes: Vec::new(),
107            revokes: Vec::new(),
108        }
109    }
110
111    /// Buffer a dirty fs block. Panics in debug if `bytes.len() != block_size`.
112    pub fn add_write(&mut self, fs_block: u64, bytes: Vec<u8>) -> Result<()> {
113        if bytes.len() != self.block_size as usize {
114            return Err(Error::Corrupt("add_write: bytes size != block_size"));
115        }
116        self.writes.push(JournaledBlock { fs_block, bytes });
117        Ok(())
118    }
119
120    /// Record a revoke — tell replay to ignore earlier writes to this fs block.
121    pub fn add_revoke(&mut self, fs_block: u64) {
122        self.revokes.push(fs_block);
123    }
124
125    /// Serialize the transaction into journal blocks. Returns a single flat
126    /// `Vec<Vec<u8>>` where each inner Vec is one `block_size` byte journal
127    /// block, in the order they should be written to the journal log:
128    ///
129    ///   [descriptor, data..., descriptor, data..., revoke..., commit]
130    ///
131    /// A descriptor block tags at most `tags_per_descriptor()` data blocks, so
132    /// a write set larger than that is split across several descriptor blocks,
133    /// each immediately followed by the data blocks it tags and each ending in
134    /// a tag with `TAG_LAST` set. Revoke records are chunked the same way, at
135    /// `revokes_per_block()` per block. A small transaction therefore still
136    /// produces the familiar `[descriptor, data_0 ..= data_N, revoke, commit]`.
137    ///
138    /// Empty transactions (no writes, no revokes) return a single commit
139    /// block — matches kernel's "empty transaction" handling.
140    ///
141    /// The checksum fields are left zero; a journal that declares checksums
142    /// needs [`Self::commit_for`].
143    pub fn commit(&self) -> Result<Vec<Vec<u8>>> {
144        let layout = if self.uses_csum_v3 {
145            Layout {
146                tag_bytes: 16,
147                uses_64bit: self.uses_64bit,
148                tail_bytes: BLOCK_TAIL_BYTES,
149                seed: None,
150                csum_v3: true,
151            }
152        } else {
153            Layout {
154                tag_bytes: if self.uses_64bit { 12 } else { 8 },
155                uses_64bit: self.uses_64bit,
156                tail_bytes: 0,
157                seed: None,
158                csum_v3: false,
159            }
160        };
161        self.serialize(layout)
162    }
163
164    /// Serialize for `jsb`'s journal, with every checksum it declares
165    /// filled in (#80). Refuses a journal whose incompat bits this driver
166    /// does not handle, and a transaction begun for a different block size
167    /// or tag width than `jsb` declares.
168    pub fn commit_for(&self, jsb: &JournalSuperblock) -> Result<Vec<Vec<u8>>> {
169        if jsb.unsupported_incompat() != 0 {
170            return Err(Error::Unsupported(
171                "journal declares an incompat feature this driver does not write",
172            ));
173        }
174        if let Some(why) = jsb.checksum_declaration_error() {
175            return Err(Error::Corrupt(why));
176        }
177        if self.block_size != jsb.block_size
178            || self.uses_64bit != jsb.uses_64bit()
179            || self.uses_csum_v3 != jsb.uses_csum_v3()
180        {
181            return Err(Error::InvalidArgument(
182                "transaction was begun for a different journal layout",
183            ));
184        }
185        let checksummed = jsb.uses_csum_v2_or_v3();
186        self.serialize(Layout {
187            tag_bytes: jsb.tag_bytes(),
188            uses_64bit: jsb.uses_64bit(),
189            tail_bytes: if checksummed { BLOCK_TAIL_BYTES } else { 0 },
190            seed: checksummed.then(|| jsb.csum_seed()),
191            csum_v3: jsb.feature_incompat & JbdIncompat::CSUM_V3.bits() != 0,
192        })
193    }
194
195    fn serialize(&self, layout: Layout) -> Result<Vec<Vec<u8>>> {
196        let mut out = Vec::new();
197
198        // A transaction may touch more blocks than a single descriptor can tag.
199        // JBD2 permits several descriptor blocks per transaction, each followed
200        // by its own data blocks and each ending in a LAST_TAG. Emit as many as
201        // the write set needs instead of failing the whole write.
202        if !self.writes.is_empty() {
203            for chunk in self.writes.chunks(self.tags_per_descriptor(layout)) {
204                out.push(self.build_descriptor_block(chunk, layout)?);
205                for w in chunk {
206                    out.push(logged_copy(&w.bytes));
207                }
208            }
209        }
210        if !self.revokes.is_empty() {
211            for chunk in self.revokes.chunks(self.revokes_per_block(layout)) {
212                out.push(self.build_revoke_block(chunk, layout)?);
213            }
214        }
215        out.push(self.build_commit_block(layout)?);
216        Ok(out)
217    }
218
219    /// How many tags fit in one descriptor block: after its 12-byte header,
220    /// and before the checksum tail a checksummed journal reserves.
221    fn tags_per_descriptor(&self, layout: Layout) -> usize {
222        let usable = self.block_size as usize - 12 - layout.tail_bytes;
223        (usable / layout.tag_bytes).max(1)
224    }
225
226    /// How many revoke records fit in one revoke block: after its header and
227    /// byte count, and before the checksum tail.
228    fn revokes_per_block(&self, layout: Layout) -> usize {
229        let record_size = if layout.uses_64bit { 8 } else { 4 };
230        let usable = self.block_size as usize - 16 - layout.tail_bytes;
231        (usable / record_size).max(1)
232    }
233
234    fn build_descriptor_block(&self, writes: &[JournaledBlock], layout: Layout) -> Result<Vec<u8>> {
235        let mut blk = vec![0u8; self.block_size as usize];
236        self.write_header(&mut blk, JBD2_DESCRIPTOR_BLOCK);
237
238        let tag_size = layout.tag_bytes;
239        let usable = blk.len() - layout.tail_bytes;
240        let mut pos = 12usize;
241        let last_idx = writes.len().saturating_sub(1);
242        for (i, w) in writes.iter().enumerate() {
243            let mut flags: u32 = if i == last_idx { TAG_LAST } else { 0 };
244            // Always set SAME_UUID so we don't have to carry a per-tag UUID
245            // (journal's own s_uuid is used implicitly).
246            flags |= TAG_SAME_UUID;
247            if needs_escape(&w.bytes) {
248                flags |= TAG_ESCAPED;
249            }
250
251            if pos + tag_size > usable {
252                return Err(Error::Corrupt("descriptor block overflow (too many tags)"));
253            }
254
255            let blocknr_lo = (w.fs_block & 0xFFFF_FFFF) as u32;
256            blk[pos..pos + 4].copy_from_slice(&blocknr_lo.to_be_bytes());
257            let checksum = layout
258                .seed
259                // Over the block as logged -- escaped -- since that is what
260                // recovery reads and verifies before it restores the magic.
261                .map(|seed| jbd2::tag_checksum(seed, self.sequence, &logged_copy(&w.bytes)));
262
263            if layout.csum_v3 {
264                // journal_block_tag3_t: t_blocknr, t_flags, t_blocknr_high,
265                // t_checksum -- 16 bytes whether or not the high half is used.
266                blk[pos + 4..pos + 8].copy_from_slice(&flags.to_be_bytes());
267                if layout.uses_64bit {
268                    let hi = (w.fs_block >> 32) as u32;
269                    blk[pos + 8..pos + 12].copy_from_slice(&hi.to_be_bytes());
270                }
271                if let Some(c) = checksum {
272                    blk[pos + 12..pos + 16].copy_from_slice(&c.to_be_bytes());
273                }
274            } else {
275                // Classical: __be16 t_checksum + __be16 t_flags
276                let c16 = checksum.map_or(0, |c| c as u16);
277                blk[pos + 4..pos + 6].copy_from_slice(&c16.to_be_bytes());
278                blk[pos + 6..pos + 8].copy_from_slice(&(flags as u16).to_be_bytes());
279                if layout.uses_64bit {
280                    let hi = (w.fs_block >> 32) as u32;
281                    blk[pos + 8..pos + 12].copy_from_slice(&hi.to_be_bytes());
282                }
283            }
284            pos += tag_size;
285        }
286        Self::seal_tail(&mut blk, layout);
287        Ok(blk)
288    }
289
290    /// Fill a descriptor or revoke block's checksum tail.
291    fn seal_tail(blk: &mut [u8], layout: Layout) {
292        if let Some(seed) = layout.seed {
293            let c = jbd2::block_tail_checksum(seed, blk);
294            let at = blk.len() - BLOCK_TAIL_BYTES;
295            blk[at..].copy_from_slice(&c.to_be_bytes());
296        }
297    }
298
299    fn build_revoke_block(&self, revokes: &[u64], layout: Layout) -> Result<Vec<u8>> {
300        let mut blk = vec![0u8; self.block_size as usize];
301        self.write_header(&mut blk, JBD2_REVOKE_BLOCK);
302
303        let record_size = if layout.uses_64bit { 8 } else { 4 };
304        let records_bytes = revokes.len() * record_size;
305        let total_bytes = 16 + records_bytes; // header(12) + count(4) + records
306        if total_bytes > blk.len() - layout.tail_bytes {
307            return Err(Error::Corrupt("revoke block overflow"));
308        }
309        blk[12..16].copy_from_slice(&(total_bytes as u32).to_be_bytes());
310        let mut pos = 16usize;
311        for &b in revokes {
312            if layout.uses_64bit {
313                blk[pos..pos + 8].copy_from_slice(&b.to_be_bytes());
314            } else {
315                blk[pos..pos + 4].copy_from_slice(&(b as u32).to_be_bytes());
316            }
317            pos += record_size;
318        }
319        Self::seal_tail(&mut blk, layout);
320        Ok(blk)
321    }
322
323    fn build_commit_block(&self, layout: Layout) -> Result<Vec<u8>> {
324        let mut blk = vec![0u8; self.block_size as usize];
325        self.write_header(&mut blk, JBD2_COMMIT_BLOCK);
326        if let Some(seed) = layout.seed {
327            let c = jbd2::commit_block_checksum(seed, &blk);
328            blk[COMMIT_CHECKSUM_AT..COMMIT_CHECKSUM_AT + 4].copy_from_slice(&c.to_be_bytes());
329        }
330        Ok(blk)
331    }
332
333    fn write_header(&self, blk: &mut [u8], block_type: u32) {
334        blk[0..4].copy_from_slice(&JBD2_MAGIC_NUMBER.to_be_bytes());
335        blk[4..8].copy_from_slice(&block_type.to_be_bytes());
336        blk[8..12].copy_from_slice(&self.sequence.to_be_bytes());
337    }
338}
339
340/// Whether a data block begins with the journal magic and so must be
341/// escaped in the log.
342///
343/// Recovery tells journal blocks from data by their first four bytes. A data
344/// block that begins with the magic, logged as it is, is one a recovery scan
345/// that reaches it -- the stale tail of an earlier, longer transaction, just
346/// past the last commit -- takes for a descriptor or commit block: a file's
347/// contents could then be replayed as a transaction of its own. JBD2 has the
348/// writer zero those four bytes in the logged copy and set `TAG_ESCAPED` on
349/// its tag; replay puts the magic back (format documentation, "Journal
350/// (jbd2)": the block tag's "escaped" flag).
351fn needs_escape(bytes: &[u8]) -> bool {
352    bytes.get(0..4) == Some(&JBD2_MAGIC_NUMBER.to_be_bytes()[..])
353}
354
355/// The copy of a data block that goes into the log: the block itself, with
356/// the magic zeroed when [`needs_escape`] says so. The final-location write
357/// keeps the original bytes.
358fn logged_copy(bytes: &[u8]) -> Vec<u8> {
359    let mut logged = bytes.to_vec();
360    if needs_escape(bytes) {
361        logged[0..4].fill(0);
362    }
363    logged
364}
365
366#[cfg(test)]
367mod tests {
368    use super::*;
369    use crate::jbd2::{JbdIncompat, JournalSuperblock, JBD2_SUPERBLOCK_V2};
370    use crate::journal::{ReplayEntry, ReplayPlan, RevokeEntry};
371
372    fn mk_tx(seq: u32) -> Transaction {
373        Transaction::begin(seq, 4096, false, false)
374    }
375
376    #[test]
377    fn empty_transaction_yields_only_commit() {
378        let tx = mk_tx(1);
379        let blocks = tx.commit().unwrap();
380        assert_eq!(blocks.len(), 1);
381        // The one block must be a commit with magic + type 2.
382        let magic = u32::from_be_bytes(blocks[0][0..4].try_into().unwrap());
383        let bt = u32::from_be_bytes(blocks[0][4..8].try_into().unwrap());
384        assert_eq!(magic, JBD2_MAGIC_NUMBER);
385        assert_eq!(bt, JBD2_COMMIT_BLOCK);
386    }
387
388    #[test]
389    fn two_block_txn_layout_is_desc_then_data_then_commit() {
390        let mut tx = mk_tx(42);
391        tx.add_write(100, vec![0xAA; 4096]).unwrap();
392        tx.add_write(200, vec![0xBB; 4096]).unwrap();
393        let blocks = tx.commit().unwrap();
394        assert_eq!(blocks.len(), 4); // desc + 2 data + commit
395                                     // blocks[0] = descriptor
396        let bt0 = u32::from_be_bytes(blocks[0][4..8].try_into().unwrap());
397        assert_eq!(bt0, JBD2_DESCRIPTOR_BLOCK);
398        // blocks[1..3] = data (no JBD header required; just payload)
399        assert_eq!(blocks[1][0], 0xAA);
400        assert_eq!(blocks[2][0], 0xBB);
401        // blocks[3] = commit
402        let bt3 = u32::from_be_bytes(blocks[3][4..8].try_into().unwrap());
403        assert_eq!(bt3, JBD2_COMMIT_BLOCK);
404    }
405
406    #[test]
407    fn descriptor_tags_are_big_endian_and_have_last_flag() {
408        let mut tx = mk_tx(7);
409        tx.add_write(0x1234_5678, vec![0u8; 4096]).unwrap();
410        let blocks = tx.commit().unwrap();
411        let desc = &blocks[0];
412        let blocknr_lo = u32::from_be_bytes(desc[12..16].try_into().unwrap());
413        assert_eq!(blocknr_lo, 0x1234_5678);
414        let flags_u16 = u16::from_be_bytes(desc[18..20].try_into().unwrap()) as u32;
415        assert_eq!(flags_u16 & TAG_LAST, TAG_LAST, "last tag must set TAG_LAST");
416        assert_eq!(
417            flags_u16 & TAG_SAME_UUID,
418            TAG_SAME_UUID,
419            "same_uuid expected"
420        );
421    }
422
423    #[test]
424    fn writes_beyond_one_descriptor_spill_into_a_second_descriptor_block() {
425        // 1 KiB blocks, classic 8-byte tags: (1024 - 12) / 8 = 126 tags fit.
426        let mut tx = Transaction::begin(9, 1024, false, false);
427        let per = 126;
428        for i in 0..(per + 1) {
429            tx.add_write(1000 + i as u64, vec![i as u8; 1024]).unwrap();
430        }
431        let blocks = tx.commit().unwrap();
432        // desc + 126 data + desc + 1 data + commit
433        assert_eq!(blocks.len(), 1 + per + 1 + 1 + 1);
434
435        let block_type = |b: &Vec<u8>| u32::from_be_bytes(b[4..8].try_into().unwrap());
436        assert_eq!(block_type(&blocks[0]), JBD2_DESCRIPTOR_BLOCK);
437        assert_eq!(block_type(&blocks[per + 1]), JBD2_DESCRIPTOR_BLOCK);
438        assert_eq!(block_type(&blocks[per + 3]), JBD2_COMMIT_BLOCK);
439        // data blocks sit directly after their own descriptor
440        assert_eq!(blocks[1][0], 0);
441        assert_eq!(blocks[per][0], (per - 1) as u8);
442        assert_eq!(blocks[per + 2][0], per as u8);
443
444        // both descriptors end in a LAST tag; the first one is full
445        let last_tag_flags = |desc: &Vec<u8>, idx: usize| {
446            u16::from_be_bytes(desc[12 + idx * 8 + 6..12 + idx * 8 + 8].try_into().unwrap()) as u32
447        };
448        assert_eq!(last_tag_flags(&blocks[0], per - 1) & TAG_LAST, TAG_LAST);
449        assert_eq!(
450            last_tag_flags(&blocks[0], 0) & TAG_LAST,
451            0,
452            "only the final tag is LAST"
453        );
454        assert_eq!(last_tag_flags(&blocks[per + 1], 0) & TAG_LAST, TAG_LAST);
455    }
456
457    #[test]
458    fn exactly_one_descriptor_worth_of_writes_stays_in_one_block() {
459        let mut tx = Transaction::begin(10, 1024, false, false);
460        let per = 126;
461        for i in 0..per {
462            tx.add_write(2000 + i as u64, vec![0u8; 1024]).unwrap();
463        }
464        let blocks = tx.commit().unwrap();
465        assert_eq!(blocks.len(), 1 + per + 1); // desc + data + commit, no spill
466    }
467
468    #[test]
469    fn revokes_beyond_one_block_spill_into_a_second_revoke_block() {
470        // 1 KiB blocks, 4-byte records: (1024 - 16) / 4 = 252 fit.
471        let mut tx = Transaction::begin(11, 1024, false, false);
472        let per = 252;
473        for i in 0..(per + 1) {
474            tx.add_revoke(5000 + i as u64);
475        }
476        let blocks = tx.commit().unwrap();
477        assert_eq!(blocks.len(), 3); // revoke + revoke + commit
478        let block_type = |b: &Vec<u8>| u32::from_be_bytes(b[4..8].try_into().unwrap());
479        assert_eq!(block_type(&blocks[0]), JBD2_REVOKE_BLOCK);
480        assert_eq!(block_type(&blocks[1]), JBD2_REVOKE_BLOCK);
481        assert_eq!(block_type(&blocks[2]), JBD2_COMMIT_BLOCK);
482        let count0 = u32::from_be_bytes(blocks[0][12..16].try_into().unwrap());
483        let count1 = u32::from_be_bytes(blocks[1][12..16].try_into().unwrap());
484        assert_eq!(count0, (16 + per * 4) as u32);
485        assert_eq!(count1, 16 + 4);
486    }
487
488    #[test]
489    fn revoke_block_serialises_count_and_records() {
490        let mut tx = mk_tx(3);
491        tx.add_revoke(1000);
492        tx.add_revoke(2000);
493        let blocks = tx.commit().unwrap();
494        assert_eq!(blocks.len(), 2); // revoke + commit (no writes)
495        let rev = &blocks[0];
496        let bt = u32::from_be_bytes(rev[4..8].try_into().unwrap());
497        assert_eq!(bt, JBD2_REVOKE_BLOCK);
498        let r_count = u32::from_be_bytes(rev[12..16].try_into().unwrap());
499        assert_eq!(r_count, 16 + 2 * 4);
500        let r0 = u32::from_be_bytes(rev[16..20].try_into().unwrap());
501        let r1 = u32::from_be_bytes(rev[20..24].try_into().unwrap());
502        assert_eq!(r0, 1000);
503        assert_eq!(r1, 2000);
504    }
505
506    /// The whole shape the module doc describes, at once: one or more
507    /// descriptor blocks each followed by its own data blocks, then zero or
508    /// more revoke blocks, then exactly one commit block, last.
509    #[test]
510    fn the_serialised_shape_is_descriptor_groups_then_revokes_then_commit() {
511        // 1 KiB blocks: 126 tags per descriptor, 252 records per revoke block.
512        let mut tx = Transaction::begin(12, 1024, false, false);
513        for i in 0..127u64 {
514            tx.add_write(3000 + i, vec![0x11; 1024]).unwrap();
515        }
516        for i in 0..253u64 {
517            tx.add_revoke(9000 + i);
518        }
519        let blocks = tx.commit().unwrap();
520
521        // 'D' descriptor, 'd' data, 'R' revoke, 'C' commit.
522        let shape: String = blocks
523            .iter()
524            .map(|b| {
525                if u32::from_be_bytes(b[0..4].try_into().unwrap()) != JBD2_MAGIC_NUMBER {
526                    return 'd';
527                }
528                match u32::from_be_bytes(b[4..8].try_into().unwrap()) {
529                    JBD2_DESCRIPTOR_BLOCK => 'D',
530                    JBD2_REVOKE_BLOCK => 'R',
531                    JBD2_COMMIT_BLOCK => 'C',
532                    other => panic!("unexpected block type {other}"),
533                }
534            })
535            .collect();
536        let want = format!("D{}DdRRC", "d".repeat(126));
537        assert_eq!(shape, want);
538    }
539
540    /// Round-trip: build a transaction, feed its output into journal::walk
541    /// via a stubbed superblock/inode, and verify the ReplayPlan names the
542    /// same fs blocks in the same order.
543    #[test]
544    fn round_trip_through_walk_style_parsing() {
545        // We can't invoke journal::walk directly without a Filesystem, but we
546        // can parse the descriptor manually using the same logic journal does.
547        let mut tx = mk_tx(100);
548        tx.add_write(500, vec![0x11; 4096]).unwrap();
549        tx.add_write(501, vec![0x22; 4096]).unwrap();
550        tx.add_write(900, vec![0x33; 4096]).unwrap();
551        let blocks = tx.commit().unwrap();
552        let desc = &blocks[0];
553        assert_eq!(
554            u32::from_be_bytes(desc[4..8].try_into().unwrap()),
555            JBD2_DESCRIPTOR_BLOCK
556        );
557
558        // Manually decode tags (classical, no 64bit, no v3) and confirm three
559        // entries naming 500, 501, 900 with the last one flagged.
560        let mut pos = 12usize;
561        let mut seen = Vec::new();
562        for _ in 0..3 {
563            let blocknr = u32::from_be_bytes(desc[pos..pos + 4].try_into().unwrap()) as u64;
564            let flags = u16::from_be_bytes(desc[pos + 6..pos + 8].try_into().unwrap()) as u32;
565            seen.push((blocknr, flags));
566            pos += 8;
567        }
568        assert_eq!(seen[0].0, 500);
569        assert_eq!(seen[1].0, 501);
570        assert_eq!(seen[2].0, 900);
571        assert_eq!(seen[0].1 & TAG_LAST, 0);
572        assert_eq!(seen[1].1 & TAG_LAST, 0);
573        assert_eq!(seen[2].1 & TAG_LAST, TAG_LAST);
574    }
575
576    /// V3 tags are 16 bytes with or without BIT64: without it the
577    /// `t_blocknr_high` slot is zero but still there. Exercise the v3 path
578    /// for encoding at least.
579    #[test]
580    fn v3_tag_layout_32bit_flags() {
581        let mut tx = Transaction::begin(50, 4096, false, true);
582        tx.add_write(0xABCD_1234, vec![0u8; 4096]).unwrap();
583        let blocks = tx.commit().unwrap();
584        let desc = &blocks[0];
585        let blocknr = u32::from_be_bytes(desc[12..16].try_into().unwrap());
586        assert_eq!(blocknr, 0xABCD_1234);
587        // v3: flags is at offset pos+4..pos+8, full 32 bits.
588        let flags = u32::from_be_bytes(desc[16..20].try_into().unwrap());
589        assert_eq!(flags & TAG_LAST, TAG_LAST);
590        assert_eq!(flags & TAG_SAME_UUID, TAG_SAME_UUID);
591    }
592
593    fn csum_jsb(incompat: u32) -> JournalSuperblock {
594        JournalSuperblock {
595            block_type: JBD2_SUPERBLOCK_V2,
596            header_sequence: 0,
597            block_size: 4096,
598            max_len: 1024,
599            first: 1,
600            sequence: 9,
601            start: 0,
602            errno: 0,
603            feature_compat: 0,
604            feature_incompat: incompat,
605            feature_ro_compat: 0,
606            uuid: *b"0123456789abcdef",
607            nr_users: 1,
608            checksum_type: 4,
609            num_fc_blocks: 0,
610            checksum: 0,
611        }
612    }
613
614    fn be32(b: &[u8], at: usize) -> u32 {
615        u32::from_be_bytes(b[at..at + 4].try_into().unwrap())
616    }
617
618    /// Every checksum a CSUM_V3 journal declares is filled, in a 16-byte tag
619    /// even without BIT64 (#80).
620    #[test]
621    fn commit_for_fills_csum_v3_checksums() {
622        let jsb = csum_jsb(JbdIncompat::REVOKE.bits() | JbdIncompat::CSUM_V3.bits());
623        let seed = jsb.csum_seed();
624        let mut tx = Transaction::begin(9, 4096, false, true);
625        tx.add_write(100, vec![0x11; 4096]).unwrap();
626        tx.add_write(200, vec![0x22; 4096]).unwrap();
627        tx.add_revoke(300);
628        let blocks = tx.commit_for(&jsb).unwrap();
629        assert_eq!(blocks.len(), 5, "descriptor, two data, revoke, commit");
630
631        let desc = &blocks[0];
632        for (i, data) in [&blocks[1], &blocks[2]].iter().enumerate() {
633            let tag = 12 + 16 * i;
634            assert_eq!(be32(desc, tag), [100, 200][i]);
635            assert_eq!(be32(desc, tag + 12), jbd2::tag_checksum(seed, 9, data));
636        }
637        assert_eq!(be32(desc, 4092), jbd2::block_tail_checksum(seed, desc));
638        assert_eq!(
639            be32(&blocks[3], 4092),
640            jbd2::block_tail_checksum(seed, &blocks[3])
641        );
642        assert_eq!(
643            be32(&blocks[4], COMMIT_CHECKSUM_AT),
644            jbd2::commit_block_checksum(seed, &blocks[4])
645        );
646        assert_ne!(be32(&blocks[4], COMMIT_CHECKSUM_AT), 0);
647    }
648
649    /// CSUM_V2: classical tags two bytes longer, the low 16 bits of the
650    /// checksum in `t_checksum`.
651    #[test]
652    fn commit_for_lays_csum_v2_tags() {
653        let jsb = csum_jsb(JbdIncompat::CSUM_V2.bits());
654        let seed = jsb.csum_seed();
655        let mut tx = Transaction::begin(9, 4096, false, false);
656        tx.add_write(100, vec![0x11; 4096]).unwrap();
657        tx.add_write(200, vec![0x22; 4096]).unwrap();
658        let blocks = tx.commit_for(&jsb).unwrap();
659        let desc = &blocks[0];
660        assert_eq!(be32(desc, 12 + 10), 200, "second tag starts ten bytes on");
661        let c16 = u16::from_be_bytes(desc[12 + 10 + 4..12 + 10 + 6].try_into().unwrap());
662        assert_eq!(c16, jbd2::tag_checksum(seed, 9, &blocks[2]) as u16);
663        assert_eq!(be32(desc, 4092), jbd2::block_tail_checksum(seed, desc));
664    }
665
666    #[test]
667    fn commit_for_refuses_what_it_cannot_write() {
668        let tx = Transaction::begin(9, 4096, false, true);
669        let v3 = JbdIncompat::CSUM_V3.bits();
670        for incompat in [
671            v3 | JbdIncompat::ASYNC_COMMIT.bits(),
672            v3 | JbdIncompat::FAST_COMMIT.bits(),
673        ] {
674            assert!(matches!(
675                tx.commit_for(&csum_jsb(incompat)),
676                Err(Error::Unsupported(_))
677            ));
678        }
679        for incompat in [0, v3 | JbdIncompat::BIT64.bits()] {
680            assert!(matches!(
681                tx.commit_for(&csum_jsb(incompat)),
682                Err(Error::InvalidArgument(_))
683            ));
684        }
685    }
686
687    /// The tail is not tag space either, and it is where a descriptor's
688    /// capacity changes: at 1 KiB with CSUM_V2 32-bit tags (10 bytes),
689    /// (1024 - 12) / 10 = 101 fit without a tail and (1024 - 12 - 4) / 10 =
690    /// 100 with it. The 101st write must start a second descriptor, each
691    /// descriptor sealed with its own tail and each tag checksummed against
692    /// the data block that follows its own descriptor.
693    #[test]
694    fn a_split_transaction_checksums_every_descriptor() {
695        let jsb = JournalSuperblock {
696            block_size: 1024,
697            ..csum_jsb(JbdIncompat::CSUM_V2.bits())
698        };
699        let seed = jsb.csum_seed();
700        let mut tx = Transaction::begin(9, 1024, false, false);
701        for i in 0..101u64 {
702            tx.add_write(3000 + i, vec![i as u8 + 1; 1024]).unwrap();
703        }
704        let blocks = tx.commit_for(&jsb).unwrap();
705        // desc + 100 data + desc + 1 data + commit
706        assert_eq!(blocks.len(), 104);
707        let (first, second) = (&blocks[0], &blocks[101]);
708        for desc in [first, second] {
709            assert_eq!(be32(desc, 4), JBD2_DESCRIPTOR_BLOCK);
710            assert_eq!(be32(desc, 1020), jbd2::block_tail_checksum(seed, desc));
711        }
712        let flags = |desc: &[u8], idx: usize| {
713            u16::from_be_bytes(
714                desc[12 + idx * 10 + 6..12 + idx * 10 + 8]
715                    .try_into()
716                    .unwrap(),
717            ) as u32
718        };
719        assert_eq!(flags(first, 99) & TAG_LAST, TAG_LAST);
720        assert_eq!(flags(first, 98) & TAG_LAST, 0);
721        assert_eq!(flags(second, 0) & TAG_LAST, TAG_LAST);
722        assert_eq!(
723            be32(second, 12),
724            3100,
725            "the spilled tag names the 101st block"
726        );
727        let c16 = u16::from_be_bytes(second[16..18].try_into().unwrap());
728        assert_eq!(c16, jbd2::tag_checksum(seed, 9, &blocks[102]) as u16);
729        assert_eq!(be32(&blocks[103], 4), JBD2_COMMIT_BLOCK);
730    }
731
732    /// The tail is not record space: a revoke block of 64-bit records holds
733    /// (4096 - 16) / 8 = 510 without one and 509 with it, so the 510th
734    /// record starts a second revoke block rather than overwriting the tail.
735    #[test]
736    fn revoke_tail_is_reserved() {
737        let b64 = JbdIncompat::BIT64.bits();
738        let jsb = csum_jsb(JbdIncompat::CSUM_V3.bits() | b64);
739        let seed = jsb.csum_seed();
740        let mut tx = Transaction::begin(9, 4096, true, true);
741        for b in 0..509 {
742            tx.add_revoke(1000 + b);
743        }
744        let blocks = tx.commit_for(&jsb).unwrap();
745        assert_eq!(blocks.len(), 2, "509 records: one revoke block + commit");
746        assert_eq!(be32(&blocks[0], 12), 16 + 509 * 8);
747
748        tx.add_revoke(2000);
749        let blocks = tx.commit_for(&jsb).unwrap();
750        assert_eq!(blocks.len(), 3, "510 records: two revoke blocks + commit");
751        assert_eq!(be32(&blocks[0], 12), 16 + 509 * 8);
752        assert_eq!(be32(&blocks[1], 12), 16 + 8);
753        assert_eq!(
754            u64::from_be_bytes(blocks[1][16..24].try_into().unwrap()),
755            2000
756        );
757        for blk in &blocks[..2] {
758            assert_eq!(be32(blk, 4), JBD2_REVOKE_BLOCK);
759            assert_eq!(be32(blk, 4092), jbd2::block_tail_checksum(seed, blk));
760        }
761    }
762
763    #[test]
764    fn wrong_block_size_payload_is_rejected() {
765        let mut tx = mk_tx(1);
766        let err = tx.add_write(100, vec![0u8; 2048]).unwrap_err();
767        match err {
768            Error::Corrupt(msg) => assert!(msg.contains("size")),
769            _ => panic!(),
770        }
771    }
772
773    // A tiny adapter that mimics ReplayPlan construction directly from a
774    // serialized classical descriptor — used to document the round-trip
775    // invariant even without a live Filesystem.
776    fn parse_descriptor_classical_no64(desc: &[u8], seq: u32) -> ReplayPlan {
777        let mut plan = ReplayPlan::default();
778        let mut pos = 12usize;
779        let mut tag_idx = 0u64;
780        loop {
781            if pos + 8 > desc.len() {
782                break;
783            }
784            let blocknr = u32::from_be_bytes(desc[pos..pos + 4].try_into().unwrap()) as u64;
785            let flags = u16::from_be_bytes(desc[pos + 6..pos + 8].try_into().unwrap()) as u32;
786            if blocknr == 0 && flags == 0 && tag_idx > 0 {
787                break;
788            }
789            plan.writes.push(ReplayEntry {
790                transaction: seq,
791                fs_block: blocknr,
792                journal_block: tag_idx + 1,
793                flags,
794            });
795            tag_idx += 1;
796            pos += 8;
797            if flags & TAG_LAST != 0 {
798                break;
799            }
800        }
801        plan
802    }
803
804    #[test]
805    fn parsed_descriptor_matches_original_tags() {
806        let mut tx = mk_tx(77);
807        tx.add_write(10, vec![0u8; 4096]).unwrap();
808        tx.add_write(20, vec![0u8; 4096]).unwrap();
809        tx.add_write(30, vec![0u8; 4096]).unwrap();
810        let blocks = tx.commit().unwrap();
811        let plan = parse_descriptor_classical_no64(&blocks[0], 77);
812        assert_eq!(plan.writes.len(), 3);
813        assert_eq!(plan.writes[0].fs_block, 10);
814        assert_eq!(plan.writes[1].fs_block, 20);
815        assert_eq!(plan.writes[2].fs_block, 30);
816    }
817
818    /// A data block whose first four bytes are the journal magic, the rest
819    /// a pattern.
820    fn magic_led(len: usize) -> Vec<u8> {
821        let mut b: Vec<u8> = (0..len).map(|j| 0x5A ^ (j % 251) as u8).collect();
822        b[0..4].copy_from_slice(&JBD2_MAGIC_NUMBER.to_be_bytes());
823        b
824    }
825
826    /// A data block that begins with the journal magic is logged escaped: the
827    /// magic zeroed in the logged copy, `TAG_ESCAPED` on its tag, the rest of
828    /// the block untouched (#292). A block that does not is logged as it is.
829    #[test]
830    fn a_data_block_starting_with_the_magic_is_escaped_in_the_log() {
831        let original = magic_led(4096);
832        let mut tx = mk_tx(5);
833        tx.add_write(100, original.clone()).unwrap();
834        tx.add_write(200, vec![0x77; 4096]).unwrap();
835        let blocks = tx.commit().unwrap();
836        assert_eq!(blocks.len(), 4, "descriptor, two data, commit");
837
838        let desc = &blocks[0];
839        let tag_flags = |i: usize| {
840            u16::from_be_bytes(desc[12 + 8 * i + 6..12 + 8 * i + 8].try_into().unwrap()) as u32
841        };
842        assert_eq!(
843            tag_flags(0) & TAG_ESCAPED,
844            TAG_ESCAPED,
845            "the tag of a block that starts with the magic is not marked escaped"
846        );
847        assert_eq!(
848            &blocks[1][0..4],
849            &[0u8; 4],
850            "the logged copy still starts with the journal magic"
851        );
852        assert!(
853            blocks[1][4..] == original[4..],
854            "escaping touched more than the magic"
855        );
856        assert_eq!(
857            tag_flags(1) & TAG_ESCAPED,
858            0,
859            "an ordinary block was escaped"
860        );
861        assert!(blocks[2] == vec![0x77; 4096]);
862        assert!(
863            tx.writes[0].bytes == original,
864            "the final-location copy lost its magic"
865        );
866    }
867
868    /// Under CSUM_V3 the flag sits in the 32-bit `t_flags`, and the tag
869    /// checksum covers the block as logged -- escaped -- which is what
870    /// recovery reads and verifies before it restores the magic.
871    #[test]
872    fn an_escaped_block_is_checksummed_as_logged() {
873        let jsb = csum_jsb(JbdIncompat::REVOKE.bits() | JbdIncompat::CSUM_V3.bits());
874        let seed = jsb.csum_seed();
875        let mut tx = Transaction::begin(9, 4096, false, true);
876        tx.add_write(100, magic_led(4096)).unwrap();
877        let blocks = tx.commit_for(&jsb).unwrap();
878        let desc = &blocks[0];
879        assert_eq!(be32(desc, 12 + 4) & TAG_ESCAPED, TAG_ESCAPED);
880        assert_eq!(&blocks[1][0..4], &[0u8; 4]);
881        assert_eq!(be32(desc, 12 + 12), jbd2::tag_checksum(seed, 9, &blocks[1]));
882    }
883
884    // Silence unused imports kept for future checksum wiring.
885    #[test]
886    fn jsb_constants_available() {
887        let _ = (
888            JournalSuperblock {
889                block_type: JBD2_SUPERBLOCK_V2,
890                header_sequence: 0,
891                block_size: 4096,
892                max_len: 1024,
893                first: 1,
894                sequence: 1,
895                start: 0,
896                errno: 0,
897                feature_compat: 0,
898                feature_incompat: JbdIncompat::REVOKE.bits(),
899                feature_ro_compat: 0,
900                uuid: [0; 16],
901                nr_users: 1,
902                checksum_type: 0,
903                num_fc_blocks: 0,
904                checksum: 0,
905            },
906            RevokeEntry {
907                transaction: 1,
908                fs_block: 0,
909            },
910        );
911    }
912}