Skip to main content

yo_format/
lib.rs

1//! The byte layouts of a `.yo` file, and nothing else.
2//!
3//! This crate is the written form of `07-yo-file-format.md`. It knows how to
4//! turn each structure into bytes and how to read it back, and it knows nothing
5//! about files, memory, shards or the engine. That separation is deliberate:
6//! the engine and `yodb check` both encode with this crate, and the independent
7//! minimal reader deliberately does not, so a change here that is not also a
8//! change to the specification shows up as the two disagreeing.
9//!
10//! Rules that hold everywhere below.
11//!
12//! Little endian, unconditionally. A big endian machine byte swaps on the way
13//! in and on the way out, and pays for it, because a format whose byte order
14//! depends on the writer is not a format.
15//!
16//! Every checksum is CRC32C over the bytes named, with the checksum field
17//! itself read as zero. That way the check is the same computation whether you
18//! are about to write the bytes or have just read them.
19//!
20//! No allocation. Everything encodes into a caller supplied slice and decodes
21//! from a borrowed one. The engine calls these from inside the shard loop and
22//! the shard loop does not allocate.
23
24#![deny(missing_docs)]
25
26pub mod catalog;
27pub mod document;
28pub mod image;
29pub mod page;
30pub mod record;
31pub mod superblock;
32pub mod vector;
33
34pub use catalog::{Band, CatalogEntry, Model, ValueType};
35pub use document::{
36    DOC_COUNT_MAX, DOC_COUNT_SHIFT, DOC_HEADER_LEN, DocumentBody, ValueTag, doc_flags,
37};
38pub use image::{
39    Chain, IMAGE_HEADER_LEN, ImageHeader, POSTING_HEADER_LEN, PostingHeader, image_kind,
40};
41pub use page::{PAGE_HEADER_LEN, PageHeader};
42pub use record::{RecordHeader, RecordKind, RecordRef, record_flags};
43pub use superblock::{CheckpointEntry, Superblock, superblock_flags};
44pub use vector::{Element, VECTOR_HEADER_LEN, VectorBody};
45
46/// The sixteen bytes at offset zero of every `.yo` file.
47///
48/// Sixteen and not eight so that the human readable part survives a hex dump,
49/// and trailing NULs rather than spaces so that a C string comparison of the
50/// first twelve bytes does the right thing.
51pub const MAGIC: [u8; 16] = *b"tamndyo fmt001\0\0";
52
53/// The format this build writes.
54pub const FORMAT_VERSION: u32 = 1;
55
56/// The lowest reader version that can read what this build writes.
57///
58/// Section 9 of the format document is the whole policy: a change a version one
59/// reader can skip past does not move this, and a change it would misread does.
60/// It is not the same number as [`FORMAT_VERSION`] and conflating them is how a
61/// reader ends up refusing a file it could have read.
62pub const MIN_READER_VERSION: u32 = 1;
63
64// A build that writes files it cannot read is a build nobody should get. The
65// two numbers are equal today and the assertion exists for the day they are
66// not, because the mistake it catches is a one character edit.
67const _: () = assert!(MIN_READER_VERSION <= FORMAT_VERSION);
68
69/// A superblock slot, and therefore the offset of the second one.
70pub const SUPERBLOCK_LEN: usize = 16 * 1024;
71
72/// Where the data starts, which is after both superblock slots.
73pub const DATA_START: u64 = 2 * SUPERBLOCK_LEN as u64;
74
75/// The default segment size, and the size a file gets if nobody chooses.
76pub const DEFAULT_PAGE_SIZE: u32 = 16384;
77
78/// The smallest legal segment size.
79///
80/// Four kibibytes is the torn write unit the format assumes and nothing
81/// smaller would be a unit at all.
82pub const MIN_PAGE_SIZE: u32 = 4096;
83
84/// The largest legal segment size.
85pub const MAX_PAGE_SIZE: u32 = 65536;
86
87/// A log page is 32 MiB laid across contiguous segments.
88///
89/// F2's constant. Three resident pages is about 96 MiB of working memory, and
90/// the contiguity is what makes writing one page one submission rather than a
91/// scatter list.
92pub const LOG_PAGE_LEN: u64 = 32 * 1024 * 1024;
93
94/// Records are eight byte aligned, so every length rounds up to this.
95pub const RECORD_ALIGN: usize = 8;
96
97/// Is `n` a legal segment size?
98///
99/// Powers of two between [`MIN_PAGE_SIZE`] and [`MAX_PAGE_SIZE`]. Anything else
100/// is refused at creation rather than at the first write, because a file with a
101/// nonsensical segment size is not a file anyone can recover.
102#[must_use]
103pub const fn is_legal_page_size(n: u32) -> bool {
104    n.is_power_of_two() && n >= MIN_PAGE_SIZE && n <= MAX_PAGE_SIZE
105}
106
107/// `n` rounded up to the next multiple of [`RECORD_ALIGN`].
108#[inline]
109#[must_use]
110pub const fn align_up(n: usize) -> usize {
111    n.next_multiple_of(RECORD_ALIGN)
112}
113
114// ---------------------------------------------------------------------------
115// The little endian primitives every layout below is built from.
116//
117// These exist rather than `from_le_bytes` at each call site because each call
118// site would need its own slice indexing and its own panic, and there are about
119// two hundred of them.
120// ---------------------------------------------------------------------------
121
122/// Reads a `u8` at `off`, or 0 if the slice is too short.
123///
124/// Short reads return zero rather than panicking because every caller here has
125/// already checked the length of the whole structure, and a bounds check per
126/// field is a branch per field on the recovery path.
127#[inline]
128#[must_use]
129pub fn get_u8(b: &[u8], off: usize) -> u8 {
130    b.get(off).copied().unwrap_or(0)
131}
132
133/// Reads a little endian `u16` at `off`, or 0 if the slice is too short.
134#[inline]
135#[must_use]
136pub fn get_u16(b: &[u8], off: usize) -> u16 {
137    match b.get(off..off + 2) {
138        Some(s) => u16::from_le_bytes([s[0], s[1]]),
139        None => 0,
140    }
141}
142
143/// Reads a little endian `u32` at `off`, or 0 if the slice is too short.
144#[inline]
145#[must_use]
146pub fn get_u32(b: &[u8], off: usize) -> u32 {
147    match b.get(off..off + 4) {
148        Some(s) => u32::from_le_bytes([s[0], s[1], s[2], s[3]]),
149        None => 0,
150    }
151}
152
153/// Reads a little endian `u64` at `off`, or 0 if the slice is too short.
154#[inline]
155#[must_use]
156pub fn get_u64(b: &[u8], off: usize) -> u64 {
157    match b.get(off..off + 8) {
158        Some(s) => u64::from_le_bytes([s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7]]),
159        None => 0,
160    }
161}
162
163/// Reads a little endian `f32` at `off`, or 0 if the slice is too short.
164///
165/// Bit for bit, through [`f32::from_le_bytes`], which is not the same as reading
166/// a number and converting it: a vector that came back nearly right would
167/// reorder search results for no reason anybody could find.
168#[inline]
169#[must_use]
170pub fn get_f32(b: &[u8], off: usize) -> f32 {
171    match b.get(off..off + 4) {
172        Some(s) => f32::from_le_bytes([s[0], s[1], s[2], s[3]]),
173        None => 0.0,
174    }
175}
176
177/// Writes `v` little endian at `off`. Does nothing if the slice is too short.
178#[inline]
179pub fn put_f32(b: &mut [u8], off: usize, v: f32) {
180    if let Some(s) = b.get_mut(off..off + 4) {
181        s.copy_from_slice(&v.to_le_bytes());
182    }
183}
184
185/// Writes `v` at `off`. Does nothing if the slice is too short.
186#[inline]
187pub fn put_u8(b: &mut [u8], off: usize, v: u8) {
188    if let Some(slot) = b.get_mut(off) {
189        *slot = v;
190    }
191}
192
193/// Writes `v` little endian at `off`. Does nothing if the slice is too short.
194#[inline]
195pub fn put_u16(b: &mut [u8], off: usize, v: u16) {
196    if let Some(s) = b.get_mut(off..off + 2) {
197        s.copy_from_slice(&v.to_le_bytes());
198    }
199}
200
201/// Writes `v` little endian at `off`. Does nothing if the slice is too short.
202#[inline]
203pub fn put_u32(b: &mut [u8], off: usize, v: u32) {
204    if let Some(s) = b.get_mut(off..off + 4) {
205        s.copy_from_slice(&v.to_le_bytes());
206    }
207}
208
209/// Writes `v` little endian at `off`. Does nothing if the slice is too short.
210#[inline]
211pub fn put_u64(b: &mut [u8], off: usize, v: u64) {
212    if let Some(s) = b.get_mut(off..off + 8) {
213        s.copy_from_slice(&v.to_le_bytes());
214    }
215}
216
217/// CRC32C over `bytes`, with the four bytes at `skip` treated as zero.
218///
219/// Every checksum in the format is defined this way, so it is one function
220/// rather than a convention each structure re-implements. The field is skipped
221/// rather than excluded so that the covered range stays contiguous and stays
222/// easy to state in the specification.
223#[must_use]
224pub fn checksum_skipping(bytes: &[u8], skip: usize) -> u32 {
225    if skip + 4 > bytes.len() {
226        return yo_common::crc32c(0, bytes);
227    }
228    let c = yo_common::crc32c(0, &bytes[..skip]);
229    let c = yo_common::crc32c(c, &[0, 0, 0, 0]);
230    yo_common::crc32c(c, &bytes[skip + 4..])
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236
237    #[test]
238    fn the_magic_is_what_the_specification_says() {
239        assert_eq!(MAGIC.len(), 16);
240        assert_eq!(&MAGIC[..14], b"tamndyo fmt001");
241        assert_eq!(&MAGIC[14..], b"\0\0");
242    }
243
244    #[test]
245    fn page_sizes_are_powers_of_two_in_range() {
246        assert!(is_legal_page_size(4096));
247        assert!(is_legal_page_size(16384));
248        assert!(is_legal_page_size(65536));
249        assert!(!is_legal_page_size(2048), "below the torn write unit");
250        assert!(!is_legal_page_size(131_072), "above the maximum");
251        assert!(!is_legal_page_size(12288), "not a power of two");
252        assert!(!is_legal_page_size(0));
253        assert!(is_legal_page_size(DEFAULT_PAGE_SIZE));
254    }
255
256    #[test]
257    fn a_log_page_is_a_whole_number_of_segments_at_every_legal_size() {
258        let mut n = MIN_PAGE_SIZE;
259        while n <= MAX_PAGE_SIZE {
260            assert_eq!(
261                LOG_PAGE_LEN % u64::from(n),
262                0,
263                "a 32 MiB log page must divide into {n} byte segments"
264            );
265            n *= 2;
266        }
267        assert_eq!(LOG_PAGE_LEN / u64::from(DEFAULT_PAGE_SIZE), 2048);
268    }
269
270    #[test]
271    fn alignment_rounds_up_and_leaves_aligned_values_alone() {
272        assert_eq!(align_up(0), 0);
273        assert_eq!(align_up(1), 8);
274        assert_eq!(align_up(8), 8);
275        assert_eq!(align_up(9), 16);
276        assert_eq!(align_up(RECORD_ALIGN * 3), RECORD_ALIGN * 3);
277    }
278
279    #[test]
280    fn data_starts_after_both_superblock_slots() {
281        assert_eq!(DATA_START, 32768);
282        assert_eq!(SUPERBLOCK_LEN, 16384);
283    }
284
285    #[test]
286    fn short_reads_give_zero_rather_than_panicking() {
287        let b = [1u8, 2, 3];
288        assert_eq!(get_u8(&b, 0), 1);
289        assert_eq!(get_u8(&b, 9), 0);
290        assert_eq!(get_u16(&b, 0), 0x0201);
291        assert_eq!(
292            get_u16(&b, 2),
293            0,
294            "would need two bytes and only one is left"
295        );
296        assert_eq!(get_u32(&b, 0), 0);
297        assert_eq!(get_u64(&b, 0), 0);
298    }
299
300    #[test]
301    fn short_writes_do_nothing_rather_than_panicking() {
302        let mut b = [0u8; 3];
303        put_u32(&mut b, 0, 0xdead_beef);
304        assert_eq!(b, [0, 0, 0], "no room, so nothing was written");
305        put_u16(&mut b, 0, 0x1234);
306        assert_eq!(b, [0x34, 0x12, 0]);
307    }
308
309    #[test]
310    fn a_float_goes_down_and_comes_back_bit_for_bit() {
311        let mut b = [0u8; 4];
312        for v in [0.0f32, -0.0, 1.0, -1.0, 1e-38, 3.4e38, 0.1] {
313            put_f32(&mut b, 0, v);
314            assert_eq!(
315                get_f32(&b, 0).to_bits(),
316                v.to_bits(),
317                "{v} came back changed"
318            );
319        }
320        assert_eq!(get_f32(&b, 2), 0.0, "two bytes are not a float");
321    }
322
323    #[test]
324    fn round_trips_are_little_endian_on_every_machine() {
325        let mut b = [0u8; 8];
326        put_u64(&mut b, 0, 0x0102_0304_0506_0708);
327        assert_eq!(b, [8, 7, 6, 5, 4, 3, 2, 1], "little endian, byte for byte");
328        assert_eq!(get_u64(&b, 0), 0x0102_0304_0506_0708);
329    }
330
331    #[test]
332    fn the_checksum_reads_its_own_field_as_zero() {
333        let mut b = vec![0u8; 32];
334        for (i, slot) in b.iter_mut().enumerate() {
335            *slot = i as u8;
336        }
337        let want = checksum_skipping(&b, 28);
338        // Whatever ends up in the field, the answer is the same, which is what
339        // makes "compute then store" and "read then verify" one computation.
340        put_u32(&mut b, 28, want);
341        assert_eq!(checksum_skipping(&b, 28), want);
342        put_u32(&mut b, 28, 0xffff_ffff);
343        assert_eq!(checksum_skipping(&b, 28), want);
344        // And a change anywhere else does move it.
345        b[3] ^= 1;
346        assert_ne!(checksum_skipping(&b, 28), want);
347    }
348}