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 
23 static DWORD s_dwFlags = 0;
24 static LPWSTR s_pszRecursiveDir = NULL;
25 static strlist_t s_patterns = strlist_default;
26 static strlist_t s_results = strlist_default;
27 static 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 
35 typedef enum WRET // return code of WHERE command
36 {
37     WRET_SUCCESS = 0,
38     WRET_NOT_FOUND = 1,
39     WRET_ERROR = 2
40 } WRET;
41 
42 static VOID WhereError(UINT nID)
43 {
44     if (!(s_dwFlags & FLAG_Q)) // not quiet mode?
45         ConResPuts(StdErr, nID);
46 }
47 
48 typedef BOOL (CALLBACK *WHERE_CALLBACK)(LPCWSTR pattern, LPCWSTR path, PWIN32_FIND_DATAW data);
49 
50 static BOOL
51 WhereSearchGeneric(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 
84 static 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 
126 static 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 
148 static BOOL WhereSearchRecursive(LPCWSTR pattern, LPCWSTR dir);
149 
150 static BOOL CALLBACK
151 WhereSearchRecursiveCallback(LPCWSTR pattern, LPCWSTR path, PWIN32_FIND_DATAW data)
152 {
153     return WhereSearchRecursive(pattern, path);
154 }
155 
156 // FIXME: Too slow. Optimize for speed.
157 static 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 
170 static 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 
181 static 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 
202 static 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 
214 static 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 
275 static 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 
309 static 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 
349 static 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 
359 static 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 
383 static 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 
435 INT 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 
481 quit:
482     strlist_destroy(&s_results);
483     strlist_destroy(&s_patterns);
484     strlist_destroy(&s_pathext);
485     return ret;
486 }
487