master
1const std = @import("std");
2const builtin = @import("builtin");
3const Allocator = std.mem.Allocator;
4
5pub fn main() anyerror!void {
6 var debug_alloc_inst: std.heap.DebugAllocator(.{}) = .init;
7 defer std.debug.assert(debug_alloc_inst.deinit() == .ok);
8 const gpa = debug_alloc_inst.allocator();
9
10 var it = try std.process.argsWithAllocator(gpa);
11 defer it.deinit();
12 _ = it.next() orelse unreachable; // skip binary name
13 const child_exe_path_orig = it.next() orelse unreachable;
14
15 const iterations: u64 = iterations: {
16 const arg = it.next() orelse "0";
17 break :iterations try std.fmt.parseUnsigned(u64, arg, 10);
18 };
19
20 var rand_seed = false;
21 const seed: u64 = seed: {
22 const seed_arg = it.next() orelse {
23 rand_seed = true;
24 var buf: [8]u8 = undefined;
25 try std.posix.getrandom(&buf);
26 break :seed std.mem.readInt(u64, &buf, builtin.cpu.arch.endian());
27 };
28 break :seed try std.fmt.parseUnsigned(u64, seed_arg, 10);
29 };
30 var random = std.Random.DefaultPrng.init(seed);
31 const rand = random.random();
32
33 // If the seed was not given via the CLI, then output the
34 // randomly chosen seed so that this run can be reproduced
35 if (rand_seed) {
36 std.debug.print("rand seed: {}\n", .{seed});
37 }
38
39 var tmp = std.testing.tmpDir(.{});
40 defer tmp.cleanup();
41
42 try tmp.dir.setAsCwd();
43 defer tmp.parent_dir.setAsCwd() catch {};
44
45 // `child_exe_path_orig` might be relative; make it relative to our new cwd.
46 const child_exe_path = try std.fs.path.resolve(gpa, &.{ "..\\..\\..", child_exe_path_orig });
47 defer gpa.free(child_exe_path);
48
49 var buf: std.ArrayList(u8) = .empty;
50 defer buf.deinit(gpa);
51 try buf.print(gpa,
52 \\@echo off
53 \\"{s}"
54 , .{child_exe_path});
55 // Trailing newline intentionally omitted above so we can add args.
56 const preamble_len = buf.items.len;
57
58 try buf.appendSlice(gpa, " %*");
59 try tmp.dir.writeFile(.{ .sub_path = "args1.bat", .data = buf.items });
60 buf.shrinkRetainingCapacity(preamble_len);
61
62 try buf.appendSlice(gpa, " %1 %2 %3 %4 %5 %6 %7 %8 %9");
63 try tmp.dir.writeFile(.{ .sub_path = "args2.bat", .data = buf.items });
64 buf.shrinkRetainingCapacity(preamble_len);
65
66 try buf.appendSlice(gpa, " \"%~1\" \"%~2\" \"%~3\" \"%~4\" \"%~5\" \"%~6\" \"%~7\" \"%~8\" \"%~9\"");
67 try tmp.dir.writeFile(.{ .sub_path = "args3.bat", .data = buf.items });
68 buf.shrinkRetainingCapacity(preamble_len);
69
70 var i: u64 = 0;
71 while (iterations == 0 or i < iterations) {
72 const rand_arg = try randomArg(gpa, rand);
73 defer gpa.free(rand_arg);
74
75 try testExec(gpa, &.{rand_arg}, null);
76
77 i += 1;
78 }
79}
80
81fn testExec(gpa: std.mem.Allocator, args: []const []const u8, env: ?*std.process.EnvMap) !void {
82 try testExecBat(gpa, "args1.bat", args, env);
83 try testExecBat(gpa, "args2.bat", args, env);
84 try testExecBat(gpa, "args3.bat", args, env);
85}
86
87fn testExecBat(gpa: std.mem.Allocator, bat: []const u8, args: []const []const u8, env: ?*std.process.EnvMap) !void {
88 const argv = try gpa.alloc([]const u8, 1 + args.len);
89 defer gpa.free(argv);
90 argv[0] = bat;
91 @memcpy(argv[1..], args);
92
93 const can_have_trailing_empty_args = std.mem.eql(u8, bat, "args3.bat");
94
95 const result = try std.process.Child.run(.{
96 .allocator = gpa,
97 .env_map = env,
98 .argv = argv,
99 });
100 defer gpa.free(result.stdout);
101 defer gpa.free(result.stderr);
102
103 try std.testing.expectEqualStrings("", result.stderr);
104 var it = std.mem.splitScalar(u8, result.stdout, '\x00');
105 var i: usize = 0;
106 while (it.next()) |actual_arg| {
107 if (i >= args.len and can_have_trailing_empty_args) {
108 try std.testing.expectEqualStrings("", actual_arg);
109 continue;
110 }
111 const expected_arg = args[i];
112 try std.testing.expectEqualSlices(u8, expected_arg, actual_arg);
113 i += 1;
114 }
115}
116
117fn randomArg(gpa: Allocator, rand: std.Random) ![]const u8 {
118 const Choice = enum {
119 backslash,
120 quote,
121 space,
122 control,
123 printable,
124 surrogate_half,
125 non_ascii,
126 };
127
128 const choices = rand.uintAtMostBiased(u16, 256);
129 var buf: std.ArrayList(u8) = try .initCapacity(gpa, choices);
130 errdefer buf.deinit(gpa);
131
132 var last_codepoint: u21 = 0;
133 for (0..choices) |_| {
134 const choice = rand.enumValue(Choice);
135 const codepoint: u21 = switch (choice) {
136 .backslash => '\\',
137 .quote => '"',
138 .space => ' ',
139 .control => switch (rand.uintAtMostBiased(u8, 0x21)) {
140 // NUL/CR/LF can't roundtrip
141 '\x00', '\r', '\n' => ' ',
142 0x21 => '\x7F',
143 else => |b| b,
144 },
145 .printable => '!' + rand.uintAtMostBiased(u8, '~' - '!'),
146 .surrogate_half => rand.intRangeAtMostBiased(u16, 0xD800, 0xDFFF),
147 .non_ascii => rand.intRangeAtMostBiased(u21, 0x80, 0x10FFFF),
148 };
149 // Ensure that we always return well-formed WTF-8.
150 // Instead of concatenating to ensure well-formed WTF-8,
151 // we just skip encoding the low surrogate.
152 if (std.unicode.isSurrogateCodepoint(last_codepoint) and std.unicode.isSurrogateCodepoint(codepoint)) {
153 if (std.unicode.utf16IsHighSurrogate(@intCast(last_codepoint)) and std.unicode.utf16IsLowSurrogate(@intCast(codepoint))) {
154 continue;
155 }
156 }
157 try buf.ensureUnusedCapacity(gpa, 4);
158 const unused_slice = buf.unusedCapacitySlice();
159 const len = std.unicode.wtf8Encode(codepoint, unused_slice) catch unreachable;
160 buf.items.len += len;
161 last_codepoint = codepoint;
162 }
163
164 return buf.toOwnedSlice(gpa);
165}