Skip to main content

reifydb_codec/log/
reader.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use crc32fast::Hasher;
5
6use crate::log::LogVersion;
7
8pub const HINT_BYTES: usize = 16;
9
10pub const MAGIC: u32 = u32::from_le_bytes(*b"RRDR");
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub struct Hint {
14	pub version: LogVersion,
15}
16
17impl Hint {
18	pub fn new(version: LogVersion) -> Self {
19		Self {
20			version,
21		}
22	}
23
24	pub fn encode(&self) -> [u8; HINT_BYTES] {
25		let mut out = [0u8; HINT_BYTES];
26		out[0..4].copy_from_slice(&MAGIC.to_le_bytes());
27		out[8..16].copy_from_slice(&self.version.as_u64().to_le_bytes());
28		let checksum = checksum(&out[8..]);
29		out[4..8].copy_from_slice(&checksum.to_le_bytes());
30		out
31	}
32
33	pub fn decode(buf: &[u8; HINT_BYTES]) -> Option<Self> {
34		if u32::from_le_bytes(buf[0..4].try_into().unwrap()) != MAGIC {
35			return None;
36		}
37		if u32::from_le_bytes(buf[4..8].try_into().unwrap()) != checksum(&buf[8..]) {
38			return None;
39		}
40		Some(Self {
41			version: LogVersion::new(u64::from_le_bytes(buf[8..16].try_into().unwrap())),
42		})
43	}
44}
45
46fn checksum(bytes: &[u8]) -> u32 {
47	let mut hasher = Hasher::new();
48	hasher.update(bytes);
49	hasher.finalize()
50}
51
52#[cfg(test)]
53mod tests {
54	use super::*;
55
56	#[test]
57	fn a_hint_lays_its_fields_out_at_the_documented_offsets() {
58		// the hint is read by a purge that is about to unlink files, so a field that moves makes an
59		// old hint decode as some other version and the log deletes a segment a reader still needs.
60		let raw = Hint::new(LogVersion::new(0x0102030405060708)).encode();
61
62		assert_eq!(raw.len(), HINT_BYTES);
63		assert_eq!(&raw[0..4], &MAGIC.to_le_bytes());
64		assert_eq!(&raw[4..8], &0xa5cced25u32.to_le_bytes());
65		assert_eq!(&raw[8..16], &0x0102030405060708u64.to_le_bytes());
66	}
67
68	#[test]
69	fn a_hint_round_trips_through_its_bytes() {
70		let hint = Hint::new(LogVersion::new(4096));
71
72		assert_eq!(Hint::decode(&hint.encode()), Some(hint));
73	}
74
75	#[test]
76	fn a_hint_of_all_zeros_does_not_decode() {
77		// decision 234: the hint is published without an fsync, so a crash can make the name durable
78		// before the bytes. zeros must read as unreadable, which pins at zero, never as version zero
79		// arrived at legitimately, which would be indistinguishable from a reader that has read nothing.
80		assert_eq!(Hint::decode(&[0u8; HINT_BYTES]), None);
81	}
82
83	#[test]
84	fn a_flipped_bit_anywhere_in_the_version_is_caught_by_the_checksum() {
85		// decision 239: retention unlinks segments on this number, and half the bit flips in the
86		// version word raise it. A raised floor deletes records a reader still needs, silently and for
87		// good, so every flip has to fail the decode and fall back to pinning at the beginning.
88		for at in 8..HINT_BYTES {
89			let mut raw = Hint::new(LogVersion::new(500_000)).encode();
90			raw[at] ^= 0x01;
91
92			assert_eq!(Hint::decode(&raw), None, "a flip at byte {at} decoded anyway");
93		}
94	}
95
96	#[test]
97	fn a_flipped_bit_in_the_reserved_word_is_caught_too() {
98		// the checksum lives there, so a file damaged in the one place that used to be ignored now
99		// reads as damaged rather than as a hint nobody wrote.
100		for at in 4..8 {
101			let mut raw = Hint::new(LogVersion::new(500_000)).encode();
102			raw[at] ^= 0x01;
103
104			assert_eq!(Hint::decode(&raw), None, "a flip at byte {at} decoded anyway");
105		}
106	}
107
108	#[test]
109	fn a_foreign_file_is_refused_by_its_magic() {
110		let mut raw = Hint::new(LogVersion::new(7)).encode();
111		raw[0..4].copy_from_slice(&u32::from_le_bytes(*b"RIDX").to_le_bytes());
112
113		assert_eq!(Hint::decode(&raw), None);
114	}
115}