authorgravatar for git@vexu.euVeikka Tuominen <git@vexu.eu> 2023-12-26 00:54:13+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2023-12-26 00:54:13+02:00
logcd302771420f0e36b1321f922b83e99f7e0ad51d
tree648e4c47dd3829bf0ca367b66a2bd36123a0e6ea
parentbb0f7d55e8c50e379fa9bdcb8758d89d08e0cc1f
parent4d9c4ab82c61a5c6f6e120924941d2b22fd15dc9
signaturebadge-question-mark Signed by PGP key 4AEE18F83AFDEB23

Merge pull request #18309 from castholm/windows-argv

More accurate argv parsing/serialization on Windows

2 files changed, 510 insertions(+), 35 deletions(-)

lib/std/child_process.zig+150-29
......@@ -744,9 +744,6 @@ pub const ChildProcess = struct {
744744 windowsDestroyPipe(g_hChildStd_ERR_Rd, g_hChildStd_ERR_Wr);
745745 };
746746
747 const cmd_line = try windowsCreateCommandLine(self.allocator, self.argv);
748 defer self.allocator.free(cmd_line);
749
750747 var siStartInfo = windows.STARTUPINFOW{
751748 .cb = @sizeOf(windows.STARTUPINFOW),
752749 .hStdError = g_hChildStd_ERR_Wr,
......@@ -818,7 +815,11 @@ pub const ChildProcess = struct {
818815 const app_name_w = try unicode.utf8ToUtf16LeWithNull(self.allocator, app_basename_utf8);
819816 defer self.allocator.free(app_name_w);
820817
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 };
822823 defer self.allocator.free(cmd_line_w);
823824
824825 run: {
......@@ -1236,39 +1237,159 @@ test "windowsCreateProcessSupportsExtension" {
12361237 try std.testing.expect(windowsCreateProcessSupportsExtension(&[_]u16{ '.', 'e', 'X', 'e', 'c' }) == null);
12371238}
12381239
1239/// Caller must dealloc.
1240fn windowsCreateCommandLine(allocator: mem.Allocator, argv: []const []const u8) ![:0]u8 {
1240pub const ArgvToCommandLineError = error{ OutOfMemory, InvalidUtf8, InvalidArg0 };
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.
1244pub fn argvToCommandLineWindows(
1245 allocator: mem.Allocator,
1246 argv: []const []const u8,
1247) ArgvToCommandLineError![:0]u16 {
12411248 var buf = std.ArrayList(u8).init(allocator);
12421249 defer buf.deinit();
12431250
1244 for (argv, 0..) |arg, arg_i| {
1245 if (arg_i != 0) try buf.append(' ');
1246 if (mem.indexOfAny(u8, arg, " \t\n\"") == null) {
1247 try buf.appendSlice(arg);
1248 continue;
1251 if (argv.len != 0) {
1252 const arg0 = argv[0];
1253
1254 // The first argument must be quoted if it contains spaces or ASCII control characters
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 }
12491273 }
1250 try buf.append('"');
1251 var backslash_count: usize = 0;
1252 for (arg) |byte| {
1253 switch (byte) {
1254 '\\' => backslash_count += 1,
1255 '"' => {
1256 try buf.appendNTimes('\\', backslash_count * 2 + 1);
1257 try buf.append('"');
1258 backslash_count = 0;
1259 },
1260 else => {
1261 try buf.appendNTimes('\\', backslash_count);
1262 try buf.append(byte);
1263 backslash_count = 0;
1264 },
1274 if (needs_quotes) {
1275 try buf.append('"');
1276 try buf.appendSlice(arg0);
1277 try buf.append('"');
1278 } else {
1279 try buf.appendSlice(arg0);
1280 }
1281
1282 for (argv[1..]) |arg| {
1283 try buf.append(' ');
1284
1285 // Subsequent arguments must be quoted if they contain spaces, tabs or double quotes,
1286 // or if they are empty. For simplicity and for maximum compatibility with different
1287 // implementations of the 'CommandLineToArgvW' algorithm, we also quote all ASCII
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 }
12651317 }
1318 try buf.appendNTimes('\\', backslash_count * 2);
1319 try buf.append('"');
12661320 }
1267 try buf.appendNTimes('\\', backslash_count * 2);
1268 try buf.append('"');
12691321 }
12701322
1271 return buf.toOwnedSliceSentinel(0);
1323 return try unicode.utf8ToUtf16LeWithNull(allocator, buf.items);
1324}
1325
1326test "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
1385fn 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);
12721393}
12731394
12741395fn windowsDestroyPipe(rd: ?windows.HANDLE, wr: ?windows.HANDLE) void {
lib/std/process.zig+360-6
......@@ -522,6 +522,236 @@ pub const ArgIteratorWasi = struct {
522522 }
523523};
524524
525/// Iterator that implements the Windows command-line parsing algorithm.
526///
527/// This iterator faithfully implements the parsing behavior observed in `CommandLineToArgvW` with
528/// one exception: if the command-line string is empty, the iterator will immediately complete
529/// without returning any arguments (whereas `CommandLineArgvW` will return a single argument
530/// representing the name of the current executable).
531pub const ArgIteratorWindows = struct {
532 allocator: Allocator,
533 /// Owned by the iterator.
534 cmd_line: []const u8,
535 index: usize = 0,
536 /// Owned by the iterator. Long enough to hold the entire `cmd_line` plus a null terminator.
537 buffer: []u8,
538 start: usize = 0,
539 end: usize = 0,
540
541 pub const InitError = error{ OutOfMemory, InvalidCmdLine };
542
543 /// `cmd_line_w` *must* be an UTF16-LE-encoded string.
544 ///
545 /// The iterator makes a copy of `cmd_line_w` converted UTF-8 and keeps it; it does *not* take
546 /// ownership of `cmd_line_w`.
547 pub fn init(allocator: Allocator, cmd_line_w: [*:0]const u16) InitError!ArgIteratorWindows {
548 const cmd_line = std.unicode.utf16leToUtf8Alloc(allocator, mem.sliceTo(cmd_line_w, 0)) catch |err| switch (err) {
549 error.DanglingSurrogateHalf,
550 error.ExpectedSecondSurrogateHalf,
551 error.UnexpectedSecondSurrogateHalf,
552 => return error.InvalidCmdLine,
553 error.OutOfMemory => return error.OutOfMemory,
554 };
555 errdefer allocator.free(cmd_line);
556
557 const buffer = try allocator.alloc(u8, cmd_line.len + 1);
558 errdefer allocator.free(buffer);
559
560 return .{
561 .allocator = allocator,
562 .cmd_line = cmd_line,
563 .buffer = buffer,
564 };
565 }
566
567 /// Returns the next argument and advances the iterator. Returns `null` if at the end of the
568 /// command-line string. The iterator owns the returned slice.
569 pub fn next(self: *ArgIteratorWindows) ?[:0]const u8 {
570 return self.nextWithStrategy(next_strategy);
571 }
572
573 /// Skips the next argument and advances the iterator. Returns `true` if an argument was
574 /// skipped, `false` if at the end of the command-line string.
575 pub fn skip(self: *ArgIteratorWindows) bool {
576 return self.nextWithStrategy(skip_strategy);
577 }
578
579 const next_strategy = struct {
580 const T = ?[:0]const u8;
581
582 const eof = null;
583
584 fn emitBackslashes(self: *ArgIteratorWindows, count: usize) void {
585 for (0..count) |_| emitCharacter(self, '\\');
586 }
587
588 fn emitCharacter(self: *ArgIteratorWindows, char: u8) void {
589 self.buffer[self.end] = char;
590 self.end += 1;
591 }
592
593 fn yieldArg(self: *ArgIteratorWindows) [:0]const u8 {
594 self.buffer[self.end] = 0;
595 const arg = self.buffer[self.start..self.end :0];
596 self.end += 1;
597 self.start = self.end;
598 return arg;
599 }
600 };
601
602 const skip_strategy = struct {
603 const T = bool;
604
605 const eof = false;
606
607 fn emitBackslashes(_: *ArgIteratorWindows, _: usize) void {}
608
609 fn emitCharacter(_: *ArgIteratorWindows, _: u8) void {}
610
611 fn yieldArg(_: *ArgIteratorWindows) bool {
612 return true;
613 }
614 };
615
616 // The essential parts of the algorithm are described in Microsoft's documentation:
617 //
618 // - <https://learn.microsoft.com/en-us/cpp/cpp/main-function-command-line-args?view=msvc-170#parsing-c-command-line-arguments>
619 // - <https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-commandlinetoargvw>
620 //
621 // David Deley explains some additional undocumented quirks in great detail:
622 //
623 // - <https://daviddeley.com/autohotkey/parameters/parameters.htm#WINCRULES>
624 //
625 // Code points <= U+0020 terminating an unquoted first argument was discovered independently by
626 // testing and observing the behavior of 'CommandLineToArgvW' on Windows 10.
627
628 fn nextWithStrategy(self: *ArgIteratorWindows, comptime strategy: type) strategy.T {
629 // The first argument (the executable name) uses different parsing rules.
630 if (self.index == 0) {
631 var char = if (self.cmd_line.len != 0) self.cmd_line[0] else 0;
632 switch (char) {
633 0 => {
634 // Immediately complete the iterator.
635 // 'CommandLineToArgvW' would return the name of the current executable here.
636 return strategy.eof;
637 },
638 '"' => {
639 // If the first character is a quote, read everything until the next quote (then
640 // skip that quote), or until the end of the string.
641 self.index += 1;
642 while (true) : (self.index += 1) {
643 char = if (self.index != self.cmd_line.len) self.cmd_line[self.index] else 0;
644 switch (char) {
645 0 => {
646 return strategy.yieldArg(self);
647 },
648 '"' => {
649 self.index += 1;
650 return strategy.yieldArg(self);
651 },
652 else => {
653 strategy.emitCharacter(self, char);
654 },
655 }
656 }
657 },
658 else => {
659 // Otherwise, read everything until the next space or ASCII control character
660 // (not including DEL) (then skip that character), or until the end of the
661 // string. This means that if the command-line string starts with one of these
662 // characters, the first returned argument will be the empty string.
663 while (true) : (self.index += 1) {
664 char = if (self.index != self.cmd_line.len) self.cmd_line[self.index] else 0;
665 switch (char) {
666 0 => {
667 return strategy.yieldArg(self);
668 },
669 '\x01'...' ' => {
670 self.index += 1;
671 return strategy.yieldArg(self);
672 },
673 else => {
674 strategy.emitCharacter(self, char);
675 },
676 }
677 }
678 },
679 }
680 }
681
682 // Skip spaces and tabs. The iterator completes if we reach the end of the string here.
683 while (true) : (self.index += 1) {
684 const char = if (self.index != self.cmd_line.len) self.cmd_line[self.index] else 0;
685 switch (char) {
686 0 => return strategy.eof,
687 ' ', '\t' => continue,
688 else => break,
689 }
690 }
691
692 // Parsing rules for subsequent arguments:
693 //
694 // - The end of the string always terminates the current argument.
695 // - When not in 'inside_quotes' mode, a space or tab terminates the current argument.
696 // - 2n backslashes followed by a quote emit n backslashes. If in 'inside_quotes' and the
697 // quote is immediately followed by a second quote, one quote is emitted and the other is
698 // skipped, otherwise, the quote is skipped. Finally, 'inside_quotes' is toggled.
699 // - 2n + 1 backslashes followed by a quote emit n backslashes followed by a quote.
700 // - n backslashes not followed by a quote emit n backslashes.
701 var backslash_count: usize = 0;
702 var inside_quotes = false;
703 while (true) : (self.index += 1) {
704 const char = if (self.index != self.cmd_line.len) self.cmd_line[self.index] else 0;
705 switch (char) {
706 0 => {
707 strategy.emitBackslashes(self, backslash_count);
708 return strategy.yieldArg(self);
709 },
710 ' ', '\t' => {
711 strategy.emitBackslashes(self, backslash_count);
712 backslash_count = 0;
713 if (inside_quotes)
714 strategy.emitCharacter(self, char)
715 else
716 return strategy.yieldArg(self);
717 },
718 '"' => {
719 const char_is_escaped_quote = backslash_count % 2 != 0;
720 strategy.emitBackslashes(self, backslash_count / 2);
721 backslash_count = 0;
722 if (char_is_escaped_quote) {
723 strategy.emitCharacter(self, '"');
724 } else {
725 if (inside_quotes and
726 self.index + 1 != self.cmd_line.len and
727 self.cmd_line[self.index + 1] == '"')
728 {
729 strategy.emitCharacter(self, '"');
730 self.index += 1;
731 }
732 inside_quotes = !inside_quotes;
733 }
734 },
735 '\\' => {
736 backslash_count += 1;
737 },
738 else => {
739 strategy.emitBackslashes(self, backslash_count);
740 backslash_count = 0;
741 strategy.emitCharacter(self, char);
742 },
743 }
744 }
745 }
746
747 /// Frees the iterator's copy of the command-line string and all previously returned
748 /// argument slices.
749 pub fn deinit(self: *ArgIteratorWindows) void {
750 self.allocator.free(self.buffer);
751 self.allocator.free(self.cmd_line);
752 }
753};
754
525755/// Optional parameters for `ArgIteratorGeneral`
526756pub const ArgIteratorGeneralOptions = struct {
527757 comments: bool = false,
......@@ -754,7 +984,7 @@ pub fn ArgIteratorGeneral(comptime options: ArgIteratorGeneralOptions) type {
754984/// Cross-platform command line argument iterator.
755985pub const ArgIterator = struct {
756986 const InnerType = switch (builtin.os.tag) {
757 .windows => ArgIteratorGeneral(.{}),
987 .windows => ArgIteratorWindows,
758988 .wasi => if (builtin.link_libc) ArgIteratorPosix else ArgIteratorWasi,
759989 else => ArgIteratorPosix,
760990 };
......@@ -774,10 +1004,7 @@ pub const ArgIterator = struct {
7741004 return ArgIterator{ .inner = InnerType.init() };
7751005 }
7761006
777 pub const InitError = switch (builtin.os.tag) {
778 .windows => InnerType.InitUtf16leError,
779 else => InnerType.InitError,
780 };
1007 pub const InitError = InnerType.InitError;
7811008
7821009 /// You must deinitialize iterator's internal buffers by calling `deinit` when done.
7831010 pub fn initWithAllocator(allocator: Allocator) InitError!ArgIterator {
......@@ -786,7 +1013,7 @@ pub const ArgIterator = struct {
7861013 }
7871014 if (builtin.os.tag == .windows) {
7881015 const cmd_line_w = os.windows.kernel32.GetCommandLineW();
789 return ArgIterator{ .inner = try InnerType.initUtf16le(allocator, cmd_line_w) };
1016 return ArgIterator{ .inner = try InnerType.init(allocator, cmd_line_w) };
7901017 }
7911018
7921019 return ArgIterator{ .inner = InnerType.init() };
......@@ -877,6 +1104,133 @@ pub fn argsFree(allocator: Allocator, args_alloc: []const [:0]u8) void {
8771104 return allocator.free(aligned_allocated_buf);
8781105}
8791106
1107test "ArgIteratorWindows" {
1108 const t = testArgIteratorWindows;
1109
1110 try t(
1111 \\"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\")"
1112 , &.{
1113 \\C:\Program Files\zig\zig.exe
1114 ,
1115 \\run
1116 ,
1117 \\.\src\main.zig
1118 ,
1119 \\-target
1120 ,
1121 \\x86_64-windows-gnu
1122 ,
1123 \\-O
1124 ,
1125 \\ReleaseSafe
1126 ,
1127 \\--
1128 ,
1129 \\--emoji=🗿
1130 ,
1131 \\--eval=new Regex("Dwayne \"The Rock\" Johnson")
1132 ,
1133 });
1134
1135 // Empty
1136 try t("", &.{});
1137
1138 // Separators
1139 try t("aa bb cc", &.{ "aa", "bb", "cc" });
1140 try t("aa\tbb\tcc", &.{ "aa", "bb", "cc" });
1141 try t("aa\nbb\ncc", &.{ "aa", "bb\ncc" });
1142 try t("aa\r\nbb\r\ncc", &.{ "aa", "\nbb\r\ncc" });
1143 try t("aa\rbb\rcc", &.{ "aa", "bb\rcc" });
1144 try t("aa\x07bb\x07cc", &.{ "aa", "bb\x07cc" });
1145 try t("aa\x7Fbb\x7Fcc", &.{"aa\x7Fbb\x7Fcc"});
1146 try t("aa🦎bb🦎cc", &.{"aa🦎bb🦎cc"});
1147
1148 // Leading/trailing whitespace
1149 try t(" ", &.{""});
1150 try t(" aa bb ", &.{ "", "aa", "bb" });
1151 try t("\t\t", &.{""});
1152 try t("\t\taa\t\tbb\t\t", &.{ "", "aa", "bb" });
1153 try t("\n\n", &.{ "", "\n" });
1154 try t("\n\naa\n\nbb\n\n", &.{ "", "\naa\n\nbb\n\n" });
1155
1156 // Executable name with quotes/backslashes
1157 try t("\"aa bb\tcc\ndd\"", &.{"aa bb\tcc\ndd"});
1158 try t("\"", &.{""});
1159 try t("\"\"", &.{""});
1160 try t("\"\"\"", &.{ "", "" });
1161 try t("\"\"\"\"", &.{ "", "" });
1162 try t("\"\"\"\"\"", &.{ "", "\"" });
1163 try t("aa\"bb\"cc\"dd", &.{"aa\"bb\"cc\"dd"});
1164 try t("aa\"bb cc\"dd", &.{ "aa\"bb", "ccdd" });
1165 try t("\"aa\\\"bb\"", &.{ "aa\\", "bb" });
1166 try t("\"aa\\\\\"", &.{"aa\\\\"});
1167 try t("aa\\\"bb", &.{"aa\\\"bb"});
1168 try t("aa\\\\\"bb", &.{"aa\\\\\"bb"});
1169
1170 // Arguments with quotes/backslashes
1171 try t(". \"aa bb\tcc\ndd\"", &.{ ".", "aa bb\tcc\ndd" });
1172 try t(". aa\" \"bb\"\t\"cc\"\n\"dd\"", &.{ ".", "aa bb\tcc\ndd" });
1173 try t(". ", &.{"."});
1174 try t(". \"", &.{ ".", "" });
1175 try t(". \"\"", &.{ ".", "" });
1176 try t(". \"\"\"", &.{ ".", "\"" });
1177 try t(". \"\"\"\"", &.{ ".", "\"" });
1178 try t(". \"\"\"\"\"", &.{ ".", "\"" });
1179 try t(". \"\"\"\"\"\"", &.{ ".", "\"\"" });
1180 try t(". \" \"", &.{ ".", " " });
1181 try t(". \" \"\"", &.{ ".", " \"" });
1182 try t(". \" \"\"\"", &.{ ".", " \"" });
1183 try t(". \" \"\"\"\"", &.{ ".", " \"" });
1184 try t(". \" \"\"\"\"\"", &.{ ".", " \"\"" });
1185 try t(". \" \"\"\"\"\"\"", &.{ ".", " \"\"" });
1186 try t(". \\\"", &.{ ".", "\"" });
1187 try t(". \\\"\"", &.{ ".", "\"" });
1188 try t(". \\\"\"\"", &.{ ".", "\"" });
1189 try t(". \\\"\"\"\"", &.{ ".", "\"\"" });
1190 try t(". \\\"\"\"\"\"", &.{ ".", "\"\"" });
1191 try t(". \\\"\"\"\"\"\"", &.{ ".", "\"\"" });
1192 try t(". \" \\\"", &.{ ".", " \"" });
1193 try t(". \" \\\"\"", &.{ ".", " \"" });
1194 try t(". \" \\\"\"\"", &.{ ".", " \"\"" });
1195 try t(". \" \\\"\"\"\"", &.{ ".", " \"\"" });
1196 try t(". \" \\\"\"\"\"\"", &.{ ".", " \"\"" });
1197 try t(". \" \\\"\"\"\"\"\"", &.{ ".", " \"\"\"" });
1198 try t(". aa\\bb\\\\cc\\\\\\dd", &.{ ".", "aa\\bb\\\\cc\\\\\\dd" });
1199 try t(". \\\\\\\"aa bb\"", &.{ ".", "\\\"aa", "bb" });
1200 try t(". \\\\\\\\\"aa bb\"", &.{ ".", "\\\\aa bb" });
1201}
1202
1203fn testArgIteratorWindows(cmd_line: []const u8, expected_args: []const []const u8) !void {
1204 const cmd_line_w = try std.unicode.utf8ToUtf16LeWithNull(testing.allocator, cmd_line);
1205 defer testing.allocator.free(cmd_line_w);
1206
1207 // next
1208 {
1209 var it = try ArgIteratorWindows.init(testing.allocator, cmd_line_w);
1210 defer it.deinit();
1211
1212 for (expected_args) |expected| {
1213 if (it.next()) |actual| {
1214 try testing.expectEqualStrings(expected, actual);
1215 } else {
1216 return error.TestUnexpectedResult;
1217 }
1218 }
1219 try testing.expect(it.next() == null);
1220 }
1221
1222 // skip
1223 {
1224 var it = try ArgIteratorWindows.init(testing.allocator, cmd_line_w);
1225 defer it.deinit();
1226
1227 for (0..expected_args.len) |_| {
1228 try testing.expect(it.skip());
1229 }
1230 try testing.expect(!it.skip());
1231 }
1232}
1233
8801234test "general arg parsing" {
8811235 try testGeneralCmdLine("a b\tc d", &.{ "a", "b", "c", "d" });
8821236 try testGeneralCmdLine("\"abc\" d e", &.{ "abc", "d", "e" });