Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Microsoft.ComponentDetection.Detectors.Vcpkg.Contracts;

using Newtonsoft.Json;

public class ManifestInfo
{
[JsonProperty("manifest-path")]
public string ManifestPath { get; set; }
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
namespace Microsoft.ComponentDetection.Detectors.Vcpkg;

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.ComponentDetection.Contracts;
Expand All @@ -15,7 +17,11 @@

public class VcpkgComponentDetector : FileComponentDetector
{
private const string VcpkgInstalledFolder = "vcpkg_installed";
private const string ManifestInfoFile = "manifest-info.json";

private readonly HashSet<string> projectRoots = [];
private readonly ConcurrentDictionary<string, string> manifestMappings = new(StringComparer.OrdinalIgnoreCase);

private readonly ICommandLineInvocationService commandLineInvocationService;
private readonly IEnvironmentVariableService envVarService;
Expand All @@ -38,11 +44,11 @@

public override IEnumerable<string> Categories => [Enum.GetName(typeof(DetectorClass), DetectorClass.Vcpkg)];

public override IList<string> SearchPatterns { get; } = ["vcpkg.spdx.json"];
public override IList<string> SearchPatterns { get; } = ["vcpkg.spdx.json", ManifestInfoFile];

public override IEnumerable<ComponentType> SupportedComponentTypes { get; } = [ComponentType.Vcpkg];

public override int Version => 2;
public override int Version => 3;

protected override async Task OnFileFoundAsync(ProcessRequest processRequest, IDictionary<string, string> detectorArgs, CancellationToken cancellationToken = default)
{
Expand All @@ -57,7 +63,44 @@
return;
}

await this.ParseSpdxFileAsync(singleFileComponentRecorder, file);
await this.ParseSpdxFileAsync(this.GetManifestComponentRecorder(singleFileComponentRecorder), file);
}

protected override async Task<IObservable<ProcessRequest>> OnPrepareDetectionAsync(IObservable<ProcessRequest> processRequests, IDictionary<string, string> detectorArgs, CancellationToken cancellationToken = default)
{
var filteredProcessRequests = new List<ProcessRequest>();

await processRequests.ForEachAsync(async pr =>
{
var fileLocation = pr.ComponentStream.Location;
var fileName = Path.GetFileName(fileLocation);

if (fileName.Equals(ManifestInfoFile, StringComparison.OrdinalIgnoreCase))
{
this.Logger.LogDebug("Discovered VCPKG package manifest file at: {Location}", pr.ComponentStream.Location);

using (var reader = new StreamReader(pr.ComponentStream.Stream))
{
var contents = await reader.ReadToEndAsync().ConfigureAwait(false);
var manifestData = JsonConvert.DeserializeObject<ManifestInfo>(contents);

if (manifestData == null || string.IsNullOrWhiteSpace(manifestData.ManifestPath))
{
this.Logger.LogDebug("Failed to deserialize manifest-info.json or missing ManifestPath at {Path}", pr.ComponentStream.Location);
}

Check warning on line 90 in src/Microsoft.ComponentDetection.Detectors/vcpkg/VcpkgComponentDetector.cs

View check run for this annotation

Codecov / codecov/patch

src/Microsoft.ComponentDetection.Detectors/vcpkg/VcpkgComponentDetector.cs#L88-L90

Added lines #L88 - L90 were not covered by tests
else
{
this.manifestMappings.TryAdd(fileLocation, manifestData.ManifestPath);
}
}
}
else
{
filteredProcessRequests.Add(pr);
}
}).ConfigureAwait(false);

return filteredProcessRequests.ToObservable();
}

private async Task ParseSpdxFileAsync(
Expand Down Expand Up @@ -123,4 +166,64 @@
}
}
}

/// <summary>
/// Attempts to resolve and return a manifest component recorder for the given recorder.
/// Returns the matching manifest component recorder if found; otherwise, returns the original recorder.
/// </summary>
private ISingleFileComponentRecorder GetManifestComponentRecorder(ISingleFileComponentRecorder singleFileComponentRecorder)
{
try
{
var manifestFileLocation = singleFileComponentRecorder.ManifestFileLocation;

var vcpkgInstalledIndex = manifestFileLocation.IndexOf(VcpkgInstalledFolder, StringComparison.OrdinalIgnoreCase);
if (vcpkgInstalledIndex < 0)
{
this.Logger.LogDebug(
"Could not find '{VcpkgInstalled}' in ManifestFileLocation: '{ManifestFileLocation}'. Returning original recorder.",
VcpkgInstalledFolder,
manifestFileLocation);

return singleFileComponentRecorder;
}

var vcpkgInstalledDir = manifestFileLocation[..(vcpkgInstalledIndex + VcpkgInstalledFolder.Length)];

var preferredManifest = Path.Combine(vcpkgInstalledDir, "vcpkg", ManifestInfoFile);
var fallbackManifest = Path.Combine(vcpkgInstalledDir, ManifestInfoFile);

// Try preferred location first
if (this.manifestMappings.TryGetValue(preferredManifest, out var manifestPath) && manifestPath != null)
{
return this.ComponentRecorder.CreateSingleFileComponentRecorder(manifestPath);
}
else if (this.manifestMappings.TryGetValue(fallbackManifest, out manifestPath) && manifestPath != null)
{
// Use the fallback location.
this.Logger.LogDebug(
"Preferred manifest at '{PreferredManifest}' was not found or invalid. Using fallback manifest at '{FallbackManifest}'.",
preferredManifest,
fallbackManifest);

return this.ComponentRecorder.CreateSingleFileComponentRecorder(manifestPath);
}

this.Logger.LogDebug(
"No valid manifest-info.json found at either '{PreferredManifest}' or '{FallbackManifest}' for base location '{VcpkgInstalledDir}'. Returning original recorder.",
preferredManifest,
fallbackManifest,
vcpkgInstalledDir);
}
catch (Exception ex)
{
this.Logger.LogWarning(
ex,
"An exception occurred while resolving manifest component recorder for '{ManifestFileLocation}'. Returning original recorder.",
singleFileComponentRecorder.ManifestFileLocation);
}

Check warning on line 224 in src/Microsoft.ComponentDetection.Detectors/vcpkg/VcpkgComponentDetector.cs

View check run for this annotation

Codecov / codecov/patch

src/Microsoft.ComponentDetection.Detectors/vcpkg/VcpkgComponentDetector.cs#L218-L224

Added lines #L218 - L224 were not covered by tests

// Always return the original recorder if no manifest is found or on error
return singleFileComponentRecorder;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace Microsoft.ComponentDetection.Detectors.Tests;

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
Expand Down Expand Up @@ -177,4 +178,60 @@ public async Task TestInvalidFileAsync()
var components = detectedComponents.ToList();
components.Should().BeEmpty();
}

[TestMethod]
[DataTestMethod]
[DataRow("vcpkg_installed\\manifest-info.json", "vcpkg.json")]
[DataRow("vcpkg_installed\\vcpkg\\manifest-info.json", "vcpkg.json")]
[DataRow("bad_location\\manifest-info.json", "vcpkg_installed\\packageLocation\\vcpkg.spdx.json")]
public async Task TestVcpkgManifestFileAsync(string manifestPath, string pathToVcpkg)
{
var t_pathToVcpkg = CrossPlatformPath(Path.GetFullPath(pathToVcpkg));
var t_manifestPath = CrossPlatformPath(Path.GetFullPath(manifestPath));

var spdxFile = @"{
""SPDXID"": ""SPDXRef - DOCUMENT"",
""documentNamespace"":
""https://spdx.org/spdxdocs/nlohmann-json-x64-linux-3.10.4-78c7f190-b402-44d1-a364-b9ac86392b84"",
""name"": ""nlohmann-json:x64-linux@3.10.4 69dcfc6886529ad2d210f71f132d743672a7e65d2c39f53456f17fc5fc08b278"",
""packages"": [
{
""name"": ""nlohmann-json"",
""SPDXID"": ""SPDXRef-port"",
""versionInfo"": ""3.10.4#5"",
""downloadLocation"": ""git+https://github.com/Microsoft/vcpkg#ports/nlohmann-json"",
""homepage"": ""https://github.com/nlohmann/json"",
""licenseConcluded"": ""NOASSERTION"",
""licenseDeclared"": ""NOASSERTION"",
""copyrightText"": ""NOASSERTION"",
""description"": ""JSON for Modern C++"",
""comment"": ""This is the port (recipe) consumed by vcpkg.""
}
]
}";
var manifestFile = $@"{{
""manifest-path"": ""{t_pathToVcpkg.Replace("\\", "\\\\")}""
}}";

var (scanResult, componentRecorder) = await this.DetectorTestUtility
.WithFile(CrossPlatformPath(Path.GetFullPath("vcpkg_installed\\packageLocation\\vcpkg.spdx.json")), spdxFile)
.WithFile(t_manifestPath, manifestFile)
.ExecuteDetectorAsync();

scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);

var detectedComponents = componentRecorder.GetDependencyGraphsByLocation();

var singleFileComponent = detectedComponents.FirstOrDefault();
singleFileComponent.Should().NotBeNull();

var expectedResult = singleFileComponent.Key.Replace("/tmp/", string.Empty);
expectedResult.Should().Be(t_pathToVcpkg);
}

private static string CrossPlatformPath(string relPath)
{
var segments = relPath.Split(['\\', '/'], StringSplitOptions.RemoveEmptyEntries);
return Path.Combine(segments);
}
}
Loading