[PATCH 0/2] MR11094: cmd: Add wildcard expansion for type command.
Fixes: https://bugs.winehq.org/show_bug.cgi?id=59816 Signed-off-by: Barath Kannan barathrk11@gmail.com -- https://gitlab.winehq.org/wine/wine/-/merge_requests/11094
From: Barath Kannan <barathrk11@gmail.com> Signed-off-by: Barath Kannan <barathrk11@gmail.com> --- programs/cmd/builtins.c | 54 +++++++++++++++++++++++++++++++---------- 1 file changed, 41 insertions(+), 13 deletions(-) diff --git a/programs/cmd/builtins.c b/programs/cmd/builtins.c index 1c9074b75d3..549e17d0655 100644 --- a/programs/cmd/builtins.c +++ b/programs/cmd/builtins.c @@ -30,6 +30,7 @@ #include "wcmd.h" #include <shellapi.h> +#include "shlwapi.h" #include "winternl.h" #include "winioctl.h" #include "ddk/ntifs.h" @@ -3439,6 +3440,10 @@ RETURN_CODE WCMD_type(WCHAR *args) int argno = 0; WCHAR *argN = args; BOOL writeHeaders = FALSE; + WIN32_FIND_DATAW fd; + HANDLE hff = INVALID_HANDLE_VALUE; + BOOL havewildcards = FALSE; + WCHAR *fileNamePart; if (param1[0] == 0x00) { WCMD_output_stderr(WCMD_LoadMessage(WCMD_NOARG)); @@ -3457,23 +3462,46 @@ RETURN_CODE WCMD_type(WCHAR *args) if (!argN) break; - WINE_TRACE("type: Processing arg '%s'\n", wine_dbgstr_w(thisArg)); - hIn = CreateFileW(thisArg, GENERIC_READ, FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE, NULL, OPEN_EXISTING, - FILE_ATTRIBUTE_NORMAL, NULL); - if (hIn == INVALID_HANDLE_VALUE) { + havewildcards = wcspbrk(thisArg, L"*?") ? TRUE : FALSE; + + fileNamePart = PathFindFileNameW(thisArg); + + hff = FindFirstFileW(thisArg, &fd); + + if (hff != INVALID_HANDLE_VALUE) { + do { + lstrcpyW(fileNamePart, fd.cFileName); + WINE_TRACE("type: thisArg: '%s'\n", wine_dbgstr_w(thisArg)); + hIn = CreateFileW(thisArg, GENERIC_READ, FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE, NULL, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, NULL); + if (hIn == INVALID_HANDLE_VALUE) { + + if(fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY && + (!lstrcmpW(fd.cFileName, L".") || !lstrcmpW(fd.cFileName, L".."))) { + WINE_TRACE("Skipping current and previous directories"); + continue; + } + + WCMD_print_error (); + WCMD_output_stderr(WCMD_LoadMessage(WCMD_READFAIL), thisArg); + return errorlevel = ERROR_INVALID_FUNCTION; + } + hOut = GetStdHandle(STD_OUTPUT_HANDLE); + + if (writeHeaders || havewildcards) { + WCMD_output_stderr(L"\n%1\n\n\n", thisArg); + } + + WCMD_copy_loop(hIn, hOut, GetConsoleMode(hIn, &console_mode) || GetConsoleMode(hOut, &console_mode)); + + CloseHandle (hIn); + } while (FindNextFileW(hff, &fd) != 0); + } + else { WCMD_print_error (); WCMD_output_stderr(WCMD_LoadMessage(WCMD_READFAIL), thisArg); return errorlevel = ERROR_INVALID_FUNCTION; } - hOut = GetStdHandle(STD_OUTPUT_HANDLE); - - if (writeHeaders) { - WCMD_output_stderr(L"\n%1\n\n\n", thisArg); - } - - WCMD_copy_loop(hIn, hOut, GetConsoleMode(hIn, &console_mode) || GetConsoleMode(hOut, &console_mode)); - - CloseHandle (hIn); } return errorlevel = return_code; -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/11094
From: Barath Kannan <barathrk11@gmail.com> Signed-off-by: Barath Kannan <barathrk11@gmail.com> --- programs/cmd/tests/test_builtins.bat | 5 +++-- programs/cmd/tests/test_builtins.bat.exp | 11 +++++++---- programs/cmd/tests/test_builtins.cmd | 4 +++- programs/cmd/tests/test_builtins.cmd.exp | 11 +++++++---- 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/programs/cmd/tests/test_builtins.bat b/programs/cmd/tests/test_builtins.bat index 4c3d0e7107e..f34492e8ab6 100644 --- a/programs/cmd/tests/test_builtins.bat +++ b/programs/cmd/tests/test_builtins.bat @@ -107,8 +107,9 @@ call :setError 666 & (type i\dont\exist\at\all.txt &&echo SUCCESS !errorlevel!|| call :setError 666 & (type file* i\dont\exist\at\all.txt &&echo SUCCESS !errorlevel!||echo FAILURE !errorlevel!) echo --- call :setError 666 & (type i\dont\exist\at\all.txt file* &&echo SUCCESS !errorlevel!||echo FAILURE !errorlevel!) -cd .. && rd /q /s foo - +cd .. +call :setError 666 & (type foo\file* &&echo SUCCESS !errorlevel!||echo FAILURE !errorlevel!) +rd /q /s foo echo --- success/failure for COPY command mkdir foo & cd foo echo a > fileA diff --git a/programs/cmd/tests/test_builtins.bat.exp b/programs/cmd/tests/test_builtins.bat.exp index 36e14d5be91..6a94358dfa8 100644 --- a/programs/cmd/tests/test_builtins.bat.exp +++ b/programs/cmd/tests/test_builtins.bat.exp @@ -67,11 +67,14 @@ SUCCESS 1024 FAILURE 1 SUCCESS 0 FAILURE 1 -@todo_wine@a@space@ -@todo_wine@b@space@ -@todo_wine@FAILURE 1 -@todo_wine@--- +a@space@ +b@space@ +FAILURE 1 +--- FAILURE 1 +a@space@ +b@space@ +SUCCESS 0 --- success/failure for COPY command FAILURE 1 SUCCESS 0 diff --git a/programs/cmd/tests/test_builtins.cmd b/programs/cmd/tests/test_builtins.cmd index f65a301d914..afa32f27c68 100644 --- a/programs/cmd/tests/test_builtins.cmd +++ b/programs/cmd/tests/test_builtins.cmd @@ -690,7 +690,9 @@ call :setError 666 & (type i\dont\exist\at\all.txt &&echo SUCCESS !errorlevel!|| call :setError 666 & (type file* i\dont\exist\at\all.txt &&echo SUCCESS !errorlevel!||echo FAILURE !errorlevel!) echo --- call :setError 666 & (type i\dont\exist\at\all.txt file* &&echo SUCCESS !errorlevel!||echo FAILURE !errorlevel!) -cd .. && rd /q /s foo +cd .. +call :setError 666 & (type foo\file* &&echo SUCCESS !errorlevel!||echo FAILURE !errorlevel!) +rd /q /s foo echo --- success/failure for COPY command mkdir foo & cd foo diff --git a/programs/cmd/tests/test_builtins.cmd.exp b/programs/cmd/tests/test_builtins.cmd.exp index 0f6d9bdb494..c640b906a03 100644 --- a/programs/cmd/tests/test_builtins.cmd.exp +++ b/programs/cmd/tests/test_builtins.cmd.exp @@ -586,11 +586,14 @@ SUCCESS 1024 FAILURE 1 SUCCESS 0 FAILURE 1 -@todo_wine@a@space@ -@todo_wine@b@space@ -@todo_wine@FAILURE 1 -@todo_wine@--- +a@space@ +b@space@ FAILURE 1 +--- +FAILURE 1 +a@space@ +b@space@ +SUCCESS 0 --- success/failure for COPY command FAILURE 1 SUCCESS 0 -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/11094
eric pouech (@epo) commented about programs/cmd/builtins.c:
+ + WCMD_print_error (); + WCMD_output_stderr(WCMD_LoadMessage(WCMD_READFAIL), thisArg); + return errorlevel = ERROR_INVALID_FUNCTION; + } + hOut = GetStdHandle(STD_OUTPUT_HANDLE); + + if (writeHeaders || havewildcards) { + WCMD_output_stderr(L"\n%1\n\n\n", thisArg); + } + + WCMD_copy_loop(hIn, hOut, GetConsoleMode(hIn, &console_mode) || GetConsoleMode(hOut, &console_mode)); + + CloseHandle (hIn); + } while (FindNextFileW(hff, &fd) != 0); + } hff isn't closed
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/11094#note_142493
eric pouech (@epo) commented about programs/cmd/builtins.c:
if (!argN) break;
- WINE_TRACE("type: Processing arg '%s'\n", wine_dbgstr_w(thisArg)); - hIn = CreateFileW(thisArg, GENERIC_READ, FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE, NULL, OPEN_EXISTING, - FILE_ATTRIBUTE_NORMAL, NULL); - if (hIn == INVALID_HANDLE_VALUE) { + havewildcards = wcspbrk(thisArg, L"*?") ? TRUE : FALSE; + + fileNamePart = PathFindFileNameW(thisArg); + + hff = FindFirstFileW(thisArg, &fd); + + if (hff != INVALID_HANDLE_VALUE) { + do { + lstrcpyW(fileNamePart, fd.cFileName);
you'd rather want a local buffer to generate path, and check for buffer overflow -- https://gitlab.winehq.org/wine/wine/-/merge_requests/11094#note_142495
eric pouech (@epo) commented about programs/cmd/builtins.c:
if (!argN) break;
- WINE_TRACE("type: Processing arg '%s'\n", wine_dbgstr_w(thisArg)); - hIn = CreateFileW(thisArg, GENERIC_READ, FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE, NULL, OPEN_EXISTING, - FILE_ATTRIBUTE_NORMAL, NULL); - if (hIn == INVALID_HANDLE_VALUE) { + havewildcards = wcspbrk(thisArg, L"*?") ? TRUE : FALSE; + + fileNamePart = PathFindFileNameW(thisArg); + + hff = FindFirstFileW(thisArg, &fd);
this will break access to DOS devices (check out 'type NUL') -- https://gitlab.winehq.org/wine/wine/-/merge_requests/11094#note_142496
eric pouech (@epo) commented about programs/cmd/builtins.c:
- if (hIn == INVALID_HANDLE_VALUE) { + havewildcards = wcspbrk(thisArg, L"*?") ? TRUE : FALSE; + + fileNamePart = PathFindFileNameW(thisArg); + + hff = FindFirstFileW(thisArg, &fd); + + if (hff != INVALID_HANDLE_VALUE) { + do { + lstrcpyW(fileNamePart, fd.cFileName); + WINE_TRACE("type: thisArg: '%s'\n", wine_dbgstr_w(thisArg)); + hIn = CreateFileW(thisArg, GENERIC_READ, FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE, NULL, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, NULL); + if (hIn == INVALID_HANDLE_VALUE) { + + if(fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY && this doesn't match native behavior... all directories are skipped from wild card searches (not just . and ..)
and native prints an error if wild card set only contains directories -- https://gitlab.winehq.org/wine/wine/-/merge_requests/11094#note_142494
eric pouech (@epo) commented about programs/cmd/builtins.c:
+ } + hOut = GetStdHandle(STD_OUTPUT_HANDLE); + + if (writeHeaders || havewildcards) { + WCMD_output_stderr(L"\n%1\n\n\n", thisArg); + } + + WCMD_copy_loop(hIn, hOut, GetConsoleMode(hIn, &console_mode) || GetConsoleMode(hOut, &console_mode)); + + CloseHandle (hIn); + } while (FindNextFileW(hff, &fd) != 0); + } + else { WCMD_print_error (); WCMD_output_stderr(WCMD_LoadMessage(WCMD_READFAIL), thisArg); return errorlevel = ERROR_INVALID_FUNCTION; this doesn't match native behavior: native continues with next arg when wild card set is empty
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/11094#note_142497
eric pouech (@epo) commented about programs/cmd/builtins.c:
int argno = 0; WCHAR *argN = args; BOOL writeHeaders = FALSE; + WIN32_FIND_DATAW fd; + HANDLE hff = INVALID_HANDLE_VALUE; + BOOL havewildcards = FALSE; useless variable (you just need to set writeHeaders to true)
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/11094#note_142498
* please don't open new merge requests; you can just update the first MR with the relevant changes * additional tests should come as first commit in MR so that one can easily review what has been changed (and tests from first MR shall not generate errors, so mark failing tests with @todo_wine@) -- https://gitlab.winehq.org/wine/wine/-/merge_requests/11094#note_142499
On Tue Jun 9 08:23:52 2026 +0000, eric pouech wrote:
you'd rather want a local buffer to generate path, and check for buffer overflow could you elaborate on this? do you mean to say thisArg should be copied to a local buffer(say WCHAR srcpath\[MAX_PATH\]) and to check for buffer overflow upon copying fd.cFileName to the end of srcpath?
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/11094#note_142501
On Tue Jun 9 09:02:46 2026 +0000, Barath Kannan wrote:
could you elaborate on this? do you mean to say thisArg should be copied to a local buffer(say WCHAR srcpath\[MAX_PATH\]) and to check for buffer overflow upon copying fd.cFileName to the end of srcpath? yes mostly, you also need to check that thisArg (in fact up to fileNamePart) fits in MAX_PATH too
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/11094#note_142510
On Tue Jun 9 08:23:54 2026 +0000, eric pouech wrote:
this will break access to DOS devices (check out 'type NUL') the `type NUL` command is present in the `test_builtins.cmd` and `test_builtins.bat` files already, and both give me `SUCCESS 0`. Could this be considered evidence enough for saying that using `FindFirstFileW` does not break access to DOS devices?
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/11094#note_142538
participants (3)
-
Barath Kannan -
Barath Kannan (@barath_kannan) -
eric pouech (@epo)