Skip to main content

yo_format/
document.rs

1//! What a document record holds, which is the document and nothing around it.
2//!
3//! `06` section 2.1 gives kind 2 to a document and says nothing about what is
4//! inside it. The answer is that a document record's value is the YOJB value
5//! byte for byte, with no header of its own, and this module is where that
6//! decision is written down and checked.
7//!
8//! ```text
9//! +---------+----------------------------------------+
10//! |  head   |            the rest of the value       |
11//! |    4    |                                        |
12//! +---------+----------------------------------------+
13//! ```
14//!
15//! # Why there is no framing
16//!
17//! A vector record needs a header because a run of `f32` says nothing about
18//! itself: the dimension and the element type have to come from somewhere, and
19//! the record is the only place that a reader with no catalogue can get them.
20//! YOJB is the opposite. Every value begins with a four byte header that carries
21//! its kind, its flags and its count, offsets inside a container are relative to
22//! that header, and the last entry is enough to work out the whole length. A
23//! frame around it would be four to eight bytes on every document that repeat
24//! what the first word already says, and it would be a second length to
25//! disagree with the first one.
26//!
27//! So the record's value is the document, `DocumentBody::decode` is the check
28//! that the first word is one this version understands, and the length a reader
29//! gets back from the log is the length of the document.
30//!
31//! # What this checks and what it does not
32//!
33//! The record layer owns the framing and `yo-doc` owns the value. This checks
34//! the head: that it is there, that the tag is one of the seven this version
35//! defines, and, for a scalar, that the payload is exactly as long as the head
36//! says and the right length for its type. It does not walk a container, because
37//! walking a container means knowing where the entry tables are and how deep the
38//! nesting is allowed to go, and there is one copy of that in `yo-doc` on
39//! purpose. `Value::validate` is the deep check and `yodb check` is what calls
40//! both.
41//!
42//! Getting this split wrong in the other direction would be worse than the
43//! duplication it saves. A reader that has to understand documents to skip a
44//! document record cannot skip a kind it does not know, and skipping is what
45//! `07` section 9 requires of it.
46//!
47//! # Why the numbering is here as well as in `yo-doc`
48//!
49//! These are the bytes on disk, so they belong with the other frozen shapes,
50//! and a reader that only wants to know whether a record is an object or an
51//! array should not have to pull in the document model to find out. `yo-doc`
52//! has the same numbers because it is the one that reads them, and a test in
53//! that crate holds the two together, which is what `yo-kv` already does with
54//! [`crate::ValueType`].
55//!
56//! # What is not in here
57//!
58//! The key table. An interned object stores two byte ids instead of key bytes,
59//! and the names those ids stand for live in the collection rather than in any
60//! one document. That is a collection chunk under a checkpoint, not a record
61//! kind, and it is not written yet.
62//!
63//! Interning needs no generation number alongside it, which is worth saying
64//! because it looks like it should. An id is the row a name sits at in a table
65//! that never removes anything, so an id handed out at any point stays the same
66//! name for the life of the collection, and a document interned against an
67//! early state of the table reads correctly against every later one.
68
69use crate::get_u32;
70use yo_common::{Code, Error, Result};
71
72/// The header every YOJB value begins with.
73pub const DOC_HEADER_LEN: usize = 4;
74
75/// Where the count starts in the header.
76pub const DOC_COUNT_SHIFT: u32 = 8;
77
78/// The largest count a header can hold, which caps a container at 16.7 M
79/// elements and a scalar at 16 MiB of payload.
80pub const DOC_COUNT_MAX: usize = (1 << 24) - 1;
81
82/// The flag bits of a value header.
83pub mod doc_flags {
84    /// Set on a container that is an array, clear on one that is an object.
85    pub const ARRAY: u32 = 1 << 3;
86    /// Set on an object whose members are in key order, which is every object
87    /// this version writes.
88    pub const SORTED: u32 = 1 << 4;
89    /// Set on a container whose entry table carries offsets, which is every
90    /// container this version writes.
91    pub const OFFSETS: u32 = 1 << 5;
92    /// Set on an object whose keys are two byte ids from the collection's key
93    /// table rather than bytes in a key region.
94    pub const INTERNED: u32 = 1 << 6;
95}
96
97/// What the low three bits of a value header say the value is.
98///
99/// The numbers are the format, so they are written out rather than derived from
100/// declaration order, and 2 is missing on purpose: false and true are 1 and 3 so
101/// that the low bit of a boolean is the boolean.
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103#[repr(u8)]
104pub enum ValueTag {
105    /// `null`.
106    Null = 0,
107    /// `false`.
108    False = 1,
109    /// `true`.
110    True = 3,
111    /// A signed integer, stored in one, two, four or eight bytes.
112    Int = 4,
113    /// A 64 bit float.
114    Float = 5,
115    /// A UTF-8 string.
116    Text = 6,
117    /// An object or an array, told apart by [`doc_flags::ARRAY`].
118    Container = 7,
119}
120
121impl ValueTag {
122    /// The byte that stands for this tag.
123    #[must_use]
124    pub const fn as_u8(self) -> u8 {
125        self as u8
126    }
127
128    /// The tag for a byte, or `None` for the one value in the range this
129    /// version does not define.
130    #[must_use]
131    pub const fn from_u8(b: u8) -> Option<ValueTag> {
132        match b {
133            0 => Some(ValueTag::Null),
134            1 => Some(ValueTag::False),
135            3 => Some(ValueTag::True),
136            4 => Some(ValueTag::Int),
137            5 => Some(ValueTag::Float),
138            6 => Some(ValueTag::Text),
139            7 => Some(ValueTag::Container),
140            _ => None,
141        }
142    }
143
144    /// Whether this tag is a container rather than a scalar.
145    #[must_use]
146    pub const fn is_container(self) -> bool {
147        matches!(self, ValueTag::Container)
148    }
149}
150
151/// A document record's value, borrowed.
152///
153/// Decoding copies nothing. See the module note for what it checks.
154#[derive(Debug, Clone, Copy)]
155pub struct DocumentBody<'a> {
156    head: u32,
157    tag: ValueTag,
158    bytes: &'a [u8],
159}
160
161impl<'a> DocumentBody<'a> {
162    /// Reads a document record's value.
163    ///
164    /// # Errors
165    ///
166    /// [`Code::Corrupt`] if the value is shorter than a header, if the tag is
167    /// one this version does not define, or if it is a scalar whose payload is
168    /// not the length the header claims.
169    pub fn decode(value: &'a [u8]) -> Result<DocumentBody<'a>> {
170        if value.len() < DOC_HEADER_LEN {
171            return Err(Error::new(
172                Code::Corrupt,
173                "a document record is shorter than a value header",
174            )
175            .with_detail(format!("len={}", value.len())));
176        }
177        let head = get_u32(value, 0);
178        let Some(tag) = ValueTag::from_u8((head & 0b111) as u8) else {
179            return Err(
180                Error::new(Code::Corrupt, "a document record has an unknown tag")
181                    .with_detail(format!("head={head:#010x}")),
182            );
183        };
184        let count = (head >> DOC_COUNT_SHIFT) as usize;
185        if !tag.is_container() {
186            // A scalar is the header and its payload and nothing else, so its
187            // length is knowable here and a short one is worth catching before
188            // anybody reads eight bytes out of a four byte record.
189            let want = DOC_HEADER_LEN + count;
190            if value.len() != want {
191                return Err(Error::new(
192                    Code::Corrupt,
193                    "a scalar document is not the length its header says",
194                )
195                .with_detail(format!("len={} want={want}", value.len())));
196            }
197            let ok = match tag {
198                ValueTag::Null | ValueTag::False | ValueTag::True => count == 0,
199                ValueTag::Int => matches!(count, 1 | 2 | 4 | 8),
200                ValueTag::Float => count == 8,
201                ValueTag::Text => true,
202                ValueTag::Container => unreachable!("checked above"),
203            };
204            if !ok {
205                return Err(Error::new(
206                    Code::Corrupt,
207                    "a scalar document has a payload its type cannot have",
208                )
209                .with_detail(format!("tag={tag:?} payload={count}")));
210            }
211        }
212        Ok(DocumentBody {
213            head,
214            tag,
215            bytes: value,
216        })
217    }
218
219    /// The header word, for a reader that wants a flag this version has no name
220    /// for.
221    #[must_use]
222    pub fn head(self) -> u32 {
223        self.head
224    }
225
226    /// What the value is.
227    #[must_use]
228    pub fn tag(self) -> ValueTag {
229        self.tag
230    }
231
232    /// How many elements a container holds, or how many bytes of payload a
233    /// scalar has.
234    #[must_use]
235    pub fn count(self) -> usize {
236        (self.head >> DOC_COUNT_SHIFT) as usize
237    }
238
239    /// Whether this is an array. False for an object and for every scalar.
240    #[must_use]
241    pub fn is_array(self) -> bool {
242        self.tag.is_container() && self.head & doc_flags::ARRAY != 0
243    }
244
245    /// Whether this is an object whose keys are ids from the collection's key
246    /// table.
247    #[must_use]
248    pub fn is_interned(self) -> bool {
249        self.tag.is_container()
250            && self.head & doc_flags::ARRAY == 0
251            && self.head & doc_flags::INTERNED != 0
252    }
253
254    /// The value, which is the whole of the record's value.
255    #[must_use]
256    pub fn bytes(self) -> &'a [u8] {
257        self.bytes
258    }
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264
265    /// A header built the way a writer would, so the tests read as documents
266    /// rather than as hex.
267    fn head(tag: ValueTag, flags: u32, count: usize) -> [u8; 4] {
268        (u32::from(tag.as_u8()) | flags | ((count as u32) << DOC_COUNT_SHIFT)).to_le_bytes()
269    }
270
271    #[test]
272    fn a_scalar_is_its_header_and_its_payload() {
273        let mut v = head(ValueTag::Int, 0, 8).to_vec();
274        v.extend_from_slice(&41_920i64.to_le_bytes());
275        let d = DocumentBody::decode(&v).unwrap();
276        assert_eq!(d.tag(), ValueTag::Int);
277        assert_eq!(d.count(), 8);
278        assert!(!d.is_array());
279        assert!(!d.is_interned());
280        assert_eq!(d.bytes(), &v[..]);
281    }
282
283    #[test]
284    fn a_scalar_that_lost_bytes_is_corrupt() {
285        let mut v = head(ValueTag::Int, 0, 8).to_vec();
286        v.extend_from_slice(&41_920i64.to_le_bytes());
287        for cut in 1..=8 {
288            let short = &v[..v.len() - cut];
289            assert!(
290                DocumentBody::decode(short).is_err(),
291                "an int missing {cut} bytes was accepted"
292            );
293        }
294    }
295
296    #[test]
297    fn a_scalar_of_a_length_its_type_cannot_have_is_corrupt() {
298        // Three byte integers and four byte floats do not exist, and a header
299        // that claims one is a corruption that lands inside the length check
300        // rather than outside it.
301        let mut v = head(ValueTag::Int, 0, 3).to_vec();
302        v.extend_from_slice(&[1, 2, 3]);
303        assert!(DocumentBody::decode(&v).is_err());
304
305        let mut v = head(ValueTag::Float, 0, 4).to_vec();
306        v.extend_from_slice(&[1, 2, 3, 4]);
307        assert!(DocumentBody::decode(&v).is_err());
308
309        // And null carries nothing at all.
310        let mut v = head(ValueTag::Null, 0, 1).to_vec();
311        v.push(0);
312        assert!(DocumentBody::decode(&v).is_err());
313    }
314
315    #[test]
316    fn a_container_is_not_walked_here() {
317        // Nine bytes is not enough for an object of four members, and this
318        // still decodes, because how much room four members need is the
319        // layout's question and the layout lives in `yo-doc`.
320        let mut v = head(
321            ValueTag::Container,
322            doc_flags::OFFSETS | doc_flags::SORTED,
323            4,
324        )
325        .to_vec();
326        v.extend_from_slice(&[0; 5]);
327        let d = DocumentBody::decode(&v).unwrap();
328        assert_eq!(d.tag(), ValueTag::Container);
329        assert_eq!(d.count(), 4);
330        assert!(!d.is_array());
331    }
332
333    #[test]
334    fn an_array_and_an_interned_object_say_so() {
335        let v = head(
336            ValueTag::Container,
337            doc_flags::ARRAY | doc_flags::OFFSETS,
338            0,
339        )
340        .to_vec();
341        let d = DocumentBody::decode(&v).unwrap();
342        assert!(d.is_array());
343        assert!(!d.is_interned(), "an array has no keys to intern");
344
345        let v = head(
346            ValueTag::Container,
347            doc_flags::INTERNED | doc_flags::OFFSETS,
348            0,
349        )
350        .to_vec();
351        let d = DocumentBody::decode(&v).unwrap();
352        assert!(!d.is_array());
353        assert!(d.is_interned());
354    }
355
356    #[test]
357    fn an_unknown_tag_is_corrupt_rather_than_a_guess() {
358        // Two is the one value in the range this version does not define, and
359        // it is the one a later version would use first.
360        let v = 2u32.to_le_bytes().to_vec();
361        assert!(DocumentBody::decode(&v).is_err());
362        assert_eq!(ValueTag::from_u8(2), None);
363    }
364
365    #[test]
366    fn a_value_shorter_than_a_header_is_corrupt() {
367        for n in 0..DOC_HEADER_LEN {
368            assert!(DocumentBody::decode(&vec![0u8; n]).is_err(), "{n} bytes");
369        }
370    }
371
372    #[test]
373    fn every_tag_round_trips_through_its_byte() {
374        for tag in [
375            ValueTag::Null,
376            ValueTag::False,
377            ValueTag::True,
378            ValueTag::Int,
379            ValueTag::Float,
380            ValueTag::Text,
381            ValueTag::Container,
382        ] {
383            assert_eq!(ValueTag::from_u8(tag.as_u8()), Some(tag));
384        }
385    }
386}