mlugg f5c8d80e08
windows_bat_args: fix path handling
The input path could be cwd-relative, in which case it must be modified
before it is written into the batch script.

Also, remove usage of deprecated `GeneralPurposeAllocator` alias, rename
`allocator` to `gpa`, use unmanaged `ArrayList`.
2025-09-30 13:44:54 +01:00

166 lines
5.7 KiB
Zig

const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
pub fn main() anyerror!void {
var debug_alloc_inst: std.heap.DebugAllocator(.{}) = .init;
defer std.debug.assert(debug_alloc_inst.deinit() == .ok);
const gpa = debug_alloc_inst.allocator();
var it = try std.process.argsWithAllocator(gpa);
defer it.deinit();
_ = it.next() orelse unreachable; // skip binary name
const child_exe_path_orig = it.next() orelse unreachable;
const iterations: u64 = iterations: {
const arg = it.next() orelse "0";
break :iterations try std.fmt.parseUnsigned(u64, arg, 10);
};
var rand_seed = false;
const seed: u64 = seed: {
const seed_arg = it.next() orelse {
rand_seed = true;
var buf: [8]u8 = undefined;
try std.posix.getrandom(&buf);
break :seed std.mem.readInt(u64, &buf, builtin.cpu.arch.endian());
};
break :seed try std.fmt.parseUnsigned(u64, seed_arg, 10);
};
var random = std.Random.DefaultPrng.init(seed);
const rand = random.random();
// If the seed was not given via the CLI, then output the
// randomly chosen seed so that this run can be reproduced
if (rand_seed) {
std.debug.print("rand seed: {}\n", .{seed});
}
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
try tmp.dir.setAsCwd();
defer tmp.parent_dir.setAsCwd() catch {};
// `child_exe_path_orig` might be relative; make it relative to our new cwd.
const child_exe_path = try std.fs.path.resolve(gpa, &.{ "..\\..\\..", child_exe_path_orig });
defer gpa.free(child_exe_path);
var buf: std.ArrayList(u8) = .empty;
defer buf.deinit(gpa);
try buf.print(gpa,
\\@echo off
\\"{s}"
, .{child_exe_path});
// Trailing newline intentionally omitted above so we can add args.
const preamble_len = buf.items.len;
try buf.appendSlice(gpa, " %*");
try tmp.dir.writeFile(.{ .sub_path = "args1.bat", .data = buf.items });
buf.shrinkRetainingCapacity(preamble_len);
try buf.appendSlice(gpa, " %1 %2 %3 %4 %5 %6 %7 %8 %9");
try tmp.dir.writeFile(.{ .sub_path = "args2.bat", .data = buf.items });
buf.shrinkRetainingCapacity(preamble_len);
try buf.appendSlice(gpa, " \"%~1\" \"%~2\" \"%~3\" \"%~4\" \"%~5\" \"%~6\" \"%~7\" \"%~8\" \"%~9\"");
try tmp.dir.writeFile(.{ .sub_path = "args3.bat", .data = buf.items });
buf.shrinkRetainingCapacity(preamble_len);
var i: u64 = 0;
while (iterations == 0 or i < iterations) {
const rand_arg = try randomArg(gpa, rand);
defer gpa.free(rand_arg);
try testExec(gpa, &.{rand_arg}, null);
i += 1;
}
}
fn testExec(gpa: std.mem.Allocator, args: []const []const u8, env: ?*std.process.EnvMap) !void {
try testExecBat(gpa, "args1.bat", args, env);
try testExecBat(gpa, "args2.bat", args, env);
try testExecBat(gpa, "args3.bat", args, env);
}
fn testExecBat(gpa: std.mem.Allocator, bat: []const u8, args: []const []const u8, env: ?*std.process.EnvMap) !void {
const argv = try gpa.alloc([]const u8, 1 + args.len);
defer gpa.free(argv);
argv[0] = bat;
@memcpy(argv[1..], args);
const can_have_trailing_empty_args = std.mem.eql(u8, bat, "args3.bat");
const result = try std.process.Child.run(.{
.allocator = gpa,
.env_map = env,
.argv = argv,
});
defer gpa.free(result.stdout);
defer gpa.free(result.stderr);
try std.testing.expectEqualStrings("", result.stderr);
var it = std.mem.splitScalar(u8, result.stdout, '\x00');
var i: usize = 0;
while (it.next()) |actual_arg| {
if (i >= args.len and can_have_trailing_empty_args) {
try std.testing.expectEqualStrings("", actual_arg);
continue;
}
const expected_arg = args[i];
try std.testing.expectEqualSlices(u8, expected_arg, actual_arg);
i += 1;
}
}
fn randomArg(gpa: Allocator, rand: std.Random) ![]const u8 {
const Choice = enum {
backslash,
quote,
space,
control,
printable,
surrogate_half,
non_ascii,
};
const choices = rand.uintAtMostBiased(u16, 256);
var buf: std.ArrayList(u8) = try .initCapacity(gpa, choices);
errdefer buf.deinit(gpa);
var last_codepoint: u21 = 0;
for (0..choices) |_| {
const choice = rand.enumValue(Choice);
const codepoint: u21 = switch (choice) {
.backslash => '\\',
.quote => '"',
.space => ' ',
.control => switch (rand.uintAtMostBiased(u8, 0x21)) {
// NUL/CR/LF can't roundtrip
'\x00', '\r', '\n' => ' ',
0x21 => '\x7F',
else => |b| b,
},
.printable => '!' + rand.uintAtMostBiased(u8, '~' - '!'),
.surrogate_half => rand.intRangeAtMostBiased(u16, 0xD800, 0xDFFF),
.non_ascii => rand.intRangeAtMostBiased(u21, 0x80, 0x10FFFF),
};
// Ensure that we always return well-formed WTF-8.
// Instead of concatenating to ensure well-formed WTF-8,
// we just skip encoding the low surrogate.
if (std.unicode.isSurrogateCodepoint(last_codepoint) and std.unicode.isSurrogateCodepoint(codepoint)) {
if (std.unicode.utf16IsHighSurrogate(@intCast(last_codepoint)) and std.unicode.utf16IsLowSurrogate(@intCast(codepoint))) {
continue;
}
}
try buf.ensureUnusedCapacity(gpa, 4);
const unused_slice = buf.unusedCapacitySlice();
const len = std.unicode.wtf8Encode(codepoint, unused_slice) catch unreachable;
buf.items.len += len;
last_codepoint = codepoint;
}
return buf.toOwnedSlice(gpa);
}