@@ -295,4 +295,197 @@ mod tests {
295295
296296 assert_eq ! ( Extractor :: get( & injector, TRACESTATE_HEADER ) , Some ( state) )
297297 }
298+
299+ #[ rustfmt:: skip]
300+ fn malformed_traceparent_test_data ( ) -> Vec < ( String , & ' static str ) > {
301+ vec ! [
302+ // Existing invalid cases are already covered, adding more edge cases
303+ ( "" . to_string( ) , "completely empty" ) ,
304+ ( " " . to_string( ) , "whitespace only" ) ,
305+ ( "00" . to_string( ) , "too few parts" ) ,
306+ ( "00-" . to_string( ) , "incomplete with separator" ) ,
307+ ( "00--00" . to_string( ) , "missing trace ID" ) ,
308+ ( "00-4bf92f3577b34da6a3ce929d0e0e4736--01" . to_string( ) , "missing span ID" ) ,
309+ ( "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-" . to_string( ) , "missing flags" ) ,
310+
311+ // Very long inputs
312+ ( format!( "00-{}-00f067aa0ba902b7-01" , "a" . repeat( 1000 ) ) , "very long trace ID" ) ,
313+ ( format!( "00-4bf92f3577b34da6a3ce929d0e0e4736-{}-01" , "b" . repeat( 1000 ) ) , "very long span ID" ) ,
314+ ( format!( "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-{}" , "c" . repeat( 1000 ) ) , "very long flags" ) ,
315+
316+ // Non-hex characters
317+ ( "00-4bf92f3577b34da6a3ce929d0e0e473g-00f067aa0ba902b7-01" . to_string( ) , "non-hex in trace ID" ) ,
318+ ( "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b$-01" . to_string( ) , "non-hex in span ID" ) ,
319+ ( "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-0g" . to_string( ) , "non-hex in flags" ) ,
320+
321+ // Unicode and special characters
322+ ( "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01🔥" . to_string( ) , "emoji in flags" ) ,
323+ ( "00-café4da6a3ce929d0e0e4736-00f067aa0ba902b7-01" . to_string( ) , "unicode in trace ID" ) ,
324+ ( "00-4bf92f3577b34da6a3ce929d0e0e4736-café67aa0ba902b7-01" . to_string( ) , "unicode in span ID" ) ,
325+
326+ // Control characters
327+ ( "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01\x00 " . to_string( ) , "null terminator" ) ,
328+ ( "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01\n " . to_string( ) , "newline" ) ,
329+ ( "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01\t " . to_string( ) , "tab character" ) ,
330+
331+ // Multiple separators
332+ ( "00--4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" . to_string( ) , "double separator" ) ,
333+ ( "00-4bf92f3577b34da6a3ce929d0e0e4736--00f067aa0ba902b7-01" . to_string( ) , "double separator middle" ) ,
334+ ( "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7--01" . to_string( ) , "double separator end" ) ,
335+ ]
336+ }
337+
338+ #[ rustfmt:: skip]
339+ fn malformed_tracestate_header_test_data ( ) -> Vec < ( String , & ' static str ) > {
340+ vec ! [
341+ // Very long tracestate headers
342+ ( format!( "key={}" , "x" . repeat( 100_000 ) ) , "extremely long value" ) ,
343+ ( format!( "{}=value" , "k" . repeat( 100_000 ) ) , "extremely long key" ) ,
344+ ( ( 0 ..10_000 ) . map( |i| format!( "k{}=v{}" , i, i) ) . collect:: <Vec <_>>( ) . join( "," ) , "many entries" ) ,
345+
346+ // Malformed but should not crash
347+ ( "key=value,malformed" . to_string( ) , "mixed valid and invalid" ) ,
348+ ( "=value1,key2=value2,=value3" . to_string( ) , "multiple empty keys" ) ,
349+ ( "key1=value1,,key2=value2" . to_string( ) , "empty entry" ) ,
350+ ( "key1=value1,key2=" . to_string( ) , "empty value" ) ,
351+ ( "key1=,key2=value2" . to_string( ) , "another empty value" ) ,
352+
353+ // Control characters and special cases
354+ ( "key=val\x00 ue" . to_string( ) , "null character" ) ,
355+ ( "key=val\n ue" . to_string( ) , "newline character" ) ,
356+ ( "key=val\t ue" . to_string( ) , "tab character" ) ,
357+ ( "key\x01 =value" . to_string( ) , "control character in key" ) ,
358+
359+ // Unicode
360+ ( "café=bücher" . to_string( ) , "unicode key and value" ) ,
361+ ( "🔥=🎉" . to_string( ) , "emoji key and value" ) ,
362+ ( "ключ=значение" . to_string( ) , "cyrillic" ) ,
363+
364+ // Invalid percent encoding patterns
365+ ( "key=%ZZ" . to_string( ) , "invalid hex in percent encoding" ) ,
366+ ( "key=%" . to_string( ) , "incomplete percent encoding" ) ,
367+ ( "key=%%" . to_string( ) , "double percent" ) ,
368+ ]
369+ }
370+
371+ #[ test]
372+ fn extract_w3c_defensive_traceparent ( ) {
373+ let propagator = TraceContextPropagator :: new ( ) ;
374+
375+ // Test all the malformed traceparent cases
376+ for ( invalid_header, reason) in malformed_traceparent_test_data ( ) {
377+ let mut extractor = HashMap :: new ( ) ;
378+ extractor. insert ( TRACEPARENT_HEADER . to_string ( ) , invalid_header. clone ( ) ) ;
379+
380+ // Should not crash and should return empty context
381+ let result = propagator. extract ( & extractor) ;
382+ assert_eq ! (
383+ result. span( ) . span_context( ) ,
384+ & SpanContext :: empty_context( ) ,
385+ "Failed to reject invalid traceparent: {} ({})" , invalid_header, reason
386+ ) ;
387+ }
388+ }
389+
390+ #[ test]
391+ fn extract_w3c_defensive_tracestate ( ) {
392+ let propagator = TraceContextPropagator :: new ( ) ;
393+
394+ // Use a valid traceparent with various malformed tracestate headers
395+ let valid_parent = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" ;
396+
397+ for ( malformed_state, description) in malformed_tracestate_header_test_data ( ) {
398+ let mut extractor = HashMap :: new ( ) ;
399+ extractor. insert ( TRACEPARENT_HEADER . to_string ( ) , valid_parent. to_string ( ) ) ;
400+ extractor. insert ( TRACESTATE_HEADER . to_string ( ) , malformed_state. clone ( ) ) ;
401+
402+ // Should not crash - malformed tracestate should fallback to default
403+ let result = propagator. extract ( & extractor) ;
404+ let span_context = result. span ( ) . span_context ( ) ;
405+
406+ // Should still have valid span context from traceparent
407+ assert ! ( span_context. is_valid( ) , "Valid traceparent should create valid context despite malformed tracestate: {}" , description) ;
408+
409+ // Tracestate should either be default or contain only valid entries
410+ let trace_state = span_context. trace_state ( ) ;
411+ let header = trace_state. header ( ) ;
412+
413+ // Verify the tracestate header is reasonable (no extremely long result)
414+ assert ! (
415+ header. len( ) <= malformed_state. len( ) + 1000 ,
416+ "TraceState header grew unreasonably for input '{}' ({}): {} -> {}" ,
417+ malformed_state, description, malformed_state. len( ) , header. len( )
418+ ) ;
419+ }
420+ }
421+
422+ #[ test]
423+ fn extract_w3c_memory_safety ( ) {
424+ let propagator = TraceContextPropagator :: new ( ) ;
425+
426+ // Test extremely long traceparent
427+ let very_long_traceparent = format ! (
428+ "00-{}-{}-01" ,
429+ "a" . repeat( 1_000_000 ) , // Very long trace ID
430+ "b" . repeat( 1_000_000 ) // Very long span ID
431+ ) ;
432+
433+ let mut extractor = HashMap :: new ( ) ;
434+ extractor. insert ( TRACEPARENT_HEADER . to_string ( ) , very_long_traceparent) ;
435+
436+ // Should not crash or consume excessive memory
437+ let result = propagator. extract ( & extractor) ;
438+ assert_eq ! ( result. span( ) . span_context( ) , & SpanContext :: empty_context( ) ) ;
439+
440+ // Test with both long traceparent and tracestate
441+ let long_tracestate = format ! ( "key={}" , "x" . repeat( 1_000_000 ) ) ;
442+ let mut extractor2 = HashMap :: new ( ) ;
443+ extractor2. insert ( TRACEPARENT_HEADER . to_string ( ) , "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" . to_string ( ) ) ;
444+ extractor2. insert ( TRACESTATE_HEADER . to_string ( ) , long_tracestate) ;
445+
446+ // Should handle gracefully without excessive memory usage
447+ let result2 = propagator. extract ( & extractor2) ;
448+ let span_context2 = result2. span ( ) . span_context ( ) ;
449+ assert ! ( span_context2. is_valid( ) ) ;
450+ }
451+
452+ #[ test]
453+ fn extract_w3c_boundary_conditions ( ) {
454+ let propagator = TraceContextPropagator :: new ( ) ;
455+
456+ // Test boundary conditions for version and flags
457+ let boundary_test_cases = vec ! [
458+ ( "ff-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" , "max version" ) ,
459+ ( "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-ff" , "max flags" ) ,
460+ ( "00-00000000000000000000000000000001-0000000000000001-01" , "minimal valid IDs" ) ,
461+ ( "00-ffffffffffffffffffffffffffffffff-ffffffffffffffff-01" , "maximal valid IDs" ) ,
462+ ] ;
463+
464+ for ( test_header, description) in boundary_test_cases {
465+ let mut extractor = HashMap :: new ( ) ;
466+ extractor. insert ( TRACEPARENT_HEADER . to_string ( ) , test_header. to_string ( ) ) ;
467+
468+ let result = propagator. extract ( & extractor) ;
469+ let span_context = result. span ( ) . span_context ( ) ;
470+
471+ // These should be handled according to W3C spec
472+ // The test passes if no panic occurs and behavior is consistent
473+ match description {
474+ "max version" => {
475+ // Version 255 should be accepted (as per spec, parsers should accept unknown versions)
476+ // But our implementation might reject it - either behavior is defensive
477+ }
478+ "max flags" => {
479+ // Max flags should be accepted but masked to valid bits
480+ if span_context. is_valid ( ) {
481+ // Only the sampled bit should be preserved
482+ assert ! ( span_context. trace_flags( ) . as_u8( ) <= 1 ) ;
483+ }
484+ }
485+ _ => {
486+ // Other cases should work normally
487+ }
488+ }
489+ }
490+ }
298491}
0 commit comments