Skip to content

Commit

Permalink
Fixing the issues related to response file. (#1196)
Browse files Browse the repository at this point in the history
Fixing the issues related to response file
  • Loading branch information
singhsarab authored and codito committed Oct 12, 2017
1 parent 2437088 commit 073c64a
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 191 deletions.
197 changes: 74 additions & 123 deletions src/Microsoft.TestPlatform.Utilities/CommandLineUtilities.cs
Original file line number Diff line number Diff line change
@@ -1,157 +1,108 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

// Copyright (c) Microsoft Corporation. All rights reserved.
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

// Code taken from: https://github.com/dotnet/roslyn/blob/d00363d8f892f4f3c514718a964ea37783d21de5/src/Compilers/Core/Portable/InternalUtilities/CommandLineUtilities.cs

namespace Microsoft.VisualStudio.TestPlatform.Utilities
{
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;

public static class CommandLineUtilities
{
/// <summary>
/// Split a command line by the same rules as Main would get the commands except the original
/// state of backslashes and quotes are preserved. For example in normal Windows command line
/// parsing the following command lines would produce equivalent Main arguments:
///
/// - /r:a,b
/// - /r:"a,b"
///
/// This method will differ as the latter will have the quotes preserved. The only case where
/// quotes are removed is when the entire argument is surrounded by quotes without any inner
/// quotes.
/// </summary>
/// <remarks>
/// Rules for command line parsing, according to MSDN:
///
/// Arguments are delimited by white space, which is either a space or a tab.
///
/// A string surrounded by double quotation marks ("string") is interpreted
/// as a single argument, regardless of white space contained within.
/// A quoted string can be embedded in an argument.
///
/// A double quotation mark preceded by a backslash (\") is interpreted as a
/// literal double quotation mark character (").
///
/// Backslashes are interpreted literally, unless they immediately precede a
/// double quotation mark.
///
/// If an even number of backslashes is followed by a double quotation mark,
/// one backslash is placed in the argv array for every pair of backslashes,
/// and the double quotation mark is interpreted as a string delimiter.
///
/// If an odd number of backslashes is followed by a double quotation mark,
/// one backslash is placed in the argv array for every pair of backslashes,
/// and the double quotation mark is "escaped" by the remaining backslash,
/// causing a literal double quotation mark (") to be placed in argv.
/// </remarks>
public static IEnumerable<string> SplitCommandLineIntoArguments(string commandLine, bool removeHashComments)
{
char? unused;
return SplitCommandLineIntoArguments(commandLine, removeHashComments, out unused);
}

public static IEnumerable<string> SplitCommandLineIntoArguments(string commandLine, bool removeHashComments, out char? illegalChar)
public static bool SplitCommandLineIntoArguments(string args, out string[] arguments)
{
var builder = new StringBuilder(commandLine.Length);
var list = new List<string>();
var i = 0;
bool hadError = false;
var argArray = new List<string>();
var currentArg = new StringBuilder();
bool inQuotes = false;
int index = 0;

illegalChar = null;
while (i < commandLine.Length)
try
{
while (i < commandLine.Length && char.IsWhiteSpace(commandLine[i]))
while (true)
{
i++;
}

if (i == commandLine.Length)
{
break;
}
// skip whitespace
while (char.IsWhiteSpace(args[index]))
{
index += 1;
}

if (commandLine[i] == '#' && removeHashComments)
{
break;
}
// # - comment to end of line
if (args[index] == '#')
{
index += 1;
while (args[index] != '\n')
{
index += 1;
}
continue;
}

var quoteCount = 0;
builder.Length = 0;
while (i < commandLine.Length && (!char.IsWhiteSpace(commandLine[i]) || (quoteCount % 2 != 0)))
{
var current = commandLine[i];
switch (current)
// do one argument
do
{
case '\\':
if (args[index] == '\\')
{
int cSlashes = 1;
index += 1;
while (index == args.Length && args[index] == '\\')
{
var slashCount = 0;
do
{
builder.Append(commandLine[i]);
i++;
slashCount++;
} while (i < commandLine.Length && commandLine[i] == '\\');

// Slashes not followed by a quote character can be ignored for now
if (i >= commandLine.Length || commandLine[i] != '"')
{
break;
}

// If there is an odd number of slashes then it is escaping the quote
// otherwise it is just a quote.
if (slashCount % 2 == 0)
{
quoteCount++;
}

builder.Append('"');
i++;
break;
cSlashes += 1;
}

case '"':
builder.Append(current);
quoteCount++;
i++;
break;

default:
if ((current >= 0x1 && current <= 0x1f) || current == '|')
if (index == args.Length || args[index] != '"')
{
if (illegalChar == null)
{
illegalChar = current;
}
currentArg.Append('\\', cSlashes);
}
else
{
builder.Append(current);
currentArg.Append('\\', (cSlashes >> 1));
if (0 != (cSlashes & 1))
{
currentArg.Append('"');
}
else
{
inQuotes = !inQuotes;
}
}

i++;
break;
}
}
else if (args[index] == '"')
{
inQuotes = !inQuotes;
index += 1;
}
else
{
currentArg.Append(args[index]);
index += 1;
}
} while (!char.IsWhiteSpace(args[index]) || inQuotes);
argArray.Add(currentArg.ToString());
currentArg.Clear();
}

// If the quote string is surrounded by quotes with no interior quotes then
// remove the quotes here.
if (quoteCount == 2 && builder[0] == '"' && builder[builder.Length - 1] == '"')
}
catch (IndexOutOfRangeException)
{
// got EOF
if (inQuotes)
{
builder.Remove(0, length: 1);
builder.Remove(builder.Length - 1, length: 1);
EqtTrace.Verbose("Executor.Execute: Exiting with exit code of {0}", 1);
EqtTrace.Error(string.Format(CultureInfo.InvariantCulture, "Error: Unbalanced '\"' in command line argument file"));
hadError = true;
}

if (builder.Length > 0)
else if (currentArg.Length > 0)
{
list.Add(builder.ToString());
// valid argument can be terminated by EOF
argArray.Add(currentArg.ToString());
}
}

return list;
arguments = argArray.ToArray();
return hadError;
}
}
}
80 changes: 38 additions & 42 deletions src/vstest.console/CommandLine/Executor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ private int GetArgumentProcessors(string[] args, out List<IArgumentProcessor> pr
this.Output.Error(false, ex.Message);
result = 1;
}
else if(ex is SettingsException)
else if (ex is SettingsException)
{
this.Output.Error(false, ex.Message);
result = 1;
Expand All @@ -246,7 +246,7 @@ private int GetArgumentProcessors(string[] args, out List<IArgumentProcessor> pr
}

// If some argument was invalid, add help argument processor in beginning(i.e. at highest priority)
if(result == 1 && this.showHelp && processors.First<IArgumentProcessor>().Metadata.Value.CommandName != HelpArgumentProcessor.CommandName)
if (result == 1 && this.showHelp && processors.First<IArgumentProcessor>().Metadata.Value.CommandName != HelpArgumentProcessor.CommandName)
{
processors.Insert(0, processorFactory.CreateArgumentProcessor(HelpArgumentProcessor.CommandName));
}
Expand Down Expand Up @@ -381,7 +381,6 @@ private void PrintSplashScreen()
/// <param name="arguments">Arguments provided to perform execution with.</param>
/// <param name="flattenedArguments">Array of flattened arguments.</param>
/// <returns>0 if successful and 1 otherwise.</returns>
/// <see href="https://github.com/dotnet/roslyn/blob/bcdcafc2d407635bc7de205d63d0182e81ef9faa/src/Compilers/Core/Portable/CommandLine/CommonCommandLineParser.cs#L297"/>
private int FlattenArguments(IEnumerable<string> arguments, out string[] flattenedArguments)
{
List<string> outputArguments = new List<string>();
Expand All @@ -393,8 +392,17 @@ private int FlattenArguments(IEnumerable<string> arguments, out string[] flatten
{
// response file:
string path = arg.Substring(1).TrimEnd(null);
result |= ParseResponseFile(path, out var responseFileArguments);
outputArguments.AddRange(responseFileArguments.Reverse());
var hadError = this.ReadArgumentsAndSanitize(path, out var responseFileArgs, out var nestedArgs);

if (hadError)
{
result |= 1;
}
else
{
this.Output.WriteLine(string.Format("vstest.console.exe {0}", responseFileArgs), OutputLevel.Information);
outputArguments.AddRange(nestedArgs);
}
}
else
{
Expand All @@ -407,58 +415,46 @@ private int FlattenArguments(IEnumerable<string> arguments, out string[] flatten
}

/// <summary>
/// Parse a response file into a set of arguments. Errors opening the response file are output as errors.
/// Read and sanitize the arguments.
/// </summary>
/// <param name="fullPath">Full path to the response file.</param>
/// <param name="responseFileArguments">Enumeration of the response file arguments.</param>
/// <param name="fileName">File provided by user.</param>
/// <param name="args">argument in the file as string.</param>
/// <param name="arguments">Modified argument after sanitizing the contents of the file.</param>
/// <returns>0 if successful and 1 otherwise.</returns>
/// <see href="https://github.com/dotnet/roslyn/blob/bcdcafc2d407635bc7de205d63d0182e81ef9faa/src/Compilers/Core/Portable/CommandLine/CommonCommandLineParser.cs#L517"/>
private int ParseResponseFile(string fullPath, out IEnumerable<string> responseFileArguments)
public bool ReadArgumentsAndSanitize(string fileName, out string args, out string[] arguments)
{
int result = 0;
List<string> lines = new List<string>();
try
arguments = null;
if (GetContentUsingFile(fileName, out args))
{
using (var reader = new StreamReader(
new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read),
detectEncodingFromByteOrderMarks: true))
{
string str;
while ((str = reader.ReadLine()) != null)
{
lines.Add(str);
}
}

responseFileArguments = ParseResponseLines(lines);
return true;
}
catch (Exception)

if (string.IsNullOrEmpty(args))
{
this.Output.Error(false, string.Format(CultureInfo.CurrentCulture, CommandLineResources.OpenResponseFileError, fullPath));
responseFileArguments = new string[0];
result = 1;
return false;
}

return result;
return CommandLineUtilities.SplitCommandLineIntoArguments(args, out arguments);
}

/// <summary>
/// Take a string of lines from a response file, remove comments,
/// and split into a set of command line arguments.
/// </summary>
/// <see href="https://github.com/dotnet/roslyn/blob/bcdcafc2d407635bc7de205d63d0182e81ef9faa/src/Compilers/Core/Portable/CommandLine/CommonCommandLineParser.cs#L545"/>
private static IEnumerable<string> ParseResponseLines(IEnumerable<string> lines)
private bool GetContentUsingFile(string fileName, out string contents)
{
List<string> arguments = new List<string>();

foreach (string line in lines)
contents = null;
try
{
contents = File.ReadAllText(fileName);
}
catch (Exception e)
{
arguments.AddRange(CommandLineUtilities.SplitCommandLineIntoArguments(line, removeHashComments: true));
EqtTrace.Verbose("Executor.Execute: Exiting with exit code of {0}", 1);
EqtTrace.Error(string.Format(CultureInfo.InvariantCulture, "Error: Can't open command line argument file '{0}' : '{1}'", fileName, e.Message));
this.Output.Error(false, string.Format(CultureInfo.CurrentCulture, CommandLineResources.OpenResponseFileError, fileName));
return true;
}

return arguments;
return false;
}

#endregion
}
}
}
Loading

0 comments on commit 073c64a

Please sign in to comment.