1 /*
2  * PROJECT:     ReactOS Attrib Command
3  * LICENSE:     GPL-2.0-or-later (https://spdx.org/licenses/GPL-2.0-or-later)
4  * PURPOSE:     Displays or changes file attributes recursively.
5  * COPYRIGHT:   Copyright 1998-2019 Eric Kohl <eric.kohl@reactos.org>
6  *              Copyright 2021 Doug Lyons <douglyons@douglyons.com>
7  *              Copyright 2021-2023 Hermès Bélusca-Maïto <hermes.belusca-maito@reactos.org>
8  */
9 
10 #include <stdio.h>
11 #include <stdlib.h>
12 
13 #include <windef.h>
14 #include <winbase.h>
15 #include <wincon.h>
16 #include <winuser.h>
17 #include <strsafe.h>
18 
19 #include <conutils.h>
20 
21 #include "resource.h"
22 
23 /* Enable to support extended attributes.
24  * See https://ss64.com/nt/attrib.html for an exhaustive list. */
25 // TODO: If you enable this, translations need to be updated as well!
26 //#define EXTENDED_ATTRIBUTES
27 
28 #define ALL_FILES_PATTERN   L"*.*" // It may also be possible to use L"*" (shorter)
29 
30 CON_SCREEN StdOutScreen = INIT_CON_SCREEN(StdOut);
31 
32 static VOID
ErrorMessage(_In_ DWORD dwErrorCode,_In_opt_ PCWSTR pszMsg,...)33 ErrorMessage(
34     _In_ DWORD dwErrorCode,
35     _In_opt_ PCWSTR pszMsg,
36     ...)
37 {
38     INT Len;
39     va_list arg_ptr;
40 
41     if (dwErrorCode == ERROR_SUCCESS)
42         return;
43 
44     va_start(arg_ptr, pszMsg);
45     Len = ConMsgPrintfV(StdErr,
46                         FORMAT_MESSAGE_FROM_SYSTEM,
47                         NULL,
48                         dwErrorCode,
49                         LANG_USER_DEFAULT,
50                         &arg_ptr);
51     va_end(arg_ptr);
52 
53     /* Fall back just in case the error is not defined */
54     if (Len <= 0)
55         ConResPrintf(StdErr, STRING_CONSOLE_ERROR, dwErrorCode);
56 
57     /* Display the extra optional message if necessary */
58     if (pszMsg)
59         ConPrintf(StdErr, L"  %s\n", pszMsg);
60 }
61 
62 
63 /**
64  * @brief   Displays attributes for the given file.
65  * @return  Always TRUE (success).
66  **/
67 static BOOL
PrintAttributes(_In_ PWIN32_FIND_DATAW pFindData,_In_ PCWSTR pszFullName,_Inout_opt_ PVOID Context)68 PrintAttributes(
69     _In_ PWIN32_FIND_DATAW pFindData,
70     _In_ PCWSTR pszFullName,
71     _Inout_opt_ PVOID Context)
72 {
73     DWORD dwAttributes = pFindData->dwFileAttributes;
74 
75     UNREFERENCED_PARAMETER(Context);
76 
77     ConPrintf(StdOut,
78 #ifdef EXTENDED_ATTRIBUTES
79               L"%c  %c%c%c  %c    %s\n",
80 #else
81               L"%c  %c%c%c     %s\n",
82 #endif
83               (dwAttributes & FILE_ATTRIBUTE_ARCHIVE)  ? L'A' : L' ',
84               (dwAttributes & FILE_ATTRIBUTE_SYSTEM)   ? L'S' : L' ',
85               (dwAttributes & FILE_ATTRIBUTE_HIDDEN)   ? L'H' : L' ',
86               (dwAttributes & FILE_ATTRIBUTE_READONLY) ? L'R' : L' ',
87 #ifdef EXTENDED_ATTRIBUTES
88               (dwAttributes & FILE_ATTRIBUTE_NOT_CONTENT_INDEXED) ? L'I' : L' ',
89 #endif
90               pszFullName);
91 
92     return TRUE;
93 }
94 
95 typedef struct _ATTRIBS_MASKS
96 {
97     DWORD dwMask;
98     DWORD dwAttrib;
99 } ATTRIBS_MASKS, *PATTRIBS_MASKS;
100 
101 /**
102  * @brief   Changes attributes for the given file.
103  * @return  TRUE if anything changed, FALSE otherwise.
104  **/
105 static BOOL
ChangeAttributes(_In_ PWIN32_FIND_DATAW pFindData,_In_ PCWSTR pszFullName,_Inout_opt_ PVOID Context)106 ChangeAttributes(
107     _In_ PWIN32_FIND_DATAW pFindData,
108     _In_ PCWSTR pszFullName,
109     _Inout_opt_ PVOID Context)
110 {
111     PATTRIBS_MASKS AttribsMasks = (PATTRIBS_MASKS)Context;
112     DWORD dwAttributes;
113 
114     dwAttributes = ((pFindData->dwFileAttributes & ~AttribsMasks->dwMask) | AttribsMasks->dwAttrib);
115     return SetFileAttributesW(pszFullName, dwAttributes);
116 }
117 
118 
119 #define ENUM_RECURSE        0x01
120 #define ENUM_DIRECTORIES    0x02
121 
122 typedef BOOL
123 (*PENUMFILES_CALLBACK)(
124     _In_ PWIN32_FIND_DATAW pFindData,
125     _In_ PCWSTR pszFullName,
126     _Inout_opt_ PVOID Context);
127 
128 typedef struct _ENUMFILES_CTX
129 {
130     /* Fixed data */
131     _In_ PCWSTR FileName;
132     _In_ DWORD Flags;
133 
134     /* Callback invoked on each enumerated file/directory */
135     _In_ PENUMFILES_CALLBACK Callback;
136     _In_ PVOID Context;
137 
138     /* Dynamic data */
139     WIN32_FIND_DATAW findData;
140     ULONG uReparseLevel;
141 
142     /* The full path buffer the function will act recursively */
143     // PWSTR FullPath; // Use a relocated buffer once long paths become supported!
144     size_t cchBuffer; // Buffer size
145     WCHAR FullPathBuffer[MAX_PATH + _countof("\\" ALL_FILES_PATTERN)];
146 
147 } ENUMFILES_CTX, *PENUMFILES_CTX;
148 
149 /* Returns TRUE if anything is done, FALSE otherwise */
150 static BOOL
EnumFilesWorker(_Inout_ PENUMFILES_CTX EnumCtx,_Inout_ _off_t offFilePart)151 EnumFilesWorker(
152     _Inout_ PENUMFILES_CTX EnumCtx,
153     _Inout_ _off_t offFilePart) // Offset to the file name inside FullPathBuffer
154 {
155     BOOL bFound = FALSE;
156     HRESULT hRes;
157     HANDLE hFind;
158     PWSTR findFileName = EnumCtx->findData.cFileName;
159     PWSTR pFilePart = EnumCtx->FullPathBuffer + offFilePart;
160     size_t cchRemaining = EnumCtx->cchBuffer - offFilePart;
161 
162     /* Recurse over all subdirectories */
163     if (EnumCtx->Flags & ENUM_RECURSE)
164     {
165         /* Append '*.*' */
166         hRes = StringCchCopyW(pFilePart, cchRemaining, ALL_FILES_PATTERN);
167         if (hRes != S_OK)
168         {
169             if (hRes == STRSAFE_E_INSUFFICIENT_BUFFER)
170             {
171                 // TODO: If this fails, try to reallocate EnumCtx->FullPathBuffer by
172                 // increasing its size by _countof(EnumCtx->findData.cFileName) + 1
173                 // to satisfy this copy, as well as the one made in the loop below.
174             }
175             // else
176             ConPrintf(StdErr, L"Directory level too deep: %s\n", EnumCtx->FullPathBuffer);
177             return FALSE;
178         }
179 
180         hFind = FindFirstFileW(EnumCtx->FullPathBuffer, &EnumCtx->findData);
181         if (hFind == INVALID_HANDLE_VALUE)
182         {
183             DWORD Error = GetLastError();
184             if ((Error != ERROR_DIRECTORY) &&
185                 (Error != ERROR_SHARING_VIOLATION) &&
186                 (Error != ERROR_FILE_NOT_FOUND))
187             {
188                 ErrorMessage(Error, EnumCtx->FullPathBuffer);
189             }
190             return FALSE;
191         }
192 
193         do
194         {
195             BOOL bIsReparse;
196             size_t offNewFilePart;
197 
198             if (!(EnumCtx->findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
199                 continue;
200 
201             if (!wcscmp(findFileName, L".") || !wcscmp(findFileName, L".."))
202                 continue;
203 
204             /* Allow at most 2 levels of reparse points / symbolic links */
205             bIsReparse = !!(EnumCtx->findData.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT);
206             if (bIsReparse)
207             {
208                 if (EnumCtx->uReparseLevel < 2)
209                     EnumCtx->uReparseLevel++;
210                 else
211                     continue;
212             }
213 
214             hRes = StringCchPrintfExW(pFilePart, cchRemaining,
215                                       NULL, &offNewFilePart, 0,
216                                       L"%s\\", findFileName);
217             /* Offset to the new file name part */
218             offNewFilePart = EnumCtx->cchBuffer - offNewFilePart;
219 
220             bFound |= EnumFilesWorker(EnumCtx, offNewFilePart);
221 
222             /* Recalculate the file part pointer and the number of characters
223              * remaining: the buffer may have been enlarged and relocated. */
224             pFilePart = EnumCtx->FullPathBuffer + offFilePart;
225             cchRemaining = EnumCtx->cchBuffer - offFilePart;
226 
227             /* If we went through a reparse point / symbolic link, decrease level */
228             if (bIsReparse)
229                 EnumCtx->uReparseLevel--;
230         }
231         while (FindNextFileW(hFind, &EnumCtx->findData));
232         FindClose(hFind);
233     }
234 
235     /* Append the file name pattern to search for */
236     hRes = StringCchCopyW(pFilePart, cchRemaining, EnumCtx->FileName);
237 
238     /* Search in the current directory */
239     hFind = FindFirstFileW(EnumCtx->FullPathBuffer, &EnumCtx->findData);
240     if (hFind == INVALID_HANDLE_VALUE)
241         return bFound;
242 
243     do
244     {
245         BOOL bIsDir = !!(EnumCtx->findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY);
246         BOOL bExactMatch = (_wcsicmp(findFileName, EnumCtx->FileName) == 0);
247 
248         if (bIsDir && !(EnumCtx->Flags & ENUM_DIRECTORIES) && !bExactMatch)
249             continue;
250 
251         if (!wcscmp(findFileName, L".") || !wcscmp(findFileName, L".."))
252             continue;
253 
254         /* If we recursively enumerate files excluding directories,
255          * exclude any directory from the enumeration. */
256         if (bIsDir && !(EnumCtx->Flags & ENUM_DIRECTORIES) && (EnumCtx->Flags & ENUM_RECURSE))
257             continue;
258 
259         StringCchCopyW(pFilePart, cchRemaining, findFileName);
260         /* bFound = */ EnumCtx->Callback(&EnumCtx->findData, EnumCtx->FullPathBuffer, EnumCtx->Context);
261         bFound = TRUE;
262     }
263     while (FindNextFileW(hFind, &EnumCtx->findData));
264     FindClose(hFind);
265 
266     return bFound;
267 }
268 
269 static BOOL
AttribEnumFiles(_In_ PCWSTR pszPath,_In_ PCWSTR pszFile,_In_ DWORD fFlags,_In_ PATTRIBS_MASKS AttribsMasks)270 AttribEnumFiles(
271     _In_ PCWSTR pszPath,
272     _In_ PCWSTR pszFile,
273     _In_ DWORD fFlags,
274     _In_ PATTRIBS_MASKS AttribsMasks)
275 {
276     ENUMFILES_CTX EnumContext = {0};
277     size_t offFilePart;
278     HRESULT hRes;
279 
280     EnumContext.FileName = pszFile;
281     EnumContext.Flags    = fFlags;
282     EnumContext.Callback = (AttribsMasks->dwMask == 0 ? PrintAttributes : ChangeAttributes);
283     EnumContext.Context  = (AttribsMasks->dwMask == 0 ? NULL : AttribsMasks);
284 
285     /* Prepare the full file path buffer */
286     EnumContext.cchBuffer = _countof(EnumContext.FullPathBuffer);
287     hRes = StringCchCopyExW(EnumContext.FullPathBuffer,
288                             EnumContext.cchBuffer,
289                             pszPath,
290                             NULL,
291                             &offFilePart,
292                             0);
293     if (hRes != S_OK)
294         return FALSE;
295 
296     /* Offset to the file name part */
297     offFilePart = EnumContext.cchBuffer - offFilePart;
298     if (EnumContext.FullPathBuffer[offFilePart - 1] != L'\\')
299     {
300         EnumContext.FullPathBuffer[offFilePart] = L'\\';
301         EnumContext.FullPathBuffer[offFilePart + 1] = UNICODE_NULL;
302         offFilePart++;
303     }
304 
305     return EnumFilesWorker(&EnumContext, offFilePart);
306 }
307 
wmain(int argc,WCHAR * argv[])308 int wmain(int argc, WCHAR *argv[])
309 {
310     INT i;
311     DWORD dwEnumFlags = 0;
312     ATTRIBS_MASKS AttribsMasks = {0};
313     BOOL bFound = FALSE;
314     PWSTR pszFileName;
315     WCHAR szFilePath[MAX_PATH + 2] = L""; // + 2 to reserve an extra path separator and a NULL-terminator.
316 
317     /* Initialize the Console Standard Streams */
318     ConInitStdStreams();
319 
320     /* Check for options and file specifications */
321     for (i = 1; i < argc; i++)
322     {
323         if (*argv[i] == L'/')
324         {
325             /* Print help and bail out if needed */
326             if (wcscmp(argv[i], L"/?") == 0)
327             {
328                 ConResPuts(StdOut, STRING_ATTRIB_HELP);
329                 return 0;
330             }
331             else
332             /* Retrieve the enumeration modes */
333             if (_wcsicmp(argv[i], L"/s") == 0)
334                 dwEnumFlags |= ENUM_RECURSE;
335             else if (_wcsicmp(argv[i], L"/d") == 0)
336                 dwEnumFlags |= ENUM_DIRECTORIES;
337             else
338             {
339                 /* Unknown option */
340                 ConResPrintf(StdErr, STRING_ERROR_INVALID_PARAM_FORMAT, argv[i]);
341                 return -1;
342             }
343         }
344         else
345         /* Build attributes and mask */
346         if ((*argv[i] == L'+') || (*argv[i] == L'-'))
347         {
348             BOOL bAdd = (*argv[i] == L'+');
349 
350             if (wcslen(argv[i]) != 2)
351             {
352                 ConResPrintf(StdErr, STRING_ERROR_INVALID_PARAM_FORMAT, argv[i]);
353                 return -1;
354             }
355 
356             switch (towupper(argv[i][1]))
357             {
358                 case L'A':
359                     AttribsMasks.dwMask |= FILE_ATTRIBUTE_ARCHIVE;
360                     if (bAdd)
361                         AttribsMasks.dwAttrib |= FILE_ATTRIBUTE_ARCHIVE;
362                     else
363                         AttribsMasks.dwAttrib &= ~FILE_ATTRIBUTE_ARCHIVE;
364                     break;
365 
366                 case L'S':
367                     AttribsMasks.dwMask |= FILE_ATTRIBUTE_SYSTEM;
368                     if (bAdd)
369                         AttribsMasks.dwAttrib |= FILE_ATTRIBUTE_SYSTEM;
370                     else
371                         AttribsMasks.dwAttrib &= ~FILE_ATTRIBUTE_SYSTEM;
372                     break;
373 
374                 case L'H':
375                     AttribsMasks.dwMask |= FILE_ATTRIBUTE_HIDDEN;
376                     if (bAdd)
377                         AttribsMasks.dwAttrib |= FILE_ATTRIBUTE_HIDDEN;
378                     else
379                         AttribsMasks.dwAttrib &= ~FILE_ATTRIBUTE_HIDDEN;
380                     break;
381 
382                 case L'R':
383                     AttribsMasks.dwMask |= FILE_ATTRIBUTE_READONLY;
384                     if (bAdd)
385                         AttribsMasks.dwAttrib |= FILE_ATTRIBUTE_READONLY;
386                     else
387                         AttribsMasks.dwAttrib &= ~FILE_ATTRIBUTE_READONLY;
388                     break;
389 
390 #ifdef EXTENDED_ATTRIBUTES
391                 case L'I':
392                     AttribsMasks.dwMask |= FILE_ATTRIBUTE_NOT_CONTENT_INDEXED;
393                     if (bAdd)
394                         AttribsMasks.dwAttrib |= FILE_ATTRIBUTE_NOT_CONTENT_INDEXED;
395                     else
396                         AttribsMasks.dwAttrib &= ~FILE_ATTRIBUTE_NOT_CONTENT_INDEXED;
397                     break;
398 #endif
399 
400                 default:
401                     ConResPrintf(StdErr, STRING_ERROR_INVALID_PARAM_FORMAT, argv[i]);
402                     return -1;
403             }
404         }
405         else
406         {
407             /* At least one file specification found */
408             bFound = TRUE;
409         }
410     }
411 
412     /* If no file specification was found, operate on all files of the current directory */
413     if (!bFound)
414     {
415         GetCurrentDirectoryW(_countof(szFilePath) - 2, szFilePath);
416         pszFileName = ALL_FILES_PATTERN;
417 
418         bFound = AttribEnumFiles(szFilePath, pszFileName, dwEnumFlags, &AttribsMasks);
419         if (!bFound)
420             ConResPrintf(StdOut, STRING_FILE_NOT_FOUND, pszFileName);
421 
422         return 0;
423     }
424 
425     /* Operate on each file specification */
426     for (i = 1; i < argc; i++)
427     {
428         /* Skip options */
429         if (*argv[i] == L'/' || *argv[i] == L'+' || *argv[i] == L'-')
430             continue;
431 
432         GetFullPathNameW(argv[i], _countof(szFilePath) - 2, szFilePath, &pszFileName);
433         if (pszFileName)
434         {
435             /* Move the file part so as to separate and NULL-terminate the directory */
436             MoveMemory(pszFileName + 1, pszFileName,
437                        sizeof(szFilePath) - (pszFileName -szFilePath + 1) * sizeof(*szFilePath));
438             *pszFileName++ = UNICODE_NULL;
439         }
440         else
441         {
442             pszFileName = L"";
443         }
444 
445         bFound = AttribEnumFiles(szFilePath, pszFileName, dwEnumFlags, &AttribsMasks);
446         if (!bFound)
447             ConResPrintf(StdOut, STRING_FILE_NOT_FOUND, argv[i]);
448     }
449 
450     return 0;
451 }
452