Skip to main content

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