11namespace Microsoft . ComponentDetection . Detectors . Vcpkg ;
22
33using System ;
4+ using System . Collections . Concurrent ;
45using System . Collections . Generic ;
56using System . IO ;
67using System . Linq ;
8+ using System . Reactive . Linq ;
79using System . Threading ;
810using System . Threading . Tasks ;
911using Microsoft . ComponentDetection . Contracts ;
@@ -15,7 +17,11 @@ namespace Microsoft.ComponentDetection.Detectors.Vcpkg;
1517
1618public class VcpkgComponentDetector : FileComponentDetector
1719{
20+ private const string VcpkgInstalledFolder = "vcpkg_installed" ;
21+ private const string ManifestInfoFile = "manifest-info.json" ;
22+
1823 private readonly HashSet < string > projectRoots = [ ] ;
24+ private readonly ConcurrentDictionary < string , string > manifestMappings = new ( StringComparer . OrdinalIgnoreCase ) ;
1925
2026 private readonly ICommandLineInvocationService commandLineInvocationService ;
2127 private readonly IEnvironmentVariableService envVarService ;
@@ -38,11 +44,11 @@ public VcpkgComponentDetector(
3844
3945 public override IEnumerable < string > Categories => [ Enum . GetName ( typeof ( DetectorClass ) , DetectorClass . Vcpkg ) ] ;
4046
41- public override IList < string > SearchPatterns { get ; } = [ "vcpkg.spdx.json" ] ;
47+ public override IList < string > SearchPatterns { get ; } = [ "vcpkg.spdx.json" , ManifestInfoFile ] ;
4248
4349 public override IEnumerable < ComponentType > SupportedComponentTypes { get ; } = [ ComponentType . Vcpkg ] ;
4450
45- public override int Version => 2 ;
51+ public override int Version => 3 ;
4652
4753 protected override async Task OnFileFoundAsync ( ProcessRequest processRequest , IDictionary < string , string > detectorArgs , CancellationToken cancellationToken = default )
4854 {
@@ -57,7 +63,44 @@ protected override async Task OnFileFoundAsync(ProcessRequest processRequest, ID
5763 return ;
5864 }
5965
60- await this . ParseSpdxFileAsync ( singleFileComponentRecorder , file ) ;
66+ await this . ParseSpdxFileAsync ( this . GetManifestComponentRecorder ( singleFileComponentRecorder ) , file ) ;
67+ }
68+
69+ protected override async Task < IObservable < ProcessRequest > > OnPrepareDetectionAsync ( IObservable < ProcessRequest > processRequests , IDictionary < string , string > detectorArgs , CancellationToken cancellationToken = default )
70+ {
71+ var filteredProcessRequests = new List < ProcessRequest > ( ) ;
72+
73+ await processRequests . ForEachAsync ( async pr =>
74+ {
75+ var fileLocation = pr . ComponentStream . Location ;
76+ var fileName = Path . GetFileName ( fileLocation ) ;
77+
78+ if ( fileName . Equals ( ManifestInfoFile , StringComparison . OrdinalIgnoreCase ) )
79+ {
80+ this . Logger . LogDebug ( "Discovered VCPKG package manifest file at: {Location}" , pr . ComponentStream . Location ) ;
81+
82+ using ( var reader = new StreamReader ( pr . ComponentStream . Stream ) )
83+ {
84+ var contents = await reader . ReadToEndAsync ( ) . ConfigureAwait ( false ) ;
85+ var manifestData = JsonConvert . DeserializeObject < ManifestInfo > ( contents ) ;
86+
87+ if ( manifestData == null || string . IsNullOrWhiteSpace ( manifestData . ManifestPath ) )
88+ {
89+ this . Logger . LogDebug ( "Failed to deserialize manifest-info.json or missing ManifestPath at {Path}" , pr . ComponentStream . Location ) ;
90+ }
91+ else
92+ {
93+ this . manifestMappings . TryAdd ( fileLocation , manifestData . ManifestPath ) ;
94+ }
95+ }
96+ }
97+ else
98+ {
99+ filteredProcessRequests . Add ( pr ) ;
100+ }
101+ } ) . ConfigureAwait ( false ) ;
102+
103+ return filteredProcessRequests . ToObservable ( ) ;
61104 }
62105
63106 private async Task ParseSpdxFileAsync (
@@ -123,4 +166,64 @@ private async Task ParseSpdxFileAsync(
123166 }
124167 }
125168 }
169+
170+ /// <summary>
171+ /// Attempts to resolve and return a manifest component recorder for the given recorder.
172+ /// Returns the matching manifest component recorder if found; otherwise, returns the original recorder.
173+ /// </summary>
174+ private ISingleFileComponentRecorder GetManifestComponentRecorder ( ISingleFileComponentRecorder singleFileComponentRecorder )
175+ {
176+ try
177+ {
178+ var manifestFileLocation = singleFileComponentRecorder . ManifestFileLocation ;
179+
180+ var vcpkgInstalledIndex = manifestFileLocation . IndexOf ( VcpkgInstalledFolder , StringComparison . OrdinalIgnoreCase ) ;
181+ if ( vcpkgInstalledIndex < 0 )
182+ {
183+ this . Logger . LogDebug (
184+ "Could not find '{VcpkgInstalled}' in ManifestFileLocation: '{ManifestFileLocation}'. Returning original recorder." ,
185+ VcpkgInstalledFolder ,
186+ manifestFileLocation ) ;
187+
188+ return singleFileComponentRecorder ;
189+ }
190+
191+ var vcpkgInstalledDir = manifestFileLocation [ ..( vcpkgInstalledIndex + VcpkgInstalledFolder . Length ) ] ;
192+
193+ var preferredManifest = Path . Combine ( vcpkgInstalledDir , "vcpkg" , ManifestInfoFile ) ;
194+ var fallbackManifest = Path . Combine ( vcpkgInstalledDir , ManifestInfoFile ) ;
195+
196+ // Try preferred location first
197+ if ( this . manifestMappings . TryGetValue ( preferredManifest , out var manifestPath ) && manifestPath != null )
198+ {
199+ return this . ComponentRecorder . CreateSingleFileComponentRecorder ( manifestPath ) ;
200+ }
201+ else if ( this . manifestMappings . TryGetValue ( fallbackManifest , out manifestPath ) && manifestPath != null )
202+ {
203+ // Use the fallback location.
204+ this . Logger . LogDebug (
205+ "Preferred manifest at '{PreferredManifest}' was not found or invalid. Using fallback manifest at '{FallbackManifest}'." ,
206+ preferredManifest ,
207+ fallbackManifest ) ;
208+
209+ return this . ComponentRecorder . CreateSingleFileComponentRecorder ( manifestPath ) ;
210+ }
211+
212+ this . Logger . LogDebug (
213+ "No valid manifest-info.json found at either '{PreferredManifest}' or '{FallbackManifest}' for base location '{VcpkgInstalledDir}'. Returning original recorder." ,
214+ preferredManifest ,
215+ fallbackManifest ,
216+ vcpkgInstalledDir ) ;
217+ }
218+ catch ( Exception ex )
219+ {
220+ this . Logger . LogWarning (
221+ ex ,
222+ "An exception occurred while resolving manifest component recorder for '{ManifestFileLocation}'. Returning original recorder." ,
223+ singleFileComponentRecorder . ManifestFileLocation ) ;
224+ }
225+
226+ // Always return the original recorder if no manifest is found or on error
227+ return singleFileComponentRecorder ;
228+ }
126229}
0 commit comments