-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathCommandLineTokenizer.cs
More file actions
162 lines (145 loc) · 5.91 KB
/
Copy pathCommandLineTokenizer.cs
File metadata and controls
162 lines (145 loc) · 5.91 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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
using System.Text;
namespace MSBuild.CompileCommands.Extractor
{
/// <summary>
/// Tokenizes command line strings using Windows CommandLineToArgvW semantics.
/// Handles quoted strings, escaped quotes, backslash rules, and @response files.
/// </summary>
public static class CommandLineTokenizer
{
/// <summary>
/// Tokenizes a command line string using Windows CommandLineToArgvW semantics.
/// </summary>
public static List<string> Tokenize(string commandLine)
{
var tokens = new List<string>();
if (string.IsNullOrWhiteSpace(commandLine))
return tokens;
int i = 0;
while (i < commandLine.Length)
{
while (i < commandLine.Length && char.IsWhiteSpace(commandLine[i]))
i++;
if (i >= commandLine.Length)
break;
tokens.Add(ParseToken(commandLine, ref i));
}
return tokens;
}
private static string ParseToken(string commandLine, ref int i)
{
var token = new StringBuilder();
bool inQuotes = false;
while (i < commandLine.Length && (inQuotes || !char.IsWhiteSpace(commandLine[i])))
{
int numBackslashes = 0;
while (i < commandLine.Length && commandLine[i] == '\\')
{
i++;
numBackslashes++;
}
if (i < commandLine.Length && commandLine[i] == '"')
{
// Backslashes followed by a double quote:
// - Even count: half become literal backslashes, quote toggles mode
// - Odd count: half become literal backslashes, last one escapes the quote
token.Append('\\', numBackslashes / 2);
if (numBackslashes % 2 == 0)
{
// "" inside a quoted region produces a literal " (CommandLineToArgvW behavior)
if (inQuotes && i + 1 < commandLine.Length && commandLine[i + 1] == '"')
{
token.Append('"');
i += 2;
}
else
{
inQuotes = !inQuotes;
i++;
}
}
else
{
token.Append('"');
i++;
}
}
else
{
// Backslashes not followed by a quote are literal
token.Append('\\', numBackslashes);
if (i < commandLine.Length && (inQuotes || !char.IsWhiteSpace(commandLine[i])))
{
token.Append(commandLine[i]);
i++;
}
}
}
return token.ToString();
}
/// <summary>
/// Tokenizes a command line and expands @response_file references by
/// reading and recursively tokenizing their contents.
/// </summary>
public static List<string> TokenizeWithResponseFiles(string commandLine, string? workingDirectory = null)
{
return TokenizeWithResponseFilesCore(commandLine, workingDirectory, new HashSet<string>(StringComparer.OrdinalIgnoreCase), 0);
}
private const int MaxResponseFileDepth = 10;
private static List<string> TokenizeWithResponseFilesCore(string commandLine, string? workingDirectory, HashSet<string> visitedFiles, int depth)
{
var tokens = Tokenize(commandLine);
var expanded = new List<string>();
foreach (var token in tokens)
{
if (token.StartsWith('@') && token.Length > 1)
{
var filePath = token[1..];
if (!Path.IsPathRooted(filePath) && workingDirectory != null)
filePath = Path.Combine(workingDirectory, filePath);
string normalizedPath;
try
{
normalizedPath = Path.GetFullPath(filePath);
}
catch (Exception)
{
// Treat as literal token on malformed path
expanded.Add(token);
continue;
}
if (depth >= MaxResponseFileDepth || visitedFiles.Contains(normalizedPath))
{
// Cycle detected or max depth exceeded; treat as literal token
expanded.Add(token);
continue;
}
if (File.Exists(normalizedPath))
{
try
{
visitedFiles.Add(normalizedPath);
var content = File.ReadAllText(normalizedPath);
expanded.AddRange(TokenizeWithResponseFilesCore(content, Path.GetDirectoryName(normalizedPath), visitedFiles, depth + 1));
visitedFiles.Remove(normalizedPath);
}
catch (Exception)
{
// Treat as literal token on any failure (malformed path, access denied, etc.)
expanded.Add(token);
}
}
else
{
expanded.Add(token);
}
}
else
{
expanded.Add(token);
}
}
return expanded;
}
}
}