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       | payload   |
10//! | u8     | 8 bytes, only if 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 payload depends on the encoding. An `int` holds the eight bytes of the
19//! integer and not its digits, which is what makes `INCR` a probe, an add and a
20//! store with no arena traffic at all (`08` section 2). An `embstr` and a `raw`
21//! hold the bytes as given. The difference between those two is the name
22//! `OBJECT ENCODING` reports and nothing else, which is also true in Redis:
23//! `embstr` there means the value was allocated next to its object header, a
24//! distinction `yo` does not have because every value is already next to its
25//! key.
26//!
27//! # The type tag
28//!
29//! The meta byte also says which type the key holds, in three bits that were
30//! spare. That is what `TYPE` reads and it is what a command will read before it
31//! decides whether it is looking at its own type or at somebody else's, and both
32//! of those want the answer to come out of the byte the lookup already fetched
33//! rather than out of a second structure. A string is zero, so nothing written
34//! before the tag existed reads back as anything else.
35
36use yo_common::num::{parse_i64, push_i64};
37
38/// The longest value Redis calls `embstr` rather than `raw`.
39///
40/// Clients and test suites read `OBJECT ENCODING` and assert on the boundary,
41/// so it is 44 here because it is 44 there (`12` section 2).
42pub const EMBSTR_MAX: usize = 44;
43
44/// What `OBJECT ENCODING` calls a string.
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum Encoding {
47    /// The value is an integer, held as an integer.
48    Int,
49    /// A short string, at or under [`EMBSTR_MAX`] bytes.
50    Embstr,
51    /// Everything else.
52    Raw,
53}
54
55impl Encoding {
56    /// The string `OBJECT ENCODING` returns.
57    #[inline]
58    pub const fn name(self) -> &'static str {
59        match self {
60            Encoding::Int => "int",
61            Encoding::Embstr => "embstr",
62            Encoding::Raw => "raw",
63        }
64    }
65
66    /// The encoding Redis would choose for these bytes.
67    ///
68    /// Integer first, because `SET k 42` is int encoded in Redis whatever the
69    /// length, then the `embstr` boundary. The integer test is Redis's own
70    /// `string2ll`, which refuses a leading zero, a leading plus and `-0`, so
71    /// `SET k 007` stays a three byte string and gives back `007`.
72    #[inline]
73    pub fn of(bytes: &[u8]) -> Encoding {
74        if parse_i64(bytes).is_some() {
75            Encoding::Int
76        } else if bytes.len() <= EMBSTR_MAX {
77            Encoding::Embstr
78        } else {
79            Encoding::Raw
80        }
81    }
82}
83
84/// What `TYPE` calls a key, and what the meta byte's tag holds.
85///
86/// The numbers are the same numbers `yo_format::ValueType` uses on disk, so that
87/// saving a key is a copy of the tag rather than a translation of it. There is a
88/// test at the bottom of this file holding the two in step, and it takes a dev
89/// dependency on `yo-format` for no other reason.
90///
91/// The list is shorter than the on disk one because some of those are the same
92/// thing in memory. A bitmap is a string and a HyperLogLog is a string, in Redis
93/// as much as here, and `TYPE` on either answers `string`. The catalog draws
94/// finer lines because a reader wants to know what a blob meant.
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub enum Kind {
97    /// A string, and everything Redis stores as one.
98    String = 0,
99    /// A hash.
100    Hash = 1,
101    /// A set.
102    Set = 2,
103    /// A sorted set.
104    Zset = 3,
105    /// A list.
106    List = 4,
107    /// A stream.
108    Stream = 5,
109    /// A sparse array.
110    Array = 6,
111}
112
113impl Kind {
114    /// The word `TYPE` replies with.
115    #[inline]
116    pub const fn name(self) -> &'static str {
117        match self {
118            Kind::String => "string",
119            Kind::Hash => "hash",
120            Kind::Set => "set",
121            Kind::Zset => "zset",
122            Kind::List => "list",
123            Kind::Stream => "stream",
124            Kind::Array => "array",
125        }
126    }
127
128    /// The kind for a three bit tag.
129    ///
130    /// Seven of the eight patterns are spoken for. The last one cannot come out
131    /// of our own writer, and it falls to `String` for the same reason an
132    /// unknown encoding falls to `raw`: it is the reading that hands the bytes
133    /// back rather than the one that reinterprets them.
134    #[inline]
135    const fn from_bits(bits: u8) -> Kind {
136        match bits {
137            KIND_HASH => Kind::Hash,
138            KIND_SET => Kind::Set,
139            KIND_ZSET => Kind::Zset,
140            KIND_LIST => Kind::List,
141            KIND_STREAM => Kind::Stream,
142            KIND_ARRAY => Kind::Array,
143            _ => Kind::String,
144        }
145    }
146}
147
148/// Bits 0 and 1 of the meta byte: which encoding.
149const ENC_MASK: u8 = 0b0000_0011;
150const ENC_INT: u8 = 0;
151const ENC_EMBSTR: u8 = 1;
152const ENC_RAW: u8 = 2;
153/// Bit 2: whether eight bytes of deadline follow the meta byte.
154const HAS_EXPIRY: u8 = 0b0000_0100;
155/// Bits 3, 4 and 5: which type the key holds. Bits 6 and 7 are still spare.
156///
157/// String is zero, so every record written before the tag existed reads back as
158/// a string, which is what it was.
159const KIND_MASK: u8 = 0b0011_1000;
160const KIND_SHIFT: u32 = 3;
161const KIND_HASH: u8 = 1;
162const KIND_SET: u8 = 2;
163const KIND_ZSET: u8 = 3;
164const KIND_LIST: u8 = 4;
165const KIND_STREAM: u8 = 5;
166const KIND_ARRAY: u8 = 6;
167
168/// Bytes of integer payload, which is a whole `i64` and never its digits.
169const INT_LEN: usize = 8;
170
171/// The meta byte in front of every stored string.
172#[derive(Debug, Clone, Copy, PartialEq, Eq)]
173pub struct Meta(u8);
174
175impl Meta {
176    /// Build the byte for a type, an encoding and the presence of a deadline.
177    #[inline]
178    pub const fn new(kind: Kind, enc: Encoding, has_expiry: bool) -> Meta {
179        let bits = match enc {
180            Encoding::Int => ENC_INT,
181            Encoding::Embstr => ENC_EMBSTR,
182            Encoding::Raw => ENC_RAW,
183        };
184        Meta(bits | ((kind as u8) << KIND_SHIFT) | if has_expiry { HAS_EXPIRY } else { 0 })
185    }
186
187    /// The byte for a string, which is what everything in `strings.rs` writes.
188    #[inline]
189    pub const fn string(enc: Encoding, has_expiry: bool) -> Meta {
190        Meta::new(Kind::String, enc, has_expiry)
191    }
192
193    /// The byte for a value that lives in a slab, with the record holding a
194    /// number that says where.
195    ///
196    /// The encoding bits are written as zero and mean nothing here. A set's
197    /// encoding is which of the three representations it is in, and that is a
198    /// property of the body and not of the record, so `OBJECT ENCODING` follows
199    /// the number and asks. Keeping a copy of it in these two bits would want
200    /// the record rewritten every time a set was promoted, for a command nobody
201    /// calls in a loop, and two places to disagree about the same fact.
202    #[inline]
203    pub const fn slot(kind: Kind, has_expiry: bool) -> Meta {
204        Meta::new(kind, Encoding::Int, has_expiry)
205    }
206
207    /// Read the byte back.
208    ///
209    /// An unknown encoding is impossible from our own writer, so the two spare
210    /// bit patterns fall to `raw`, which is the reading that returns the bytes
211    /// unchanged rather than reinterpreting them as something else.
212    #[inline]
213    pub const fn from_byte(b: u8) -> Meta {
214        Meta(b)
215    }
216
217    /// The raw byte, as stored.
218    #[inline]
219    pub const fn byte(self) -> u8 {
220        self.0
221    }
222
223    /// Which encoding this value carries.
224    #[inline]
225    pub const fn encoding(self) -> Encoding {
226        match self.0 & ENC_MASK {
227            ENC_INT => Encoding::Int,
228            ENC_EMBSTR => Encoding::Embstr,
229            _ => Encoding::Raw,
230        }
231    }
232
233    /// Which type this key holds.
234    #[inline]
235    pub const fn kind(self) -> Kind {
236        Kind::from_bits((self.0 & KIND_MASK) >> KIND_SHIFT)
237    }
238
239    /// Whether a deadline follows.
240    #[inline]
241    pub const fn has_expiry(self) -> bool {
242        self.0 & HAS_EXPIRY != 0
243    }
244
245    /// Where the payload starts, counting from the meta byte.
246    #[inline]
247    pub const fn payload_at(self) -> usize {
248        if self.has_expiry() { 1 + 8 } else { 1 }
249    }
250}
251
252/// A stored value, read back out of a record.
253///
254/// The point of the two arms is that neither of them copies. An integer comes
255/// back as an integer and is written into the reply buffer as digits at the
256/// moment the reply is built, and a string comes back as a slice of the record
257/// it lives in. Y18 asks for the reply to be constructed once in wire form, and
258/// a `Vec<u8>` in the middle of that is the thing it is asking to remove.
259#[derive(Debug, Clone, Copy, PartialEq, Eq)]
260pub enum Str<'a> {
261    /// An int encoded value.
262    Int(i64),
263    /// Everything else, as it lies in the record.
264    Bytes(&'a [u8]),
265}
266
267impl Str<'_> {
268    /// How many bytes this value is as a string, which is what `STRLEN` returns.
269    #[inline]
270    pub fn len(&self) -> usize {
271        match self {
272            Str::Int(n) => yo_common::num::i64_len(*n),
273            Str::Bytes(b) => b.len(),
274        }
275    }
276
277    /// Whether the value is the empty string.
278    #[inline]
279    pub fn is_empty(&self) -> bool {
280        match self {
281            // No integer writes as no digits.
282            Str::Int(_) => false,
283            Str::Bytes(b) => b.is_empty(),
284        }
285    }
286
287    /// Append the string form to a buffer, which for an integer is its digits.
288    #[inline]
289    pub fn write_to(&self, out: &mut Vec<u8>) {
290        match self {
291            Str::Int(n) => push_i64(out, *n),
292            Str::Bytes(b) => out.extend_from_slice(b),
293        }
294    }
295
296    /// The string form, copied. For the reply path prefer [`Str::write_to`].
297    pub fn to_vec(&self) -> Vec<u8> {
298        let mut v = Vec::with_capacity(self.len());
299        self.write_to(&mut v);
300        v
301    }
302
303    /// The integer this value is, if it is one.
304    ///
305    /// A value can be an integer without being int encoded: `APPEND` and
306    /// `SETRANGE` leave a `raw` string behind, and `INCR` on `"10"` built that
307    /// way is 11 in Redis. So the bytes are parsed rather than the encoding
308    /// being trusted.
309    #[inline]
310    pub fn as_int(&self) -> Option<i64> {
311        match self {
312            Str::Int(n) => Some(*n),
313            Str::Bytes(b) => parse_i64(b),
314        }
315    }
316
317    /// The XXH3 of the value's string form, which is Redis's `DIGEST`.
318    ///
319    /// An int encoded value hashes its digits and not the eight bytes the
320    /// record holds, because the digest a client compares against is the digest
321    /// of what a client would have read.
322    #[must_use]
323    pub fn digest(&self) -> u64 {
324        match self {
325            Str::Bytes(b) => yo_common::xxh3::hash64(b),
326            // Twenty bytes at the most, and the alternative is a second
327            // formatter that writes into a stack buffer for a path nobody calls
328            // in a loop.
329            Str::Int(_) => yo_common::xxh3::hash64(&self.to_vec()),
330        }
331    }
332
333    /// Whether this value's string form is exactly `want`.
334    ///
335    /// `IFEQ` compares against what the client would have read, so an int
336    /// encoded 42 is equal to `"42"` and not to `"042"`. Doing that without
337    /// materialising the digits is why the integer arm exists.
338    #[inline]
339    pub(crate) fn eq_bytes(&self, want: &[u8]) -> bool {
340        match self {
341            Str::Bytes(b) => *b == want,
342            Str::Int(n) => parse_i64(want) == Some(*n),
343        }
344    }
345}
346
347/// How many bytes a record holding this value will occupy.
348#[inline]
349pub fn record_len(enc: Encoding, payload: usize, has_expiry: bool) -> usize {
350    let head = if has_expiry { 1 + 8 } else { 1 };
351    head + if enc == Encoding::Int {
352        INT_LEN
353    } else {
354        payload
355    }
356}
357
358/// Write a whole record into `out`, which must be exactly [`record_len`] long.
359///
360/// `bytes` is the string as the caller gave it. When `enc` is [`Encoding::Int`]
361/// the digits are not stored, the integer they parse to is, and the caller has
362/// already established that they parse by choosing that encoding.
363#[inline]
364pub fn write_record(out: &mut [u8], enc: Encoding, bytes: &[u8], expire_at: Option<u64>) {
365    out[0] = Meta::string(enc, expire_at.is_some()).byte();
366    let mut at = 1;
367    if let Some(ms) = expire_at {
368        out[at..at + 8].copy_from_slice(&ms.to_le_bytes());
369        at += 8;
370    }
371    match enc {
372        Encoding::Int => {
373            let n =
374                parse_i64(bytes).expect("int encoding was chosen for bytes that are not an int");
375            out[at..at + INT_LEN].copy_from_slice(&n.to_le_bytes());
376        }
377        _ => out[at..].copy_from_slice(bytes),
378    }
379}
380
381/// Write a record whose value is an integer the caller already has.
382#[inline]
383pub fn write_int_record(out: &mut [u8], n: i64, expire_at: Option<u64>) {
384    out[0] = Meta::string(Encoding::Int, expire_at.is_some()).byte();
385    let mut at = 1;
386    if let Some(ms) = expire_at {
387        out[at..at + 8].copy_from_slice(&ms.to_le_bytes());
388        at += 8;
389    }
390    out[at..at + INT_LEN].copy_from_slice(&n.to_le_bytes());
391}
392
393/// Bytes of slab number, which is how a record points at a body.
394const SLOT_LEN: usize = 4;
395
396/// How many bytes a record pointing at a slab slot occupies.
397#[inline]
398pub fn slot_record_len(has_expiry: bool) -> usize {
399    (if has_expiry { 1 + 8 } else { 1 }) + SLOT_LEN
400}
401
402/// Write a record that points at `slot` in the slab for `kind`.
403///
404/// `out` must be exactly [`slot_record_len`] long.
405#[inline]
406pub fn write_slot_record(out: &mut [u8], kind: Kind, slot: u32, expire_at: Option<u64>) {
407    out[0] = Meta::slot(kind, expire_at.is_some()).byte();
408    let mut at = 1;
409    if let Some(ms) = expire_at {
410        out[at..at + 8].copy_from_slice(&ms.to_le_bytes());
411        at += 8;
412    }
413    out[at..at + SLOT_LEN].copy_from_slice(&slot.to_le_bytes());
414}
415
416/// The slab number in a record that has one.
417///
418/// # Panics
419///
420/// If the record is not one [`write_slot_record`] wrote, which is a caller that
421/// did not read the kind first.
422#[inline]
423pub fn slot(rec: &[u8]) -> u32 {
424    let at = Meta::from_byte(rec[0]).payload_at();
425    let mut b = [0u8; SLOT_LEN];
426    b.copy_from_slice(&rec[at..at + SLOT_LEN]);
427    u32::from_le_bytes(b)
428}
429
430/// The type a record holds.
431#[inline]
432pub fn kind(rec: &[u8]) -> Kind {
433    Meta::from_byte(rec[0]).kind()
434}
435
436/// The deadline in a record, if it has one.
437#[inline]
438pub fn expire_at(rec: &[u8]) -> Option<u64> {
439    let m = Meta::from_byte(rec[0]);
440    if !m.has_expiry() {
441        return None;
442    }
443    let mut b = [0u8; 8];
444    b.copy_from_slice(&rec[1..9]);
445    Some(u64::from_le_bytes(b))
446}
447
448/// Whether a record's deadline has passed at `now_ms`.
449///
450/// A deadline exactly equal to now has passed, which is Redis's reading: a key
451/// set to expire at time T is gone at time T.
452#[inline]
453pub fn is_expired(rec: &[u8], now_ms: u64) -> bool {
454    match expire_at(rec) {
455        Some(at) => at <= now_ms,
456        None => false,
457    }
458}
459
460/// The value in a record.
461#[inline]
462pub fn read(rec: &[u8]) -> Str<'_> {
463    let m = Meta::from_byte(rec[0]);
464    let at = m.payload_at();
465    match m.encoding() {
466        Encoding::Int => {
467            let mut b = [0u8; INT_LEN];
468            b.copy_from_slice(&rec[at..at + INT_LEN]);
469            Str::Int(i64::from_le_bytes(b))
470        }
471        _ => Str::Bytes(&rec[at..]),
472    }
473}
474
475/// The integer in an int encoded record, and where its bytes start.
476///
477/// Returns `None` for any other encoding. This is the read half of `INCR`'s
478/// fast path, and the offset it hands back is what the write half stores into.
479#[inline]
480pub fn read_int_in_place(rec: &[u8]) -> Option<(i64, usize)> {
481    let m = Meta::from_byte(rec[0]);
482    if m.encoding() != Encoding::Int {
483        return None;
484    }
485    let at = m.payload_at();
486    let mut b = [0u8; INT_LEN];
487    b.copy_from_slice(&rec[at..at + INT_LEN]);
488    Some((i64::from_le_bytes(b), at))
489}
490
491/// Store `n` back over an int payload that starts at `at`.
492#[inline]
493pub fn write_int_in_place(rec: &mut [u8], at: usize, n: i64) {
494    rec[at..at + INT_LEN].copy_from_slice(&n.to_le_bytes());
495}
496
497#[cfg(test)]
498mod tests {
499    use super::*;
500
501    #[test]
502    fn encoding_follows_redis_boundaries() {
503        assert_eq!(Encoding::of(b"0"), Encoding::Int);
504        assert_eq!(Encoding::of(b"-1"), Encoding::Int);
505        assert_eq!(Encoding::of(b"9223372036854775807"), Encoding::Int);
506        // Past an i64, so it is text and not a number.
507        assert_eq!(Encoding::of(b"9223372036854775808"), Encoding::Embstr);
508        // The three shapes string2ll refuses, all of which must survive as text.
509        assert_eq!(Encoding::of(b"007"), Encoding::Embstr);
510        assert_eq!(Encoding::of(b"+1"), Encoding::Embstr);
511        assert_eq!(Encoding::of(b"-0"), Encoding::Embstr);
512        assert_eq!(Encoding::of(b""), Encoding::Embstr);
513        assert_eq!(Encoding::of(&[b'x'; EMBSTR_MAX]), Encoding::Embstr);
514        assert_eq!(Encoding::of(&[b'x'; EMBSTR_MAX + 1]), Encoding::Raw);
515    }
516
517    const KINDS: [Kind; 7] = [
518        Kind::String,
519        Kind::Hash,
520        Kind::Set,
521        Kind::Zset,
522        Kind::List,
523        Kind::Stream,
524        Kind::Array,
525    ];
526
527    #[test]
528    fn the_meta_byte_survives_a_round_trip() {
529        for kind in KINDS {
530            for enc in [Encoding::Int, Encoding::Embstr, Encoding::Raw] {
531                for expiry in [false, true] {
532                    let m = Meta::new(kind, enc, expiry);
533                    let back = Meta::from_byte(m.byte());
534                    assert_eq!(back.kind(), kind);
535                    assert_eq!(back.encoding(), enc);
536                    assert_eq!(back.has_expiry(), expiry);
537                    assert_eq!(back.payload_at(), if expiry { 9 } else { 1 });
538                }
539            }
540        }
541    }
542
543    #[test]
544    fn the_three_fields_of_the_meta_byte_do_not_reach_into_each_other() {
545        // Forty two combinations, all of which have to come out of one byte
546        // with nothing borrowed from a neighbour. A tag that overlapped the
547        // expiry bit would read the payload at the wrong offset, which is a
548        // corrupt value rather than a wrong answer.
549        let mut seen = std::collections::HashSet::new();
550        for kind in KINDS {
551            for enc in [Encoding::Int, Encoding::Embstr, Encoding::Raw] {
552                for expiry in [false, true] {
553                    assert!(
554                        seen.insert(Meta::new(kind, enc, expiry).byte()),
555                        "{kind:?} {enc:?} {expiry} collides with something else"
556                    );
557                }
558            }
559        }
560        assert_eq!(seen.len(), KINDS.len() * 6);
561    }
562
563    #[test]
564    fn a_record_written_before_the_tag_existed_is_a_string() {
565        // Bits 3 to 7 were zero in every record M2 wrote, and zero is String.
566        // This is the whole reason String is zero, so it is worth a test that
567        // fails if somebody renumbers the enum alphabetically one day.
568        assert_eq!(Kind::String as u8, 0);
569        assert_eq!(Meta::from_byte(0b0000_0101).kind(), Kind::String);
570        assert_eq!(Meta::from_byte(0b0000_0101).encoding(), Encoding::Embstr);
571        assert!(Meta::from_byte(0b0000_0101).has_expiry());
572    }
573
574    #[test]
575    fn the_tag_is_the_number_the_file_format_uses() {
576        use yo_format::catalog::ValueType;
577        // Not a translation table, an assertion that no translation is needed.
578        // If these ever diverge, saving a key has to map between them, and the
579        // mapping is the kind of thing that gets one arm wrong.
580        assert_eq!(Kind::String as u8, ValueType::String as u8);
581        assert_eq!(Kind::Hash as u8, ValueType::Hash as u8);
582        assert_eq!(Kind::Set as u8, ValueType::Set as u8);
583        assert_eq!(Kind::Zset as u8, ValueType::Zset as u8);
584        assert_eq!(Kind::List as u8, ValueType::List as u8);
585        assert_eq!(Kind::Stream as u8, ValueType::Stream as u8);
586        assert_eq!(Kind::Array as u8, ValueType::Array as u8);
587        // And the words agree, because both of them end up on a wire.
588        for k in KINDS {
589            let v = ValueType::from_u8(k as u8).expect("the catalog knows this one");
590            assert_eq!(k.name(), v.redis_name(), "{k:?}");
591        }
592    }
593
594    #[test]
595    fn a_string_record_is_tagged_as_one() {
596        for text in [&b"42"[..], b"hello", &[b'z'; 100]] {
597            for expire in [None, Some(9_000u64)] {
598                assert_eq!(kind(&record(text, expire)), Kind::String);
599            }
600        }
601        let mut v = vec![0u8; record_len(Encoding::Int, 0, false)];
602        write_int_record(&mut v, 7, None);
603        assert_eq!(kind(&v), Kind::String);
604    }
605
606    fn record(bytes: &[u8], expire: Option<u64>) -> Vec<u8> {
607        let enc = Encoding::of(bytes);
608        let mut v = vec![0u8; record_len(enc, bytes.len(), expire.is_some())];
609        write_record(&mut v, enc, bytes, expire);
610        v
611    }
612
613    #[test]
614    fn a_record_gives_back_what_went_into_it() {
615        for text in [
616            &b""[..],
617            b"x",
618            b"0",
619            b"-1",
620            b"42",
621            b"007",
622            b"-0",
623            b"hello world",
624            &[b'z'; 100],
625        ] {
626            for expire in [None, Some(1_234_567_890_123u64)] {
627                let r = record(text, expire);
628                assert_eq!(read(&r).to_vec(), text, "{text:?} at {expire:?}");
629                assert_eq!(read(&r).len(), text.len(), "{text:?} length");
630                assert_eq!(expire_at(&r), expire, "{text:?} deadline");
631            }
632        }
633    }
634
635    #[test]
636    fn an_integer_costs_the_same_however_many_digits_it_has() {
637        let small = record(b"1", None);
638        let large = record(b"-9223372036854775808", None);
639        assert_eq!(small.len(), large.len());
640        assert_eq!(read(&large), Str::Int(i64::MIN));
641        assert_eq!(read(&large).to_vec(), b"-9223372036854775808");
642    }
643
644    #[test]
645    fn an_integer_is_incremented_where_it_lies() {
646        let mut r = record(b"41", Some(99));
647        let (n, at) = read_int_in_place(&r).expect("int encoded");
648        assert_eq!(n, 41);
649        write_int_in_place(&mut r, at, n + 1);
650        assert_eq!(read(&r), Str::Int(42));
651        // The deadline was in front of the payload and is still there.
652        assert_eq!(expire_at(&r), Some(99));
653    }
654
655    #[test]
656    fn a_string_is_not_read_as_an_integer_in_place() {
657        let r = record(b"hello", None);
658        assert!(read_int_in_place(&r).is_none());
659    }
660
661    #[test]
662    fn a_deadline_that_is_now_has_passed() {
663        let r = record(b"v", Some(100));
664        assert!(!is_expired(&r, 99));
665        assert!(is_expired(&r, 100));
666        assert!(is_expired(&r, 101));
667        let forever = record(b"v", None);
668        assert!(!is_expired(&forever, u64::MAX));
669    }
670
671    #[test]
672    fn a_value_that_is_text_can_still_be_a_number() {
673        // What `APPEND` leaves behind, and what `INCR` has to accept.
674        assert_eq!(Str::Bytes(b"10").as_int(), Some(10));
675        assert_eq!(Str::Bytes(b"10x").as_int(), None);
676        assert_eq!(Str::Int(-5).as_int(), Some(-5));
677    }
678
679    #[test]
680    fn an_unknown_encoding_reads_as_raw_bytes() {
681        // Nothing we write produces bit pattern three, but a record that has
682        // been through a future writer might, and guessing `int` on it would
683        // reinterpret eight bytes of somebody's string as a number.
684        let m = Meta::from_byte(0b11);
685        assert_eq!(m.encoding(), Encoding::Raw);
686    }
687
688    #[test]
689    fn an_unknown_type_tag_reads_as_a_string() {
690        // Six of eight patterns are used, and the other two answer String for
691        // the same reason: handing the bytes back is the harmless reading.
692        assert_eq!(Meta::from_byte(6 << 3).kind(), Kind::Array);
693        assert_eq!(Meta::from_byte(7 << 3).kind(), Kind::String);
694    }
695
696    #[test]
697    fn the_top_two_bits_are_still_free() {
698        // Whatever lands in them next must not disturb the tag, so this is the
699        // check that the tag is three bits and not five.
700        assert_eq!(Meta::from_byte(0b1100_0000).kind(), Kind::String);
701        assert_eq!(Meta::from_byte(0b1101_0000).kind(), Kind::Set);
702    }
703}