Skip to main content

reddb_server/storage/engine/vector_btree/
page_format.rs

1//! On-disk page format for the vector B-tree large-value path.
2//!
3//! Self-contained format module: knows nothing about pagers, MVCC, or
4//! the overflow chain. Subsequent slices wire this format into the
5//! engine; this slice lands the format itself with round-trip and v1
6//! backward-read coverage.
7//!
8//! Two changes relative to v1:
9//!
10//! 1. **`PageType::Overflow`** so deserialisation can tell overflow
11//!    pages from leaf / internal / free pages.
12//! 2. **Two leaf-cell flag bits** — `pointer` (vs inline) and
13//!    `compressed` (vs raw) — encoding the four shapes the read path
14//!    must dispatch on:
15//!      - inline + raw    → bytes-as-stored
16//!      - inline + compressed → decode then return
17//!      - pointer + raw   → follow pointer then return
18//!      - pointer + compressed → follow pointer then decode
19//!
20//! V1 cells have no flag byte. The loader infers `(inline, raw)` for
21//! every v1 cell so existing files keep reading byte-identically.
22//! New writes always emit v2.
23//!
24//! The version is exposed as a constant — callers must read it from
25//! [`FORMAT_VERSION`] / [`FORMAT_VERSION_V1`] rather than hard-coding.
26
27use std::fmt;
28
29/// Legacy on-disk format. Cells are stored as `[key_len: u16,
30/// value_len: u32, key, value]` with no flag byte; the loader infers
31/// `(inline, raw)` for every cell. v1 files keep reading correctly
32/// under v2 code.
33pub const FORMAT_VERSION_V1: u16 = 1;
34
35/// Current on-disk format. Adds `PageType::Overflow` and a one-byte
36/// flag prefix on every leaf cell.
37pub const FORMAT_VERSION_V2: u16 = 2;
38
39/// Format version stamped into freshly-written page headers. Always
40/// the latest version the code knows how to write.
41pub const FORMAT_VERSION: u16 = FORMAT_VERSION_V2;
42
43/// Size of an encoded page header in bytes.
44pub const PAGE_HEADER_SIZE: usize = 5;
45
46/// Cell flag byte layout for v2 leaf cells. Bit 0 = pointer, bit 1 =
47/// compressed. Higher bits are reserved and must be zero on disk —
48/// the decoder rejects unknown bits so a future format extension
49/// fails loudly instead of being silently misread.
50const FLAG_POINTER: u8 = 0b0000_0001;
51const FLAG_COMPRESSED: u8 = 0b0000_0010;
52const FLAG_RESERVED_MASK: u8 = !(FLAG_POINTER | FLAG_COMPRESSED);
53
54/// Type of a vector B-tree page. The byte encoding is part of the
55/// stable on-disk contract — do not reorder existing variants.
56#[repr(u8)]
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
58pub enum PageType {
59    /// Free page available for allocation.
60    Free = 0,
61    /// Leaf page holding key-value cells.
62    Leaf = 1,
63    /// Internal (interior) page holding routing keys.
64    Internal = 2,
65    /// Overflow page — continuation of a spilled large value.
66    /// Added in v2 so the engine can dispatch on page type without
67    /// touching the cell payload.
68    Overflow = 3,
69}
70
71impl PageType {
72    /// Decode a page-type byte. Unknown bytes are rejected so format
73    /// drift fails loudly at read time.
74    pub fn from_byte(b: u8) -> Result<Self, PageFormatError> {
75        match b {
76            0 => Ok(PageType::Free),
77            1 => Ok(PageType::Leaf),
78            2 => Ok(PageType::Internal),
79            3 => Ok(PageType::Overflow),
80            other => Err(PageFormatError::UnknownPageType(other)),
81        }
82    }
83
84    /// Encode as the on-disk byte.
85    #[inline]
86    pub fn to_byte(self) -> u8 {
87        self as u8
88    }
89}
90
91/// Leaf-cell flag bits. Each bit is independent — the four
92/// combinations describe how the read path interprets the payload.
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
94pub struct LeafCellFlags {
95    /// `true` → payload is a pointer to an overflow chain head.
96    /// `false` → payload bytes live in the cell.
97    pub is_pointer: bool,
98    /// `true` → payload (or what the pointer resolves to) is
99    /// compressed and must be decoded before return.
100    pub is_compressed: bool,
101}
102
103impl LeafCellFlags {
104    /// `(inline, raw)` — the v1-equivalent shape. Used as the
105    /// inferred flag for every cell read out of a v1 page.
106    pub const INLINE_RAW: Self = LeafCellFlags {
107        is_pointer: false,
108        is_compressed: false,
109    };
110
111    /// Encode the flag bits as the on-disk byte.
112    pub fn to_byte(self) -> u8 {
113        let mut b = 0u8;
114        if self.is_pointer {
115            b |= FLAG_POINTER;
116        }
117        if self.is_compressed {
118            b |= FLAG_COMPRESSED;
119        }
120        b
121    }
122
123    /// Decode a flag byte. Reserved bits must be zero — non-zero
124    /// reserved bits indicate format drift and are rejected.
125    pub fn from_byte(b: u8) -> Result<Self, PageFormatError> {
126        if b & FLAG_RESERVED_MASK != 0 {
127            return Err(PageFormatError::UnknownCellFlags(b));
128        }
129        Ok(LeafCellFlags {
130            is_pointer: b & FLAG_POINTER != 0,
131            is_compressed: b & FLAG_COMPRESSED != 0,
132        })
133    }
134}
135
136/// Decoded page header. Encoded on disk as
137/// `[version: u16 LE, page_type: u8, cell_count: u16 LE]`.
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139pub struct PageHeader {
140    pub version: u16,
141    pub page_type: PageType,
142    pub cell_count: u16,
143}
144
145impl PageHeader {
146    /// Build a fresh header at the current format version.
147    pub fn new(page_type: PageType, cell_count: u16) -> Self {
148        Self {
149            version: FORMAT_VERSION,
150            page_type,
151            cell_count,
152        }
153    }
154
155    /// Serialise into the first [`PAGE_HEADER_SIZE`] bytes of `out`.
156    pub fn encode(&self, out: &mut [u8]) -> Result<(), PageFormatError> {
157        if out.len() < PAGE_HEADER_SIZE {
158            return Err(PageFormatError::ShortBuffer {
159                need: PAGE_HEADER_SIZE,
160                got: out.len(),
161            });
162        }
163        out[0..2].copy_from_slice(&self.version.to_le_bytes());
164        out[2] = self.page_type.to_byte();
165        out[3..5].copy_from_slice(&self.cell_count.to_le_bytes());
166        Ok(())
167    }
168
169    /// Parse the first [`PAGE_HEADER_SIZE`] bytes of `bytes`. Versions
170    /// newer than [`FORMAT_VERSION`] are rejected — we never silently
171    /// read a format we cannot write.
172    pub fn decode(bytes: &[u8]) -> Result<Self, PageFormatError> {
173        if bytes.len() < PAGE_HEADER_SIZE {
174            return Err(PageFormatError::ShortBuffer {
175                need: PAGE_HEADER_SIZE,
176                got: bytes.len(),
177            });
178        }
179        let version = u16::from_le_bytes([bytes[0], bytes[1]]);
180        if version == 0 || version > FORMAT_VERSION {
181            return Err(PageFormatError::UnsupportedVersion(version));
182        }
183        let page_type = PageType::from_byte(bytes[2])?;
184        let cell_count = u16::from_le_bytes([bytes[3], bytes[4]]);
185        Ok(Self {
186            version,
187            page_type,
188            cell_count,
189        })
190    }
191}
192
193/// View of a decoded leaf cell. The payload slice borrows from the
194/// underlying buffer so decode is allocation-free; callers materialise
195/// (e.g. follow pointer + decompress) downstream.
196#[derive(Debug, Clone, Copy, PartialEq, Eq)]
197pub struct LeafCell<'a> {
198    pub flags: LeafCellFlags,
199    pub key: &'a [u8],
200    pub payload: &'a [u8],
201}
202
203/// Encode a v2 leaf cell into `out`. Format:
204/// `[flags: u8, key_len: u16 LE, payload_len: u32 LE, key, payload]`.
205pub fn encode_leaf_cell_v2(
206    flags: LeafCellFlags,
207    key: &[u8],
208    payload: &[u8],
209    out: &mut Vec<u8>,
210) -> Result<(), PageFormatError> {
211    if key.len() > u16::MAX as usize {
212        return Err(PageFormatError::FieldTooLarge {
213            field: "key",
214            len: key.len(),
215        });
216    }
217    if payload.len() > u32::MAX as usize {
218        return Err(PageFormatError::FieldTooLarge {
219            field: "payload",
220            len: payload.len(),
221        });
222    }
223    out.reserve(1 + 2 + 4 + key.len() + payload.len());
224    out.push(flags.to_byte());
225    out.extend_from_slice(&(key.len() as u16).to_le_bytes());
226    out.extend_from_slice(&(payload.len() as u32).to_le_bytes());
227    out.extend_from_slice(key);
228    out.extend_from_slice(payload);
229    Ok(())
230}
231
232/// Encode a v1 leaf cell into `out`. Format:
233/// `[key_len: u16 LE, payload_len: u32 LE, key, payload]` — no flag
234/// byte. Only used by tests that build legacy fixtures; production
235/// writes always go through [`encode_leaf_cell_v2`].
236pub fn encode_leaf_cell_v1(
237    key: &[u8],
238    payload: &[u8],
239    out: &mut Vec<u8>,
240) -> Result<(), PageFormatError> {
241    if key.len() > u16::MAX as usize {
242        return Err(PageFormatError::FieldTooLarge {
243            field: "key",
244            len: key.len(),
245        });
246    }
247    if payload.len() > u32::MAX as usize {
248        return Err(PageFormatError::FieldTooLarge {
249            field: "payload",
250            len: payload.len(),
251        });
252    }
253    out.reserve(2 + 4 + key.len() + payload.len());
254    out.extend_from_slice(&(key.len() as u16).to_le_bytes());
255    out.extend_from_slice(&(payload.len() as u32).to_le_bytes());
256    out.extend_from_slice(key);
257    out.extend_from_slice(payload);
258    Ok(())
259}
260
261/// Decode one leaf cell from the head of `bytes`, dispatching on
262/// `version`. Returns the decoded cell and the number of bytes
263/// consumed so callers can walk a packed cell stream.
264///
265/// For [`FORMAT_VERSION_V1`] there is no flag byte; the cell is
266/// reported with [`LeafCellFlags::INLINE_RAW`] and the payload bytes
267/// are returned byte-identically — that is the v1 read-compat
268/// contract.
269pub fn decode_leaf_cell(
270    version: u16,
271    bytes: &[u8],
272) -> Result<(LeafCell<'_>, usize), PageFormatError> {
273    match version {
274        FORMAT_VERSION_V1 => decode_leaf_cell_v1(bytes),
275        FORMAT_VERSION_V2 => decode_leaf_cell_v2(bytes),
276        other => Err(PageFormatError::UnsupportedVersion(other)),
277    }
278}
279
280fn decode_leaf_cell_v1(bytes: &[u8]) -> Result<(LeafCell<'_>, usize), PageFormatError> {
281    if bytes.len() < 6 {
282        return Err(PageFormatError::TruncatedCell);
283    }
284    let key_len = u16::from_le_bytes([bytes[0], bytes[1]]) as usize;
285    let payload_len = u32::from_le_bytes([bytes[2], bytes[3], bytes[4], bytes[5]]) as usize;
286    let total = 6 + key_len + payload_len;
287    if bytes.len() < total {
288        return Err(PageFormatError::TruncatedCell);
289    }
290    let key = &bytes[6..6 + key_len];
291    let payload = &bytes[6 + key_len..6 + key_len + payload_len];
292    Ok((
293        LeafCell {
294            flags: LeafCellFlags::INLINE_RAW,
295            key,
296            payload,
297        },
298        total,
299    ))
300}
301
302fn decode_leaf_cell_v2(bytes: &[u8]) -> Result<(LeafCell<'_>, usize), PageFormatError> {
303    if bytes.len() < 7 {
304        return Err(PageFormatError::TruncatedCell);
305    }
306    let flags = LeafCellFlags::from_byte(bytes[0])?;
307    let key_len = u16::from_le_bytes([bytes[1], bytes[2]]) as usize;
308    let payload_len = u32::from_le_bytes([bytes[3], bytes[4], bytes[5], bytes[6]]) as usize;
309    let total = 7 + key_len + payload_len;
310    if bytes.len() < total {
311        return Err(PageFormatError::TruncatedCell);
312    }
313    let key = &bytes[7..7 + key_len];
314    let payload = &bytes[7 + key_len..7 + key_len + payload_len];
315    Ok((
316        LeafCell {
317            flags,
318            key,
319            payload,
320        },
321        total,
322    ))
323}
324
325/// Errors returned by the page-format codec.
326#[derive(Debug, PartialEq, Eq)]
327pub enum PageFormatError {
328    UnknownPageType(u8),
329    UnknownCellFlags(u8),
330    UnsupportedVersion(u16),
331    ShortBuffer { need: usize, got: usize },
332    TruncatedCell,
333    FieldTooLarge { field: &'static str, len: usize },
334}
335
336impl fmt::Display for PageFormatError {
337    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
338        match self {
339            PageFormatError::UnknownPageType(b) => write!(f, "unknown page type byte: {}", b),
340            PageFormatError::UnknownCellFlags(b) => {
341                write!(f, "unknown leaf-cell flag bits: 0b{:08b}", b)
342            }
343            PageFormatError::UnsupportedVersion(v) => {
344                write!(f, "unsupported page format version: {}", v)
345            }
346            PageFormatError::ShortBuffer { need, got } => {
347                write!(f, "buffer too small: need {} bytes, got {}", need, got)
348            }
349            PageFormatError::TruncatedCell => write!(f, "leaf cell truncated"),
350            PageFormatError::FieldTooLarge { field, len } => {
351                write!(f, "{} length {} exceeds on-disk encoding limit", field, len)
352            }
353        }
354    }
355}
356
357impl std::error::Error for PageFormatError {}
358
359#[cfg(test)]
360mod tests {
361    use super::*;
362
363    #[test]
364    fn format_version_constant_is_v2() {
365        // The constant is the single source of truth for new writes —
366        // tests pin it so a stealth bump shows up here, not later in
367        // a corrupt-file bug report.
368        assert_eq!(FORMAT_VERSION, 2);
369        assert_eq!(FORMAT_VERSION_V1, 1);
370        assert_eq!(FORMAT_VERSION_V2, 2);
371    }
372
373    #[test]
374    fn page_header_round_trips_overflow_type() {
375        // Acceptance #1: PageType::Overflow round-trips through page
376        // header serialisation.
377        let header = PageHeader::new(PageType::Overflow, 0);
378        let mut buf = [0u8; PAGE_HEADER_SIZE];
379        header.encode(&mut buf).expect("encode");
380        let decoded = PageHeader::decode(&buf).expect("decode");
381        assert_eq!(decoded, header);
382        assert_eq!(decoded.page_type, PageType::Overflow);
383        assert_eq!(decoded.version, FORMAT_VERSION_V2);
384    }
385
386    #[test]
387    fn page_header_round_trips_every_type() {
388        for pt in [
389            PageType::Free,
390            PageType::Leaf,
391            PageType::Internal,
392            PageType::Overflow,
393        ] {
394            let header = PageHeader::new(pt, 42);
395            let mut buf = [0u8; PAGE_HEADER_SIZE];
396            header.encode(&mut buf).unwrap();
397            let decoded = PageHeader::decode(&buf).unwrap();
398            assert_eq!(decoded.page_type, pt);
399            assert_eq!(decoded.cell_count, 42);
400        }
401    }
402
403    #[test]
404    fn page_header_rejects_unknown_type_byte() {
405        let mut buf = [0u8; PAGE_HEADER_SIZE];
406        buf[0..2].copy_from_slice(&FORMAT_VERSION_V2.to_le_bytes());
407        buf[2] = 99;
408        assert_eq!(
409            PageHeader::decode(&buf).unwrap_err(),
410            PageFormatError::UnknownPageType(99)
411        );
412    }
413
414    #[test]
415    fn page_header_rejects_version_newer_than_known() {
416        let mut buf = [0u8; PAGE_HEADER_SIZE];
417        buf[0..2].copy_from_slice(&7u16.to_le_bytes());
418        buf[2] = PageType::Leaf.to_byte();
419        assert_eq!(
420            PageHeader::decode(&buf).unwrap_err(),
421            PageFormatError::UnsupportedVersion(7)
422        );
423    }
424
425    #[test]
426    fn page_header_rejects_version_zero() {
427        let mut buf = [0u8; PAGE_HEADER_SIZE];
428        buf[2] = PageType::Leaf.to_byte();
429        assert_eq!(
430            PageHeader::decode(&buf).unwrap_err(),
431            PageFormatError::UnsupportedVersion(0)
432        );
433    }
434
435    #[test]
436    fn page_header_decode_rejects_short_buffer() {
437        let buf = [0u8; PAGE_HEADER_SIZE - 1];
438        assert!(matches!(
439            PageHeader::decode(&buf),
440            Err(PageFormatError::ShortBuffer { .. })
441        ));
442    }
443
444    #[test]
445    fn leaf_cell_flags_byte_round_trip() {
446        for is_pointer in [false, true] {
447            for is_compressed in [false, true] {
448                let flags = LeafCellFlags {
449                    is_pointer,
450                    is_compressed,
451                };
452                let b = flags.to_byte();
453                assert_eq!(LeafCellFlags::from_byte(b).unwrap(), flags);
454            }
455        }
456    }
457
458    #[test]
459    fn leaf_cell_flags_reject_reserved_bits() {
460        // Acceptance: unknown bits in the flag byte are not silently
461        // dropped. A future format extension setting bit 2 must blow
462        // up under v2 code rather than be misread.
463        for reserved in [0b0000_0100u8, 0b1000_0000, 0xFF] {
464            assert_eq!(
465                LeafCellFlags::from_byte(reserved).unwrap_err(),
466                PageFormatError::UnknownCellFlags(reserved)
467            );
468        }
469    }
470
471    #[test]
472    fn all_four_leaf_cell_shapes_round_trip() {
473        // Acceptance #2: all four flag combinations round-trip with
474        // their payload preserved byte-identically.
475        let key = b"vec:42".as_slice();
476        let payload = b"\xDE\xAD\xBE\xEF\x00\x01\x02\x03".as_slice();
477        for flags in [
478            LeafCellFlags {
479                is_pointer: false,
480                is_compressed: false,
481            },
482            LeafCellFlags {
483                is_pointer: false,
484                is_compressed: true,
485            },
486            LeafCellFlags {
487                is_pointer: true,
488                is_compressed: false,
489            },
490            LeafCellFlags {
491                is_pointer: true,
492                is_compressed: true,
493            },
494        ] {
495            let mut buf = Vec::new();
496            encode_leaf_cell_v2(flags, key, payload, &mut buf).unwrap();
497            let (cell, consumed) = decode_leaf_cell(FORMAT_VERSION_V2, &buf).unwrap();
498            assert_eq!(consumed, buf.len(), "consumed must equal encoded size");
499            assert_eq!(cell.flags, flags);
500            assert_eq!(cell.key, key);
501            assert_eq!(cell.payload, payload);
502        }
503    }
504
505    #[test]
506    fn v1_cell_reads_as_inline_raw() {
507        // Acceptance #3: v1 cells have no flag byte; the loader
508        // infers (inline, raw) and returns payload byte-identically.
509        let key = b"legacy-key".as_slice();
510        let payload = b"\x00\xFF\x10\x20\x30".as_slice();
511        let mut buf = Vec::new();
512        encode_leaf_cell_v1(key, payload, &mut buf).unwrap();
513        let (cell, consumed) = decode_leaf_cell(FORMAT_VERSION_V1, &buf).unwrap();
514        assert_eq!(consumed, buf.len());
515        assert_eq!(cell.flags, LeafCellFlags::INLINE_RAW);
516        assert!(!cell.flags.is_pointer);
517        assert!(!cell.flags.is_compressed);
518        assert_eq!(cell.key, key);
519        assert_eq!(cell.payload, payload);
520    }
521
522    #[test]
523    fn v1_stream_of_cells_decodes_byte_identically() {
524        // Acceptance #3 (stream form): a v1 page is a packed stream of
525        // cells; walk the whole stream and confirm every cell comes
526        // back inline+raw with original bytes.
527        let cells: Vec<(&[u8], &[u8])> = vec![
528            (b"k0", b"v0"),
529            (b"k1", b"\x00\x01\x02"),
530            (b"", b"empty-key"),
531            (b"large", &[0xABu8; 300][..]),
532        ];
533        let mut buf = Vec::new();
534        for (k, v) in &cells {
535            encode_leaf_cell_v1(k, v, &mut buf).unwrap();
536        }
537        let mut cursor = 0;
538        for (k, v) in &cells {
539            let (cell, n) = decode_leaf_cell(FORMAT_VERSION_V1, &buf[cursor..]).unwrap();
540            assert_eq!(cell.flags, LeafCellFlags::INLINE_RAW);
541            assert_eq!(cell.key, *k);
542            assert_eq!(cell.payload, *v);
543            cursor += n;
544        }
545        assert_eq!(cursor, buf.len(), "stream fully consumed");
546    }
547
548    #[test]
549    fn freshly_created_page_writes_v2_header() {
550        // Acceptance #4 (write side): a freshly-created page header
551        // pins version = v2 even when callers don't pass a version
552        // explicitly.
553        let header = PageHeader::new(PageType::Leaf, 0);
554        assert_eq!(header.version, FORMAT_VERSION_V2);
555        let mut buf = [0u8; PAGE_HEADER_SIZE];
556        header.encode(&mut buf).unwrap();
557        assert_eq!(u16::from_le_bytes([buf[0], buf[1]]), FORMAT_VERSION_V2);
558    }
559
560    #[test]
561    fn v1_page_still_reads_after_partial_rewrites_in_place() {
562        // Acceptance #4 (read side): a v1 file rewritten in-place
563        // (some original cells, some freshly-written v1 cells) keeps
564        // reading correctly. Updated cells stay v1-format because the
565        // page header still says v1 — the v1 read path doesn't care
566        // when each cell was written, only that none of them carry a
567        // flag byte.
568        let originals: Vec<(Vec<u8>, Vec<u8>)> = vec![
569            (b"orig-a".to_vec(), b"value-a".to_vec()),
570            (b"orig-b".to_vec(), b"value-b".to_vec()),
571            (b"orig-c".to_vec(), b"value-c".to_vec()),
572        ];
573        let mut page_bytes = Vec::new();
574        // Write a v1 page header so the page's format version is 1.
575        let v1_header = PageHeader {
576            version: FORMAT_VERSION_V1,
577            page_type: PageType::Leaf,
578            cell_count: originals.len() as u16,
579        };
580        let mut hdr_buf = [0u8; PAGE_HEADER_SIZE];
581        v1_header.encode(&mut hdr_buf).unwrap();
582        page_bytes.extend_from_slice(&hdr_buf);
583
584        // Pack three v1 cells, then rewrite the middle one in-place
585        // with a new payload — still v1, no flag byte.
586        let mut cell_offsets = Vec::new();
587        for (k, v) in &originals {
588            cell_offsets.push(page_bytes.len());
589            encode_leaf_cell_v1(k, v, &mut page_bytes).unwrap();
590        }
591
592        // Replace cell 1's payload in-place with one of the same length.
593        // (Same length keeps offsets stable, which is the realistic
594        // shape of an in-place rewrite — the only kind v1 supports
595        // without restructuring the page.)
596        let new_value = b"VALUE-B"; // same length as "value-b"
597        assert_eq!(new_value.len(), originals[1].1.len());
598        let rewrite_start = cell_offsets[1] + 2 + 4 + originals[1].0.len();
599        page_bytes[rewrite_start..rewrite_start + new_value.len()].copy_from_slice(new_value);
600
601        // Reopen: header says v1, so every cell — original or
602        // rewritten — must read as (inline, raw) with the latest
603        // bytes on disk.
604        let header = PageHeader::decode(&page_bytes[..PAGE_HEADER_SIZE]).unwrap();
605        assert_eq!(header.version, FORMAT_VERSION_V1);
606        let mut cursor = PAGE_HEADER_SIZE;
607        let expected: Vec<(&[u8], &[u8])> = vec![
608            (&originals[0].0, &originals[0].1),
609            (&originals[1].0, new_value),
610            (&originals[2].0, &originals[2].1),
611        ];
612        for (k, v) in expected {
613            let (cell, n) = decode_leaf_cell(header.version, &page_bytes[cursor..]).unwrap();
614            assert_eq!(cell.flags, LeafCellFlags::INLINE_RAW);
615            assert_eq!(cell.key, k);
616            assert_eq!(cell.payload, v);
617            cursor += n;
618        }
619        assert_eq!(cursor, page_bytes.len());
620    }
621
622    #[test]
623    fn page_type_byte_values_are_stable() {
624        // Pin the on-disk encoding so a future reorder of the enum
625        // can't silently break v1 files. These bytes are the contract.
626        assert_eq!(PageType::Free.to_byte(), 0);
627        assert_eq!(PageType::Leaf.to_byte(), 1);
628        assert_eq!(PageType::Internal.to_byte(), 2);
629        assert_eq!(PageType::Overflow.to_byte(), 3);
630    }
631
632    #[test]
633    fn decode_leaf_cell_rejects_truncation() {
634        let mut buf = Vec::new();
635        encode_leaf_cell_v2(LeafCellFlags::INLINE_RAW, b"abc", b"xyz", &mut buf).unwrap();
636        for trunc in 0..buf.len() {
637            assert_eq!(
638                decode_leaf_cell(FORMAT_VERSION_V2, &buf[..trunc]).unwrap_err(),
639                PageFormatError::TruncatedCell,
640                "truncation at {} bytes must be rejected",
641                trunc
642            );
643        }
644    }
645
646    #[test]
647    fn decode_leaf_cell_unknown_version_rejected() {
648        let buf = [0u8; 16];
649        assert_eq!(
650            decode_leaf_cell(99, &buf).unwrap_err(),
651            PageFormatError::UnsupportedVersion(99)
652        );
653    }
654
655    #[test]
656    fn encoded_v2_cell_has_flag_byte_then_v1_layout() {
657        // The encoded shape is the contract a future format-aware
658        // tool will rely on. Pin it: byte 0 is flags, then v1 layout
659        // follows verbatim.
660        let mut v2 = Vec::new();
661        encode_leaf_cell_v2(
662            LeafCellFlags {
663                is_pointer: true,
664                is_compressed: false,
665            },
666            b"k",
667            b"p",
668            &mut v2,
669        )
670        .unwrap();
671        let mut v1 = Vec::new();
672        encode_leaf_cell_v1(b"k", b"p", &mut v1).unwrap();
673        assert_eq!(v2[0], FLAG_POINTER);
674        assert_eq!(&v2[1..], &v1[..]);
675    }
676}