1//! Tests belong here if they access internal state of std.Io.Threaded or
2//! otherwise assume details of that particular implementation.
3const builtin = @import("builtin");
4
5const std = @import("std");
6const Io = std.Io;
7const testing = std.testing;
8const assert = std.debug.assert;
9const windows = std.os.windows;
10
11test "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
34fn put(io: Io, queue: *Io.Queue(u8)) void {
35 queue.putOneUncancelable(io, 42) catch unreachable;
36}
37
38fn get(io: Io, queue: *Io.Queue(u8)) void {
39 assert(queue.getOneUncancelable(io) catch unreachable == 42);
40}
41
42test "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
69const ByteArray256 = struct { x: [32]u8 align(32) };
70const ByteArray512 = struct { x: [64]u8 align(64) };
71
72fn concatByteArrays(a: ByteArray256, b: ByteArray256) ByteArray512 {
73 return .{ .x = a.x ++ b.x };
74}
75
76test "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
108fn concatByteArraysResultPtr(a: ByteArray256, b: ByteArray256, result: *ByteArray512) void {
109 result.* = .{ .x = a.x ++ b.x };
110}
111
112test "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
134fn returnArray() [32]u8 {
135 return @splat(5);
136}
137
138test "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
151test "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
203test "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.
276fn 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).
292fn 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.
304fn 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.
310fn 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
320test "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
445fn 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}
451fn 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}
456test "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
504const RTL_PATH_TYPE = enum(c_int) {
505 Unknown,
506 UncAbsolute,
507 DriveAbsolute,
508 DriveRelative,
509 Rooted,
510 Relative,
511 LocalDevice,
512 RootLocalDevice,
513};
514
515pub extern "ntdll" fn RtlDetermineDosPathNameType_U(
516 Path: [*:0]const u16,
517) callconv(.winapi) RTL_PATH_TYPE;
518
519test "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
562fn 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
575fn 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}