Skip to main content

nodedb_query/msgpack_scan/reader/
value.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! `read_value`: decode one scalar into `nodedb_types::Value`.
4
5use nodedb_types::read_instant;
6
7use super::scalar::read_str;
8use super::tags::*;
9
10/// Read a scalar msgpack value at `offset` into `nodedb_types::Value`.
11///
12/// Handles null, bool, integers, floats, strings, and instant ext
13/// (`fixext8` type 1 / 2 → `Value::DateTime` / `Value::NaiveDateTime`).
14/// For complex types (array, map, bin, other ext), returns `None` — caller
15/// should use `json_from_msgpack` for those.
16pub fn read_value(buf: &[u8], offset: usize) -> Option<nodedb_types::Value> {
17    let tag = get(buf, offset)?;
18    match tag {
19        NIL => Some(nodedb_types::Value::Null),
20        TRUE => Some(nodedb_types::Value::Bool(true)),
21        FALSE => Some(nodedb_types::Value::Bool(false)),
22        // Integers
23        0x00..=0x7f => Some(nodedb_types::Value::Integer(tag as i64)),
24        0xe0..=0xff => Some(nodedb_types::Value::Integer((tag as i8) as i64)),
25        UINT8 => Some(nodedb_types::Value::Integer(get(buf, offset + 1)? as i64)),
26        UINT16 => Some(nodedb_types::Value::Integer(
27            read_u16_be(buf, offset + 1)? as i64
28        )),
29        UINT32 => Some(nodedb_types::Value::Integer(
30            read_u32_be(buf, offset + 1)? as i64
31        )),
32        // Above `i64::MAX` this is a `Decimal`, never a wrapped negative.
33        UINT64 => Some(nodedb_types::Value::from_u64(read_u64_be(buf, offset + 1)?)),
34        INT8 => Some(nodedb_types::Value::Integer(
35            get(buf, offset + 1)? as i8 as i64
36        )),
37        INT16 => Some(nodedb_types::Value::Integer(
38            read_u16_be(buf, offset + 1)? as i16 as i64,
39        )),
40        INT32 => Some(nodedb_types::Value::Integer(
41            read_u32_be(buf, offset + 1)? as i32 as i64,
42        )),
43        INT64 => Some(nodedb_types::Value::Integer(
44            read_u64_be(buf, offset + 1)? as i64
45        )),
46        // Floats
47        FLOAT32 => {
48            let bits = read_u32_be(buf, offset + 1)?;
49            Some(nodedb_types::Value::Float(f32::from_bits(bits) as f64))
50        }
51        FLOAT64 => {
52            let bits = read_u64_be(buf, offset + 1)?;
53            Some(nodedb_types::Value::Float(f64::from_bits(bits)))
54        }
55        // Strings
56        0xa0..=0xbf | STR8 | STR16 | STR32 => {
57            read_str(buf, offset).map(|s| nodedb_types::Value::String(s.to_string()))
58        }
59        // Instants
60        FIXEXT8 => read_instant(buf, offset).map(|(kind, micros)| kind.from_micros(micros)),
61        _ => None,
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68    use crate::msgpack_scan::reader::{
69        array_header, map_header, read_bool, read_f64, read_i64, read_null, skip_value,
70    };
71    use nodedb_types::{InstantKind, NdbDateTime, Value, write_instant};
72
73    use serde_json::json;
74
75    /// Helper: encode a serde_json::Value to MessagePack bytes.
76    fn encode(v: &serde_json::Value) -> Vec<u8> {
77        nodedb_types::json_msgpack::json_to_msgpack(v).expect("encode")
78    }
79
80    #[test]
81    fn read_value_instants() {
82        let mut buf = vec![0xc0];
83        write_instant(&mut buf, InstantKind::Utc, 1_710_498_600_000_000);
84        write_instant(&mut buf, InstantKind::Naive, -5);
85        assert_eq!(
86            read_value(&buf, 1),
87            Some(Value::DateTime(NdbDateTime::from_micros(
88                1_710_498_600_000_000
89            )))
90        );
91        assert_eq!(
92            read_value(&buf, 11),
93            Some(Value::NaiveDateTime(NdbDateTime::from_micros(-5)))
94        );
95        assert_eq!(skip_value(&buf, 1), Some(11));
96    }
97
98    #[test]
99    fn read_value_uint64_above_i64_max_is_decimal() {
100        let mut buf = vec![UINT64];
101        buf.extend_from_slice(&u64::MAX.to_be_bytes());
102        assert_eq!(
103            read_value(&buf, 0),
104            Some(Value::Decimal(rust_decimal::Decimal::from(u64::MAX)))
105        );
106        let mut edge = vec![UINT64];
107        edge.extend_from_slice(&(i64::MAX as u64).to_be_bytes());
108        assert_eq!(read_value(&edge, 0), Some(Value::Integer(i64::MAX)));
109        let buf = encode(&json!(9_223_372_036_854_775_808_u64));
110        assert_eq!(
111            read_value(&buf, 0),
112            Some(Value::Decimal(rust_decimal::Decimal::from(
113                9_223_372_036_854_775_808_u64
114            )))
115        );
116    }
117
118    #[test]
119    fn read_value_unknown_ext_is_none() {
120        let buf = [FIXEXT8, 0x09, 0, 0, 0, 0, 0, 0, 0, 1];
121        assert_eq!(read_value(&buf, 0), None);
122        let buf = [FIXEXT1, 0x01, 0xab];
123        assert_eq!(read_value(&buf, 0), None);
124    }
125
126    #[test]
127    fn read_value_truncated_instant_is_none() {
128        let mut buf = Vec::new();
129        write_instant(&mut buf, InstantKind::Utc, 42);
130        buf.truncate(9);
131        assert_eq!(read_value(&buf, 0), None);
132    }
133
134    /// Feed every single-byte sequence through all reader functions. None may
135    /// panic — they must return `None` or a valid result.
136    #[test]
137    fn fuzz_all_single_byte_sequences() {
138        for byte in 0u8..=255 {
139            let buf = [byte];
140            // None of these must panic
141            let _ = skip_value(&buf, 0);
142            let _ = read_f64(&buf, 0);
143            let _ = read_i64(&buf, 0);
144            let _ = read_str(&buf, 0);
145            let _ = read_bool(&buf, 0);
146            let _ = read_null(&buf, 0);
147            let _ = map_header(&buf, 0);
148            let _ = array_header(&buf, 0);
149            let _ = read_value(&buf, 0);
150        }
151    }
152
153    /// Feed two-byte patterns to cover tag + partial payload (truncated).
154    #[test]
155    fn fuzz_two_byte_patterns() {
156        // Tags that expect more bytes than we provide
157        let tags_need_extra: &[u8] = &[
158            0xca, // FLOAT32 needs 4 more
159            0xcb, // FLOAT64 needs 8 more
160            0xcc, // UINT8 needs 1 more
161            0xcd, // UINT16 needs 2 more
162            0xce, // UINT32 needs 4 more
163            0xcf, // UINT64 needs 8 more
164            0xd0, // INT8 needs 1 more
165            0xd1, // INT16 needs 2 more
166            0xd2, // INT32 needs 4 more
167            0xd3, // INT64 needs 8 more
168            0xd9, // STR8 length byte then data
169            0xda, // STR16 2-byte length then data
170            0xdb, // STR32 4-byte length then data
171            0xdc, // ARRAY16 2-byte count then elements
172            0xdd, // ARRAY32 4-byte count then elements
173            0xde, // MAP16 2-byte count then pairs
174            0xdf, // MAP32 4-byte count then pairs
175            0xc4, // BIN8
176            0xc5, // BIN16
177            0xc6, // BIN32
178            0xd4, // FIXEXT1
179            0xd5, // FIXEXT2
180            0xd6, // FIXEXT4
181            0xd7, // FIXEXT8
182            0xd8, // FIXEXT16
183        ];
184        for &tag in tags_need_extra {
185            // Single byte (completely truncated payload)
186            let buf = [tag];
187            let _ = skip_value(&buf, 0);
188            let _ = read_f64(&buf, 0);
189            let _ = read_i64(&buf, 0);
190            let _ = read_value(&buf, 0);
191
192            // Tag + one garbage byte
193            for second in [0x00u8, 0x01, 0x7f, 0x80, 0xff] {
194                let buf = [tag, second];
195                let _ = skip_value(&buf, 0);
196                let _ = read_f64(&buf, 0);
197                let _ = read_i64(&buf, 0);
198                let _ = read_value(&buf, 0);
199            }
200        }
201    }
202
203    /// Deterministic pseudo-random byte sequences must not cause panics.
204    #[test]
205    fn fuzz_deterministic_random_payloads() {
206        // Generate deterministic sequences without external crates using a
207        // simple LCG (Knuth multiplicative hash).
208        let mut state: u64 = 0xdeadbeef_cafebabe;
209        let next = |s: &mut u64| -> u8 {
210            *s = s
211                .wrapping_mul(6364136223846793005)
212                .wrapping_add(1442695040888963407);
213            (*s >> 33) as u8
214        };
215
216        let mut buf = vec![0u8; 256];
217        for _ in 0..2000 {
218            // Randomize buffer length (1..=256) and contents
219            let len = (next(&mut state) as usize % 256) + 1;
220            for b in buf[..len].iter_mut() {
221                *b = next(&mut state);
222            }
223            let slice = &buf[..len];
224
225            // Try reading from multiple offsets
226            for offset in [0, 1, len / 2, len.saturating_sub(1)] {
227                let _ = skip_value(slice, offset);
228                let _ = read_f64(slice, offset);
229                let _ = read_i64(slice, offset);
230                let _ = read_str(slice, offset);
231                let _ = read_bool(slice, offset);
232                let _ = read_null(slice, offset);
233                let _ = map_header(slice, offset);
234                let _ = array_header(slice, offset);
235                let _ = read_value(slice, offset);
236            }
237        }
238    }
239
240    /// Truncate a valid msgpack buffer at every byte position.
241    /// All reader functions must return `None` — never panic.
242    #[test]
243    fn fuzz_truncated_valid_payloads() {
244        let docs = [
245            json!({"key": "value", "num": 42, "flag": true}),
246            json!({"nested": {"a": 1, "b": [1, 2, 3]}}),
247            json!([1, "two", 3.0, null, false]),
248            json!({"large": 9999999999_i64}),
249            json!({"float": 1.23456789}),
250        ];
251
252        for doc in &docs {
253            let full = encode(doc);
254            // Truncate at every position from 0 to full.len()-1
255            for truncate_at in 0..full.len() {
256                let slice = &full[..truncate_at];
257                // None of these may panic; result doesn't matter
258                let _ = skip_value(slice, 0);
259                let _ = read_f64(slice, 0);
260                let _ = read_i64(slice, 0);
261                let _ = read_str(slice, 0);
262                let _ = read_bool(slice, 0);
263                let _ = map_header(slice, 0);
264                let _ = array_header(slice, 0);
265                let _ = read_value(slice, 0);
266            }
267        }
268    }
269
270    /// The never-used 0xc1 tag must return `None` for all functions.
271    #[test]
272    fn fuzz_never_used_tag_c1() {
273        // 0xc1 is explicitly "never used" in the msgpack spec
274        let buf = [0xc1u8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
275        assert_eq!(
276            skip_value(&buf, 0),
277            None,
278            "0xc1 must return None from skip_value"
279        );
280        assert_eq!(read_f64(&buf, 0), None);
281        assert_eq!(read_i64(&buf, 0), None);
282        assert_eq!(read_str(&buf, 0), None);
283        assert_eq!(read_bool(&buf, 0), None);
284        assert_eq!(map_header(&buf, 0), None);
285        assert_eq!(array_header(&buf, 0), None);
286        assert_eq!(read_value(&buf, 0), None);
287    }
288
289    /// Out-of-bounds offset must return `None` — not panic.
290    #[test]
291    fn fuzz_out_of_bounds_offset() {
292        let buf = encode(&json!({"x": 1}));
293        let way_out = buf.len() + 1000;
294        assert_eq!(skip_value(&buf, way_out), None);
295        assert_eq!(read_f64(&buf, way_out), None);
296        assert_eq!(read_i64(&buf, way_out), None);
297        assert_eq!(read_str(&buf, way_out), None);
298        assert_eq!(read_bool(&buf, way_out), None);
299        assert_eq!(map_header(&buf, way_out), None);
300        assert_eq!(array_header(&buf, way_out), None);
301        assert_eq!(read_value(&buf, way_out), None);
302    }
303
304    /// Empty buffer must return `None` for all functions that can.
305    #[test]
306    fn fuzz_empty_buffer() {
307        let buf: &[u8] = &[];
308        assert_eq!(skip_value(buf, 0), None);
309        assert_eq!(read_f64(buf, 0), None);
310        assert_eq!(read_i64(buf, 0), None);
311        assert_eq!(read_str(buf, 0), None);
312        assert_eq!(read_bool(buf, 0), None);
313        assert!(!read_null(buf, 0)); // returns bool, not Option
314        assert_eq!(map_header(buf, 0), None);
315        assert_eq!(array_header(buf, 0), None);
316        assert_eq!(read_value(buf, 0), None);
317    }
318}