diff --git a/src/BinSkim.Driver/BinSkim.cs b/src/BinSkim.Driver/BinSkim.cs index 70310a72..91671476 100644 --- a/src/BinSkim.Driver/BinSkim.cs +++ b/src/BinSkim.Driver/BinSkim.cs @@ -16,7 +16,7 @@ internal static class BinSkim { private static int Main(string[] args) { - args = EntryPointUtilities.GenerateArguments(args, new FileSystem(), new EnvironmentVariables()); + args = ExpandArguments.GenerateArguments(args, new FileSystem(), new EnvironmentVariables()); args = RewriteArgs(args); var rewrittenArgs = new List(args); diff --git a/src/BinSkim.Driver/ExpandArguments.cs b/src/BinSkim.Driver/ExpandArguments.cs new file mode 100644 index 00000000..f1cf5d7b --- /dev/null +++ b/src/BinSkim.Driver/ExpandArguments.cs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; + +using Microsoft.CodeAnalysis.Sarif; +using Microsoft.CodeAnalysis.Sarif.Driver; + +namespace Microsoft.CodeAnalysis.IL +{ + public static class ExpandArguments + { + public static string[] GenerateArguments( + string[] args, + IFileSystem fileSystem, + IEnvironmentVariables environmentVariables) + { + var expandedArguments = new List(); + + foreach (string argument in args) + { + string trimArgument = argument.Trim('"'); + if (!IsResponseFileArgument(trimArgument)) + { + expandedArguments.Add(trimArgument); + continue; + } + + string responseFile = trimArgument.Substring(1); + + responseFile = environmentVariables.ExpandEnvironmentVariables(responseFile); + responseFile = fileSystem.PathGetFullPath(responseFile); + + string[] responseFileLines = fileSystem.FileReadAllLines(responseFile); + ExpandResponseFile(responseFileLines, expandedArguments); + } + + return expandedArguments.ToArray(); + } + + private static bool IsResponseFileArgument(string argument) + { + return argument.Length > 1 && argument[0] == '@'; + } + + private static void ExpandResponseFile(string[] responseFileLines, List expandedArguments) + { + foreach (string responseFileLine in responseFileLines) + { + string responseFilePath = responseFileLine; + + // Ignore comments from response file lines + int commentIndex = responseFileLine.IndexOf('#'); + if (commentIndex >= 0) + { + responseFilePath = responseFileLine.Substring(0, commentIndex); + } + + List fileList = ArgumentSplitter.CommandLineToArgvW(responseFilePath.Trim()) ?? + throw new InvalidOperationException("Could not parse response file line:" + responseFileLine); + + expandedArguments.AddRange(fileList); + } + } + } +} diff --git a/src/Test.UnitTests.BinSkim.Driver/ExpandArgumentsUnitTests.cs b/src/Test.UnitTests.BinSkim.Driver/ExpandArgumentsUnitTests.cs new file mode 100644 index 00000000..e328541d --- /dev/null +++ b/src/Test.UnitTests.BinSkim.Driver/ExpandArgumentsUnitTests.cs @@ -0,0 +1,180 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Globalization; +using System.IO; + +using FluentAssertions; + +using Microsoft.CodeAnalysis.IL; + +using Microsoft.CodeAnalysis.Sarif; +using Microsoft.CodeAnalysis.Sarif.Driver; + +using Moq; + +using Xunit; + + +namespace Microsoft.CodeAnalysis.BinSkim.Rules +{ + public class ExpandArgumentsUnitTests + { + private static void SetupTestMocks( + string responseFileName, + string[] responseFileContents, + out Mock fileSystemMock, + out Mock environmentVariablesMock) + { + fileSystemMock = new Mock(); + environmentVariablesMock = new Mock(); + + fileSystemMock.Setup(fs => fs.PathGetFullPath(responseFileName)).Returns(responseFileName); + fileSystemMock.Setup(fs => fs.FileReadAllLines(responseFileName)).Returns(responseFileContents); + environmentVariablesMock.Setup(ev => ev.ExpandEnvironmentVariables(responseFileName)).Returns(responseFileName); + } + + [Fact] + public void GenerateArguments_SucceedsWithEmptyArgumentList() + { + string[] result = ExpandArguments.GenerateArguments(Array.Empty(), null, null); + + result.Should().BeEmpty(); + } + + [Fact] + public void GenerateArguments_SucceedsWithNormalArguments() + { + string[] args = new[] { "/y:z", "/x" }; + + string[] result = ExpandArguments.GenerateArguments(args, null, null); + + result.Length.Should().Be(2); + result.Should().ContainInOrder(args); + } + + [Fact] + public void GenerateArguments_ExceptionIfResponseFileDoesNotExist() + { + string NonexistentResponseFile = Guid.NewGuid().ToString() + ".rsp"; + string[] args = new[] { "/a", "@" + NonexistentResponseFile, "/f" }; + + Assert.Throws( + () => ExpandArguments.GenerateArguments(args, new FileSystem(), new EnvironmentVariables()) + ); + } + + [Theory] + [InlineData(new[] { "/b", "/c:val /d", " /e " }, new[] { "/a", "/b", "/c:val", "/d", "/e", "/f" })] + public void GenerateArguments_ExpandsResponseFileContents(string[] rspContent, string[] expected) + { + const string ResponseFileName = "Mocked.rsp"; + string[] args = new[] { "/a", "@" + ResponseFileName, "/f" }; + + SetupTestMocks( + ResponseFileName, + rspContent, + out Mock fileSystemMock, + out Mock environmentVariablesMock); + + IFileSystem fileSystem = fileSystemMock.Object; + IEnvironmentVariables environmentVariables = environmentVariablesMock.Object; + + string[] result = ExpandArguments.GenerateArguments(args, fileSystem, environmentVariables); + + result.Should().ContainInOrder(expected); + + fileSystemMock.Verify(fs => fs.PathGetFullPath(ResponseFileName), Times.Once); + fileSystemMock.Verify(fs => fs.FileReadAllLines(ResponseFileName), Times.Once); + environmentVariablesMock.Verify(ev => ev.ExpandEnvironmentVariables(ResponseFileName), Times.Once); + } + + [Theory] + [InlineData(new[] { "/b", "/c:val /d", "# Random Comment", " /e " }, new[] { "/a", "/b", "/c:val", "/d", "/e", "/f" })] + [InlineData(new[] { "/b", "/c:val /d#Another Comment", " /e " }, new[] { "/a", "/b", "/c:val", "/d", "/e", "/f" })] + public void GenerateArguments_TrimCommentsFromResponseFileContents(string[] rspContent, string[] expected) + { + const string ResponseFileName = "Mocked.rsp"; + string[] args = new[] { "/a", "@" + ResponseFileName, "/f" }; + + SetupTestMocks( + ResponseFileName, + rspContent, + out Mock fileSystemMock, + out Mock environmentVariablesMock); + + IFileSystem fileSystem = fileSystemMock.Object; + IEnvironmentVariables environmentVariables = environmentVariablesMock.Object; + + string[] result = ExpandArguments.GenerateArguments(args, fileSystem, environmentVariables); + + result.Should().ContainInOrder(expected); + + fileSystemMock.Verify(fs => fs.PathGetFullPath(ResponseFileName), Times.Once); + fileSystemMock.Verify(fs => fs.FileReadAllLines(ResponseFileName), Times.Once); + environmentVariablesMock.Verify(ev => ev.ExpandEnvironmentVariables(ResponseFileName), Times.Once); + } + + [Theory] + [InlineData(new[] { "a \"one two\" b" }, new[] { "a", "one two", "b" })] + public void GenerateArguments_StripsQuotesFromAroundArgsWithSpacesInResponseFiles(string[] rspContent, string[] expected) + { + const string ResponseFileName = "Mocked.rsp"; + string[] args = new[] { "@" + ResponseFileName }; + + SetupTestMocks( + ResponseFileName, + rspContent, + out Mock fileSystemMock, + out Mock environmentVariablesMock); + + IFileSystem fileSystem = fileSystemMock.Object; + IEnvironmentVariables environmentVariables = environmentVariablesMock.Object; + + string[] result = ExpandArguments.GenerateArguments(args, fileSystem, environmentVariables); + + result.Length.Should().Be(3); + result.Should().ContainInOrder(expected); + + fileSystemMock.Verify(fs => fs.PathGetFullPath(ResponseFileName), Times.Once); + fileSystemMock.Verify(fs => fs.FileReadAllLines(ResponseFileName), Times.Once); + environmentVariablesMock.Verify(ev => ev.ExpandEnvironmentVariables(ResponseFileName), Times.Once); + } + + [Theory] + [InlineData(new[] { "a \"one two\" b" }, new[] { "a", "one two", "b" })] + public void GenerateArguments_ExpandsEnvironmentVariablesInResponseFilePathName(string[] rspContent, string[] expected) + { + const string DirectoryVariableName = "InstallationDirectory"; + const string ResponseFileName = "Mocked.rsp"; + + string responseFileNameArgument = string.Format( + CultureInfo.InvariantCulture, + @"%{0}%\{1}", + DirectoryVariableName, + ResponseFileName + ); + + string[] args = new[] { "@" + responseFileNameArgument }; + + SetupTestMocks( + responseFileNameArgument, + rspContent, + out Mock fileSystemMock, + out Mock environmentVariablesMock); + + IFileSystem fileSystem = fileSystemMock.Object; + IEnvironmentVariables environmentVariables = environmentVariablesMock.Object; + + string[] result = ExpandArguments.GenerateArguments(args, fileSystem, environmentVariables); + + result.Length.Should().Be(3); + result.Should().ContainInOrder(expected); + + fileSystemMock.Verify(fs => fs.PathGetFullPath(responseFileNameArgument), Times.Once); + fileSystemMock.Verify(fs => fs.FileReadAllLines(responseFileNameArgument), Times.Once); + environmentVariablesMock.Verify(ev => ev.ExpandEnvironmentVariables(responseFileNameArgument), Times.Once); + } + } +}