1use 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
19pub const ENTRY_HEAD_LEN: usize = 64;
21
22pub const ENTRY_TRAILER_LEN: usize = 4;
24
25pub const MAX_NAME_LEN: usize = u16::MAX as usize;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33#[repr(u8)]
34pub enum Model {
35 Kv = 0,
37 Document = 1,
39 Vector = 2,
41 Graph = 3,
43}
44
45impl Model {
46 pub const ALL: [Model; 4] = [Model::Kv, Model::Document, Model::Vector, Model::Graph];
48
49 #[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 #[must_use]
63 pub const fn as_u8(self) -> u8 {
64 self as u8
65 }
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73#[repr(u8)]
74pub enum ValueType {
75 String = 0,
77 Hash = 1,
79 Set = 2,
81 Zset = 3,
83 List = 4,
85 Stream = 5,
87 Array = 6,
89 Bitmap = 7,
91 Hll = 8,
93 Geo = 9,
95}
96
97impl ValueType {
98 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 #[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 #[must_use]
132 pub const fn as_u8(self) -> u8 {
133 self as u8
134 }
135
136 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153#[repr(u8)]
154pub enum Band {
155 Inline = 0,
157 Native = 1,
159 Partitioned = 2,
161 ChunkedCold = 3,
163}
164
165impl Band {
166 pub const ALL: [Band; 4] = [
168 Band::Inline,
169 Band::Native,
170 Band::Partitioned,
171 Band::ChunkedCold,
172 ];
173
174 #[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 #[must_use]
188 pub const fn as_u8(self) -> u8 {
189 self as u8
190 }
191}
192
193#[derive(Debug, Clone, Copy, PartialEq, Eq)]
195pub struct CatalogEntry<'a> {
196 pub model: u8,
198 pub value_type: u8,
200 pub band: u8,
202 pub p_exp: u8,
205 pub shape_tag: [u8; 16],
208 pub root_addr: u64,
210 pub element_count: u64,
212 pub bytes: u64,
214 pub db: u16,
216 pub next: u64,
218 pub name: &'a [u8],
220 pub schema: &'a [u8],
222}
223
224pub 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 #[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 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 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 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 #[must_use]
372 pub fn model(&self) -> Option<Model> {
373 Model::from_u8(self.model)
374 }
375
376 #[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 #[must_use]
387 pub fn band(&self) -> Option<Band> {
388 Band::from_u8(self.band)
389 }
390
391 #[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 #[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 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 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 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 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 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 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}