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