use std::collections::HashMap;
use std::fmt;
use std::sync::{Mutex, OnceLock};
use hmac::{Hmac, Mac};
use sha2::Sha256;
use uuid::Uuid;
use zeroize::Zeroize;
use crate::error::Error;
use crate::source::{Probe, Source, SourceKind};
type HmacSha256 = Hmac<Sha256>;
pub struct AppSpecific<S: Source> {
inner: S,
app_id: Vec<u8>,
label: &'static str,
}
impl<S: Source> AppSpecific<S> {
#[must_use]
pub fn new(inner: S, app_id: impl Into<Vec<u8>>) -> Self {
let label = intern_label(inner.kind().as_str());
Self {
inner,
app_id: app_id.into(),
label,
}
}
}
fn intern_label(inner_id: &'static str) -> &'static str {
static INTERNER: OnceLock<Mutex<HashMap<&'static str, &'static str>>> = OnceLock::new();
let mut map = INTERNER
.get_or_init(|| Mutex::new(HashMap::new()))
.lock()
.expect("label interner mutex poisoned");
if let Some(&existing) = map.get(inner_id) {
return existing;
}
let leaked: &'static str = Box::leak(format!("app-specific:{inner_id}").into_boxed_str());
map.insert(inner_id, leaked);
leaked
}
impl<S: Source> fmt::Debug for AppSpecific<S> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AppSpecific")
.field("inner", &self.inner.kind())
.field("app_id_len", &self.app_id.len())
.finish_non_exhaustive()
}
}
impl<S: Source> Drop for AppSpecific<S> {
fn drop(&mut self) {
self.app_id.zeroize();
}
}
impl<S: Source> Source for AppSpecific<S> {
fn kind(&self) -> SourceKind {
SourceKind::Custom(self.label)
}
fn probe(&self) -> Result<Option<Probe>, Error> {
let Some(probe) = self.inner.probe()? else {
return Ok(None);
};
let (_inner_kind, raw) = probe.into_parts();
let uuid = derive_app_specific_uuid(raw.as_bytes(), &self.app_id);
Ok(Some(Probe::new(self.kind(), uuid.hyphenated().to_string())))
}
}
fn derive_app_specific_uuid(raw: &[u8], app_id: &[u8]) -> Uuid {
let mut mac = HmacSha256::new_from_slice(raw).expect("HMAC-SHA256 accepts keys of any length");
mac.update(app_id);
let digest = mac.finalize().into_bytes();
let mut buf = [0u8; 16];
buf.copy_from_slice(&digest[..16]);
buf[6] = (buf[6] & 0x0F) | 0x40;
buf[8] = (buf[8] & 0x3F) | 0x80;
let uuid = Uuid::from_bytes(buf);
buf.zeroize();
uuid
}
#[cfg(test)]
mod tests {
use super::*;
use crate::source::{Probe, Source, SourceKind};
use crate::wrap::Wrap;
#[derive(Debug)]
struct Stub {
kind: SourceKind,
result: Result<Option<String>, &'static str>,
}
impl Stub {
fn ok(kind: SourceKind, v: &str) -> Self {
Self {
kind,
result: Ok(Some(v.to_owned())),
}
}
fn none(kind: SourceKind) -> Self {
Self {
kind,
result: Ok(None),
}
}
fn err(kind: SourceKind, msg: &'static str) -> Self {
Self {
kind,
result: Err(msg),
}
}
}
impl Source for Stub {
fn kind(&self) -> SourceKind {
self.kind
}
fn probe(&self) -> Result<Option<Probe>, Error> {
match &self.result {
Ok(Some(v)) => Ok(Some(Probe::new(self.kind, v.clone()))),
Ok(None) => Ok(None),
Err(msg) => Err(Error::Malformed {
source_kind: self.kind,
reason: (*msg).to_owned(),
}),
}
}
}
fn probe_value(s: &impl Source) -> String {
s.probe().unwrap().unwrap().value().to_owned()
}
#[test]
fn output_is_a_valid_version4_uuid() {
let wrapped = AppSpecific::new(
Stub::ok(SourceKind::MachineId, "abcdef0123456789abcdef0123456789"),
b"com.example.test".to_vec(),
);
let v = probe_value(&wrapped);
let parsed = Uuid::parse_str(&v).expect("valid UUID");
assert_eq!(parsed.get_version_num(), 4);
let variant_byte = parsed.as_bytes()[8];
assert_eq!(variant_byte & 0xC0, 0x80, "variant must be 10xx");
let re_parts: Vec<_> = v.split('-').collect();
assert_eq!(
re_parts.iter().map(|p| p.len()).collect::<Vec<_>>(),
vec![8, 4, 4, 4, 12]
);
assert!(v.chars().all(|c| c.is_ascii_hexdigit() || c == '-'));
}
#[test]
fn construction_matches_manual_hmac_sha256() {
let raw = b"abcdef0123456789abcdef0123456789";
let app_id: [u8; 16] = [
0xa2, 0xb1, 0x6c, 0x2f, 0x0f, 0xa0, 0x4d, 0x32, 0xb3, 0xc3, 0x1e, 0xe8, 0xc2, 0x2c,
0x0b, 0x7e,
];
let got = derive_app_specific_uuid(raw, &app_id);
let mut mac = HmacSha256::new_from_slice(raw).unwrap();
mac.update(&app_id);
let digest = mac.finalize().into_bytes();
let mut buf = [0u8; 16];
buf.copy_from_slice(&digest[..16]);
buf[6] = (buf[6] & 0x0F) | 0x40;
buf[8] = (buf[8] & 0x3F) | 0x80;
assert_eq!(got, Uuid::from_bytes(buf));
assert_eq!(got.get_version_num(), 4);
}
#[test]
fn determinism_over_many_iterations() {
let wrapped = AppSpecific::new(
Stub::ok(SourceKind::MachineId, "raw-value"),
b"app".to_vec(),
);
let first = probe_value(&wrapped);
for _ in 0..100 {
assert_eq!(probe_value(&wrapped), first);
}
}
#[test]
fn different_app_ids_produce_different_outputs() {
let a = AppSpecific::new(Stub::ok(SourceKind::MachineId, "x"), b"app-1".to_vec());
let b = AppSpecific::new(Stub::ok(SourceKind::MachineId, "x"), b"app-2".to_vec());
assert_ne!(probe_value(&a), probe_value(&b));
}
#[test]
fn different_inner_values_produce_different_outputs() {
let a = AppSpecific::new(Stub::ok(SourceKind::MachineId, "x"), b"app".to_vec());
let b = AppSpecific::new(Stub::ok(SourceKind::MachineId, "y"), b"app".to_vec());
assert_ne!(probe_value(&a), probe_value(&b));
}
#[test]
fn passthrough_wrap_round_trips_the_probe() {
let wrapped = AppSpecific::new(Stub::ok(SourceKind::MachineId, "raw"), b"app".to_vec());
let v = probe_value(&wrapped);
let roundtrip = Wrap::Passthrough.apply(&v).expect("UUID-shaped");
assert_eq!(roundtrip, Uuid::parse_str(&v).unwrap());
}
#[test]
fn default_wrap_is_stable() {
let wrapped = AppSpecific::new(Stub::ok(SourceKind::MachineId, "raw"), b"app".to_vec());
let v1 = probe_value(&wrapped);
let v2 = probe_value(&wrapped);
assert_eq!(
Wrap::UuidV5Namespaced.apply(&v1),
Wrap::UuidV5Namespaced.apply(&v2),
);
}
#[test]
fn scope_label_is_app_specific_prefixed() {
let wrapped = AppSpecific::new(Stub::ok(SourceKind::MachineId, "raw"), b"app".to_vec());
assert_eq!(wrapped.kind().as_str(), "app-specific:machine-id");
let probe = wrapped.probe().unwrap().unwrap();
assert_eq!(probe.kind().as_str(), "app-specific:machine-id");
}
#[test]
fn inner_none_is_passed_through() {
let wrapped = AppSpecific::new(Stub::none(SourceKind::MachineId), b"app".to_vec());
assert!(wrapped.probe().unwrap().is_none());
}
#[test]
fn inner_err_is_passed_through() {
let wrapped = AppSpecific::new(Stub::err(SourceKind::MachineId, "boom"), b"app".to_vec());
let err = wrapped.probe().expect_err("error must propagate");
match err {
Error::Malformed {
source_kind,
reason,
} => {
assert_eq!(source_kind, SourceKind::MachineId);
assert_eq!(reason, "boom");
}
other => panic!("unexpected error variant: {other:?}"),
}
}
#[test]
fn label_is_interned_across_constructions() {
let a = AppSpecific::new(Stub::ok(SourceKind::MachineId, "x"), b"a".to_vec());
let b = AppSpecific::new(Stub::ok(SourceKind::MachineId, "y"), b"b".to_vec());
let (SourceKind::Custom(la), SourceKind::Custom(lb)) = (a.kind(), b.kind()) else {
panic!("AppSpecific must report SourceKind::Custom");
};
assert!(std::ptr::eq(la, lb), "label must be interned");
}
#[test]
fn empty_inputs_do_not_panic_and_produce_valid_uuids() {
let wrapped = AppSpecific::new(Stub::ok(SourceKind::MachineId, ""), Vec::<u8>::new());
let v = probe_value(&wrapped);
let parsed = Uuid::parse_str(&v).expect("valid UUID even with empty inputs");
assert_eq!(parsed.get_version_num(), 4);
}
}