Skip to content

Commit

Permalink
Implement managed SegmentCommandLine (dotnet#82883)
Browse files Browse the repository at this point in the history
* Implement managed version of SegmentCommandLine

* Remove P/Invoke for CommandLineToArgv

* Update parsing to latest UCRT

* Add test and fix trailing space in command

* Fix test cases
  • Loading branch information
huoyaoyuan authored and radical committed Mar 4, 2023
1 parent dac9582 commit 80d3107
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 30 deletions.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -1938,9 +1938,6 @@
<Compile Include="$(CommonPath)Interop\Windows\Secur32\Interop.GetUserNameExW.cs">
<Link>Common\Interop\Windows\Secur32\Interop.GetUserNameExW.cs</Link>
</Compile>
<Compile Include="$(CommonPath)Interop\Windows\Shell32\Interop.CommandLineToArgv.cs">
<Link>Common\Interop\Windows\Shell32\Interop.CommandLineToArgv.cs</Link>
</Compile>
<Compile Include="$(CommonPath)Interop\Windows\Shell32\Interop.SHGetKnownFolderPath.cs">
<Link>Common\Interop\Windows\Shell32\Interop.SHGetKnownFolderPath.cs</Link>
</Compile>
Expand Down Expand Up @@ -2577,4 +2574,4 @@
<Compile Include="$(MSBuildThisFileDirectory)System\Numerics\IUnaryPlusOperators.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Numerics\IUnsignedNumber.cs" />
</ItemGroup>
</Project>
</Project>
147 changes: 134 additions & 13 deletions src/libraries/System.Private.CoreLib/src/System/Environment.Windows.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.Win32.SafeHandles;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
Expand Down Expand Up @@ -216,26 +217,146 @@ private static unsafe string[] GetCommandLineArgsNative()
char* lpCmdLine = Interop.Kernel32.GetCommandLine();
Debug.Assert(lpCmdLine != null);

int numArgs = 0;
char** argvW = Interop.Shell32.CommandLineToArgv(lpCmdLine, &numArgs);
if (argvW == null)
{
ThrowHelper.ThrowOutOfMemoryException();
}
return SegmentCommandLine(lpCmdLine);
}

try
private static unsafe string[] SegmentCommandLine(char* cmdLine)
{
// Parse command line arguments using the rules documented at
// https://learn.microsoft.com/cpp/cpp/main-function-command-line-args#parsing-c-command-line-arguments

// CommandLineToArgvW API cannot be used here since
// it has slightly different behavior.

ArrayBuilder<string> arrayBuilder = default;

Span<char> stringBuffer = stackalloc char[260]; // Use MAX_PATH for a typical maximum
scoped ValueStringBuilder stringBuilder;

char c;

// First scan the program name, copy it, and count the bytes

char* p = cmdLine;

// A quoted program name is handled here. The handling is much
// simpler than for other arguments. Basically, whatever lies
// between the leading double-quote and next one, or a terminal null
// character is simply accepted. Fancier handling is not required
// because the program name must be a legal NTFS/HPFS file name.
// Note that the double-quote characters are not copied, nor do they
// contribyte to character_count.

bool inQuotes = false;
stringBuilder = new ValueStringBuilder(stringBuffer);

do
{
string[] result = new string[numArgs];
for (int i = 0; i < result.Length; i++)
if (*p == '"')
{
result[i] = new string(*(argvW + i));
inQuotes = !inQuotes;
c = *p++;
continue;
}
return result;

c = *p++;
stringBuilder.Append(c);
}
finally
while (c != '\0' && (inQuotes || (c is not (' ' or '\t'))));

if (c == '\0')
{
Interop.Kernel32.LocalFree((IntPtr)argvW);
p--;
}

stringBuilder.Length--;
arrayBuilder.Add(stringBuilder.ToString());
inQuotes = false;

// loop on each argument
while (true)
{
if (*p != '\0')
{
while (*p is ' ' or '\t')
{
++p;
}
}

if (*p == '\0')
{
// end of args
break;
}

// scan an argument
stringBuilder = new ValueStringBuilder(stringBuffer);

// loop through scanning one argument
while (true)
{
bool copyChar = true;

// Rules:
// 2N backslashes + " ==> N backslashes and begin/end quote
// 2N+1 backslashes + " ==> N backslashes + literal "
// N backslashes ==> N backslashes
int numSlash = 0;

while (*p == '\\')
{
// Count number of backslashes for use below
++p;
++numSlash;
}

if (*p == '"')
{
// if 2N backslashes before, start / end quote, otherwise
// copy literally:
if (numSlash % 2 == 0)
{
if (inQuotes && p[1] == '"')
{
p++; // Double quote inside quoted string
}
else
{
// Skip first quote char and copy second:
copyChar = false; // Don't copy quote
inQuotes = !inQuotes;
}
}

numSlash /= 2;
}

// Copy slashes:
while (numSlash-- > 0)
{
stringBuilder.Append('\\');
}

// If at end of arg, break loop:
if (*p == '\0' || (!inQuotes && *p is ' ' or '\t'))
{
break;
}

// Copy character into argument:
if (copyChar)
{
stringBuilder.Append(*p);
}

++p;
}

arrayBuilder.Add(stringBuilder.ToString());
}

return arrayBuilder.ToArray();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -103,5 +103,30 @@ public static int CheckCommandLineArgsFallback()

return RemoteExecutor.SuccessExitCode;
}

public static bool IsWindowsCoreCLRJit
=> PlatformDetection.IsWindows
&& PlatformDetection.IsNotMonoRuntime
&& PlatformDetection.IsNotNativeAot;

[ConditionalTheory(typeof(GetCommandLineArgs), nameof(IsWindowsCoreCLRJit))]
[InlineData(@"cmd ""abc"" d e", new[] { "cmd", "abc", "d", "e" })]
[InlineData(@"cmd a\\b d""e f""g h", new[] { "cmd", @"a\\b", "de fg", "h" })]
[InlineData(@"cmd a\\\""b c d", new[] { "cmd", @"a\""b", "c", "d" })]
[InlineData(@"cmd a\\\\""b c"" d e", new[] { "cmd", @"a\\b c", "d", "e" })]
[InlineData(@"cmd a""b"""" c d", new[] { "cmd", @"ab"" c d" })]
[InlineData(@"X:\No""t A"""" P""ath arg", new[] { @"X:\Not A Path", "arg" })]
[InlineData(@"""\\Some Server\cmd"" ""arg", new[] { @"\\Some Server\cmd", "arg" })]
public static unsafe void CheckCommandLineParser(string cmdLine, string[] args)
{
var method = typeof(Environment).GetMethod("SegmentCommandLine", BindingFlags.Static | BindingFlags.NonPublic);
Assert.NotNull(method);

var span = cmdLine.AsSpan(); // Workaround
fixed (char* p = span)
{
Assert.Equal(args, method.Invoke(null, new object[] { Pointer.Box(p, typeof(char*)) }));
}
}
}
}

0 comments on commit 80d3107

Please sign in to comment.