Related to #6354 and #6344
As far as I'm aware, the intention of std.os.renameW/std.os.renameatW is to emulate the behavior of rename/renameat but on Windows. This is currently not the case. From #6354:
- On Windows, the implementation of both
renameAbsolute and Dir.rename do not allow renaming directories.
Some info about what I tried and ultimately abandoned: Swapping out:
| constsrc_fd=windows.OpenFile(old_path_w, .{ |
| .dir=old_dir_fd, |
| .access_mask=windows.SYNCHRONIZE|windows.GENERIC_WRITE|windows.DELETE, |
| .creation=windows.FILE_OPEN, |
| .io_mode=.blocking, |
| }) catch|err|switch (err) { |
| error.WouldBlock=>unreachable, // Not possible without `.share_access_nonblocking = true`. |
| else=>|e|returne, |
| }; |
| deferwindows.CloseHandle(src_fd); |
for NtCreateFile like so:
constpath_len_bytes=@intCast(u16, old_path_w.len*2);
varnt_name=windows.UNICODE_STRING{
.Length=path_len_bytes,
.MaximumLength=path_len_bytes,
// The Windows API makes this mutable, but it will not mutate here.
.Buffer=@intToPtr([*]u16, @ptrToInt(old_path_w.ptr)),
};
varattr=windows.OBJECT_ATTRIBUTES{
.Length=@sizeOf(windows.OBJECT_ATTRIBUTES),
.RootDirectory=if (std.fs.path.isAbsoluteWindowsWTF16(old_path_w)) nullelseold_dir_fd,
.Attributes=0, // Note we do not use OBJ_CASE_INSENSITIVE here.
.ObjectName=&nt_name,
.SecurityDescriptor=null,
.SecurityQualityOfService=null,
};
vario: windows.IO_STATUS_BLOCK=undefined;
varsrc_fd: windows.HANDLE=undefined;
varrc=windows.ntdll.NtCreateFile(
&src_fd,
windows.SYNCHRONIZE|windows.DELETE,
&attr,
&io,
null,
0,
windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE,
windows.FILE_OPEN,
windows.FILE_OPEN_REPARSE_POINT,
null,
0,
);
switch (rc) {
.SUCCESS=> {},
.OBJECT_NAME_INVALID=>unreachable,
.OBJECT_NAME_NOT_FOUND=>returnerror.FileNotFound,
.INVALID_PARAMETER=>unreachable,
.FILE_IS_A_DIRECTORY=>returnerror.IsDir,
.NOT_A_DIRECTORY=>returnerror.NotDir,
else=>returnwindows.unexpectedStatus(rc),
}
deferwindows.CloseHandle(src_fd);fixes the simple case of directories being renamed, but there are some edge cases:
- Attempting to rename a file to the path of an existing directory gives
ACCESS_DENIED during the rename in NtSetInformationFile. Expected behavior (to match renameat) would be returning error.IsDir. This is achievable by querying the destination path to check if it's a directory, my attempt at this is in this details block:
Details
.ACCESS_DENIED=> {
// When renaming a file to the path of an existing directory,// ACCESS_DENIED can be returned. We need to translate this to error.IsDir// in that case.constdest_path_len_bytes=@intCast(u16, new_path_w.len*2);
vardest_nt_name=windows.UNICODE_STRING{
.Length=dest_path_len_bytes,
.MaximumLength=dest_path_len_bytes,
// The Windows API makes this mutable, but it will not mutate here.
.Buffer=@intToPtr([*]u16, @ptrToInt(new_path_w.ptr)),
};
vardest_attr=windows.OBJECT_ATTRIBUTES{
.Length=@sizeOf(windows.OBJECT_ATTRIBUTES),
.RootDirectory=if (std.fs.path.isAbsoluteWindowsWTF16(new_path_w)) nullelsenew_dir_fd,
.Attributes=0, // Note we do not use OBJ_CASE_INSENSITIVE here.
.ObjectName=&dest_nt_name,
.SecurityDescriptor=null,
.SecurityQualityOfService=null,
};
vardest_io: windows.IO_STATUS_BLOCK=undefined;
vardest_fd: windows.HANDLE=undefined;
rc=windows.ntdll.NtCreateFile(
&dest_fd,
windows.FILE_READ_ATTRIBUTES,
&dest_attr,
&dest_io,
null,
0,
windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE,
windows.FILE_OPEN,
windows.FILE_DIRECTORY_FILE,
null,
0,
);
switch (rc) {
.SUCCESS=> {
windows.CloseHandle(dest_fd);
returnerror.IsDir;
},
.NOT_A_DIRECTORY,
.OBJECT_NAME_NOT_FOUND,
.OBJECT_PATH_NOT_FOUND,
=>returnerror.AccessDenied,
else=>returnwindows.unexpectedStatus(rc),
}
},- Attempting to rename a directory to the path of an existing file succeeds and turns the destination into a directory. Expected behavior would be to fail with
error.NotDir. I'm not really sure what the solution here would be other than a preemptive check on the attributes of both the files.
I ultimately abandoned this because I don't feel I'm familiar enough with the API's and their edge cases to implement this properly, and I feel like a partial implementation (like just replacing the OpenFile with NtCreateFile to seemingly fix the naive case of renaming a directory) would just create bad/unpredictable silent edge cases that are worse than the current loud failures when renaming a directory (like the file turning into a directory case mentioned above).
I couldn't find much in terms of precedence for implementation of a rename(2)-like thing on Windows. The only somewhat related things I found:
Related to #6354 and #6344
As far as I'm aware, the intention of
std.os.renameW/std.os.renameatWis to emulate the behavior ofrename/renameatbut on Windows. This is currently not the case. From #6354:Some info about what I tried and ultimately abandoned: Swapping out:
zig/lib/std/os.zig
Lines 2066 to 2075 in 2962be8
for
NtCreateFilelike so:fixes the simple case of directories being renamed, but there are some edge cases:
ACCESS_DENIEDduring the rename inNtSetInformationFile. Expected behavior (to matchrenameat) would be returningerror.IsDir. This is achievable by querying the destination path to check if it's a directory, my attempt at this is in this details block:Details
error.NotDir. I'm not really sure what the solution here would be other than a preemptive check on the attributes of both the files.I ultimately abandoned this because I don't feel I'm familiar enough with the API's and their edge cases to implement this properly, and I feel like a partial implementation (like just replacing the
OpenFilewithNtCreateFileto seemingly fix the naive case of renaming a directory) would just create bad/unpredictable silent edge cases that are worse than the current loud failures when renaming a directory (like the file turning into a directory case mentioned above).I couldn't find much in terms of precedence for implementation of a
rename(2)-like thing on Windows. The only somewhat related things I found:IsDir/NotDircases mentioned above (which makes sense, not sure how Win32'sMoveFileWhandles those specific edge cases when not usingMOVEFILE_REPLACE_EXISTINGlike Zig currently does inrenameAbsolute.