use std::fmt;
use std::sync::OnceLock;
use crate::identity::{AgentId, MachineId, UserId};
pub use saorsa_gossip_types::{LogPeerId, LogTopicId};
static LOG_SALT: OnceLock<[u8; 32]> = OnceLock::new();
fn salt() -> &'static [u8; 32] {
LOG_SALT.get_or_init(|| {
use rand::RngCore;
let mut s = [0u8; 32];
rand::thread_rng().fill_bytes(&mut s);
s
})
}
#[doc(hidden)]
pub fn init_log_salt_for_tests(seed: [u8; 32]) -> bool {
LOG_SALT.set(seed).is_ok()
}
fn opaque_token(bytes: &[u8]) -> String {
let mut hasher = blake3::Hasher::new_keyed(salt());
hasher.update(bytes);
hex::encode(&hasher.finalize().as_bytes()[..4])
}
macro_rules! log_id_wrapper {
($(#[$doc:meta])* $name:ident, $inner:ty, $prefix:literal) => {
$(#[$doc])*
#[derive(Clone, Copy)]
pub struct $name([u8; 32]);
impl From<&$inner> for $name {
fn from(id: &$inner) -> Self {
Self(id.0)
}
}
impl From<$inner> for $name {
fn from(id: $inner) -> Self {
Self(id.0)
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, concat!($prefix, "_{}"), opaque_token(&self.0))
}
}
impl fmt::Debug for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self, f)
}
}
};
}
log_id_wrapper!(
LogAgentId,
AgentId,
"agent"
);
log_id_wrapper!(
LogMachineId,
MachineId,
"machine"
);
log_id_wrapper!(
LogUserId,
UserId,
"user"
);
log_id_wrapper!(
LogTransportPeerId,
ant_quic::PeerId,
"peer"
);
#[derive(Clone, Copy)]
pub struct LogHexId<'a> {
prefix: &'static str,
id: &'a str,
}
impl<'a> LogHexId<'a> {
#[must_use]
pub fn new<S: AsRef<str> + ?Sized>(prefix: &'static str, id: &'a S) -> Self {
Self {
prefix,
id: id.as_ref(),
}
}
#[must_use]
pub fn group<S: AsRef<str> + ?Sized>(id: &'a S) -> Self {
Self::new("group", id)
}
#[must_use]
pub fn topic<S: AsRef<str> + ?Sized>(id: &'a S) -> Self {
Self::new("topic", id)
}
#[must_use]
pub fn agent<S: AsRef<str> + ?Sized>(id: &'a S) -> Self {
Self::new("agent", id)
}
#[must_use]
pub fn addr<S: AsRef<str> + ?Sized>(id: &'a S) -> Self {
Self::new("addr", id)
}
}
impl fmt::Display for LogHexId<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}_{}", self.prefix, opaque_token(self.id.as_bytes()))
}
}
impl fmt::Debug for LogHexId<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self, f)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tokens_are_stable_within_process_and_leak_nothing() {
init_log_salt_for_tests([42u8; 32]);
let agent = AgentId([0xAB; 32]);
let a1 = format!("{}", LogAgentId::from(&agent));
let a2 = format!("{}", LogAgentId::from(&agent));
assert_eq!(a1, a2, "same id must render the same token in-process");
assert!(a1.starts_with("agent_"), "prefixed for readability: {a1}");
assert_eq!(a1.len(), "agent_".len() + 8, "8 hex chars: {a1}");
let real_hex = hex::encode([0xAB; 32]);
assert!(
!a1.contains(&real_hex[..8]),
"token must not embed the real id"
);
}
#[test]
fn different_ids_get_different_tokens() {
init_log_salt_for_tests([42u8; 32]);
let a = format!("{}", LogAgentId::from(AgentId([1u8; 32])));
let b = format!("{}", LogAgentId::from(AgentId([2u8; 32])));
assert_ne!(a, b);
}
#[test]
fn debug_formatter_does_not_leak() {
init_log_salt_for_tests([42u8; 32]);
let agent = AgentId([0xCD; 32]);
assert_eq!(
format!("{:?}", LogAgentId::from(&agent)),
format!("{}", LogAgentId::from(&agent)),
"Debug must mirror Display so tracing's ? sigil cannot leak"
);
}
#[test]
fn string_identifier_wrapper_redacts_groups_and_topics() {
init_log_salt_for_tests([42u8; 32]);
let group_hex = "deadbeefdeadbeefdeadbeefdeadbeef";
let g = format!("{}", LogHexId::group(group_hex));
assert!(g.starts_with("group_"));
assert!(!g.contains("deadbeef"), "must not leak the group id: {g}");
let t = format!("{}", LogHexId::topic("secret-team-channel"));
assert!(!t.contains("secret"), "must not leak the topic name: {t}");
}
}