From 55060911e4070ba3aa82a88042232786866c7d8a Mon Sep 17 00:00:00 2001 From: Katayama Hirofumi MZ Date: Tue, 11 May 2021 14:37:49 +0900 Subject: [PATCH] [CMDUTILS][WHERE] Implement WHERE command (#3642) WHERE is a Windows command that finds the file location from a executable file name. This PR implements it in ReactOS. CORE-17443 --- base/applications/cmdutils/CMakeLists.txt | 1 + .../cmdutils/where/CMakeLists.txt | 7 + .../applications/cmdutils/where/lang/en-US.rc | 52 ++ base/applications/cmdutils/where/resource.h | 15 + base/applications/cmdutils/where/strlist.h | 66 +++ base/applications/cmdutils/where/where.c | 486 ++++++++++++++++++ base/applications/cmdutils/where/where.rc | 12 + 7 files changed, 639 insertions(+) create mode 100644 base/applications/cmdutils/where/CMakeLists.txt create mode 100644 base/applications/cmdutils/where/lang/en-US.rc create mode 100644 base/applications/cmdutils/where/resource.h create mode 100644 base/applications/cmdutils/where/strlist.h create mode 100644 base/applications/cmdutils/where/where.c create mode 100644 base/applications/cmdutils/where/where.rc diff --git a/base/applications/cmdutils/CMakeLists.txt b/base/applications/cmdutils/CMakeLists.txt index f1cd0b9fd1d..7f8038789c8 100644 --- a/base/applications/cmdutils/CMakeLists.txt +++ b/base/applications/cmdutils/CMakeLists.txt @@ -25,6 +25,7 @@ add_subdirectory(taskkill) add_subdirectory(tasklist) add_subdirectory(timeout) add_subdirectory(tree) +add_subdirectory(where) add_subdirectory(whoami) add_subdirectory(wmic) add_subdirectory(wscript) diff --git a/base/applications/cmdutils/where/CMakeLists.txt b/base/applications/cmdutils/where/CMakeLists.txt new file mode 100644 index 00000000000..790187f06de --- /dev/null +++ b/base/applications/cmdutils/where/CMakeLists.txt @@ -0,0 +1,7 @@ +include_directories(${REACTOS_SOURCE_DIR}/sdk/lib/conutils) + +add_executable(where where.c where.rc) +set_module_type(where win32cui UNICODE) +target_link_libraries(where conutils ${PSEH_LIB}) +add_importlibs(where msvcrt kernel32) +add_cd_file(TARGET where DESTINATION reactos/system32 FOR all) diff --git a/base/applications/cmdutils/where/lang/en-US.rc b/base/applications/cmdutils/where/lang/en-US.rc new file mode 100644 index 00000000000..ce4144fda77 --- /dev/null +++ b/base/applications/cmdutils/where/lang/en-US.rc @@ -0,0 +1,52 @@ +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +STRINGTABLE +BEGIN + IDS_USAGE "Usage: WHERE [options] pattern...\n\ +\n\ +Description:\n\ + Shows the location of the file(s) specified by the pattern(s).\n\ + By default, this tool searches by using the pattern(s) and the paths\n\ + of the PATH environment variable.\n\ +\n\ +Options:\n\ + /F Displays all matched file(s) in double quotes.\n\ + /Q Quiet mode. Doesn't show any files and messages.\n\ + /R dir Starts searching from the specified directory and recursively\n\ + performs the search.\n\ + /T Shows the file size and last modified date of all matched\n\ + files.\n\ + pattern Specifies the pattern to search files. Wildcards * and ? can\n\ + be used. ""$env:pattern"" and ""path:pattern"" formats can also\n\ + be used, where ""env"" is an environment variable and\n\ + the search is done in the paths of the ""env"" environment\n\ + variable. Don't use these formats with /R. The search is also\n\ + performed by adding the extension of the PATHEXT variable to\n\ + the pattern.\n\ + /? Displays this message.\n\ +\n\ +NOTE: This tool returns an error level of 0 if the search was successful,\n\ + 1 if the file was not found, and 2 if there was an error.\n\ +\n\ +Example:\n\ + WHERE myfile*.exe\n\ + WHERE /F /T mspaint\n\ + WHERE $WINDIR:notepad myfile???\n\ + WHERE C:\\ReactOS;C:\\ReactOS\\system32:exp*.exe\n\ + WHERE /R ""C:\\Program Files"" *.dll\n" + + IDS_BAD_ARG "ERROR: Invalid argument - '%ls'.\n" + IDS_NOT_FOUND "INFO: Could not find files for the given pattern(s).\n" + IDS_FILE_INFO "%10I64u %-12ls %-12ls %ls\n" + IDS_WANT_VALUE "ERROR: Value is needed for '%ls'.\n" + IDS_TYPE_HELP "Type ""WHERE /?"" for usage help.\n" + IDS_ENVPAT_WITH_R "ERROR: ""$env:pattern"" cannot be used with /R.\n" + IDS_PATHPAT_WITH_R "ERROR: ""path:pattern"" format cannot be used with /R.\n" + IDS_BAD_PATHPAT "ERROR: Invalid pattern is specified in ""path:pattern"".\n" + IDS_OUTOFMEMORY "ERROR: Out of memory.\n" + IDS_BAD_ENVVAR "ERROR: Environment variable ""%ls"" is not found.\n" + IDS_CANT_FOUND "ERROR: The system could not find the file specified.\n" + IDS_BAD_DIR "ERROR: Invalid directory is specified.\n" + IDS_BAD_NAME "ERROR: The filename, directory name or volume label syntax is wrong.\n" + IDS_TOO_MANY "ERROR: '%ls' option is not allowed more than '%u' time(s).\n" +END diff --git a/base/applications/cmdutils/where/resource.h b/base/applications/cmdutils/where/resource.h new file mode 100644 index 00000000000..a1dc35e828b --- /dev/null +++ b/base/applications/cmdutils/where/resource.h @@ -0,0 +1,15 @@ +#define IDS_USAGE 100 +#define IDS_BAD_ARG 101 +#define IDS_NOT_FOUND 103 +#define IDS_FILE_INFO 104 +#define IDS_WANT_VALUE 106 +#define IDS_TYPE_HELP 107 +#define IDS_ENVPAT_WITH_R 108 +#define IDS_PATHPAT_WITH_R 109 +#define IDS_BAD_PATHPAT 110 +#define IDS_OUTOFMEMORY 111 +#define IDS_BAD_ENVVAR 112 +#define IDS_CANT_FOUND 113 +#define IDS_BAD_DIR 114 +#define IDS_BAD_NAME 115 +#define IDS_TOO_MANY 116 diff --git a/base/applications/cmdutils/where/strlist.h b/base/applications/cmdutils/where/strlist.h new file mode 100644 index 00000000000..c877ff55863 --- /dev/null +++ b/base/applications/cmdutils/where/strlist.h @@ -0,0 +1,66 @@ +/* + * PROJECT: ReactOS WHERE command + * LICENSE: GPL-2.0-or-later (https://spdx.org/licenses/GPL-2.0-or-later) + * PURPOSE: Providing string list + * COPYRIGHT: Copyright 2021 Katayama Hirofumi MZ + */ + +#pragma once + +#define str_clone _wcsdup + +typedef struct strlist_t +{ + LPWSTR *ppsz; + unsigned int count; +} strlist_t; +#define strlist_default { NULL, 0 } + +static inline void strlist_init(strlist_t *plist) +{ + plist->ppsz = NULL; + plist->count = 0; +} + +static inline LPWSTR strlist_get_at(strlist_t *plist, unsigned int i) +{ + return plist->ppsz[i]; +} + +static int strlist_add(strlist_t *plist, LPCWSTR psz) +{ + LPWSTR *ppsz, clone = str_clone(psz); + if (!clone) + return 0; + ppsz = (LPWSTR *)realloc(plist->ppsz, (plist->count + 1) * sizeof(LPWSTR)); + if (!ppsz) + { + free(clone); + return 0; + } + plist->ppsz = ppsz; + plist->ppsz[plist->count] = clone; + ++(plist->count); + return 1; +} + +static void strlist_destroy(strlist_t *plist) +{ + unsigned int i; + for (i = 0; i < plist->count; ++i) + free(plist->ppsz[i]); + plist->count = 0; + free(plist->ppsz); + plist->ppsz = NULL; +} + +static inline int strlist_find_i(strlist_t *plist, LPCWSTR psz) +{ + unsigned int i; + for (i = 0; i < plist->count; ++i) + { + if (_wcsicmp(plist->ppsz[i], psz) == 0) + return i; + } + return -1; +} diff --git a/base/applications/cmdutils/where/where.c b/base/applications/cmdutils/where/where.c new file mode 100644 index 00000000000..f4f68a14944 --- /dev/null +++ b/base/applications/cmdutils/where/where.c @@ -0,0 +1,486 @@ +/* + * PROJECT: ReactOS WHERE command + * LICENSE: GPL-2.0-or-later (https://spdx.org/licenses/GPL-2.0-or-later) + * PURPOSE: Search executable files + * COPYRIGHT: Copyright 2021 Katayama Hirofumi MZ + */ + +#include +#include +#include +#include +#include +#include +#include "strlist.h" // strlist_... +#include "resource.h" + +#define FLAG_HELP (1 << 0) // "/?" +#define FLAG_R (1 << 1) // recursive directory +#define FLAG_Q (1 << 2) // quiet mode +#define FLAG_F (1 << 3) // double quote +#define FLAG_T (1 << 4) // detailed info + +static DWORD s_dwFlags = 0; +static LPWSTR s_pszRecursiveDir = NULL; +static strlist_t s_patterns = strlist_default; +static strlist_t s_results = strlist_default; +static strlist_t s_pathext = strlist_default; + +// is it either "." or ".."? +#define IS_DOTS(pch) \ + (*(pch) == L'.' && ((pch)[1] == 0 || ((pch)[1] == L'.' && (pch)[2] == 0))) + +#define DEFAULT_PATHEXT L".com;.exe;.bat;.cmd" + +typedef enum WRET // return code of WHERE command +{ + WRET_SUCCESS = 0, + WRET_NOT_FOUND = 1, + WRET_ERROR = 2 +} WRET; + +static VOID WhereError(UINT nID) +{ + if (!(s_dwFlags & FLAG_Q)) // not quiet mode? + ConResPuts(StdErr, nID); +} + +typedef BOOL (CALLBACK *WHERE_CALLBACK)(LPCWSTR pattern, LPCWSTR path, PWIN32_FIND_DATAW data); + +static BOOL +WhereSearchGeneric(LPCWSTR pattern, LPWSTR path, size_t path_len, BOOL bDir, + WHERE_CALLBACK callback) +{ + LPWSTR pch; + size_t cch; + BOOL ret; + WIN32_FIND_DATAW data; + HANDLE hFind = FindFirstFileExW(path, FindExInfoStandard, &data, FindExSearchNameMatch, + NULL, 0); + if (hFind == INVALID_HANDLE_VALUE) + return TRUE; // not found + + pch = wcsrchr(path, L'\\') + 1; + cch = path_len - (pch - path); + do + { + if (bDir != !!(data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) + continue; + if (bDir && IS_DOTS(data.cFileName)) + continue; // ignore "." and ".." + if (data.dwFileAttributes & FILE_ATTRIBUTE_VIRTUAL) + continue; // ignore virtual + + StringCchCopyW(pch, cch, data.cFileName); // build full path + + ret = callback(pattern, path, &data); + if (!ret) // out of memory + break; + } while (FindNextFileW(hFind, &data)); + FindClose(hFind); + return ret; +} + +static BOOL CALLBACK WherePrintPath(LPCWSTR pattern, LPCWSTR path, PWIN32_FIND_DATAW data) +{ + WCHAR szPath[MAX_PATH + 2], szDate[32], szTime[32]; + LARGE_INTEGER FileSize; + FILETIME ftLocal; + SYSTEMTIME st; + + if (strlist_find_i(&s_results, path) >= 0) + return TRUE; // already exists + if (!strlist_add(&s_results, path)) + return FALSE; // out of memory + if (s_dwFlags & FLAG_Q) // quiet mode? + return TRUE; + + if (s_dwFlags & FLAG_T) // print detailed info + { + // convert date/time + FileTimeToLocalFileTime(&data->ftLastWriteTime, &ftLocal); + FileTimeToSystemTime(&ftLocal, &st); + // get date/time strings + GetDateFormatW(LOCALE_USER_DEFAULT, 0, &st, NULL, szDate, _countof(szDate)); + GetTimeFormatW(LOCALE_USER_DEFAULT, 0, &st, NULL, szTime, _countof(szTime)); + // set size + FileSize.LowPart = data->nFileSizeLow; + FileSize.HighPart = data->nFileSizeHigh; + // print + if (s_dwFlags & FLAG_F) // double quote + StringCchPrintfW(szPath, _countof(szPath), L"\"%s\"", path); + else + StringCchCopyW(szPath, _countof(szPath), path); + ConResPrintf(StdOut, IDS_FILE_INFO, FileSize.QuadPart, szDate, szTime, szPath); + } + else // print path only + { + if (s_dwFlags & FLAG_F) // double quote + ConPrintf(StdOut, L"\"%ls\"\n", path); + else + ConPrintf(StdOut, L"%ls\n", path); + } + return TRUE; // success +} + +static BOOL WhereSearchFiles(LPCWSTR pattern, LPCWSTR dir) +{ + INT iExt; + size_t cch; + WCHAR szPath[MAX_PATH]; + StringCchCopyW(szPath, _countof(szPath), dir); + StringCchCatW(szPath, _countof(szPath), L"\\"); + StringCchCatW(szPath, _countof(szPath), pattern); + cch = wcslen(szPath); + + for (iExt = 0; iExt < s_pathext.count; ++iExt) + { + szPath[cch] = 0; // cut off extension + // append extension + StringCchCatW(szPath, _countof(szPath), strlist_get_at(&s_pathext, iExt)); + + if (!WhereSearchGeneric(pattern, szPath, _countof(szPath), FALSE, WherePrintPath)) + return FALSE; + } + return TRUE; +} + +static BOOL WhereSearchRecursive(LPCWSTR pattern, LPCWSTR dir); + +static BOOL CALLBACK +WhereSearchRecursiveCallback(LPCWSTR pattern, LPCWSTR path, PWIN32_FIND_DATAW data) +{ + return WhereSearchRecursive(pattern, path); +} + +// FIXME: Too slow. Optimize for speed. +static BOOL WhereSearchRecursive(LPCWSTR pattern, LPCWSTR dir) +{ + WCHAR szPath[MAX_PATH]; + if (!WhereSearchFiles(pattern, dir)) + return FALSE; // out of memory + + // build path with wildcard + StringCchCopyW(szPath, _countof(szPath), dir); + StringCchCatW(szPath, _countof(szPath), L"\\*"); + return WhereSearchGeneric(pattern, szPath, _countof(szPath), TRUE, + WhereSearchRecursiveCallback); +} + +static BOOL WhereSearch(LPCWSTR pattern, strlist_t *dirlist) +{ + UINT iDir; + for (iDir = 0; iDir < dirlist->count; ++iDir) + { + if (!WhereSearchFiles(pattern, strlist_get_at(dirlist, iDir))) + return FALSE; + } + return TRUE; +} + +static BOOL WhereGetVariable(LPCWSTR name, LPWSTR *value) +{ + DWORD cch = GetEnvironmentVariableW(name, NULL, 0); + if (cch == 0) // variable not found + { + *value = NULL; + if (!(s_dwFlags & FLAG_Q)) // not quiet mode? + ConResPrintf(StdErr, IDS_BAD_ENVVAR, name); + return TRUE; // it is error, but continue the task + } + + *value = malloc(cch * sizeof(WCHAR)); + if (!*value || !GetEnvironmentVariableW(name, *value, cch)) + { + free(*value); + *value = NULL; + return FALSE; // error + } + return TRUE; +} + +static BOOL WhereDoOption(DWORD flag, LPCWSTR option) +{ + if (s_dwFlags & flag) + { + ConResPrintf(StdErr, IDS_TOO_MANY, option, 1); + ConResPuts(StdErr, IDS_TYPE_HELP); + return FALSE; + } + s_dwFlags |= flag; + return TRUE; +} + +static BOOL WhereParseCommandLine(INT argc, WCHAR** argv) +{ + INT iArg; + for (iArg = 1; iArg < argc; ++iArg) + { + LPWSTR arg = argv[iArg]; + if (arg[0] == L'/' || arg[0] == L'-') + { + if (arg[2] == 0) + { + switch (towupper(arg[1])) + { + case L'?': + if (!WhereDoOption(FLAG_HELP, L"/?")) + return FALSE; + continue; + case L'F': + if (!WhereDoOption(FLAG_F, L"/F")) + return FALSE; + continue; + case L'Q': + if (!WhereDoOption(FLAG_Q, L"/Q")) + return FALSE; + continue; + case L'T': + if (!WhereDoOption(FLAG_T, L"/T")) + return FALSE; + continue; + case L'R': + { + if (!WhereDoOption(FLAG_R, L"/R")) + return FALSE; + if (iArg + 1 < argc) + { + ++iArg; + s_pszRecursiveDir = argv[iArg]; + continue; + } + ConResPrintf(StdErr, IDS_WANT_VALUE, L"/R"); + ConResPuts(StdErr, IDS_TYPE_HELP); + return FALSE; + } + } + } + ConResPrintf(StdErr, IDS_BAD_ARG, argv[iArg]); + ConResPuts(StdErr, IDS_TYPE_HELP); + return FALSE; + } + else // pattern? + { + if (!strlist_add(&s_patterns, argv[iArg])) // append pattern + { + ConResPuts(StdErr, IDS_OUTOFMEMORY); + return FALSE; + } + } + } + + return TRUE; // success +} + +static BOOL WhereGetPathExt(strlist_t *ext_list) +{ + BOOL ret = TRUE; + LPWSTR pszPathExt, ext; + DWORD cchPathExt = GetEnvironmentVariableW(L"PATHEXT", NULL, 0); + + pszPathExt = (cchPathExt ? malloc(cchPathExt * sizeof(WCHAR)) : str_clone(DEFAULT_PATHEXT)); + if (!pszPathExt) + return FALSE; // out of memory + + if (cchPathExt) + GetEnvironmentVariableW(L"PATHEXT", pszPathExt, cchPathExt); + + if (!strlist_add(ext_list, L"")) // add empty extension for normal search + { + strlist_destroy(ext_list); + free(pszPathExt); + return FALSE; + } + + for (ext = wcstok(pszPathExt, L";"); ext; ext = wcstok(NULL, L";")) // for all extensions + { + if (!strlist_add(ext_list, ext)) // add extension to ext_list + { + strlist_destroy(ext_list); + ret = FALSE; + break; + } + } + + free(pszPathExt); + return ret; +} + +static BOOL WhereFindByDirs(LPCWSTR pattern, LPWSTR dirs) +{ + BOOL ret; + size_t cch; + WCHAR szPath[MAX_PATH]; + LPWSTR dir, pch; + strlist_t dirlist = strlist_default; + + GetCurrentDirectoryW(_countof(szPath), szPath); + if (!strlist_add(&dirlist, szPath)) + return FALSE; // out of memory + + for (dir = wcstok(dirs, L";"); dir; dir = wcstok(NULL, L";")) + { + if (*dir == L'"') // began from '"' + { + pch = wcschr(++dir, L'"'); // find '"' + if (*pch) + *pch = 0; // cut off + } + + if (*dir != '\\' && dir[1] != L':') + continue; // relative path + + cch = wcslen(dir); + if (cch > 0 && dir[cch - 1] == L'\\') + dir[cch - 1] = 0; // remove trailing backslash + + if (!strlist_add(&dirlist, dir)) + { + strlist_destroy(&dirlist); + return FALSE; // out of memory + } + } + + ret = WhereSearch(pattern, &dirlist); + strlist_destroy(&dirlist); + return ret; +} + +static BOOL WhereFindByVar(LPCWSTR pattern, LPCWSTR name) +{ + LPWSTR value; + BOOL ret = WhereGetVariable(name, &value); + if (ret && value) + ret = WhereFindByDirs(pattern, value); + free(value); + return ret; +} + +static BOOL WhereIsRecursiveDirOK(LPCWSTR name) +{ + if (wcschr(name, L';') != NULL) + { + WhereError(IDS_BAD_NAME); + return FALSE; + } + else + { + DWORD attrs = GetFileAttributesW(name); + if (attrs == INVALID_FILE_ATTRIBUTES) // file not found + { + WhereError(IDS_CANT_FOUND); + return FALSE; + } + if (!(attrs & FILE_ATTRIBUTE_DIRECTORY)) + { + WhereError(IDS_BAD_DIR); + return FALSE; + } + return TRUE; + } +} + +static BOOL WhereDoPattern(LPWSTR pattern) +{ + BOOL ret; + LPWSTR pch = wcsrchr(pattern, L':'); + if (pch) + { + *pch++ = 0; + if (pattern[0] == L'$') // $env:pattern + { + if (s_dwFlags & FLAG_R) // recursive? + { + WhereError(IDS_ENVPAT_WITH_R); + return FALSE; + } + ret = WhereFindByVar(pch, pattern + 1); + } + else // path:pattern + { + if (s_dwFlags & FLAG_R) // recursive? + { + WhereError(IDS_PATHPAT_WITH_R); + return FALSE; + } + if (wcschr(pch, L'\\') != NULL) // found '\\'? + { + WhereError(IDS_BAD_PATHPAT); + return FALSE; + } + ret = WhereFindByDirs(pch, pattern); + } + } + else if (s_pszRecursiveDir) // recursive + { + WCHAR szPath[MAX_PATH]; + + if (!WhereIsRecursiveDirOK(s_pszRecursiveDir)) + return FALSE; + + GetFullPathNameW(s_pszRecursiveDir, _countof(szPath), szPath, NULL); + + ret = WhereSearchRecursive(pattern, szPath); + } + else // otherwise + { + ret = WhereFindByVar(pattern, L"PATH"); + } + + if (!ret) + WhereError(IDS_OUTOFMEMORY); + return ret; +} + +INT wmain(INT argc, WCHAR **argv) +{ + typedef BOOL (WINAPI *FN_DISABLE_WOW)(PVOID *); + HANDLE hKernel32 = GetModuleHandleA("kernel32"); + FN_DISABLE_WOW DisableWOW = + (FN_DISABLE_WOW)GetProcAddress(hKernel32, "Wow64DisableWow64FsRedirection"); + DWORD iPattern; + WRET ret = WRET_ERROR; + PVOID dummy; + + ConInitStdStreams(); // Initialize the Console Standard Streams + + if (!WhereParseCommandLine(argc, argv)) + goto quit; + + if ((s_dwFlags & FLAG_HELP) || !s_patterns.count) + { + ConResPuts(StdOut, IDS_USAGE); + goto quit; + } + + if (DisableWOW) + DisableWOW(&dummy); + + if (!WhereGetPathExt(&s_pathext)) + { + WhereError(IDS_OUTOFMEMORY); + goto quit; + } + + ret = WRET_SUCCESS; + for (iPattern = 0; iPattern < s_patterns.count; ++iPattern) + { + if (!WhereDoPattern(strlist_get_at(&s_patterns, iPattern))) + { + ret = WRET_ERROR; + goto quit; + } + } + + if (!s_results.count) + { + WhereError(IDS_NOT_FOUND); + ret = WRET_NOT_FOUND; + } + +quit: + strlist_destroy(&s_results); + strlist_destroy(&s_patterns); + strlist_destroy(&s_pathext); + return ret; +} diff --git a/base/applications/cmdutils/where/where.rc b/base/applications/cmdutils/where/where.rc new file mode 100644 index 00000000000..24452c476b5 --- /dev/null +++ b/base/applications/cmdutils/where/where.rc @@ -0,0 +1,12 @@ +#include +#include "resource.h" + +#define REACTOS_STR_FILE_DESCRIPTION "ReactOS WHERE Command" +#define REACTOS_STR_INTERNAL_NAME "where" +#define REACTOS_STR_ORIGINAL_FILENAME "where.exe" +#include + +#pragma code_page(65001) /* UTF-8 */ +#ifdef LANGUAGE_EN_US + #include "lang/en-US.rc" +#endif