master
  1const std = @import("../../std.zig");
  2const builtin = @import("builtin");
  3const windows = std.os.windows;
  4const mem = std.mem;
  5const testing = std.testing;
  6
  7/// Wrapper around RtlDosPathNameToNtPathName_U for use in comparing
  8/// the behavior of RtlDosPathNameToNtPathName_U with wToPrefixedFileW
  9/// Note: RtlDosPathNameToNtPathName_U is not used in the Zig implementation
 10//        because it allocates.
 11fn RtlDosPathNameToNtPathName_U(path: [:0]const u16) !windows.PathSpace {
 12    var out: windows.UNICODE_STRING = undefined;
 13    const rc = windows.ntdll.RtlDosPathNameToNtPathName_U(path, &out, null, null);
 14    if (rc != windows.TRUE) return error.BadPathName;
 15    defer windows.ntdll.RtlFreeUnicodeString(&out);
 16
 17    var path_space: windows.PathSpace = undefined;
 18    const out_path = out.Buffer.?[0 .. out.Length / 2];
 19    @memcpy(path_space.data[0..out_path.len], out_path);
 20    path_space.len = out.Length / 2;
 21    path_space.data[path_space.len] = 0;
 22
 23    return path_space;
 24}
 25
 26/// Test that the Zig conversion matches the expected_path (for instances where
 27/// the Zig implementation intentionally diverges from what RtlDosPathNameToNtPathName_U does).
 28fn testToPrefixedFileNoOracle(comptime path: []const u8, comptime expected_path: []const u8) !void {
 29    const path_utf16 = std.unicode.utf8ToUtf16LeStringLiteral(path);
 30    const expected_path_utf16 = std.unicode.utf8ToUtf16LeStringLiteral(expected_path);
 31    const actual_path = try windows.wToPrefixedFileW(null, path_utf16);
 32    std.testing.expectEqualSlices(u16, expected_path_utf16, actual_path.span()) catch |e| {
 33        std.debug.print("got '{f}', expected '{f}'\n", .{ std.unicode.fmtUtf16Le(actual_path.span()), std.unicode.fmtUtf16Le(expected_path_utf16) });
 34        return e;
 35    };
 36}
 37
 38/// Test that the Zig conversion matches the expected_path and that the
 39/// expected_path matches the conversion that RtlDosPathNameToNtPathName_U does.
 40fn testToPrefixedFileWithOracle(comptime path: []const u8, comptime expected_path: []const u8) !void {
 41    try testToPrefixedFileNoOracle(path, expected_path);
 42    try testToPrefixedFileOnlyOracle(path);
 43}
 44
 45/// Test that the Zig conversion matches the conversion that RtlDosPathNameToNtPathName_U does.
 46fn testToPrefixedFileOnlyOracle(comptime path: []const u8) !void {
 47    const path_utf16 = std.unicode.utf8ToUtf16LeStringLiteral(path);
 48    const zig_result = try windows.wToPrefixedFileW(null, path_utf16);
 49    const win32_api_result = try RtlDosPathNameToNtPathName_U(path_utf16);
 50    std.testing.expectEqualSlices(u16, win32_api_result.span(), zig_result.span()) catch |e| {
 51        std.debug.print("got '{f}', expected '{f}'\n", .{ std.unicode.fmtUtf16Le(zig_result.span()), std.unicode.fmtUtf16Le(win32_api_result.span()) });
 52        return e;
 53    };
 54}
 55
 56test "toPrefixedFileW" {
 57    if (builtin.os.tag != .windows) return error.SkipZigTest;
 58
 59    // Most test cases come from https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
 60    // Note that these tests do not actually touch the filesystem or care about whether or not
 61    // any of the paths actually exist or are otherwise valid.
 62
 63    // Drive Absolute
 64    try testToPrefixedFileWithOracle("X:\\ABC\\DEF", "\\??\\X:\\ABC\\DEF");
 65    try testToPrefixedFileWithOracle("X:\\", "\\??\\X:\\");
 66    try testToPrefixedFileWithOracle("X:\\ABC\\", "\\??\\X:\\ABC\\");
 67    // Trailing . and space characters are stripped
 68    try testToPrefixedFileWithOracle("X:\\ABC\\DEF. .", "\\??\\X:\\ABC\\DEF");
 69    try testToPrefixedFileWithOracle("X:/ABC/DEF", "\\??\\X:\\ABC\\DEF");
 70    try testToPrefixedFileWithOracle("X:\\ABC\\..\\XYZ", "\\??\\X:\\XYZ");
 71    try testToPrefixedFileWithOracle("X:\\ABC\\..\\..\\..", "\\??\\X:\\");
 72    // Drive letter casing is unchanged
 73    try testToPrefixedFileWithOracle("x:\\", "\\??\\x:\\");
 74
 75    // Drive Relative
 76    // These tests depend on the CWD of the specified drive letter which can vary,
 77    // so instead we just test that the Zig implementation matches the result of
 78    // RtlDosPathNameToNtPathName_U.
 79    // TODO: Setting the =X: environment variable didn't seem to affect
 80    //       RtlDosPathNameToNtPathName_U, not sure why that is but getting that
 81    //       to work could be an avenue to making these cases environment-independent.
 82    // All -> are examples of the result if the X drive's cwd was X:\ABC
 83    try testToPrefixedFileOnlyOracle("X:DEF\\GHI"); // -> \??\X:\ABC\DEF\GHI
 84    try testToPrefixedFileOnlyOracle("X:"); // -> \??\X:\ABC
 85    try testToPrefixedFileOnlyOracle("X:DEF. ."); // -> \??\X:\ABC\DEF
 86    try testToPrefixedFileOnlyOracle("X:ABC\\..\\XYZ"); // -> \??\X:\ABC\XYZ
 87    try testToPrefixedFileOnlyOracle("X:ABC\\..\\..\\.."); // -> \??\X:\
 88    try testToPrefixedFileOnlyOracle("x:"); // -> \??\X:\ABC
 89
 90    // Rooted
 91    // These tests depend on the drive letter of the CWD which can vary, so
 92    // instead we just test that the Zig implementation matches the result of
 93    // RtlDosPathNameToNtPathName_U.
 94    // TODO: Getting the CWD path, getting the drive letter from it, and using it to
 95    //       construct the expected NT paths could be an avenue to making these cases
 96    //       environment-independent and therefore able to use testToPrefixedFileWithOracle.
 97    // All -> are examples of the result if the CWD's drive letter was X
 98    try testToPrefixedFileOnlyOracle("\\ABC\\DEF"); // -> \??\X:\ABC\DEF
 99    try testToPrefixedFileOnlyOracle("\\"); // -> \??\X:\
100    try testToPrefixedFileOnlyOracle("\\ABC\\DEF. ."); // -> \??\X:\ABC\DEF
101    try testToPrefixedFileOnlyOracle("/ABC/DEF"); // -> \??\X:\ABC\DEF
102    try testToPrefixedFileOnlyOracle("\\ABC\\..\\XYZ"); // -> \??\X:\XYZ
103    try testToPrefixedFileOnlyOracle("\\ABC\\..\\..\\.."); // -> \??\X:\
104
105    // Relative
106    // These cases differ in functionality to RtlDosPathNameToNtPathName_U.
107    // Relative paths remain relative if they don't have enough .. components
108    // to error with TooManyParentDirs
109    try testToPrefixedFileNoOracle("ABC\\DEF", "ABC\\DEF");
110    // TODO: enable this if trailing . and spaces are stripped from relative paths
111    //try testToPrefixedFileNoOracle("ABC\\DEF. .", "ABC\\DEF");
112    try testToPrefixedFileNoOracle("ABC/DEF", "ABC\\DEF");
113    try testToPrefixedFileNoOracle("./ABC/.././DEF", "DEF");
114    // TooManyParentDirs, so resolved relative to the CWD
115    // All -> are examples of the result if the CWD was X:\ABC\DEF
116    try testToPrefixedFileOnlyOracle("..\\GHI"); // -> \??\X:\ABC\GHI
117    try testToPrefixedFileOnlyOracle("GHI\\..\\..\\.."); // -> \??\X:\
118
119    // UNC Absolute
120    try testToPrefixedFileWithOracle("\\\\server\\share\\ABC\\DEF", "\\??\\UNC\\server\\share\\ABC\\DEF");
121    try testToPrefixedFileWithOracle("\\\\server", "\\??\\UNC\\server");
122    try testToPrefixedFileWithOracle("\\\\server\\share", "\\??\\UNC\\server\\share");
123    try testToPrefixedFileWithOracle("\\\\server\\share\\ABC. .", "\\??\\UNC\\server\\share\\ABC");
124    try testToPrefixedFileWithOracle("//server/share/ABC/DEF", "\\??\\UNC\\server\\share\\ABC\\DEF");
125    try testToPrefixedFileWithOracle("\\\\server\\share\\ABC\\..\\XYZ", "\\??\\UNC\\server\\share\\XYZ");
126    try testToPrefixedFileWithOracle("\\\\server\\share\\ABC\\..\\..\\..", "\\??\\UNC\\server\\share");
127
128    // Local Device
129    try testToPrefixedFileWithOracle("\\\\.\\COM20", "\\??\\COM20");
130    try testToPrefixedFileWithOracle("\\\\.\\pipe\\mypipe", "\\??\\pipe\\mypipe");
131    try testToPrefixedFileWithOracle("\\\\.\\X:\\ABC\\DEF. .", "\\??\\X:\\ABC\\DEF");
132    try testToPrefixedFileWithOracle("\\\\.\\X:/ABC/DEF", "\\??\\X:\\ABC\\DEF");
133    try testToPrefixedFileWithOracle("\\\\.\\X:\\ABC\\..\\XYZ", "\\??\\X:\\XYZ");
134    // Can replace the first component of the path (contrary to drive absolute and UNC absolute paths)
135    try testToPrefixedFileWithOracle("\\\\.\\X:\\ABC\\..\\..\\C:\\", "\\??\\C:\\");
136    try testToPrefixedFileWithOracle("\\\\.\\pipe\\mypipe\\..\\notmine", "\\??\\pipe\\notmine");
137
138    // Special-case device names
139    // TODO: Enable once these are supported
140    //       more cases to test here: https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
141    //try testToPrefixedFileWithOracle("COM1", "\\??\\COM1");
142    // Sometimes the special-cased device names are not respected
143    try testToPrefixedFileWithOracle("\\\\.\\X:\\COM1", "\\??\\X:\\COM1");
144    try testToPrefixedFileWithOracle("\\\\abc\\xyz\\COM1", "\\??\\UNC\\abc\\xyz\\COM1");
145
146    // Verbatim
147    // Left untouched except \\?\ is replaced by \??\
148    try testToPrefixedFileWithOracle("\\\\?\\X:", "\\??\\X:");
149    try testToPrefixedFileWithOracle("\\\\?\\X:\\COM1", "\\??\\X:\\COM1");
150    try testToPrefixedFileWithOracle("\\\\?\\X:/ABC/DEF. .", "\\??\\X:/ABC/DEF. .");
151    try testToPrefixedFileWithOracle("\\\\?\\X:\\ABC\\..\\..\\..", "\\??\\X:\\ABC\\..\\..\\..");
152    // NT Namespace
153    // Fully unmodified
154    try testToPrefixedFileWithOracle("\\??\\X:", "\\??\\X:");
155    try testToPrefixedFileWithOracle("\\??\\X:\\COM1", "\\??\\X:\\COM1");
156    try testToPrefixedFileWithOracle("\\??\\X:/ABC/DEF. .", "\\??\\X:/ABC/DEF. .");
157    try testToPrefixedFileWithOracle("\\??\\X:\\ABC\\..\\..\\..", "\\??\\X:\\ABC\\..\\..\\..");
158
159    // 'Fake' Verbatim
160    // If the prefix looks like the verbatim prefix but not all path separators in the
161    // prefix are backslashes, then it gets canonicalized and the prefix is dropped in favor
162    // of the NT prefix.
163    try testToPrefixedFileWithOracle("//?/C:/ABC", "\\??\\C:\\ABC");
164    // 'Fake' NT
165    // If the prefix looks like the NT prefix but not all path separators in the prefix
166    // are backslashes, then it gets canonicalized and the /??/ is not dropped but
167    // rather treated as part of the path. In other words, the path is treated
168    // as a rooted path, so the final path is resolved relative to the CWD's
169    // drive letter.
170    // The -> shows an example of the result if the CWD's drive letter was X
171    try testToPrefixedFileOnlyOracle("/??/C:/ABC"); // -> \??\X:\??\C:\ABC
172
173    // Root Local Device
174    // \\. and \\? always get converted to \??\
175    try testToPrefixedFileWithOracle("\\\\.", "\\??\\");
176    try testToPrefixedFileWithOracle("\\\\?", "\\??\\");
177    try testToPrefixedFileWithOracle("//?", "\\??\\");
178    try testToPrefixedFileWithOracle("//.", "\\??\\");
179}
180
181fn testRemoveDotDirs(str: []const u8, expected: []const u8) !void {
182    const mutable = try testing.allocator.dupe(u8, str);
183    defer testing.allocator.free(mutable);
184    const actual = mutable[0..try windows.removeDotDirsSanitized(u8, mutable)];
185    try testing.expect(mem.eql(u8, actual, expected));
186}
187fn testRemoveDotDirsError(err: anyerror, str: []const u8) !void {
188    const mutable = try testing.allocator.dupe(u8, str);
189    defer testing.allocator.free(mutable);
190    try testing.expectError(err, windows.removeDotDirsSanitized(u8, mutable));
191}
192test "removeDotDirs" {
193    try testRemoveDotDirs("", "");
194    try testRemoveDotDirs(".", "");
195    try testRemoveDotDirs(".\\", "");
196    try testRemoveDotDirs(".\\.", "");
197    try testRemoveDotDirs(".\\.\\", "");
198    try testRemoveDotDirs(".\\.\\.", "");
199
200    try testRemoveDotDirs("a", "a");
201    try testRemoveDotDirs("a\\", "a\\");
202    try testRemoveDotDirs("a\\b", "a\\b");
203    try testRemoveDotDirs("a\\.", "a\\");
204    try testRemoveDotDirs("a\\b\\.", "a\\b\\");
205    try testRemoveDotDirs("a\\.\\b", "a\\b");
206
207    try testRemoveDotDirs(".a", ".a");
208    try testRemoveDotDirs(".a\\", ".a\\");
209    try testRemoveDotDirs(".a\\.b", ".a\\.b");
210    try testRemoveDotDirs(".a\\.", ".a\\");
211    try testRemoveDotDirs(".a\\.\\.", ".a\\");
212    try testRemoveDotDirs(".a\\.\\.\\.b", ".a\\.b");
213    try testRemoveDotDirs(".a\\.\\.\\.b\\", ".a\\.b\\");
214
215    try testRemoveDotDirsError(error.TooManyParentDirs, "..");
216    try testRemoveDotDirsError(error.TooManyParentDirs, "..\\");
217    try testRemoveDotDirsError(error.TooManyParentDirs, ".\\..\\");
218    try testRemoveDotDirsError(error.TooManyParentDirs, ".\\.\\..\\");
219
220    try testRemoveDotDirs("a\\..", "");
221    try testRemoveDotDirs("a\\..\\", "");
222    try testRemoveDotDirs("a\\..\\.", "");
223    try testRemoveDotDirs("a\\..\\.\\", "");
224    try testRemoveDotDirs("a\\..\\.\\.", "");
225    try testRemoveDotDirsError(error.TooManyParentDirs, "a\\..\\.\\.\\..");
226
227    try testRemoveDotDirs("a\\..\\.\\.\\b", "b");
228    try testRemoveDotDirs("a\\..\\.\\.\\b\\", "b\\");
229    try testRemoveDotDirs("a\\..\\.\\.\\b\\.", "b\\");
230    try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\", "b\\");
231    try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\..", "");
232    try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\..\\", "");
233    try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\..\\.", "");
234    try testRemoveDotDirsError(error.TooManyParentDirs, "a\\..\\.\\.\\b\\.\\..\\.\\..");
235
236    try testRemoveDotDirs("a\\b\\..\\", "a\\");
237    try testRemoveDotDirs("a\\b\\..\\c", "a\\c");
238}
239
240const RTL_PATH_TYPE = enum(c_int) {
241    Unknown,
242    UncAbsolute,
243    DriveAbsolute,
244    DriveRelative,
245    Rooted,
246    Relative,
247    LocalDevice,
248    RootLocalDevice,
249};
250
251pub extern "ntdll" fn RtlDetermineDosPathNameType_U(
252    Path: [*:0]const u16,
253) callconv(.winapi) RTL_PATH_TYPE;
254
255test "getWin32PathType vs RtlDetermineDosPathNameType_U" {
256    if (builtin.os.tag != .windows) return error.SkipZigTest;
257
258    var buf: std.ArrayList(u16) = .empty;
259    defer buf.deinit(std.testing.allocator);
260
261    var wtf8_buf: std.ArrayList(u8) = .empty;
262    defer wtf8_buf.deinit(std.testing.allocator);
263
264    var random = std.Random.DefaultPrng.init(std.testing.random_seed);
265    const rand = random.random();
266
267    for (0..1000) |_| {
268        buf.clearRetainingCapacity();
269        const path = try getRandomWtf16Path(std.testing.allocator, &buf, rand);
270        wtf8_buf.clearRetainingCapacity();
271        const wtf8_len = std.unicode.calcWtf8Len(path);
272        try wtf8_buf.ensureTotalCapacity(std.testing.allocator, wtf8_len);
273        wtf8_buf.items.len = wtf8_len;
274        std.debug.assert(std.unicode.wtf16LeToWtf8(wtf8_buf.items, path) == wtf8_len);
275
276        const windows_type = RtlDetermineDosPathNameType_U(path);
277        const wtf16_type = windows.getWin32PathType(u16, path);
278        const wtf8_type = windows.getWin32PathType(u8, wtf8_buf.items);
279
280        checkPathType(windows_type, wtf16_type) catch |err| {
281            std.debug.print("expected type {}, got {} for path: {f}\n", .{ windows_type, wtf16_type, std.unicode.fmtUtf16Le(path) });
282            std.debug.print("path bytes:\n", .{});
283            std.debug.dumpHex(std.mem.sliceAsBytes(path));
284            return err;
285        };
286
287        if (wtf16_type != wtf8_type) {
288            std.debug.print("type mismatch between wtf8: {} and wtf16: {} for path: {f}\n", .{ wtf8_type, wtf16_type, std.unicode.fmtUtf16Le(path) });
289            std.debug.print("wtf-16 path bytes:\n", .{});
290            std.debug.dumpHex(std.mem.sliceAsBytes(path));
291            std.debug.print("wtf-8 path bytes:\n", .{});
292            std.debug.dumpHex(std.mem.sliceAsBytes(wtf8_buf.items));
293            return error.Wtf8Wtf16Mismatch;
294        }
295    }
296}
297
298fn checkPathType(windows_type: RTL_PATH_TYPE, zig_type: windows.Win32PathType) !void {
299    const expected_windows_type: RTL_PATH_TYPE = switch (zig_type) {
300        .unc_absolute => .UncAbsolute,
301        .drive_absolute => .DriveAbsolute,
302        .drive_relative => .DriveRelative,
303        .rooted => .Rooted,
304        .relative => .Relative,
305        .local_device => .LocalDevice,
306        .root_local_device => .RootLocalDevice,
307    };
308    if (windows_type != expected_windows_type) return error.PathTypeMismatch;
309}
310
311fn getRandomWtf16Path(allocator: std.mem.Allocator, buf: *std.ArrayList(u16), rand: std.Random) ![:0]const u16 {
312    const Choice = enum {
313        backslash,
314        slash,
315        control,
316        printable,
317        non_ascii,
318    };
319
320    const choices = rand.uintAtMostBiased(u16, 32);
321
322    for (0..choices) |_| {
323        const choice = rand.enumValue(Choice);
324        const code_unit = switch (choice) {
325            .backslash => '\\',
326            .slash => '/',
327            .control => switch (rand.uintAtMostBiased(u8, 0x20)) {
328                0x20 => '\x7F',
329                else => |b| b + 1, // no NUL
330            },
331            .printable => '!' + rand.uintAtMostBiased(u8, '~' - '!'),
332            .non_ascii => rand.intRangeAtMostBiased(u16, 0x80, 0xFFFF),
333        };
334        try buf.append(allocator, std.mem.nativeToLittle(u16, code_unit));
335    }
336
337    try buf.append(allocator, 0);
338    return buf.items[0 .. buf.items.len - 1 :0];
339}