| ... | ... | @@ -704,6 +704,7 @@ pub const CreateSymbolicLinkError = error{ |
| 704 | 704 | NameTooLong, |
| 705 | 705 | NoDevice, |
| 706 | 706 | NetworkNotFound, |
| 707 | BadPathName, |
| 707 | 708 | Unexpected, |
| 708 | 709 | }; |
| 709 | 710 | |
| ... | ... | @@ -716,7 +717,7 @@ pub const CreateSymbolicLinkError = error{ |
| 716 | 717 | pub fn CreateSymbolicLink( |
| 717 | 718 | dir: ?HANDLE, |
| 718 | 719 | sym_link_path: []const u16, |
| 719 | | target_path: []const u16, |
| 720 | target_path: [:0]const u16, |
| 720 | 721 | is_directory: bool, |
| 721 | 722 | ) CreateSymbolicLinkError!void { |
| 722 | 723 | const SYMLINK_DATA = extern struct { |
| ... | ... | @@ -745,25 +746,58 @@ pub fn CreateSymbolicLink( |
| 745 | 746 | }; |
| 746 | 747 | defer CloseHandle(symlink_handle); |
| 747 | 748 | |
| 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 | |
| 748 | 781 | // prepare reparse data buffer |
| 749 | 782 | 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; |
| 751 | 784 | const header_len = @sizeOf(ULONG) + @sizeOf(USHORT) * 2; |
| 785 | const target_is_absolute = std.fs.path.isAbsoluteWindowsWTF16(final_target_path); |
| 752 | 786 | const symlink_data = SYMLINK_DATA{ |
| 753 | 787 | .ReparseTag = IO_REPARSE_TAG_SYMLINK, |
| 754 | 788 | .ReparseDataLength = @as(u16, @intCast(buf_len - header_len)), |
| 755 | 789 | .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)), |
| 758 | 792 | .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, |
| 761 | 795 | }; |
| 762 | 796 | |
| 763 | 797 | @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))); |
| 767 | 801 | _ = try DeviceIoControl(symlink_handle, FSCTL_SET_REPARSE_POINT, buffer[0..buf_len], null); |
| 768 | 802 | } |
| 769 | 803 | |
| ... | ... | @@ -861,12 +895,15 @@ pub fn ReadLink(dir: ?HANDLE, sub_path_w: []const u16, out_buffer: []u8) ReadLin |
| 861 | 895 | } |
| 862 | 896 | |
| 863 | 897 | fn 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; |
| 870 | 907 | return out_buffer[0..out_len]; |
| 871 | 908 | } |
| 872 | 909 | |
| ... | ... | @@ -2393,6 +2430,69 @@ test getUnprefixedPathType { |
| 2393 | 2430 | try std.testing.expectEqual(UnprefixedPathType.drive_absolute, getUnprefixedPathType(u8, "x:/a/b/c")); |
| 2394 | 2431 | } |
| 2395 | 2432 | |
| 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 |
| 2441 | pub 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 | |
| 2472 | test "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 | |
| 2491 | fn 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 | |
| 2396 | 2496 | fn getFullPathNameW(path: [*:0]const u16, out: []u16) !usize { |
| 2397 | 2497 | const result = kernel32.GetFullPathNameW(path, @as(u32, @intCast(out.len)), out.ptr, null); |
| 2398 | 2498 | if (result == 0) { |