From 1697d44809151e4759f6b5f9447a908c30ac1e84 Mon Sep 17 00:00:00 2001 From: Ryan Liptak Date: Thu, 18 May 2023 03:45:21 -0700 Subject: [PATCH] Windows: Support UNC, rooted, drive relative, and namespaced/device paths There are many different types of Windows paths, and there are a few different possible namespaces on top of that. Before this commit, NT namespaced paths were somewhat supported, and for Win32 paths (those without a namespace prefix), only relative and drive absolute paths were supported. After this commit, all of the following are supported: - Device namespaced paths (`\\.\`) - Verbatim paths (`\\?\`) - NT-namespaced paths (`\??\`) - Relative paths (`foo`) - Drive-absolute paths (`C:\foo`) - Drive-relative paths (`C:foo`) - Rooted paths (`\foo`) - UNC absolute paths (`\\server\share\foo`) - Root local device paths (`\\.` or `\\?` exactly) Plus: - Any of the path types and namespace types can be mixed and matched together as appropriate. - All of the `std.os.windows.*ToPrefixedFileW` functions will accept any path type, prefixed or not, and do the appropriate thing to convert them to an NT-prefixed path if necessary. This is achieved by making the `std.os.windows.*ToPrefixedFileW` functions behave like `ntdll.RtlDosPathNameToNtPathName_U`, but with a few differences: - Does not allocate on the heap (this is why we can't use `ntdll.RtlDosPathNameToNtPathName_U` directly, it does internal heap allocation). - Relative paths are kept as relative unless they contain too many .. components, in which case they are treated as 'drive relative' and resolved against the CWD (this is how it behaved before this commit as well). - Special case device names like COM1, NUL, etc are not handled specially (TODO) - `.` and space are not stripped from the end of relative paths (potential TODO) Most of the non-trivial conversion of non-relative paths is done via `ntdll.RtlGetFullPathName_U`, which AFAIK is used internally by `ntdll.RtlDosPathNameToNtPathName_U`. Some relevant reading on Windows paths: - https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html - https://chrisdenton.github.io/omnipath/Overview.html Closes #8205 Might close (untested) #12729 Note: - This removes checking for illegal characters in `std.os.windows.sliceToPrefixedFileW`, since the previous solution (iterate the whole string and error if any illegal characters were found) was naive and won't work for all path types. This is further complicated by things like file streams (where `:` is used as a delimiter, e.g. `file.ext:stream_name:$DATA`) and things in the device namespace (where a path like `\\.\GLOBALROOT\??\UNC\localhost\C$\foo` is valid despite the `?`s in the path and is effectively equivalent to `C:\foo`). Truly validating paths is complicated and would need to be tailored to each path type. The illegal character checking being removed may open up users to more instances of hitting `OBJECT_NAME_INVALID => unreachable` when using `fs` APIs. + This is related to https://github.com/ziglang/zig/issues/15607 --- lib/std/child_process.zig | 9 +- lib/std/os/windows.zig | 307 +++++++++++++++++++++++++++-------- lib/std/os/windows/ntdll.zig | 10 ++ lib/std/os/windows/test.zig | 176 +++++++++++++++++++- 4 files changed, 430 insertions(+), 72 deletions(-) diff --git a/lib/std/child_process.zig b/lib/std/child_process.zig index daaa1689bc77..12cc06f39789 100644 --- a/lib/std/child_process.zig +++ b/lib/std/child_process.zig @@ -957,15 +957,12 @@ fn windowsCreateProcessPathExt( // NtQueryDirectoryFile calls. var dir = dir: { - if (fs.path.isAbsoluteWindowsWTF16(dir_buf.items[0..dir_path_len])) { - const prefixed_path = try windows.wToPrefixedFileW(dir_buf.items[0..dir_path_len]); - break :dir fs.cwd().openDirW(prefixed_path.span().ptr, .{}, true) catch return error.FileNotFound; - } // needs to be null-terminated try dir_buf.append(allocator, 0); - defer dir_buf.shrinkRetainingCapacity(dir_buf.items[0..dir_path_len].len); + defer dir_buf.shrinkRetainingCapacity(dir_path_len); const dir_path_z = dir_buf.items[0 .. dir_buf.items.len - 1 :0]; - break :dir std.fs.cwd().openDirW(dir_path_z.ptr, .{}, true) catch return error.FileNotFound; + const prefixed_path = try windows.wToPrefixedFileW(dir_path_z); + break :dir fs.cwd().openDirW(prefixed_path.span().ptr, .{}, true) catch return error.FileNotFound; }; defer dir.close(); diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index 6755e7adf28b..e559e4891584 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -1157,9 +1157,9 @@ pub fn GetFinalPathNameByHandle( // This surprising path is a filesystem path to the mount manager on Windows. // Source: https://stackoverflow.com/questions/3012828/using-ioctl-mountmgr-query-points - const mgmt_path = "\\MountPointManager"; - const mgmt_path_u16 = sliceToPrefixedFileW(mgmt_path) catch unreachable; - const mgmt_handle = OpenFile(mgmt_path_u16.span(), .{ + // This is the NT namespaced version of \\.\MountPointManager + const mgmt_path_u16 = std.unicode.utf8ToUtf16LeStringLiteral("\\??\\MountPointManager"); + const mgmt_handle = OpenFile(mgmt_path_u16, .{ .access_mask = SYNCHRONIZE, .share_access = FILE_SHARE_READ | FILE_SHARE_WRITE, .creation = FILE_OPEN, @@ -1997,43 +1997,248 @@ pub fn cStrToPrefixedFileW(s: [*:0]const u8) !PathSpace { return sliceToPrefixedFileW(mem.sliceTo(s, 0)); } -/// Converts the path `s` to WTF16, null-terminated. If the path is absolute, -/// it will get NT-style prefix `\??\` prepended automatically. -pub fn sliceToPrefixedFileW(s: []const u8) !PathSpace { - // TODO https://github.com/ziglang/zig/issues/2765 - var path_space: PathSpace = undefined; - const prefix = "\\??\\"; - const prefix_index: usize = if (mem.startsWith(u8, s, prefix)) prefix.len else 0; - for (s[prefix_index..]) |byte| { - switch (byte) { - '*', '?', '"', '<', '>', '|' => return error.BadPathName, - else => {}, - } - } - const prefix_u16 = [_]u16{ '\\', '?', '?', '\\' }; - const start_index = if (prefix_index > 0 or !std.fs.path.isAbsolute(s)) 0 else blk: { - path_space.data[0..prefix_u16.len].* = prefix_u16; - break :blk prefix_u16.len; - }; - path_space.len = start_index + try std.unicode.utf8ToUtf16Le(path_space.data[start_index..], s); - if (path_space.len > path_space.data.len) return error.NameTooLong; - path_space.len = start_index + (normalizePath(u16, path_space.data[start_index..path_space.len]) catch |err| switch (err) { - error.TooManyParentDirs => { - if (!std.fs.path.isAbsolute(s)) { - var temp_path: PathSpace = undefined; - temp_path.len = try std.unicode.utf8ToUtf16Le(&temp_path.data, s); - std.debug.assert(temp_path.len == path_space.len); - temp_path.data[path_space.len] = 0; - path_space.len = prefix_u16.len + try getFullPathNameW(&temp_path.data, path_space.data[prefix_u16.len..]); - path_space.data[0..prefix_u16.len].* = prefix_u16; - std.debug.assert(path_space.data[path_space.len] == 0); +/// Same as `wToPrefixedFileW` but accepts a UTF-8 encoded path. +pub fn sliceToPrefixedFileW(path: []const u8) !PathSpace { + var temp_path: PathSpace = undefined; + temp_path.len = try std.unicode.utf8ToUtf16Le(&temp_path.data, path); + temp_path.data[temp_path.len] = 0; + return wToPrefixedFileW(temp_path.span()); +} + +/// Converts the `path` to WTF16, null-terminated. If the path contains any +/// namespace prefix, or is anything but a relative path (rooted, drive relative, +/// etc) the result will have the NT-style prefix `\??\`. +/// +/// Similar to RtlDosPathNameToNtPathName_U with a few differences: +/// - Does not allocate on the heap. +/// - Relative paths are kept as relative unless they contain too many .. +/// components, in which case they are treated as drive-relative and resolved +/// against the CWD. +/// - Special case device names like COM1, NUL, etc are not handled specially (TODO) +/// - . and space are not stripped from the end of relative paths (potential TODO) +pub fn wToPrefixedFileW(path: [:0]const u16) !PathSpace { + const nt_prefix = [_]u16{ '\\', '?', '?', '\\' }; + switch (getNamespacePrefix(u16, path)) { + // TODO: Figure out a way to design an API that can avoid the copy for .nt, + // since it is always returned fully unmodified. + .nt, .verbatim => { + var path_space: PathSpace = undefined; + path_space.data[0..nt_prefix.len].* = nt_prefix; + const len_after_prefix = path.len - nt_prefix.len; + @memcpy(path_space.data[nt_prefix.len..][0..len_after_prefix], path[nt_prefix.len..]); + path_space.len = path.len; + path_space.data[path_space.len] = 0; + return path_space; + }, + .local_device, .fake_verbatim => { + var path_space: PathSpace = undefined; + const path_byte_len = ntdll.RtlGetFullPathName_U( + path.ptr, + path_space.data.len * 2, + &path_space.data, + null, + ); + if (path_byte_len == 0) { + // TODO: This may not be the right error + return error.BadPathName; + } else if (path_byte_len / 2 > path_space.data.len) { + return error.NameTooLong; + } + path_space.len = path_byte_len / 2; + // Both prefixes will be normalized but retained, so all + // we need to do now is replace them with the NT prefix + path_space.data[0..nt_prefix.len].* = nt_prefix; + return path_space; + }, + .none => { + const path_type = getUnprefixedPathType(u16, path); + var path_space: PathSpace = undefined; + relative: { + if (path_type == .relative) { + // TODO: Handle special case device names like COM1, AUX, NUL, CONIN$, CONOUT$, etc. + // See https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html + + // TODO: Potentially strip all trailing . and space characters from the + // end of the path. This is something that both RtlDosPathNameToNtPathName_U + // and RtlGetFullPathName_U do. Technically, trailing . and spaces + // are allowed, but such paths may not interact well with Windows (i.e. + // files with these paths can't be deleted from explorer.exe, etc). + // This could be something that normalizePath may want to do. + + @memcpy(path_space.data[0..path.len], path); + // Try to normalize, but if we get too many parent directories, + // then this is effectively a 'drive relative' path, so we need to + // start over and use RtlGetFullPathName_U instead. + path_space.len = normalizePath(u16, path_space.data[0..path.len]) catch |err| switch (err) { + error.TooManyParentDirs => break :relative, + }; + path_space.data[path_space.len] = 0; + return path_space; + } + } + // We now know we are going to return an absolute NT path, so + // we can unconditionally prefix it with the NT prefix. + path_space.data[0..nt_prefix.len].* = nt_prefix; + if (path_type == .root_local_device) { + // `\\.` and `\\?` always get converted to `\??\` exactly, so + // we can just stop here + path_space.len = nt_prefix.len; + path_space.data[path_space.len] = 0; return path_space; } - return error.BadPathName; + const path_buf_offset = switch (path_type) { + // UNC paths will always start with `\\`. However, we want to + // end up with something like `\??\UNC\server\share`, so to get + // RtlGetFullPathName to write into the spot we want the `server` + // part to end up, we need to provide an offset such that + // the `\\` part gets written where the `C\` of `UNC\` will be + // in the final NT path. + .unc_absolute => nt_prefix.len + 2, + else => nt_prefix.len, + }; + const buf_len = @intCast(u32, path_space.data.len - path_buf_offset); + const path_byte_len = ntdll.RtlGetFullPathName_U( + path.ptr, + buf_len * 2, + path_space.data[path_buf_offset..].ptr, + null, + ); + if (path_byte_len == 0) { + // TODO: This may not be the right error + return error.BadPathName; + } else if (path_byte_len / 2 > buf_len) { + return error.NameTooLong; + } + path_space.len = path_buf_offset + (path_byte_len / 2); + if (path_type == .unc_absolute) { + // Now add in the UNC, the `C` should overwrite the first `\` of the + // FullPathName, ultimately resulting in `\??\UNC\` + std.debug.assert(path_space.data[path_buf_offset] == '\\'); + std.debug.assert(path_space.data[path_buf_offset + 1] == '\\'); + const unc = [_]u16{ 'U', 'N', 'C' }; + path_space.data[nt_prefix.len..][0..unc.len].* = unc; + } + return path_space; }, - }); - path_space.data[path_space.len] = 0; - return path_space; + } +} + +pub const NamespacePrefix = enum { + none, + /// `\\.\` (path separators can be `\` or `/`) + local_device, + /// `\\?\` + /// When converted to an NT path, everything past the prefix is left + /// untouched and `\\?\` is replaced by `\??\`. + verbatim, + /// `\\?\` without all path separators being `\`. + /// This seems to be recognized as a prefix, but the 'verbatim' aspect + /// is not respected (i.e. if `//?/C:/foo` is converted to an NT path, + /// it will become `\??\C:\foo` [it will be canonicalized and the //?/ won't + /// be treated as part of the final path]) + fake_verbatim, + /// `\??\` + nt, +}; + +pub fn getNamespacePrefix(comptime T: type, path: []const T) NamespacePrefix { + if (path.len < 4) return .none; + var all_backslash = switch (path[0]) { + '\\' => true, + '/' => false, + else => return .none, + }; + all_backslash = all_backslash and switch (path[3]) { + '\\' => true, + '/' => false, + else => return .none, + }; + switch (path[1]) { + '?' => if (path[2] == '?' and all_backslash) return .nt else return .none, + '\\' => {}, + '/' => all_backslash = false, + else => return .none, + } + return switch (path[2]) { + '?' => if (all_backslash) .verbatim else .fake_verbatim, + '.' => .local_device, + else => .none, + }; +} + +test getNamespacePrefix { + try std.testing.expectEqual(NamespacePrefix.none, getNamespacePrefix(u8, "")); + try std.testing.expectEqual(NamespacePrefix.nt, getNamespacePrefix(u8, "\\??\\")); + try std.testing.expectEqual(NamespacePrefix.none, getNamespacePrefix(u8, "/??/")); + try std.testing.expectEqual(NamespacePrefix.none, getNamespacePrefix(u8, "/??\\")); + try std.testing.expectEqual(NamespacePrefix.none, getNamespacePrefix(u8, "\\?\\\\")); + try std.testing.expectEqual(NamespacePrefix.local_device, getNamespacePrefix(u8, "\\\\.\\")); + try std.testing.expectEqual(NamespacePrefix.local_device, getNamespacePrefix(u8, "\\\\./")); + try std.testing.expectEqual(NamespacePrefix.local_device, getNamespacePrefix(u8, "/\\./")); + try std.testing.expectEqual(NamespacePrefix.local_device, getNamespacePrefix(u8, "//./")); + try std.testing.expectEqual(NamespacePrefix.none, getNamespacePrefix(u8, "/.//")); + try std.testing.expectEqual(NamespacePrefix.verbatim, getNamespacePrefix(u8, "\\\\?\\")); + try std.testing.expectEqual(NamespacePrefix.fake_verbatim, getNamespacePrefix(u8, "\\/?\\")); + try std.testing.expectEqual(NamespacePrefix.fake_verbatim, getNamespacePrefix(u8, "\\/?/")); + try std.testing.expectEqual(NamespacePrefix.fake_verbatim, getNamespacePrefix(u8, "//?/")); +} + +pub const UnprefixedPathType = enum { + unc_absolute, + drive_absolute, + drive_relative, + rooted, + relative, + root_local_device, +}; + +inline fn isSepW(c: u16) bool { + return c == '/' or c == '\\'; +} + +/// Get the path type of a path that is known to not have any namespace prefixes +/// (`\\?\`, `\\.\`, `\??\`). +pub fn getUnprefixedPathType(comptime T: type, path: []const T) UnprefixedPathType { + if (path.len < 1) return .relative; + + if (std.debug.runtime_safety) { + std.debug.assert(getNamespacePrefix(T, path) == .none); + } + + if (isSepW(path[0])) { + // \x + if (path.len < 2 or !isSepW(path[1])) return .rooted; + // exactly \\. or \\? with nothing trailing + if (path.len == 3 and (path[2] == '.' or path[2] == '?')) return .root_local_device; + // \\x + return .unc_absolute; + } else { + // x + if (path.len < 2 or path[1] != ':') return .relative; + // x:\ + if (path.len > 2 and isSepW(path[2])) return .drive_absolute; + // x: + return .drive_relative; + } +} + +test getUnprefixedPathType { + try std.testing.expectEqual(UnprefixedPathType.relative, getUnprefixedPathType(u8, "")); + try std.testing.expectEqual(UnprefixedPathType.relative, getUnprefixedPathType(u8, "x")); + try std.testing.expectEqual(UnprefixedPathType.relative, getUnprefixedPathType(u8, "x\\")); + try std.testing.expectEqual(UnprefixedPathType.root_local_device, getUnprefixedPathType(u8, "//.")); + try std.testing.expectEqual(UnprefixedPathType.root_local_device, getUnprefixedPathType(u8, "/\\?")); + try std.testing.expectEqual(UnprefixedPathType.root_local_device, getUnprefixedPathType(u8, "\\\\?")); + try std.testing.expectEqual(UnprefixedPathType.unc_absolute, getUnprefixedPathType(u8, "\\\\x")); + try std.testing.expectEqual(UnprefixedPathType.unc_absolute, getUnprefixedPathType(u8, "//x")); + try std.testing.expectEqual(UnprefixedPathType.rooted, getUnprefixedPathType(u8, "\\x")); + try std.testing.expectEqual(UnprefixedPathType.rooted, getUnprefixedPathType(u8, "/")); + try std.testing.expectEqual(UnprefixedPathType.drive_relative, getUnprefixedPathType(u8, "x:")); + try std.testing.expectEqual(UnprefixedPathType.drive_relative, getUnprefixedPathType(u8, "x:abc")); + try std.testing.expectEqual(UnprefixedPathType.drive_relative, getUnprefixedPathType(u8, "x:a/b/c")); + try std.testing.expectEqual(UnprefixedPathType.drive_absolute, getUnprefixedPathType(u8, "x:\\")); + try std.testing.expectEqual(UnprefixedPathType.drive_absolute, getUnprefixedPathType(u8, "x:\\abc")); + try std.testing.expectEqual(UnprefixedPathType.drive_absolute, getUnprefixedPathType(u8, "x:/a/b/c")); } fn getFullPathNameW(path: [*:0]const u16, out: []u16) !usize { @@ -2046,34 +2251,6 @@ fn getFullPathNameW(path: [*:0]const u16, out: []u16) !usize { return result; } -/// Assumes an absolute path. -pub fn wToPrefixedFileW(s: []const u16) !PathSpace { - // TODO https://github.com/ziglang/zig/issues/2765 - var path_space: PathSpace = undefined; - - const start_index = if (mem.startsWith(u16, s, &[_]u16{ '\\', '?' })) 0 else blk: { - const prefix = [_]u16{ '\\', '?', '?', '\\' }; - path_space.data[0..prefix.len].* = prefix; - break :blk prefix.len; - }; - path_space.len = start_index + s.len; - if (path_space.len > path_space.data.len) return error.NameTooLong; - @memcpy(path_space.data[start_index..][0..s.len], s); - // > File I/O functions in the Windows API convert "/" to "\" as part of - // > converting the name to an NT-style name, except when using the "\\?\" - // > prefix as detailed in the following sections. - // from https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file#maximum-path-length-limitation - // Because we want the larger maximum path length for absolute paths, we - // convert forward slashes to backward slashes here. - for (path_space.data[0..path_space.len]) |*elem| { - if (elem.* == '/') { - elem.* = '\\'; - } - } - path_space.data[path_space.len] = 0; - return path_space; -} - inline fn MAKELANGID(p: c_ushort, s: c_ushort) LANGID { return (s << 10) | p; } diff --git a/lib/std/os/windows/ntdll.zig b/lib/std/os/windows/ntdll.zig index ad0e6c70ad89..328ecb80f562 100644 --- a/lib/std/os/windows/ntdll.zig +++ b/lib/std/os/windows/ntdll.zig @@ -158,6 +158,16 @@ pub extern "ntdll" fn RtlDosPathNameToNtPathName_U( ) callconv(WINAPI) BOOL; pub extern "ntdll" fn RtlFreeUnicodeString(UnicodeString: *UNICODE_STRING) callconv(WINAPI) void; +/// Returns the number of bytes written to `Buffer`. +/// If the returned count is larger than `BufferByteLength`, the buffer was too small. +/// If the returned count is zero, an error occurred. +pub extern "ntdll" fn RtlGetFullPathName_U( + FileName: [*:0]const u16, + BufferByteLength: ULONG, + Buffer: [*]u16, + ShortName: ?*[*:0]const u16, +) callconv(windows.WINAPI) windows.ULONG; + pub extern "ntdll" fn NtQueryDirectoryFile( FileHandle: HANDLE, Event: ?HANDLE, diff --git a/lib/std/os/windows/test.zig b/lib/std/os/windows/test.zig index fa340da17832..1ec605eaa3b1 100644 --- a/lib/std/os/windows/test.zig +++ b/lib/std/os/windows/test.zig @@ -3,7 +3,181 @@ const builtin = @import("builtin"); const windows = std.os.windows; const mem = std.mem; const testing = std.testing; -const expect = testing.expect; + +/// Wrapper around RtlDosPathNameToNtPathName_U for use in comparing +/// the behavior of RtlDosPathNameToNtPathName_U with wToPrefixedFileW +/// Note: RtlDosPathNameToNtPathName_U is not used in the Zig implementation +// because it allocates. +fn RtlDosPathNameToNtPathName_U(path: [:0]const u16) !windows.PathSpace { + var out: windows.UNICODE_STRING = undefined; + const rc = windows.ntdll.RtlDosPathNameToNtPathName_U(path, &out, null, null); + if (rc != windows.TRUE) return error.BadPathName; + defer windows.ntdll.RtlFreeUnicodeString(&out); + + var path_space: windows.PathSpace = undefined; + const out_path = out.Buffer[0 .. out.Length / 2]; + std.mem.copy(u16, path_space.data[0..], out_path); + path_space.len = out.Length / 2; + path_space.data[path_space.len] = 0; + + return path_space; +} + +/// Test that the Zig conversion matches the expected_path (for instances where +/// the Zig implementation intentionally diverges from what RtlDosPathNameToNtPathName_U does). +fn testToPrefixedFileNoOracle(comptime path: []const u8, comptime expected_path: []const u8) !void { + const path_utf16 = std.unicode.utf8ToUtf16LeStringLiteral(path); + const expected_path_utf16 = std.unicode.utf8ToUtf16LeStringLiteral(expected_path); + const actual_path = try windows.wToPrefixedFileW(path_utf16); + std.testing.expectEqualSlices(u16, expected_path_utf16, actual_path.span()) catch |e| { + std.debug.print("got '{s}', expected '{s}'\n", .{ std.unicode.fmtUtf16le(actual_path.span()), std.unicode.fmtUtf16le(expected_path_utf16) }); + return e; + }; +} + +/// Test that the Zig conversion matches the expected_path and that the +/// expected_path matches the conversion that RtlDosPathNameToNtPathName_U does. +fn testToPrefixedFileWithOracle(comptime path: []const u8, comptime expected_path: []const u8) !void { + try testToPrefixedFileNoOracle(path, expected_path); + try testToPrefixedFileOnlyOracle(path); +} + +/// Test that the Zig conversion matches the conversion that RtlDosPathNameToNtPathName_U does. +fn testToPrefixedFileOnlyOracle(comptime path: []const u8) !void { + const path_utf16 = std.unicode.utf8ToUtf16LeStringLiteral(path); + const zig_result = try windows.wToPrefixedFileW(path_utf16); + const win32_api_result = try RtlDosPathNameToNtPathName_U(path_utf16); + std.testing.expectEqualSlices(u16, win32_api_result.span(), zig_result.span()) catch |e| { + std.debug.print("got '{s}', expected '{s}'\n", .{ std.unicode.fmtUtf16le(zig_result.span()), std.unicode.fmtUtf16le(win32_api_result.span()) }); + return e; + }; +} + +test "toPrefixedFileW" { + if (builtin.os.tag != .windows) + return; + + // Most test cases come from https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html + // Note that these tests do not actually touch the filesystem or care about whether or not + // any of the paths actually exist or are otherwise valid. + + // Drive Absolute + try testToPrefixedFileWithOracle("X:\\ABC\\DEF", "\\??\\X:\\ABC\\DEF"); + try testToPrefixedFileWithOracle("X:\\", "\\??\\X:\\"); + try testToPrefixedFileWithOracle("X:\\ABC\\", "\\??\\X:\\ABC\\"); + // Trailing . and space characters are stripped + try testToPrefixedFileWithOracle("X:\\ABC\\DEF. .", "\\??\\X:\\ABC\\DEF"); + try testToPrefixedFileWithOracle("X:/ABC/DEF", "\\??\\X:\\ABC\\DEF"); + try testToPrefixedFileWithOracle("X:\\ABC\\..\\XYZ", "\\??\\X:\\XYZ"); + try testToPrefixedFileWithOracle("X:\\ABC\\..\\..\\..", "\\??\\X:\\"); + // Drive letter casing is unchanged + try testToPrefixedFileWithOracle("x:\\", "\\??\\x:\\"); + + // Drive Relative + // These tests depend on the CWD of the specified drive letter which can vary, + // so instead we just test that the Zig implementation matches the result of + // RtlDosPathNameToNtPathName_U. + // TODO: Setting the =X: environment variable didn't seem to affect + // RtlDosPathNameToNtPathName_U, not sure why that is but getting that + // to work could be an avenue to making these cases environment-independent. + // All -> are examples of the result if the X drive's cwd was X:\ABC + try testToPrefixedFileOnlyOracle("X:DEF\\GHI"); // -> \??\X:\ABC\DEF\GHI + try testToPrefixedFileOnlyOracle("X:"); // -> \??\X:\ABC + try testToPrefixedFileOnlyOracle("X:DEF. ."); // -> \??\X:\ABC\DEF + try testToPrefixedFileOnlyOracle("X:ABC\\..\\XYZ"); // -> \??\X:\ABC\XYZ + try testToPrefixedFileOnlyOracle("X:ABC\\..\\..\\.."); // -> \??\X:\ + try testToPrefixedFileOnlyOracle("x:"); // -> \??\X:\ABC + + // Rooted + // These tests depend on the drive letter of the CWD which can vary, so + // instead we just test that the Zig implementation matches the result of + // RtlDosPathNameToNtPathName_U. + // TODO: Getting the CWD path, getting the drive letter from it, and using it to + // construct the expected NT paths could be an avenue to making these cases + // environment-independent and therefore able to use testToPrefixedFileWithOracle. + // All -> are examples of the result if the CWD's drive letter was X + try testToPrefixedFileOnlyOracle("\\ABC\\DEF"); // -> \??\X:\ABC\DEF + try testToPrefixedFileOnlyOracle("\\"); // -> \??\X:\ + try testToPrefixedFileOnlyOracle("\\ABC\\DEF. ."); // -> \??\X:\ABC\DEF + try testToPrefixedFileOnlyOracle("/ABC/DEF"); // -> \??\X:\ABC\DEF + try testToPrefixedFileOnlyOracle("\\ABC\\..\\XYZ"); // -> \??\X:\XYZ + try testToPrefixedFileOnlyOracle("\\ABC\\..\\..\\.."); // -> \??\X:\ + + // Relative + // These cases differ in functionality to RtlDosPathNameToNtPathName_U. + // Relative paths remain relative if they don't have enough .. components + // to error with TooManyParentDirs + try testToPrefixedFileNoOracle("ABC\\DEF", "ABC\\DEF"); + // TODO: enable this if trailing . and spaces are stripped from relative paths + //try testToPrefixedFileNoOracle("ABC\\DEF. .", "ABC\\DEF"); + try testToPrefixedFileNoOracle("ABC/DEF", "ABC\\DEF"); + try testToPrefixedFileNoOracle("./ABC/.././DEF", "DEF"); + // TooManyParentDirs, so resolved relative to the CWD + // All -> are examples of the result if the CWD was X:\ABC\DEF + try testToPrefixedFileOnlyOracle("..\\GHI"); // -> \??\X:\ABC\GHI + try testToPrefixedFileOnlyOracle("GHI\\..\\..\\.."); // -> \??\X:\ + + // UNC Absolute + try testToPrefixedFileWithOracle("\\\\server\\share\\ABC\\DEF", "\\??\\UNC\\server\\share\\ABC\\DEF"); + try testToPrefixedFileWithOracle("\\\\server", "\\??\\UNC\\server"); + try testToPrefixedFileWithOracle("\\\\server\\share", "\\??\\UNC\\server\\share"); + try testToPrefixedFileWithOracle("\\\\server\\share\\ABC. .", "\\??\\UNC\\server\\share\\ABC"); + try testToPrefixedFileWithOracle("//server/share/ABC/DEF", "\\??\\UNC\\server\\share\\ABC\\DEF"); + try testToPrefixedFileWithOracle("\\\\server\\share\\ABC\\..\\XYZ", "\\??\\UNC\\server\\share\\XYZ"); + try testToPrefixedFileWithOracle("\\\\server\\share\\ABC\\..\\..\\..", "\\??\\UNC\\server\\share"); + + // Local Device + try testToPrefixedFileWithOracle("\\\\.\\COM20", "\\??\\COM20"); + try testToPrefixedFileWithOracle("\\\\.\\pipe\\mypipe", "\\??\\pipe\\mypipe"); + try testToPrefixedFileWithOracle("\\\\.\\X:\\ABC\\DEF. .", "\\??\\X:\\ABC\\DEF"); + try testToPrefixedFileWithOracle("\\\\.\\X:/ABC/DEF", "\\??\\X:\\ABC\\DEF"); + try testToPrefixedFileWithOracle("\\\\.\\X:\\ABC\\..\\XYZ", "\\??\\X:\\XYZ"); + // Can replace the first component of the path (contrary to drive absolute and UNC absolute paths) + try testToPrefixedFileWithOracle("\\\\.\\X:\\ABC\\..\\..\\C:\\", "\\??\\C:\\"); + try testToPrefixedFileWithOracle("\\\\.\\pipe\\mypipe\\..\\notmine", "\\??\\pipe\\notmine"); + + // Special-case device names + // TODO: Enable once these are supported + // more cases to test here: https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html + //try testToPrefixedFileWithOracle("COM1", "\\??\\COM1"); + // Sometimes the special-cased device names are not respected + try testToPrefixedFileWithOracle("\\\\.\\X:\\COM1", "\\??\\X:\\COM1"); + try testToPrefixedFileWithOracle("\\\\abc\\xyz\\COM1", "\\??\\UNC\\abc\\xyz\\COM1"); + + // Verbatim + // Left untouched except \\?\ is replaced by \??\ + try testToPrefixedFileWithOracle("\\\\?\\X:", "\\??\\X:"); + try testToPrefixedFileWithOracle("\\\\?\\X:\\COM1", "\\??\\X:\\COM1"); + try testToPrefixedFileWithOracle("\\\\?\\X:/ABC/DEF. .", "\\??\\X:/ABC/DEF. ."); + try testToPrefixedFileWithOracle("\\\\?\\X:\\ABC\\..\\..\\..", "\\??\\X:\\ABC\\..\\..\\.."); + // NT Namespace + // Fully unmodified + try testToPrefixedFileWithOracle("\\??\\X:", "\\??\\X:"); + try testToPrefixedFileWithOracle("\\??\\X:\\COM1", "\\??\\X:\\COM1"); + try testToPrefixedFileWithOracle("\\??\\X:/ABC/DEF. .", "\\??\\X:/ABC/DEF. ."); + try testToPrefixedFileWithOracle("\\??\\X:\\ABC\\..\\..\\..", "\\??\\X:\\ABC\\..\\..\\.."); + + // 'Fake' Verbatim + // If the prefix looks like the verbatim prefix but not all path separators in the + // prefix are backslashes, then it gets canonicalized and the prefix is dropped in favor + // of the NT prefix. + try testToPrefixedFileWithOracle("//?/C:/ABC", "\\??\\C:\\ABC"); + // 'Fake' NT + // If the prefix looks like the NT prefix but not all path separators in the prefix + // are backslashes, then it gets canonicalized and the /??/ is not dropped but + // rather treated as part of the path. In other words, the path is treated + // as a rooted path, so the final path is resolved relative to the CWD's + // drive letter. + // The -> shows an example of the result if the CWD's drive letter was X + try testToPrefixedFileOnlyOracle("/??/C:/ABC"); // -> \??\X:\??\C:\ABC + + // Root Local Device + // \\. and \\? always get converted to \??\ + try testToPrefixedFileWithOracle("\\\\.", "\\??\\"); + try testToPrefixedFileWithOracle("\\\\?", "\\??\\"); + try testToPrefixedFileWithOracle("//?", "\\??\\"); + try testToPrefixedFileWithOracle("//.", "\\??\\"); +} fn testRemoveDotDirs(str: []const u8, expected: []const u8) !void { const mutable = try testing.allocator.dupe(u8, str);