Renaming a file or directory from e.g. foobar to FooBar (or any other case change) should work, like on Windows, instead of being a no-op. Clobbering an existing file must also respect the new case.
Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=46203 Signed-off-by: Gabriel Ivăncescu gabrielopcode@gmail.com ---
To simplify the code and avoid another trailing slash quirk, the case is not sent if the conversion fails (which keeps current behavior), because it should be very rare in practice so it's not worth the extra complication that doesn't really bring any benefit to begin with.
Note that the server has to check for caselen == 0; it's not due to errors, it's due to the path mapping to a device or unix root directory. In this case, there is no such thing as "last component" because there are no components at all, so obviously there's no case to replace with. Handling the rare error above with the same code is just an implicit side effect without adding any extra code, so I see it as a bonus.
dlls/kernel32/tests/file.c | 4 +- dlls/ntdll/tests/file.c | 4 +- dlls/ntdll/unix/file.c | 72 ++++++++++++++++++++++++-- server/fd.c | 100 +++++++++++++++++++++++++------------ server/protocol.def | 2 + 5 files changed, 142 insertions(+), 40 deletions(-)
diff --git a/dlls/kernel32/tests/file.c b/dlls/kernel32/tests/file.c index ea0a32f..15bfc13 100644 --- a/dlls/kernel32/tests/file.c +++ b/dlls/kernel32/tests/file.c @@ -2040,7 +2040,7 @@ static void test_MoveFileA(void) ok(hfile != INVALID_HANDLE_VALUE, "FindFirstFileA: failed, error %d\n", GetLastError()); if (hfile != INVALID_HANDLE_VALUE) { - todo_wine ok(!lstrcmpA(strrchr(tempdir, '\') + 1, find_data.cFileName), + ok(!lstrcmpA(strrchr(tempdir, '\') + 1, find_data.cFileName), "MoveFile failed to change casing on same file: got %s\n", find_data.cFileName); } CloseHandle(hfile); @@ -2085,7 +2085,7 @@ static void test_MoveFileA(void) ok(hfile != INVALID_HANDLE_VALUE, "FindFirstFileA: failed, error %d\n", GetLastError()); if (hfile != INVALID_HANDLE_VALUE) { - todo_wine ok(!lstrcmpA(strrchr(tempdir, '\') + 1, find_data.cFileName), + ok(!lstrcmpA(strrchr(tempdir, '\') + 1, find_data.cFileName), "MoveFile failed to change casing on same directory: got %s\n", find_data.cFileName); } CloseHandle(hfile); diff --git a/dlls/ntdll/tests/file.c b/dlls/ntdll/tests/file.c index 19ae5f2..c6178d3 100644 --- a/dlls/ntdll/tests/file.c +++ b/dlls/ntdll/tests/file.c @@ -2315,7 +2315,7 @@ static void test_file_link_information(void) ok(handle != INVALID_HANDLE_VALUE, "FindFirstFileW: failed, error %d\n", GetLastError()); if (handle != INVALID_HANDLE_VALUE) { - todo_wine ok(!lstrcmpW(wcsrchr(newpath, '\') + 1, find_data.cFileName), + ok(!lstrcmpW(wcsrchr(newpath, '\') + 1, find_data.cFileName), "Link did not change casing on existing target file: got %s\n", wine_dbgstr_w(find_data.cFileName)); }
@@ -2900,7 +2900,7 @@ static void test_file_link_information(void) ok(handle != INVALID_HANDLE_VALUE, "FindFirstFileW: failed, error %d\n", GetLastError()); if (handle != INVALID_HANDLE_VALUE) { - todo_wine ok(!lstrcmpW(wcsrchr(oldpath, '\') + 1, find_data.cFileName), + ok(!lstrcmpW(wcsrchr(oldpath, '\') + 1, find_data.cFileName), "Link did not change casing on same file: got %s\n", wine_dbgstr_w(find_data.cFileName)); }
diff --git a/dlls/ntdll/unix/file.c b/dlls/ntdll/unix/file.c index 977b0df..c033ffd 100644 --- a/dlls/ntdll/unix/file.c +++ b/dlls/ntdll/unix/file.c @@ -3426,6 +3426,64 @@ NTSTATUS nt_to_unix_file_name( const OBJECT_ATTRIBUTES *attr, char **name_ret, U }
+/*********************************************************************** + * nt_to_unix_file_name_with_case + * + * Same as nt_to_unix_file_name, but additionally return unix file name + * without path, with the actual case from the NT name's last component. + */ +static NTSTATUS nt_to_unix_file_name_with_case( const OBJECT_ATTRIBUTES *attr, char **name_ret, + char **case_ret, UINT disposition ) +{ + const WCHAR *p, *nt_filename = attr->ObjectName->Buffer; + int len = attr->ObjectName->Length / sizeof(WCHAR); + NTSTATUS status; + char *file_case; + + status = nt_to_unix_file_name( attr, name_ret, disposition ); + if (status != STATUS_SUCCESS && status != STATUS_NO_SUCH_FILE) + return status; + + /* skip the device and prefix (allow slashes for unix namespace) */ + if (!attr->RootDirectory) + { + int pos = get_dos_prefix_len( attr->ObjectName ); + while (pos < len) + { + WCHAR c = nt_filename[pos++]; + if (c == '\' || c == '/') break; + } + nt_filename += pos; + len -= pos; + } + + /* strip off trailing backslashes */ + for (; len; len--) + if (nt_filename[len - 1] != '\' && nt_filename[len - 1] != '/') + break; + + /* get the last component */ + for (p = nt_filename + len; p != nt_filename; p--) + if (p[-1] == '\' || p[-1] == '/') + break; + len -= p - nt_filename; + nt_filename = p; + + if (!(file_case = malloc( len * 3 + 1 ))) + { + free( *name_ret ); + return STATUS_NO_MEMORY; + } + + len = ntdll_wcstoumbs( nt_filename, len, file_case, len * 3, TRUE ); + if (len < 0 || len > MAX_DIR_ENTRY_LEN) len = 0; + file_case[len] = 0; + + *case_ret = file_case; + return status; +} + + /****************************************************************************** * wine_nt_to_unix_file_name * @@ -4532,8 +4590,8 @@ NTSTATUS WINAPI NtSetInformationFile( HANDLE handle, IO_STATUS_BLOCK *io, { FILE_RENAME_INFORMATION *info = ptr; UNICODE_STRING name_str, redir; + char *unix_name, *file_case; OBJECT_ATTRIBUTES attr; - char *unix_name;
name_str.Buffer = info->FileName; name_str.Length = info->FileNameLength; @@ -4541,7 +4599,7 @@ NTSTATUS WINAPI NtSetInformationFile( HANDLE handle, IO_STATUS_BLOCK *io, InitializeObjectAttributes( &attr, &name_str, OBJ_CASE_INSENSITIVE, info->RootDirectory, NULL ); get_redirect( &attr, &redir );
- io->u.Status = nt_to_unix_file_name( &attr, &unix_name, FILE_OPEN_IF ); + io->u.Status = nt_to_unix_file_name_with_case( &attr, &unix_name, &file_case, FILE_OPEN_IF ); if (io->u.Status == STATUS_SUCCESS || io->u.Status == STATUS_NO_SUCH_FILE) { SERVER_START_REQ( set_fd_name_info ) @@ -4549,15 +4607,18 @@ NTSTATUS WINAPI NtSetInformationFile( HANDLE handle, IO_STATUS_BLOCK *io, req->handle = wine_server_obj_handle( handle ); req->rootdir = wine_server_obj_handle( attr.RootDirectory ); req->namelen = attr.ObjectName->Length; + req->caselen = strlen( file_case ); req->link = FALSE; req->replace = info->ReplaceIfExists; wine_server_add_data( req, attr.ObjectName->Buffer, attr.ObjectName->Length ); + wine_server_add_data( req, file_case, req->caselen ); wine_server_add_data( req, unix_name, strlen(unix_name) ); io->u.Status = wine_server_call( req ); } SERVER_END_REQ;
free( unix_name ); + free( file_case ); } free( redir.Buffer ); } @@ -4569,8 +4630,8 @@ NTSTATUS WINAPI NtSetInformationFile( HANDLE handle, IO_STATUS_BLOCK *io, { FILE_LINK_INFORMATION *info = ptr; UNICODE_STRING name_str, redir; + char *unix_name, *file_case; OBJECT_ATTRIBUTES attr; - char *unix_name;
name_str.Buffer = info->FileName; name_str.Length = info->FileNameLength; @@ -4578,7 +4639,7 @@ NTSTATUS WINAPI NtSetInformationFile( HANDLE handle, IO_STATUS_BLOCK *io, InitializeObjectAttributes( &attr, &name_str, OBJ_CASE_INSENSITIVE, info->RootDirectory, NULL ); get_redirect( &attr, &redir );
- io->u.Status = nt_to_unix_file_name( &attr, &unix_name, FILE_OPEN_IF ); + io->u.Status = nt_to_unix_file_name_with_case( &attr, &unix_name, &file_case, FILE_OPEN_IF ); if (io->u.Status == STATUS_SUCCESS || io->u.Status == STATUS_NO_SUCH_FILE) { SERVER_START_REQ( set_fd_name_info ) @@ -4586,15 +4647,18 @@ NTSTATUS WINAPI NtSetInformationFile( HANDLE handle, IO_STATUS_BLOCK *io, req->handle = wine_server_obj_handle( handle ); req->rootdir = wine_server_obj_handle( attr.RootDirectory ); req->namelen = attr.ObjectName->Length; + req->caselen = strlen( file_case ); req->link = TRUE; req->replace = info->ReplaceIfExists; wine_server_add_data( req, attr.ObjectName->Buffer, attr.ObjectName->Length ); + wine_server_add_data( req, file_case, req->caselen ); wine_server_add_data( req, unix_name, strlen(unix_name) ); io->u.Status = wine_server_call( req ); } SERVER_END_REQ;
free( unix_name ); + free( file_case ); } free( redir.Buffer ); } diff --git a/server/fd.c b/server/fd.c index b953da2..0cb7729 100644 --- a/server/fd.c +++ b/server/fd.c @@ -2502,11 +2502,14 @@ 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, - struct unicode_str nt_name, int create_link, int replace ) + const char *file_case, data_size_t caselen, struct unicode_str nt_name, + int create_link, int replace ) { + size_t pathlen = 0, filenamelen; struct inode *inode; struct stat st, st2; - char *name; + int different_case; + char *name, *p;
if (!fd->inode || !fd->unix_name) { @@ -2540,6 +2543,22 @@ static void set_fd_name( struct fd *fd, struct fd *root, const char *nameptr, da name = combined_name; }
+ for (p = name; *p;) if (*p++ == '/' && *p) pathlen = p - name; + filenamelen = p - name - pathlen; + for (p = name + pathlen; filenamelen && p[filenamelen - 1] == '/'; filenamelen--) { } + different_case = caselen && (filenamelen != caselen || memcmp( p, file_case, caselen )); + + if (filenamelen < caselen) + { + p = realloc( name, pathlen + caselen + 1 ); + if (!p) + { + set_error( STATUS_NO_MEMORY ); + goto failed; + } + name = p; + } + /* when creating a hard link, source cannot be a dir */ if (create_link && !fstat( fd->unix_fd, &st ) && S_ISDIR( st.st_mode )) { @@ -2552,47 +2571,61 @@ static void set_fd_name( struct fd *fd, struct fd *root, const char *nameptr, da 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 (!different_case) + { + free( name ); + return; + }
- if (!replace) - { - set_error( STATUS_OBJECT_NAME_COLLISION ); - goto failed; + /* creating a link with a different case on itself renames the file */ + create_link = 0; } - - /* 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; } - }
- /* link() expects that the target doesn't exist */ - /* rename() cannot replace files with directories */ - if (create_link || S_ISDIR( st2.st_mode )) - { - if (unlink( name )) + /* can't replace an opened file */ + if ((inode = get_inode( st.st_dev, st.st_ino, -1 ))) { - file_set_error(); - goto failed; + 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 */ + if (create_link || S_ISDIR( st2.st_mode ) || different_case) + { + if (unlink( name )) + { + file_set_error(); + goto failed; + } } } }
+ if (different_case) + { + memcpy( name + pathlen, file_case, caselen ); + name[pathlen + caselen] = 0; + } + if (create_link) { if (link( fd->unix_name, name )) @@ -2929,16 +2962,19 @@ DECL_HANDLER(set_fd_disp_info) /* set fd name information */ DECL_HANDLER(set_fd_name_info) { + const char *fullname, *file_case; struct fd *fd, *root_fd = NULL; struct unicode_str nt_name;
- if (req->namelen > get_req_data_size()) + if (req->namelen > get_req_data_size() || get_req_data_size() - req->namelen < req->caselen) { set_error( STATUS_INVALID_PARAMETER ); return; } nt_name.str = get_req_data(); nt_name.len = (req->namelen / sizeof(WCHAR)) * sizeof(WCHAR); + file_case = (const char *)get_req_data() + req->namelen; + fullname = file_case + req->caselen;
if (req->rootdir) { @@ -2952,8 +2988,8 @@ DECL_HANDLER(set_fd_name_info)
if ((fd = get_handle_fd_obj( current->process, req->handle, 0 ))) { - set_fd_name( fd, root_fd, (const char *)get_req_data() + req->namelen, - get_req_data_size() - req->namelen, nt_name, req->link, req->replace ); + set_fd_name( fd, root_fd, fullname, (const char *)get_req_data() + get_req_data_size() - fullname, + file_case, req->caselen, nt_name, 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 b4049eb..d546226 100644 --- a/server/protocol.def +++ b/server/protocol.def @@ -3537,9 +3537,11 @@ struct handle_info obj_handle_t handle; /* handle to a file or directory */ obj_handle_t rootdir; /* root directory */ data_size_t namelen; /* length of NT name in bytes */ + data_size_t caselen; /* length of the actual case filename */ int link; /* link instead of renaming */ int replace; /* replace an existing file? */ VARARG(name,unicode_str,namelen); /* NT name */ + VARARG(actual_case,string,caselen); /* new file name's actual case (without path) */ VARARG(filename,string); /* new file name */ @END
Gabriel Ivăncescu gabrielopcode@gmail.com writes:
@@ -3426,6 +3426,64 @@ NTSTATUS nt_to_unix_file_name( const OBJECT_ATTRIBUTES *attr, char **name_ret, U }
+/***********************************************************************
nt_to_unix_file_name_with_case
- Same as nt_to_unix_file_name, but additionally return unix file name
- without path, with the actual case from the NT name's last component.
- */
+static NTSTATUS nt_to_unix_file_name_with_case( const OBJECT_ATTRIBUTES *attr, char **name_ret,
char **case_ret, UINT disposition )
I still don't see any reason for this to be a wrapper around nt_to_unix_file_name(). What it does is unrelated, it should be a separate helper function.
On 27/07/2021 23:31, Alexandre Julliard wrote:
Gabriel Ivăncescu gabrielopcode@gmail.com writes:
@@ -3426,6 +3426,64 @@ NTSTATUS nt_to_unix_file_name( const OBJECT_ATTRIBUTES *attr, char **name_ret, U }
+/***********************************************************************
nt_to_unix_file_name_with_case
- Same as nt_to_unix_file_name, but additionally return unix file name
- without path, with the actual case from the NT name's last component.
- */
+static NTSTATUS nt_to_unix_file_name_with_case( const OBJECT_ATTRIBUTES *attr, char **name_ret,
char **case_ret, UINT disposition )
I still don't see any reason for this to be a wrapper around nt_to_unix_file_name(). What it does is unrelated, it should be a separate helper function.
Sure, but do you mean just the name? Or what do you want the helper function to do specifically?
Gabriel Ivăncescu gabrielopcode@gmail.com writes:
On 27/07/2021 23:31, Alexandre Julliard wrote:
Gabriel Ivăncescu gabrielopcode@gmail.com writes:
@@ -3426,6 +3426,64 @@ NTSTATUS nt_to_unix_file_name( const OBJECT_ATTRIBUTES *attr, char **name_ret, U }
+/***********************************************************************
nt_to_unix_file_name_with_case
- Same as nt_to_unix_file_name, but additionally return unix file name
- without path, with the actual case from the NT name's last component.
- */
+static NTSTATUS nt_to_unix_file_name_with_case( const OBJECT_ATTRIBUTES *attr, char **name_ret,
char **case_ret, UINT disposition )
I still don't see any reason for this to be a wrapper around nt_to_unix_file_name(). What it does is unrelated, it should be a separate helper function.
Sure, but do you mean just the name? Or what do you want the helper function to do specifically?
Simply return the last component of the input path. There's no reason for the helper to call nt_to_unix_file_name().