authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-04-15 15:28:33-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2024-04-15 15:28:33-07:00
logb78b2689ed9955487bf9a719e048bfeb5b230724
tree33b4eb1d86c249070d07d4b2a68ac3f63009b8a8
parentff18103ef6d383b6c6f81d996925f3a7856851e9
parentcffe1999c695c60d2c61c0781a1e16edaa2263f3
signaturebadge-check Signed by PGP key B5690EEEBB952194

Merge pull request #19655 from squeek502/windows-argv-post-2008

ArgIteratorWindows: Match post-2008 C runtime rather than `CommandLineToArgvW`

8 files changed, 502 insertions(+), 84 deletions(-)

lib/std/process.zig+147-84
......@@ -625,11 +625,22 @@ pub const ArgIteratorWasi = struct {
625625};
626626
627627/// Iterator that implements the Windows command-line parsing algorithm.
628/// The implementation is intended to be compatible with the post-2008 C runtime,
629/// but is *not* intended to be compatible with `CommandLineToArgvW` since
630/// `CommandLineToArgvW` uses the pre-2008 parsing rules.
628631///
629/// This iterator faithfully implements the parsing behavior observed in `CommandLineToArgvW` with
632/// This iterator faithfully implements the parsing behavior observed from the C runtime with
630633/// one exception: if the command-line string is empty, the iterator will immediately complete
631/// without returning any arguments (whereas `CommandLineArgvW` will return a single argument
634/// without returning any arguments (whereas the C runtime will return a single argument
632635/// representing the name of the current executable).
636///
637/// The essential parts of the algorithm are described in Microsoft's documentation:
638///
639/// - https://learn.microsoft.com/en-us/cpp/cpp/main-function-command-line-args?view=msvc-170#parsing-c-command-line-arguments
640///
641/// David Deley explains some additional undocumented quirks in great detail:
642///
643/// - https://daviddeley.com/autohotkey/parameters/parameters.htm#WINCRULES
633644pub const ArgIteratorWindows = struct {
634645 allocator: Allocator,
635646 /// Owned by the iterator.
......@@ -686,6 +697,51 @@ pub const ArgIteratorWindows = struct {
686697 fn emitCharacter(self: *ArgIteratorWindows, char: u8) void {
687698 self.buffer[self.end] = char;
688699 self.end += 1;
700
701 // Because we are emitting WTF-8 byte-by-byte, we need to
702 // check to see if we've emitted two consecutive surrogate
703 // codepoints that form a valid surrogate pair in order
704 // to ensure that we're always emitting well-formed WTF-8
705 // (https://simonsapin.github.io/wtf-8/#concatenating).
706 //
707 // If we do have a valid surrogate pair, we need to emit
708 // the UTF-8 sequence for the codepoint that they encode
709 // instead of the WTF-8 encoding for the two surrogate pairs
710 // separately.
711 //
712 // This is relevant when dealing with a WTF-16 encoded
713 // command line like this:
714 // "<0xD801>"<0xDC37>
715 // which would get converted to WTF-8 in `cmd_line` as:
716 // "<0xED><0xA0><0x81>"<0xED><0xB0><0xB7>
717 // and then after parsing it'd naively get emitted as:
718 // <0xED><0xA0><0x81><0xED><0xB0><0xB7>
719 // but instead, we need to recognize the surrogate pair
720 // and emit the codepoint it encodes, which in this
721 // example is U+10437 (𐐷), which is encoded in UTF-8 as:
722 // <0xF0><0x90><0x90><0xB7>
723 concatSurrogatePair(self);
724 }
725
726 fn concatSurrogatePair(self: *ArgIteratorWindows) void {
727 // Surrogate codepoints are always encoded as 3 bytes, so there
728 // must be 6 bytes for a surrogate pair to exist.
729 if (self.end - self.start >= 6) {
730 const window = self.buffer[self.end - 6 .. self.end];
731 const view = std.unicode.Wtf8View.init(window) catch return;
732 var it = view.iterator();
733 var pair: [2]u16 = undefined;
734 pair[0] = std.mem.nativeToLittle(u16, std.math.cast(u16, it.nextCodepoint().?) orelse return);
735 if (!std.unicode.utf16IsHighSurrogate(std.mem.littleToNative(u16, pair[0]))) return;
736 pair[1] = std.mem.nativeToLittle(u16, std.math.cast(u16, it.nextCodepoint().?) orelse return);
737 if (!std.unicode.utf16IsLowSurrogate(std.mem.littleToNative(u16, pair[1]))) return;
738 // We know we have a valid surrogate pair, so convert
739 // it to UTF-8, overwriting the surrogate pair's bytes
740 // and then chop off the extra bytes.
741 const len = std.unicode.utf16LeToUtf8(window, &pair) catch unreachable;
742 const delta = 6 - len;
743 self.end -= delta;
744 }
689745 }
690746
691747 fn yieldArg(self: *ArgIteratorWindows) [:0]const u8 {
......@@ -711,69 +767,37 @@ pub const ArgIteratorWindows = struct {
711767 }
712768 };
713769
714 // The essential parts of the algorithm are described in Microsoft's documentation:
715 //
716 // - <https://learn.microsoft.com/en-us/cpp/cpp/main-function-command-line-args?view=msvc-170#parsing-c-command-line-arguments>
717 // - <https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-commandlinetoargvw>
718 //
719 // David Deley explains some additional undocumented quirks in great detail:
720 //
721 // - <https://daviddeley.com/autohotkey/parameters/parameters.htm#WINCRULES>
722 //
723 // Code points <= U+0020 terminating an unquoted first argument was discovered independently by
724 // testing and observing the behavior of 'CommandLineToArgvW' on Windows 10.
725
726770 fn nextWithStrategy(self: *ArgIteratorWindows, comptime strategy: type) strategy.T {
727771 // The first argument (the executable name) uses different parsing rules.
728772 if (self.index == 0) {
729 var char = if (self.cmd_line.len != 0) self.cmd_line[0] else 0;
730 switch (char) {
731 0 => {
732 // Immediately complete the iterator.
733 // 'CommandLineToArgvW' would return the name of the current executable here.
734 return strategy.eof;
735 },
736 '"' => {
737 // If the first character is a quote, read everything until the next quote (then
738 // skip that quote), or until the end of the string.
739 self.index += 1;
740 while (true) : (self.index += 1) {
741 char = if (self.index != self.cmd_line.len) self.cmd_line[self.index] else 0;
742 switch (char) {
743 0 => {
744 return strategy.yieldArg(self);
745 },
746 '"' => {
747 self.index += 1;
748 return strategy.yieldArg(self);
749 },
750 else => {
751 strategy.emitCharacter(self, char);
752 },
753 }
754 }
755 },
756 else => {
757 // Otherwise, read everything until the next space or ASCII control character
758 // (not including DEL) (then skip that character), or until the end of the
759 // string. This means that if the command-line string starts with one of these
760 // characters, the first returned argument will be the empty string.
761 while (true) : (self.index += 1) {
762 char = if (self.index != self.cmd_line.len) self.cmd_line[self.index] else 0;
763 switch (char) {
764 0 => {
765 return strategy.yieldArg(self);
766 },
767 '\x01'...' ' => {
768 self.index += 1;
769 return strategy.yieldArg(self);
770 },
771 else => {
772 strategy.emitCharacter(self, char);
773 },
773 if (self.cmd_line.len == 0 or self.cmd_line[0] == 0) {
774 // Immediately complete the iterator.
775 // The C runtime would return the name of the current executable here.
776 return strategy.eof;
777 }
778
779 var inside_quotes = false;
780 while (true) : (self.index += 1) {
781 const char = if (self.index != self.cmd_line.len) self.cmd_line[self.index] else 0;
782 switch (char) {
783 0 => {
784 return strategy.yieldArg(self);
785 },
786 '"' => {
787 inside_quotes = !inside_quotes;
788 },
789 ' ', '\t' => {
790 if (inside_quotes)
791 strategy.emitCharacter(self, char)
792 else {
793 self.index += 1;
794 return strategy.yieldArg(self);
774795 }
775 }
776 },
796 },
797 else => {
798 strategy.emitCharacter(self, char);
799 },
800 }
777801 }
778802 }
779803
......@@ -791,9 +815,10 @@ pub const ArgIteratorWindows = struct {
791815 //
792816 // - The end of the string always terminates the current argument.
793817 // - When not in 'inside_quotes' mode, a space or tab terminates the current argument.
794 // - 2n backslashes followed by a quote emit n backslashes. If in 'inside_quotes' and the
795 // quote is immediately followed by a second quote, one quote is emitted and the other is
796 // skipped, otherwise, the quote is skipped. Finally, 'inside_quotes' is toggled.
818 // - 2n backslashes followed by a quote emit n backslashes (note: n can be zero).
819 // If in 'inside_quotes' and the quote is immediately followed by a second quote,
820 // one quote is emitted and the other is skipped, otherwise, the quote is skipped
821 // and 'inside_quotes' is toggled.
797822 // - 2n + 1 backslashes followed by a quote emit n backslashes followed by a quote.
798823 // - n backslashes not followed by a quote emit n backslashes.
799824 var backslash_count: usize = 0;
......@@ -826,8 +851,9 @@ pub const ArgIteratorWindows = struct {
826851 {
827852 strategy.emitCharacter(self, '"');
828853 self.index += 1;
854 } else {
855 inside_quotes = !inside_quotes;
829856 }
830 inside_quotes = !inside_quotes;
831857 }
832858 },
833859 '\\' => {
......@@ -1215,10 +1241,10 @@ test ArgIteratorWindows {
12151241 // Separators
12161242 try t("aa bb cc", &.{ "aa", "bb", "cc" });
12171243 try t("aa\tbb\tcc", &.{ "aa", "bb", "cc" });
1218 try t("aa\nbb\ncc", &.{ "aa", "bb\ncc" });
1219 try t("aa\r\nbb\r\ncc", &.{ "aa", "\nbb\r\ncc" });
1220 try t("aa\rbb\rcc", &.{ "aa", "bb\rcc" });
1221 try t("aa\x07bb\x07cc", &.{ "aa", "bb\x07cc" });
1244 try t("aa\nbb\ncc", &.{"aa\nbb\ncc"});
1245 try t("aa\r\nbb\r\ncc", &.{"aa\r\nbb\r\ncc"});
1246 try t("aa\rbb\rcc", &.{"aa\rbb\rcc"});
1247 try t("aa\x07bb\x07cc", &.{"aa\x07bb\x07cc"});
12221248 try t("aa\x7Fbb\x7Fcc", &.{"aa\x7Fbb\x7Fcc"});
12231249 try t("aa🦎bb🦎cc", &.{"aa🦎bb🦎cc"});
12241250
......@@ -1227,22 +1253,22 @@ test ArgIteratorWindows {
12271253 try t(" aa bb ", &.{ "", "aa", "bb" });
12281254 try t("\t\t", &.{""});
12291255 try t("\t\taa\t\tbb\t\t", &.{ "", "aa", "bb" });
1230 try t("\n\n", &.{ "", "\n" });
1231 try t("\n\naa\n\nbb\n\n", &.{ "", "\naa\n\nbb\n\n" });
1256 try t("\n\n", &.{"\n\n"});
1257 try t("\n\naa\n\nbb\n\n", &.{"\n\naa\n\nbb\n\n"});
12321258
12331259 // Executable name with quotes/backslashes
12341260 try t("\"aa bb\tcc\ndd\"", &.{"aa bb\tcc\ndd"});
12351261 try t("\"", &.{""});
12361262 try t("\"\"", &.{""});
1237 try t("\"\"\"", &.{ "", "" });
1238 try t("\"\"\"\"", &.{ "", "" });
1239 try t("\"\"\"\"\"", &.{ "", "\"" });
1240 try t("aa\"bb\"cc\"dd", &.{"aa\"bb\"cc\"dd"});
1241 try t("aa\"bb cc\"dd", &.{ "aa\"bb", "ccdd" });
1242 try t("\"aa\\\"bb\"", &.{ "aa\\", "bb" });
1263 try t("\"\"\"", &.{""});
1264 try t("\"\"\"\"", &.{""});
1265 try t("\"\"\"\"\"", &.{""});
1266 try t("aa\"bb\"cc\"dd", &.{"aabbccdd"});
1267 try t("aa\"bb cc\"dd", &.{"aabb ccdd"});
1268 try t("\"aa\\\"bb\"", &.{"aa\\bb"});
12431269 try t("\"aa\\\\\"", &.{"aa\\\\"});
1244 try t("aa\\\"bb", &.{"aa\\\"bb"});
1245 try t("aa\\\\\"bb", &.{"aa\\\\\"bb"});
1270 try t("aa\\\"bb", &.{"aa\\bb"});
1271 try t("aa\\\\\"bb", &.{"aa\\\\bb"});
12461272
12471273 // Arguments with quotes/backslashes
12481274 try t(". \"aa bb\tcc\ndd\"", &.{ ".", "aa bb\tcc\ndd" });
......@@ -1252,29 +1278,66 @@ test ArgIteratorWindows {
12521278 try t(". \"\"", &.{ ".", "" });
12531279 try t(". \"\"\"", &.{ ".", "\"" });
12541280 try t(". \"\"\"\"", &.{ ".", "\"" });
1255 try t(". \"\"\"\"\"", &.{ ".", "\"" });
1281 try t(". \"\"\"\"\"", &.{ ".", "\"\"" });
12561282 try t(". \"\"\"\"\"\"", &.{ ".", "\"\"" });
12571283 try t(". \" \"", &.{ ".", " " });
12581284 try t(". \" \"\"", &.{ ".", " \"" });
12591285 try t(". \" \"\"\"", &.{ ".", " \"" });
1260 try t(". \" \"\"\"\"", &.{ ".", " \"" });
1286 try t(". \" \"\"\"\"", &.{ ".", " \"\"" });
12611287 try t(". \" \"\"\"\"\"", &.{ ".", " \"\"" });
1262 try t(". \" \"\"\"\"\"\"", &.{ ".", " \"\"" });
1288 try t(". \" \"\"\"\"\"\"", &.{ ".", " \"\"\"" });
12631289 try t(". \\\"", &.{ ".", "\"" });
12641290 try t(". \\\"\"", &.{ ".", "\"" });
12651291 try t(". \\\"\"\"", &.{ ".", "\"" });
12661292 try t(". \\\"\"\"\"", &.{ ".", "\"\"" });
12671293 try t(". \\\"\"\"\"\"", &.{ ".", "\"\"" });
1268 try t(". \\\"\"\"\"\"\"", &.{ ".", "\"\"" });
1294 try t(". \\\"\"\"\"\"\"", &.{ ".", "\"\"\"" });
12691295 try t(". \" \\\"", &.{ ".", " \"" });
12701296 try t(". \" \\\"\"", &.{ ".", " \"" });
12711297 try t(". \" \\\"\"\"", &.{ ".", " \"\"" });
12721298 try t(". \" \\\"\"\"\"", &.{ ".", " \"\"" });
1273 try t(". \" \\\"\"\"\"\"", &.{ ".", " \"\"" });
1299 try t(". \" \\\"\"\"\"\"", &.{ ".", " \"\"\"" });
12741300 try t(". \" \\\"\"\"\"\"\"", &.{ ".", " \"\"\"" });
12751301 try t(". aa\\bb\\\\cc\\\\\\dd", &.{ ".", "aa\\bb\\\\cc\\\\\\dd" });
12761302 try t(". \\\\\\\"aa bb\"", &.{ ".", "\\\"aa", "bb" });
12771303 try t(". \\\\\\\\\"aa bb\"", &.{ ".", "\\\\aa bb" });
1304
1305 // From https://learn.microsoft.com/en-us/cpp/cpp/main-function-command-line-args#results-of-parsing-command-lines
1306 try t(
1307 \\foo.exe "abc" d e
1308 , &.{ "foo.exe", "abc", "d", "e" });
1309 try t(
1310 \\foo.exe a\\b d"e f"g h
1311 , &.{ "foo.exe", "a\\\\b", "de fg", "h" });
1312 try t(
1313 \\foo.exe a\\\"b c d
1314 , &.{ "foo.exe", "a\\\"b", "c", "d" });
1315 try t(
1316 \\foo.exe a\\\\"b c" d e
1317 , &.{ "foo.exe", "a\\\\b c", "d", "e" });
1318 try t(
1319 \\foo.exe a"b"" c d
1320 , &.{ "foo.exe", "ab\" c d" });
1321
1322 // From https://daviddeley.com/autohotkey/parameters/parameters.htm#WINCRULESEX
1323 try t("foo.exe CallMeIshmael", &.{ "foo.exe", "CallMeIshmael" });
1324 try t("foo.exe \"Call Me Ishmael\"", &.{ "foo.exe", "Call Me Ishmael" });
1325 try t("foo.exe Cal\"l Me I\"shmael", &.{ "foo.exe", "Call Me Ishmael" });
1326 try t("foo.exe CallMe\\\"Ishmael", &.{ "foo.exe", "CallMe\"Ishmael" });
1327 try t("foo.exe \"CallMe\\\"Ishmael\"", &.{ "foo.exe", "CallMe\"Ishmael" });
1328 try t("foo.exe \"Call Me Ishmael\\\\\"", &.{ "foo.exe", "Call Me Ishmael\\" });
1329 try t("foo.exe \"CallMe\\\\\\\"Ishmael\"", &.{ "foo.exe", "CallMe\\\"Ishmael" });
1330 try t("foo.exe a\\\\\\b", &.{ "foo.exe", "a\\\\\\b" });
1331 try t("foo.exe \"a\\\\\\b\"", &.{ "foo.exe", "a\\\\\\b" });
1332
1333 // Surrogate pair encoding of 𐐷 separated by quotes.
1334 // Encoded as WTF-16:
1335 // "<0xD801>"<0xDC37>
1336 // Encoded as WTF-8:
1337 // "<0xED><0xA0><0x81>"<0xED><0xB0><0xB7>
1338 // During parsing, the quotes drop out and the surrogate pair
1339 // should end up encoded as its normal UTF-8 representation.
1340 try t("foo.exe \"\xed\xa0\x81\"\xed\xb0\xb7", &.{ "foo.exe", "𐐷" });
12781341}
12791342
12801343fn testArgIteratorWindows(cmd_line: []const u8, expected_args: []const []const u8) !void {
test/standalone/build.zig.zon+3
......@@ -104,6 +104,9 @@
104104 .windows_spawn = .{
105105 .path = "windows_spawn",
106106 },
107 .windows_argv = .{
108 .path = "windows_argv",
109 },
107110 .self_exe_symlink = .{
108111 .path = "self_exe_symlink",
109112 },
test/standalone/windows_argv/README.md created+19
......@@ -0,0 +1,19 @@
1Tests that Zig's `std.process.ArgIteratorWindows` is compatible with both the MSVC and MinGW C runtimes' argv splitting algorithms.
2
3The method of testing is:
4- Compile a C file with `wmain` as its entry point
5- The C `wmain` calls a Zig-implemented `verify` function that takes the `argv` from `wmain` and compares it to the argv gotten from `std.proccess.argsAlloc` (which takes `kernel32.GetCommandLineW()` and splits it)
6- The compiled C program is spawned continuously as a child process by the implementation in `fuzz.zig` with randomly generated command lines
7 + On Windows, the 'application name' and the 'command line' are disjoint concepts. That is, you can spawn `foo.exe` but set the command line to `bar.exe`, and `CreateProcessW` will spawn `foo.exe` but `argv[0]` will be `bar.exe`. This quirk allows us to test arbitrary `argv[0]` values as well which otherwise wouldn't be possible.
8
9Note: This is intentionally testing against the C runtime argv splitting and *not* [`CommandLineToArgvW`](https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-commandlinetoargvw), since the C runtime argv splitting was updated in 2008 but `CommandLineToArgvW` still uses the pre-2008 algorithm (which differs in both `argv[0]` rules and `""`; see [here](https://daviddeley.com/autohotkey/parameters/parameters.htm#WINCRULESDOC) for details)
10
11---
12
13In addition to being run during `zig build test-standalone`, this test can be run on its own via `zig build test` from within this directory.
14
15When run on its own:
16- `-Diterations=<num>` can be used to set the max fuzzing iterations, and `-Diterations=0` can be used to fuzz indefinitely
17- `-Dseed=<num>` can be used to set the PRNG seed for fuzz testing. If not provided, then the seed is chosen at random during `build.zig` compilation.
18
19On failure, the number of iterations and the seed can be seen in the failing command, e.g. in `path\to\fuzz.exe path\to\verify-msvc.exe 100 2780392459403250529`, the iterations is `100` and the seed is `2780392459403250529`.
test/standalone/windows_argv/build.zig created+100
......@@ -0,0 +1,100 @@
1const std = @import("std");
2const builtin = @import("builtin");
3
4pub fn build(b: *std.Build) !void {
5 const test_step = b.step("test", "Test it");
6 b.default_step = test_step;
7
8 if (builtin.os.tag != .windows) return;
9
10 const optimize: std.builtin.OptimizeMode = .Debug;
11
12 const lib_gnu = b.addStaticLibrary(.{
13 .name = "toargv-gnu",
14 .root_source_file = .{ .path = "lib.zig" },
15 .target = b.resolveTargetQuery(.{
16 .abi = .gnu,
17 }),
18 .optimize = optimize,
19 });
20 const verify_gnu = b.addExecutable(.{
21 .name = "verify-gnu",
22 .target = b.resolveTargetQuery(.{
23 .abi = .gnu,
24 }),
25 .optimize = optimize,
26 });
27 verify_gnu.addCSourceFile(.{
28 .file = .{ .path = "verify.c" },
29 .flags = &.{ "-DUNICODE", "-D_UNICODE" },
30 });
31 verify_gnu.mingw_unicode_entry_point = true;
32 verify_gnu.linkLibrary(lib_gnu);
33 verify_gnu.linkLibC();
34
35 const fuzz = b.addExecutable(.{
36 .name = "fuzz",
37 .root_source_file = .{ .path = "fuzz.zig" },
38 .target = b.host,
39 .optimize = optimize,
40 });
41
42 const fuzz_max_iterations = b.option(u64, "iterations", "The max fuzz iterations (default: 100)") orelse 100;
43 const fuzz_iterations_arg = std.fmt.allocPrint(b.allocator, "{}", .{fuzz_max_iterations}) catch @panic("oom");
44
45 const fuzz_seed = b.option(u64, "seed", "Seed to use for the PRNG (default: random)") orelse seed: {
46 var buf: [8]u8 = undefined;
47 try std.posix.getrandom(&buf);
48 break :seed std.mem.readInt(u64, &buf, builtin.cpu.arch.endian());
49 };
50 const fuzz_seed_arg = std.fmt.allocPrint(b.allocator, "{}", .{fuzz_seed}) catch @panic("oom");
51
52 const run_gnu = b.addRunArtifact(fuzz);
53 run_gnu.setName("fuzz-gnu");
54 run_gnu.addArtifactArg(verify_gnu);
55 run_gnu.addArgs(&.{ fuzz_iterations_arg, fuzz_seed_arg });
56 run_gnu.expectExitCode(0);
57
58 test_step.dependOn(&run_gnu.step);
59
60 // Only target the MSVC ABI if MSVC/Windows SDK is available
61 const has_msvc = has_msvc: {
62 const sdk = std.zig.WindowsSdk.find(b.allocator) catch |err| switch (err) {
63 error.OutOfMemory => @panic("oom"),
64 else => break :has_msvc false,
65 };
66 defer sdk.free(b.allocator);
67 break :has_msvc true;
68 };
69 if (has_msvc) {
70 const lib_msvc = b.addStaticLibrary(.{
71 .name = "toargv-msvc",
72 .root_source_file = .{ .path = "lib.zig" },
73 .target = b.resolveTargetQuery(.{
74 .abi = .msvc,
75 }),
76 .optimize = optimize,
77 });
78 const verify_msvc = b.addExecutable(.{
79 .name = "verify-msvc",
80 .target = b.resolveTargetQuery(.{
81 .abi = .msvc,
82 }),
83 .optimize = optimize,
84 });
85 verify_msvc.addCSourceFile(.{
86 .file = .{ .path = "verify.c" },
87 .flags = &.{ "-DUNICODE", "-D_UNICODE" },
88 });
89 verify_msvc.linkLibrary(lib_msvc);
90 verify_msvc.linkLibC();
91
92 const run_msvc = b.addRunArtifact(fuzz);
93 run_msvc.setName("fuzz-msvc");
94 run_msvc.addArtifactArg(verify_msvc);
95 run_msvc.addArgs(&.{ fuzz_iterations_arg, fuzz_seed_arg });
96 run_msvc.expectExitCode(0);
97
98 test_step.dependOn(&run_msvc.step);
99 }
100}
test/standalone/windows_argv/fuzz.zig created+159
......@@ -0,0 +1,159 @@
1const std = @import("std");
2const builtin = @import("builtin");
3const windows = std.os.windows;
4const Allocator = std.mem.Allocator;
5
6pub fn main() !void {
7 var gpa = std.heap.GeneralPurposeAllocator(.{}){};
8 defer std.debug.assert(gpa.deinit() == .ok);
9 const allocator = gpa.allocator();
10
11 const args = try std.process.argsAlloc(allocator);
12 defer std.process.argsFree(allocator, args);
13
14 if (args.len < 2) return error.MissingArgs;
15
16 const verify_path_wtf8 = args[1];
17 const verify_path_w = try std.unicode.wtf8ToWtf16LeAllocZ(allocator, verify_path_wtf8);
18 defer allocator.free(verify_path_w);
19
20 const iterations: u64 = iterations: {
21 if (args.len < 3) break :iterations 0;
22 break :iterations try std.fmt.parseUnsigned(u64, args[2], 10);
23 };
24
25 var rand_seed = false;
26 const seed: u64 = seed: {
27 if (args.len < 4) {
28 rand_seed = true;
29 var buf: [8]u8 = undefined;
30 try std.posix.getrandom(&buf);
31 break :seed std.mem.readInt(u64, &buf, builtin.cpu.arch.endian());
32 }
33 break :seed try std.fmt.parseUnsigned(u64, args[3], 10);
34 };
35 var random = std.rand.DefaultPrng.init(seed);
36 const rand = random.random();
37
38 // If the seed was not given via the CLI, then output the
39 // randomly chosen seed so that this run can be reproduced
40 if (rand_seed) {
41 std.debug.print("rand seed: {}\n", .{seed});
42 }
43
44 var cmd_line_w_buf = std.ArrayList(u16).init(allocator);
45 defer cmd_line_w_buf.deinit();
46
47 var i: u64 = 0;
48 var errors: u64 = 0;
49 while (iterations == 0 or i < iterations) {
50 const cmd_line_w = try randomCommandLineW(allocator, rand);
51 defer allocator.free(cmd_line_w);
52
53 // avoid known difference for 0-length command lines
54 if (cmd_line_w.len == 0 or cmd_line_w[0] == '\x00') continue;
55
56 const exit_code = try spawnVerify(verify_path_w, cmd_line_w);
57 if (exit_code != 0) {
58 std.debug.print(">>> found discrepancy <<<\n", .{});
59 const cmd_line_wtf8 = try std.unicode.wtf16LeToWtf8Alloc(allocator, cmd_line_w);
60 defer allocator.free(cmd_line_wtf8);
61 std.debug.print("\"{}\"\n\n", .{std.zig.fmtEscapes(cmd_line_wtf8)});
62
63 errors += 1;
64 }
65
66 i += 1;
67 }
68 if (errors > 0) {
69 // we never get here if iterations is 0 so we don't have to worry about that case
70 std.debug.print("found {} discrepancies in {} iterations\n", .{ errors, iterations });
71 return error.FoundDiscrepancies;
72 }
73}
74
75fn randomCommandLineW(allocator: Allocator, rand: std.rand.Random) ![:0]const u16 {
76 const Choice = enum {
77 backslash,
78 quote,
79 space,
80 tab,
81 control,
82 printable,
83 non_ascii,
84 };
85
86 const choices = rand.uintAtMostBiased(u16, 256);
87 var buf = try std.ArrayList(u16).initCapacity(allocator, choices);
88 errdefer buf.deinit();
89
90 for (0..choices) |_| {
91 const choice = rand.enumValue(Choice);
92 const code_unit = switch (choice) {
93 .backslash => '\\',
94 .quote => '"',
95 .space => ' ',
96 .tab => '\t',
97 .control => switch (rand.uintAtMostBiased(u8, 0x21)) {
98 0x21 => '\x7F',
99 else => |b| b,
100 },
101 .printable => '!' + rand.uintAtMostBiased(u8, '~' - '!'),
102 .non_ascii => rand.intRangeAtMostBiased(u16, 0x80, 0xFFFF),
103 };
104 try buf.append(std.mem.nativeToLittle(u16, code_unit));
105 }
106
107 return buf.toOwnedSliceSentinel(0);
108}
109
110/// Returns the exit code of the verify process
111fn spawnVerify(verify_path: [:0]const u16, cmd_line: [:0]const u16) !windows.DWORD {
112 const child_proc = spawn: {
113 var startup_info: windows.STARTUPINFOW = .{
114 .cb = @sizeOf(windows.STARTUPINFOW),
115 .lpReserved = null,
116 .lpDesktop = null,
117 .lpTitle = null,
118 .dwX = 0,
119 .dwY = 0,
120 .dwXSize = 0,
121 .dwYSize = 0,
122 .dwXCountChars = 0,
123 .dwYCountChars = 0,
124 .dwFillAttribute = 0,
125 .dwFlags = windows.STARTF_USESTDHANDLES,
126 .wShowWindow = 0,
127 .cbReserved2 = 0,
128 .lpReserved2 = null,
129 .hStdInput = null,
130 .hStdOutput = null,
131 .hStdError = windows.GetStdHandle(windows.STD_ERROR_HANDLE) catch null,
132 };
133 var proc_info: windows.PROCESS_INFORMATION = undefined;
134
135 try windows.CreateProcessW(
136 @constCast(verify_path.ptr),
137 @constCast(cmd_line.ptr),
138 null,
139 null,
140 windows.TRUE,
141 0,
142 null,
143 null,
144 &startup_info,
145 &proc_info,
146 );
147 windows.CloseHandle(proc_info.hThread);
148
149 break :spawn proc_info.hProcess;
150 };
151 defer windows.CloseHandle(child_proc);
152 try windows.WaitForSingleObjectEx(child_proc, windows.INFINITE, false);
153
154 var exit_code: windows.DWORD = undefined;
155 if (windows.kernel32.GetExitCodeProcess(child_proc, &exit_code) == 0) {
156 return error.UnableToGetExitCode;
157 }
158 return exit_code;
159}
test/standalone/windows_argv/lib.h created+8
......@@ -0,0 +1,8 @@
1#ifndef _LIB_H_
2#define _LIB_H_
3
4#include <windows.h>
5
6int verify(int argc, wchar_t *argv[]);
7
8#endif
\ No newline at end of file
test/standalone/windows_argv/lib.zig created+59
......@@ -0,0 +1,59 @@
1const std = @import("std");
2
3/// Returns 1 on success, 0 on failure
4export fn verify(argc: c_int, argv: [*]const [*:0]const u16) c_int {
5 const argv_slice = argv[0..@intCast(argc)];
6 testArgv(argv_slice) catch |err| switch (err) {
7 error.OutOfMemory => @panic("oom"),
8 error.Overflow => @panic("bytes needed to contain args would overflow usize"),
9 error.ArgvMismatch => return 0,
10 };
11 return 1;
12}
13
14fn testArgv(expected_args: []const [*:0]const u16) !void {
15 var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator);
16 defer arena_state.deinit();
17 const allocator = arena_state.allocator();
18
19 const args = try std.process.argsAlloc(allocator);
20 var wtf8_buf = std.ArrayList(u8).init(allocator);
21
22 var eql = true;
23 if (args.len != expected_args.len) eql = false;
24
25 const min_len = @min(expected_args.len, args.len);
26 for (expected_args[0..min_len], args[0..min_len], 0..) |expected_arg, arg_wtf8, i| {
27 wtf8_buf.clearRetainingCapacity();
28 try std.unicode.wtf16LeToWtf8ArrayList(&wtf8_buf, std.mem.span(expected_arg));
29 if (!std.mem.eql(u8, wtf8_buf.items, arg_wtf8)) {
30 std.debug.print("{}: expected: \"{}\"\n", .{ i, std.zig.fmtEscapes(wtf8_buf.items) });
31 std.debug.print("{}: actual: \"{}\"\n", .{ i, std.zig.fmtEscapes(arg_wtf8) });
32 eql = false;
33 }
34 }
35 if (!eql) {
36 for (expected_args[min_len..], min_len..) |arg, i| {
37 wtf8_buf.clearRetainingCapacity();
38 try std.unicode.wtf16LeToWtf8ArrayList(&wtf8_buf, std.mem.span(arg));
39 std.debug.print("{}: expected: \"{}\"\n", .{ i, std.zig.fmtEscapes(wtf8_buf.items) });
40 }
41 for (args[min_len..], min_len..) |arg, i| {
42 std.debug.print("{}: actual: \"{}\"\n", .{ i, std.zig.fmtEscapes(arg) });
43 }
44 const peb = std.os.windows.peb();
45 const lpCmdLine: [*:0]u16 = @ptrCast(peb.ProcessParameters.CommandLine.Buffer);
46 wtf8_buf.clearRetainingCapacity();
47 try std.unicode.wtf16LeToWtf8ArrayList(&wtf8_buf, std.mem.span(lpCmdLine));
48 std.debug.print("command line: \"{}\"\n", .{std.zig.fmtEscapes(wtf8_buf.items)});
49 std.debug.print("expected argv:\n", .{});
50 std.debug.print("&.{{\n", .{});
51 for (expected_args) |arg| {
52 wtf8_buf.clearRetainingCapacity();
53 try std.unicode.wtf16LeToWtf8ArrayList(&wtf8_buf, std.mem.span(arg));
54 std.debug.print(" \"{}\",\n", .{std.zig.fmtEscapes(wtf8_buf.items)});
55 }
56 std.debug.print("}}\n", .{});
57 return error.ArgvMismatch;
58 }
59}
test/standalone/windows_argv/verify.c created+7
......@@ -0,0 +1,7 @@
1#include <windows.h>
2#include "lib.h"
3
4int wmain(int argc, wchar_t *argv[]) {
5 if (!verify(argc, argv)) return 1;
6 return 0;
7}
\ No newline at end of file