use blake3;
use sha2::{Digest, Sha256};
use std::fmt;
use std::hash::Hash;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SymbolIdError {
InvalidLength { length: usize },
InvalidHex { invalid_char: char },
InvalidCase,
}
impl fmt::Display for SymbolIdError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidLength { length } => write!(
f,
"Invalid symbol ID length: {} (expected 16 or 32 characters)",
length
),
Self::InvalidHex { invalid_char } => {
write!(f, "Invalid character in symbol ID: '{}'", invalid_char)
}
Self::InvalidCase => write!(f, "Symbol ID must be lowercase hexadecimal"),
}
}
}
impl std::error::Error for SymbolIdError {}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum SymbolId {
V1(String),
V2(String),
}
impl SymbolId {
pub fn parse(input: &str) -> Result<Self, SymbolIdError> {
Self::validate(input)?;
match input.len() {
16 => Ok(SymbolId::V1(input.to_string())),
32 => Ok(SymbolId::V2(input.to_string())),
_ => Err(SymbolIdError::InvalidLength {
length: input.len(),
}),
}
}
pub fn new_v1_unchecked(id: impl Into<String>) -> Self {
SymbolId::V1(id.into())
}
pub fn new_v2_unchecked(id: impl Into<String>) -> Self {
SymbolId::V2(id.into())
}
fn validate(id: &str) -> Result<(), SymbolIdError> {
if id.len() != 16 && id.len() != 32 {
return Err(SymbolIdError::InvalidLength { length: id.len() });
}
for c in id.chars() {
if !c.is_ascii_hexdigit() {
return Err(SymbolIdError::InvalidHex { invalid_char: c });
}
if c.is_ascii_alphabetic() && c.is_ascii_uppercase() {
return Err(SymbolIdError::InvalidCase);
}
}
Ok(())
}
pub fn as_str(&self) -> &str {
match self {
SymbolId::V1(s) => s,
SymbolId::V2(s) => s,
}
}
pub fn into_inner(self) -> String {
match self {
SymbolId::V1(s) => s,
SymbolId::V2(s) => s,
}
}
pub fn is_v1(&self) -> bool {
matches!(self, SymbolId::V1(_))
}
pub fn is_v2(&self) -> bool {
matches!(self, SymbolId::V2(_))
}
}
impl fmt::Display for SymbolId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SymbolId::V1(s) => write!(f, "{}", s),
SymbolId::V2(s) => write!(f, "{}", s),
}
}
}
impl AsRef<str> for SymbolId {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl TryFrom<String> for SymbolId {
type Error = SymbolIdError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::parse(&value)
}
}
impl TryFrom<&str> for SymbolId {
type Error = SymbolIdError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::parse(value)
}
}
pub fn generate_v1(name: &str, file_path: &str, byte_start: usize) -> SymbolId {
let mut hasher = Sha256::new();
hasher.update(name.as_bytes());
hasher.update(b":");
hasher.update(file_path.as_bytes());
hasher.update(b":");
hasher.update(byte_start.to_be_bytes());
let result = hasher.finalize();
let hex_id = format!(
"{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
result[0], result[1], result[2], result[3], result[4], result[5], result[6], result[7]
);
SymbolId::V1(hex_id)
}
pub fn generate_v2(name: &str, file_path: &str, byte_start: usize) -> SymbolId {
let mut hasher = blake3::Hasher::new();
hasher.update(name.as_bytes());
hasher.update(b":");
hasher.update(file_path.as_bytes());
hasher.update(b":");
hasher.update(&byte_start.to_be_bytes());
let hash = hasher.finalize();
let hex_id = format!(
"{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
hash.as_bytes()[0], hash.as_bytes()[1], hash.as_bytes()[2], hash.as_bytes()[3],
hash.as_bytes()[4], hash.as_bytes()[5], hash.as_bytes()[6], hash.as_bytes()[7],
hash.as_bytes()[8], hash.as_bytes()[9], hash.as_bytes()[10], hash.as_bytes()[11],
hash.as_bytes()[12], hash.as_bytes()[13], hash.as_bytes()[14], hash.as_bytes()[15]
);
SymbolId::V2(hex_id)
}
pub fn generate_symbol_id(name: &str, file_path: &str, byte_start: usize) -> SymbolId {
generate_v2(name, file_path, byte_start)
}
pub fn generate_execution_id() -> String {
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let pid = std::process::id();
format!(
"{:08x}-{:04x}",
timestamp & 0xFFFFFFFF, pid & 0xFFFF )
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_symbol_id_parse_v1_format() {
let id = generate_v1("test_function", "src/test.rs", 100);
let id_str = id.as_str();
assert_eq!(
id_str.len(),
16,
"V1 Symbol ID should be exactly 16 characters"
);
assert!(
id_str.chars().all(|c| {
c.is_ascii_hexdigit() && (!c.is_ascii_alphabetic() || c.is_lowercase())
}),
"All characters should be lowercase hex"
);
}
#[test]
fn test_symbol_id_parse_v2_format() {
let id = generate_v2("test_function", "src/test.rs", 100);
let id_str = id.as_str();
assert_eq!(
id_str.len(),
32,
"V2 Symbol ID should be exactly 32 characters"
);
assert!(
id_str.chars().all(|c| {
c.is_ascii_hexdigit() && (!c.is_ascii_alphabetic() || c.is_lowercase())
}),
"All characters should be lowercase hex"
);
}
#[test]
fn test_symbol_id_parse_accepts_both_formats() {
let id_v1 = SymbolId::parse("a1b2c3d4e5f67890").unwrap();
assert!(id_v1.is_v1());
assert!(!id_v1.is_v2());
assert_eq!(id_v1.as_str(), "a1b2c3d4e5f67890");
let id_v2 = SymbolId::parse("a1b2c3d4e5f67890a1b2c3d4e5f67890").unwrap();
assert!(id_v2.is_v2());
assert!(!id_v2.is_v1());
assert_eq!(id_v2.as_str(), "a1b2c3d4e5f67890a1b2c3d4e5f67890");
assert!(SymbolId::parse("abc123").is_err());
assert!(SymbolId::parse("a1b2c3d4e5f67890123").is_err());
}
#[test]
fn test_symbol_id_v1_deterministic() {
let id1 = generate_v1("my_func", "src/lib.rs", 42);
let id2 = generate_v1("my_func", "src/lib.rs", 42);
assert_eq!(id1, id2, "Same inputs should produce same V1 ID");
assert_eq!(id1.as_str(), id2.as_str());
assert!(id1.is_v1());
}
#[test]
fn test_symbol_id_v2_deterministic() {
let id1 = generate_v2("my_func", "src/lib.rs", 42);
let id2 = generate_v2("my_func", "src/lib.rs", 42);
assert_eq!(id1, id2, "Same inputs should produce same V2 ID");
assert_eq!(id1.as_str(), id2.as_str());
assert!(id2.is_v2());
}
#[test]
fn test_symbol_id_v1_different_inputs() {
let id1 = generate_v1("func_a", "src/lib.rs", 0);
let id2 = generate_v1("func_b", "src/lib.rs", 0);
let id3 = generate_v1("func_a", "src/main.rs", 0);
let id4 = generate_v1("func_a", "src/lib.rs", 10);
assert_ne!(id1, id2, "Different names should produce different V1 IDs");
assert_ne!(id1, id3, "Different paths should produce different V1 IDs");
assert_ne!(
id1, id4,
"Different byte offsets should produce different V1 IDs"
);
assert_ne!(id2, id3);
assert_ne!(id2, id4);
assert_ne!(id3, id4);
}
#[test]
fn test_symbol_id_v2_different_inputs() {
let id1 = generate_v2("func_a", "src/lib.rs", 0);
let id2 = generate_v2("func_b", "src/lib.rs", 0);
let id3 = generate_v2("func_a", "src/main.rs", 0);
let id4 = generate_v2("func_a", "src/lib.rs", 10);
assert_ne!(id1, id2, "Different names should produce different V2 IDs");
assert_ne!(id1, id3, "Different paths should produce different V2 IDs");
assert_ne!(
id1, id4,
"Different byte offsets should produce different V2 IDs"
);
assert_ne!(id2, id3);
assert_ne!(id2, id4);
assert_ne!(id3, id4);
}
#[test]
fn test_execution_id_format() {
let exec_id = generate_execution_id();
assert_eq!(
exec_id.len(),
13,
"Execution ID should be 13 characters (8-1-4)"
);
let parts: Vec<&str> = exec_id.split('-').collect();
assert_eq!(parts.len(), 2, "Should have exactly one dash");
let (timestamp_part, pid_part) = (parts[0], parts[1]);
assert_eq!(
timestamp_part.len(),
8,
"Timestamp part should be 8 hex characters"
);
assert_eq!(pid_part.len(), 4, "PID part should be 4 hex characters");
assert!(
timestamp_part
.chars()
.all(|c| c.is_ascii_hexdigit() && (!c.is_ascii_alphabetic() || c.is_lowercase())),
"Timestamp part should be lowercase hex"
);
assert!(
pid_part
.chars()
.all(|c| c.is_ascii_hexdigit() && (!c.is_ascii_alphabetic() || c.is_lowercase())),
"PID part should be lowercase hex"
);
}
#[test]
fn test_symbol_id_display() {
let id_v1 = SymbolId::parse("a1b2c3d4e5f67890").unwrap();
let displayed_v1 = format!("{}", id_v1);
assert_eq!(displayed_v1, "a1b2c3d4e5f67890");
let id_v2 = SymbolId::parse("a1b2c3d4e5f67890a1b2c3d4e5f67890").unwrap();
let displayed_v2 = format!("{}", id_v2);
assert_eq!(displayed_v2, "a1b2c3d4e5f67890a1b2c3d4e5f67890");
}
#[test]
fn test_symbol_id_invalid_rejected() {
assert!(matches!(
SymbolId::parse("abc123").unwrap_err(),
SymbolIdError::InvalidLength { length: 6 }
));
assert!(matches!(
SymbolId::parse("a1b2c3d4e5f678901234").unwrap_err(),
SymbolIdError::InvalidLength { .. }
));
assert!(matches!(
SymbolId::parse("abcdefghijklmnop").unwrap_err(),
SymbolIdError::InvalidHex { .. }
));
assert!(matches!(
SymbolId::parse("A1B2C3D4E5F67890").unwrap_err(),
SymbolIdError::InvalidCase
));
assert!(SymbolId::parse("a1b2c3d4e5f6789g").is_err());
}
#[test]
fn test_symbol_id_hash() {
use std::collections::hash_map::DefaultHasher;
use std::hash::Hasher;
let id1 = SymbolId::parse("a1b2c3d4e5f67890").unwrap();
let id2 = SymbolId::parse("a1b2c3d4e5f67890").unwrap();
let id3 = SymbolId::parse("b2c3d4e5f67890a1").unwrap();
let mut h1 = DefaultHasher::new();
let mut h2 = DefaultHasher::new();
let mut h3 = DefaultHasher::new();
id1.hash(&mut h1);
id2.hash(&mut h2);
id3.hash(&mut h3);
assert_eq!(h1.finish(), h2.finish());
assert_ne!(h1.finish(), h3.finish());
}
#[test]
fn test_symbol_id_clone() {
let id1 = SymbolId::parse("a1b2c3d4e5f67890").unwrap();
let id2 = id1.clone();
assert_eq!(id1, id2);
assert_eq!(id1.as_str(), id2.as_str());
}
#[test]
fn test_symbol_id_try_from() {
let id1 = SymbolId::try_from("a1b2c3d4e5f67890".to_string()).unwrap();
assert_eq!(id1.as_str(), "a1b2c3d4e5f67890");
assert!(id1.is_v1());
let id2 = SymbolId::try_from("a1b2c3d4e5f67890").unwrap();
assert_eq!(id2.as_str(), "a1b2c3d4e5f67890");
assert!(id2.is_v1());
let id3 = SymbolId::try_from("a1b2c3d4e5f67890a1b2c3d4e5f67890").unwrap();
assert_eq!(id3.as_str(), "a1b2c3d4e5f67890a1b2c3d4e5f67890");
assert!(id3.is_v2());
assert!(SymbolId::try_from("invalid").is_err());
assert!(SymbolId::try_from("".to_string()).is_err());
}
#[test]
fn test_symbol_id_into_inner() {
let original = "a1b2c3d4e5f67890";
let id = SymbolId::parse(original).unwrap();
let inner = id.into_inner();
assert_eq!(inner, original);
}
#[test]
fn test_symbol_id_as_ref() {
let id = SymbolId::parse("a1b2c3d4e5f67890").unwrap();
let s: &str = id.as_ref();
assert_eq!(s, "a1b2c3d4e5f67890");
}
#[test]
fn test_generate_v1_edge_cases() {
let id1 = generate_v1("", "src/test.rs", 0);
assert_eq!(id1.as_str().len(), 16);
assert!(id1.is_v1());
let id2 = generate_v1("func", "", 0);
assert_eq!(id2.as_str().len(), 16);
assert!(id2.is_v1());
let id3 = generate_v1("func", "src/test.rs", usize::MAX);
assert_eq!(id3.as_str().len(), 16);
assert!(id3.is_v1());
assert_ne!(id1, id2);
assert_ne!(id2, id3);
}
#[test]
fn test_generate_v2_edge_cases() {
let id1 = generate_v2("", "src/test.rs", 0);
assert_eq!(id1.as_str().len(), 32);
assert!(id1.is_v2());
let id2 = generate_v2("func", "", 0);
assert_eq!(id2.as_str().len(), 32);
assert!(id2.is_v2());
let id3 = generate_v2("func", "src/test.rs", usize::MAX);
assert_eq!(id3.as_str().len(), 32);
assert!(id3.is_v2());
assert_ne!(id1, id2);
assert_ne!(id2, id3);
}
#[test]
fn test_execution_id_uniqueness() {
let id1 = generate_execution_id();
let id2 = generate_execution_id();
assert_eq!(id1.len(), 13);
assert_eq!(id2.len(), 13);
for id in [id1, id2] {
let parts: Vec<&str> = id.split('-').collect();
assert_eq!(parts.len(), 2);
assert_eq!(parts[0].len(), 8);
assert_eq!(parts[1].len(), 4);
}
}
#[test]
fn test_generate_symbol_id_defaults_to_v2() {
let id = generate_symbol_id("test", "src/test.rs", 0);
assert_eq!(id.as_str().len(), 32);
assert!(id.is_v2());
assert!(!id.is_v1());
}
#[test]
fn test_new_v1_unchecked() {
let id = SymbolId::new_v1_unchecked("a1b2c3d4e5f67890");
assert!(id.is_v1());
assert_eq!(id.as_str(), "a1b2c3d4e5f67890");
}
#[test]
fn test_new_v2_unchecked() {
let id = SymbolId::new_v2_unchecked("a1b2c3d4e5f67890a1b2c3d4e5f67890");
assert!(id.is_v2());
assert_eq!(id.as_str(), "a1b2c3d4e5f67890a1b2c3d4e5f67890");
}
}