authorgravatar for squeek502@hotmail.comRyan Liptak <squeek502@hotmail.com> 2023-05-18 03:45:21-07:00
committergravatar for git@vexu.euVeikka Tuominen <git@vexu.eu> 2023-05-29 13:08:51+03:00
log1697d44809151e4759f6b5f9447a908c30ac1e84
treef0506031e27b3500ce8bc4750ed96a21436a20e4
parentec58b475b7ee913ff7ad0bf59bb2c71f0705e76e

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

4 files changed, 430 insertions(+), 72 deletions(-)

lib/std/child_process.zig+3-6
......@@ -957,15 +957,12 @@ fn windowsCreateProcessPathExt(
957957 // NtQueryDirectoryFile calls.
958958
959959 var dir = dir: {
960 if (fs.path.isAbsoluteWindowsWTF16(dir_buf.items[0..dir_path_len])) {
961 const prefixed_path = try windows.wToPrefixedFileW(dir_buf.items[0..dir_path_len]);
962 break :dir fs.cwd().openDirW(prefixed_path.span().ptr, .{}, true) catch return error.FileNotFound;
963 }
964960 // needs to be null-terminated
965961 try dir_buf.append(allocator, 0);
966 defer dir_buf.shrinkRetainingCapacity(dir_buf.items[0..dir_path_len].len);
962 defer dir_buf.shrinkRetainingCapacity(dir_path_len);
967963 const dir_path_z = dir_buf.items[0 .. dir_buf.items.len - 1 :0];
968 break :dir std.fs.cwd().openDirW(dir_path_z.ptr, .{}, true) catch return error.FileNotFound;
964 const prefixed_path = try windows.wToPrefixedFileW(dir_path_z);
965 break :dir fs.cwd().openDirW(prefixed_path.span().ptr, .{}, true) catch return error.FileNotFound;
969966 };
970967 defer dir.close();
971968
lib/std/os/windows.zig+242-65
......@@ -1157,9 +1157,9 @@ pub fn GetFinalPathNameByHandle(
11571157
11581158 // This surprising path is a filesystem path to the mount manager on Windows.
11591159 // Source: https://stackoverflow.com/questions/3012828/using-ioctl-mountmgr-query-points
1160 const mgmt_path = "\\MountPointManager";
1161 const mgmt_path_u16 = sliceToPrefixedFileW(mgmt_path) catch unreachable;
1162 const mgmt_handle = OpenFile(mgmt_path_u16.span(), .{
1160 // This is the NT namespaced version of \\.\MountPointManager
1161 const mgmt_path_u16 = std.unicode.utf8ToUtf16LeStringLiteral("\\??\\MountPointManager");
1162 const mgmt_handle = OpenFile(mgmt_path_u16, .{
11631163 .access_mask = SYNCHRONIZE,
11641164 .share_access = FILE_SHARE_READ | FILE_SHARE_WRITE,
11651165 .creation = FILE_OPEN,
......@@ -1997,43 +1997,248 @@ pub fn cStrToPrefixedFileW(s: [*:0]const u8) !PathSpace {
19971997 return sliceToPrefixedFileW(mem.sliceTo(s, 0));
19981998}
19991999
2000/// Converts the path `s` to WTF16, null-terminated. If the path is absolute,
2001/// it will get NT-style prefix `\??\` prepended automatically.
2002pub fn sliceToPrefixedFileW(s: []const u8) !PathSpace {
2003 // TODO https://github.com/ziglang/zig/issues/2765
2004 var path_space: PathSpace = undefined;
2005 const prefix = "\\??\\";
2006 const prefix_index: usize = if (mem.startsWith(u8, s, prefix)) prefix.len else 0;
2007 for (s[prefix_index..]) |byte| {
2008 switch (byte) {
2009 '*', '?', '"', '<', '>', '|' => return error.BadPathName,
2010 else => {},
2011 }
2012 }
2013 const prefix_u16 = [_]u16{ '\\', '?', '?', '\\' };
2014 const start_index = if (prefix_index > 0 or !std.fs.path.isAbsolute(s)) 0 else blk: {
2015 path_space.data[0..prefix_u16.len].* = prefix_u16;
2016 break :blk prefix_u16.len;
2017 };
2018 path_space.len = start_index + try std.unicode.utf8ToUtf16Le(path_space.data[start_index..], s);
2019 if (path_space.len > path_space.data.len) return error.NameTooLong;
2020 path_space.len = start_index + (normalizePath(u16, path_space.data[start_index..path_space.len]) catch |err| switch (err) {
2021 error.TooManyParentDirs => {
2022 if (!std.fs.path.isAbsolute(s)) {
2023 var temp_path: PathSpace = undefined;
2024 temp_path.len = try std.unicode.utf8ToUtf16Le(&temp_path.data, s);
2025 std.debug.assert(temp_path.len == path_space.len);
2026 temp_path.data[path_space.len] = 0;
2027 path_space.len = prefix_u16.len + try getFullPathNameW(&temp_path.data, path_space.data[prefix_u16.len..]);
2028 path_space.data[0..prefix_u16.len].* = prefix_u16;
2029 std.debug.assert(path_space.data[path_space.len] == 0);
2000/// Same as `wToPrefixedFileW` but accepts a UTF-8 encoded path.
2001pub fn sliceToPrefixedFileW(path: []const u8) !PathSpace {
2002 var temp_path: PathSpace = undefined;
2003 temp_path.len = try std.unicode.utf8ToUtf16Le(&temp_path.data, path);
2004 temp_path.data[temp_path.len] = 0;
2005 return wToPrefixedFileW(temp_path.span());
2006}
2007
2008/// Converts the `path` to WTF16, null-terminated. If the path contains any
2009/// namespace prefix, or is anything but a relative path (rooted, drive relative,
2010/// etc) the result will have the NT-style prefix `\??\`.
2011///
2012/// Similar to RtlDosPathNameToNtPathName_U with a few differences:
2013/// - Does not allocate on the heap.
2014/// - Relative paths are kept as relative unless they contain too many ..
2015/// components, in which case they are treated as drive-relative and resolved
2016/// against the CWD.
2017/// - Special case device names like COM1, NUL, etc are not handled specially (TODO)
2018/// - . and space are not stripped from the end of relative paths (potential TODO)
2019pub fn wToPrefixedFileW(path: [:0]const u16) !PathSpace {
2020 const nt_prefix = [_]u16{ '\\', '?', '?', '\\' };
2021 switch (getNamespacePrefix(u16, path)) {
2022 // TODO: Figure out a way to design an API that can avoid the copy for .nt,
2023 // since it is always returned fully unmodified.
2024 .nt, .verbatim => {
2025 var path_space: PathSpace = undefined;
2026 path_space.data[0..nt_prefix.len].* = nt_prefix;
2027 const len_after_prefix = path.len - nt_prefix.len;
2028 @memcpy(path_space.data[nt_prefix.len..][0..len_after_prefix], path[nt_prefix.len..]);
2029 path_space.len = path.len;
2030 path_space.data[path_space.len] = 0;
2031 return path_space;
2032 },
2033 .local_device, .fake_verbatim => {
2034 var path_space: PathSpace = undefined;
2035 const path_byte_len = ntdll.RtlGetFullPathName_U(
2036 path.ptr,
2037 path_space.data.len * 2,
2038 &path_space.data,
2039 null,
2040 );
2041 if (path_byte_len == 0) {
2042 // TODO: This may not be the right error
2043 return error.BadPathName;
2044 } else if (path_byte_len / 2 > path_space.data.len) {
2045 return error.NameTooLong;
2046 }
2047 path_space.len = path_byte_len / 2;
2048 // Both prefixes will be normalized but retained, so all
2049 // we need to do now is replace them with the NT prefix
2050 path_space.data[0..nt_prefix.len].* = nt_prefix;
2051 return path_space;
2052 },
2053 .none => {
2054 const path_type = getUnprefixedPathType(u16, path);
2055 var path_space: PathSpace = undefined;
2056 relative: {
2057 if (path_type == .relative) {
2058 // TODO: Handle special case device names like COM1, AUX, NUL, CONIN$, CONOUT$, etc.
2059 // See https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
2060
2061 // TODO: Potentially strip all trailing . and space characters from the
2062 // end of the path. This is something that both RtlDosPathNameToNtPathName_U
2063 // and RtlGetFullPathName_U do. Technically, trailing . and spaces
2064 // are allowed, but such paths may not interact well with Windows (i.e.
2065 // files with these paths can't be deleted from explorer.exe, etc).
2066 // This could be something that normalizePath may want to do.
2067
2068 @memcpy(path_space.data[0..path.len], path);
2069 // Try to normalize, but if we get too many parent directories,
2070 // then this is effectively a 'drive relative' path, so we need to
2071 // start over and use RtlGetFullPathName_U instead.
2072 path_space.len = normalizePath(u16, path_space.data[0..path.len]) catch |err| switch (err) {
2073 error.TooManyParentDirs => break :relative,
2074 };
2075 path_space.data[path_space.len] = 0;
2076 return path_space;
2077 }
2078 }
2079 // We now know we are going to return an absolute NT path, so
2080 // we can unconditionally prefix it with the NT prefix.
2081 path_space.data[0..nt_prefix.len].* = nt_prefix;
2082 if (path_type == .root_local_device) {
2083 // `\\.` and `\\?` always get converted to `\??\` exactly, so
2084 // we can just stop here
2085 path_space.len = nt_prefix.len;
2086 path_space.data[path_space.len] = 0;
20302087 return path_space;
20312088 }
2032 return error.BadPathName;
2089 const path_buf_offset = switch (path_type) {
2090 // UNC paths will always start with `\\`. However, we want to
2091 // end up with something like `\??\UNC\server\share`, so to get
2092 // RtlGetFullPathName to write into the spot we want the `server`
2093 // part to end up, we need to provide an offset such that
2094 // the `\\` part gets written where the `C\` of `UNC\` will be
2095 // in the final NT path.
2096 .unc_absolute => nt_prefix.len + 2,
2097 else => nt_prefix.len,
2098 };
2099 const buf_len = @intCast(u32, path_space.data.len - path_buf_offset);
2100 const path_byte_len = ntdll.RtlGetFullPathName_U(
2101 path.ptr,
2102 buf_len * 2,
2103 path_space.data[path_buf_offset..].ptr,
2104 null,
2105 );
2106 if (path_byte_len == 0) {
2107 // TODO: This may not be the right error
2108 return error.BadPathName;
2109 } else if (path_byte_len / 2 > buf_len) {
2110 return error.NameTooLong;
2111 }
2112 path_space.len = path_buf_offset + (path_byte_len / 2);
2113 if (path_type == .unc_absolute) {
2114 // Now add in the UNC, the `C` should overwrite the first `\` of the
2115 // FullPathName, ultimately resulting in `\??\UNC\<the rest of the path>`
2116 std.debug.assert(path_space.data[path_buf_offset] == '\\');
2117 std.debug.assert(path_space.data[path_buf_offset + 1] == '\\');
2118 const unc = [_]u16{ 'U', 'N', 'C' };
2119 path_space.data[nt_prefix.len..][0..unc.len].* = unc;
2120 }
2121 return path_space;
20332122 },
2034 });
2035 path_space.data[path_space.len] = 0;
2036 return path_space;
2123 }
2124}
2125
2126pub const NamespacePrefix = enum {
2127 none,
2128 /// `\\.\` (path separators can be `\` or `/`)
2129 local_device,
2130 /// `\\?\`
2131 /// When converted to an NT path, everything past the prefix is left
2132 /// untouched and `\\?\` is replaced by `\??\`.
2133 verbatim,
2134 /// `\\?\` without all path separators being `\`.
2135 /// This seems to be recognized as a prefix, but the 'verbatim' aspect
2136 /// is not respected (i.e. if `//?/C:/foo` is converted to an NT path,
2137 /// it will become `\??\C:\foo` [it will be canonicalized and the //?/ won't
2138 /// be treated as part of the final path])
2139 fake_verbatim,
2140 /// `\??\`
2141 nt,
2142};
2143
2144pub fn getNamespacePrefix(comptime T: type, path: []const T) NamespacePrefix {
2145 if (path.len < 4) return .none;
2146 var all_backslash = switch (path[0]) {
2147 '\\' => true,
2148 '/' => false,
2149 else => return .none,
2150 };
2151 all_backslash = all_backslash and switch (path[3]) {
2152 '\\' => true,
2153 '/' => false,
2154 else => return .none,
2155 };
2156 switch (path[1]) {
2157 '?' => if (path[2] == '?' and all_backslash) return .nt else return .none,
2158 '\\' => {},
2159 '/' => all_backslash = false,
2160 else => return .none,
2161 }
2162 return switch (path[2]) {
2163 '?' => if (all_backslash) .verbatim else .fake_verbatim,
2164 '.' => .local_device,
2165 else => .none,
2166 };
2167}
2168
2169test getNamespacePrefix {
2170 try std.testing.expectEqual(NamespacePrefix.none, getNamespacePrefix(u8, ""));
2171 try std.testing.expectEqual(NamespacePrefix.nt, getNamespacePrefix(u8, "\\??\\"));
2172 try std.testing.expectEqual(NamespacePrefix.none, getNamespacePrefix(u8, "/??/"));
2173 try std.testing.expectEqual(NamespacePrefix.none, getNamespacePrefix(u8, "/??\\"));
2174 try std.testing.expectEqual(NamespacePrefix.none, getNamespacePrefix(u8, "\\?\\\\"));
2175 try std.testing.expectEqual(NamespacePrefix.local_device, getNamespacePrefix(u8, "\\\\.\\"));
2176 try std.testing.expectEqual(NamespacePrefix.local_device, getNamespacePrefix(u8, "\\\\./"));
2177 try std.testing.expectEqual(NamespacePrefix.local_device, getNamespacePrefix(u8, "/\\./"));
2178 try std.testing.expectEqual(NamespacePrefix.local_device, getNamespacePrefix(u8, "//./"));
2179 try std.testing.expectEqual(NamespacePrefix.none, getNamespacePrefix(u8, "/.//"));
2180 try std.testing.expectEqual(NamespacePrefix.verbatim, getNamespacePrefix(u8, "\\\\?\\"));
2181 try std.testing.expectEqual(NamespacePrefix.fake_verbatim, getNamespacePrefix(u8, "\\/?\\"));
2182 try std.testing.expectEqual(NamespacePrefix.fake_verbatim, getNamespacePrefix(u8, "\\/?/"));
2183 try std.testing.expectEqual(NamespacePrefix.fake_verbatim, getNamespacePrefix(u8, "//?/"));
2184}
2185
2186pub const UnprefixedPathType = enum {
2187 unc_absolute,
2188 drive_absolute,
2189 drive_relative,
2190 rooted,
2191 relative,
2192 root_local_device,
2193};
2194
2195inline fn isSepW(c: u16) bool {
2196 return c == '/' or c == '\\';
2197}
2198
2199/// Get the path type of a path that is known to not have any namespace prefixes
2200/// (`\\?\`, `\\.\`, `\??\`).
2201pub fn getUnprefixedPathType(comptime T: type, path: []const T) UnprefixedPathType {
2202 if (path.len < 1) return .relative;
2203
2204 if (std.debug.runtime_safety) {
2205 std.debug.assert(getNamespacePrefix(T, path) == .none);
2206 }
2207
2208 if (isSepW(path[0])) {
2209 // \x
2210 if (path.len < 2 or !isSepW(path[1])) return .rooted;
2211 // exactly \\. or \\? with nothing trailing
2212 if (path.len == 3 and (path[2] == '.' or path[2] == '?')) return .root_local_device;
2213 // \\x
2214 return .unc_absolute;
2215 } else {
2216 // x
2217 if (path.len < 2 or path[1] != ':') return .relative;
2218 // x:\
2219 if (path.len > 2 and isSepW(path[2])) return .drive_absolute;
2220 // x:
2221 return .drive_relative;
2222 }
2223}
2224
2225test getUnprefixedPathType {
2226 try std.testing.expectEqual(UnprefixedPathType.relative, getUnprefixedPathType(u8, ""));
2227 try std.testing.expectEqual(UnprefixedPathType.relative, getUnprefixedPathType(u8, "x"));
2228 try std.testing.expectEqual(UnprefixedPathType.relative, getUnprefixedPathType(u8, "x\\"));
2229 try std.testing.expectEqual(UnprefixedPathType.root_local_device, getUnprefixedPathType(u8, "//."));
2230 try std.testing.expectEqual(UnprefixedPathType.root_local_device, getUnprefixedPathType(u8, "/\\?"));
2231 try std.testing.expectEqual(UnprefixedPathType.root_local_device, getUnprefixedPathType(u8, "\\\\?"));
2232 try std.testing.expectEqual(UnprefixedPathType.unc_absolute, getUnprefixedPathType(u8, "\\\\x"));
2233 try std.testing.expectEqual(UnprefixedPathType.unc_absolute, getUnprefixedPathType(u8, "//x"));
2234 try std.testing.expectEqual(UnprefixedPathType.rooted, getUnprefixedPathType(u8, "\\x"));
2235 try std.testing.expectEqual(UnprefixedPathType.rooted, getUnprefixedPathType(u8, "/"));
2236 try std.testing.expectEqual(UnprefixedPathType.drive_relative, getUnprefixedPathType(u8, "x:"));
2237 try std.testing.expectEqual(UnprefixedPathType.drive_relative, getUnprefixedPathType(u8, "x:abc"));
2238 try std.testing.expectEqual(UnprefixedPathType.drive_relative, getUnprefixedPathType(u8, "x:a/b/c"));
2239 try std.testing.expectEqual(UnprefixedPathType.drive_absolute, getUnprefixedPathType(u8, "x:\\"));
2240 try std.testing.expectEqual(UnprefixedPathType.drive_absolute, getUnprefixedPathType(u8, "x:\\abc"));
2241 try std.testing.expectEqual(UnprefixedPathType.drive_absolute, getUnprefixedPathType(u8, "x:/a/b/c"));
20372242}
20382243
20392244fn getFullPathNameW(path: [*:0]const u16, out: []u16) !usize {
......@@ -2046,34 +2251,6 @@ fn getFullPathNameW(path: [*:0]const u16, out: []u16) !usize {
20462251 return result;
20472252}
20482253
2049/// Assumes an absolute path.
2050pub fn wToPrefixedFileW(s: []const u16) !PathSpace {
2051 // TODO https://github.com/ziglang/zig/issues/2765
2052 var path_space: PathSpace = undefined;
2053
2054 const start_index = if (mem.startsWith(u16, s, &[_]u16{ '\\', '?' })) 0 else blk: {
2055 const prefix = [_]u16{ '\\', '?', '?', '\\' };
2056 path_space.data[0..prefix.len].* = prefix;
2057 break :blk prefix.len;
2058 };
2059 path_space.len = start_index + s.len;
2060 if (path_space.len > path_space.data.len) return error.NameTooLong;
2061 @memcpy(path_space.data[start_index..][0..s.len], s);
2062 // > File I/O functions in the Windows API convert "/" to "\" as part of
2063 // > converting the name to an NT-style name, except when using the "\\?\"
2064 // > prefix as detailed in the following sections.
2065 // from https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file#maximum-path-length-limitation
2066 // Because we want the larger maximum path length for absolute paths, we
2067 // convert forward slashes to backward slashes here.
2068 for (path_space.data[0..path_space.len]) |*elem| {
2069 if (elem.* == '/') {
2070 elem.* = '\\';
2071 }
2072 }
2073 path_space.data[path_space.len] = 0;
2074 return path_space;
2075}
2076
20772254inline fn MAKELANGID(p: c_ushort, s: c_ushort) LANGID {
20782255 return (s << 10) | p;
20792256}
lib/std/os/windows/ntdll.zig+10
......@@ -158,6 +158,16 @@ pub extern "ntdll" fn RtlDosPathNameToNtPathName_U(
158158) callconv(WINAPI) BOOL;
159159pub extern "ntdll" fn RtlFreeUnicodeString(UnicodeString: *UNICODE_STRING) callconv(WINAPI) void;
160160
161/// Returns the number of bytes written to `Buffer`.
162/// If the returned count is larger than `BufferByteLength`, the buffer was too small.
163/// If the returned count is zero, an error occurred.
164pub extern "ntdll" fn RtlGetFullPathName_U(
165 FileName: [*:0]const u16,
166 BufferByteLength: ULONG,
167 Buffer: [*]u16,
168 ShortName: ?*[*:0]const u16,
169) callconv(windows.WINAPI) windows.ULONG;
170
161171pub extern "ntdll" fn NtQueryDirectoryFile(
162172 FileHandle: HANDLE,
163173 Event: ?HANDLE,
lib/std/os/windows/test.zig+175-1
......@@ -3,7 +3,181 @@ const builtin = @import("builtin");
33const windows = std.os.windows;
44const mem = std.mem;
55const testing = std.testing;
6const expect = testing.expect;
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 std.mem.copy(u16, path_space.data[0..], 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(path_utf16);
32 std.testing.expectEqualSlices(u16, expected_path_utf16, actual_path.span()) catch |e| {
33 std.debug.print("got '{s}', expected '{s}'\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(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 '{s}', expected '{s}'\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)
58 return;
59
60 // Most test cases come from https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
61 // Note that these tests do not actually touch the filesystem or care about whether or not
62 // any of the paths actually exist or are otherwise valid.
63
64 // Drive Absolute
65 try testToPrefixedFileWithOracle("X:\\ABC\\DEF", "\\??\\X:\\ABC\\DEF");
66 try testToPrefixedFileWithOracle("X:\\", "\\??\\X:\\");
67 try testToPrefixedFileWithOracle("X:\\ABC\\", "\\??\\X:\\ABC\\");
68 // Trailing . and space characters are stripped
69 try testToPrefixedFileWithOracle("X:\\ABC\\DEF. .", "\\??\\X:\\ABC\\DEF");
70 try testToPrefixedFileWithOracle("X:/ABC/DEF", "\\??\\X:\\ABC\\DEF");
71 try testToPrefixedFileWithOracle("X:\\ABC\\..\\XYZ", "\\??\\X:\\XYZ");
72 try testToPrefixedFileWithOracle("X:\\ABC\\..\\..\\..", "\\??\\X:\\");
73 // Drive letter casing is unchanged
74 try testToPrefixedFileWithOracle("x:\\", "\\??\\x:\\");
75
76 // Drive Relative
77 // These tests depend on the CWD of the specified drive letter which can vary,
78 // so instead we just test that the Zig implementation matches the result of
79 // RtlDosPathNameToNtPathName_U.
80 // TODO: Setting the =X: environment variable didn't seem to affect
81 // RtlDosPathNameToNtPathName_U, not sure why that is but getting that
82 // to work could be an avenue to making these cases environment-independent.
83 // All -> are examples of the result if the X drive's cwd was X:\ABC
84 try testToPrefixedFileOnlyOracle("X:DEF\\GHI"); // -> \??\X:\ABC\DEF\GHI
85 try testToPrefixedFileOnlyOracle("X:"); // -> \??\X:\ABC
86 try testToPrefixedFileOnlyOracle("X:DEF. ."); // -> \??\X:\ABC\DEF
87 try testToPrefixedFileOnlyOracle("X:ABC\\..\\XYZ"); // -> \??\X:\ABC\XYZ
88 try testToPrefixedFileOnlyOracle("X:ABC\\..\\..\\.."); // -> \??\X:\
89 try testToPrefixedFileOnlyOracle("x:"); // -> \??\X:\ABC
90
91 // Rooted
92 // These tests depend on the drive letter of the CWD which can vary, so
93 // instead we just test that the Zig implementation matches the result of
94 // RtlDosPathNameToNtPathName_U.
95 // TODO: Getting the CWD path, getting the drive letter from it, and using it to
96 // construct the expected NT paths could be an avenue to making these cases
97 // environment-independent and therefore able to use testToPrefixedFileWithOracle.
98 // All -> are examples of the result if the CWD's drive letter was X
99 try testToPrefixedFileOnlyOracle("\\ABC\\DEF"); // -> \??\X:\ABC\DEF
100 try testToPrefixedFileOnlyOracle("\\"); // -> \??\X:\
101 try testToPrefixedFileOnlyOracle("\\ABC\\DEF. ."); // -> \??\X:\ABC\DEF
102 try testToPrefixedFileOnlyOracle("/ABC/DEF"); // -> \??\X:\ABC\DEF
103 try testToPrefixedFileOnlyOracle("\\ABC\\..\\XYZ"); // -> \??\X:\XYZ
104 try testToPrefixedFileOnlyOracle("\\ABC\\..\\..\\.."); // -> \??\X:\
105
106 // Relative
107 // These cases differ in functionality to RtlDosPathNameToNtPathName_U.
108 // Relative paths remain relative if they don't have enough .. components
109 // to error with TooManyParentDirs
110 try testToPrefixedFileNoOracle("ABC\\DEF", "ABC\\DEF");
111 // TODO: enable this if trailing . and spaces are stripped from relative paths
112 //try testToPrefixedFileNoOracle("ABC\\DEF. .", "ABC\\DEF");
113 try testToPrefixedFileNoOracle("ABC/DEF", "ABC\\DEF");
114 try testToPrefixedFileNoOracle("./ABC/.././DEF", "DEF");
115 // TooManyParentDirs, so resolved relative to the CWD
116 // All -> are examples of the result if the CWD was X:\ABC\DEF
117 try testToPrefixedFileOnlyOracle("..\\GHI"); // -> \??\X:\ABC\GHI
118 try testToPrefixedFileOnlyOracle("GHI\\..\\..\\.."); // -> \??\X:\
119
120 // UNC Absolute
121 try testToPrefixedFileWithOracle("\\\\server\\share\\ABC\\DEF", "\\??\\UNC\\server\\share\\ABC\\DEF");
122 try testToPrefixedFileWithOracle("\\\\server", "\\??\\UNC\\server");
123 try testToPrefixedFileWithOracle("\\\\server\\share", "\\??\\UNC\\server\\share");
124 try testToPrefixedFileWithOracle("\\\\server\\share\\ABC. .", "\\??\\UNC\\server\\share\\ABC");
125 try testToPrefixedFileWithOracle("//server/share/ABC/DEF", "\\??\\UNC\\server\\share\\ABC\\DEF");
126 try testToPrefixedFileWithOracle("\\\\server\\share\\ABC\\..\\XYZ", "\\??\\UNC\\server\\share\\XYZ");
127 try testToPrefixedFileWithOracle("\\\\server\\share\\ABC\\..\\..\\..", "\\??\\UNC\\server\\share");
128
129 // Local Device
130 try testToPrefixedFileWithOracle("\\\\.\\COM20", "\\??\\COM20");
131 try testToPrefixedFileWithOracle("\\\\.\\pipe\\mypipe", "\\??\\pipe\\mypipe");
132 try testToPrefixedFileWithOracle("\\\\.\\X:\\ABC\\DEF. .", "\\??\\X:\\ABC\\DEF");
133 try testToPrefixedFileWithOracle("\\\\.\\X:/ABC/DEF", "\\??\\X:\\ABC\\DEF");
134 try testToPrefixedFileWithOracle("\\\\.\\X:\\ABC\\..\\XYZ", "\\??\\X:\\XYZ");
135 // Can replace the first component of the path (contrary to drive absolute and UNC absolute paths)
136 try testToPrefixedFileWithOracle("\\\\.\\X:\\ABC\\..\\..\\C:\\", "\\??\\C:\\");
137 try testToPrefixedFileWithOracle("\\\\.\\pipe\\mypipe\\..\\notmine", "\\??\\pipe\\notmine");
138
139 // Special-case device names
140 // TODO: Enable once these are supported
141 // more cases to test here: https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
142 //try testToPrefixedFileWithOracle("COM1", "\\??\\COM1");
143 // Sometimes the special-cased device names are not respected
144 try testToPrefixedFileWithOracle("\\\\.\\X:\\COM1", "\\??\\X:\\COM1");
145 try testToPrefixedFileWithOracle("\\\\abc\\xyz\\COM1", "\\??\\UNC\\abc\\xyz\\COM1");
146
147 // Verbatim
148 // Left untouched except \\?\ is replaced by \??\
149 try testToPrefixedFileWithOracle("\\\\?\\X:", "\\??\\X:");
150 try testToPrefixedFileWithOracle("\\\\?\\X:\\COM1", "\\??\\X:\\COM1");
151 try testToPrefixedFileWithOracle("\\\\?\\X:/ABC/DEF. .", "\\??\\X:/ABC/DEF. .");
152 try testToPrefixedFileWithOracle("\\\\?\\X:\\ABC\\..\\..\\..", "\\??\\X:\\ABC\\..\\..\\..");
153 // NT Namespace
154 // Fully unmodified
155 try testToPrefixedFileWithOracle("\\??\\X:", "\\??\\X:");
156 try testToPrefixedFileWithOracle("\\??\\X:\\COM1", "\\??\\X:\\COM1");
157 try testToPrefixedFileWithOracle("\\??\\X:/ABC/DEF. .", "\\??\\X:/ABC/DEF. .");
158 try testToPrefixedFileWithOracle("\\??\\X:\\ABC\\..\\..\\..", "\\??\\X:\\ABC\\..\\..\\..");
159
160 // 'Fake' Verbatim
161 // If the prefix looks like the verbatim prefix but not all path separators in the
162 // prefix are backslashes, then it gets canonicalized and the prefix is dropped in favor
163 // of the NT prefix.
164 try testToPrefixedFileWithOracle("//?/C:/ABC", "\\??\\C:\\ABC");
165 // 'Fake' NT
166 // If the prefix looks like the NT prefix but not all path separators in the prefix
167 // are backslashes, then it gets canonicalized and the /??/ is not dropped but
168 // rather treated as part of the path. In other words, the path is treated
169 // as a rooted path, so the final path is resolved relative to the CWD's
170 // drive letter.
171 // The -> shows an example of the result if the CWD's drive letter was X
172 try testToPrefixedFileOnlyOracle("/??/C:/ABC"); // -> \??\X:\??\C:\ABC
173
174 // Root Local Device
175 // \\. and \\? always get converted to \??\
176 try testToPrefixedFileWithOracle("\\\\.", "\\??\\");
177 try testToPrefixedFileWithOracle("\\\\?", "\\??\\");
178 try testToPrefixedFileWithOracle("//?", "\\??\\");
179 try testToPrefixedFileWithOracle("//.", "\\??\\");
180}
7181
8182fn testRemoveDotDirs(str: []const u8, expected: []const u8) !void {
9183 const mutable = try testing.allocator.dupe(u8, str);