Skip to main content

saf_core/
id.rs

1//! BLAKE3-based deterministic ID generation for SAF entities.
2//!
3//! All AIR entities use `u128` IDs derived from BLAKE3 hashes, ensuring
4//! deterministic identification across runs (FR-AIR-002).
5
6/// Compute a deterministic `u128` ID from a domain tag and arbitrary data.
7///
8/// The domain tag prevents collisions between different entity kinds
9/// that might share the same data bytes.
10pub fn make_id(domain: &str, data: &[u8]) -> u128 {
11    let mut hasher = blake3::Hasher::new();
12    hasher.update(domain.as_bytes());
13    hasher.update(b":");
14    hasher.update(data);
15    let hash = hasher.finalize();
16    let bytes: &[u8; 16] = hash.as_bytes()[..16].try_into().unwrap_or_else(|_| {
17        // BLAKE3 always produces 32+ bytes; this branch is unreachable.
18        unreachable!("BLAKE3 hash is always at least 16 bytes")
19    });
20    u128::from_le_bytes(*bytes)
21}
22
23/// Format a `u128` ID as a lowercase hex string with `0x` prefix.
24pub fn id_to_hex(id: u128) -> String {
25    format!("0x{id:032x}")
26}
27
28#[cfg(test)]
29mod tests {
30    use super::*;
31
32    #[test]
33    fn make_id_is_deterministic() {
34        let a = make_id("test", b"hello");
35        let b = make_id("test", b"hello");
36        assert_eq!(a, b);
37    }
38
39    #[test]
40    fn different_domains_produce_different_ids() {
41        let a = make_id("fn", b"main");
42        let b = make_id("bb", b"main");
43        assert_ne!(a, b);
44    }
45
46    #[test]
47    fn id_to_hex_format() {
48        let id = make_id("test", b"hello");
49        let hex = id_to_hex(id);
50        assert!(hex.starts_with("0x"));
51        // 0x prefix + 32 hex chars = 34 total
52        assert_eq!(hex.len(), 34);
53    }
54}