Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Runtime.InteropServices;

internal static partial class Interop
{
internal static partial class Sys
{
[LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_GetCommandLine", StringMarshalling = StringMarshalling.Utf8, SetLastError = true)]
internal static partial string? GetCommandLine();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2523,6 +2523,9 @@
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.GetProcessPath.cs">
<Link>Common\Interop\Unix\System.Native\Interop.GetProcessPath.cs</Link>
</Compile>
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.GetCommandLine.cs">
<Link>Common\Interop\Unix\System.Native\Interop.GetCommandLine.cs</Link>
</Compile>
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.GetRandomBytes.cs">
<Link>Common\Interop\Unix\System.Native\Interop.GetRandomBytes.cs</Link>
</Compile>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,5 @@ private static OperatingSystem GetOSVersion()
/// </summary>
/// <returns>Path of the executable that started the currently executing process</returns>
private static string? GetProcessPath() => null;

#if !MONO
// This is only used for delegate created from native host
private static string[] GetCommandLineArgsNative() => [];
#endif

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,5 @@ public static string MachineName

[MethodImplAttribute(MethodImplOptions.NoInlining)] // Avoid inlining PInvoke frame into the hot path
private static string? GetProcessPath() => Interop.Sys.GetProcessPath();

private static string[] GetCommandLineArgsNative()
{
// This is only used for delegate created from native host

// Consider to use /proc/self/cmdline to get command line
return [];
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -97,5 +97,9 @@ public static ProcessCpuUsage CpuUsage
/// <summary>Gets the number of milliseconds elapsed since the system started.</summary>
/// <value>A 64-bit signed integer containing the amount of time in milliseconds that has passed since the last time the computer was started.</value>
public static long TickCount64 => Interop.Sys.GetLowResolutionTimestamp();

private static unsafe string[] GetCommandLineArgsNative() =>
// This is only used for delegate created from native host
SegmentCommandLine(Interop.Sys.GetCommandLine());
}
}
144 changes: 6 additions & 138 deletions src/libraries/System.Private.CoreLib/src/System/Environment.Windows.cs
Original file line number Diff line number Diff line change
Expand Up @@ -226,146 +226,14 @@ private static unsafe string[] GetCommandLineArgsNative()
char* lpCmdLine = Interop.Kernel32.GetCommandLine();
Debug.Assert(lpCmdLine != null);

return SegmentCommandLine(lpCmdLine);
}

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
// contribute to character_count.

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

do
{
if (*p == '"')
{
inQuotes = !inQuotes;
c = *p++;
continue;
}

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

if (c == '\0')
{
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;
}
}
// Determine length of the null-terminated native string
int len = 0;
while (lpCmdLine[len] != '\0') len++;

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());
}
// Construct a ReadOnlySpan<char> from the raw pointer
ReadOnlySpan<char> cmdLineSpan = new ReadOnlySpan<char>(lpCmdLine, len);

return arrayBuilder.ToArray();
return SegmentCommandLine(cmdLineSpan);
}

/// <summary>
Expand Down
145 changes: 145 additions & 0 deletions src/libraries/System.Private.CoreLib/src/System/Environment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Text;

namespace System
{
Expand Down Expand Up @@ -277,5 +279,148 @@ private static void ValidateVariable(string variable)
if (variable.Contains('='))
throw new ArgumentException(SR.Argument_IllegalEnvVarName, nameof(variable));
}

private static string[] SegmentCommandLine(ReadOnlySpan<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

int p = 0;
int length = cmdLine.Length;

// 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
// contribute to character_count.

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

do
{
char current = p < length ? cmdLine[p] : '\0';
if (current == '"')
{
inQuotes = !inQuotes;
c = p < length ? cmdLine[p++] : '\0';
continue;
}

c = p < length ? cmdLine[p++] : '\0';
stringBuilder.Append(c);
}
while (c != '\0' && (inQuotes || (c is not (' ' or '\t'))));

if (c == '\0')
{
p--;
}

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

// loop on each argument
while (true)
{
char currentP = p < length ? cmdLine[p] : '\0';
if (currentP != '\0')
{
while ((p < length ? cmdLine[p] : '\0') is ' ' or '\t')
{
++p;
}
}

if ((p < length ? cmdLine[p] : '\0') == '\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 < length ? cmdLine[p] : '\0') == '\\')
{
// Count number of backslashes for use below
++p;
++numSlash;
}

if ((p < length ? cmdLine[p] : '\0') == '"')
{
// if 2N backslashes before, start / end quote, otherwise
// copy literally:
if (numSlash % 2 == 0)
{
if (inQuotes && (p + 1 < length ? cmdLine[p + 1] : '\0') == '"')
{
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:
char endCheckChar = p < length ? cmdLine[p] : '\0';
if (endCheckChar == '\0' || (!inQuotes && endCheckChar is ' ' or '\t'))
{
break;
}

// Copy character into argument:
if (copyChar)
{
stringBuilder.Append(p < length ? cmdLine[p] : '\0');
}

++p;
}

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

return arrayBuilder.ToArray();
}
}
}
Loading
Loading