aboutsummaryrefslogtreecommitdiff
path: root/bin/sqfsdiff
diff options
context:
space:
mode:
Diffstat (limited to 'bin/sqfsdiff')
-rw-r--r--bin/sqfsdiff/compare_dir.c94
-rw-r--r--bin/sqfsdiff/compare_files.c72
-rw-r--r--bin/sqfsdiff/extract.c57
-rw-r--r--bin/sqfsdiff/node_compare.c203
-rw-r--r--bin/sqfsdiff/options.c131
-rw-r--r--bin/sqfsdiff/sqfsdiff.c168
-rw-r--r--bin/sqfsdiff/sqfsdiff.h69
-rw-r--r--bin/sqfsdiff/super.c125
-rw-r--r--bin/sqfsdiff/util.c25
9 files changed, 944 insertions, 0 deletions
diff --git a/bin/sqfsdiff/compare_dir.c b/bin/sqfsdiff/compare_dir.c
new file mode 100644
index 0000000..1a4c800
--- /dev/null
+++ b/bin/sqfsdiff/compare_dir.c
@@ -0,0 +1,94 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * compare_dir.c
+ *
+ * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
+ */
+#include "sqfsdiff.h"
+
+static int print_omitted(sqfsdiff_t *sd, bool is_old, sqfs_tree_node_t *n)
+{
+ char *path = node_path(n);
+
+ if (path == NULL)
+ return -1;
+
+ fprintf(stdout, "%c %s\n", is_old ? '<' : '>', path);
+
+ if ((sd->compare_flags & COMPARE_EXTRACT_FILES) &&
+ S_ISREG(n->inode->base.mode)) {
+ if (extract_files(sd, is_old ? n->inode : NULL,
+ is_old ? NULL : n->inode, path)) {
+ free(path);
+ return -1;
+ }
+ }
+
+ free(path);
+
+ for (n = n->children; n != NULL; n = n->next) {
+ if (print_omitted(sd, is_old, n))
+ return -1;
+ }
+
+ return 0;
+}
+
+int compare_dir_entries(sqfsdiff_t *sd, sqfs_tree_node_t *old,
+ sqfs_tree_node_t *new)
+{
+ sqfs_tree_node_t *old_it = old->children, *old_prev = NULL;
+ sqfs_tree_node_t *new_it = new->children, *new_prev = NULL;
+ int ret, result = 0;
+
+ while (old_it != NULL || new_it != NULL) {
+ if (old_it != NULL && new_it != NULL) {
+ ret = strcmp((const char *)old_it->name,
+ (const char *)new_it->name);
+ } else if (old_it == NULL) {
+ ret = 1;
+ } else {
+ ret = -1;
+ }
+
+ if (ret < 0) {
+ result = 1;
+
+ if (print_omitted(sd, true, old_it))
+ return -1;
+
+ if (old_prev == NULL) {
+ old->children = old_it->next;
+ sqfs_dir_tree_destroy(old_it);
+ old_it = old->children;
+ } else {
+ old_prev->next = old_it->next;
+ sqfs_dir_tree_destroy(old_it);
+ old_it = old_prev->next;
+ }
+ } else if (ret > 0) {
+ result = 1;
+
+ if (print_omitted(sd, false, new_it))
+ return -1;
+
+ if (new_prev == NULL) {
+ new->children = new_it->next;
+ sqfs_dir_tree_destroy(new_it);
+ new_it = new->children;
+ } else {
+ new_prev->next = new_it->next;
+ sqfs_dir_tree_destroy(new_it);
+ new_it = new_prev->next;
+ }
+ } else {
+ old_prev = old_it;
+ old_it = old_it->next;
+
+ new_prev = new_it;
+ new_it = new_it->next;
+ }
+ }
+
+ return result;
+}
diff --git a/bin/sqfsdiff/compare_files.c b/bin/sqfsdiff/compare_files.c
new file mode 100644
index 0000000..51b66bb
--- /dev/null
+++ b/bin/sqfsdiff/compare_files.c
@@ -0,0 +1,72 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * compare_files.c
+ *
+ * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
+ */
+#include "sqfsdiff.h"
+
+static unsigned char old_buf[MAX_WINDOW_SIZE];
+static unsigned char new_buf[MAX_WINDOW_SIZE];
+
+static int read_blob(const char *prefix, const char *path,
+ sqfs_data_reader_t *rd, const sqfs_inode_generic_t *inode,
+ void *buffer, sqfs_u64 offset, size_t size)
+{
+ ssize_t ret;
+
+ ret = sqfs_data_reader_read(rd, inode, offset, buffer, size);
+ ret = (ret < 0 || (size_t)ret < size) ? -1 : 0;
+
+ if (ret) {
+ fprintf(stderr, "Failed to read %s from %s\n",
+ path, prefix);
+ return -1;
+ }
+
+ return 0;
+}
+
+int compare_files(sqfsdiff_t *sd, const sqfs_inode_generic_t *old,
+ const sqfs_inode_generic_t *new, const char *path)
+{
+ sqfs_u64 offset, diff, oldsz, newsz;
+ int status = 0, ret;
+
+ sqfs_inode_get_file_size(old, &oldsz);
+ sqfs_inode_get_file_size(new, &newsz);
+
+ if (oldsz != newsz)
+ goto out_different;
+
+ if (sd->compare_flags & COMPARE_NO_CONTENTS)
+ return 0;
+
+ for (offset = 0; offset < oldsz; offset += diff) {
+ diff = oldsz - offset;
+
+ if (diff > MAX_WINDOW_SIZE)
+ diff = MAX_WINDOW_SIZE;
+
+ ret = read_blob(sd->old_path, path,
+ sd->sqfs_old.data, old, old_buf, offset, diff);
+ if (ret)
+ return -1;
+
+ ret = read_blob(sd->new_path, path,
+ sd->sqfs_new.data, new, new_buf, offset, diff);
+ if (ret)
+ return -1;
+
+ if (memcmp(old_buf, new_buf, diff) != 0)
+ goto out_different;
+ }
+
+ return status;
+out_different:
+ if (sd->compare_flags & COMPARE_EXTRACT_FILES) {
+ if (extract_files(sd, old, new, path))
+ return -1;
+ }
+ return 1;
+}
diff --git a/bin/sqfsdiff/extract.c b/bin/sqfsdiff/extract.c
new file mode 100644
index 0000000..979572a
--- /dev/null
+++ b/bin/sqfsdiff/extract.c
@@ -0,0 +1,57 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * extract.c
+ *
+ * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
+ */
+#include "sqfsdiff.h"
+
+static int extract(sqfs_data_reader_t *data, const sqfs_inode_generic_t *inode,
+ const char *prefix, const char *path, size_t block_size)
+{
+ char *ptr, *temp;
+ FILE *fp;
+
+ temp = alloca(strlen(prefix) + strlen(path) + 2);
+ sprintf(temp, "%s/%s", prefix, path);
+
+ ptr = strrchr(temp, '/');
+ *ptr = '\0';
+ if (mkdir_p(temp))
+ return -1;
+ *ptr = '/';
+
+ fp = fopen(temp, "wb");
+ if (fp == NULL) {
+ perror(temp);
+ return -1;
+ }
+
+ if (sqfs_data_reader_dump(path, data, inode, fp, block_size, true)) {
+ fclose(fp);
+ return -1;
+ }
+
+ fflush(fp);
+ fclose(fp);
+ return 0;
+}
+
+int extract_files(sqfsdiff_t *sd, const sqfs_inode_generic_t *old,
+ const sqfs_inode_generic_t *new,
+ const char *path)
+{
+ if (old != NULL) {
+ if (extract(sd->sqfs_old.data, old, "old",
+ path, sd->sqfs_old.super.block_size))
+ return -1;
+ }
+
+ if (new != NULL) {
+ if (extract(sd->sqfs_new.data, new, "new",
+ path, sd->sqfs_new.super.block_size))
+ return -1;
+ }
+
+ return 0;
+}
diff --git a/bin/sqfsdiff/node_compare.c b/bin/sqfsdiff/node_compare.c
new file mode 100644
index 0000000..59d1831
--- /dev/null
+++ b/bin/sqfsdiff/node_compare.c
@@ -0,0 +1,203 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * node_compare.c
+ *
+ * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
+ */
+#include "sqfsdiff.h"
+
+int node_compare(sqfsdiff_t *sd, sqfs_tree_node_t *a, sqfs_tree_node_t *b)
+{
+ char *path = sqfs_tree_node_get_path(a);
+ sqfs_tree_node_t *ait, *bit;
+ bool promoted, demoted;
+ int ret, status = 0;
+
+ if (path == NULL) {
+ perror("constructing absolute file path");
+ return -1;
+ }
+
+ if (a->inode->base.type != b->inode->base.type) {
+ promoted = demoted = false;
+
+ switch (a->inode->base.type) {
+ case SQFS_INODE_DIR:
+ if (b->inode->base.type == SQFS_INODE_EXT_DIR)
+ promoted = true;
+ break;
+ case SQFS_INODE_FILE:
+ if (b->inode->base.type == SQFS_INODE_EXT_FILE)
+ promoted = true;
+ break;
+ case SQFS_INODE_SLINK:
+ if (b->inode->base.type == SQFS_INODE_EXT_SLINK)
+ promoted = true;
+ break;
+ case SQFS_INODE_BDEV:
+ if (b->inode->base.type == SQFS_INODE_EXT_BDEV)
+ promoted = true;
+ break;
+ case SQFS_INODE_CDEV:
+ if (b->inode->base.type == SQFS_INODE_EXT_CDEV)
+ promoted = true;
+ break;
+ case SQFS_INODE_FIFO:
+ if (b->inode->base.type == SQFS_INODE_EXT_FIFO)
+ promoted = true;
+ break;
+ case SQFS_INODE_SOCKET:
+ if (b->inode->base.type == SQFS_INODE_EXT_SOCKET)
+ promoted = true;
+ break;
+ case SQFS_INODE_EXT_DIR:
+ if (b->inode->base.type == SQFS_INODE_DIR)
+ demoted = true;
+ break;
+ case SQFS_INODE_EXT_FILE:
+ if (b->inode->base.type == SQFS_INODE_FILE)
+ demoted = true;
+ break;
+ case SQFS_INODE_EXT_SLINK:
+ if (b->inode->base.type == SQFS_INODE_SLINK)
+ demoted = true;
+ break;
+ case SQFS_INODE_EXT_BDEV:
+ if (b->inode->base.type == SQFS_INODE_BDEV)
+ demoted = true;
+ break;
+ case SQFS_INODE_EXT_CDEV:
+ if (b->inode->base.type == SQFS_INODE_CDEV)
+ demoted = true;
+ break;
+ case SQFS_INODE_EXT_FIFO:
+ if (b->inode->base.type == SQFS_INODE_FIFO)
+ demoted = true;
+ break;
+ case SQFS_INODE_EXT_SOCKET:
+ if (b->inode->base.type == SQFS_INODE_SOCKET)
+ demoted = true;
+ break;
+ }
+
+ if (promoted) {
+ fprintf(stdout, "%s has an extended type\n", path);
+ status = 1;
+ } else if (demoted) {
+ fprintf(stdout, "%s has a basic type\n", path);
+ status = 1;
+ } else {
+ fprintf(stdout, "%s has a different type\n", path);
+ free(path);
+ return 1;
+ }
+ }
+
+ if (!(sd->compare_flags & COMPARE_NO_PERM)) {
+ if ((a->inode->base.mode & ~S_IFMT) !=
+ (b->inode->base.mode & ~S_IFMT)) {
+ fprintf(stdout, "%s has different permissions\n",
+ path);
+ status = 1;
+ }
+ }
+
+ if (!(sd->compare_flags & COMPARE_NO_OWNER)) {
+ if (a->uid != b->uid || a->gid != b->gid) {
+ fprintf(stdout, "%s has different ownership\n", path);
+ status = 1;
+ }
+ }
+
+ if (sd->compare_flags & COMPARE_TIMESTAMP) {
+ if (a->inode->base.mod_time != b->inode->base.mod_time) {
+ fprintf(stdout, "%s has a different timestamp\n", path);
+ status = 1;
+ }
+ }
+
+ if (sd->compare_flags & COMPARE_INODE_NUM) {
+ if (a->inode->base.inode_number !=
+ b->inode->base.inode_number) {
+ fprintf(stdout, "%s has a different inode number\n",
+ path);
+ status = 1;
+ }
+ }
+
+ switch (a->inode->base.type) {
+ case SQFS_INODE_SOCKET:
+ case SQFS_INODE_EXT_SOCKET:
+ case SQFS_INODE_FIFO:
+ case SQFS_INODE_EXT_FIFO:
+ break;
+ case SQFS_INODE_BDEV:
+ case SQFS_INODE_CDEV:
+ if (a->inode->data.dev.devno != b->inode->data.dev.devno) {
+ fprintf(stdout, "%s has different device number\n",
+ path);
+ status = 1;
+ }
+ break;
+ case SQFS_INODE_EXT_BDEV:
+ case SQFS_INODE_EXT_CDEV:
+ if (a->inode->data.dev_ext.devno !=
+ b->inode->data.dev_ext.devno) {
+ fprintf(stdout, "%s has different device number\n",
+ path);
+ status = 1;
+ }
+ break;
+ case SQFS_INODE_SLINK:
+ case SQFS_INODE_EXT_SLINK:
+ if (strcmp((const char *)a->inode->extra,
+ (const char *)b->inode->extra)) {
+ fprintf(stdout, "%s has a different link target\n",
+ path);
+ }
+ break;
+ case SQFS_INODE_DIR:
+ case SQFS_INODE_EXT_DIR:
+ ret = compare_dir_entries(sd, a, b);
+ if (ret < 0) {
+ status = -1;
+ break;
+ }
+ if (ret > 0)
+ status = 1;
+
+ free(path);
+ path = NULL;
+
+ ait = a->children;
+ bit = b->children;
+
+ while (ait != NULL && bit != NULL) {
+ ret = node_compare(sd, ait, bit);
+ if (ret < 0)
+ return -1;
+ if (ret > 0)
+ status = 1;
+
+ ait = ait->next;
+ bit = bit->next;
+ }
+ break;
+ case SQFS_INODE_FILE:
+ case SQFS_INODE_EXT_FILE:
+ ret = compare_files(sd, a->inode, b->inode, path);
+ if (ret < 0) {
+ status = -1;
+ } else if (ret > 0) {
+ fprintf(stdout, "regular file %s differs\n", path);
+ status = 1;
+ }
+ break;
+ default:
+ fprintf(stdout, "%s has unknown type, ignoring\n", path);
+ break;
+ }
+
+ free(path);
+ return status;
+}
diff --git a/bin/sqfsdiff/options.c b/bin/sqfsdiff/options.c
new file mode 100644
index 0000000..b8ce7f0
--- /dev/null
+++ b/bin/sqfsdiff/options.c
@@ -0,0 +1,131 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * sqfsdiff.c
+ *
+ * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
+ */
+#include "sqfsdiff.h"
+
+static struct option long_opts[] = {
+ { "old", required_argument, NULL, 'a' },
+ { "new", required_argument, NULL, 'b' },
+ { "no-owner", no_argument, NULL, 'O' },
+ { "no-permissions", no_argument, NULL, 'P' },
+ { "no-contents", no_argument, NULL, 'C' },
+ { "timestamps", no_argument, NULL, 'T' },
+ { "inode-num", no_argument, NULL, 'I' },
+ { "super", no_argument, NULL, 'S' },
+ { "extract", required_argument, NULL, 'e' },
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'V' },
+ { NULL, 0, NULL, 0 },
+};
+
+static const char *short_opts = "a:b:OPCTISe:hV";
+
+static const char *usagestr =
+"Usage: sqfsdiff [OPTIONS...] --old,-a <first> --new,-b <second>\n"
+"\n"
+"Compare two squashfs images. In contrast to doing a direct diff of the\n"
+"images, this actually parses the filesystems and generates a more\n"
+"meaningful difference report.\n"
+"\n"
+"If only contents are compared, any differences in packed file layout,\n"
+"ordering, compression, inode meta data and so on is ignored and the two\n"
+"images are considered equal if each directory contains the same entries,\n"
+"symlink with the same paths have the same targets, device nodes the same\n"
+"device number and files the same size and contents.\n"
+"\n"
+"A report of any difference is printed to stdout. The exit status is similar\n"
+"that of diff(1): 0 means equal, 1 means different, 2 means problem.\n"
+"\n"
+"Possible options:\n"
+"\n"
+" --old, -a <first> The first of the two filesystems to compare.\n"
+" --new, -b <second> The second of the two filesystems to compare.\n"
+"\n"
+" --no-contents, -C Do not compare file contents.\n"
+" --no-owner, -O Do not compare file owners.\n"
+" --no-permissions, -P Do not compare permission bits.\n"
+"\n"
+" --timestamps, -T Compare file timestamps.\n"
+" --inode-num, -I Compare inode numbers of all files.\n"
+" --super, -S Also compare meta data in super blocks.\n"
+"\n"
+" --extract, -e <path> Extract files that differ to the specified\n"
+" directory. Contents of the first filesystem\n"
+" end up in a subdirectory 'old' and of the\n"
+" second filesystem in a subdirectory 'new'.\n"
+"\n"
+" --help, -h Print help text and exit.\n"
+" --version, -V Print version information and exit.\n"
+"\n";
+
+void process_options(sqfsdiff_t *sd, int argc, char **argv)
+{
+ int i;
+
+ for (;;) {
+ i = getopt_long(argc, argv, short_opts, long_opts, NULL);
+ if (i == -1)
+ break;
+
+ switch (i) {
+ case 'a':
+ sd->old_path = optarg;
+ break;
+ case 'b':
+ sd->new_path = optarg;
+ break;
+ case 'O':
+ sd->compare_flags |= COMPARE_NO_OWNER;
+ break;
+ case 'P':
+ sd->compare_flags |= COMPARE_NO_PERM;
+ break;
+ case 'C':
+ sd->compare_flags |= COMPARE_NO_CONTENTS;
+ break;
+ case 'T':
+ sd->compare_flags |= COMPARE_TIMESTAMP;
+ break;
+ case 'I':
+ sd->compare_flags |= COMPARE_INODE_NUM;
+ break;
+ case 'S':
+ sd->compare_super = true;
+ break;
+ case 'e':
+ sd->compare_flags |= COMPARE_EXTRACT_FILES;
+ sd->extract_dir = optarg;
+ break;
+ case 'h':
+ fputs(usagestr, stdout);
+ exit(0);
+ case 'V':
+ print_version("sqfsdiff");
+ exit(0);
+ default:
+ goto fail_arg;
+ }
+ }
+
+ if (sd->old_path == NULL) {
+ fputs("Missing arguments: first filesystem\n", stderr);
+ goto fail_arg;
+ }
+
+ if (sd->new_path == NULL) {
+ fputs("Missing arguments: second filesystem\n", stderr);
+ goto fail_arg;
+ }
+
+ if (optind < argc) {
+ fputs("Unknown extra arguments\n", stderr);
+ goto fail_arg;
+ }
+ return;
+fail_arg:
+ fprintf(stderr, "Try `sqfsdiff --help' for more information.\n");
+ exit(2);
+}
diff --git a/bin/sqfsdiff/sqfsdiff.c b/bin/sqfsdiff/sqfsdiff.c
new file mode 100644
index 0000000..2871322
--- /dev/null
+++ b/bin/sqfsdiff/sqfsdiff.c
@@ -0,0 +1,168 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * sqfsdiff.c
+ *
+ * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
+ */
+#include "sqfsdiff.h"
+
+static int open_sfqs(sqfs_state_t *state, const char *path)
+{
+ int ret;
+
+ state->file = sqfs_open_file(path, SQFS_FILE_OPEN_READ_ONLY);
+ if (state->file == NULL) {
+ perror(path);
+ return -1;
+ }
+
+ ret = sqfs_super_read(&state->super, state->file);
+ if (ret) {
+ sqfs_perror(path, "reading super block", ret);
+ goto fail_file;
+ }
+
+ sqfs_compressor_config_init(&state->cfg, state->super.compression_id,
+ state->super.block_size,
+ SQFS_COMP_FLAG_UNCOMPRESS);
+
+ ret = sqfs_compressor_create(&state->cfg, &state->cmp);
+
+#ifdef WITH_LZO
+ if (state->super.compression_id == SQFS_COMP_LZO && ret != 0)
+ ret = lzo_compressor_create(&state->cfg, &state->cmp);
+#endif
+
+ if (ret != 0) {
+ sqfs_perror(path, "creating compressor", ret);
+ goto fail_file;
+ }
+
+ if (state->super.flags & SQFS_FLAG_COMPRESSOR_OPTIONS) {
+ ret = state->cmp->read_options(state->cmp, state->file);
+ if (ret) {
+ sqfs_perror(path, "reading compressor options", ret);
+ goto fail_cmp;
+ }
+ }
+
+ state->idtbl = sqfs_id_table_create(0);
+ if (state->idtbl == NULL) {
+ sqfs_perror(path, "creating ID table", SQFS_ERROR_ALLOC);
+ goto fail_cmp;
+ }
+
+ ret = sqfs_id_table_read(state->idtbl, state->file,
+ &state->super, state->cmp);
+ if (ret) {
+ sqfs_perror(path, "loading ID table", ret);
+ goto fail_id;
+ }
+
+ state->dr = sqfs_dir_reader_create(&state->super, state->cmp,
+ state->file);
+ if (state->dr == NULL) {
+ sqfs_perror(path, "creating directory reader",
+ SQFS_ERROR_ALLOC);
+ goto fail_id;
+ }
+
+ ret = sqfs_dir_reader_get_full_hierarchy(state->dr, state->idtbl,
+ NULL, 0, &state->root);
+ if (ret) {
+ sqfs_perror(path, "loading filesystem tree", ret);
+ goto fail_dr;
+ }
+
+ state->data = sqfs_data_reader_create(state->file,
+ state->super.block_size,
+ state->cmp);
+ if (state->data == NULL) {
+ sqfs_perror(path, "creating data reader", SQFS_ERROR_ALLOC);
+ goto fail_tree;
+ }
+
+ ret = sqfs_data_reader_load_fragment_table(state->data, &state->super);
+ if (ret) {
+ sqfs_perror(path, "loading fragment table", ret);
+ goto fail_data;
+ }
+
+ return 0;
+fail_data:
+ sqfs_destroy(state->data);
+fail_tree:
+ sqfs_dir_tree_destroy(state->root);
+fail_dr:
+ sqfs_destroy(state->dr);
+fail_id:
+ sqfs_destroy(state->idtbl);
+fail_cmp:
+ sqfs_destroy(state->cmp);
+fail_file:
+ sqfs_destroy(state->file);
+ return -1;
+}
+
+static void close_sfqs(sqfs_state_t *state)
+{
+ sqfs_destroy(state->data);
+ sqfs_dir_tree_destroy(state->root);
+ sqfs_destroy(state->dr);
+ sqfs_destroy(state->idtbl);
+ sqfs_destroy(state->cmp);
+ sqfs_destroy(state->file);
+}
+
+int main(int argc, char **argv)
+{
+ int status, ret = 0;
+ sqfsdiff_t sd;
+
+ memset(&sd, 0, sizeof(sd));
+ process_options(&sd, argc, argv);
+
+ if (sd.extract_dir != NULL) {
+ if (mkdir_p(sd.extract_dir))
+ return 2;
+ }
+
+ if (open_sfqs(&sd.sqfs_old, sd.old_path))
+ return 2;
+
+ if (open_sfqs(&sd.sqfs_new, sd.new_path)) {
+ status = 2;
+ goto out_sqfs_old;
+ }
+
+ if (sd.extract_dir != NULL) {
+ if (chdir(sd.extract_dir)) {
+ perror(sd.extract_dir);
+ ret = -1;
+ goto out;
+ }
+ }
+
+ ret = node_compare(&sd, sd.sqfs_old.root, sd.sqfs_new.root);
+ if (ret != 0)
+ goto out;
+
+ if (sd.compare_super) {
+ ret = compare_super_blocks(&sd.sqfs_old.super,
+ &sd.sqfs_new.super);
+ if (ret != 0)
+ goto out;
+ }
+out:
+ if (ret < 0) {
+ status = 2;
+ } else if (ret > 0) {
+ status = 1;
+ } else {
+ status = 0;
+ }
+ close_sfqs(&sd.sqfs_new);
+out_sqfs_old:
+ close_sfqs(&sd.sqfs_old);
+ return status;
+}
diff --git a/bin/sqfsdiff/sqfsdiff.h b/bin/sqfsdiff/sqfsdiff.h
new file mode 100644
index 0000000..94fce93
--- /dev/null
+++ b/bin/sqfsdiff/sqfsdiff.h
@@ -0,0 +1,69 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * sqfsdiff.h
+ *
+ * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
+ */
+#ifndef DIFFTOOL_H
+#define DIFFTOOL_H
+
+#include "config.h"
+#include "common.h"
+#include "fstree.h"
+
+#include <stdlib.h>
+#include <getopt.h>
+#include <string.h>
+#include <errno.h>
+
+#define MAX_WINDOW_SIZE (1024 * 1024 * 4)
+
+typedef struct {
+ sqfs_compressor_config_t cfg;
+ sqfs_compressor_t *cmp;
+ sqfs_super_t super;
+ sqfs_file_t *file;
+ sqfs_id_table_t *idtbl;
+ sqfs_dir_reader_t *dr;
+ sqfs_tree_node_t *root;
+ sqfs_data_reader_t *data;
+} sqfs_state_t;
+
+typedef struct {
+ const char *old_path;
+ const char *new_path;
+ int compare_flags;
+ sqfs_state_t sqfs_old;
+ sqfs_state_t sqfs_new;
+ bool compare_super;
+ const char *extract_dir;
+} sqfsdiff_t;
+
+enum {
+ COMPARE_NO_PERM = 0x01,
+ COMPARE_NO_OWNER = 0x02,
+ COMPARE_NO_CONTENTS = 0x04,
+ COMPARE_TIMESTAMP = 0x08,
+ COMPARE_INODE_NUM = 0x10,
+ COMPARE_EXTRACT_FILES = 0x20,
+};
+
+int compare_dir_entries(sqfsdiff_t *sd, sqfs_tree_node_t *old,
+ sqfs_tree_node_t *new);
+
+char *node_path(const sqfs_tree_node_t *n);
+
+int compare_files(sqfsdiff_t *sd, const sqfs_inode_generic_t *old,
+ const sqfs_inode_generic_t *new, const char *path);
+
+int node_compare(sqfsdiff_t *sd, sqfs_tree_node_t *a, sqfs_tree_node_t *b);
+
+int compare_super_blocks(const sqfs_super_t *a, const sqfs_super_t *b);
+
+int extract_files(sqfsdiff_t *sd, const sqfs_inode_generic_t *old,
+ const sqfs_inode_generic_t *new,
+ const char *path);
+
+void process_options(sqfsdiff_t *sd, int argc, char **argv);
+
+#endif /* DIFFTOOL_H */
diff --git a/bin/sqfsdiff/super.c b/bin/sqfsdiff/super.c
new file mode 100644
index 0000000..111412a
--- /dev/null
+++ b/bin/sqfsdiff/super.c
@@ -0,0 +1,125 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * super.c
+ *
+ * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
+ */
+#include "sqfsdiff.h"
+
+static const struct {
+ sqfs_u16 mask;
+ const char *name;
+} sqfs_flags[] = {
+ { SQFS_FLAG_UNCOMPRESSED_INODES, "uncompressed inodes" },
+ { SQFS_FLAG_UNCOMPRESSED_DATA, "uncompressed data" },
+ { SQFS_FLAG_UNCOMPRESSED_FRAGMENTS, "uncompressed fragments" },
+ { SQFS_FLAG_NO_FRAGMENTS, "no fragments" },
+ { SQFS_FLAG_ALWAYS_FRAGMENTS, "always fragments" },
+ { SQFS_FLAG_DUPLICATES, "duplicates" },
+ { SQFS_FLAG_EXPORTABLE, "exportable" },
+ { SQFS_FLAG_UNCOMPRESSED_XATTRS, "uncompressed xattrs" },
+ { SQFS_FLAG_NO_XATTRS, "no xattrs" },
+ { SQFS_FLAG_COMPRESSOR_OPTIONS, "compressor options" },
+ { SQFS_FLAG_UNCOMPRESSED_IDS, "uncompressed ids" },
+};
+
+static void print_value_difference(const char *name, sqfs_u64 a, sqfs_u64 b)
+{
+ sqfs_u64 diff;
+ char c;
+
+ if (a != b) {
+ if (a < b) {
+ c = '+';
+ diff = b - a;
+ } else {
+ c = '-';
+ diff = a - b;
+ }
+ fprintf(stdout, "%s: %c%lu\n", name, c,
+ (unsigned long)diff);
+ }
+}
+
+static void print_offset_diff(const char *name, sqfs_u64 a, sqfs_u64 b)
+{
+ if (a != b)
+ fprintf(stdout, "Location of %s differs\n", name);
+}
+
+static void print_flag_diff(sqfs_u16 a, sqfs_u16 b)
+{
+ sqfs_u16 diff = a ^ b, mask;
+ size_t i;
+ char c;
+
+ if (diff == 0)
+ return;
+
+ fputs("flags:\n", stdout);
+
+ for (i = 0; i < sizeof(sqfs_flags) / sizeof(sqfs_flags[0]); ++i) {
+ if (diff & sqfs_flags[i].mask) {
+ c = a & sqfs_flags[i].mask ? '<' : '>';
+
+ fprintf(stdout, "\t%c%s\n", c, sqfs_flags[i].name);
+ }
+
+ a &= ~sqfs_flags[i].mask;
+ b &= ~sqfs_flags[i].mask;
+ diff &= ~sqfs_flags[i].mask;
+ }
+
+ for (i = 0, mask = 0x01; i < 16; ++i, mask <<= 1) {
+ if (diff & mask) {
+ fprintf(stdout, "\t%c additional unknown\n",
+ a & mask ? '<' : '>');
+ }
+ }
+}
+
+int compare_super_blocks(const sqfs_super_t *a, const sqfs_super_t *b)
+{
+ if (memcmp(a, b, sizeof(*a)) == 0)
+ return 0;
+
+ fputs("======== super blocks are different ========\n", stdout);
+
+ /* TODO: if a new magic number or squashfs version is introduced,
+ compare them. */
+
+ print_value_difference("inode count", a->inode_count, b->inode_count);
+ print_value_difference("modification time", a->modification_time,
+ b->modification_time);
+ print_value_difference("block size", a->block_size, b->block_size);
+ print_value_difference("block log", a->block_log, b->block_log);
+ print_value_difference("fragment table entries",
+ a->fragment_entry_count,
+ b->fragment_entry_count);
+ print_value_difference("ID table entries", a->id_count, b->id_count);
+
+ if (a->compression_id != b->compression_id) {
+ fprintf(stdout, "compressor: %s vs %s\n",
+ sqfs_compressor_name_from_id(a->compression_id),
+ sqfs_compressor_name_from_id(b->compression_id));
+ }
+
+ print_flag_diff(a->flags, b->flags);
+
+ print_value_difference("total bytes used", a->bytes_used,
+ b->bytes_used);
+
+ print_offset_diff("root inode", a->root_inode_ref, b->root_inode_ref);
+ print_offset_diff("ID table", a->id_table_start, b->id_table_start);
+ print_offset_diff("xattr ID table", a->xattr_id_table_start,
+ b->xattr_id_table_start);
+ print_offset_diff("inode table", a->inode_table_start,
+ b->inode_table_start);
+ print_offset_diff("directory table", a->directory_table_start,
+ b->directory_table_start);
+ print_offset_diff("fragment table", a->fragment_table_start,
+ b->fragment_table_start);
+ print_offset_diff("export table", a->export_table_start,
+ b->export_table_start);
+ return 1;
+}
diff --git a/bin/sqfsdiff/util.c b/bin/sqfsdiff/util.c
new file mode 100644
index 0000000..5e9161a
--- /dev/null
+++ b/bin/sqfsdiff/util.c
@@ -0,0 +1,25 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * util.c
+ *
+ * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
+ */
+#include "sqfsdiff.h"
+
+char *node_path(const sqfs_tree_node_t *n)
+{
+ char *path = sqfs_tree_node_get_path(n);
+
+ if (path == NULL) {
+ perror("get path");
+ return NULL;
+ }
+
+ if (canonicalize_name(path)) {
+ fprintf(stderr, "failed to canonicalization '%s'\n", path);
+ free(path);
+ return NULL;
+ }
+
+ return path;
+}