@@ -153,6 +153,53 @@ public void SourceLinkUrlsAreEscaped()
153153 }
154154 }
155155
156+ [ Fact ]
157+ public void SourceLinkSupportsWildcardAndExactPathMappings ( )
158+ {
159+ // Create a test symbol module that returns SourceLink JSON with both wildcard and exact path mappings
160+ var testModule = new TestSymbolModuleWithSourceLink ( _symbolReader ) ;
161+
162+ // Test wildcard pattern matching
163+ bool result1 = testModule . GetUrlForFilePathUsingSourceLink (
164+ @"C:\src\myproject\subfolder\file.cs" ,
165+ out string url1 ,
166+ out string relativePath1 ) ;
167+
168+ Assert . True ( result1 , "Should match wildcard pattern" ) ;
169+ Assert . Equal ( "https://raw.githubusercontent.com/org/repo/commit/subfolder/file.cs" , url1 ) ;
170+ Assert . Equal ( "subfolder/file.cs" , relativePath1 ) ;
171+
172+ // Test exact path matching
173+ bool result2 = testModule . GetUrlForFilePathUsingSourceLink (
174+ @"c:\external\sdk\inc\header.h" ,
175+ out string url2 ,
176+ out string relativePath2 ) ;
177+
178+ Assert . True ( result2 , "Should match exact path" ) ;
179+ Assert . Equal ( "https://example.com/blobs/ABC123?download=true&filename=header.h" , url2 ) ;
180+ Assert . Equal ( "" , relativePath2 ) ;
181+
182+ // Test another wildcard pattern with escaped characters
183+ bool result3 = testModule . GetUrlForFilePathUsingSourceLink (
184+ @"C:\src\myproject\some folder\another file.cs" ,
185+ out string url3 ,
186+ out string relativePath3 ) ;
187+
188+ Assert . True ( result3 , "Should match wildcard pattern with spaces" ) ;
189+ Assert . Equal ( "https://raw.githubusercontent.com/org/repo/commit/some%20folder/another%20file.cs" , url3 ) ;
190+ Assert . Equal ( "some folder/another file.cs" , relativePath3 ) ;
191+
192+ // Test non-matching path
193+ bool result4 = testModule . GetUrlForFilePathUsingSourceLink (
194+ @"C:\other\path\file.cs" ,
195+ out string url4 ,
196+ out string relativePath4 ) ;
197+
198+ Assert . False ( result4 , "Should not match any pattern" ) ;
199+ Assert . Null ( url4 ) ;
200+ Assert . Null ( relativePath4 ) ;
201+ }
202+
156203 /// <summary>
157204 /// Tests that the checksum matching allows for different line endings.
158205 /// Open the PDB and try to retrieve the source code for one of the files,
@@ -370,31 +417,31 @@ public void MsfzFileDetectionWorks()
370417 var tempDir = Path . GetTempPath ( ) ;
371418 var testFile = Path . Combine ( tempDir , "test_msfz.pdb" ) ;
372419 var nonMsfzFile = Path . Combine ( tempDir , "test_non_msfz.pdb" ) ;
373-
420+
374421 try
375422 {
376423 // Write MSFZ header followed by some dummy data
377424 var msfzHeader = "Microsoft MSFZ Container" ;
378425 var headerBytes = Encoding . UTF8 . GetBytes ( msfzHeader ) ;
379426 var dummyData = new byte [ ] { 0x01 , 0x02 , 0x03 , 0x04 } ;
380-
427+
381428 using ( var stream = File . Create ( testFile ) )
382429 {
383430 stream . Write ( headerBytes , 0 , headerBytes . Length ) ;
384431 stream . Write ( dummyData , 0 , dummyData . Length ) ;
385432 }
386-
433+
387434 // Use reflection to call the private IsMsfzFile method
388- var method = typeof ( SymbolReader ) . GetMethod ( "IsMsfzFile" ,
435+ var method = typeof ( SymbolReader ) . GetMethod ( "IsMsfzFile" ,
389436 System . Reflection . BindingFlags . NonPublic | System . Reflection . BindingFlags . Instance ) ;
390-
437+
391438 var result = ( bool ) method . Invoke ( _symbolReader , new object [ ] { testFile } ) ;
392-
439+
393440 Assert . True ( result , "File with MSFZ header should be detected as MSFZ file" ) ;
394-
441+
395442 // Test with non-MSFZ file
396443 File . WriteAllText ( nonMsfzFile , "This is not an MSFZ file" ) ;
397-
444+
398445 result = ( bool ) method . Invoke ( _symbolReader , new object [ ] { nonMsfzFile } ) ;
399446 Assert . False ( result , "File without MSFZ header should not be detected as MSFZ file" ) ;
400447 }
@@ -412,30 +459,30 @@ public void MsfzFileMovesToCorrectSubdirectory()
412459 {
413460 var tempDir = Path . Combine ( Path . GetTempPath ( ) , "msfz_test_" + Guid . NewGuid ( ) . ToString ( "N" ) ) ;
414461 Directory . CreateDirectory ( tempDir ) ;
415-
462+
416463 try
417464 {
418465 var testFile = Path . Combine ( tempDir , "test.pdb" ) ;
419-
466+
420467 // Create MSFZ file
421468 var msfzHeader = "Microsoft MSFZ Container" ;
422469 var headerBytes = Encoding . UTF8 . GetBytes ( msfzHeader ) ;
423470 var dummyData = new byte [ ] { 0x01 , 0x02 , 0x03 , 0x04 } ;
424-
471+
425472 using ( var stream = File . Create ( testFile ) )
426473 {
427474 stream . Write ( headerBytes , 0 , headerBytes . Length ) ;
428475 stream . Write ( dummyData , 0 , dummyData . Length ) ;
429476 }
430-
477+
431478 // Since MSFZ logic is now integrated into GetFileFromServer,
432479 // this test validates the MSFZ detection logic which remains the same
433- var isMsfzMethod = typeof ( SymbolReader ) . GetMethod ( "IsMsfzFile" ,
480+ var isMsfzMethod = typeof ( SymbolReader ) . GetMethod ( "IsMsfzFile" ,
434481 System . Reflection . BindingFlags . NonPublic | System . Reflection . BindingFlags . Instance ) ;
435-
482+
436483 var isMsfz = ( bool ) isMsfzMethod . Invoke ( _symbolReader , new object [ ] { testFile } ) ;
437484 Assert . True ( isMsfz , "File should be detected as MSFZ file" ) ;
438-
485+
439486 // The file moving functionality is now tested through integration tests
440487 // since it's part of the GetFileFromServer method
441488 }
@@ -451,39 +498,40 @@ public void HttpRequestIncludesMsfzAcceptHeader()
451498 {
452499 // This test verifies that our HttpRequestMessage creation includes the MSFZ accept header
453500 // We'll create a minimal test by checking the private method behavior indirectly
454-
501+
455502 var tempDir = Path . Combine ( Path . GetTempPath ( ) , "msfz_http_test_" + Guid . NewGuid ( ) . ToString ( "N" ) ) ;
456503 Directory . CreateDirectory ( tempDir ) ;
457504 var targetPath = Path . Combine ( tempDir , "test.pdb" ) ;
458-
505+
459506 try
460507 {
461508 // Configure intercepting handler to capture the request with MSFZ content
462- _handler . AddIntercept ( new Uri ( "https://test.example.com/test.pdb" ) , HttpMethod . Get , HttpStatusCode . OK , ( ) => {
509+ _handler . AddIntercept ( new Uri ( "https://test.example.com/test.pdb" ) , HttpMethod . Get , HttpStatusCode . OK , ( ) =>
510+ {
463511 var msfzContent = "Microsoft MSFZ Container\x00 \x01 \x02 \x03 " ;
464512 return new StringContent ( msfzContent , Encoding . UTF8 , "application/msfz0" ) ;
465513 } ) ;
466-
514+
467515 // This will trigger an HTTP request that should include the Accept header
468- var method = typeof ( SymbolReader ) . GetMethod ( "GetPhysicalFileFromServer" ,
516+ var method = typeof ( SymbolReader ) . GetMethod ( "GetPhysicalFileFromServer" ,
469517 System . Reflection . BindingFlags . NonPublic | System . Reflection . BindingFlags . Instance ) ;
470-
471- var result = ( bool ) method . Invoke ( _symbolReader , new object [ ] {
472- "https://test.example.com" ,
473- "test.pdb" ,
474- targetPath ,
475- null
518+
519+ var result = ( bool ) method . Invoke ( _symbolReader , new object [ ] {
520+ "https://test.example.com" ,
521+ "test.pdb" ,
522+ targetPath ,
523+ null
476524 } ) ;
477-
525+
478526 // Verify that the download was successful
479527 Assert . True ( result , "GetPhysicalFileFromServer should succeed with MSFZ content" ) ;
480-
528+
481529 // In the new architecture, GetPhysicalFileFromServer just downloads the file
482530 // The MSFZ moving logic is handled by GetFileFromServer
483531 Assert . True ( File . Exists ( targetPath ) , "Downloaded file should exist at target path" ) ;
484-
532+
485533 // Verify the content is MSFZ
486- var isMsfzMethod = typeof ( SymbolReader ) . GetMethod ( "IsMsfzFile" ,
534+ var isMsfzMethod = typeof ( SymbolReader ) . GetMethod ( "IsMsfzFile" ,
487535 System . Reflection . BindingFlags . NonPublic | System . Reflection . BindingFlags . Instance ) ;
488536 var isMsfz = ( bool ) isMsfzMethod . Invoke ( _symbolReader , new object [ ] { targetPath } ) ;
489537 Assert . True ( isMsfz , "Downloaded file should be detected as MSFZ" ) ;
@@ -577,5 +625,37 @@ protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage reques
577625 return base . SendAsync ( request , cancellationToken ) ;
578626 }
579627 }
628+
629+ /// <summary>
630+ /// A test symbol module that provides SourceLink JSON for testing.
631+ /// </summary>
632+ private class TestSymbolModuleWithSourceLink : ManagedSymbolModule
633+ {
634+ public TestSymbolModuleWithSourceLink ( SymbolReader reader )
635+ : base ( reader , "test.pdb" )
636+ {
637+ }
638+
639+ public override SourceLocation SourceLocationForManagedCode ( uint methodMetadataToken , int ilOffset )
640+ {
641+ // Not used in this test
642+ return null ;
643+ }
644+
645+ protected override IEnumerable < string > GetSourceLinkJson ( )
646+ {
647+ // Return SourceLink JSON with both wildcard and exact path mappings
648+ // This mimics the example from issue #2350
649+ return new [ ]
650+ {
651+ @"{
652+ ""documents"": {
653+ ""C:\\src\\myproject\\*"": ""https://raw.githubusercontent.com/org/repo/commit/*"",
654+ ""c:\\external\\sdk\\inc\\header.h"": ""https://example.com/blobs/ABC123?download=true&filename=header.h""
655+ }
656+ }"
657+ } ;
658+ }
659+ }
580660 }
581- }
661+ }
0 commit comments