Commit 19b219bc8a

Ryan Liptak <squeek502@hotmail.com>
2023-08-10 11:27:24
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)
1 parent 3e69115
Changed files (3)
lib/std/os/test.zig
@@ -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.
lib/std/os/windows.zig
@@ -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) {
lib/std/fs.zig
@@ -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 {