1 /* 2 * PROJECT: ReactOS FC Command 3 * LICENSE: GPL-2.0-or-later (https://spdx.org/licenses/GPL-2.0-or-later) 4 * PURPOSE: Comparing files 5 * COPYRIGHT: Copyright 2021 Katayama Hirofumi MZ (katayama.hirofumi.mz@gmail.com) 6 */ 7 #include "fc.h" 8 9 #ifdef __REACTOS__ 10 #include <conutils.h> 11 #else 12 #include <stdio.h> 13 #define ConInitStdStreams() /* empty */ 14 #define StdOut stdout 15 #define StdErr stderr 16 void ConPuts(FILE *fp, LPCWSTR psz) 17 { 18 fputws(psz, fp); 19 } 20 void ConPrintf(FILE *fp, LPCWSTR psz, ...) 21 { 22 va_list va; 23 va_start(va, psz); 24 vfwprintf(fp, psz, va); 25 va_end(va); 26 } 27 void ConResPuts(FILE *fp, UINT nID) 28 { 29 WCHAR sz[MAX_PATH]; 30 LoadStringW(NULL, nID, sz, _countof(sz)); 31 fputws(sz, fp); 32 } 33 void ConResPrintf(FILE *fp, UINT nID, ...) 34 { 35 va_list va; 36 WCHAR sz[MAX_PATH]; 37 va_start(va, nID); 38 LoadStringW(NULL, nID, sz, _countof(sz)); 39 vfwprintf(fp, sz, va); 40 va_end(va); 41 } 42 #endif 43 #include <strsafe.h> 44 #include <shlwapi.h> 45 46 FCRET NoDifference(VOID) 47 { 48 ConResPuts(StdOut, IDS_NO_DIFFERENCE); 49 return FCRET_IDENTICAL; 50 } 51 52 FCRET Different(LPCWSTR file0, LPCWSTR file1) 53 { 54 ConResPrintf(StdOut, IDS_DIFFERENT, file0, file1); 55 return FCRET_DIFFERENT; 56 } 57 58 FCRET LongerThan(LPCWSTR file0, LPCWSTR file1) 59 { 60 ConResPrintf(StdOut, IDS_LONGER_THAN, file0, file1); 61 return FCRET_DIFFERENT; 62 } 63 64 FCRET OutOfMemory(VOID) 65 { 66 ConResPuts(StdErr, IDS_OUT_OF_MEMORY); 67 return FCRET_INVALID; 68 } 69 70 FCRET CannotRead(LPCWSTR file) 71 { 72 ConResPrintf(StdErr, IDS_CANNOT_READ, file); 73 return FCRET_INVALID; 74 } 75 76 FCRET InvalidSwitch(VOID) 77 { 78 ConResPuts(StdErr, IDS_INVALID_SWITCH); 79 return FCRET_INVALID; 80 } 81 82 FCRET ResyncFailed(VOID) 83 { 84 ConResPuts(StdOut, IDS_RESYNC_FAILED); 85 return FCRET_DIFFERENT; 86 } 87 88 VOID PrintCaption(LPCWSTR file) 89 { 90 ConPrintf(StdOut, L"***** %ls\n", file); 91 } 92 93 VOID PrintEndOfDiff(VOID) 94 { 95 ConPuts(StdOut, L"*****\n\n"); 96 } 97 98 VOID PrintDots(VOID) 99 { 100 ConPuts(StdOut, L"...\n"); 101 } 102 103 VOID PrintLineW(const FILECOMPARE *pFC, DWORD lineno, LPCWSTR psz) 104 { 105 if (pFC->dwFlags & FLAG_N) 106 ConPrintf(StdOut, L"%5d: %ls\n", lineno, psz); 107 else 108 ConPrintf(StdOut, L"%ls\n", psz); 109 } 110 VOID PrintLineA(const FILECOMPARE *pFC, DWORD lineno, LPCSTR psz) 111 { 112 if (pFC->dwFlags & FLAG_N) 113 ConPrintf(StdOut, L"%5d: %hs\n", lineno, psz); 114 else 115 ConPrintf(StdOut, L"%hs\n", psz); 116 } 117 118 HANDLE DoOpenFileForInput(LPCWSTR file) 119 { 120 HANDLE hFile = CreateFileW(file, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); 121 if (hFile == INVALID_HANDLE_VALUE) 122 { 123 ConResPrintf(StdErr, IDS_CANNOT_OPEN, file); 124 } 125 return hFile; 126 } 127 128 static FCRET BinaryFileCompare(FILECOMPARE *pFC) 129 { 130 FCRET ret; 131 HANDLE hFile0, hFile1, hMapping0 = NULL, hMapping1 = NULL; 132 LPBYTE pb0 = NULL, pb1 = NULL; 133 LARGE_INTEGER ib, cb0, cb1, cbCommon; 134 DWORD cbView, ibView; 135 BOOL fDifferent = FALSE; 136 137 hFile0 = DoOpenFileForInput(pFC->file[0]); 138 if (hFile0 == INVALID_HANDLE_VALUE) 139 return FCRET_CANT_FIND; 140 hFile1 = DoOpenFileForInput(pFC->file[1]); 141 if (hFile1 == INVALID_HANDLE_VALUE) 142 { 143 CloseHandle(hFile0); 144 return FCRET_CANT_FIND; 145 } 146 147 do 148 { 149 if (_wcsicmp(pFC->file[0], pFC->file[1]) == 0) 150 { 151 ret = NoDifference(); 152 break; 153 } 154 if (!GetFileSizeEx(hFile0, &cb0)) 155 { 156 ret = CannotRead(pFC->file[0]); 157 break; 158 } 159 if (!GetFileSizeEx(hFile1, &cb1)) 160 { 161 ret = CannotRead(pFC->file[1]); 162 break; 163 } 164 cbCommon.QuadPart = min(cb0.QuadPart, cb1.QuadPart); 165 if (cbCommon.QuadPart > 0) 166 { 167 hMapping0 = CreateFileMappingW(hFile0, NULL, PAGE_READONLY, 168 cb0.HighPart, cb0.LowPart, NULL); 169 if (hMapping0 == NULL) 170 { 171 ret = CannotRead(pFC->file[0]); 172 break; 173 } 174 hMapping1 = CreateFileMappingW(hFile1, NULL, PAGE_READONLY, 175 cb1.HighPart, cb1.LowPart, NULL); 176 if (hMapping1 == NULL) 177 { 178 ret = CannotRead(pFC->file[1]); 179 break; 180 } 181 182 ret = FCRET_IDENTICAL; 183 for (ib.QuadPart = 0; ib.QuadPart < cbCommon.QuadPart; ) 184 { 185 cbView = (DWORD)min(cbCommon.QuadPart - ib.QuadPart, MAX_VIEW_SIZE); 186 pb0 = MapViewOfFile(hMapping0, FILE_MAP_READ, ib.HighPart, ib.LowPart, cbView); 187 pb1 = MapViewOfFile(hMapping1, FILE_MAP_READ, ib.HighPart, ib.LowPart, cbView); 188 if (!pb0 || !pb1) 189 { 190 ret = OutOfMemory(); 191 break; 192 } 193 for (ibView = 0; ibView < cbView; ++ib.QuadPart, ++ibView) 194 { 195 if (pb0[ibView] == pb1[ibView]) 196 continue; 197 198 fDifferent = TRUE; 199 if (cbCommon.QuadPart > MAXDWORD) 200 { 201 ConPrintf(StdOut, L"%016I64X: %02X %02X\n", ib.QuadPart, 202 pb0[ibView], pb1[ibView]); 203 } 204 else 205 { 206 ConPrintf(StdOut, L"%08lX: %02X %02X\n", ib.LowPart, 207 pb0[ibView], pb1[ibView]); 208 } 209 } 210 UnmapViewOfFile(pb0); 211 UnmapViewOfFile(pb1); 212 pb0 = pb1 = NULL; 213 } 214 if (ret != FCRET_IDENTICAL) 215 break; 216 } 217 218 if (cb0.QuadPart < cb1.QuadPart) 219 ret = LongerThan(pFC->file[1], pFC->file[0]); 220 else if (cb0.QuadPart > cb1.QuadPart) 221 ret = LongerThan(pFC->file[0], pFC->file[1]); 222 else if (fDifferent) 223 ret = Different(pFC->file[0], pFC->file[1]); 224 else 225 ret = NoDifference(); 226 } while (0); 227 228 UnmapViewOfFile(pb0); 229 UnmapViewOfFile(pb1); 230 CloseHandle(hMapping0); 231 CloseHandle(hMapping1); 232 CloseHandle(hFile0); 233 CloseHandle(hFile1); 234 return ret; 235 } 236 237 static FCRET TextFileCompare(FILECOMPARE *pFC) 238 { 239 FCRET ret; 240 HANDLE hFile0, hFile1, hMapping0 = NULL, hMapping1 = NULL; 241 LARGE_INTEGER cb0, cb1; 242 BOOL fUnicode = !!(pFC->dwFlags & FLAG_U); 243 244 hFile0 = DoOpenFileForInput(pFC->file[0]); 245 if (hFile0 == INVALID_HANDLE_VALUE) 246 return FCRET_CANT_FIND; 247 hFile1 = DoOpenFileForInput(pFC->file[1]); 248 if (hFile1 == INVALID_HANDLE_VALUE) 249 { 250 CloseHandle(hFile0); 251 return FCRET_CANT_FIND; 252 } 253 254 do 255 { 256 if (_wcsicmp(pFC->file[0], pFC->file[1]) == 0) 257 { 258 ret = NoDifference(); 259 break; 260 } 261 if (!GetFileSizeEx(hFile0, &cb0)) 262 { 263 ret = CannotRead(pFC->file[0]); 264 break; 265 } 266 if (!GetFileSizeEx(hFile1, &cb1)) 267 { 268 ret = CannotRead(pFC->file[1]); 269 break; 270 } 271 if (cb0.QuadPart == 0 && cb1.QuadPart == 0) 272 { 273 ret = NoDifference(); 274 break; 275 } 276 if (cb0.QuadPart > 0) 277 { 278 hMapping0 = CreateFileMappingW(hFile0, NULL, PAGE_READONLY, 279 cb0.HighPart, cb0.LowPart, NULL); 280 if (hMapping0 == NULL) 281 { 282 ret = CannotRead(pFC->file[0]); 283 break; 284 } 285 } 286 if (cb1.QuadPart > 0) 287 { 288 hMapping1 = CreateFileMappingW(hFile1, NULL, PAGE_READONLY, 289 cb1.HighPart, cb1.LowPart, NULL); 290 if (hMapping1 == NULL) 291 { 292 ret = CannotRead(pFC->file[1]); 293 break; 294 } 295 } 296 if (fUnicode) 297 ret = TextCompareW(pFC, &hMapping0, &cb0, &hMapping1, &cb1); 298 else 299 ret = TextCompareA(pFC, &hMapping0, &cb0, &hMapping1, &cb1); 300 } while (0); 301 302 CloseHandle(hMapping0); 303 CloseHandle(hMapping1); 304 CloseHandle(hFile0); 305 CloseHandle(hFile1); 306 return ret; 307 } 308 309 static BOOL IsBinaryExt(LPCWSTR filename) 310 { 311 // Don't change this array. This is by design. 312 // See also: https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/fc 313 static const LPCWSTR s_exts[] = { L"EXE", L"COM", L"SYS", L"OBJ", L"LIB", L"BIN" }; 314 size_t iext; 315 LPCWSTR pch, ext, pch0 = wcsrchr(filename, L'\\'), pch1 = wcsrchr(filename, L'/'); 316 if (!pch0 && !pch1) 317 pch = filename; 318 else if (!pch0 && pch1) 319 pch = pch1; 320 else if (pch0 && !pch1) 321 pch = pch0; 322 else if (pch0 < pch1) 323 pch = pch1; 324 else 325 pch = pch0; 326 327 ext = wcsrchr(pch, L'.'); 328 if (ext) 329 { 330 ++ext; 331 for (iext = 0; iext < _countof(s_exts); ++iext) 332 { 333 if (_wcsicmp(ext, s_exts[iext]) == 0) 334 return TRUE; 335 } 336 } 337 return FALSE; 338 } 339 340 static FCRET FileCompare(FILECOMPARE *pFC) 341 { 342 FCRET ret; 343 ConResPrintf(StdOut, IDS_COMPARING, pFC->file[0], pFC->file[1]); 344 345 if (!(pFC->dwFlags & FLAG_L) && 346 ((pFC->dwFlags & FLAG_B) || IsBinaryExt(pFC->file[0]) || IsBinaryExt(pFC->file[1]))) 347 { 348 ret = BinaryFileCompare(pFC); 349 } 350 else 351 { 352 ret = TextFileCompare(pFC); 353 } 354 355 ConPuts(StdOut, L"\n"); 356 return ret; 357 } 358 359 /* Is it L"." or L".."? */ 360 #define IS_DOTS(pch) \ 361 ((*(pch) == L'.') && (((pch)[1] == 0) || (((pch)[1] == L'.') && ((pch)[2] == 0)))) 362 #define HasWildcard(filename) \ 363 ((wcschr((filename), L'*') != NULL) || (wcschr((filename), L'?') != NULL)) 364 365 static inline BOOL IsTitleWild(LPCWSTR filename) 366 { 367 LPCWSTR pch = PathFindFileNameW(filename); 368 return (pch && *pch == L'*' && pch[1] == L'.' && !HasWildcard(&pch[2])); 369 } 370 371 static FCRET FileCompareOneSideWild(const FILECOMPARE *pFC, BOOL bWildRight) 372 { 373 FCRET ret = FCRET_IDENTICAL; 374 WIN32_FIND_DATAW find; 375 HANDLE hFind; 376 WCHAR szPath[MAX_PATH]; 377 FILECOMPARE fc; 378 379 hFind = FindFirstFileW(pFC->file[bWildRight], &find); 380 if (hFind == INVALID_HANDLE_VALUE) 381 { 382 ConResPrintf(StdErr, IDS_CANNOT_OPEN, pFC->file[bWildRight]); 383 ConPuts(StdOut, L"\n"); 384 return FCRET_CANT_FIND; 385 } 386 StringCbCopyW(szPath, sizeof(szPath), pFC->file[bWildRight]); 387 388 fc = *pFC; 389 fc.file[!bWildRight] = pFC->file[!bWildRight]; 390 fc.file[bWildRight] = szPath; 391 do 392 { 393 if (IS_DOTS(find.cFileName)) 394 continue; 395 396 // replace file title 397 PathRemoveFileSpecW(szPath); 398 PathAppendW(szPath, find.cFileName); 399 400 switch (FileCompare(&fc)) 401 { 402 case FCRET_IDENTICAL: 403 break; 404 case FCRET_DIFFERENT: 405 if (ret != FCRET_INVALID) 406 ret = FCRET_DIFFERENT; 407 break; 408 default: 409 ret = FCRET_INVALID; 410 break; 411 } 412 } while (FindNextFileW(hFind, &find)); 413 414 FindClose(hFind); 415 return ret; 416 } 417 418 static FCRET FileCompareWildTitle(const FILECOMPARE *pFC) 419 { 420 FCRET ret = FCRET_IDENTICAL; 421 WIN32_FIND_DATAW find; 422 HANDLE hFind; 423 WCHAR szPath0[MAX_PATH], szPath1[MAX_PATH]; 424 FILECOMPARE fc; 425 LPWSTR pch; 426 427 hFind = FindFirstFileW(pFC->file[0], &find); 428 if (hFind == INVALID_HANDLE_VALUE) 429 { 430 ConResPrintf(StdErr, IDS_CANNOT_OPEN, pFC->file[0]); 431 ConPuts(StdOut, L"\n"); 432 return FCRET_CANT_FIND; 433 } 434 StringCbCopyW(szPath0, sizeof(szPath0), pFC->file[0]); 435 StringCbCopyW(szPath1, sizeof(szPath1), pFC->file[1]); 436 pch = PathFindExtensionW(pFC->file[1]); 437 438 fc = *pFC; 439 fc.file[0] = szPath0; 440 fc.file[1] = szPath1; 441 do 442 { 443 if (IS_DOTS(find.cFileName)) 444 continue; 445 446 // replace file title 447 PathRemoveFileSpecW(szPath0); 448 PathRemoveFileSpecW(szPath1); 449 PathAppendW(szPath0, find.cFileName); 450 PathAppendW(szPath1, find.cFileName); 451 452 // replace dot extension 453 PathRemoveExtensionW(szPath1); 454 PathAddExtensionW(szPath1, pch); 455 456 switch (FileCompare(&fc)) 457 { 458 case FCRET_IDENTICAL: 459 break; 460 case FCRET_DIFFERENT: 461 if (ret != FCRET_INVALID) 462 ret = FCRET_DIFFERENT; 463 break; 464 default: 465 ret = FCRET_INVALID; 466 break; 467 } 468 } while (FindNextFileW(hFind, &find)); 469 470 FindClose(hFind); 471 return ret; 472 } 473 474 static FCRET FileCompareBothWild(const FILECOMPARE *pFC) 475 { 476 FCRET ret = FCRET_IDENTICAL; 477 WIN32_FIND_DATAW find0, find1; 478 HANDLE hFind0, hFind1; 479 WCHAR szPath0[MAX_PATH], szPath1[MAX_PATH]; 480 FILECOMPARE fc; 481 482 hFind0 = FindFirstFileW(pFC->file[0], &find0); 483 if (hFind0 == INVALID_HANDLE_VALUE) 484 { 485 ConResPrintf(StdErr, IDS_CANNOT_OPEN, pFC->file[0]); 486 ConPuts(StdOut, L"\n"); 487 return FCRET_CANT_FIND; 488 } 489 hFind1 = FindFirstFileW(pFC->file[1], &find1); 490 if (hFind1 == INVALID_HANDLE_VALUE) 491 { 492 CloseHandle(hFind0); 493 ConResPrintf(StdErr, IDS_CANNOT_OPEN, pFC->file[1]); 494 ConPuts(StdOut, L"\n"); 495 return FCRET_CANT_FIND; 496 } 497 StringCbCopyW(szPath0, sizeof(szPath0), pFC->file[0]); 498 StringCbCopyW(szPath1, sizeof(szPath1), pFC->file[1]); 499 500 fc = *pFC; 501 fc.file[0] = szPath0; 502 fc.file[1] = szPath1; 503 do 504 { 505 while (IS_DOTS(find0.cFileName)) 506 { 507 if (!FindNextFileW(hFind0, &find0)) 508 goto quit; 509 } 510 while (IS_DOTS(find1.cFileName)) 511 { 512 if (!FindNextFileW(hFind1, &find1)) 513 goto quit; 514 } 515 516 // replace file title 517 PathRemoveFileSpecW(szPath0); 518 PathRemoveFileSpecW(szPath1); 519 PathAppendW(szPath0, find0.cFileName); 520 PathAppendW(szPath1, find1.cFileName); 521 522 switch (FileCompare(&fc)) 523 { 524 case FCRET_IDENTICAL: 525 break; 526 case FCRET_DIFFERENT: 527 if (ret != FCRET_INVALID) 528 ret = FCRET_DIFFERENT; 529 break; 530 default: 531 ret = FCRET_INVALID; 532 break; 533 } 534 } while (FindNextFileW(hFind0, &find0) && FindNextFileW(hFind1, &find1)); 535 quit: 536 CloseHandle(hFind0); 537 CloseHandle(hFind1); 538 return ret; 539 } 540 541 static FCRET WildcardFileCompare(FILECOMPARE *pFC) 542 { 543 BOOL fWild0, fWild1; 544 545 if (pFC->dwFlags & FLAG_HELP) 546 { 547 ConResPuts(StdOut, IDS_USAGE); 548 return FCRET_INVALID; 549 } 550 551 if (!pFC->file[0] || !pFC->file[1]) 552 { 553 ConResPuts(StdErr, IDS_NEEDS_FILES); 554 return FCRET_INVALID; 555 } 556 557 fWild0 = HasWildcard(pFC->file[0]); 558 fWild1 = HasWildcard(pFC->file[1]); 559 if (fWild0 && fWild1) 560 { 561 if (IsTitleWild(pFC->file[0]) && IsTitleWild(pFC->file[1])) 562 return FileCompareWildTitle(pFC); 563 else 564 return FileCompareBothWild(pFC); 565 } 566 else if (fWild0) 567 { 568 return FileCompareOneSideWild(pFC, FALSE); 569 } 570 else if (fWild1) 571 { 572 return FileCompareOneSideWild(pFC, TRUE); 573 } 574 return FileCompare(pFC); 575 } 576 577 int wmain(int argc, WCHAR **argv) 578 { 579 FILECOMPARE fc = { .dwFlags = 0, .n = 100, .nnnn = 2 }; 580 PWCHAR endptr; 581 INT i; 582 583 /* Initialize the Console Standard Streams */ 584 ConInitStdStreams(); 585 586 for (i = 1; i < argc; ++i) 587 { 588 if (argv[i][0] != L'/') 589 { 590 if (!fc.file[0]) 591 fc.file[0] = argv[i]; 592 else if (!fc.file[1]) 593 fc.file[1] = argv[i]; 594 else 595 return InvalidSwitch(); 596 continue; 597 } 598 switch (towupper(argv[i][1])) 599 { 600 case L'A': 601 fc.dwFlags |= FLAG_A; 602 break; 603 case L'B': 604 fc.dwFlags |= FLAG_B; 605 break; 606 case L'C': 607 fc.dwFlags |= FLAG_C; 608 break; 609 case L'L': 610 if (_wcsicmp(argv[i], L"/L") == 0) 611 { 612 fc.dwFlags |= FLAG_L; 613 } 614 else if (towupper(argv[i][2]) == L'B') 615 { 616 if (iswdigit(argv[i][3])) 617 { 618 fc.dwFlags |= FLAG_LBn; 619 fc.n = wcstoul(&argv[i][3], &endptr, 10); 620 if (endptr == NULL || *endptr != 0) 621 return InvalidSwitch(); 622 } 623 else 624 { 625 return InvalidSwitch(); 626 } 627 } 628 break; 629 case L'N': 630 fc.dwFlags |= FLAG_N; 631 break; 632 case L'O': 633 if (_wcsicmp(argv[i], L"/OFF") == 0 || _wcsicmp(argv[i], L"/OFFLINE") == 0) 634 { 635 fc.dwFlags |= FLAG_OFFLINE; 636 } 637 break; 638 case L'T': 639 fc.dwFlags |= FLAG_T; 640 break; 641 case L'U': 642 fc.dwFlags |= FLAG_U; 643 break; 644 case L'W': 645 fc.dwFlags |= FLAG_W; 646 break; 647 case L'0': case L'1': case L'2': case L'3': case L'4': 648 case L'5': case L'6': case L'7': case L'8': case L'9': 649 fc.nnnn = wcstoul(&argv[i][1], &endptr, 10); 650 if (endptr == NULL || *endptr != 0) 651 return InvalidSwitch(); 652 fc.dwFlags |= FLAG_nnnn; 653 break; 654 case L'?': 655 fc.dwFlags |= FLAG_HELP; 656 break; 657 default: 658 return InvalidSwitch(); 659 } 660 } 661 return WildcardFileCompare(&fc); 662 } 663 664 #ifndef __REACTOS__ 665 int main(int argc, char **argv) 666 { 667 INT my_argc; 668 LPWSTR *my_argv = CommandLineToArgvW(GetCommandLineW(), &my_argc); 669 INT ret = wmain(my_argc, my_argv); 670 LocalFree(my_argv); 671 return ret; 672 } 673 #endif 674