| ... | @@ -19,8 +19,8 @@ pub fn main() anyerror!void { | ... | @@ -19,8 +19,8 @@ pub fn main() anyerror!void { |
| 19 | _ = it.next() orelse unreachable; // skip binary name | 19 | _ = it.next() orelse unreachable; // skip binary name |
| 20 | const hello_exe_cache_path = it.next() orelse unreachable; | 20 | const hello_exe_cache_path = it.next() orelse unreachable; |
| 21 | | 21 | |
| 22 | var tmp = std.testing.tmpDir(.{}); | 22 | var tmp = tmpDir(io, .{}); |
| 23 | defer tmp.cleanup(); | 23 | defer tmp.cleanup(io); |
| 24 | | 24 | |
| 25 | const tmp_absolute_path = try tmp.dir.realPathFileAlloc(io, ".", gpa); | 25 | const tmp_absolute_path = try tmp.dir.realPathFileAlloc(io, ".", gpa); |
| 26 | defer gpa.free(tmp_absolute_path); | 26 | defer gpa.free(tmp_absolute_path); |
| ... | @@ -44,10 +44,10 @@ pub fn main() anyerror!void { | ... | @@ -44,10 +44,10 @@ pub fn main() anyerror!void { |
| 44 | ) == windows.TRUE); | 44 | ) == windows.TRUE); |
| 45 | | 45 | |
| 46 | // No PATH, so it should fail to find anything not in the cwd | 46 | // No PATH, so it should fail to find anything not in the cwd |
| 47 | try testExecError(error.FileNotFound, gpa, "something_missing"); | 47 | try testExecError(error.FileNotFound, gpa, io, "something_missing"); |
| 48 | | 48 | |
| 49 | // make sure we don't get error.BadPath traversing out of cwd with a relative path | 49 | // make sure we don't get error.BadPath traversing out of cwd with a relative path |
| 50 | try testExecError(error.FileNotFound, gpa, "..\\.\\.\\.\\\\..\\more_missing"); | 50 | try testExecError(error.FileNotFound, gpa, io, "..\\.\\.\\.\\\\..\\more_missing"); |
| 51 | | 51 | |
| 52 | std.debug.assert(windows.kernel32.SetEnvironmentVariableW( | 52 | std.debug.assert(windows.kernel32.SetEnvironmentVariableW( |
| 53 | utf16Literal("PATH"), | 53 | utf16Literal("PATH"), |
| ... | @@ -55,12 +55,12 @@ pub fn main() anyerror!void { | ... | @@ -55,12 +55,12 @@ pub fn main() anyerror!void { |
| 55 | ) == windows.TRUE); | 55 | ) == windows.TRUE); |
| 56 | | 56 | |
| 57 | // Move hello.exe into the tmp dir which is now added to the path | 57 | // Move hello.exe into the tmp dir which is now added to the path |
| 58 | try Io.Dir.cwd().copyFile(hello_exe_cache_path, tmp.dir, "hello.exe", .{}); | 58 | try Io.Dir.cwd().copyFile(hello_exe_cache_path, tmp.dir, "hello.exe", io, .{}); |
| 59 | | 59 | |
| 60 | // with extension should find the .exe (case insensitive) | 60 | // with extension should find the .exe (case insensitive) |
| 61 | try testExec(gpa, "HeLLo.exe", "hello from exe\n"); | 61 | try testExec(gpa, io, "HeLLo.exe", "hello from exe\n"); |
| 62 | // without extension should find the .exe (case insensitive) | 62 | // without extension should find the .exe (case insensitive) |
| 63 | try testExec(gpa, "heLLo", "hello from exe\n"); | 63 | try testExec(gpa, io, "heLLo", "hello from exe\n"); |
| 64 | // with invalid cwd | 64 | // with invalid cwd |
| 65 | try std.testing.expectError(error.FileNotFound, testExecWithCwd(gpa, io, "hello.exe", "missing_dir", "")); | 65 | try std.testing.expectError(error.FileNotFound, testExecWithCwd(gpa, io, "hello.exe", "missing_dir", "")); |
| 66 | | 66 | |
| ... | @@ -70,33 +70,33 @@ pub fn main() anyerror!void { | ... | @@ -70,33 +70,33 @@ pub fn main() anyerror!void { |
| 70 | try tmp.dir.writeFile(io, .{ .sub_path = "hello.cmd", .data = "@echo hello from cmd" }); | 70 | try tmp.dir.writeFile(io, .{ .sub_path = "hello.cmd", .data = "@echo hello from cmd" }); |
| 71 | | 71 | |
| 72 | // with extension should find the .bat (case insensitive) | 72 | // with extension should find the .bat (case insensitive) |
| 73 | try testExec(gpa, "heLLo.bat", "hello from bat\r\n"); | 73 | try testExec(gpa, io, "heLLo.bat", "hello from bat\r\n"); |
| 74 | // with extension should find the .cmd (case insensitive) | 74 | // with extension should find the .cmd (case insensitive) |
| 75 | try testExec(gpa, "heLLo.cmd", "hello from cmd\r\n"); | 75 | try testExec(gpa, io, "heLLo.cmd", "hello from cmd\r\n"); |
| 76 | // without extension should find the .exe (since its first in PATHEXT) | 76 | // without extension should find the .exe (since its first in PATHEXT) |
| 77 | try testExec(gpa, "heLLo", "hello from exe\n"); | 77 | try testExec(gpa, io, "heLLo", "hello from exe\n"); |
| 78 | | 78 | |
| 79 | // now rename the exe to not have an extension | 79 | // now rename the exe to not have an extension |
| 80 | try renameExe(tmp.dir, "hello.exe", "hello"); | 80 | try renameExe(tmp.dir, io, "hello.exe", "hello"); |
| 81 | | 81 | |
| 82 | // with extension should now fail | 82 | // with extension should now fail |
| 83 | try testExecError(error.FileNotFound, gpa, "hello.exe"); | 83 | try testExecError(error.FileNotFound, gpa, io, "hello.exe"); |
| 84 | // without extension should succeed (case insensitive) | 84 | // without extension should succeed (case insensitive) |
| 85 | try testExec(gpa, "heLLo", "hello from exe\n"); | 85 | try testExec(gpa, io, "heLLo", "hello from exe\n"); |
| 86 | | 86 | |
| 87 | try tmp.dir.createDir(io, "something", .default_dir); | 87 | try tmp.dir.createDir(io, "something", .default_dir); |
| 88 | try renameExe(tmp.dir, "hello", "something/hello.exe"); | 88 | try renameExe(tmp.dir, io, "hello", "something/hello.exe"); |
| 89 | | 89 | |
| 90 | const relative_path_no_ext = try std.fs.path.join(gpa, &.{ tmp_relative_path, "something/hello" }); | 90 | const relative_path_no_ext = try std.fs.path.join(gpa, &.{ tmp_relative_path, "something/hello" }); |
| 91 | defer gpa.free(relative_path_no_ext); | 91 | defer gpa.free(relative_path_no_ext); |
| 92 | | 92 | |
| 93 | // Giving a full relative path to something/hello should work | 93 | // Giving a full relative path to something/hello should work |
| 94 | try testExec(gpa, relative_path_no_ext, "hello from exe\n"); | 94 | try testExec(gpa, io, relative_path_no_ext, "hello from exe\n"); |
| 95 | // But commands with path separators get excluded from PATH searching, so this will fail | 95 | // But commands with path separators get excluded from PATH searching, so this will fail |
| 96 | try testExecError(error.FileNotFound, gpa, "something/hello"); | 96 | try testExecError(error.FileNotFound, gpa, io, "something/hello"); |
| 97 | | 97 | |
| 98 | // Now that .BAT is the first PATHEXT that should be found, this should succeed | 98 | // Now that .BAT is the first PATHEXT that should be found, this should succeed |
| 99 | try testExec(gpa, "heLLo", "hello from bat\r\n"); | 99 | try testExec(gpa, io, "heLLo", "hello from bat\r\n"); |
| 100 | | 100 | |
| 101 | // Add a hello.exe that is not a valid executable | 101 | // Add a hello.exe that is not a valid executable |
| 102 | try tmp.dir.writeFile(io, .{ .sub_path = "hello.exe", .data = "invalid" }); | 102 | try tmp.dir.writeFile(io, .{ .sub_path = "hello.exe", .data = "invalid" }); |
| ... | @@ -105,26 +105,26 @@ pub fn main() anyerror!void { | ... | @@ -105,26 +105,26 @@ pub fn main() anyerror!void { |
| 105 | // case for .EXE extensions, where if they ever try to get executed but they are | 105 | // case for .EXE extensions, where if they ever try to get executed but they are |
| 106 | // invalid, that gets treated as a fatal error wherever they are found and InvalidExe | 106 | // invalid, that gets treated as a fatal error wherever they are found and InvalidExe |
| 107 | // is returned immediately. | 107 | // is returned immediately. |
| 108 | try testExecError(error.InvalidExe, gpa, "hello.exe"); | 108 | try testExecError(error.InvalidExe, gpa, io, "hello.exe"); |
| 109 | // Same thing applies to the command with no extension--even though there is a | 109 | // Same thing applies to the command with no extension--even though there is a |
| 110 | // hello.bat that could be executed, it should stop after it tries executing | 110 | // hello.bat that could be executed, it should stop after it tries executing |
| 111 | // hello.exe and getting InvalidExe. | 111 | // hello.exe and getting InvalidExe. |
| 112 | try testExecError(error.InvalidExe, gpa, "hello"); | 112 | try testExecError(error.InvalidExe, gpa, io, "hello"); |
| 113 | | 113 | |
| 114 | // If we now rename hello.exe to have no extension, it will behave differently | 114 | // If we now rename hello.exe to have no extension, it will behave differently |
| 115 | try renameExe(tmp.dir, "hello.exe", "hello"); | 115 | try renameExe(tmp.dir, io, "hello.exe", "hello"); |
| 116 | | 116 | |
| 117 | // Now, trying to execute it without an extension should treat InvalidExe as recoverable | 117 | // Now, trying to execute it without an extension should treat InvalidExe as recoverable |
| 118 | // and skip over it and find hello.bat and execute that | 118 | // and skip over it and find hello.bat and execute that |
| 119 | try testExec(gpa, "hello", "hello from bat\r\n"); | 119 | try testExec(gpa, io, "hello", "hello from bat\r\n"); |
| 120 | | 120 | |
| 121 | // If we rename the invalid exe to something else | 121 | // If we rename the invalid exe to something else |
| 122 | try renameExe(tmp.dir, "hello", "goodbye"); | 122 | try renameExe(tmp.dir, io, "hello", "goodbye"); |
| 123 | // Then we should now get FileNotFound when trying to execute 'goodbye', | 123 | // Then we should now get FileNotFound when trying to execute 'goodbye', |
| 124 | // since that is what the original error will be after searching for 'goodbye' | 124 | // since that is what the original error will be after searching for 'goodbye' |
| 125 | // in the cwd. It will try to execute 'goodbye' from the PATH but the InvalidExe error | 125 | // in the cwd. It will try to execute 'goodbye' from the PATH but the InvalidExe error |
| 126 | // should be ignored in this case. | 126 | // should be ignored in this case. |
| 127 | try testExecError(error.FileNotFound, gpa, "goodbye"); | 127 | try testExecError(error.FileNotFound, gpa, io, "goodbye"); |
| 128 | | 128 | |
| 129 | // Now let's set the tmp dir as the cwd and set the path only include the "something" sub dir | 129 | // Now let's set the tmp dir as the cwd and set the path only include the "something" sub dir |
| 130 | try std.process.setCurrentDir(io, tmp.dir); | 130 | try std.process.setCurrentDir(io, tmp.dir); |
| ... | @@ -139,26 +139,26 @@ pub fn main() anyerror!void { | ... | @@ -139,26 +139,26 @@ pub fn main() anyerror!void { |
| 139 | | 139 | |
| 140 | // Now trying to execute goodbye should give error.InvalidExe since it's the original | 140 | // Now trying to execute goodbye should give error.InvalidExe since it's the original |
| 141 | // error that we got when trying within the cwd | 141 | // error that we got when trying within the cwd |
| 142 | try testExecError(error.InvalidExe, gpa, "goodbye"); | 142 | try testExecError(error.InvalidExe, gpa, io, "goodbye"); |
| 143 | | 143 | |
| 144 | // hello should still find the .bat | 144 | // hello should still find the .bat |
| 145 | try testExec(gpa, "hello", "hello from bat\r\n"); | 145 | try testExec(gpa, io, "hello", "hello from bat\r\n"); |
| 146 | | 146 | |
| 147 | // If we rename something/hello.exe to something/goodbye.exe | 147 | // If we rename something/hello.exe to something/goodbye.exe |
| 148 | try renameExe(tmp.dir, "something/hello.exe", "something/goodbye.exe"); | 148 | try renameExe(tmp.dir, io, "something/hello.exe", "something/goodbye.exe"); |
| 149 | // And try to execute goodbye, then the one in something should be found | 149 | // And try to execute goodbye, then the one in something should be found |
| 150 | // since the one in cwd is an invalid executable | 150 | // since the one in cwd is an invalid executable |
| 151 | try testExec(gpa, "goodbye", "hello from exe\n"); | 151 | try testExec(gpa, io, "goodbye", "hello from exe\n"); |
| 152 | | 152 | |
| 153 | // If we use an absolute path to execute the invalid goodbye | 153 | // If we use an absolute path to execute the invalid goodbye |
| 154 | const goodbye_abs_path = try std.mem.join(gpa, "\\", &.{ tmp_absolute_path, "goodbye" }); | 154 | const goodbye_abs_path = try std.mem.join(gpa, "\\", &.{ tmp_absolute_path, "goodbye" }); |
| 155 | defer gpa.free(goodbye_abs_path); | 155 | defer gpa.free(goodbye_abs_path); |
| 156 | // then the PATH should not be searched and we should get InvalidExe | 156 | // then the PATH should not be searched and we should get InvalidExe |
| 157 | try testExecError(error.InvalidExe, gpa, goodbye_abs_path); | 157 | try testExecError(error.InvalidExe, gpa, io, goodbye_abs_path); |
| 158 | | 158 | |
| 159 | // If we try to exec but provide a cwd that is an absolute path, the PATH | 159 | // If we try to exec but provide a cwd that is an absolute path, the PATH |
| 160 | // should still be searched and the goodbye.exe in something should be found. | 160 | // should still be searched and the goodbye.exe in something should be found. |
| 161 | try testExecWithCwd(gpa, "goodbye", tmp_absolute_path, "hello from exe\n"); | 161 | try testExecWithCwd(gpa, io, "goodbye", tmp_absolute_path, "hello from exe\n"); |
| 162 | | 162 | |
| 163 | // introduce some extra path separators into the path which is dealt with inside the spawn call. | 163 | // introduce some extra path separators into the path which is dealt with inside the spawn call. |
| 164 | const denormed_something_subdir_size = std.mem.replacementSize(u16, something_subdir_abs_path, utf16Literal("\\"), utf16Literal("\\\\\\\\")); | 164 | const denormed_something_subdir_size = std.mem.replacementSize(u16, something_subdir_abs_path, utf16Literal("\\"), utf16Literal("\\\\\\\\")); |
| ... | @@ -177,20 +177,20 @@ pub fn main() anyerror!void { | ... | @@ -177,20 +177,20 @@ pub fn main() anyerror!void { |
| 177 | null, | 177 | null, |
| 178 | ) == windows.TRUE); | 178 | ) == windows.TRUE); |
| 179 | | 179 | |
| 180 | try testExecWithCwd(gpa, "goodbye", denormed_something_subdir_wtf8, "hello from exe\n"); | 180 | try testExecWithCwd(gpa, io, "goodbye", denormed_something_subdir_wtf8, "hello from exe\n"); |
| 181 | | 181 | |
| 182 | // normalization should also work if the non-normalized path is found in the PATH var. | 182 | // normalization should also work if the non-normalized path is found in the PATH var. |
| 183 | std.debug.assert(windows.kernel32.SetEnvironmentVariableW( | 183 | std.debug.assert(windows.kernel32.SetEnvironmentVariableW( |
| 184 | utf16Literal("PATH"), | 184 | utf16Literal("PATH"), |
| 185 | denormed_something_subdir_abs_path, | 185 | denormed_something_subdir_abs_path, |
| 186 | ) == windows.TRUE); | 186 | ) == windows.TRUE); |
| 187 | try testExec(gpa, "goodbye", "hello from exe\n"); | 187 | try testExec(gpa, io, "goodbye", "hello from exe\n"); |
| 188 | | 188 | |
| 189 | // now make sure we can launch executables "outside" of the cwd | 189 | // now make sure we can launch executables "outside" of the cwd |
| 190 | var subdir_cwd = try tmp.dir.openDir(denormed_something_subdir_wtf8, .{}); | 190 | var subdir_cwd = try tmp.dir.openDir(io, denormed_something_subdir_wtf8, .{}); |
| 191 | defer subdir_cwd.close(io); | 191 | defer subdir_cwd.close(io); |
| 192 | | 192 | |
| 193 | try renameExe(tmp.dir, "something/goodbye.exe", "hello.exe"); | 193 | try renameExe(tmp.dir, io, "something/goodbye.exe", "hello.exe"); |
| 194 | try std.process.setCurrentDir(io, subdir_cwd); | 194 | try std.process.setCurrentDir(io, subdir_cwd); |
| 195 | | 195 | |
| 196 | // clear the PATH again | 196 | // clear the PATH again |
| ... | @@ -200,15 +200,15 @@ pub fn main() anyerror!void { | ... | @@ -200,15 +200,15 @@ pub fn main() anyerror!void { |
| 200 | ) == windows.TRUE); | 200 | ) == windows.TRUE); |
| 201 | | 201 | |
| 202 | // while we're at it make sure non-windows separators work fine | 202 | // while we're at it make sure non-windows separators work fine |
| 203 | try testExec(gpa, "../hello", "hello from exe\n"); | 203 | try testExec(gpa, io, "../hello", "hello from exe\n"); |
| 204 | } | 204 | } |
| 205 | | 205 | |
| 206 | fn testExecError(err: anyerror, gpa: Allocator, command: []const u8) !void { | 206 | fn testExecError(err: anyerror, gpa: Allocator, io: Io, command: []const u8) !void { |
| 207 | return std.testing.expectError(err, testExec(gpa, command, "")); | 207 | return std.testing.expectError(err, testExec(gpa, io, command, "")); |
| 208 | } | 208 | } |
| 209 | | 209 | |
| 210 | fn testExec(gpa: Allocator, command: []const u8, expected_stdout: []const u8) !void { | 210 | fn testExec(gpa: Allocator, io: Io, command: []const u8, expected_stdout: []const u8) !void { |
| 211 | return testExecWithCwd(gpa, command, null, expected_stdout); | 211 | return testExecWithCwd(gpa, io, command, null, expected_stdout); |
| 212 | } | 212 | } |
| 213 | | 213 | |
| 214 | fn testExecWithCwd(gpa: Allocator, io: Io, command: []const u8, cwd: ?[]const u8, expected_stdout: []const u8) !void { | 214 | fn testExecWithCwd(gpa: Allocator, io: Io, command: []const u8, cwd: ?[]const u8, expected_stdout: []const u8) !void { |
| ... | @@ -223,9 +223,9 @@ fn testExecWithCwd(gpa: Allocator, io: Io, command: []const u8, cwd: ?[]const u8 | ... | @@ -223,9 +223,9 @@ fn testExecWithCwd(gpa: Allocator, io: Io, command: []const u8, cwd: ?[]const u8 |
| 223 | try std.testing.expectEqualStrings(expected_stdout, result.stdout); | 223 | try std.testing.expectEqualStrings(expected_stdout, result.stdout); |
| 224 | } | 224 | } |
| 225 | | 225 | |
| 226 | fn renameExe(dir: Io.Dir, old_sub_path: []const u8, new_sub_path: []const u8) !void { | 226 | fn renameExe(dir: Io.Dir, io: Io, old_sub_path: []const u8, new_sub_path: []const u8) !void { |
| 227 | var attempt: u5 = 0; | 227 | var attempt: u5 = 0; |
| 228 | while (true) break dir.rename(old_sub_path, new_sub_path) catch |err| switch (err) { | 228 | while (true) break dir.rename(old_sub_path, dir, new_sub_path, io) catch |err| switch (err) { |
| 229 | error.AccessDenied => { | 229 | error.AccessDenied => { |
| 230 | if (attempt == 13) return error.AccessDenied; | 230 | if (attempt == 13) return error.AccessDenied; |
| 231 | // give the kernel a chance to finish closing the executable handle | 231 | // give the kernel a chance to finish closing the executable handle |
| ... | @@ -236,3 +236,41 @@ fn renameExe(dir: Io.Dir, old_sub_path: []const u8, new_sub_path: []const u8) !v | ... | @@ -236,3 +236,41 @@ fn renameExe(dir: Io.Dir, old_sub_path: []const u8, new_sub_path: []const u8) !v |
| 236 | else => |e| return e, | 236 | else => |e| return e, |
| 237 | }; | 237 | }; |
| 238 | } | 238 | } |
| | 239 | |
| | 240 | pub fn tmpDir(io: Io, opts: Io.Dir.OpenOptions) TmpDir { |
| | 241 | var random_bytes: [TmpDir.random_bytes_count]u8 = undefined; |
| | 242 | std.crypto.random.bytes(&random_bytes); |
| | 243 | var sub_path: [TmpDir.sub_path_len]u8 = undefined; |
| | 244 | _ = std.fs.base64_encoder.encode(&sub_path, &random_bytes); |
| | 245 | |
| | 246 | const cwd = Io.Dir.cwd(); |
| | 247 | var cache_dir = cwd.createDirPathOpen(io, ".zig-cache", .{}) catch |
| | 248 | @panic("unable to make tmp dir for testing: unable to make and open .zig-cache dir"); |
| | 249 | defer cache_dir.close(io); |
| | 250 | const parent_dir = cache_dir.createDirPathOpen(io, "tmp", .{}) catch |
| | 251 | @panic("unable to make tmp dir for testing: unable to make and open .zig-cache/tmp dir"); |
| | 252 | const dir = parent_dir.createDirPathOpen(io, &sub_path, .{ .open_options = opts }) catch |
| | 253 | @panic("unable to make tmp dir for testing: unable to make and open the tmp dir"); |
| | 254 | |
| | 255 | return .{ |
| | 256 | .dir = dir, |
| | 257 | .parent_dir = parent_dir, |
| | 258 | .sub_path = sub_path, |
| | 259 | }; |
| | 260 | } |
| | 261 | |
| | 262 | pub const TmpDir = struct { |
| | 263 | dir: Io.Dir, |
| | 264 | parent_dir: Io.Dir, |
| | 265 | sub_path: [sub_path_len]u8, |
| | 266 | |
| | 267 | const random_bytes_count = 12; |
| | 268 | const sub_path_len = std.fs.base64_encoder.calcSize(random_bytes_count); |
| | 269 | |
| | 270 | pub fn cleanup(self: *TmpDir, io: Io) void { |
| | 271 | self.dir.close(io); |
| | 272 | self.parent_dir.deleteTree(io, &self.sub_path) catch {}; |
| | 273 | self.parent_dir.close(io); |
| | 274 | self.* = undefined; |
| | 275 | } |
| | 276 | }; |