// Copyright (c) .NET Foundation and contributors. 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 System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Text; using Xunit; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Tools.Test.Utilities; using System.Diagnostics; using FluentAssertions; namespace Microsoft.DotNet.Tests.ArgumentForwarding { public class ArgumentForwardingTests : TestBase { private static readonly string s_reflectorDllName = "ArgumentsReflector.dll"; private static readonly string s_reflectorCmdName = "reflector_cmd"; private string ReflectorPath { get; set; } private string ReflectorCmdPath { get; set; } public ArgumentForwardingTests() { Environment.SetEnvironmentVariable( Constants.MSBUILD_EXE_PATH, Path.Combine(new RepoDirectoriesProvider().Stage2Sdk, "MSBuild.dll")); // This test has a dependency on an argument reflector // Make sure it's been binplaced properly FindAndEnsureReflectorPresent(); } private void FindAndEnsureReflectorPresent() { ReflectorPath = Path.Combine(AppContext.BaseDirectory, s_reflectorDllName); ReflectorCmdPath = Path.Combine(AppContext.BaseDirectory, s_reflectorCmdName); File.Exists(ReflectorPath).Should().BeTrue(); } /// /// Tests argument forwarding in Command.Create /// This is a critical scenario for the driver. /// /// [Theory] [InlineData(@"""abc"" d e")] [InlineData(@"""ábc"" d é")] [InlineData(@"""abc"" d e")] [InlineData("\"abc\"\t\td\te")] [InlineData(@"a\\b d""e f""g h")] [InlineData(@"\ \\ \\\")] [InlineData(@"a\""b c d")] [InlineData(@"a\\""b c d")] [InlineData(@"a\\\""b c d")] [InlineData(@"a\\\\""b c d")] [InlineData(@"a\\\\""b c d")] [InlineData(@"a\\\\""b c"" d e")] [InlineData(@"a""b c""d e""f g""h i""j k""l")] [InlineData(@"a b c""def")] [InlineData(@"""\a\"" \\""\\\ b c")] [InlineData(@"a\""b \\ cd ""\e f\"" \\""\\\")] public void TestArgumentForwarding(string testUserArgument) { // Get Baseline Argument Evaluation via Reflector var rawEvaluatedArgument = RawEvaluateArgumentString(testUserArgument); // Escape and Re-Evaluate the rawEvaluatedArgument var escapedEvaluatedRawArgument = EscapeAndEvaluateArgumentString(rawEvaluatedArgument); rawEvaluatedArgument.Length.Should().Be(escapedEvaluatedRawArgument.Length); for (int i=0; i /// Tests argument forwarding in Command.Create to a cmd file /// This is a critical scenario for the driver. /// /// [Theory] [InlineData(@"""abc"" d e")] [InlineData(@"""abc"" d e")] [InlineData("\"abc\"\t\td\te")] [InlineData(@"a\\b d""e f""g h")] [InlineData(@"\ \\ \\\")] [InlineData(@"a\\""b c d")] [InlineData(@"a\\\\""b c d")] [InlineData(@"a\\\\""b c d")] [InlineData(@"a\\\\""b c"" d e")] [InlineData(@"a""b c""d e""f g""h i""j k""l")] [InlineData(@"a b c""def")] public void TestArgumentForwardingCmd(string testUserArgument) { if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { return; } // Get Baseline Argument Evaluation via Reflector // This does not need to be different for cmd because // it only establishes what the string[] args should be var rawEvaluatedArgument = RawEvaluateArgumentString(testUserArgument); // Escape and Re-Evaluate the rawEvaluatedArgument var escapedEvaluatedRawArgument = EscapeAndEvaluateArgumentStringCmd(rawEvaluatedArgument); try { rawEvaluatedArgument.Length.Should().Be(escapedEvaluatedRawArgument.Length); } catch(Exception e) { Console.WriteLine("Argument Lists differ in length."); var expected = string.Join(",", rawEvaluatedArgument); var actual = string.Join(",", escapedEvaluatedRawArgument); Console.WriteLine($"Expected: {expected}"); Console.WriteLine($"Actual: {actual}"); throw e; } for (int i = 0; i < rawEvaluatedArgument.Length; ++i) { var rawArg = rawEvaluatedArgument[i]; var escapedArg = escapedEvaluatedRawArgument[i]; try { rawArg.Should().Be(escapedArg); } catch(Exception e) { Console.WriteLine($"Expected: {rawArg}"); Console.WriteLine($"Actual: {escapedArg}"); throw e; } } } [Theory] [InlineData(@"a\""b c d")] [InlineData(@"a\\\""b c d")] [InlineData(@"""\a\"" \\""\\\ b c")] [InlineData(@"a\""b \\ cd ""\e f\"" \\""\\\")] public void TestArgumentForwardingCmdFailsWithUnbalancedQuote(string testArgString) { if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { return; } // Get Baseline Argument Evaluation via Reflector // This does not need to be different for cmd because // it only establishes what the string[] args should be var rawEvaluatedArgument = RawEvaluateArgumentString(testArgString); // Escape and Re-Evaluate the rawEvaluatedArgument var escapedEvaluatedRawArgument = EscapeAndEvaluateArgumentStringCmd(rawEvaluatedArgument); rawEvaluatedArgument.Length.Should().NotBe(escapedEvaluatedRawArgument.Length); } /// /// EscapeAndEvaluateArgumentString returns a representation of string[] args /// when rawEvaluatedArgument is passed as an argument to a process using /// Command.Create(). Ideally this should escape the argument such that /// the output is == rawEvaluatedArgument. /// /// A string[] representing string[] args as already evaluated by a process /// private string[] EscapeAndEvaluateArgumentString(string[] rawEvaluatedArgument) { var commandResult = Command.Create("dotnet", new[] { ReflectorPath }.Concat(rawEvaluatedArgument)) .CaptureStdErr() .CaptureStdOut() .Execute(); commandResult.ExitCode.Should().Be(0); return ParseReflectorOutput(commandResult.StdOut); } /// /// EscapeAndEvaluateArgumentString returns a representation of string[] args /// when rawEvaluatedArgument is passed as an argument to a process using /// Command.Create(). Ideally this should escape the argument such that /// the output is == rawEvaluatedArgument. /// /// A string[] representing string[] args as already evaluated by a process /// private string[] EscapeAndEvaluateArgumentStringCmd(string[] rawEvaluatedArgument) { var cmd = Command.Create(s_reflectorCmdName, rawEvaluatedArgument); var commandResult = cmd .CaptureStdErr() .CaptureStdOut() .Execute(); Console.WriteLine(commandResult.StdOut); Console.WriteLine(commandResult.StdErr); commandResult.ExitCode.Should().Be(0); return ParseReflectorCmdOutput(commandResult.StdOut); } /// /// Parse the output of the reflector into a string array. /// Reflector output is simply string[] args written to /// one string separated by commas. /// /// /// private string[] ParseReflectorOutput(string reflectorOutput) { return reflectorOutput.TrimEnd('\r', '\n').Split(','); } /// /// Parse the output of the reflector into a string array. /// Reflector output is simply string[] args written to /// one string separated by commas. /// /// /// private string[] ParseReflectorCmdOutput(string reflectorOutput) { var args = reflectorOutput.Split(new string[] { "," }, StringSplitOptions.None); args[args.Length-1] = args[args.Length-1].TrimEnd('\r', '\n'); // To properly pass args to cmd, quotes inside a parameter are doubled // Count them as a single quote for our comparison. for (int i=0; i < args.Length; ++i) { args[i] = args[i].Replace(@"""""", @""""); } return args; } /// /// RawEvaluateArgumentString returns a representation of string[] args /// when testUserArgument is provided (unmodified) as arguments to a c# /// process. /// /// A test argument representing what a "user" would provide to a process /// A string[] representing string[] args with the provided testUserArgument private string[] RawEvaluateArgumentString(string testUserArgument) { var proc = new Process { StartInfo = new ProcessStartInfo { FileName = Env.GetCommandPath("dotnet", ".exe", ""), Arguments = $"{ReflectorPath} {testUserArgument}", UseShellExecute = false, RedirectStandardOutput = true, CreateNoWindow = true } }; proc.Start(); proc.WaitForExit(); var stdOut = proc.StandardOutput.ReadToEnd(); Assert.Equal(0, proc.ExitCode); return ParseReflectorOutput(stdOut); } } }