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}