tatara_process/
identity.rs1use schemars::JsonSchema;
10use serde::{Deserialize, Serialize};
11
12const HASH_BYTES: usize = 16;
14
15const BASE32_ALPHABET: &[u8] = b"0123456789abcdefghjkmnpqrstvwxyz";
19
20#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
22pub struct Identity {
23 pub name: String,
25 pub content_hash: String,
27 pub name_override: bool,
29}
30
31pub fn content_hash<T: Serialize>(spec: &T) -> String {
33 let canonical = serde_json::to_vec(spec).unwrap_or_default();
34 let digest = blake3::hash(&canonical);
35 base32_encode(&digest.as_bytes()[..HASH_BYTES])
36}
37
38pub fn derive_identity<T: Serialize>(spec: &T, name_override: Option<&str>) -> Identity {
42 let hash = content_hash(spec);
43 match name_override.map(str::trim).filter(|s| !s.is_empty()) {
44 Some(name) => Identity {
45 name: name.to_string(),
46 content_hash: hash,
47 name_override: true,
48 },
49 None => Identity {
50 name: hash.clone(),
51 content_hash: hash,
52 name_override: false,
53 },
54 }
55}
56
57pub fn format_process_address(identity: &Identity, pid_path: &str) -> String {
61 format!("{}.{}", identity.name, pid_path)
62}
63
64fn base32_encode(bytes: &[u8]) -> String {
65 let mut out = String::with_capacity((bytes.len() * 8).div_ceil(5));
66 let mut bits: u64 = 0;
67 let mut n: u32 = 0;
68 for &b in bytes {
69 bits = (bits << 8) | u64::from(b);
70 n += 8;
71 while n >= 5 {
72 n -= 5;
73 out.push(BASE32_ALPHABET[((bits >> n) & 0x1f) as usize] as char);
74 }
75 }
76 if n > 0 {
77 out.push(BASE32_ALPHABET[((bits << (5 - n)) & 0x1f) as usize] as char);
78 }
79 out
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 #[derive(Serialize)]
87 struct Dummy {
88 a: u32,
89 b: &'static str,
90 }
91
92 #[test]
93 fn content_hash_is_deterministic() {
94 let s = Dummy { a: 1, b: "x" };
95 assert_eq!(content_hash(&s), content_hash(&s));
96 }
97
98 #[test]
99 fn content_hash_differs_for_different_input() {
100 assert_ne!(
101 content_hash(&Dummy { a: 1, b: "x" }),
102 content_hash(&Dummy { a: 2, b: "x" })
103 );
104 }
105
106 #[test]
107 fn content_hash_length_is_26() {
108 assert_eq!(content_hash(&Dummy { a: 0, b: "" }).len(), 26);
109 }
110
111 #[test]
112 fn alphabet_excludes_ambiguous() {
113 let h = content_hash(&Dummy {
115 a: u32::MAX,
116 b: "qwertyuiopasdfghjklzxcvbnm",
117 });
118 for c in h.chars() {
119 assert!(!matches!(c, 'i' | 'l' | 'o' | 'u'), "saw {c}");
120 }
121 }
122
123 #[test]
124 fn override_wins() {
125 let id = derive_identity(&Dummy { a: 1, b: "x" }, Some("seph"));
126 assert_eq!(id.name, "seph");
127 assert!(id.name_override);
128 assert_eq!(id.content_hash.len(), 26);
129 }
130
131 #[test]
132 fn empty_override_falls_back_to_hash() {
133 let id = derive_identity(&Dummy { a: 1, b: "x" }, Some(" "));
134 assert!(!id.name_override);
135 assert_eq!(id.name, id.content_hash);
136 }
137
138 #[test]
139 fn address_format() {
140 let id = Identity {
141 name: "seph".into(),
142 content_hash: "a".repeat(26),
143 name_override: true,
144 };
145 assert_eq!(format_process_address(&id, "1.7"), "seph.1.7");
146 }
147}