Skip to main content

yo_doc/
read.rs

1//! Reading a YOJB value without decoding it.
2//!
3//! Every accessor here is bounds checked and answers `None` rather than
4//! panicking, because these bytes come off a disk and a corrupt document is a
5//! thing that happens. Nothing here allocates and nothing here copies: a child
6//! is a slice of its parent and a string is a slice of the document.
7
8use core::cmp::Ordering;
9
10use crate::head::{self, ARRAY, COUNT_MAX, DEPTH_MAX, INTERNED, Kind, OFFSETS, SORTED, Tag};
11
12/// A value, borrowed from the bytes it is stored in.
13///
14/// The slice starts at the value's header and may run past its end, which is
15/// what makes a child free: it is the parent's slice from the child's offset,
16/// with no length to compute. Use [`Value::encoded_len`] when the exact end
17/// matters, which is when the value is being copied somewhere else.
18#[derive(Clone, Copy)]
19pub struct Value<'a> {
20    b: &'a [u8],
21}
22
23impl<'a> Value<'a> {
24    /// A value over `bytes`, if the header at the front is one this version
25    /// understands and its payload is there.
26    ///
27    /// This is a header check and not a walk. It is what a read does, because a
28    /// read touches one path and checking the whole document to answer one
29    /// field would cost more than the read. [`Value::validate`] is the walk,
30    /// for the caller that is about to trust the whole thing.
31    #[must_use]
32    pub fn new(bytes: &'a [u8]) -> Option<Value<'a>> {
33        let v = Value { b: bytes };
34        let h = head::read(bytes, 0)?;
35        let tag = Tag::of(h)?;
36        if matches!(tag, Tag::Container) {
37            if h & OFFSETS == 0 || head::count(h) > COUNT_MAX {
38                return None;
39            }
40            // The entry table has to be there before anything can be indexed.
41            // The value region is checked per element, on the way in.
42            let end = v.entries_end()?;
43            if bytes.len() < end {
44                return None;
45            }
46        } else if bytes.len() < 4 + head::count(h) {
47            return None;
48        }
49        Some(v)
50    }
51
52    /// What this value is.
53    #[must_use]
54    pub fn kind(&self) -> Kind {
55        match self.tag() {
56            Tag::Null => Kind::Null,
57            Tag::False | Tag::True => Kind::Bool,
58            Tag::Int => Kind::Int,
59            Tag::Float => Kind::Float,
60            Tag::Text => Kind::Text,
61            Tag::Container if self.head() & ARRAY == 0 => Kind::Object,
62            Tag::Container => Kind::Array,
63        }
64    }
65
66    /// Whether this is `null`.
67    #[must_use]
68    pub fn is_null(&self) -> bool {
69        matches!(self.tag(), Tag::Null)
70    }
71
72    /// The boolean this holds, if it holds one.
73    #[must_use]
74    pub fn as_bool(&self) -> Option<bool> {
75        match self.tag() {
76            Tag::False => Some(false),
77            Tag::True => Some(true),
78            _ => None,
79        }
80    }
81
82    /// The integer this holds, if it holds one.
83    ///
84    /// The payload is as narrow as the number allows, so a document full of
85    /// small numbers costs five bytes each rather than twelve, and reading one
86    /// back is a sign extending load of one, two, four or eight bytes.
87    #[must_use]
88    pub fn as_int(&self) -> Option<i64> {
89        if !matches!(self.tag(), Tag::Int) {
90            return None;
91        }
92        let raw = self.payload()?;
93        Some(match raw.len() {
94            1 => i64::from(raw[0] as i8),
95            2 => i64::from(i16::from_le_bytes(raw.try_into().ok()?)),
96            4 => i64::from(i32::from_le_bytes(raw.try_into().ok()?)),
97            8 => i64::from_le_bytes(raw.try_into().ok()?),
98            _ => return None,
99        })
100    }
101
102    /// The float this holds, if it holds one.
103    #[must_use]
104    pub fn as_float(&self) -> Option<f64> {
105        if !matches!(self.tag(), Tag::Float) {
106            return None;
107        }
108        let raw = self.payload()?;
109        Some(f64::from_le_bytes(raw.try_into().ok()?))
110    }
111
112    /// The string this holds, if it holds one and it is UTF-8.
113    #[must_use]
114    pub fn as_text(&self) -> Option<&'a str> {
115        core::str::from_utf8(self.text_bytes()?).ok()
116    }
117
118    /// The string this holds as it is stored, without the UTF-8 check.
119    ///
120    /// A string written through this crate is UTF-8 by construction, so the
121    /// check only ever catches a damaged file. A caller that is going to hand
122    /// the bytes straight back out over RESP does not need it.
123    #[must_use]
124    pub fn text_bytes(&self) -> Option<&'a [u8]> {
125        if !matches!(self.tag(), Tag::Text) {
126            return None;
127        }
128        self.payload()
129    }
130
131    /// How many elements a container holds. Zero for anything else.
132    #[must_use]
133    pub fn len(&self) -> usize {
134        if matches!(self.tag(), Tag::Container) {
135            head::count(self.head())
136        } else {
137            0
138        }
139    }
140
141    /// Whether this is a container with nothing in it.
142    ///
143    /// A scalar is not empty, it is not a container, so this is false for one.
144    #[must_use]
145    pub fn is_empty(&self) -> bool {
146        matches!(self.tag(), Tag::Container) && self.len() == 0
147    }
148
149    /// Whether this object's keys are ids from a collection's intern table
150    /// rather than bytes stored with the document.
151    ///
152    /// Nothing else about reading changes, except that a lookup is by id and
153    /// getting a key's name back needs the table.
154    #[must_use]
155    pub fn is_interned(&self) -> bool {
156        self.is_container() && self.head() & INTERNED != 0
157    }
158
159    /// The value of element `i`, counting in the container's own order.
160    ///
161    /// For an array that is the order the elements were written in. For an
162    /// object it is key order, which is not the order the document was written
163    /// in, and it is the order [`Value::members`] walks.
164    #[must_use]
165    pub fn at(&self, i: usize) -> Option<Value<'a>> {
166        let (_, off) = self.entry(i)?;
167        let child = self.b.get(off..)?;
168        Value::new(child)
169    }
170
171    /// The key of member `i` of an object, if the object stores its keys as
172    /// bytes.
173    #[must_use]
174    pub fn key_at(&self, i: usize) -> Option<&'a [u8]> {
175        if !self.is_object() || self.is_interned() {
176            return None;
177        }
178        let at = self.key_off(i)?;
179        let end = self.key_end(i)?;
180        self.b.get(at..end)
181    }
182
183    /// The intern table id of member `i` of an object, if the object stores its
184    /// keys as ids.
185    #[must_use]
186    pub fn key_id_at(&self, i: usize) -> Option<u16> {
187        if !self.is_interned() {
188            return None;
189        }
190        let at = 4 + i * 2;
191        let raw = self.b.get(at..at + 2)?;
192        Some(u16::from_le_bytes(raw.try_into().expect("two bytes")))
193    }
194
195    /// The value stored under `key`, by binary search over the entry table.
196    ///
197    /// Keys are ordered by length and then by bytes, so the search compares a
198    /// length before it compares anything else and most steps never touch the
199    /// key region at all. This is the lookup G15 is about: for a document whose
200    /// keys are interned it is not even this, it is [`Value::get_id`].
201    #[must_use]
202    pub fn get(&self, key: &[u8]) -> Option<Value<'a>> {
203        self.at(self.find(key)?)
204    }
205
206    /// The index of `key` among this object's members.
207    #[must_use]
208    pub fn find(&self, key: &[u8]) -> Option<usize> {
209        if !self.is_object() || self.is_interned() {
210            return None;
211        }
212        let n = self.len();
213        if self.head() & SORTED == 0 {
214            return (0..n).find(|&i| self.key_at(i) == Some(key));
215        }
216        let (mut lo, mut hi) = (0usize, n);
217        while lo < hi {
218            let mid = (lo + hi) / 2;
219            match key_order(self.key_at(mid)?, key) {
220                Ordering::Less => lo = mid + 1,
221                Ordering::Greater => hi = mid,
222                Ordering::Equal => return Some(mid),
223            }
224        }
225        None
226    }
227
228    /// The value stored under intern table id `id`.
229    #[must_use]
230    pub fn get_id(&self, id: u16) -> Option<Value<'a>> {
231        self.at(self.find_id(id)?)
232    }
233
234    /// The index of intern table id `id` among this object's members.
235    #[must_use]
236    pub fn find_id(&self, id: u16) -> Option<usize> {
237        if !self.is_interned() {
238            return None;
239        }
240        let n = self.len();
241        if self.head() & SORTED == 0 {
242            return (0..n).find(|&i| self.key_id_at(i) == Some(id));
243        }
244        let (mut lo, mut hi) = (0usize, n);
245        while lo < hi {
246            let mid = (lo + hi) / 2;
247            match self.key_id_at(mid)?.cmp(&id) {
248                Ordering::Less => lo = mid + 1,
249                Ordering::Greater => hi = mid,
250                Ordering::Equal => return Some(mid),
251            }
252        }
253        None
254    }
255
256    /// Every element of a container, in the container's own order.
257    #[must_use]
258    pub fn iter(&self) -> Elems<'a> {
259        Elems { v: *self, i: 0 }
260    }
261
262    /// Every member of an object, key first, in key order.
263    ///
264    /// An interned object yields nothing here, because the names are not in the
265    /// document. Walk it with [`Value::key_id_at`] and [`Value::at`].
266    #[must_use]
267    pub fn members(&self) -> Members<'a> {
268        Members { v: *self, i: 0 }
269    }
270
271    /// How many bytes this value occupies, header included.
272    ///
273    /// A container works this out from its last element, which recurses down
274    /// the right hand edge of the document and so costs one step per level
275    /// rather than one per element. That is the price of not spending four
276    /// bytes a container on a length nothing else needs.
277    #[must_use]
278    pub fn encoded_len(&self) -> Option<usize> {
279        self.encoded_len_at(0)
280    }
281
282    fn encoded_len_at(&self, depth: usize) -> Option<usize> {
283        if depth > DEPTH_MAX {
284            return None;
285        }
286        let h = self.head();
287        if !matches!(Tag::of(h)?, Tag::Container) {
288            return Some(4 + head::count(h));
289        }
290        let n = head::count(h);
291        if n == 0 {
292            return self.entries_end();
293        }
294        let (_, off) = self.entry(n - 1)?;
295        let last = Value::new(self.b.get(off..)?)?;
296        off.checked_add(last.encoded_len_at(depth + 1)?)
297    }
298
299    /// This value's bytes and nothing after them.
300    #[must_use]
301    pub fn as_bytes(&self) -> Option<&'a [u8]> {
302        self.b.get(..self.encoded_len()?)
303    }
304
305    /// Walk the whole value and check that every part of it is there.
306    ///
307    /// This is what a caller runs over bytes it did not write: a record read
308    /// back from a file that failed its checksum in an interesting way, or a
309    /// document handed in over a socket. Everything it checks, the accessors
310    /// also check one at a time, so a document that fails here still cannot
311    /// make a read panic. It is O(the document).
312    #[must_use]
313    pub fn validate(&self) -> bool {
314        self.validate_at(0)
315    }
316
317    fn validate_at(&self, depth: usize) -> bool {
318        if depth > DEPTH_MAX {
319            return false;
320        }
321        let Some(h) = head::read(self.b, 0) else {
322            return false;
323        };
324        let Some(tag) = Tag::of(h) else {
325            return false;
326        };
327        if !matches!(tag, Tag::Container) {
328            let n = head::count(h);
329            if matches!(tag, Tag::Int) && !matches!(n, 1 | 2 | 4 | 8) {
330                return false;
331            }
332            if matches!(tag, Tag::Float) && n != 8 {
333                return false;
334            }
335            if matches!(tag, Tag::Null | Tag::False | Tag::True) && n != 0 {
336                return false;
337            }
338            return self.b.len() >= 4 + n;
339        }
340        if h & OFFSETS == 0 {
341            return false;
342        }
343        let n = head::count(h);
344        let Some(mut want) = self.entries_end() else {
345            return false;
346        };
347        if self.b.len() < want {
348            return false;
349        }
350        if self.is_object() && !self.is_interned() {
351            // The key region runs from the end of the entry table to the first
352            // value, and the keys inside it have to tile it in order.
353            for i in 0..n {
354                let (Some(at), Some(end)) = (self.key_off(i), self.key_end(i)) else {
355                    return false;
356                };
357                if at != want || end < at || self.b.len() < end {
358                    return false;
359                }
360                want = end;
361            }
362        }
363        for i in 0..n {
364            let Some((copy, off)) = self.entry(i) else {
365                return false;
366            };
367            // Values are stored in entry order and they tile the value region,
368            // which is what lets a length be a difference of two offsets.
369            if off != want {
370                return false;
371            }
372            let Some(child) = self.b.get(off..).and_then(Value::new) else {
373                return false;
374            };
375            if child.head() != copy || !child.validate_at(depth + 1) {
376                return false;
377            }
378            let Some(len) = child.encoded_len_at(depth + 1) else {
379                return false;
380            };
381            want = off + len;
382        }
383        if self.is_object() && h & SORTED != 0 && !self.keys_ascend(n) {
384            return false;
385        }
386        true
387    }
388
389    /// The header word.
390    fn head(&self) -> u32 {
391        head::read(self.b, 0).unwrap_or(0)
392    }
393
394    fn tag(&self) -> Tag {
395        Tag::of(self.head()).unwrap_or(Tag::Null)
396    }
397
398    fn is_container(&self) -> bool {
399        matches!(self.tag(), Tag::Container)
400    }
401
402    fn is_object(&self) -> bool {
403        self.is_container() && self.head() & ARRAY == 0
404    }
405
406    /// A scalar's bytes, after the header.
407    fn payload(&self) -> Option<&'a [u8]> {
408        let n = head::count(self.head());
409        self.b.get(4..4 + n)
410    }
411
412    /// Where the entry table starts, which is after the key entries.
413    fn entries_at(&self) -> usize {
414        4 + crate::layout::keys_area(self.head(), self.len())
415    }
416
417    /// Where the key region starts, which is after the entry table.
418    fn entries_end(&self) -> Option<usize> {
419        self.entries_at().checked_add(self.len().checked_mul(8)?)
420    }
421
422    /// Element `i`'s header copy and where its value starts.
423    fn entry(&self, i: usize) -> Option<(u32, usize)> {
424        if !self.is_container() || i >= self.len() {
425            return None;
426        }
427        let at = self.entries_at() + i * 8;
428        let copy = head::read(self.b, at)?;
429        let off = head::read(self.b, at + 4)? as usize;
430        // A child starts after its parent's entry table, always. Checking it
431        // here rather than only in [`Value::validate`] is what keeps a damaged
432        // offset from making a child that contains its own parent, and so keeps
433        // every walk over a document finite.
434        if off < self.entries_end()? {
435            return None;
436        }
437        Some((copy, off))
438    }
439
440    /// Where member `i`'s key starts.
441    fn key_off(&self, i: usize) -> Option<usize> {
442        if i >= self.len() {
443            return None;
444        }
445        Some(head::read(self.b, 4 + i * 4)? as usize)
446    }
447
448    /// Where member `i`'s key ends.
449    ///
450    /// Keys are stored in member order and tile the key region, so a key ends
451    /// where the next one starts, and the last one ends where the first value
452    /// starts. That is why the region costs four bytes a key rather than eight.
453    fn key_end(&self, i: usize) -> Option<usize> {
454        if i + 1 < self.len() {
455            self.key_off(i + 1)
456        } else {
457            self.entry(0).map(|(_, off)| off)
458        }
459    }
460
461    fn keys_ascend(&self, n: usize) -> bool {
462        for i in 1..n {
463            let ord = if self.is_interned() {
464                match (self.key_id_at(i - 1), self.key_id_at(i)) {
465                    (Some(a), Some(b)) => a.cmp(&b),
466                    _ => return false,
467                }
468            } else {
469                match (self.key_at(i - 1), self.key_at(i)) {
470                    (Some(a), Some(b)) => key_order(a, b),
471                    _ => return false,
472                }
473            };
474            if ord != Ordering::Less {
475                return false;
476            }
477        }
478        true
479    }
480}
481
482impl core::fmt::Debug for Value<'_> {
483    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
484        match self.kind() {
485            Kind::Null => f.write_str("null"),
486            Kind::Bool => write!(f, "{}", self.as_bool().unwrap_or(false)),
487            Kind::Int => write!(f, "{}", self.as_int().unwrap_or(0)),
488            Kind::Float => write!(f, "{}", self.as_float().unwrap_or(0.0)),
489            Kind::Text => write!(f, "{:?}", self.as_text().unwrap_or("")),
490            Kind::Array => f.debug_list().entries(self.iter()).finish(),
491            Kind::Object => {
492                let mut m = f.debug_map();
493                for (k, v) in self.members() {
494                    m.entry(&String::from_utf8_lossy(k), &v);
495                }
496                m.finish()
497            }
498        }
499    }
500}
501
502/// How two object keys compare: shorter first, then by bytes.
503///
504/// Length first is not arbitrary. It puts the cheapest comparison at the front
505/// of the search, so most steps of a lookup are an integer compare against a
506/// number the reader already has, and it keeps keys of one length together in
507/// the key region.
508#[must_use]
509pub fn key_order(a: &[u8], b: &[u8]) -> Ordering {
510    a.len().cmp(&b.len()).then_with(|| a.cmp(b))
511}
512
513/// Every element of a container, from [`Value::iter`].
514#[derive(Clone)]
515pub struct Elems<'a> {
516    v: Value<'a>,
517    i: usize,
518}
519
520impl<'a> Iterator for Elems<'a> {
521    type Item = Value<'a>;
522
523    fn next(&mut self) -> Option<Value<'a>> {
524        let out = self.v.at(self.i)?;
525        self.i += 1;
526        Some(out)
527    }
528
529    fn size_hint(&self) -> (usize, Option<usize>) {
530        let left = self.v.len().saturating_sub(self.i);
531        (left, Some(left))
532    }
533}
534
535/// Every member of an object, from [`Value::members`].
536#[derive(Clone)]
537pub struct Members<'a> {
538    v: Value<'a>,
539    i: usize,
540}
541
542impl<'a> Iterator for Members<'a> {
543    type Item = (&'a [u8], Value<'a>);
544
545    fn next(&mut self) -> Option<(&'a [u8], Value<'a>)> {
546        let key = self.v.key_at(self.i)?;
547        let val = self.v.at(self.i)?;
548        self.i += 1;
549        Some((key, val))
550    }
551
552    fn size_hint(&self) -> (usize, Option<usize>) {
553        let left = self.v.len().saturating_sub(self.i);
554        (left, Some(left))
555    }
556}
557
558#[cfg(test)]
559mod tests {
560    use super::*;
561    use crate::Builder;
562    use yo_common::Rng;
563
564    /// A document with one of everything in it.
565    fn sample() -> Vec<u8> {
566        let mut b = Builder::new();
567        b.begin_object().expect("open");
568        for (k, write) in [
569            ("nil", 0),
570            ("yes", 1),
571            ("n", 2),
572            ("big", 3),
573            ("f", 4),
574            ("s", 5),
575            ("arr", 6),
576            ("obj", 7),
577        ] {
578            b.key(k.as_bytes()).expect("key");
579            match write {
580                0 => b.null().expect("value"),
581                1 => b.bool(true).expect("value"),
582                2 => b.int(-3).expect("value"),
583                3 => b.int(i64::MAX).expect("value"),
584                4 => b.float(0.125).expect("value"),
585                5 => b.text("a string with some length to it").expect("value"),
586                6 => {
587                    b.begin_array().expect("open");
588                    b.int(1).expect("value");
589                    b.text("two").expect("value");
590                    b.end_array().expect("close");
591                }
592                _ => {
593                    b.begin_object().expect("open");
594                    b.key(b"deep").expect("key");
595                    b.int(9).expect("value");
596                    b.end_object().expect("close");
597                }
598            }
599        }
600        b.end_object().expect("close");
601        b.finish().expect("finished").to_vec()
602    }
603
604    /// Touch every accessor on every part of `bytes`, however damaged it is.
605    ///
606    /// The point is that nothing here panics and nothing here runs forever. A
607    /// corrupt count can claim sixteen million elements over four bytes, so the
608    /// walk stops after a few, and a corrupt offset cannot point backwards
609    /// because [`Value::entry`] refuses one that does.
610    fn walk(bytes: &[u8]) {
611        fn go(v: Value<'_>, depth: usize) {
612            if depth > 8 {
613                return;
614            }
615            let _ = v.kind();
616            let _ = v.is_null();
617            let _ = v.as_bool();
618            let _ = v.as_int();
619            let _ = v.as_float();
620            let _ = v.as_text();
621            let _ = v.text_bytes();
622            let _ = v.is_empty();
623            let _ = v.encoded_len();
624            let _ = v.as_bytes();
625            let _ = v.get(b"nil");
626            let _ = v.get_id(3);
627            let _ = v.path("$.a.b[0]");
628            let _ = format!("{v:?}");
629            for i in 0..v.len().min(16) {
630                let _ = v.key_at(i);
631                let _ = v.key_id_at(i);
632                if let Some(child) = v.at(i) {
633                    go(child, depth + 1);
634                }
635            }
636        }
637        if let Some(v) = Value::new(bytes) {
638            let _ = v.validate();
639            go(v, 0);
640        }
641    }
642
643    #[test]
644    fn a_document_cut_short_anywhere_is_refused_and_never_panics() {
645        let bytes = sample();
646        for n in 0..bytes.len() {
647            let cut = &bytes[..n];
648            walk(cut);
649            if let Some(v) = Value::new(cut) {
650                assert!(!v.validate(), "a document missing its tail is not sound");
651            }
652        }
653        assert!(Value::new(&bytes).expect("readable").validate());
654    }
655
656    #[test]
657    fn a_document_with_a_byte_changed_is_never_worse_than_wrong() {
658        let bytes = sample();
659        let mut rng = Rng::new(0x5eed_0d0c);
660        for _ in 0..20_000 {
661            let mut damaged = bytes.clone();
662            let at = rng.below(damaged.len());
663            damaged[at] ^= 1 << rng.below(8);
664            walk(&damaged);
665        }
666    }
667
668    #[test]
669    fn a_child_that_points_at_its_own_parent_is_refused() {
670        let mut bytes = sample();
671        let v = Value::new(&bytes).expect("readable");
672        assert!(v.validate());
673        // The first entry's offset lives right after the key entries and the
674        // header copy. Point it at the container itself.
675        let n = v.len();
676        let entry = 4 + n * 4 + 4;
677        bytes[entry..entry + 4].copy_from_slice(&0u32.to_le_bytes());
678        let v = Value::new(&bytes).expect("the header is still fine");
679        assert!(v.at(0).is_none(), "the child is not readable");
680        assert!(!v.validate());
681        walk(&bytes);
682    }
683
684    #[test]
685    fn a_container_that_claims_more_elements_than_it_has_is_refused() {
686        let bytes = sample();
687        let mut damaged = bytes.clone();
688        let h = u32::from_le_bytes(damaged[..4].try_into().expect("four bytes"));
689        let bigger = (h & 0xff) | ((1u32 << 20) << 8);
690        damaged[..4].copy_from_slice(&bigger.to_le_bytes());
691        assert!(
692            Value::new(&damaged).is_none(),
693            "the entry table would not fit, so the value is not readable at all"
694        );
695        walk(&damaged);
696    }
697
698    #[test]
699    fn keys_sort_by_length_and_then_by_bytes() {
700        let mut keys: Vec<&[u8]> = vec![b"bb", b"a", b"", b"ab", b"z", b"aaa"];
701        keys.sort_by(|a, b| key_order(a, b));
702        assert_eq!(keys, [&b""[..], b"a", b"z", b"ab", b"bb", b"aaa"]);
703    }
704
705    #[test]
706    fn a_document_prints_as_itself() {
707        let bytes = sample();
708        let v = Value::new(&bytes).expect("readable");
709        let text = format!("{v:?}");
710        assert!(text.contains("\"n\": -3"), "{text}");
711        assert!(text.contains("\"arr\": [1, \"two\"]"), "{text}");
712        assert!(text.contains("\"nil\": null"), "{text}");
713    }
714
715    #[test]
716    fn an_unsorted_object_is_still_readable() {
717        // Nothing this crate writes clears the sorted flag, but a later version
718        // might, so a reader that finds it clear falls back to a scan rather
719        // than refusing the document.
720        let mut bytes = sample();
721        let h = u32::from_le_bytes(bytes[..4].try_into().expect("four bytes"));
722        bytes[..4].copy_from_slice(&(h & !SORTED).to_le_bytes());
723        let v = Value::new(&bytes).expect("readable");
724        assert!(v.validate(), "clearing the claim does not make it unsound");
725        assert_eq!(v.get(b"n").expect("found by scan").as_int(), Some(-3));
726        assert!(v.get(b"missing").is_none());
727    }
728}