Repository navigation
Expand file tree
/
Copy pathPosixArgumentSplitter.cs
More file actions
140 lines (126 loc) · 4.86 KB
/
Copy pathPosixArgumentSplitter.cs
File metadata and controls
140 lines (126 loc) · 4.86 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
using System;
using System.Collections.Generic;
using System.Text;
namespace MissionController.Core;
/// <summary>
/// Splits a shell-style argument string into an argument vector.
///
/// This exists because the guest daemon used to do the job with
/// <c>arguments.split_whitespace()</c>, which is not a parse but a guess. MCP's <c>run_code</c>
/// shipped <c>-c '<code>'</c> and the split handed python the literal <c>'print(123)'</c> - a
/// string expression that evaluates, prints nothing and exits 0, so a caller asking to print a
/// number got a successful run with no output. Any argument containing a space tore in half:
/// <c>print(1, 2)</c> arrived as <c>'print(1,</c> and died on an unterminated string literal.
///
/// Splitting belongs on this side of the wire, where the original text is still intact and the
/// result can be sent as an explicit argv array. It is deliberately NOT a shell: no variable
/// expansion, no globbing, no command substitution, no operators. Those would be new capabilities
/// smuggled in through a parser, and a caller that wants a shell can name one as its command.
/// </summary>
public static class PosixArgumentSplitter
{
/// <summary>
/// Tokenises <paramref name="arguments"/> the way a POSIX shell splits a simple command.
///
/// Handles single quotes (fully literal), double quotes (backslash escapes only before
/// <c>" \ $ `</c>), and backslash escapes outside quotes. An unterminated quote is an error
/// rather than a best guess: the caller's intent is genuinely unknown at that point, and
/// guessing is what produced the defect this replaces.
/// </summary>
/// <exception cref="FormatException">The string ends inside a quote or on a trailing backslash.</exception>
public static IReadOnlyList<string> Split(string? arguments)
{
var argv = new List<string>();
if (string.IsNullOrEmpty(arguments))
{
return argv;
}
var current = new StringBuilder();
bool inArgument = false;
char quote = '\0';
for (int i = 0; i < arguments.Length; i++)
{
char c = arguments[i];
if (quote == '\'')
{
// Single quotes are fully literal - not even a backslash escapes inside them.
if (c == '\'')
{
quote = '\0';
}
else
{
current.Append(c);
}
continue;
}
if (quote == '"')
{
if (c == '\\' && i + 1 < arguments.Length && IsDoubleQuoteEscapable(arguments[i + 1]))
{
current.Append(arguments[++i]);
}
else if (c == '"')
{
quote = '\0';
}
else
{
current.Append(c);
}
continue;
}
switch (c)
{
case '\'':
case '"':
quote = c;
// An opening quote starts an argument even if it turns out to be empty, so
// `''` is one empty argument rather than none.
inArgument = true;
break;
case '\\':
if (i + 1 >= arguments.Length)
{
throw new FormatException(
"Argument string ends with a trailing backslash; the intended argument is ambiguous.");
}
current.Append(arguments[++i]);
inArgument = true;
break;
case ' ':
case '\t':
case '\n':
case '\r':
if (inArgument)
{
argv.Add(current.ToString());
current.Clear();
inArgument = false;
}
break;
default:
current.Append(c);
inArgument = true;
break;
}
}
if (quote != '\0')
{
throw new FormatException(
$"Argument string ends inside a {(quote == '\'' ? "single" : "double")}-quoted section.");
}
if (inArgument)
{
argv.Add(current.ToString());
}
return argv;
}
/// <summary>
/// Inside double quotes a backslash only escapes these; before anything else it is literal.
/// That is the POSIX rule, and getting it wrong would silently eat backslashes out of a
/// Windows path or a regex.
/// </summary>
private static bool IsDoubleQuoteEscapable(char c) =>
c is '"' or '\\' or '$' or '`';
}