X-Git-Url: https://wimlib.net/git/?a=blobdiff_plain;f=src%2Ftest_support.c;h=5c566dcb25db164e5fc9a1d42e550df017e6d6fe;hb=HEAD;hp=a7a92f52ef790e07b746a7908eb32475b04b5c37;hpb=88afc46984c143cbf928a3186f8cc93d819e9269;p=wimlib diff --git a/src/test_support.c b/src/test_support.c index a7a92f52..6a438fd9 100644 --- a/src/test_support.c +++ b/src/test_support.c @@ -3,7 +3,7 @@ */ /* - * Copyright (C) 2015-2016 Eric Biggers + * Copyright 2015-2023 Eric Biggers * * This file is free software; you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free @@ -16,7 +16,7 @@ * details. * * You should have received a copy of the GNU Lesser General Public License - * along with this file; if not, see http://www.gnu.org/licenses/. + * along with this file; if not, see https://www.gnu.org/licenses/. */ /* @@ -35,6 +35,14 @@ #include #include +#include +#include +#include +#ifdef _WIN32 +# include +# include +# undef ERROR +#endif #include "wimlib.h" #include "wimlib/endianness.h" @@ -47,6 +55,9 @@ #include "wimlib/scan.h" #include "wimlib/security_descriptor.h" #include "wimlib/test_support.h" +#include "wimlib/timestamp.h" +#include "wimlib/unix_data.h" +#include "wimlib/xattr.h" /*----------------------------------------------------------------------------* * File tree generation * @@ -58,20 +69,26 @@ struct generation_context { bool metadata_only; }; +static u64 random_state; + +WIMLIBAPI void +wimlib_seed_random(u64 seed) +{ + random_state = seed; +} + static u32 rand32(void) { - static u64 state = 0x55DB93D0AB838771; - - /* A simple linear congruential generator */ - state = (state * 25214903917 + 11) & ((1ULL << 48) - 1); - return state >> 16; + /* A simple linear congruential generator */ + random_state = (random_state * 25214903917 + 11) % (1ULL << 48); + return random_state >> 16; } static bool randbool(void) { - return (rand32() & 1) != 0; + return rand32() % 2; } static u8 @@ -95,10 +112,18 @@ rand64(void) static u64 generate_random_timestamp(void) { - /* When setting timestamps on Windows: + u64 ts; + + if (randbool()) + ts = rand64(); + else + ts = time_t_to_wim_timestamp(rand64() % (1ULL << 34)); + /* + * When setting timestamps on Windows: * - 0 is a special value meaning "not specified" - * - if the high bit is set you get STATUS_INVALID_PARAMETER */ - return (1 + rand64()) & ~(1ULL << 63); + * - if the high bit is set you get STATUS_INVALID_PARAMETER + */ + return max(1, ts % (1ULL << 63)); } static inline bool @@ -121,7 +146,7 @@ is_valid_windows_filename_char(utf16lechar c) static inline bool is_valid_filename_char(utf16lechar c) { -#ifdef __WIN32__ +#ifdef _WIN32 return is_valid_windows_filename_char(c); #else return c != cpu_to_le16('\0') && c != cpu_to_le16('/'); @@ -163,7 +188,7 @@ retry: /* Generate the characters in the name. */ for (int i = 0; i < len; i++) { do { - name[i] = rand16(); + name[i] = cpu_to_le16(rand16()); } while (!is_valid_filename_char(name[i])); } @@ -377,6 +402,172 @@ generate_random_security_descriptor(void *_desc, struct generation_context *ctx) return p - (u8 *)desc; } +static bool +am_root(void) +{ +#ifdef _WIN32 + return false; +#else + return (getuid() == 0); +#endif +} + +static u32 +generate_uid(void) +{ +#ifdef _WIN32 + return 0; +#else + if (am_root()) + return rand32(); + return getuid(); +#endif +} + +static u32 +generate_gid(void) +{ +#ifdef _WIN32 + return 0; +#else + if (am_root()) + return rand32(); + return getgid(); +#endif +} + +#ifdef _WIN32 +# ifndef S_IFLNK +# define S_IFLNK 0120000 +# endif +# ifndef S_IFSOCK +# define S_IFSOCK 0140000 +# endif +#endif + +static int +set_random_unix_metadata(struct wim_inode *inode) +{ + struct wimlib_unix_data dat; + + dat.uid = generate_uid(); + dat.gid = generate_gid(); + if (inode_is_symlink(inode)) + dat.mode = S_IFLNK | 0777; + else if (inode->i_attributes & FILE_ATTRIBUTE_DIRECTORY) + dat.mode = S_IFDIR | 0700 | (rand32() % 07777); + else if (is_zero_hash(inode_get_hash_of_unnamed_data_stream(inode)) && + randbool() && am_root()) + { + dat.mode = rand32() % 07777; + switch (rand32() % 4) { + case 0: + dat.mode |= S_IFIFO; + break; + case 1: + dat.mode |= S_IFCHR; + dat.rdev = 261; /* /dev/zero */ + break; + case 2: + dat.mode |= S_IFBLK; + dat.rdev = 261; /* /dev/zero */ + break; + default: + dat.mode |= S_IFSOCK; + break; + } + } else { + dat.mode = S_IFREG | 0400 | (rand32() % 07777); + } + dat.rdev = 0; + + if (!inode_set_unix_data(inode, &dat, UNIX_DATA_ALL)) + return WIMLIB_ERR_NOMEM; + + return 0; +} + +static noinline_for_stack int +set_random_xattrs(struct wim_inode *inode) +{ + int num_xattrs = 1 + rand32() % 16; + char entries[8192]; + struct wim_xattr_entry *entry = (void *)entries; + size_t entries_size; + struct wimlib_unix_data unix_data; +#ifdef _WIN32 + const char *prefix = ""; +#else + const char *prefix = "user."; +#endif + static const char capability_name[] = "security.capability"; + bool generated_capability_xattr = false; + + /* + * On Linux, xattrs in the "user" namespace are only permitted on + * regular files and directories. For other types of files we can use + * the "trusted" namespace, but this requires root. + */ + if (inode_is_symlink(inode) || + (inode_get_unix_data(inode, &unix_data) && + !S_ISREG(unix_data.mode) && !S_ISDIR(unix_data.mode))) + { + if (!am_root()) + return 0; + prefix = "trusted."; + } + + for (int i = 0; i < num_xattrs; i++) { + int value_len = rand32() % 64; + u8 *p; + + #ifdef _WIN32 + if (value_len == 0) + value_len++; + #endif + + entry->value_len = cpu_to_le16(value_len); + entry->flags = 0; + + if (rand32() % 16 == 0 && am_root() && + !generated_capability_xattr) { + int name_len = sizeof(capability_name) - 1; + entry->name_len = name_len; + p = mempcpy(entry->name, capability_name, name_len + 1); + generated_capability_xattr = true; + } else { + int name_len = 1 + rand32() % 64; + + entry->name_len = strlen(prefix) + name_len; + p = mempcpy(entry->name, prefix, strlen(prefix)); + *p++ = 'A' + i; + for (int j = 1; j < name_len; j++) { + do { + #ifdef _WIN32 + *p = 'A' + rand8() % 26; + #else + *p = rand8(); + #endif + } while (*p == '\0'); + p++; + } + *p++ = '\0'; + } + for (int j = 0; j < value_len; j++) + *p++ = rand8(); + + entry = (void *)p; + } + + entries_size = (char *)entry - entries; + wimlib_assert(entries_size > 0 && entries_size <= sizeof(entries)); + + if (!inode_set_xattrs(inode, entries, entries_size)) + return WIMLIB_ERR_NOMEM; + + return 0; +} + static int set_random_metadata(struct wim_inode *inode, struct generation_context *ctx) { @@ -398,7 +589,7 @@ set_random_metadata(struct wim_inode *inode, struct generation_context *ctx) /* Security descriptor */ if (randbool()) { - char desc[8192] _aligned_attribute(8); + char desc[8192] __attribute__((aligned(8))); size_t size; size = generate_random_security_descriptor(desc, ctx); @@ -421,6 +612,20 @@ set_random_metadata(struct wim_inode *inode, struct generation_context *ctx) return WIMLIB_ERR_NOMEM; } + /* Standard UNIX permissions and special files */ + if (rand32() % 16 == 0) { + int ret = set_random_unix_metadata(inode); + if (ret) + return ret; + } + + /* Extended attributes */ + if (rand32() % 32 == 0) { + int ret = set_random_xattrs(inode); + if (ret) + return ret; + } + return 0; } @@ -507,36 +712,6 @@ generate_data(u8 *buffer, size_t size, struct generation_context *ctx) } } -static int -add_stream(struct wim_inode *inode, struct generation_context *ctx, - int stream_type, const utf16lechar *stream_name, - void *buffer, size_t size) -{ - struct blob_descriptor *blob = NULL; - struct wim_inode_stream *strm; - - if (size) { - blob = new_blob_descriptor(); - if (!blob) - goto err_nomem; - blob->attached_buffer = buffer; - blob->blob_location = BLOB_IN_ATTACHED_BUFFER; - blob->size = size; - } - - strm = inode_add_stream(inode, stream_type, stream_name, blob); - if (unlikely(!strm)) - goto err_nomem; - - prepare_unhashed_blob(blob, inode, strm->stream_id, - ctx->params->unhashed_blobs); - return 0; - -err_nomem: - free_blob_descriptor(blob); - return WIMLIB_ERR_NOMEM; -} - static noinline_for_stack int set_random_reparse_point(struct wim_inode *inode, struct generation_context *ctx) { @@ -598,6 +773,7 @@ add_random_data_stream(struct wim_inode *inode, struct generation_context *ctx, { void *buffer = NULL; size_t size; + int ret; size = select_stream_size(ctx); if (size) { @@ -607,8 +783,12 @@ add_random_data_stream(struct wim_inode *inode, struct generation_context *ctx, generate_data(buffer, size, ctx); } - return add_stream(inode, ctx, STREAM_TYPE_DATA, stream_name, - buffer, size); + ret = 0; + if (!inode_add_stream_with_data(inode, STREAM_TYPE_DATA, stream_name, + buffer, size, ctx->params->blob_table)) + ret = WIMLIB_ERR_NOMEM; + FREE(buffer); + return ret; } static int @@ -641,7 +821,8 @@ set_random_streams(struct wim_inode *inode, struct generation_context *ctx) ret = add_random_data_stream(inode, ctx, stream_name); if (ret) return ret; - stream_name[0] += cpu_to_le16(1); + stream_name[0] = + cpu_to_le16(le16_to_cpu(stream_name[0]) + 1); } } @@ -743,7 +924,7 @@ retry: * within the same directory. */ hash = 0; for (const utf16lechar *p = name; *p; p++) - hash = (hash * 31) + *p; + hash = (hash * 31) + le16_to_cpu(*p); FREE(child->d_short_name); child->d_short_name = memdup(name, (name_len + 1) * 2); child->d_short_name_nbytes = name_len * 2; @@ -1087,14 +1268,11 @@ cmp_attributes(const struct wim_inode *inode1, !inode_is_symlink(inode1))) goto mismatch; - /* SPARSE_FILE may be cleared in UNIX and NTFS-3G modes, or in Windows - * mode if the inode is a directory. */ + /* SPARSE_FILE may be cleared. This is true in UNIX and NTFS-3G modes. + * In Windows mode it should only be true for directories, but even on + * nondirectories it doesn't work 100% of the time for some reason. */ if ((changed & FILE_ATTRIBUTE_SPARSE_FILE) && - !((cleared & FILE_ATTRIBUTE_SPARSE_FILE) && - ((cmp_flags & (WIMLIB_CMP_FLAG_UNIX_MODE | - WIMLIB_CMP_FLAG_NTFS_3G_MODE)) || - ((cmp_flags & WIMLIB_CMP_FLAG_WINDOWS_MODE) && - (inode1->i_attributes & FILE_ATTRIBUTE_DIRECTORY))))) + !(cleared & FILE_ATTRIBUTE_SPARSE_FILE)) goto mismatch; /* COMPRESSED may change in UNIX and NTFS-3G modes. (It *should* be @@ -1131,6 +1309,73 @@ mismatch: return WIMLIB_ERR_IMAGES_ARE_DIFFERENT; } +static void +print_security_descriptor(const void *desc, size_t size, FILE *fp) +{ + print_byte_field(desc, size, fp); +#ifdef _WIN32 + wchar_t *str = NULL; + ConvertSecurityDescriptorToStringSecurityDescriptorW( + (void *)desc, + SDDL_REVISION_1, + OWNER_SECURITY_INFORMATION | + GROUP_SECURITY_INFORMATION | + DACL_SECURITY_INFORMATION | + SACL_SECURITY_INFORMATION, + &str, + NULL); + if (str) { + fprintf(fp, " [ %ls ]", str); + LocalFree(str); + } +#endif /* _WIN32 */ +} + +static int +cmp_security(const struct wim_inode *inode1, const struct wim_inode *inode2, + const struct wim_image_metadata *imd1, + const struct wim_image_metadata *imd2, int cmp_flags) +{ + /* + * Unfortunately this has to be disabled on Windows for now, since + * Windows changes security descriptors upon backup/restore in ways that + * are difficult to replicate... + */ + if (cmp_flags & WIMLIB_CMP_FLAG_WINDOWS_MODE) + return 0; + + if (inode_has_security_descriptor(inode1)) { + if (inode_has_security_descriptor(inode2)) { + const void *desc1 = imd1->security_data->descriptors[inode1->i_security_id]; + const void *desc2 = imd2->security_data->descriptors[inode2->i_security_id]; + size_t size1 = imd1->security_data->sizes[inode1->i_security_id]; + size_t size2 = imd2->security_data->sizes[inode2->i_security_id]; + + if (size1 != size2 || memcmp(desc1, desc2, size1)) { + ERROR("Security descriptor of %"TS" differs!", + inode_any_full_path(inode1)); + fprintf(stderr, "desc1="); + print_security_descriptor(desc1, size1, stderr); + fprintf(stderr, "\ndesc2="); + print_security_descriptor(desc2, size2, stderr); + fprintf(stderr, "\n"); + return WIMLIB_ERR_IMAGES_ARE_DIFFERENT; + } + } else if (!(cmp_flags & WIMLIB_CMP_FLAG_UNIX_MODE)) { + ERROR("%"TS" has a security descriptor in the first image but " + "not in the second image!", inode_any_full_path(inode1)); + return WIMLIB_ERR_IMAGES_ARE_DIFFERENT; + } + } else if (inode_has_security_descriptor(inode2)) { + /* okay --- consider it acceptable if a default security + * descriptor was assigned */ + /*ERROR("%"TS" has a security descriptor in the second image but "*/ + /*"not in the first image!", inode_any_full_path(inode1));*/ + /*return WIMLIB_ERR_IMAGES_ARE_DIFFERENT;*/ + } + return 0; +} + static int cmp_object_ids(const struct wim_inode *inode1, const struct wim_inode *inode2, int cmp_flags) @@ -1172,6 +1417,234 @@ cmp_object_ids(const struct wim_inode *inode1, return 0; } +static int +cmp_unix_metadata(const struct wim_inode *inode1, + const struct wim_inode *inode2, int cmp_flags) +{ + struct wimlib_unix_data dat1, dat2; + bool present1, present2; + + present1 = inode_get_unix_data(inode1, &dat1); + present2 = inode_get_unix_data(inode2, &dat2); + + if (!present1 && !present2) + return 0; + + if (present1 && !present2) { + if (cmp_flags & (WIMLIB_CMP_FLAG_NTFS_3G_MODE | + WIMLIB_CMP_FLAG_WINDOWS_MODE)) + return 0; + ERROR("%"TS" unexpectedly lost its UNIX metadata", + inode_any_full_path(inode1)); + return WIMLIB_ERR_IMAGES_ARE_DIFFERENT; + } + + if (!present1 && present2) { + if (cmp_flags & WIMLIB_CMP_FLAG_UNIX_MODE) + return 0; + ERROR("%"TS" unexpectedly gained UNIX metadata", + inode_any_full_path(inode1)); + return WIMLIB_ERR_IMAGES_ARE_DIFFERENT; + } + + if (memcmp(&dat1, &dat2, sizeof(dat1)) != 0) { + ERROR("UNIX metadata of %"TS" differs: " + "[uid=%u, gid=%u, mode=0%o, rdev=%u] vs. " + "[uid=%u, gid=%u, mode=0%o, rdev=%u]", + inode_any_full_path(inode1), + dat1.uid, dat1.gid, dat1.mode, dat1.rdev, + dat2.uid, dat2.gid, dat2.mode, dat2.rdev); + return WIMLIB_ERR_IMAGES_ARE_DIFFERENT; + } + + return 0; +} + +static int +cmp_xattr_names(const void *p1, const void *p2) +{ + const struct wim_xattr_entry *entry1 = *(const struct wim_xattr_entry **)p1; + const struct wim_xattr_entry *entry2 = *(const struct wim_xattr_entry **)p2; + int res; + + res = entry1->name_len - entry2->name_len; + if (res) + return res; + + return memcmp(entry1->name, entry2->name, entry1->name_len); +} + +/* Validate and sort by name a list of extended attributes */ +static int +parse_xattrs(const void *xattrs, u32 len, + const struct wim_xattr_entry *entries[], + u32 *num_entries_p) +{ + u32 limit = *num_entries_p; + u32 num_entries = 0; + const struct wim_xattr_entry *entry = xattrs; + + while ((void *)entry < xattrs + len) { + if (!valid_xattr_entry(entry, xattrs + len - (void *)entry)) { + ERROR("Invalid xattr entry"); + return WIMLIB_ERR_INVALID_XATTR; + } + if (num_entries >= limit) { + ERROR("Too many xattr entries"); + return WIMLIB_ERR_INVALID_XATTR; + } + entries[num_entries++] = entry; + entry = xattr_entry_next(entry); + } + + if (num_entries == 0) { + ERROR("No xattr entries"); + return WIMLIB_ERR_INVALID_XATTR; + } + + qsort(entries, num_entries, sizeof(entries[0]), cmp_xattr_names); + + for (u32 i = 1; i < num_entries; i++) { + if (cmp_xattr_names(&entries[i - 1], &entries[i]) == 0) { + ERROR("Duplicate xattr names"); + return WIMLIB_ERR_INVALID_XATTR; + } + } + + *num_entries_p = num_entries; + return 0; +} + +static int +cmp_xattrs(const struct wim_inode *inode1, const struct wim_inode *inode2, + int cmp_flags) +{ + const void *xattrs1, *xattrs2; + u32 len1, len2; + + xattrs1 = inode_get_xattrs(inode1, &len1); + xattrs2 = inode_get_xattrs(inode2, &len2); + + if (!xattrs1 && !xattrs2) { + return 0; + } else if (xattrs1 && !xattrs2) { + if (cmp_flags & WIMLIB_CMP_FLAG_NTFS_3G_MODE) + return 0; + ERROR("%"TS" unexpectedly lost its xattrs", + inode_any_full_path(inode1)); + return WIMLIB_ERR_IMAGES_ARE_DIFFERENT; + } else if (!xattrs1 && xattrs2) { + ERROR("%"TS" unexpectedly gained xattrs", + inode_any_full_path(inode1)); + return WIMLIB_ERR_IMAGES_ARE_DIFFERENT; + } else { + const int max_entries = 64; + const struct wim_xattr_entry *entries1[max_entries]; + const struct wim_xattr_entry *entries2[max_entries]; + u32 xattr_count1 = max_entries; + u32 xattr_count2 = max_entries; + int ret; + + ret = parse_xattrs(xattrs1, len1, entries1, &xattr_count1); + if (ret) { + ERROR("%"TS": invalid xattrs", + inode_any_full_path(inode1)); + return ret; + } + ret = parse_xattrs(xattrs2, len2, entries2, &xattr_count2); + if (ret) { + ERROR("%"TS": invalid xattrs", + inode_any_full_path(inode2)); + return ret; + } + if (xattr_count1 != xattr_count2) { + ERROR("%"TS": number of xattrs changed. had %u " + "before, now has %u", inode_any_full_path(inode1), + xattr_count1, xattr_count2); + } + for (u32 i = 0; i < xattr_count1; i++) { + const struct wim_xattr_entry *entry1 = entries1[i]; + const struct wim_xattr_entry *entry2 = entries2[i]; + + if (entry1->value_len != entry2->value_len || + entry1->name_len != entry2->name_len || + entry1->flags != entry2->flags || + memcmp(entry1->name, entry2->name, + entry1->name_len) || + memcmp(entry1->name + entry1->name_len + 1, + entry2->name + entry2->name_len + 1, + le16_to_cpu(entry1->value_len))) + { + ERROR("xattr %.*s of %"TS" differs", + entry1->name_len, entry1->name, + inode_any_full_path(inode1)); + return WIMLIB_ERR_IMAGES_ARE_DIFFERENT; + } + } + return 0; + } +} + +/* + * ext4 only supports timestamps from years 1901 to 2446, more specifically the + * range [-0x80000000, 0x380000000) seconds relative to the start of UNIX epoch. + */ +static bool +in_ext4_range(u64 ts) +{ + return ts >= time_t_to_wim_timestamp(-0x80000000LL) && + ts < time_t_to_wim_timestamp(0x380000000LL); +} + +static bool +timestamps_differ(u64 ts1, u64 ts2, int cmp_flags) +{ + if (ts1 == ts2) + return false; + if ((cmp_flags & WIMLIB_CMP_FLAG_EXT4) && + (!in_ext4_range(ts1) || !in_ext4_range(ts2))) + return false; + return true; +} + +static int +cmp_timestamps(const struct wim_inode *inode1, const struct wim_inode *inode2, + int cmp_flags) +{ + if (timestamps_differ(inode1->i_creation_time, + inode2->i_creation_time, cmp_flags) && + !(cmp_flags & WIMLIB_CMP_FLAG_UNIX_MODE)) { + ERROR("Creation time of %"TS" differs; %"PRIu64" != %"PRIu64, + inode_any_full_path(inode1), + inode1->i_creation_time, inode2->i_creation_time); + return WIMLIB_ERR_IMAGES_ARE_DIFFERENT; + } + + if (timestamps_differ(inode1->i_last_write_time, + inode2->i_last_write_time, cmp_flags)) { + ERROR("Last write time of %"TS" differs; %"PRIu64" != %"PRIu64, + inode_any_full_path(inode1), + inode1->i_last_write_time, inode2->i_last_write_time); + return WIMLIB_ERR_IMAGES_ARE_DIFFERENT; + } + + if (timestamps_differ(inode1->i_last_access_time, + inode2->i_last_access_time, cmp_flags) && + /* + * On Windows, sometimes a file's last access time will end up as + * the current time rather than the expected time. Maybe caused by + * some OS process scanning the files? + */ + !(cmp_flags & WIMLIB_CMP_FLAG_WINDOWS_MODE)) { + ERROR("Last access time of %"TS" differs; %"PRIu64" != %"PRIu64, + inode_any_full_path(inode1), + inode1->i_last_access_time, inode2->i_last_access_time); + return WIMLIB_ERR_IMAGES_ARE_DIFFERENT; + } + + return 0; +} + static int cmp_inodes(const struct wim_inode *inode1, const struct wim_inode *inode2, const struct wim_image_metadata *imd1, @@ -1185,30 +1658,9 @@ cmp_inodes(const struct wim_inode *inode1, const struct wim_inode *inode2, return ret; /* Compare security descriptors */ - if (inode_has_security_descriptor(inode1)) { - if (inode_has_security_descriptor(inode2)) { - const void *desc1 = imd1->security_data->descriptors[inode1->i_security_id]; - const void *desc2 = imd2->security_data->descriptors[inode2->i_security_id]; - size_t size1 = imd1->security_data->sizes[inode1->i_security_id]; - size_t size2 = imd2->security_data->sizes[inode2->i_security_id]; - - if (size1 != size2 || memcmp(desc1, desc2, size1)) { - ERROR("Security descriptor of %"TS" differs!", - inode_any_full_path(inode1)); - return WIMLIB_ERR_IMAGES_ARE_DIFFERENT; - } - } else if (!(cmp_flags & WIMLIB_CMP_FLAG_UNIX_MODE)) { - ERROR("%"TS" has a security descriptor in the first image but " - "not in the second image!", inode_any_full_path(inode1)); - return WIMLIB_ERR_IMAGES_ARE_DIFFERENT; - } - } else if (inode_has_security_descriptor(inode2)) { - /* okay --- consider it acceptable if a default security - * descriptor was assigned */ - /*ERROR("%"TS" has a security descriptor in the second image but "*/ - /*"not in the first image!", inode_any_full_path(inode1));*/ - /*return WIMLIB_ERR_IMAGES_ARE_DIFFERENT;*/ - } + ret = cmp_security(inode1, inode2, imd1, imd2, cmp_flags); + if (ret) + return ret; /* Compare streams */ for (unsigned i = 0; i < inode1->i_num_streams; i++) { @@ -1252,6 +1704,21 @@ cmp_inodes(const struct wim_inode *inode1, const struct wim_inode *inode2, if (ret) return ret; + /* Compare timestamps */ + ret = cmp_timestamps(inode1, inode2, cmp_flags); + if (ret) + return ret; + + /* Compare standard UNIX metadata */ + ret = cmp_unix_metadata(inode1, inode2, cmp_flags); + if (ret) + return ret; + + /* Compare extended attributes */ + ret = cmp_xattrs(inode1, inode2, cmp_flags); + if (ret) + return ret; + return 0; }