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