obj_core/pager/checksum.rs
1//! CRC32C helpers used by both the page-0 header and the page trailer.
2//!
3//! The choice of CRC32C (Castagnoli) is fixed at format-major 0; see
4//! `docs/format.md` § Checksum algorithm for the rationale. This
5//! module is the only place that calls into the `crc32c` crate so the
6//! algorithm can be revisited in one edit.
7//!
8//! # Trailer formats
9//!
10//! Two trailer interpretations live side by side:
11//!
12//! - **v0** ([`write_page_trailer`] / [`page_trailer_valid`]) — the
13//! full 32-bit CRC32C of bytes `[0..PAGE_SIZE - PAGE_TRAILER_SIZE]`.
14//! Used by every non-header page in a `format_minor = 0` file, and
15//! by the in-memory representation of every page in every file
16//! (the pager re-stamps the on-disk trailer on `write_back_page`).
17//! - **v1** ([`write_page_trailer_v1`] / [`page_trailer_valid_v1`] /
18//! [`page_trailer_flag_v1`]) — bit 31 of the trailer is the
19//! per-page compression flag; bits 0..30 are the 31-bit CRC32C of
20//! the on-disk bytes. Phase 3 (issue #8) — see `docs/format.md`
21//! § Page trailer for the rationale of the 32→31-bit precision
22//! tradeoff.
23
24#![forbid(unsafe_code)]
25
26use crate::pager::page::{Page, PAGE_SIZE, PAGE_TRAILER_SIZE};
27
28/// Compute the CRC32C of `bytes` using the Castagnoli polynomial.
29#[must_use]
30pub fn crc32c(bytes: &[u8]) -> u32 {
31 crc32c::crc32c(bytes)
32}
33
34/// Continue a CRC32C computation: fold `bytes` into a running CRC
35/// produced by a prior [`crc32c()`] / [`crc32c_append`] call. The
36/// result is byte-identical to `crc32c(prefix ++ bytes)` where
37/// `prefix` was the input that produced `crc`. This is the only
38/// place that calls into the `crc32c` crate's incremental API, so
39/// the WAL frame CRC can be computed over discontiguous segments
40/// (header sans-CRC, zeroed CRC field, page body) without allocating
41/// a contiguous scratch buffer.
42#[must_use]
43pub fn crc32c_append(crc: u32, bytes: &[u8]) -> u32 {
44 crc32c::crc32c_append(crc, bytes)
45}
46
47/// Byte offset of the page trailer inside any non-header page.
48pub const TRAILER_OFFSET: usize = PAGE_SIZE - PAGE_TRAILER_SIZE;
49
50/// Mask isolating the lower 31 bits of the v1 trailer (CRC region).
51pub const V1_CRC_MASK: u32 = 0x7FFF_FFFF;
52/// Mask isolating bit 31 of the v1 trailer (compression flag).
53pub const V1_FLAG_MASK: u32 = 0x8000_0000;
54
55/// Compute the page trailer for `page` and write it into the last
56/// [`PAGE_TRAILER_SIZE`] bytes (v0 interpretation: full 32-bit CRC).
57pub fn write_page_trailer(page: &mut Page) {
58 let buf = page.as_bytes_mut();
59 let crc = crc32c(&buf[..TRAILER_OFFSET]);
60 buf[TRAILER_OFFSET..].copy_from_slice(&crc.to_le_bytes());
61}
62
63/// `true` iff the page trailer matches the recomputed CRC32C of the
64/// body (v0 interpretation). Caller decides what error to surface
65/// on mismatch.
66#[must_use]
67pub fn page_trailer_valid(page: &Page) -> bool {
68 let buf = page.as_bytes();
69 let stored = read_trailer_u32(buf);
70 let computed = crc32c(&buf[..TRAILER_OFFSET]);
71 stored == computed
72}
73
74/// Phase 3 (issue #8): v1 trailer writer. Stamps the per-page
75/// `compressed` flag into bit 31 and the 31-bit CRC32C of bytes
76/// `[0..TRAILER_OFFSET]` into bits 0..30.
77pub fn write_page_trailer_v1(page: &mut Page, compressed: bool) {
78 let buf = page.as_bytes_mut();
79 let crc = crc32c(&buf[..TRAILER_OFFSET]) & V1_CRC_MASK;
80 let flag = if compressed { V1_FLAG_MASK } else { 0 };
81 let trailer = crc | flag;
82 buf[TRAILER_OFFSET..].copy_from_slice(&trailer.to_le_bytes());
83}
84
85/// Phase 3 (issue #8): v1 trailer verifier. Compares the lower 31
86/// bits of the trailer against the recomputed 31-bit CRC32C of
87/// bytes `[0..TRAILER_OFFSET]`. The flag bit (bit 31) is NOT part
88/// of the checksum.
89///
90/// #61 (residual risk — intentionally NOT fixed): masking bit 31
91/// out of the CRC means the per-page compression flag is not
92/// covered by page integrity. On a PLAINTEXT (`format_minor < 2`
93/// path or non-encrypted) file, a bit-rot flip of bit 31 alone is
94/// NOT detected here — the CRC over `[0..TRAILER_OFFSET]` still
95/// matches, yet the flipped flag changes whether the body is
96/// decoded as LZ4-compressed or raw, which decompression will
97/// usually (but not always) reject downstream. On ENCRYPTED files
98/// the body's AEAD tag covers the post-CRC physical bytes, so a
99/// flipped flag fails Poly1305 verification and IS detected.
100/// Including bit 31 in the CRC is deliberately avoided because it
101/// would change the on-disk trailer of every existing v1 page and
102/// break reading already-written files.
103#[must_use]
104pub fn page_trailer_valid_v1(page: &Page) -> bool {
105 let buf = page.as_bytes();
106 let stored = read_trailer_u32(buf) & V1_CRC_MASK;
107 let computed = crc32c(&buf[..TRAILER_OFFSET]) & V1_CRC_MASK;
108 stored == computed
109}
110
111/// Phase 3 (issue #8): read the v1 compression flag (bit 31) from
112/// the trailer. The caller is responsible for verifying the
113/// trailer with [`page_trailer_valid_v1`] BEFORE consulting the
114/// flag — an unverified buffer's flag is meaningless.
115#[must_use]
116pub fn page_trailer_flag_v1(page: &Page) -> bool {
117 let buf = page.as_bytes();
118 (read_trailer_u32(buf) & V1_FLAG_MASK) != 0
119}
120
121/// Internal helper: read the 4-byte trailer as a little-endian `u32`.
122fn read_trailer_u32(buf: &[u8; PAGE_SIZE]) -> u32 {
123 let mut t = [0u8; PAGE_TRAILER_SIZE];
124 t.copy_from_slice(&buf[TRAILER_OFFSET..]);
125 u32::from_le_bytes(t)
126}
127
128#[cfg(test)]
129mod tests {
130 use super::{
131 page_trailer_flag_v1, page_trailer_valid, page_trailer_valid_v1, write_page_trailer,
132 write_page_trailer_v1, V1_FLAG_MASK,
133 };
134 use crate::pager::page::Page;
135
136 #[test]
137 fn trailer_round_trip() {
138 let mut p = Page::zeroed();
139 for (i, b) in p.as_bytes_mut().iter_mut().enumerate().take(64) {
140 *b = u8::try_from(i).expect("i < 64");
141 }
142 write_page_trailer(&mut p);
143 assert!(page_trailer_valid(&p));
144 }
145
146 #[test]
147 fn flipping_any_body_byte_invalidates_trailer() {
148 let mut p = Page::zeroed();
149 p.as_bytes_mut()[100] = 0xAA;
150 write_page_trailer(&mut p);
151 assert!(page_trailer_valid(&p));
152 p.as_bytes_mut()[42] ^= 0x01;
153 assert!(!page_trailer_valid(&p));
154 }
155
156 #[test]
157 fn flipping_trailer_byte_invalidates_trailer() {
158 let mut p = Page::zeroed();
159 write_page_trailer(&mut p);
160 assert!(page_trailer_valid(&p));
161 // Flip the high byte of the trailer.
162 let len = p.as_bytes().len();
163 p.as_bytes_mut()[len - 1] ^= 0x80;
164 assert!(!page_trailer_valid(&p));
165 }
166
167 #[test]
168 fn v1_trailer_round_trip_uncompressed() {
169 let mut p = Page::zeroed();
170 p.as_bytes_mut()[10] = 0xAB;
171 write_page_trailer_v1(&mut p, false);
172 assert!(page_trailer_valid_v1(&p));
173 assert!(!page_trailer_flag_v1(&p));
174 }
175
176 #[test]
177 fn v1_trailer_round_trip_compressed() {
178 let mut p = Page::zeroed();
179 p.as_bytes_mut()[10] = 0xCD;
180 write_page_trailer_v1(&mut p, true);
181 assert!(page_trailer_valid_v1(&p));
182 assert!(page_trailer_flag_v1(&p));
183 }
184
185 #[test]
186 fn v1_trailer_flag_independent_of_crc() {
187 // Stamp with flag=false, then flip bit 31 manually. The CRC
188 // (bits 0..30) is unchanged so the trailer must STILL verify.
189 let mut p = Page::zeroed();
190 p.as_bytes_mut()[10] = 0xAB;
191 write_page_trailer_v1(&mut p, false);
192 assert!(page_trailer_valid_v1(&p));
193 assert!(!page_trailer_flag_v1(&p));
194 let len = p.as_bytes().len();
195 let trailer_off = len - 4;
196 let mut t = [0u8; 4];
197 t.copy_from_slice(&p.as_bytes()[trailer_off..]);
198 let mut trailer = u32::from_le_bytes(t);
199 trailer ^= V1_FLAG_MASK;
200 p.as_bytes_mut()[trailer_off..].copy_from_slice(&trailer.to_le_bytes());
201 assert!(page_trailer_valid_v1(&p));
202 assert!(page_trailer_flag_v1(&p));
203 }
204
205 #[test]
206 fn v1_trailer_flipping_body_invalidates() {
207 let mut p = Page::zeroed();
208 write_page_trailer_v1(&mut p, true);
209 assert!(page_trailer_valid_v1(&p));
210 p.as_bytes_mut()[42] ^= 0x01;
211 assert!(!page_trailer_valid_v1(&p));
212 }
213}