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}