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}