| ... | ... | @@ -1,14 +1,14 @@ |
| 1 | 1 | const std = @import("std"); |
| 2 | 2 | |
| 3 | 3 | pub fn main() anyerror!void { |
| 4 | | var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init; |
| 5 | | defer if (gpa.deinit() == .leak) @panic("found memory leaks"); |
| 6 | | const allocator = gpa.allocator(); |
| 4 | var debug_alloc_inst: std.heap.DebugAllocator(.{}) = .init; |
| 5 | defer std.debug.assert(debug_alloc_inst.deinit() == .ok); |
| 6 | const gpa = debug_alloc_inst.allocator(); |
| 7 | 7 | |
| 8 | | var it = try std.process.argsWithAllocator(allocator); |
| 8 | var it = try std.process.argsWithAllocator(gpa); |
| 9 | 9 | defer it.deinit(); |
| 10 | 10 | _ = it.next() orelse unreachable; // skip binary name |
| 11 | | const child_exe_path = it.next() orelse unreachable; |
| 11 | const child_exe_path_orig = it.next() orelse unreachable; |
| 12 | 12 | |
| 13 | 13 | var tmp = std.testing.tmpDir(.{}); |
| 14 | 14 | defer tmp.cleanup(); |
| ... | ... | @@ -16,62 +16,67 @@ pub fn main() anyerror!void { |
| 16 | 16 | try tmp.dir.setAsCwd(); |
| 17 | 17 | defer tmp.parent_dir.setAsCwd() catch {}; |
| 18 | 18 | |
| 19 | | var buf = try std.array_list.Managed(u8).initCapacity(allocator, 128); |
| 20 | | defer buf.deinit(); |
| 21 | | try buf.appendSlice("@echo off\n"); |
| 22 | | try buf.append('"'); |
| 23 | | try buf.appendSlice(child_exe_path); |
| 24 | | try buf.append('"'); |
| 19 | // `child_exe_path_orig` might be relative; make it relative to our new cwd. |
| 20 | const child_exe_path = try std.fs.path.resolve(gpa, &.{ "..\\..\\..", child_exe_path_orig }); |
| 21 | defer gpa.free(child_exe_path); |
| 22 | |
| 23 | var buf: std.ArrayList(u8) = .empty; |
| 24 | defer buf.deinit(gpa); |
| 25 | try buf.print(gpa, |
| 26 | \\@echo off |
| 27 | \\"{s}" |
| 28 | , .{child_exe_path}); |
| 29 | // Trailing newline intentionally omitted above so we can add args. |
| 25 | 30 | const preamble_len = buf.items.len; |
| 26 | 31 | |
| 27 | | try buf.appendSlice(" %*"); |
| 32 | try buf.appendSlice(gpa, " %*"); |
| 28 | 33 | try tmp.dir.writeFile(.{ .sub_path = "args1.bat", .data = buf.items }); |
| 29 | 34 | buf.shrinkRetainingCapacity(preamble_len); |
| 30 | 35 | |
| 31 | | try buf.appendSlice(" %1 %2 %3 %4 %5 %6 %7 %8 %9"); |
| 36 | try buf.appendSlice(gpa, " %1 %2 %3 %4 %5 %6 %7 %8 %9"); |
| 32 | 37 | try tmp.dir.writeFile(.{ .sub_path = "args2.bat", .data = buf.items }); |
| 33 | 38 | buf.shrinkRetainingCapacity(preamble_len); |
| 34 | 39 | |
| 35 | | try buf.appendSlice(" \"%~1\" \"%~2\" \"%~3\" \"%~4\" \"%~5\" \"%~6\" \"%~7\" \"%~8\" \"%~9\""); |
| 40 | try buf.appendSlice(gpa, " \"%~1\" \"%~2\" \"%~3\" \"%~4\" \"%~5\" \"%~6\" \"%~7\" \"%~8\" \"%~9\""); |
| 36 | 41 | try tmp.dir.writeFile(.{ .sub_path = "args3.bat", .data = buf.items }); |
| 37 | 42 | buf.shrinkRetainingCapacity(preamble_len); |
| 38 | 43 | |
| 39 | 44 | // Test cases are from https://github.com/rust-lang/rust/blob/master/tests/ui/std/windows-bat-args.rs |
| 40 | | try testExecError(error.InvalidBatchScriptArg, allocator, &.{"\x00"}); |
| 41 | | try testExecError(error.InvalidBatchScriptArg, allocator, &.{"\n"}); |
| 42 | | try testExecError(error.InvalidBatchScriptArg, allocator, &.{"\r"}); |
| 43 | | try testExec(allocator, &.{ "a", "b" }, null); |
| 44 | | try testExec(allocator, &.{ "c is for cat", "d is for dog" }, null); |
| 45 | | try testExec(allocator, &.{ "\"", " \"" }, null); |
| 46 | | try testExec(allocator, &.{ "\\", "\\" }, null); |
| 47 | | try testExec(allocator, &.{">file.txt"}, null); |
| 48 | | try testExec(allocator, &.{"whoami.exe"}, null); |
| 49 | | try testExec(allocator, &.{"&a.exe"}, null); |
| 50 | | try testExec(allocator, &.{"&echo hello "}, null); |
| 51 | | try testExec(allocator, &.{ "&echo hello", "&whoami", ">file.txt" }, null); |
| 52 | | try testExec(allocator, &.{"!TMP!"}, null); |
| 53 | | try testExec(allocator, &.{"key=value"}, null); |
| 54 | | try testExec(allocator, &.{"\"key=value\""}, null); |
| 55 | | try testExec(allocator, &.{"key = value"}, null); |
| 56 | | try testExec(allocator, &.{"key=[\"value\"]"}, null); |
| 57 | | try testExec(allocator, &.{ "", "a=b" }, null); |
| 58 | | try testExec(allocator, &.{"key=\"foo bar\""}, null); |
| 59 | | try testExec(allocator, &.{"key=[\"my_value]"}, null); |
| 60 | | try testExec(allocator, &.{"key=[\"my_value\",\"other-value\"]"}, null); |
| 61 | | try testExec(allocator, &.{"key\\=value"}, null); |
| 62 | | try testExec(allocator, &.{"key=\"&whoami\""}, null); |
| 63 | | try testExec(allocator, &.{"key=\"value\"=5"}, null); |
| 64 | | try testExec(allocator, &.{"key=[\">file.txt\"]"}, null); |
| 65 | | try testExec(allocator, &.{"%hello"}, null); |
| 66 | | try testExec(allocator, &.{"%PATH%"}, null); |
| 67 | | try testExec(allocator, &.{"%%cd:~,%"}, null); |
| 68 | | try testExec(allocator, &.{"%PATH%PATH%"}, null); |
| 69 | | try testExec(allocator, &.{"\">file.txt"}, null); |
| 70 | | try testExec(allocator, &.{"abc\"&echo hello"}, null); |
| 71 | | try testExec(allocator, &.{"123\">file.txt"}, null); |
| 72 | | try testExec(allocator, &.{"\"&echo hello&whoami.exe"}, null); |
| 73 | | try testExec(allocator, &.{ "\"hello^\"world\"", "hello &echo oh no >file.txt" }, null); |
| 74 | | try testExec(allocator, &.{"&whoami.exe"}, null); |
| 45 | try testExecError(error.InvalidBatchScriptArg, gpa, &.{"\x00"}); |
| 46 | try testExecError(error.InvalidBatchScriptArg, gpa, &.{"\n"}); |
| 47 | try testExecError(error.InvalidBatchScriptArg, gpa, &.{"\r"}); |
| 48 | try testExec(gpa, &.{ "a", "b" }, null); |
| 49 | try testExec(gpa, &.{ "c is for cat", "d is for dog" }, null); |
| 50 | try testExec(gpa, &.{ "\"", " \"" }, null); |
| 51 | try testExec(gpa, &.{ "\\", "\\" }, null); |
| 52 | try testExec(gpa, &.{">file.txt"}, null); |
| 53 | try testExec(gpa, &.{"whoami.exe"}, null); |
| 54 | try testExec(gpa, &.{"&a.exe"}, null); |
| 55 | try testExec(gpa, &.{"&echo hello "}, null); |
| 56 | try testExec(gpa, &.{ "&echo hello", "&whoami", ">file.txt" }, null); |
| 57 | try testExec(gpa, &.{"!TMP!"}, null); |
| 58 | try testExec(gpa, &.{"key=value"}, null); |
| 59 | try testExec(gpa, &.{"\"key=value\""}, null); |
| 60 | try testExec(gpa, &.{"key = value"}, null); |
| 61 | try testExec(gpa, &.{"key=[\"value\"]"}, null); |
| 62 | try testExec(gpa, &.{ "", "a=b" }, null); |
| 63 | try testExec(gpa, &.{"key=\"foo bar\""}, null); |
| 64 | try testExec(gpa, &.{"key=[\"my_value]"}, null); |
| 65 | try testExec(gpa, &.{"key=[\"my_value\",\"other-value\"]"}, null); |
| 66 | try testExec(gpa, &.{"key\\=value"}, null); |
| 67 | try testExec(gpa, &.{"key=\"&whoami\""}, null); |
| 68 | try testExec(gpa, &.{"key=\"value\"=5"}, null); |
| 69 | try testExec(gpa, &.{"key=[\">file.txt\"]"}, null); |
| 70 | try testExec(gpa, &.{"%hello"}, null); |
| 71 | try testExec(gpa, &.{"%PATH%"}, null); |
| 72 | try testExec(gpa, &.{"%%cd:~,%"}, null); |
| 73 | try testExec(gpa, &.{"%PATH%PATH%"}, null); |
| 74 | try testExec(gpa, &.{"\">file.txt"}, null); |
| 75 | try testExec(gpa, &.{"abc\"&echo hello"}, null); |
| 76 | try testExec(gpa, &.{"123\">file.txt"}, null); |
| 77 | try testExec(gpa, &.{"\"&echo hello&whoami.exe"}, null); |
| 78 | try testExec(gpa, &.{ "\"hello^\"world\"", "hello &echo oh no >file.txt" }, null); |
| 79 | try testExec(gpa, &.{"&whoami.exe"}, null); |
| 75 | 80 | |
| 76 | 81 | // Ensure that trailing space and . characters can't lead to unexpected bat/cmd script execution. |
| 77 | 82 | // In many Windows APIs (including CreateProcess), trailing space and . characters are stripped |
| ... | ... | @@ -89,17 +94,17 @@ pub fn main() anyerror!void { |
| 89 | 94 | // > "args1.bat .. " |
| 90 | 95 | // '"args1.bat .. "' is not recognized as an internal or external command, |
| 91 | 96 | // operable program or batch file. |
| 92 | | try std.testing.expectError(error.FileNotFound, testExecBat(allocator, "args1.bat .. ", &.{"abc"}, null)); |
| 97 | try std.testing.expectError(error.FileNotFound, testExecBat(gpa, "args1.bat .. ", &.{"abc"}, null)); |
| 93 | 98 | const absolute_with_trailing = blk: { |
| 94 | | const absolute_path = try std.fs.realpathAlloc(allocator, "args1.bat"); |
| 95 | | defer allocator.free(absolute_path); |
| 96 | | break :blk try std.mem.concat(allocator, u8, &.{ absolute_path, " .. " }); |
| 99 | const absolute_path = try std.fs.realpathAlloc(gpa, "args1.bat"); |
| 100 | defer gpa.free(absolute_path); |
| 101 | break :blk try std.mem.concat(gpa, u8, &.{ absolute_path, " .. " }); |
| 97 | 102 | }; |
| 98 | | defer allocator.free(absolute_with_trailing); |
| 99 | | try std.testing.expectError(error.FileNotFound, testExecBat(allocator, absolute_with_trailing, &.{"abc"}, null)); |
| 103 | defer gpa.free(absolute_with_trailing); |
| 104 | try std.testing.expectError(error.FileNotFound, testExecBat(gpa, absolute_with_trailing, &.{"abc"}, null)); |
| 100 | 105 | |
| 101 | 106 | var env = env: { |
| 102 | | var env = try std.process.getEnvMap(allocator); |
| 107 | var env = try std.process.getEnvMap(gpa); |
| 103 | 108 | errdefer env.deinit(); |
| 104 | 109 | // No escaping |
| 105 | 110 | try env.put("FOO", "123"); |
| ... | ... | @@ -110,37 +115,37 @@ pub fn main() anyerror!void { |
| 110 | 115 | break :env env; |
| 111 | 116 | }; |
| 112 | 117 | defer env.deinit(); |
| 113 | | try testExec(allocator, &.{"%FOO%"}, &env); |
| 118 | try testExec(gpa, &.{"%FOO%"}, &env); |
| 114 | 119 | |
| 115 | 120 | // Ensure that none of the `>file.txt`s have caused file.txt to be created |
| 116 | 121 | try std.testing.expectError(error.FileNotFound, tmp.dir.access("file.txt", .{})); |
| 117 | 122 | } |
| 118 | 123 | |
| 119 | | fn testExecError(err: anyerror, allocator: std.mem.Allocator, args: []const []const u8) !void { |
| 120 | | return std.testing.expectError(err, testExec(allocator, args, null)); |
| 124 | fn testExecError(err: anyerror, gpa: std.mem.Allocator, args: []const []const u8) !void { |
| 125 | return std.testing.expectError(err, testExec(gpa, args, null)); |
| 121 | 126 | } |
| 122 | 127 | |
| 123 | | fn testExec(allocator: std.mem.Allocator, args: []const []const u8, env: ?*std.process.EnvMap) !void { |
| 124 | | try testExecBat(allocator, "args1.bat", args, env); |
| 125 | | try testExecBat(allocator, "args2.bat", args, env); |
| 126 | | try testExecBat(allocator, "args3.bat", args, env); |
| 128 | fn testExec(gpa: std.mem.Allocator, args: []const []const u8, env: ?*std.process.EnvMap) !void { |
| 129 | try testExecBat(gpa, "args1.bat", args, env); |
| 130 | try testExecBat(gpa, "args2.bat", args, env); |
| 131 | try testExecBat(gpa, "args3.bat", args, env); |
| 127 | 132 | } |
| 128 | 133 | |
| 129 | | fn testExecBat(allocator: std.mem.Allocator, bat: []const u8, args: []const []const u8, env: ?*std.process.EnvMap) !void { |
| 130 | | var argv = try std.array_list.Managed([]const u8).initCapacity(allocator, 1 + args.len); |
| 131 | | defer argv.deinit(); |
| 132 | | argv.appendAssumeCapacity(bat); |
| 133 | | argv.appendSliceAssumeCapacity(args); |
| 134 | fn testExecBat(gpa: std.mem.Allocator, bat: []const u8, args: []const []const u8, env: ?*std.process.EnvMap) !void { |
| 135 | const argv = try gpa.alloc([]const u8, 1 + args.len); |
| 136 | defer gpa.free(argv); |
| 137 | argv[0] = bat; |
| 138 | @memcpy(argv[1..], args); |
| 134 | 139 | |
| 135 | 140 | const can_have_trailing_empty_args = std.mem.eql(u8, bat, "args3.bat"); |
| 136 | 141 | |
| 137 | 142 | const result = try std.process.Child.run(.{ |
| 138 | | .allocator = allocator, |
| 143 | .allocator = gpa, |
| 139 | 144 | .env_map = env, |
| 140 | | .argv = argv.items, |
| 145 | .argv = argv, |
| 141 | 146 | }); |
| 142 | | defer allocator.free(result.stdout); |
| 143 | | defer allocator.free(result.stderr); |
| 147 | defer gpa.free(result.stdout); |
| 148 | defer gpa.free(result.stderr); |
| 144 | 149 | |
| 145 | 150 | try std.testing.expectEqualStrings("", result.stderr); |
| 146 | 151 | var it = std.mem.splitScalar(u8, result.stdout, '\x00'); |