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