authorgravatar for squeek502@hotmail.comRyan Liptak <squeek502@hotmail.com> 2025-10-17 00:50:16-07:00
committergravatar for squeek502@hotmail.comRyan Liptak <squeek502@hotmail.com> 2025-10-17 00:50:16-07:00
log88fd8ce8604f1c8ea79797f7263ac7b81910da9b
tree9550825076c18c92feae5dce15a8d1640a4237ca
parent3eb3fbec9c455b4fcea144919ddcd0932cefb23a

windows: Always try using POSIX_SEMANTICS/etc for rename/delete

The compile-time check against the minimum version here wasn't appropriate, since it still makes sense to try using FILE_RENAME_INFORMATION_EX even if the minimum version is something like `xp`, since that doesn't rule out the possibility of the compiled code running on Windows 10/11. This compile-time check was doubly bad since the default minimum windows version (`.win10`) was below the `.win10_rs5` that was checked for, so when providing a target like `x86_64-windows-gnu` it'd always rule out using this syscall. After this commit, we always try using FILE_RENAME_INFORMATION_EX and then let the operating system tell us when some aspect of it is not supported. This allows us to get the benefits of these new syscalls/flags whenever it's actually possible. The possible error returns were validated experimentally: - INVALID_PARAMETER is returned when the underlying filesystem is FAT32 - INVALID_INFO_CLASS is returned on Windows 7 when trying to use FileRenameInformationEx/FileDispositionInformationEx - NOT_SUPPORTED is returned on Windows 10 >= .win10_rs5 when setting a bogus flag value (I used `0x1000`)

2 files changed, 50 insertions(+), 31 deletions(-)

lib/std/os/windows.zig+22-11
......@@ -1071,13 +1071,18 @@ pub fn DeleteFile(sub_path_w: []const u16, options: DeleteFileOptions) DeleteFil
10711071 }
10721072 defer CloseHandle(tmp_handle);
10731073
1074 // FileDispositionInformationEx (and therefore FILE_DISPOSITION_POSIX_SEMANTICS and FILE_DISPOSITION_IGNORE_READONLY_ATTRIBUTE)
1075 // are only supported on NTFS filesystems, so the version check on its own is only a partial solution. To support non-NTFS filesystems
1076 // like FAT32, we need to fallback to FileDispositionInformation if the usage of FileDispositionInformationEx gives
1077 // us INVALID_PARAMETER.
1078 // The same reasoning for win10_rs5 as in os.renameatW() applies (FILE_DISPOSITION_IGNORE_READONLY_ATTRIBUTE requires >= win10_rs5).
1079 var need_fallback = true;
1080 if (comptime builtin.target.os.version_range.windows.min.isAtLeast(.win10_rs5)) {
1074 // FileDispositionInformationEx has varying levels of support:
1075 // - FILE_DISPOSITION_INFORMATION_EX requires >= win10_rs1
1076 // (INVALID_INFO_CLASS is returned if not supported)
1077 // - Requires the NTFS filesystem
1078 // (on filesystems like FAT32, INVALID_PARAMETER is returned)
1079 // - FILE_DISPOSITION_POSIX_SEMANTICS requires >= win10_rs1
1080 // - FILE_DISPOSITION_IGNORE_READONLY_ATTRIBUTE requires >= win10_rs5
1081 // (NOT_SUPPORTED is returned if a flag is unsupported)
1082 //
1083 // The strategy here is just to try using FileDispositionInformationEx and fall back to
1084 // FileDispositionInformation if the return value lets us know that some aspect of it is not supported.
1085 const need_fallback = need_fallback: {
10811086 // Deletion with posix semantics if the filesystem supports it.
10821087 var info = FILE_DISPOSITION_INFORMATION_EX{
10831088 .Flags = FILE_DISPOSITION_DELETE |
......@@ -1094,12 +1099,18 @@ pub fn DeleteFile(sub_path_w: []const u16, options: DeleteFileOptions) DeleteFil
10941099 );
10951100 switch (rc) {
10961101 .SUCCESS => return,
1097 // INVALID_PARAMETER here means that the filesystem does not support FileDispositionInformationEx
1098 .INVALID_PARAMETER => {},
1102 // The filesystem does not support FileDispositionInformationEx
1103 .INVALID_PARAMETER,
1104 // The operating system does not support FileDispositionInformationEx
1105 .INVALID_INFO_CLASS,
1106 // The operating system does not support one of the flags
1107 .NOT_SUPPORTED,
1108 => break :need_fallback true,
10991109 // For all other statuses, fall down to the switch below to handle them.
1100 else => need_fallback = false,
1110 else => break :need_fallback false,
11011111 }
1102 }
1112 };
1113
11031114 if (need_fallback) {
11041115 // Deletion with file pending semantics, which requires waiting or moving
11051116 // files to get them removed (from here).
lib/std/posix.zig+28-20
......@@ -2844,15 +2844,19 @@ pub fn renameatW(
28442844 };
28452845 defer windows.CloseHandle(src_fd);
28462846
2847 var need_fallback = true;
28482847 var rc: windows.NTSTATUS = undefined;
2849 // FILE_RENAME_INFORMATION_EX and FILE_RENAME_POSIX_SEMANTICS require >= win10_rs1,
2850 // but FILE_RENAME_IGNORE_READONLY_ATTRIBUTE requires >= win10_rs5. We check >= rs5 here
2851 // so that we only use POSIX_SEMANTICS when we know IGNORE_READONLY_ATTRIBUTE will also be
2852 // supported in order to avoid either (1) using a redundant call that we can know in advance will return
2853 // STATUS_NOT_SUPPORTED or (2) only setting IGNORE_READONLY_ATTRIBUTE when >= rs5
2854 // and therefore having different behavior when the Windows version is >= rs1 but < rs5.
2855 if (builtin.target.os.isAtLeast(.windows, .win10_rs5) orelse false) {
2848 // FileRenameInformationEx has varying levels of support:
2849 // - FILE_RENAME_INFORMATION_EX requires >= win10_rs1
2850 // (INVALID_INFO_CLASS is returned if not supported)
2851 // - Requires the NTFS filesystem
2852 // (on filesystems like FAT32, INVALID_PARAMETER is returned)
2853 // - FILE_RENAME_POSIX_SEMANTICS requires >= win10_rs1
2854 // - FILE_RENAME_IGNORE_READONLY_ATTRIBUTE requires >= win10_rs5
2855 // (NOT_SUPPORTED is returned if a flag is unsupported)
2856 //
2857 // The strategy here is just to try using FileRenameInformationEx and fall back to
2858 // FileRenameInformation if the return value lets us know that some aspect of it is not supported.
2859 const need_fallback = need_fallback: {
28562860 const struct_buf_len = @sizeOf(windows.FILE_RENAME_INFORMATION_EX) + (max_path_bytes - 1);
28572861 var rename_info_buf: [struct_buf_len]u8 align(@alignOf(windows.FILE_RENAME_INFORMATION_EX)) = undefined;
28582862 const struct_len = @sizeOf(windows.FILE_RENAME_INFORMATION_EX) - 1 + new_path_w.len * 2;
......@@ -2879,12 +2883,17 @@ pub fn renameatW(
28792883 );
28802884 switch (rc) {
28812885 .SUCCESS => return,
2882 // INVALID_PARAMETER here means that the filesystem does not support FileRenameInformationEx
2883 .INVALID_PARAMETER => {},
2886 // The filesystem does not support FileDispositionInformationEx
2887 .INVALID_PARAMETER,
2888 // The operating system does not support FileDispositionInformationEx
2889 .INVALID_INFO_CLASS,
2890 // The operating system does not support one of the flags
2891 .NOT_SUPPORTED,
2892 => break :need_fallback true,
28842893 // For all other statuses, fall down to the switch below to handle them.
2885 else => need_fallback = false,
2894 else => break :need_fallback false,
28862895 }
2887 }
2896 };
28882897
28892898 if (need_fallback) {
28902899 const struct_buf_len = @sizeOf(windows.FILE_RENAME_INFORMATION) + (max_path_bytes - 1);
......@@ -2903,14 +2912,13 @@ pub fn renameatW(
29032912 };
29042913 @memcpy((&rename_info.FileName).ptr, new_path_w);
29052914
2906 rc =
2907 windows.ntdll.NtSetInformationFile(
2908 src_fd,
2909 &io_status_block,
2910 rename_info,
2911 @intCast(struct_len), // already checked for error.NameTooLong
2912 .FileRenameInformation,
2913 );
2915 rc = windows.ntdll.NtSetInformationFile(
2916 src_fd,
2917 &io_status_block,
2918 rename_info,
2919 @intCast(struct_len), // already checked for error.NameTooLong
2920 .FileRenameInformation,
2921 );
29142922 }
29152923
29162924 switch (rc) {