use yo_common::dtoa;
use yo_common::num::{self, DIGITS_MAX, DOUBLE_MAX};
use yo_common::sha1;
use crate::array::Array;
use crate::hash::Hash;
use crate::list::List;
use crate::listpack::Entry;
use crate::set::Set;
use crate::stream::{Id, Stream};
use crate::zset::Zset;
pub type Digest = [u8; 20];
pub const EMPTY: Digest = [0; 20];
mod kinds {
pub const STRING: u32 = 0;
pub const LIST: u32 = 1;
pub const SET: u32 = 2;
pub const ZSET: u32 = 3;
pub const HASH: u32 = 4;
pub const MODULE: u32 = 5;
pub const STREAM: u32 = 6;
pub const ARRAY: u32 = 7;
}
pub fn xor(digest: &mut Digest, bytes: &[u8]) {
for (into, byte) in digest.iter_mut().zip(sha1::digest(bytes)) {
*into ^= byte;
}
}
pub fn mix(digest: &mut Digest, bytes: &[u8]) {
xor(digest, bytes);
*digest = sha1::digest(digest);
}
#[must_use]
pub fn hex(digest: &Digest) -> [u8; 40] {
const DIGITS: &[u8; 16] = b"0123456789abcdef";
let mut out = [0u8; 40];
for (pair, byte) in out.as_chunks_mut::<2>().0.iter_mut().zip(digest) {
pair[0] = DIGITS[usize::from(byte >> 4)];
pair[1] = DIGITS[usize::from(byte & 0xf)];
}
out
}
pub fn number(digest: &mut Digest, n: u32) {
mix(digest, &n.to_be_bytes());
}
pub const EXPIRE: &[u8] = b"!!expire!!";
const HASH_EXPIRE: &[u8] = b"!!hexpire!!";
fn element(entry: &Entry<'_>, digest: &mut Digest, how: fn(&mut Digest, &[u8])) {
match entry {
Entry::Str(bytes) => how(digest, bytes),
Entry::Int(n) => {
let mut buf = [0u8; DIGITS_MAX];
how(digest, num::i64_digits(&mut buf, *n));
}
}
}
pub fn string(digest: &mut Digest, bytes: &[u8]) {
number(digest, kinds::STRING);
mix(digest, bytes);
}
pub fn list(digest: &mut Digest, list: &List) {
number(digest, kinds::LIST);
for entry in list.iter() {
element(&entry, digest, mix);
}
}
pub fn set(digest: &mut Digest, set: &Set) {
number(digest, kinds::SET);
for member in set.iter() {
element(&member, digest, xor);
}
}
pub fn zset(digest: &mut Digest, zset: &Zset) {
number(digest, kinds::ZSET);
zset.walk(0, zset.len(), false, |member, score| {
let mut pair = EMPTY;
element(&member, &mut pair, mix);
let mut buf = [0u8; DOUBLE_MAX];
let len = dtoa::dtoa(score, &mut buf);
mix(&mut pair, &buf[..len]);
xor(digest, &pair);
});
}
pub fn hash(digest: &mut Digest, hash: &Hash) {
number(digest, kinds::HASH);
for i in 0..hash.len() {
let Some((field, value)) = hash.at(i) else {
continue;
};
let mut pair = EMPTY;
element(&field, &mut pair, mix);
element(&value, &mut pair, mix);
if hash.deadline_at(i).is_some() {
xor(&mut pair, HASH_EXPIRE);
}
xor(digest, &pair);
}
}
pub fn stream(digest: &mut Digest, stream: &Stream) {
number(digest, kinds::STREAM);
stream.range(Id::MIN, Id::MAX, None, |at, fields| {
let mut buf = [0u8; DIGITS_MAX * 2 + 1];
let mut written = 0;
for (part, sep) in [(at.ms, true), (at.seq, false)] {
let mut digits = [0u8; DIGITS_MAX];
let text = num::u64_digits(&mut digits, part);
buf[written..written + text.len()].copy_from_slice(text);
written += text.len();
if sep {
buf[written] = b'.';
written += 1;
}
}
mix(digest, &buf[..written]);
for (field, value) in fields {
element(&field, digest, mix);
element(&value, digest, mix);
}
true
});
}
pub fn array(digest: &mut Digest, array: &Array) {
number(digest, kinds::ARRAY);
for i in 0..array.len() {
match array.get(i) {
Some(value) => {
let mut buf = [0u8; crate::array::ELEMENT_MAX];
mix(digest, value.text(&mut buf));
}
None => mix(digest, b"(null)"),
}
}
}
pub fn foreign(digest: &mut Digest) {
number(digest, kinds::MODULE);
}
#[cfg(test)]
mod tests {
use super::{Digest, EMPTY, hex, mix, xor};
#[test]
fn xoring_forgets_the_order_and_mixing_keeps_it() {
let (mut ab, mut ba) = (EMPTY, EMPTY);
xor(&mut ab, b"a");
xor(&mut ab, b"b");
xor(&mut ba, b"b");
xor(&mut ba, b"a");
assert_eq!(ab, ba);
let (mut ab, mut ba) = (EMPTY, EMPTY);
mix(&mut ab, b"a");
mix(&mut ab, b"b");
mix(&mut ba, b"b");
mix(&mut ba, b"a");
assert_ne!(ab, ba);
}
#[test]
fn the_same_bytes_twice_leave_nothing_behind() {
let mut d = EMPTY;
xor(&mut d, b"member");
assert_ne!(d, EMPTY);
xor(&mut d, b"member");
assert_eq!(d, EMPTY);
}
#[test]
fn the_hex_form_is_the_bytes_in_order() {
let mut d: Digest = [0; 20];
d[0] = 0x0a;
d[19] = 0xff;
let text = hex(&d);
assert_eq!(&text[..2], b"0a");
assert_eq!(&text[38..], b"ff");
}
}