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