Skip to content

Implement std.os.windows.GetPathNameByHandle using NT routines only #5993

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Aug 11, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 2 additions & 8 deletions lib/std/os.zig
Original file line number Diff line number Diff line change
Expand Up @@ -4060,7 +4060,6 @@ pub fn realpathZ(pathname: [*:0]const u8, out_buffer: *[MAX_PATH_BYTES]u8) RealP
}

/// Same as `realpath` except `pathname` is UTF16LE-encoded.
/// TODO use ntdll to emulate `GetFinalPathNameByHandleW` routine
pub fn realpathW(pathname: []const u16, out_buffer: *[MAX_PATH_BYTES]u8) RealPathError![]u8 {
const w = windows;

Expand Down Expand Up @@ -4095,15 +4094,10 @@ pub fn realpathW(pathname: []const u16, out_buffer: *[MAX_PATH_BYTES]u8) RealPat
defer w.CloseHandle(h_file);

var wide_buf: [w.PATH_MAX_WIDE]u16 = undefined;
const wide_slice = try w.GetFinalPathNameByHandleW(h_file, &wide_buf, wide_buf.len, w.VOLUME_NAME_DOS);

// Windows returns \\?\ prepended to the path.
// We strip it to make this function consistent across platforms.
const prefix = [_]u16{ '\\', '\\', '?', '\\' };
const start_index = if (mem.startsWith(u16, wide_slice, &prefix)) prefix.len else 0;
const wide_slice = try w.GetFinalPathNameByHandle(h_file, .{}, wide_buf[0..]);

// Trust that Windows gives us valid UTF-16LE.
const end_index = std.unicode.utf16leToUtf8(out_buffer, wide_slice[start_index..]) catch unreachable;
const end_index = std.unicode.utf16leToUtf8(out_buffer, wide_slice) catch unreachable;
return out_buffer[0..end_index];
}

Expand Down
160 changes: 143 additions & 17 deletions lib/std/os/windows.zig
Original file line number Diff line number Diff line change
Expand Up @@ -897,30 +897,156 @@ pub fn SetFilePointerEx_CURRENT_get(handle: HANDLE) SetFilePointerError!u64 {
}

pub const GetFinalPathNameByHandleError = error{
BadPathName,
FileNotFound,
SystemResources,
NameTooLong,
Unexpected,
};

pub fn GetFinalPathNameByHandleW(
/// Specifies how to format volume path in the result of `GetFinalPathNameByHandle`.
/// Defaults to DOS volume names.
pub const GetFinalPathNameByHandleFormat = struct {
volume_name: enum {
/// Format as DOS volume name
Dos,
/// Format as NT volume name
Nt,
} = .Dos,
};

/// Returns canonical (normalized) path of handle.
/// Use `GetFinalPathNameByHandleFormat` to specify whether the path is meant to include
/// NT or DOS volume name (e.g., `\Device\HarddiskVolume0\foo.txt` versus `C:\foo.txt`).
/// If DOS volume name format is selected, note that this function does *not* prepend
/// `\\?\` prefix to the resultant path.
pub fn GetFinalPathNameByHandle(
hFile: HANDLE,
buf_ptr: [*]u16,
buf_len: DWORD,
flags: DWORD,
) GetFinalPathNameByHandleError![:0]u16 {
const rc = kernel32.GetFinalPathNameByHandleW(hFile, buf_ptr, buf_len, flags);
if (rc == 0) {
switch (kernel32.GetLastError()) {
.FILE_NOT_FOUND => return error.FileNotFound,
.PATH_NOT_FOUND => return error.FileNotFound,
.NOT_ENOUGH_MEMORY => return error.SystemResources,
.FILENAME_EXCED_RANGE => return error.NameTooLong,
.INVALID_PARAMETER => unreachable,
else => |err| return unexpectedError(err),
}
fmt: GetFinalPathNameByHandleFormat,
out_buffer: []u16,
) GetFinalPathNameByHandleError![]u16 {
// Get normalized path; doesn't include volume name though.
var path_buffer: [@sizeOf(FILE_NAME_INFORMATION) + PATH_MAX_WIDE * 2]u8 align(@alignOf(FILE_NAME_INFORMATION)) = undefined;
try QueryInformationFile(hFile, .FileNormalizedNameInformation, path_buffer[0..]);

// Get NT volume name.
var volume_buffer: [@sizeOf(FILE_NAME_INFORMATION) + MAX_PATH]u8 align(@alignOf(FILE_NAME_INFORMATION)) = undefined; // MAX_PATH bytes should be enough since it's Windows-defined name
try QueryInformationFile(hFile, .FileVolumeNameInformation, volume_buffer[0..]);

const file_name = @ptrCast(*const FILE_NAME_INFORMATION, &path_buffer[0]);
const file_name_u16 = @ptrCast([*]const u16, &file_name.FileName[0])[0 .. file_name.FileNameLength / 2];

const volume_name = @ptrCast(*const FILE_NAME_INFORMATION, &volume_buffer[0]);

switch (fmt.volume_name) {
.Nt => {
// Nothing to do, we simply copy the bytes to the user-provided buffer.
const volume_name_u16 = @ptrCast([*]const u16, &volume_name.FileName[0])[0 .. volume_name.FileNameLength / 2];

if (out_buffer.len < volume_name_u16.len + file_name_u16.len) return error.NameTooLong;

std.mem.copy(u16, out_buffer[0..], volume_name_u16);
std.mem.copy(u16, out_buffer[volume_name_u16.len..], file_name_u16);

return out_buffer[0 .. volume_name_u16.len + file_name_u16.len];
},
.Dos => {
// Get DOS volume name. DOS volume names are actually symbolic link objects to the
// actual NT volume. For example:
// (NT) \Device\HarddiskVolume4 => (DOS) \DosDevices\C: == (DOS) C:
const MIN_SIZE = @sizeOf(MOUNTMGR_MOUNT_POINT) + MAX_PATH;
// We initialize the input buffer to all zeros for convenience since
// `DeviceIoControl` with `IOCTL_MOUNTMGR_QUERY_POINTS` expects this.
var input_buf: [MIN_SIZE]u8 align(@alignOf(MOUNTMGR_MOUNT_POINT)) = [_]u8{0} ** MIN_SIZE;
var output_buf: [MIN_SIZE * 4]u8 align(@alignOf(MOUNTMGR_MOUNT_POINTS)) = undefined;

// 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(), .{
.access_mask = SYNCHRONIZE,
.share_access = FILE_SHARE_READ | FILE_SHARE_WRITE,
.creation = FILE_OPEN,
.io_mode = .blocking,
}) catch |err| switch (err) {
error.IsDir => unreachable,
error.NotDir => unreachable,
error.NoDevice => unreachable,
error.AccessDenied => unreachable,
error.PipeBusy => unreachable,
error.PathAlreadyExists => unreachable,
error.WouldBlock => unreachable,
else => |e| return e,
};
defer CloseHandle(mgmt_handle);

var input_struct = @ptrCast(*MOUNTMGR_MOUNT_POINT, &input_buf[0]);
input_struct.DeviceNameOffset = @sizeOf(MOUNTMGR_MOUNT_POINT);
input_struct.DeviceNameLength = @intCast(USHORT, volume_name.FileNameLength);
@memcpy(input_buf[@sizeOf(MOUNTMGR_MOUNT_POINT)..], @ptrCast([*]const u8, &volume_name.FileName[0]), volume_name.FileNameLength);

try DeviceIoControl(mgmt_handle, IOCTL_MOUNTMGR_QUERY_POINTS, input_buf[0..], output_buf[0..]);
const mount_points_struct = @ptrCast(*const MOUNTMGR_MOUNT_POINTS, &output_buf[0]);

const mount_points = @ptrCast(
[*]const MOUNTMGR_MOUNT_POINT,
&mount_points_struct.MountPoints[0],
)[0..mount_points_struct.NumberOfMountPoints];

var found: bool = false;
for (mount_points) |mount_point| {
const symlink = @ptrCast(
[*]const u16,
@alignCast(@alignOf(u16), &output_buf[mount_point.SymbolicLinkNameOffset]),
)[0 .. mount_point.SymbolicLinkNameLength / 2];

// Look for `\DosDevices\` prefix. We don't really care if there are more than one symlinks
// with traditional DOS drive letters, so pick the first one available.
const prefix_u8 = "\\DosDevices\\";
var prefix_buf_u16: [prefix_u8.len]u16 = undefined;
const prefix_len_u16 = std.unicode.utf8ToUtf16Le(prefix_buf_u16[0..], prefix_u8[0..]) catch unreachable;
const prefix = prefix_buf_u16[0..prefix_len_u16];

if (std.mem.startsWith(u16, symlink, prefix)) {
const drive_letter = symlink[prefix.len..];

if (out_buffer.len < drive_letter.len + file_name_u16.len) return error.NameTooLong;

std.mem.copy(u16, out_buffer[0..], drive_letter);
std.mem.copy(u16, out_buffer[drive_letter.len..], file_name_u16);
const total_len = drive_letter.len + file_name_u16.len;

// Validate that DOS does not contain any spurious nul bytes.
if (std.mem.indexOfScalar(u16, out_buffer[0..total_len], 0)) |_| {
return error.BadPathName;
}

return out_buffer[0..total_len];
}
}

// If we've ended up here, then something went wrong/is corrupted in the OS,
// so error out!
return error.FileNotFound;
},
}
}

pub const QueryInformationFileError = error{Unexpected};

pub fn QueryInformationFile(
handle: HANDLE,
info_class: FILE_INFORMATION_CLASS,
out_buffer: []u8,
) QueryInformationFileError!void {
var io: IO_STATUS_BLOCK = undefined;
const len_bytes = std.math.cast(u32, out_buffer.len) catch unreachable;
const rc = ntdll.NtQueryInformationFile(handle, &io, out_buffer.ptr, len_bytes, info_class);
switch (rc) {
.SUCCESS => {},
.INVALID_PARAMETER => unreachable,
else => return unexpectedStatus(rc),
}
return buf_ptr[0..rc :0];
}

pub const GetFileSizeError = error{Unexpected};
Expand Down
18 changes: 18 additions & 0 deletions lib/std/os/windows/bits.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1573,3 +1573,21 @@ pub const SYMLINK_FLAG_RELATIVE: ULONG = 0x1;

pub const SYMBOLIC_LINK_FLAG_DIRECTORY: DWORD = 0x1;
pub const SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE: DWORD = 0x2;

pub const MOUNTMGR_MOUNT_POINT = extern struct {
SymbolicLinkNameOffset: ULONG,
SymbolicLinkNameLength: USHORT,
Reserved1: USHORT,
UniqueIdOffset: ULONG,
UniqueIdLength: USHORT,
Reserved2: USHORT,
DeviceNameOffset: ULONG,
DeviceNameLength: USHORT,
Reserved3: USHORT,
};
pub const MOUNTMGR_MOUNT_POINTS = extern struct {
Size: ULONG,
NumberOfMountPoints: ULONG,
MountPoints: [1]MOUNTMGR_MOUNT_POINT,
};
pub const IOCTL_MOUNTMGR_QUERY_POINTS: ULONG = 0x6d0008;