1use crate::error::{CoreError, Result};
26
27pub const PAGE_SIZE: usize = 16 * 1024;
29pub const PAGE_HEADER_SIZE: usize = 32;
31pub const PAGE_BODY_CAP: usize = PAGE_SIZE - PAGE_HEADER_SIZE;
33pub const PAGE_MAGIC: u32 = u32::from_le_bytes(*b"QVPG");
35pub const PAGE_FORMAT_VERSION: u16 = 1;
37
38const OFF_MAGIC: usize = 0;
40const OFF_FORMAT_VER: usize = 4;
41const OFF_PAGE_TYPE: usize = 6;
42const OFF_PAGE_ID: usize = 8;
43const OFF_LSN: usize = 16;
44const OFF_PAYLOAD_LEN: usize = 24;
45const OFF_CRC: usize = 28;
46const CRC_HEADER_BYTES: usize = OFF_CRC; #[derive(Debug, Clone, Copy, PartialEq, Eq)]
52#[repr(u8)]
53#[non_exhaustive]
54pub enum PageType {
55 Manifest = 1,
57 Segment = 2,
59 IndexBlock = 3,
61}
62
63impl PageType {
64 fn from_u8(v: u8) -> Result<Self> {
65 match v {
66 1 => Ok(Self::Manifest),
67 2 => Ok(Self::Segment),
68 3 => Ok(Self::IndexBlock),
69 other => Err(CoreError::MalformedPage(format!(
70 "unknown page type {other}"
71 ))),
72 }
73 }
74}
75
76#[inline]
77fn rd_u16(p: &[u8; PAGE_SIZE], off: usize) -> u16 {
78 u16::from_le_bytes([p[off], p[off + 1]])
79}
80
81#[inline]
82fn rd_u32(p: &[u8; PAGE_SIZE], off: usize) -> u32 {
83 u32::from_le_bytes([p[off], p[off + 1], p[off + 2], p[off + 3]])
84}
85
86#[inline]
87fn rd_u64(p: &[u8; PAGE_SIZE], off: usize) -> u64 {
88 u64::from_le_bytes([
89 p[off],
90 p[off + 1],
91 p[off + 2],
92 p[off + 3],
93 p[off + 4],
94 p[off + 5],
95 p[off + 6],
96 p[off + 7],
97 ])
98}
99
100fn page_crc(page: &[u8; PAGE_SIZE], payload_len: usize) -> u32 {
102 let crc = crc32c::crc32c(&page[..CRC_HEADER_BYTES]);
103 crc32c::crc32c_append(crc, &page[PAGE_HEADER_SIZE..PAGE_HEADER_SIZE + payload_len])
104}
105
106pub fn build_page(
109 page_type: PageType,
110 page_id: u64,
111 lsn: u64,
112 body: &[u8],
113) -> Result<[u8; PAGE_SIZE]> {
114 if body.len() > PAGE_BODY_CAP {
115 return Err(CoreError::TooLarge(format!(
116 "page body {} exceeds capacity {PAGE_BODY_CAP}",
117 body.len()
118 )));
119 }
120 let mut page = [0u8; PAGE_SIZE];
121 page[OFF_MAGIC..OFF_MAGIC + 4].copy_from_slice(&PAGE_MAGIC.to_le_bytes());
122 page[OFF_FORMAT_VER..OFF_FORMAT_VER + 2].copy_from_slice(&PAGE_FORMAT_VERSION.to_le_bytes());
123 page[OFF_PAGE_TYPE] = page_type as u8;
124 page[OFF_PAGE_ID..OFF_PAGE_ID + 8].copy_from_slice(&page_id.to_le_bytes());
126 page[OFF_LSN..OFF_LSN + 8].copy_from_slice(&lsn.to_le_bytes());
127 let payload_len = body.len() as u32;
128 page[OFF_PAYLOAD_LEN..OFF_PAYLOAD_LEN + 4].copy_from_slice(&payload_len.to_le_bytes());
129 page[PAGE_HEADER_SIZE..PAGE_HEADER_SIZE + body.len()].copy_from_slice(body);
130 let crc = page_crc(&page, body.len());
131 page[OFF_CRC..OFF_CRC + 4].copy_from_slice(&crc.to_le_bytes());
132 Ok(page)
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq)]
137pub struct PageHeader {
138 pub page_type: PageType,
140 pub page_id: u64,
142 pub lsn: u64,
144 pub payload_len: u32,
146}
147
148pub fn parse_page(page: &[u8; PAGE_SIZE], expected: PageType) -> Result<(PageHeader, &[u8])> {
152 let magic = rd_u32(page, OFF_MAGIC);
153 if magic != PAGE_MAGIC {
154 return Err(CoreError::BadMagic {
155 expected: PAGE_MAGIC,
156 found: magic,
157 });
158 }
159 let format_ver = rd_u16(page, OFF_FORMAT_VER);
160 if format_ver != PAGE_FORMAT_VERSION {
161 return Err(CoreError::UnsupportedVersion {
162 found: format_ver,
163 supported: PAGE_FORMAT_VERSION,
164 });
165 }
166 let page_type = PageType::from_u8(page[OFF_PAGE_TYPE])?;
167 if page_type != expected {
168 return Err(CoreError::MalformedPage(format!(
169 "page type {page_type:?} does not match expected {expected:?}"
170 )));
171 }
172 let page_id = rd_u64(page, OFF_PAGE_ID);
173 let lsn = rd_u64(page, OFF_LSN);
174 let payload_len = rd_u32(page, OFF_PAYLOAD_LEN);
175 if payload_len as usize > PAGE_BODY_CAP {
178 return Err(CoreError::MalformedPage(format!(
179 "payload_len {payload_len} exceeds body capacity {PAGE_BODY_CAP}"
180 )));
181 }
182 let stored_crc = rd_u32(page, OFF_CRC);
183 let computed = page_crc(page, payload_len as usize);
184 if stored_crc != computed {
185 return Err(CoreError::PageCorrupt {
186 page_id,
187 expected: stored_crc,
188 computed,
189 });
190 }
191 let body = &page[PAGE_HEADER_SIZE..PAGE_HEADER_SIZE + payload_len as usize];
192 Ok((
193 PageHeader {
194 page_type,
195 page_id,
196 lsn,
197 payload_len,
198 },
199 body,
200 ))
201}
202
203pub trait PageCodec: Send + Sync {
219 fn block_size(&self) -> usize;
221
222 fn seal(&self, page_id: u64, plaintext: &[u8; PAGE_SIZE], out: &mut [u8]) -> Result<()>;
226
227 fn open(&self, page_id: u64, block: &[u8], out: &mut [u8; PAGE_SIZE]) -> Result<()>;
230
231 fn clone_box(&self) -> Box<dyn PageCodec>;
236
237 fn seal_record(&self, plaintext: &[u8]) -> Result<Vec<u8>> {
242 Ok(plaintext.to_vec())
243 }
244
245 fn open_record(&self, sealed: &[u8]) -> Result<Vec<u8>> {
249 Ok(sealed.to_vec())
250 }
251}
252
253#[derive(Debug, Default, Clone, Copy)]
256pub struct PlainCodec;
257
258impl PageCodec for PlainCodec {
259 fn block_size(&self) -> usize {
260 PAGE_SIZE
261 }
262
263 fn seal(&self, _page_id: u64, plaintext: &[u8; PAGE_SIZE], out: &mut [u8]) -> Result<()> {
264 if out.len() != PAGE_SIZE {
265 return Err(CoreError::MalformedPage(format!(
266 "seal output buffer is {} bytes, expected {PAGE_SIZE}",
267 out.len()
268 )));
269 }
270 out.copy_from_slice(plaintext);
271 Ok(())
272 }
273
274 fn open(&self, _page_id: u64, block: &[u8], out: &mut [u8; PAGE_SIZE]) -> Result<()> {
275 if block.len() != PAGE_SIZE {
276 return Err(CoreError::MalformedPage(format!(
277 "page block is {} bytes, expected {PAGE_SIZE}",
278 block.len()
279 )));
280 }
281 out.copy_from_slice(block);
282 Ok(())
283 }
284
285 fn clone_box(&self) -> Box<dyn PageCodec> {
286 Box::new(*self)
287 }
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293 use proptest::prelude::*;
294
295 #[test]
296 fn header_offsets_are_consistent() {
297 assert_eq!(PAGE_HEADER_SIZE, 32);
298 assert_eq!(PAGE_BODY_CAP, PAGE_SIZE - PAGE_HEADER_SIZE);
299 assert_eq!(OFF_CRC + 4, PAGE_HEADER_SIZE);
300 }
301
302 #[test]
303 fn build_then_parse_roundtrips() {
304 for len in [0usize, 1, 32, 1000, PAGE_BODY_CAP] {
305 let body: Vec<u8> = (0..len).map(|i| (i % 251) as u8).collect();
306 let page = build_page(PageType::Manifest, 7, 99, &body).unwrap();
307 let (hdr, got) = parse_page(&page, PageType::Manifest).unwrap();
308 assert_eq!(hdr.page_type, PageType::Manifest);
309 assert_eq!(hdr.page_id, 7);
310 assert_eq!(hdr.lsn, 99);
311 assert_eq!(hdr.payload_len as usize, len);
312 assert_eq!(got, &body[..]);
313 }
314 }
315
316 #[test]
317 fn oversized_body_is_rejected() {
318 let body = vec![0u8; PAGE_BODY_CAP + 1];
319 assert!(matches!(
320 build_page(PageType::Manifest, 0, 0, &body),
321 Err(CoreError::TooLarge(_))
322 ));
323 }
324
325 #[test]
326 fn corrupt_body_byte_is_detected() {
327 let body = vec![0xABu8; 512];
328 let mut page = build_page(PageType::Manifest, 1, 1, &body).unwrap();
329 page[PAGE_HEADER_SIZE + 10] ^= 0xFF;
330 assert!(matches!(
331 parse_page(&page, PageType::Manifest),
332 Err(CoreError::PageCorrupt { .. })
333 ));
334 }
335
336 #[test]
337 fn corrupt_header_field_is_detected() {
338 let body = vec![1u8; 64];
339 let mut page = build_page(PageType::Manifest, 1, 1, &body).unwrap();
340 page[OFF_LSN] ^= 0x01;
342 assert!(matches!(
343 parse_page(&page, PageType::Manifest),
344 Err(CoreError::PageCorrupt { .. })
345 ));
346 }
347
348 #[test]
349 fn bad_magic_is_detected() {
350 let mut page = build_page(PageType::Manifest, 1, 1, b"hi").unwrap();
351 page[OFF_MAGIC] ^= 0xFF;
352 assert!(matches!(
353 parse_page(&page, PageType::Manifest),
354 Err(CoreError::BadMagic { .. })
355 ));
356 }
357
358 #[test]
359 fn unknown_version_is_refused() {
360 let mut page = build_page(PageType::Manifest, 1, 1, b"hi").unwrap();
361 page[OFF_FORMAT_VER..OFF_FORMAT_VER + 2].copy_from_slice(&9u16.to_le_bytes());
362 let crc = page_crc(&page, 2);
364 page[OFF_CRC..OFF_CRC + 4].copy_from_slice(&crc.to_le_bytes());
365 assert!(matches!(
366 parse_page(&page, PageType::Manifest),
367 Err(CoreError::UnsupportedVersion { found: 9, .. })
368 ));
369 }
370
371 #[test]
372 fn impossible_length_is_rejected_without_panicking() {
373 let mut page = build_page(PageType::Manifest, 1, 1, b"hi").unwrap();
374 page[OFF_PAYLOAD_LEN..OFF_PAYLOAD_LEN + 4]
375 .copy_from_slice(&(PAGE_BODY_CAP as u32 + 1).to_le_bytes());
376 assert!(matches!(
377 parse_page(&page, PageType::Manifest),
378 Err(CoreError::MalformedPage(_))
379 ));
380 }
381
382 #[test]
383 fn plain_codec_roundtrips() {
384 let codec = PlainCodec;
385 assert_eq!(codec.block_size(), PAGE_SIZE);
386 let page = build_page(PageType::Manifest, 3, 3, b"payload").unwrap();
387 let mut block = vec![0u8; codec.block_size()];
388 codec.seal(3, &page, &mut block).unwrap();
389 let mut back = [0u8; PAGE_SIZE];
390 codec.open(3, &block, &mut back).unwrap();
391 assert_eq!(page, back);
392 }
393
394 #[test]
395 fn plain_codec_rejects_wrong_buffer_sizes() {
396 let codec = PlainCodec;
397 let page = [0u8; PAGE_SIZE];
398 let mut small = vec![0u8; PAGE_SIZE - 1];
399 assert!(codec.seal(0, &page, &mut small).is_err());
400 let mut back = [0u8; PAGE_SIZE];
401 assert!(codec.open(0, &small, &mut back).is_err());
402 }
403
404 proptest! {
405 #[test]
406 fn any_body_roundtrips(body in proptest::collection::vec(any::<u8>(), 0..PAGE_BODY_CAP)) {
407 let page = build_page(PageType::Manifest, 42, 7, &body).unwrap();
408 let (hdr, got) = parse_page(&page, PageType::Manifest).unwrap();
409 prop_assert_eq!(hdr.payload_len as usize, body.len());
410 prop_assert_eq!(got, &body[..]);
411 }
412
413 #[test]
414 fn any_single_byte_flip_in_live_region_is_detected(
415 body in proptest::collection::vec(any::<u8>(), 1..2048usize),
416 flip in 0usize..(PAGE_HEADER_SIZE + 2048),
417 ) {
418 let page = build_page(PageType::Manifest, 1, 1, &body).unwrap();
419 let live = PAGE_HEADER_SIZE + body.len();
420 prop_assume!(flip < live);
423 prop_assume!(!(OFF_CRC..PAGE_HEADER_SIZE).contains(&flip));
424 let mut corrupt = page;
425 corrupt[flip] ^= 0x80;
426 prop_assert!(parse_page(&corrupt, PageType::Manifest).is_err());
428 }
429 }
430}