mirror of
https://codeberg.org/ziglang/zig.git
synced 2025-12-06 13:54:21 +00:00
besides simply being redundant work, the now removed normalize call would cause spawn to errantly fail (BadPath) when passing a relative path which traversed 'above' the current working directory. This case is already handled by leaving normalization to the windows.wToPrefixedFileW call in windowsCreateProcessPathExt
219 lines
10 KiB
Zig
219 lines
10 KiB
Zig
const std = @import("std");
|
|
|
|
const windows = std.os.windows;
|
|
const utf16Literal = std.unicode.utf8ToUtf16LeStringLiteral;
|
|
|
|
pub fn main() anyerror!void {
|
|
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
|
defer if (gpa.deinit() == .leak) @panic("found memory leaks");
|
|
const allocator = gpa.allocator();
|
|
|
|
var it = try std.process.argsWithAllocator(allocator);
|
|
defer it.deinit();
|
|
_ = it.next() orelse unreachable; // skip binary name
|
|
const hello_exe_cache_path = it.next() orelse unreachable;
|
|
|
|
var tmp = std.testing.tmpDir(.{});
|
|
defer tmp.cleanup();
|
|
|
|
const tmp_absolute_path = try tmp.dir.realpathAlloc(allocator, ".");
|
|
defer allocator.free(tmp_absolute_path);
|
|
const tmp_absolute_path_w = try std.unicode.utf8ToUtf16LeAllocZ(allocator, tmp_absolute_path);
|
|
defer allocator.free(tmp_absolute_path_w);
|
|
const cwd_absolute_path = try std.fs.cwd().realpathAlloc(allocator, ".");
|
|
defer allocator.free(cwd_absolute_path);
|
|
const tmp_relative_path = try std.fs.path.relative(allocator, cwd_absolute_path, tmp_absolute_path);
|
|
defer allocator.free(tmp_relative_path);
|
|
|
|
// Clear PATH
|
|
std.debug.assert(windows.kernel32.SetEnvironmentVariableW(
|
|
utf16Literal("PATH"),
|
|
null,
|
|
) == windows.TRUE);
|
|
|
|
// Set PATHEXT to something predictable
|
|
std.debug.assert(windows.kernel32.SetEnvironmentVariableW(
|
|
utf16Literal("PATHEXT"),
|
|
utf16Literal(".COM;.EXE;.BAT;.CMD;.JS"),
|
|
) == windows.TRUE);
|
|
|
|
// No PATH, so it should fail to find anything not in the cwd
|
|
try testExecError(error.FileNotFound, allocator, "something_missing");
|
|
|
|
// make sure we don't get error.BadPath traversing out of cwd with a relative path
|
|
try testExecError(error.FileNotFound, allocator, "..\\.\\.\\.\\\\..\\more_missing");
|
|
|
|
std.debug.assert(windows.kernel32.SetEnvironmentVariableW(
|
|
utf16Literal("PATH"),
|
|
tmp_absolute_path_w,
|
|
) == windows.TRUE);
|
|
|
|
// Move hello.exe into the tmp dir which is now added to the path
|
|
try std.fs.cwd().copyFile(hello_exe_cache_path, tmp.dir, "hello.exe", .{});
|
|
|
|
// with extension should find the .exe (case insensitive)
|
|
try testExec(allocator, "HeLLo.exe", "hello from exe\n");
|
|
// without extension should find the .exe (case insensitive)
|
|
try testExec(allocator, "heLLo", "hello from exe\n");
|
|
// with invalid cwd
|
|
try std.testing.expectError(error.FileNotFound, testExecWithCwd(allocator, "hello.exe", "missing_dir", ""));
|
|
|
|
// now add a .bat
|
|
try tmp.dir.writeFile(.{ .sub_path = "hello.bat", .data = "@echo hello from bat" });
|
|
// and a .cmd
|
|
try tmp.dir.writeFile(.{ .sub_path = "hello.cmd", .data = "@echo hello from cmd" });
|
|
|
|
// with extension should find the .bat (case insensitive)
|
|
try testExec(allocator, "heLLo.bat", "hello from bat\r\n");
|
|
// with extension should find the .cmd (case insensitive)
|
|
try testExec(allocator, "heLLo.cmd", "hello from cmd\r\n");
|
|
// without extension should find the .exe (since its first in PATHEXT)
|
|
try testExec(allocator, "heLLo", "hello from exe\n");
|
|
|
|
// now rename the exe to not have an extension
|
|
try tmp.dir.rename("hello.exe", "hello");
|
|
|
|
// with extension should now fail
|
|
try testExecError(error.FileNotFound, allocator, "hello.exe");
|
|
// without extension should succeed (case insensitive)
|
|
try testExec(allocator, "heLLo", "hello from exe\n");
|
|
|
|
try tmp.dir.makeDir("something");
|
|
try tmp.dir.rename("hello", "something/hello.exe");
|
|
|
|
const relative_path_no_ext = try std.fs.path.join(allocator, &.{ tmp_relative_path, "something/hello" });
|
|
defer allocator.free(relative_path_no_ext);
|
|
|
|
// Giving a full relative path to something/hello should work
|
|
try testExec(allocator, relative_path_no_ext, "hello from exe\n");
|
|
// But commands with path separators get excluded from PATH searching, so this will fail
|
|
try testExecError(error.FileNotFound, allocator, "something/hello");
|
|
|
|
// Now that .BAT is the first PATHEXT that should be found, this should succeed
|
|
try testExec(allocator, "heLLo", "hello from bat\r\n");
|
|
|
|
// Add a hello.exe that is not a valid executable
|
|
try tmp.dir.writeFile(.{ .sub_path = "hello.exe", .data = "invalid" });
|
|
|
|
// Trying to execute it with extension will give InvalidExe. This is a special
|
|
// case for .EXE extensions, where if they ever try to get executed but they are
|
|
// invalid, that gets treated as a fatal error wherever they are found and InvalidExe
|
|
// is returned immediately.
|
|
try testExecError(error.InvalidExe, allocator, "hello.exe");
|
|
// Same thing applies to the command with no extension--even though there is a
|
|
// hello.bat that could be executed, it should stop after it tries executing
|
|
// hello.exe and getting InvalidExe.
|
|
try testExecError(error.InvalidExe, allocator, "hello");
|
|
|
|
// If we now rename hello.exe to have no extension, it will behave differently
|
|
try tmp.dir.rename("hello.exe", "hello");
|
|
|
|
// Now, trying to execute it without an extension should treat InvalidExe as recoverable
|
|
// and skip over it and find hello.bat and execute that
|
|
try testExec(allocator, "hello", "hello from bat\r\n");
|
|
|
|
// If we rename the invalid exe to something else
|
|
try tmp.dir.rename("hello", "goodbye");
|
|
// Then we should now get FileNotFound when trying to execute 'goodbye',
|
|
// since that is what the original error will be after searching for 'goodbye'
|
|
// in the cwd. It will try to execute 'goodbye' from the PATH but the InvalidExe error
|
|
// should be ignored in this case.
|
|
try testExecError(error.FileNotFound, allocator, "goodbye");
|
|
|
|
// Now let's set the tmp dir as the cwd and set the path only include the "something" sub dir
|
|
try tmp.dir.setAsCwd();
|
|
defer tmp.parent_dir.setAsCwd() catch {};
|
|
const something_subdir_abs_path = try std.mem.concatWithSentinel(allocator, u16, &.{ tmp_absolute_path_w, utf16Literal("\\something") }, 0);
|
|
defer allocator.free(something_subdir_abs_path);
|
|
|
|
std.debug.assert(windows.kernel32.SetEnvironmentVariableW(
|
|
utf16Literal("PATH"),
|
|
something_subdir_abs_path,
|
|
) == windows.TRUE);
|
|
|
|
// Now trying to execute goodbye should give error.InvalidExe since it's the original
|
|
// error that we got when trying within the cwd
|
|
try testExecError(error.InvalidExe, allocator, "goodbye");
|
|
|
|
// hello should still find the .bat
|
|
try testExec(allocator, "hello", "hello from bat\r\n");
|
|
|
|
// If we rename something/hello.exe to something/goodbye.exe
|
|
try tmp.dir.rename("something/hello.exe", "something/goodbye.exe");
|
|
// And try to execute goodbye, then the one in something should be found
|
|
// since the one in cwd is an invalid executable
|
|
try testExec(allocator, "goodbye", "hello from exe\n");
|
|
|
|
// If we use an absolute path to execute the invalid goodbye
|
|
const goodbye_abs_path = try std.mem.join(allocator, "\\", &.{ tmp_absolute_path, "goodbye" });
|
|
defer allocator.free(goodbye_abs_path);
|
|
// then the PATH should not be searched and we should get InvalidExe
|
|
try testExecError(error.InvalidExe, allocator, goodbye_abs_path);
|
|
|
|
// If we try to exec but provide a cwd that is an absolute path, the PATH
|
|
// should still be searched and the goodbye.exe in something should be found.
|
|
try testExecWithCwd(allocator, "goodbye", tmp_absolute_path, "hello from exe\n");
|
|
|
|
// introduce some extra path separators into the path which is dealt with inside the spawn call.
|
|
const denormed_something_subdir_size = std.mem.replacementSize(u16, something_subdir_abs_path, utf16Literal("\\"), utf16Literal("\\\\\\\\"));
|
|
|
|
const denormed_something_subdir_abs_path = try allocator.allocSentinel(u16, denormed_something_subdir_size, 0);
|
|
defer allocator.free(denormed_something_subdir_abs_path);
|
|
|
|
_ = std.mem.replace(u16, something_subdir_abs_path, utf16Literal("\\"), utf16Literal("\\\\\\\\"), denormed_something_subdir_abs_path);
|
|
|
|
const denormed_something_subdir_wtf8 = try std.unicode.wtf16LeToWtf8Alloc(allocator, denormed_something_subdir_abs_path);
|
|
defer allocator.free(denormed_something_subdir_wtf8);
|
|
|
|
// clear the path to ensure that the match comes from the cwd
|
|
std.debug.assert(windows.kernel32.SetEnvironmentVariableW(
|
|
utf16Literal("PATH"),
|
|
null,
|
|
) == windows.TRUE);
|
|
|
|
try testExecWithCwd(allocator, "goodbye", denormed_something_subdir_wtf8, "hello from exe\n");
|
|
|
|
// normalization should also work if the non-normalized path is found in the PATH var.
|
|
std.debug.assert(windows.kernel32.SetEnvironmentVariableW(
|
|
utf16Literal("PATH"),
|
|
denormed_something_subdir_abs_path,
|
|
) == windows.TRUE);
|
|
try testExec(allocator, "goodbye", "hello from exe\n");
|
|
|
|
// now make sure we can launch executables "outside" of the cwd
|
|
var subdir_cwd = try tmp.dir.openDir(denormed_something_subdir_wtf8, .{});
|
|
defer subdir_cwd.close();
|
|
|
|
try tmp.dir.rename("something/goodbye.exe", "hello.exe");
|
|
try subdir_cwd.setAsCwd();
|
|
|
|
// clear the PATH again
|
|
std.debug.assert(windows.kernel32.SetEnvironmentVariableW(
|
|
utf16Literal("PATH"),
|
|
null,
|
|
) == windows.TRUE);
|
|
|
|
// while we're at it make sure non-windows separators work fine
|
|
try testExec(allocator, "../hello", "hello from exe\n");
|
|
}
|
|
|
|
fn testExecError(err: anyerror, allocator: std.mem.Allocator, command: []const u8) !void {
|
|
return std.testing.expectError(err, testExec(allocator, command, ""));
|
|
}
|
|
|
|
fn testExec(allocator: std.mem.Allocator, command: []const u8, expected_stdout: []const u8) !void {
|
|
return testExecWithCwd(allocator, command, null, expected_stdout);
|
|
}
|
|
|
|
fn testExecWithCwd(allocator: std.mem.Allocator, command: []const u8, cwd: ?[]const u8, expected_stdout: []const u8) !void {
|
|
const result = try std.process.Child.run(.{
|
|
.allocator = allocator,
|
|
.argv = &[_][]const u8{command},
|
|
.cwd = cwd,
|
|
});
|
|
defer allocator.free(result.stdout);
|
|
defer allocator.free(result.stderr);
|
|
|
|
try std.testing.expectEqualStrings("", result.stderr);
|
|
try std.testing.expectEqualStrings(expected_stdout, result.stdout);
|
|
}
|