|
6 | 6 | namespace node::task_runner { |
7 | 7 |
|
8 | 8 | #ifdef _WIN32 |
9 | | -static constexpr char bin_path[] = "\\node_modules\\.bin"; |
| 9 | +static constexpr const char* bin_path = "\\node_modules\\.bin"; |
10 | 10 | #else |
11 | | -static constexpr char bin_path[] = "/node_modules/.bin"; |
| 11 | +static constexpr const char* bin_path = "/node_modules/.bin"; |
12 | 12 | #endif // _WIN32 |
13 | 13 |
|
14 | | -ProcessRunner::ProcessRunner( |
15 | | - std::shared_ptr<InitializationResultImpl> result, |
16 | | - std::string_view command, |
17 | | - const std::optional<std::string>& positional_args) { |
| 14 | +ProcessRunner::ProcessRunner(std::shared_ptr<InitializationResultImpl> result, |
| 15 | + std::string_view command, |
| 16 | + const PositionalArgs& positional_args) { |
18 | 17 | memset(&options_, 0, sizeof(uv_process_options_t)); |
19 | 18 |
|
20 | 19 | // Get the current working directory. |
@@ -54,10 +53,6 @@ ProcessRunner::ProcessRunner( |
54 | 53 |
|
55 | 54 | std::string command_str(command); |
56 | 55 |
|
57 | | - if (positional_args.has_value()) { |
58 | | - command_str += " " + EscapeShell(positional_args.value()); |
59 | | - } |
60 | | - |
61 | 56 | // Set environment variables |
62 | 57 | uv_env_item_t* env_items; |
63 | 58 | int env_count; |
@@ -69,33 +64,45 @@ ProcessRunner::ProcessRunner( |
69 | 64 | // ProcessRunner instance. |
70 | 65 | for (int i = 0; i < env_count; i++) { |
71 | 66 | std::string name = env_items[i].name; |
72 | | - std::string value = env_items[i].value; |
| 67 | + auto value = env_items[i].value; |
73 | 68 |
|
74 | 69 | #ifdef _WIN32 |
75 | 70 | // We use comspec environment variable to find cmd.exe path on Windows |
76 | 71 | // Example: 'C:\\Windows\\system32\\cmd.exe' |
77 | 72 | // If we don't find it, we fallback to 'cmd.exe' for Windows |
78 | | - if (name.size() == 7 && StringEqualNoCaseN(name.c_str(), "comspec", 7)) { |
| 73 | + if (StringEqualNoCase(name.c_str(), "comspec")) { |
79 | 74 | file_ = value; |
80 | 75 | } |
81 | 76 | #endif // _WIN32 |
82 | 77 |
|
83 | 78 | // Check if environment variable key is matching case-insensitive "path" |
84 | | - if (name.size() == 4 && StringEqualNoCaseN(name.c_str(), "path", 4)) { |
85 | | - value.insert(0, current_bin_path); |
| 79 | + if (StringEqualNoCase(name.c_str(), "path")) { |
| 80 | + env_vars_.push_back(name + "=" + current_bin_path + value); |
| 81 | + } else { |
| 82 | + // Environment variables should be in "KEY=value" format |
| 83 | + env_vars_.push_back(name + "=" + value); |
86 | 84 | } |
87 | | - |
88 | | - // Environment variables should be in "KEY=value" format |
89 | | - value.insert(0, name + "="); |
90 | | - env_vars_.push_back(value); |
91 | 85 | } |
92 | 86 | uv_os_free_environ(env_items, env_count); |
93 | 87 |
|
94 | 88 | // Use the stored reference on the instance. |
95 | 89 | options_.file = file_.c_str(); |
96 | 90 |
|
| 91 | + // Add positional arguments to the command string. |
| 92 | + // Note that each argument needs to be escaped. |
| 93 | + if (!positional_args.empty()) { |
| 94 | + for (const auto& arg : positional_args) { |
| 95 | + command_str += " " + EscapeShell(arg); |
| 96 | + } |
| 97 | + } |
| 98 | + |
97 | 99 | #ifdef _WIN32 |
98 | | - if (file_.find("cmd.exe") != std::string::npos) { |
| 100 | + // We check whether file_ ends with cmd.exe in a case-insensitive manner. |
| 101 | + // C++20 provides ends_with, but we roll our own for compatibility. |
| 102 | + const char* cmdexe = "cmd.exe"; |
| 103 | + if (file_.size() >= strlen(cmdexe) && |
| 104 | + StringEqualNoCase(cmdexe, |
| 105 | + file_.c_str() + file_.size() - strlen(cmdexe))) { |
99 | 106 | // If the file is cmd.exe, use the following command line arguments: |
100 | 107 | // "/c" Carries out the command and exit. |
101 | 108 | // "/d" Disables execution of AutoRun commands. |
@@ -104,6 +111,9 @@ ProcessRunner::ProcessRunner( |
104 | 111 | command_args_ = { |
105 | 112 | options_.file, "/d", "/s", "/c", "\"" + command_str + "\""}; |
106 | 113 | } else { |
| 114 | + // If the file is not cmd.exe, and it is unclear wich shell is being used, |
| 115 | + // so assume -c is the correct syntax (Unix-like shells use -c for this |
| 116 | + // purpose). |
107 | 117 | command_args_ = {options_.file, "-c", command_str}; |
108 | 118 | } |
109 | 119 | #else |
@@ -126,12 +136,19 @@ ProcessRunner::ProcessRunner( |
126 | 136 | } |
127 | 137 |
|
128 | 138 | // EscapeShell escapes a string to be used as a command line argument. |
| 139 | +// Under Windows, we follow: |
| 140 | +// https://daviddeley.com/autohotkey/parameters/parameters.htm |
| 141 | +// Elsewhere: |
129 | 142 | // It replaces single quotes with "\\'" and double quotes with "\\\"". |
130 | 143 | // It also removes excessive quote pairs and handles edge cases. |
131 | | -std::string EscapeShell(const std::string& input) { |
| 144 | +std::string EscapeShell(const std::string_view input) { |
132 | 145 | // If the input is an empty string, return a pair of quotes |
133 | 146 | if (input.empty()) { |
| 147 | +#ifdef _WIN32 |
| 148 | + return "\"\""; |
| 149 | +#else |
134 | 150 | return "''"; |
| 151 | +#endif |
135 | 152 | } |
136 | 153 |
|
137 | 154 | static const std::string_view forbidden_characters = |
@@ -140,21 +157,32 @@ std::string EscapeShell(const std::string& input) { |
140 | 157 | // Check if input contains any forbidden characters |
141 | 158 | // If it doesn't, return the input as is. |
142 | 159 | if (input.find_first_of(forbidden_characters) == std::string::npos) { |
143 | | - return input; |
| 160 | + return std::string(input); |
144 | 161 | } |
145 | 162 |
|
146 | | - // Replace single quotes("'") with "\\'" |
147 | | - std::string escaped = std::regex_replace(input, std::regex("'"), "\\'"); |
| 163 | + static const std::regex leadingQuotePairs("^(?:'')+(?!$)"); |
148 | 164 |
|
149 | | - // Wrap the result in single quotes |
| 165 | +#ifdef _WIN32 |
| 166 | + // Replace double quotes with single quotes and surround the string |
| 167 | + // with double quotes for Windows. |
| 168 | + std::string escaped = |
| 169 | + std::regex_replace(std::string(input), std::regex("\""), "\"\""); |
| 170 | + escaped = "\"" + escaped + "\""; |
| 171 | + // Remove excessive quote pairs and handle edge cases |
| 172 | + static const std::regex tripleSingleQuote("\\\\\"\"\""); |
| 173 | + escaped = std::regex_replace(escaped, leadingQuotePairs, ""); |
| 174 | + escaped = std::regex_replace(escaped, tripleSingleQuote, "\\\""); |
| 175 | +#else |
| 176 | + // Replace single quotes("'") with "\\'" and wrap the result |
| 177 | + // in single quotes. |
| 178 | + std::string escaped = |
| 179 | + std::regex_replace(std::string(input), std::regex("'"), "\\'"); |
150 | 180 | escaped = "'" + escaped + "'"; |
151 | | - |
152 | 181 | // Remove excessive quote pairs and handle edge cases |
153 | | - static const std::regex leadingQuotePairs("^(?:'')+(?!$)"); |
154 | 182 | static const std::regex tripleSingleQuote("\\\\'''"); |
155 | | - |
156 | 183 | escaped = std::regex_replace(escaped, leadingQuotePairs, ""); |
157 | 184 | escaped = std::regex_replace(escaped, tripleSingleQuote, "\\'"); |
| 185 | +#endif // _WIN32 |
158 | 186 |
|
159 | 187 | return escaped; |
160 | 188 | } |
@@ -188,7 +216,7 @@ void ProcessRunner::Run() { |
188 | 216 |
|
189 | 217 | void RunTask(std::shared_ptr<InitializationResultImpl> result, |
190 | 218 | std::string_view command_id, |
191 | | - const std::optional<std::string>& positional_args) { |
| 219 | + const std::vector<std::string_view>& positional_args) { |
192 | 220 | std::string_view path = "package.json"; |
193 | 221 | std::string raw_json; |
194 | 222 |
|
@@ -256,20 +284,21 @@ void RunTask(std::shared_ptr<InitializationResultImpl> result, |
256 | 284 | // If the "--" flag is not found, it returns an empty optional. |
257 | 285 | // Otherwise, it returns the positional arguments as a single string. |
258 | 286 | // Example: "node -- script.js arg1 arg2" returns "arg1 arg2". |
259 | | -std::optional<std::string> GetPositionalArgs( |
260 | | - const std::vector<std::string>& args) { |
| 287 | +PositionalArgs GetPositionalArgs(const std::vector<std::string>& args) { |
261 | 288 | // If the "--" flag is not found, return an empty optional |
262 | 289 | // Otherwise, return the positional arguments as a single string |
263 | 290 | if (auto dash_dash = std::find(args.begin(), args.end(), "--"); |
264 | 291 | dash_dash != args.end()) { |
265 | | - std::string positional_args; |
| 292 | + PositionalArgs positional_args{}; |
266 | 293 | for (auto it = dash_dash + 1; it != args.end(); ++it) { |
267 | | - positional_args += it->c_str(); |
| 294 | + // SAFETY: The following code is safe because the lifetime of the |
| 295 | + // arguments is guaranteed to be valid until the end of the task runner. |
| 296 | + positional_args.push_back(std::string_view(it->c_str(), it->size())); |
268 | 297 | } |
269 | 298 | return positional_args; |
270 | 299 | } |
271 | 300 |
|
272 | | - return std::nullopt; |
| 301 | + return {}; |
273 | 302 | } |
274 | 303 |
|
275 | 304 | } // namespace node::task_runner |