e9ec950bc2dd62e784b8ed0276d394dbe47e66ed
[reactos.git] / base / applications / cmdutils / fc / fc.c
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 <stdlib.h>
8 #include <string.h>
9 #include <ctype.h>
10 #include <windef.h>
11 #include <winbase.h>
12 #include <winuser.h>
13 #include <winnls.h>
14 #include <conutils.h>
15 #include "resource.h"
16
17 // See also: https://stackoverflow.com/questions/33125766/compare-files-with-a-cmd
18 typedef enum FCRET { // return code of FC command
19 FCRET_INVALID = -1,
20 FCRET_IDENTICAL = 0,
21 FCRET_DIFFERENT = 1,
22 FCRET_CANT_FIND = 2
23 } FCRET;
24
25 #ifdef _WIN64
26 #define MAX_VIEW_SIZE (256 * 1024 * 1024) // 256 MB
27 #else
28 #define MAX_VIEW_SIZE (64 * 1024 * 1024) // 64 MB
29 #endif
30
31 #define FLAG_A (1 << 0)
32 #define FLAG_B (1 << 1)
33 #define FLAG_C (1 << 2)
34 #define FLAG_L (1 << 3)
35 #define FLAG_LBn (1 << 4)
36 #define FLAG_N (1 << 5)
37 #define FLAG_OFFLINE (1 << 6)
38 #define FLAG_T (1 << 7)
39 #define FLAG_U (1 << 8)
40 #define FLAG_W (1 << 9)
41 #define FLAG_nnnn (1 << 10)
42 #define FLAG_HELP (1 << 11)
43
44 typedef struct FILECOMPARE
45 {
46 DWORD dwFlags; // FLAG_...
47 INT n, nnnn;
48 LPCWSTR file1, file2;
49 } FILECOMPARE;
50
51 static FCRET NoDifference(VOID)
52 {
53 ConResPuts(StdOut, IDS_NO_DIFFERENCE);
54 return FCRET_IDENTICAL;
55 }
56
57 static FCRET Different(LPCWSTR file1, LPCWSTR file2)
58 {
59 ConResPrintf(StdOut, IDS_DIFFERENT, file1, file2);
60 return FCRET_DIFFERENT;
61 }
62
63 static FCRET LongerThan(LPCWSTR file1, LPCWSTR file2)
64 {
65 ConResPrintf(StdOut, IDS_LONGER_THAN, file1, file2);
66 return FCRET_DIFFERENT;
67 }
68
69 static FCRET OutOfMemory(VOID)
70 {
71 ConResPuts(StdErr, IDS_OUT_OF_MEMORY);
72 return FCRET_INVALID;
73 }
74
75 static FCRET CannotRead(LPCWSTR file)
76 {
77 ConResPrintf(StdErr, IDS_CANNOT_READ, file);
78 return FCRET_INVALID;
79 }
80
81 static FCRET InvalidSwitch(VOID)
82 {
83 ConResPuts(StdErr, IDS_INVALID_SWITCH);
84 return FCRET_INVALID;
85 }
86
87 static HANDLE DoOpenFileForInput(LPCWSTR file)
88 {
89 HANDLE hFile = CreateFileW(file, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
90 if (hFile == INVALID_HANDLE_VALUE)
91 {
92 ConResPrintf(StdErr, IDS_CANNOT_OPEN, file);
93 }
94 return hFile;
95 }
96
97 static FCRET BinaryFileCompare(FILECOMPARE *pFC)
98 {
99 FCRET ret;
100 HANDLE hFile1, hFile2, hMapping1 = NULL, hMapping2 = NULL;
101 LPBYTE pb1 = NULL, pb2 = NULL;
102 LARGE_INTEGER ib, cb1, cb2, cbCommon;
103 DWORD cbView, ibView;
104 BOOL fDifferent = FALSE;
105
106 hFile1 = DoOpenFileForInput(pFC->file1);
107 if (hFile1 == INVALID_HANDLE_VALUE)
108 return FCRET_CANT_FIND;
109 hFile2 = DoOpenFileForInput(pFC->file2);
110 if (hFile2 == INVALID_HANDLE_VALUE)
111 {
112 CloseHandle(hFile1);
113 return FCRET_CANT_FIND;
114 }
115
116 do
117 {
118 if (_wcsicmp(pFC->file1, pFC->file2) == 0)
119 {
120 ret = NoDifference();
121 break;
122 }
123 if (!GetFileSizeEx(hFile1, &cb1))
124 {
125 ret = CannotRead(pFC->file1);
126 break;
127 }
128 if (!GetFileSizeEx(hFile2, &cb2))
129 {
130 ret = CannotRead(pFC->file2);
131 break;
132 }
133 cbCommon.QuadPart = min(cb1.QuadPart, cb2.QuadPart);
134 if (cbCommon.QuadPart > 0)
135 {
136 hMapping1 = CreateFileMappingW(hFile1, NULL, PAGE_READONLY,
137 cb1.HighPart, cb1.LowPart, NULL);
138 if (hMapping1 == NULL)
139 {
140 ret = CannotRead(pFC->file1);
141 break;
142 }
143 hMapping2 = CreateFileMappingW(hFile2, NULL, PAGE_READONLY,
144 cb2.HighPart, cb2.LowPart, NULL);
145 if (hMapping2 == NULL)
146 {
147 ret = CannotRead(pFC->file2);
148 break;
149 }
150
151 ret = FCRET_IDENTICAL;
152 for (ib.QuadPart = 0; ib.QuadPart < cbCommon.QuadPart; )
153 {
154 cbView = (DWORD)min(cbCommon.QuadPart - ib.QuadPart, MAX_VIEW_SIZE);
155 pb1 = MapViewOfFile(hMapping1, FILE_MAP_READ, ib.HighPart, ib.LowPart, cbView);
156 pb2 = MapViewOfFile(hMapping2, FILE_MAP_READ, ib.HighPart, ib.LowPart, cbView);
157 if (!pb1 || !pb2)
158 {
159 ret = OutOfMemory();
160 break;
161 }
162 for (ibView = 0; ibView < cbView; ++ib.QuadPart, ++ibView)
163 {
164 if (pb1[ibView] == pb2[ibView])
165 continue;
166
167 fDifferent = TRUE;
168 if (cbCommon.QuadPart > MAXDWORD)
169 {
170 ConPrintf(StdOut, L"%016I64X: %02X %02X\n", ib.QuadPart,
171 pb1[ibView], pb2[ibView]);
172 }
173 else
174 {
175 ConPrintf(StdOut, L"%08lX: %02X %02X\n", ib.LowPart,
176 pb1[ibView], pb2[ibView]);
177 }
178 }
179 UnmapViewOfFile(pb1);
180 UnmapViewOfFile(pb2);
181 pb1 = pb2 = NULL;
182 }
183 if (ret != FCRET_IDENTICAL)
184 break;
185 }
186
187 if (cb1.QuadPart < cb2.QuadPart)
188 ret = LongerThan(pFC->file2, pFC->file1);
189 else if (cb1.QuadPart > cb2.QuadPart)
190 ret = LongerThan(pFC->file1, pFC->file2);
191 else if (fDifferent)
192 ret = Different(pFC->file1, pFC->file2);
193 else
194 ret = NoDifference();
195 } while (0);
196
197 UnmapViewOfFile(pb1);
198 UnmapViewOfFile(pb2);
199 CloseHandle(hMapping1);
200 CloseHandle(hMapping2);
201 CloseHandle(hFile1);
202 CloseHandle(hFile2);
203 return ret;
204 }
205
206 static FCRET
207 UnicodeTextCompare(FILECOMPARE *pFC, HANDLE hMapping1, const LARGE_INTEGER *pcb1,
208 HANDLE hMapping2, const LARGE_INTEGER *pcb2)
209 {
210 FCRET ret;
211 BOOL fIgnoreCase = !!(pFC->dwFlags & FLAG_C);
212 DWORD dwCmpFlags = (fIgnoreCase ? NORM_IGNORECASE : 0);
213 LPCWSTR psz1, psz2;
214 LARGE_INTEGER cch1 = { .QuadPart = pcb1->QuadPart / sizeof(WCHAR) };
215 LARGE_INTEGER cch2 = { .QuadPart = pcb1->QuadPart / sizeof(WCHAR) };
216
217 do
218 {
219 psz1 = MapViewOfFile(hMapping1, FILE_MAP_READ, 0, 0, pcb1->LowPart);
220 psz2 = MapViewOfFile(hMapping2, FILE_MAP_READ, 0, 0, pcb2->LowPart);
221 if (!psz1 || !psz2)
222 {
223 ret = OutOfMemory();
224 break;
225 }
226 if (cch1.QuadPart < MAXLONG && cch2.QuadPart < MAXLONG)
227 {
228 if (CompareStringW(0, dwCmpFlags, psz1, cch1.LowPart,
229 psz2, cch2.LowPart) == CSTR_EQUAL)
230 {
231 ret = NoDifference();
232 break;
233 }
234 }
235 // TODO: compare each lines
236 // TODO: large file support
237 ret = Different(pFC->file1, pFC->file2);
238 } while (0);
239
240 UnmapViewOfFile(psz1);
241 UnmapViewOfFile(psz2);
242 return ret;
243 }
244
245 static FCRET
246 AnsiTextCompare(FILECOMPARE *pFC, HANDLE hMapping1, const LARGE_INTEGER *pcb1,
247 HANDLE hMapping2, const LARGE_INTEGER *pcb2)
248 {
249 FCRET ret;
250 BOOL fIgnoreCase = !!(pFC->dwFlags & FLAG_C);
251 DWORD dwCmpFlags = (fIgnoreCase ? NORM_IGNORECASE : 0);
252 LPSTR psz1, psz2;
253
254 do
255 {
256 psz1 = MapViewOfFile(hMapping1, FILE_MAP_READ, 0, 0, pcb1->LowPart);
257 psz2 = MapViewOfFile(hMapping2, FILE_MAP_READ, 0, 0, pcb2->LowPart);
258 if (!psz1 || !psz2)
259 {
260 ret = OutOfMemory();
261 break;
262 }
263 if (pcb1->QuadPart < MAXLONG && pcb2->QuadPart < MAXLONG)
264 {
265 if (CompareStringA(0, dwCmpFlags, psz1, pcb1->LowPart,
266 psz2, pcb2->LowPart) == CSTR_EQUAL)
267 {
268 ret = NoDifference();
269 break;
270 }
271 }
272 // TODO: compare each lines
273 // TODO: large file support
274 ret = Different(pFC->file1, pFC->file2);
275 } while (0);
276
277 UnmapViewOfFile(psz1);
278 UnmapViewOfFile(psz2);
279 return ret;
280 }
281
282 static FCRET TextFileCompare(FILECOMPARE *pFC)
283 {
284 FCRET ret;
285 HANDLE hFile1, hFile2, hMapping1 = NULL, hMapping2 = NULL;
286 LARGE_INTEGER cb1, cb2;
287 BOOL fUnicode = !!(pFC->dwFlags & FLAG_U);
288
289 hFile1 = DoOpenFileForInput(pFC->file1);
290 if (hFile1 == INVALID_HANDLE_VALUE)
291 return FCRET_CANT_FIND;
292 hFile2 = DoOpenFileForInput(pFC->file2);
293 if (hFile2 == INVALID_HANDLE_VALUE)
294 {
295 CloseHandle(hFile1);
296 return FCRET_CANT_FIND;
297 }
298
299 do
300 {
301 if (_wcsicmp(pFC->file1, pFC->file2) == 0)
302 {
303 ret = NoDifference();
304 break;
305 }
306 if (!GetFileSizeEx(hFile1, &cb1))
307 {
308 ret = CannotRead(pFC->file1);
309 break;
310 }
311 if (!GetFileSizeEx(hFile2, &cb2))
312 {
313 ret = CannotRead(pFC->file2);
314 break;
315 }
316 if (cb1.QuadPart == 0 && cb2.QuadPart == 0)
317 {
318 ret = NoDifference();
319 break;
320 }
321 hMapping1 = CreateFileMappingW(hFile1, NULL, PAGE_READONLY,
322 cb1.HighPart, cb1.LowPart, NULL);
323 if (hMapping1 == NULL)
324 {
325 ret = CannotRead(pFC->file1);
326 break;
327 }
328 hMapping2 = CreateFileMappingW(hFile2, NULL, PAGE_READONLY,
329 cb2.HighPart, cb2.LowPart, NULL);
330 if (hMapping2 == NULL)
331 {
332 ret = CannotRead(pFC->file2);
333 break;
334 }
335
336 if (fUnicode)
337 ret = UnicodeTextCompare(pFC, hMapping1, &cb1, hMapping2, &cb2);
338 else
339 ret = AnsiTextCompare(pFC, hMapping1, &cb1, hMapping2, &cb2);
340 } while (0);
341
342 CloseHandle(hMapping1);
343 CloseHandle(hMapping2);
344 CloseHandle(hFile1);
345 CloseHandle(hFile2);
346 return ret;
347 }
348
349 static BOOL IsBinaryExt(LPCWSTR filename)
350 {
351 // Don't change this array. This is by design.
352 // See also: https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/fc
353 static const LPCWSTR s_exts[] = { L"EXE", L"COM", L"SYS", L"OBJ", L"LIB", L"BIN" };
354 size_t iext;
355 LPCWSTR pch, ext, pch1 = wcsrchr(filename, L'\\'), pch2 = wcsrchr(filename, L'/');
356 if (!pch1 && !pch2)
357 pch = filename;
358 else if (!pch1 && pch2)
359 pch = pch2;
360 else if (pch1 && !pch2)
361 pch = pch1;
362 else if (pch1 < pch2)
363 pch = pch2;
364 else
365 pch = pch1;
366
367 ext = wcsrchr(pch, L'.');
368 if (ext)
369 {
370 ++ext;
371 for (iext = 0; iext < _countof(s_exts); ++iext)
372 {
373 if (_wcsicmp(ext, s_exts[iext]) == 0)
374 return TRUE;
375 }
376 }
377 return FALSE;
378 }
379
380 #define HasWildcard(filename) \
381 ((wcschr((filename), L'*') != NULL) || (wcschr((filename), L'?') != NULL))
382
383 static FCRET FileCompare(FILECOMPARE *pFC)
384 {
385 ConResPrintf(StdOut, IDS_COMPARING, pFC->file1, pFC->file2);
386
387 if (!(pFC->dwFlags & FLAG_L) &&
388 ((pFC->dwFlags & FLAG_B) || IsBinaryExt(pFC->file1) || IsBinaryExt(pFC->file2)))
389 {
390 return BinaryFileCompare(pFC);
391 }
392 return TextFileCompare(pFC);
393 }
394
395 static FCRET WildcardFileCompare(FILECOMPARE *pFC)
396 {
397 if (pFC->dwFlags & FLAG_HELP)
398 {
399 ConResPuts(StdOut, IDS_USAGE);
400 return FCRET_INVALID;
401 }
402
403 if (!pFC->file1 || !pFC->file2)
404 {
405 ConResPuts(StdErr, IDS_NEEDS_FILES);
406 return FCRET_INVALID;
407 }
408
409 if (HasWildcard(pFC->file1) || HasWildcard(pFC->file2))
410 {
411 // TODO: wildcard
412 ConResPuts(StdErr, IDS_CANT_USE_WILDCARD);
413 }
414
415 return FileCompare(pFC);
416 }
417
418 int wmain(int argc, WCHAR **argv)
419 {
420 FILECOMPARE fc = { .dwFlags = 0, .n = 100, .nnnn = 2 };
421 PWCHAR endptr;
422 INT i;
423
424 /* Initialize the Console Standard Streams */
425 ConInitStdStreams();
426
427 for (i = 1; i < argc; ++i)
428 {
429 if (argv[i][0] != L'/')
430 {
431 if (!fc.file1)
432 fc.file1 = argv[i];
433 else if (!fc.file2)
434 fc.file2 = argv[i];
435 else
436 return InvalidSwitch();
437 continue;
438 }
439 switch (towupper(argv[i][1]))
440 {
441 case L'A':
442 fc.dwFlags |= FLAG_A;
443 break;
444 case L'B':
445 fc.dwFlags |= FLAG_B;
446 break;
447 case L'C':
448 fc.dwFlags |= FLAG_C;
449 break;
450 case L'L':
451 if (_wcsicmp(argv[i], L"/L") == 0)
452 {
453 fc.dwFlags |= FLAG_L;
454 }
455 else if (towupper(argv[i][2]) == L'B')
456 {
457 if (iswdigit(argv[i][3]))
458 {
459 fc.dwFlags |= FLAG_LBn;
460 fc.n = wcstoul(&argv[i][3], &endptr, 10);
461 if (endptr == NULL || *endptr != 0)
462 return InvalidSwitch();
463 }
464 else
465 {
466 return InvalidSwitch();
467 }
468 }
469 break;
470 case L'N':
471 fc.dwFlags |= FLAG_N;
472 break;
473 case L'O':
474 if (_wcsicmp(argv[i], L"/OFF") == 0 || _wcsicmp(argv[i], L"/OFFLINE") == 0)
475 {
476 fc.dwFlags |= FLAG_OFFLINE;
477 }
478 break;
479 case L'T':
480 fc.dwFlags |= FLAG_T;
481 break;
482 case L'W':
483 fc.dwFlags |= FLAG_W;
484 break;
485 case L'0': case L'1': case L'2': case L'3': case L'4':
486 case L'5': case L'6': case L'7': case L'8': case L'9':
487 fc.nnnn = wcstoul(&argv[i][1], &endptr, 10);
488 if (endptr == NULL || *endptr != 0)
489 return InvalidSwitch();
490 fc.dwFlags |= FLAG_nnnn;
491 break;
492 case L'?':
493 fc.dwFlags |= FLAG_HELP;
494 break;
495 default:
496 return InvalidSwitch();
497 }
498 }
499 return WildcardFileCompare(&fc);
500 }