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}