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:
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 directory–relative—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 directory–relative 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) {
|
||||
|
||||
Reference in New Issue
Block a user