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 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 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 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 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 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 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