#[derive(Debug, Clone, Eq, PartialEq)]
pub(crate) struct ParsedTraceParent {
pub trace_id: String,
pub span_id: String,
}
pub(crate) fn parse_traceparent(value: &str) -> Option<ParsedTraceParent> {
let mut parts = value.split('-');
let version = parts.next()?;
let trace_id = parts.next()?;
let span_id = parts.next()?;
let flags = parts.next()?;
if parts.next().is_some() || version.len() != 2 || flags.len() != 2 {
return None;
}
if !is_lower_hex(version) || !is_lower_hex(flags) || version == "ff" {
return None;
}
if trace_id.len() != 32 || span_id.len() != 16 {
return None;
}
if trace_id.chars().all(|c| c == '0') || span_id.chars().all(|c| c == '0') {
return None;
}
if !is_lower_hex(trace_id) || !is_lower_hex(span_id) {
return None;
}
Some(ParsedTraceParent {
trace_id: trace_id.to_string(),
span_id: span_id.to_string(),
})
}
fn is_lower_hex(value: &str) -> bool {
value
.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_valid_traceparent() {
let parsed =
parse_traceparent("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01").unwrap();
assert_eq!(parsed.trace_id, "4bf92f3577b34da6a3ce929d0e0e4736");
assert_eq!(parsed.span_id, "00f067aa0ba902b7");
}
#[test]
fn rejects_forbidden_ff_version() {
assert!(
parse_traceparent("ff-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01").is_none()
);
}
#[test]
fn rejects_non_hex_version_and_flags() {
assert!(
parse_traceparent("0g-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01").is_none()
);
assert!(
parse_traceparent("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-zz").is_none()
);
}
#[test]
fn rejects_uppercase_hex_fields() {
assert!(
parse_traceparent("00-4BF92F3577B34DA6A3CE929D0E0E4736-00f067aa0ba902b7-01").is_none()
);
}
}