Improve zipcmp output.
diff --git a/developer-xcode/libzip.xcodeproj/project.pbxproj b/developer-xcode/libzip.xcodeproj/project.pbxproj
index 41dfdf5..02262ff 100644
--- a/developer-xcode/libzip.xcodeproj/project.pbxproj
+++ b/developer-xcode/libzip.xcodeproj/project.pbxproj
@@ -164,6 +164,7 @@
 		4B0454BA1E8E3E08002FA1F9 /* zip_source_compress.c in Sources */ = {isa = PBXBuildFile; fileRef = 4B0454B61E8E3DF7002FA1F9 /* zip_source_compress.c */; };
 		4B0454BC1E8E3E24002FA1F9 /* zip_algorithm_bzip2.c in Sources */ = {isa = PBXBuildFile; fileRef = 4B0454B41E8E3DF7002FA1F9 /* zip_algorithm_bzip2.c */; };
 		4B0454BD1E8E3E24002FA1F9 /* zip_algorithm_deflate.c in Sources */ = {isa = PBXBuildFile; fileRef = 4B0454B51E8E3DF7002FA1F9 /* zip_algorithm_deflate.c */; };
+		4B20D28726021D3600D77FA8 /* diff_output.c in Sources */ = {isa = PBXBuildFile; fileRef = 4B20D28626021D3600D77FA8 /* diff_output.c */; };
 		4B30B50225F11684002CE070 /* zip_algorithm_zstd.c in Sources */ = {isa = PBXBuildFile; fileRef = 4B9E577A24C7026B00CEE0D6 /* zip_algorithm_zstd.c */; };
 		4B30B53425F117C4002CE070 /* libzstd.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B30B53325F117C4002CE070 /* libzstd.dylib */; };
 		4B3A5F521DF96EB4005A53A1 /* zip_fseek.c in Sources */ = {isa = PBXBuildFile; fileRef = 4B3A5F4D1DF96D83005A53A1 /* zip_fseek.c */; };
@@ -541,6 +542,11 @@
 		4B1E46E91A08CB7600A376D2 /* zip_error_set.mdoc */ = {isa = PBXFileReference; lastKnownFileType = text; path = zip_error_set.mdoc; sourceTree = "<group>"; };
 		4B1E46EA1A08CB7600A376D2 /* zip_error_strerror.mdoc */ = {isa = PBXFileReference; lastKnownFileType = text; path = zip_error_strerror.mdoc; sourceTree = "<group>"; };
 		4B1E46EB1A08CB7600A376D2 /* zip_error_system_type.mdoc */ = {isa = PBXFileReference; lastKnownFileType = text; path = zip_error_system_type.mdoc; sourceTree = "<group>"; };
+		4B20D28526021D3600D77FA8 /* diff_output.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = diff_output.h; sourceTree = "<group>"; };
+		4B20D28626021D3600D77FA8 /* diff_output.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = diff_output.c; sourceTree = "<group>"; };
+		4B20D2982602314A00D77FA8 /* zipcmp_zip_dir.test */ = {isa = PBXFileReference; lastKnownFileType = text; path = zipcmp_zip_dir.test; sourceTree = "<group>"; };
+		4B20D2992602314A00D77FA8 /* add_from_file_unchange.test */ = {isa = PBXFileReference; lastKnownFileType = text; path = add_from_file_unchange.test; sourceTree = "<group>"; };
+		4B20D29A2602314A00D77FA8 /* encryption-stat.test */ = {isa = PBXFileReference; lastKnownFileType = text; path = "encryption-stat.test"; sourceTree = "<group>"; };
 		4B26FF151A07DF1A000E9788 /* zip_file_get_external_attributes.mdoc */ = {isa = PBXFileReference; lastKnownFileType = text; path = zip_file_get_external_attributes.mdoc; sourceTree = "<group>"; };
 		4B26FF161A07DF1A000E9788 /* zip_file_set_external_attributes.mdoc */ = {isa = PBXFileReference; lastKnownFileType = text; path = zip_file_set_external_attributes.mdoc; sourceTree = "<group>"; };
 		4B26FF171A07DF1A000E9788 /* zip_file_set_mtime.mdoc */ = {isa = PBXFileReference; lastKnownFileType = text; path = zip_file_set_mtime.mdoc; sourceTree = "<group>"; };
@@ -1068,6 +1074,8 @@
 			isa = PBXGroup;
 			children = (
 				4B55D93F2475274B00CE8C38 /* CMakeLists.txt */,
+				4B20D28626021D3600D77FA8 /* diff_output.c */,
+				4B20D28526021D3600D77FA8 /* diff_output.h */,
 				4B01D72115B2F572002D5007 /* zipcmp.c */,
 				4B01D72215B2F572002D5007 /* zipmerge.c */,
 				4BACD57C15BC2AEF00920691 /* ziptool.c */,
@@ -1208,8 +1216,8 @@
 				4BC03F9A1FDD5617003C7B62 /* fseek.c */,
 				4B30B4D025F0ECF7002CE070 /* fuzz_main.c */,
 				4BD6CB5E19E71B3B00710654 /* hole.c */,
-				4B30B4BF25F0ECC8002CE070 /* liboverride.c */,
 				4B30B4D125F0EE6D002CE070 /* liboverride-test.c */,
+				4B30B4BF25F0ECC8002CE070 /* liboverride.c */,
 				4BC03F9C1FDD5617003C7B62 /* malloc.c */,
 				4B4CB5572483D7B7005C5428 /* nihtest.conf.in */,
 				4BD155CE191CD28D0046F012 /* NiHTest.pm */,
@@ -1232,6 +1240,7 @@
 				4BD35E421A33366200256CB7 /* add_from_buffer.test */,
 				4BD35E431A33366200256CB7 /* add_from_file_duplicate.test */,
 				4BD35E441A33366200256CB7 /* add_from_file_twice_duplicate.test */,
+				4B20D2992602314A00D77FA8 /* add_from_file_unchange.test */,
 				4BD35E451A33366200256CB7 /* add_from_file.test */,
 				4BD35E461A33366200256CB7 /* add_from_filep.test */,
 				4BD35E471A33366200256CB7 /* add_from_stdin.test */,
@@ -1276,6 +1285,7 @@
 				4BC03F831FDD55C2003C7B62 /* encryption-nonrandom-aes256.test */,
 				4B00CA2A242F5C2500E0B71C /* encryption-nonrandom-pkware.test */,
 				4BC03F881FDD55C3003C7B62 /* encryption-remove.test */,
+				4B20D29A2602314A00D77FA8 /* encryption-stat.test */,
 				4BD35E5C1A33366200256CB7 /* extra_add_multiple.test */,
 				4BD35E5D1A33366200256CB7 /* extra_add.test */,
 				4BD35E5E1A33366200256CB7 /* extra_count_by_id.test */,
@@ -1363,6 +1373,7 @@
 				4BC03F781FDD55C1003C7B62 /* zip-in-archive-comment.test */,
 				4BD35EE31A33366300256CB7 /* zip64_creation.test */,
 				4BD35EE41A33366300256CB7 /* zip64_stored_creation.test */,
+				4B20D2982602314A00D77FA8 /* zipcmp_zip_dir.test */,
 			);
 			name = "test cases";
 			sourceTree = "<group>";
@@ -2058,6 +2069,7 @@
 			buildActionMask = 2147483647;
 			files = (
 				4B01D72515B2F57B002D5007 /* zipcmp.c in Sources */,
+				4B20D28726021D3600D77FA8 /* diff_output.c in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
diff --git a/man/zipcmp.mdoc b/man/zipcmp.mdoc
index c29ee69..aa553d4 100644
--- a/man/zipcmp.mdoc
+++ b/man/zipcmp.mdoc
@@ -29,7 +29,7 @@
 .\" OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
 .\" IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 .\"
-.Dd March 4, 2021
+.Dd March 17, 2021
 .Dt ZIPCMP 1
 .Os
 .Sh NAME
@@ -61,8 +61,9 @@
 Compare names ignoring case distinctions.
 .It Fl p
 Enable paranoid checks.
-Compares extra fields and other meta data.
+Compares extra fields, comments, and other meta data.
 (Automatically disabled if one of the archives is a directory.)
+These checks are skipped for files where the data differs.
 .It Fl q
 Quiet mode.
 Compare
diff --git a/regress/zipcmp_zip_dir.test b/regress/zipcmp_zip_dir.test
index e8ba46e..91413bc 100644
--- a/regress/zipcmp_zip_dir.test
+++ b/regress/zipcmp_zip_dir.test
@@ -8,7 +8,7 @@
 return 1
 stdout --- zipcmp_zip_dir.zip
 stdout +++ a
-stdout -          0 00000000 00-empty-dir/
-stdout -          1 e8b7be43 dir-with-file/a
-stdout +          0 00000000 empty-dir-in-dir/
-stdout -          0 00000000 empty-dir/
+stdout - file '00-empty-dir/', size 0, crc 00000000
+stdout - file 'dir-with-file/a', size 1, crc e8b7be43
+stdout + file 'empty-dir-in-dir/', size 0, crc 00000000
+stdout - file 'empty-dir/', size 0, crc 00000000
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index b6eb0b1..5d75b8a 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -10,4 +10,5 @@
     target_sources(${PROGRAM} PRIVATE getopt.c)
   endif(NOT HAVE_GETOPT)
 endforeach()
+target_sources(zipcmp PRIVATE diff_output.c)
 target_link_libraries(zipcmp ${FTS_LIB} ZLIB::ZLIB)
diff --git a/src/diff_output.c b/src/diff_output.c
new file mode 100644
index 0000000..472ee89
--- /dev/null
+++ b/src/diff_output.c
@@ -0,0 +1,101 @@
+#include "diff_output.h"
+
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "compat.h"
+
+static void ensure_header(diff_output_t *output) {
+    if (output->archive_names[0] != NULL) {
+        printf("--- %s\n", output->archive_names[0]);
+        printf("+++ %s\n", output->archive_names[1]);
+        output->archive_names[0] = NULL;
+        output->archive_names[1] = NULL;
+    }
+}
+
+void diff_output_init(diff_output_t *output, int verbose, char *const archive_names[]) {
+    output->archive_names[0] = archive_names[0];
+    output->archive_names[1] = archive_names[1];
+    output->verbose = verbose;
+    output->file_name = NULL;
+    output->file_size = 0;
+    output->file_crc = 0;
+}
+
+void diff_output_start_file(diff_output_t *output, const char *name, zip_uint64_t size, zip_uint32_t crc) {
+    output->file_name = name;
+    output->file_size = size;
+    output->file_crc = crc;
+}
+
+void diff_output_end_file(diff_output_t *output) {
+    output->file_name = NULL;
+}
+
+void diff_output(diff_output_t *output, int side, const char *fmt, ...) {
+    va_list ap;
+
+    if (!output->verbose) {
+        return;
+    }
+
+    ensure_header(output);
+    
+    if (output->file_name != NULL) {
+        diff_output_file(output, ' ', output->file_name, output->file_size, output->file_crc);
+        output->file_name = NULL;
+    }
+    
+    printf("%c ", side);
+    va_start(ap, fmt);
+    vprintf(fmt, ap);
+    va_end(ap);
+    printf("\n");
+}
+
+void diff_output_file(diff_output_t *output, char side, const char *name, zip_uint64_t size, zip_uint32_t crc) {
+    if (!output->verbose) {
+        return;
+    }
+    
+    ensure_header(output);
+    
+    printf("%c file '%s', size %" PRIu64 ", crc %08x\n", side, name, size, crc);
+}
+
+#define MAX_BYTES 64
+void diff_output_data(diff_output_t *output, int side, const zip_uint8_t *data, zip_uint64_t data_length, const char *fmt, ...) {
+    char hexdata[MAX_BYTES * 3 + 6];
+    size_t i, offset;
+    va_list ap;
+    char *prefix;
+
+    if (!output->verbose) {
+        return;
+    }
+    
+    offset = 0;
+    for (i = 0; i < data_length; i++) {
+        hexdata[offset++] = (i == 0 ? '<' : ' ');
+
+        if (i >= MAX_BYTES) {
+            sprintf(hexdata + offset, "...");
+            break;
+        }
+        sprintf(hexdata + offset, "%02x", data[i]);
+        offset += 2;
+    }
+
+    hexdata[offset++] = '>';
+    hexdata[offset] = '\0';
+    
+    va_start(ap, fmt);
+    vasprintf(&prefix, fmt, ap);
+    va_end(ap);
+    
+    diff_output(output, side, "%s, length %" PRIu64 ", data %s", prefix, data_length, hexdata);
+    
+    free(prefix);
+}
diff --git a/src/diff_output.h b/src/diff_output.h
new file mode 100644
index 0000000..6ed9396
--- /dev/null
+++ b/src/diff_output.h
@@ -0,0 +1,23 @@
+#ifndef HAD_DIFF_OUTPUT_H
+#define HAD_DIFF_OUTPUT_H
+
+#include <zip.h>
+
+typedef struct {
+    const char *archive_names[2];
+    const char *file_name;
+    zip_uint64_t file_size;
+    zip_uint32_t file_crc;
+    int verbose;
+} diff_output_t;
+
+void diff_output_init(diff_output_t *output, int verbose, char *const archive_names[]);
+void diff_output_start_file(diff_output_t *output, const char *name, zip_uint64_t size, zip_uint32_t crc);
+void diff_output_end_file(diff_output_t *output);
+
+void diff_output(diff_output_t *output, int side, const char *fmt, ...) __attribute__((__format__(__printf__, 3, 4)));
+void diff_output_data(diff_output_t *output, int side, const zip_uint8_t *data, zip_uint64_t data_length, const char *fmt, ...) __attribute__((__format__(__printf__, 5, 6)));
+void diff_output_file(diff_output_t *output, char side, const char *name, zip_uint64_t size, zip_uint32_t crc);
+
+
+#endif /* HAD_DIFF_OUTPUT_H */
diff --git a/src/zipcmp.c b/src/zipcmp.c
index 66e4b24..41a36ea 100644
--- a/src/zipcmp.c
+++ b/src/zipcmp.c
@@ -55,6 +55,8 @@
 
 #include "compat.h"
 
+#include "diff_output.h"
+
 struct archive {
     const char *name;
     zip_t *za;
@@ -84,6 +86,100 @@
 };
 
 
+typedef struct {
+    uint32_t value;
+    const char * const name;
+} enum_map_t;
+
+const enum_map_t comp_methods[] = {
+    { 0, "Stored (no compression)" },
+    { 1, "Shrunk" },
+    { 2, "Reduced with compression factor 1" },
+    { 3, "Reduced with compression factor 2" },
+    { 4, "Reduced with compression factor 3" },
+    { 5, "Reduced with compression factor 4" },
+    { 6, "Imploded" },
+    { 7, "Reserved for Tokenizing compression algorithm" },
+    { 8, "Deflated" },
+    { 9, "Enhanced Deflating using Deflate64(tm)" },
+    { 10, "PKWARE Data Compression Library Imploding (old IBM TERSE)" },
+    { 11, "11 (Reserved by PKWARE)" },
+    { 12, "BZIP2" },
+    { 13, "13 (Reserved by PKWARE)" },
+    { 14, "LZMA (EFS)" },
+    { 15, "15 (Reserved by PKWARE)" },
+    { 16, "16 (Reserved by PKWARE)" },
+    { 17, "17 (Reserved by PKWARE)" },
+    { 18, "IBM TERSE (new)" },
+    { 19, "IBM LZ77 z Architecture (PFS)" },
+    { 20, "Zstandard compressed data (obsolete)" },
+    { 93, "Zstandard compressed data" },
+    { 95, "XZ compressed data" },
+    { 97, "WavPack compressed data" },
+    { 98, "PPMd version I, Rev 1" },
+    { 99, "WinZIP AES Encryption" },
+    { UINT32_MAX, NULL }
+};
+
+const enum_map_t extra_fields[] = {
+    /* PKWARE defined */
+    { 0x0001, "Zip64 extended information Extra field" },
+    { 0x0007, "AV Info" },
+    { 0x0008, "Reserved for extended language encoding data (PFS)" },
+    { 0x0009, "OS/2" },
+    { 0x000a, "NTFS" },
+    { 0x000c, "OpenVMS" },
+    { 0x000d, "UNIX" },
+    { 0x000e, "Reserved for file stream and fork descriptors" },
+    { 0x000f, "Patch Descriptor" },
+    { 0x0014, "PKCS#7 Store for X.509 Certificates" },
+    { 0x0015, "X.509 Certificate ID and Signature for individual file" },
+    { 0x0016, "X.509 Certificate ID for Central Directory" },
+    { 0x0017, "Strong Encryption Header" },
+    { 0x0018, "Record Management Controls" },
+    { 0x0019, "PKCS#7 Encryption Recipient Certificate List" },
+    { 0x0065, "IBM S/390 (Z390), AS/400 (I400) attributes - uncompressed" },
+    { 0x0066, "Reserved for IBM S/390 (Z390), AS/400 (I400) attributes - compressed" },
+    { 0x4690, "POSZIP 4690 (reserved)" },
+
+    /* Third-Party defined; see InfoZIP unzip sources proginfo/extrafld.txt */
+    { 0x07c8, "Info-ZIP Macintosh (old)" },
+    { 0x2605, "ZipIt Macintosh (first version)" },
+    { 0x2705, "ZipIt Macintosh 1.3.5+ (w/o full filename)" },
+    { 0x2805, "ZipIt Macintosh 1.3.5+" },
+    { 0x334d, "Info-ZIP Macintosh (new)" },
+    { 0x4154, "Tandem NSK" },
+    { 0x4341, "Acorn/SparkFS" },
+    { 0x4453, "Windows NT security descriptor" },
+    { 0x4704, "VM/CMS" },
+    { 0x470f, "MVS" },
+    { 0x4854, "Theos, old inofficial port" },
+    { 0x4b46, "FWKCS MD5" },
+    { 0x4c41, "OS/2 access control list (text ACL)" },
+    { 0x4d49, "Info-ZIP OpenVMS (obsolete)" },
+    { 0x4d63, "Macintosh SmartZIP" },
+    { 0x4f4c, "Xceed original location extra field" },
+    { 0x5356, "AOS/VS (ACL)" },
+    { 0x5455, "extended timestamp" },
+    { 0x554e, "Xceed unicode extra field" },
+    { 0x5855, "Info-ZIP UNIX (original)" },
+    { 0x6375, "Info-ZIP UTF-8 comment field" },
+    { 0x6542, "BeOS (BeBox, PowerMac, etc.)" },
+    { 0x6854, "Theos" },
+    { 0x7075, "Info-ZIP UTF-8 name field" },
+    { 0x7441, "AtheOS (AtheOS/Syllable attributes)" },
+    { 0x756e, "ASi UNIX" },
+    { 0x7855, "Info-ZIP UNIX" },
+    { 0x7875, "Info-ZIP UNIX 3rd generation" },
+    { 0x9901, "WinZIP AES encryption" },
+    { 0xa220, "Microsoft Open Packaging Growth Hint" },
+    { 0xcafe, "executable Java JAR file" },
+    { 0xfb4a, "SMS/QDOS" }, /* per InfoZIP extrafld.txt */
+    { 0xfd4a, "SMS/QDOS" }, /* per appnote.txt */
+    { UINT32_MAX, NULL }
+};
+
+
 const char *progname;
 
 #define PROGRAM "zipcmp"
@@ -114,16 +210,19 @@
 #define BOTH_ARE_ZIPS(a) (a[0].za && a[1].za)
 
 static int comment_compare(const char *c1, size_t l1, const char *c2, size_t l2);
-static int compare_list(char *const name[], const void *l[], const zip_uint64_t n[], int size, int (*cmp)(const void *, const void *), int (*ignore)(const void *l, int last, const void *o), int (*checks)(char *const name[2], const void *, const void *), void (*print)(const void *));
+static int compare_list(char *const name[2], const void *list[2], const zip_uint64_t list_length[2], int element_size, int (*cmp)(const void *a, const void *b), int (*ignore)(const void *list, int last, const void *other), int (*check)(char *const name[2], const void *a, const void *b), void (*print)(char side, const void *element), void (*start_file)(const void *element));
 static int compare_zip(char *const zn[]);
 static int ef_compare(char *const name[2], const struct entry *e1, const struct entry *e2);
 static int ef_order(const void *a, const void *b);
-static void ef_print(const void *p);
+static void ef_print(char side, const void *p);
 static int ef_read(zip_t *za, zip_uint64_t idx, struct entry *e);
 static int entry_cmp(const void *p1, const void *p2);
 static int entry_ignore(const void *p1, int last, const void *o);
 static int entry_paranoia_checks(char *const name[2], const void *p1, const void *p2);
-static void entry_print(const void *p);
+static void entry_print(char side, const void *p);
+static void entry_start_file(const void *p);
+static const char *map_enum(const enum_map_t *map, uint32_t value);
+
 static int is_directory(const char *name);
 #ifdef HAVE_FTS_H
 static int list_directory(const char *name, struct archive *a);
@@ -132,7 +231,8 @@
 static int test_file(zip_t *za, zip_uint64_t idx, const char *zipname, const char *filename, zip_uint64_t size, zip_uint32_t crc);
 
 int ignore_case, test_files, paranoid, verbose, have_directory, check_consistency;
-int header_done;
+
+diff_output_t output;
 
 
 int
@@ -228,19 +328,21 @@
             qsort(a[i].entry, a[i].nentry, sizeof(a[i].entry[0]), entry_cmp);
     }
 
-    header_done = 0;
+    diff_output_init(&output, verbose, zn);
 
     e[0] = a[0].entry;
     e[1] = a[1].entry;
     n[0] = a[0].nentry;
     n[1] = a[1].nentry;
-    res = compare_list(zn, (const void **)e, n, sizeof(e[i][0]), entry_cmp, have_directory ? entry_ignore : NULL, paranoid ? entry_paranoia_checks : NULL, entry_print);
+    res = compare_list(zn, (const void **)e, n, sizeof(e[i][0]), entry_cmp, have_directory ? entry_ignore : NULL, paranoid ? entry_paranoia_checks : NULL, entry_print, entry_start_file);
 
     if (paranoid) {
         if (comment_compare(a[0].comment, a[0].comment_length, a[1].comment, a[1].comment_length) != 0) {
-            if (verbose) {
-                printf("--- archive comment (%zu)\n", a[0].comment_length);
-                printf("+++ archive comment (%zu)\n", a[1].comment_length);
+            if (a[0].comment_length > 0) {
+                diff_output_data(&output, '-', (const zip_uint8_t *)a[0].comment, a[0].comment_length, "archive comment");
+            }
+            if (a[1].comment_length > 0) {
+                diff_output_data(&output, '+', (const zip_uint8_t *)a[1].comment, a[1].comment_length, "archive comment");
             }
             res = 1;
         }
@@ -482,37 +584,36 @@
 }
 
 
-static int
-compare_list(char *const name[2], const void *l[2], const zip_uint64_t n[2], int size, int (*cmp)(const void *, const void *), int (*ignore)(const void *l, int last, const void *o), int (*check)(char *const name[2], const void *, const void *), void (*print)(const void *)) {
+static int compare_list(char *const name[2], const void *list[2], const zip_uint64_t list_length[2], int element_size, int (*cmp)(const void *a, const void *b), int (*ignore)(const void *list, int last, const void *other), int (*check)(char *const name[2], const void *a, const void *b), void (*print)(char side, const void *element), void (*start_file)(const void *element)) {
     unsigned int i[2];
-    int j, c;
+    int j;
     int diff;
 
-#define INC(k) (i[k]++, l[k] = ((const char *)l[k]) + size)
+#define INC(k) (i[k]++, list[k] = ((const char *)list[k]) + element_size)
 #define PRINT(k)                                          \
     do {                                                  \
-        if (ignore && ignore(l[k], i[k] >= n[k] - 1, i[1-k] < n[1-k] ? l[1-k] : NULL)) {   \
+        if (ignore && ignore(list[k], i[k] >= list_length[k] - 1, i[1-k] < list_length[1-k] ? list[1-k] : NULL)) {   \
             break;                                        \
         }                                                 \
-        if (header_done == 0 && verbose) {                \
-            printf("--- %s\n+++ %s\n", name[0], name[1]); \
-            header_done = 1;                              \
-        }                                                 \
-        if (verbose) {                                    \
-            printf("%c ", (k) ? '+' : '-');               \
-            print(l[k]);                                  \
-        }                                                 \
+        print((k) ? '+' : '-', list[k]);                  \
         diff = 1;                                         \
     } while (0)
 
     i[0] = i[1] = 0;
     diff = 0;
-    while (i[0] < n[0] && i[1] < n[1]) {
-        c = cmp(l[0], l[1]);
+    while (i[0] < list_length[0] && i[1] < list_length[1]) {
+        int c = cmp(list[0], list[1]);
 
         if (c == 0) {
-            if (check)
-                diff |= check(name, l[0], l[1]);
+            if (check) {
+                if (start_file) {
+                    start_file(list[0]);
+                }
+                diff |= check(name, list[0], list[1]);
+                if (start_file) {
+                    diff_output_end_file(&output);
+                }
+            }
             INC(0);
             INC(1);
         }
@@ -527,7 +628,7 @@
     }
 
     for (j = 0; j < 2; j++) {
-        while (i[j] < n[j]) {
+        while (i[j] < list_length[j]) {
             PRINT(j);
             INC(j);
         }
@@ -582,7 +683,7 @@
     n[0] = e1->n_extra_fields;
     n[1] = e2->n_extra_fields;
 
-    return compare_list(name, (const void **)ef, n, sizeof(struct ef), ef_order, NULL, NULL, ef_print);
+    return compare_list(name, (const void **)ef, n, sizeof(struct ef), ef_order, NULL, NULL, ef_print, NULL);
 }
 
 
@@ -604,15 +705,10 @@
 
 
 static void
-ef_print(const void *p) {
+ef_print(char side, const void *p) {
     const struct ef *ef = (struct ef *)p;
-    int i;
 
-    printf("                    %s  ", ef->name);
-    printf("%04x %c <", ef->id, ef->flags == ZIP_FL_LOCAL ? 'l' : 'c');
-    for (i = 0; i < ef->size; i++)
-        printf("%s%02x", i ? " " : "", ef->data[i]);
-    printf(">\n");
+    diff_output_data(&output, side, ef->data, ef->size, "  %s extra field %s", ef->flags == ZIP_FL_LOCAL ? "local" : "central", map_enum(extra_fields, ef->id));
 }
 
 
@@ -676,33 +772,19 @@
 
     ret = 0;
 
-    if (ef_compare(name, e1, e2) != 0)
-        ret = 1;
-
     if (e1->comp_method != e2->comp_method) {
-        if (verbose) {
-            if (header_done == 0) {
-                printf("--- %s\n+++ %s\n", name[0], name[1]);
-                header_done = 1;
-            }
-            printf("---                     %s  ", e1->name);
-            printf("method %u\n", e1->comp_method);
-            printf("+++                     %s  ", e1->name);
-            printf("method %u\n", e2->comp_method);
-        }
+        diff_output(&output, '-', "  compression method %s", map_enum(comp_methods, e1->comp_method));
+        diff_output(&output, '+', "  compression method %s", map_enum(comp_methods, e2->comp_method));
         ret = 1;
     }
+
+    if (ef_compare(name, e1, e2) != 0) {
+        ret = 1;
+    }
+
     if (comment_compare(e1->comment, e1->comment_length, e2->comment, e2->comment_length) != 0) {
-        if (verbose) {
-            if (header_done == 0) {
-                printf("--- %s\n+++ %s\n", name[0], name[1]);
-                header_done = 1;
-            }
-            printf("---                     %s  ", e1->name);
-            printf("comment %" PRIu32 "\n", e1->comment_length);
-            printf("+++                     %s  ", e1->name);
-            printf("comment %" PRIu32 "\n", e2->comment_length);
-        }
+        diff_output_data(&output, '-', (const zip_uint8_t *)e1->comment, e1->comment_length, "  comment");
+        diff_output_data(&output, '+', (const zip_uint8_t *)e2->comment, e2->comment_length, "  comment");
         ret = 1;
     }
 
@@ -710,14 +792,17 @@
 }
 
 
-static void
-entry_print(const void *p) {
-    const struct entry *e;
+static void entry_print(char side, const void *p) {
+    const struct entry *e = (struct entry *)p;
 
-    e = (struct entry *)p;
+    diff_output_file(&output, side, e->name, e->size, e->crc);
+}
 
-    /* TODO PRId64 */
-    printf("%10lu %08x %s\n", (unsigned long)e->size, e->crc, e->name);
+
+static void entry_start_file(const void *p) {
+    const struct entry *e = (struct entry *)p;
+    
+    diff_output_start_file(&output, e->name, e->size, e->crc);
 }
 
 
@@ -761,3 +846,21 @@
 
     return 0;
 }
+
+
+static const char *map_enum(const enum_map_t *map, uint32_t value) {
+    static char unknown[16];
+    size_t i = 0;
+    
+    while (map[i].value < UINT32_MAX) {
+        if (map[i].value == value) {
+            return map[i].name;
+        }
+        i++;
+    }
+    
+    snprintf(unknown, sizeof(unknown), "unknown (%u)", value);
+    unknown[sizeof(unknown) - 1] = '\0';
+    
+    return unknown;
+}