Skip to main content

reddb_types/
value_codec.rs

1//! On-disk codec registry for [`Value`].
2//!
3//! This module is the **single source of truth** for the byte layout
4//! of every [`Value`] variant. Adding a new variant means:
5//!
6//! 1. Add the variant to [`Value`].
7//! 2. Add the matching [`DataType`] tag (the on-disk type byte).
8//! 3. Add an arm to [`encode`] and [`decode`] in this file.
9//!
10//! That's it — no other file needs to learn the layout. The inherent
11//! [`Value::to_bytes`] / [`Value::from_bytes`] methods stay as the
12//! public API, but they only delegate here.
13//!
14//! ## Why a registry
15//!
16//! Before this module the encode / decode arms lived inside
17//! `types.rs`, mixed with display / coercion / hashing logic. A
18//! parallel `value_type_tag` helper in `storage::query` carried a
19//! third numbering scheme. The result was that every new variant
20//! required edits in three or more places and the tag spaces were
21//! free to drift.
22//!
23//! With the registry there is exactly one mapping
24//! `Value <-> on-disk bytes`. The wire protocol keeps its own,
25//! independent `VAL_*` tag space (see `wire/protocol.rs`); the two
26//! were never identical and any future unification is out of scope.
27//!
28//! ## On-disk format
29//!
30//! Bytes are unchanged versus the previous in-place implementation.
31//! The pinned-byte regression test [`tests::pinned_bytes`] guards
32//! the layout for the canonical variants (Null, Integer, Text, Bool,
33//! Blob).
34
35use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
36
37use super::types::{read_varint, write_varint, DataType, Value, ValueError};
38
39mod limit;
40
41/// Alias kept for callers that prefer the registry's own name. The
42/// on-disk tag space is owned by [`DataType`]; `ValueKind` reads
43/// better in registry contexts where the type name is the schema
44/// label rather than a parser concept.
45pub type ValueKind = DataType;
46
47/// On-disk tag byte for a value.
48///
49/// `Value::Null` uses tag `0` (the same byte the legacy code reserved
50/// as the explicit null marker before the [`DataType`] enum existed).
51/// Every other variant returns `data_type().to_byte()`.
52#[inline]
53pub fn type_tag(value: &Value) -> u8 {
54    match value {
55        Value::Null => 0,
56        other => other.data_type().to_byte(),
57    }
58}
59
60/// Reverse lookup for [`type_tag`]. Returns `None` for unknown bytes;
61/// `Some(DataType::Nullable)` for the dedicated null marker `0`.
62#[inline]
63pub fn type_for_tag(tag: u8) -> Option<ValueKind> {
64    if tag == 0 {
65        Some(DataType::Nullable)
66    } else {
67        DataType::from_byte(tag)
68    }
69}
70
71/// C3 TOAST: minimum byte length to attempt zstd compression. Values
72/// shorter than this are stored uncompressed — compression overhead
73/// (~50 ns + header bytes) outweighs savings for small values.
74pub(super) const TOAST_THRESHOLD: usize = 2048;
75
76/// zstd compression level for TOAST values. Level 3 is PG's default
77/// (balanced speed/ratio).
78pub(super) const TOAST_ZSTD_LEVEL: i32 = 3;
79
80/// Encode a value into `out`, appending its on-disk byte sequence.
81///
82/// The first byte is always [`type_tag`] of `value`; the remainder
83/// is the variant-specific payload.
84pub fn encode(value: &Value, out: &mut Vec<u8>) {
85    match value {
86        Value::Null => {
87            out.push(0); // Null marker
88        }
89        Value::Integer(v) => {
90            out.push(DataType::Integer.to_byte());
91            out.extend_from_slice(&v.to_le_bytes());
92        }
93        Value::UnsignedInteger(v) => {
94            out.push(DataType::UnsignedInteger.to_byte());
95            out.extend_from_slice(&v.to_le_bytes());
96        }
97        Value::Float(v) => {
98            out.push(DataType::Float.to_byte());
99            out.extend_from_slice(&v.to_le_bytes());
100        }
101        Value::Text(s) => {
102            let bytes = s.as_bytes();
103            // C3 TOAST: compress text values larger than the threshold.
104            // Stores with `TextZstd` type byte when compression wins;
105            // falls back to plain `Text` for small values or when zstd
106            // doesn't reduce the size (e.g. already-compressed content).
107            if bytes.len() > TOAST_THRESHOLD {
108                if let Ok(compressed) = zstd::bulk::compress(bytes, TOAST_ZSTD_LEVEL) {
109                    if compressed.len() < bytes.len() {
110                        out.push(DataType::TextZstd.to_byte());
111                        // original length first (needed to pre-allocate decompression buffer)
112                        write_varint(out, bytes.len() as u64);
113                        write_varint(out, compressed.len() as u64);
114                        out.extend_from_slice(&compressed);
115                        return;
116                    }
117                }
118            }
119            out.push(DataType::Text.to_byte());
120            write_varint(out, bytes.len() as u64);
121            out.extend_from_slice(bytes);
122        }
123        Value::Blob(data) => {
124            // C3 TOAST: same pattern as Text.
125            if data.len() > TOAST_THRESHOLD {
126                if let Ok(compressed) = zstd::bulk::compress(data, TOAST_ZSTD_LEVEL) {
127                    if compressed.len() < data.len() {
128                        out.push(DataType::BlobZstd.to_byte());
129                        write_varint(out, data.len() as u64);
130                        write_varint(out, compressed.len() as u64);
131                        out.extend_from_slice(&compressed);
132                        return;
133                    }
134                }
135            }
136            out.push(DataType::Blob.to_byte());
137            write_varint(out, data.len() as u64);
138            out.extend_from_slice(data);
139        }
140        Value::Boolean(v) => {
141            out.push(DataType::Boolean.to_byte());
142            out.push(if *v { 1 } else { 0 });
143        }
144        Value::Timestamp(v) => {
145            out.push(DataType::Timestamp.to_byte());
146            out.extend_from_slice(&v.to_le_bytes());
147        }
148        Value::Duration(v) => {
149            out.push(DataType::Duration.to_byte());
150            out.extend_from_slice(&v.to_le_bytes());
151        }
152        Value::IpAddr(addr) => {
153            out.push(DataType::IpAddr.to_byte());
154            match addr {
155                IpAddr::V4(v4) => {
156                    out.push(4); // IPv4 marker
157                    out.extend_from_slice(&v4.octets());
158                }
159                IpAddr::V6(v6) => {
160                    out.push(6); // IPv6 marker
161                    out.extend_from_slice(&v6.octets());
162                }
163            }
164        }
165        Value::MacAddr(mac) => {
166            out.push(DataType::MacAddr.to_byte());
167            out.extend_from_slice(mac);
168        }
169        Value::Vector(vec) => {
170            out.push(DataType::Vector.to_byte());
171            write_varint(out, vec.len() as u64);
172            for v in vec {
173                out.extend_from_slice(&v.to_le_bytes());
174            }
175        }
176        Value::Json(data) => {
177            out.push(DataType::Json.to_byte());
178            write_varint(out, data.len() as u64);
179            out.extend_from_slice(data);
180        }
181        Value::Uuid(uuid) => {
182            out.push(DataType::Uuid.to_byte());
183            out.extend_from_slice(uuid);
184        }
185        Value::NodeRef(node_id) => {
186            out.push(DataType::NodeRef.to_byte());
187            let bytes = node_id.as_bytes();
188            write_varint(out, bytes.len() as u64);
189            out.extend_from_slice(bytes);
190        }
191        Value::EdgeRef(edge_id) => {
192            out.push(DataType::EdgeRef.to_byte());
193            let bytes = edge_id.as_bytes();
194            write_varint(out, bytes.len() as u64);
195            out.extend_from_slice(bytes);
196        }
197        Value::VectorRef(collection, vector_id) => {
198            out.push(DataType::VectorRef.to_byte());
199            let coll_bytes = collection.as_bytes();
200            write_varint(out, coll_bytes.len() as u64);
201            out.extend_from_slice(coll_bytes);
202            out.extend_from_slice(&vector_id.to_le_bytes());
203        }
204        Value::RowRef(table, row_id) => {
205            out.push(DataType::RowRef.to_byte());
206            let table_bytes = table.as_bytes();
207            write_varint(out, table_bytes.len() as u64);
208            out.extend_from_slice(table_bytes);
209            out.extend_from_slice(&row_id.to_le_bytes());
210        }
211        Value::Color(rgb) => {
212            out.push(DataType::Color.to_byte());
213            out.extend_from_slice(rgb);
214        }
215        Value::Email(s) => {
216            out.push(DataType::Email.to_byte());
217            let bytes = s.as_bytes();
218            write_varint(out, bytes.len() as u64);
219            out.extend_from_slice(bytes);
220        }
221        Value::Url(s) => {
222            out.push(DataType::Url.to_byte());
223            let bytes = s.as_bytes();
224            write_varint(out, bytes.len() as u64);
225            out.extend_from_slice(bytes);
226        }
227        Value::Phone(n) => {
228            out.push(DataType::Phone.to_byte());
229            out.extend_from_slice(&n.to_le_bytes());
230        }
231        Value::Semver(packed) => {
232            out.push(DataType::Semver.to_byte());
233            out.extend_from_slice(&packed.to_le_bytes());
234        }
235        Value::Cidr(ip, prefix) => {
236            out.push(DataType::Cidr.to_byte());
237            out.extend_from_slice(&ip.to_le_bytes());
238            out.push(*prefix);
239        }
240        Value::Date(days) => {
241            out.push(DataType::Date.to_byte());
242            out.extend_from_slice(&days.to_le_bytes());
243        }
244        Value::Time(ms) => {
245            out.push(DataType::Time.to_byte());
246            out.extend_from_slice(&ms.to_le_bytes());
247        }
248        Value::Decimal(v) => {
249            out.push(DataType::Decimal.to_byte());
250            out.extend_from_slice(&v.to_le_bytes());
251        }
252        Value::DecimalText(s) => {
253            out.push(DataType::DecimalText.to_byte());
254            let bytes = s.as_bytes();
255            write_varint(out, bytes.len() as u64);
256            out.extend_from_slice(bytes);
257        }
258        Value::EnumValue(idx) => {
259            out.push(DataType::Enum.to_byte());
260            out.push(*idx);
261        }
262        Value::Array(elements) => {
263            out.push(DataType::Array.to_byte());
264            write_varint(out, elements.len() as u64);
265            for elem in elements {
266                encode(elem, out);
267            }
268        }
269        Value::TimestampMs(v) => {
270            out.push(DataType::TimestampMs.to_byte());
271            out.extend_from_slice(&v.to_le_bytes());
272        }
273        Value::Ipv4(v) => {
274            out.push(DataType::Ipv4.to_byte());
275            out.extend_from_slice(&v.to_le_bytes());
276        }
277        Value::Ipv6(bytes) => {
278            out.push(DataType::Ipv6.to_byte());
279            out.extend_from_slice(bytes);
280        }
281        Value::Subnet(ip, mask) => {
282            out.push(DataType::Subnet.to_byte());
283            out.extend_from_slice(&ip.to_le_bytes());
284            out.extend_from_slice(&mask.to_le_bytes());
285        }
286        Value::Port(v) => {
287            out.push(DataType::Port.to_byte());
288            out.extend_from_slice(&v.to_le_bytes());
289        }
290        Value::Latitude(v) => {
291            out.push(DataType::Latitude.to_byte());
292            out.extend_from_slice(&v.to_le_bytes());
293        }
294        Value::Longitude(v) => {
295            out.push(DataType::Longitude.to_byte());
296            out.extend_from_slice(&v.to_le_bytes());
297        }
298        Value::GeoPoint(lat, lon) => {
299            out.push(DataType::GeoPoint.to_byte());
300            out.extend_from_slice(&lat.to_le_bytes());
301            out.extend_from_slice(&lon.to_le_bytes());
302        }
303        Value::Country2(c) => {
304            out.push(DataType::Country2.to_byte());
305            out.extend_from_slice(c);
306        }
307        Value::Country3(c) => {
308            out.push(DataType::Country3.to_byte());
309            out.extend_from_slice(c);
310        }
311        Value::Lang2(c) => {
312            out.push(DataType::Lang2.to_byte());
313            out.extend_from_slice(c);
314        }
315        Value::Lang5(c) => {
316            out.push(DataType::Lang5.to_byte());
317            out.extend_from_slice(c);
318        }
319        Value::Currency(c) => {
320            out.push(DataType::Currency.to_byte());
321            out.extend_from_slice(c);
322        }
323        Value::AssetCode(code) => {
324            out.push(DataType::AssetCode.to_byte());
325            let bytes = code.as_bytes();
326            write_varint(out, bytes.len() as u64);
327            out.extend_from_slice(bytes);
328        }
329        Value::Money {
330            asset_code,
331            minor_units,
332            scale,
333        } => {
334            out.push(DataType::Money.to_byte());
335            let bytes = asset_code.as_bytes();
336            write_varint(out, bytes.len() as u64);
337            out.extend_from_slice(bytes);
338            out.push(*scale);
339            out.extend_from_slice(&minor_units.to_le_bytes());
340        }
341        Value::ColorAlpha(rgba) => {
342            out.push(DataType::ColorAlpha.to_byte());
343            out.extend_from_slice(rgba);
344        }
345        Value::BigInt(v) => {
346            out.push(DataType::BigInt.to_byte());
347            out.extend_from_slice(&v.to_le_bytes());
348        }
349        Value::KeyRef(col, key) => {
350            out.push(DataType::KeyRef.to_byte());
351            let col_bytes = col.as_bytes();
352            write_varint(out, col_bytes.len() as u64);
353            out.extend_from_slice(col_bytes);
354            let key_bytes = key.as_bytes();
355            write_varint(out, key_bytes.len() as u64);
356            out.extend_from_slice(key_bytes);
357        }
358        Value::DocRef(col, id) => {
359            out.push(DataType::DocRef.to_byte());
360            let col_bytes = col.as_bytes();
361            write_varint(out, col_bytes.len() as u64);
362            out.extend_from_slice(col_bytes);
363            out.extend_from_slice(&id.to_le_bytes());
364        }
365        Value::TableRef(name) => {
366            out.push(DataType::TableRef.to_byte());
367            let name_bytes = name.as_bytes();
368            write_varint(out, name_bytes.len() as u64);
369            out.extend_from_slice(name_bytes);
370        }
371        Value::PageRef(page_id) => {
372            out.push(DataType::PageRef.to_byte());
373            out.extend_from_slice(&page_id.to_le_bytes());
374        }
375        Value::Secret(bytes) => {
376            out.push(DataType::Secret.to_byte());
377            write_varint(out, bytes.len() as u64);
378            out.extend_from_slice(bytes);
379        }
380        Value::Password(hash) => {
381            out.push(DataType::Password.to_byte());
382            let bytes = hash.as_bytes();
383            write_varint(out, bytes.len() as u64);
384            out.extend_from_slice(bytes);
385        }
386    }
387}
388
389/// Decode a single value from `data`, returning the value and the
390/// number of bytes consumed.
391pub fn decode_with_limit(data: &[u8], limit_bytes: usize) -> Result<(Value, usize), ValueError> {
392    let mut remaining_bytes = limit_bytes;
393    decode_with_budget(data, &mut remaining_bytes)
394}
395
396/// Consume one conservative allocation allowance across multiple value frames.
397pub fn decode_with_budget(
398    data: &[u8],
399    remaining_bytes: &mut usize,
400) -> Result<(Value, usize), ValueError> {
401    let (expected, allocation_bytes) = limit::preflight(data, *remaining_bytes)?;
402    let (value, consumed) = decode(&data[..expected])?;
403    assert_eq!(
404        consumed, expected,
405        "invariant: value preflight and typed decoder agree"
406    );
407    *remaining_bytes -= allocation_bytes;
408    Ok((value, consumed))
409}
410
411/// Decode a canonical value without a caller-supplied allocation allowance.
412pub fn decode(data: &[u8]) -> Result<(Value, usize), ValueError> {
413    if data.is_empty() {
414        return Err(ValueError::EmptyData);
415    }
416
417    let type_byte = data[0];
418    let mut offset = 1;
419
420    // Null marker
421    if type_byte == 0 {
422        return Ok((Value::Null, 1));
423    }
424
425    let data_type = DataType::from_byte(type_byte).ok_or(ValueError::InvalidType(type_byte))?;
426
427    let value = match data_type {
428        DataType::Integer => {
429            if data.len() < offset + 8 {
430                return Err(ValueError::TruncatedData);
431            }
432            let v = i64::from_le_bytes(data[offset..offset + 8].try_into().unwrap());
433            offset += 8;
434            Value::Integer(v)
435        }
436        DataType::UnsignedInteger => {
437            if data.len() < offset + 8 {
438                return Err(ValueError::TruncatedData);
439            }
440            let v = u64::from_le_bytes(data[offset..offset + 8].try_into().unwrap());
441            offset += 8;
442            Value::UnsignedInteger(v)
443        }
444        DataType::Float => {
445            if data.len() < offset + 8 {
446                return Err(ValueError::TruncatedData);
447            }
448            let v = f64::from_le_bytes(data[offset..offset + 8].try_into().unwrap());
449            offset += 8;
450            Value::Float(v)
451        }
452        DataType::Text => {
453            let (len, varint_size) = read_varint(&data[offset..])?;
454            offset += varint_size;
455            if data.len() < offset + len as usize {
456                return Err(ValueError::TruncatedData);
457            }
458            let s = String::from_utf8(data[offset..offset + len as usize].to_vec())
459                .map_err(|_| ValueError::InvalidUtf8)?;
460            offset += len as usize;
461            Value::text(s)
462        }
463        DataType::Blob => {
464            let (len, varint_size) = read_varint(&data[offset..])?;
465            offset += varint_size;
466            if data.len() < offset + len as usize {
467                return Err(ValueError::TruncatedData);
468            }
469            let blob = data[offset..offset + len as usize].to_vec();
470            offset += len as usize;
471            Value::Blob(blob)
472        }
473        DataType::Boolean => {
474            if data.len() < offset + 1 {
475                return Err(ValueError::TruncatedData);
476            }
477            let v = data[offset] != 0;
478            offset += 1;
479            Value::Boolean(v)
480        }
481        DataType::Timestamp => {
482            if data.len() < offset + 8 {
483                return Err(ValueError::TruncatedData);
484            }
485            let v = i64::from_le_bytes(data[offset..offset + 8].try_into().unwrap());
486            offset += 8;
487            Value::Timestamp(v)
488        }
489        DataType::Duration => {
490            if data.len() < offset + 8 {
491                return Err(ValueError::TruncatedData);
492            }
493            let v = i64::from_le_bytes(data[offset..offset + 8].try_into().unwrap());
494            offset += 8;
495            Value::Duration(v)
496        }
497        DataType::IpAddr => {
498            if data.len() < offset + 1 {
499                return Err(ValueError::TruncatedData);
500            }
501            let version = data[offset];
502            offset += 1;
503            match version {
504                4 => {
505                    if data.len() < offset + 4 {
506                        return Err(ValueError::TruncatedData);
507                    }
508                    let octets: [u8; 4] = data[offset..offset + 4].try_into().unwrap();
509                    offset += 4;
510                    Value::IpAddr(IpAddr::V4(Ipv4Addr::from(octets)))
511                }
512                6 => {
513                    if data.len() < offset + 16 {
514                        return Err(ValueError::TruncatedData);
515                    }
516                    let octets: [u8; 16] = data[offset..offset + 16].try_into().unwrap();
517                    offset += 16;
518                    Value::IpAddr(IpAddr::V6(Ipv6Addr::from(octets)))
519                }
520                _ => return Err(ValueError::InvalidIpVersion(version)),
521            }
522        }
523        DataType::MacAddr => {
524            if data.len() < offset + 6 {
525                return Err(ValueError::TruncatedData);
526            }
527            let mac: [u8; 6] = data[offset..offset + 6].try_into().unwrap();
528            offset += 6;
529            Value::MacAddr(mac)
530        }
531        DataType::Vector => {
532            let (len, varint_size) = read_varint(&data[offset..])?;
533            offset += varint_size;
534            let float_count = len as usize;
535            if data.len() < offset + float_count * 4 {
536                return Err(ValueError::TruncatedData);
537            }
538            let mut vec = Vec::with_capacity(float_count);
539            for _ in 0..float_count {
540                let v = f32::from_le_bytes(data[offset..offset + 4].try_into().unwrap());
541                offset += 4;
542                vec.push(v);
543            }
544            Value::Vector(vec)
545        }
546        DataType::Json => {
547            let (len, varint_size) = read_varint(&data[offset..])?;
548            offset += varint_size;
549            if data.len() < offset + len as usize {
550                return Err(ValueError::TruncatedData);
551            }
552            let json = data[offset..offset + len as usize].to_vec();
553            offset += len as usize;
554            Value::Json(json)
555        }
556        DataType::Uuid => {
557            if data.len() < offset + 16 {
558                return Err(ValueError::TruncatedData);
559            }
560            let uuid: [u8; 16] = data[offset..offset + 16].try_into().unwrap();
561            offset += 16;
562            Value::Uuid(uuid)
563        }
564        DataType::NodeRef => {
565            let (len, len_bytes) = read_varint(&data[offset..])?;
566            offset += len_bytes;
567            if data.len() < offset + len as usize {
568                return Err(ValueError::TruncatedData);
569            }
570            let node_id = String::from_utf8_lossy(&data[offset..offset + len as usize]).to_string();
571            offset += len as usize;
572            Value::NodeRef(node_id)
573        }
574        DataType::EdgeRef => {
575            let (len, len_bytes) = read_varint(&data[offset..])?;
576            offset += len_bytes;
577            if data.len() < offset + len as usize {
578                return Err(ValueError::TruncatedData);
579            }
580            let edge_id = String::from_utf8_lossy(&data[offset..offset + len as usize]).to_string();
581            offset += len as usize;
582            Value::EdgeRef(edge_id)
583        }
584        DataType::VectorRef => {
585            let (len, len_bytes) = read_varint(&data[offset..])?;
586            offset += len_bytes;
587            if data.len() < offset + len as usize + 8 {
588                return Err(ValueError::TruncatedData);
589            }
590            let collection =
591                String::from_utf8_lossy(&data[offset..offset + len as usize]).to_string();
592            offset += len as usize;
593            let vector_id = u64::from_le_bytes(data[offset..offset + 8].try_into().unwrap());
594            offset += 8;
595            Value::VectorRef(collection, vector_id)
596        }
597        DataType::RowRef => {
598            let (len, len_bytes) = read_varint(&data[offset..])?;
599            offset += len_bytes;
600            if data.len() < offset + len as usize + 8 {
601                return Err(ValueError::TruncatedData);
602            }
603            let table = String::from_utf8_lossy(&data[offset..offset + len as usize]).to_string();
604            offset += len as usize;
605            let row_id = u64::from_le_bytes(data[offset..offset + 8].try_into().unwrap());
606            offset += 8;
607            Value::RowRef(table, row_id)
608        }
609        DataType::Color => {
610            if data.len() < offset + 3 {
611                return Err(ValueError::TruncatedData);
612            }
613            let rgb: [u8; 3] = data[offset..offset + 3].try_into().unwrap();
614            offset += 3;
615            Value::Color(rgb)
616        }
617        DataType::Email => {
618            let (len, varint_size) = read_varint(&data[offset..])?;
619            offset += varint_size;
620            if data.len() < offset + len as usize {
621                return Err(ValueError::TruncatedData);
622            }
623            let s = String::from_utf8(data[offset..offset + len as usize].to_vec())
624                .map_err(|_| ValueError::InvalidUtf8)?;
625            offset += len as usize;
626            Value::Email(s)
627        }
628        DataType::Url => {
629            let (len, varint_size) = read_varint(&data[offset..])?;
630            offset += varint_size;
631            if data.len() < offset + len as usize {
632                return Err(ValueError::TruncatedData);
633            }
634            let s = String::from_utf8(data[offset..offset + len as usize].to_vec())
635                .map_err(|_| ValueError::InvalidUtf8)?;
636            offset += len as usize;
637            Value::Url(s)
638        }
639        DataType::Phone => {
640            if data.len() < offset + 8 {
641                return Err(ValueError::TruncatedData);
642            }
643            let v = u64::from_le_bytes(data[offset..offset + 8].try_into().unwrap());
644            offset += 8;
645            Value::Phone(v)
646        }
647        DataType::Semver => {
648            if data.len() < offset + 4 {
649                return Err(ValueError::TruncatedData);
650            }
651            let v = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap());
652            offset += 4;
653            Value::Semver(v)
654        }
655        DataType::Cidr => {
656            if data.len() < offset + 5 {
657                return Err(ValueError::TruncatedData);
658            }
659            let ip = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap());
660            offset += 4;
661            let prefix = data[offset];
662            offset += 1;
663            Value::Cidr(ip, prefix)
664        }
665        DataType::Date => {
666            if data.len() < offset + 4 {
667                return Err(ValueError::TruncatedData);
668            }
669            let v = i32::from_le_bytes(data[offset..offset + 4].try_into().unwrap());
670            offset += 4;
671            Value::Date(v)
672        }
673        DataType::Time => {
674            if data.len() < offset + 4 {
675                return Err(ValueError::TruncatedData);
676            }
677            let v = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap());
678            offset += 4;
679            Value::Time(v)
680        }
681        DataType::Decimal => {
682            if data.len() < offset + 8 {
683                return Err(ValueError::TruncatedData);
684            }
685            let v = i64::from_le_bytes(data[offset..offset + 8].try_into().unwrap());
686            offset += 8;
687            Value::Decimal(v)
688        }
689        DataType::DecimalText => {
690            let (len, varint_size) = read_varint(&data[offset..])?;
691            offset += varint_size;
692            if data.len() < offset + len as usize {
693                return Err(ValueError::TruncatedData);
694            }
695            let value = String::from_utf8(data[offset..offset + len as usize].to_vec())
696                .map_err(|_| ValueError::InvalidUtf8)?;
697            offset += len as usize;
698            Value::DecimalText(value)
699        }
700        DataType::Enum => {
701            if data.len() < offset + 1 {
702                return Err(ValueError::TruncatedData);
703            }
704            let idx = data[offset];
705            offset += 1;
706            Value::EnumValue(idx)
707        }
708        DataType::Array => {
709            let (len, varint_size) = read_varint(&data[offset..])?;
710            offset += varint_size;
711            let count = len as usize;
712            let mut elements = Vec::with_capacity(count);
713            for _ in 0..count {
714                let (elem, elem_size) = decode(&data[offset..])?;
715                offset += elem_size;
716                elements.push(elem);
717            }
718            Value::Array(elements)
719        }
720        DataType::TimestampMs => {
721            if data.len() < offset + 8 {
722                return Err(ValueError::TruncatedData);
723            }
724            let v = i64::from_le_bytes(data[offset..offset + 8].try_into().unwrap());
725            offset += 8;
726            Value::TimestampMs(v)
727        }
728        DataType::Ipv4 => {
729            if data.len() < offset + 4 {
730                return Err(ValueError::TruncatedData);
731            }
732            let v = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap());
733            offset += 4;
734            Value::Ipv4(v)
735        }
736        DataType::Ipv6 => {
737            if data.len() < offset + 16 {
738                return Err(ValueError::TruncatedData);
739            }
740            let bytes: [u8; 16] = data[offset..offset + 16].try_into().unwrap();
741            offset += 16;
742            Value::Ipv6(bytes)
743        }
744        DataType::Subnet => {
745            if data.len() < offset + 8 {
746                return Err(ValueError::TruncatedData);
747            }
748            let ip = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap());
749            offset += 4;
750            let mask = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap());
751            offset += 4;
752            Value::Subnet(ip, mask)
753        }
754        DataType::Port => {
755            if data.len() < offset + 2 {
756                return Err(ValueError::TruncatedData);
757            }
758            let v = u16::from_le_bytes(data[offset..offset + 2].try_into().unwrap());
759            offset += 2;
760            Value::Port(v)
761        }
762        DataType::Latitude => {
763            if data.len() < offset + 4 {
764                return Err(ValueError::TruncatedData);
765            }
766            let v = i32::from_le_bytes(data[offset..offset + 4].try_into().unwrap());
767            offset += 4;
768            Value::Latitude(v)
769        }
770        DataType::Longitude => {
771            if data.len() < offset + 4 {
772                return Err(ValueError::TruncatedData);
773            }
774            let v = i32::from_le_bytes(data[offset..offset + 4].try_into().unwrap());
775            offset += 4;
776            Value::Longitude(v)
777        }
778        DataType::GeoPoint => {
779            if data.len() < offset + 8 {
780                return Err(ValueError::TruncatedData);
781            }
782            let lat = i32::from_le_bytes(data[offset..offset + 4].try_into().unwrap());
783            offset += 4;
784            let lon = i32::from_le_bytes(data[offset..offset + 4].try_into().unwrap());
785            offset += 4;
786            Value::GeoPoint(lat, lon)
787        }
788        DataType::Country2 => {
789            if data.len() < offset + 2 {
790                return Err(ValueError::TruncatedData);
791            }
792            let c: [u8; 2] = data[offset..offset + 2].try_into().unwrap();
793            offset += 2;
794            Value::Country2(c)
795        }
796        DataType::Country3 => {
797            if data.len() < offset + 3 {
798                return Err(ValueError::TruncatedData);
799            }
800            let c: [u8; 3] = data[offset..offset + 3].try_into().unwrap();
801            offset += 3;
802            Value::Country3(c)
803        }
804        DataType::Lang2 => {
805            if data.len() < offset + 2 {
806                return Err(ValueError::TruncatedData);
807            }
808            let c: [u8; 2] = data[offset..offset + 2].try_into().unwrap();
809            offset += 2;
810            Value::Lang2(c)
811        }
812        DataType::Lang5 => {
813            if data.len() < offset + 5 {
814                return Err(ValueError::TruncatedData);
815            }
816            let c: [u8; 5] = data[offset..offset + 5].try_into().unwrap();
817            offset += 5;
818            Value::Lang5(c)
819        }
820        DataType::Currency => {
821            if data.len() < offset + 3 {
822                return Err(ValueError::TruncatedData);
823            }
824            let c: [u8; 3] = data[offset..offset + 3].try_into().unwrap();
825            offset += 3;
826            Value::Currency(c)
827        }
828        DataType::AssetCode => {
829            let (len, len_bytes) = read_varint(&data[offset..])?;
830            offset += len_bytes;
831            if data.len() < offset + len as usize {
832                return Err(ValueError::TruncatedData);
833            }
834            let code = String::from_utf8(data[offset..offset + len as usize].to_vec())
835                .map_err(|_| ValueError::InvalidUtf8)?;
836            offset += len as usize;
837            Value::AssetCode(code)
838        }
839        DataType::Money => {
840            let (len, len_bytes) = read_varint(&data[offset..])?;
841            offset += len_bytes;
842            if data.len() < offset + len as usize + 1 + 8 {
843                return Err(ValueError::TruncatedData);
844            }
845            let asset_code = String::from_utf8(data[offset..offset + len as usize].to_vec())
846                .map_err(|_| ValueError::InvalidUtf8)?;
847            offset += len as usize;
848            let scale = data[offset];
849            offset += 1;
850            let minor_units = i64::from_le_bytes(data[offset..offset + 8].try_into().unwrap());
851            offset += 8;
852            Value::Money {
853                asset_code,
854                minor_units,
855                scale,
856            }
857        }
858        DataType::ColorAlpha => {
859            if data.len() < offset + 4 {
860                return Err(ValueError::TruncatedData);
861            }
862            let rgba: [u8; 4] = data[offset..offset + 4].try_into().unwrap();
863            offset += 4;
864            Value::ColorAlpha(rgba)
865        }
866        DataType::BigInt => {
867            if data.len() < offset + 8 {
868                return Err(ValueError::TruncatedData);
869            }
870            let v = i64::from_le_bytes(data[offset..offset + 8].try_into().unwrap());
871            offset += 8;
872            Value::BigInt(v)
873        }
874        DataType::KeyRef => {
875            let (col_len, col_varint) = read_varint(&data[offset..])?;
876            offset += col_varint;
877            if data.len() < offset + col_len as usize {
878                return Err(ValueError::TruncatedData);
879            }
880            let col = String::from_utf8(data[offset..offset + col_len as usize].to_vec())
881                .map_err(|_| ValueError::InvalidUtf8)?;
882            offset += col_len as usize;
883            let (key_len, key_varint) = read_varint(&data[offset..])?;
884            offset += key_varint;
885            if data.len() < offset + key_len as usize {
886                return Err(ValueError::TruncatedData);
887            }
888            let key = String::from_utf8(data[offset..offset + key_len as usize].to_vec())
889                .map_err(|_| ValueError::InvalidUtf8)?;
890            offset += key_len as usize;
891            Value::KeyRef(col, key)
892        }
893        DataType::DocRef => {
894            let (col_len, col_varint) = read_varint(&data[offset..])?;
895            offset += col_varint;
896            if data.len() < offset + col_len as usize + 8 {
897                return Err(ValueError::TruncatedData);
898            }
899            let col = String::from_utf8(data[offset..offset + col_len as usize].to_vec())
900                .map_err(|_| ValueError::InvalidUtf8)?;
901            offset += col_len as usize;
902            let id = u64::from_le_bytes(data[offset..offset + 8].try_into().unwrap());
903            offset += 8;
904            Value::DocRef(col, id)
905        }
906        DataType::TableRef => {
907            let (len, varint_size) = read_varint(&data[offset..])?;
908            offset += varint_size;
909            if data.len() < offset + len as usize {
910                return Err(ValueError::TruncatedData);
911            }
912            let name = String::from_utf8(data[offset..offset + len as usize].to_vec())
913                .map_err(|_| ValueError::InvalidUtf8)?;
914            offset += len as usize;
915            Value::TableRef(name)
916        }
917        DataType::PageRef => {
918            if data.len() < offset + 4 {
919                return Err(ValueError::TruncatedData);
920            }
921            let page_id = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap());
922            offset += 4;
923            Value::PageRef(page_id)
924        }
925        DataType::Secret => {
926            let (len, varint_size) = read_varint(&data[offset..])?;
927            offset += varint_size;
928            if data.len() < offset + len as usize {
929                return Err(ValueError::TruncatedData);
930            }
931            let bytes = data[offset..offset + len as usize].to_vec();
932            offset += len as usize;
933            Value::Secret(bytes)
934        }
935        DataType::Password => {
936            let (len, varint_size) = read_varint(&data[offset..])?;
937            offset += varint_size;
938            if data.len() < offset + len as usize {
939                return Err(ValueError::TruncatedData);
940            }
941            let hash = String::from_utf8(data[offset..offset + len as usize].to_vec())
942                .map_err(|_| ValueError::InvalidUtf8)?;
943            offset += len as usize;
944            Value::Password(hash)
945        }
946        DataType::Nullable => {
947            // Nullable without inner type means null
948            Value::Null
949        }
950        DataType::Unknown => {
951            // Polymorphic placeholder — never stored on disk.
952            // Reaching here means corrupted data or a bug; treat
953            // as null to stay forward-compatible.
954            Value::Null
955        }
956        // C3 TOAST: zstd-compressed Text — transparent decompression.
957        // Wire: encode writes TextZstd when text > TOAST_THRESHOLD and
958        // compression saves space; decode always materialises as Value::Text.
959        DataType::TextZstd => {
960            let (orig_len, vs1) = read_varint(&data[offset..])?;
961            offset += vs1;
962            let (comp_len, vs2) = read_varint(&data[offset..])?;
963            offset += vs2;
964            if data.len() < offset + comp_len as usize {
965                return Err(ValueError::TruncatedData);
966            }
967            let compressed = &data[offset..offset + comp_len as usize];
968            let mut decompressed = vec![0u8; orig_len as usize];
969            let written = zstd::bulk::decompress_to_buffer(compressed, &mut decompressed)
970                .map_err(|_| ValueError::InvalidUtf8)?;
971            if written != decompressed.len() {
972                return Err(ValueError::TruncatedData);
973            }
974            offset += comp_len as usize;
975            let s = String::from_utf8(decompressed).map_err(|_| ValueError::InvalidUtf8)?;
976            Value::text(s)
977        }
978        // C3 TOAST: zstd-compressed Blob — same pattern as TextZstd.
979        DataType::BlobZstd => {
980            let (orig_len, vs1) = read_varint(&data[offset..])?;
981            offset += vs1;
982            let (comp_len, vs2) = read_varint(&data[offset..])?;
983            offset += vs2;
984            if data.len() < offset + comp_len as usize {
985                return Err(ValueError::TruncatedData);
986            }
987            let compressed = &data[offset..offset + comp_len as usize];
988            let mut decompressed = vec![0u8; orig_len as usize];
989            let written = zstd::bulk::decompress_to_buffer(compressed, &mut decompressed)
990                .map_err(|_| ValueError::InvalidUtf8)?;
991            if written != decompressed.len() {
992                return Err(ValueError::TruncatedData);
993            }
994            offset += comp_len as usize;
995            Value::Blob(decompressed)
996        }
997    };
998
999    Ok((value, offset))
1000}
1001
1002#[cfg(test)]
1003mod tests {
1004    use super::*;
1005    use proptest::prelude::*;
1006    use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
1007
1008    /// Pinned on-disk byte layout for the canonical [`Value`]
1009    /// variants. **If this test breaks, callers with persisted data
1010    /// will fail to read older files** — only update the expected
1011    /// bytes when you have intentionally migrated the format. A
1012    /// silent rewrite is a corruption bug.
1013    ///
1014    /// Variants pinned: Null, Integer, Text, Boolean, Blob — the
1015    /// minimum five required by the codec registry contract.
1016    #[test]
1017    fn pinned_bytes() {
1018        // Null: just the null marker (0x00).
1019        let mut buf = Vec::new();
1020        encode(&Value::Null, &mut buf);
1021        assert_eq!(buf, vec![0x00], "Value::Null layout drifted");
1022
1023        // Integer(-1): tag (Integer = 1) + i64 little-endian.
1024        let mut buf = Vec::new();
1025        encode(&Value::Integer(-1), &mut buf);
1026        assert_eq!(
1027            buf,
1028            vec![0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF],
1029            "Value::Integer layout drifted"
1030        );
1031
1032        // Text("hi"): tag (Text = 4) + varint(2) + UTF-8 bytes.
1033        let mut buf = Vec::new();
1034        encode(&Value::text("hi"), &mut buf);
1035        assert_eq!(
1036            buf,
1037            vec![0x04, 0x02, b'h', b'i'],
1038            "Value::Text layout drifted"
1039        );
1040
1041        // Boolean(true): tag (Boolean = 6) + 0x01.
1042        let mut buf = Vec::new();
1043        encode(&Value::Boolean(true), &mut buf);
1044        assert_eq!(buf, vec![0x06, 0x01], "Value::Boolean layout drifted");
1045
1046        // Blob([0x01, 0x02, 0x03]): tag (Blob = 5) + varint(3) + raw.
1047        let mut buf = Vec::new();
1048        encode(&Value::Blob(vec![0x01, 0x02, 0x03]), &mut buf);
1049        assert_eq!(
1050            buf,
1051            vec![0x05, 0x03, 0x01, 0x02, 0x03],
1052            "Value::Blob layout drifted"
1053        );
1054    }
1055
1056    /// Sanity check that the registry's [`type_tag`] lines up with
1057    /// [`DataType::to_byte`] for every storable variant — this is
1058    /// what guarantees the on-disk tag space stays single-source.
1059    #[test]
1060    fn type_tag_matches_data_type_byte() {
1061        let samples: &[Value] = &[
1062            Value::Null,
1063            Value::Integer(0),
1064            Value::UnsignedInteger(0),
1065            Value::Float(0.0),
1066            Value::text(""),
1067            Value::Blob(Vec::new()),
1068            Value::Boolean(false),
1069            Value::Timestamp(0),
1070            Value::Duration(0),
1071            Value::Uuid([0; 16]),
1072        ];
1073        for v in samples {
1074            let tag = type_tag(v);
1075            if matches!(v, Value::Null) {
1076                assert_eq!(tag, 0);
1077            } else {
1078                assert_eq!(tag, v.data_type().to_byte());
1079                let kind = type_for_tag(tag).expect("registered tag");
1080                assert_eq!(kind, v.data_type());
1081            }
1082        }
1083    }
1084
1085    /// Decoder must reject a type byte it does not recognise rather
1086    /// than silently returning a default. Guards against on-disk
1087    /// corruption being interpreted as a valid value.
1088    #[test]
1089    fn rejects_unknown_type_tag() {
1090        // 0xFF is outside the registered DataType range.
1091        let buf = [0xFFu8];
1092        let err = decode(&buf).expect_err("unknown tag must error");
1093        assert!(matches!(err, ValueError::InvalidType(0xFF)));
1094    }
1095
1096    /// A buffer truncated mid-payload must surface as
1097    /// `TruncatedData`, not panic on a slice index. Covers the
1098    /// fixed-width and length-prefixed code paths.
1099    #[test]
1100    fn rejects_truncated_buffer() {
1101        // Empty buffer.
1102        assert!(matches!(decode(&[]), Err(ValueError::EmptyData)));
1103
1104        // Integer tag (0x01) needs 8 payload bytes; supply 3.
1105        let mut buf = vec![DataType::Integer.to_byte()];
1106        buf.extend_from_slice(&[0x01, 0x02, 0x03]);
1107        assert!(matches!(decode(&buf), Err(ValueError::TruncatedData)));
1108
1109        // Text tag (0x04) with varint len=5 but only 2 payload bytes.
1110        let mut buf = vec![DataType::Text.to_byte()];
1111        write_varint(&mut buf, 5);
1112        buf.extend_from_slice(b"ab");
1113        assert!(matches!(decode(&buf), Err(ValueError::TruncatedData)));
1114    }
1115
1116    #[test]
1117    fn type_for_tag_handles_null_registered_and_unknown_tags() {
1118        assert_eq!(type_for_tag(0), Some(DataType::Nullable));
1119        assert_eq!(
1120            type_for_tag(DataType::Money.to_byte()),
1121            Some(DataType::Money)
1122        );
1123        assert_eq!(type_for_tag(0xFF), None);
1124    }
1125
1126    #[test]
1127    fn round_trip_every_value_variant_directly_through_registry() {
1128        let values = vec![
1129            Value::Null,
1130            Value::Integer(-1),
1131            Value::UnsignedInteger(2),
1132            Value::Float(3.5),
1133            Value::text("hello"),
1134            Value::Blob(vec![1, 2, 3]),
1135            Value::Boolean(true),
1136            Value::Timestamp(4),
1137            Value::Duration(5),
1138            Value::IpAddr(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
1139            Value::IpAddr(IpAddr::V6(Ipv6Addr::LOCALHOST)),
1140            Value::MacAddr([1, 2, 3, 4, 5, 6]),
1141            Value::Vector(vec![1.0, 2.0]),
1142            Value::Json(br#"{"ok":true}"#.to_vec()),
1143            Value::Uuid([7; 16]),
1144            Value::NodeRef("node".to_string()),
1145            Value::EdgeRef("edge".to_string()),
1146            Value::VectorRef("vectors".to_string(), 8),
1147            Value::RowRef("rows".to_string(), 9),
1148            Value::Color([0xAA, 0xBB, 0xCC]),
1149            Value::Email("a@example.com".to_string()),
1150            Value::Url("https://example.com".to_string()),
1151            Value::Phone(5511999),
1152            Value::Semver(1_002_003),
1153            Value::Cidr(10 << 24, 8),
1154            Value::Date(20_000),
1155            Value::Time(43_200_000),
1156            Value::Decimal(123_456),
1157            Value::DecimalText("18446744073709551616.00000000000000000001".to_string()),
1158            Value::EnumValue(3),
1159            Value::Array(vec![Value::Integer(1), Value::text("two")]),
1160            Value::TimestampMs(123_456),
1161            Value::Ipv4(0x7f000001),
1162            Value::Ipv6([1; 16]),
1163            Value::Subnet(10 << 24, 0xff000000),
1164            Value::Port(5432),
1165            Value::Latitude(-23_550_520),
1166            Value::Longitude(-46_633_308),
1167            Value::GeoPoint(-23_550_520, -46_633_308),
1168            Value::Country2(*b"BR"),
1169            Value::Country3(*b"BRA"),
1170            Value::Lang2(*b"pt"),
1171            Value::Lang5(*b"pt-BR"),
1172            Value::Currency(*b"USD"),
1173            Value::AssetCode("BTC".to_string()),
1174            Value::Money {
1175                asset_code: "USD".to_string(),
1176                minor_units: 1234,
1177                scale: 2,
1178            },
1179            Value::ColorAlpha([1, 2, 3, 4]),
1180            Value::BigInt(-10),
1181            Value::KeyRef("kv".to_string(), "key".to_string()),
1182            Value::DocRef("docs".to_string(), 42),
1183            Value::TableRef("users".to_string()),
1184            Value::PageRef(99),
1185            Value::Secret(vec![9, 8, 7]),
1186            Value::Password("$argon2id$v=19$hash".to_string()),
1187        ];
1188
1189        for original in values {
1190            let mut bytes = Vec::new();
1191            encode(&original, &mut bytes);
1192            let (decoded, consumed) = decode(&bytes).expect("decode");
1193            assert_eq!(consumed, bytes.len());
1194            assert_eq!(decoded, original, "{bytes:?}");
1195        }
1196    }
1197
1198    #[test]
1199    fn compressed_text_and_blob_decode_to_plain_values() {
1200        let text = Value::text("reddb ".repeat(700));
1201        let mut bytes = Vec::new();
1202        encode(&text, &mut bytes);
1203        assert_eq!(bytes[0], DataType::TextZstd.to_byte());
1204        let (decoded, consumed) = decode(&bytes).unwrap();
1205        assert_eq!(consumed, bytes.len());
1206        assert_eq!(decoded, text);
1207
1208        let blob = Value::Blob(vec![0xAB; TOAST_THRESHOLD + 512]);
1209        let mut bytes = Vec::new();
1210        encode(&blob, &mut bytes);
1211        assert_eq!(bytes[0], DataType::BlobZstd.to_byte());
1212        let (decoded, consumed) = decode(&bytes).unwrap();
1213        assert_eq!(consumed, bytes.len());
1214        assert_eq!(decoded, blob);
1215    }
1216
1217    #[test]
1218    fn decode_rejects_short_payload_for_registered_tags() {
1219        let truncated_tags = [
1220            DataType::Integer,
1221            DataType::UnsignedInteger,
1222            DataType::Float,
1223            DataType::Text,
1224            DataType::Blob,
1225            DataType::Boolean,
1226            DataType::Timestamp,
1227            DataType::Duration,
1228            DataType::IpAddr,
1229            DataType::MacAddr,
1230            DataType::Vector,
1231            DataType::Json,
1232            DataType::Uuid,
1233            DataType::NodeRef,
1234            DataType::EdgeRef,
1235            DataType::VectorRef,
1236            DataType::RowRef,
1237            DataType::Color,
1238            DataType::Email,
1239            DataType::Url,
1240            DataType::Phone,
1241            DataType::Semver,
1242            DataType::Cidr,
1243            DataType::Date,
1244            DataType::Time,
1245            DataType::Decimal,
1246            DataType::DecimalText,
1247            DataType::Enum,
1248            DataType::Array,
1249            DataType::TimestampMs,
1250            DataType::Ipv4,
1251            DataType::Ipv6,
1252            DataType::Subnet,
1253            DataType::Port,
1254            DataType::Latitude,
1255            DataType::Longitude,
1256            DataType::GeoPoint,
1257            DataType::Country2,
1258            DataType::Country3,
1259            DataType::Lang2,
1260            DataType::Lang5,
1261            DataType::Currency,
1262            DataType::AssetCode,
1263            DataType::Money,
1264            DataType::ColorAlpha,
1265            DataType::BigInt,
1266            DataType::KeyRef,
1267            DataType::DocRef,
1268            DataType::TableRef,
1269            DataType::PageRef,
1270            DataType::Secret,
1271            DataType::Password,
1272            DataType::TextZstd,
1273            DataType::BlobZstd,
1274        ];
1275
1276        for data_type in truncated_tags {
1277            let err = decode(&[data_type.to_byte()]).expect_err("short payload must error");
1278            assert_eq!(err, ValueError::TruncatedData, "{data_type:?}");
1279        }
1280
1281        assert_eq!(
1282            decode(&[DataType::Nullable.to_byte()]).unwrap().0,
1283            Value::Null
1284        );
1285    }
1286
1287    #[test]
1288    fn decode_rejects_invalid_embedded_tags_and_utf8_payloads() {
1289        assert_eq!(
1290            decode(&[DataType::IpAddr.to_byte(), 5]).expect_err("bad ip version"),
1291            ValueError::InvalidIpVersion(5)
1292        );
1293
1294        let invalid_text = [DataType::Text.to_byte(), 1, 0xff];
1295        assert_eq!(
1296            decode(&invalid_text).expect_err("invalid utf8"),
1297            ValueError::InvalidUtf8
1298        );
1299
1300        let invalid_email = [DataType::Email.to_byte(), 1, 0xff];
1301        assert_eq!(
1302            decode(&invalid_email).expect_err("invalid utf8"),
1303            ValueError::InvalidUtf8
1304        );
1305
1306        let invalid_url = [DataType::Url.to_byte(), 1, 0xff];
1307        assert_eq!(
1308            decode(&invalid_url).expect_err("invalid utf8"),
1309            ValueError::InvalidUtf8
1310        );
1311
1312        let invalid_asset = [DataType::AssetCode.to_byte(), 1, 0xff];
1313        assert_eq!(
1314            decode(&invalid_asset).expect_err("invalid utf8"),
1315            ValueError::InvalidUtf8
1316        );
1317    }
1318
1319    /// Round-trip: encode then decode must recover the original
1320    /// value, byte for byte.
1321    #[test]
1322    fn round_trip_canonical_variants() {
1323        let cases = vec![
1324            Value::Null,
1325            Value::Integer(-12345),
1326            Value::text("hello"),
1327            Value::Boolean(true),
1328            Value::Blob(vec![1, 2, 3, 4, 5]),
1329        ];
1330        for original in cases {
1331            let mut bytes = Vec::new();
1332            encode(&original, &mut bytes);
1333            let (recovered, consumed) = decode(&bytes).expect("decode");
1334            assert_eq!(consumed, bytes.len());
1335            assert_eq!(original, recovered);
1336        }
1337    }
1338
1339    fn value_variant_strategy() -> impl Strategy<Value = Value> {
1340        prop_oneof![
1341            Just(Value::Null),
1342            any::<bool>().prop_map(Value::Boolean),
1343            any::<i64>().prop_map(Value::Integer),
1344            prop_oneof![
1345                any::<f64>(),
1346                Just(f64::NAN),
1347                Just(f64::INFINITY),
1348                Just(f64::NEG_INFINITY),
1349                Just(f64::MIN_POSITIVE),
1350                Just(f64::from_bits(1)),
1351            ]
1352            .prop_map(Value::Float),
1353            "[1-9][0-9]{18,36}\\.[0-9]{1,24}".prop_map(Value::DecimalText),
1354            proptest::collection::vec(any::<u8>(), 0..4096).prop_map(Value::Blob),
1355            prop_oneof![
1356                Just(br#"{"a":null,"b":[1,true]}"#.to_vec()),
1357                Just(br#"[]"#.to_vec()),
1358                Just(br#"{"deep":{"nest":{"leaf":[false]}}}"#.to_vec()),
1359            ]
1360            .prop_map(Value::Json),
1361            any::<i64>().prop_map(Value::Timestamp),
1362            any::<[u8; 16]>().prop_map(Value::Uuid),
1363        ]
1364    }
1365
1366    proptest! {
1367        #![proptest_config(ProptestConfig::with_cases(256))]
1368
1369        #[test]
1370        fn prop_value_codec_round_trips_remaining_variants(original in value_variant_strategy()) {
1371            let mut bytes = Vec::new();
1372            encode(&original, &mut bytes);
1373            let (recovered, consumed) = decode(&bytes).expect("decode");
1374            prop_assert_eq!(consumed, bytes.len());
1375            prop_assert!(
1376                values_equivalent(&recovered, &original),
1377                "recovered={recovered:?} original={original:?}"
1378            );
1379        }
1380    }
1381
1382    fn values_equivalent(left: &Value, right: &Value) -> bool {
1383        match (left, right) {
1384            (Value::Float(a), Value::Float(b)) => a.to_bits() == b.to_bits(),
1385            _ => left == right,
1386        }
1387    }
1388}