1const std = @import("std");
2const Io = std.Io;
3const Allocator = std.mem.Allocator;
4
5pub fn main(init: std.process.Init) !void {
6 const gpa = init.gpa;
7 const io = init.io;
8
9 // Test cases are from https://github.com/rust-lang/rust/blob/master/tests/ui/std/windows-bat-args.rs
10 try testExecError(error.InvalidBatchScriptArg, gpa, io, &.{"\x00"});
11 try testExecError(error.InvalidBatchScriptArg, gpa, io, &.{"\n"});
12 try testExecError(error.InvalidBatchScriptArg, gpa, io, &.{"\r"});
13 try testExec(gpa, io, &.{ "a", "b" }, null);
14 try testExec(gpa, io, &.{ "c is for cat", "d is for dog" }, null);
15 try testExec(gpa, io, &.{ "\"", " \"" }, null);
16 try testExec(gpa, io, &.{ "\\", "\\" }, null);
17 try testExec(gpa, io, &.{">file.txt"}, null);
18 try testExec(gpa, io, &.{"whoami.exe"}, null);
19 try testExec(gpa, io, &.{"&a.exe"}, null);
20 try testExec(gpa, io, &.{"&echo hello "}, null);
21 try testExec(gpa, io, &.{ "&echo hello", "&whoami", ">file.txt" }, null);
22 try testExec(gpa, io, &.{"!TMP!"}, null);
23 try testExec(gpa, io, &.{"key=value"}, null);
24 try testExec(gpa, io, &.{"\"key=value\""}, null);
25 try testExec(gpa, io, &.{"key = value"}, null);
26 try testExec(gpa, io, &.{"key=[\"value\"]"}, null);
27 try testExec(gpa, io, &.{ "", "a=b" }, null);
28 try testExec(gpa, io, &.{"key=\"foo bar\""}, null);
29 try testExec(gpa, io, &.{"key=[\"my_value]"}, null);
30 try testExec(gpa, io, &.{"key=[\"my_value\",\"other-value\"]"}, null);
31 try testExec(gpa, io, &.{"key\\=value"}, null);
32 try testExec(gpa, io, &.{"key=\"&whoami\""}, null);
33 try testExec(gpa, io, &.{"key=\"value\"=5"}, null);
34 try testExec(gpa, io, &.{"key=[\">file.txt\"]"}, null);
35 try testExec(gpa, io, &.{"%hello"}, null);
36 try testExec(gpa, io, &.{"%PATH%"}, null);
37 try testExec(gpa, io, &.{"%%cd:~,%"}, null);
38 try testExec(gpa, io, &.{"%PATH%PATH%"}, null);
39 try testExec(gpa, io, &.{"\">file.txt"}, null);
40 try testExec(gpa, io, &.{"abc\"&echo hello"}, null);
41 try testExec(gpa, io, &.{"123\">file.txt"}, null);
42 try testExec(gpa, io, &.{"\"&echo hello&whoami.exe"}, null);
43 try testExec(gpa, io, &.{ "\"hello^\"world\"", "hello &echo oh no >file.txt" }, null);
44 try testExec(gpa, io, &.{"&whoami.exe"}, null);
45
46 // Ensure that trailing space and . characters can't lead to unexpected bat/cmd script execution.
47 // In many Windows APIs (including CreateProcess), trailing space and . characters are stripped
48 // from paths, so if a path with trailing . and space character(s) is passed directly to
49 // CreateProcess, then it could end up executing a batch/cmd script that naive extension detection
50 // would not flag as .bat/.cmd.
51 //
52 // Note that we expect an error here, though, which *is* a valid mitigation, but also an implementation detail.
53 // This error is caused by the use of a wildcard with NtQueryDirectoryFile to optimize PATHEXT searching. That is,
54 // the trailing characters in the app name will lead to a FileNotFound error as the wildcard-appended path will not
55 // match any real paths on the filesystem (e.g. `foo.bat .. *` will not match `foo.bat`; only `foo.bat*` will).
56 //
57 // This being an error matches the behavior of running a command via the command line of cmd.exe, too:
58 //
59 // > "args1.bat .. "
60 // '"args1.bat .. "' is not recognized as an internal or external command,
61 // operable program or batch file.
62 try std.testing.expectError(error.FileNotFound, testExecBat(gpa, io, "args1.bat .. ", &.{"abc"}, null));
63 const absolute_with_trailing = blk: {
64 const absolute_path = try Io.Dir.cwd().realPathFileAlloc(io, "args1.bat", gpa);
65 defer gpa.free(absolute_path);
66 break :blk try std.mem.concat(gpa, u8, &.{ absolute_path, " .. " });
67 };
68 defer gpa.free(absolute_with_trailing);
69 try std.testing.expectError(error.FileNotFound, testExecBat(gpa, io, absolute_with_trailing, &.{"abc"}, null));
70
71 var env = env: {
72 var env = try init.environ_map.clone(gpa);
73 errdefer env.deinit();
74 // No escaping
75 try env.put("FOO", "123");
76 // Some possible escaping of %FOO% that could be expanded
77 // when escaping cmd.exe meta characters with ^
78 try env.put("FOO^", "123"); // only escaping %
79 try env.put("^F^O^O^", "123"); // escaping every char
80 break :env env;
81 };
82 defer env.deinit();
83 try testExec(gpa, io, &.{"%FOO%"}, &env);
84
85 // Ensure that none of the `>file.txt`s have caused file.txt to be created
86 try std.testing.expectError(error.FileNotFound, Io.Dir.cwd().access(io, "file.txt", .{}));
87}
88
89fn testExecError(err: anyerror, gpa: Allocator, io: Io, args: []const []const u8) !void {
90 return std.testing.expectError(err, testExec(gpa, io, args, null));
91}
92
93fn testExec(gpa: Allocator, io: Io, args: []const []const u8, env: ?*std.process.Environ.Map) !void {
94 try testExecBat(gpa, io, "args1.bat", args, env);
95 try testExecBat(gpa, io, "args2.bat", args, env);
96 try testExecBat(gpa, io, "args3.bat", args, env);
97}
98
99fn testExecBat(gpa: Allocator, io: Io, bat: []const u8, args: []const []const u8, env: ?*std.process.Environ.Map) !void {
100 const argv = try gpa.alloc([]const u8, 1 + args.len);
101 defer gpa.free(argv);
102 argv[0] = bat;
103 @memcpy(argv[1..], args);
104
105 const can_have_trailing_empty_args = std.mem.eql(u8, bat, "args3.bat");
106
107 const result = try std.process.run(gpa, io, .{
108 .environ_map = env,
109 .argv = argv,
110 });
111 defer gpa.free(result.stdout);
112 defer gpa.free(result.stderr);
113
114 try std.testing.expectEqualStrings("", result.stderr);
115 var it = std.mem.splitScalar(u8, result.stdout, '\x00');
116 var i: usize = 0;
117 while (it.next()) |actual_arg| {
118 if (i >= args.len and can_have_trailing_empty_args) {
119 try std.testing.expectEqualStrings("", actual_arg);
120 continue;
121 }
122 const expected_arg = args[i];
123 try std.testing.expectEqualStrings(expected_arg, actual_arg);
124 i += 1;
125 }
126}