mirror of
https://codeberg.org/ziglang/zig.git
synced 2026-04-26 13:01:34 +03:00
201 lines
8.7 KiB
Zig
201 lines
8.7 KiB
Zig
const std = @import("std");
|
|
const Io = std.Io;
|
|
const Allocator = std.mem.Allocator;
|
|
|
|
pub fn main(init: std.process.Init) !void {
|
|
const gpa = init.gpa;
|
|
const io = init.io;
|
|
|
|
var it = try init.minimal.argsAllocator(gpa);
|
|
defer it.deinit();
|
|
_ = it.next() orelse unreachable; // skip binary name
|
|
const child_exe_path_orig = it.next() orelse unreachable;
|
|
|
|
var tmp = tmpDir(io, .{});
|
|
defer tmp.cleanup(io);
|
|
|
|
try std.process.setCurrentDir(io, tmp.dir);
|
|
defer std.process.setCurrentDir(io, tmp.parent_dir) 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(io, .{ .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(io, .{ .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(io, .{ .sub_path = "args3.bat", .data = buf.items });
|
|
buf.shrinkRetainingCapacity(preamble_len);
|
|
|
|
// Test cases are from https://github.com/rust-lang/rust/blob/master/tests/ui/std/windows-bat-args.rs
|
|
try testExecError(error.InvalidBatchScriptArg, gpa, io, &.{"\x00"});
|
|
try testExecError(error.InvalidBatchScriptArg, gpa, io, &.{"\n"});
|
|
try testExecError(error.InvalidBatchScriptArg, gpa, io, &.{"\r"});
|
|
try testExec(gpa, io, &.{ "a", "b" }, null);
|
|
try testExec(gpa, io, &.{ "c is for cat", "d is for dog" }, null);
|
|
try testExec(gpa, io, &.{ "\"", " \"" }, null);
|
|
try testExec(gpa, io, &.{ "\\", "\\" }, null);
|
|
try testExec(gpa, io, &.{">file.txt"}, null);
|
|
try testExec(gpa, io, &.{"whoami.exe"}, null);
|
|
try testExec(gpa, io, &.{"&a.exe"}, null);
|
|
try testExec(gpa, io, &.{"&echo hello "}, null);
|
|
try testExec(gpa, io, &.{ "&echo hello", "&whoami", ">file.txt" }, null);
|
|
try testExec(gpa, io, &.{"!TMP!"}, null);
|
|
try testExec(gpa, io, &.{"key=value"}, null);
|
|
try testExec(gpa, io, &.{"\"key=value\""}, null);
|
|
try testExec(gpa, io, &.{"key = value"}, null);
|
|
try testExec(gpa, io, &.{"key=[\"value\"]"}, null);
|
|
try testExec(gpa, io, &.{ "", "a=b" }, null);
|
|
try testExec(gpa, io, &.{"key=\"foo bar\""}, null);
|
|
try testExec(gpa, io, &.{"key=[\"my_value]"}, null);
|
|
try testExec(gpa, io, &.{"key=[\"my_value\",\"other-value\"]"}, null);
|
|
try testExec(gpa, io, &.{"key\\=value"}, null);
|
|
try testExec(gpa, io, &.{"key=\"&whoami\""}, null);
|
|
try testExec(gpa, io, &.{"key=\"value\"=5"}, null);
|
|
try testExec(gpa, io, &.{"key=[\">file.txt\"]"}, null);
|
|
try testExec(gpa, io, &.{"%hello"}, null);
|
|
try testExec(gpa, io, &.{"%PATH%"}, null);
|
|
try testExec(gpa, io, &.{"%%cd:~,%"}, null);
|
|
try testExec(gpa, io, &.{"%PATH%PATH%"}, null);
|
|
try testExec(gpa, io, &.{"\">file.txt"}, null);
|
|
try testExec(gpa, io, &.{"abc\"&echo hello"}, null);
|
|
try testExec(gpa, io, &.{"123\">file.txt"}, null);
|
|
try testExec(gpa, io, &.{"\"&echo hello&whoami.exe"}, null);
|
|
try testExec(gpa, io, &.{ "\"hello^\"world\"", "hello &echo oh no >file.txt" }, null);
|
|
try testExec(gpa, io, &.{"&whoami.exe"}, null);
|
|
|
|
// Ensure that trailing space and . characters can't lead to unexpected bat/cmd script execution.
|
|
// In many Windows APIs (including CreateProcess), trailing space and . characters are stripped
|
|
// from paths, so if a path with trailing . and space character(s) is passed directly to
|
|
// CreateProcess, then it could end up executing a batch/cmd script that naive extension detection
|
|
// would not flag as .bat/.cmd.
|
|
//
|
|
// Note that we expect an error here, though, which *is* a valid mitigation, but also an implementation detail.
|
|
// This error is caused by the use of a wildcard with NtQueryDirectoryFile to optimize PATHEXT searching. That is,
|
|
// the trailing characters in the app name will lead to a FileNotFound error as the wildcard-appended path will not
|
|
// match any real paths on the filesystem (e.g. `foo.bat .. *` will not match `foo.bat`; only `foo.bat*` will).
|
|
//
|
|
// This being an error matches the behavior of running a command via the command line of cmd.exe, too:
|
|
//
|
|
// > "args1.bat .. "
|
|
// '"args1.bat .. "' is not recognized as an internal or external command,
|
|
// operable program or batch file.
|
|
try std.testing.expectError(error.FileNotFound, testExecBat(gpa, io, "args1.bat .. ", &.{"abc"}, null));
|
|
const absolute_with_trailing = blk: {
|
|
const absolute_path = try Io.Dir.cwd().realPathFileAlloc(io, "args1.bat", gpa);
|
|
defer gpa.free(absolute_path);
|
|
break :blk try std.mem.concat(gpa, u8, &.{ absolute_path, " .. " });
|
|
};
|
|
defer gpa.free(absolute_with_trailing);
|
|
try std.testing.expectError(error.FileNotFound, testExecBat(gpa, io, absolute_with_trailing, &.{"abc"}, null));
|
|
|
|
var env = env: {
|
|
var env = try std.process.getEnvMap(gpa);
|
|
errdefer env.deinit();
|
|
// No escaping
|
|
try env.put("FOO", "123");
|
|
// Some possible escaping of %FOO% that could be expanded
|
|
// when escaping cmd.exe meta characters with ^
|
|
try env.put("FOO^", "123"); // only escaping %
|
|
try env.put("^F^O^O^", "123"); // escaping every char
|
|
break :env env;
|
|
};
|
|
defer env.deinit();
|
|
try testExec(gpa, io, &.{"%FOO%"}, &env);
|
|
|
|
// Ensure that none of the `>file.txt`s have caused file.txt to be created
|
|
try std.testing.expectError(error.FileNotFound, tmp.dir.access(io, "file.txt", .{}));
|
|
}
|
|
|
|
fn testExecError(err: anyerror, gpa: Allocator, io: Io, args: []const []const u8) !void {
|
|
return std.testing.expectError(err, testExec(gpa, io, args, null));
|
|
}
|
|
|
|
fn testExec(gpa: Allocator, io: Io, args: []const []const u8, env: ?*std.process.Environ.Map) !void {
|
|
try testExecBat(gpa, io, "args1.bat", args, env);
|
|
try testExecBat(gpa, io, "args2.bat", args, env);
|
|
try testExecBat(gpa, io, "args3.bat", args, env);
|
|
}
|
|
|
|
fn testExecBat(gpa: Allocator, io: Io, bat: []const u8, args: []const []const u8, env: ?*std.process.Environ.Map) !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.run(gpa, io, .{
|
|
.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.expectEqualStrings(expected_arg, actual_arg);
|
|
i += 1;
|
|
}
|
|
}
|
|
|
|
pub fn tmpDir(io: Io, opts: Io.Dir.OpenOptions) TmpDir {
|
|
var random_bytes: [TmpDir.random_bytes_count]u8 = undefined;
|
|
std.crypto.random.bytes(&random_bytes);
|
|
var sub_path: [TmpDir.sub_path_len]u8 = undefined;
|
|
_ = std.fs.base64_encoder.encode(&sub_path, &random_bytes);
|
|
|
|
const cwd = Io.Dir.cwd();
|
|
var cache_dir = cwd.createDirPathOpen(io, ".zig-cache", .{}) catch
|
|
@panic("unable to make tmp dir for testing: unable to make and open .zig-cache dir");
|
|
defer cache_dir.close(io);
|
|
const parent_dir = cache_dir.createDirPathOpen(io, "tmp", .{}) catch
|
|
@panic("unable to make tmp dir for testing: unable to make and open .zig-cache/tmp dir");
|
|
const dir = parent_dir.createDirPathOpen(io, &sub_path, .{ .open_options = opts }) catch
|
|
@panic("unable to make tmp dir for testing: unable to make and open the tmp dir");
|
|
|
|
return .{
|
|
.dir = dir,
|
|
.parent_dir = parent_dir,
|
|
.sub_path = sub_path,
|
|
};
|
|
}
|
|
|
|
pub const TmpDir = struct {
|
|
dir: Io.Dir,
|
|
parent_dir: Io.Dir,
|
|
sub_path: [sub_path_len]u8,
|
|
|
|
const random_bytes_count = 12;
|
|
const sub_path_len = std.fs.base64_encoder.calcSize(random_bytes_count);
|
|
|
|
pub fn cleanup(self: *TmpDir, io: Io) void {
|
|
self.dir.close(io);
|
|
self.parent_dir.deleteTree(io, &self.sub_path) catch {};
|
|
self.parent_dir.close(io);
|
|
self.* = undefined;
|
|
}
|
|
};
|