mirror of
https://codeberg.org/ziglang/zig.git
synced 2026-03-08 01:24:49 +01:00
This branch regressed the child process "run" mechanism because it didn't pass the correct stdin, stdout, stderr values to process.spawn Fixed now.
200 lines
8.7 KiB
Zig
200 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.Child.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;
|
|
}
|
|
};
|