From: "Gabriel Ivăncescu" Subject: [PATCH v2 1/2] ntdll: Allow renaming a file/directory to a different casing of itself. Message-Id: <2b11a524f6054353a6c53e0560ee3f2e110adcba.1603889541.git.gabrielopcode@gmail.com> Date: Wed, 28 Oct 2020 14:54:35 +0200 Renaming a file or directory from e.g. foobar to FooBar (or any other casing change) should work, like on Windows, instead of being a no-op. Clobbering an existing file must also respect the new casing. Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=46203 Signed-off-by: Gabriel Ivăncescu --- v2: Handle clobbering existing file with different casing. The implementation I came up with is 100% compatible with what was before, and is very straightforward to decode. We just append the casing filename (without path) to the wineserver data, and add an extra (optional) field `casing_len` which is the length of this appended data. If there's no casing appended, this is zero and the behavior is identical to before. Note that we *only* send the different casing (and thus casing_len != 0) if it's actually different. This is important to preserve atomicity of rename() in wineserver in all cases that don't require this special casing. The only extra time rename() is not atomic is when an existing file is clobbered with a different casing now, since we have to unlink the old file, then rename the new file into the new casing. dlls/ntdll/unix/file.c | 47 +++++++++++++++++++--- server/fd.c | 88 +++++++++++++++++++++++++++++------------- server/protocol.def | 3 +- 3 files changed, 105 insertions(+), 33 deletions(-) diff --git a/dlls/ntdll/unix/file.c b/dlls/ntdll/unix/file.c index d12a3ff..361930c 100644 --- a/dlls/ntdll/unix/file.c +++ b/dlls/ntdll/unix/file.c @@ -4328,6 +4328,7 @@ NTSTATUS WINAPI NtSetInformationFile( HANDLE handle, IO_STATUS_BLOCK *io, if (len >= sizeof(FILE_RENAME_INFORMATION)) { FILE_RENAME_INFORMATION *info = ptr; + size_t unix_len, casing_len = 0; UNICODE_STRING name_str; OBJECT_ATTRIBUTES attr; char *unix_name; @@ -4345,13 +4346,49 @@ NTSTATUS WINAPI NtSetInformationFile( HANDLE handle, IO_STATUS_BLOCK *io, if (io->u.Status != STATUS_SUCCESS && io->u.Status != STATUS_NO_SUCH_FILE) break; + unix_len = strlen(unix_name); + + /* Append the casing of the last component if target exists */ + if (io->u.Status != STATUS_NO_SUCH_FILE) + { + size_t nt_filename_len, pathlen; + const WCHAR *nt_filename; + char *tmp; + + /* name_str is not NUL terminated; look for last \ character */ + for (pathlen = name_str.Length / sizeof(WCHAR); pathlen; pathlen--) + if (name_str.Buffer[pathlen - 1] == '\\') + break; + + nt_filename = name_str.Buffer + pathlen; + nt_filename_len = name_str.Length / sizeof(WCHAR) - pathlen; + + tmp = realloc(unix_name, unix_len + nt_filename_len * 3); + if (tmp) + { + unix_name = tmp; + casing_len = ntdll_wcstoumbs(nt_filename, nt_filename_len, unix_name + unix_len, + nt_filename_len * 3, TRUE); + + /* Only send it if the casing is actually different */ + tmp = strrchr(unix_name, '/'); + tmp = tmp ? tmp + 1 : unix_name; + if (unix_name + unix_len - tmp == casing_len && + !memcmp(tmp, unix_name + unix_len, casing_len)) + casing_len = 0; + + unix_len += casing_len; + } + } + SERVER_START_REQ( set_fd_name_info ) { - req->handle = wine_server_obj_handle( handle ); - req->rootdir = wine_server_obj_handle( attr.RootDirectory ); - req->link = FALSE; - req->replace = info->ReplaceIfExists; - wine_server_add_data( req, unix_name, strlen(unix_name) ); + req->handle = wine_server_obj_handle( handle ); + req->rootdir = wine_server_obj_handle( attr.RootDirectory ); + req->link = FALSE; + req->replace = info->ReplaceIfExists; + req->casing_len = casing_len; + wine_server_add_data( req, unix_name, unix_len ); io->u.Status = wine_server_call( req ); } SERVER_END_REQ; diff --git a/server/fd.c b/server/fd.c index edb59b0..060e713 100644 --- a/server/fd.c +++ b/server/fd.c @@ -2448,7 +2448,8 @@ static void set_fd_disposition( struct fd *fd, int unlink ) /* set new name for the fd */ static void set_fd_name( struct fd *fd, struct fd *root, const char *nameptr, - data_size_t len, int create_link, int replace ) + data_size_t len, unsigned int casing_len, int create_link, + int replace ) { struct inode *inode; struct stat st, st2; @@ -2498,45 +2499,77 @@ static void set_fd_name( struct fd *fd, struct fd *root, const char *nameptr, if (!fstat( fd->unix_fd, &st2 ) && st.st_ino == st2.st_ino && st.st_dev == st2.st_dev) { if (create_link && !replace) set_error( STATUS_OBJECT_NAME_COLLISION ); - free( name ); - return; - } - - if (!replace) - { - set_error( STATUS_OBJECT_NAME_COLLISION ); - goto failed; + if (!casing_len) + { + free( name ); + return; + } } - - /* can't replace directories or special files */ - if (!S_ISREG( st.st_mode )) + else { - set_error( STATUS_ACCESS_DENIED ); - goto failed; - } + if (!replace) + { + set_error( STATUS_OBJECT_NAME_COLLISION ); + goto failed; + } - /* can't replace an opened file */ - if ((inode = get_inode( st.st_dev, st.st_ino, -1 ))) - { - int is_empty = list_empty( &inode->open ); - release_object( inode ); - if (!is_empty) + /* can't replace directories or special files */ + if (!S_ISREG( st.st_mode )) { set_error( STATUS_ACCESS_DENIED ); goto failed; } + + /* can't replace an opened file */ + if ((inode = get_inode( st.st_dev, st.st_ino, -1 ))) + { + int is_empty = list_empty( &inode->open ); + release_object( inode ); + if (!is_empty) + { + set_error( STATUS_ACCESS_DENIED ); + goto failed; + } + } + + /* link() expects that the target doesn't exist */ + /* rename() cannot replace files with directories */ + /* we also have to unlink it if target has different casing */ + if (create_link || S_ISDIR( st2.st_mode ) || casing_len) + { + if (unlink( name )) + { + file_set_error(); + goto failed; + } + } } + } + + /* replace the last component with its actual casing */ + if (casing_len) + { + char *p = strrchr( name, '/' ); + size_t orig_len, path_len; + + p = p ? p + 1 : name; + path_len = p - name; + orig_len = strlen(p); - /* link() expects that the target doesn't exist */ - /* rename() cannot replace files with directories */ - if (create_link || S_ISDIR( st2.st_mode )) + if (orig_len < casing_len) { - if (unlink( name )) + char *new_name = mem_alloc( path_len + casing_len + 1 ); + if (!new_name) { - file_set_error(); + set_error( STATUS_NO_MEMORY ); goto failed; } + memcpy( new_name, name, path_len ); + free( name ); + name = new_name; } + memcpy(name + path_len, nameptr + len, casing_len); + name[path_len + casing_len] = 0; } if (create_link) @@ -2847,7 +2880,8 @@ DECL_HANDLER(set_fd_name_info) if ((fd = get_handle_fd_obj( current->process, req->handle, 0 ))) { - set_fd_name( fd, root_fd, get_req_data(), get_req_data_size(), req->link, req->replace ); + set_fd_name( fd, root_fd, get_req_data(), get_req_data_size() - req->casing_len, + req->casing_len, req->link, req->replace ); release_object( fd ); } if (root_fd) release_object( root_fd ); diff --git a/server/protocol.def b/server/protocol.def index 846d2e1..a5a7168 100644 --- a/server/protocol.def +++ b/server/protocol.def @@ -3499,7 +3499,8 @@ struct handle_info obj_handle_t rootdir; /* root directory */ int link; /* link instead of renaming */ int replace; /* replace an existing file? */ - VARARG(filename,string); /* new file name */ + unsigned int casing_len; /* optional length of filename with actual casing (w/o path) */ + VARARG(filename,string); /* new file name; casing_len chars are appended to this, if any */ @END -- 2.21.0