use std::borrow::Cow;
use yo_common::crc::crc64;
use yo_common::num::{self, DIGITS_MAX};
use crate::hash::{self, Hash};
use crate::intset::Intset;
use crate::keys::{Body, Record};
use crate::list::{self, List};
use crate::listpack::{Entry, Listpack};
use crate::set::{self, Set};
use crate::zset::{self, Zset};
pub const VERSION: u16 = 12;
pub const READS_UP_TO: u16 = 15;
const FOOTER: usize = 10;
const T_STRING: u8 = 0;
const T_LIST: u8 = 1;
const T_SET: u8 = 2;
const T_ZSET: u8 = 3;
const T_HASH: u8 = 4;
const T_ZSET_2: u8 = 5;
const T_SET_INTSET: u8 = 11;
const T_HASH_LISTPACK: u8 = 16;
const T_ZSET_LISTPACK: u8 = 17;
const T_LIST_QUICKLIST_2: u8 = 18;
const T_SET_LISTPACK: u8 = 20;
const T_HASH_METADATA: u8 = 24;
const T_HASH_LISTPACK_EX: u8 = 25;
const LEN_6BIT: u8 = 0;
const LEN_14BIT: u8 = 1;
const LEN_32BIT: u8 = 0x80;
const LEN_64BIT: u8 = 0x81;
const LEN_ENCODED: u8 = 3;
const ENC_INT8: u64 = 0;
const ENC_INT16: u64 = 1;
const ENC_INT32: u64 = 2;
const ENC_LZF: u64 = 3;
const NODE_PACKED: u64 = 2;
const NODE_PLAIN: u64 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Bad {
Footer,
Format,
}
#[derive(Debug, Clone, Copy)]
pub struct Limits<'a> {
pub set: &'a set::Limits,
pub hash: &'a hash::Limits,
pub list: &'a list::Limits,
pub zset: &'a zset::Limits,
}
fn seal(mut body: Vec<u8>) -> Vec<u8> {
body.extend_from_slice(&VERSION.to_le_bytes());
let crc = crc64(0, &body);
body.extend_from_slice(&crc.to_le_bytes());
body
}
fn unseal(payload: &[u8]) -> Result<&[u8], Bad> {
if payload.len() < FOOTER {
return Err(Bad::Footer);
}
let split = payload.len() - FOOTER;
let (body, foot) = payload.split_at(split);
let version = u16::from_le_bytes([foot[0], foot[1]]);
if version > READS_UP_TO {
return Err(Bad::Footer);
}
let stored = u64::from_le_bytes(foot[2..].try_into().expect("ten byte footer, eight left"));
if stored != crc64(0, &payload[..payload.len() - 8]) {
return Err(Bad::Footer);
}
Ok(body)
}
pub(crate) fn dump(rec: &Record) -> Option<Vec<u8>> {
let mut out = Vec::new();
match rec.body() {
Body::Foreign(_) => return None,
Body::String(bytes) => {
out.push(T_STRING);
put_str(&mut out, bytes);
}
Body::List(list) => {
out.push(T_LIST);
put_len(&mut out, list.len() as u64);
for element in list.iter() {
put_entry(&mut out, element);
}
}
Body::Set(set) => match (set.encoding(), set.packed_bytes()) {
(set::Encoding::Intset, Some(blob)) => {
out.push(T_SET_INTSET);
put_str(&mut out, blob);
}
(set::Encoding::Listpack, Some(blob)) => {
out.push(T_SET_LISTPACK);
put_str(&mut out, blob);
}
_ => {
out.push(T_SET);
put_len(&mut out, set.len() as u64);
for member in set.iter() {
put_entry(&mut out, member);
}
}
},
Body::Zset(zset) => match zset.packed_bytes() {
Some(blob) => {
out.push(T_ZSET_LISTPACK);
put_str(&mut out, blob);
}
None => {
out.push(T_ZSET_2);
put_len(&mut out, zset.len() as u64);
zset.walk(0, zset.len(), false, |member, score| {
put_entry(&mut out, member);
out.extend_from_slice(&score.to_le_bytes());
});
}
},
Body::Hash(hash) => put_hash(&mut out, hash),
Body::Array(_) | Body::Stream(_) => return None,
}
Some(seal(out))
}
fn put_hash(out: &mut Vec<u8>, hash: &Hash) {
let Some(soonest) = hash.soonest_deadline() else {
if let Some(blob) = hash.packed_bytes() {
out.push(T_HASH_LISTPACK);
put_str(out, blob);
return;
}
out.push(T_HASH);
put_len(out, hash.len() as u64);
for (field, value) in hash.iter() {
put_entry(out, field);
put_entry(out, value);
}
return;
};
out.push(T_HASH_METADATA);
out.extend_from_slice(&soonest.to_le_bytes());
put_len(out, hash.len() as u64);
for i in 0..hash.len() {
let (field, value) = hash.at(i).expect("index is under the length");
let ttl = match hash.deadline_at(i) {
Some(at) => at.saturating_sub(soonest) + 1,
None => 0,
};
put_len(out, ttl);
put_entry(out, field);
put_entry(out, value);
}
}
fn put_len(out: &mut Vec<u8>, n: u64) {
if n < 1 << 6 {
out.push((LEN_6BIT << 6) | n as u8);
} else if n < 1 << 14 {
out.push((LEN_14BIT << 6) | (n >> 8) as u8);
out.push(n as u8);
} else if n <= u64::from(u32::MAX) {
out.push(LEN_32BIT);
out.extend_from_slice(&(n as u32).to_be_bytes());
} else {
out.push(LEN_64BIT);
out.extend_from_slice(&n.to_be_bytes());
}
}
fn put_str(out: &mut Vec<u8>, s: &[u8]) {
if s.len() <= 11
&& let Some(n) = num::parse_i64(s)
&& let mut buf = [0u8; DIGITS_MAX]
&& num::i64_digits(&mut buf, n) == s
&& put_int(out, n)
{
return;
}
put_len(out, s.len() as u64);
out.extend_from_slice(s);
}
fn put_entry(out: &mut Vec<u8>, entry: Entry<'_>) {
match entry {
Entry::Int(n) => {
if !put_int(out, n) {
let mut buf = [0u8; DIGITS_MAX];
let digits = num::i64_digits(&mut buf, n);
put_len(out, digits.len() as u64);
out.extend_from_slice(digits);
}
}
Entry::Str(s) => put_str(out, s),
}
}
fn put_int(out: &mut Vec<u8>, n: i64) -> bool {
if let Ok(v) = i8::try_from(n) {
out.push((LEN_ENCODED << 6) | ENC_INT8 as u8);
out.push(v as u8);
} else if let Ok(v) = i16::try_from(n) {
out.push((LEN_ENCODED << 6) | ENC_INT16 as u8);
out.extend_from_slice(&v.to_le_bytes());
} else if let Ok(v) = i32::try_from(n) {
out.push((LEN_ENCODED << 6) | ENC_INT32 as u8);
out.extend_from_slice(&v.to_le_bytes());
} else {
return false;
}
true
}
struct Reader<'a> {
buf: &'a [u8],
at: usize,
}
impl<'a> Reader<'a> {
const fn new(buf: &'a [u8]) -> Reader<'a> {
Reader { buf, at: 0 }
}
fn byte(&mut self) -> Result<u8, Bad> {
let b = *self.buf.get(self.at).ok_or(Bad::Format)?;
self.at += 1;
Ok(b)
}
fn take(&mut self, n: usize) -> Result<&'a [u8], Bad> {
let end = self.at.checked_add(n).ok_or(Bad::Format)?;
let s = self.buf.get(self.at..end).ok_or(Bad::Format)?;
self.at = end;
Ok(s)
}
fn count(&mut self) -> Result<usize, Bad> {
let n = non_empty(self.len()?)?;
if n > self.buf.len() - self.at {
return Err(Bad::Format);
}
Ok(n)
}
fn len(&mut self) -> Result<usize, Bad> {
match self.len_or_encoding()? {
(n, false) => usize::try_from(n).map_err(|_| Bad::Format),
(_, true) => Err(Bad::Format),
}
}
fn len_or_encoding(&mut self) -> Result<(u64, bool), Bad> {
let first = self.byte()?;
match first >> 6 {
LEN_6BIT => Ok((u64::from(first & 0x3f), false)),
LEN_14BIT => {
let second = self.byte()?;
Ok(((u64::from(first & 0x3f) << 8) | u64::from(second), false))
}
LEN_ENCODED => Ok((u64::from(first & 0x3f), true)),
_ => match first {
LEN_32BIT => {
let b = self.take(4)?;
Ok((
u64::from(u32::from_be_bytes(b.try_into().expect("four bytes"))),
false,
))
}
LEN_64BIT => {
let b = self.take(8)?;
Ok((
u64::from_be_bytes(b.try_into().expect("eight bytes")),
false,
))
}
_ => Err(Bad::Format),
},
}
}
fn str(&mut self) -> Result<Cow<'a, [u8]>, Bad> {
let (n, encoded) = self.len_or_encoding()?;
if !encoded {
let n = usize::try_from(n).map_err(|_| Bad::Format)?;
return Ok(Cow::Borrowed(self.take(n)?));
}
let value = match n {
ENC_INT8 => i64::from(self.byte()? as i8),
ENC_INT16 => {
let b = self.take(2)?;
i64::from(i16::from_le_bytes(b.try_into().expect("two bytes")))
}
ENC_INT32 => {
let b = self.take(4)?;
i64::from(i32::from_le_bytes(b.try_into().expect("four bytes")))
}
ENC_LZF => {
let packed = self.len()?;
let plain = self.len()?;
let bytes = self.take(packed)?;
return unpack(bytes, plain).map(Cow::Owned).ok_or(Bad::Format);
}
_ => return Err(Bad::Format),
};
let mut buf = [0u8; DIGITS_MAX];
Ok(Cow::Owned(num::i64_digits(&mut buf, value).to_vec()))
}
fn double(&mut self) -> Result<f64, Bad> {
let b = self.take(8)?;
Ok(f64::from_le_bytes(b.try_into().expect("eight bytes")))
}
fn double_text(&mut self) -> Result<f64, Bad> {
match self.byte()? {
255 => Ok(f64::NEG_INFINITY),
254 => Ok(f64::INFINITY),
253 => Ok(f64::NAN),
n => {
let digits = self.take(n as usize)?;
num::parse_f64(digits).ok_or(Bad::Format)
}
}
}
const fn done(&self) -> bool {
self.at == self.buf.len()
}
}
fn unpack(packed: &[u8], plain: usize) -> Option<Vec<u8>> {
let mut out = Vec::with_capacity(plain.min(1 << 20));
let mut i = 0;
while i < packed.len() {
let ctrl = usize::from(packed[i]);
i += 1;
if ctrl < 32 {
let run = ctrl + 1;
let end = i.checked_add(run)?;
if end > packed.len() || out.len() + run > plain {
return None;
}
out.extend_from_slice(&packed[i..end]);
i = end;
} else {
let mut run = ctrl >> 5;
if run == 7 {
run += usize::from(*packed.get(i)?);
i += 1;
}
let back = ((ctrl & 0x1f) << 8) + usize::from(*packed.get(i)?) + 1;
i += 1;
let run = run + 2;
if back > out.len() || out.len() + run > plain {
return None;
}
let from = out.len() - back;
for at in from..from + run {
out.push(out[at]);
}
}
}
(out.len() == plain).then_some(out)
}
pub(crate) fn load(payload: &[u8], limits: Limits<'_>, now: u64) -> Result<Body, Bad> {
let body = unseal(payload)?;
let mut r = Reader::new(body);
let kind = r.byte()?;
let value = match kind {
T_STRING => Body::String(r.str()?.into_owned()),
T_LIST => read_list(&mut r, limits.list)?,
T_LIST_QUICKLIST_2 => read_quicklist(&mut r, limits.list)?,
T_SET => read_set(&mut r, limits.set)?,
T_SET_INTSET => read_intset(&mut r, limits.set)?,
T_SET_LISTPACK => read_set_listpack(&mut r, limits.set)?,
T_ZSET | T_ZSET_2 => read_zset(&mut r, limits.zset, kind == T_ZSET_2)?,
T_ZSET_LISTPACK => read_zset_listpack(&mut r, limits.zset)?,
T_HASH => read_hash(&mut r, limits.hash)?,
T_HASH_METADATA => read_hash_metadata(&mut r, limits.hash, now)?,
T_HASH_LISTPACK => read_hash_listpack(&mut r, limits.hash, false, now)?,
T_HASH_LISTPACK_EX => read_hash_listpack(&mut r, limits.hash, true, now)?,
_ => return Err(Bad::Format),
};
if !r.done() {
return Err(Bad::Format);
}
Ok(value)
}
fn non_empty(n: usize) -> Result<usize, Bad> {
if n == 0 { Err(Bad::Format) } else { Ok(n) }
}
fn read_list(r: &mut Reader<'_>, limits: &list::Limits) -> Result<Body, Bad> {
let n = r.count()?;
let mut list = List::new();
for _ in 0..n {
list.push_back(&r.str()?, limits);
}
Ok(Body::List(list))
}
fn read_quicklist(r: &mut Reader<'_>, limits: &list::Limits) -> Result<Body, Bad> {
let nodes = r.count()?;
let mut list = List::new();
for _ in 0..nodes {
let container = r.len_or_encoding()?.0;
let blob = r.str()?;
match container {
NODE_PLAIN => list.push_back(&blob, limits),
NODE_PACKED => {
let lp = Listpack::from_bytes(&blob).map_err(|_| Bad::Format)?;
let mut buf = [0u8; DIGITS_MAX];
for entry in lp.iter() {
list.push_back(text(entry, &mut buf), limits);
}
}
_ => return Err(Bad::Format),
}
}
if list.is_empty() {
return Err(Bad::Format);
}
Ok(Body::List(list))
}
fn read_set(r: &mut Reader<'_>, limits: &set::Limits) -> Result<Body, Bad> {
let n = r.count()?;
let first = r.str()?;
let mut set = Set::with_hint(&first, n, limits);
set.add(&first, limits);
for _ in 1..n {
set.add(&r.str()?, limits);
}
Ok(Body::Set(set))
}
fn read_intset(r: &mut Reader<'_>, limits: &set::Limits) -> Result<Body, Bad> {
let blob = r.str()?;
let ints = Intset::from_bytes(&blob).map_err(|_| Bad::Format)?;
non_empty(ints.len())?;
let mut buf = [0u8; DIGITS_MAX];
let mut set = Set::with_hint(num::i64_digits(&mut buf, ints.at(0)), ints.len(), limits);
for v in ints.iter() {
set.add(num::i64_digits(&mut buf, v), limits);
}
Ok(Body::Set(set))
}
fn read_set_listpack(r: &mut Reader<'_>, limits: &set::Limits) -> Result<Body, Bad> {
let blob = r.str()?;
let lp = Listpack::from_bytes(&blob).map_err(|_| Bad::Format)?;
non_empty(lp.len())?;
let mut buf = [0u8; DIGITS_MAX];
let first = text(lp.get(0).ok_or(Bad::Format)?, &mut buf).to_vec();
let mut set = Set::with_hint(&first, lp.len(), limits);
for entry in lp.iter() {
set.add(text(entry, &mut buf), limits);
}
Ok(Body::Set(set))
}
fn read_zset(r: &mut Reader<'_>, limits: &zset::Limits, binary: bool) -> Result<Body, Bad> {
let n = r.count()?;
let mut zset = Zset::with_hint(n, limits);
for _ in 0..n {
let member = r.str()?;
let score = if binary {
r.double()?
} else {
r.double_text()?
};
zset.add(&member, score, limits);
}
Ok(Body::Zset(zset))
}
fn read_zset_listpack(r: &mut Reader<'_>, limits: &zset::Limits) -> Result<Body, Bad> {
let blob = r.str()?;
let lp = Listpack::from_bytes(&blob).map_err(|_| Bad::Format)?;
if lp.is_empty() || lp.len() % 2 != 0 {
return Err(Bad::Format);
}
let lp = match Zset::from_packed(lp, limits) {
Ok(zset) => return Ok(Body::Zset(zset)),
Err(lp) => lp,
};
let mut zset = Zset::with_hint(lp.len() / 2, limits);
let mut member = [0u8; DIGITS_MAX];
let mut score = [0u8; DIGITS_MAX];
let mut walk = lp.iter();
while let Some(entry) = walk.next() {
let name = text(entry, &mut member).to_vec();
let at = text(walk.next().ok_or(Bad::Format)?, &mut score);
let at = num::parse_f64(at).ok_or(Bad::Format)?;
zset.add(&name, at, limits);
}
Ok(Body::Zset(zset))
}
fn read_hash(r: &mut Reader<'_>, limits: &hash::Limits) -> Result<Body, Bad> {
let n = r.count()?;
let mut hash = Hash::with_hint(n, limits);
for _ in 0..n {
let field = r.str()?;
let value = r.str()?;
hash.set(&field, &value, limits);
}
Ok(Body::Hash(hash))
}
fn read_hash_metadata(r: &mut Reader<'_>, limits: &hash::Limits, now: u64) -> Result<Body, Bad> {
let soonest = u64::from_le_bytes(r.take(8)?.try_into().expect("eight bytes"));
let n = r.count()?;
let mut hash = Hash::with_hint(n, limits);
for _ in 0..n {
let ttl = r.len_or_encoding()?.0;
let field = r.str()?;
let value = r.str()?;
put_field(
&mut hash,
&field,
&value,
deadline(soonest, ttl),
limits,
now,
);
}
if hash.is_empty() {
return Err(Bad::Format);
}
Ok(Body::Hash(hash))
}
fn read_hash_listpack(
r: &mut Reader<'_>,
limits: &hash::Limits,
with_ttl: bool,
now: u64,
) -> Result<Body, Bad> {
let soonest = if with_ttl {
u64::from_le_bytes(r.take(8)?.try_into().expect("eight bytes"))
} else {
0
};
let blob = r.str()?;
let lp = Listpack::from_bytes(&blob).map_err(|_| Bad::Format)?;
let step = if with_ttl { 3 } else { 2 };
if lp.is_empty() || lp.len() % step != 0 {
return Err(Bad::Format);
}
let lp = if with_ttl {
lp
} else {
match Hash::from_packed(lp, limits) {
Ok(hash) => return Ok(Body::Hash(hash)),
Err(lp) => lp,
}
};
let mut hash = Hash::with_hint(lp.len() / step, limits);
let mut field_buf = [0u8; DIGITS_MAX];
let mut value_buf = [0u8; DIGITS_MAX];
let mut walk = lp.iter();
while let Some(entry) = walk.next() {
let field = text(entry, &mut field_buf).to_vec();
let value = text(walk.next().ok_or(Bad::Format)?, &mut value_buf).to_vec();
let at = if with_ttl {
match walk.next().ok_or(Bad::Format)? {
Entry::Int(0) => None,
Entry::Int(n) => Some(u64::try_from(n).map_err(|_| Bad::Format)?),
Entry::Str(_) => return Err(Bad::Format),
}
} else {
None
};
put_field(&mut hash, &field, &value, at, limits, now);
}
let _ = soonest;
if hash.is_empty() {
return Err(Bad::Format);
}
Ok(Body::Hash(hash))
}
const fn deadline(soonest: u64, ttl: u64) -> Option<u64> {
if ttl == 0 {
None
} else {
Some(soonest + ttl - 1)
}
}
fn put_field(
hash: &mut Hash,
field: &[u8],
value: &[u8],
at: Option<u64>,
limits: &hash::Limits,
now: u64,
) {
if let Some(at) = at
&& at <= now
{
return;
}
hash.set(field, value, limits);
if let Some(at) = at {
hash.expire(field, at, crate::ttl::Cond::Always, now);
}
}
fn text<'a>(entry: Entry<'a>, buf: &'a mut [u8; DIGITS_MAX]) -> &'a [u8] {
match entry {
Entry::Int(n) => num::i64_digits(buf, n),
Entry::Str(s) => s,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ttl::{Ask, Cond};
fn limits() -> (set::Limits, hash::Limits, list::Limits, zset::Limits) {
(
set::Limits::DEFAULT,
hash::Limits::DEFAULT,
list::Limits::default(),
zset::Limits::DEFAULT,
)
}
fn round_trip(body: Body) -> Body {
let (s, h, l, z) = limits();
let all = Limits {
set: &s,
hash: &h,
list: &l,
zset: &z,
};
let rec = Record::new(body, None);
let payload = dump(&rec).expect("this type has an RDB shape");
load(&payload, all, 0).expect("what we wrote we can read")
}
fn set_of(members: &[&[u8]]) -> Set {
let l = set::Limits::DEFAULT;
let mut set = Set::with_hint(members[0], members.len(), &l);
for m in members {
set.add(m, &l);
}
set
}
#[test]
fn a_payload_is_ten_bytes_longer_than_the_object() {
let rec = Record::new(Body::String(b"hello".to_vec()), None);
let payload = dump(&rec).expect("a string has an RDB shape");
assert_eq!(payload.len(), 1 + 1 + 5 + FOOTER);
assert_eq!(payload[0], T_STRING);
}
#[test]
fn a_string_that_is_a_number_goes_out_as_one() {
let rec = Record::new(Body::String(b"1234".to_vec()), None);
let payload = dump(&rec).expect("a string has an RDB shape");
assert_eq!(payload.len(), 1 + 1 + 2 + FOOTER);
let Body::String(back) = round_trip(Body::String(b"1234".to_vec())) else {
panic!("a string came back as something else");
};
assert_eq!(back, b"1234");
}
#[test]
fn a_string_that_only_looks_like_a_number_stays_text() {
for s in [&b"007"[..], b"+7", b"-0", b" 7", b"9223372036854775808"] {
let Body::String(back) = round_trip(Body::String(s.to_vec())) else {
panic!("a string came back as something else");
};
assert_eq!(back, s, "{} did not survive", String::from_utf8_lossy(s));
}
}
#[test]
fn every_integer_width_survives() {
for n in [0i64, 1, -1, 127, -128, 128, -129, 32767, -32768, 32768] {
let mut buf = [0u8; DIGITS_MAX];
let s = num::i64_digits(&mut buf, n).to_vec();
let Body::String(back) = round_trip(Body::String(s.clone())) else {
panic!("a string came back as something else");
};
assert_eq!(back, s, "{n} did not survive");
}
let big = b"2147483648".to_vec();
let Body::String(back) = round_trip(Body::String(big.clone())) else {
panic!("a string came back as something else");
};
assert_eq!(back, big);
}
#[test]
fn a_set_comes_back_with_the_same_members() {
let set = set_of(&[b"alpha", b"beta", b"gamma"]);
let Body::Set(back) = round_trip(Body::Set(set)) else {
panic!("a set came back as something else");
};
assert_eq!(back.len(), 3);
for m in [&b"alpha"[..], b"beta", b"gamma"] {
assert!(
back.contains(m),
"{} went missing",
String::from_utf8_lossy(m)
);
}
}
#[test]
fn an_integer_set_comes_back_as_an_integer_set() {
let set = set_of(&[b"1", b"2", b"3"]);
let was = set.encoding();
let Body::Set(back) = round_trip(Body::Set(set)) else {
panic!("a set came back as something else");
};
assert_eq!(back.encoding(), was);
assert_eq!(back.len(), 3);
assert!(back.contains(b"2"));
}
#[test]
fn a_list_keeps_its_order() {
let l = list::Limits::default();
let mut list = List::new();
for v in [&b"one"[..], b"two", b"three"] {
list.push_back(v, &l);
}
let Body::List(back) = round_trip(Body::List(list)) else {
panic!("a list came back as something else");
};
let mut seen = Vec::new();
for e in back.iter() {
let mut buf = Vec::new();
e.write_to(&mut buf);
seen.push(buf);
}
assert_eq!(
seen,
vec![b"one".to_vec(), b"two".to_vec(), b"three".to_vec()]
);
}
#[test]
fn a_sorted_set_keeps_its_scores() {
let l = zset::Limits::DEFAULT;
let mut zset = Zset::new();
zset.add(b"a", 1.5, &l);
zset.add(b"b", -2.0, &l);
zset.add(b"c", f64::INFINITY, &l);
let Body::Zset(back) = round_trip(Body::Zset(zset)) else {
panic!("a sorted set came back as something else");
};
assert_eq!(back.score(b"a"), Some(1.5));
assert_eq!(back.score(b"b"), Some(-2.0));
assert_eq!(back.score(b"c"), Some(f64::INFINITY));
}
#[test]
fn a_hash_with_no_deadlines_uses_the_plain_type() {
let l = hash::Limits::DEFAULT;
let mut hash = Hash::new();
for i in 0..1000 {
hash.set(format!("f{i}").as_bytes(), b"1", &l);
}
assert_eq!(hash.encoding(), hash::Encoding::Hashtable);
let rec = Record::new(Body::Hash(hash.clone()), None);
assert_eq!(dump(&rec).expect("a hash has an RDB shape")[0], T_HASH);
let Body::Hash(back) = round_trip(Body::Hash(hash)) else {
panic!("a hash came back as something else");
};
assert_eq!(back.len(), 1000);
assert_eq!(back.get(b"f7").map(|v| v.byte_len()), Some(1));
}
#[test]
fn a_packed_value_goes_out_as_its_blob() {
let (sl, hl, _, zl) = limits();
let mut hash = Hash::new();
hash.set(b"one", b"1", &hl);
hash.set(b"two", b"2", &hl);
assert_eq!(hash.encoding(), hash::Encoding::Listpack);
let mut set = Set::new();
set.add(b"alpha", &sl);
set.add(b"beta", &sl);
assert_eq!(set.encoding(), set::Encoding::Listpack);
let mut ints = Set::new();
ints.add(b"1", &sl);
ints.add(b"9", &sl);
assert_eq!(ints.encoding(), set::Encoding::Intset);
let mut zset = Zset::new();
zset.add(b"a", 1.5, &zl);
assert_eq!(zset.encoding(), zset::Encoding::Listpack);
for (want, body) in [
(T_HASH_LISTPACK, Body::Hash(hash)),
(T_SET_LISTPACK, Body::Set(set)),
(T_SET_INTSET, Body::Set(ints)),
(T_ZSET_LISTPACK, Body::Zset(zset)),
] {
let rec = Record::new(body.clone(), None);
let payload = dump(&rec).expect("a packed value has an RDB shape");
assert_eq!(payload[0], want, "wrong type byte for {body:?}");
assert_eq!(
format!("{:?}", round_trip(body.clone())),
format!("{body:?}")
);
}
}
#[test]
fn a_widened_hash_is_not_copied() {
let l = hash::Limits::DEFAULT;
let mut hash = Hash::new();
hash.set(b"one", b"1", &l);
hash.expire(b"one", 5_000, Cond::Always, 0);
hash.persist(b"one");
hash.reap(6_000);
assert_eq!(hash.encoding(), hash::Encoding::ListpackEx);
assert_eq!(hash.soonest_deadline(), None);
let rec = Record::new(Body::Hash(hash.clone()), None);
assert_eq!(dump(&rec).expect("a hash has an RDB shape")[0], T_HASH);
let Body::Hash(back) = round_trip(Body::Hash(hash)) else {
panic!("a hash came back as something else");
};
assert_eq!(back.len(), 1);
assert_eq!(back.deadline(b"one"), Ask::NoDeadline);
}
#[test]
fn a_hash_carries_its_field_deadlines_across() {
let l = hash::Limits::DEFAULT;
let mut hash = Hash::new();
hash.set(b"keep", b"1", &l);
hash.set(b"timed", b"2", &l);
hash.expire(b"timed", 5_000, Cond::Always, 1_000);
let rec = Record::new(Body::Hash(hash.clone()), None);
assert_eq!(
dump(&rec).expect("a hash has an RDB shape")[0],
T_HASH_METADATA
);
let (s, h, li, z) = limits();
let all = Limits {
set: &s,
hash: &h,
list: &li,
zset: &z,
};
let payload = dump(&rec).expect("a hash has an RDB shape");
let Body::Hash(back) = load(&payload, all, 1_000).expect("it reads back") else {
panic!("a hash came back as something else");
};
assert_eq!(back.len(), 2);
assert_eq!(back.deadline(b"timed"), crate::ttl::Ask::At(5_000));
assert_eq!(back.deadline(b"keep"), crate::ttl::Ask::NoDeadline);
}
#[test]
fn a_field_that_expired_in_transit_does_not_come_back() {
let l = hash::Limits::DEFAULT;
let mut hash = Hash::new();
hash.set(b"keep", b"1", &l);
hash.set(b"gone", b"2", &l);
hash.expire(b"gone", 5_000, Cond::Always, 1_000);
let rec = Record::new(Body::Hash(hash), None);
let payload = dump(&rec).expect("a hash has an RDB shape");
let (s, h, li, z) = limits();
let all = Limits {
set: &s,
hash: &h,
list: &li,
zset: &z,
};
let Body::Hash(back) = load(&payload, all, 9_000).expect("it reads back") else {
panic!("a hash came back as something else");
};
assert_eq!(back.len(), 1);
assert!(back.contains(b"keep"));
assert!(!back.contains(b"gone"));
}
#[test]
fn a_flipped_byte_is_caught() {
let rec = Record::new(Body::String(b"hello there".to_vec()), None);
let good = dump(&rec).expect("a string has an RDB shape");
for i in 0..good.len() {
let mut bad = good.clone();
bad[i] ^= 1;
let (s, h, l, z) = limits();
let all = Limits {
set: &s,
hash: &h,
list: &l,
zset: &z,
};
assert!(
load(&bad, all, 0).is_err(),
"byte {i} could be changed without anything noticing"
);
}
}
#[test]
fn a_payload_from_a_newer_server_is_refused() {
let rec = Record::new(Body::String(b"hello".to_vec()), None);
let mut payload = dump(&rec).expect("a string has an RDB shape");
let n = payload.len();
payload[n - 10] = 99;
let crc = crc64(0, &payload[..n - 8]);
payload[n - 8..].copy_from_slice(&crc.to_le_bytes());
let (s, h, l, z) = limits();
let all = Limits {
set: &s,
hash: &h,
list: &l,
zset: &z,
};
assert_eq!(load(&payload, all, 0).unwrap_err(), Bad::Footer);
}
#[test]
fn a_payload_is_read_up_to_the_version_that_has_been_checked() {
let rec = Record::new(Body::String(b"hello".to_vec()), None);
let (s, h, l, z) = limits();
let all = Limits {
set: &s,
hash: &h,
list: &l,
zset: &z,
};
for stamp in [VERSION, READS_UP_TO, READS_UP_TO + 1] {
let mut payload = dump(&rec).expect("a string has an RDB shape");
let n = payload.len();
payload[n - 10..n - 8].copy_from_slice(&stamp.to_le_bytes());
let crc = crc64(0, &payload[..n - 8]);
payload[n - 8..].copy_from_slice(&crc.to_le_bytes());
let got = load(&payload, all, 0);
if stamp > READS_UP_TO {
assert_eq!(got.unwrap_err(), Bad::Footer, "{stamp} should be refused");
} else {
assert!(got.is_ok(), "{stamp} should be read");
}
}
const {
assert!(
VERSION <= READS_UP_TO,
"a server that cannot read what it writes is no use to anybody"
)
};
}
#[test]
fn a_payload_shorter_than_its_footer_is_refused() {
for n in 0..FOOTER {
assert_eq!(unseal(&vec![0u8; n]), Err(Bad::Footer));
}
}
#[test]
fn arbitrary_bytes_are_an_error_and_not_a_panic() {
let (s, h, l, z) = limits();
let all = Limits {
set: &s,
hash: &h,
list: &l,
zset: &z,
};
for kind in 0u8..=26 {
for len in 0usize..6 {
for fill in [0u8, 1, 0x40, 0x80, 0x81, 0xc0, 0xc3, 0xff] {
let mut body = vec![kind];
body.extend(std::iter::repeat_n(fill, len));
let payload = seal(body);
let _ = load(&payload, all, 0);
}
}
}
}
#[test]
fn a_count_past_the_payload_is_refused_before_anything_is_reserved() {
let (s, h, l, z) = limits();
let all = Limits {
set: &s,
hash: &h,
list: &l,
zset: &z,
};
for kind in [T_SET, T_HASH, T_ZSET_2, T_ZSET, T_LIST_QUICKLIST_2] {
let payload = seal(vec![kind, 0x80, 0x80, 0x80, 0x80, 0x80]);
assert_eq!(
load(&payload, all, 0).err(),
Some(Bad::Format),
"type {kind} took a count of 0x80808080 from six bytes"
);
}
let mut body = vec![T_SET];
put_len(&mut body, 10);
assert_eq!(load(&seal(body), all, 0).err(), Some(Bad::Format));
let mut body = vec![T_SET];
put_len(&mut body, 2);
put_str(&mut body, b"a");
put_str(&mut body, b"b");
assert!(load(&seal(body), all, 0).is_ok());
}
#[test]
fn lzf_unpacks_a_literal_run() {
assert_eq!(
unpack(&[3, b'a', b'b', b'c', b'd'], 4).as_deref(),
Some(&b"abcd"[..])
);
}
#[test]
fn lzf_unpacks_an_overlapping_reference() {
let packed = [0u8, b'a', 3 << 5, 0];
assert_eq!(unpack(&packed, 6).as_deref(), Some(&b"aaaaaa"[..]));
}
#[test]
fn lzf_refuses_a_reference_to_nothing() {
assert_eq!(unpack(&[(3 << 5), 0], 5), None);
assert_eq!(unpack(&[3, b'a'], 4), None);
}
#[test]
fn an_empty_collection_is_not_a_value() {
let mut body = vec![T_SET];
put_len(&mut body, 0);
let payload = seal(body);
let (s, h, l, z) = limits();
let all = Limits {
set: &s,
hash: &h,
list: &l,
zset: &z,
};
assert_eq!(load(&payload, all, 0).unwrap_err(), Bad::Format);
}
#[test]
fn trailing_bytes_are_refused() {
let mut body = vec![T_STRING];
put_str(&mut body, b"hello");
body.push(0);
let payload = seal(body);
let (s, h, l, z) = limits();
let all = Limits {
set: &s,
hash: &h,
list: &l,
zset: &z,
};
assert_eq!(load(&payload, all, 0).unwrap_err(), Bad::Format);
}
#[test]
fn every_length_form_round_trips() {
for n in [0u64, 63, 64, 16383, 16384, u64::from(u32::MAX), 1 << 40] {
let mut out = Vec::new();
put_len(&mut out, n);
let mut r = Reader::new(&out);
assert_eq!(r.len_or_encoding(), Ok((n, false)), "{n} did not survive");
assert!(r.done(), "{n} left bytes behind");
}
}
}