use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize)]
pub struct TileId {
pub x: i32,
pub z: i32,
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct ContentId([u8; 16]);
impl ContentId {
pub fn of(parts: &[&[u8]]) -> Self {
let mut hasher = blake3::Hasher::new();
for part in parts {
hasher.update(&(part.len() as u64).to_le_bytes());
hasher.update(part);
}
let mut out = [0u8; 16];
out.copy_from_slice(&hasher.finalize().as_bytes()[..16]);
ContentId(out)
}
pub fn as_bytes(&self) -> &[u8; 16] {
&self.0
}
}
impl fmt::Display for ContentId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for b in &self.0 {
write!(f, "{b:02x}")?;
}
Ok(())
}
}
impl fmt::Debug for ContentId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "ContentId({self})")
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize)]
pub struct ClusterId(pub ContentId);
impl ClusterId {
pub fn new(
config: ContentId,
tile: TileId,
partition: Option<&str>,
anchor_cell: (i32, i32, i32),
) -> Self {
let present = [u8::from(partition.is_some())];
let name = partition.unwrap_or("").as_bytes();
ClusterId(ContentId::of(&[
b"cluster.v2",
config.as_bytes(),
&tile.x.to_le_bytes(),
&tile.z.to_le_bytes(),
&present,
name,
&anchor_cell.0.to_le_bytes(),
&anchor_cell.1.to_le_bytes(),
&anchor_cell.2.to_le_bytes(),
]))
}
}
impl fmt::Display for ClusterId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn content_id_is_stable_and_order_sensitive() {
let a = ContentId::of(&[b"alpha", b"beta"]);
let b = ContentId::of(&[b"alpha", b"beta"]);
let c = ContentId::of(&[b"beta", b"alpha"]);
assert_eq!(a, b, "same parts must give the same id");
assert_ne!(a, c, "part order must matter (fields are positional)");
}
#[test]
fn content_id_resists_concatenation_collisions() {
assert_ne!(ContentId::of(&[b"ab", b"c"]), ContentId::of(&[b"a", b"bc"]));
}
#[test]
fn cluster_id_derives_from_config_tile_partition_and_anchor() {
let t = TileId { x: 3, z: -7 };
let cfg = ContentId::of(&[b"config-a"]);
let base = ClusterId::new(cfg, t, None, (1, 2, 3));
assert_eq!(base, ClusterId::new(cfg, t, None, (1, 2, 3)), "same inputs, same id");
assert_ne!(base, ClusterId::new(cfg, t, None, (1, 2, 4)), "anchor matters");
assert_ne!(
base,
ClusterId::new(cfg, TileId { x: 4, z: -7 }, None, (1, 2, 3)),
"tile matters"
);
assert_ne!(
base,
ClusterId::new(ContentId::of(&[b"config-b"]), t, None, (1, 2, 3)),
"config matters"
);
assert_ne!(base, ClusterId::new(cfg, t, Some("left"), (1, 2, 3)), "partition matters");
assert_ne!(
ClusterId::new(cfg, t, Some("left"), (1, 2, 3)),
ClusterId::new(cfg, t, Some("right"), (1, 2, 3)),
"the partition name, not just its presence, matters"
);
}
#[test]
fn absent_and_empty_partition_names_do_not_collide() {
let t = TileId { x: 0, z: 0 };
let cfg = ContentId::of(&[b"c"]);
assert_ne!(ClusterId::new(cfg, t, None, (0, 0, 0)), ClusterId::new(cfg, t, Some(""), (0, 0, 0)));
}
}