| ... | @@ -744,9 +744,6 @@ pub const ChildProcess = struct { | ... | @@ -744,9 +744,6 @@ pub const ChildProcess = struct { |
| 744 | windowsDestroyPipe(g_hChildStd_ERR_Rd, g_hChildStd_ERR_Wr); | 744 | windowsDestroyPipe(g_hChildStd_ERR_Rd, g_hChildStd_ERR_Wr); |
| 745 | }; | 745 | }; |
| 746 | | 746 | |
| 747 | const cmd_line = try windowsCreateCommandLine(self.allocator, self.argv); | | |
| 748 | defer self.allocator.free(cmd_line); | | |
| 749 | | | |
| 750 | var siStartInfo = windows.STARTUPINFOW{ | 747 | var siStartInfo = windows.STARTUPINFOW{ |
| 751 | .cb = @sizeOf(windows.STARTUPINFOW), | 748 | .cb = @sizeOf(windows.STARTUPINFOW), |
| 752 | .hStdError = g_hChildStd_ERR_Wr, | 749 | .hStdError = g_hChildStd_ERR_Wr, |
| ... | @@ -818,7 +815,11 @@ pub const ChildProcess = struct { | ... | @@ -818,7 +815,11 @@ pub const ChildProcess = struct { |
| 818 | const app_name_w = try unicode.utf8ToUtf16LeWithNull(self.allocator, app_basename_utf8); | 815 | const app_name_w = try unicode.utf8ToUtf16LeWithNull(self.allocator, app_basename_utf8); |
| 819 | defer self.allocator.free(app_name_w); | 816 | defer self.allocator.free(app_name_w); |
| 820 | | 817 | |
| 821 | const cmd_line_w = try unicode.utf8ToUtf16LeWithNull(self.allocator, cmd_line); | 818 | const cmd_line_w = argvToCommandLineWindows(self.allocator, self.argv) catch |err| switch (err) { |
| | 819 | // argv[0] contains unsupported characters that will never resolve to a valid exe. |
| | 820 | error.InvalidArg0 => return error.FileNotFound, |
| | 821 | else => |e| return e, |
| | 822 | }; |
| 822 | defer self.allocator.free(cmd_line_w); | 823 | defer self.allocator.free(cmd_line_w); |
| 823 | | 824 | |
| 824 | run: { | 825 | run: { |
| ... | @@ -1236,39 +1237,159 @@ test "windowsCreateProcessSupportsExtension" { | ... | @@ -1236,39 +1237,159 @@ test "windowsCreateProcessSupportsExtension" { |
| 1236 | try std.testing.expect(windowsCreateProcessSupportsExtension(&[_]u16{ '.', 'e', 'X', 'e', 'c' }) == null); | 1237 | try std.testing.expect(windowsCreateProcessSupportsExtension(&[_]u16{ '.', 'e', 'X', 'e', 'c' }) == null); |
| 1237 | } | 1238 | } |
| 1238 | | 1239 | |
| 1239 | /// Caller must dealloc. | 1240 | pub const ArgvToCommandLineError = error{ OutOfMemory, InvalidUtf8, InvalidArg0 }; |
| 1240 | fn windowsCreateCommandLine(allocator: mem.Allocator, argv: []const []const u8) ![:0]u8 { | 1241 | |
| | 1242 | /// Serializes `argv` to a Windows command-line string suitable for passing to a child process and |
| | 1243 | /// parsing by the `CommandLineToArgvW` algorithm. The caller owns the returned slice. |
| | 1244 | pub fn argvToCommandLineWindows( |
| | 1245 | allocator: mem.Allocator, |
| | 1246 | argv: []const []const u8, |
| | 1247 | ) ArgvToCommandLineError![:0]u16 { |
| 1241 | var buf = std.ArrayList(u8).init(allocator); | 1248 | var buf = std.ArrayList(u8).init(allocator); |
| 1242 | defer buf.deinit(); | 1249 | defer buf.deinit(); |
| 1243 | | 1250 | |
| 1244 | for (argv, 0..) |arg, arg_i| { | 1251 | if (argv.len != 0) { |
| 1245 | if (arg_i != 0) try buf.append(' '); | 1252 | const arg0 = argv[0]; |
| 1246 | if (mem.indexOfAny(u8, arg, " \t\n\"") == null) { | 1253 | |
| 1247 | try buf.appendSlice(arg); | 1254 | // The first argument must be quoted if it contains spaces or ASCII control characters |
| 1248 | continue; | 1255 | // (excluding DEL). It also follows special quoting rules where backslashes have no special |
| | 1256 | // interpretation, which makes it impossible to pass certain first arguments containing |
| | 1257 | // double quotes to a child process without characters from the first argument leaking into |
| | 1258 | // subsequent ones (which could have security implications). |
| | 1259 | // |
| | 1260 | // Empty arguments technically don't need quotes, but we quote them anyway for maximum |
| | 1261 | // compatibility with different implementations of the 'CommandLineToArgvW' algorithm. |
| | 1262 | // |
| | 1263 | // Double quotes are illegal in paths on Windows, so for the sake of simplicity we reject |
| | 1264 | // all first arguments containing double quotes, even ones that we could theoretically |
| | 1265 | // serialize in unquoted form. |
| | 1266 | var needs_quotes = arg0.len == 0; |
| | 1267 | for (arg0) |c| { |
| | 1268 | if (c <= ' ') { |
| | 1269 | needs_quotes = true; |
| | 1270 | } else if (c == '"') { |
| | 1271 | return error.InvalidArg0; |
| | 1272 | } |
| 1249 | } | 1273 | } |
| 1250 | try buf.append('"'); | 1274 | if (needs_quotes) { |
| 1251 | var backslash_count: usize = 0; | 1275 | try buf.append('"'); |
| 1252 | for (arg) |byte| { | 1276 | try buf.appendSlice(arg0); |
| 1253 | switch (byte) { | 1277 | try buf.append('"'); |
| 1254 | '\\' => backslash_count += 1, | 1278 | } else { |
| 1255 | '"' => { | 1279 | try buf.appendSlice(arg0); |
| 1256 | try buf.appendNTimes('\\', backslash_count * 2 + 1); | 1280 | } |
| 1257 | try buf.append('"'); | 1281 | |
| 1258 | backslash_count = 0; | 1282 | for (argv[1..]) |arg| { |
| 1259 | }, | 1283 | try buf.append(' '); |
| 1260 | else => { | 1284 | |
| 1261 | try buf.appendNTimes('\\', backslash_count); | 1285 | // Subsequent arguments must be quoted if they contain spaces, tabs or double quotes, |
| 1262 | try buf.append(byte); | 1286 | // or if they are empty. For simplicity and for maximum compatibility with different |
| 1263 | backslash_count = 0; | 1287 | // implementations of the 'CommandLineToArgvW' algorithm, we also quote all ASCII |
| 1264 | }, | 1288 | // control characters (again, excluding DEL). |
| | 1289 | needs_quotes = for (arg) |c| { |
| | 1290 | if (c <= ' ' or c == '"') { |
| | 1291 | break true; |
| | 1292 | } |
| | 1293 | } else arg.len == 0; |
| | 1294 | if (!needs_quotes) { |
| | 1295 | try buf.appendSlice(arg); |
| | 1296 | continue; |
| | 1297 | } |
| | 1298 | |
| | 1299 | try buf.append('"'); |
| | 1300 | var backslash_count: usize = 0; |
| | 1301 | for (arg) |byte| { |
| | 1302 | switch (byte) { |
| | 1303 | '\\' => { |
| | 1304 | backslash_count += 1; |
| | 1305 | }, |
| | 1306 | '"' => { |
| | 1307 | try buf.appendNTimes('\\', backslash_count * 2 + 1); |
| | 1308 | try buf.append('"'); |
| | 1309 | backslash_count = 0; |
| | 1310 | }, |
| | 1311 | else => { |
| | 1312 | try buf.appendNTimes('\\', backslash_count); |
| | 1313 | try buf.append(byte); |
| | 1314 | backslash_count = 0; |
| | 1315 | }, |
| | 1316 | } |
| 1265 | } | 1317 | } |
| | 1318 | try buf.appendNTimes('\\', backslash_count * 2); |
| | 1319 | try buf.append('"'); |
| 1266 | } | 1320 | } |
| 1267 | try buf.appendNTimes('\\', backslash_count * 2); | | |
| 1268 | try buf.append('"'); | | |
| 1269 | } | 1321 | } |
| 1270 | | 1322 | |
| 1271 | return buf.toOwnedSliceSentinel(0); | 1323 | return try unicode.utf8ToUtf16LeWithNull(allocator, buf.items); |
| | 1324 | } |
| | 1325 | |
| | 1326 | test "argvToCommandLineWindows" { |
| | 1327 | const t = testArgvToCommandLineWindows; |
| | 1328 | |
| | 1329 | try t(&.{ |
| | 1330 | \\C:\Program Files\zig\zig.exe |
| | 1331 | , |
| | 1332 | \\run |
| | 1333 | , |
| | 1334 | \\.\src\main.zig |
| | 1335 | , |
| | 1336 | \\-target |
| | 1337 | , |
| | 1338 | \\x86_64-windows-gnu |
| | 1339 | , |
| | 1340 | \\-O |
| | 1341 | , |
| | 1342 | \\ReleaseSafe |
| | 1343 | , |
| | 1344 | \\-- |
| | 1345 | , |
| | 1346 | \\--emoji=🗿 |
| | 1347 | , |
| | 1348 | \\--eval=new Regex("Dwayne \"The Rock\" Johnson") |
| | 1349 | , |
| | 1350 | }, |
| | 1351 | \\"C:\Program Files\zig\zig.exe" run .\src\main.zig -target x86_64-windows-gnu -O ReleaseSafe -- --emoji=🗿 "--eval=new Regex(\"Dwayne \\\"The Rock\\\" Johnson\")" |
| | 1352 | ); |
| | 1353 | |
| | 1354 | try t(&.{}, ""); |
| | 1355 | try t(&.{""}, "\"\""); |
| | 1356 | try t(&.{" "}, "\" \""); |
| | 1357 | try t(&.{"\t"}, "\"\t\""); |
| | 1358 | try t(&.{"\x07"}, "\"\x07\""); |
| | 1359 | try t(&.{"🦎"}, "🦎"); |
| | 1360 | |
| | 1361 | try t( |
| | 1362 | &.{ "zig", "aa aa", "bb\tbb", "cc\ncc", "dd\r\ndd", "ee\x7Fee" }, |
| | 1363 | "zig \"aa aa\" \"bb\tbb\" \"cc\ncc\" \"dd\r\ndd\" ee\x7Fee", |
| | 1364 | ); |
| | 1365 | |
| | 1366 | try t( |
| | 1367 | &.{ "\\\\foo bar\\foo bar\\", "\\\\zig zag\\zig zag\\" }, |
| | 1368 | "\"\\\\foo bar\\foo bar\\\" \"\\\\zig zag\\zig zag\\\\\"", |
| | 1369 | ); |
| | 1370 | |
| | 1371 | try std.testing.expectError( |
| | 1372 | error.InvalidArg0, |
| | 1373 | argvToCommandLineWindows(std.testing.allocator, &.{"\"quotes\"quotes\""}), |
| | 1374 | ); |
| | 1375 | try std.testing.expectError( |
| | 1376 | error.InvalidArg0, |
| | 1377 | argvToCommandLineWindows(std.testing.allocator, &.{"quotes\"quotes"}), |
| | 1378 | ); |
| | 1379 | try std.testing.expectError( |
| | 1380 | error.InvalidArg0, |
| | 1381 | argvToCommandLineWindows(std.testing.allocator, &.{"q u o t e s \" q u o t e s"}), |
| | 1382 | ); |
| | 1383 | } |
| | 1384 | |
| | 1385 | fn testArgvToCommandLineWindows(argv: []const []const u8, expected_cmd_line: []const u8) !void { |
| | 1386 | const cmd_line_w = try argvToCommandLineWindows(std.testing.allocator, argv); |
| | 1387 | defer std.testing.allocator.free(cmd_line_w); |
| | 1388 | |
| | 1389 | const cmd_line = try unicode.utf16leToUtf8Alloc(std.testing.allocator, cmd_line_w); |
| | 1390 | defer std.testing.allocator.free(cmd_line); |
| | 1391 | |
| | 1392 | try std.testing.expectEqualStrings(expected_cmd_line, cmd_line); |
| 1272 | } | 1393 | } |
| 1273 | | 1394 | |
| 1274 | fn windowsDestroyPipe(rd: ?windows.HANDLE, wr: ?windows.HANDLE) void { | 1395 | fn windowsDestroyPipe(rd: ?windows.HANDLE, wr: ?windows.HANDLE) void { |