]> wimlib.net Git - wimlib/blobdiff - src/test_support.c
wlfuzz: don't generate hard linked reparse points
[wimlib] / src / test_support.c
index 0db98e654158ecbb1846217eb0c380aaaff24775..15c707773bc73b6e26e8e3a8e9beeeb14615bc4c 100644 (file)
@@ -251,13 +251,13 @@ generate_random_security_descriptor(void *_desc, struct generation_context *ctx)
 static int
 set_random_metadata(struct wim_inode *inode, struct generation_context *ctx)
 {
-       u32 v = rand32();
-       u32 attrib = (v & (FILE_ATTRIBUTE_READONLY |
-                          FILE_ATTRIBUTE_HIDDEN |
-                          FILE_ATTRIBUTE_SYSTEM |
-                          FILE_ATTRIBUTE_ARCHIVE |
-                          FILE_ATTRIBUTE_NOT_CONTENT_INDEXED |
-                          FILE_ATTRIBUTE_COMPRESSED));
+       u32 attrib = (rand32() & (FILE_ATTRIBUTE_READONLY |
+                                 FILE_ATTRIBUTE_HIDDEN |
+                                 FILE_ATTRIBUTE_SYSTEM |
+                                 FILE_ATTRIBUTE_ARCHIVE |
+                                 FILE_ATTRIBUTE_NOT_CONTENT_INDEXED |
+                                 FILE_ATTRIBUTE_COMPRESSED |
+                                 FILE_ATTRIBUTE_SPARSE_FILE));
 
        /* File attributes  */
        inode->i_attributes |= attrib;
@@ -323,8 +323,10 @@ generate_data(u8 *buffer, size_t size, struct generation_context *ctx)
        size_t mask = -1;
        size_t num_byte_fills = rand32() % 256;
 
+       /* Start by initializing to a random byte */
        memset(buffer, rand32() % 256, size);
 
+       /* Add some random bytes in some random places */
        for (size_t i = 0; i < num_byte_fills; i++) {
                u8 b = rand8();
 
@@ -342,6 +344,7 @@ generate_data(u8 *buffer, size_t size, struct generation_context *ctx)
                        mask = (size_t)-1 << rand32() % 4;
        }
 
+       /* Sometimes add a wave pattern */
        if (rand32() % 8 == 0) {
                double magnitude = rand32() % 128;
                double scale = 1.0 / (1 + (rand32() % 256));
@@ -349,6 +352,17 @@ generate_data(u8 *buffer, size_t size, struct generation_context *ctx)
                for (size_t i = 0; i < size; i++)
                        buffer[i] += (int)(magnitude * cos(i * scale));
        }
+
+       /* Sometimes add some zero regions (holes) */
+       if (rand32() % 4 == 0) {
+               size_t num_holes = 1 + (rand32() % 16);
+               for (size_t i = 0; i < num_holes; i++) {
+                       size_t hole_offset = rand32() % size;
+                       size_t hole_len = min(size - hole_offset,
+                                             size / (1 + (rand32() % 16)));
+                       memset(&buffer[hole_offset], 0, hole_len);
+               }
+       }
 }
 
 static int
@@ -432,14 +446,13 @@ add_random_data_stream(struct wim_inode *inode, struct generation_context *ctx,
 }
 
 static int
-set_random_streams(struct wim_inode *inode, struct generation_context *ctx,
-                  bool reparse_ok)
+set_random_streams(struct wim_inode *inode, struct generation_context *ctx)
 {
        int ret;
        u32 r;
 
        /* Reparse point (sometimes)  */
-       if (reparse_ok && rand32() % 8 == 0) {
+       if (inode->i_attributes & FILE_ATTRIBUTE_REPARSE_POINT) {
                ret = set_random_reparse_point(inode, ctx);
                if (ret)
                        return ret;
@@ -468,37 +481,131 @@ set_random_streams(struct wim_inode *inode, struct generation_context *ctx,
        return 0;
 }
 
+static inline bool
+is_valid_windows_filename_char(utf16lechar c)
+{
+       return le16_to_cpu(c) > 31 &&
+               c != cpu_to_le16('/') &&
+               c != cpu_to_le16('<') &&
+               c != cpu_to_le16('>') &&
+               c != cpu_to_le16(':') &&
+               c != cpu_to_le16('"') &&
+               c != cpu_to_le16('/' ) &&
+               c != cpu_to_le16('\\') &&
+               c != cpu_to_le16('|') &&
+               c != cpu_to_le16('?') &&
+               c != cpu_to_le16('*');
+}
+
+/* Is the character valid in a filename on the current platform? */
+static inline bool
+is_valid_filename_char(utf16lechar c)
+{
+#ifdef __WIN32__
+       return is_valid_windows_filename_char(c);
+#else
+       return c != cpu_to_le16('\0') && c != cpu_to_le16('/');
+#endif
+}
+
+/* Generate a random filename and return its length. */
 static int
-generate_random_file_name(tchar name[], int max_len,
-                         struct generation_context *ctx)
+generate_random_filename(utf16lechar name[], int max_len,
+                        struct generation_context *ctx)
 {
-       int length;
+       int len;
+
+       /* Choose the length of the name. */
        switch (rand32() % 8) {
        default:
                /* short name  */
-               length = 1 + (rand32() % 6);
+               len = 1 + (rand32() % 6);
                break;
        case 2:
        case 3:
        case 4:
                /* medium-length name  */
-               length = 7 + (rand32() % 8);
+               len = 7 + (rand32() % 8);
                break;
        case 5:
        case 6:
                /* long name  */
-               length = 15 + (rand32() % 15);
+               len = 15 + (rand32() % 15);
                break;
        case 7:
                /* very long name  */
-               length = 30 + (rand32() % 90);
+               len = 30 + (rand32() % 90);
                break;
        }
-       length = min(length, max_len);
-       for (int i = 0; i < length; i++)
-               name[i] = 'a' + (rand32() % 26);
-       name[length] = 0;
-       return length;
+       len = min(len, max_len);
+
+retry:
+       /* Generate the characters in the name. */
+       for (int i = 0; i < len; i++) {
+               do {
+                       name[i] = rand16();
+               } while (!is_valid_filename_char(name[i]));
+       }
+
+       /* Add a null terminator. */
+       name[len] = cpu_to_le16('\0');
+
+       /* Don't generate . and .. */
+       if (name[0] == cpu_to_le16('.') &&
+           (len == 1 || (len == 2 && name[1] == cpu_to_le16('.'))))
+               goto retry;
+
+       return len;
+}
+
+/* The set of characters which are valid in short filenames. */
+static const char valid_short_name_chars[] = {
+       'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
+       'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
+       '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+       '!', '#', '$', '%', '&', '\'', '(', ')', '-', '@', '^', '_', '`', '{',
+       '}', '~',
+       /* Note: Windows does not allow space and 128-255 in short filenames
+        * (tested on both NTFS and FAT). */
+};
+
+static int
+generate_short_name_component(utf16lechar p[], int len)
+{
+       for (int i = 0; i < len; i++) {
+               char c = valid_short_name_chars[rand32() %
+                                               ARRAY_LEN(valid_short_name_chars)];
+               p[i] = cpu_to_le16(c);
+       }
+       return len;
+}
+
+/* Generate a random short (8.3) filename and return its length.
+ * The @name array must have length >= 13 (8 + 1 + 3 + 1). */
+static int
+generate_random_short_name(utf16lechar name[], struct generation_context *ctx)
+{
+       /*
+        * Legal short names on Windows consist of 1 to 8 characters, optionally
+        * followed by a dot then 1 to 3 more characters.  Only certain
+        * characters are allowed.
+        */
+       int base_len = 1 + (rand32() % 8);
+       int ext_len = rand32() % 4;
+       int total_len;
+
+       base_len = generate_short_name_component(name, base_len);
+
+       if (ext_len) {
+               name[base_len] = cpu_to_le16('.');
+               ext_len = generate_short_name_component(&name[base_len + 1],
+                                                       ext_len);
+               total_len = base_len + 1 + ext_len;
+       } else {
+               total_len = base_len;
+       }
+       name[total_len] = cpu_to_le16('\0');
+       return total_len;
 }
 
 static u64
@@ -526,91 +633,80 @@ select_num_children(u32 depth, struct generation_context *ctx)
 }
 
 static bool
-is_name_forbidden_in_win32_namespace(const utf16lechar *name)
+is_name_valid_in_win32_namespace(const utf16lechar *name)
 {
-       static const utf16lechar forbidden_names[][5] = {
-               { cpu_to_le16('C'), cpu_to_le16('O'), cpu_to_le16('N'), },
-               { cpu_to_le16('P'), cpu_to_le16('R'), cpu_to_le16('N'), },
-               { cpu_to_le16('A'), cpu_to_le16('U'), cpu_to_le16('X'), },
-               { cpu_to_le16('N'), cpu_to_le16('U'), cpu_to_le16('L'), },
-               { cpu_to_le16('C'), cpu_to_le16('O'), cpu_to_le16('M'), cpu_to_le16('1'), },
-               { cpu_to_le16('C'), cpu_to_le16('O'), cpu_to_le16('M'), cpu_to_le16('2'), },
-               { cpu_to_le16('C'), cpu_to_le16('O'), cpu_to_le16('M'), cpu_to_le16('3'), },
-               { cpu_to_le16('C'), cpu_to_le16('O'), cpu_to_le16('M'), cpu_to_le16('4'), },
-               { cpu_to_le16('C'), cpu_to_le16('O'), cpu_to_le16('M'), cpu_to_le16('5'), },
-               { cpu_to_le16('C'), cpu_to_le16('O'), cpu_to_le16('M'), cpu_to_le16('6'), },
-               { cpu_to_le16('C'), cpu_to_le16('O'), cpu_to_le16('M'), cpu_to_le16('7'), },
-               { cpu_to_le16('C'), cpu_to_le16('O'), cpu_to_le16('M'), cpu_to_le16('8'), },
-               { cpu_to_le16('C'), cpu_to_le16('O'), cpu_to_le16('M'), cpu_to_le16('9'), },
-               { cpu_to_le16('L'), cpu_to_le16('P'), cpu_to_le16('T'), cpu_to_le16('1'), },
-               { cpu_to_le16('L'), cpu_to_le16('P'), cpu_to_le16('T'), cpu_to_le16('2'), },
-               { cpu_to_le16('L'), cpu_to_le16('P'), cpu_to_le16('T'), cpu_to_le16('3'), },
-               { cpu_to_le16('L'), cpu_to_le16('P'), cpu_to_le16('T'), cpu_to_le16('4'), },
-               { cpu_to_le16('L'), cpu_to_le16('P'), cpu_to_le16('T'), cpu_to_le16('5'), },
-               { cpu_to_le16('L'), cpu_to_le16('P'), cpu_to_le16('T'), cpu_to_le16('6'), },
-               { cpu_to_le16('L'), cpu_to_le16('P'), cpu_to_le16('T'), cpu_to_le16('7'), },
-               { cpu_to_le16('L'), cpu_to_le16('P'), cpu_to_le16('T'), cpu_to_le16('8'), },
-               { cpu_to_le16('L'), cpu_to_le16('P'), cpu_to_le16('T'), cpu_to_le16('9'), },
+       const utf16lechar *p;
+
+       static const char * const reserved_names[] = {
+                "CON",  "PRN",  "AUX",  "NUL",
+                "COM1", "COM2", "COM3", "COM4", "COM5",
+                "COM6", "COM7", "COM8", "COM9",
+                "LPT1", "LPT2", "LPT3", "LPT4", "LPT5",
+                "LPT6", "LPT7", "LPT8", "LPT9",
        };
 
-       if (!name)
+       /* The name must be nonempty. */
+       if (!name || !*name)
                return false;
 
-       for (size_t i = 0; i < ARRAY_LEN(forbidden_names); i++)
-               if (!cmp_utf16le_strings_z(forbidden_names[i], name, true))
-                       return true;
+       /* All characters must be valid on Windows. */
+       for (p = name; *p; p++)
+               if (!is_valid_windows_filename_char(*p))
+                       return false;
+
+       /* Note: a trailing dot or space is permitted, even though on Windows
+        * such a file can only be accessed using a WinNT-style path. */
+
+       /* The name can't be one of the reserved names or be a reserved name
+        * with an extension.  Case insensitive. */
+       for (size_t i = 0; i < ARRAY_LEN(reserved_names); i++) {
+               for (size_t j = 0; ; j++) {
+                       u16 c1 = le16_to_cpu(name[j]);
+                       u16 c2 = reserved_names[i][j];
+                       if (c2 == '\0') {
+                               if (c1 == '\0' || c1 == '.')
+                                       return false;
+                               break;
+                       }
+                       if (upcase[c1] != upcase[c2])
+                               break;
+               }
+       }
 
-       return false;
+       return true;
 }
 
 static int
 set_random_short_name(struct wim_dentry *dir, struct wim_dentry *child,
                      struct generation_context *ctx)
 {
-       tchar name[12 + 1];
-       int ret;
-       const utf16lechar *short_name;
+       utf16lechar name[12 + 1];
+       int name_len;
        u32 hash;
        struct wim_dentry **bucket;
 
        /* If the long name is not allowed in the Win32 namespace, then it
         * cannot be assigned a corresponding short name.  */
-       if (is_name_forbidden_in_win32_namespace(child->d_name))
+       if (!is_name_valid_in_win32_namespace(child->d_name))
                return 0;
 
 retry:
        /* Don't select a short name that is already used by a long name within
         * the same directory.  */
        do {
-               int len = generate_random_file_name(name, 12, ctx);
-
-               /* Legal short names on Windows take one of the following forms:
-                *
-                *    - 1 to 8 characters
-                *    - 1 to 8 characters, then a dot, then 1 to 3 characters */
-               if (len >= 9) {
-                       if (len == 9)
-                               len--;
-                       else
-                               name[8] = T('.');
-               }
-               name[len] = 0;
-       } while (get_dentry_child_with_name(dir, name,
-                                           WIMLIB_CASE_PLATFORM_DEFAULT));
+               name_len = generate_random_short_name(name, ctx);
+       } while (get_dentry_child_with_utf16le_name(dir, name, name_len * 2,
+                                                   WIMLIB_CASE_INSENSITIVE));
 
 
        /* Don't select a short name that is already used by another short name
         * within the same directory.  */
        hash = 0;
-       for (const tchar *p = name; *p; p++)
-               hash = (hash * 31) + totlower(*p);
-       ret = tstr_get_utf16le(name, &short_name);
-       if (ret)
-               return ret;
+       for (const utf16lechar *p = name; *p; p++)
+               hash = (hash * 31) + *p;
        FREE(child->d_short_name);
-       child->d_short_name = utf16le_dup(short_name);
-       child->d_short_name_nbytes = utf16le_len_bytes(short_name);
-       tstr_put_utf16le(short_name);
+       child->d_short_name = memdup(name, (name_len + 1) * 2);
+       child->d_short_name_nbytes = name_len * 2;
 
        if (!child->d_short_name)
                return WIMLIB_ERR_NOMEM;
@@ -618,12 +714,15 @@ retry:
        bucket = &ctx->used_short_names[hash % ARRAY_LEN(ctx->used_short_names)];
 
        for (struct wim_dentry *d = *bucket; d != NULL;
-            d = d->d_next_extraction_alias)
-               if (!cmp_utf16le_strings_z(child->d_short_name,
-                                          d->d_short_name, true))
+            d = d->d_next_extraction_alias) {
+               if (!cmp_utf16le_strings(child->d_short_name, name_len,
+                                        d->d_short_name, d->d_short_name_nbytes / 2,
+                                        true)) {
                        goto retry;
+               }
+       }
 
-       if (is_name_forbidden_in_win32_namespace(child->d_short_name))
+       if (!is_name_valid_in_win32_namespace(child->d_short_name))
                goto retry;
 
        child->d_next_extraction_alias = *bucket;
@@ -659,34 +758,47 @@ generate_dentry_tree_recursive(struct wim_dentry *dir, u32 depth,
        for (u32 i = 0; i < num_children; i++) {
 
                /* Generate the next child dentry.  */
-
-               tchar name[128 + 1];
-               struct wim_dentry *duplicate;
                struct wim_inode *inode;
                u64 ino;
-               bool is_directory;
+               bool is_directory = (rand32() % 16 <= 6);
+               bool is_reparse = (rand32() % 8 == 0);
+               utf16lechar name[63 + 1]; /* for UNIX extraction: 63 * 4 <= 255 */
+               int name_len;
+               struct wim_dentry *duplicate;
 
-               /* Choose a long filename that is unique within the directory.*/
-               do {
-                       generate_random_file_name(name, 128, ctx);
-               } while (get_dentry_child_with_name(dir, name,
-                                                   WIMLIB_CASE_PLATFORM_DEFAULT));
-
-               /* Decide whether to create a directory or not.
-                * If not a directory, also decide on the inode number (i.e. we
-                * may generate a "hard link" to an existing file).  */
-               is_directory = ((rand32() % 16) <= 6);
-               if (is_directory)
+               /*
+                * Select an inode number for the new file.  Sometimes choose an
+                * existing inode number (i.e. create a hard link).  However,
+                * wimlib intentionally doesn't honor directory hard links, and
+                * reparse points cannot be represented in the WIM file format
+                * at all; so don't create hard links for such files.
+                */
+               if (is_directory || is_reparse)
                        ino = 0;
                else
                        ino = select_inode_number(ctx);
 
-               /* Create the dentry and add it to the directory.  */
-               ret = inode_table_new_dentry(ctx->params->inode_table, name,
-                                            ino, 0, is_directory, &child);
+               /* Create the dentry. */
+               ret = inode_table_new_dentry(ctx->params->inode_table, NULL,
+                                            ino, 0, ino == 0, &child);
                if (ret)
                        return ret;
 
+               /* Choose a filename that is unique within the directory.*/
+               do {
+                       name_len = generate_random_filename(name,
+                                                           ARRAY_LEN(name) - 1,
+                                                           ctx);
+               } while (get_dentry_child_with_utf16le_name(dir, name, name_len * 2,
+                                                           WIMLIB_CASE_PLATFORM_DEFAULT));
+
+               ret = dentry_set_name_utf16le(child, name, name_len * 2);
+               if (ret) {
+                       free_dentry(child);
+                       return ret;
+               }
+
+               /* Add the dentry to the directory. */
                duplicate = dentry_add_child(dir, child);
                wimlib_assert(!duplicate);
 
@@ -699,19 +811,19 @@ generate_dentry_tree_recursive(struct wim_dentry *dir, u32 depth,
 
                if (is_directory)
                        inode->i_attributes |= FILE_ATTRIBUTE_DIRECTORY;
+               if (is_reparse)
+                       inode->i_attributes |= FILE_ATTRIBUTE_REPARSE_POINT;
 
-               ret = set_random_metadata(inode, ctx);
+               ret = set_random_streams(inode, ctx);
                if (ret)
                        return ret;
 
-               ret = set_random_streams(inode, ctx, true);
+               ret = set_random_metadata(inode, ctx);
                if (ret)
                        return ret;
 
                /* Recurse if it's a directory.  */
-               if (is_directory &&
-                   !(inode->i_attributes & FILE_ATTRIBUTE_REPARSE_POINT))
-               {
+               if (is_directory && !is_reparse) {
                        ret = generate_dentry_tree_recursive(child, depth + 1,
                                                             ctx);
                        if (ret)
@@ -746,10 +858,10 @@ generate_dentry_tree(struct wim_dentry **root_ret, const tchar *_ignored,
        ret = inode_table_new_dentry(params->inode_table, NULL, 0, 0, true, &root);
        if (!ret) {
                root->d_inode->i_attributes = FILE_ATTRIBUTE_DIRECTORY;
-               ret = set_random_metadata(root->d_inode, &ctx);
+               ret = set_random_streams(root->d_inode, &ctx);
        }
        if (!ret)
-               ret = set_random_streams(root->d_inode, &ctx, false);
+               ret = set_random_metadata(root->d_inode, &ctx);
        if (!ret)
                ret = generate_dentry_tree_recursive(root, 1, &ctx);
        if (!ret)
@@ -807,7 +919,7 @@ calc_corresponding_files_recursive(struct wim_dentry *d1, struct wim_dentry *d2,
 
        /* Compare short filenames, case insensitively.  */
        if (!(d2->d_short_name_nbytes == 0 &&
-             (cmp_flags & WIMLIB_CMP_FLAG_SHORT_NAMES_NOT_PRESERVED)) &&
+             (cmp_flags & WIMLIB_CMP_FLAG_UNIX_MODE)) &&
            cmp_utf16le_strings(d1->d_short_name, d1->d_short_name_nbytes / 2,
                                d2->d_short_name, d2->d_short_name_nbytes / 2,
                                true))
@@ -885,59 +997,108 @@ check_hard_link(struct wim_dentry *dentry, void *_ignore)
        return WIMLIB_ERR_IMAGES_ARE_DIFFERENT;
 }
 
+static const struct {
+       u32 flag;
+       const char *name;
+} file_attr_flags[] = {
+       {FILE_ATTRIBUTE_READONLY,            "READONLY"},
+       {FILE_ATTRIBUTE_HIDDEN,              "HIDDEN"},
+       {FILE_ATTRIBUTE_SYSTEM,              "SYSTEM"},
+       {FILE_ATTRIBUTE_DIRECTORY,           "DIRECTORY"},
+       {FILE_ATTRIBUTE_ARCHIVE,             "ARCHIVE"},
+       {FILE_ATTRIBUTE_DEVICE,              "DEVICE"},
+       {FILE_ATTRIBUTE_NORMAL,              "NORMAL"},
+       {FILE_ATTRIBUTE_TEMPORARY,           "TEMPORARY"},
+       {FILE_ATTRIBUTE_SPARSE_FILE,         "SPARSE_FILE"},
+       {FILE_ATTRIBUTE_REPARSE_POINT,       "REPARSE_POINT"},
+       {FILE_ATTRIBUTE_COMPRESSED,          "COMPRESSED"},
+       {FILE_ATTRIBUTE_OFFLINE,             "OFFLINE"},
+       {FILE_ATTRIBUTE_NOT_CONTENT_INDEXED, "NOT_CONTENT_INDEXED"},
+       {FILE_ATTRIBUTE_ENCRYPTED,           "ENCRYPTED"},
+       {FILE_ATTRIBUTE_VIRTUAL,             "VIRTUAL"},
+};
+
+static int
+cmp_attributes(const struct wim_inode *inode1,
+              const struct wim_inode *inode2, int cmp_flags)
+{
+       const u32 changed = inode1->i_attributes ^ inode2->i_attributes;
+       const u32 set = inode2->i_attributes & ~inode1->i_attributes;
+       const u32 cleared = inode1->i_attributes & ~inode2->i_attributes;
+
+       /* NORMAL may change, but it must never be set along with other
+        * attributes. */
+       if ((inode2->i_attributes & FILE_ATTRIBUTE_NORMAL) &&
+           (inode2->i_attributes & ~FILE_ATTRIBUTE_NORMAL))
+               goto mismatch;
+
+       /* DIRECTORY must not change. */
+       if (changed & FILE_ATTRIBUTE_DIRECTORY)
+               goto mismatch;
+
+       /* REPARSE_POINT may be cleared in UNIX mode if the inode is not a
+        * symlink. */
+       if ((changed & FILE_ATTRIBUTE_REPARSE_POINT) &&
+           !((cleared & FILE_ATTRIBUTE_REPARSE_POINT) &&
+             (cmp_flags & WIMLIB_CMP_FLAG_UNIX_MODE) &&
+             !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. */
+       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)))))
+               goto mismatch;
+
+       /* COMPRESSED may change in UNIX and NTFS-3G modes.  (It *should* be
+        * preserved in NTFS-3G mode, but it's not implemented yet.) */
+       if ((changed & FILE_ATTRIBUTE_COMPRESSED) &&
+           !(cmp_flags & (WIMLIB_CMP_FLAG_UNIX_MODE |
+                          WIMLIB_CMP_FLAG_NTFS_3G_MODE)))
+               goto mismatch;
+
+       /* All other attributes can change in UNIX mode, but not in any other
+        * mode. */
+       if ((changed & ~(FILE_ATTRIBUTE_NORMAL |
+                        FILE_ATTRIBUTE_DIRECTORY |
+                        FILE_ATTRIBUTE_REPARSE_POINT |
+                        FILE_ATTRIBUTE_SPARSE_FILE |
+                        FILE_ATTRIBUTE_COMPRESSED)) &&
+           !(cmp_flags & WIMLIB_CMP_FLAG_UNIX_MODE))
+               goto mismatch;
+
+       return 0;
+
+mismatch:
+       ERROR("Attribute mismatch for %"TS": 0x%08"PRIx32" vs. 0x%08"PRIx32":",
+             inode_any_full_path(inode1), inode1->i_attributes,
+             inode2->i_attributes);
+       for (size_t i = 0; i < ARRAY_LEN(file_attr_flags); i++) {
+               u32 flag = file_attr_flags[i].flag;
+               if (changed & flag) {
+                       fprintf(stderr, "\tFILE_ATTRIBUTE_%s was %s\n",
+                               file_attr_flags[i].name,
+                               (set & flag) ? "set" : "cleared");
+               }
+       }
+       return WIMLIB_ERR_IMAGES_ARE_DIFFERENT;
+}
+
 static int
 cmp_inodes(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)
 {
-       const u32 attrib_diff = inode1->i_attributes ^ inode2->i_attributes;
-       bool reparse_point_should_preserved = true;
+       int ret;
 
        /* Compare attributes  */
-       if (cmp_flags & WIMLIB_CMP_FLAG_ATTRIBUTES_NOT_PRESERVED) {
-
-               /* In this mode, we expect that most attributes are not
-                * preserved.  However, FILE_ATTRIBUTE_DIRECTORY should always
-                * match.  */
-               if (attrib_diff & FILE_ATTRIBUTE_DIRECTORY)
-                       goto attrib_mismatch;
-
-               /* We may also expect FILE_ATTRIBUTE_REPARSE_POINT to be
-                * preserved for symlinks.  It also shouldn't be set if it
-                * wasn't set before.  */
-
-               if ((cmp_flags & WIMLIB_CMP_FLAG_IMAGE2_SHOULD_HAVE_SYMLINKS) &&
-                   inode_is_symlink(inode1))
-                       reparse_point_should_preserved = true;
-               else
-                       reparse_point_should_preserved = false;
-
-               if ((attrib_diff & FILE_ATTRIBUTE_REPARSE_POINT) &&
-                   (reparse_point_should_preserved ||
-                    (inode2->i_attributes & FILE_ATTRIBUTE_REPARSE_POINT)))
-                       goto attrib_mismatch;
-       } else {
-
-               /* Most attributes should be preserved.  */
-
-               /* Nothing other than COMPRESSED and NORMAL should have changed.
-                */
-               if (attrib_diff & ~(FILE_ATTRIBUTE_COMPRESSED |
-                                   FILE_ATTRIBUTE_NORMAL))
-                       goto attrib_mismatch;
-
-               /* COMPRESSED shouldn't have changed unless specifically
-                * excluded.  */
-               if ((attrib_diff & FILE_ATTRIBUTE_COMPRESSED) &&
-                   !(cmp_flags & WIMLIB_CMP_FLAG_COMPRESSION_NOT_PRESERVED))
-                       goto attrib_mismatch;
-
-               /* We allow NORMAL to change, but not if the file ended up with
-                * other attributes set as well.  */
-               if ((attrib_diff & FILE_ATTRIBUTE_NORMAL) &&
-                   (inode2->i_attributes & ~FILE_ATTRIBUTE_NORMAL))
-                       goto attrib_mismatch;
-       }
+       ret = cmp_attributes(inode1, inode2, cmp_flags);
+       if (ret)
+               return ret;
 
        /* Compare security descriptors  */
        if (inode_has_security_descriptor(inode1)) {
@@ -952,7 +1113,7 @@ cmp_inodes(const struct wim_inode *inode1, const struct wim_inode *inode2,
                                      inode_any_full_path(inode1));
                                return WIMLIB_ERR_IMAGES_ARE_DIFFERENT;
                        }
-               } else if (!(cmp_flags & WIMLIB_CMP_FLAG_SECURITY_NOT_PRESERVED)) {
+               } 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;
@@ -971,7 +1132,8 @@ cmp_inodes(const struct wim_inode *inode1, const struct wim_inode *inode2,
                const struct wim_inode_stream *strm2;
 
                if (strm1->stream_type == STREAM_TYPE_REPARSE_POINT &&
-                   !reparse_point_should_preserved)
+                   (cmp_flags & WIMLIB_CMP_FLAG_UNIX_MODE &&
+                    !inode_is_symlink(inode1)))
                        continue;
 
                if (strm1->stream_type == STREAM_TYPE_UNKNOWN)
@@ -983,7 +1145,7 @@ cmp_inodes(const struct wim_inode *inode1, const struct wim_inode *inode2,
                if (!strm2) {
                        /* Corresponding stream not found  */
                        if (stream_is_named(strm1) &&
-                           (cmp_flags & WIMLIB_CMP_FLAG_ADS_NOT_PRESERVED))
+                           (cmp_flags & WIMLIB_CMP_FLAG_UNIX_MODE))
                                continue;
                        ERROR("Stream of %"TS" is missing in second image; "
                              "type %d, named=%d, empty=%d",
@@ -1002,13 +1164,6 @@ cmp_inodes(const struct wim_inode *inode1, const struct wim_inode *inode2,
        }
 
        return 0;
-
-attrib_mismatch:
-       ERROR("Attribute mismatch; %"TS" has attributes 0x%08"PRIx32" "
-             "in first image but attributes 0x%08"PRIx32" in second image",
-             inode_any_full_path(inode1), inode1->i_attributes,
-             inode2->i_attributes);
-       return WIMLIB_ERR_IMAGES_ARE_DIFFERENT;
 }
 
 static int