diff --git a/src/Microsoft.ComponentDetection.Detectors/rust/Parsers/RustCliParser.cs b/src/Microsoft.ComponentDetection.Detectors/rust/Parsers/RustCliParser.cs index 11ac3c101..f1a4f3271 100644 --- a/src/Microsoft.ComponentDetection.Detectors/rust/Parsers/RustCliParser.cs +++ b/src/Microsoft.ComponentDetection.Detectors/rust/Parsers/RustCliParser.cs @@ -70,6 +70,10 @@ public async Task ParseAsync( return result; } + var manifestPath = componentStream.Location.Contains(' ') && !componentStream.Location.StartsWith('"') + ? $"\"{componentStream.Location}\"" + : componentStream.Location; + var cliResult = await this.cliService.ExecuteCommandAsync( command: "cargo", additionalCandidateCommands: null, @@ -77,7 +81,7 @@ public async Task ParseAsync( cancellationToken: cancellationToken, "metadata", "--manifest-path", - componentStream.Location, + manifestPath, "--format-version=1", "--locked"); diff --git a/src/Microsoft.ComponentDetection.Detectors/rust/RustMetadataContextBuilder.cs b/src/Microsoft.ComponentDetection.Detectors/rust/RustMetadataContextBuilder.cs index c728e227e..55229734f 100644 --- a/src/Microsoft.ComponentDetection.Detectors/rust/RustMetadataContextBuilder.cs +++ b/src/Microsoft.ComponentDetection.Detectors/rust/RustMetadataContextBuilder.cs @@ -185,6 +185,10 @@ private async Task RunCargoMetadataAsync(string manifestPath, Can return null; } + var manifestPathArg = manifestPath.Contains(' ') && !manifestPath.StartsWith('"') + ? $"\"{manifestPath}\"" + : manifestPath; + var res = await this.cliService.ExecuteCommandAsync( "cargo", additionalCandidateCommands: null, @@ -192,7 +196,7 @@ private async Task RunCargoMetadataAsync(string manifestPath, Can cancellationToken: token, "metadata", "--manifest-path", - manifestPath, + manifestPathArg, "--format-version=1", "--locked"); diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/RustCliParserTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/RustCliParserTests.cs index 1fb1b7b54..c9e3ac017 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/RustCliParserTests.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/RustCliParserTests.cs @@ -1136,6 +1136,37 @@ public async Task VirtualManifest_MultipleRootNodes_AllProcessed() distinctNames.Should().Contain("pkgB"); } + [TestMethod] + public async Task ParseAsync_QuotesManifestPathWhenPathContainsSpaces() + { + var tomlPath = "C:/repo with space/AppControl Manager/Cargo.toml"; + var quotedTomlPath = $"\"{tomlPath}\""; + + var json = """ + { + "packages": [ + { "name": "my-pkg", "version": "1.0.0", "id": "my-pkg 1.0.0 (path+file:///C:/repo)", "dependencies": [], "source": null } + ], + "workspace_members": [ "my-pkg 1.0.0 (path+file:///C:/repo)" ], + "resolve": { + "nodes": [ + { "id": "my-pkg 1.0.0 (path+file:///C:/repo)", "deps": [] } + ] + } + } + """; + + this.cli.Setup(c => c.CanCommandBeLocatedAsync("cargo", null)).ReturnsAsync(true); + this.cli.Setup(c => c.ExecuteCommandAsync("cargo", null, null, It.IsAny(), "metadata", "--manifest-path", quotedTomlPath, "--format-version=1", "--locked")) + .ReturnsAsync(new CommandLineExecutionResult { ExitCode = 0, StdOut = json }); + + var recorder = new Mock(MockBehavior.Loose); + var result = await this.parser.ParseAsync(MakeTomlStream(tomlPath), recorder.Object); + + result.Success.Should().BeTrue(); + this.cli.Verify(c => c.ExecuteCommandAsync("cargo", null, null, It.IsAny(), "metadata", "--manifest-path", quotedTomlPath, "--format-version=1", "--locked"), Times.Once()); + } + private async Task InvokeProcessMetadataAsync(string manifestLocation, ISingleFileComponentRecorder fallbackRecorder, CargoMetadata metadata) => await this.parser.ParseFromMetadataAsync( new ComponentStream { Location = manifestLocation, Pattern = "Cargo.toml", Stream = new MemoryStream([]) }, diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/RustMetadataContextBuilderTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/RustMetadataContextBuilderTests.cs index 5e70e9b80..9e135cef8 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/RustMetadataContextBuilderTests.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/RustMetadataContextBuilderTests.cs @@ -180,6 +180,57 @@ public async Task BuildPackageOwnershipMapAsync_CargoMetadataFails_AddsToFailedM result.ManifestToMetadata.Should().BeEmpty(); } + [TestMethod] + public async Task BuildPackageOwnershipMapAsync_QuotesManifestPathWhenPathContainsSpaces() + { + var tomlPath = "C:/repo with space/AppControl Manager/Cargo.toml"; + var quotedTomlPath = $"\"{tomlPath}\""; + + var json = """ + { + "packages": [ + { "name": "my-pkg", "version": "1.0.0", "id": "my-pkg 1.0.0 (path+file:///C:/repo)", "dependencies": [], "source": null } + ], + "workspace_members": [ "my-pkg 1.0.0 (path+file:///C:/repo)" ], + "resolve": { + "nodes": [ + { "id": "my-pkg 1.0.0 (path+file:///C:/repo)", "deps": [] } + ] + } + } + """; + + this.envVarService.Setup(e => e.IsEnvironmentVariableValueTrue("DisableRustCliScan")).Returns(false); + this.cliService.Setup(c => c.CanCommandBeLocatedAsync("cargo", null)).ReturnsAsync(true); + this.cliService.Setup(c => c.ExecuteCommandAsync( + "cargo", + null, + null, + It.IsAny(), + "metadata", + "--manifest-path", + quotedTomlPath, + "--format-version=1", + "--locked")) + .ReturnsAsync(new CommandLineExecutionResult { ExitCode = 0, StdOut = json }); + + var result = await this.builder.BuildPackageOwnershipMapAsync([tomlPath]); + + result.FailedManifests.Should().BeEmpty(); + this.cliService.Verify( + c => c.ExecuteCommandAsync( + "cargo", + null, + null, + It.IsAny(), + "metadata", + "--manifest-path", + quotedTomlPath, + "--format-version=1", + "--locked"), + Times.Once()); + } + [TestMethod] public async Task BuildPackageOwnershipMapAsync_SimpleDependency_BuildsOwnershipMap() {