1use byteorder::{BigEndian, ByteOrder, LittleEndian};
25use std::collections::BTreeMap;
26use std::io::{Read, Seek, SeekFrom, Write};
27
28pub const JOURNAL_HEADER_MAGIC: u32 = 0x4a4e_4c78;
30pub const OLD_JOURNAL_HEADER_MAGIC: u32 = 0x4a48_4452;
32pub const ENDIAN_MAGIC: u32 = 0x1234_5678;
35
36const JOURNAL_HEADER_CKSUM_SIZE: usize = 44;
39const BLHDR_CHECKSUM_SIZE: usize = 32;
42const BLOCK_INFO_SIZE: usize = 16;
44const BLOCK_LIST_HEADER_PREFIX: usize = 16;
46
47#[allow(dead_code)]
50const BLHDR_CHECK_CHECKSUMS: u32 = 0x0001;
51const BLHDR_FIRST_HEADER: u32 = 0x0002;
53
54pub const KJI_JOURNAL_IN_FS_MASK: u32 = 0x0000_0001;
56pub const KJI_JOURNAL_ON_OTHER_DEVICE_MASK: u32 = 0x0000_0002;
58pub const KJI_JOURNAL_NEED_INIT_MASK: u32 = 0x0000_0004;
60
61const KILLED_BLOCK: u64 = u64::MAX;
63
64#[derive(Debug)]
66pub enum JournalError {
67 NotJournaled,
69 ExternalJournal,
71 BadMagic(u32),
73 BadEndian(u32),
75 OutOfRange(String),
77 CorruptTransaction { sequence_num: u32 },
79 Io(std::io::Error),
81}
82
83impl std::fmt::Display for JournalError {
84 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85 match self {
86 JournalError::NotJournaled => write!(f, "volume is not journaled"),
87 JournalError::ExternalJournal => {
88 write!(f, "journal lives on an external device (unsupported)")
89 }
90 JournalError::BadMagic(m) => write!(f, "bad journal header magic 0x{m:08x}"),
91 JournalError::BadEndian(e) => write!(f, "bad journal endian magic 0x{e:08x}"),
92 JournalError::OutOfRange(msg) => write!(f, "journal field out of range: {msg}"),
93 JournalError::CorruptTransaction { sequence_num } => {
94 write!(f, "corrupt journal transaction (sequence {sequence_num})")
95 }
96 JournalError::Io(e) => write!(f, "journal I/O error: {e}"),
97 }
98 }
99}
100
101impl std::error::Error for JournalError {}
102
103impl From<std::io::Error> for JournalError {
104 fn from(e: std::io::Error) -> Self {
105 JournalError::Io(e)
106 }
107}
108
109type Result<T> = std::result::Result<T, JournalError>;
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub enum JournalEndian {
114 Big,
115 Little,
116}
117
118impl JournalEndian {
119 fn read_u16(self, b: &[u8]) -> u16 {
120 match self {
121 JournalEndian::Big => BigEndian::read_u16(b),
122 JournalEndian::Little => LittleEndian::read_u16(b),
123 }
124 }
125 fn read_u32(self, b: &[u8]) -> u32 {
126 match self {
127 JournalEndian::Big => BigEndian::read_u32(b),
128 JournalEndian::Little => LittleEndian::read_u32(b),
129 }
130 }
131 fn read_u64(self, b: &[u8]) -> u64 {
132 match self {
133 JournalEndian::Big => BigEndian::read_u64(b),
134 JournalEndian::Little => LittleEndian::read_u64(b),
135 }
136 }
137 fn write_u16(self, b: &mut [u8], v: u16) {
138 match self {
139 JournalEndian::Big => BigEndian::write_u16(b, v),
140 JournalEndian::Little => LittleEndian::write_u16(b, v),
141 }
142 }
143 fn write_u32(self, b: &mut [u8], v: u32) {
144 match self {
145 JournalEndian::Big => BigEndian::write_u32(b, v),
146 JournalEndian::Little => LittleEndian::write_u32(b, v),
147 }
148 }
149 fn write_u64(self, b: &mut [u8], v: u64) {
150 match self {
151 JournalEndian::Big => BigEndian::write_u64(b, v),
152 JournalEndian::Little => LittleEndian::write_u64(b, v),
153 }
154 }
155}
156
157pub fn calc_checksum(data: &[u8]) -> u32 {
160 let mut cksum: u32 = 0;
161 for &byte in data {
162 cksum = cksum.wrapping_shl(8) ^ cksum.wrapping_add(byte as u32);
163 }
164 !cksum
165}
166
167#[derive(Debug, Clone)]
171pub struct JournalInfoBlock {
172 pub flags: u32,
173 pub offset: u64,
175 pub size: u64,
177}
178
179impl JournalInfoBlock {
180 pub fn parse(data: &[u8]) -> Result<Self> {
184 if data.len() < 52 {
185 return Err(JournalError::OutOfRange(
186 "journal info block shorter than 52 bytes".into(),
187 ));
188 }
189 Ok(JournalInfoBlock {
190 flags: BigEndian::read_u32(&data[0..4]),
191 offset: BigEndian::read_u64(&data[36..44]),
192 size: BigEndian::read_u64(&data[44..52]),
193 })
194 }
195
196 pub fn is_in_fs(&self) -> bool {
198 self.flags & KJI_JOURNAL_IN_FS_MASK != 0
199 }
200
201 pub fn is_external(&self) -> bool {
203 self.flags & KJI_JOURNAL_ON_OTHER_DEVICE_MASK != 0
204 }
205}
206
207#[derive(Debug, Clone)]
210pub struct JournalHeader {
211 pub endian: JournalEndian,
212 pub magic: u32,
213 pub start: u64,
216 pub end: u64,
218 pub size: u64,
220 pub blhdr_size: u32,
222 pub checksum: u32,
223 pub jhdr_size: u32,
225 pub sequence_num: u32,
226}
227
228impl JournalHeader {
229 pub fn parse(data: &[u8]) -> Result<Self> {
231 if data.len() < JOURNAL_HEADER_CKSUM_SIZE + 4 {
232 return Err(JournalError::OutOfRange(
233 "journal header shorter than 48 bytes".into(),
234 ));
235 }
236 let endian = if LittleEndian::read_u32(&data[4..8]) == ENDIAN_MAGIC {
238 JournalEndian::Little
239 } else if BigEndian::read_u32(&data[4..8]) == ENDIAN_MAGIC {
240 JournalEndian::Big
241 } else {
242 return Err(JournalError::BadEndian(BigEndian::read_u32(&data[4..8])));
243 };
244
245 let magic = endian.read_u32(&data[0..4]);
246 if magic != JOURNAL_HEADER_MAGIC && magic != OLD_JOURNAL_HEADER_MAGIC {
247 return Err(JournalError::BadMagic(magic));
248 }
249
250 let hdr = JournalHeader {
251 endian,
252 magic,
253 start: endian.read_u64(&data[8..16]),
254 end: endian.read_u64(&data[16..24]),
255 size: endian.read_u64(&data[24..32]),
256 blhdr_size: endian.read_u32(&data[32..36]),
257 checksum: endian.read_u32(&data[36..40]),
258 jhdr_size: endian.read_u32(&data[40..44]),
259 sequence_num: endian.read_u32(&data[44..48]),
260 };
261
262 if magic == JOURNAL_HEADER_MAGIC {
266 let mut tmp = data[..JOURNAL_HEADER_CKSUM_SIZE].to_vec();
267 tmp[36..40].copy_from_slice(&[0u8; 4]);
268 let computed = calc_checksum(&tmp);
269 if computed != hdr.checksum {
270 return Err(JournalError::OutOfRange(format!(
271 "journal header checksum mismatch (computed 0x{computed:08x} != stored 0x{:08x})",
272 hdr.checksum
273 )));
274 }
275 }
276
277 hdr.validate()?;
278 Ok(hdr)
279 }
280
281 fn validate(&self) -> Result<()> {
282 if self.jhdr_size == 0 || (self.jhdr_size as u64) > self.size {
283 return Err(JournalError::OutOfRange(format!(
284 "jhdr_size {} invalid for journal size {}",
285 self.jhdr_size, self.size
286 )));
287 }
288 if self.blhdr_size < BLHDR_CHECKSUM_SIZE as u32 || (self.blhdr_size as u64) > self.size {
289 return Err(JournalError::OutOfRange(format!(
290 "blhdr_size {} invalid",
291 self.blhdr_size
292 )));
293 }
294 if self.start > self.size || self.end > self.size {
295 return Err(JournalError::OutOfRange(format!(
296 "start {} / end {} out of range for size {}",
297 self.start, self.end, self.size
298 )));
299 }
300 Ok(())
301 }
302
303 pub fn is_empty(&self) -> bool {
305 self.normalized_start() == self.normalized_end()
306 }
307
308 fn normalized_start(&self) -> u64 {
311 if self.start == self.size {
312 self.jhdr_size as u64
313 } else {
314 self.start
315 }
316 }
317
318 fn normalized_end(&self) -> u64 {
319 if self.end == self.size {
320 self.jhdr_size as u64
321 } else {
322 self.end
323 }
324 }
325
326 pub fn serialize(&self, original: Option<&[u8]>) -> Vec<u8> {
330 let len = self.jhdr_size as usize;
331 let mut out = vec![0u8; len];
332 if let Some(orig) = original {
333 let copy = orig.len().min(len);
334 out[..copy].copy_from_slice(&orig[..copy]);
335 }
336 let e = self.endian;
337 e.write_u32(&mut out[0..4], self.magic);
338 e.write_u32(&mut out[4..8], ENDIAN_MAGIC);
340 e.write_u64(&mut out[8..16], self.start);
341 e.write_u64(&mut out[16..24], self.end);
342 e.write_u64(&mut out[24..32], self.size);
343 e.write_u32(&mut out[32..36], self.blhdr_size);
344 e.write_u32(&mut out[36..40], 0); e.write_u32(&mut out[40..44], self.jhdr_size);
346 e.write_u32(&mut out[44..48], self.sequence_num);
347 let cksum = calc_checksum(&out[..JOURNAL_HEADER_CKSUM_SIZE]);
348 e.write_u32(&mut out[36..40], cksum);
349 out
350 }
351}
352
353#[derive(Debug, Clone)]
355pub struct BlockInfo {
356 pub bnum: u64,
359 pub bsize: u32,
361 pub next: u32,
364}
365
366#[derive(Debug, Clone)]
369pub struct TransactionView {
370 pub sequence_num: u32,
371 pub flags: u32,
372 pub bytes_used: u32,
373 pub blocks: Vec<(u64, Vec<u8>)>,
376 pub total_payload: u64,
378}
379
380#[derive(Debug, Clone)]
382pub struct TransactionSummary {
383 pub sequence_num: u32,
384 pub block_count: u32,
385 pub total_bytes: u64,
386 pub target_range: Option<(u64, u64)>,
389}
390
391#[derive(Debug, Clone)]
394pub struct JournalState {
395 pub info: JournalInfoBlock,
396 pub header: JournalHeader,
397 pub transactions: Vec<TransactionSummary>,
398 pub partial: bool,
400 pub checksum_mismatch: Option<u32>,
403 pub sequence_jumps: Vec<(u32, u32)>,
406}
407
408impl JournalState {
409 pub fn pending_bytes(&self) -> u64 {
411 self.transactions.iter().map(|t| t.total_bytes).sum()
412 }
413}
414
415#[derive(Debug, Clone)]
418pub struct MetadataRegion {
419 pub name: &'static str,
420 pub start: u64,
421 pub end: u64,
422}
423
424pub fn classify_target(regions: &[MetadataRegion], target: u64) -> &'static str {
427 for r in regions {
428 if target >= r.start && target < r.end {
429 return r.name;
430 }
431 }
432 "fork data"
433}
434
435#[derive(Debug, Clone)]
438pub struct JournalBlockDetail {
439 pub target: u64,
441 pub bsize: u32,
442 pub preview: Vec<u8>,
444}
445
446#[derive(Debug, Clone)]
449pub struct JournalTxnDetail {
450 pub sequence_num: u32,
451 pub flags: u32,
452 pub bytes_used: u32,
453 pub total_bytes: u64,
454 pub blocks: Vec<JournalBlockDetail>,
455}
456
457#[derive(Debug, Clone)]
461pub struct JournalDetail {
462 pub info: JournalInfoBlock,
463 pub header: JournalHeader,
464 pub transactions: Vec<JournalTxnDetail>,
465 pub regions: Vec<MetadataRegion>,
466 pub checksum_mismatch: Option<u32>,
467 pub sequence_jumps: Vec<(u32, u32)>,
468 pub partial: bool,
469}
470
471const PREVIEW_BYTES: usize = 64;
473
474pub fn read_journal_detail<R: Read + Seek>(
478 reader: &mut R,
479 partition_offset: u64,
480 jib: JournalInfoBlock,
481 header: JournalHeader,
482 regions: Vec<MetadataRegion>,
483) -> JournalDetail {
484 let mut transactions = Vec::new();
485 let mut checksum_mismatch = None;
486 let mut sequence_jumps = Vec::new();
487 let mut partial = false;
488 {
489 let mut walker = JournalWalker::new(reader, partition_offset, &jib, &header);
490 let mut prev_seq: Option<u32> = None;
491 while let Some(item) = walker.next() {
492 match item {
493 Ok(view) => {
494 if let Some(prev) = prev_seq {
495 if view.sequence_num != prev && view.sequence_num != prev.wrapping_add(1) {
496 sequence_jumps.push((prev, view.sequence_num));
497 }
498 }
499 prev_seq = Some(view.sequence_num);
500 let blocks = view
501 .blocks
502 .iter()
503 .map(|(target, data)| JournalBlockDetail {
504 target: *target,
505 bsize: data.len() as u32,
506 preview: data[..data.len().min(PREVIEW_BYTES)].to_vec(),
507 })
508 .collect();
509 transactions.push(JournalTxnDetail {
510 sequence_num: view.sequence_num,
511 flags: view.flags,
512 bytes_used: view.bytes_used,
513 total_bytes: view.total_payload,
514 blocks,
515 });
516 }
517 Err(JournalError::CorruptTransaction { sequence_num }) => {
518 checksum_mismatch = Some(sequence_num);
519 partial = true;
520 break;
521 }
522 Err(_) => {
523 partial = true;
524 break;
525 }
526 }
527 }
528 }
529 JournalDetail {
530 info: jib,
531 header,
532 transactions,
533 regions,
534 checksum_mismatch,
535 sequence_jumps,
536 partial,
537 }
538}
539
540pub struct JournalWalker<'a, R: Read + Seek> {
544 reader: &'a mut R,
545 partition_offset: u64,
546 jib_offset: u64,
547 jhdr_size: u64,
548 blhdr_size: usize,
549 journal_size: u64,
550 endian: JournalEndian,
551 cursor: u64,
553 end: u64,
554 done: bool,
556}
557
558impl<'a, R: Read + Seek> JournalWalker<'a, R> {
559 pub fn new(
560 reader: &'a mut R,
561 partition_offset: u64,
562 jib: &JournalInfoBlock,
563 jh: &JournalHeader,
564 ) -> Self {
565 JournalWalker {
566 reader,
567 partition_offset,
568 jib_offset: jib.offset,
569 jhdr_size: jh.jhdr_size as u64,
570 blhdr_size: jh.blhdr_size as usize,
571 journal_size: jh.size,
572 endian: jh.endian,
573 cursor: jh.normalized_start(),
574 end: jh.normalized_end(),
575 done: jh.is_empty(),
576 }
577 }
578
579 fn wrap(&self, off: u64) -> u64 {
582 if off >= self.journal_size {
583 self.jhdr_size + (off - self.journal_size)
584 } else {
585 off
586 }
587 }
588
589 fn read_wrapped(&mut self, rel: u64, len: usize) -> Result<Vec<u8>> {
592 let mut out = Vec::with_capacity(len);
593 let mut pos = self.wrap(rel);
594 while out.len() < len {
595 let space = (self.journal_size - pos) as usize;
596 let chunk = (len - out.len()).min(space);
597 let abs = self.partition_offset + self.jib_offset + pos;
598 self.reader.seek(SeekFrom::Start(abs))?;
599 let mut buf = vec![0u8; chunk];
600 self.reader.read_exact(&mut buf)?;
601 out.extend_from_slice(&buf);
602 pos += chunk as u64;
603 if pos >= self.journal_size {
604 pos = self.jhdr_size;
605 }
606 }
607 Ok(out)
608 }
609
610 #[allow(clippy::should_implement_trait)]
614 pub fn next(&mut self) -> Option<Result<TransactionView>> {
615 if self.done || self.cursor == self.end {
616 return None;
617 }
618 match self.read_transaction() {
619 Ok(view) => Some(Ok(view)),
620 Err(e) => {
621 self.done = true;
622 Some(Err(e))
623 }
624 }
625 }
626
627 fn read_transaction(&mut self) -> Result<TransactionView> {
628 let start = self.cursor;
629 let hdr = self.read_wrapped(start, self.blhdr_size)?;
631
632 let num_blocks = self.endian.read_u16(&hdr[2..4]) as usize;
633 let bytes_used = self.endian.read_u32(&hdr[4..8]);
634 let stored_cksum = self.endian.read_u32(&hdr[8..12]);
635 let flags = self.endian.read_u32(&hdr[12..16]);
636 let max_blocks = self.endian.read_u16(&hdr[0..2]) as usize;
637 let sequence_num = self.endian.read_u32(&hdr[28..32]); let mut cksum_buf = hdr[..BLHDR_CHECKSUM_SIZE].to_vec();
642 cksum_buf[8..12].copy_from_slice(&[0u8; 4]);
643 if calc_checksum(&cksum_buf) != stored_cksum {
644 return Err(JournalError::CorruptTransaction { sequence_num });
645 }
646
647 if num_blocks == 0 || num_blocks > max_blocks {
648 return Err(JournalError::OutOfRange(format!(
649 "num_blocks {num_blocks} out of range (max {max_blocks})"
650 )));
651 }
652 if (bytes_used as usize) < self.blhdr_size || (bytes_used as u64) > self.journal_size {
653 return Err(JournalError::OutOfRange(format!(
654 "bytes_used {bytes_used} invalid"
655 )));
656 }
657
658 let mut infos: Vec<BlockInfo> = Vec::with_capacity(num_blocks.saturating_sub(1));
660 for i in 1..num_blocks {
661 let off = BLOCK_LIST_HEADER_PREFIX + i * BLOCK_INFO_SIZE;
662 if off + BLOCK_INFO_SIZE > hdr.len() {
663 return Err(JournalError::OutOfRange(
664 "binfo entry beyond block list header".into(),
665 ));
666 }
667 infos.push(BlockInfo {
668 bnum: self.endian.read_u64(&hdr[off..off + 8]),
669 bsize: self.endian.read_u32(&hdr[off + 8..off + 12]),
670 next: self.endian.read_u32(&hdr[off + 12..off + 16]),
671 });
672 }
673
674 let data_len = bytes_used as usize - self.blhdr_size;
676 let data = self.read_wrapped(self.wrap(start + self.blhdr_size as u64), data_len)?;
677
678 let mut blocks = Vec::new();
681 let mut total_payload: u64 = 0;
682 let mut data_pos = 0usize;
683 for info in &infos {
684 let bsize = info.bsize as usize;
685 if data_pos + bsize > data.len() {
686 return Err(JournalError::OutOfRange(
687 "block payload beyond transaction data".into(),
688 ));
689 }
690 if info.bnum != KILLED_BLOCK && info.bsize != 0 {
691 let target = info.bnum * self.jhdr_size;
692 blocks.push((target, data[data_pos..data_pos + bsize].to_vec()));
693 total_payload += bsize as u64;
694 }
695 data_pos += bsize;
696 }
697
698 self.cursor = self.wrap(start + bytes_used as u64);
700
701 Ok(TransactionView {
702 sequence_num,
703 flags,
704 bytes_used,
705 blocks,
706 total_payload,
707 })
708 }
709}
710
711impl TransactionView {
712 pub fn summary(&self) -> TransactionSummary {
714 let target_range = self.blocks.iter().fold(None, |acc, (off, data)| {
715 let lo = *off;
716 let hi = off + data.len() as u64;
717 match acc {
718 None => Some((lo, hi)),
719 Some((amin, amax)) => Some((amin.min(lo), amax.max(hi))),
720 }
721 });
722 TransactionSummary {
723 sequence_num: self.sequence_num,
724 block_count: self.blocks.len() as u32,
725 total_bytes: self.total_payload,
726 target_range,
727 }
728 }
729}
730
731#[derive(Debug, Clone, Default)]
733pub struct ReplayReport {
734 pub transactions_applied: u32,
735 pub blocks_written: u64,
736 pub bytes_written: u64,
737 pub last_sequence_num: u32,
738 pub partial: bool,
741 pub summaries: Vec<TransactionSummary>,
742}
743
744pub fn replay_journal<RW: Read + Write + Seek>(
753 disk: &mut RW,
754 jib: &JournalInfoBlock,
755 jh: &mut JournalHeader,
756 partition_offset: u64,
757) -> Result<ReplayReport> {
758 if jib.is_external() {
759 return Err(JournalError::ExternalJournal);
760 }
761
762 let mut report = ReplayReport {
763 last_sequence_num: jh.sequence_num,
764 ..Default::default()
765 };
766
767 jh.start = jh.normalized_start();
769 jh.end = jh.normalized_end();
770
771 loop {
772 if jh.start == jh.end {
773 break;
774 }
775
776 let view = {
778 let mut walker = JournalWalker::new(disk, partition_offset, jib, jh);
779 match walker.next() {
780 Some(Ok(view)) => {
781 let next_start = walker.cursor;
783 (view, next_start)
784 }
785 Some(Err(JournalError::CorruptTransaction { .. })) | None => {
786 report.partial = true;
787 break;
788 }
789 Some(Err(e)) => return Err(e),
790 }
791 };
792 let (view, next_start) = view;
793
794 for (target, payload) in &view.blocks {
796 disk.seek(SeekFrom::Start(partition_offset + *target))?;
797 disk.write_all(payload)?;
798 report.blocks_written += 1;
799 report.bytes_written += payload.len() as u64;
800 }
801 disk.flush()?;
802
803 report.transactions_applied += 1;
804 report.last_sequence_num = view.sequence_num;
805 report.summaries.push(view.summary());
806
807 jh.start = next_start;
810 jh.sequence_num = view.sequence_num;
811 write_journal_header(disk, jib, jh, partition_offset)?;
812 }
813
814 if jh.start != jh.end {
817 jh.end = jh.start;
818 }
819 write_journal_header(disk, jib, jh, partition_offset)?;
820 Ok(report)
821}
822
823fn write_journal_header<RW: Read + Write + Seek>(
825 disk: &mut RW,
826 jib: &JournalInfoBlock,
827 jh: &JournalHeader,
828 partition_offset: u64,
829) -> Result<()> {
830 let abs = partition_offset + jib.offset;
831 disk.seek(SeekFrom::Start(abs))?;
833 let mut orig = vec![0u8; jh.jhdr_size as usize];
834 disk.read_exact(&mut orig)?;
835 let bytes = jh.serialize(Some(&orig));
836 disk.seek(SeekFrom::Start(abs))?;
837 disk.write_all(&bytes)?;
838 disk.flush()?;
839 Ok(())
840}
841
842#[derive(Debug, Default)]
847pub struct TransactionBuilder {
848 dirty: BTreeMap<u64, Vec<u8>>,
851 block_size: u32,
852}
853
854impl TransactionBuilder {
855 pub fn new(block_size: u32) -> Self {
858 TransactionBuilder {
859 dirty: BTreeMap::new(),
860 block_size: block_size.max(1),
861 }
862 }
863
864 pub fn is_empty(&self) -> bool {
865 self.dirty.is_empty()
866 }
867
868 pub fn block_count(&self) -> usize {
869 self.dirty.len()
870 }
871
872 pub fn record_block(&mut self, offset: u64, bytes: &[u8]) {
875 debug_assert_eq!(offset % self.block_size as u64, 0, "unaligned record_block");
876 debug_assert_eq!(bytes.len(), self.block_size as usize, "wrong block length");
877 self.dirty.insert(offset, bytes.to_vec());
878 }
879
880 pub fn record_range(&mut self, offset: u64, bytes: &[u8]) {
883 let bs = self.block_size as usize;
884 debug_assert_eq!(offset % self.block_size as u64, 0, "unaligned record_range");
885 debug_assert_eq!(bytes.len() % bs, 0, "record_range length not block-aligned");
886 for (i, chunk) in bytes.chunks(bs).enumerate() {
887 self.dirty.insert(offset + (i * bs) as u64, chunk.to_vec());
888 }
889 }
890
891 pub fn summary(&self, sequence_num: u32) -> TransactionSummary {
893 let total: u64 = self.dirty.values().map(|v| v.len() as u64).sum();
894 let target_range = self.dirty.iter().fold(None, |acc, (off, data)| {
895 let hi = off + data.len() as u64;
896 match acc {
897 None => Some((*off, hi)),
898 Some((amin, amax)) => Some((amin.min(*off), amax.max(hi))),
899 }
900 });
901 TransactionSummary {
902 sequence_num,
903 block_count: self.dirty.len() as u32,
904 total_bytes: total,
905 target_range,
906 }
907 }
908
909 pub fn commit<RW: Read + Write + Seek>(
915 self,
916 disk: &mut RW,
917 jib: &JournalInfoBlock,
918 jh: &mut JournalHeader,
919 partition_offset: u64,
920 ) -> Result<()> {
921 if jib.is_external() {
922 return Err(JournalError::ExternalJournal);
923 }
924 if self.dirty.is_empty() {
925 return Ok(());
926 }
927
928 self.write_to_journal(disk, jib, jh, partition_offset)?;
932
933 for (offset, bytes) in &self.dirty {
935 disk.seek(SeekFrom::Start(partition_offset + *offset))?;
936 disk.write_all(bytes)?;
937 }
938 disk.flush()?;
939
940 jh.start = jh.end;
942 write_journal_header(disk, jib, jh, partition_offset)?;
943 Ok(())
944 }
945
946 pub fn write_to_journal<RW: Read + Write + Seek>(
953 &self,
954 disk: &mut RW,
955 jib: &JournalInfoBlock,
956 jh: &mut JournalHeader,
957 partition_offset: u64,
958 ) -> Result<()> {
959 if jib.is_external() {
960 return Err(JournalError::ExternalJournal);
961 }
962 if self.dirty.is_empty() {
963 return Ok(());
964 }
965
966 let blhdr_size = jh.blhdr_size as usize;
967 let endian = jh.endian;
968 let num_data = self.dirty.len();
969 let num_blocks = num_data + 1; let max_blocks = blhdr_size / BLOCK_INFO_SIZE - 1;
971 if num_data > max_blocks {
972 return Err(JournalError::OutOfRange(format!(
973 "transaction has {num_data} blocks, exceeds journal max {max_blocks}"
974 )));
975 }
976
977 let mut hdr = vec![0u8; blhdr_size];
979 for b in hdr.iter_mut().skip(BLHDR_CHECKSUM_SIZE) {
982 *b = 0x5a;
983 }
984
985 let bs = self.block_size;
986 let mut data_region: Vec<u8> = Vec::new();
987 for (idx, (offset, bytes)) in (1usize..).zip(self.dirty.iter()) {
989 let bnum = offset / bs as u64;
990 let info_off = BLOCK_LIST_HEADER_PREFIX + idx * BLOCK_INFO_SIZE;
991 endian.write_u64(&mut hdr[info_off..info_off + 8], bnum);
992 endian.write_u32(&mut hdr[info_off + 8..info_off + 12], bytes.len() as u32);
993 endian.write_u32(&mut hdr[info_off + 12..info_off + 16], 0); data_region.extend_from_slice(bytes);
995 }
996
997 let bytes_used = (blhdr_size + data_region.len()) as u32;
998
999 endian.write_u16(&mut hdr[0..2], num_blocks as u16); endian.write_u16(&mut hdr[2..4], num_blocks as u16); endian.write_u32(&mut hdr[4..8], bytes_used);
1003 endian.write_u32(&mut hdr[8..12], 0); endian.write_u32(&mut hdr[12..16], BLHDR_FIRST_HEADER); let seq = jh.sequence_num.wrapping_add(1);
1007 endian.write_u64(&mut hdr[16..24], 0); endian.write_u32(&mut hdr[24..28], 0); endian.write_u32(&mut hdr[28..32], seq); let mut cksum_buf = hdr[..BLHDR_CHECKSUM_SIZE].to_vec();
1013 cksum_buf[8..12].copy_from_slice(&[0u8; 4]);
1014 let cksum = calc_checksum(&cksum_buf);
1015 endian.write_u32(&mut hdr[8..12], cksum);
1016
1017 let mut body = hdr;
1019 body.extend_from_slice(&data_region);
1020 let write_start = if jh.end == jh.size {
1021 jh.jhdr_size as u64
1022 } else {
1023 jh.end
1024 };
1025 write_journal_wrapped(disk, jib, jh, partition_offset, write_start, &body)?;
1026
1027 let mut new_end = write_start + body.len() as u64;
1029 if new_end >= jh.size {
1030 new_end = jh.jhdr_size as u64 + (new_end - jh.size);
1031 }
1032 jh.end = new_end;
1033 jh.sequence_num = seq;
1034 write_journal_header(disk, jib, jh, partition_offset)?;
1035 Ok(())
1036 }
1037}
1038
1039fn write_journal_wrapped<RW: Read + Write + Seek>(
1042 disk: &mut RW,
1043 jib: &JournalInfoBlock,
1044 jh: &JournalHeader,
1045 partition_offset: u64,
1046 rel: u64,
1047 body: &[u8],
1048) -> Result<()> {
1049 let mut pos = rel;
1050 let mut written = 0usize;
1051 while written < body.len() {
1052 let space = (jh.size - pos) as usize;
1053 let chunk = (body.len() - written).min(space);
1054 let abs = partition_offset + jib.offset + pos;
1055 disk.seek(SeekFrom::Start(abs))?;
1056 disk.write_all(&body[written..written + chunk])?;
1057 written += chunk;
1058 pos += chunk as u64;
1059 if pos >= jh.size {
1060 pos = jh.jhdr_size as u64;
1061 }
1062 }
1063 Ok(())
1064}
1065
1066#[cfg(test)]
1067mod tests {
1068 use super::*;
1069 use std::io::Cursor;
1070
1071 struct SyntheticJournal {
1074 disk: Vec<u8>,
1075 partition_offset: u64,
1076 jib: JournalInfoBlock,
1077 header: JournalHeader,
1078 }
1079
1080 const JHDR_SIZE: u32 = 512;
1081 const BLHDR_SIZE: u32 = 512;
1082 const JOURNAL_SIZE: u64 = 64 * 1024;
1083 const JIB_OFFSET: u64 = 4096; fn build_journal(endian: JournalEndian, part_off: u64) -> SyntheticJournal {
1086 let total = part_off + JIB_OFFSET + JOURNAL_SIZE + 4096;
1087 let disk = vec![0u8; total as usize];
1088 let jib = JournalInfoBlock {
1089 flags: KJI_JOURNAL_IN_FS_MASK,
1090 offset: JIB_OFFSET,
1091 size: JOURNAL_SIZE,
1092 };
1093 let header = JournalHeader {
1094 endian,
1095 magic: JOURNAL_HEADER_MAGIC,
1096 start: JHDR_SIZE as u64,
1097 end: JHDR_SIZE as u64,
1098 size: JOURNAL_SIZE,
1099 blhdr_size: BLHDR_SIZE,
1100 checksum: 0,
1101 jhdr_size: JHDR_SIZE,
1102 sequence_num: 0,
1103 };
1104 let mut s = SyntheticJournal {
1105 disk,
1106 partition_offset: part_off,
1107 jib,
1108 header,
1109 };
1110 s.write_header();
1111 s
1112 }
1113
1114 impl SyntheticJournal {
1115 fn write_header(&mut self) {
1116 let abs = (self.partition_offset + self.jib.offset) as usize;
1117 let bytes = self.header.serialize(None);
1118 self.disk[abs..abs + bytes.len()].copy_from_slice(&bytes);
1119 }
1120
1121 fn cursor(&mut self) -> Cursor<&mut Vec<u8>> {
1122 Cursor::new(&mut self.disk)
1123 }
1124
1125 fn dirty_blocks(&mut self, blocks: &[(u64, Vec<u8>)]) {
1129 let mut builder = TransactionBuilder::new(self.header.jhdr_size);
1130 for (off, data) in blocks {
1131 builder.record_block(*off, data);
1132 }
1133 let jib = self.jib.clone();
1134 let mut jh = self.header.clone();
1135 let part = self.partition_offset;
1136 {
1137 let mut cur = self.cursor();
1138 builder
1139 .write_to_journal(&mut cur, &jib, &mut jh, part)
1140 .unwrap();
1141 }
1142 self.header = jh;
1143 }
1144
1145 #[allow(dead_code)]
1148 fn commit_blocks(&mut self, blocks: &[(u64, Vec<u8>)]) {
1149 let mut builder = TransactionBuilder::new(self.header.jhdr_size);
1150 for (off, data) in blocks {
1151 builder.record_block(*off, data);
1152 }
1153 let jib = self.jib.clone();
1154 let mut jh = self.header.clone();
1155 let part = self.partition_offset;
1156 {
1157 let mut cur = self.cursor();
1158 builder.commit(&mut cur, &jib, &mut jh, part).unwrap();
1159 }
1160 self.header = jh;
1161 }
1162
1163 fn read_block_at(&self, target: u64, len: usize) -> Vec<u8> {
1164 let abs = (self.partition_offset + target) as usize;
1165 self.disk[abs..abs + len].to_vec()
1166 }
1167 }
1168
1169 #[test]
1170 fn header_roundtrip_both_endians() {
1171 for endian in [JournalEndian::Big, JournalEndian::Little] {
1172 let s = build_journal(endian, 0);
1173 let abs = (s.partition_offset + s.jib.offset) as usize;
1174 let parsed = JournalHeader::parse(&s.disk[abs..abs + JHDR_SIZE as usize]).unwrap();
1175 assert_eq!(parsed.endian, endian);
1176 assert_eq!(parsed.magic, JOURNAL_HEADER_MAGIC);
1177 assert_eq!(parsed.size, JOURNAL_SIZE);
1178 assert_eq!(parsed.jhdr_size, JHDR_SIZE);
1179 }
1180 }
1181
1182 #[test]
1183 fn walk_two_transactions_both_endians() {
1184 for endian in [JournalEndian::Big, JournalEndian::Little] {
1185 let mut s = build_journal(endian, 8192);
1186 let blk_a = vec![0xAA; JHDR_SIZE as usize];
1188 let blk_b = vec![0xBB; JHDR_SIZE as usize];
1189 s.dirty_blocks(&[(JIB_OFFSET + JOURNAL_SIZE + 1024, blk_a.clone())]);
1190 s.dirty_blocks(&[(JIB_OFFSET + JOURNAL_SIZE + 2048, blk_b.clone())]);
1191
1192 let abs = (s.partition_offset + s.jib.offset) as usize;
1194 let jh = JournalHeader::parse(&s.disk[abs..abs + JHDR_SIZE as usize]).unwrap();
1195 let jib = s.jib.clone();
1196 let part = s.partition_offset;
1197 let mut cur = s.cursor();
1198 let mut walker = JournalWalker::new(&mut cur, part, &jib, &jh);
1199
1200 let t1 = walker.next().unwrap().unwrap();
1201 assert_eq!(t1.sequence_num, 1);
1202 assert_eq!(t1.blocks.len(), 1);
1203 assert_eq!(t1.blocks[0].1, blk_a);
1204
1205 let t2 = walker.next().unwrap().unwrap();
1206 assert_eq!(t2.sequence_num, 2);
1207 assert_eq!(t2.blocks[0].1, blk_b);
1208
1209 assert!(walker.next().is_none());
1210 }
1211 }
1212
1213 #[test]
1214 fn bad_magic_rejected() {
1215 let mut s = build_journal(JournalEndian::Big, 0);
1216 let abs = (s.partition_offset + s.jib.offset) as usize;
1217 s.disk[abs] ^= 0xFF; let err = JournalHeader::parse(&s.disk[abs..abs + JHDR_SIZE as usize]).unwrap_err();
1219 assert!(matches!(
1220 err,
1221 JournalError::BadMagic(_) | JournalError::OutOfRange(_) | JournalError::BadEndian(_)
1222 ));
1223 }
1224
1225 #[test]
1226 fn truncated_header_rejected() {
1227 let s = build_journal(JournalEndian::Big, 0);
1228 let abs = (s.partition_offset + s.jib.offset) as usize;
1229 let err = JournalHeader::parse(&s.disk[abs..abs + 10]).unwrap_err();
1230 assert!(matches!(err, JournalError::OutOfRange(_)));
1231 }
1232
1233 #[test]
1234 fn out_of_range_end_rejected() {
1235 let mut s = build_journal(JournalEndian::Big, 0);
1236 s.header.end = JOURNAL_SIZE + 4096; s.write_header();
1238 let abs = (s.partition_offset + s.jib.offset) as usize;
1239 let err = JournalHeader::parse(&s.disk[abs..abs + JHDR_SIZE as usize]).unwrap_err();
1240 assert!(matches!(err, JournalError::OutOfRange(_)));
1241 }
1242
1243 #[test]
1244 fn jib_parse_flags() {
1245 let mut block = vec![0u8; 512];
1246 BigEndian::write_u32(&mut block[0..4], KJI_JOURNAL_IN_FS_MASK);
1247 BigEndian::write_u64(&mut block[36..44], 4096);
1248 BigEndian::write_u64(&mut block[44..52], 64 * 1024);
1249 let jib = JournalInfoBlock::parse(&block).unwrap();
1250 assert!(jib.is_in_fs());
1251 assert!(!jib.is_external());
1252 assert_eq!(jib.offset, 4096);
1253 assert_eq!(jib.size, 64 * 1024);
1254 }
1255
1256 #[test]
1259 fn replay_applies_dirty_transaction() {
1260 for endian in [JournalEndian::Big, JournalEndian::Little] {
1261 let mut s = build_journal(endian, 8192);
1262 let target = JIB_OFFSET + JOURNAL_SIZE + 1024;
1263 let payload = vec![0x42; JHDR_SIZE as usize];
1264 s.dirty_blocks(&[(target, payload.clone())]);
1266 assert_eq!(
1268 s.read_block_at(target, JHDR_SIZE as usize),
1269 vec![0u8; JHDR_SIZE as usize]
1270 );
1271
1272 let jib = s.jib.clone();
1273 let mut jh = s.header.clone();
1274 let part = s.partition_offset;
1275 let report = {
1276 let mut cur = s.cursor();
1277 replay_journal(&mut cur, &jib, &mut jh, part).unwrap()
1278 };
1279 assert_eq!(report.transactions_applied, 1);
1280 assert_eq!(report.blocks_written, 1);
1281 assert!(!report.partial);
1282 assert_eq!(s.read_block_at(target, JHDR_SIZE as usize), payload);
1285 assert_eq!(jh.start, jh.end);
1286 }
1287 }
1288
1289 #[test]
1290 fn replay_is_idempotent() {
1291 let mut s = build_journal(JournalEndian::Big, 0);
1292 let target = JIB_OFFSET + JOURNAL_SIZE + 512;
1293 let payload = vec![0x7e; JHDR_SIZE as usize];
1294 s.dirty_blocks(&[(target, payload.clone())]);
1295
1296 let jib = s.jib.clone();
1297 let part = s.partition_offset;
1298
1299 let mut jh = s.header.clone();
1301 {
1302 let mut cur = s.cursor();
1303 replay_journal(&mut cur, &jib, &mut jh, part).unwrap();
1304 }
1305 s.header = jh;
1306 let abs = (part + jib.offset) as usize;
1308 let mut jh2 = JournalHeader::parse(&s.disk[abs..abs + JHDR_SIZE as usize]).unwrap();
1309 let report2 = {
1310 let mut cur = s.cursor();
1311 replay_journal(&mut cur, &jib, &mut jh2, part).unwrap()
1312 };
1313 assert_eq!(report2.transactions_applied, 0);
1314 assert_eq!(s.read_block_at(target, JHDR_SIZE as usize), payload);
1315 }
1316
1317 #[test]
1318 fn replay_stops_at_corrupt_transaction() {
1319 let mut s = build_journal(JournalEndian::Big, 0);
1320 let t1 = JIB_OFFSET + JOURNAL_SIZE + 512;
1321 let t2 = JIB_OFFSET + JOURNAL_SIZE + 1024;
1322 let p1 = vec![0x11; JHDR_SIZE as usize];
1323 let p2 = vec![0x22; JHDR_SIZE as usize];
1324 s.dirty_blocks(&[(t1, p1.clone())]);
1325 let second_blhdr = (s.partition_offset + s.jib.offset + s.header.end) as usize;
1328 s.dirty_blocks(&[(t2, p2.clone())]);
1329 s.disk[second_blhdr + 4] ^= 0xFF;
1331
1332 let jib = s.jib.clone();
1333 let mut jh = s.header.clone();
1334 let part = s.partition_offset;
1335 let report = {
1336 let mut cur = s.cursor();
1337 replay_journal(&mut cur, &jib, &mut jh, part).unwrap()
1338 };
1339 assert_eq!(report.transactions_applied, 1);
1340 assert!(report.partial);
1341 assert_eq!(s.read_block_at(t1, JHDR_SIZE as usize), p1);
1343 assert_eq!(
1344 s.read_block_at(t2, JHDR_SIZE as usize),
1345 vec![0u8; JHDR_SIZE as usize]
1346 );
1347 }
1348
1349 #[test]
1352 fn recorder_roundtrips_three_blocks() {
1353 let mut s = build_journal(JournalEndian::Big, 4096);
1354 let mut builder = TransactionBuilder::new(s.header.jhdr_size);
1355 let base = JIB_OFFSET + JOURNAL_SIZE;
1356 let blocks: Vec<(u64, Vec<u8>)> = vec![
1357 (base, vec![1u8; JHDR_SIZE as usize]),
1358 (base + 512, vec![2u8; JHDR_SIZE as usize]),
1359 (base + 1024, vec![3u8; JHDR_SIZE as usize]),
1360 ];
1361 for (off, data) in &blocks {
1362 builder.record_block(*off, data);
1363 }
1364 assert_eq!(builder.block_count(), 3);
1365
1366 let jib = s.jib.clone();
1367 let mut jh = s.header.clone();
1368 let part = s.partition_offset;
1369 {
1370 let mut cur = s.cursor();
1371 builder
1373 .write_to_journal(&mut cur, &jib, &mut jh, part)
1374 .unwrap();
1375 }
1376
1377 let mut cur = s.cursor();
1378 let mut walker = JournalWalker::new(&mut cur, part, &jib, &jh);
1379 let txn = walker.next().unwrap().unwrap();
1380 assert_eq!(txn.sequence_num, 1);
1381 assert_eq!(txn.blocks.len(), 3);
1382 for (i, (off, data)) in blocks.iter().enumerate() {
1383 assert_eq!(txn.blocks[i].0, *off);
1384 assert_eq!(&txn.blocks[i].1, data);
1385 }
1386 assert!(walker.next().is_none());
1387 }
1388
1389 #[test]
1390 fn recorder_last_write_wins() {
1391 let mut s = build_journal(JournalEndian::Big, 0);
1392 let mut builder = TransactionBuilder::new(s.header.jhdr_size);
1393 let target = JIB_OFFSET + JOURNAL_SIZE;
1394 builder.record_block(target, &vec![0xAA; JHDR_SIZE as usize]);
1395 builder.record_block(target, &vec![0xBB; JHDR_SIZE as usize]); assert_eq!(builder.block_count(), 1);
1397
1398 let jib = s.jib.clone();
1399 let mut jh = s.header.clone();
1400 let part = s.partition_offset;
1401 {
1402 let mut cur = s.cursor();
1403 builder.commit(&mut cur, &jib, &mut jh, part).unwrap();
1404 }
1405 assert_eq!(
1406 s.read_block_at(target, JHDR_SIZE as usize),
1407 vec![0xBB; JHDR_SIZE as usize]
1408 );
1409 }
1410
1411 #[test]
1412 fn recorder_wraparound_splits_and_replays() {
1413 let part = 0u64;
1415 let jhdr = 512u32;
1416 let jsize = 8 * 1024u64;
1417 let jib_off = 4096u64;
1418 let disk = vec![0u8; (part + jib_off + jsize + 8192) as usize];
1419 let jib = JournalInfoBlock {
1420 flags: KJI_JOURNAL_IN_FS_MASK,
1421 offset: jib_off,
1422 size: jsize,
1423 };
1424 let mut header = JournalHeader {
1425 endian: JournalEndian::Big,
1426 magic: JOURNAL_HEADER_MAGIC,
1427 start: jsize - 1024,
1429 end: jsize - 1024,
1430 size: jsize,
1431 blhdr_size: 512,
1432 checksum: 0,
1433 jhdr_size: jhdr,
1434 sequence_num: 0,
1435 };
1436 let mut s = SyntheticJournal {
1437 disk,
1438 partition_offset: part,
1439 jib: jib.clone(),
1440 header: header.clone(),
1441 };
1442 s.write_header();
1443 header = s.header.clone();
1444
1445 let target = jib_off + jsize; let mut builder = TransactionBuilder::new(jhdr);
1449 builder.record_block(target, &vec![0xC1; 512]);
1450 builder.record_block(target + 512, &vec![0xC2; 512]);
1451 {
1452 let mut cur = s.cursor();
1453 builder
1454 .write_to_journal(&mut cur, &jib, &mut header, part)
1455 .unwrap();
1456 }
1457 s.header = header.clone();
1458
1459 {
1461 let mut cur = s.cursor();
1462 let mut walker = JournalWalker::new(&mut cur, part, &jib, &header);
1463 let txn = walker.next().unwrap().unwrap();
1464 assert_eq!(txn.blocks.len(), 2);
1465 assert_eq!(txn.blocks[0].1, vec![0xC1; 512]);
1466 assert_eq!(txn.blocks[1].1, vec![0xC2; 512]);
1467 }
1468
1469 let mut jh = header.clone();
1471 let report = {
1472 let mut cur = s.cursor();
1473 replay_journal(&mut cur, &jib, &mut jh, part).unwrap()
1474 };
1475 assert_eq!(report.blocks_written, 2);
1476 assert_eq!(s.read_block_at(target, 512), vec![0xC1; 512]);
1477 assert_eq!(s.read_block_at(target + 512, 512), vec![0xC2; 512]);
1478 }
1479}