Skip to main content

yo_format/
catalog.rs

1//! The collection catalogue, which is what makes a `.yo` file self describing.
2//!
3//! `07` section 5. One entry per named collection, chained from the
4//! superblock's `catalog_addr`.
5//!
6//! The entry stores the `schema` bytes in full and not just their hash. That
7//! costs space in a structure there is one of per collection, and it buys two
8//! things. A shape mismatch can print both shapes instead of two hex digests,
9//! which is the difference between an error a user can act on and an error a
10//! user files a bug about. And a tool that has never seen the writer's source
11//! can still say what is in the file, which is the concrete form of the promise
12//! that the format is readable without us.
13
14use crate::{
15    checksum_skipping, get_u8, get_u16, get_u32, get_u64, put_u8, put_u16, put_u32, put_u64,
16};
17use yo_common::{Code, Error, Result};
18
19/// The fixed part of an entry. Name and schema follow, then the checksum.
20pub const ENTRY_HEAD_LEN: usize = 64;
21
22/// The checksum at the end.
23pub const ENTRY_TRAILER_LEN: usize = 4;
24
25/// The largest name, because `name_len` is a `u16`.
26pub const MAX_NAME_LEN: usize = u16::MAX as usize;
27
28/// Which of the four models a collection belongs to.
29///
30/// This is the axis the whole engine is organised along, so it is one byte at a
31/// fixed offset rather than something inferred from the value type.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33#[repr(u8)]
34pub enum Model {
35    /// Redis data types.
36    Kv = 0,
37    /// Documents.
38    Document = 1,
39    /// Vectors.
40    Vector = 2,
41    /// Graph.
42    Graph = 3,
43}
44
45impl Model {
46    /// Every model, in order.
47    pub const ALL: [Model; 4] = [Model::Kv, Model::Document, Model::Vector, Model::Graph];
48
49    /// The model for a byte, or `None` if this version does not know it.
50    #[must_use]
51    pub const fn from_u8(b: u8) -> Option<Model> {
52        match b {
53            0 => Some(Model::Kv),
54            1 => Some(Model::Document),
55            2 => Some(Model::Vector),
56            3 => Some(Model::Graph),
57            _ => None,
58        }
59    }
60
61    /// The byte.
62    #[must_use]
63    pub const fn as_u8(self) -> u8 {
64        self as u8
65    }
66}
67
68/// The Redis type of a key value collection.
69///
70/// Meaningful only when the model is [`Model::Kv`]. For the other three models
71/// the byte is zero and this is not the question to ask.
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73#[repr(u8)]
74pub enum ValueType {
75    /// A string.
76    String = 0,
77    /// A hash.
78    Hash = 1,
79    /// A set.
80    Set = 2,
81    /// A sorted set.
82    Zset = 3,
83    /// A list.
84    List = 4,
85    /// A stream.
86    Stream = 5,
87    /// A typed array, which RESP has no name for and the embedded API does.
88    Array = 6,
89    /// A bitmap.
90    Bitmap = 7,
91    /// A HyperLogLog.
92    Hll = 8,
93    /// A geospatial index.
94    Geo = 9,
95}
96
97impl ValueType {
98    /// Every type, in order.
99    pub const ALL: [ValueType; 10] = [
100        ValueType::String,
101        ValueType::Hash,
102        ValueType::Set,
103        ValueType::Zset,
104        ValueType::List,
105        ValueType::Stream,
106        ValueType::Array,
107        ValueType::Bitmap,
108        ValueType::Hll,
109        ValueType::Geo,
110    ];
111
112    /// The type for a byte, or `None` if this version does not know it.
113    #[must_use]
114    pub const fn from_u8(b: u8) -> Option<ValueType> {
115        match b {
116            0 => Some(ValueType::String),
117            1 => Some(ValueType::Hash),
118            2 => Some(ValueType::Set),
119            3 => Some(ValueType::Zset),
120            4 => Some(ValueType::List),
121            5 => Some(ValueType::Stream),
122            6 => Some(ValueType::Array),
123            7 => Some(ValueType::Bitmap),
124            8 => Some(ValueType::Hll),
125            9 => Some(ValueType::Geo),
126            _ => None,
127        }
128    }
129
130    /// The byte.
131    #[must_use]
132    pub const fn as_u8(self) -> u8 {
133        self as u8
134    }
135
136    /// The name `TYPE` replies with, for the ones Redis has a name for.
137    #[must_use]
138    pub const fn redis_name(self) -> &'static str {
139        match self {
140            ValueType::String | ValueType::Bitmap | ValueType::Hll => "string",
141            ValueType::Hash => "hash",
142            ValueType::Set => "set",
143            ValueType::Zset | ValueType::Geo => "zset",
144            ValueType::List => "list",
145            ValueType::Stream => "stream",
146            ValueType::Array => "array",
147        }
148    }
149}
150
151/// How a collection is laid out, which is chosen by size and not by the caller.
152#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153#[repr(u8)]
154pub enum Band {
155    /// Small enough to live inside the index entry's neighbourhood.
156    Inline = 0,
157    /// One structure, one owner, in memory.
158    Native = 1,
159    /// Split across partitions so a large collection is not one hot object.
160    Partitioned = 2,
161    /// Spilled, and read back a chunk at a time.
162    ChunkedCold = 3,
163}
164
165impl Band {
166    /// Every band, in order.
167    pub const ALL: [Band; 4] = [
168        Band::Inline,
169        Band::Native,
170        Band::Partitioned,
171        Band::ChunkedCold,
172    ];
173
174    /// The band for a byte, or `None` if this version does not know it.
175    #[must_use]
176    pub const fn from_u8(b: u8) -> Option<Band> {
177        match b {
178            0 => Some(Band::Inline),
179            1 => Some(Band::Native),
180            2 => Some(Band::Partitioned),
181            3 => Some(Band::ChunkedCold),
182            _ => None,
183        }
184    }
185
186    /// The byte.
187    #[must_use]
188    pub const fn as_u8(self) -> u8 {
189        self as u8
190    }
191}
192
193/// One catalogue entry, decoded, borrowing its name and schema from the file.
194#[derive(Debug, Clone, Copy, PartialEq, Eq)]
195pub struct CatalogEntry<'a> {
196    /// Raw model byte. Ask [`CatalogEntry::model`] what it means.
197    pub model: u8,
198    /// Raw value type byte, zero unless the model is `kv`.
199    pub value_type: u8,
200    /// Raw band byte.
201    pub band: u8,
202    /// Partition count as a base two exponent. Never 1: a two way split buys
203    /// nothing and costs an indirection, so the ladder goes one, four, sixteen.
204    pub p_exp: u8,
205    /// The 128 bit shape hash, or all zeroes for a collection that RESP created
206    /// and that therefore has no declared shape.
207    pub shape_tag: [u8; 16],
208    /// Where the collection's root lives.
209    pub root_addr: u64,
210    /// Elements, for `SCARD` and friends without walking anything.
211    pub element_count: u64,
212    /// Bytes the collection occupies.
213    pub bytes: u64,
214    /// Which logical database.
215    pub db: u16,
216    /// The next entry in the chain, or 0.
217    pub next: u64,
218    /// The collection's name.
219    pub name: &'a [u8],
220    /// The canonical shape description the `shape_tag` hashes, or empty.
221    pub schema: &'a [u8],
222}
223
224/// The bytes an entry with this name and schema needs.
225///
226/// # Errors
227///
228/// [`Code::Invalid`] if the name is longer than [`MAX_NAME_LEN`].
229pub fn entry_len(name_len: usize, schema_len: usize) -> Result<usize> {
230    if name_len > MAX_NAME_LEN {
231        return Err(Error::new(
232            Code::Invalid,
233            "a collection name is at most 65535 bytes",
234        ));
235    }
236    Ok(ENTRY_HEAD_LEN + name_len + schema_len + ENTRY_TRAILER_LEN)
237}
238
239impl<'a> CatalogEntry<'a> {
240    /// An entry for a named collection with no declared shape.
241    #[must_use]
242    pub const fn new(model: Model, name: &'a [u8]) -> CatalogEntry<'a> {
243        CatalogEntry {
244            model: model.as_u8(),
245            value_type: 0,
246            band: Band::Native.as_u8(),
247            p_exp: 0,
248            shape_tag: [0; 16],
249            root_addr: 0,
250            element_count: 0,
251            bytes: 0,
252            db: 0,
253            next: 0,
254            name,
255            schema: &[],
256        }
257    }
258
259    /// Writes the entry and its checksum, returning the length written.
260    ///
261    /// Unlike a log record this one writes its `len` up front, because a
262    /// catalogue entry is not appended to a log that a reader is scanning. It
263    /// is written to a free segment and then reached by a pointer that is
264    /// published in the next superblock flip, so the entry is either reachable
265    /// and whole or not reachable at all.
266    ///
267    /// # Errors
268    ///
269    /// [`Code::Invalid`] for an oversized name or an illegal `p_exp`,
270    /// [`Code::Full`] if `buf` is too small.
271    pub fn encode(&self, buf: &mut [u8]) -> Result<usize> {
272        if self.p_exp == 1 {
273            return Err(Error::new(
274                Code::Invalid,
275                "a two way partition split is not a thing; p_exp is 0 or 2 and up",
276            ));
277        }
278        let n = entry_len(self.name.len(), self.schema.len())?;
279        if buf.len() < n {
280            return Err(Error::new(Code::Full, "the catalogue entry does not fit")
281                .with_detail(format!("need={n} have={}", buf.len())));
282        }
283        put_u32(buf, 0, n as u32);
284        put_u8(buf, 4, self.model);
285        put_u8(buf, 5, self.value_type);
286        put_u8(buf, 6, self.band);
287        put_u8(buf, 7, self.p_exp);
288        buf[8..24].copy_from_slice(&self.shape_tag);
289        put_u64(buf, 24, self.root_addr);
290        put_u64(buf, 32, self.element_count);
291        put_u64(buf, 40, self.bytes);
292        put_u16(buf, 48, self.db);
293        put_u16(buf, 50, self.name.len() as u16);
294        put_u32(buf, 52, self.schema.len() as u32);
295        put_u64(buf, 56, self.next);
296        let name_end = ENTRY_HEAD_LEN + self.name.len();
297        buf[ENTRY_HEAD_LEN..name_end].copy_from_slice(self.name);
298        buf[name_end..name_end + self.schema.len()].copy_from_slice(self.schema);
299        let crc = checksum_skipping(&buf[..n], n - ENTRY_TRAILER_LEN);
300        put_u32(buf, n - ENTRY_TRAILER_LEN, crc);
301        Ok(n)
302    }
303
304    /// Reads the entry at the front of `bytes`.
305    ///
306    /// # Errors
307    ///
308    /// [`Code::Corrupt`] if the length is impossible, if the entry runs past
309    /// the end of `bytes`, if the name and schema do not fit inside it, or if
310    /// the checksum fails.
311    pub fn decode(bytes: &'a [u8]) -> Result<CatalogEntry<'a>> {
312        if bytes.len() < ENTRY_HEAD_LEN + ENTRY_TRAILER_LEN {
313            return Err(Error::new(Code::Corrupt, "shorter than a catalogue entry"));
314        }
315        let n = get_u32(bytes, 0) as usize;
316        if n < ENTRY_HEAD_LEN + ENTRY_TRAILER_LEN || n > bytes.len() {
317            return Err(
318                Error::new(Code::Corrupt, "the catalogue entry length is impossible")
319                    .with_detail(format!("len={n} available={}", bytes.len())),
320            );
321        }
322        let want = get_u32(bytes, n - ENTRY_TRAILER_LEN);
323        let got = checksum_skipping(&bytes[..n], n - ENTRY_TRAILER_LEN);
324        if want != got {
325            return Err(
326                Error::new(Code::Corrupt, "catalogue entry checksum mismatch")
327                    .with_detail(format!("stored={want:#010x} computed={got:#010x}")),
328            );
329        }
330
331        let name_len = get_u16(bytes, 50) as usize;
332        let schema_len = get_u32(bytes, 52) as usize;
333        // Checked even though the checksum passed. A checksum says the bytes are
334        // the bytes that were written; it does not say the writer was us. A file
335        // from a buggy or hostile writer must not be able to point a slice past
336        // the end of the entry.
337        if ENTRY_HEAD_LEN + name_len + schema_len + ENTRY_TRAILER_LEN != n {
338            return Err(
339                Error::new(Code::Corrupt, "the name and schema do not fill the entry").with_detail(
340                    format!("len={n} name_len={name_len} schema_len={schema_len}"),
341                ),
342            );
343        }
344
345        let p_exp = get_u8(bytes, 7);
346        if p_exp == 1 {
347            return Err(Error::new(Code::Corrupt, "p_exp of 1 is not a legal value"));
348        }
349
350        let mut shape_tag = [0u8; 16];
351        shape_tag.copy_from_slice(&bytes[8..24]);
352        let name_end = ENTRY_HEAD_LEN + name_len;
353
354        Ok(CatalogEntry {
355            model: get_u8(bytes, 4),
356            value_type: get_u8(bytes, 5),
357            band: get_u8(bytes, 6),
358            p_exp,
359            shape_tag,
360            root_addr: get_u64(bytes, 24),
361            element_count: get_u64(bytes, 32),
362            bytes: get_u64(bytes, 40),
363            db: get_u16(bytes, 48),
364            next: get_u64(bytes, 56),
365            name: &bytes[ENTRY_HEAD_LEN..name_end],
366            schema: &bytes[name_end..name_end + schema_len],
367        })
368    }
369
370    /// The model, if this version knows it.
371    #[must_use]
372    pub fn model(&self) -> Option<Model> {
373        Model::from_u8(self.model)
374    }
375
376    /// The value type, if the model is `kv` and this version knows the byte.
377    #[must_use]
378    pub fn value_type(&self) -> Option<ValueType> {
379        if self.model()? != Model::Kv {
380            return None;
381        }
382        ValueType::from_u8(self.value_type)
383    }
384
385    /// The band, if this version knows it.
386    #[must_use]
387    pub fn band(&self) -> Option<Band> {
388        Band::from_u8(self.band)
389    }
390
391    /// How many partitions the collection is split into.
392    #[must_use]
393    pub const fn partitions(&self) -> u32 {
394        if self.p_exp == 0 {
395            1
396        } else {
397            1u32 << self.p_exp
398        }
399    }
400
401    /// Whether the collection was declared with a shape.
402    ///
403    /// An all zero tag means it was not, which is the case for anything a RESP
404    /// client created. Shape checking has nothing to check against there and
405    /// says so rather than inventing a shape.
406    #[must_use]
407    pub fn is_shape_tagged(&self) -> bool {
408        self.shape_tag != [0u8; 16]
409    }
410}
411
412#[cfg(test)]
413mod tests {
414    use super::*;
415
416    fn an_entry<'a>(name: &'a [u8], schema: &'a [u8]) -> CatalogEntry<'a> {
417        CatalogEntry {
418            model: Model::Kv.as_u8(),
419            value_type: ValueType::Zset.as_u8(),
420            band: Band::Partitioned.as_u8(),
421            p_exp: 4,
422            shape_tag: [7; 16],
423            root_addr: 1 << 20,
424            element_count: 1234,
425            bytes: 98765,
426            db: 3,
427            next: 1 << 30,
428            name,
429            schema,
430        }
431    }
432
433    #[test]
434    fn an_entry_round_trips() {
435        let e = an_entry(b"leaderboard", b"zset<u64, f64>");
436        let mut buf = [0u8; 256];
437        let n = e.encode(&mut buf).unwrap();
438        assert_eq!(n, ENTRY_HEAD_LEN + 11 + 14 + 4);
439        assert_eq!(CatalogEntry::decode(&buf[..n]).unwrap(), e);
440    }
441
442    #[test]
443    fn every_field_lands_where_the_specification_says() {
444        let e = an_entry(b"name", b"schema");
445        let mut buf = [0u8; 256];
446        let n = e.encode(&mut buf).unwrap();
447        assert_eq!(get_u32(&buf, 0) as usize, n);
448        assert_eq!(get_u8(&buf, 4), 0, "kv is model 0");
449        assert_eq!(get_u8(&buf, 5), 3, "zset is type 3");
450        assert_eq!(get_u8(&buf, 6), 2, "partitioned is band 2");
451        assert_eq!(get_u8(&buf, 7), 4);
452        assert_eq!(&buf[8..24], &[7u8; 16]);
453        assert_eq!(get_u64(&buf, 24), 1 << 20);
454        assert_eq!(get_u64(&buf, 32), 1234);
455        assert_eq!(get_u64(&buf, 40), 98765);
456        assert_eq!(get_u16(&buf, 48), 3);
457        assert_eq!(get_u16(&buf, 50), 4);
458        assert_eq!(get_u32(&buf, 52), 6);
459        assert_eq!(get_u64(&buf, 56), 1 << 30);
460        assert_eq!(&buf[64..68], b"name");
461        assert_eq!(&buf[68..74], b"schema");
462    }
463
464    #[test]
465    fn the_schema_is_stored_whole_and_not_hashed() {
466        // The point of `07` section 5: a mismatch can print both shapes, and a
467        // tool that has never seen our source can read the file.
468        let schema = b"document { id: u64, tags: [string], score: f32 }";
469        let e = CatalogEntry {
470            schema,
471            ..an_entry(b"docs", schema)
472        };
473        let mut buf = [0u8; 256];
474        let n = e.encode(&mut buf).unwrap();
475        let back = CatalogEntry::decode(&buf[..n]).unwrap();
476        assert_eq!(back.schema, schema);
477        assert!(back.is_shape_tagged());
478    }
479
480    #[test]
481    fn an_untagged_collection_is_one_a_resp_client_made() {
482        let e = CatalogEntry::new(Model::Kv, b"made-by-SET");
483        let mut buf = [0u8; 128];
484        let n = e.encode(&mut buf).unwrap();
485        let back = CatalogEntry::decode(&buf[..n]).unwrap();
486        assert!(!back.is_shape_tagged());
487        assert_eq!(back.schema, b"");
488        assert_eq!(back.partitions(), 1);
489    }
490
491    #[test]
492    fn a_flipped_bit_anywhere_in_an_entry_is_caught() {
493        let e = an_entry(b"leaderboard", b"zset<u64, f64>");
494        let mut good = [0u8; 256];
495        let n = e.encode(&mut good).unwrap();
496        for i in 0..n {
497            let mut bad = good;
498            bad[i] ^= 0x08;
499            assert!(
500                CatalogEntry::decode(&bad[..n]).is_err(),
501                "byte {i} was not caught"
502            );
503        }
504    }
505
506    #[test]
507    fn lengths_that_do_not_add_up_are_refused_even_with_a_good_checksum() {
508        // The attack this stops: a name_len that reaches past the entry into
509        // whatever the next segment holds. A checksum does not stop it because
510        // the attacker computes the checksum too.
511        let e = an_entry(b"name", b"schema");
512        let mut buf = [0u8; 256];
513        let n = e.encode(&mut buf).unwrap();
514        put_u16(&mut buf, 50, 4000);
515        let crc = checksum_skipping(&buf[..n], n - ENTRY_TRAILER_LEN);
516        put_u32(&mut buf, n - ENTRY_TRAILER_LEN, crc);
517        let err = CatalogEntry::decode(&buf[..n]).unwrap_err();
518        assert_eq!(err.code(), Code::Corrupt);
519        assert!(err.detail().unwrap().contains("name_len=4000"));
520    }
521
522    #[test]
523    fn an_entry_that_claims_to_be_longer_than_its_buffer_is_refused() {
524        let e = an_entry(b"n", b"");
525        let mut buf = [0u8; 128];
526        let n = e.encode(&mut buf).unwrap();
527        put_u32(&mut buf, 0, 100_000);
528        let err = CatalogEntry::decode(&buf[..n]).unwrap_err();
529        assert_eq!(err.code(), Code::Corrupt);
530        assert!(err.detail().unwrap().contains("len=100000"));
531    }
532
533    #[test]
534    fn a_two_way_partition_split_is_not_a_thing() {
535        // Y5. One partition or four, never two: a two way split pays a full
536        // indirection to halve a collection, which is never the right trade.
537        let e = CatalogEntry {
538            p_exp: 1,
539            ..an_entry(b"n", b"")
540        };
541        let mut buf = [0u8; 128];
542        assert_eq!(e.encode(&mut buf).unwrap_err().code(), Code::Invalid);
543
544        let ok = CatalogEntry {
545            p_exp: 0,
546            ..an_entry(b"n", b"")
547        };
548        let n = ok.encode(&mut buf).unwrap();
549        put_u8(&mut buf, 7, 1);
550        let crc = checksum_skipping(&buf[..n], n - ENTRY_TRAILER_LEN);
551        put_u32(&mut buf, n - ENTRY_TRAILER_LEN, crc);
552        assert_eq!(
553            CatalogEntry::decode(&buf[..n]).unwrap_err().code(),
554            Code::Corrupt
555        );
556    }
557
558    #[test]
559    fn partition_counts_are_powers_of_two_from_four_up() {
560        for (p_exp, want) in [(0u8, 1u32), (2, 4), (3, 8), (4, 16), (8, 256)] {
561            let e = CatalogEntry {
562                p_exp,
563                ..an_entry(b"n", b"")
564            };
565            assert_eq!(e.partitions(), want);
566        }
567    }
568
569    #[test]
570    fn a_chain_of_entries_walks() {
571        let mut buf = [0u8; 1024];
572        // Not offset zero. In a real file the catalogue lives past `DATA_START`
573        // so address zero can mean "no next entry", and a test that put the
574        // first entry at zero would be testing a layout that cannot happen.
575        let mut at = 64usize;
576        let mut offsets = Vec::new();
577        for i in 0..5usize {
578            let name = format!("collection{i}");
579            let e = CatalogEntry {
580                next: 0,
581                ..CatalogEntry::new(Model::Document, name.as_bytes())
582            };
583            let n = e.encode(&mut buf[at..]).unwrap();
584            offsets.push((at, n));
585            at += n;
586        }
587        // Link them backwards, which is how the writer does it: an entry points
588        // at the one written before it, and the superblock points at the last.
589        for i in 1..offsets.len() {
590            let (off, n) = offsets[i];
591            let prev = offsets[i - 1].0 as u64;
592            put_u64(&mut buf[off..], 56, prev);
593            let crc = checksum_skipping(&buf[off..off + n], n - ENTRY_TRAILER_LEN);
594            put_u32(&mut buf[off..], n - ENTRY_TRAILER_LEN, crc);
595        }
596
597        let mut seen = Vec::new();
598        let mut cursor = offsets.last().unwrap().0;
599        loop {
600            let e = CatalogEntry::decode(&buf[cursor..]).unwrap();
601            seen.push(String::from_utf8(e.name.to_vec()).unwrap());
602            if e.next == 0 {
603                break;
604            }
605            cursor = e.next as usize;
606        }
607        seen.reverse();
608        assert_eq!(
609            seen,
610            (0..5).map(|i| format!("collection{i}")).collect::<Vec<_>>()
611        );
612    }
613
614    #[test]
615    fn unknown_bytes_are_questions_with_no_answer_rather_than_errors() {
616        let e = CatalogEntry {
617            model: 9,
618            value_type: 200,
619            band: 250,
620            ..an_entry(b"future", b"")
621        };
622        let mut buf = [0u8; 128];
623        let n = e.encode(&mut buf).unwrap();
624        let back = CatalogEntry::decode(&buf[..n]).unwrap();
625        assert_eq!(back.model(), None);
626        assert_eq!(back.value_type(), None);
627        assert_eq!(back.band(), None);
628        assert_eq!(
629            back.model, 9,
630            "the raw byte survives so it can be copied on"
631        );
632    }
633
634    #[test]
635    fn a_value_type_only_means_something_for_the_kv_model() {
636        let e = CatalogEntry {
637            model: Model::Vector.as_u8(),
638            value_type: ValueType::Hash.as_u8(),
639            ..an_entry(b"embeddings", b"")
640        };
641        assert_eq!(e.value_type(), None, "a vector has no Redis type");
642        assert_eq!(e.model(), Some(Model::Vector));
643    }
644
645    #[test]
646    fn the_enums_round_trip_and_stop_where_the_specification_stops() {
647        for m in Model::ALL {
648            assert_eq!(Model::from_u8(m.as_u8()), Some(m));
649        }
650        assert_eq!(Model::from_u8(4), None);
651        for t in ValueType::ALL {
652            assert_eq!(ValueType::from_u8(t.as_u8()), Some(t));
653        }
654        assert_eq!(ValueType::from_u8(10), None);
655        for b in Band::ALL {
656            assert_eq!(Band::from_u8(b.as_u8()), Some(b));
657        }
658        assert_eq!(Band::from_u8(4), None);
659    }
660
661    #[test]
662    fn type_replies_the_way_redis_replies() {
663        // Bitmaps and HyperLogLogs are strings to a Redis client, and a geo
664        // index is a zset. A client that switches on TYPE has to see what it
665        // would see from Redis or the compatibility claim is not true.
666        assert_eq!(ValueType::String.redis_name(), "string");
667        assert_eq!(ValueType::Bitmap.redis_name(), "string");
668        assert_eq!(ValueType::Hll.redis_name(), "string");
669        assert_eq!(ValueType::Geo.redis_name(), "zset");
670        assert_eq!(ValueType::Zset.redis_name(), "zset");
671        assert_eq!(ValueType::Stream.redis_name(), "stream");
672    }
673
674    #[test]
675    fn a_buffer_with_no_room_says_how_much_it_needed() {
676        let e = an_entry(b"a long collection name", b"");
677        let mut buf = [0u8; 32];
678        let err = e.encode(&mut buf).unwrap_err();
679        assert_eq!(err.code(), Code::Full);
680        assert!(err.detail().unwrap().contains("have=32"));
681    }
682
683    #[test]
684    fn a_short_buffer_is_an_error_and_not_a_panic() {
685        assert_eq!(
686            CatalogEntry::decode(&[0u8; 16]).unwrap_err().code(),
687            Code::Corrupt
688        );
689    }
690}