2 * Compare directory trees (Windows version)
20 #define REPARSE_POINT_MAX_SIZE (16 * 1024)
22 #define ARRAY_LEN(array) (sizeof(array) / sizeof((array)[0]))
25 win32_error_string(DWORD err_code)
27 static wchar_t buf[1024];
29 FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
30 NULL, err_code, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
31 buf, ARRAY_LEN(buf), NULL);
35 static void __attribute__((noreturn))
36 fatal_win32_error(const wchar_t *format, ...)
39 DWORD err = GetLastError();
42 fputws(L"FATAL ERROR: ", stderr);
43 vfwprintf(stderr, format, va);
44 fwprintf(stderr, L": %ls\n", win32_error_string(err));
50 static unsigned long difference_count = 0;
53 difference(const wchar_t *format, ...)
58 fputws(L"DIFFERENCE: ", stderr);
59 vfwprintf(stderr, format, va);
66 struct inode_mapping_node {
69 struct inode_mapping_node *left;
70 struct inode_mapping_node *right;
73 static struct inode_mapping_node *inode_map = NULL;
75 #define INODE_NOT_SEEN_YET ((u64)-1)
78 do_lookup_ino(struct inode_mapping_node *tree, u64 key)
81 return INODE_NOT_SEEN_YET;
83 return do_lookup_ino(tree->left, key);
85 return do_lookup_ino(tree->right, key);
90 do_insert(struct inode_mapping_node *tree, struct inode_mapping_node *node)
92 if (node->key < tree->key) {
94 return do_insert(tree->left, node);
98 if (node->key > tree->key) {
100 return do_insert(tree->right, node);
110 return do_lookup_ino(inode_map, key);
114 insert_ino(u64 key, u64 value)
116 struct inode_mapping_node *node = malloc(sizeof(*node));
125 do_insert(inode_map, node);
129 open_file(const wchar_t *path)
131 HANDLE hFile = CreateFile(path,
132 GENERIC_READ | ACCESS_SYSTEM_SECURITY,
133 FILE_SHARE_VALID_FLAGS,
136 FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
138 if (hFile == INVALID_HANDLE_VALUE)
139 fatal_win32_error(L"Failed to open file %ls read-only", path);
144 get_reparse_data(HANDLE hFile, const wchar_t *path, char *rpbuf)
146 DWORD bytesReturned = 0;
147 if (!DeviceIoControl(hFile, FSCTL_GET_REPARSE_POINT, NULL, 0,
148 rpbuf, REPARSE_POINT_MAX_SIZE, &bytesReturned, NULL))
149 fatal_win32_error(L"Can't get reparse data from %ls", path);
150 return bytesReturned;
154 cmp_reparse_data(HANDLE hFile_1, const wchar_t *path_1,
155 HANDLE hFile_2, const wchar_t *path_2)
157 char rpbuf_1[REPARSE_POINT_MAX_SIZE];
158 char rpbuf_2[REPARSE_POINT_MAX_SIZE];
162 len_1 = get_reparse_data(hFile_1, path_1, rpbuf_1);
163 len_2 = get_reparse_data(hFile_2, path_2, rpbuf_2);
164 if (len_1 != len_2 || memcmp(rpbuf_1, rpbuf_2, len_1))
165 difference(L"Reparse point buffers for %ls and %ls differ",
169 struct win32_stream_wrapper {
170 struct win32_stream_wrapper *next;
171 WIN32_FIND_STREAM_DATA dat;
175 cmp_FIND_STREAM_DATA_by_name(const void *p1, const void *p2)
177 const WIN32_FIND_STREAM_DATA *s1 = p1;
178 const WIN32_FIND_STREAM_DATA *s2 = p2;
179 return wcscmp(s1->cStreamName, s2->cStreamName);
182 static WIN32_FIND_STREAM_DATA *
183 get_stream_array(const wchar_t *path, size_t *nstreams_ret)
185 WIN32_FIND_STREAM_DATA dat;
186 WIN32_FIND_STREAM_DATA *array = NULL;
187 WIN32_FIND_STREAM_DATA *p;
189 struct win32_stream_wrapper *stream_list = NULL;
192 hFind = FindFirstStreamW(path, FindStreamInfoStandard, &dat, 0);
193 if (hFind != INVALID_HANDLE_VALUE) {
195 struct win32_stream_wrapper *wrapper;
197 wrapper = malloc(sizeof(*wrapper));
198 memcpy(&wrapper->dat, &dat, sizeof(dat));
199 wrapper->next = stream_list;
200 stream_list = wrapper;
202 } while (FindNextStreamW(hFind, &dat));
204 if (GetLastError() != ERROR_HANDLE_EOF)
205 fatal_win32_error(L"Can't lookup streams from %ls", path);
206 if (hFind != INVALID_HANDLE_VALUE)
208 array = malloc(nstreams * sizeof(array[0]));
210 while (stream_list) {
211 struct win32_stream_wrapper *next;
213 memcpy(p, &stream_list->dat, sizeof(*p));
214 next = stream_list->next;
219 assert(p - array == nstreams);
220 qsort(array, nstreams, sizeof(array[0]), cmp_FIND_STREAM_DATA_by_name);
221 *nstreams_ret = nstreams;
225 static const wchar_t *
226 fix_stream_name(wchar_t *stream_name)
230 /* The stream name should be returned as :NAME:TYPE */
231 if (stream_name[0] != L':')
233 colon = wcschr(stream_name + 1, L':');
236 if (wcscmp(colon + 1, L"$DATA"))
239 if (stream_name == colon - 1)
244 #define BUFSIZE 32768
247 cmp_data(HANDLE hFile_1, const wchar_t *path_1,
248 HANDLE hFile_2, const wchar_t *path_2, u64 size)
252 u64 bytes_remaining = size;
256 while (bytes_remaining) {
257 bytesToRead = BUFSIZE;
258 if (bytesToRead > bytes_remaining)
259 bytesToRead = bytes_remaining;
260 if (!ReadFile(hFile_1, buf_1, bytesToRead, &bytesRead, NULL) ||
261 bytesRead != bytesToRead)
263 fatal_win32_error(L"Error reading from %ls", path_1);
265 if (!ReadFile(hFile_2, buf_2, bytesToRead, &bytesRead, NULL) ||
266 bytesRead != bytesToRead)
268 fatal_win32_error(L"Error reading from %ls", path_2);
270 if (memcmp(buf_1, buf_2, bytesToRead))
271 difference(L"Data of %ls and %ls differs", path_1, path_2);
272 bytes_remaining -= bytesToRead;
277 cmp_stream(wchar_t *path_1, size_t path_1_len, WIN32_FIND_STREAM_DATA *dat_1,
278 wchar_t *path_2, size_t path_2_len, WIN32_FIND_STREAM_DATA *dat_2)
280 const wchar_t *stream_name;
282 /* Compare stream names */
283 if (wcscmp(dat_1->cStreamName, dat_2->cStreamName)) {
284 difference(L"Data streams %ls%ls and %ls%ls are not named the same",
285 path_1, dat_1->cStreamName,
286 path_2, dat_2->cStreamName);
290 /* Compare stream sizes */
291 if (dat_1->StreamSize.QuadPart != dat_2->StreamSize.QuadPart) {
292 difference(L"Data streams %ls%ls (%"PRIu64" bytes) and %ls%ls "
293 "(%"PRIu64" bytes) are not the same size",
294 path_1, dat_1->cStreamName, dat_1->StreamSize.QuadPart,
295 path_2, dat_2->cStreamName, dat_2->StreamSize.QuadPart);
299 /* Compare stream data */
301 stream_name = fix_stream_name(dat_1->cStreamName);
304 fwprintf(stderr, L"WARNING: unrecognized stream name format %ls\n",
309 wcscpy(&path_1[path_1_len], stream_name);
310 wcscpy(&path_2[path_2_len], stream_name);
312 HANDLE hFile_1 = open_file(path_1);
313 HANDLE hFile_2 = open_file(path_2);
315 cmp_data(hFile_1, path_1, hFile_2, path_2, dat_1->StreamSize.QuadPart);
317 CloseHandle(hFile_1);
318 CloseHandle(hFile_2);
319 path_1[path_1_len] = L'\0';
320 path_2[path_2_len] = L'\0';
324 cmp_streams(wchar_t *path_1, size_t path_1_len,
325 wchar_t *path_2, size_t path_2_len)
327 WIN32_FIND_STREAM_DATA *streams_1, *streams_2;
328 size_t nstreams_1, nstreams_2;
331 streams_1 = get_stream_array(path_1, &nstreams_1);
332 streams_2 = get_stream_array(path_2, &nstreams_2);
334 if (nstreams_1 != nstreams_2) {
335 difference(L"%ls and %ls do not have the same number of streams "
337 path_1, path_2, nstreams_1, nstreams_2);
341 for (i = 0; i < nstreams_1; i++)
342 cmp_stream(path_1, path_1_len, &streams_1[i],
343 path_2, path_2_len, &streams_2[i]);
350 struct win32_dentry_wrapper {
351 struct win32_dentry_wrapper *next;
356 qsort_cmp_dentries_by_name(const void *p1, const void *p2)
358 const WIN32_FIND_DATA *d1 = p1;
359 const WIN32_FIND_DATA *d2 = p2;
360 return wcscmp(d1->cFileName, d2->cFileName);
363 static WIN32_FIND_DATA *
364 get_dentry_array(wchar_t *path, size_t path_len, size_t *ndentries_ret)
367 WIN32_FIND_DATA *array = NULL;
369 size_t ndentries = 0;
370 struct win32_dentry_wrapper *dentry_list = NULL;
374 path[path_len] = L'\\';
375 path[path_len + 1] = L'*';
376 path[path_len + 2] = L'\0';
377 hFind = FindFirstFile(path, &dat);
378 path[path_len] = L'\0';
379 if (hFind != INVALID_HANDLE_VALUE) {
381 struct win32_dentry_wrapper *wrapper;
383 wrapper = malloc(sizeof(*wrapper));
384 memcpy(&wrapper->dat, &dat, sizeof(dat));
385 wrapper->next = dentry_list;
386 dentry_list = wrapper;
388 } while (FindNextFile(hFind, &dat));
390 err = GetLastError();
391 if (err != ERROR_NO_MORE_FILES && err != ERROR_FILE_NOT_FOUND)
392 fatal_win32_error(L"Can't lookup dentries from %ls", path);
393 if (hFind != INVALID_HANDLE_VALUE)
395 array = malloc(ndentries * sizeof(array[0]));
397 while (dentry_list) {
398 struct win32_dentry_wrapper *next;
400 memcpy(p, &dentry_list->dat, sizeof(*p));
401 next = dentry_list->next;
406 assert(p - array == ndentries);
407 qsort(array, ndentries, sizeof(array[0]), qsort_cmp_dentries_by_name);
408 *ndentries_ret = ndentries;
413 tree_cmp(wchar_t *path_1, size_t path_1_len, wchar_t *path_2, size_t path_2_len);
416 recurse_directory(wchar_t *path_1, size_t path_1_len,
417 wchar_t *path_2, size_t path_2_len)
419 WIN32_FIND_DATA *dentries_1, *dentries_2;
420 size_t ndentries_1, ndentries_2;
423 dentries_1 = get_dentry_array(path_1, path_1_len, &ndentries_1);
424 dentries_2 = get_dentry_array(path_2, path_2_len, &ndentries_2);
426 if (ndentries_1 != ndentries_2) {
427 difference(L"Directories %ls and %ls do not contain the "
428 "same number of entries", path_1, path_2);
432 path_1[path_1_len] = L'\\';
433 path_2[path_2_len] = L'\\';
434 for (i = 0; i < ndentries_1; i++) {
435 size_t name_1_len, name_2_len;
437 name_1_len = wcslen(dentries_1[i].cFileName);
438 name_2_len = wcslen(dentries_2[i].cFileName);
439 wmemcpy(&path_1[path_1_len + 1], dentries_1[i].cFileName, name_1_len + 1);
440 wmemcpy(&path_2[path_2_len + 1], dentries_2[i].cFileName, name_2_len + 1);
442 if (wcscmp(dentries_1[i].cFileName,
443 dentries_2[i].cFileName))
444 difference(L"%ls and %ls do not have the same name",
447 if (wcscmp(dentries_1[i].cAlternateFileName,
448 dentries_2[i].cAlternateFileName))
449 difference(L"%ls and %ls do not have the same short name "
450 "(%ls vs. %ls)", path_1, path_2,
451 dentries_1[i].cAlternateFileName,
452 dentries_2[i].cAlternateFileName);
454 if (!wcscmp(dentries_1[i].cFileName, L".") ||
455 !wcscmp(dentries_1[i].cFileName, L".."))
457 tree_cmp(path_1, path_1_len + 1 + name_1_len,
458 path_2, path_2_len + 1 + name_2_len);
462 path_1[path_1_len] = L'\0';
463 path_2[path_2_len] = L'\0';
469 file_times_equal(const FILETIME *t1, const FILETIME *t2)
471 return t1->dwLowDateTime == t2->dwLowDateTime &&
472 t1->dwHighDateTime == t2->dwHighDateTime;
476 get_security(const wchar_t *path, size_t *len_ret)
479 DWORD requestedInformation = DACL_SECURITY_INFORMATION |
480 SACL_SECURITY_INFORMATION |
481 OWNER_SECURITY_INFORMATION |
482 GROUP_SECURITY_INFORMATION |
483 BACKUP_SECURITY_INFORMATION;
487 bret = GetFileSecurity(path, requestedInformation, NULL, 0, &lenNeeded);
489 if (bret || GetLastError() != ERROR_INSUFFICIENT_BUFFER)
491 descr = malloc(lenNeeded);
492 if (!GetFileSecurity(path, requestedInformation, descr, lenNeeded,
495 *len_ret = lenNeeded;
498 fatal_win32_error(L"Can't read security descriptor of %ls", path);
502 get_security_descriptor_string(PSECURITY_DESCRIPTOR desc)
505 ConvertSecurityDescriptorToStringSecurityDescriptor(desc,
507 OWNER_SECURITY_INFORMATION |
508 GROUP_SECURITY_INFORMATION |
509 DACL_SECURITY_INFORMATION |
510 SACL_SECURITY_INFORMATION |
511 BACKUP_SECURITY_INFORMATION,
518 cmp_security(const wchar_t *path_1, const wchar_t *path_2)
520 void *descr_1, *descr_2;
522 const wchar_t *str_1, *str_2;
524 descr_1 = get_security(path_1, &len_1);
525 descr_2 = get_security(path_2, &len_2);
527 if (len_1 != len_2 || memcmp(descr_1, descr_2, len_1)) {
528 str_1 = get_security_descriptor_string(descr_1);
529 str_2 = get_security_descriptor_string(descr_2);
530 difference(L"%ls and %ls do not have the same security "
531 "descriptor:\n\t%ls\nvs.\n\t%ls",
532 path_1, path_2, str_1, str_2);
538 static const struct {
541 } file_attr_flags[] = {
542 {FILE_ATTRIBUTE_READONLY, L"READONLY"},
543 {FILE_ATTRIBUTE_HIDDEN, L"HIDDEN"},
544 {FILE_ATTRIBUTE_SYSTEM, L"SYSTEM"},
545 {FILE_ATTRIBUTE_DIRECTORY, L"DIRECTORY"},
546 {FILE_ATTRIBUTE_ARCHIVE, L"ARCHIVE"},
547 {FILE_ATTRIBUTE_DEVICE, L"DEVICE"},
548 {FILE_ATTRIBUTE_NORMAL, L"NORMAL"},
549 {FILE_ATTRIBUTE_TEMPORARY, L"TEMPORARY"},
550 {FILE_ATTRIBUTE_SPARSE_FILE, L"SPARSE_FILE"},
551 {FILE_ATTRIBUTE_REPARSE_POINT, L"REPARSE_POINT"},
552 {FILE_ATTRIBUTE_COMPRESSED, L"COMPRESSED"},
553 {FILE_ATTRIBUTE_OFFLINE, L"OFFLINE"},
554 {FILE_ATTRIBUTE_NOT_CONTENT_INDEXED, L"NOT_CONTENT_INDEXED"},
555 {FILE_ATTRIBUTE_ENCRYPTED, L"ENCRYPTED"},
556 {FILE_ATTRIBUTE_VIRTUAL, L"VIRTUAL"},
560 cmp_attributes(const wchar_t *path_1, const BY_HANDLE_FILE_INFORMATION *file_info_1,
561 const wchar_t *path_2, const BY_HANDLE_FILE_INFORMATION *file_info_2)
563 u32 attrib_1 = file_info_1->dwFileAttributes;
564 u32 attrib_2 = file_info_2->dwFileAttributes;
565 u32 differences = attrib_1 ^ attrib_2;
570 difference(L"Attributes for %ls (0x%"PRIx32") differ "
571 "from attributes for %ls (0x%"PRIx32"):",
572 path_1, attrib_1, path_2, attrib_2);
573 for (size_t i = 0; i < ARRAY_LEN(file_attr_flags); i++) {
574 if (differences & file_attr_flags[i].flag) {
575 const wchar_t *set_path;
576 const wchar_t *unset_path;
577 if (attrib_1 & file_attr_flags[i].flag) {
584 fwprintf(stderr, L"\t%ls has FILE_ATTRIBUTE_%ls set but %ls does not\n",
585 set_path, file_attr_flags[i].name, unset_path);
591 tree_cmp(wchar_t *path_1, size_t path_1_len, wchar_t *path_2, size_t path_2_len)
593 HANDLE hFile_1, hFile_2;
594 BY_HANDLE_FILE_INFORMATION file_info_1, file_info_2;
598 DWORD common_attribs;
600 /* Open each file. */
601 hFile_1 = open_file(path_1);
602 hFile_2 = open_file(path_2);
604 /* Get basic file information. */
605 if (!GetFileInformationByHandle(hFile_1, &file_info_1))
606 fatal_win32_error(L"Failed to get file information for %ls", path_1);
607 if (!GetFileInformationByHandle(hFile_2, &file_info_2))
608 fatal_win32_error(L"Failed to get file information for %ls", path_2);
610 /* Compare file attributes. */
611 cmp_attributes(path_1, &file_info_1, path_2, &file_info_2);
613 common_attribs = file_info_1.dwFileAttributes & file_info_2.dwFileAttributes;
615 /* Compare file sizes, unless the files are both directories in which
616 * cases the sizes can legitimately differ. */
617 if (!(common_attribs & FILE_ATTRIBUTE_DIRECTORY)) {
618 size_1 = ((u64)file_info_1.nFileSizeHigh << 32) | file_info_1.nFileSizeLow;
619 size_2 = ((u64)file_info_2.nFileSizeHigh << 32) | file_info_2.nFileSizeLow;
620 if (size_1 != size_2)
621 difference(L"Size for %ls (%"PRIu64") differs from size for %ls (%"PRIu64")",
622 path_1, size_1, path_2, size_2);
625 /* Compare file times. */
626 if (!file_times_equal(&file_info_1.ftCreationTime, &file_info_2.ftCreationTime))
627 difference(L"Creation times on %ls and %ls differ",
630 if (!file_times_equal(&file_info_1.ftLastWriteTime, &file_info_2.ftLastWriteTime))
631 difference(L"Last write times on %ls and %ls differ",
634 /* If we've detected a hard link in tree 1, check that we've detected
635 * the same hard link in tree 2. */
636 ino_1 = ((u64)file_info_1.nFileIndexHigh << 32) | file_info_1.nFileIndexLow;
637 ino_2 = ((u64)file_info_2.nFileIndexHigh << 32) | file_info_2.nFileIndexLow;
638 ino_to = lookup_ino(ino_1);
639 if (ino_to == INODE_NOT_SEEN_YET)
640 insert_ino(ino_1, ino_2);
641 else if (ino_to != ino_2)
642 difference(L"%ls and %ls are hard linked differently", path_1, path_2);
644 /* Compare security descriptors. */
645 cmp_security(path_1, path_2);
647 /* Compare data streams. */
648 cmp_streams(path_1, path_1_len, path_2, path_2_len);
650 /* Compare reparse data (if both files are reparse points) */
651 if (common_attribs & FILE_ATTRIBUTE_REPARSE_POINT)
652 cmp_reparse_data(hFile_1, path_1, hFile_2, path_2);
654 /* Recurse to directory (if both files are directories) */
655 if ((common_attribs & FILE_ATTRIBUTE_DIRECTORY) &&
656 !(common_attribs & FILE_ATTRIBUTE_REPARSE_POINT))
657 recurse_directory(path_1, path_1_len, path_2, path_2_len);
659 CloseHandle(hFile_1);
660 CloseHandle(hFile_2);
664 enable_privilege(const wchar_t *privilege)
668 TOKEN_PRIVILEGES newState;
670 if (!OpenProcessToken(GetCurrentProcess(),
671 TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken))
672 fatal_win32_error(L"Failed to open process token");
674 if (!LookupPrivilegeValue(NULL, privilege, &luid))
675 fatal_win32_error(L"Failed to look up privileges %ls", privilege);
677 newState.PrivilegeCount = 1;
678 newState.Privileges[0].Luid = luid;
679 newState.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
680 if (!AdjustTokenPrivileges(hToken, FALSE, &newState, 0, NULL, NULL))
681 fatal_win32_error(L"Failed to acquire privilege %ls", privilege);
686 wmain(int argc, wchar_t **argv)
688 wchar_t *path_1 = malloc(32768 * sizeof(wchar_t));
689 wchar_t *path_2 = malloc(32768 * sizeof(wchar_t));
693 fwprintf(stderr, L"Usage: win32-tree-cmp DIR1 DIR2\n");
697 enable_privilege(SE_BACKUP_NAME);
698 enable_privilege(SE_SECURITY_NAME);
700 len_1 = wcslen(argv[1]);
701 len_2 = wcslen(argv[2]);
702 wmemcpy(path_1, argv[1], len_1 + 1);
703 wmemcpy(path_2, argv[2], len_2 + 1);
705 tree_cmp(path_1, len_1, path_2, len_2);
707 if (difference_count) {
708 fwprintf(stderr, L"Found %lu differences; exiting with failure status.\n",