authorgravatar for squeek502@hotmail.comRyan Liptak <squeek502@hotmail.com> 2023-08-10 02:27:24-07:00
committergravatar for squeek502@hotmail.comRyan Liptak <squeek502@hotmail.com> 2023-08-15 15:52:33-07:00
log19b219bc8adbfeb1d1a9faa691563a8e3ff390b8
treecb907f834adcde3dd47bd1a0fbba12e0a9db969f
parent3e691157847e4787cb61265e42fcbf89336b4c3a

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)

3 files changed, 128 insertions(+), 19 deletions(-)

lib/std/fs.zig+11-2
......@@ -1949,7 +1949,13 @@ pub const Dir = struct {
19491949 return self.symLinkWasi(target_path, sym_link_path, flags);
19501950 }
19511951 if (builtin.os.tag == .windows) {
1952 const target_path_w = try os.windows.sliceToPrefixedFileW(self.fd, target_path);
1952 // Target path does not use sliceToPrefixedFileW because certain paths
1953 // are handled differently when creating a symlink than they would be
1954 // when converting to an NT namespaced path. CreateSymbolicLink in
1955 // symLinkW will handle the necessary conversion.
1956 var target_path_w: os.windows.PathSpace = undefined;
1957 target_path_w.len = try std.unicode.utf8ToUtf16Le(&target_path_w.data, target_path);
1958 target_path_w.data[target_path_w.len] = 0;
19531959 const sym_link_path_w = try os.windows.sliceToPrefixedFileW(self.fd, sym_link_path);
19541960 return self.symLinkW(target_path_w.span(), sym_link_path_w.span(), flags);
19551961 }
......@@ -1987,7 +1993,10 @@ pub const Dir = struct {
19871993 /// are null-terminated, WTF16 encoded.
19881994 pub fn symLinkW(
19891995 self: Dir,
1990 target_path_w: []const u16,
1996 /// WTF-16, does not need to be NT-prefixed. The NT-prefixing
1997 /// of this path is handled by CreateSymbolicLink.
1998 target_path_w: [:0]const u16,
1999 /// WTF-16, must be NT-prefixed or relative
19912000 sym_link_path_w: []const u16,
19922001 flags: SymLinkFlags,
19932002 ) !void {
lib/std/os/test.zig+2-2
......@@ -193,7 +193,7 @@ test "symlink with relative paths" {
193193 os.windows.CreateSymbolicLink(
194194 cwd.fd,
195195 &[_]u16{ 's', 'y', 'm', 'l', 'i', 'n', 'k', 'e', 'd' },
196 &[_]u16{ 'f', 'i', 'l', 'e', '.', 't', 'x', 't' },
196 &[_:0]u16{ 'f', 'i', 'l', 'e', '.', 't', 'x', 't' },
197197 false,
198198 ) catch |err| switch (err) {
199199 // Symlink requires admin privileges on windows, so this test can legitimately fail.
......@@ -351,7 +351,7 @@ test "readlinkat" {
351351 os.windows.CreateSymbolicLink(
352352 tmp.dir.fd,
353353 &[_]u16{ 'l', 'i', 'n', 'k' },
354 &[_]u16{ 'f', 'i', 'l', 'e', '.', 't', 'x', 't' },
354 &[_:0]u16{ 'f', 'i', 'l', 'e', '.', 't', 'x', 't' },
355355 false,
356356 ) catch |err| switch (err) {
357357 // Symlink requires admin privileges on windows, so this test can legitimately fail.
lib/std/os/windows.zig+115-15
......@@ -704,6 +704,7 @@ pub const CreateSymbolicLinkError = error{
704704 NameTooLong,
705705 NoDevice,
706706 NetworkNotFound,
707 BadPathName,
707708 Unexpected,
708709};
709710
......@@ -716,7 +717,7 @@ pub const CreateSymbolicLinkError = error{
716717pub fn CreateSymbolicLink(
717718 dir: ?HANDLE,
718719 sym_link_path: []const u16,
719 target_path: []const u16,
720 target_path: [:0]const u16,
720721 is_directory: bool,
721722) CreateSymbolicLinkError!void {
722723 const SYMLINK_DATA = extern struct {
......@@ -745,25 +746,58 @@ pub fn CreateSymbolicLink(
745746 };
746747 defer CloseHandle(symlink_handle);
747748
749 // Relevant portions of the documentation:
750 // > Relative links are specified using the following conventions:
751 // > - Root relative—for example, "\Windows\System32" resolves to "current drive:\Windows\System32".
752 // > - Current working directory–relative—for example, if the current working directory is
753 // > C:\Windows\System32, "C:File.txt" resolves to "C:\Windows\System32\File.txt".
754 // > Note: If you specify a current working directory–relative link, it is created as an absolute
755 // > link, due to the way the current working directory is processed based on the user and the thread.
756 // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createsymboliclinkw
757 var is_target_absolute = false;
758 const final_target_path = target_path: {
759 switch (getNamespacePrefix(u16, target_path)) {
760 .none => switch (getUnprefixedPathType(u16, target_path)) {
761 // Rooted paths need to avoid getting put through wToPrefixedFileW
762 // (and they are treated as relative in this context)
763 // Note: It seems that rooted paths in symbolic links are relative to
764 // the drive that the symbolic exists on, not to the CWD's drive.
765 // So, if the symlink is on C:\ and the CWD is on D:\,
766 // it will still resolve the path relative to the root of
767 // the C:\ drive.
768 .rooted => break :target_path target_path,
769 else => {},
770 },
771 // Already an NT path, no need to do anything to it
772 .nt => break :target_path target_path,
773 else => {},
774 }
775 var prefixed_target_path = try wToPrefixedFileW(dir, target_path);
776 // We do this after prefixing to ensure that drive-relative paths are treated as absolute
777 is_target_absolute = std.fs.path.isAbsoluteWindowsWTF16(prefixed_target_path.span());
778 break :target_path prefixed_target_path.span();
779 };
780
748781 // prepare reparse data buffer
749782 var buffer: [MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined;
750 const buf_len = @sizeOf(SYMLINK_DATA) + target_path.len * 4;
783 const buf_len = @sizeOf(SYMLINK_DATA) + final_target_path.len * 4;
751784 const header_len = @sizeOf(ULONG) + @sizeOf(USHORT) * 2;
785 const target_is_absolute = std.fs.path.isAbsoluteWindowsWTF16(final_target_path);
752786 const symlink_data = SYMLINK_DATA{
753787 .ReparseTag = IO_REPARSE_TAG_SYMLINK,
754788 .ReparseDataLength = @as(u16, @intCast(buf_len - header_len)),
755789 .Reserved = 0,
756 .SubstituteNameOffset = @as(u16, @intCast(target_path.len * 2)),
757 .SubstituteNameLength = @as(u16, @intCast(target_path.len * 2)),
790 .SubstituteNameOffset = @as(u16, @intCast(final_target_path.len * 2)),
791 .SubstituteNameLength = @as(u16, @intCast(final_target_path.len * 2)),
758792 .PrintNameOffset = 0,
759 .PrintNameLength = @as(u16, @intCast(target_path.len * 2)),
760 .Flags = if (dir) |_| SYMLINK_FLAG_RELATIVE else 0,
793 .PrintNameLength = @as(u16, @intCast(final_target_path.len * 2)),
794 .Flags = if (!target_is_absolute) SYMLINK_FLAG_RELATIVE else 0,
761795 };
762796
763797 @memcpy(buffer[0..@sizeOf(SYMLINK_DATA)], std.mem.asBytes(&symlink_data));
764 @memcpy(buffer[@sizeOf(SYMLINK_DATA)..][0 .. target_path.len * 2], @as([*]const u8, @ptrCast(target_path)));
765 const paths_start = @sizeOf(SYMLINK_DATA) + target_path.len * 2;
766 @memcpy(buffer[paths_start..][0 .. target_path.len * 2], @as([*]const u8, @ptrCast(target_path)));
798 @memcpy(buffer[@sizeOf(SYMLINK_DATA)..][0 .. final_target_path.len * 2], @as([*]const u8, @ptrCast(final_target_path)));
799 const paths_start = @sizeOf(SYMLINK_DATA) + final_target_path.len * 2;
800 @memcpy(buffer[paths_start..][0 .. final_target_path.len * 2], @as([*]const u8, @ptrCast(final_target_path)));
767801 _ = try DeviceIoControl(symlink_handle, FSCTL_SET_REPARSE_POINT, buffer[0..buf_len], null);
768802}
769803
......@@ -861,12 +895,15 @@ pub fn ReadLink(dir: ?HANDLE, sub_path_w: []const u16, out_buffer: []u8) ReadLin
861895}
862896
863897fn parseReadlinkPath(path: []const u16, is_relative: bool, out_buffer: []u8) []u8 {
864 const prefix = [_]u16{ '\\', '?', '?', '\\' };
865 var start_index: usize = 0;
866 if (!is_relative and std.mem.startsWith(u16, path, &prefix)) {
867 start_index = prefix.len;
868 }
869 const out_len = std.unicode.utf16leToUtf8(out_buffer, path[start_index..]) catch unreachable;
898 const win32_namespace_path = path: {
899 if (is_relative) break :path path;
900 const win32_path = ntToWin32Namespace(path) catch |err| switch (err) {
901 error.NameTooLong => unreachable,
902 error.NotNtPath => break :path path,
903 };
904 break :path win32_path.span();
905 };
906 const out_len = std.unicode.utf16leToUtf8(out_buffer, win32_namespace_path) catch unreachable;
870907 return out_buffer[0..out_len];
871908}
872909
......@@ -2393,6 +2430,69 @@ test getUnprefixedPathType {
23932430 try std.testing.expectEqual(UnprefixedPathType.drive_absolute, getUnprefixedPathType(u8, "x:/a/b/c"));
23942431}
23952432
2433/// Similar to `RtlNtPathNameToDosPathName` but does not do any heap allocation.
2434/// The possible transformations are:
2435/// \??\C:\Some\Path -> C:\Some\Path
2436/// \??\UNC\server\share\foo -> \\server\share\foo
2437/// If the path does not have the NT namespace prefix, then `error.NotNtPath` is returned.
2438///
2439/// Functionality is based on the ReactOS test cases found here:
2440/// https://github.com/reactos/reactos/blob/master/modules/rostests/apitests/ntdll/RtlNtPathNameToDosPathName.c
2441pub fn ntToWin32Namespace(path: []const u16) !PathSpace {
2442 if (path.len > PATH_MAX_WIDE) return error.NameTooLong;
2443
2444 var path_space: PathSpace = undefined;
2445 const namespace_prefix = getNamespacePrefix(u16, path);
2446 switch (namespace_prefix) {
2447 .nt => {
2448 var dest_index: usize = 0;
2449 var after_prefix = path[4..]; // after the `\??\`
2450 // The prefix \??\UNC\ means this is a UNC path, in which case the
2451 // `\??\UNC\` should be replaced by `\\` (two backslashes)
2452 // TODO: the "UNC" should technically be matched case-insensitively, but
2453 // it's unlikely to matter since most/all paths passed into this
2454 // function will have come from the OS meaning it should have
2455 // the 'canonical' uppercase UNC.
2456 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]);
2457 if (is_unc) {
2458 path_space.data[0] = '\\';
2459 dest_index += 1;
2460 // We want to include the last `\` of `\??\UNC\`
2461 after_prefix = path[7..];
2462 }
2463 @memcpy(path_space.data[dest_index..][0..after_prefix.len], after_prefix);
2464 path_space.len = dest_index + after_prefix.len;
2465 path_space.data[path_space.len] = 0;
2466 return path_space;
2467 },
2468 else => return error.NotNtPath,
2469 }
2470}
2471
2472test "ntToWin32Namespace" {
2473 const L = std.unicode.utf8ToUtf16LeStringLiteral;
2474
2475 try testNtToWin32Namespace(L("UNC"), L("\\??\\UNC"));
2476 try testNtToWin32Namespace(L("\\\\"), L("\\??\\UNC\\"));
2477 try testNtToWin32Namespace(L("\\\\path1"), L("\\??\\UNC\\path1"));
2478 try testNtToWin32Namespace(L("\\\\path1\\path2"), L("\\??\\UNC\\path1\\path2"));
2479
2480 try testNtToWin32Namespace(L(""), L("\\??\\"));
2481 try testNtToWin32Namespace(L("C:"), L("\\??\\C:"));
2482 try testNtToWin32Namespace(L("C:\\"), L("\\??\\C:\\"));
2483 try testNtToWin32Namespace(L("C:\\test"), L("\\??\\C:\\test"));
2484 try testNtToWin32Namespace(L("C:\\test\\"), L("\\??\\C:\\test\\"));
2485
2486 try std.testing.expectError(error.NotNtPath, ntToWin32Namespace(L("foo")));
2487 try std.testing.expectError(error.NotNtPath, ntToWin32Namespace(L("C:\\test")));
2488 try std.testing.expectError(error.NotNtPath, ntToWin32Namespace(L("\\\\.\\test")));
2489}
2490
2491fn testNtToWin32Namespace(expected: []const u16, path: []const u16) !void {
2492 const converted = try ntToWin32Namespace(path);
2493 try std.testing.expectEqualSlices(u16, expected, converted.span());
2494}
2495
23962496fn getFullPathNameW(path: [*:0]const u16, out: []u16) !usize {
23972497 const result = kernel32.GetFullPathNameW(path, @as(u32, @intCast(out.len)), out.ptr, null);
23982498 if (result == 0) {