1use 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#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct JournaledBlock {
64 pub fs_block: u64,
67 pub bytes: Vec<u8>,
69}
70
71#[derive(Debug, Clone, Copy)]
73struct Layout {
74 tag_bytes: usize,
75 uses_64bit: bool,
76 tail_bytes: usize,
78 seed: Option<u32>,
80 csum_v3: bool,
82}
83
84#[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 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 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 pub fn add_revoke(&mut self, fs_block: u64) {
122 self.revokes.push(fs_block);
123 }
124
125 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 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 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 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 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 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 .map(|seed| jbd2::tag_checksum(seed, self.sequence, &logged_copy(&w.bytes)));
262
263 if layout.csum_v3 {
264 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 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 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; 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
340fn needs_escape(bytes: &[u8]) -> bool {
352 bytes.get(0..4) == Some(&JBD2_MAGIC_NUMBER.to_be_bytes()[..])
353}
354
355fn 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 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); let bt0 = u32::from_be_bytes(blocks[0][4..8].try_into().unwrap());
397 assert_eq!(bt0, JBD2_DESCRIPTOR_BLOCK);
398 assert_eq!(blocks[1][0], 0xAA);
400 assert_eq!(blocks[2][0], 0xBB);
401 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 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 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 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 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); }
467
468 #[test]
469 fn revokes_beyond_one_block_spill_into_a_second_revoke_block() {
470 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); 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); 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 #[test]
510 fn the_serialised_shape_is_descriptor_groups_then_revokes_then_commit() {
511 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 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 #[test]
544 fn round_trip_through_walk_style_parsing() {
545 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 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 #[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 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 #[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 #[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 #[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 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 #[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 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 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 #[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 #[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 #[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}