mirror of
https://github.com/ziglang/zig.git
synced 2025-12-06 14:23:09 +00:00
Revert "Revert "Windows: Support UNC, rooted, drive relative, and namespaced/device paths""
This reverts commit b5fad3a40a86eb379903d6a803bdbe66dcaa5487. The latent x86_64 backend bug has been fixed.
This commit is contained in:
parent
0c438ab616
commit
28df1d09dc
@ -957,15 +957,12 @@ fn windowsCreateProcessPathExt(
|
|||||||
// NtQueryDirectoryFile calls.
|
// NtQueryDirectoryFile calls.
|
||||||
|
|
||||||
var dir = dir: {
|
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
|
// needs to be null-terminated
|
||||||
try dir_buf.append(allocator, 0);
|
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];
|
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();
|
defer dir.close();
|
||||||
|
|
||||||
|
|||||||
@ -1157,9 +1157,9 @@ pub fn GetFinalPathNameByHandle(
|
|||||||
|
|
||||||
// This surprising path is a filesystem path to the mount manager on Windows.
|
// This surprising path is a filesystem path to the mount manager on Windows.
|
||||||
// Source: https://stackoverflow.com/questions/3012828/using-ioctl-mountmgr-query-points
|
// Source: https://stackoverflow.com/questions/3012828/using-ioctl-mountmgr-query-points
|
||||||
const mgmt_path = "\\MountPointManager";
|
// This is the NT namespaced version of \\.\MountPointManager
|
||||||
const mgmt_path_u16 = sliceToPrefixedFileW(mgmt_path) catch unreachable;
|
const mgmt_path_u16 = std.unicode.utf8ToUtf16LeStringLiteral("\\??\\MountPointManager");
|
||||||
const mgmt_handle = OpenFile(mgmt_path_u16.span(), .{
|
const mgmt_handle = OpenFile(mgmt_path_u16, .{
|
||||||
.access_mask = SYNCHRONIZE,
|
.access_mask = SYNCHRONIZE,
|
||||||
.share_access = FILE_SHARE_READ | FILE_SHARE_WRITE,
|
.share_access = FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||||
.creation = FILE_OPEN,
|
.creation = FILE_OPEN,
|
||||||
@ -1997,43 +1997,248 @@ pub fn cStrToPrefixedFileW(s: [*:0]const u8) !PathSpace {
|
|||||||
return sliceToPrefixedFileW(mem.sliceTo(s, 0));
|
return sliceToPrefixedFileW(mem.sliceTo(s, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts the path `s` to WTF16, null-terminated. If the path is absolute,
|
/// Same as `wToPrefixedFileW` but accepts a UTF-8 encoded path.
|
||||||
/// it will get NT-style prefix `\??\` prepended automatically.
|
pub fn sliceToPrefixedFileW(path: []const u8) !PathSpace {
|
||||||
pub fn sliceToPrefixedFileW(s: []const u8) !PathSpace {
|
var temp_path: PathSpace = undefined;
|
||||||
// TODO https://github.com/ziglang/zig/issues/2765
|
temp_path.len = try std.unicode.utf8ToUtf16Le(&temp_path.data, path);
|
||||||
var path_space: PathSpace = undefined;
|
temp_path.data[temp_path.len] = 0;
|
||||||
const prefix = "\\??\\";
|
return wToPrefixedFileW(temp_path.span());
|
||||||
const prefix_index: usize = if (mem.startsWith(u8, s, prefix)) prefix.len else 0;
|
}
|
||||||
for (s[prefix_index..]) |byte| {
|
|
||||||
switch (byte) {
|
/// Converts the `path` to WTF16, null-terminated. If the path contains any
|
||||||
'*', '?', '"', '<', '>', '|' => return error.BadPathName,
|
/// namespace prefix, or is anything but a relative path (rooted, drive relative,
|
||||||
else => {},
|
/// etc) the result will have the NT-style prefix `\??\`.
|
||||||
}
|
///
|
||||||
}
|
/// Similar to RtlDosPathNameToNtPathName_U with a few differences:
|
||||||
const prefix_u16 = [_]u16{ '\\', '?', '?', '\\' };
|
/// - Does not allocate on the heap.
|
||||||
const start_index = if (prefix_index > 0 or !std.fs.path.isAbsolute(s)) 0 else blk: {
|
/// - Relative paths are kept as relative unless they contain too many ..
|
||||||
path_space.data[0..prefix_u16.len].* = prefix_u16;
|
/// components, in which case they are treated as drive-relative and resolved
|
||||||
break :blk prefix_u16.len;
|
/// against the CWD.
|
||||||
};
|
/// - Special case device names like COM1, NUL, etc are not handled specially (TODO)
|
||||||
path_space.len = start_index + try std.unicode.utf8ToUtf16Le(path_space.data[start_index..], s);
|
/// - . and space are not stripped from the end of relative paths (potential TODO)
|
||||||
if (path_space.len > path_space.data.len) return error.NameTooLong;
|
pub fn wToPrefixedFileW(path: [:0]const u16) !PathSpace {
|
||||||
path_space.len = start_index + (normalizePath(u16, path_space.data[start_index..path_space.len]) catch |err| switch (err) {
|
const nt_prefix = [_]u16{ '\\', '?', '?', '\\' };
|
||||||
error.TooManyParentDirs => {
|
switch (getNamespacePrefix(u16, path)) {
|
||||||
if (!std.fs.path.isAbsolute(s)) {
|
// TODO: Figure out a way to design an API that can avoid the copy for .nt,
|
||||||
var temp_path: PathSpace = undefined;
|
// since it is always returned fully unmodified.
|
||||||
temp_path.len = try std.unicode.utf8ToUtf16Le(&temp_path.data, s);
|
.nt, .verbatim => {
|
||||||
std.debug.assert(temp_path.len == path_space.len);
|
var path_space: PathSpace = undefined;
|
||||||
temp_path.data[path_space.len] = 0;
|
path_space.data[0..nt_prefix.len].* = nt_prefix;
|
||||||
path_space.len = prefix_u16.len + try getFullPathNameW(&temp_path.data, path_space.data[prefix_u16.len..]);
|
const len_after_prefix = path.len - nt_prefix.len;
|
||||||
path_space.data[0..prefix_u16.len].* = prefix_u16;
|
@memcpy(path_space.data[nt_prefix.len..][0..len_after_prefix], path[nt_prefix.len..]);
|
||||||
std.debug.assert(path_space.data[path_space.len] == 0);
|
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 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\<the rest of the path>`
|
||||||
|
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 {
|
fn getFullPathNameW(path: [*:0]const u16, out: []u16) !usize {
|
||||||
@ -2046,34 +2251,6 @@ fn getFullPathNameW(path: [*:0]const u16, out: []u16) !usize {
|
|||||||
return result;
|
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 {
|
inline fn MAKELANGID(p: c_ushort, s: c_ushort) LANGID {
|
||||||
return (s << 10) | p;
|
return (s << 10) | p;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -158,6 +158,16 @@ pub extern "ntdll" fn RtlDosPathNameToNtPathName_U(
|
|||||||
) callconv(WINAPI) BOOL;
|
) callconv(WINAPI) BOOL;
|
||||||
pub extern "ntdll" fn RtlFreeUnicodeString(UnicodeString: *UNICODE_STRING) callconv(WINAPI) void;
|
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(
|
pub extern "ntdll" fn NtQueryDirectoryFile(
|
||||||
FileHandle: HANDLE,
|
FileHandle: HANDLE,
|
||||||
Event: ?HANDLE,
|
Event: ?HANDLE,
|
||||||
|
|||||||
@ -3,7 +3,181 @@ const builtin = @import("builtin");
|
|||||||
const windows = std.os.windows;
|
const windows = std.os.windows;
|
||||||
const mem = std.mem;
|
const mem = std.mem;
|
||||||
const testing = std.testing;
|
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 {
|
fn testRemoveDotDirs(str: []const u8, expected: []const u8) !void {
|
||||||
const mutable = try testing.allocator.dupe(u8, str);
|
const mutable = try testing.allocator.dupe(u8, str);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user