Skip to main content

nodedb_query/msgpack_scan/reader/
skip.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! `skip_value`: advance past one MessagePack value without decoding it.
4
5use super::tags::*;
6
7/// Advance past the MessagePack value starting at `offset`, returning the
8/// offset of the next value. Returns `None` if the buffer is truncated or
9/// nesting exceeds `MAX_DEPTH`.
10///
11/// This is the performance-critical primitive. It never allocates.
12pub fn skip_value(buf: &[u8], offset: usize) -> Option<usize> {
13    skip_value_depth(buf, offset, 0)
14}
15
16fn skip_value_depth(buf: &[u8], offset: usize, depth: u16) -> Option<usize> {
17    if depth > MAX_DEPTH {
18        return None;
19    }
20    let tag = get(buf, offset)?;
21    match tag {
22        // positive fixint (0x00..=0x7f)
23        0x00..=0x7f => Some(offset + 1),
24        // negative fixint (0xe0..=0xff)
25        0xe0..=0xff => Some(offset + 1),
26        // nil, false, true
27        NIL | FALSE | TRUE => Some(offset + 1),
28
29        // fixmap (0x80..=0x8f)
30        0x80..=0x8f => {
31            let count = (tag & 0x0f) as usize;
32            skip_n_pairs(buf, offset + 1, count, depth)
33        }
34        MAP16 => {
35            let count = read_u16_be(buf, offset + 1)? as usize;
36            skip_n_pairs(buf, offset + 3, count, depth)
37        }
38        MAP32 => {
39            let count = read_u32_be(buf, offset + 1)? as usize;
40            skip_n_pairs(buf, offset + 5, count, depth)
41        }
42
43        // fixarray (0x90..=0x9f)
44        0x90..=0x9f => {
45            let count = (tag & 0x0f) as usize;
46            skip_n_values(buf, offset + 1, count, depth)
47        }
48        ARRAY16 => {
49            let count = read_u16_be(buf, offset + 1)? as usize;
50            skip_n_values(buf, offset + 3, count, depth)
51        }
52        ARRAY32 => {
53            let count = read_u32_be(buf, offset + 1)? as usize;
54            skip_n_values(buf, offset + 5, count, depth)
55        }
56
57        // fixstr (0xa0..=0xbf)
58        0xa0..=0xbf => {
59            let len = (tag & 0x1f) as usize;
60            checked_advance(buf, offset, 1 + len)
61        }
62        STR8 => {
63            let len = get(buf, offset + 1)? as usize;
64            checked_advance(buf, offset, 2 + len)
65        }
66        STR16 => {
67            let len = read_u16_be(buf, offset + 1)? as usize;
68            checked_advance(buf, offset, 3 + len)
69        }
70        STR32 => {
71            let len = read_u32_be(buf, offset + 1)? as usize;
72            checked_advance(buf, offset, 5 + len)
73        }
74
75        // bin
76        BIN8 => {
77            let len = get(buf, offset + 1)? as usize;
78            checked_advance(buf, offset, 2 + len)
79        }
80        BIN16 => {
81            let len = read_u16_be(buf, offset + 1)? as usize;
82            checked_advance(buf, offset, 3 + len)
83        }
84        BIN32 => {
85            let len = read_u32_be(buf, offset + 1)? as usize;
86            checked_advance(buf, offset, 5 + len)
87        }
88
89        // fixed-width numerics (bounds-check against buffer length)
90        FLOAT32 => checked_advance(buf, offset, 5),
91        FLOAT64 => checked_advance(buf, offset, 9),
92        UINT8 | INT8 => checked_advance(buf, offset, 2),
93        UINT16 | INT16 => checked_advance(buf, offset, 3),
94        UINT32 | INT32 => checked_advance(buf, offset, 5),
95        UINT64 | INT64 => checked_advance(buf, offset, 9),
96
97        // ext
98        FIXEXT1 => checked_advance(buf, offset, 3),
99        FIXEXT2 => checked_advance(buf, offset, 4),
100        FIXEXT4 => checked_advance(buf, offset, 6),
101        FIXEXT8 => checked_advance(buf, offset, 10),
102        FIXEXT16 => checked_advance(buf, offset, 18),
103        EXT8 => {
104            let len = get(buf, offset + 1)? as usize;
105            checked_advance(buf, offset, 3 + len)
106        }
107        EXT16 => {
108            let len = read_u16_be(buf, offset + 1)? as usize;
109            checked_advance(buf, offset, 4 + len)
110        }
111        EXT32 => {
112            let len = read_u32_be(buf, offset + 1)? as usize;
113            checked_advance(buf, offset, 6 + len)
114        }
115
116        // 0xc1 is never used in the spec
117        _ => None,
118    }
119}
120
121fn skip_n_values(buf: &[u8], mut pos: usize, count: usize, depth: u16) -> Option<usize> {
122    for _ in 0..count {
123        pos = skip_value_depth(buf, pos, depth + 1)?;
124    }
125    Some(pos)
126}
127
128fn skip_n_pairs(buf: &[u8], mut pos: usize, count: usize, depth: u16) -> Option<usize> {
129    for _ in 0..count {
130        pos = skip_value_depth(buf, pos, depth + 1)?; // key
131        pos = skip_value_depth(buf, pos, depth + 1)?; // value
132    }
133    Some(pos)
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139    use crate::msgpack_scan::reader::read_str;
140
141    use serde_json::json;
142
143    /// Helper: encode a serde_json::Value to MessagePack bytes.
144    fn encode(v: &serde_json::Value) -> Vec<u8> {
145        nodedb_types::json_msgpack::json_to_msgpack(v).expect("encode")
146    }
147
148    #[test]
149    fn skip_positive_fixint() {
150        let buf = [0x05, 0xff];
151        assert_eq!(skip_value(&buf, 0), Some(1));
152    }
153
154    #[test]
155    fn skip_negative_fixint() {
156        let buf = [0xe0, 0x00];
157        assert_eq!(skip_value(&buf, 0), Some(1));
158    }
159
160    #[test]
161    fn skip_nil_bool() {
162        assert_eq!(skip_value(&[NIL], 0), Some(1));
163        assert_eq!(skip_value(&[TRUE], 0), Some(1));
164        assert_eq!(skip_value(&[FALSE], 0), Some(1));
165    }
166
167    #[test]
168    fn skip_float64() {
169        let buf = encode(&json!(9.81));
170        assert_eq!(skip_value(&buf, 0), Some(buf.len()));
171    }
172
173    #[test]
174    fn skip_string() {
175        let buf = encode(&json!("hello"));
176        assert_eq!(skip_value(&buf, 0), Some(buf.len()));
177    }
178
179    #[test]
180    fn skip_map() {
181        let buf = encode(&json!({"a": 1, "b": 2}));
182        assert_eq!(skip_value(&buf, 0), Some(buf.len()));
183    }
184
185    #[test]
186    fn skip_nested_array() {
187        let buf = encode(&json!([[1, 2], [3, 4, 5]]));
188        assert_eq!(skip_value(&buf, 0), Some(buf.len()));
189    }
190
191    #[test]
192    fn skip_truncated_returns_none() {
193        let buf = [FLOAT64, 0x40]; // truncated float64
194        assert_eq!(skip_value(&buf, 0), None);
195    }
196
197    #[test]
198    fn skip_bin() {
199        // bin8: 0xc4, len=3, 3 bytes of data
200        let buf = [BIN8, 3, 0xde, 0xad, 0xbe, 0xff];
201        assert_eq!(skip_value(&buf, 0), Some(5));
202    }
203
204    #[test]
205    fn skip_ext() {
206        // fixext1: 0xd4, type byte, 1 data byte
207        let buf = [FIXEXT1, 0x01, 0xab, 0xff];
208        assert_eq!(skip_value(&buf, 0), Some(3));
209    }
210
211    #[test]
212    fn skip_empty_containers() {
213        // empty fixmap
214        assert_eq!(skip_value(&[0x80], 0), Some(1));
215        // empty fixarray
216        assert_eq!(skip_value(&[0x90], 0), Some(1));
217    }
218
219    /// All tag boundary bytes — test transitions at fixint/fixmap/fixarray/fixstr edges.
220    #[test]
221    fn fuzz_tag_boundaries() {
222        // Each entry: (tag, expected_skip_result)
223        // For tags that are self-contained single bytes, skip returns Some(1).
224        // For tags requiring more data we just verify no panic with empty tail.
225        let boundary_tags: &[(u8, bool)] = &[
226            (0x00, true),  // positive fixint 0
227            (0x7f, true),  // positive fixint 127
228            (0x80, true),  // fixmap length 0 (empty map)
229            (0x8f, false), // fixmap length 15 — needs 15 pairs
230            (0x90, true),  // fixarray length 0 (empty array)
231            (0x9f, false), // fixarray length 15 — needs 15 elements
232            (0xa0, true),  // fixstr length 0 (empty string)
233            (0xbf, false), // fixstr length 31 — needs 31 bytes after
234            (0xc0, true),  // nil
235            (0xc1, false), // never used — must return None
236            (0xc2, true),  // false
237            (0xc3, true),  // true
238            (0xe0, true),  // negative fixint -32
239            (0xff, true),  // negative fixint -1
240        ];
241        for &(tag, self_contained) in boundary_tags {
242            let buf = [tag; 64]; // fill with the same tag as padding
243            let result = skip_value(&buf, 0);
244            if self_contained {
245                assert!(result.is_some(), "tag 0x{tag:02x} should skip OK");
246            } else if tag == 0xc1 {
247                assert_eq!(result, None, "0xc1 must always return None");
248            }
249            // For non-self-contained tags with valid padding we just verify no panic.
250        }
251    }
252
253    /// Buffers where length fields claim enormous sizes but the buffer is tiny.
254    #[test]
255    fn fuzz_adversarial_length_fields() {
256        // STR32: tag 0xdb + 4-byte big-endian length claiming 0xffffffff bytes
257        let buf = [0xdbu8, 0xff, 0xff, 0xff, 0xff, b'x', b'y'];
258        assert_eq!(skip_value(&buf, 0), None);
259        assert_eq!(read_str(&buf, 0), None);
260
261        // STR16: tag 0xda + 2-byte length claiming 0xffff bytes
262        let buf = [0xdau8, 0xff, 0xff, b'x'];
263        assert_eq!(skip_value(&buf, 0), None);
264
265        // ARRAY32: claims 0xffffffff elements but buffer is empty after header
266        let buf = [0xddu8, 0xff, 0xff, 0xff, 0xff];
267        assert_eq!(skip_value(&buf, 0), None);
268
269        // MAP32: claims 0xffffffff pairs but buffer is empty after header
270        let buf = [0xdfu8, 0xff, 0xff, 0xff, 0xff];
271        assert_eq!(skip_value(&buf, 0), None);
272
273        // ARRAY16: claims 0xffff elements
274        let buf = [0xdcu8, 0xff, 0xff];
275        assert_eq!(skip_value(&buf, 0), None);
276
277        // MAP16: claims 0xffff pairs
278        let buf = [0xdeu8, 0xff, 0xff];
279        assert_eq!(skip_value(&buf, 0), None);
280
281        // BIN32: claims max length
282        let buf = [0xc6u8, 0xff, 0xff, 0xff, 0xff, 0x00];
283        assert_eq!(skip_value(&buf, 0), None);
284
285        // EXT32: claims max length
286        let buf = [0xc9u8, 0xff, 0xff, 0xff, 0xff, 0x01, 0x00];
287        assert_eq!(skip_value(&buf, 0), None);
288    }
289
290    /// Deeply nested maps/arrays must cause `skip_value` to return `None`
291    /// once nesting exceeds MAX_DEPTH (128).
292    #[test]
293    fn fuzz_malicious_nesting_depth() {
294        // Build a buffer with 200 levels of fixarray (each containing 1 element)
295        // fixarray tag for 1 element = 0x91
296        let depth = 200usize;
297        let mut buf = vec![0x91u8; depth]; // fixarray(1) — opens 1-element array
298        buf.push(0xc0u8); // nil at the innermost leaf
299
300        // skip_value must return None because nesting > MAX_DEPTH
301        assert_eq!(
302            skip_value(&buf, 0),
303            None,
304            "deeply nested arrays must return None to guard against stack overflow"
305        );
306
307        // Same with maps: fixmap(1) = 0x81, then a fixstr(1) key + value
308        // Build 200 levels of fixmap(1) — each pair is (fixstr key, next map)
309        let mut map_buf: Vec<u8> = Vec::new();
310        for i in 0..(depth as u8) {
311            map_buf.push(0x81); // fixmap(1)
312            map_buf.push(0xa1); // fixstr(1) key
313            map_buf.push(b'a'.wrapping_add(i % 26));
314            // value = next map (already pushed in next iteration), or nil at end
315        }
316        map_buf.push(0xc0); // nil leaf
317
318        assert_eq!(
319            skip_value(&map_buf, 0),
320            None,
321            "deeply nested maps must return None"
322        );
323    }
324
325    /// Verify skip_value correctly consumes exactly the right number of bytes
326    /// for all fixed-width numeric types and returns the correct next offset.
327    #[test]
328    fn fuzz_fixed_width_numeric_skip_offsets() {
329        // (tag, expected_total_bytes_consumed)
330        let cases: &[(u8, usize)] = &[
331            (0xca, 5), // FLOAT32: 1 tag + 4 data
332            (0xcb, 9), // FLOAT64: 1 tag + 8 data
333            (0xcc, 2), // UINT8
334            (0xcd, 3), // UINT16
335            (0xce, 5), // UINT32
336            (0xcf, 9), // UINT64
337            (0xd0, 2), // INT8
338            (0xd1, 3), // INT16
339            (0xd2, 5), // INT32
340            (0xd3, 9), // INT64
341        ];
342        for &(tag, size) in cases {
343            let mut buf = vec![0u8; size + 4]; // extra padding
344            buf[0] = tag;
345            let result = skip_value(&buf, 0);
346            assert_eq!(
347                result,
348                Some(size),
349                "tag 0x{tag:02x} should advance by {size} bytes"
350            );
351        }
352    }
353
354    /// Verify all fixext types consume the correct byte count.
355    #[test]
356    fn fuzz_fixext_skip_offsets() {
357        // (tag, expected_bytes_consumed)
358        let cases: &[(u8, usize)] = &[
359            (0xd4, 3),  // FIXEXT1: 1+1+1
360            (0xd5, 4),  // FIXEXT2: 1+1+2
361            (0xd6, 6),  // FIXEXT4: 1+1+4
362            (0xd7, 10), // FIXEXT8: 1+1+8
363            (0xd8, 18), // FIXEXT16: 1+1+16
364        ];
365        for &(tag, size) in cases {
366            let mut buf = vec![0u8; size + 4];
367            buf[0] = tag;
368            let result = skip_value(&buf, 0);
369            assert_eq!(
370                result,
371                Some(size),
372                "fixext tag 0x{tag:02x} should advance by {size} bytes"
373            );
374        }
375    }
376}