Skip to main content

yo_kv/
value.rs

1//! How a string value sits in a record, and what comes back out of one.
2//!
3//! The map underneath stores opaque bytes against a key. Everything a string
4//! needs beyond those bytes, which is its encoding and its expiry deadline, is
5//! carried in a one byte header in front of them:
6//!
7//! ```text
8//! +--------+-------------------------+-------------------+-----------+
9//! | meta   | expire at, u64 LE       | access, 24 bits   | payload   |
10//! | u8     | 8 bytes, only if tagged | 3 bytes, tagged   | see below |
11//! +--------+-------------------------+-------------------+-----------+
12//! ```
13//!
14//! One byte, and eight more only for a key that has a deadline, which most keys
15//! do not. The alternative is a second lookup into a side table for the TTL, and
16//! a second lookup is a second cache miss on a path whose whole budget is one.
17//!
18//! # The access field
19//!
20//! Three bytes saying when the key was last read, or how often, depending on
21//! which eviction policy is in force. It is [`crate::access::Access`] and the
22//! reasoning about what goes in it lives there.
23//!
24//! It is behind a tag bit the same way the deadline is, but unlike the deadline
25//! every record written now has one. The bit is there so that a record written
26//! before the field existed still reads back correctly rather than to make the
27//! field optional, and it sits after the deadline for the same reason: the
28//! deadline stays at offset one and everything that reads one goes on working.
29//!
30//! Three bytes on every key is a real cost and it is worth being straight about
31//! why it is paid unconditionally rather than only under a policy that reads it.
32//! A key written under `noeviction` and then read under `allkeys-lru` has to be
33//! rankable, and it cannot become rankable later without the record growing,
34//! which means moving it, on what is usually a read. Paying three bytes always
35//! is the version where switching policy at runtime does the obvious thing.
36//!
37//! The payload depends on the encoding. An `int` holds the eight bytes of the
38//! integer and not its digits, which is what makes `INCR` a probe, an add and a
39//! store with no arena traffic at all (`08` section 2). An `embstr` and a `raw`
40//! hold the bytes as given. The difference between those two is the name
41//! `OBJECT ENCODING` reports and nothing else, which is also true in Redis:
42//! `embstr` there means the value was allocated next to its object header, a
43//! distinction `yo` does not have because every value is already next to its
44//! key.
45//!
46//! # The type tag
47//!
48//! The meta byte also says which type the key holds, in three bits that were
49//! spare. That is what `TYPE` reads and it is what a command will read before it
50//! decides whether it is looking at its own type or at somebody else's, and both
51//! of those want the answer to come out of the byte the lookup already fetched
52//! rather than out of a second structure. A string is zero, so nothing written
53//! before the tag existed reads back as anything else.
54
55use crate::access::Access;
56use yo_common::num::{parse_i64, push_i64};
57
58/// The longest value Redis calls `embstr` rather than `raw`.
59///
60/// Clients and test suites read `OBJECT ENCODING` and assert on the boundary,
61/// so it is 44 here because it is 44 there (`12` section 2).
62pub const EMBSTR_MAX: usize = 44;
63
64/// What `OBJECT ENCODING` calls a string.
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum Encoding {
67    /// The value is an integer, held as an integer.
68    Int,
69    /// A short string, at or under [`EMBSTR_MAX`] bytes.
70    Embstr,
71    /// Everything else.
72    Raw,
73}
74
75impl Encoding {
76    /// The string `OBJECT ENCODING` returns.
77    #[inline]
78    pub const fn name(self) -> &'static str {
79        match self {
80            Encoding::Int => "int",
81            Encoding::Embstr => "embstr",
82            Encoding::Raw => "raw",
83        }
84    }
85
86    /// The encoding Redis would choose for these bytes.
87    ///
88    /// Integer first, because `SET k 42` is int encoded in Redis whatever the
89    /// length, then the `embstr` boundary. The integer test is Redis's own
90    /// `string2ll`, which refuses a leading zero, a leading plus and `-0`, so
91    /// `SET k 007` stays a three byte string and gives back `007`.
92    #[inline]
93    pub fn of(bytes: &[u8]) -> Encoding {
94        if parse_i64(bytes).is_some() {
95            Encoding::Int
96        } else if bytes.len() <= EMBSTR_MAX {
97            Encoding::Embstr
98        } else {
99            Encoding::Raw
100        }
101    }
102}
103
104/// What `TYPE` calls a key, and what the meta byte's tag holds.
105///
106/// The numbers are the same numbers `yo_format::ValueType` uses on disk, so that
107/// saving a key is a copy of the tag rather than a translation of it. There is a
108/// test at the bottom of this file holding the two in step, and it takes a dev
109/// dependency on `yo-format` for no other reason.
110///
111/// The list is shorter than the on disk one because some of those are the same
112/// thing in memory. A bitmap is a string and a HyperLogLog is a string, in Redis
113/// as much as here, and `TYPE` on either answers `string`. The catalog draws
114/// finer lines because a reader wants to know what a blob meant.
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116pub enum Kind {
117    /// A string, and everything Redis stores as one.
118    String = 0,
119    /// A hash.
120    Hash = 1,
121    /// A set.
122    Set = 2,
123    /// A sorted set.
124    Zset = 3,
125    /// A list.
126    List = 4,
127    /// A stream.
128    Stream = 5,
129    /// A sparse array.
130    Array = 6,
131}
132
133impl Kind {
134    /// The word `TYPE` replies with.
135    #[inline]
136    pub const fn name(self) -> &'static str {
137        match self {
138            Kind::String => "string",
139            Kind::Hash => "hash",
140            Kind::Set => "set",
141            Kind::Zset => "zset",
142            Kind::List => "list",
143            Kind::Stream => "stream",
144            Kind::Array => "array",
145        }
146    }
147
148    /// The kind for a three bit tag.
149    ///
150    /// Seven of the eight patterns are spoken for. The last one cannot come out
151    /// of our own writer, and it falls to `String` for the same reason an
152    /// unknown encoding falls to `raw`: it is the reading that hands the bytes
153    /// back rather than the one that reinterprets them.
154    #[inline]
155    const fn from_bits(bits: u8) -> Kind {
156        match bits {
157            KIND_HASH => Kind::Hash,
158            KIND_SET => Kind::Set,
159            KIND_ZSET => Kind::Zset,
160            KIND_LIST => Kind::List,
161            KIND_STREAM => Kind::Stream,
162            KIND_ARRAY => Kind::Array,
163            _ => Kind::String,
164        }
165    }
166}
167
168/// Bits 0 and 1 of the meta byte: which encoding.
169const ENC_MASK: u8 = 0b0000_0011;
170const ENC_INT: u8 = 0;
171const ENC_EMBSTR: u8 = 1;
172const ENC_RAW: u8 = 2;
173/// Bit 2: whether eight bytes of deadline follow the meta byte.
174const HAS_EXPIRY: u8 = 0b0000_0100;
175/// Bit 6: whether three bytes of access data follow the deadline.
176///
177/// Everything this crate writes now sets it, and the bit exists so that a file
178/// written before it did still opens. A record with the bit clear has no access
179/// field and its payload starts where it always did, which is what makes reading
180/// an older file a matter of asking rather than of knowing which version wrote
181/// it.
182///
183/// It does not work in the other direction. A binary from before this bit reads
184/// a record that sets it, ignores the bit it does not know about, and takes the
185/// three access bytes for the front of the payload. There is no format version
186/// in the file to refuse on, which is worth fixing and is not this change.
187const HAS_ACCESS: u8 = 0b0100_0000;
188/// Bits 3, 4 and 5: which type the key holds. Bit 7 is still spare.
189///
190/// String is zero, so every record written before the tag existed reads back as
191/// a string, which is what it was.
192const KIND_MASK: u8 = 0b0011_1000;
193const KIND_SHIFT: u32 = 3;
194const KIND_HASH: u8 = 1;
195const KIND_SET: u8 = 2;
196const KIND_ZSET: u8 = 3;
197const KIND_LIST: u8 = 4;
198const KIND_STREAM: u8 = 5;
199const KIND_ARRAY: u8 = 6;
200
201/// Bytes of integer payload, which is a whole `i64` and never its digits.
202const INT_LEN: usize = 8;
203
204/// Bytes of access data, which is [`Access`] and is twenty four bits.
205const ACCESS_LEN: usize = 3;
206
207/// The meta byte in front of every stored string.
208#[derive(Debug, Clone, Copy, PartialEq, Eq)]
209pub struct Meta(u8);
210
211impl Meta {
212    /// Build the byte for a type, an encoding and the presence of a deadline.
213    #[inline]
214    pub const fn new(kind: Kind, enc: Encoding, has_expiry: bool) -> Meta {
215        let bits = match enc {
216            Encoding::Int => ENC_INT,
217            Encoding::Embstr => ENC_EMBSTR,
218            Encoding::Raw => ENC_RAW,
219        };
220        Meta(bits | ((kind as u8) << KIND_SHIFT) | if has_expiry { HAS_EXPIRY } else { 0 })
221    }
222
223    /// The byte for a string, which is what everything in `strings.rs` writes.
224    #[inline]
225    pub const fn string(enc: Encoding, has_expiry: bool) -> Meta {
226        Meta::new(Kind::String, enc, has_expiry)
227    }
228
229    /// The byte for a value that lives in a slab, with the record holding a
230    /// number that says where.
231    ///
232    /// The encoding bits are written as zero and mean nothing here. A set's
233    /// encoding is which of the three representations it is in, and that is a
234    /// property of the body and not of the record, so `OBJECT ENCODING` follows
235    /// the number and asks. Keeping a copy of it in these two bits would want
236    /// the record rewritten every time a set was promoted, for a command nobody
237    /// calls in a loop, and two places to disagree about the same fact.
238    #[inline]
239    pub const fn slot(kind: Kind, has_expiry: bool) -> Meta {
240        Meta::new(kind, Encoding::Int, has_expiry)
241    }
242
243    /// Read the byte back.
244    ///
245    /// An unknown encoding is impossible from our own writer, so the two spare
246    /// bit patterns fall to `raw`, which is the reading that returns the bytes
247    /// unchanged rather than reinterpreting them as something else.
248    #[inline]
249    pub const fn from_byte(b: u8) -> Meta {
250        Meta(b)
251    }
252
253    /// The raw byte, as stored.
254    #[inline]
255    pub const fn byte(self) -> u8 {
256        self.0
257    }
258
259    /// Which encoding this value carries.
260    #[inline]
261    pub const fn encoding(self) -> Encoding {
262        match self.0 & ENC_MASK {
263            ENC_INT => Encoding::Int,
264            ENC_EMBSTR => Encoding::Embstr,
265            _ => Encoding::Raw,
266        }
267    }
268
269    /// Which type this key holds.
270    #[inline]
271    pub const fn kind(self) -> Kind {
272        Kind::from_bits((self.0 & KIND_MASK) >> KIND_SHIFT)
273    }
274
275    /// Whether a deadline follows.
276    #[inline]
277    pub const fn has_expiry(self) -> bool {
278        self.0 & HAS_EXPIRY != 0
279    }
280
281    /// Whether an access field follows the deadline.
282    ///
283    /// Everything this crate writes sets it, so in a running server it is always
284    /// true and the reader that checks it is checking something that cannot
285    /// happen. It is here anyway because a bit in the byte costs nothing and the
286    /// alternative was a flag day: without it, the day the field arrived, every
287    /// reader had to agree with every writer at exactly the same moment.
288    ///
289    /// It is not a file format concern. These records live in the arena and
290    /// never reach a file, and the on disk record in `yo_format` has its own
291    /// layout and its own versioning.
292    #[inline]
293    pub const fn has_access(self) -> bool {
294        self.0 & HAS_ACCESS != 0
295    }
296
297    /// The same byte with the access field declared.
298    #[inline]
299    const fn with_access(self) -> Meta {
300        Meta(self.0 | HAS_ACCESS)
301    }
302
303    /// Where the access field starts, counting from the meta byte.
304    ///
305    /// After the deadline rather than before it, which is what keeps a record
306    /// written before this field existed readable: the deadline is still at
307    /// offset one and everything that reads one can go on doing so.
308    #[inline]
309    const fn access_at(self) -> usize {
310        if self.has_expiry() { 1 + 8 } else { 1 }
311    }
312
313    /// Where the payload starts, counting from the meta byte.
314    #[inline]
315    pub const fn payload_at(self) -> usize {
316        self.access_at() + if self.has_access() { ACCESS_LEN } else { 0 }
317    }
318}
319
320/// A stored value, read back out of a record.
321///
322/// The point of the two arms is that neither of them copies. An integer comes
323/// back as an integer and is written into the reply buffer as digits at the
324/// moment the reply is built, and a string comes back as a slice of the record
325/// it lives in. Y18 asks for the reply to be constructed once in wire form, and
326/// a `Vec<u8>` in the middle of that is the thing it is asking to remove.
327#[derive(Debug, Clone, Copy, PartialEq, Eq)]
328pub enum Str<'a> {
329    /// An int encoded value.
330    Int(i64),
331    /// Everything else, as it lies in the record.
332    Bytes(&'a [u8]),
333}
334
335impl Str<'_> {
336    /// How many bytes this value is as a string, which is what `STRLEN` returns.
337    #[inline]
338    pub fn len(&self) -> usize {
339        match self {
340            Str::Int(n) => yo_common::num::i64_len(*n),
341            Str::Bytes(b) => b.len(),
342        }
343    }
344
345    /// Whether the value is the empty string.
346    #[inline]
347    pub fn is_empty(&self) -> bool {
348        match self {
349            // No integer writes as no digits.
350            Str::Int(_) => false,
351            Str::Bytes(b) => b.is_empty(),
352        }
353    }
354
355    /// Append the string form to a buffer, which for an integer is its digits.
356    #[inline]
357    pub fn write_to(&self, out: &mut Vec<u8>) {
358        match self {
359            Str::Int(n) => push_i64(out, *n),
360            Str::Bytes(b) => out.extend_from_slice(b),
361        }
362    }
363
364    /// The string form, copied. For the reply path prefer [`Str::write_to`].
365    pub fn to_vec(&self) -> Vec<u8> {
366        let mut v = Vec::with_capacity(self.len());
367        self.write_to(&mut v);
368        v
369    }
370
371    /// The integer this value is, if it is one.
372    ///
373    /// A value can be an integer without being int encoded: `APPEND` and
374    /// `SETRANGE` leave a `raw` string behind, and `INCR` on `"10"` built that
375    /// way is 11 in Redis. So the bytes are parsed rather than the encoding
376    /// being trusted.
377    #[inline]
378    pub fn as_int(&self) -> Option<i64> {
379        match self {
380            Str::Int(n) => Some(*n),
381            Str::Bytes(b) => parse_i64(b),
382        }
383    }
384
385    /// The XXH3 of the value's string form, which is Redis's `DIGEST`.
386    ///
387    /// An int encoded value hashes its digits and not the eight bytes the
388    /// record holds, because the digest a client compares against is the digest
389    /// of what a client would have read.
390    #[must_use]
391    pub fn digest(&self) -> u64 {
392        match self {
393            Str::Bytes(b) => yo_common::xxh3::hash64(b),
394            // Twenty bytes at the most, and the alternative is a second
395            // formatter that writes into a stack buffer for a path nobody calls
396            // in a loop.
397            Str::Int(_) => yo_common::xxh3::hash64(&self.to_vec()),
398        }
399    }
400
401    /// Whether this value's string form is exactly `want`.
402    ///
403    /// `IFEQ` compares against what the client would have read, so an int
404    /// encoded 42 is equal to `"42"` and not to `"042"`. Doing that without
405    /// materialising the digits is why the integer arm exists.
406    #[inline]
407    pub(crate) fn eq_bytes(&self, want: &[u8]) -> bool {
408        match self {
409            Str::Bytes(b) => *b == want,
410            Str::Int(n) => parse_i64(want) == Some(*n),
411        }
412    }
413}
414
415/// How many bytes a record holding this value will occupy.
416///
417/// The access field is counted unconditionally, because every record this crate
418/// writes now carries one. It is not a parameter for that reason: making it one
419/// would put a flag through twenty six call sites to describe something none of
420/// them gets to decide.
421#[inline]
422pub fn record_len(enc: Encoding, payload: usize, has_expiry: bool) -> usize {
423    let head = (if has_expiry { 1 + 8 } else { 1 }) + ACCESS_LEN;
424    head + if enc == Encoding::Int {
425        INT_LEN
426    } else {
427        payload
428    }
429}
430
431/// Write a whole record into `out`, which must be exactly [`record_len`] long.
432///
433/// `bytes` is the string as the caller gave it. When `enc` is [`Encoding::Int`]
434/// the digits are not stored, the integer they parse to is, and the caller has
435/// already established that they parse by choosing that encoding.
436#[inline]
437pub fn write_record(out: &mut [u8], enc: Encoding, bytes: &[u8], expire_at: Option<u64>) {
438    out[0] = Meta::string(enc, expire_at.is_some()).with_access().byte();
439    let mut at = 1;
440    if let Some(ms) = expire_at {
441        out[at..at + 8].copy_from_slice(&ms.to_le_bytes());
442        at += 8;
443    }
444    at += write_blank_access(&mut out[at..]);
445    match enc {
446        Encoding::Int => {
447            let n =
448                parse_i64(bytes).expect("int encoding was chosen for bytes that are not an int");
449            out[at..at + INT_LEN].copy_from_slice(&n.to_le_bytes());
450        }
451        _ => out[at..].copy_from_slice(bytes),
452    }
453}
454
455/// Write a record whose value is an integer the caller already has.
456#[inline]
457pub fn write_int_record(out: &mut [u8], n: i64, expire_at: Option<u64>) {
458    out[0] = Meta::string(Encoding::Int, expire_at.is_some())
459        .with_access()
460        .byte();
461    let mut at = 1;
462    if let Some(ms) = expire_at {
463        out[at..at + 8].copy_from_slice(&ms.to_le_bytes());
464        at += 8;
465    }
466    at += write_blank_access(&mut out[at..]);
467    out[at..at + INT_LEN].copy_from_slice(&n.to_le_bytes());
468}
469
470/// Leave room for the access field and put nothing in it.
471///
472/// The writers here do not know the clock or which policy is in force, and a
473/// record layout is the wrong place to learn either. The keyspace stamps the
474/// field through [`set_access`] once the record is in, which is also where the
475/// decision about whether to stamp at all belongs.
476///
477/// Zero is the most evictable value a key can hold under either reading, which
478/// is the right way round for a default: a key that somehow never got stamped
479/// goes first rather than never.
480#[inline]
481fn write_blank_access(out: &mut [u8]) -> usize {
482    out[..ACCESS_LEN].fill(0);
483    ACCESS_LEN
484}
485
486/// Bytes of slab number, which is how a record points at a body.
487const SLOT_LEN: usize = 4;
488
489/// How many bytes a record pointing at a slab slot occupies.
490#[inline]
491pub fn slot_record_len(has_expiry: bool) -> usize {
492    (if has_expiry { 1 + 8 } else { 1 }) + ACCESS_LEN + SLOT_LEN
493}
494
495/// Write a record that points at `slot` in the slab for `kind`.
496///
497/// `out` must be exactly [`slot_record_len`] long.
498#[inline]
499pub fn write_slot_record(out: &mut [u8], kind: Kind, slot: u32, expire_at: Option<u64>) {
500    out[0] = Meta::slot(kind, expire_at.is_some()).with_access().byte();
501    let mut at = 1;
502    if let Some(ms) = expire_at {
503        out[at..at + 8].copy_from_slice(&ms.to_le_bytes());
504        at += 8;
505    }
506    at += write_blank_access(&mut out[at..]);
507    out[at..at + SLOT_LEN].copy_from_slice(&slot.to_le_bytes());
508}
509
510/// What the access field in a record says, or `None` if it has no room for one.
511///
512/// `None` means the record predates the field, which is a key that was written
513/// by an older build and read back out of a file. It is not an error and the
514/// caller should treat it as a key it knows nothing about rather than as a key
515/// that has never been touched.
516#[inline]
517#[must_use]
518pub fn access(rec: &[u8]) -> Option<Access> {
519    let m = Meta::from_byte(rec[0]);
520    if !m.has_access() {
521        return None;
522    }
523    let at = m.access_at();
524    Some(Access::from_bits(u32::from_le_bytes([
525        rec[at],
526        rec[at + 1],
527        rec[at + 2],
528        0,
529    ])))
530}
531
532/// Stamp the access field, in place, over whatever was there.
533///
534/// Returns false for a record with no room, which is the same older record
535/// [`access`] answers `None` for. It is not worth growing one to make room: the
536/// record would have to move, on a path that is usually a read, and the next
537/// write to that key rewrites it with a field anyway.
538#[inline]
539pub fn set_access(rec: &mut [u8], a: Access) -> bool {
540    let m = Meta::from_byte(rec[0]);
541    if !m.has_access() {
542        return false;
543    }
544    let at = m.access_at();
545    rec[at..at + ACCESS_LEN].copy_from_slice(&a.bits().to_le_bytes()[..ACCESS_LEN]);
546    true
547}
548
549/// The slab number in a record that has one.
550///
551/// # Panics
552///
553/// If the record is not one [`write_slot_record`] wrote, which is a caller that
554/// did not read the kind first.
555#[inline]
556pub fn slot(rec: &[u8]) -> u32 {
557    let at = Meta::from_byte(rec[0]).payload_at();
558    let mut b = [0u8; SLOT_LEN];
559    b.copy_from_slice(&rec[at..at + SLOT_LEN]);
560    u32::from_le_bytes(b)
561}
562
563/// The type a record holds.
564#[inline]
565pub fn kind(rec: &[u8]) -> Kind {
566    Meta::from_byte(rec[0]).kind()
567}
568
569/// The deadline in a record, if it has one.
570#[inline]
571pub fn expire_at(rec: &[u8]) -> Option<u64> {
572    let m = Meta::from_byte(rec[0]);
573    if !m.has_expiry() {
574        return None;
575    }
576    let mut b = [0u8; 8];
577    b.copy_from_slice(&rec[1..9]);
578    Some(u64::from_le_bytes(b))
579}
580
581/// Whether a record's deadline has passed at `now_ms`.
582///
583/// A deadline exactly equal to now has passed, which is Redis's reading: a key
584/// set to expire at time T is gone at time T.
585#[inline]
586pub fn is_expired(rec: &[u8], now_ms: u64) -> bool {
587    match expire_at(rec) {
588        Some(at) => at <= now_ms,
589        None => false,
590    }
591}
592
593/// The value in a record.
594#[inline]
595pub fn read(rec: &[u8]) -> Str<'_> {
596    let m = Meta::from_byte(rec[0]);
597    let at = m.payload_at();
598    match m.encoding() {
599        Encoding::Int => {
600            let mut b = [0u8; INT_LEN];
601            b.copy_from_slice(&rec[at..at + INT_LEN]);
602            Str::Int(i64::from_le_bytes(b))
603        }
604        _ => Str::Bytes(&rec[at..]),
605    }
606}
607
608/// The integer in an int encoded record, and where its bytes start.
609///
610/// Returns `None` for any other encoding. This is the read half of `INCR`'s
611/// fast path, and the offset it hands back is what the write half stores into.
612#[inline]
613pub fn read_int_in_place(rec: &[u8]) -> Option<(i64, usize)> {
614    let m = Meta::from_byte(rec[0]);
615    if m.encoding() != Encoding::Int {
616        return None;
617    }
618    let at = m.payload_at();
619    let mut b = [0u8; INT_LEN];
620    b.copy_from_slice(&rec[at..at + INT_LEN]);
621    Some((i64::from_le_bytes(b), at))
622}
623
624/// Store `n` back over an int payload that starts at `at`.
625#[inline]
626pub fn write_int_in_place(rec: &mut [u8], at: usize, n: i64) {
627    rec[at..at + INT_LEN].copy_from_slice(&n.to_le_bytes());
628}
629
630#[cfg(test)]
631mod tests {
632    use super::*;
633
634    #[test]
635    fn encoding_follows_redis_boundaries() {
636        assert_eq!(Encoding::of(b"0"), Encoding::Int);
637        assert_eq!(Encoding::of(b"-1"), Encoding::Int);
638        assert_eq!(Encoding::of(b"9223372036854775807"), Encoding::Int);
639        // Past an i64, so it is text and not a number.
640        assert_eq!(Encoding::of(b"9223372036854775808"), Encoding::Embstr);
641        // The three shapes string2ll refuses, all of which must survive as text.
642        assert_eq!(Encoding::of(b"007"), Encoding::Embstr);
643        assert_eq!(Encoding::of(b"+1"), Encoding::Embstr);
644        assert_eq!(Encoding::of(b"-0"), Encoding::Embstr);
645        assert_eq!(Encoding::of(b""), Encoding::Embstr);
646        assert_eq!(Encoding::of(&[b'x'; EMBSTR_MAX]), Encoding::Embstr);
647        assert_eq!(Encoding::of(&[b'x'; EMBSTR_MAX + 1]), Encoding::Raw);
648    }
649
650    const KINDS: [Kind; 7] = [
651        Kind::String,
652        Kind::Hash,
653        Kind::Set,
654        Kind::Zset,
655        Kind::List,
656        Kind::Stream,
657        Kind::Array,
658    ];
659
660    #[test]
661    fn the_meta_byte_survives_a_round_trip() {
662        for kind in KINDS {
663            for enc in [Encoding::Int, Encoding::Embstr, Encoding::Raw] {
664                for expiry in [false, true] {
665                    let m = Meta::new(kind, enc, expiry);
666                    let back = Meta::from_byte(m.byte());
667                    assert_eq!(back.kind(), kind);
668                    assert_eq!(back.encoding(), enc);
669                    assert_eq!(back.has_expiry(), expiry);
670                    assert_eq!(back.payload_at(), if expiry { 9 } else { 1 });
671                }
672            }
673        }
674    }
675
676    #[test]
677    fn the_three_fields_of_the_meta_byte_do_not_reach_into_each_other() {
678        // Forty two combinations, all of which have to come out of one byte
679        // with nothing borrowed from a neighbour. A tag that overlapped the
680        // expiry bit would read the payload at the wrong offset, which is a
681        // corrupt value rather than a wrong answer.
682        let mut seen = std::collections::HashSet::new();
683        for kind in KINDS {
684            for enc in [Encoding::Int, Encoding::Embstr, Encoding::Raw] {
685                for expiry in [false, true] {
686                    assert!(
687                        seen.insert(Meta::new(kind, enc, expiry).byte()),
688                        "{kind:?} {enc:?} {expiry} collides with something else"
689                    );
690                }
691            }
692        }
693        assert_eq!(seen.len(), KINDS.len() * 6);
694    }
695
696    #[test]
697    fn a_record_written_before_the_tag_existed_is_a_string() {
698        // Bits 3 to 7 were zero in every record M2 wrote, and zero is String.
699        // This is the whole reason String is zero, so it is worth a test that
700        // fails if somebody renumbers the enum alphabetically one day.
701        assert_eq!(Kind::String as u8, 0);
702        assert_eq!(Meta::from_byte(0b0000_0101).kind(), Kind::String);
703        assert_eq!(Meta::from_byte(0b0000_0101).encoding(), Encoding::Embstr);
704        assert!(Meta::from_byte(0b0000_0101).has_expiry());
705    }
706
707    #[test]
708    fn the_tag_is_the_number_the_file_format_uses() {
709        use yo_format::catalog::ValueType;
710        // Not a translation table, an assertion that no translation is needed.
711        // If these ever diverge, saving a key has to map between them, and the
712        // mapping is the kind of thing that gets one arm wrong.
713        assert_eq!(Kind::String as u8, ValueType::String as u8);
714        assert_eq!(Kind::Hash as u8, ValueType::Hash as u8);
715        assert_eq!(Kind::Set as u8, ValueType::Set as u8);
716        assert_eq!(Kind::Zset as u8, ValueType::Zset as u8);
717        assert_eq!(Kind::List as u8, ValueType::List as u8);
718        assert_eq!(Kind::Stream as u8, ValueType::Stream as u8);
719        assert_eq!(Kind::Array as u8, ValueType::Array as u8);
720        // And the words agree, because both of them end up on a wire.
721        for k in KINDS {
722            let v = ValueType::from_u8(k as u8).expect("the catalog knows this one");
723            assert_eq!(k.name(), v.redis_name(), "{k:?}");
724        }
725    }
726
727    #[test]
728    fn a_string_record_is_tagged_as_one() {
729        for text in [&b"42"[..], b"hello", &[b'z'; 100]] {
730            for expire in [None, Some(9_000u64)] {
731                assert_eq!(kind(&record(text, expire)), Kind::String);
732            }
733        }
734        let mut v = vec![0u8; record_len(Encoding::Int, 0, false)];
735        write_int_record(&mut v, 7, None);
736        assert_eq!(kind(&v), Kind::String);
737    }
738
739    fn record(bytes: &[u8], expire: Option<u64>) -> Vec<u8> {
740        let enc = Encoding::of(bytes);
741        let mut v = vec![0u8; record_len(enc, bytes.len(), expire.is_some())];
742        write_record(&mut v, enc, bytes, expire);
743        v
744    }
745
746    #[test]
747    fn a_record_gives_back_what_went_into_it() {
748        for text in [
749            &b""[..],
750            b"x",
751            b"0",
752            b"-1",
753            b"42",
754            b"007",
755            b"-0",
756            b"hello world",
757            &[b'z'; 100],
758        ] {
759            for expire in [None, Some(1_234_567_890_123u64)] {
760                let r = record(text, expire);
761                assert_eq!(read(&r).to_vec(), text, "{text:?} at {expire:?}");
762                assert_eq!(read(&r).len(), text.len(), "{text:?} length");
763                assert_eq!(expire_at(&r), expire, "{text:?} deadline");
764            }
765        }
766    }
767
768    #[test]
769    fn an_integer_costs_the_same_however_many_digits_it_has() {
770        let small = record(b"1", None);
771        let large = record(b"-9223372036854775808", None);
772        assert_eq!(small.len(), large.len());
773        assert_eq!(read(&large), Str::Int(i64::MIN));
774        assert_eq!(read(&large).to_vec(), b"-9223372036854775808");
775    }
776
777    #[test]
778    fn an_integer_is_incremented_where_it_lies() {
779        let mut r = record(b"41", Some(99));
780        let (n, at) = read_int_in_place(&r).expect("int encoded");
781        assert_eq!(n, 41);
782        write_int_in_place(&mut r, at, n + 1);
783        assert_eq!(read(&r), Str::Int(42));
784        // The deadline was in front of the payload and is still there.
785        assert_eq!(expire_at(&r), Some(99));
786    }
787
788    #[test]
789    fn a_string_is_not_read_as_an_integer_in_place() {
790        let r = record(b"hello", None);
791        assert!(read_int_in_place(&r).is_none());
792    }
793
794    #[test]
795    fn a_deadline_that_is_now_has_passed() {
796        let r = record(b"v", Some(100));
797        assert!(!is_expired(&r, 99));
798        assert!(is_expired(&r, 100));
799        assert!(is_expired(&r, 101));
800        let forever = record(b"v", None);
801        assert!(!is_expired(&forever, u64::MAX));
802    }
803
804    #[test]
805    fn a_value_that_is_text_can_still_be_a_number() {
806        // What `APPEND` leaves behind, and what `INCR` has to accept.
807        assert_eq!(Str::Bytes(b"10").as_int(), Some(10));
808        assert_eq!(Str::Bytes(b"10x").as_int(), None);
809        assert_eq!(Str::Int(-5).as_int(), Some(-5));
810    }
811
812    #[test]
813    fn an_unknown_encoding_reads_as_raw_bytes() {
814        // Nothing we write produces bit pattern three, but a record that has
815        // been through a future writer might, and guessing `int` on it would
816        // reinterpret eight bytes of somebody's string as a number.
817        let m = Meta::from_byte(0b11);
818        assert_eq!(m.encoding(), Encoding::Raw);
819    }
820
821    #[test]
822    fn an_unknown_type_tag_reads_as_a_string() {
823        // Six of eight patterns are used, and the other two answer String for
824        // the same reason: handing the bytes back is the harmless reading.
825        assert_eq!(Meta::from_byte(6 << 3).kind(), Kind::Array);
826        assert_eq!(Meta::from_byte(7 << 3).kind(), Kind::String);
827    }
828
829    #[test]
830    fn the_bits_above_the_tag_do_not_disturb_it() {
831        // Bit 6 is now the access flag and bit 7 is still free, and neither of
832        // them may move the tag, so this is the check that the tag is three
833        // bits and not five.
834        assert_eq!(Meta::from_byte(0b1100_0000).kind(), Kind::String);
835        assert_eq!(Meta::from_byte(0b1101_0000).kind(), Kind::Set);
836        // And the flag itself reads off the byte rather than off the tag.
837        assert!(Meta::from_byte(0b0100_0000).has_access());
838        assert!(!Meta::from_byte(0b1011_1111).has_access());
839    }
840
841    /// Every record this crate writes has room for an access field, and the
842    /// field starts empty.
843    #[test]
844    fn a_fresh_record_has_an_unstamped_access_field() {
845        for expire in [None, Some(1_700_000_000_000)] {
846            for (enc, bytes) in [
847                (Encoding::Int, &b"42"[..]),
848                (Encoding::Embstr, b"hello"),
849                (Encoding::Raw, &[b'x'; 64][..]),
850            ] {
851                let mut rec = vec![0u8; record_len(enc, bytes.len(), expire.is_some())];
852                write_record(&mut rec, enc, bytes, expire);
853                let a = access(&rec).expect("a record we just wrote has the field");
854                assert!(a.is_unset(), "{enc:?} came out stamped");
855                assert_eq!(expire_at(&rec), expire, "{enc:?} lost its deadline");
856                match enc {
857                    Encoding::Int => assert_eq!(read(&rec), Str::Int(42)),
858                    _ => assert_eq!(read(&rec), Str::Bytes(bytes)),
859                }
860            }
861        }
862    }
863
864    /// The same for the other two writers, which is every record shape there is.
865    #[test]
866    fn slot_and_int_records_have_the_field_too() {
867        for expire in [None, Some(9_000)] {
868            let mut rec = vec![0u8; slot_record_len(expire.is_some())];
869            write_slot_record(&mut rec, Kind::Set, 77, expire);
870            assert!(access(&rec).expect("the field").is_unset());
871            assert_eq!(slot(&rec), 77);
872            assert_eq!(kind(&rec), Kind::Set);
873            assert_eq!(expire_at(&rec), expire);
874
875            let mut rec = vec![0u8; record_len(Encoding::Int, 0, expire.is_some())];
876            write_int_record(&mut rec, -5, expire);
877            assert!(access(&rec).expect("the field").is_unset());
878            assert_eq!(read(&rec), Str::Int(-5));
879            assert_eq!(expire_at(&rec), expire);
880        }
881    }
882
883    /// Stamping the field does not disturb anything either side of it.
884    ///
885    /// It sits between the deadline and the payload and it is written in place
886    /// on a path that is usually a read, so an off by one here would corrupt a
887    /// value quietly rather than fail.
888    #[test]
889    fn stamping_the_field_leaves_the_deadline_and_the_payload_alone() {
890        let deadline = 1_700_000_000_123u64;
891        let body = b"the payload nobody should touch";
892        let mut rec = vec![0u8; record_len(Encoding::Raw, body.len(), true)];
893        write_record(&mut rec, Encoding::Raw, body, Some(deadline));
894
895        for bits in [1u32, 0xff, 0x00ff_ffff, 0x0012_3456] {
896            let a = Access::from_bits(bits);
897            assert!(set_access(&mut rec, a));
898            assert_eq!(access(&rec), Some(a), "{bits:#x} did not survive");
899            assert_eq!(
900                expire_at(&rec),
901                Some(deadline),
902                "{bits:#x} hit the deadline"
903            );
904            assert_eq!(read(&rec), Str::Bytes(body), "{bits:#x} hit the payload");
905        }
906    }
907
908    /// A record written before the field existed still reads correctly, and
909    /// refuses to be stamped rather than being stamped over its payload.
910    ///
911    /// This is the whole reason the field is behind a tag bit instead of just
912    /// always being there. A file written by an older build has records with the
913    /// bit clear, and their payload starts three bytes earlier.
914    #[test]
915    fn a_record_from_before_the_field_still_reads() {
916        // Built by hand, the way the old writer did it: meta, deadline, payload,
917        // and no access field.
918        let body = b"older";
919        let mut old = vec![Meta::string(Encoding::Raw, true).byte()];
920        old.extend_from_slice(&7_000u64.to_le_bytes());
921        old.extend_from_slice(body);
922
923        assert!(!Meta::from_byte(old[0]).has_access());
924        assert_eq!(access(&old), None, "there is no field to read");
925        assert_eq!(expire_at(&old), Some(7_000));
926        assert_eq!(read(&old), Str::Bytes(body));
927
928        let before = old.clone();
929        assert!(
930            !set_access(&mut old, Access::from_bits(0xabcdef)),
931            "it should refuse rather than write over the payload"
932        );
933        assert_eq!(old, before, "it wrote something anyway");
934    }
935
936    /// The field costs three bytes on every record and no more.
937    #[test]
938    fn the_field_costs_three_bytes() {
939        assert_eq!(record_len(Encoding::Raw, 10, false), 1 + 3 + 10);
940        assert_eq!(record_len(Encoding::Raw, 10, true), 1 + 8 + 3 + 10);
941        assert_eq!(record_len(Encoding::Int, 10, false), 1 + 3 + 8);
942        assert_eq!(slot_record_len(false), 1 + 3 + 4);
943        assert_eq!(slot_record_len(true), 1 + 8 + 3 + 4);
944    }
945}