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 carries a deadline at all, without reading it.
582///
583/// One byte where [`expire_at`] reads nine, which is the difference between a
584/// question worth asking on every write and one that is not. The count of keys
585/// with deadlines is kept up to date on every record written and every record
586/// deleted, and all it ever needs is this bit.
587#[inline]
588pub fn has_expiry(rec: &[u8]) -> bool {
589    Meta::from_byte(rec[0]).has_expiry()
590}
591
592/// Whether a record's deadline has passed at `now_ms`.
593///
594/// A deadline exactly equal to now has passed, which is Redis's reading: a key
595/// set to expire at time T is gone at time T.
596#[inline]
597pub fn is_expired(rec: &[u8], now_ms: u64) -> bool {
598    match expire_at(rec) {
599        Some(at) => at <= now_ms,
600        None => false,
601    }
602}
603
604/// The value in a record.
605#[inline]
606pub fn read(rec: &[u8]) -> Str<'_> {
607    let m = Meta::from_byte(rec[0]);
608    let at = m.payload_at();
609    match m.encoding() {
610        Encoding::Int => {
611            let mut b = [0u8; INT_LEN];
612            b.copy_from_slice(&rec[at..at + INT_LEN]);
613            Str::Int(i64::from_le_bytes(b))
614        }
615        _ => Str::Bytes(&rec[at..]),
616    }
617}
618
619/// The integer in an int encoded record, and where its bytes start.
620///
621/// Returns `None` for any other encoding. This is the read half of `INCR`'s
622/// fast path, and the offset it hands back is what the write half stores into.
623#[inline]
624pub fn read_int_in_place(rec: &[u8]) -> Option<(i64, usize)> {
625    let m = Meta::from_byte(rec[0]);
626    if m.encoding() != Encoding::Int {
627        return None;
628    }
629    let at = m.payload_at();
630    let mut b = [0u8; INT_LEN];
631    b.copy_from_slice(&rec[at..at + INT_LEN]);
632    Some((i64::from_le_bytes(b), at))
633}
634
635/// Store `n` back over an int payload that starts at `at`.
636#[inline]
637pub fn write_int_in_place(rec: &mut [u8], at: usize, n: i64) {
638    rec[at..at + INT_LEN].copy_from_slice(&n.to_le_bytes());
639}
640
641#[cfg(test)]
642mod tests {
643    use super::*;
644
645    #[test]
646    fn encoding_follows_redis_boundaries() {
647        assert_eq!(Encoding::of(b"0"), Encoding::Int);
648        assert_eq!(Encoding::of(b"-1"), Encoding::Int);
649        assert_eq!(Encoding::of(b"9223372036854775807"), Encoding::Int);
650        // Past an i64, so it is text and not a number.
651        assert_eq!(Encoding::of(b"9223372036854775808"), Encoding::Embstr);
652        // The three shapes string2ll refuses, all of which must survive as text.
653        assert_eq!(Encoding::of(b"007"), Encoding::Embstr);
654        assert_eq!(Encoding::of(b"+1"), Encoding::Embstr);
655        assert_eq!(Encoding::of(b"-0"), Encoding::Embstr);
656        assert_eq!(Encoding::of(b""), Encoding::Embstr);
657        assert_eq!(Encoding::of(&[b'x'; EMBSTR_MAX]), Encoding::Embstr);
658        assert_eq!(Encoding::of(&[b'x'; EMBSTR_MAX + 1]), Encoding::Raw);
659    }
660
661    const KINDS: [Kind; 7] = [
662        Kind::String,
663        Kind::Hash,
664        Kind::Set,
665        Kind::Zset,
666        Kind::List,
667        Kind::Stream,
668        Kind::Array,
669    ];
670
671    #[test]
672    fn the_meta_byte_survives_a_round_trip() {
673        for kind in KINDS {
674            for enc in [Encoding::Int, Encoding::Embstr, Encoding::Raw] {
675                for expiry in [false, true] {
676                    let m = Meta::new(kind, enc, expiry);
677                    let back = Meta::from_byte(m.byte());
678                    assert_eq!(back.kind(), kind);
679                    assert_eq!(back.encoding(), enc);
680                    assert_eq!(back.has_expiry(), expiry);
681                    assert_eq!(back.payload_at(), if expiry { 9 } else { 1 });
682                }
683            }
684        }
685    }
686
687    #[test]
688    fn the_three_fields_of_the_meta_byte_do_not_reach_into_each_other() {
689        // Forty two combinations, all of which have to come out of one byte
690        // with nothing borrowed from a neighbour. A tag that overlapped the
691        // expiry bit would read the payload at the wrong offset, which is a
692        // corrupt value rather than a wrong answer.
693        let mut seen = std::collections::HashSet::new();
694        for kind in KINDS {
695            for enc in [Encoding::Int, Encoding::Embstr, Encoding::Raw] {
696                for expiry in [false, true] {
697                    assert!(
698                        seen.insert(Meta::new(kind, enc, expiry).byte()),
699                        "{kind:?} {enc:?} {expiry} collides with something else"
700                    );
701                }
702            }
703        }
704        assert_eq!(seen.len(), KINDS.len() * 6);
705    }
706
707    #[test]
708    fn a_record_written_before_the_tag_existed_is_a_string() {
709        // Bits 3 to 7 were zero in every record M2 wrote, and zero is String.
710        // This is the whole reason String is zero, so it is worth a test that
711        // fails if somebody renumbers the enum alphabetically one day.
712        assert_eq!(Kind::String as u8, 0);
713        assert_eq!(Meta::from_byte(0b0000_0101).kind(), Kind::String);
714        assert_eq!(Meta::from_byte(0b0000_0101).encoding(), Encoding::Embstr);
715        assert!(Meta::from_byte(0b0000_0101).has_expiry());
716    }
717
718    #[test]
719    fn the_tag_is_the_number_the_file_format_uses() {
720        use yo_format::catalog::ValueType;
721        // Not a translation table, an assertion that no translation is needed.
722        // If these ever diverge, saving a key has to map between them, and the
723        // mapping is the kind of thing that gets one arm wrong.
724        assert_eq!(Kind::String as u8, ValueType::String as u8);
725        assert_eq!(Kind::Hash as u8, ValueType::Hash as u8);
726        assert_eq!(Kind::Set as u8, ValueType::Set as u8);
727        assert_eq!(Kind::Zset as u8, ValueType::Zset as u8);
728        assert_eq!(Kind::List as u8, ValueType::List as u8);
729        assert_eq!(Kind::Stream as u8, ValueType::Stream as u8);
730        assert_eq!(Kind::Array as u8, ValueType::Array as u8);
731        // And the words agree, because both of them end up on a wire.
732        for k in KINDS {
733            let v = ValueType::from_u8(k as u8).expect("the catalog knows this one");
734            assert_eq!(k.name(), v.redis_name(), "{k:?}");
735        }
736    }
737
738    #[test]
739    fn a_string_record_is_tagged_as_one() {
740        for text in [&b"42"[..], b"hello", &[b'z'; 100]] {
741            for expire in [None, Some(9_000u64)] {
742                assert_eq!(kind(&record(text, expire)), Kind::String);
743            }
744        }
745        let mut v = vec![0u8; record_len(Encoding::Int, 0, false)];
746        write_int_record(&mut v, 7, None);
747        assert_eq!(kind(&v), Kind::String);
748    }
749
750    fn record(bytes: &[u8], expire: Option<u64>) -> Vec<u8> {
751        let enc = Encoding::of(bytes);
752        let mut v = vec![0u8; record_len(enc, bytes.len(), expire.is_some())];
753        write_record(&mut v, enc, bytes, expire);
754        v
755    }
756
757    #[test]
758    fn a_record_gives_back_what_went_into_it() {
759        for text in [
760            &b""[..],
761            b"x",
762            b"0",
763            b"-1",
764            b"42",
765            b"007",
766            b"-0",
767            b"hello world",
768            &[b'z'; 100],
769        ] {
770            for expire in [None, Some(1_234_567_890_123u64)] {
771                let r = record(text, expire);
772                assert_eq!(read(&r).to_vec(), text, "{text:?} at {expire:?}");
773                assert_eq!(read(&r).len(), text.len(), "{text:?} length");
774                assert_eq!(expire_at(&r), expire, "{text:?} deadline");
775            }
776        }
777    }
778
779    #[test]
780    fn an_integer_costs_the_same_however_many_digits_it_has() {
781        let small = record(b"1", None);
782        let large = record(b"-9223372036854775808", None);
783        assert_eq!(small.len(), large.len());
784        assert_eq!(read(&large), Str::Int(i64::MIN));
785        assert_eq!(read(&large).to_vec(), b"-9223372036854775808");
786    }
787
788    #[test]
789    fn an_integer_is_incremented_where_it_lies() {
790        let mut r = record(b"41", Some(99));
791        let (n, at) = read_int_in_place(&r).expect("int encoded");
792        assert_eq!(n, 41);
793        write_int_in_place(&mut r, at, n + 1);
794        assert_eq!(read(&r), Str::Int(42));
795        // The deadline was in front of the payload and is still there.
796        assert_eq!(expire_at(&r), Some(99));
797    }
798
799    #[test]
800    fn a_string_is_not_read_as_an_integer_in_place() {
801        let r = record(b"hello", None);
802        assert!(read_int_in_place(&r).is_none());
803    }
804
805    #[test]
806    fn a_deadline_that_is_now_has_passed() {
807        let r = record(b"v", Some(100));
808        assert!(!is_expired(&r, 99));
809        assert!(is_expired(&r, 100));
810        assert!(is_expired(&r, 101));
811        let forever = record(b"v", None);
812        assert!(!is_expired(&forever, u64::MAX));
813    }
814
815    #[test]
816    fn a_value_that_is_text_can_still_be_a_number() {
817        // What `APPEND` leaves behind, and what `INCR` has to accept.
818        assert_eq!(Str::Bytes(b"10").as_int(), Some(10));
819        assert_eq!(Str::Bytes(b"10x").as_int(), None);
820        assert_eq!(Str::Int(-5).as_int(), Some(-5));
821    }
822
823    #[test]
824    fn an_unknown_encoding_reads_as_raw_bytes() {
825        // Nothing we write produces bit pattern three, but a record that has
826        // been through a future writer might, and guessing `int` on it would
827        // reinterpret eight bytes of somebody's string as a number.
828        let m = Meta::from_byte(0b11);
829        assert_eq!(m.encoding(), Encoding::Raw);
830    }
831
832    #[test]
833    fn an_unknown_type_tag_reads_as_a_string() {
834        // Six of eight patterns are used, and the other two answer String for
835        // the same reason: handing the bytes back is the harmless reading.
836        assert_eq!(Meta::from_byte(6 << 3).kind(), Kind::Array);
837        assert_eq!(Meta::from_byte(7 << 3).kind(), Kind::String);
838    }
839
840    #[test]
841    fn the_bits_above_the_tag_do_not_disturb_it() {
842        // Bit 6 is now the access flag and bit 7 is still free, and neither of
843        // them may move the tag, so this is the check that the tag is three
844        // bits and not five.
845        assert_eq!(Meta::from_byte(0b1100_0000).kind(), Kind::String);
846        assert_eq!(Meta::from_byte(0b1101_0000).kind(), Kind::Set);
847        // And the flag itself reads off the byte rather than off the tag.
848        assert!(Meta::from_byte(0b0100_0000).has_access());
849        assert!(!Meta::from_byte(0b1011_1111).has_access());
850    }
851
852    /// Every record this crate writes has room for an access field, and the
853    /// field starts empty.
854    #[test]
855    fn a_fresh_record_has_an_unstamped_access_field() {
856        for expire in [None, Some(1_700_000_000_000)] {
857            for (enc, bytes) in [
858                (Encoding::Int, &b"42"[..]),
859                (Encoding::Embstr, b"hello"),
860                (Encoding::Raw, &[b'x'; 64][..]),
861            ] {
862                let mut rec = vec![0u8; record_len(enc, bytes.len(), expire.is_some())];
863                write_record(&mut rec, enc, bytes, expire);
864                let a = access(&rec).expect("a record we just wrote has the field");
865                assert!(a.is_unset(), "{enc:?} came out stamped");
866                assert_eq!(expire_at(&rec), expire, "{enc:?} lost its deadline");
867                match enc {
868                    Encoding::Int => assert_eq!(read(&rec), Str::Int(42)),
869                    _ => assert_eq!(read(&rec), Str::Bytes(bytes)),
870                }
871            }
872        }
873    }
874
875    /// The same for the other two writers, which is every record shape there is.
876    #[test]
877    fn slot_and_int_records_have_the_field_too() {
878        for expire in [None, Some(9_000)] {
879            let mut rec = vec![0u8; slot_record_len(expire.is_some())];
880            write_slot_record(&mut rec, Kind::Set, 77, expire);
881            assert!(access(&rec).expect("the field").is_unset());
882            assert_eq!(slot(&rec), 77);
883            assert_eq!(kind(&rec), Kind::Set);
884            assert_eq!(expire_at(&rec), expire);
885
886            let mut rec = vec![0u8; record_len(Encoding::Int, 0, expire.is_some())];
887            write_int_record(&mut rec, -5, expire);
888            assert!(access(&rec).expect("the field").is_unset());
889            assert_eq!(read(&rec), Str::Int(-5));
890            assert_eq!(expire_at(&rec), expire);
891        }
892    }
893
894    /// Stamping the field does not disturb anything either side of it.
895    ///
896    /// It sits between the deadline and the payload and it is written in place
897    /// on a path that is usually a read, so an off by one here would corrupt a
898    /// value quietly rather than fail.
899    #[test]
900    fn stamping_the_field_leaves_the_deadline_and_the_payload_alone() {
901        let deadline = 1_700_000_000_123u64;
902        let body = b"the payload nobody should touch";
903        let mut rec = vec![0u8; record_len(Encoding::Raw, body.len(), true)];
904        write_record(&mut rec, Encoding::Raw, body, Some(deadline));
905
906        for bits in [1u32, 0xff, 0x00ff_ffff, 0x0012_3456] {
907            let a = Access::from_bits(bits);
908            assert!(set_access(&mut rec, a));
909            assert_eq!(access(&rec), Some(a), "{bits:#x} did not survive");
910            assert_eq!(
911                expire_at(&rec),
912                Some(deadline),
913                "{bits:#x} hit the deadline"
914            );
915            assert_eq!(read(&rec), Str::Bytes(body), "{bits:#x} hit the payload");
916        }
917    }
918
919    /// A record written before the field existed still reads correctly, and
920    /// refuses to be stamped rather than being stamped over its payload.
921    ///
922    /// This is the whole reason the field is behind a tag bit instead of just
923    /// always being there. A file written by an older build has records with the
924    /// bit clear, and their payload starts three bytes earlier.
925    #[test]
926    fn a_record_from_before_the_field_still_reads() {
927        // Built by hand, the way the old writer did it: meta, deadline, payload,
928        // and no access field.
929        let body = b"older";
930        let mut old = vec![Meta::string(Encoding::Raw, true).byte()];
931        old.extend_from_slice(&7_000u64.to_le_bytes());
932        old.extend_from_slice(body);
933
934        assert!(!Meta::from_byte(old[0]).has_access());
935        assert_eq!(access(&old), None, "there is no field to read");
936        assert_eq!(expire_at(&old), Some(7_000));
937        assert_eq!(read(&old), Str::Bytes(body));
938
939        let before = old.clone();
940        assert!(
941            !set_access(&mut old, Access::from_bits(0xabcdef)),
942            "it should refuse rather than write over the payload"
943        );
944        assert_eq!(old, before, "it wrote something anyway");
945    }
946
947    /// The field costs three bytes on every record and no more.
948    #[test]
949    fn the_field_costs_three_bytes() {
950        assert_eq!(record_len(Encoding::Raw, 10, false), 1 + 3 + 10);
951        assert_eq!(record_len(Encoding::Raw, 10, true), 1 + 8 + 3 + 10);
952        assert_eq!(record_len(Encoding::Int, 10, false), 1 + 3 + 8);
953        assert_eq!(slot_record_len(false), 1 + 3 + 4);
954        assert_eq!(slot_record_len(true), 1 + 8 + 3 + 4);
955    }
956}