xref: /reactos/base/applications/cmdutils/fc/fc.c (revision 9393fc32)
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