/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ use nohash_hasher::IsEnabled; use std::{ collections::{BTreeMap, HashMap, HashSet}, fmt::Debug, hash::Hash, str::FromStr, }; // A hash that can cheekily store small inputs directly without hashing them. #[derive( Copy, Clone, PartialEq, Eq, PartialOrd, Ord, rkyv::Serialize, rkyv::Deserialize, rkyv::Archive, )] #[repr(transparent)] pub struct CheekyHash([u8; HASH_SIZE]); const HASH_SIZE: usize = std::mem::size_of::() * 2; const HASH_PAYLOAD: usize = HASH_SIZE - 1; pub type CheekyHashSet = HashSet>; pub type CheekyHashMap = HashMap>; pub type CheekyBTreeMap = BTreeMap; impl CheekyHash { pub const HASH_SIZE: usize = HASH_SIZE; pub const NULL: CheekyHash = CheekyHash([0u8; HASH_SIZE]); pub const FULL: CheekyHash = CheekyHash([u8::MAX; HASH_SIZE]); pub fn new(bytes: impl AsRef<[u8]>) -> Self { let mut hash = [0u8; HASH_SIZE]; let bytes = bytes.as_ref(); if bytes.len() <= HASH_PAYLOAD { hash[0] = bytes.len() as u8; hash[1..1 + bytes.len()].copy_from_slice(bytes); } else { let h1 = xxhash_rust::xxh3::xxh3_64(bytes).to_be_bytes(); let h2 = farmhash::fingerprint64(bytes).to_be_bytes(); hash[0] = bytes.len().min(u8::MAX as usize) as u8; hash[1..1 + std::mem::size_of::()].copy_from_slice(&h1); hash[1 + std::mem::size_of::()..] .copy_from_slice(&h2[..std::mem::size_of::() - 1]); } CheekyHash(hash) } pub fn deserialize(bytes: &[u8]) -> Option { let len = *bytes.first()?; let mut hash = [0u8; HASH_SIZE]; let hash_len = 1 + (len as usize).min(HASH_PAYLOAD); hash[0] = len; hash[1..hash_len].copy_from_slice(bytes.get(1..hash_len)?); Some(CheekyHash(hash)) } #[allow(clippy::len_without_is_empty)] #[inline(always)] pub fn len(&self) -> usize { (self.0[0] as usize).min(HASH_PAYLOAD) + 1 } #[inline(always)] pub fn as_bytes(&self) -> &[u8] { &self.0[..self.len()] } #[inline(always)] pub fn as_raw_bytes(&self) -> &[u8; HASH_SIZE] { &self.0 } pub fn into_inner(self) -> [u8; HASH_SIZE] { self.0 } pub fn payload(&self) -> &[u8] { let len = self.0[0] as usize; if len <= HASH_PAYLOAD { &self.0[1..1 + len] } else { &self.0[1..] } } pub fn payload_len(&self) -> u8 { self.0[0] } fn as_u128(&self) -> u128 { u128::from_be_bytes(self.0) } } impl AsRef<[u8]> for CheekyHash { fn as_ref(&self) -> &[u8] { self.as_bytes() } } impl FromStr for CheekyHash { type Err = std::num::ParseIntError; fn from_str(s: &str) -> Result { u128::from_str_radix(s, 16).map(|n| CheekyHash(n.to_be_bytes())) } } impl std::fmt::Display for CheekyHash { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{:032x}", self.as_u128()) } } impl Hash for CheekyHash { fn hash(&self, state: &mut H) { let len = self.0[0] as usize; if len <= HASH_PAYLOAD { state.write_u64(xxhash_rust::xxh3::xxh3_64(&self.0[1..1 + len])); } else { state.write_u64(u64::from_be_bytes( self.0[1..1 + std::mem::size_of::()] .try_into() .unwrap(), )); } } } impl IsEnabled for CheekyHash {} impl ArchivedCheekyHash { #[inline(always)] pub fn as_raw_bytes(&self) -> &[u8; HASH_SIZE] { &self.0 } #[inline(always)] pub fn as_bytes(&self) -> &[u8] { let len = self.0[0] as usize; &self.0[..1 + len.min(HASH_PAYLOAD)] } #[inline(always)] pub fn to_native(&self) -> CheekyHash { CheekyHash(self.0) } } impl Debug for CheekyHash { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let len = self.payload_len(); let payload = self.payload(); let payload_str = if len <= HASH_PAYLOAD as u8 { std::str::from_utf8(payload).unwrap_or("") } else { "" }; f.debug_struct("CheekyHash") .field("length", &len) .field("bytes", &payload_str) .finish() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_cheeky_hash_all() { // Test 1: Empty input let hash_empty = CheekyHash::new([]); assert_eq!( hash_empty.as_bytes()[0], 0, "Empty input should have length 0" ); assert_eq!( hash_empty.as_bytes().len(), 1, "Empty input should only have length byte" ); // Test 2: Single byte input let hash_single = CheekyHash::new([42]); assert_eq!( hash_single.as_bytes()[0], 1, "Single byte should have length 1" ); assert_eq!( hash_single.as_bytes()[1], 42, "Single byte value should be preserved" ); assert_eq!(hash_single.as_bytes().len(), 2); // Test 3: Small input (less than HASH_LEN) let small_data = b"hello"; let hash_small = CheekyHash::new(small_data); assert_eq!(hash_small.as_bytes()[0], 5, "Length should be 5"); assert_eq!( &hash_small.as_bytes()[1..6], small_data, "Small data should be stored directly" ); assert_eq!(hash_small.as_bytes().len(), 6); // Test 4: Input exactly at HASH_PAYLOAD boundary let boundary_data = vec![1u8; HASH_PAYLOAD - 1]; let hash_boundary = CheekyHash::new(&boundary_data); assert_eq!( hash_boundary.as_bytes()[0], (HASH_PAYLOAD - 1) as u8, "Length should be HASH_LEN" ); assert_eq!( &hash_boundary.as_bytes()[1..], &boundary_data[..], "Boundary data should be stored directly" ); // Test 5: Large input (greater than HASH_LEN) - uses hashing let large_data = vec![7u8; HASH_SIZE]; let hash_large = CheekyHash::new(&large_data); assert_eq!( hash_large.as_bytes()[0], HASH_SIZE as u8, "Large data should have length byte set to HASH_LEN" ); assert_eq!( hash_large.as_bytes().len(), HASH_SIZE, "Large data hash should be full length" ); // Verify it's actually hashed (not raw data) assert_ne!( &hash_large.as_bytes()[1..], &large_data[..HASH_PAYLOAD], "Large data should be hashed, not stored directly" ); // Test 6: AsRef<[u8]> trait let hash = CheekyHash::new(b"test"); let bytes_ref: &[u8] = hash.as_ref(); assert_eq!(bytes_ref, hash.as_bytes(), "AsRef should match as_bytes"); // Test 7: Copy, Clone, PartialEq traits let hash1 = CheekyHash::new(b"identical"); let hash2 = hash1; // Copy assert_eq!(hash1, hash2, "Copied hashes should be equal"); // Test 8: Different inputs produce different hashes let hash_a = CheekyHash::new(b"abc"); let hash_b = CheekyHash::new(b"def"); assert_ne!( hash_a, hash_b, "Different inputs should produce different hashes" ); // Test 9: Same input produces same hash (deterministic) let hash_x1 = CheekyHash::new(b"deterministic"); let hash_x2 = CheekyHash::new(b"deterministic"); assert_eq!( hash_x1, hash_x2, "Same input should produce identical hashes" ); // Test 10: Large inputs with different content produce different hashes let large1 = vec![1u8; 100]; let large2 = vec![2u8; 100]; let hash_large1 = CheekyHash::new(&large1); let hash_large2 = CheekyHash::new(&large2); assert_ne!( hash_large1, hash_large2, "Different large inputs should produce different hashes" ); // Test 11: Hash trait (can be used in HashMap/HashSet) use std::collections::HashMap; let mut map = HashMap::new(); let key = CheekyHash::new(b"key"); map.insert(key, "value"); assert_eq!( map.get(&key), Some(&"value"), "CheekyHash should work as HashMap key" ); // Test 12: Debug trait let hash = CheekyHash::new(b"debug"); let debug_str = format!("{:?}", hash); assert!( debug_str.contains("CheekyHash"), "Debug output should contain type name" ); // Test 13: CheekyHashSet and CheekyHashMap let mut cheeky_set: CheekyHashSet = CheekyHashSet::default(); cheeky_set.insert(CheekyHash::new(b"set_item")); assert!(cheeky_set.contains(&CheekyHash::new(b"set_item"))); let mut cheeky_map: CheekyHashMap<&str> = CheekyHashMap::default(); cheeky_map.insert(CheekyHash::new(b"map_key"), "map_value"); assert_eq!( cheeky_map.get(&CheekyHash::new(b"map_key")), Some(&"map_value") ); println!("All CheekyHash tests passed!"); } }