Skip to main content

yo_format/
record.rs

1//! The record, which is the unit of everything: a commit, a replay step, and a
2//! thing compaction either copies or drops.
3//!
4//! `06` section 2.1 for the layout, `06` section 3 for the ordering rule that
5//! makes the layout safe.
6//!
7//! Two details that the table in the specification does not make obvious, and
8//! that everything downstream depends on.
9//!
10//! **`len` is exact and the stride is aligned.** Records are eight byte aligned,
11//! but the alignment is padding between records rather than padding inside one.
12//! If `len` were rounded up, a value's length would be `len` minus the header
13//! minus the key minus somewhere between zero and seven, and nobody could say
14//! which. So `len` is the exact byte count and the next record starts at
15//! [`RecordRef::stride`] bytes later. A reader that walks by `len` instead of by
16//! `stride` desynchronises on the first odd sized value, which is why there is a
17//! test for exactly that.
18//!
19//! **The trailer, and why it is not optional.** `07` section 4 says a per
20//! record checksum lives in the record's own trailer for records marked
21//! durable, and the field table in `06` does not list one. This module
22//! reconciles them: [`record_flags::CHECKSUMMED`] is the mark, and when it is
23//! set the last four bytes of the record are a CRC32C over everything before
24//! them, `len` included.
25//!
26//! It started out as a real choice, on the reasoning that in `none` mode nobody
27//! reads the record back off a disk and four bytes on a sixty byte record is
28//! real money. That reasoning is wrong, and the way it is wrong is worth
29//! keeping written down, because it is the sort of thing that reads as
30//! reasonable right up until a fuzzer finds it.
31//!
32//! The flag lives in the record. So a single bit flip in the flags byte turns
33//! the check off, and with it off there is nothing left to notice that the bit
34//! flipped. Worse than undetected: clearing the bit also moves the trailer
35//! boundary, so those four checksum bytes become four bytes of value, and a
36//! reader hands back a value four bytes longer than the one that was stored and
37//! is confident about it. A self describing checksum cannot describe its own
38//! absence.
39//!
40//! So [`RecordRef::parse`] refuses a record with the bit clear rather than
41//! believing it, and [`RecordHeader::fill`] always sets it. The flag stays in
42//! the layout so a later version can define what an unchecksummed record means
43//! with a second bit that is itself covered by something.
44
45use crate::{align_up, get_u8, get_u16, get_u32, get_u64, put_u8, put_u16, put_u32, put_u64};
46use yo_common::{Code, Error, Result, crc32c};
47
48/// The header without a TTL: `len`, `kind`, `flags`, `klen`, `prev`.
49pub const HEADER_LEN: usize = 16;
50
51/// The header with a TTL, which adds eight bytes at offset 16.
52pub const HEADER_LEN_TTL: usize = 24;
53
54/// The trailer, when [`record_flags::CHECKSUMMED`] is set.
55pub const TRAILER_LEN: usize = 4;
56
57/// The largest key, because `klen` is a `u16`.
58///
59/// Redis allows 512 MB keys and we do not, deliberately. A key is a thing you
60/// look up by, it lives in the index bucket's neighbourhood, and 64 KiB is
61/// already three orders of magnitude past any key anyone has a reason to use.
62/// A caller that wants a 512 MB key wants a value.
63pub const MAX_KEY_LEN: usize = u16::MAX as usize;
64
65/// The `flags` byte at offset 5.
66pub mod record_flags {
67    /// The value is not here; the index entry points at the cold tier.
68    pub const TIERED: u8 = 1 << 0;
69    /// The value is compressed.
70    pub const COMPRESSED: u8 = 1 << 1;
71    /// An eight byte `ttl_ms` sits at offset 16, before the key.
72    pub const HAS_TTL: u8 = 1 << 2;
73    /// The collection this record belongs to has a shape tag in the catalogue.
74    pub const SHAPE_TAGGED: u8 = 1 << 3;
75    /// The last four bytes are a CRC32C over everything before them.
76    ///
77    /// Always set on anything this version writes, and a record with it clear
78    /// is rejected rather than read. See the note at the top of this module:
79    /// a checksum whose own presence is announced by an unprotected bit is not
80    /// a checksum.
81    pub const CHECKSUMMED: u8 = 1 << 4;
82}
83
84/// What a record holds. `06` section 2.1.
85///
86/// This is deliberately not the type a reader gets back. A version one reader
87/// must skip a `kind` it has never heard of rather than refuse the file (`07`
88/// section 9), so [`RecordRef`] keeps the raw byte and offers this as a
89/// question.
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91#[repr(u8)]
92pub enum RecordKind {
93    /// A plain string value.
94    String = 0,
95    /// One chunk of a collection, or of a value too large for a page.
96    CollectionChunk = 1,
97    /// A document.
98    Document = 2,
99    /// A vector.
100    Vector = 3,
101    /// A graph node.
102    GraphNode = 4,
103    /// A graph adjacency run.
104    GraphAdj = 5,
105    /// A checkpoint marker.
106    Checkpoint = 6,
107    /// A deletion.
108    Tombstone = 7,
109    /// An index change that replay applies directly instead of reinserting.
110    IndexDelta = 8,
111}
112
113impl RecordKind {
114    /// Every kind this version knows, in order.
115    pub const ALL: [RecordKind; 9] = [
116        RecordKind::String,
117        RecordKind::CollectionChunk,
118        RecordKind::Document,
119        RecordKind::Vector,
120        RecordKind::GraphNode,
121        RecordKind::GraphAdj,
122        RecordKind::Checkpoint,
123        RecordKind::Tombstone,
124        RecordKind::IndexDelta,
125    ];
126
127    /// The kind for a byte, or `None` if this version has not heard of it.
128    #[must_use]
129    pub const fn from_u8(b: u8) -> Option<RecordKind> {
130        match b {
131            0 => Some(RecordKind::String),
132            1 => Some(RecordKind::CollectionChunk),
133            2 => Some(RecordKind::Document),
134            3 => Some(RecordKind::Vector),
135            4 => Some(RecordKind::GraphNode),
136            5 => Some(RecordKind::GraphAdj),
137            6 => Some(RecordKind::Checkpoint),
138            7 => Some(RecordKind::Tombstone),
139            8 => Some(RecordKind::IndexDelta),
140            _ => None,
141        }
142    }
143
144    /// The byte.
145    #[must_use]
146    pub const fn as_u8(self) -> u8 {
147        self as u8
148    }
149
150    /// Whether a record of this kind carries a key.
151    ///
152    /// Chunks do not: they are addressed by the header record that lists them,
153    /// and repeating the key in every chunk of a large collection would be the
154    /// dominant cost of storing it.
155    #[must_use]
156    pub const fn carries_a_key(self) -> bool {
157        !matches!(self, RecordKind::CollectionChunk)
158    }
159}
160
161/// The bytes needed for a record with this shape.
162///
163/// # Errors
164///
165/// [`Code::Invalid`] if the key is longer than [`MAX_KEY_LEN`], or if the total
166/// would not fit in the `u32` that has to hold it.
167pub fn total_len(flags: u8, klen: usize, vlen: usize) -> Result<usize> {
168    if klen > MAX_KEY_LEN {
169        return Err(
170            Error::new(Code::Invalid, "the key is longer than 65535 bytes")
171                .with_detail(format!("klen={klen}")),
172        );
173    }
174    let n = header_len(flags) + klen + vlen + trailer_len(flags);
175    if n > u32::MAX as usize {
176        return Err(Error::new(
177            Code::Invalid,
178            "the record does not fit in a u32",
179        ));
180    }
181    Ok(n)
182}
183
184/// The header length implied by `flags`.
185#[inline]
186#[must_use]
187pub const fn header_len(flags: u8) -> usize {
188    if flags & record_flags::HAS_TTL != 0 {
189        HEADER_LEN_TTL
190    } else {
191        HEADER_LEN
192    }
193}
194
195/// The trailer length implied by `flags`.
196///
197/// Always [`TRAILER_LEN`] for anything this version will accept. The flag is
198/// still read, because a record with it clear has to be rejected rather than
199/// reinterpreted, and [`RecordRef::parse`] is where that happens. See the note
200/// on [`record_flags::CHECKSUMMED`] for why the flag cannot be allowed to mean
201/// what it says.
202#[inline]
203#[must_use]
204pub const fn trailer_len(flags: u8) -> usize {
205    if flags & record_flags::CHECKSUMMED != 0 {
206        TRAILER_LEN
207    } else {
208        0
209    }
210}
211
212/// A record about to be written.
213#[derive(Debug, Clone, Copy, PartialEq, Eq)]
214pub struct RecordHeader {
215    /// What the value is. A raw byte rather than a [`RecordKind`] so that a
216    /// future version can be written by a build that predates it, which is not
217    /// as strange as it sounds: compaction copies records it does not interpret.
218    pub kind: u8,
219    /// See [`record_flags`].
220    pub flags: u8,
221    /// The previous address in this key's chain, or 0.
222    pub prev: u64,
223    /// Unix milliseconds, meaningful only when [`record_flags::HAS_TTL`] is set.
224    pub ttl_ms: u64,
225}
226
227impl RecordHeader {
228    /// A header for a live value with no TTL, checksummed.
229    #[must_use]
230    pub const fn new(kind: RecordKind) -> RecordHeader {
231        RecordHeader {
232            kind: kind.as_u8(),
233            flags: record_flags::CHECKSUMMED,
234            prev: 0,
235            ttl_ms: 0,
236        }
237    }
238
239    /// Sets the TTL and the flag that says it is there.
240    #[must_use]
241    pub const fn with_ttl(mut self, unix_ms: u64) -> RecordHeader {
242        self.flags |= record_flags::HAS_TTL;
243        self.ttl_ms = unix_ms;
244        self
245    }
246
247    /// Chains this record to the previous version of the same key.
248    #[must_use]
249    pub const fn after(mut self, prev: u64) -> RecordHeader {
250        self.prev = prev;
251        self
252    }
253
254    /// Writes everything except `len`, and returns the exact record length.
255    ///
256    /// The first four bytes are left untouched, which is the point. `06`
257    /// section 3 requires `len` to be stored last with a release store, so that
258    /// a reader either sees a whole record or sees a zero and stops. Writing it
259    /// here would put a length in front of a body that is not there yet, and a
260    /// crash in between produces a record that claims bytes it does not have.
261    ///
262    /// The caller finishes with [`seal_len`], which is the release store's
263    /// payload. Ordering is the caller's job because this crate does not know
264    /// what memory the buffer is in.
265    ///
266    /// # Errors
267    ///
268    /// [`Code::Invalid`] for an oversized key, [`Code::Full`] if `buf` is too
269    /// small for the record.
270    pub fn fill(&self, buf: &mut [u8], key: &[u8], value: &[u8]) -> Result<usize> {
271        // Set rather than checked. A caller that built a header by hand and
272        // forgot the flag would otherwise write a record that this crate's own
273        // parser refuses, and failing at read time for a mistake made at write
274        // time is the worst place to put the error.
275        let flags = self.flags | record_flags::CHECKSUMMED;
276        let n = total_len(flags, key.len(), value.len())?;
277        if buf.len() < n {
278            return Err(
279                Error::new(Code::Full, "the record does not fit in the buffer")
280                    .with_detail(format!("need={n} have={}", buf.len())),
281            );
282        }
283        let h = header_len(flags);
284        put_u8(buf, 4, self.kind);
285        put_u8(buf, 5, flags);
286        put_u16(buf, 6, key.len() as u16);
287        put_u64(buf, 8, self.prev);
288        if flags & record_flags::HAS_TTL != 0 {
289            put_u64(buf, 16, self.ttl_ms);
290        }
291        buf[h..h + key.len()].copy_from_slice(key);
292        let v = h + key.len();
293        buf[v..v + value.len()].copy_from_slice(value);
294
295        // The `len` field is part of what the trailer covers, and it is not in
296        // the buffer yet, so it is fed in from the value that is about to go
297        // there. Doing it any other way means either writing `len` early, which
298        // breaks the ordering rule, or leaving the length out of the checksum,
299        // which leaves the one field a torn write is most likely to damage
300        // unprotected.
301        let c = crc32c(0, &(n as u32).to_le_bytes());
302        let c = crc32c(c, &buf[4..n - TRAILER_LEN]);
303        put_u32(buf, n - TRAILER_LEN, c);
304        Ok(n)
305    }
306}
307
308/// Stores `len`, which is what publishes a record.
309///
310/// Separate from [`RecordHeader::fill`] so that the release store in the log is
311/// visibly the last thing that happens. The caller does the fence.
312///
313/// # Panics
314///
315/// If `len` is zero, because zero is the end of log sentinel and a record of
316/// length zero would end the log at itself.
317pub fn seal_len(buf: &mut [u8], len: usize) {
318    assert!(len != 0, "zero is the end of log sentinel, not a length");
319    put_u32(buf, 0, len as u32);
320}
321
322/// A record read back out of a page.
323#[derive(Debug, Clone, Copy, PartialEq, Eq)]
324pub struct RecordRef<'a> {
325    /// The exact length, header and trailer included.
326    pub len: u32,
327    /// The raw kind byte. Use [`RecordRef::kind`] to ask what it means.
328    pub kind: u8,
329    /// See [`record_flags`].
330    pub flags: u8,
331    /// The previous address in this key's chain, or 0.
332    pub prev: u64,
333    /// Unix milliseconds, or `None` if the record has no TTL.
334    pub ttl_ms: Option<u64>,
335    /// The key, borrowed from the page.
336    pub key: &'a [u8],
337    /// The value, borrowed from the page.
338    pub value: &'a [u8],
339}
340
341impl<'a> RecordRef<'a> {
342    /// Parses the record at the front of `bytes`.
343    ///
344    /// `Ok(None)` means `len` was zero, which is the end of the log and not an
345    /// error. That is the normal way replay finishes (`06` section 4) and it is
346    /// also what a half written tail looks like, which is exactly the point:
347    /// the two are indistinguishable and both mean stop here.
348    ///
349    /// # Errors
350    ///
351    /// [`Code::Corrupt`] if the length is impossible, if the record runs past
352    /// the end of `bytes`, or if a checksummed record fails its checksum.
353    pub fn parse(bytes: &'a [u8]) -> Result<Option<RecordRef<'a>>> {
354        if bytes.len() < 4 {
355            return Ok(None);
356        }
357        let len = get_u32(bytes, 0) as usize;
358        if len == 0 {
359            return Ok(None);
360        }
361        let flags = get_u8(bytes, 5);
362        if flags & record_flags::CHECKSUMMED == 0 {
363            // Not "this record has no checksum". There is no such record, so
364            // this is a flipped bit in the flags byte, and it has to be caught
365            // here because clearing this particular bit is the one corruption
366            // the checksum cannot catch: it turns the checksum off.
367            return Err(
368                Error::new(Code::Corrupt, "a record with its checksum flag clear")
369                    .with_detail(format!("flags={flags:#04x}")),
370            );
371        }
372        let h = header_len(flags);
373        let t = trailer_len(flags);
374        let klen = get_u16(bytes, 6) as usize;
375
376        if len < h + klen + t {
377            return Err(
378                Error::new(Code::Corrupt, "the record is shorter than its own header")
379                    .with_detail(format!("len={len} header={h} klen={klen} trailer={t}")),
380            );
381        }
382        if len > bytes.len() {
383            // A tail that was cut mid record. `06` section 4 calls for
384            // truncating to the last good record, so this is corruption the
385            // caller is expected to handle rather than a reason to give up.
386            return Err(
387                Error::new(Code::Corrupt, "the record runs past the end of the page")
388                    .with_detail(format!("len={len} available={}", bytes.len())),
389            );
390        }
391
392        if flags & record_flags::CHECKSUMMED != 0 {
393            let want = get_u32(bytes, len - TRAILER_LEN);
394            let got = crc32c(0, &bytes[..len - TRAILER_LEN]);
395            if want != got {
396                return Err(Error::new(Code::Corrupt, "record checksum mismatch")
397                    .with_detail(format!("stored={want:#010x} computed={got:#010x}")));
398            }
399        }
400
401        let ttl_ms = if flags & record_flags::HAS_TTL != 0 {
402            Some(get_u64(bytes, 16))
403        } else {
404            None
405        };
406
407        Ok(Some(RecordRef {
408            len: len as u32,
409            kind: get_u8(bytes, 4),
410            flags,
411            prev: get_u64(bytes, 8),
412            ttl_ms,
413            key: &bytes[h..h + klen],
414            value: &bytes[h + klen..len - t],
415        }))
416    }
417
418    /// How far the next record is, which is [`RecordRef::len`] rounded up to
419    /// eight.
420    ///
421    /// Walk by this and not by `len`.
422    #[must_use]
423    pub fn stride(&self) -> usize {
424        align_up(self.len as usize)
425    }
426
427    /// The kind, if this version knows it.
428    ///
429    /// `None` is not an error. It means a newer writer put something here and
430    /// `len` says how far to jump to get past it.
431    #[must_use]
432    pub fn kind(&self) -> Option<RecordKind> {
433        RecordKind::from_u8(self.kind)
434    }
435
436    /// Whether this record deletes its key.
437    #[must_use]
438    pub fn is_tombstone(&self) -> bool {
439        self.kind == RecordKind::Tombstone.as_u8()
440    }
441
442    /// Whether the value is elsewhere and this record only points at it.
443    #[must_use]
444    pub fn is_tiered(&self) -> bool {
445        self.flags & record_flags::TIERED != 0
446    }
447}
448
449/// Walks the records in a page payload.
450///
451/// Stops at the first `len == 0`, and yields an error for the first record that
452/// does not parse, after which it stops too. Both are how replay ends, so
453/// neither is exceptional.
454pub struct RecordIter<'a> {
455    bytes: &'a [u8],
456    at: usize,
457    done: bool,
458}
459
460impl<'a> RecordIter<'a> {
461    /// Walks `bytes`, which is a page payload, not a whole page.
462    #[must_use]
463    pub const fn new(bytes: &'a [u8]) -> RecordIter<'a> {
464        RecordIter {
465            bytes,
466            at: 0,
467            done: false,
468        }
469    }
470
471    /// The offset the walk stopped at, which is what a truncation truncates to.
472    #[must_use]
473    pub const fn offset(&self) -> usize {
474        self.at
475    }
476}
477
478impl<'a> Iterator for RecordIter<'a> {
479    type Item = Result<RecordRef<'a>>;
480
481    fn next(&mut self) -> Option<Self::Item> {
482        if self.done || self.at >= self.bytes.len() {
483            return None;
484        }
485        match RecordRef::parse(&self.bytes[self.at..]) {
486            Ok(Some(r)) => {
487                self.at += r.stride();
488                Some(Ok(r))
489            }
490            Ok(None) => {
491                self.done = true;
492                None
493            }
494            Err(e) => {
495                self.done = true;
496                Some(Err(e))
497            }
498        }
499    }
500}
501
502#[cfg(test)]
503mod tests {
504    use super::*;
505    use crate::RECORD_ALIGN;
506
507    fn write(h: RecordHeader, key: &[u8], value: &[u8]) -> Vec<u8> {
508        let mut buf = vec![0u8; 4096];
509        let n = h.fill(&mut buf, key, value).unwrap();
510        seal_len(&mut buf, n);
511        buf.truncate(align_up(n));
512        buf
513    }
514
515    #[test]
516    fn a_record_round_trips() {
517        let h = RecordHeader::new(RecordKind::String).after(4096);
518        let buf = write(h, b"greeting", b"hello");
519        let r = RecordRef::parse(&buf).unwrap().unwrap();
520        assert_eq!(r.kind(), Some(RecordKind::String));
521        assert_eq!(r.key, b"greeting");
522        assert_eq!(r.value, b"hello");
523        assert_eq!(r.prev, 4096);
524        assert_eq!(r.ttl_ms, None);
525        assert!(!r.is_tombstone());
526    }
527
528    #[test]
529    fn every_field_lands_where_the_specification_says() {
530        let h = RecordHeader::new(RecordKind::Document)
531            .with_ttl(1_700_000_000_000)
532            .after(0x1122_3344_5566_7788);
533        let buf = write(h, b"k", b"v");
534        assert_eq!(get_u32(&buf, 0) as usize, HEADER_LEN_TTL + 1 + 1 + 4);
535        assert_eq!(get_u8(&buf, 4), 2, "document is kind 2");
536        assert_eq!(
537            get_u8(&buf, 5),
538            record_flags::CHECKSUMMED | record_flags::HAS_TTL
539        );
540        assert_eq!(get_u16(&buf, 6), 1);
541        assert_eq!(get_u64(&buf, 8), 0x1122_3344_5566_7788);
542        assert_eq!(get_u64(&buf, 16), 1_700_000_000_000);
543        assert_eq!(buf[24], b'k');
544        assert_eq!(buf[25], b'v');
545    }
546
547    #[test]
548    fn a_ttl_costs_eight_bytes_and_moves_the_key() {
549        let plain = write(RecordHeader::new(RecordKind::String), b"key", b"value");
550        let ttl = write(
551            RecordHeader::new(RecordKind::String).with_ttl(1),
552            b"key",
553            b"value",
554        );
555        assert_eq!(get_u32(&ttl, 0) - get_u32(&plain, 0), 8);
556        let r = RecordRef::parse(&ttl).unwrap().unwrap();
557        assert_eq!(r.ttl_ms, Some(1));
558        assert_eq!(r.key, b"key");
559        assert_eq!(r.value, b"value");
560    }
561
562    #[test]
563    fn a_zero_length_is_the_end_of_the_log_and_not_an_error() {
564        assert!(RecordRef::parse(&[0u8; 64]).unwrap().is_none());
565        assert!(RecordRef::parse(&[]).unwrap().is_none());
566        assert!(RecordRef::parse(&[1, 2, 3]).unwrap().is_none());
567    }
568
569    #[test]
570    fn len_is_exact_so_a_value_of_any_length_survives() {
571        // The failure this guards against is padding counted as value. Any odd
572        // length would show it, so every length from zero to thirty two is
573        // checked.
574        for n in 0..=32usize {
575            let value: Vec<u8> = (0..n).map(|i| i as u8).collect();
576            let buf = write(RecordHeader::new(RecordKind::String), b"k", &value);
577            let r = RecordRef::parse(&buf).unwrap().unwrap();
578            assert_eq!(r.value, &value[..], "value of {n} bytes came back wrong");
579            assert_eq!(r.value.len(), n);
580        }
581    }
582
583    #[test]
584    fn the_stride_is_aligned_even_when_the_length_is_not() {
585        let buf = write(RecordHeader::new(RecordKind::String), b"k", b"abc");
586        let r = RecordRef::parse(&buf).unwrap().unwrap();
587        assert_eq!(r.len as usize, HEADER_LEN + 1 + 3 + TRAILER_LEN);
588        assert_eq!(r.len % RECORD_ALIGN as u32, 0);
589
590        let buf = write(RecordHeader::new(RecordKind::String), b"k", b"ab");
591        let r = RecordRef::parse(&buf).unwrap().unwrap();
592        assert_eq!(r.len as usize, 23);
593        assert_eq!(r.stride(), 24, "the padding is between records, not inside");
594    }
595
596    #[test]
597    fn walking_a_page_by_stride_stays_in_step() {
598        // Odd sized values on purpose: walking by `len` desynchronises on the
599        // first one and this test is the reason `stride` exists.
600        let mut page = vec![0u8; 4096];
601        let mut at = 0usize;
602        let mut written = Vec::new();
603        for i in 0..40usize {
604            let key = format!("key{i}");
605            let value = vec![b'v'; i];
606            let h = RecordHeader::new(RecordKind::String).after(at as u64);
607            let n = h.fill(&mut page[at..], key.as_bytes(), &value).unwrap();
608            seal_len(&mut page[at..], n);
609            written.push((key, value));
610            at += align_up(n);
611        }
612
613        let got: Vec<_> = RecordIter::new(&page).map(|r| r.unwrap()).collect();
614        assert_eq!(got.len(), 40);
615        for (r, (key, value)) in got.iter().zip(&written) {
616            assert_eq!(r.key, key.as_bytes());
617            assert_eq!(r.value, &value[..]);
618        }
619    }
620
621    #[test]
622    fn the_iterator_reports_where_it_stopped() {
623        let mut page = vec![0u8; 512];
624        let h = RecordHeader::new(RecordKind::String);
625        let n = h.fill(&mut page, b"a", b"bb").unwrap();
626        seal_len(&mut page, n);
627        let mut it = RecordIter::new(&page);
628        assert!(it.next().is_some());
629        assert!(it.next().is_none());
630        assert_eq!(it.offset(), align_up(n), "the tail is here");
631    }
632
633    #[test]
634    fn a_flipped_bit_anywhere_in_a_checksummed_record_is_caught() {
635        let good = write(
636            RecordHeader::new(RecordKind::String).with_ttl(99),
637            b"the key",
638            b"the value, which is long enough to be worth checking",
639        );
640        let len = get_u32(&good, 0) as usize;
641        for i in 0..len {
642            let mut bad = good.clone();
643            bad[i] ^= 0x20;
644            let r = RecordRef::parse(&bad);
645            // A hit in `len` may shorten the record into the end of log
646            // sentinel or past the buffer. Any of those is fine. What is not
647            // fine is a record that parses and claims the damaged bytes are the
648            // value.
649            match r {
650                Err(_) => {}
651                Ok(None) => {}
652                Ok(Some(rec)) => panic!("byte {i} was not caught, got {rec:?}"),
653            }
654        }
655    }
656
657    /// A caller that builds a header by hand and forgets the flag gets the
658    /// checksum anyway. The alternative is a record that this crate's own parser
659    /// refuses, which turns a mistake made at write time into a failure at read
660    /// time, and that is the worst place to put it.
661    #[test]
662    fn a_header_written_without_the_checksum_flag_gets_one_anyway() {
663        let h = RecordHeader {
664            kind: RecordKind::String.as_u8(),
665            flags: 0,
666            prev: 0,
667            ttl_ms: 0,
668        };
669        let buf = write(h, b"k", b"v");
670        assert_eq!(get_u32(&buf, 0) as usize, HEADER_LEN + 2 + TRAILER_LEN);
671        assert_ne!(get_u8(&buf, 5) & record_flags::CHECKSUMMED, 0);
672        let r = RecordRef::parse(&buf).unwrap().unwrap();
673        assert_eq!(r.value, b"v");
674    }
675
676    /// Clearing the flag is the one corruption a checksum cannot catch, because
677    /// what it corrupts is the checksum itself. So the flag being clear is
678    /// treated as damage rather than as a record that chose not to have one.
679    #[test]
680    fn a_record_with_its_checksum_flag_cleared_is_corruption() {
681        let mut buf = write(RecordHeader::new(RecordKind::String), b"key", b"value");
682        buf[5] &= !record_flags::CHECKSUMMED;
683        let err = RecordRef::parse(&buf).unwrap_err();
684        assert_eq!(err.code(), Code::Corrupt);
685    }
686
687    #[test]
688    fn a_length_that_is_shorter_than_the_header_is_corruption() {
689        let mut buf = write(RecordHeader::new(RecordKind::String), b"key", b"value");
690        put_u32(&mut buf, 0, 12);
691        let err = RecordRef::parse(&buf).unwrap_err();
692        assert_eq!(err.code(), Code::Corrupt);
693        assert!(err.detail().unwrap().contains("len=12"));
694    }
695
696    #[test]
697    fn a_record_cut_off_by_a_torn_write_is_corruption() {
698        let buf = write(RecordHeader::new(RecordKind::String), b"key", b"value");
699        let err = RecordRef::parse(&buf[..8]).unwrap_err();
700        assert_eq!(err.code(), Code::Corrupt);
701        assert!(err.detail().unwrap().contains("available=8"));
702    }
703
704    #[test]
705    fn an_unknown_kind_is_skipped_rather_than_refused() {
706        // `07` section 9: a version one reader jumps a kind it does not know by
707        // `len` and carries on. If this ever starts failing, the format has
708        // stopped being forward compatible.
709        let h = RecordHeader {
710            kind: 200,
711            flags: record_flags::CHECKSUMMED,
712            prev: 0,
713            ttl_ms: 0,
714        };
715        let mut page = vec![0u8; 512];
716        let n = h.fill(&mut page, b"future", b"stuff").unwrap();
717        seal_len(&mut page, n);
718        let after = align_up(n);
719        let m = RecordHeader::new(RecordKind::String)
720            .fill(&mut page[after..], b"k", b"v")
721            .unwrap();
722        seal_len(&mut page[after..], m);
723
724        let got: Vec<_> = RecordIter::new(&page).map(|r| r.unwrap()).collect();
725        assert_eq!(got.len(), 2);
726        assert_eq!(got[0].kind(), None, "not a kind this version knows");
727        assert_eq!(got[0].key, b"future");
728        assert_eq!(got[1].kind(), Some(RecordKind::String));
729    }
730
731    #[test]
732    fn a_key_larger_than_a_u16_is_refused_rather_than_truncated() {
733        let key = vec![b'k'; MAX_KEY_LEN + 1];
734        let mut buf = vec![0u8; MAX_KEY_LEN + 64];
735        let err = RecordHeader::new(RecordKind::String)
736            .fill(&mut buf, &key, b"v")
737            .unwrap_err();
738        assert_eq!(err.code(), Code::Invalid);
739    }
740
741    #[test]
742    fn a_buffer_with_no_room_says_how_much_it_needed() {
743        let mut buf = [0u8; 8];
744        let err = RecordHeader::new(RecordKind::String)
745            .fill(&mut buf, b"key", b"value")
746            .unwrap_err();
747        assert_eq!(err.code(), Code::Full);
748        assert!(err.detail().unwrap().contains("have=8"));
749    }
750
751    #[test]
752    fn kinds_round_trip_and_chunks_have_no_key() {
753        for k in RecordKind::ALL {
754            assert_eq!(RecordKind::from_u8(k.as_u8()), Some(k));
755        }
756        assert_eq!(RecordKind::from_u8(9), None);
757        assert!(!RecordKind::CollectionChunk.carries_a_key());
758        assert!(RecordKind::String.carries_a_key());
759        assert!(RecordKind::Tombstone.carries_a_key());
760    }
761
762    #[test]
763    fn a_tombstone_is_a_record_with_no_value() {
764        let buf = write(RecordHeader::new(RecordKind::Tombstone), b"gone", b"");
765        let r = RecordRef::parse(&buf).unwrap().unwrap();
766        assert!(r.is_tombstone());
767        assert_eq!(r.value, b"");
768        assert_eq!(r.key, b"gone");
769    }
770}