Reactos
1/*
2 * PROJECT: ReactOS WHERE command
3 * LICENSE: GPL-2.0-or-later (https://spdx.org/licenses/GPL-2.0-or-later)
4 * PURPOSE: Search executable files
5 * COPYRIGHT: Copyright 2021 Katayama Hirofumi MZ <katayama.hirofumi.mz@gmail.com>
6 */
7
8#include <stdlib.h>
9#include <windef.h>
10#include <winbase.h>
11#include <winnls.h>
12#include <strsafe.h>
13#include <conutils.h>
14#include "strlist.h" // strlist_...
15#include "resource.h"
16
17#define FLAG_HELP (1 << 0) // "/?"
18#define FLAG_R (1 << 1) // recursive directory
19#define FLAG_Q (1 << 2) // quiet mode
20#define FLAG_F (1 << 3) // double quote
21#define FLAG_T (1 << 4) // detailed info
22
23static DWORD s_dwFlags = 0;
24static LPWSTR s_pszRecursiveDir = NULL;
25static strlist_t s_patterns = strlist_default;
26static strlist_t s_results = strlist_default;
27static strlist_t s_pathext = strlist_default;
28
29// is it either "." or ".."?
30#define IS_DOTS(pch) \
31 (*(pch) == L'.' && ((pch)[1] == 0 || ((pch)[1] == L'.' && (pch)[2] == 0)))
32
33#define DEFAULT_PATHEXT L".com;.exe;.bat;.cmd"
34
35typedef enum WRET // return code of WHERE command
36{
37 WRET_SUCCESS = 0,
38 WRET_NOT_FOUND = 1,
39 WRET_ERROR = 2
40} WRET;
41
42static VOID WhereError(UINT nID)
43{
44 if (!(s_dwFlags & FLAG_Q)) // not quiet mode?
45 ConResPuts(StdErr, nID);
46}
47
48typedef BOOL (CALLBACK *WHERE_CALLBACK)(LPCWSTR pattern, LPCWSTR path, PWIN32_FIND_DATAW data);
49
50static BOOL
51WhereSearchGeneric(LPCWSTR pattern, LPWSTR path, size_t path_len, BOOL bDir,
52 WHERE_CALLBACK callback)
53{
54 LPWSTR pch;
55 size_t cch;
56 BOOL ret;
57 WIN32_FIND_DATAW data;
58 HANDLE hFind = FindFirstFileExW(path, FindExInfoStandard, &data, FindExSearchNameMatch,
59 NULL, 0);
60 if (hFind == INVALID_HANDLE_VALUE)
61 return TRUE; // not found
62
63 pch = wcsrchr(path, L'\\') + 1;
64 cch = path_len - (pch - path);
65 do
66 {
67 if (bDir != !!(data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
68 continue;
69 if (bDir && IS_DOTS(data.cFileName))
70 continue; // ignore "." and ".."
71 if (data.dwFileAttributes & FILE_ATTRIBUTE_VIRTUAL)
72 continue; // ignore virtual
73
74 StringCchCopyW(pch, cch, data.cFileName); // build full path
75
76 ret = callback(pattern, path, &data);
77 if (!ret) // out of memory
78 break;
79 } while (FindNextFileW(hFind, &data));
80 FindClose(hFind);
81 return ret;
82}
83
84static BOOL CALLBACK WherePrintPath(LPCWSTR pattern, LPCWSTR path, PWIN32_FIND_DATAW data)
85{
86 WCHAR szPath[MAX_PATH + 2], szDate[32], szTime[32];
87 LARGE_INTEGER FileSize;
88 FILETIME ftLocal;
89 SYSTEMTIME st;
90
91 if (strlist_find_i(&s_results, path) >= 0)
92 return TRUE; // already exists
93 if (!strlist_add(&s_results, path))
94 return FALSE; // out of memory
95 if (s_dwFlags & FLAG_Q) // quiet mode?
96 return TRUE;
97
98 if (s_dwFlags & FLAG_T) // print detailed info
99 {
100 // convert date/time
101 FileTimeToLocalFileTime(&data->ftLastWriteTime, &ftLocal);
102 FileTimeToSystemTime(&ftLocal, &st);
103 // get date/time strings
104 GetDateFormatW(LOCALE_USER_DEFAULT, 0, &st, NULL, szDate, _countof(szDate));
105 GetTimeFormatW(LOCALE_USER_DEFAULT, 0, &st, NULL, szTime, _countof(szTime));
106 // set size
107 FileSize.LowPart = data->nFileSizeLow;
108 FileSize.HighPart = data->nFileSizeHigh;
109 // print
110 if (s_dwFlags & FLAG_F) // double quote
111 StringCchPrintfW(szPath, _countof(szPath), L"\"%s\"", path);
112 else
113 StringCchCopyW(szPath, _countof(szPath), path);
114 ConResPrintf(StdOut, IDS_FILE_INFO, FileSize.QuadPart, szDate, szTime, szPath);
115 }
116 else // print path only
117 {
118 if (s_dwFlags & FLAG_F) // double quote
119 ConPrintf(StdOut, L"\"%ls\"\n", path);
120 else
121 ConPrintf(StdOut, L"%ls\n", path);
122 }
123 return TRUE; // success
124}
125
126static BOOL WhereSearchFiles(LPCWSTR pattern, LPCWSTR dir)
127{
128 INT iExt;
129 size_t cch;
130 WCHAR szPath[MAX_PATH];
131 StringCchCopyW(szPath, _countof(szPath), dir);
132 StringCchCatW(szPath, _countof(szPath), L"\\");
133 StringCchCatW(szPath, _countof(szPath), pattern);
134 cch = wcslen(szPath);
135
136 for (iExt = 0; iExt < s_pathext.count; ++iExt)
137 {
138 szPath[cch] = 0; // cut off extension
139 // append extension
140 StringCchCatW(szPath, _countof(szPath), strlist_get_at(&s_pathext, iExt));
141
142 if (!WhereSearchGeneric(pattern, szPath, _countof(szPath), FALSE, WherePrintPath))
143 return FALSE;
144 }
145 return TRUE;
146}
147
148static BOOL WhereSearchRecursive(LPCWSTR pattern, LPCWSTR dir);
149
150static BOOL CALLBACK
151WhereSearchRecursiveCallback(LPCWSTR pattern, LPCWSTR path, PWIN32_FIND_DATAW data)
152{
153 return WhereSearchRecursive(pattern, path);
154}
155
156// FIXME: Too slow. Optimize for speed.
157static BOOL WhereSearchRecursive(LPCWSTR pattern, LPCWSTR dir)
158{
159 WCHAR szPath[MAX_PATH];
160 if (!WhereSearchFiles(pattern, dir))
161 return FALSE; // out of memory
162
163 // build path with wildcard
164 StringCchCopyW(szPath, _countof(szPath), dir);
165 StringCchCatW(szPath, _countof(szPath), L"\\*");
166 return WhereSearchGeneric(pattern, szPath, _countof(szPath), TRUE,
167 WhereSearchRecursiveCallback);
168}
169
170static BOOL WhereSearch(LPCWSTR pattern, strlist_t *dirlist)
171{
172 UINT iDir;
173 for (iDir = 0; iDir < dirlist->count; ++iDir)
174 {
175 if (!WhereSearchFiles(pattern, strlist_get_at(dirlist, iDir)))
176 return FALSE;
177 }
178 return TRUE;
179}
180
181static BOOL WhereGetVariable(LPCWSTR name, LPWSTR *value)
182{
183 DWORD cch = GetEnvironmentVariableW(name, NULL, 0);
184 if (cch == 0) // variable not found
185 {
186 *value = NULL;
187 if (!(s_dwFlags & FLAG_Q)) // not quiet mode?
188 ConResPrintf(StdErr, IDS_BAD_ENVVAR, name);
189 return TRUE; // it is error, but continue the task
190 }
191
192 *value = malloc(cch * sizeof(WCHAR));
193 if (!*value || !GetEnvironmentVariableW(name, *value, cch))
194 {
195 free(*value);
196 *value = NULL;
197 return FALSE; // error
198 }
199 return TRUE;
200}
201
202static BOOL WhereDoOption(DWORD flag, LPCWSTR option)
203{
204 if (s_dwFlags & flag)
205 {
206 ConResPrintf(StdErr, IDS_TOO_MANY, option, 1);
207 ConResPuts(StdErr, IDS_TYPE_HELP);
208 return FALSE;
209 }
210 s_dwFlags |= flag;
211 return TRUE;
212}
213
214static BOOL WhereParseCommandLine(INT argc, WCHAR** argv)
215{
216 INT iArg;
217 for (iArg = 1; iArg < argc; ++iArg)
218 {
219 LPWSTR arg = argv[iArg];
220 if (arg[0] == L'/' || arg[0] == L'-')
221 {
222 if (arg[2] == 0)
223 {
224 switch (towupper(arg[1]))
225 {
226 case L'?':
227 if (!WhereDoOption(FLAG_HELP, L"/?"))
228 return FALSE;
229 continue;
230 case L'F':
231 if (!WhereDoOption(FLAG_F, L"/F"))
232 return FALSE;
233 continue;
234 case L'Q':
235 if (!WhereDoOption(FLAG_Q, L"/Q"))
236 return FALSE;
237 continue;
238 case L'T':
239 if (!WhereDoOption(FLAG_T, L"/T"))
240 return FALSE;
241 continue;
242 case L'R':
243 {
244 if (!WhereDoOption(FLAG_R, L"/R"))
245 return FALSE;
246 if (iArg + 1 < argc)
247 {
248 ++iArg;
249 s_pszRecursiveDir = argv[iArg];
250 continue;
251 }
252 ConResPrintf(StdErr, IDS_WANT_VALUE, L"/R");
253 ConResPuts(StdErr, IDS_TYPE_HELP);
254 return FALSE;
255 }
256 }
257 }
258 ConResPrintf(StdErr, IDS_BAD_ARG, argv[iArg]);
259 ConResPuts(StdErr, IDS_TYPE_HELP);
260 return FALSE;
261 }
262 else // pattern?
263 {
264 if (!strlist_add(&s_patterns, argv[iArg])) // append pattern
265 {
266 ConResPuts(StdErr, IDS_OUTOFMEMORY);
267 return FALSE;
268 }
269 }
270 }
271
272 return TRUE; // success
273}
274
275static BOOL WhereGetPathExt(strlist_t *ext_list)
276{
277 BOOL ret = TRUE;
278 LPWSTR pszPathExt, ext;
279 DWORD cchPathExt = GetEnvironmentVariableW(L"PATHEXT", NULL, 0);
280
281 pszPathExt = (cchPathExt ? malloc(cchPathExt * sizeof(WCHAR)) : str_clone(DEFAULT_PATHEXT));
282 if (!pszPathExt)
283 return FALSE; // out of memory
284
285 if (cchPathExt)
286 GetEnvironmentVariableW(L"PATHEXT", pszPathExt, cchPathExt);
287
288 if (!strlist_add(ext_list, L"")) // add empty extension for normal search
289 {
290 strlist_destroy(ext_list);
291 free(pszPathExt);
292 return FALSE;
293 }
294
295 for (ext = wcstok(pszPathExt, L";"); ext; ext = wcstok(NULL, L";")) // for all extensions
296 {
297 if (!strlist_add(ext_list, ext)) // add extension to ext_list
298 {
299 strlist_destroy(ext_list);
300 ret = FALSE;
301 break;
302 }
303 }
304
305 free(pszPathExt);
306 return ret;
307}
308
309static BOOL WhereFindByDirs(LPCWSTR pattern, LPWSTR dirs)
310{
311 BOOL ret;
312 size_t cch;
313 WCHAR szPath[MAX_PATH];
314 LPWSTR dir, pch;
315 strlist_t dirlist = strlist_default;
316
317 GetCurrentDirectoryW(_countof(szPath), szPath);
318 if (!strlist_add(&dirlist, szPath))
319 return FALSE; // out of memory
320
321 for (dir = wcstok(dirs, L";"); dir; dir = wcstok(NULL, L";"))
322 {
323 if (*dir == L'"') // began from '"'
324 {
325 pch = wcschr(++dir, L'"'); // find '"'
326 if (*pch)
327 *pch = 0; // cut off
328 }
329
330 if (*dir != '\\' && dir[1] != L':')
331 continue; // relative path
332
333 cch = wcslen(dir);
334 if (cch > 0 && dir[cch - 1] == L'\\')
335 dir[cch - 1] = 0; // remove trailing backslash
336
337 if (!strlist_add(&dirlist, dir))
338 {
339 strlist_destroy(&dirlist);
340 return FALSE; // out of memory
341 }
342 }
343
344 ret = WhereSearch(pattern, &dirlist);
345 strlist_destroy(&dirlist);
346 return ret;
347}
348
349static BOOL WhereFindByVar(LPCWSTR pattern, LPCWSTR name)
350{
351 LPWSTR value;
352 BOOL ret = WhereGetVariable(name, &value);
353 if (ret && value)
354 ret = WhereFindByDirs(pattern, value);
355 free(value);
356 return ret;
357}
358
359static BOOL WhereIsRecursiveDirOK(LPCWSTR name)
360{
361 if (wcschr(name, L';') != NULL)
362 {
363 WhereError(IDS_BAD_NAME);
364 return FALSE;
365 }
366 else
367 {
368 DWORD attrs = GetFileAttributesW(name);
369 if (attrs == INVALID_FILE_ATTRIBUTES) // file not found
370 {
371 WhereError(IDS_CANT_FOUND);
372 return FALSE;
373 }
374 if (!(attrs & FILE_ATTRIBUTE_DIRECTORY))
375 {
376 WhereError(IDS_BAD_DIR);
377 return FALSE;
378 }
379 return TRUE;
380 }
381}
382
383static BOOL WhereDoPattern(LPWSTR pattern)
384{
385 BOOL ret;
386 LPWSTR pch = wcsrchr(pattern, L':');
387 if (pch)
388 {
389 *pch++ = 0;
390 if (pattern[0] == L'$') // $env:pattern
391 {
392 if (s_dwFlags & FLAG_R) // recursive?
393 {
394 WhereError(IDS_ENVPAT_WITH_R);
395 return FALSE;
396 }
397 ret = WhereFindByVar(pch, pattern + 1);
398 }
399 else // path:pattern
400 {
401 if (s_dwFlags & FLAG_R) // recursive?
402 {
403 WhereError(IDS_PATHPAT_WITH_R);
404 return FALSE;
405 }
406 if (wcschr(pch, L'\\') != NULL) // found '\\'?
407 {
408 WhereError(IDS_BAD_PATHPAT);
409 return FALSE;
410 }
411 ret = WhereFindByDirs(pch, pattern);
412 }
413 }
414 else if (s_pszRecursiveDir) // recursive
415 {
416 WCHAR szPath[MAX_PATH];
417
418 if (!WhereIsRecursiveDirOK(s_pszRecursiveDir))
419 return FALSE;
420
421 GetFullPathNameW(s_pszRecursiveDir, _countof(szPath), szPath, NULL);
422
423 ret = WhereSearchRecursive(pattern, szPath);
424 }
425 else // otherwise
426 {
427 ret = WhereFindByVar(pattern, L"PATH");
428 }
429
430 if (!ret)
431 WhereError(IDS_OUTOFMEMORY);
432 return ret;
433}
434
435INT wmain(INT argc, WCHAR **argv)
436{
437 typedef BOOL (WINAPI *FN_DISABLE_WOW)(PVOID *);
438 HANDLE hKernel32 = GetModuleHandleA("kernel32");
439 FN_DISABLE_WOW DisableWOW =
440 (FN_DISABLE_WOW)GetProcAddress(hKernel32, "Wow64DisableWow64FsRedirection");
441 DWORD iPattern;
442 WRET ret = WRET_ERROR;
443 PVOID dummy;
444
445 ConInitStdStreams(); // Initialize the Console Standard Streams
446
447 if (!WhereParseCommandLine(argc, argv))
448 goto quit;
449
450 if ((s_dwFlags & FLAG_HELP) || !s_patterns.count)
451 {
452 ConResPuts(StdOut, IDS_USAGE);
453 goto quit;
454 }
455
456 if (DisableWOW)
457 DisableWOW(&dummy);
458
459 if (!WhereGetPathExt(&s_pathext))
460 {
461 WhereError(IDS_OUTOFMEMORY);
462 goto quit;
463 }
464
465 ret = WRET_SUCCESS;
466 for (iPattern = 0; iPattern < s_patterns.count; ++iPattern)
467 {
468 if (!WhereDoPattern(strlist_get_at(&s_patterns, iPattern)))
469 {
470 ret = WRET_ERROR;
471 goto quit;
472 }
473 }
474
475 if (!s_results.count)
476 {
477 WhereError(IDS_NOT_FOUND);
478 ret = WRET_NOT_FOUND;
479 }
480
481quit:
482 strlist_destroy(&s_results);
483 strlist_destroy(&s_patterns);
484 strlist_destroy(&s_pathext);
485 return ret;
486}