use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
const HASH_BYTES: usize = 16;
const BASE32_ALPHABET: &[u8] = b"0123456789abcdefghjkmnpqrstvwxyz";
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct Identity {
pub name: String,
pub content_hash: String,
pub name_override: bool,
}
pub fn content_hash<T: Serialize>(spec: &T) -> String {
let canonical = crate::three_pillar::pillar_bytes(spec);
let digest = blake3::hash(&canonical);
base32_encode(&digest.as_bytes()[..HASH_BYTES])
}
pub fn derive_identity<T: Serialize>(spec: &T, name_override: Option<&str>) -> Identity {
let hash = content_hash(spec);
match name_override.map(str::trim).filter(|s| !s.is_empty()) {
Some(name) => Identity {
name: name.to_string(),
content_hash: hash,
name_override: true,
},
None => Identity {
name: hash.clone(),
content_hash: hash,
name_override: false,
},
}
}
pub const PID_PATH_SEPARATOR: char = '.';
#[must_use]
pub fn join_pid_segment(head: &str, tail: impl std::fmt::Display) -> String {
format!("{head}{PID_PATH_SEPARATOR}{tail}")
}
pub fn format_process_address(identity: &Identity, pid_path: &str) -> String {
join_pid_segment(&identity.name, pid_path)
}
fn base32_encode(bytes: &[u8]) -> String {
let mut out = String::with_capacity((bytes.len() * 8).div_ceil(5));
let mut bits: u64 = 0;
let mut n: u32 = 0;
for &b in bytes {
bits = (bits << 8) | u64::from(b);
n += 8;
while n >= 5 {
n -= 5;
out.push(BASE32_ALPHABET[((bits >> n) & 0x1f) as usize] as char);
}
}
if n > 0 {
out.push(BASE32_ALPHABET[((bits << (5 - n)) & 0x1f) as usize] as char);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Serialize)]
struct Dummy {
a: u32,
b: &'static str,
}
#[test]
fn content_hash_is_deterministic() {
let s = Dummy { a: 1, b: "x" };
assert_eq!(content_hash(&s), content_hash(&s));
}
#[test]
fn content_hash_differs_for_different_input() {
assert_ne!(
content_hash(&Dummy { a: 1, b: "x" }),
content_hash(&Dummy { a: 2, b: "x" })
);
}
#[test]
fn content_hash_length_is_26() {
assert_eq!(content_hash(&Dummy { a: 0, b: "" }).len(), 26);
}
#[test]
fn alphabet_excludes_ambiguous() {
let h = content_hash(&Dummy {
a: u32::MAX,
b: "qwertyuiopasdfghjklzxcvbnm",
});
for c in h.chars() {
assert!(!matches!(c, 'i' | 'l' | 'o' | 'u'), "saw {c}");
}
}
#[test]
fn override_wins() {
let id = derive_identity(&Dummy { a: 1, b: "x" }, Some("seph"));
assert_eq!(id.name, "seph");
assert!(id.name_override);
assert_eq!(id.content_hash.len(), 26);
}
#[test]
fn empty_override_falls_back_to_hash() {
let id = derive_identity(&Dummy { a: 1, b: "x" }, Some(" "));
assert!(!id.name_override);
assert_eq!(id.name, id.content_hash);
}
#[test]
fn address_format() {
let id = Identity {
name: "seph".into(),
content_hash: "a".repeat(26),
name_override: true,
};
assert_eq!(format_process_address(&id, "1.7"), "seph.1.7");
}
#[test]
fn pid_path_separator_is_dot() {
assert_eq!(PID_PATH_SEPARATOR, '.');
}
#[test]
fn join_pid_segment_composes_head_then_separator_then_tail() {
assert_eq!(join_pid_segment("seph", "1"), "seph.1");
}
#[test]
fn join_pid_segment_composes_pid_path_tail_verbatim() {
assert_eq!(join_pid_segment("seph", "1.7"), "seph.1.7");
assert_eq!(join_pid_segment("seph.1", "7"), "seph.1.7");
}
#[test]
fn join_pid_segment_accepts_display_tail() {
assert_eq!(join_pid_segment("seph.1", 7u32), "seph.1.7");
for seq in [0u32, 1, 42, u32::MAX] {
assert_eq!(
join_pid_segment("seph.1", seq),
format!("seph.1.{seq}"),
"join must match pre-lift `format!(\"{{parent}}.{{seq}}\")` for seq={seq}"
);
}
}
#[test]
fn join_pid_segment_routes_through_pid_path_separator_const() {
let composed = join_pid_segment("seph", "1");
let mut chars = composed.chars();
assert_eq!(chars.next(), Some('s'));
assert_eq!(chars.next(), Some('e'));
assert_eq!(chars.next(), Some('p'));
assert_eq!(chars.next(), Some('h'));
assert_eq!(chars.next(), Some(PID_PATH_SEPARATOR));
assert_eq!(chars.next(), Some('1'));
}
#[test]
fn format_process_address_composes_through_join_pid_segment() {
let id = Identity {
name: "seph".into(),
content_hash: "a".repeat(26),
name_override: true,
};
for pid_path in ["1", "1.7", "1.7.3"] {
assert_eq!(
format_process_address(&id, pid_path),
join_pid_segment(&id.name, pid_path),
);
}
}
}