@@ -295,4 +295,197 @@ mod tests {
295
295
296
296
assert_eq ! ( Extractor :: get( & injector, TRACESTATE_HEADER ) , Some ( state) )
297
297
}
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
+ }
298
491
}
0 commit comments