]> wimlib.net Git - wimlib/blobdiff - src/test_support.c
mount_image.c: add fallback definitions of RENAME_* constants
[wimlib] / src / test_support.c
index a7a92f52ef790e07b746a7908eb32475b04b5c37..6a438fd9396fb3b34c6e7b53b4132bc4259e65a4 100644 (file)
@@ -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/.
  */
 
 /*
 
 #include <ctype.h>
 #include <math.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#ifdef _WIN32
+#  include <windows.h>
+#  include <sddl.h>
+#  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;
 }