#[derive(Debug, thiserror::Error)]
pub enum SidError {
#[error("failed to read platform user identity: {0}")]
PlatformLookup(String),
}
pub fn user_sid_hash() -> Result<String, SidError> {
let input = platform_identity_string()?;
Ok(hash_to_16_hex(input.as_bytes()))
}
pub fn hash_to_16_hex(input: &[u8]) -> String {
let digest = blake3::hash(input);
let bytes = digest.as_bytes();
let mut out = String::with_capacity(16);
for b in &bytes[..8] {
out.push(nibble_to_hex(b >> 4));
out.push(nibble_to_hex(b & 0x0F));
}
out
}
#[inline]
fn nibble_to_hex(n: u8) -> char {
match n {
0..=9 => (b'0' + n) as char,
10..=15 => (b'a' + (n - 10)) as char,
_ => unreachable!("nibble out of range"),
}
}
fn platform_identity_string() -> Result<String, SidError> {
crate::platform::host::user_machine_identity()
.map_err(|error| SidError::PlatformLookup(error.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hash_is_16_lowercase_hex() {
let h = hash_to_16_hex(b"sample-input");
assert_eq!(h.len(), 16, "hash must be 16 chars");
for c in h.chars() {
assert!(
c.is_ascii_digit() || ('a'..='f').contains(&c),
"non-lowercase-hex char in {h:?}"
);
}
}
#[test]
fn different_inputs_yield_different_hashes() {
let a = hash_to_16_hex(b"alice:machine-1");
let b = hash_to_16_hex(b"bob:machine-1");
assert_ne!(a, b);
}
#[test]
fn same_input_is_stable() {
let a = hash_to_16_hex(b"alice:machine-1");
let b = hash_to_16_hex(b"alice:machine-1");
assert_eq!(a, b);
}
#[test]
fn current_user_hash_resolves() {
match user_sid_hash() {
Ok(h) => {
assert_eq!(h.len(), 16);
}
Err(e) => {
eprintln!("user_sid_hash unavailable on this host: {e}");
}
}
}
}