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