use crate::state::ConstraintState;
use crate::tile::FleetTile;
use crate::vclock::VectorClock;
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct StateHash(pub [u8; 32]);
impl StateHash {
pub fn from_state(state: &ConstraintState) -> Self {
let mut hasher = FnvHasher::new();
let mut constraints = state.constraints.active_constraints();
constraints.sort();
for c in &constraints {
hasher.update(c.as_bytes());
}
hasher.update(&state.metrics.total_satisfied().to_le_bytes());
hasher.update(&state.metrics.total_violations().to_le_bytes());
hasher.update(&state.position.position.0.to_le_bytes());
hasher.update(&state.position.position.1.to_le_bytes());
hasher.update(&state.position.norm.to_le_bytes());
hasher.update(&state.version.to_le_bytes());
StateHash(hasher.finalize())
}
pub fn from_tile(tile: &FleetTile) -> Self {
let mut hasher = FnvHasher::new();
hasher.update(tile.room.as_bytes());
hasher.update(tile.id.as_bytes());
hasher.update(&tile.content_hash.to_le_bytes());
hasher.update(&tile.updated_at.to_le_bytes());
StateHash(hasher.finalize())
}
pub fn from_clock(clock: &VectorClock) -> Self {
let mut hasher = FnvHasher::new();
let mut nodes: Vec<_> = clock.nodes();
nodes.sort();
for node in &nodes {
hasher.update(node.as_bytes());
hasher.update(&clock.get(node).to_le_bytes());
}
StateHash(hasher.finalize())
}
pub fn zero() -> Self {
StateHash([0u8; 32])
}
pub fn is_zero(&self) -> bool {
self.0 == [0u8; 32]
}
pub fn to_hex(&self) -> String {
self.0.iter().map(|b| format!("{:02x}", b)).collect()
}
}
impl fmt::Display for StateHash {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}…{}", &self.to_hex()[..8], &self.to_hex()[24..32])
}
}
struct FnvHasher {
state: [u64; 4],
}
impl FnvHasher {
fn new() -> Self {
Self {
state: [
0xcbf29ce484222325,
0x100000001b3,
0x9e3779b97f4a7c15,
0x6c62272e07bb0142,
],
}
}
fn update(&mut self, data: &[u8]) {
for &byte in data {
self.state[0] ^= byte as u64;
self.state[0] = self.state[0].wrapping_mul(0x100000001b3);
self.state[1] ^= byte as u64;
self.state[1] = self.state[1].wrapping_mul(0x100000001b3).wrapping_add(1);
self.state[2] ^= byte as u64;
self.state[2] = self.state[2].wrapping_mul(0x100000001b3).wrapping_add(2);
self.state[3] ^= byte as u64;
self.state[3] = self.state[3].wrapping_mul(0x100000001b3).wrapping_add(3);
}
}
fn finalize(&self) -> [u8; 32] {
let mut result = [0u8; 32];
for (i, lane) in self.state.iter().enumerate() {
let bytes = lane.to_le_bytes();
result[i * 8..(i + 1) * 8].copy_from_slice(&bytes);
}
result
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_state_hash_deterministic() {
let mut s = ConstraintState::new("test");
s.add_constraint("c1");
s.record_satisfied(100);
let h1 = StateHash::from_state(&s);
let h2 = StateHash::from_state(&s);
assert_eq!(h1, h2);
}
#[test]
fn test_state_hash_changes_on_mutation() {
let mut s = ConstraintState::new("test");
let h1 = StateHash::from_state(&s);
s.add_constraint("c1");
let h2 = StateHash::from_state(&s);
assert_ne!(h1, h2);
}
#[test]
fn test_state_hash_order_independent() {
let mut a = ConstraintState::new("a");
a.add_constraint("c2");
a.add_constraint("c1");
a.record_satisfied(100);
let mut b = ConstraintState::new("b");
b.add_constraint("c1");
b.add_constraint("c2");
b.record_satisfied(100);
assert_eq!(StateHash::from_state(&a), StateHash::from_state(&b));
}
#[test]
fn test_zero_hash() {
let z = StateHash::zero();
assert!(z.is_zero());
}
#[test]
fn test_display() {
let mut s = ConstraintState::new("test");
let h = StateHash::from_state(&s);
let display = format!("{}", h);
assert!(display.contains("…"));
assert_eq!(display.chars().count(), 17); }
#[test]
fn test_tile_hash() {
let t = FleetTile::new("room", "1", "content", "author");
let h = StateHash::from_tile(&t);
assert!(!h.is_zero());
}
#[test]
fn test_clock_hash() {
let mut vc = VectorClock::new();
vc.increment("a");
let h = StateHash::from_clock(&vc);
assert!(!h.is_zero());
}
}