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}