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);