Skip to main content

reddb_types/
document_body_codec.rs

1//! Native binary container for document bodies (PRD-1398, ADR-0063).
2//!
3//! Encodes a named-field document (`&[(&str, &Value)]`) into a compact binary
4//! container with a **header offset table** that enables O(1) access to any
5//! top-level field value without decoding the entire document.
6//!
7//! ## Format — version 3
8//!
9//! ```text
10//! [0..4]  magic        = b"RDOC"
11//! [4]     version      = 0x03
12//! [5..7]  num_fields   : u16 LE          (max 65 535 fields per document)
13//! [7..]   offset table : num_fields × [key_len: u16 LE, val_offset: u32 LE]
14//!         keys section : concatenated UTF-8 field name bytes (key_len[i] each)
15//!         values section: concatenated value_codec-encoded bytes
16//! ```
17//!
18//! `val_offset` is an **absolute** byte offset from byte 0 of the container,
19//! allowing a single value to be decoded with [`decode_value_at_offset`] after
20//! reading only the header — the rest of the payload stays untouched.
21//!
22//! ## Flag-dark
23//!
24//! This codec is compiled and tested but not yet wired into any storage or
25//! query path.  Behaviour cutover happens in a later PRD-1398 slice.
26
27use crate::key_dictionary::KeyDictionary;
28use crate::types::Value;
29use crate::value_codec;
30
31/// Magic bytes at the start of every document body container.
32pub const MAGIC: &[u8; 4] = b"RDOC";
33
34/// Format version byte for the plain (inline-keys-only) container.
35pub const VERSION: u8 = 0x03;
36
37/// Format version byte for the dictionary-aware container (PRD-1398).
38///
39/// Identical layout to the plain container except each offset-table entry carries a key *kind*
40/// tag: a field name is either a key-id into the per-collection
41/// [`KeyDictionary`] (common keys) or stored inline (rare/unique keys).
42pub const VERSION_DICT: u8 = 0x04;
43
44const SUPERSEDED_VERSION: u8 = 0x01;
45const SUPERSEDED_VERSION_DICT: u8 = 0x02;
46
47/// Byte size of one entry in the offset table: u16 key_len + u32 val_offset.
48const ENTRY_SIZE: usize = 6;
49
50/// Byte size of one v2 offset-table entry: u8 key_kind + u32 key_ref + u32 val_offset.
51///
52/// The key-id is stored as a fixed `u32` (rather than a varint) so the table
53/// keeps a fixed stride and field access stays O(1); homogeneous collections
54/// never approach `u32::MAX` distinct common keys.
55const DICT_ENTRY_SIZE: usize = 9;
56
57/// `key_kind` tag: the field name is a key-id into the [`KeyDictionary`].
58const KEY_KIND_DICT: u8 = 0x00;
59
60/// `key_kind` tag: the field name is stored inline in the keys section.
61const KEY_KIND_INLINE: u8 = 0x01;
62
63/// One parsed v2 offset-table entry: `(key_kind, key_ref, val_offset)`.
64///
65/// `key_ref` is a dictionary key-id when `key_kind == KEY_KIND_DICT`, or the
66/// inline key length when `key_kind == KEY_KIND_INLINE`.
67pub type DictEntry = (u8, u32, u32);
68
69/// Errors produced by the document body codec.
70#[derive(Debug, PartialEq)]
71pub enum DocBodyError {
72    /// Buffer is too short to hold the header or offset table.
73    TruncatedData,
74    /// First 4 bytes do not match `b"RDOC"`.
75    BadMagic,
76    /// Version byte is not a version this codec understands.
77    UnsupportedVersion(u8),
78    /// Version byte is from a pre-lossless-integer format that must not be reinterpreted.
79    SupersededVersion(u8),
80    /// A field name or value points outside the container buffer.
81    OffsetOutOfBounds,
82    /// A field name is not valid UTF-8.
83    InvalidFieldName,
84    /// The document has more than 65 535 fields, or a field name exceeds 65 535 bytes.
85    FieldLimitExceeded,
86    /// A dictionary key-id in the body is absent from the supplied dictionary.
87    UnknownKeyId(u32),
88    /// A v2 offset-table entry carried an unrecognised key-kind tag.
89    BadKeyKind(u8),
90    /// The underlying value codec rejected a value.
91    ValueCodecError(crate::types::ValueError),
92}
93
94impl From<crate::types::ValueError> for DocBodyError {
95    fn from(e: crate::types::ValueError) -> Self {
96        DocBodyError::ValueCodecError(e)
97    }
98}
99
100impl std::fmt::Display for DocBodyError {
101    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102        match self {
103            Self::TruncatedData => write!(f, "document body: truncated data"),
104            Self::BadMagic => write!(f, "document body: bad magic bytes (expected RDOC)"),
105            Self::UnsupportedVersion(v) => write!(f, "document body: unsupported version {v}"),
106            Self::SupersededVersion(v) => write!(
107                f,
108                "document body: format version {v} is superseded by version {VERSION} for the \
109                 lossless-integer clean break (#1768). Pre-bump bodies stored numbers as f64, \
110                 so they are refused rather than silently reinterpreted; re-insert the document \
111                 to rewrite it in the current format."
112            ),
113            Self::OffsetOutOfBounds => write!(f, "document body: offset points outside buffer"),
114            Self::InvalidFieldName => write!(f, "document body: field name is not valid UTF-8"),
115            Self::FieldLimitExceeded => {
116                write!(f, "document body: field or key-length limit exceeded")
117            }
118            Self::UnknownKeyId(id) => {
119                write!(f, "document body: key-id {id} not found in dictionary")
120            }
121            Self::BadKeyKind(k) => write!(f, "document body: unrecognised key kind {k:#04x}"),
122            Self::ValueCodecError(e) => write!(f, "document body: value codec error: {e}"),
123        }
124    }
125}
126
127impl std::error::Error for DocBodyError {}
128
129/// Encode `fields` as a document body container, appending bytes to `out`.
130///
131/// Fields are written in iteration order.  Duplicate field names are allowed
132/// (deduplication is the caller's responsibility).
133///
134/// Returns an error only if `fields.len() > 65535` or a field name is longer
135/// than 65535 bytes; both are pathological in practice.
136pub fn encode(fields: &[(&str, &Value)], out: &mut Vec<u8>) -> Result<(), DocBodyError> {
137    let n = fields.len();
138    if n > u16::MAX as usize {
139        return Err(DocBodyError::FieldLimitExceeded);
140    }
141
142    // Encode all keys and values into scratch buffers, tracking per-field
143    // sizes so we can compute absolute offsets before writing anything.
144    let mut key_buf: Vec<u8> = Vec::new();
145    let mut val_buf: Vec<u8> = Vec::new();
146    let mut key_lens: Vec<u16> = Vec::with_capacity(n);
147    let mut val_starts: Vec<u32> = Vec::with_capacity(n);
148
149    for (key, value) in fields {
150        let kb = key.as_bytes();
151        if kb.len() > u16::MAX as usize {
152            return Err(DocBodyError::FieldLimitExceeded);
153        }
154        key_lens.push(kb.len() as u16);
155        key_buf.extend_from_slice(kb);
156
157        val_starts.push(val_buf.len() as u32); // relative for now
158        value_codec::encode(value, &mut val_buf);
159    }
160
161    // Absolute offset of the values section within the final container:
162    //   magic(4) + version(1) + num_fields(2) + table(n * ENTRY_SIZE) + keys
163    let vals_abs_start = 4 + 1 + 2 + n * ENTRY_SIZE + key_buf.len();
164
165    // Write header
166    out.extend_from_slice(MAGIC);
167    out.push(VERSION);
168    out.extend_from_slice(&(n as u16).to_le_bytes());
169
170    // Write offset table
171    for i in 0..n {
172        let abs_val_offset = vals_abs_start + val_starts[i] as usize;
173        out.extend_from_slice(&key_lens[i].to_le_bytes());
174        out.extend_from_slice(&(abs_val_offset as u32).to_le_bytes());
175    }
176
177    // Write keys then values
178    out.extend_from_slice(&key_buf);
179    out.extend_from_slice(&val_buf);
180
181    Ok(())
182}
183
184/// Decode all fields from a document body container.
185///
186/// Fields are returned in the same order they were encoded.
187pub fn decode(data: &[u8]) -> Result<Vec<(String, Value)>, DocBodyError> {
188    let (n, table) = parse_header(data)?;
189    let header_end = 7 + n * ENTRY_SIZE; // where the keys section begins
190    let mut key_cursor = header_end;
191    let mut result = Vec::with_capacity(n);
192
193    for &(key_len, val_offset) in &table {
194        // Read field name
195        let key_end = key_cursor + key_len as usize;
196        if key_end > data.len() {
197            return Err(DocBodyError::OffsetOutOfBounds);
198        }
199        let key = std::str::from_utf8(&data[key_cursor..key_end])
200            .map_err(|_| DocBodyError::InvalidFieldName)?
201            .to_string();
202        key_cursor = key_end;
203
204        // Decode value by absolute offset
205        let value = decode_value_at_offset(data, val_offset)?;
206        result.push((key, value));
207    }
208
209    Ok(result)
210}
211
212/// Return the top-level field names in encode order without decoding any value.
213///
214/// Reads only the offset table and the keys section — the values section is
215/// never touched. Used to build a field-name index/bloom over a stored body
216/// (e.g. so a `WHERE`/projection on a promoted field can route to the body)
217/// without paying the cost of a full [`decode`].
218pub fn field_names(data: &[u8]) -> Result<Vec<String>, DocBodyError> {
219    let (n, table) = parse_header(data)?;
220    let header_end = 7 + n * ENTRY_SIZE; // where the keys section begins
221    let mut key_cursor = header_end;
222    let mut names = Vec::with_capacity(n);
223
224    for &(key_len, _val_offset) in &table {
225        let key_end = key_cursor + key_len as usize;
226        if key_end > data.len() {
227            return Err(DocBodyError::OffsetOutOfBounds);
228        }
229        let key = std::str::from_utf8(&data[key_cursor..key_end])
230            .map_err(|_| DocBodyError::InvalidFieldName)?
231            .to_string();
232        key_cursor = key_end;
233        names.push(key);
234    }
235
236    Ok(names)
237}
238
239/// Read a single field by name without decoding any other value.
240///
241/// Returns `None` when the field is absent.  Only the matching field's
242/// encoded bytes are passed to [`value_codec::decode`]; everything else
243/// is skipped as raw bytes.
244pub fn read_field_by_name(data: &[u8], name: &str) -> Result<Option<Value>, DocBodyError> {
245    let (n, table) = parse_header(data)?;
246    let header_end = 7 + n * ENTRY_SIZE;
247    let mut key_cursor = header_end;
248
249    for &(key_len, val_offset) in &table {
250        let key_end = key_cursor + key_len as usize;
251        if key_end > data.len() {
252            return Err(DocBodyError::OffsetOutOfBounds);
253        }
254        let key_bytes = &data[key_cursor..key_end];
255        key_cursor = key_end;
256
257        if key_bytes == name.as_bytes() {
258            let value = decode_value_at_offset(data, val_offset)?;
259            return Ok(Some(value));
260        }
261    }
262
263    Ok(None)
264}
265
266/// Decode a single value using its **absolute** byte offset within the
267/// container.
268///
269/// This is the O(1) access path once the caller has read the offset table
270/// (e.g. via [`parse_header`]).  No other field is decoded.
271pub fn decode_value_at_offset(data: &[u8], val_offset: u32) -> Result<Value, DocBodyError> {
272    let off = val_offset as usize;
273    if off >= data.len() {
274        return Err(DocBodyError::OffsetOutOfBounds);
275    }
276    let (value, _) = value_codec::decode(&data[off..])?;
277    Ok(value)
278}
279
280fn version_error(version: u8) -> DocBodyError {
281    if version == SUPERSEDED_VERSION || version == SUPERSEDED_VERSION_DICT {
282        DocBodyError::SupersededVersion(version)
283    } else {
284        DocBodyError::UnsupportedVersion(version)
285    }
286}
287
288/// Parse the container header and return `(num_fields, offset_table)`.
289///
290/// Each table entry is `(key_len: u16, val_offset: u32)`.  The table may be
291/// empty for zero-field documents.
292///
293/// Does **not** validate that field names or values are within bounds — that
294/// is deferred to the decode functions that actually walk those sections.
295pub fn parse_header(data: &[u8]) -> Result<(usize, Vec<(u16, u32)>), DocBodyError> {
296    if data.len() < 7 {
297        return Err(DocBodyError::TruncatedData);
298    }
299    if &data[0..4] != MAGIC.as_slice() {
300        return Err(DocBodyError::BadMagic);
301    }
302    if data[4] != VERSION {
303        return Err(version_error(data[4]));
304    }
305
306    let n = u16::from_le_bytes([data[5], data[6]]) as usize;
307    let table_end = 7 + n * ENTRY_SIZE;
308    if data.len() < table_end {
309        return Err(DocBodyError::TruncatedData);
310    }
311
312    let mut table = Vec::with_capacity(n);
313    for i in 0..n {
314        let base = 7 + i * ENTRY_SIZE;
315        let key_len = u16::from_le_bytes([data[base], data[base + 1]]);
316        let val_offset = u32::from_le_bytes([
317            data[base + 2],
318            data[base + 3],
319            data[base + 4],
320            data[base + 5],
321        ]);
322        table.push((key_len, val_offset));
323    }
324
325    Ok((n, table))
326}
327
328/// Encode `fields` as a **dictionary-aware** (v2) document body container.
329///
330/// For each field `classify(name)` decides how the key is stored:
331///
332/// * `true`  — the key is *common*: it is interned into `dict` (appending a new
333///   id transactionally if it is not already present) and the body stores the
334///   compact key-id.
335/// * `false` — the key is *rare/unique*: it is stored **inline** in the body
336///   and never enters `dict`, so a heterogeneous collection cannot bloat the
337///   shared catalogue.
338///
339/// Decode with [`decode_with_dictionary`] using the (post-encode) dictionary.
340pub fn encode_with_dictionary(
341    fields: &[(&str, &Value)],
342    dict: &mut KeyDictionary,
343    classify: impl Fn(&str) -> bool,
344    out: &mut Vec<u8>,
345) -> Result<(), DocBodyError> {
346    let n = fields.len();
347    if n > u16::MAX as usize {
348        return Err(DocBodyError::FieldLimitExceeded);
349    }
350
351    // Inline key bytes (only for rare keys) and value bytes, plus per-field
352    // table data, are staged so absolute offsets can be computed up front.
353    let mut key_buf: Vec<u8> = Vec::new();
354    let mut val_buf: Vec<u8> = Vec::new();
355    let mut kinds: Vec<u8> = Vec::with_capacity(n);
356    let mut refs: Vec<u32> = Vec::with_capacity(n);
357    let mut val_starts: Vec<u32> = Vec::with_capacity(n);
358
359    for (key, value) in fields {
360        if classify(key) {
361            let id = dict.intern(key);
362            kinds.push(KEY_KIND_DICT);
363            refs.push(id);
364        } else {
365            let kb = key.as_bytes();
366            if kb.len() > u16::MAX as usize {
367                return Err(DocBodyError::FieldLimitExceeded);
368            }
369            kinds.push(KEY_KIND_INLINE);
370            refs.push(kb.len() as u32);
371            key_buf.extend_from_slice(kb);
372        }
373        val_starts.push(val_buf.len() as u32); // relative for now
374        value_codec::encode(value, &mut val_buf);
375    }
376
377    // Absolute offset of the values section within the final container:
378    //   magic(4) + version(1) + num_fields(2) + table(n * DICT_ENTRY_SIZE) + inline keys
379    let vals_abs_start = 4 + 1 + 2 + n * DICT_ENTRY_SIZE + key_buf.len();
380
381    out.extend_from_slice(MAGIC);
382    out.push(VERSION_DICT);
383    out.extend_from_slice(&(n as u16).to_le_bytes());
384
385    for i in 0..n {
386        let abs_val_offset = vals_abs_start + val_starts[i] as usize;
387        out.push(kinds[i]);
388        out.extend_from_slice(&refs[i].to_le_bytes());
389        out.extend_from_slice(&(abs_val_offset as u32).to_le_bytes());
390    }
391
392    out.extend_from_slice(&key_buf);
393    out.extend_from_slice(&val_buf);
394
395    Ok(())
396}
397
398/// Decode all fields from a **dictionary-aware** (v2) document body container.
399///
400/// `dict` must be the per-collection dictionary that was used (and possibly
401/// extended) during [`encode_with_dictionary`]; dictionary key-ids are resolved
402/// back to field names through it, while inline keys are read straight from the
403/// body.  Fields are returned in encode order.
404pub fn decode_with_dictionary(
405    data: &[u8],
406    dict: &KeyDictionary,
407) -> Result<Vec<(String, Value)>, DocBodyError> {
408    let (n, table) = parse_dict_header(data)?;
409    let header_end = 7 + n * DICT_ENTRY_SIZE; // where the inline-keys section begins
410    let mut key_cursor = header_end;
411    let mut result = Vec::with_capacity(n);
412
413    for &(kind, key_ref, val_offset) in &table {
414        let key = match kind {
415            KEY_KIND_DICT => dict
416                .name_of(key_ref)
417                .ok_or(DocBodyError::UnknownKeyId(key_ref))?
418                .to_string(),
419            KEY_KIND_INLINE => {
420                let key_end = key_cursor + key_ref as usize;
421                if key_end > data.len() {
422                    return Err(DocBodyError::OffsetOutOfBounds);
423                }
424                let name = std::str::from_utf8(&data[key_cursor..key_end])
425                    .map_err(|_| DocBodyError::InvalidFieldName)?
426                    .to_string();
427                key_cursor = key_end;
428                name
429            }
430            other => return Err(DocBodyError::BadKeyKind(other)),
431        };
432
433        let value = decode_value_at_offset(data, val_offset)?;
434        result.push((key, value));
435    }
436
437    Ok(result)
438}
439
440/// Parse a **v2** container header, returning `(num_fields, table)` where each
441/// table entry is `(key_kind, key_ref, val_offset)`.
442///
443/// As with [`parse_header`], inline key names and values are not validated
444/// here — that is deferred to [`decode_with_dictionary`].
445pub fn parse_dict_header(data: &[u8]) -> Result<(usize, Vec<DictEntry>), DocBodyError> {
446    if data.len() < 7 {
447        return Err(DocBodyError::TruncatedData);
448    }
449    if &data[0..4] != MAGIC.as_slice() {
450        return Err(DocBodyError::BadMagic);
451    }
452    if data[4] != VERSION_DICT {
453        return Err(version_error(data[4]));
454    }
455
456    let n = u16::from_le_bytes([data[5], data[6]]) as usize;
457    let table_end = 7 + n * DICT_ENTRY_SIZE;
458    if data.len() < table_end {
459        return Err(DocBodyError::TruncatedData);
460    }
461
462    let mut table = Vec::with_capacity(n);
463    for i in 0..n {
464        let base = 7 + i * DICT_ENTRY_SIZE;
465        let kind = data[base];
466        let key_ref = u32::from_le_bytes([
467            data[base + 1],
468            data[base + 2],
469            data[base + 3],
470            data[base + 4],
471        ]);
472        let val_offset = u32::from_le_bytes([
473            data[base + 5],
474            data[base + 6],
475            data[base + 7],
476            data[base + 8],
477        ]);
478        table.push((kind, key_ref, val_offset));
479    }
480
481    Ok((n, table))
482}
483
484#[cfg(test)]
485mod tests {
486    use super::*;
487    use crate::types::Value;
488    use std::net::{IpAddr, Ipv4Addr};
489
490    fn round_trip(fields: &[(&str, Value)]) -> Vec<(String, Value)> {
491        let refs: Vec<(&str, &Value)> = fields.iter().map(|(k, v)| (*k, v)).collect();
492        let mut buf = Vec::new();
493        encode(&refs, &mut buf).expect("encode");
494        decode(&buf).expect("decode")
495    }
496
497    #[test]
498    fn empty_document_round_trips() {
499        let got = round_trip(&[]);
500        assert!(got.is_empty());
501    }
502
503    #[test]
504    fn single_field_round_trip() {
505        let fields = [("name", Value::text("Alice"))];
506        let got = round_trip(&fields);
507        assert_eq!(got.len(), 1);
508        assert_eq!(got[0].0, "name");
509        assert_eq!(got[0].1, Value::text("Alice"));
510    }
511
512    #[test]
513    fn multi_field_round_trip() {
514        let fields = [
515            ("id", Value::Integer(42)),
516            ("email", Value::Email("a@example.com".to_string())),
517            ("active", Value::Boolean(true)),
518            ("score", Value::Float(9.5)),
519        ];
520        let got = round_trip(&fields);
521        assert_eq!(got.len(), 4);
522        for (i, (k, v)) in fields.iter().enumerate() {
523            assert_eq!(got[i].0, *k);
524            assert_eq!(got[i].1, *v);
525        }
526    }
527
528    /// Every rich semantic type must survive the round-trip unchanged.
529    #[test]
530    fn rich_semantic_types_round_trip() {
531        let fields = [
532            ("email", Value::Email("user@example.com".to_string())),
533            ("ipv4", Value::Ipv4(0x7f000001)),
534            ("subnet", Value::Subnet(0x0a000000, 0xff000000)),
535            ("color", Value::Color([0xDE, 0xAD, 0xBE])),
536            ("phone", Value::Phone(5511999000000)),
537            ("semver", Value::Semver(1_002_003)),
538            ("uuid", Value::Uuid([0xAB; 16])),
539            (
540                "money",
541                Value::Money {
542                    asset_code: "USD".to_string(),
543                    minor_units: 9999,
544                    scale: 2,
545                },
546            ),
547            ("geo", Value::GeoPoint(-23_550_520, -46_633_308)),
548            (
549                "ip_mixed",
550                Value::IpAddr(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))),
551            ),
552            ("url", Value::Url("https://reddb.io".to_string())),
553            ("color_alpha", Value::ColorAlpha([1, 2, 3, 255])),
554            ("lang", Value::Lang2(*b"en")),
555            ("country", Value::Country3(*b"USA")),
556        ];
557        let got = round_trip(&fields);
558        assert_eq!(got.len(), fields.len());
559        for (i, (k, v)) in fields.iter().enumerate() {
560            assert_eq!(got[i].0, *k, "key mismatch at {i}");
561            assert_eq!(got[i].1, *v, "value mismatch for field {k}");
562        }
563    }
564
565    /// A single field can be read by name without decoding the rest.
566    #[test]
567    fn read_field_by_name_skips_other_fields() {
568        let fields = [
569            ("a", Value::Integer(1)),
570            ("b", Value::text("hello")),
571            ("c", Value::Boolean(false)),
572        ];
573        let refs: Vec<(&str, &Value)> = fields.iter().map(|(k, v)| (*k, v)).collect();
574        let mut buf = Vec::new();
575        encode(&refs, &mut buf).expect("encode");
576
577        let v = read_field_by_name(&buf, "b")
578            .expect("lookup")
579            .expect("found");
580        assert_eq!(v, Value::text("hello"));
581
582        let missing = read_field_by_name(&buf, "z").expect("lookup");
583        assert!(missing.is_none());
584    }
585
586    /// `field_names` lists the keys in encode order without decoding values.
587    #[test]
588    fn field_names_lists_keys_in_encode_order() {
589        let fields = [
590            ("name", Value::text("Alice")),
591            ("score", Value::Integer(30)),
592            ("active", Value::Boolean(true)),
593        ];
594        let refs: Vec<(&str, &Value)> = fields.iter().map(|(k, v)| (*k, v)).collect();
595        let mut buf = Vec::new();
596        encode(&refs, &mut buf).expect("encode");
597
598        let names = field_names(&buf).expect("field_names");
599        assert_eq!(names, vec!["name", "score", "active"]);
600
601        assert!(field_names(&[]).is_err());
602    }
603
604    /// decode_value_at_offset provides direct O(1) access given a known offset.
605    #[test]
606    fn decode_value_at_offset_matches_full_decode() {
607        let fields = [("x", Value::Integer(100)), ("y", Value::Float(1.5))];
608        let refs: Vec<(&str, &Value)> = fields.iter().map(|(k, v)| (*k, v)).collect();
609        let mut buf = Vec::new();
610        encode(&refs, &mut buf).expect("encode");
611
612        let (n, table) = parse_header(&buf).expect("parse_header");
613        assert_eq!(n, 2);
614
615        for (i, (k, v)) in fields.iter().enumerate() {
616            let (_key_len, val_offset) = table[i];
617            let got = decode_value_at_offset(&buf, val_offset).expect("decode_at_offset");
618            assert_eq!(got, *v, "field {k} mismatch");
619        }
620    }
621
622    #[test]
623    fn rejects_bad_magic() {
624        let mut buf = vec![b'X', b'X', b'X', b'X', VERSION, 0, 0];
625        assert_eq!(decode(&buf), Err(DocBodyError::BadMagic));
626        buf[0..4].copy_from_slice(MAGIC);
627        buf[4] = 0x99;
628        assert_eq!(decode(&buf), Err(DocBodyError::UnsupportedVersion(0x99)));
629    }
630
631    #[test]
632    fn superseded_plain_version_is_rejected_didactically() {
633        let fields = [("id", Value::Integer(1))];
634        let refs: Vec<(&str, &Value)> = fields.iter().map(|(k, v)| (*k, v)).collect();
635        let mut buf = Vec::new();
636        encode(&refs, &mut buf).expect("encode");
637        assert_eq!(buf[4], VERSION);
638
639        buf[4] = SUPERSEDED_VERSION;
640        let err = decode(&buf).expect_err("pre-bump body must be refused");
641        assert_eq!(err, DocBodyError::SupersededVersion(SUPERSEDED_VERSION));
642
643        let msg = err.to_string();
644        assert!(msg.contains("superseded"), "{msg}");
645        assert!(msg.contains("#1768"), "{msg}");
646        assert!(msg.contains("re-insert"), "{msg}");
647    }
648
649    #[test]
650    fn superseded_dict_version_is_rejected_didactically() {
651        let fields = [("id", Value::Integer(1))];
652        let refs: Vec<(&str, &Value)> = fields.iter().map(|(k, v)| (*k, v)).collect();
653        let mut dict = KeyDictionary::new();
654        let mut buf = Vec::new();
655        encode_with_dictionary(&refs, &mut dict, |_| true, &mut buf).expect("encode");
656        assert_eq!(buf[4], VERSION_DICT);
657
658        buf[4] = SUPERSEDED_VERSION_DICT;
659        let err = decode_with_dictionary(&buf, &dict).expect_err("pre-bump body must be refused");
660        assert_eq!(
661            err,
662            DocBodyError::SupersededVersion(SUPERSEDED_VERSION_DICT)
663        );
664    }
665
666    #[test]
667    fn rejects_truncated_buffer() {
668        assert_eq!(decode(&[]), Err(DocBodyError::TruncatedData));
669        assert_eq!(
670            decode(&[b'R', b'D', b'O', b'C', 1, 0]),
671            Err(DocBodyError::TruncatedData)
672        );
673    }
674
675    #[test]
676    fn null_values_round_trip() {
677        let fields = [("nothing", Value::Null), ("something", Value::Integer(7))];
678        let got = round_trip(&fields);
679        assert_eq!(got[0].1, Value::Null);
680        assert_eq!(got[1].1, Value::Integer(7));
681    }
682
683    #[test]
684    fn array_value_round_trip() {
685        let fields = [(
686            "tags",
687            Value::Array(vec![Value::text("a"), Value::text("b"), Value::Integer(3)]),
688        )];
689        let got = round_trip(&fields);
690        assert_eq!(
691            got[0].1,
692            Value::Array(vec![Value::text("a"), Value::text("b"), Value::Integer(3)])
693        );
694    }
695
696    /// Verify the offset table points to the correct byte positions.
697    #[test]
698    fn offset_table_offsets_are_valid() {
699        let fields = [("k", Value::Boolean(true))];
700        let refs: Vec<(&str, &Value)> = fields.iter().map(|(k, v)| (*k, v)).collect();
701        let mut buf = Vec::new();
702        encode(&refs, &mut buf).expect("encode");
703
704        let (n, table) = parse_header(&buf).expect("header");
705        assert_eq!(n, 1);
706        let (_klen, val_offset) = table[0];
707        // The value bytes at val_offset must decode to Boolean(true)
708        let (v, _) = value_codec::decode(&buf[val_offset as usize..]).expect("decode at offset");
709        assert_eq!(v, Value::Boolean(true));
710    }
711
712    // ---- Dictionary-aware (v2) container ----------------------------------
713
714    /// Classify every key as common — exercises the homogeneous-collection path.
715    fn all_common(_: &str) -> bool {
716        true
717    }
718
719    /// Common field names are interned in the dictionary and the body stores
720    /// compact key-ids, not the key strings.
721    #[test]
722    fn common_keys_are_interned_as_key_ids() {
723        let fields = [
724            ("id", Value::Integer(1)),
725            ("name", Value::text("Alice")),
726            ("active", Value::Boolean(true)),
727        ];
728        let refs: Vec<(&str, &Value)> = fields.iter().map(|(k, v)| (*k, v)).collect();
729        let mut dict = KeyDictionary::new();
730        let mut buf = Vec::new();
731        encode_with_dictionary(&refs, &mut dict, all_common, &mut buf).expect("encode");
732
733        // All three keys entered the dictionary with dense ids.
734        assert_eq!(dict.len(), 3);
735        assert_eq!(dict.id_of("id"), Some(0));
736        assert_eq!(dict.id_of("name"), Some(1));
737        assert_eq!(dict.id_of("active"), Some(2));
738
739        // Every table entry is a dictionary reference, no inline keys.
740        let (n, table) = parse_dict_header(&buf).expect("header");
741        assert_eq!(n, 3);
742        for (kind, _key_ref, _off) in &table {
743            assert_eq!(*kind, KEY_KIND_DICT);
744        }
745        // The string "name" is not repeated in the body bytes — only its id is.
746        assert!(!buf.windows(4).any(|w| w == b"name"));
747    }
748
749    /// Encoding a second document with a new common key appends to the existing
750    /// dictionary transactionally (alongside the write), reusing prior ids.
751    #[test]
752    fn new_common_key_appends_to_dictionary() {
753        let mut dict = KeyDictionary::new();
754
755        let doc1 = [("id", Value::Integer(1)), ("name", Value::text("Alice"))];
756        let r1: Vec<(&str, &Value)> = doc1.iter().map(|(k, v)| (*k, v)).collect();
757        let mut b1 = Vec::new();
758        encode_with_dictionary(&r1, &mut dict, all_common, &mut b1).expect("encode 1");
759        assert_eq!(dict.len(), 2);
760
761        // Second document reuses id/name and introduces a brand-new common key.
762        let doc2 = [
763            ("id", Value::Integer(2)),
764            ("name", Value::text("Bob")),
765            ("email", Value::Email("bob@example.com".to_string())),
766        ];
767        let r2: Vec<(&str, &Value)> = doc2.iter().map(|(k, v)| (*k, v)).collect();
768        let mut b2 = Vec::new();
769        encode_with_dictionary(&r2, &mut dict, all_common, &mut b2).expect("encode 2");
770
771        // Only the new key appended; existing ids are stable.
772        assert_eq!(dict.len(), 3);
773        assert_eq!(dict.id_of("id"), Some(0));
774        assert_eq!(dict.id_of("name"), Some(1));
775        assert_eq!(dict.id_of("email"), Some(2));
776
777        // Both documents decode losslessly against the shared dictionary.
778        let g1 = decode_with_dictionary(&b1, &dict).expect("decode 1");
779        assert_eq!(g1[1], ("name".to_string(), Value::text("Alice")));
780        let g2 = decode_with_dictionary(&b2, &dict).expect("decode 2");
781        assert_eq!(
782            g2[2],
783            (
784                "email".to_string(),
785                Value::Email("bob@example.com".to_string())
786            )
787        );
788    }
789
790    /// A rare/unique key is stored inline and never enters the dictionary.
791    #[test]
792    fn rare_key_stays_inline_and_out_of_dictionary() {
793        // "id"/"name" are common; anything else is rare.
794        let common = |k: &str| k == "id" || k == "name";
795        let fields = [
796            ("id", Value::Integer(7)),
797            ("name", Value::text("Carol")),
798            ("x9f3_one_off_attr", Value::text("rare-value")),
799        ];
800        let refs: Vec<(&str, &Value)> = fields.iter().map(|(k, v)| (*k, v)).collect();
801        let mut dict = KeyDictionary::new();
802        let mut buf = Vec::new();
803        encode_with_dictionary(&refs, &mut dict, common, &mut buf).expect("encode");
804
805        // The rare key never entered the catalogue.
806        assert_eq!(dict.len(), 2);
807        assert_eq!(dict.id_of("x9f3_one_off_attr"), None);
808
809        // Its kind tag is inline and its name bytes live in the body.
810        let (_n, table) = parse_dict_header(&buf).expect("header");
811        assert_eq!(table[0].0, KEY_KIND_DICT);
812        assert_eq!(table[1].0, KEY_KIND_DICT);
813        assert_eq!(table[2].0, KEY_KIND_INLINE);
814        assert!(buf
815            .windows("x9f3_one_off_attr".len())
816            .any(|w| w == b"x9f3_one_off_attr"));
817
818        // And it round-trips losslessly.
819        let got = decode_with_dictionary(&buf, &dict).expect("decode");
820        assert_eq!(
821            got[2],
822            ("x9f3_one_off_attr".to_string(), Value::text("rare-value"))
823        );
824    }
825
826    /// Round-trip mixing dictionary keys and inline keys is lossless, including
827    /// rich semantic types and after persisting/reloading the dictionary.
828    #[test]
829    fn mixed_dictionary_and_inline_round_trip() {
830        let common = |k: &str| matches!(k, "id" | "email" | "active");
831        let fields = [
832            ("id", Value::Integer(42)),
833            ("email", Value::Email("a@example.com".to_string())),
834            ("active", Value::Boolean(true)),
835            ("one_off_color", Value::Color([0xDE, 0xAD, 0xBE])),
836            ("rare_geo", Value::GeoPoint(-23_550_520, -46_633_308)),
837        ];
838        let refs: Vec<(&str, &Value)> = fields.iter().map(|(k, v)| (*k, v)).collect();
839        let mut dict = KeyDictionary::new();
840        let mut buf = Vec::new();
841        encode_with_dictionary(&refs, &mut dict, common, &mut buf).expect("encode");
842
843        // Persist and reload the dictionary, then decode against the reloaded copy.
844        let mut dict_bytes = Vec::new();
845        dict.encode(&mut dict_bytes);
846        let reloaded = KeyDictionary::decode(&dict_bytes).expect("reload dict");
847
848        let got = decode_with_dictionary(&buf, &reloaded).expect("decode");
849        assert_eq!(got.len(), fields.len());
850        for (i, (k, v)) in fields.iter().enumerate() {
851            assert_eq!(got[i].0, *k, "key mismatch at {i}");
852            assert_eq!(got[i].1, *v, "value mismatch for field {k}");
853        }
854    }
855
856    #[test]
857    fn empty_dictionary_document_round_trips() {
858        let mut dict = KeyDictionary::new();
859        let mut buf = Vec::new();
860        encode_with_dictionary(&[], &mut dict, all_common, &mut buf).expect("encode");
861        let got = decode_with_dictionary(&buf, &dict).expect("decode");
862        assert!(got.is_empty());
863        assert!(dict.is_empty());
864    }
865
866    /// A v2 body decoded against a dictionary that lacks the id is rejected,
867    /// not silently mis-decoded.
868    #[test]
869    fn unknown_key_id_is_rejected() {
870        let fields = [("only_key", Value::Integer(1))];
871        let refs: Vec<(&str, &Value)> = fields.iter().map(|(k, v)| (*k, v)).collect();
872        let mut dict = KeyDictionary::new();
873        let mut buf = Vec::new();
874        encode_with_dictionary(&refs, &mut dict, all_common, &mut buf).expect("encode");
875
876        let empty = KeyDictionary::new();
877        assert_eq!(
878            decode_with_dictionary(&buf, &empty),
879            Err(DocBodyError::UnknownKeyId(0))
880        );
881    }
882
883    /// v1 and v2 containers are distinguished by their version byte; decoding a
884    /// v2 body with the v1 path (and vice versa) fails on the version check.
885    #[test]
886    fn v1_and_v2_versions_do_not_alias() {
887        let fields = [("k", Value::Integer(1))];
888        let refs: Vec<(&str, &Value)> = fields.iter().map(|(k, v)| (*k, v)).collect();
889
890        let mut v1 = Vec::new();
891        encode(&refs, &mut v1).expect("v1");
892        assert_eq!(v1[4], VERSION);
893
894        let mut dict = KeyDictionary::new();
895        let mut v2 = Vec::new();
896        encode_with_dictionary(&refs, &mut dict, all_common, &mut v2).expect("v2");
897        assert_eq!(v2[4], VERSION_DICT);
898
899        // Cross-decoding hits the version guard.
900        assert_eq!(
901            decode(&v2),
902            Err(DocBodyError::UnsupportedVersion(VERSION_DICT))
903        );
904        assert_eq!(
905            parse_dict_header(&v1),
906            Err(DocBodyError::UnsupportedVersion(VERSION))
907        );
908    }
909}