diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/VcpkgComponent.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/VcpkgComponent.cs index ae70a95c9..4905d6066 100644 --- a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/VcpkgComponent.cs +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/VcpkgComponent.cs @@ -1,6 +1,7 @@ #nullable disable namespace Microsoft.ComponentDetection.Contracts.TypedComponent; +using System.Collections.Generic; using System.Text.Json.Serialization; using PackageUrl; @@ -53,18 +54,11 @@ public override PackageUrl PackageUrl { get { - if (this.PortVersion > 0) - { - return new PackageUrl($"pkg:vcpkg/{this.Name}@{this.Version}?port_version={this.PortVersion}"); - } - else if (this.Version != null) - { - return new PackageUrl($"pkg:vcpkg/{this.Name}@{this.Version}"); - } - else - { - return new PackageUrl($"pkg:vcpkg/{this.Name}"); - } + var qualifiers = this.PortVersion > 0 + ? new SortedDictionary { { "port_version", this.PortVersion.ToString() } } + : null; + + return new PackageUrl("vcpkg", null, this.Name, this.Version, qualifiers, null); } } diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/VcpkgComponentDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/VcpkgComponentDetectorTests.cs index da64513bd..4cf279fd4 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/VcpkgComponentDetectorTests.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/VcpkgComponentDetectorTests.cs @@ -150,6 +150,59 @@ public async Task TestTinyxmlAndResourceAsync() sbomComponent.DownloadLocation.Should().Be("git+https://github.com/leethomason/tinyxml2"); } + [TestMethod] + public async Task TestResourceWithSlashInNameProducesValidPackageUrlAsync() + { + var spdxFile = """ + { + "SPDXID": "SPDXRef-DOCUMENT", + "documentNamespace": "https://spdx.org/spdxdocs/brotli-x64-windows", + "name": "brotli:x64-windows@1.0.9", + "packages": [ + { + "name": "brotli", + "SPDXID": "SPDXRef-port", + "versionInfo": "1.0.9#0", + "downloadLocation": "git+https://github.com/Microsoft/vcpkg#ports/brotli", + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "copyrightText": "NOASSERTION" + }, + { + "SPDXID": "SPDXRef-resource-1", + "name": "google/brotli", + "downloadLocation": "git+https://github.com/google/brotli@1.0.9", + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "copyrightText": "NOASSERTION" + } + ] + } + """; + var (scanResult, componentRecorder) = await this + .detectorTestUtility.WithFile("vcpkg.spdx.json", spdxFile) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + + var detectedComponents = componentRecorder.GetDetectedComponents(); + var components = detectedComponents.ToList(); + + components.Should().HaveCount(2); + + var resourceComponent = (VcpkgComponent)components + .First(c => ((VcpkgComponent)c.Component).SPDXID == "SPDXRef-resource-1") + .Component; + resourceComponent.Name.Should().Be("google/brotli"); + resourceComponent.Version.Should().Be("1.0.9"); + + // This was the bug: names with slashes caused MalformedPackageUrlException + var purl = resourceComponent.PackageUrl; + purl.Should().NotBeNull(); + purl.ToString().Should().Contain("vcpkg"); + purl.ToString().Should().Contain("brotli"); + } + [TestMethod] public async Task TestBlankJsonAsync() {