master
1const std = @import("std");
2
3const windows = std.os.windows;
4const utf16Literal = std.unicode.utf8ToUtf16LeStringLiteral;
5
6pub fn main() anyerror!void {
7 var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
8 defer if (gpa.deinit() == .leak) @panic("found memory leaks");
9 const allocator = gpa.allocator();
10
11 var it = try std.process.argsWithAllocator(allocator);
12 defer it.deinit();
13 _ = it.next() orelse unreachable; // skip binary name
14 const hello_exe_cache_path = it.next() orelse unreachable;
15
16 var tmp = std.testing.tmpDir(.{});
17 defer tmp.cleanup();
18
19 const tmp_absolute_path = try tmp.dir.realpathAlloc(allocator, ".");
20 defer allocator.free(tmp_absolute_path);
21 const tmp_absolute_path_w = try std.unicode.utf8ToUtf16LeAllocZ(allocator, tmp_absolute_path);
22 defer allocator.free(tmp_absolute_path_w);
23 const cwd_absolute_path = try std.fs.cwd().realpathAlloc(allocator, ".");
24 defer allocator.free(cwd_absolute_path);
25 const tmp_relative_path = try std.fs.path.relative(allocator, cwd_absolute_path, tmp_absolute_path);
26 defer allocator.free(tmp_relative_path);
27
28 // Clear PATH
29 std.debug.assert(windows.kernel32.SetEnvironmentVariableW(
30 utf16Literal("PATH"),
31 null,
32 ) == windows.TRUE);
33
34 // Set PATHEXT to something predictable
35 std.debug.assert(windows.kernel32.SetEnvironmentVariableW(
36 utf16Literal("PATHEXT"),
37 utf16Literal(".COM;.EXE;.BAT;.CMD;.JS"),
38 ) == windows.TRUE);
39
40 // No PATH, so it should fail to find anything not in the cwd
41 try testExecError(error.FileNotFound, allocator, "something_missing");
42
43 // make sure we don't get error.BadPath traversing out of cwd with a relative path
44 try testExecError(error.FileNotFound, allocator, "..\\.\\.\\.\\\\..\\more_missing");
45
46 std.debug.assert(windows.kernel32.SetEnvironmentVariableW(
47 utf16Literal("PATH"),
48 tmp_absolute_path_w,
49 ) == windows.TRUE);
50
51 // Move hello.exe into the tmp dir which is now added to the path
52 try std.fs.cwd().copyFile(hello_exe_cache_path, tmp.dir, "hello.exe", .{});
53
54 // with extension should find the .exe (case insensitive)
55 try testExec(allocator, "HeLLo.exe", "hello from exe\n");
56 // without extension should find the .exe (case insensitive)
57 try testExec(allocator, "heLLo", "hello from exe\n");
58 // with invalid cwd
59 try std.testing.expectError(error.FileNotFound, testExecWithCwd(allocator, "hello.exe", "missing_dir", ""));
60
61 // now add a .bat
62 try tmp.dir.writeFile(.{ .sub_path = "hello.bat", .data = "@echo hello from bat" });
63 // and a .cmd
64 try tmp.dir.writeFile(.{ .sub_path = "hello.cmd", .data = "@echo hello from cmd" });
65
66 // with extension should find the .bat (case insensitive)
67 try testExec(allocator, "heLLo.bat", "hello from bat\r\n");
68 // with extension should find the .cmd (case insensitive)
69 try testExec(allocator, "heLLo.cmd", "hello from cmd\r\n");
70 // without extension should find the .exe (since its first in PATHEXT)
71 try testExec(allocator, "heLLo", "hello from exe\n");
72
73 // now rename the exe to not have an extension
74 try renameExe(tmp.dir, "hello.exe", "hello");
75
76 // with extension should now fail
77 try testExecError(error.FileNotFound, allocator, "hello.exe");
78 // without extension should succeed (case insensitive)
79 try testExec(allocator, "heLLo", "hello from exe\n");
80
81 try tmp.dir.makeDir("something");
82 try renameExe(tmp.dir, "hello", "something/hello.exe");
83
84 const relative_path_no_ext = try std.fs.path.join(allocator, &.{ tmp_relative_path, "something/hello" });
85 defer allocator.free(relative_path_no_ext);
86
87 // Giving a full relative path to something/hello should work
88 try testExec(allocator, relative_path_no_ext, "hello from exe\n");
89 // But commands with path separators get excluded from PATH searching, so this will fail
90 try testExecError(error.FileNotFound, allocator, "something/hello");
91
92 // Now that .BAT is the first PATHEXT that should be found, this should succeed
93 try testExec(allocator, "heLLo", "hello from bat\r\n");
94
95 // Add a hello.exe that is not a valid executable
96 try tmp.dir.writeFile(.{ .sub_path = "hello.exe", .data = "invalid" });
97
98 // Trying to execute it with extension will give InvalidExe. This is a special
99 // case for .EXE extensions, where if they ever try to get executed but they are
100 // invalid, that gets treated as a fatal error wherever they are found and InvalidExe
101 // is returned immediately.
102 try testExecError(error.InvalidExe, allocator, "hello.exe");
103 // Same thing applies to the command with no extension--even though there is a
104 // hello.bat that could be executed, it should stop after it tries executing
105 // hello.exe and getting InvalidExe.
106 try testExecError(error.InvalidExe, allocator, "hello");
107
108 // If we now rename hello.exe to have no extension, it will behave differently
109 try renameExe(tmp.dir, "hello.exe", "hello");
110
111 // Now, trying to execute it without an extension should treat InvalidExe as recoverable
112 // and skip over it and find hello.bat and execute that
113 try testExec(allocator, "hello", "hello from bat\r\n");
114
115 // If we rename the invalid exe to something else
116 try renameExe(tmp.dir, "hello", "goodbye");
117 // Then we should now get FileNotFound when trying to execute 'goodbye',
118 // since that is what the original error will be after searching for 'goodbye'
119 // in the cwd. It will try to execute 'goodbye' from the PATH but the InvalidExe error
120 // should be ignored in this case.
121 try testExecError(error.FileNotFound, allocator, "goodbye");
122
123 // Now let's set the tmp dir as the cwd and set the path only include the "something" sub dir
124 try tmp.dir.setAsCwd();
125 defer tmp.parent_dir.setAsCwd() catch {};
126 const something_subdir_abs_path = try std.mem.concatWithSentinel(allocator, u16, &.{ tmp_absolute_path_w, utf16Literal("\\something") }, 0);
127 defer allocator.free(something_subdir_abs_path);
128
129 std.debug.assert(windows.kernel32.SetEnvironmentVariableW(
130 utf16Literal("PATH"),
131 something_subdir_abs_path,
132 ) == windows.TRUE);
133
134 // Now trying to execute goodbye should give error.InvalidExe since it's the original
135 // error that we got when trying within the cwd
136 try testExecError(error.InvalidExe, allocator, "goodbye");
137
138 // hello should still find the .bat
139 try testExec(allocator, "hello", "hello from bat\r\n");
140
141 // If we rename something/hello.exe to something/goodbye.exe
142 try renameExe(tmp.dir, "something/hello.exe", "something/goodbye.exe");
143 // And try to execute goodbye, then the one in something should be found
144 // since the one in cwd is an invalid executable
145 try testExec(allocator, "goodbye", "hello from exe\n");
146
147 // If we use an absolute path to execute the invalid goodbye
148 const goodbye_abs_path = try std.mem.join(allocator, "\\", &.{ tmp_absolute_path, "goodbye" });
149 defer allocator.free(goodbye_abs_path);
150 // then the PATH should not be searched and we should get InvalidExe
151 try testExecError(error.InvalidExe, allocator, goodbye_abs_path);
152
153 // If we try to exec but provide a cwd that is an absolute path, the PATH
154 // should still be searched and the goodbye.exe in something should be found.
155 try testExecWithCwd(allocator, "goodbye", tmp_absolute_path, "hello from exe\n");
156
157 // introduce some extra path separators into the path which is dealt with inside the spawn call.
158 const denormed_something_subdir_size = std.mem.replacementSize(u16, something_subdir_abs_path, utf16Literal("\\"), utf16Literal("\\\\\\\\"));
159
160 const denormed_something_subdir_abs_path = try allocator.allocSentinel(u16, denormed_something_subdir_size, 0);
161 defer allocator.free(denormed_something_subdir_abs_path);
162
163 _ = std.mem.replace(u16, something_subdir_abs_path, utf16Literal("\\"), utf16Literal("\\\\\\\\"), denormed_something_subdir_abs_path);
164
165 const denormed_something_subdir_wtf8 = try std.unicode.wtf16LeToWtf8Alloc(allocator, denormed_something_subdir_abs_path);
166 defer allocator.free(denormed_something_subdir_wtf8);
167
168 // clear the path to ensure that the match comes from the cwd
169 std.debug.assert(windows.kernel32.SetEnvironmentVariableW(
170 utf16Literal("PATH"),
171 null,
172 ) == windows.TRUE);
173
174 try testExecWithCwd(allocator, "goodbye", denormed_something_subdir_wtf8, "hello from exe\n");
175
176 // normalization should also work if the non-normalized path is found in the PATH var.
177 std.debug.assert(windows.kernel32.SetEnvironmentVariableW(
178 utf16Literal("PATH"),
179 denormed_something_subdir_abs_path,
180 ) == windows.TRUE);
181 try testExec(allocator, "goodbye", "hello from exe\n");
182
183 // now make sure we can launch executables "outside" of the cwd
184 var subdir_cwd = try tmp.dir.openDir(denormed_something_subdir_wtf8, .{});
185 defer subdir_cwd.close();
186
187 try renameExe(tmp.dir, "something/goodbye.exe", "hello.exe");
188 try subdir_cwd.setAsCwd();
189
190 // clear the PATH again
191 std.debug.assert(windows.kernel32.SetEnvironmentVariableW(
192 utf16Literal("PATH"),
193 null,
194 ) == windows.TRUE);
195
196 // while we're at it make sure non-windows separators work fine
197 try testExec(allocator, "../hello", "hello from exe\n");
198}
199
200fn testExecError(err: anyerror, allocator: std.mem.Allocator, command: []const u8) !void {
201 return std.testing.expectError(err, testExec(allocator, command, ""));
202}
203
204fn testExec(allocator: std.mem.Allocator, command: []const u8, expected_stdout: []const u8) !void {
205 return testExecWithCwd(allocator, command, null, expected_stdout);
206}
207
208fn testExecWithCwd(allocator: std.mem.Allocator, command: []const u8, cwd: ?[]const u8, expected_stdout: []const u8) !void {
209 const result = try std.process.Child.run(.{
210 .allocator = allocator,
211 .argv = &[_][]const u8{command},
212 .cwd = cwd,
213 });
214 defer allocator.free(result.stdout);
215 defer allocator.free(result.stderr);
216
217 try std.testing.expectEqualStrings("", result.stderr);
218 try std.testing.expectEqualStrings(expected_stdout, result.stdout);
219}
220
221fn renameExe(dir: std.fs.Dir, old_sub_path: []const u8, new_sub_path: []const u8) !void {
222 var attempt: u5 = 0;
223 while (true) break dir.rename(old_sub_path, new_sub_path) catch |err| switch (err) {
224 error.AccessDenied => {
225 if (attempt == 13) return error.AccessDenied;
226 // give the kernel a chance to finish closing the executable handle
227 _ = std.os.windows.kernel32.SleepEx(@as(u32, 1) << attempt >> 1, std.os.windows.FALSE);
228 attempt += 1;
229 continue;
230 },
231 else => |e| return e,
232 };
233}