| 1 | //! Tests belong here if they access internal state of std.Io.Threaded or |
| 2 | //! otherwise assume details of that particular implementation. |
| 3 | const builtin = @import("builtin"); |
| 4 | |
| 5 | const std = @import("std"); |
| 6 | const Io = std.Io; |
| 7 | const testing = std.testing; |
| 8 | const assert = std.debug.assert; |
| 9 | const windows = std.os.windows; |
| 10 | |
| 11 | test "concurrent vs main prevents deadlock via oversubscription" { |
| 12 | var threaded: Io.Threaded = .init(std.testing.allocator, .{ |
| 13 | .argv0 = .empty, |
| 14 | .environ = .empty, |
| 15 | }); |
| 16 | defer threaded.deinit(); |
| 17 | const io = threaded.io(); |
| 18 | |
| 19 | threaded.async_limit = .nothing; |
| 20 | |
| 21 | var queue: Io.Queue(u8) = .init(&.{}); |
| 22 | |
| 23 | var putter = io.concurrent(put, .{ io, &queue }) catch |err| switch (err) { |
| 24 | error.ConcurrencyUnavailable => { |
| 25 | try testing.expect(builtin.single_threaded); |
| 26 | return; |
| 27 | }, |
| 28 | }; |
| 29 | defer putter.cancel(io); |
| 30 | |
| 31 | try testing.expectEqual(42, queue.getOneUncancelable(io)); |
| 32 | } |
| 33 | |
| 34 | fn put(io: Io, queue: *Io.Queue(u8)) void { |
| 35 | queue.putOneUncancelable(io, 42) catch unreachable; |
| 36 | } |
| 37 | |
| 38 | fn get(io: Io, queue: *Io.Queue(u8)) void { |
| 39 | assert(queue.getOneUncancelable(io) catch unreachable == 42); |
| 40 | } |
| 41 | |
| 42 | test "concurrent vs concurrent prevents deadlock via oversubscription" { |
| 43 | var threaded: Io.Threaded = .init(std.testing.allocator, .{ |
| 44 | .argv0 = .empty, |
| 45 | .environ = .empty, |
| 46 | }); |
| 47 | defer threaded.deinit(); |
| 48 | const io = threaded.io(); |
| 49 | |
| 50 | threaded.async_limit = .nothing; |
| 51 | |
| 52 | var queue: Io.Queue(u8) = .init(&.{}); |
| 53 | |
| 54 | var putter = io.concurrent(put, .{ io, &queue }) catch |err| switch (err) { |
| 55 | error.ConcurrencyUnavailable => { |
| 56 | try testing.expect(builtin.single_threaded); |
| 57 | return; |
| 58 | }, |
| 59 | }; |
| 60 | defer putter.cancel(io); |
| 61 | |
| 62 | var getter = try io.concurrent(get, .{ io, &queue }); |
| 63 | defer getter.cancel(io); |
| 64 | |
| 65 | getter.await(io); |
| 66 | putter.await(io); |
| 67 | } |
| 68 | |
| 69 | const ByteArray256 = struct { x: [32]u8 align(32) }; |
| 70 | const ByteArray512 = struct { x: [64]u8 align(64) }; |
| 71 | |
| 72 | fn concatByteArrays(a: ByteArray256, b: ByteArray256) ByteArray512 { |
| 73 | return .{ .x = a.x ++ b.x }; |
| 74 | } |
| 75 | |
| 76 | test "async/concurrent context and result alignment" { |
| 77 | var buffer: [2048]u8 align(@alignOf(ByteArray512)) = undefined; |
| 78 | var fba: std.heap.FixedBufferAllocator = .init(&buffer); |
| 79 | |
| 80 | var threaded: std.Io.Threaded = .init(fba.allocator(), .{ |
| 81 | .argv0 = .empty, |
| 82 | .environ = .empty, |
| 83 | }); |
| 84 | defer threaded.deinit(); |
| 85 | const io = threaded.io(); |
| 86 | |
| 87 | const a: ByteArray256 = .{ .x = @splat(2) }; |
| 88 | const b: ByteArray256 = .{ .x = @splat(3) }; |
| 89 | const expected: ByteArray512 = .{ .x = @as([32]u8, @splat(2)) ++ @as([32]u8, @splat(3)) }; |
| 90 | |
| 91 | { |
| 92 | var future = io.async(concatByteArrays, .{ a, b }); |
| 93 | const result = future.await(io); |
| 94 | try std.testing.expectEqualSlices(u8, &expected.x, &result.x); |
| 95 | } |
| 96 | { |
| 97 | var future = io.concurrent(concatByteArrays, .{ a, b }) catch |err| switch (err) { |
| 98 | error.ConcurrencyUnavailable => { |
| 99 | try testing.expect(builtin.single_threaded); |
| 100 | return; |
| 101 | }, |
| 102 | }; |
| 103 | const result = future.await(io); |
| 104 | try std.testing.expectEqualSlices(u8, &expected.x, &result.x); |
| 105 | } |
| 106 | } |
| 107 | |
| 108 | fn concatByteArraysResultPtr(a: ByteArray256, b: ByteArray256, result: *ByteArray512) void { |
| 109 | result.* = .{ .x = a.x ++ b.x }; |
| 110 | } |
| 111 | |
| 112 | test "Group.async context alignment" { |
| 113 | var buffer: [2048]u8 align(@alignOf(ByteArray512)) = undefined; |
| 114 | var fba: std.heap.FixedBufferAllocator = .init(&buffer); |
| 115 | |
| 116 | var threaded: std.Io.Threaded = .init(fba.allocator(), .{ |
| 117 | .argv0 = .empty, |
| 118 | .environ = .empty, |
| 119 | }); |
| 120 | defer threaded.deinit(); |
| 121 | const io = threaded.io(); |
| 122 | |
| 123 | const a: ByteArray256 = .{ .x = @splat(2) }; |
| 124 | const b: ByteArray256 = .{ .x = @splat(3) }; |
| 125 | const expected: ByteArray512 = .{ .x = @as([32]u8, @splat(2)) ++ @as([32]u8, @splat(3)) }; |
| 126 | |
| 127 | var group: std.Io.Group = .init; |
| 128 | var result: ByteArray512 = undefined; |
| 129 | group.async(io, concatByteArraysResultPtr, .{ a, b, &result }); |
| 130 | try group.await(io); |
| 131 | try std.testing.expectEqualSlices(u8, &expected.x, &result.x); |
| 132 | } |
| 133 | |
| 134 | fn returnArray() [32]u8 { |
| 135 | return @splat(5); |
| 136 | } |
| 137 | |
| 138 | test "async with array return type" { |
| 139 | var threaded: std.Io.Threaded = .init(std.testing.allocator, .{ |
| 140 | .argv0 = .empty, |
| 141 | .environ = .empty, |
| 142 | }); |
| 143 | defer threaded.deinit(); |
| 144 | const io = threaded.io(); |
| 145 | |
| 146 | var future = io.async(returnArray, .{}); |
| 147 | const result = future.await(io); |
| 148 | try std.testing.expectEqualSlices(u8, &@as([32]u8, @splat(5)), &result); |
| 149 | } |
| 150 | |
| 151 | test "cancel blocked read from pipe" { |
| 152 | const global = struct { |
| 153 | fn readFromPipe(io: Io, pipe: Io.File) !void { |
| 154 | var buf: [1]u8 = undefined; |
| 155 | if (pipe.readStreaming(io, &.{&buf})) |_| { |
| 156 | return error.UnexpectedData; |
| 157 | } else |err| switch (err) { |
| 158 | error.Canceled => return, |
| 159 | else => |e| return e, |
| 160 | } |
| 161 | } |
| 162 | }; |
| 163 | |
| 164 | var threaded: std.Io.Threaded = .init(std.testing.allocator, .{ |
| 165 | .argv0 = .empty, |
| 166 | .environ = .empty, |
| 167 | }); |
| 168 | defer threaded.deinit(); |
| 169 | const io = threaded.io(); |
| 170 | |
| 171 | var read_end: Io.File = undefined; |
| 172 | var write_end: Io.File = undefined; |
| 173 | switch (builtin.target.os.tag) { |
| 174 | .wasi => return error.SkipZigTest, |
| 175 | .windows => { |
| 176 | const pipe = try threaded.windowsCreatePipe(.{ |
| 177 | .server = .{ .mode = .{ .IO = .SYNCHRONOUS_NONALERT } }, |
| 178 | .client = .{ .mode = .{ .IO = .SYNCHRONOUS_NONALERT } }, |
| 179 | .inbound = true, |
| 180 | }); |
| 181 | read_end = .{ .handle = pipe[0], .flags = .{ .nonblocking = false } }; |
| 182 | write_end = .{ .handle = pipe[1], .flags = .{ .nonblocking = false } }; |
| 183 | }, |
| 184 | else => { |
| 185 | const pipe = try std.Io.Threaded.pipe2(.{ .CLOEXEC = true }); |
| 186 | read_end = .{ .handle = pipe[0], .flags = .{ .nonblocking = false } }; |
| 187 | write_end = .{ .handle = pipe[1], .flags = .{ .nonblocking = false } }; |
| 188 | }, |
| 189 | } |
| 190 | defer { |
| 191 | read_end.close(io); |
| 192 | write_end.close(io); |
| 193 | } |
| 194 | |
| 195 | var future = io.concurrent(global.readFromPipe, .{ io, read_end }) catch |err| switch (err) { |
| 196 | error.ConcurrencyUnavailable => return error.SkipZigTest, |
| 197 | }; |
| 198 | defer _ = future.cancel(io) catch {}; |
| 199 | try io.sleep(.fromMilliseconds(10), .awake); |
| 200 | try future.cancel(io); |
| 201 | } |
| 202 | |
| 203 | test "memory mapping fallback" { |
| 204 | if (builtin.os.tag == .wasi and builtin.link_libc) { |
| 205 | // https://github.com/ziglang/zig/issues/20747 (open fd does not have write permission) |
| 206 | return error.SkipZigTest; |
| 207 | } |
| 208 | |
| 209 | var threaded: std.Io.Threaded = .init(std.testing.allocator, .{ |
| 210 | .argv0 = .empty, |
| 211 | .environ = .empty, |
| 212 | .disable_memory_mapping = true, |
| 213 | }); |
| 214 | defer threaded.deinit(); |
| 215 | const io = threaded.io(); |
| 216 | |
| 217 | var tmp = testing.tmpDir(.{}); |
| 218 | defer tmp.cleanup(); |
| 219 | |
| 220 | try tmp.dir.writeFile(io, .{ |
| 221 | .sub_path = "blah.txt", |
| 222 | .data = "this is my data123", |
| 223 | }); |
| 224 | |
| 225 | { |
| 226 | var file = try tmp.dir.openFile(io, "blah.txt", .{ .mode = .read_write }); |
| 227 | defer file.close(io); |
| 228 | |
| 229 | // The `Io.File.MemoryMap` API does not specify what happens if we supply a |
| 230 | // length greater than file size, but this is testing specifically std.Io.Threaded |
| 231 | // with disable_memory_mapping = true. |
| 232 | var mm = try file.createMemoryMap(io, .{ .len = "this is my data123".len + 3 }); |
| 233 | defer mm.destroy(io); |
| 234 | |
| 235 | try testing.expectEqualStrings("this is my data123\x00\x00\x00", mm.memory); |
| 236 | mm.memory[4] = '9'; |
| 237 | mm.memory[7] = '9'; |
| 238 | |
| 239 | try mm.write(io); |
| 240 | } |
| 241 | |
| 242 | var buffer: [100]u8 = undefined; |
| 243 | const updated_contents = try tmp.dir.readFile(io, "blah.txt", &buffer); |
| 244 | try testing.expectEqualStrings("this9is9my data123\x00\x00\x00", updated_contents); |
| 245 | |
| 246 | { |
| 247 | var file = try tmp.dir.openFile(io, "blah.txt", .{ .mode = .read_only }); |
| 248 | defer file.close(io); |
| 249 | |
| 250 | var mm = try file.createMemoryMap(io, .{ |
| 251 | .len = "this9is9my".len, |
| 252 | .protection = .{ .read = true }, |
| 253 | }); |
| 254 | defer mm.destroy(io); |
| 255 | |
| 256 | try testing.expectEqualStrings("this9is9my", mm.memory); |
| 257 | |
| 258 | const new_len = "this9is9my data123".len; |
| 259 | mm.setLength(io, new_len) catch |err| switch (err) { |
| 260 | error.OperationUnsupported => { |
| 261 | mm.destroy(io); |
| 262 | mm = try file.createMemoryMap(io, .{ .len = new_len }); |
| 263 | }, |
| 264 | else => |e| return e, |
| 265 | }; |
| 266 | try mm.read(io); |
| 267 | |
| 268 | try testing.expectEqualStrings("this9is9my data123", mm.memory); |
| 269 | } |
| 270 | } |
| 271 | |
| 272 | /// Wrapper around RtlDosPathNameToNtPathName_U for use in comparing |
| 273 | /// the behavior of RtlDosPathNameToNtPathName_U with wToPrefixedFileW |
| 274 | /// Note: RtlDosPathNameToNtPathName_U is not used in the Zig implementation |
| 275 | /// because it allocates. |
| 276 | fn RtlDosPathNameToNtPathName_U(path: [:0]const u16) !Io.Threaded.WindowsPathSpace { |
| 277 | var out: windows.UNICODE_STRING = undefined; |
| 278 | if (!windows.ntdll.RtlDosPathNameToNtPathName_U(path, &out, null, null).toBool()) return error.BadPathName; |
| 279 | defer windows.ntdll.RtlFreeUnicodeString(&out); |
| 280 | |
| 281 | var path_space: Io.Threaded.WindowsPathSpace = undefined; |
| 282 | const out_path = out.slice(); |
| 283 | @memcpy(path_space.data[0..out_path.len], out_path); |
| 284 | path_space.len = out.Length / 2; |
| 285 | path_space.data[path_space.len] = 0; |
| 286 | |
| 287 | return path_space; |
| 288 | } |
| 289 | |
| 290 | /// Test that the Zig conversion matches the expected_path (for instances where |
| 291 | /// the Zig implementation intentionally diverges from what RtlDosPathNameToNtPathName_U does). |
| 292 | fn testToPrefixedFileNoOracle(comptime path: []const u8, comptime expected_path: []const u8) !void { |
| 293 | const path_utf16 = std.unicode.utf8ToUtf16LeStringLiteral(path); |
| 294 | const expected_path_utf16 = std.unicode.utf8ToUtf16LeStringLiteral(expected_path); |
| 295 | const actual_path = try Io.Threaded.wToPrefixedFileW(null, path_utf16, .{}); |
| 296 | std.testing.expectEqualSlices(u16, expected_path_utf16, actual_path.span()) catch |e| { |
| 297 | std.debug.print("got '{f}', expected '{f}'\n", .{ std.unicode.fmtUtf16Le(actual_path.span()), std.unicode.fmtUtf16Le(expected_path_utf16) }); |
| 298 | return e; |
| 299 | }; |
| 300 | } |
| 301 | |
| 302 | /// Test that the Zig conversion matches the expected_path and that the |
| 303 | /// expected_path matches the conversion that RtlDosPathNameToNtPathName_U does. |
| 304 | fn testToPrefixedFileWithOracle(comptime path: []const u8, comptime expected_path: []const u8) !void { |
| 305 | try testToPrefixedFileNoOracle(path, expected_path); |
| 306 | try testToPrefixedFileOnlyOracle(path); |
| 307 | } |
| 308 | |
| 309 | /// Test that the Zig conversion matches the conversion that RtlDosPathNameToNtPathName_U does. |
| 310 | fn testToPrefixedFileOnlyOracle(comptime path: []const u8) !void { |
| 311 | const path_utf16 = std.unicode.utf8ToUtf16LeStringLiteral(path); |
| 312 | const zig_result = try Io.Threaded.wToPrefixedFileW(null, path_utf16, .{}); |
| 313 | const win32_api_result = try RtlDosPathNameToNtPathName_U(path_utf16); |
| 314 | std.testing.expectEqualSlices(u16, win32_api_result.span(), zig_result.span()) catch |e| { |
| 315 | std.debug.print("got '{f}', expected '{f}'\n", .{ std.unicode.fmtUtf16Le(zig_result.span()), std.unicode.fmtUtf16Le(win32_api_result.span()) }); |
| 316 | return e; |
| 317 | }; |
| 318 | } |
| 319 | |
| 320 | test "toPrefixedFileW" { |
| 321 | if (builtin.os.tag != .windows) return error.SkipZigTest; |
| 322 | |
| 323 | // Most test cases come from https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html |
| 324 | // Note that these tests do not actually touch the filesystem or care about whether or not |
| 325 | // any of the paths actually exist or are otherwise valid. |
| 326 | |
| 327 | // Drive Absolute |
| 328 | try testToPrefixedFileWithOracle("X:\\ABC\\DEF", "\\??\\X:\\ABC\\DEF"); |
| 329 | try testToPrefixedFileWithOracle("X:\\", "\\??\\X:\\"); |
| 330 | try testToPrefixedFileWithOracle("X:\\ABC\\", "\\??\\X:\\ABC\\"); |
| 331 | // Trailing . and space characters are stripped |
| 332 | try testToPrefixedFileWithOracle("X:\\ABC\\DEF. .", "\\??\\X:\\ABC\\DEF"); |
| 333 | try testToPrefixedFileWithOracle("X:/ABC/DEF", "\\??\\X:\\ABC\\DEF"); |
| 334 | try testToPrefixedFileWithOracle("X:\\ABC\\..\\XYZ", "\\??\\X:\\XYZ"); |
| 335 | try testToPrefixedFileWithOracle("X:\\ABC\\..\\..\\..", "\\??\\X:\\"); |
| 336 | // Drive letter casing is unchanged |
| 337 | try testToPrefixedFileWithOracle("x:\\", "\\??\\x:\\"); |
| 338 | |
| 339 | // Drive Relative |
| 340 | // These tests depend on the CWD of the specified drive letter which can vary, |
| 341 | // so instead we just test that the Zig implementation matches the result of |
| 342 | // RtlDosPathNameToNtPathName_U. |
| 343 | // TODO: Setting the =X: environment variable didn't seem to affect |
| 344 | // RtlDosPathNameToNtPathName_U, not sure why that is but getting that |
| 345 | // to work could be an avenue to making these cases environment-independent. |
| 346 | // All -> are examples of the result if the X drive's cwd was X:\ABC |
| 347 | try testToPrefixedFileOnlyOracle("X:DEF\\GHI"); // -> \??\X:\ABC\DEF\GHI |
| 348 | try testToPrefixedFileOnlyOracle("X:"); // -> \??\X:\ABC |
| 349 | try testToPrefixedFileOnlyOracle("X:DEF. ."); // -> \??\X:\ABC\DEF |
| 350 | try testToPrefixedFileOnlyOracle("X:ABC\\..\\XYZ"); // -> \??\X:\ABC\XYZ |
| 351 | try testToPrefixedFileOnlyOracle("X:ABC\\..\\..\\.."); // -> \??\X:\ |
| 352 | try testToPrefixedFileOnlyOracle("x:"); // -> \??\X:\ABC |
| 353 | |
| 354 | // Rooted |
| 355 | // These tests depend on the drive letter of the CWD which can vary, so |
| 356 | // instead we just test that the Zig implementation matches the result of |
| 357 | // RtlDosPathNameToNtPathName_U. |
| 358 | // TODO: Getting the CWD path, getting the drive letter from it, and using it to |
| 359 | // construct the expected NT paths could be an avenue to making these cases |
| 360 | // environment-independent and therefore able to use testToPrefixedFileWithOracle. |
| 361 | // All -> are examples of the result if the CWD's drive letter was X |
| 362 | try testToPrefixedFileOnlyOracle("\\ABC\\DEF"); // -> \??\X:\ABC\DEF |
| 363 | try testToPrefixedFileOnlyOracle("\\"); // -> \??\X:\ |
| 364 | try testToPrefixedFileOnlyOracle("\\ABC\\DEF. ."); // -> \??\X:\ABC\DEF |
| 365 | try testToPrefixedFileOnlyOracle("/ABC/DEF"); // -> \??\X:\ABC\DEF |
| 366 | try testToPrefixedFileOnlyOracle("\\ABC\\..\\XYZ"); // -> \??\X:\XYZ |
| 367 | try testToPrefixedFileOnlyOracle("\\ABC\\..\\..\\.."); // -> \??\X:\ |
| 368 | |
| 369 | // Relative |
| 370 | // These cases differ in functionality to RtlDosPathNameToNtPathName_U. |
| 371 | // Relative paths remain relative if they don't have enough .. components |
| 372 | // to error with TooManyParentDirs |
| 373 | try testToPrefixedFileNoOracle("ABC\\DEF", "ABC\\DEF"); |
| 374 | // TODO: enable this if trailing . and spaces are stripped from relative paths |
| 375 | //try testToPrefixedFileNoOracle("ABC\\DEF. .", "ABC\\DEF"); |
| 376 | try testToPrefixedFileNoOracle("ABC/DEF", "ABC\\DEF"); |
| 377 | try testToPrefixedFileNoOracle("./ABC/.././DEF", "DEF"); |
| 378 | // TooManyParentDirs, so resolved relative to the CWD |
| 379 | // All -> are examples of the result if the CWD was X:\ABC\DEF |
| 380 | try testToPrefixedFileOnlyOracle("..\\GHI"); // -> \??\X:\ABC\GHI |
| 381 | try testToPrefixedFileOnlyOracle("GHI\\..\\..\\.."); // -> \??\X:\ |
| 382 | |
| 383 | // UNC Absolute |
| 384 | try testToPrefixedFileWithOracle("\\\\server\\share\\ABC\\DEF", "\\??\\UNC\\server\\share\\ABC\\DEF"); |
| 385 | try testToPrefixedFileWithOracle("\\\\server", "\\??\\UNC\\server"); |
| 386 | try testToPrefixedFileWithOracle("\\\\server\\share", "\\??\\UNC\\server\\share"); |
| 387 | try testToPrefixedFileWithOracle("\\\\server\\share\\ABC. .", "\\??\\UNC\\server\\share\\ABC"); |
| 388 | try testToPrefixedFileWithOracle("//server/share/ABC/DEF", "\\??\\UNC\\server\\share\\ABC\\DEF"); |
| 389 | try testToPrefixedFileWithOracle("\\\\server\\share\\ABC\\..\\XYZ", "\\??\\UNC\\server\\share\\XYZ"); |
| 390 | try testToPrefixedFileWithOracle("\\\\server\\share\\ABC\\..\\..\\..", "\\??\\UNC\\server\\share"); |
| 391 | |
| 392 | // Local Device |
| 393 | try testToPrefixedFileWithOracle("\\\\.\\COM20", "\\??\\COM20"); |
| 394 | try testToPrefixedFileWithOracle("\\\\.\\pipe\\mypipe", "\\??\\pipe\\mypipe"); |
| 395 | try testToPrefixedFileWithOracle("\\\\.\\X:\\ABC\\DEF. .", "\\??\\X:\\ABC\\DEF"); |
| 396 | try testToPrefixedFileWithOracle("\\\\.\\X:/ABC/DEF", "\\??\\X:\\ABC\\DEF"); |
| 397 | try testToPrefixedFileWithOracle("\\\\.\\X:\\ABC\\..\\XYZ", "\\??\\X:\\XYZ"); |
| 398 | // Can replace the first component of the path (contrary to drive absolute and UNC absolute paths) |
| 399 | try testToPrefixedFileWithOracle("\\\\.\\X:\\ABC\\..\\..\\C:\\", "\\??\\C:\\"); |
| 400 | try testToPrefixedFileWithOracle("\\\\.\\pipe\\mypipe\\..\\notmine", "\\??\\pipe\\notmine"); |
| 401 | |
| 402 | // Special-case device names |
| 403 | // TODO: Enable once these are supported |
| 404 | // more cases to test here: https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html |
| 405 | //try testToPrefixedFileWithOracle("COM1", "\\??\\COM1"); |
| 406 | // Sometimes the special-cased device names are not respected |
| 407 | try testToPrefixedFileWithOracle("\\\\.\\X:\\COM1", "\\??\\X:\\COM1"); |
| 408 | try testToPrefixedFileWithOracle("\\\\abc\\xyz\\COM1", "\\??\\UNC\\abc\\xyz\\COM1"); |
| 409 | |
| 410 | // Verbatim |
| 411 | // Left untouched except \\?\ is replaced by \??\ |
| 412 | try testToPrefixedFileWithOracle("\\\\?\\X:", "\\??\\X:"); |
| 413 | try testToPrefixedFileWithOracle("\\\\?\\X:\\COM1", "\\??\\X:\\COM1"); |
| 414 | try testToPrefixedFileWithOracle("\\\\?\\X:/ABC/DEF. .", "\\??\\X:/ABC/DEF. ."); |
| 415 | try testToPrefixedFileWithOracle("\\\\?\\X:\\ABC\\..\\..\\..", "\\??\\X:\\ABC\\..\\..\\.."); |
| 416 | // NT Namespace |
| 417 | // Fully unmodified |
| 418 | try testToPrefixedFileWithOracle("\\??\\X:", "\\??\\X:"); |
| 419 | try testToPrefixedFileWithOracle("\\??\\X:\\COM1", "\\??\\X:\\COM1"); |
| 420 | try testToPrefixedFileWithOracle("\\??\\X:/ABC/DEF. .", "\\??\\X:/ABC/DEF. ."); |
| 421 | try testToPrefixedFileWithOracle("\\??\\X:\\ABC\\..\\..\\..", "\\??\\X:\\ABC\\..\\..\\.."); |
| 422 | |
| 423 | // 'Fake' Verbatim |
| 424 | // If the prefix looks like the verbatim prefix but not all path separators in the |
| 425 | // prefix are backslashes, then it gets canonicalized and the prefix is dropped in favor |
| 426 | // of the NT prefix. |
| 427 | try testToPrefixedFileWithOracle("//?/C:/ABC", "\\??\\C:\\ABC"); |
| 428 | // 'Fake' NT |
| 429 | // If the prefix looks like the NT prefix but not all path separators in the prefix |
| 430 | // are backslashes, then it gets canonicalized and the /??/ is not dropped but |
| 431 | // rather treated as part of the path. In other words, the path is treated |
| 432 | // as a rooted path, so the final path is resolved relative to the CWD's |
| 433 | // drive letter. |
| 434 | // The -> shows an example of the result if the CWD's drive letter was X |
| 435 | try testToPrefixedFileOnlyOracle("/??/C:/ABC"); // -> \??\X:\??\C:\ABC |
| 436 | |
| 437 | // Root Local Device |
| 438 | // \\. and \\? always get converted to \??\ |
| 439 | try testToPrefixedFileWithOracle("\\\\.", "\\??\\"); |
| 440 | try testToPrefixedFileWithOracle("\\\\?", "\\??\\"); |
| 441 | try testToPrefixedFileWithOracle("//?", "\\??\\"); |
| 442 | try testToPrefixedFileWithOracle("//.", "\\??\\"); |
| 443 | } |
| 444 | |
| 445 | fn testRemoveDotDirs(str: []const u8, expected: []const u8) !void { |
| 446 | const mutable = try testing.allocator.dupe(u8, str); |
| 447 | defer testing.allocator.free(mutable); |
| 448 | const actual = mutable[0..try windows.removeDotDirsSanitized(u8, mutable)]; |
| 449 | try testing.expect(std.mem.eql(u8, actual, expected)); |
| 450 | } |
| 451 | fn testRemoveDotDirsError(err: anyerror, str: []const u8) !void { |
| 452 | const mutable = try testing.allocator.dupe(u8, str); |
| 453 | defer testing.allocator.free(mutable); |
| 454 | try testing.expectError(err, windows.removeDotDirsSanitized(u8, mutable)); |
| 455 | } |
| 456 | test "removeDotDirs" { |
| 457 | try testRemoveDotDirs("", ""); |
| 458 | try testRemoveDotDirs(".", ""); |
| 459 | try testRemoveDotDirs(".\\", ""); |
| 460 | try testRemoveDotDirs(".\\.", ""); |
| 461 | try testRemoveDotDirs(".\\.\\", ""); |
| 462 | try testRemoveDotDirs(".\\.\\.", ""); |
| 463 | |
| 464 | try testRemoveDotDirs("a", "a"); |
| 465 | try testRemoveDotDirs("a\\", "a\\"); |
| 466 | try testRemoveDotDirs("a\\b", "a\\b"); |
| 467 | try testRemoveDotDirs("a\\.", "a\\"); |
| 468 | try testRemoveDotDirs("a\\b\\.", "a\\b\\"); |
| 469 | try testRemoveDotDirs("a\\.\\b", "a\\b"); |
| 470 | |
| 471 | try testRemoveDotDirs(".a", ".a"); |
| 472 | try testRemoveDotDirs(".a\\", ".a\\"); |
| 473 | try testRemoveDotDirs(".a\\.b", ".a\\.b"); |
| 474 | try testRemoveDotDirs(".a\\.", ".a\\"); |
| 475 | try testRemoveDotDirs(".a\\.\\.", ".a\\"); |
| 476 | try testRemoveDotDirs(".a\\.\\.\\.b", ".a\\.b"); |
| 477 | try testRemoveDotDirs(".a\\.\\.\\.b\\", ".a\\.b\\"); |
| 478 | |
| 479 | try testRemoveDotDirsError(error.TooManyParentDirs, ".."); |
| 480 | try testRemoveDotDirsError(error.TooManyParentDirs, "..\\"); |
| 481 | try testRemoveDotDirsError(error.TooManyParentDirs, ".\\..\\"); |
| 482 | try testRemoveDotDirsError(error.TooManyParentDirs, ".\\.\\..\\"); |
| 483 | |
| 484 | try testRemoveDotDirs("a\\..", ""); |
| 485 | try testRemoveDotDirs("a\\..\\", ""); |
| 486 | try testRemoveDotDirs("a\\..\\.", ""); |
| 487 | try testRemoveDotDirs("a\\..\\.\\", ""); |
| 488 | try testRemoveDotDirs("a\\..\\.\\.", ""); |
| 489 | try testRemoveDotDirsError(error.TooManyParentDirs, "a\\..\\.\\.\\.."); |
| 490 | |
| 491 | try testRemoveDotDirs("a\\..\\.\\.\\b", "b"); |
| 492 | try testRemoveDotDirs("a\\..\\.\\.\\b\\", "b\\"); |
| 493 | try testRemoveDotDirs("a\\..\\.\\.\\b\\.", "b\\"); |
| 494 | try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\", "b\\"); |
| 495 | try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\..", ""); |
| 496 | try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\..\\", ""); |
| 497 | try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\..\\.", ""); |
| 498 | try testRemoveDotDirsError(error.TooManyParentDirs, "a\\..\\.\\.\\b\\.\\..\\.\\.."); |
| 499 | |
| 500 | try testRemoveDotDirs("a\\b\\..\\", "a\\"); |
| 501 | try testRemoveDotDirs("a\\b\\..\\c", "a\\c"); |
| 502 | } |
| 503 | |
| 504 | const RTL_PATH_TYPE = enum(c_int) { |
| 505 | Unknown, |
| 506 | UncAbsolute, |
| 507 | DriveAbsolute, |
| 508 | DriveRelative, |
| 509 | Rooted, |
| 510 | Relative, |
| 511 | LocalDevice, |
| 512 | RootLocalDevice, |
| 513 | }; |
| 514 | |
| 515 | pub extern "ntdll" fn RtlDetermineDosPathNameType_U( |
| 516 | Path: [*:0]const u16, |
| 517 | ) callconv(.winapi) RTL_PATH_TYPE; |
| 518 | |
| 519 | test "getWin32PathType vs RtlDetermineDosPathNameType_U" { |
| 520 | if (builtin.os.tag != .windows) return error.SkipZigTest; |
| 521 | |
| 522 | var buf: std.ArrayList(u16) = .empty; |
| 523 | defer buf.deinit(std.testing.allocator); |
| 524 | |
| 525 | var wtf8_buf: std.ArrayList(u8) = .empty; |
| 526 | defer wtf8_buf.deinit(std.testing.allocator); |
| 527 | |
| 528 | var random = std.Random.DefaultPrng.init(std.testing.random_seed); |
| 529 | const rand = random.random(); |
| 530 | |
| 531 | for (0..1000) |_| { |
| 532 | buf.clearRetainingCapacity(); |
| 533 | const path = try getRandomWtf16Path(std.testing.allocator, &buf, rand); |
| 534 | wtf8_buf.clearRetainingCapacity(); |
| 535 | const wtf8_len = std.unicode.calcWtf8Len(path); |
| 536 | try wtf8_buf.ensureTotalCapacity(std.testing.allocator, wtf8_len); |
| 537 | wtf8_buf.items.len = wtf8_len; |
| 538 | std.debug.assert(std.unicode.wtf16LeToWtf8(wtf8_buf.items, path) == wtf8_len); |
| 539 | |
| 540 | const windows_type = RtlDetermineDosPathNameType_U(path); |
| 541 | const wtf16_type = std.fs.path.getWin32PathType(u16, path); |
| 542 | const wtf8_type = std.fs.path.getWin32PathType(u8, wtf8_buf.items); |
| 543 | |
| 544 | checkPathType(windows_type, wtf16_type) catch |err| { |
| 545 | std.debug.print("expected type {}, got {} for path: {f}\n", .{ windows_type, wtf16_type, std.unicode.fmtUtf16Le(path) }); |
| 546 | std.debug.print("path bytes:\n", .{}); |
| 547 | std.debug.dumpHex(std.mem.sliceAsBytes(path)); |
| 548 | return err; |
| 549 | }; |
| 550 | |
| 551 | if (wtf16_type != wtf8_type) { |
| 552 | std.debug.print("type mismatch between wtf8: {} and wtf16: {} for path: {f}\n", .{ wtf8_type, wtf16_type, std.unicode.fmtUtf16Le(path) }); |
| 553 | std.debug.print("wtf-16 path bytes:\n", .{}); |
| 554 | std.debug.dumpHex(std.mem.sliceAsBytes(path)); |
| 555 | std.debug.print("wtf-8 path bytes:\n", .{}); |
| 556 | std.debug.dumpHex(std.mem.sliceAsBytes(wtf8_buf.items)); |
| 557 | return error.Wtf8Wtf16Mismatch; |
| 558 | } |
| 559 | } |
| 560 | } |
| 561 | |
| 562 | fn checkPathType(windows_type: RTL_PATH_TYPE, zig_type: std.fs.path.Win32PathType) !void { |
| 563 | const expected_windows_type: RTL_PATH_TYPE = switch (zig_type) { |
| 564 | .unc_absolute => .UncAbsolute, |
| 565 | .drive_absolute => .DriveAbsolute, |
| 566 | .drive_relative => .DriveRelative, |
| 567 | .rooted => .Rooted, |
| 568 | .relative => .Relative, |
| 569 | .local_device => .LocalDevice, |
| 570 | .root_local_device => .RootLocalDevice, |
| 571 | }; |
| 572 | if (windows_type != expected_windows_type) return error.PathTypeMismatch; |
| 573 | } |
| 574 | |
| 575 | fn getRandomWtf16Path(allocator: std.mem.Allocator, buf: *std.ArrayList(u16), rand: std.Random) ![:0]const u16 { |
| 576 | const Choice = enum { |
| 577 | backslash, |
| 578 | slash, |
| 579 | control, |
| 580 | printable, |
| 581 | non_ascii, |
| 582 | }; |
| 583 | |
| 584 | const choices = rand.uintAtMostBiased(u16, 32); |
| 585 | |
| 586 | for (0..choices) |_| { |
| 587 | const choice = rand.enumValue(Choice); |
| 588 | const code_unit = switch (choice) { |
| 589 | .backslash => '\\', |
| 590 | .slash => '/', |
| 591 | .control => switch (rand.uintAtMostBiased(u8, 0x20)) { |
| 592 | 0x20 => '\x7F', |
| 593 | else => |b| b + 1, // no NUL |
| 594 | }, |
| 595 | .printable => '!' + rand.uintAtMostBiased(u8, '~' - '!'), |
| 596 | .non_ascii => rand.intRangeAtMostBiased(u16, 0x80, 0xFFFF), |
| 597 | }; |
| 598 | try buf.append(allocator, std.mem.nativeToLittle(u16, code_unit)); |
| 599 | } |
| 600 | |
| 601 | try buf.append(allocator, 0); |
| 602 | return buf.items[0 .. buf.items.len - 1 :0]; |
| 603 | } |