| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -6,15 +6,14 @@ | |||
| 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 | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -14,6 +14,8 @@ | |||
| 14 | 14 | namespace node { | |
| 15 | 15 | namespace task_runner { | |
| 16 | 16 | ||
| 17 | + using PositionalArgs = std::vector<std::string_view>; | ||
| 18 | + | ||
| 17 | 19 | // ProcessRunner is the class responsible for running a process. | |
| 18 | 20 | // A class instance is created for each process to be run. | |
| 19 | 21 | // The class is responsible for spawning the process and handling its exit. | |
@@ -22,7 +24,7 @@ class ProcessRunner { | |||
| 22 | 24 | public: | |
| 23 | 25 | ProcessRunner(std::shared_ptr<InitializationResultImpl> result, | |
| 24 | 26 | std::string_view command_id, | |
| 25 | - const std::optional<std::string>& positional_args); | ||
| 27 | + const PositionalArgs& positional_args); | ||
| 26 | 28 | void Run(); | |
| 27 | 29 | static void ExitCallback(uv_process_t* req, | |
| 28 | 30 | int64_t exit_status, | |
@@ -51,10 +53,9 @@ class ProcessRunner { | |||
| 51 | 53 | ||
| 52 | 54 | void RunTask(std::shared_ptr<InitializationResultImpl> result, | |
| 53 | 55 | std::string_view command_id, | |
| 54 | - const std::optional<std::string>& positional_args); | ||
| 55 | - std::optional<std::string> GetPositionalArgs( | ||
| 56 | - const std::vector<std::string>& args); | ||
| 57 | - std::string EscapeShell(const std::string& command); | ||
| 56 | + const PositionalArgs& positional_args); | ||
| 57 | + PositionalArgs GetPositionalArgs(const std::vector<std::string>& args); | ||
| 58 | + std::string EscapeShell(const std::string_view command); | ||
| 58 | 59 | ||
| 59 | 60 | } // namespace task_runner | |
| 60 | 61 | } // namespace node | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -9,6 +9,20 @@ class TaskRunnerTest : public EnvironmentTestFixture {}; | |||
| 9 | 9 | ||
| 10 | 10 | TEST_F(TaskRunnerTest, EscapeShell) { | |
| 11 | 11 | std::vector<std::pair<std::string, std::string>> expectations = { | |
| 12 | + #ifdef _WIN32 | ||
| 13 | + {"", "\"\""}, | ||
| 14 | + {"test", "test"}, | ||
| 15 | + {"test words", "\"test words\""}, | ||
| 16 | + {"$1", "\"$1\""}, | ||
| 17 | + {"\"$1\"", "\"\"\"$1\"\"\""}, | ||
| 18 | + {"'$1'", "\"'$1'\""}, | ||
| 19 | + {"\\$1", "\"\\$1\""}, | ||
| 20 | + {"--arg=\"$1\"", "\"--arg=\"\"$1\"\"\""}, | ||
| 21 | + {"--arg=node exec -c \"$1\"", "\"--arg=node exec -c \"\"$1\"\"\""}, | ||
| 22 | + {"--arg=node exec -c '$1'", "\"--arg=node exec -c '$1'\""}, | ||
| 23 | + {"'--arg=node exec -c \"$1\"'", "\"'--arg=node exec -c \"\"$1\"\"'\""} | ||
| 24 | + | ||
| 25 | + #else | ||
| 12 | 26 | {"", "''"}, | |
| 13 | 27 | {"test", "test"}, | |
| 14 | 28 | {"test words", "'test words'"}, | |
@@ -19,7 +33,9 @@ TEST_F(TaskRunnerTest, EscapeShell) { | |||
| 19 | 33 | {"--arg=\"$1\"", "'--arg=\"$1\"'"}, | |
| 20 | 34 | {"--arg=node exec -c \"$1\"", "'--arg=node exec -c \"$1\"'"}, | |
| 21 | 35 | {"--arg=node exec -c '$1'", "'--arg=node exec -c \\'$1\\''"}, | |
| 22 | - {"'--arg=node exec -c \"$1\"'", "'\\'--arg=node exec -c \"$1\"\\''"}}; | ||
| 36 | + {"'--arg=node exec -c \"$1\"'", "'\\'--arg=node exec -c \"$1\"\\''"} | ||
| 37 | + #endif | ||
| 38 | + }; | ||
| 23 | 39 | ||
| 24 | 40 | for (const auto& [input, expected] : expectations) { | |
| 25 | 41 | EXPECT_EQ(node::task_runner::EscapeShell(input), expected); | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -57,10 +57,15 @@ describe('node run [command]', () => { | |||
| 57 | 57 | it('appends positional arguments', async () => { | |
| 58 | 58 | const child = await common.spawnPromisified( | |
| 59 | 59 | process.execPath, | |
| 60 | - [ '--no-warnings', '--run', `positional-args${envSuffix}`, '--', '--help "hello world test"'], | ||
| 60 | + [ '--no-warnings', '--run', `positional-args${envSuffix}`, '--', '--help "hello world test"', 'A', 'B', 'C'], | ||
| 61 | 61 | { cwd: fixtures.path('run-script') }, | |
| 62 | 62 | ); | |
| 63 | - assert.match(child.stdout, /--help "hello world test"/); | ||
| 63 | + if (common.isWindows) { | ||
| 64 | + assert.match(child.stdout, /Arguments: '--help ""hello world test"" A B C'/); | ||
| 65 | + } else { | ||
| 66 | + assert.match(child.stdout, /Arguments: '--help "hello world test" A B C'/); | ||
| 67 | + } | ||
| 68 | + assert.match(child.stdout, /The total number of arguments are: 4/); | ||
| 64 | 69 | assert.strictEqual(child.stderr, ''); | |
| 65 | 70 | assert.strictEqual(child.code, 0); | |
| 66 | 71 | }); | |
| Back | FazBrowse Home | New Git URL |
0 commit comments