Fix windows.CreateSymbolicLink/ReadLink for non-relative paths

This fixes a few things:
- Previously, CreateSymbolicLink would always create a relative link if a `dir` was provided, but the relative-ness of a link should be determined by the target path, not the null-ness of the `dir`.
- Special handling is now done to symlink to 'rooted' paths correctly (they are treated as a relative link, which is different than how the xToPrefixedFileW functions treat them)
- ReadLink now correctly supports UNC paths via a new `ntToWin32Namespace` function which intends to be an analog of `RtlNtPathNameToDosPathName` (RtlNtPathNameToDosPathName is not used because it seems to heap allocate as it takes an RTL_UNICODE_STRING_BUFFER)
This commit is contained in:
Ryan Liptak
2023-08-10 02:27:24 -07:00
parent 3e69115784
commit 19b219bc8a
3 changed files with 128 additions and 19 deletions

View File

@@ -1949,7 +1949,13 @@ pub const Dir = struct {
return self.symLinkWasi(target_path, sym_link_path, flags);
}
if (builtin.os.tag == .windows) {
const target_path_w = try os.windows.sliceToPrefixedFileW(self.fd, target_path);
// Target path does not use sliceToPrefixedFileW because certain paths
// are handled differently when creating a symlink than they would be
// when converting to an NT namespaced path. CreateSymbolicLink in
// symLinkW will handle the necessary conversion.
var target_path_w: os.windows.PathSpace = undefined;
target_path_w.len = try std.unicode.utf8ToUtf16Le(&target_path_w.data, target_path);
target_path_w.data[target_path_w.len] = 0;
const sym_link_path_w = try os.windows.sliceToPrefixedFileW(self.fd, sym_link_path);
return self.symLinkW(target_path_w.span(), sym_link_path_w.span(), flags);
}
@@ -1987,7 +1993,10 @@ pub const Dir = struct {
/// are null-terminated, WTF16 encoded.
pub fn symLinkW(
self: Dir,
target_path_w: []const u16,
/// WTF-16, does not need to be NT-prefixed. The NT-prefixing
/// of this path is handled by CreateSymbolicLink.
target_path_w: [:0]const u16,
/// WTF-16, must be NT-prefixed or relative
sym_link_path_w: []const u16,
flags: SymLinkFlags,
) !void {

View File

@@ -193,7 +193,7 @@ test "symlink with relative paths" {
os.windows.CreateSymbolicLink(
cwd.fd,
&[_]u16{ 's', 'y', 'm', 'l', 'i', 'n', 'k', 'e', 'd' },
&[_]u16{ 'f', 'i', 'l', 'e', '.', 't', 'x', 't' },
&[_:0]u16{ 'f', 'i', 'l', 'e', '.', 't', 'x', 't' },
false,
) catch |err| switch (err) {
// Symlink requires admin privileges on windows, so this test can legitimately fail.
@@ -351,7 +351,7 @@ test "readlinkat" {
os.windows.CreateSymbolicLink(
tmp.dir.fd,
&[_]u16{ 'l', 'i', 'n', 'k' },
&[_]u16{ 'f', 'i', 'l', 'e', '.', 't', 'x', 't' },
&[_:0]u16{ 'f', 'i', 'l', 'e', '.', 't', 'x', 't' },
false,
) catch |err| switch (err) {
// Symlink requires admin privileges on windows, so this test can legitimately fail.

View File

@@ -704,6 +704,7 @@ pub const CreateSymbolicLinkError = error{
NameTooLong,
NoDevice,
NetworkNotFound,
BadPathName,
Unexpected,
};
@@ -716,7 +717,7 @@ pub const CreateSymbolicLinkError = error{
pub fn CreateSymbolicLink(
dir: ?HANDLE,
sym_link_path: []const u16,
target_path: []const u16,
target_path: [:0]const u16,
is_directory: bool,
) CreateSymbolicLinkError!void {
const SYMLINK_DATA = extern struct {
@@ -745,25 +746,58 @@ pub fn CreateSymbolicLink(
};
defer CloseHandle(symlink_handle);
// Relevant portions of the documentation:
// > Relative links are specified using the following conventions:
// > - Root relative—for example, "\Windows\System32" resolves to "current drive:\Windows\System32".
// > - Current working directoryrelative—for example, if the current working directory is
// > C:\Windows\System32, "C:File.txt" resolves to "C:\Windows\System32\File.txt".
// > Note: If you specify a current working directoryrelative link, it is created as an absolute
// > link, due to the way the current working directory is processed based on the user and the thread.
// https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createsymboliclinkw
var is_target_absolute = false;
const final_target_path = target_path: {
switch (getNamespacePrefix(u16, target_path)) {
.none => switch (getUnprefixedPathType(u16, target_path)) {
// Rooted paths need to avoid getting put through wToPrefixedFileW
// (and they are treated as relative in this context)
// Note: It seems that rooted paths in symbolic links are relative to
// the drive that the symbolic exists on, not to the CWD's drive.
// So, if the symlink is on C:\ and the CWD is on D:\,
// it will still resolve the path relative to the root of
// the C:\ drive.
.rooted => break :target_path target_path,
else => {},
},
// Already an NT path, no need to do anything to it
.nt => break :target_path target_path,
else => {},
}
var prefixed_target_path = try wToPrefixedFileW(dir, target_path);
// We do this after prefixing to ensure that drive-relative paths are treated as absolute
is_target_absolute = std.fs.path.isAbsoluteWindowsWTF16(prefixed_target_path.span());
break :target_path prefixed_target_path.span();
};
// prepare reparse data buffer
var buffer: [MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined;
const buf_len = @sizeOf(SYMLINK_DATA) + target_path.len * 4;
const buf_len = @sizeOf(SYMLINK_DATA) + final_target_path.len * 4;
const header_len = @sizeOf(ULONG) + @sizeOf(USHORT) * 2;
const target_is_absolute = std.fs.path.isAbsoluteWindowsWTF16(final_target_path);
const symlink_data = SYMLINK_DATA{
.ReparseTag = IO_REPARSE_TAG_SYMLINK,
.ReparseDataLength = @as(u16, @intCast(buf_len - header_len)),
.Reserved = 0,
.SubstituteNameOffset = @as(u16, @intCast(target_path.len * 2)),
.SubstituteNameLength = @as(u16, @intCast(target_path.len * 2)),
.SubstituteNameOffset = @as(u16, @intCast(final_target_path.len * 2)),
.SubstituteNameLength = @as(u16, @intCast(final_target_path.len * 2)),
.PrintNameOffset = 0,
.PrintNameLength = @as(u16, @intCast(target_path.len * 2)),
.Flags = if (dir) |_| SYMLINK_FLAG_RELATIVE else 0,
.PrintNameLength = @as(u16, @intCast(final_target_path.len * 2)),
.Flags = if (!target_is_absolute) SYMLINK_FLAG_RELATIVE else 0,
};
@memcpy(buffer[0..@sizeOf(SYMLINK_DATA)], std.mem.asBytes(&symlink_data));
@memcpy(buffer[@sizeOf(SYMLINK_DATA)..][0 .. target_path.len * 2], @as([*]const u8, @ptrCast(target_path)));
const paths_start = @sizeOf(SYMLINK_DATA) + target_path.len * 2;
@memcpy(buffer[paths_start..][0 .. target_path.len * 2], @as([*]const u8, @ptrCast(target_path)));
@memcpy(buffer[@sizeOf(SYMLINK_DATA)..][0 .. final_target_path.len * 2], @as([*]const u8, @ptrCast(final_target_path)));
const paths_start = @sizeOf(SYMLINK_DATA) + final_target_path.len * 2;
@memcpy(buffer[paths_start..][0 .. final_target_path.len * 2], @as([*]const u8, @ptrCast(final_target_path)));
_ = try DeviceIoControl(symlink_handle, FSCTL_SET_REPARSE_POINT, buffer[0..buf_len], null);
}
@@ -861,12 +895,15 @@ pub fn ReadLink(dir: ?HANDLE, sub_path_w: []const u16, out_buffer: []u8) ReadLin
}
fn parseReadlinkPath(path: []const u16, is_relative: bool, out_buffer: []u8) []u8 {
const prefix = [_]u16{ '\\', '?', '?', '\\' };
var start_index: usize = 0;
if (!is_relative and std.mem.startsWith(u16, path, &prefix)) {
start_index = prefix.len;
}
const out_len = std.unicode.utf16leToUtf8(out_buffer, path[start_index..]) catch unreachable;
const win32_namespace_path = path: {
if (is_relative) break :path path;
const win32_path = ntToWin32Namespace(path) catch |err| switch (err) {
error.NameTooLong => unreachable,
error.NotNtPath => break :path path,
};
break :path win32_path.span();
};
const out_len = std.unicode.utf16leToUtf8(out_buffer, win32_namespace_path) catch unreachable;
return out_buffer[0..out_len];
}
@@ -2393,6 +2430,69 @@ test getUnprefixedPathType {
try std.testing.expectEqual(UnprefixedPathType.drive_absolute, getUnprefixedPathType(u8, "x:/a/b/c"));
}
/// Similar to `RtlNtPathNameToDosPathName` but does not do any heap allocation.
/// The possible transformations are:
/// \??\C:\Some\Path -> C:\Some\Path
/// \??\UNC\server\share\foo -> \\server\share\foo
/// If the path does not have the NT namespace prefix, then `error.NotNtPath` is returned.
///
/// Functionality is based on the ReactOS test cases found here:
/// https://github.com/reactos/reactos/blob/master/modules/rostests/apitests/ntdll/RtlNtPathNameToDosPathName.c
pub fn ntToWin32Namespace(path: []const u16) !PathSpace {
if (path.len > PATH_MAX_WIDE) return error.NameTooLong;
var path_space: PathSpace = undefined;
const namespace_prefix = getNamespacePrefix(u16, path);
switch (namespace_prefix) {
.nt => {
var dest_index: usize = 0;
var after_prefix = path[4..]; // after the `\??\`
// The prefix \??\UNC\ means this is a UNC path, in which case the
// `\??\UNC\` should be replaced by `\\` (two backslashes)
// TODO: the "UNC" should technically be matched case-insensitively, but
// it's unlikely to matter since most/all paths passed into this
// function will have come from the OS meaning it should have
// the 'canonical' uppercase UNC.
const is_unc = after_prefix.len >= 4 and std.mem.eql(u16, after_prefix[0..3], std.unicode.utf8ToUtf16LeStringLiteral("UNC")) and std.fs.path.PathType.windows.isSep(u16, after_prefix[3]);
if (is_unc) {
path_space.data[0] = '\\';
dest_index += 1;
// We want to include the last `\` of `\??\UNC\`
after_prefix = path[7..];
}
@memcpy(path_space.data[dest_index..][0..after_prefix.len], after_prefix);
path_space.len = dest_index + after_prefix.len;
path_space.data[path_space.len] = 0;
return path_space;
},
else => return error.NotNtPath,
}
}
test "ntToWin32Namespace" {
const L = std.unicode.utf8ToUtf16LeStringLiteral;
try testNtToWin32Namespace(L("UNC"), L("\\??\\UNC"));
try testNtToWin32Namespace(L("\\\\"), L("\\??\\UNC\\"));
try testNtToWin32Namespace(L("\\\\path1"), L("\\??\\UNC\\path1"));
try testNtToWin32Namespace(L("\\\\path1\\path2"), L("\\??\\UNC\\path1\\path2"));
try testNtToWin32Namespace(L(""), L("\\??\\"));
try testNtToWin32Namespace(L("C:"), L("\\??\\C:"));
try testNtToWin32Namespace(L("C:\\"), L("\\??\\C:\\"));
try testNtToWin32Namespace(L("C:\\test"), L("\\??\\C:\\test"));
try testNtToWin32Namespace(L("C:\\test\\"), L("\\??\\C:\\test\\"));
try std.testing.expectError(error.NotNtPath, ntToWin32Namespace(L("foo")));
try std.testing.expectError(error.NotNtPath, ntToWin32Namespace(L("C:\\test")));
try std.testing.expectError(error.NotNtPath, ntToWin32Namespace(L("\\\\.\\test")));
}
fn testNtToWin32Namespace(expected: []const u16, path: []const u16) !void {
const converted = try ntToWin32Namespace(path);
try std.testing.expectEqualSlices(u16, expected, converted.span());
}
fn getFullPathNameW(path: [*:0]const u16, out: []u16) !usize {
const result = kernel32.GetFullPathNameW(path, @as(u32, @intCast(out.len)), out.ptr, null);
if (result == 0) {