https://git.reactos.org/?p=reactos.git;a=commitdiff;h=8bf471105e2d0bbb3c1d91...
commit 8bf471105e2d0bbb3c1d913647e5ce88d9b3d0d4 Author: Katayama Hirofumi MZ katayama.hirofumi.mz@gmail.com AuthorDate: Sun Apr 25 13:06:13 2021 +0900 Commit: GitHub noreply@github.com CommitDate: Sun Apr 25 13:06:13 2021 +0900
[CMDUTILS][FC] Initial implement FC command (#3622)
Implement FC (file comparison) command. As a starting point, we support binary mode comparison at first. Text mode comparison and wildcard are not supported yet. CORE-17500 --- base/applications/cmdutils/CMakeLists.txt | 1 + base/applications/cmdutils/fc/CMakeLists.txt | 7 + base/applications/cmdutils/fc/fc.c | 496 +++++++++++++++++++++++++++ base/applications/cmdutils/fc/fc.rc | 12 + base/applications/cmdutils/fc/lang/en-US.rc | 41 +++ base/applications/cmdutils/fc/resource.h | 13 + 6 files changed, 570 insertions(+)
diff --git a/base/applications/cmdutils/CMakeLists.txt b/base/applications/cmdutils/CMakeLists.txt index 43b10eb6c73..f1cd0b9fd1d 100644 --- a/base/applications/cmdutils/CMakeLists.txt +++ b/base/applications/cmdutils/CMakeLists.txt @@ -8,6 +8,7 @@ add_subdirectory(cscript) add_subdirectory(dbgprint) add_subdirectory(doskey) add_subdirectory(eventcreate) +add_subdirectory(fc) add_subdirectory(find) add_subdirectory(fsutil) add_subdirectory(help) diff --git a/base/applications/cmdutils/fc/CMakeLists.txt b/base/applications/cmdutils/fc/CMakeLists.txt new file mode 100644 index 00000000000..fec64446ae1 --- /dev/null +++ b/base/applications/cmdutils/fc/CMakeLists.txt @@ -0,0 +1,7 @@ +include_directories(${REACTOS_SOURCE_DIR}/sdk/lib/conutils) + +add_executable(fc fc.c fc.rc) +set_module_type(fc win32cui UNICODE) +target_link_libraries(fc conutils ${PSEH_LIB}) +add_importlibs(fc msvcrt user32 kernel32) +add_cd_file(TARGET fc DESTINATION reactos/system32 FOR all) diff --git a/base/applications/cmdutils/fc/fc.c b/base/applications/cmdutils/fc/fc.c new file mode 100644 index 00000000000..1697219c6cb --- /dev/null +++ b/base/applications/cmdutils/fc/fc.c @@ -0,0 +1,496 @@ +/* + * PROJECT: ReactOS FC Command + * LICENSE: GPL-2.0-or-later (https://spdx.org/licenses/GPL-2.0-or-later) + * PURPOSE: Comparing files + * COPYRIGHT: Copyright 2021 Katayama Hirofumi MZ (katayama.hirofumi.mz@gmail.com) + */ +#include <stdlib.h> +#include <string.h> +#include <ctype.h> +#include <windef.h> +#include <winbase.h> +#include <winuser.h> +#include <winnls.h> +#include <conutils.h> +#include "resource.h" + +// See also: https://stackoverflow.com/questions/33125766/compare-files-with-a-cmd +typedef enum FCRET { // return code of FC command + FCRET_INVALID = -1, FCRET_IDENTICAL = 0, FCRET_DIFFERENT = 1, FCRET_CANT_FIND = 2 +} FCRET; + +#ifdef _WIN64 + #define MAX_VIEW_SIZE (256 * 1024 * 1024) // 256 MB +#else + #define MAX_VIEW_SIZE (64 * 1024 * 1024) // 64 MB +#endif + +#define FLAG_A (1 << 0) +#define FLAG_B (1 << 1) +#define FLAG_C (1 << 2) +#define FLAG_L (1 << 3) +#define FLAG_LBn (1 << 4) +#define FLAG_N (1 << 5) +#define FLAG_OFFLINE (1 << 6) +#define FLAG_T (1 << 7) +#define FLAG_U (1 << 8) +#define FLAG_W (1 << 9) +#define FLAG_nnnn (1 << 10) +#define FLAG_HELP (1 << 11) + +typedef struct FILECOMPARE { + DWORD dwFlags; // FLAG_... + INT n, nnnn; + LPCWSTR file1, file2; +} FILECOMPARE; + +static FCRET NoDifference(VOID) +{ + ConResPuts(StdOut, IDS_NO_DIFFERENCE); + return FCRET_IDENTICAL; +} + +static FCRET Different(LPCWSTR file1, LPCWSTR file2) +{ + ConResPrintf(StdOut, IDS_DIFFERENT, file1, file2); + return FCRET_DIFFERENT; +} + +static FCRET LongerThan(LPCWSTR file1, LPCWSTR file2) +{ + ConResPrintf(StdOut, IDS_LONGER_THAN, file1, file2); + return FCRET_DIFFERENT; +} + +static FCRET OutOfMemory(VOID) +{ + ConResPuts(StdErr, IDS_OUT_OF_MEMORY); + return FCRET_INVALID; +} + +static FCRET CannotRead(LPCWSTR file) +{ + ConResPrintf(StdErr, IDS_CANNOT_READ, file); + return FCRET_INVALID; +} + +static FCRET InvalidSwitch(VOID) +{ + ConResPuts(StdErr, IDS_INVALID_SWITCH); + return FCRET_INVALID; +} + +static HANDLE DoOpenFileForInput(LPCWSTR file) +{ + HANDLE hFile = CreateFileW(file, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); + if (hFile == INVALID_HANDLE_VALUE) + { + ConResPrintf(StdErr, IDS_CANNOT_OPEN, file); + } + return hFile; +} + +static FCRET BinaryFileCompare(FILECOMPARE *pFC) +{ + FCRET ret; + HANDLE hFile1, hFile2, hMapping1 = NULL, hMapping2 = NULL; + LPBYTE pb1 = NULL, pb2 = NULL; + LARGE_INTEGER ib, cb1, cb2, cbCommon; + DWORD cbView, ibView; + BOOL fDifferent = FALSE; + + hFile1 = DoOpenFileForInput(pFC->file1); + if (hFile1 == INVALID_HANDLE_VALUE) + return FCRET_CANT_FIND; + hFile2 = DoOpenFileForInput(pFC->file2); + if (hFile2 == INVALID_HANDLE_VALUE) + { + CloseHandle(hFile1); + return FCRET_CANT_FIND; + } + + do + { + if (_wcsicmp(pFC->file1, pFC->file2) == 0) + { + ret = NoDifference(); + break; + } + if (!GetFileSizeEx(hFile1, &cb1)) + { + ret = CannotRead(pFC->file1); + break; + } + if (!GetFileSizeEx(hFile2, &cb2)) + { + ret = CannotRead(pFC->file2); + break; + } + cbCommon.QuadPart = min(cb1.QuadPart, cb2.QuadPart); + if (cbCommon.QuadPart > 0) + { + hMapping1 = CreateFileMappingW(hFile1, NULL, PAGE_READONLY, + cb1.HighPart, cb1.LowPart, NULL); + if (hMapping1 == NULL) + { + ret = CannotRead(pFC->file1); + break; + } + hMapping2 = CreateFileMappingW(hFile2, NULL, PAGE_READONLY, + cb2.HighPart, cb2.LowPart, NULL); + if (hMapping2 == NULL) + { + ret = CannotRead(pFC->file2); + break; + } + + ret = FCRET_IDENTICAL; + for (ib.QuadPart = 0; ib.QuadPart < cbCommon.QuadPart; ) + { + cbView = (DWORD)min(cbCommon.QuadPart - ib.QuadPart, MAX_VIEW_SIZE); + pb1 = MapViewOfFile(hMapping1, FILE_MAP_READ, ib.HighPart, ib.LowPart, cbView); + pb2 = MapViewOfFile(hMapping2, FILE_MAP_READ, ib.HighPart, ib.LowPart, cbView); + if (!pb1 || !pb2) + { + ret = OutOfMemory(); + break; + } + for (ibView = 0; ibView < cbView; ++ib.QuadPart, ++ibView) + { + if (pb1[ibView] == pb2[ibView]) + continue; + + fDifferent = TRUE; + if (cbCommon.QuadPart > MAXDWORD) + { + ConPrintf(StdOut, L"%016I64X: %02X %02X\n", ib.QuadPart, + pb1[ibView], pb2[ibView]); + } + else + { + ConPrintf(StdOut, L"%08lX: %02X %02X\n", ib.LowPart, + pb1[ibView], pb2[ibView]); + } + } + UnmapViewOfFile(pb1); + UnmapViewOfFile(pb2); + pb1 = pb2 = NULL; + } + if (ret != FCRET_IDENTICAL) + break; + } + + if (cb1.QuadPart < cb2.QuadPart) + ret = LongerThan(pFC->file2, pFC->file1); + else if (cb1.QuadPart > cb2.QuadPart) + ret = LongerThan(pFC->file1, pFC->file2); + else if (fDifferent) + ret = Different(pFC->file1, pFC->file2); + else + ret = NoDifference(); + } while (0); + + UnmapViewOfFile(pb1); + UnmapViewOfFile(pb2); + CloseHandle(hMapping1); + CloseHandle(hMapping2); + CloseHandle(hFile1); + CloseHandle(hFile2); + return ret; +} + +static FCRET +UnicodeTextCompare(FILECOMPARE *pFC, HANDLE hMapping1, const LARGE_INTEGER *pcb1, + HANDLE hMapping2, const LARGE_INTEGER *pcb2) +{ + FCRET ret; + BOOL fIgnoreCase = !!(pFC->dwFlags & FLAG_C); + DWORD dwCmpFlags = (fIgnoreCase ? NORM_IGNORECASE : 0); + LPCWSTR psz1, psz2; + LARGE_INTEGER cch1 = { .QuadPart = pcb1->QuadPart / sizeof(WCHAR) }; + LARGE_INTEGER cch2 = { .QuadPart = pcb1->QuadPart / sizeof(WCHAR) }; + + do + { + psz1 = MapViewOfFile(hMapping1, FILE_MAP_READ, 0, 0, pcb1->LowPart); + psz2 = MapViewOfFile(hMapping2, FILE_MAP_READ, 0, 0, pcb2->LowPart); + if (!psz1 || !psz2) + { + ret = OutOfMemory(); + break; + } + if (cch1.QuadPart < MAXLONG && cch2.QuadPart < MAXLONG) + { + if (CompareStringW(0, dwCmpFlags, psz1, cch1.LowPart, + psz2, cch2.LowPart) == CSTR_EQUAL) + { + ret = NoDifference(); + break; + } + } + // TODO: compare each lines + // TODO: large file support + ret = Different(pFC->file1, pFC->file2); + } while (0); + + UnmapViewOfFile(psz1); + UnmapViewOfFile(psz2); + return ret; +} + +static FCRET +AnsiTextCompare(FILECOMPARE *pFC, HANDLE hMapping1, const LARGE_INTEGER *pcb1, + HANDLE hMapping2, const LARGE_INTEGER *pcb2) +{ + FCRET ret; + BOOL fIgnoreCase = !!(pFC->dwFlags & FLAG_C); + DWORD dwCmpFlags = (fIgnoreCase ? NORM_IGNORECASE : 0); + LPSTR psz1, psz2; + + do + { + psz1 = MapViewOfFile(hMapping1, FILE_MAP_READ, 0, 0, pcb1->LowPart); + psz2 = MapViewOfFile(hMapping2, FILE_MAP_READ, 0, 0, pcb2->LowPart); + if (!psz1 || !psz2) + { + ret = OutOfMemory(); + break; + } + if (pcb1->QuadPart < MAXLONG && pcb2->QuadPart < MAXLONG) + { + if (CompareStringA(0, dwCmpFlags, psz1, pcb1->LowPart, + psz2, pcb2->LowPart) == CSTR_EQUAL) + { + ret = NoDifference(); + break; + } + } + // TODO: compare each lines + // TODO: large file support + ret = Different(pFC->file1, pFC->file2); + } while (0); + + UnmapViewOfFile(psz1); + UnmapViewOfFile(psz2); + return ret; +} + +static FCRET TextFileCompare(FILECOMPARE *pFC) +{ + FCRET ret; + HANDLE hFile1, hFile2, hMapping1 = NULL, hMapping2 = NULL; + LARGE_INTEGER cb1, cb2; + BOOL fUnicode = !!(pFC->dwFlags & FLAG_U); + + hFile1 = DoOpenFileForInput(pFC->file1); + if (hFile1 == INVALID_HANDLE_VALUE) + return FCRET_CANT_FIND; + hFile2 = DoOpenFileForInput(pFC->file2); + if (hFile2 == INVALID_HANDLE_VALUE) + { + CloseHandle(hFile1); + return FCRET_CANT_FIND; + } + + do + { + if (_wcsicmp(pFC->file1, pFC->file2) == 0) + { + ret = NoDifference(); + break; + } + if (!GetFileSizeEx(hFile1, &cb1)) + { + ret = CannotRead(pFC->file1); + break; + } + if (!GetFileSizeEx(hFile2, &cb2)) + { + ret = CannotRead(pFC->file2); + break; + } + if (cb1.QuadPart == 0 && cb2.QuadPart == 0) + { + ret = NoDifference(); + break; + } + hMapping1 = CreateFileMappingW(hFile1, NULL, PAGE_READONLY, + cb1.HighPart, cb1.LowPart, NULL); + if (hMapping1 == NULL) + { + ret = CannotRead(pFC->file1); + break; + } + hMapping2 = CreateFileMappingW(hFile2, NULL, PAGE_READONLY, + cb2.HighPart, cb2.LowPart, NULL); + if (hMapping2 == NULL) + { + ret = CannotRead(pFC->file2); + break; + } + + if (fUnicode) + ret = UnicodeTextCompare(pFC, hMapping1, &cb1, hMapping2, &cb2); + else + ret = AnsiTextCompare(pFC, hMapping1, &cb1, hMapping2, &cb2); + } while (0); + + CloseHandle(hMapping1); + CloseHandle(hMapping2); + CloseHandle(hFile1); + CloseHandle(hFile2); + return ret; +} + +static BOOL IsBinaryExt(LPCWSTR filename) +{ + // Don't change this array. This is by design. + // See also: https://docs.microsoft.com/en-us/windows-server/administration/windows-comma... + static const LPCWSTR s_exts[] = { L"EXE", L"COM", L"SYS", L"OBJ", L"LIB", L"BIN" }; + size_t iext; + LPCWSTR pch, ext, pch1 = wcsrchr(filename, L'\'), pch2 = wcsrchr(filename, L'/'); + if (!pch1 && !pch2) + pch = filename; + else if (!pch1 && pch2) + pch = pch2; + else if (pch1 && !pch2) + pch = pch1; + else if (pch1 < pch2) + pch = pch2; + else + pch = pch1; + + ext = wcsrchr(pch, L'.'); + if (ext) + { + ++ext; + for (iext = 0; iext < _countof(s_exts); ++iext) + { + if (_wcsicmp(ext, s_exts[iext]) == 0) + return TRUE; + } + } + return FALSE; +} + +#define HasWildcard(filename) \ + ((wcschr((filename), L'*') != NULL) || (wcschr((filename), L'?') != NULL)) + +static FCRET FileCompare(FILECOMPARE *pFC) +{ + ConResPrintf(StdOut, IDS_COMPARING, pFC->file1, pFC->file2); + + if (!(pFC->dwFlags & FLAG_L) && + ((pFC->dwFlags & FLAG_B) || IsBinaryExt(pFC->file1) || IsBinaryExt(pFC->file2))) + { + return BinaryFileCompare(pFC); + } + return TextFileCompare(pFC); +} + +static FCRET WildcardFileCompare(FILECOMPARE *pFC) +{ + if (pFC->dwFlags & FLAG_HELP) + { + ConResPuts(StdOut, IDS_USAGE); + return FCRET_INVALID; + } + + if (!pFC->file1 || !pFC->file2) + { + ConResPuts(StdErr, IDS_NEEDS_FILES); + return FCRET_INVALID; + } + + if (HasWildcard(pFC->file1) || HasWildcard(pFC->file2)) + { + // TODO: wildcard + ConResPuts(StdErr, IDS_CANT_USE_WILDCARD); + } + + return FileCompare(pFC); +} + +int wmain(int argc, WCHAR **argv) +{ + FILECOMPARE fc = { .dwFlags = 0, .n = 100, .nnnn = 2 }; + wchar_t *endptr; + INT i; + + /* Initialize the Console Standard Streams */ + ConInitStdStreams(); + + for (i = 1; i < argc; ++i) + { + if (argv[i][0] != L'/') + { + if (!fc.file1) + fc.file1 = argv[i]; + else if (!fc.file2) + fc.file2 = argv[i]; + else + return InvalidSwitch(); + continue; + } + switch (towupper(argv[i][1])) + { + case L'A': + fc.dwFlags |= FLAG_A; + break; + case L'B': + fc.dwFlags |= FLAG_B; + break; + case L'C': + fc.dwFlags |= FLAG_C; + break; + case L'L': + if (_wcsicmp(argv[i], L"/L") == 0) + { + fc.dwFlags |= FLAG_L; + } + else if (towupper(argv[i][2]) == L'B') + { + if (iswdigit(argv[i][3])) + { + fc.dwFlags |= FLAG_LBn; + fc.n = wcstoul(&argv[i][3], &endptr, 10); + if (endptr == NULL || *endptr != 0) + return InvalidSwitch(); + } + else + { + return InvalidSwitch(); + } + } + break; + case L'N': + fc.dwFlags |= FLAG_N; + break; + case L'O': + if (_wcsicmp(argv[i], L"/OFF") == 0 || _wcsicmp(argv[i], L"/OFFLINE") == 0) + { + fc.dwFlags |= FLAG_OFFLINE; + } + break; + case L'T': + fc.dwFlags |= FLAG_T; + break; + case L'W': + fc.dwFlags |= FLAG_W; + break; + case L'0': case L'1': case L'2': case L'3': case L'4': + case L'5': case L'6': case L'7': case L'8': case L'9': + fc.nnnn = wcstoul(&argv[i][1], &endptr, 10); + if (endptr == NULL || *endptr != 0) + return InvalidSwitch(); + fc.dwFlags |= FLAG_nnnn; + break; + case L'?': + fc.dwFlags |= FLAG_HELP; + break; + default: + return InvalidSwitch(); + } + } + return WildcardFileCompare(&fc); +} diff --git a/base/applications/cmdutils/fc/fc.rc b/base/applications/cmdutils/fc/fc.rc new file mode 100644 index 00000000000..322c3678dee --- /dev/null +++ b/base/applications/cmdutils/fc/fc.rc @@ -0,0 +1,12 @@ +#include <windef.h> +#include "resource.h" + +#define REACTOS_STR_FILE_DESCRIPTION "ReactOS FC Command" +#define REACTOS_STR_INTERNAL_NAME "fc" +#define REACTOS_STR_ORIGINAL_FILENAME "fc.exe" +#include <reactos/version.rc> + +#pragma code_page(65001) /* UTF-8 */ +#ifdef LANGUAGE_EN_US + #include "lang/en-US.rc" +#endif diff --git a/base/applications/cmdutils/fc/lang/en-US.rc b/base/applications/cmdutils/fc/lang/en-US.rc new file mode 100644 index 00000000000..6c8516297b3 --- /dev/null +++ b/base/applications/cmdutils/fc/lang/en-US.rc @@ -0,0 +1,41 @@ +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +STRINGTABLE +BEGIN + IDS_USAGE "Compares two files or sets of files and displays the differences between\n\ +them\n\ +\n\ +FC [/A] [/C] [/L] [/LBn] [/N] [/OFF[LINE]] [/T] [/U] [/W] [/nnnn]\n\ + [drive1:][path1]filename1 [drive2:][path2]filename2\n\ +FC /B [drive1:][path1]filename1 [drive2:][path2]filename2\n\ +\n\ + /A Displays only first and last lines for each set of differences.\n\ + /B Performs a binary comparison.\n\ + /C Disregards the case of letters.\n\ + /L Compares files as ASCII text.\n\ + /LBn Sets the maximum consecutive mismatches to the specified\n\ + number of lines (default: 100).\n\ + /N Displays the line numbers on an ASCII comparison.\n\ + /OFF[LINE] Doesn't skip files with offline attribute set.\n\ + /T Doesn't expand tabs to spaces (default: expand).\n\ + /U Compare files as UNICODE text files.\n\ + /W Compresses white space (tabs and spaces) for comparison.\n\ + /nnnn Specifies the number of consecutive lines that must match\n\ + after a mismatch (default: 2).\n\ + [drive1:][path1]filename1\n\ + Specifies the first file or set of files to compare.\n\ + [drive2:][path2]filename2\n\ + Specifies the second file or set of files to compare.\n" + IDS_NO_DIFFERENCE "FC: no differences encountered\n" + IDS_LONGER_THAN "FC: %ls longer than %ls\n" + IDS_COMPARING "Comparing files %ls and %ls\n" + IDS_OUT_OF_MEMORY "FC: Out of memory\n" + IDS_CANNOT_READ "FC: cannot read from %ls\n" + IDS_INVALID_SWITCH "FC: Invalid Switch\n" + IDS_CANNOT_OPEN "FC: cannot open %ls - No such file or folder\n" + IDS_NEEDS_FILES "FC: Insufficient number of file specifications\n" + IDS_CANT_USE_WILDCARD "Wildcard ('*' and '?') are not supported yet\n" + IDS_DIFFERENT "FC: File %ls and %ls are different\n" + IDS_TOO_LARGE "FC: File %ls too large\n" + IDS_RESYNCH_FAILED "Resynch failed. Files are too different." +END diff --git a/base/applications/cmdutils/fc/resource.h b/base/applications/cmdutils/fc/resource.h new file mode 100644 index 00000000000..45f8e032bd8 --- /dev/null +++ b/base/applications/cmdutils/fc/resource.h @@ -0,0 +1,13 @@ +#define IDS_USAGE 1000 +#define IDS_NO_DIFFERENCE 1001 +#define IDS_LONGER_THAN 1002 +#define IDS_COMPARING 1003 +#define IDS_OUT_OF_MEMORY 1004 +#define IDS_CANNOT_READ 1005 +#define IDS_INVALID_SWITCH 1006 +#define IDS_CANNOT_OPEN 1007 +#define IDS_NEEDS_FILES 1008 +#define IDS_CANT_USE_WILDCARD 1009 +#define IDS_DIFFERENT 1010 +#define IDS_TOO_LARGE 1011 +#define IDS_RESYNCH_FAILED 1012