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 ---
Fix handling of '/' root component, as it has no named component, and it must be treated as a filename (not path).
dlls/kernel32/tests/file.c | 4 +- dlls/ntdll/tests/file.c | 4 +- dlls/ntdll/unix/file.c | 70 +++++++++++++++++++++++-- server/fd.c | 104 +++++++++++++++++++++++++------------ server/protocol.def | 2 + 5 files changed, 144 insertions(+), 40 deletions(-)
diff --git a/dlls/kernel32/tests/file.c b/dlls/kernel32/tests/file.c index 8560524..9ca56e1 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 d469b44..49f4e34 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 2f18325..1571f63 100644 --- a/dlls/ntdll/unix/file.c +++ b/dlls/ntdll/unix/file.c @@ -3400,6 +3400,62 @@ NTSTATUS nt_to_unix_file_name( const OBJECT_ATTRIBUTES *attr, char **name_ret, U }
+/*********************************************************************** + * nt_to_unix_file_name_with_actual_case + * + * Same as nt_to_unix_file_name, but additionally return unix file name + * without path, with the actual case from the NT file name. + */ +static NTSTATUS nt_to_unix_file_name_with_actual_case( const OBJECT_ATTRIBUTES *attr, char **name_ret, + char **actual_case_ret, UINT disposition ) +{ + const WCHAR *nt_filename = attr->ObjectName->Buffer; + char *actual_case, *p; + NTSTATUS status; + int len; + + /* strip off trailing backslashes; we also accept '/' for unix namespaces */ + for (len = attr->ObjectName->Length / sizeof(WCHAR); len != 0; len--) + if (nt_filename[len - 1] != '\' && nt_filename[len - 1] != '/') + break; + + /* get the last component */ + for (nt_filename += len; nt_filename != attr->ObjectName->Buffer; nt_filename--) + if (nt_filename[-1] == '\' || nt_filename[-1] == '/') + break; + len = attr->ObjectName->Buffer + len - nt_filename; + + if (!(actual_case = malloc( len * 3 + 1 ))) return STATUS_NO_MEMORY; + + status = nt_to_unix_file_name( attr, name_ret, disposition ); + if (status != STATUS_SUCCESS && status != STATUS_NO_SUCH_FILE) + { + free( actual_case ); + return status; + } + + /* special case for '/' root itself, as it has no named components */ + for (p = *name_ret; *p == '/'; p++) { } + if (!*p) + strcpy( actual_case, "/" ); + else + { + len = ntdll_wcstoumbs( nt_filename, len, actual_case, len * 3, TRUE ); + if (len > 0) + actual_case[len] = 0; + else + { + char *p = strrchr( *name_ret, '/' ); + p = p ? p + 1 : *name_ret; + strcpy( actual_case, p ); + } + } + + *actual_case_ret = actual_case; + return status; +} + + /****************************************************************************** * wine_nt_to_unix_file_name * @@ -4522,8 +4578,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; @@ -4531,7 +4587,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_actual_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 ) @@ -4539,15 +4595,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 ); } @@ -4559,8 +4618,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; @@ -4568,7 +4627,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_actual_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 ) @@ -4576,15 +4635,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 481e9a8..d73ea56 100644 --- a/server/fd.c +++ b/server/fd.c @@ -2488,11 +2488,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, filenamelen; struct inode *inode; struct stat st, st2; - char *name; + int different_case; + char *name, *p;
if (!fd->inode || !fd->unix_name) { @@ -2526,6 +2529,29 @@ static void set_fd_name( struct fd *fd, struct fd *root, const char *nameptr, da name = combined_name; }
+ if (!(p = strrchr( name, '/' ))) p = name; + else if (!*(++p)) + { + /* get the last slash that's not trailing, but treat '/' root as a filename */ + while (p > name && p[-1] == '/') p--; + p[p <= name] = 0; + while (p > name && p[-1] != '/') p--; + } + pathlen = p - name; + filenamelen = strlen( p ); + different_case = (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 )) { @@ -2538,47 +2564,58 @@ 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; + } } } }
+ memcpy( name + pathlen, file_case, caselen ); + name[pathlen + caselen] = 0; + if (create_link) { if (link( fd->unix_name, name )) @@ -2879,16 +2916,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) { @@ -2902,8 +2942,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 9361933..34c2c50 100644 --- a/server/protocol.def +++ b/server/protocol.def @@ -3519,9 +3519,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
Hi,
While running your changed tests, I think I found new failures. Being a bot and all I'm not very good at pattern recognition, so I might be wrong, but could you please double-check?
Full results can be found at: https://testbot.winehq.org/JobDetails.pl?Key=89647
Your paranoid android.
=== debiant2 (32 bit report) ===
ntdll: om.c:2307: Test failed: got 86
=== debiant2 (32 bit WoW report) ===
ntdll: om.c:2307: Test failed: got 89
On 4/30/21 3:58 PM, Gabriel Ivăncescu wrote:
+/***********************************************************************
nt_to_unix_file_name_with_actual_case
- Same as nt_to_unix_file_name, but additionally return unix file name
- without path, with the actual case from the NT file name.
- */
+static NTSTATUS nt_to_unix_file_name_with_actual_case( const OBJECT_ATTRIBUTES *attr, char **name_ret,
char **actual_case_ret, UINT disposition )
+{
- const WCHAR *nt_filename = attr->ObjectName->Buffer;
- char *actual_case, *p;
- NTSTATUS status;
- int len;
- /* strip off trailing backslashes; we also accept '/' for unix namespaces */
- for (len = attr->ObjectName->Length / sizeof(WCHAR); len != 0; len--)
if (nt_filename[len - 1] != '\\' && nt_filename[len - 1] != '/')
break;
- /* get the last component */
- for (nt_filename += len; nt_filename != attr->ObjectName->Buffer; nt_filename--)
if (nt_filename[-1] == '\\' || nt_filename[-1] == '/')
break;
- len = attr->ObjectName->Buffer + len - nt_filename;
- if (!(actual_case = malloc( len * 3 + 1 ))) return STATUS_NO_MEMORY;
- status = nt_to_unix_file_name( attr, name_ret, disposition );
- if (status != STATUS_SUCCESS && status != STATUS_NO_SUCH_FILE)
- {
free( actual_case );
return status;
- }
- /* special case for '/' root itself, as it has no named components */
- for (p = *name_ret; *p == '/'; p++) { }
- if (!*p)
strcpy( actual_case, "/" );
- else
- {
len = ntdll_wcstoumbs( nt_filename, len, actual_case, len * 3, TRUE );
if (len > 0)
actual_case[len] = 0;
else
{
char *p = strrchr( *name_ret, '/' );
p = p ? p + 1 : *name_ret;
strcpy( actual_case, p );
}
- }
- *actual_case_ret = actual_case;
- return status;
+}
I feel like this is more complicated than it needs to be and that it will still miss some cases. For instance, the strrchr is incorrect if name_ret has trailing slashes.
Since 405666b736f7e471e301f051cfbe68bcbef7e0f6 there's checks in nt_to_unix_file_name to prevent names to have more than one trailing slash. However, when the unix name shortcut matches, it may still return it with a trailing slash, while when it does it component by component, it won't.
So in my opinion, to get the case of the last path component in a way that is consistent with the matched unix path, it all should be done in lookup_unix_name instead, as in the attached patch (although I did it quickly and I'm not sure it's completely correct).
The patch doesn't try to normalize the returned paths (stripping the last / when the shortcut is taken), and maybe it would be better to do so (for the part below).
diff --git a/server/fd.c b/server/fd.c index 481e9a8..d73ea56 100644 --- a/server/fd.c +++ b/server/fd.c @@ -2488,11 +2488,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, filenamelen; struct inode *inode; struct stat st, st2;
- char *name;
int different_case;
char *name, *p;
if (!fd->inode || !fd->unix_name) {
@@ -2526,6 +2529,29 @@ static void set_fd_name( struct fd *fd, struct fd *root, const char *nameptr, da name = combined_name; }
- if (!(p = strrchr( name, '/' ))) p = name;
- else if (!*(++p))
- {
/* get the last slash that's not trailing, but treat '/' root as a filename */
while (p > name && p[-1] == '/') p--;
p[p <= name] = 0;
while (p > name && p[-1] != '/') p--;
- }
- pathlen = p - name;
- filenamelen = strlen( p );
- different_case = (filenamelen != caselen || memcmp( p, file_case, caselen ));
Here I'm not completely sure. Looking for the last path component could be made simpler with something like that:
p = name; pathlen = 0; while (*p) if (*p++ == '/' && *p) pathlen = p - name; filenamelen = p - name - pathlen; p = name + pathlen;
But then I don't know what we can assume, and whether we can trust client input to have both name and file_case be consistent together.
Cheers,
Hi Rémi,
On 20/05/2021 14:49, Rémi Bernon wrote:
On 4/30/21 3:58 PM, Gabriel Ivăncescu wrote:
+/***********************************************************************
- * nt_to_unix_file_name_with_actual_case
- Same as nt_to_unix_file_name, but additionally return unix file name
- without path, with the actual case from the NT file name.
- */
+static NTSTATUS nt_to_unix_file_name_with_actual_case( const OBJECT_ATTRIBUTES *attr, char **name_ret, + char **actual_case_ret, UINT disposition ) +{ + const WCHAR *nt_filename = attr->ObjectName->Buffer; + char *actual_case, *p; + NTSTATUS status; + int len;
+ /* strip off trailing backslashes; we also accept '/' for unix namespaces */ + for (len = attr->ObjectName->Length / sizeof(WCHAR); len != 0; len--) + if (nt_filename[len - 1] != '\' && nt_filename[len - 1] != '/') + break;
+ /* get the last component */ + for (nt_filename += len; nt_filename != attr->ObjectName->Buffer; nt_filename--) + if (nt_filename[-1] == '\' || nt_filename[-1] == '/') + break; + len = attr->ObjectName->Buffer + len - nt_filename;
+ if (!(actual_case = malloc( len * 3 + 1 ))) return STATUS_NO_MEMORY;
+ status = nt_to_unix_file_name( attr, name_ret, disposition ); + if (status != STATUS_SUCCESS && status != STATUS_NO_SUCH_FILE) + { + free( actual_case ); + return status; + }
+ /* special case for '/' root itself, as it has no named components */ + for (p = *name_ret; *p == '/'; p++) { } + if (!*p) + strcpy( actual_case, "/" ); + else + { + len = ntdll_wcstoumbs( nt_filename, len, actual_case, len * 3, TRUE ); + if (len > 0) + actual_case[len] = 0; + else + { + char *p = strrchr( *name_ret, '/' ); + p = p ? p + 1 : *name_ret; + strcpy( actual_case, p ); + } + }
+ *actual_case_ret = actual_case; + return status; +}
I feel like this is more complicated than it needs to be and that it will still miss some cases. For instance, the strrchr is incorrect if name_ret has trailing slashes.
Since 405666b736f7e471e301f051cfbe68bcbef7e0f6 there's checks in nt_to_unix_file_name to prevent names to have more than one trailing slash. However, when the unix name shortcut matches, it may still return it with a trailing slash, while when it does it component by component, it won't.
So in my opinion, to get the case of the last path component in a way that is consistent with the matched unix path, it all should be done in lookup_unix_name instead, as in the attached patch (although I did it quickly and I'm not sure it's completely correct).
The patch doesn't try to normalize the returned paths (stripping the last / when the shortcut is taken), and maybe it would be better to do so (for the part below).
Thanks, on retrospect, that does look a lot better. Although it seems slightly off in some corner cases (when the shortcut's ret == 0):
1) For unix root, e.g.: \?\unix\ 2) For a DOS device path with a backslash, e.g.: \?\C:\
In those cases the full path maps to the underlying path with an actual component (/ for root), while the file_case is an empty string.
The simplest fix, IMO, is to simply treat an empty file_case as "the same case" as I had originally. It's literally just a couple extra checks on the server and avoids a lot of such problems. Such situations would simply avoid the case thing altogether.
Without that, we'd have to special case the unix root again (treat it as filename) but also the other root DOS device paths by walking backwards or something. Really not worth the trouble.
It also avoids the string copy when the case fails, removing an extra line of code in ntdll. :-)
diff --git a/server/fd.c b/server/fd.c index 481e9a8..d73ea56 100644 --- a/server/fd.c +++ b/server/fd.c @@ -2488,11 +2488,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, filenamelen; struct inode *inode; struct stat st, st2; - char *name; + int different_case; + char *name, *p; if (!fd->inode || !fd->unix_name) { @@ -2526,6 +2529,29 @@ static void set_fd_name( struct fd *fd, struct fd *root, const char *nameptr, da name = combined_name; } + if (!(p = strrchr( name, '/' ))) p = name; + else if (!*(++p)) + { + /* get the last slash that's not trailing, but treat '/' root as a filename */ + while (p > name && p[-1] == '/') p--; + p[p <= name] = 0; + while (p > name && p[-1] != '/') p--; + } + pathlen = p - name; + filenamelen = strlen( p ); + different_case = (filenamelen != caselen || memcmp( p, file_case, caselen ));
Here I'm not completely sure. Looking for the last path component could be made simpler with something like that:
p = name; pathlen = 0; while (*p) if (*p++ == '/' && *p) pathlen = p - name; filenamelen = p - name - pathlen; p = name + pathlen;
But then I don't know what we can assume, and whether we can trust client input to have both name and file_case be consistent together.
Cheers,
Yeah, that's actually really clever, I like it!
On 5/20/21 4:57 PM, Gabriel Ivăncescu wrote:
Hi Rémi,
On 20/05/2021 14:49, Rémi Bernon wrote:
On 4/30/21 3:58 PM, Gabriel Ivăncescu wrote:
+/***********************************************************************
- * nt_to_unix_file_name_with_actual_case
- Same as nt_to_unix_file_name, but additionally return unix file name
- without path, with the actual case from the NT file name.
- */
+static NTSTATUS nt_to_unix_file_name_with_actual_case( const OBJECT_ATTRIBUTES *attr, char **name_ret, + char **actual_case_ret, UINT disposition ) +{ + const WCHAR *nt_filename = attr->ObjectName->Buffer; + char *actual_case, *p; + NTSTATUS status; + int len;
+ /* strip off trailing backslashes; we also accept '/' for unix namespaces */ + for (len = attr->ObjectName->Length / sizeof(WCHAR); len != 0; len--) + if (nt_filename[len - 1] != '\' && nt_filename[len - 1] != '/') + break;
+ /* get the last component */ + for (nt_filename += len; nt_filename != attr->ObjectName->Buffer; nt_filename--) + if (nt_filename[-1] == '\' || nt_filename[-1] == '/') + break; + len = attr->ObjectName->Buffer + len - nt_filename;
+ if (!(actual_case = malloc( len * 3 + 1 ))) return STATUS_NO_MEMORY;
+ status = nt_to_unix_file_name( attr, name_ret, disposition ); + if (status != STATUS_SUCCESS && status != STATUS_NO_SUCH_FILE) + { + free( actual_case ); + return status; + }
+ /* special case for '/' root itself, as it has no named components */ + for (p = *name_ret; *p == '/'; p++) { } + if (!*p) + strcpy( actual_case, "/" ); + else + { + len = ntdll_wcstoumbs( nt_filename, len, actual_case, len * 3, TRUE ); + if (len > 0) + actual_case[len] = 0; + else + { + char *p = strrchr( *name_ret, '/' ); + p = p ? p + 1 : *name_ret; + strcpy( actual_case, p ); + } + }
+ *actual_case_ret = actual_case; + return status; +}
I feel like this is more complicated than it needs to be and that it will still miss some cases. For instance, the strrchr is incorrect if name_ret has trailing slashes.
Since 405666b736f7e471e301f051cfbe68bcbef7e0f6 there's checks in nt_to_unix_file_name to prevent names to have more than one trailing slash. However, when the unix name shortcut matches, it may still return it with a trailing slash, while when it does it component by component, it won't.
So in my opinion, to get the case of the last path component in a way that is consistent with the matched unix path, it all should be done in lookup_unix_name instead, as in the attached patch (although I did it quickly and I'm not sure it's completely correct).
The patch doesn't try to normalize the returned paths (stripping the last / when the shortcut is taken), and maybe it would be better to do so (for the part below).
Thanks, on retrospect, that does look a lot better. Although it seems slightly off in some corner cases (when the shortcut's ret == 0):
- For unix root, e.g.: \?\unix\
- For a DOS device path with a backslash, e.g.: \?\C:\
In those cases the full path maps to the underlying path with an actual component (/ for root), while the file_case is an empty string.
The simplest fix, IMO, is to simply treat an empty file_case as "the same case" as I had originally. It's literally just a couple extra checks on the server and avoids a lot of such problems. Such situations would simply avoid the case thing altogether.
Without that, we'd have to special case the unix root again (treat it as filename) but also the other root DOS device paths by walking backwards or something. Really not worth the trouble.
It also avoids the string copy when the case fails, removing an extra line of code in ntdll. :-)
I don't know, I feel like having file_case sometimes set and sometimes not, confusing. Especially if it's sometimes set but with the same case for some reason. And checking if the case it the same to discard the result feels also a bit ad-hoc.
Regarding the helper itself, as it takes a case_ret parameter, I would say it's expected to fill it when name_ret it, with the last component original case (and an eventual trailing slash I guess, if name_ret has).
Also note that I added a new helper because I was lazy and didn't want to change everywhere nt_to_unix_file_name is used, but maybe it's not worth a new function.
On 5/20/21 5:39 PM, Rémi Bernon wrote:
say it's expected to fill it when name_ret it, with the last component
s/name_ret it/name_ret is/
On 20/05/2021 18:39, Rémi Bernon wrote:
On 5/20/21 4:57 PM, Gabriel Ivăncescu wrote:
Hi Rémi,
On 20/05/2021 14:49, Rémi Bernon wrote:
On 4/30/21 3:58 PM, Gabriel Ivăncescu wrote:
+/***********************************************************************
- * nt_to_unix_file_name_with_actual_case
- Same as nt_to_unix_file_name, but additionally return unix file
name
- without path, with the actual case from the NT file name.
- */
+static NTSTATUS nt_to_unix_file_name_with_actual_case( const OBJECT_ATTRIBUTES *attr, char **name_ret, + char **actual_case_ret, UINT disposition ) +{ + const WCHAR *nt_filename = attr->ObjectName->Buffer; + char *actual_case, *p; + NTSTATUS status; + int len;
+ /* strip off trailing backslashes; we also accept '/' for unix namespaces */ + for (len = attr->ObjectName->Length / sizeof(WCHAR); len != 0; len--) + if (nt_filename[len - 1] != '\' && nt_filename[len - 1] != '/') + break;
+ /* get the last component */ + for (nt_filename += len; nt_filename != attr->ObjectName->Buffer; nt_filename--) + if (nt_filename[-1] == '\' || nt_filename[-1] == '/') + break; + len = attr->ObjectName->Buffer + len - nt_filename;
+ if (!(actual_case = malloc( len * 3 + 1 ))) return STATUS_NO_MEMORY;
+ status = nt_to_unix_file_name( attr, name_ret, disposition ); + if (status != STATUS_SUCCESS && status != STATUS_NO_SUCH_FILE) + { + free( actual_case ); + return status; + }
+ /* special case for '/' root itself, as it has no named components */ + for (p = *name_ret; *p == '/'; p++) { } + if (!*p) + strcpy( actual_case, "/" ); + else + { + len = ntdll_wcstoumbs( nt_filename, len, actual_case, len * 3, TRUE ); + if (len > 0) + actual_case[len] = 0; + else + { + char *p = strrchr( *name_ret, '/' ); + p = p ? p + 1 : *name_ret; + strcpy( actual_case, p ); + } + }
+ *actual_case_ret = actual_case; + return status; +}
I feel like this is more complicated than it needs to be and that it will still miss some cases. For instance, the strrchr is incorrect if name_ret has trailing slashes.
Since 405666b736f7e471e301f051cfbe68bcbef7e0f6 there's checks in nt_to_unix_file_name to prevent names to have more than one trailing slash. However, when the unix name shortcut matches, it may still return it with a trailing slash, while when it does it component by component, it won't.
So in my opinion, to get the case of the last path component in a way that is consistent with the matched unix path, it all should be done in lookup_unix_name instead, as in the attached patch (although I did it quickly and I'm not sure it's completely correct).
The patch doesn't try to normalize the returned paths (stripping the last / when the shortcut is taken), and maybe it would be better to do so (for the part below).
Thanks, on retrospect, that does look a lot better. Although it seems slightly off in some corner cases (when the shortcut's ret == 0):
- For unix root, e.g.: \?\unix\
- For a DOS device path with a backslash, e.g.: \?\C:\
In those cases the full path maps to the underlying path with an actual component (/ for root), while the file_case is an empty string.
The simplest fix, IMO, is to simply treat an empty file_case as "the same case" as I had originally. It's literally just a couple extra checks on the server and avoids a lot of such problems. Such situations would simply avoid the case thing altogether.
Without that, we'd have to special case the unix root again (treat it as filename) but also the other root DOS device paths by walking backwards or something. Really not worth the trouble.
It also avoids the string copy when the case fails, removing an extra line of code in ntdll. :-)
I don't know, I feel like having file_case sometimes set and sometimes not, confusing. Especially if it's sometimes set but with the same case for some reason. And checking if the case it the same to discard the result feels also a bit ad-hoc.
Regarding the helper itself, as it takes a case_ret parameter, I would say it's expected to fill it when name_ret it, with the last component original case (and an eventual trailing slash I guess, if name_ret has).
Also note that I added a new helper because I was lazy and didn't want to change everywhere nt_to_unix_file_name is used, but maybe it's not worth a new function.
Yeah, it still fills it right now. The only time it doesn't, is in weird cases when it maps to a device name (or the root / dir), or when the allocation fails, which makes sense to me as it uses it as fallback. Note that there's not really any special casing done for this in ntdll, it's implicit based on the code already. Just the server needs to check for it.
It's more along the lines of "don't apply the case check at all" rather than "this has a case but I don't want to send it" thing, if that makes sense.
For the leak: good point, I'll see if a buffer is better, or can just free the buffer if it's easier.