use std::fmt::{self, Display, LowerHex};
use xxhash_rust::{xxh3::xxh3_64, xxh64::xxh64};
use crate::{fnv1a, BinHash};
#[repr(u8)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, std::hash::Hash)]
pub enum HashWidth {
#[default]
W4 = 4,
W8 = 8,
}
impl HashWidth {
#[inline]
#[must_use]
pub const fn bytes(self) -> usize {
self as usize
}
#[inline]
#[must_use]
pub const fn from_bytes(bytes: usize) -> Option<Self> {
match bytes {
4 => Some(Self::W4),
8 => Some(Self::W8),
_ => None,
}
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, std::hash::Hash)]
pub struct HashValue {
hash: u64,
width: HashWidth,
}
impl HashValue {
#[inline]
#[must_use]
pub const fn narrow(hash: u32) -> Self {
Self {
hash: hash as u64,
width: HashWidth::W4,
}
}
#[inline]
#[must_use]
pub const fn wide(hash: u64) -> Self {
Self {
hash,
width: HashWidth::W8,
}
}
#[inline]
#[must_use]
pub const fn with_width(hash: u64, width: HashWidth) -> Self {
match width {
HashWidth::W4 => Self::narrow(hash as u32),
HashWidth::W8 => Self::wide(hash),
}
}
#[inline]
#[must_use]
pub const fn try_with_width(hash: u64, width: HashWidth) -> Option<Self> {
match width {
HashWidth::W4 if hash > u32::MAX as u64 => None,
_ => Some(Self::with_width(hash, width)),
}
}
#[inline]
#[must_use]
pub const fn as_u64(self) -> u64 {
self.hash
}
#[inline]
#[must_use]
pub const fn as_u32(self) -> u32 {
self.hash as u32
}
#[inline]
#[must_use]
pub const fn try_as_u32(self) -> Option<u32> {
match self.width {
HashWidth::W4 => Some(self.hash as u32),
HashWidth::W8 => None,
}
}
#[inline]
#[must_use]
pub const fn try_as_bin_hash(self) -> Option<BinHash> {
match self.try_as_u32() {
Some(hash) => Some(BinHash(hash)),
None => None,
}
}
#[inline]
#[must_use]
pub const fn width(self) -> HashWidth {
self.width
}
}
impl From<u32> for HashValue {
fn from(hash: u32) -> Self {
Self::narrow(hash)
}
}
impl From<BinHash> for HashValue {
fn from(hash: BinHash) -> Self {
Self::narrow(hash.0)
}
}
impl From<&str> for HashValue {
fn from(text: &str) -> Self {
Hasher::DEFAULT.hash_str(text)
}
}
impl Display for HashValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
LowerHex::fmt(self, f)
}
}
impl LowerHex for HashValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.width {
HashWidth::W4 => write!(f, "{:08x}", self.hash),
HashWidth::W8 => write!(f, "{:016x}", self.hash),
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, std::hash::Hash)]
pub enum HashAlgorithm {
Fnv1a32,
Xxh64,
Xxh3,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, std::hash::Hash)]
pub struct Hasher {
pub width: HashWidth,
pub algorithm: HashAlgorithm,
pub lowercased: bool,
}
impl Hasher {
pub const DEFAULT: Self = Self {
width: HashWidth::W4,
algorithm: HashAlgorithm::Fnv1a32,
lowercased: true,
};
#[must_use]
pub fn hash_str(&self, text: &str) -> HashValue {
let hash = match (self.algorithm, self.lowercased) {
(HashAlgorithm::Fnv1a32, true) => u64::from(fnv1a::hash_lower(text)),
(HashAlgorithm::Fnv1a32, false) => u64::from(fnv1a::hash(text)),
(HashAlgorithm::Xxh64, true) => xxh64(text.to_ascii_lowercase().as_bytes(), 0),
(HashAlgorithm::Xxh64, false) => xxh64(text.as_bytes(), 0),
(HashAlgorithm::Xxh3, true) => xxh3_64(text.to_ascii_lowercase().as_bytes()),
(HashAlgorithm::Xxh3, false) => xxh3_64(text.as_bytes()),
};
self.from_hash(hash)
}
#[must_use]
pub const fn from_hash(&self, hash: u64) -> HashValue {
HashValue::with_width(hash, self.width)
}
}
impl Default for Hasher {
fn default() -> Self {
Self::DEFAULT
}
}
#[cfg(feature = "serde")]
mod serde_impl {
use serde::{
de::{self, Unexpected, Visitor},
Deserialize, Deserializer, Serialize, Serializer,
};
use super::{HashValue, HashWidth};
const NAME: &str = "BinHash";
impl Serialize for HashValue {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
if !serializer.is_human_readable() {
return (self.hash, self.width as u8).serialize(serializer);
}
match self.width {
HashWidth::W4 => serializer.serialize_newtype_struct(NAME, &(self.hash as u32)),
HashWidth::W8 => {
serializer.serialize_newtype_struct(NAME, &format!("0x{:016x}", self.hash))
}
}
}
}
impl<'de> Deserialize<'de> for HashValue {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
if !deserializer.is_human_readable() {
let (hash, width) = <(u64, u8)>::deserialize(deserializer)?;
let width = HashWidth::from_bytes(usize::from(width)).ok_or_else(|| {
de::Error::invalid_value(Unexpected::Unsigned(u64::from(width)), &"4 or 8")
})?;
return HashValue::try_with_width(hash, width).ok_or_else(|| {
de::Error::invalid_value(Unexpected::Unsigned(hash), &"a 32-bit hash")
});
}
deserializer.deserialize_newtype_struct(NAME, HumanReadable)
}
}
struct HumanReadable;
impl<'de> Visitor<'de> for HumanReadable {
type Value = HashValue;
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("a 32-bit hash as a number, or a 64-bit hash as `0x` and 16 hex digits")
}
fn visit_newtype_struct<D: Deserializer<'de>>(
self,
deserializer: D,
) -> Result<Self::Value, D::Error> {
deserializer.deserialize_any(self)
}
fn visit_u64<E: de::Error>(self, hash: u64) -> Result<Self::Value, E> {
u32::try_from(hash)
.map(HashValue::narrow)
.map_err(|_| E::invalid_value(Unexpected::Unsigned(hash), &self))
}
fn visit_i64<E: de::Error>(self, hash: i64) -> Result<Self::Value, E> {
u32::try_from(hash)
.map(HashValue::narrow)
.map_err(|_| E::invalid_value(Unexpected::Signed(hash), &self))
}
fn visit_str<E: de::Error>(self, text: &str) -> Result<Self::Value, E> {
text.strip_prefix("0x")
.filter(|digits| {
digits.len() == 16 && digits.bytes().all(|b| b.is_ascii_hexdigit())
})
.and_then(|digits| u64::from_str_radix(digits, 16).ok())
.map(HashValue::wide)
.ok_or_else(|| E::invalid_value(Unexpected::Str(text), &self))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Hash as _;
#[test]
fn with_width_keeps_low_32_bits_at_width_4() {
let value = HashValue::with_width(0x1122_3344_5566_7788, HashWidth::W4);
assert_eq!(value, HashValue::narrow(0x5566_7788));
assert_eq!(value.as_u64(), 0x5566_7788);
}
#[test]
fn try_with_width_returns_none_above_u32_max_at_width_4() {
assert_eq!(HashValue::try_with_width(1 << 32, HashWidth::W4), None);
assert_eq!(
HashValue::try_with_width(1 << 32, HashWidth::W8),
Some(HashValue::wide(1 << 32))
);
assert_eq!(
HashValue::try_with_width(7, HashWidth::W4),
Some(HashValue::narrow(7))
);
}
#[test]
fn as_u32_returns_low_32_bits_at_width_8() {
let value = HashValue::wide(0x1122_3344_5566_7788);
assert_eq!(value.as_u32(), 0x5566_7788);
assert_eq!(value.try_as_u32(), None);
assert_eq!(value.try_as_bin_hash(), None);
assert_eq!(HashValue::narrow(9).try_as_u32(), Some(9));
}
#[test]
fn eq_compares_width() {
assert_ne!(HashValue::narrow(0), HashValue::wide(0));
assert_eq!(HashValue::default(), HashValue::narrow(0));
assert_eq!(HashValue::narrow(5), HashValue::from(BinHash(5)));
assert_eq!(HashValue::narrow(5).try_as_bin_hash(), Some(BinHash(5)));
}
#[test]
fn display_pads_to_width() {
assert_eq!(HashValue::narrow(0x1a).to_string(), "0000001a");
assert_eq!(HashValue::wide(0x1a).to_string(), "000000000000001a");
assert_eq!(format!("{:x}", HashValue::narrow(0x1a)), "0000001a");
}
#[test]
fn hasher_default_matches_bin_hash() {
for text in ["", "Test", "Characters/Zac", "\u{c9}t\u{e9}"] {
assert_eq!(
Hasher::DEFAULT.hash_str(text),
HashValue::from(BinHash::hash_str(text))
);
}
}
#[test]
fn hasher_lowercases_when_lowercased() {
let lowercased = Hasher {
width: HashWidth::W8,
algorithm: HashAlgorithm::Xxh3,
lowercased: true,
};
let exact = Hasher {
lowercased: false,
..lowercased
};
assert_eq!(lowercased.hash_str("ABC"), lowercased.hash_str("abc"));
assert_eq!(exact.hash_str("abc"), lowercased.hash_str("abc"));
assert_ne!(exact.hash_str("ABC"), exact.hash_str("abc"));
assert_eq!(exact.hash_str("abc").as_u64(), xxh3_64(b"abc"));
}
#[test]
fn hasher_xxh3_matches_shipped_material_name() {
let hasher = Hasher {
width: HashWidth::W8,
algorithm: HashAlgorithm::Xxh3,
lowercased: true,
};
assert_eq!(
hasher.hash_str("Characters/Zac/Skins/Skin31/Materials/ult"),
HashValue::wide(0x5d07_ca0d_22ff_9588)
);
}
#[test]
fn hasher_keeps_low_32_bits_at_width_4() {
let hasher = Hasher {
width: HashWidth::W4,
algorithm: HashAlgorithm::Xxh64,
lowercased: false,
};
let full = xxh64(b"abc", 0);
assert_eq!(hasher.hash_str("abc"), HashValue::narrow(full as u32));
}
}