diff --git a/src/Microsoft.ComponentDetection.Detectors/ruby/RubyComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/ruby/RubyComponentDetector.cs index 93d9b7f82..ac4b886bb 100644 --- a/src/Microsoft.ComponentDetection.Detectors/ruby/RubyComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/ruby/RubyComponentDetector.cs @@ -220,17 +220,27 @@ private void ParseSection(ISingleFileComponentRecorder singleFileComponentRecord wasParentDependencyExcluded = false; var splits = line.Trim().Split(" "); name = splits[0].Trim(); - var version = splits[1][1..^1]; + var fullVersion = splits[1][1..^1]; TypedComponent newComponent; - if (this.IsVersionRelative(version)) + if (this.IsVersionRelative(fullVersion)) { - this.Logger.LogWarning("Found component with invalid version, name = {RubyComponentName} and version = {RubyComponentVersion}", name, version); - singleFileComponentRecorder.RegisterPackageParseFailure($"{name} - {version}"); + this.Logger.LogWarning("Found component with invalid version, name = {RubyComponentName} and version = {RubyComponentVersion}", name, fullVersion); + singleFileComponentRecorder.RegisterPackageParseFailure($"{name} - {fullVersion}"); wasParentDependencyExcluded = true; continue; } + // Ruby version strings are formatted differently from pure semantic versioning. + // Specifically, everything after the dash is the platform, not the pre-release version. For + // example: "1.19.4-x86_64-linux-gnu". Ruby represents pre-release versions by placing a letter + // in the main version string. For example: "2.0.0.rc1". + // See: https://guides.rubygems.org/patterns/#prerelease-gems + + // Remove the platform from the version string (if it exists). + var versionParts = fullVersion.Split("-", 2); + var version = versionParts[0]; + if (sectionType == SectionType.GEM || sectionType == SectionType.PATH) { newComponent = new RubyGemsComponent(name, version, remote); diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/RubyDetectorTest.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/RubyDetectorTest.cs index 44cb9543c..59cde69b3 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/RubyDetectorTest.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/RubyDetectorTest.cs @@ -405,6 +405,41 @@ public async Task TestRubyDetector_DetectorRecognizeLocalDependenciesAsync() this.AssertRubyComponentNameAndVersion(detectedComponents, name: "test2", version: "1.0.0"); } + [TestMethod] + public async Task TestRubyDetector_StripPlatformAsync() + { + var gemFileLockContent = @"GEM + remote: https://rubygems.org/ + specs: + nokogiri (1.19.4-aarch64-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.4-aarch64-linux-musl) + racc (~> 1.4) + nokogiri (1.19.4-arm-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.4-arm-linux-musl) + racc (~> 1.4) + nokogiri (1.19.4-arm64-darwin) + racc (~> 1.4) + nokogiri (1.19.4-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.19.4-x86_64-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.4-x86_64-linux-musl) + racc (~> 1.4)"; + + var (scanResult, componentRecorder) = await this.detectorTestUtility + .WithFile("1Gemfile.lock", gemFileLockContent) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + + var detectedComponents = componentRecorder.GetDetectedComponents(); + detectedComponents.Should().HaveCount(1); + + this.AssertRubyComponentNameAndVersion(detectedComponents, "nokogiri", "1.19.4"); + } + private void AssertRubyComponentNameAndVersion(IEnumerable detectedComponents, string name, string version) { detectedComponents.SingleOrDefault(c =>