Skip to content

Commit 1697d44

Browse files
squeek502Vexu
authored andcommitted
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 #15607
1 parent ec58b47 commit 1697d44

File tree

4 files changed

+430
-72
lines changed

4 files changed

+430
-72
lines changed

lib/std/child_process.zig

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 242 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
2002-
pub 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.
2001+
pub 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)
2019+
pub 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+
2126+
pub 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+
2144+
pub 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+
2169+
test 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+
2186+
pub const UnprefixedPathType = enum {
2187+
unc_absolute,
2188+
drive_absolute,
2189+
drive_relative,
2190+
rooted,
2191+
relative,
2192+
root_local_device,
2193+
};
2194+
2195+
inline 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+
/// (`\\?\`, `\\.\`, `\??\`).
2201+
pub 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+
2225+
test 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

20392244
fn 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.
2050-
pub 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-
20772254
inline fn MAKELANGID(p: c_ushort, s: c_ushort) LANGID {
20782255
return (s << 10) | p;
20792256
}

lib/std/os/windows/ntdll.zig

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,16 @@ pub extern "ntdll" fn RtlDosPathNameToNtPathName_U(
158158
) callconv(WINAPI) BOOL;
159159
pub 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.
164+
pub 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+
161171
pub extern "ntdll" fn NtQueryDirectoryFile(
162172
FileHandle: HANDLE,
163173
Event: ?HANDLE,

0 commit comments

Comments
 (0)