From 4585d7565c56fe0e324818381ac7047654ce256a Mon Sep 17 00:00:00 2001 From: RKS Date: Wed, 16 Sep 2026 23:43:29 -0400 Subject: [PATCH] Quote manifest path when invoking cargo metadata When scanning Rust projects located in paths containing spaces, the unquoted manifest path passed after --manifest-path is split by the underlying command invocation mechanism, causing cargo metadata to fail with 'unexpected argument found'. This change wraps the manifest path in quotes if it contains spaces and is not already quoted, in both RustCliParser and RustMetadataContextBuilder. --- .../rust/Parsers/RustCliParser.cs | 6 ++- .../rust/RustMetadataContextBuilder.cs | 6 ++- .../RustCliParserTests.cs | 31 +++++++++++ .../RustMetadataContextBuilderTests.cs | 51 +++++++++++++++++++ 4 files changed, 92 insertions(+), 2 deletions(-) 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() {