Skip to content

Windows: rename fails for directories #6364

Description

@squeek502

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:

Some info about what I tried and ultimately abandoned: Swapping out:

zig/lib/std/os.zig

Lines 2066 to 2075 in 2962be8

const src_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| return e,
};
defer windows.CloseHandle(src_fd);

for NtCreateFile like so:

    const path_len_bytes = @intCast(u16, old_path_w.len * 2);
    var nt_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)),
    };

    var attr = windows.OBJECT_ATTRIBUTES{
        .Length = @sizeOf(windows.OBJECT_ATTRIBUTES),
        .RootDirectory = if (std.fs.path.isAbsoluteWindowsWTF16(old_path_w)) null else old_dir_fd,
        .Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here.
        .ObjectName = &nt_name,
        .SecurityDescriptor = null,
        .SecurityQualityOfService = null,
    };
    var io: windows.IO_STATUS_BLOCK = undefined;
    var src_fd: windows.HANDLE = undefined;
    var rc = 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 => return error.FileNotFound,
        .INVALID_PARAMETER => unreachable,
        .FILE_IS_A_DIRECTORY => return error.IsDir,
        .NOT_A_DIRECTORY => return error.NotDir,
        else => return windows.unexpectedStatus(rc),
    }
    defer windows.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.
            const dest_path_len_bytes = @intCast(u16, new_path_w.len * 2);
            var dest_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)),
            };

            var dest_attr = windows.OBJECT_ATTRIBUTES{
                .Length = @sizeOf(windows.OBJECT_ATTRIBUTES),
                .RootDirectory = if (std.fs.path.isAbsoluteWindowsWTF16(new_path_w)) null else new_dir_fd,
                .Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here.
                .ObjectName = &dest_nt_name,
                .SecurityDescriptor = null,
                .SecurityQualityOfService = null,
            };
            var dest_io: windows.IO_STATUS_BLOCK = undefined;
            var dest_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);
                    return error.IsDir;
                },
                .NOT_A_DIRECTORY,
                .OBJECT_NAME_NOT_FOUND,
                .OBJECT_PATH_NOT_FOUND,
                => return error.AccessDenied,
                else => return windows.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:

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugObserved behavior contradicts documented or intended behaviorenhancementSolving this issue will likely involve adding new logic or components to the codebase.os-windowsMicrosoft Windowsstandard libraryThis issue involves writing Zig code for the standard library.

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions