use std::hash::Hasher;
#[derive(Debug, Clone)]
pub struct StableHasher {
v: [u64; 4],
tail: u64,
tail_len: usize,
len: usize,
}
impl Default for StableHasher {
fn default() -> Self {
Self::new()
}
}
impl StableHasher {
#[must_use]
pub fn new() -> Self {
Self {
v: [
0x736f_6d65_7073_6575,
0x646f_7261_6e64_6f6d,
0x6c79_6765_6e65_7261,
0x7465_6462_7974_6573,
],
tail: 0,
tail_len: 0,
len: 0,
}
}
fn round(&mut self) {
let [v0, v1, v2, v3] = &mut self.v;
*v0 = v0.wrapping_add(*v1);
*v1 = v1.rotate_left(13) ^ *v0;
*v0 = v0.rotate_left(32);
*v2 = v2.wrapping_add(*v3);
*v3 = v3.rotate_left(16) ^ *v2;
*v0 = v0.wrapping_add(*v3);
*v3 = v3.rotate_left(21) ^ *v0;
*v2 = v2.wrapping_add(*v1);
*v1 = v1.rotate_left(17) ^ *v2;
*v2 = v2.rotate_left(32);
}
fn compress(&mut self, word: u64) {
self.v[3] ^= word;
self.round();
self.v[0] ^= word;
}
}
impl Hasher for StableHasher {
fn write(&mut self, bytes: &[u8]) {
self.len = self.len.wrapping_add(bytes.len());
for &byte in bytes {
self.tail |= u64::from(byte) << (8 * self.tail_len);
self.tail_len += 1;
if self.tail_len == 8 {
let word = self.tail;
self.compress(word);
self.tail = 0;
self.tail_len = 0;
}
}
}
fn finish(&self) -> u64 {
let mut state = self.clone();
let last = ((state.len as u64 & 0xff) << 56) | state.tail;
state.compress(last);
state.v[2] ^= 0xff;
for _ in 0..3 {
state.round();
}
state.v[0] ^ state.v[1] ^ state.v[2] ^ state.v[3]
}
}
#[must_use]
pub fn hex_of_strs<S: AsRef<str>>(parts: &[S]) -> String {
use std::hash::Hash;
let mut hasher = StableHasher::new();
for part in parts {
part.as_ref().hash(&mut hasher);
}
format!("{:016x}", hasher.finish())
}
#[cfg(test)]
mod tests {
use super::*;
use std::hash::Hash;
#[test]
fn names_are_pinned() {
let long = "a".repeat(100);
let cases: [(&[&str], &str); 5] = [
(&[""], "30406ea523c53def"),
(&["/workspace"], "ffb2f5bddb564686"),
(
&["/Users/a/repo", "git:https://github.com/acme/repo.git"],
"d6b034eec52f1cb5",
),
(&["D:/repo"], "330132ce3328cddd"),
(&[long.as_str()], "5c4d00aef4d91df1"),
];
for (parts, name) in cases {
assert_eq!(hex_of_strs(parts), name, "{parts:?}");
}
}
#[test]
fn split_writes_hash_like_one_write() {
let data: Vec<u8> = (0u8..=40).collect();
for cut in 0..data.len() {
let mut whole = StableHasher::new();
whole.write(&data);
let mut split = StableHasher::new();
split.write(&data[..cut]);
split.write(&data[cut..]);
assert_eq!(whole.finish(), split.finish(), "cut at {cut}");
}
}
#[test]
fn parts_hash_in_order() {
let mut hasher = StableHasher::new();
"/workspace".hash(&mut hasher);
"git:x".hash(&mut hasher);
assert_eq!(
hex_of_strs(&["/workspace", "git:x"]),
format!("{:016x}", hasher.finish())
);
assert_ne!(
hex_of_strs(&["/workspace", "git:x"]),
hex_of_strs(&["/workspacegit:x"])
);
}
}