Skip to main content

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}