use serde::{Deserialize, Serialize};
use xxhash_rust::xxh64::xxh64;
pub const HASH_SEED_0: u64 = 0x5351_5259_4455_5030;
pub const HASH_SEED_1: u64 = 0x5351_5259_4455_5031;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
pub struct BodyHash128 {
pub high: u64,
pub low: u64,
}
impl BodyHash128 {
#[must_use]
pub fn compute(content: &[u8]) -> Self {
Self {
high: xxh64(content, HASH_SEED_0),
low: xxh64(content, HASH_SEED_1),
}
}
#[must_use]
pub fn as_u128(&self) -> u128 {
(u128::from(self.high) << 64) | u128::from(self.low)
}
#[must_use]
pub fn from_u128(value: u128) -> Self {
let high = u64::try_from(value >> 64).unwrap_or(u64::MAX);
let low = u64::try_from(value & u128::from(u64::MAX)).unwrap_or(u64::MAX);
Self { high, low }
}
}
impl std::fmt::Display for BodyHash128 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:016x}{:016x}", self.high, self.low)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_body_hash_deterministic() {
let content = b"fn example() { return 42; }";
let hash1 = BodyHash128::compute(content);
let hash2 = BodyHash128::compute(content);
assert_eq!(hash1, hash2, "Hash must be deterministic");
}
#[test]
fn test_body_hash_different_content() {
let content1 = b"fn example() { return 42; }";
let content2 = b"fn example() { return 43; }";
let hash1 = BodyHash128::compute(content1);
let hash2 = BodyHash128::compute(content2);
assert_ne!(
hash1, hash2,
"Different content should produce different hash"
);
}
#[test]
fn test_body_hash_empty() {
let hash = BodyHash128::compute(b"");
assert_ne!(hash.high, 0, "Empty content hash high should not be zero");
assert_ne!(hash.low, 0, "Empty content hash low should not be zero");
}
#[test]
fn test_body_hash_u128_roundtrip() {
let content = b"test content for roundtrip";
let hash = BodyHash128::compute(content);
let as_u128 = hash.as_u128();
let roundtrip = BodyHash128::from_u128(as_u128);
assert_eq!(hash, roundtrip, "u128 roundtrip should preserve hash");
}
#[test]
fn test_xxh64_fixed_seed() {
assert_eq!(HASH_SEED_0, 0x5351_5259_4455_5030);
assert_eq!(HASH_SEED_1, 0x5351_5259_4455_5031);
}
#[test]
fn test_xxh64_cross_endian() {
let test_data = b"fn example() { return 42; }";
let hash = BodyHash128::compute(test_data);
assert_ne!(
hash.high, 0,
"High bits should be non-zero for non-empty content"
);
assert_ne!(
hash.low, 0,
"Low bits should be non-zero for non-empty content"
);
assert_ne!(
hash.high, hash.low,
"High and low should differ due to different seeds"
);
}
#[test]
fn test_body_hash_display() {
let hash = BodyHash128 {
high: 0x1234_5678_90AB_CDEF,
low: 0xFEDC_BA09_8765_4321,
};
let display = format!("{hash}");
assert_eq!(display, "1234567890abcdeffedcba0987654321");
}
#[test]
fn test_body_hash_serde_json() {
let hash = BodyHash128::compute(b"test");
let json = serde_json::to_string(&hash).unwrap();
let parsed: BodyHash128 = serde_json::from_str(&json).unwrap();
assert_eq!(hash, parsed, "JSON roundtrip should preserve hash");
}
}