Skip to main content

rmcp_traces/
trace_context.rs

1use std::{collections::BTreeSet, error::Error, fmt};
2
3use rmcp::model::Meta;
4use serde_json::Value;
5
6pub const TRACEPARENT_KEY: &str = "traceparent";
7pub const TRACESTATE_KEY: &str = "tracestate";
8pub const BAGGAGE_KEY: &str = "baggage";
9
10const TRACEPARENT_V00_LEN: usize = 55;
11const TRACEPARENT_VERSION_END: usize = 2;
12const TRACEPARENT_TRACE_ID_START: usize = 3;
13const TRACEPARENT_TRACE_ID_END: usize = 35;
14const TRACEPARENT_SPAN_ID_START: usize = 36;
15const TRACEPARENT_SPAN_ID_END: usize = 52;
16const TRACEPARENT_FLAGS_START: usize = 53;
17const TRACEPARENT_FLAGS_END: usize = 55;
18const TRACEPARENT_NEXT_SEPARATOR: usize = 55;
19const MAX_TRACESTATE_MEMBERS: usize = 32;
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub enum TraceTrust {
23    Untrusted,
24    Trusted,
25}
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28pub struct TraceLimits {
29    pub max_traceparent_len: usize,
30    pub max_tracestate_len: usize,
31    pub max_baggage_len: usize,
32    pub max_baggage_members: usize,
33}
34
35impl Default for TraceLimits {
36    fn default() -> Self {
37        Self {
38            max_traceparent_len: 512,
39            max_tracestate_len: 512,
40            max_baggage_len: 8 * 1024,
41            max_baggage_members: 64,
42        }
43    }
44}
45
46#[derive(Clone, PartialEq, Eq)]
47pub(crate) struct TraceParent {
48    trace_id: String,
49    span_id: String,
50    sampled: bool,
51}
52
53impl TraceParent {
54    #[cfg(test)]
55    fn parse(value: &str) -> Result<Self, TraceParseError> {
56        let limits = TraceLimits::default();
57        if value.len() > limits.max_traceparent_len {
58            return Err(TraceParseError::ValueTooLong {
59                field: TRACEPARENT_KEY,
60                actual: value.len(),
61                max: limits.max_traceparent_len,
62            });
63        }
64        parse_traceparent(value)
65    }
66
67    #[cfg(test)]
68    fn trace_id(&self) -> &str {
69        &self.trace_id
70    }
71
72    #[cfg(test)]
73    fn span_id(&self) -> &str {
74        &self.span_id
75    }
76
77    fn sampled(&self) -> bool {
78        self.sampled
79    }
80
81    fn trace_id_short(&self) -> &str {
82        &self.trace_id[..8]
83    }
84
85    fn span_id_short(&self) -> &str {
86        &self.span_id[..8]
87    }
88}
89
90impl fmt::Debug for TraceParent {
91    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        f.debug_struct("TraceParent")
93            .field("trace_id", &self.trace_id_short())
94            .field("span_id", &self.span_id_short())
95            .field("sampled", &self.sampled)
96            .finish()
97    }
98}
99
100#[cfg(test)]
101#[derive(Clone, PartialEq, Eq)]
102struct TraceContext {
103    traceparent: TraceParent,
104    tracestate: Option<String>,
105    baggage: Option<String>,
106    trust: TraceTrust,
107}
108
109#[cfg(test)]
110impl TraceContext {
111    fn from_meta(meta: &Meta, trust: TraceTrust) -> Result<Option<Self>, TraceParseError> {
112        Self::from_meta_with_limits(meta, trust, TraceLimits::default())
113    }
114
115    fn from_meta_with_limits(
116        meta: &Meta,
117        trust: TraceTrust,
118        limits: TraceLimits,
119    ) -> Result<Option<Self>, TraceParseError> {
120        let Some(traceparent) = parse_meta_traceparent(meta, limits)? else {
121            return Ok(None);
122        };
123        let tracestate =
124            bounded_optional_meta_string(meta, TRACESTATE_KEY, limits.max_tracestate_len)?;
125        validate_tracestate(tracestate.as_deref())?;
126        let baggage = bounded_optional_meta_string(meta, BAGGAGE_KEY, limits.max_baggage_len)?;
127        validate_baggage(baggage.as_deref(), limits.max_baggage_members)?;
128        Ok(Some(Self {
129            traceparent,
130            tracestate,
131            baggage,
132            trust,
133        }))
134    }
135
136    fn summary(&self) -> TraceSummary {
137        TraceSummary::from_context(self)
138    }
139}
140
141#[cfg(test)]
142impl fmt::Debug for TraceContext {
143    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144        self.summary().fmt(f)
145    }
146}
147
148#[derive(Clone, Debug, PartialEq, Eq)]
149pub struct TraceSummary {
150    trace_id_prefix: Option<String>,
151    span_id_prefix: Option<String>,
152    sampled: Option<bool>,
153    trust: TraceTrust,
154    has_tracestate: bool,
155    baggage_member_count: usize,
156    sensitive_baggage_member_count: usize,
157    invalid_reasons: Vec<String>,
158}
159
160impl TraceSummary {
161    pub fn absent() -> Self {
162        Self::absent_with_trust(TraceTrust::Untrusted)
163    }
164
165    pub(crate) fn absent_with_trust(trust: TraceTrust) -> Self {
166        Self {
167            trace_id_prefix: None,
168            span_id_prefix: None,
169            sampled: None,
170            trust,
171            has_tracestate: false,
172            baggage_member_count: 0,
173            sensitive_baggage_member_count: 0,
174            invalid_reasons: Vec::new(),
175        }
176    }
177
178    pub fn invalid(error: &TraceParseError) -> Self {
179        let mut summary = Self::absent();
180        summary.record_invalid(error);
181        summary
182    }
183
184    pub fn from_meta(meta: &Meta, trust: TraceTrust) -> Self {
185        Self::from_meta_with_limits(meta, trust, TraceLimits::default())
186    }
187
188    pub fn from_meta_with_limits(meta: &Meta, trust: TraceTrust, limits: TraceLimits) -> Self {
189        let (mut summary, has_valid_traceparent) = match parse_meta_traceparent(meta, limits) {
190            Ok(Some(traceparent)) => (Self::from_traceparent(&traceparent, trust), true),
191            Ok(None) => (Self::absent_with_trust(trust), false),
192            Err(error) => {
193                let mut summary = Self::absent_with_trust(trust);
194                summary.record_invalid(&error);
195                (summary, false)
196            }
197        };
198        match bounded_optional_meta_str(meta, TRACESTATE_KEY, limits.max_tracestate_len) {
199            Ok(Some(tracestate)) if has_valid_traceparent => {
200                match validate_tracestate(Some(tracestate)) {
201                    Ok(()) => summary.has_tracestate = true,
202                    Err(error) => summary.record_invalid(&error),
203                }
204            }
205            Ok(Some(_)) => {
206                summary.record_invalid(&TraceParseError::TraceStateRequiresTraceParent);
207            }
208            Ok(None) => {}
209            Err(error) => summary.record_invalid(&error),
210        };
211        match bounded_optional_meta_str(meta, BAGGAGE_KEY, limits.max_baggage_len) {
212            Ok(baggage) => match validate_baggage(baggage, limits.max_baggage_members) {
213                Ok(()) => {
214                    let (baggage_member_count, sensitive_baggage_member_count) =
215                        summarize_baggage(baggage);
216                    summary.baggage_member_count = baggage_member_count;
217                    summary.sensitive_baggage_member_count = sensitive_baggage_member_count;
218                }
219                Err(error) => summary.record_invalid(&error),
220            },
221            Err(error) => {
222                summary.record_invalid(&error);
223            }
224        };
225        summary
226    }
227
228    #[cfg(test)]
229    fn from_context(context: &TraceContext) -> Self {
230        let (baggage_member_count, sensitive_baggage_member_count) =
231            summarize_baggage(context.baggage.as_deref());
232        let mut summary = Self::from_traceparent(&context.traceparent, context.trust);
233        summary.has_tracestate = context.tracestate.is_some();
234        summary.baggage_member_count = baggage_member_count;
235        summary.sensitive_baggage_member_count = sensitive_baggage_member_count;
236        summary
237    }
238
239    fn from_traceparent(traceparent: &TraceParent, trust: TraceTrust) -> Self {
240        Self {
241            trace_id_prefix: Some(traceparent.trace_id_short().to_owned()),
242            span_id_prefix: Some(traceparent.span_id_short().to_owned()),
243            sampled: Some(traceparent.sampled()),
244            trust,
245            has_tracestate: false,
246            baggage_member_count: 0,
247            sensitive_baggage_member_count: 0,
248            invalid_reasons: Vec::new(),
249        }
250    }
251
252    #[cfg(feature = "http")]
253    pub(crate) fn from_valid_traceparent(traceparent: &TraceParent, trust: TraceTrust) -> Self {
254        Self::from_traceparent(traceparent, trust)
255    }
256
257    fn record_invalid(&mut self, error: &TraceParseError) {
258        self.invalid_reasons.push(error.safe_reason());
259    }
260
261    #[cfg(feature = "http")]
262    pub(crate) fn record_invalid_reason(&mut self, error: &TraceParseError) {
263        self.record_invalid(error);
264    }
265
266    #[cfg(feature = "http")]
267    pub(crate) fn set_has_tracestate_for_http(&mut self) {
268        self.has_tracestate = true;
269    }
270
271    #[cfg(feature = "http")]
272    pub(crate) fn set_baggage_counts_for_http(&mut self, baggage: Option<&str>) {
273        let (baggage_member_count, sensitive_baggage_member_count) = summarize_baggage(baggage);
274        self.baggage_member_count = baggage_member_count;
275        self.sensitive_baggage_member_count = sensitive_baggage_member_count;
276    }
277
278    pub fn trace_id_prefix(&self) -> Option<&str> {
279        self.trace_id_prefix.as_deref()
280    }
281
282    pub fn span_id_prefix(&self) -> Option<&str> {
283        self.span_id_prefix.as_deref()
284    }
285
286    pub fn sampled(&self) -> Option<bool> {
287        self.sampled
288    }
289
290    pub fn trust(&self) -> TraceTrust {
291        self.trust
292    }
293
294    pub fn has_tracestate(&self) -> bool {
295        self.has_tracestate
296    }
297
298    pub fn baggage_member_count(&self) -> usize {
299        self.baggage_member_count
300    }
301
302    pub fn sensitive_baggage_member_count(&self) -> usize {
303        self.sensitive_baggage_member_count
304    }
305
306    pub fn invalid_reasons(&self) -> &[String] {
307        &self.invalid_reasons
308    }
309
310    pub fn invalid_count(&self) -> usize {
311        self.invalid_reasons.len()
312    }
313}
314
315#[derive(Clone, Debug, PartialEq, Eq)]
316pub enum TraceParseError {
317    NonStringMeta {
318        field: &'static str,
319    },
320    ValueTooLong {
321        field: &'static str,
322        actual: usize,
323        max: usize,
324    },
325    TooManyBaggageMembers {
326        actual: usize,
327        max: usize,
328    },
329    TooManyTraceStateMembers {
330        actual: usize,
331        max: usize,
332    },
333    InvalidTraceParentLength {
334        actual: usize,
335    },
336    InvalidTraceParentFormat,
337    UnsupportedVersion,
338    InvalidTraceId,
339    InvalidSpanId,
340    TraceStateRequiresTraceParent,
341    InvalidTraceState,
342    DuplicateTraceStateKey,
343    InvalidBaggageMember,
344    #[cfg(feature = "http")]
345    MultipleHeaderValues {
346        field: &'static str,
347    },
348    #[cfg(feature = "http")]
349    InvalidHeaderValue {
350        field: &'static str,
351    },
352}
353
354impl TraceParseError {
355    pub fn safe_reason(&self) -> String {
356        match self {
357            Self::NonStringMeta { field } => format!("{field} was not a string"),
358            Self::ValueTooLong { field, actual, max } => {
359                format!("{field} exceeded {max} bytes (actual {actual})")
360            }
361            Self::TooManyBaggageMembers { actual, max } => {
362                format!("baggage exceeded {max} members (actual at least {actual})")
363            }
364            Self::TooManyTraceStateMembers { actual, max } => {
365                format!("tracestate exceeded {max} members (actual at least {actual})")
366            }
367            Self::InvalidTraceParentLength { actual } => {
368                format!("traceparent length was {actual}, expected 55")
369            }
370            Self::InvalidTraceParentFormat => "traceparent format was invalid".to_owned(),
371            Self::UnsupportedVersion => "traceparent version was unsupported".to_owned(),
372            Self::InvalidTraceId => "traceparent trace id was invalid".to_owned(),
373            Self::InvalidSpanId => "traceparent span id was invalid".to_owned(),
374            Self::TraceStateRequiresTraceParent => {
375                "tracestate requires a valid traceparent".to_owned()
376            }
377            Self::InvalidTraceState => "tracestate format was invalid".to_owned(),
378            Self::DuplicateTraceStateKey => "tracestate contained a duplicate key".to_owned(),
379            Self::InvalidBaggageMember => "baggage member format was invalid".to_owned(),
380            #[cfg(feature = "http")]
381            Self::MultipleHeaderValues { field } => {
382                format!("{field} had multiple header values")
383            }
384            #[cfg(feature = "http")]
385            Self::InvalidHeaderValue { field } => {
386                format!("{field} header value was not visible ASCII")
387            }
388        }
389    }
390}
391
392impl fmt::Display for TraceParseError {
393    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
394        f.write_str(&self.safe_reason())
395    }
396}
397
398impl Error for TraceParseError {}
399
400fn optional_meta_str<'a>(
401    meta: &'a Meta,
402    field: &'static str,
403) -> Result<Option<&'a str>, TraceParseError> {
404    match meta.get(field) {
405        None => Ok(None),
406        Some(Value::String(value)) => Ok(Some(value.as_str())),
407        Some(_) => Err(TraceParseError::NonStringMeta { field }),
408    }
409}
410
411#[cfg(test)]
412fn bounded_optional_meta_string(
413    meta: &Meta,
414    field: &'static str,
415    max: usize,
416) -> Result<Option<String>, TraceParseError> {
417    bounded_optional_meta_str(meta, field, max).map(|value| value.map(str::to_owned))
418}
419
420fn bounded_optional_meta_str<'a>(
421    meta: &'a Meta,
422    field: &'static str,
423    max: usize,
424) -> Result<Option<&'a str>, TraceParseError> {
425    let Some(value) = optional_meta_str(meta, field)? else {
426        return Ok(None);
427    };
428    if value.len() > max {
429        return Err(TraceParseError::ValueTooLong {
430            field,
431            actual: value.len(),
432            max,
433        });
434    }
435    Ok(Some(value))
436}
437
438fn parse_meta_traceparent(
439    meta: &Meta,
440    limits: TraceLimits,
441) -> Result<Option<TraceParent>, TraceParseError> {
442    let Some(traceparent) = optional_meta_str(meta, TRACEPARENT_KEY)? else {
443        return Ok(None);
444    };
445    if traceparent.len() > limits.max_traceparent_len {
446        return Err(TraceParseError::ValueTooLong {
447            field: TRACEPARENT_KEY,
448            actual: traceparent.len(),
449            max: limits.max_traceparent_len,
450        });
451    }
452    parse_traceparent(traceparent).map(Some)
453}
454
455#[cfg(feature = "http")]
456pub(crate) fn parse_traceparent_value(
457    value: &str,
458    limits: TraceLimits,
459) -> Result<TraceParent, TraceParseError> {
460    if value.len() > limits.max_traceparent_len {
461        return Err(TraceParseError::ValueTooLong {
462            field: TRACEPARENT_KEY,
463            actual: value.len(),
464            max: limits.max_traceparent_len,
465        });
466    }
467    parse_traceparent(value)
468}
469
470fn parse_traceparent(value: &str) -> Result<TraceParent, TraceParseError> {
471    if value.len() < TRACEPARENT_V00_LEN {
472        return Err(TraceParseError::InvalidTraceParentLength {
473            actual: value.len(),
474        });
475    }
476    if !value.is_ascii() {
477        return Err(TraceParseError::InvalidTraceParentFormat);
478    }
479    let bytes = value.as_bytes();
480    if bytes[TRACEPARENT_VERSION_END] != b'-'
481        || bytes[TRACEPARENT_TRACE_ID_END] != b'-'
482        || bytes[TRACEPARENT_SPAN_ID_END] != b'-'
483    {
484        return Err(TraceParseError::InvalidTraceParentFormat);
485    }
486    let version = &value[..TRACEPARENT_VERSION_END];
487    let trace_id = &value[TRACEPARENT_TRACE_ID_START..TRACEPARENT_TRACE_ID_END];
488    let span_id = &value[TRACEPARENT_SPAN_ID_START..TRACEPARENT_SPAN_ID_END];
489    let flags = &value[TRACEPARENT_FLAGS_START..TRACEPARENT_FLAGS_END];
490    if !is_lower_hex(version)
491        || !is_lower_hex(trace_id)
492        || !is_lower_hex(span_id)
493        || !is_lower_hex(flags)
494    {
495        return Err(TraceParseError::InvalidTraceParentFormat);
496    }
497    if version == "ff" {
498        return Err(TraceParseError::UnsupportedVersion);
499    }
500    if version == "00" && value.len() != TRACEPARENT_V00_LEN {
501        return Err(TraceParseError::InvalidTraceParentLength {
502            actual: value.len(),
503        });
504    }
505    if version != "00"
506        && value.len() > TRACEPARENT_V00_LEN
507        && bytes[TRACEPARENT_NEXT_SEPARATOR] != b'-'
508    {
509        return Err(TraceParseError::InvalidTraceParentFormat);
510    }
511    if trace_id.bytes().all(|b| b == b'0') {
512        return Err(TraceParseError::InvalidTraceId);
513    }
514    if span_id.bytes().all(|b| b == b'0') {
515        return Err(TraceParseError::InvalidSpanId);
516    }
517    let flag_byte =
518        u8::from_str_radix(flags, 16).map_err(|_| TraceParseError::InvalidTraceParentFormat)?;
519    Ok(TraceParent {
520        trace_id: trace_id.to_owned(),
521        span_id: span_id.to_owned(),
522        sampled: flag_byte & 0x01 == 0x01,
523    })
524}
525
526fn is_lower_hex(value: &str) -> bool {
527    value
528        .bytes()
529        .all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase())
530}
531
532fn summarize_baggage(baggage: Option<&str>) -> (usize, usize) {
533    let Some(baggage) = baggage else {
534        return (0, 0);
535    };
536    let mut total = 0;
537    let mut sensitive = 0;
538    for key in baggage_keys(baggage) {
539        total += 1;
540        if is_sensitive_key(key) {
541            sensitive += 1;
542        }
543    }
544    (total, sensitive)
545}
546
547fn validate_tracestate(tracestate: Option<&str>) -> Result<(), TraceParseError> {
548    let Some(tracestate) = tracestate else {
549        return Ok(());
550    };
551    let mut keys = BTreeSet::new();
552    let mut total = 0;
553    for member in tracestate.split(',') {
554        let member = member.trim();
555        if member.is_empty() {
556            return Err(TraceParseError::InvalidTraceState);
557        }
558        total += 1;
559        if total > MAX_TRACESTATE_MEMBERS {
560            return Err(TraceParseError::TooManyTraceStateMembers {
561                actual: total,
562                max: MAX_TRACESTATE_MEMBERS,
563            });
564        }
565        let Some((key, value)) = member.split_once('=') else {
566            return Err(TraceParseError::InvalidTraceState);
567        };
568        let key = key.trim();
569        if !is_valid_tracestate_key(key) || !is_valid_tracestate_value(value) {
570            return Err(TraceParseError::InvalidTraceState);
571        }
572        if !keys.insert(key) {
573            return Err(TraceParseError::DuplicateTraceStateKey);
574        }
575    }
576    Ok(())
577}
578
579fn validate_baggage(baggage: Option<&str>, max: usize) -> Result<(), TraceParseError> {
580    let Some(baggage) = baggage else {
581        return Ok(());
582    };
583    let mut total = 0;
584    for member in baggage
585        .split(',')
586        .map(str::trim)
587        .filter(|member| !member.is_empty())
588    {
589        total += 1;
590        if total > max {
591            return Err(TraceParseError::TooManyBaggageMembers { actual: total, max });
592        }
593        let Some((key, rest)) = member.split_once('=') else {
594            return Err(TraceParseError::InvalidBaggageMember);
595        };
596        if !is_valid_baggage_key(key.trim()) {
597            return Err(TraceParseError::InvalidBaggageMember);
598        }
599        let mut value_and_props = rest.split(';');
600        let value = value_and_props.next().unwrap_or("").trim();
601        if !is_valid_baggage_value(value) {
602            return Err(TraceParseError::InvalidBaggageMember);
603        }
604        for property in value_and_props {
605            let property = property.trim();
606            if property.is_empty() {
607                return Err(TraceParseError::InvalidBaggageMember);
608            }
609            if let Some((property_key, property_value)) = property.split_once('=') {
610                if !is_valid_baggage_key(property_key.trim())
611                    || !is_valid_baggage_value(property_value.trim())
612                {
613                    return Err(TraceParseError::InvalidBaggageMember);
614                }
615            } else if !is_valid_baggage_key(property) {
616                return Err(TraceParseError::InvalidBaggageMember);
617            }
618        }
619    }
620    Ok(())
621}
622
623#[cfg(feature = "http")]
624pub(crate) fn validate_tracestate_value(tracestate: Option<&str>) -> Result<(), TraceParseError> {
625    validate_tracestate(tracestate)
626}
627
628#[cfg(feature = "http")]
629pub(crate) fn validate_baggage_value(
630    baggage: Option<&str>,
631    max_members: usize,
632) -> Result<(), TraceParseError> {
633    validate_baggage(baggage, max_members)
634}
635
636fn baggage_keys(baggage: &str) -> impl Iterator<Item = &str> {
637    baggage.split(',').filter_map(|member| {
638        let (key, _) = member.split_once('=')?;
639        let key = key.trim();
640        (!key.is_empty()).then_some(key)
641    })
642}
643
644fn is_sensitive_key(key: &str) -> bool {
645    const SENSITIVE_KEYS: &[&str] = &[
646        "authorization",
647        "cookie",
648        "setcookie",
649        "password",
650        "secret",
651        "token",
652        "accesstoken",
653        "refreshtoken",
654        "apikey",
655        "xapikey",
656        "privatekey",
657        "session",
658        "sessionid",
659    ];
660    SENSITIVE_KEYS
661        .iter()
662        .any(|sensitive_key| normalized_ascii_key_eq(key, sensitive_key))
663}
664
665fn normalized_ascii_key_eq(key: &str, expected: &str) -> bool {
666    let mut key_bytes = key
667        .bytes()
668        .filter(|byte| byte.is_ascii_alphanumeric())
669        .map(|byte| byte.to_ascii_lowercase());
670    let mut expected_bytes = expected.bytes();
671    loop {
672        match (key_bytes.next(), expected_bytes.next()) {
673            (Some(left), Some(right)) if left == right => {}
674            (None, None) => return true,
675            _ => return false,
676        }
677    }
678}
679
680fn is_valid_tracestate_key(key: &str) -> bool {
681    !key.is_empty()
682        && key.is_ascii()
683        && key.bytes().all(|byte| {
684            byte.is_ascii_lowercase()
685                || byte.is_ascii_digit()
686                || matches!(byte, b'_' | b'-' | b'*' | b'/' | b'@')
687        })
688}
689
690fn is_valid_tracestate_value(value: &str) -> bool {
691    !value.is_empty()
692        && value.is_ascii()
693        && value.trim() == value
694        && value
695            .bytes()
696            .all(|byte| matches!(byte, 0x20..=0x2b | 0x2d..=0x3c | 0x3e..=0x7e))
697}
698
699fn is_valid_baggage_key(key: &str) -> bool {
700    !key.is_empty()
701        && key.is_ascii()
702        && key.bytes().all(|byte| {
703            byte.is_ascii_alphanumeric()
704                || matches!(
705                    byte,
706                    b'!' | b'#'
707                        | b'$'
708                        | b'%'
709                        | b'&'
710                        | b'\''
711                        | b'*'
712                        | b'+'
713                        | b'-'
714                        | b'.'
715                        | b'^'
716                        | b'_'
717                        | b'`'
718                        | b'|'
719                        | b'~'
720                )
721        })
722}
723
724fn is_valid_baggage_value(value: &str) -> bool {
725    value
726        .bytes()
727        .all(|byte| matches!(byte, 0x21 | 0x23..=0x2b | 0x2d..=0x3a | 0x3c..=0x5b | 0x5d..=0x7e))
728}
729
730#[cfg(test)]
731mod tests {
732    use super::*;
733
734    const VALID_TRACEPARENT: &str = "00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01";
735
736    #[test]
737    fn trace_context_parses_meta_and_summarizes_safely() {
738        let mut meta = Meta::new();
739        meta.set_traceparent(VALID_TRACEPARENT);
740        meta.set_tracestate("vendor=value");
741        meta.set_baggage("region=us-east-1,accessToken=super-secret-token");
742
743        let context = TraceContext::from_meta(&meta, TraceTrust::Untrusted)
744            .expect("valid trace metadata")
745            .expect("trace context exists");
746
747        let summary = context.summary();
748
749        assert_eq!(summary.trace_id_prefix(), Some("0af76519"));
750        assert_eq!(summary.span_id_prefix(), Some("00f067aa"));
751        assert_eq!(summary.sampled(), Some(true));
752        assert_eq!(summary.trust(), TraceTrust::Untrusted);
753        assert!(summary.has_tracestate());
754        assert_eq!(summary.baggage_member_count(), 2);
755        assert_eq!(summary.sensitive_baggage_member_count(), 1);
756        assert_eq!(summary.invalid_count(), 0);
757    }
758
759    #[test]
760    fn malformed_traceparents_are_rejected() {
761        for value in [
762            "",
763            "00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7",
764            "ff-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01",
765            "00-00000000000000000000000000000000-00f067aa0ba902b7-01",
766            "00-0af7651916cd43dd8448eb211c80319c-0000000000000000-01",
767            "00-0AF7651916CD43DD8448EB211C80319C-00f067aa0ba902b7-01",
768            "00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-zz",
769        ] {
770            assert!(
771                TraceParent::parse(value).is_err(),
772                "{value} should be rejected"
773            );
774        }
775    }
776
777    #[test]
778    fn non_ascii_traceparents_are_rejected_without_panicking() {
779        let value = format!("{}\u{00e9}", &VALID_TRACEPARENT[..54]);
780        let result = std::panic::catch_unwind(|| TraceParent::parse(&value));
781
782        assert!(result.is_ok(), "non-ASCII input must not panic");
783        assert!(matches!(
784            result.unwrap(),
785            Err(TraceParseError::InvalidTraceParentFormat)
786        ));
787
788        let higher_version = "01-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01-\u{00e9}";
789        assert!(matches!(
790            TraceParent::parse(higher_version),
791            Err(TraceParseError::InvalidTraceParentFormat)
792        ));
793    }
794
795    #[test]
796    fn traceparent_version_rules_cover_v00_and_higher_version_bounds() {
797        assert!(matches!(
798            TraceParent::parse(&format!("{VALID_TRACEPARENT}-extra")),
799            Err(TraceParseError::InvalidTraceParentLength { actual }) if actual == 61
800        ));
801
802        let higher_base = "01-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01";
803        let max_extra_len = 512 - higher_base.len() - 1;
804        let max_len_value = format!("{higher_base}-{}", "a".repeat(max_extra_len));
805        assert_eq!(max_len_value.len(), 512);
806        TraceParent::parse(&max_len_value).expect("512-byte higher version should be accepted");
807
808        let too_long = format!("{higher_base}-{}", "a".repeat(max_extra_len + 1));
809        assert_eq!(too_long.len(), 513);
810        assert!(matches!(
811            TraceParent::parse(&too_long),
812            Err(TraceParseError::ValueTooLong {
813                field: TRACEPARENT_KEY,
814                actual: 513,
815                max: 512,
816            })
817        ));
818    }
819
820    #[test]
821    fn higher_version_traceparents_preserve_stable_fields() {
822        let traceparent =
823            TraceParent::parse("01-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01-extra")
824                .expect("higher versions can carry additive fields");
825
826        assert_eq!(traceparent.trace_id(), "0af7651916cd43dd8448eb211c80319c");
827        assert_eq!(traceparent.span_id(), "00f067aa0ba902b7");
828        assert!(traceparent.sampled());
829    }
830
831    #[test]
832    fn strict_context_rejects_oversized_optional_metadata() {
833        let mut meta = Meta::new();
834        meta.set_traceparent("x".repeat(4096));
835        assert!(TraceContext::from_meta(&meta, TraceTrust::Untrusted).is_err());
836
837        let mut meta = Meta::new();
838        meta.set_traceparent(VALID_TRACEPARENT);
839        meta.set_tracestate("v".repeat(20));
840        let limits = TraceLimits {
841            max_tracestate_len: 8,
842            ..TraceLimits::default()
843        };
844        assert!(TraceContext::from_meta_with_limits(&meta, TraceTrust::Untrusted, limits).is_err());
845
846        let mut meta = Meta::new();
847        meta.set_traceparent(VALID_TRACEPARENT);
848        meta.set_baggage("a".repeat(20));
849        let limits = TraceLimits {
850            max_baggage_len: 8,
851            ..TraceLimits::default()
852        };
853        assert!(TraceContext::from_meta_with_limits(&meta, TraceTrust::Untrusted, limits).is_err());
854    }
855
856    #[test]
857    fn strict_context_rejects_invalid_optional_metadata() {
858        let mut meta = Meta::new();
859        meta.set_traceparent(VALID_TRACEPARENT);
860        meta.set_tracestate("vendor=value,vendor=other");
861        let error = TraceContext::from_meta(&meta, TraceTrust::Untrusted)
862            .expect_err("duplicate tracestate keys should be rejected");
863        assert!(matches!(error, TraceParseError::DuplicateTraceStateKey));
864
865        let mut meta = Meta::new();
866        meta.set_traceparent(VALID_TRACEPARENT);
867        meta.set_baggage("a=1,b=2,c=3");
868        let limits = TraceLimits {
869            max_baggage_members: 2,
870            ..TraceLimits::default()
871        };
872        let error = TraceContext::from_meta_with_limits(&meta, TraceTrust::Untrusted, limits)
873            .expect_err("baggage member cap should be enforced");
874
875        assert!(matches!(
876            error,
877            TraceParseError::TooManyBaggageMembers { actual: 3, max: 2 }
878        ));
879        assert!(!error.safe_reason().contains("a=1"));
880    }
881}