From 20b0d509f67dea802706cd6b80b5e20d14988931 Mon Sep 17 00:00:00 2001
From: David Oberhollenzer <david.oberhollenzer@sigma-star.at>
Date: Mon, 27 Apr 2020 11:59:02 +0200
Subject: Cleanup directory structure of the binary programs

Instead of having the binary programs in randomly named subdirectories,
move all of them to a "bin" subdirectory, similar to the utility
libraries that have subdirectories within "lib" and give the
subdirectories the propper names (e.g. have gensquashfs source in a
directory *actually* named "gensquashfs").

Signed-off-by: David Oberhollenzer <david.oberhollenzer@sigma-star.at>
---
 bin/Makemodule.am               |  42 +++
 bin/gensquashfs/dirscan.c       | 321 +++++++++++++++++++
 bin/gensquashfs/mkfs.c          | 216 +++++++++++++
 bin/gensquashfs/mkfs.h          |  72 +++++
 bin/gensquashfs/options.c       | 289 +++++++++++++++++
 bin/gensquashfs/selinux.c       |  78 +++++
 bin/rdsquashfs/describe.c       | 125 ++++++++
 bin/rdsquashfs/dump_xattrs.c    | 120 +++++++
 bin/rdsquashfs/fill_files.c     | 183 +++++++++++
 bin/rdsquashfs/list_files.c     | 156 +++++++++
 bin/rdsquashfs/options.c        | 217 +++++++++++++
 bin/rdsquashfs/rdsquashfs.c     | 175 ++++++++++
 bin/rdsquashfs/rdsquashfs.h     |  77 +++++
 bin/rdsquashfs/restore_fstree.c | 320 +++++++++++++++++++
 bin/sqfs2tar.c                  | 688 ++++++++++++++++++++++++++++++++++++++++
 bin/sqfsdiff/compare_dir.c      |  94 ++++++
 bin/sqfsdiff/compare_files.c    |  72 +++++
 bin/sqfsdiff/extract.c          |  57 ++++
 bin/sqfsdiff/node_compare.c     | 203 ++++++++++++
 bin/sqfsdiff/options.c          | 131 ++++++++
 bin/sqfsdiff/sqfsdiff.c         | 168 ++++++++++
 bin/sqfsdiff/sqfsdiff.h         |  69 ++++
 bin/sqfsdiff/super.c            | 125 ++++++++
 bin/sqfsdiff/util.c             |  25 ++
 bin/tar2sqfs.c                  | 544 +++++++++++++++++++++++++++++++
 25 files changed, 4567 insertions(+)
 create mode 100644 bin/Makemodule.am
 create mode 100644 bin/gensquashfs/dirscan.c
 create mode 100644 bin/gensquashfs/mkfs.c
 create mode 100644 bin/gensquashfs/mkfs.h
 create mode 100644 bin/gensquashfs/options.c
 create mode 100644 bin/gensquashfs/selinux.c
 create mode 100644 bin/rdsquashfs/describe.c
 create mode 100644 bin/rdsquashfs/dump_xattrs.c
 create mode 100644 bin/rdsquashfs/fill_files.c
 create mode 100644 bin/rdsquashfs/list_files.c
 create mode 100644 bin/rdsquashfs/options.c
 create mode 100644 bin/rdsquashfs/rdsquashfs.c
 create mode 100644 bin/rdsquashfs/rdsquashfs.h
 create mode 100644 bin/rdsquashfs/restore_fstree.c
 create mode 100644 bin/sqfs2tar.c
 create mode 100644 bin/sqfsdiff/compare_dir.c
 create mode 100644 bin/sqfsdiff/compare_files.c
 create mode 100644 bin/sqfsdiff/extract.c
 create mode 100644 bin/sqfsdiff/node_compare.c
 create mode 100644 bin/sqfsdiff/options.c
 create mode 100644 bin/sqfsdiff/sqfsdiff.c
 create mode 100644 bin/sqfsdiff/sqfsdiff.h
 create mode 100644 bin/sqfsdiff/super.c
 create mode 100644 bin/sqfsdiff/util.c
 create mode 100644 bin/tar2sqfs.c

(limited to 'bin')

diff --git a/bin/Makemodule.am b/bin/Makemodule.am
new file mode 100644
index 0000000..4199ac5
--- /dev/null
+++ b/bin/Makemodule.am
@@ -0,0 +1,42 @@
+sqfs2tar_SOURCES = bin/sqfs2tar.c
+sqfs2tar_CFLAGS = $(AM_CFLAGS) $(PTHREAD_CFLAGS)
+sqfs2tar_LDADD = libcommon.a libutil.a libsquashfs.la libtar.a libcompat.a
+sqfs2tar_LDADD += libfstree.a $(LZO_LIBS) $(PTHREAD_LIBS)
+
+tar2sqfs_SOURCES = bin/tar2sqfs.c
+tar2sqfs_CFLAGS = $(AM_CFLAGS) $(PTHREAD_CFLAGS)
+tar2sqfs_LDADD = libcommon.a libsquashfs.la libtar.a
+tar2sqfs_LDADD += libfstree.a libcompat.a libfstree.a $(LZO_LIBS)
+tar2sqfs_LDADD += $(PTHREAD_LIBS)
+
+rdsquashfs_SOURCES = bin/rdsquashfs/rdsquashfs.c bin/rdsquashfs/rdsquashfs.h
+rdsquashfs_SOURCES += bin/rdsquashfs/list_files.c bin/rdsquashfs/options.c
+rdsquashfs_SOURCES += bin/rdsquashfs/restore_fstree.c bin/rdsquashfs/describe.c
+rdsquashfs_SOURCES += bin/rdsquashfs/fill_files.c bin/rdsquashfs/dump_xattrs.c
+rdsquashfs_CFLAGS = $(AM_CFLAGS) $(PTHREAD_CFLAGS)
+rdsquashfs_LDADD = libcommon.a libcompat.a libsquashfs.la
+rdsquashfs_LDADD += libfstree.a $(LZO_LIBS) $(PTHREAD_LIBS)
+
+sqfsdiff_SOURCES = bin/sqfsdiff/sqfsdiff.c bin/sqfsdiff/sqfsdiff.h
+sqfsdiff_SOURCES += bin/sqfsdiff/util.c bin/sqfsdiff/options.c
+sqfsdiff_SOURCES += bin/sqfsdiff/compare_dir.c bin/sqfsdiff/node_compare.c
+sqfsdiff_SOURCES += bin/sqfsdiff/compare_files.c bin/sqfsdiff/super.c
+sqfsdiff_SOURCES += bin/sqfsdiff/extract.c
+sqfsdiff_CFLAGS = $(AM_CFLAGS) $(PTHREAD_CFLAGS)
+sqfsdiff_LDADD = libcommon.a libsquashfs.la libcompat.a $(LZO_LIBS) libfstree.a
+sqfsdiff_LDADD += $(PTHREAD_LIBS)
+
+gensquashfs_SOURCES = bin/gensquashfs/mkfs.c bin/gensquashfs/mkfs.h
+gensquashfs_SOURCES += bin/gensquashfs/options.c bin/gensquashfs/selinux.c
+gensquashfs_SOURCES += bin/gensquashfs/dirscan.c
+gensquashfs_LDADD = libcommon.a libsquashfs.la libfstree.a
+gensquashfs_LDADD += libcompat.a $(LIBSELINUX_LIBS) $(LZO_LIBS)
+gensquashfs_LDADD += $(PTHREAD_LIBS)
+gensquashfs_CPPFLAGS = $(AM_CPPFLAGS)
+gensquashfs_CFLAGS = $(AM_CFLAGS) $(LIBSELINUX_CFLAGS) $(PTHREAD_CFLAGS)
+
+if WITH_SELINUX
+gensquashfs_CPPFLAGS += -DWITH_SELINUX
+endif
+
+bin_PROGRAMS += sqfs2tar tar2sqfs gensquashfs rdsquashfs sqfsdiff
diff --git a/bin/gensquashfs/dirscan.c b/bin/gensquashfs/dirscan.c
new file mode 100644
index 0000000..dbc862c
--- /dev/null
+++ b/bin/gensquashfs/dirscan.c
@@ -0,0 +1,321 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * fstree_from_dir.c
+ *
+ * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
+ */
+#include "mkfs.h"
+
+#ifdef HAVE_SYS_XATTR_H
+static char *get_full_path(const char *prefix, tree_node_t *node)
+{
+	char *path = NULL, *new = NULL;
+	size_t path_len, prefix_len;
+	int ret;
+
+	path = fstree_get_path(node);
+	if (path == NULL)
+		goto fail;
+
+	ret = canonicalize_name(path);
+	assert(ret == 0);
+
+	path_len = strlen(path);
+	prefix_len = strlen(prefix);
+
+	while (prefix_len > 0 && prefix[prefix_len - 1] == '/')
+		--prefix_len;
+
+	if (prefix_len > 0) {
+		new = realloc(path, path_len + prefix_len + 2);
+		if (new == NULL)
+			goto fail;
+
+		path = new;
+
+		memmove(path + prefix_len + 1, path, path_len + 1);
+		memcpy(path, prefix, prefix_len);
+		path[prefix_len] = '/';
+	}
+
+	return path;
+fail:
+	perror("getting full path for xattr scan");
+	free(path);
+	return NULL;
+}
+
+static int xattr_from_path(sqfs_xattr_writer_t *xwr, const char *path)
+{
+	char *key, *value = NULL, *buffer = NULL;
+	ssize_t buflen, vallen, keylen;
+	int ret;
+
+	buflen = llistxattr(path, NULL, 0);
+	if (buflen < 0) {
+		fprintf(stderr, "llistxattr %s: %s", path, strerror(errno));
+		return -1;
+	}
+
+	if (buflen == 0)
+		return 0;
+
+	buffer = malloc(buflen);
+	if (buffer == NULL) {
+		perror("xattr name buffer");
+		return -1;
+	}
+
+	buflen = llistxattr(path, buffer, buflen);
+	if (buflen == -1) {
+		fprintf(stderr, "llistxattr %s: %s", path, strerror(errno));
+		goto fail;
+	}
+
+	key = buffer;
+	while (buflen > 0) {
+		vallen = lgetxattr(path, key, NULL, 0);
+		if (vallen == -1) {
+			fprintf(stderr, "lgetxattr %s: %s",
+				path, strerror(errno));
+			goto fail;
+		}
+
+		if (vallen > 0) {
+			value = calloc(1, vallen);
+			if (value == NULL) {
+				perror("allocating xattr value buffer");
+				goto fail;
+			}
+
+			vallen = lgetxattr(path, key, value, vallen);
+			if (vallen == -1) {
+				fprintf(stderr, "lgetxattr %s: %s\n",
+					path, strerror(errno));
+				goto fail;
+			}
+
+			ret = sqfs_xattr_writer_add(xwr, key, value, vallen);
+			if (ret) {
+				sqfs_perror(path,
+					    "storing xattr key-value pairs",
+					    ret);
+				goto fail;
+			}
+
+			free(value);
+			value = NULL;
+		}
+
+		keylen = strlen(key) + 1;
+		buflen -= keylen;
+		key += keylen;
+	}
+
+	free(buffer);
+	return 0;
+fail:
+	free(value);
+	free(buffer);
+	return -1;
+}
+#endif
+
+#ifdef _WIN32
+int fstree_from_dir(fstree_t *fs, const char *path, void *selinux_handle,
+		    sqfs_xattr_writer_t *xwr, unsigned int flags)
+{
+	(void)fs; (void)path; (void)selinux_handle; (void)xwr; (void)flags;
+	fputs("Packing a directory is not supported on Windows.\n", stderr);
+	return -1;
+}
+#else
+static int xattr_xcan_dfs(const char *path_prefix, void *selinux_handle,
+			  sqfs_xattr_writer_t *xwr, unsigned int flags,
+			  tree_node_t *node)
+{
+	char *path;
+	int ret;
+
+	ret = sqfs_xattr_writer_begin(xwr);
+	if (ret) {
+		sqfs_perror(node->name, "recoding xattr key-value pairs\n",
+			    ret);
+		return -1;
+	}
+
+#ifdef HAVE_SYS_XATTR_H
+	if (flags & DIR_SCAN_READ_XATTR) {
+		path = get_full_path(path_prefix, node);
+		if (path == NULL)
+			return -1;
+
+		ret = xattr_from_path(xwr, path);
+		free(path);
+
+		if (ret)
+			return -1;
+	}
+#else
+	(void)path_prefix;
+#endif
+
+	if (selinux_handle != NULL) {
+		path = fstree_get_path(node);
+		if (path == NULL) {
+			perror("reconstructing absolute path");
+			return -1;
+		}
+
+		ret = selinux_relable_node(selinux_handle, xwr, node, path);
+		free(path);
+
+		if (ret)
+			return -1;
+	}
+
+	if (sqfs_xattr_writer_end(xwr, &node->xattr_idx)) {
+		sqfs_perror(node->name, "completing xattr key-value pairs",
+			    ret);
+		return -1;
+	}
+
+	if (S_ISDIR(node->mode)) {
+		node = node->data.dir.children;
+
+		while (node != NULL) {
+			if (xattr_xcan_dfs(path_prefix, selinux_handle, xwr,
+					   flags, node)) {
+				return -1;
+			}
+
+			node = node->next;
+		}
+	}
+
+	return 0;
+}
+
+static int populate_dir(int dir_fd, fstree_t *fs, tree_node_t *root,
+			dev_t devstart, unsigned int flags)
+{
+	char *extra = NULL;
+	struct dirent *ent;
+	struct stat sb;
+	tree_node_t *n;
+	int childfd;
+	DIR *dir;
+
+	dir = fdopendir(dir_fd);
+	if (dir == NULL) {
+		perror("fdopendir");
+		close(dir_fd);
+		return -1;
+	}
+
+	/* XXX: fdopendir can dup and close dir_fd internally
+	   and still be compliant with the spec. */
+	dir_fd = dirfd(dir);
+
+	for (;;) {
+		errno = 0;
+		ent = readdir(dir);
+
+		if (ent == NULL) {
+			if (errno) {
+				perror("readdir");
+				goto fail;
+			}
+			break;
+		}
+
+		if (!strcmp(ent->d_name, "..") || !strcmp(ent->d_name, "."))
+			continue;
+
+		if (fstatat(dir_fd, ent->d_name, &sb, AT_SYMLINK_NOFOLLOW)) {
+			perror(ent->d_name);
+			goto fail;
+		}
+
+		if ((flags & DIR_SCAN_ONE_FILESYSTEM) && sb.st_dev != devstart)
+			continue;
+
+		if (S_ISLNK(sb.st_mode)) {
+			extra = calloc(1, sb.st_size + 1);
+			if (extra == NULL)
+				goto fail_rdlink;
+
+			if (readlinkat(dir_fd, ent->d_name,
+				       extra, sb.st_size) < 0) {
+				goto fail_rdlink;
+			}
+
+			extra[sb.st_size] = '\0';
+		}
+
+		if (!(flags & DIR_SCAN_KEEP_TIME))
+			sb.st_mtime = fs->defaults.st_mtime;
+
+		n = fstree_mknode(root, ent->d_name, strlen(ent->d_name),
+				  extra, &sb);
+		if (n == NULL) {
+			perror("creating tree node");
+			goto fail;
+		}
+
+		free(extra);
+		extra = NULL;
+
+		if (S_ISDIR(n->mode)) {
+			childfd = openat(dir_fd, n->name, O_DIRECTORY |
+					 O_RDONLY | O_CLOEXEC);
+			if (childfd < 0) {
+				perror(n->name);
+				goto fail;
+			}
+
+			if (populate_dir(childfd, fs, n, devstart, flags))
+				goto fail;
+		}
+	}
+
+	closedir(dir);
+	return 0;
+fail_rdlink:
+	perror("readlink");
+fail:
+	closedir(dir);
+	free(extra);
+	return -1;
+}
+
+int fstree_from_dir(fstree_t *fs, const char *path, void *selinux_handle,
+		    sqfs_xattr_writer_t *xwr, unsigned int flags)
+{
+	struct stat sb;
+	int fd;
+
+	fd = open(path, O_DIRECTORY | O_RDONLY | O_CLOEXEC);
+	if (fd < 0) {
+		perror(path);
+		return -1;
+	}
+
+	if (fstat(fd, &sb)) {
+		perror(path);
+		close(fd);
+		return -1;
+	}
+
+	if (populate_dir(fd, fs, fs->root, sb.st_dev, flags))
+		return -1;
+
+	if (xwr != NULL && (selinux_handle != NULL ||
+			    (flags & DIR_SCAN_READ_XATTR))) {
+		if (xattr_xcan_dfs(path, selinux_handle, xwr, flags, fs->root))
+			return -1;
+	}
+
+	return 0;
+}
+#endif
diff --git a/bin/gensquashfs/mkfs.c b/bin/gensquashfs/mkfs.c
new file mode 100644
index 0000000..9ffbb94
--- /dev/null
+++ b/bin/gensquashfs/mkfs.c
@@ -0,0 +1,216 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * mkfs.c
+ *
+ * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
+ */
+#include "mkfs.h"
+
+static int set_working_dir(options_t *opt)
+{
+	const char *ptr;
+	char *path;
+
+	if (opt->packdir != NULL) {
+		if (chdir(opt->packdir)) {
+			perror(opt->packdir);
+			return -1;
+		}
+		return 0;
+	}
+
+	ptr = strrchr(opt->infile, '/');
+	if (ptr == NULL)
+		return 0;
+
+	path = strndup(opt->infile, ptr - opt->infile);
+	if (path == NULL) {
+		perror("constructing input directory path");
+		return -1;
+	}
+
+	if (chdir(path)) {
+		perror(path);
+		free(path);
+		return -1;
+	}
+
+	free(path);
+	return 0;
+}
+
+static int pack_files(sqfs_block_processor_t *data, fstree_t *fs,
+		      options_t *opt)
+{
+	sqfs_inode_generic_t **inode_ptr;
+	sqfs_u64 filesize;
+	sqfs_file_t *file;
+	tree_node_t *node;
+	const char *path;
+	char *node_path;
+	file_info_t *fi;
+	int flags;
+	int ret;
+
+	if (set_working_dir(opt))
+		return -1;
+
+	for (fi = fs->files; fi != NULL; fi = fi->next) {
+		if (fi->input_file == NULL) {
+			node = container_of(fi, tree_node_t, data.file);
+
+			node_path = fstree_get_path(node);
+			if (node_path == NULL) {
+				perror("reconstructing file path");
+				return -1;
+			}
+
+			ret = canonicalize_name(node_path);
+			assert(ret == 0);
+
+			path = node_path;
+		} else {
+			node_path = NULL;
+			path = fi->input_file;
+		}
+
+		if (!opt->cfg.quiet)
+			printf("packing %s\n", path);
+
+		file = sqfs_open_file(path, SQFS_FILE_OPEN_READ_ONLY);
+		if (file == NULL) {
+			perror(path);
+			free(node_path);
+			return -1;
+		}
+
+		flags = 0;
+		filesize = file->get_size(file);
+
+		if (opt->no_tail_packing && filesize > opt->cfg.block_size)
+			flags |= SQFS_BLK_DONT_FRAGMENT;
+
+		inode_ptr = (sqfs_inode_generic_t **)&fi->user_ptr;
+
+		ret = write_data_from_file(path, data, inode_ptr, file, flags);
+		sqfs_destroy(file);
+		free(node_path);
+
+		if (ret)
+			return -1;
+	}
+
+	return 0;
+}
+
+static int relabel_tree_dfs(const char *filename, sqfs_xattr_writer_t *xwr,
+			    tree_node_t *n, void *selinux_handle)
+{
+	char *path = fstree_get_path(n);
+	int ret;
+
+	if (path == NULL) {
+		perror("getting absolute node path for SELinux relabeling");
+		return -1;
+	}
+
+	ret = sqfs_xattr_writer_begin(xwr);
+	if (ret) {
+		sqfs_perror(filename, "recording xattr key-value pairs", ret);
+		return -1;
+	}
+
+	if (selinux_relable_node(selinux_handle, xwr, n, path)) {
+		free(path);
+		return -1;
+	}
+
+	ret = sqfs_xattr_writer_end(xwr, &n->xattr_idx);
+	if (ret) {
+		sqfs_perror(filename, "flushing completed key-value pairs",
+			    ret);
+		return -1;
+	}
+
+	free(path);
+
+	if (S_ISDIR(n->mode)) {
+		for (n = n->data.dir.children; n != NULL; n = n->next) {
+			if (relabel_tree_dfs(filename, xwr, n, selinux_handle))
+				return -1;
+		}
+	}
+
+	return 0;
+}
+
+static int read_fstree(fstree_t *fs, options_t *opt, sqfs_xattr_writer_t *xwr,
+		       void *selinux_handle)
+{
+	FILE *fp;
+	int ret;
+
+	if (opt->infile == NULL) {
+		return fstree_from_dir(fs, opt->packdir, selinux_handle,
+				       xwr, opt->dirscan_flags);
+	}
+
+	fp = fopen(opt->infile, "rb");
+	if (fp == NULL) {
+		perror(opt->infile);
+		return -1;
+	}
+
+	ret = fstree_from_file(fs, opt->infile, fp);
+	fclose(fp);
+
+	if (ret == 0 && selinux_handle != NULL)
+		ret = relabel_tree_dfs(opt->cfg.filename, xwr,
+				       fs->root, selinux_handle);
+
+	return ret;
+}
+
+int main(int argc, char **argv)
+{
+	int status = EXIT_FAILURE;
+	void *sehnd = NULL;
+	sqfs_writer_t sqfs;
+	options_t opt;
+
+	process_command_line(&opt, argc, argv);
+
+	if (sqfs_writer_init(&sqfs, &opt.cfg))
+		return EXIT_FAILURE;
+
+	if (opt.selinux != NULL) {
+		sehnd = selinux_open_context_file(opt.selinux);
+		if (sehnd == NULL)
+			goto out;
+	}
+
+	if (read_fstree(&sqfs.fs, &opt, sqfs.xwr, sehnd)) {
+		if (sehnd != NULL)
+			selinux_close_context_file(sehnd);
+		goto out;
+	}
+
+	if (sehnd != NULL) {
+		selinux_close_context_file(sehnd);
+		sehnd = NULL;
+	}
+
+	if (fstree_post_process(&sqfs.fs))
+		goto out;
+
+	if (pack_files(sqfs.data, &sqfs.fs, &opt))
+		goto out;
+
+	if (sqfs_writer_finish(&sqfs, &opt.cfg))
+		goto out;
+
+	status = EXIT_SUCCESS;
+out:
+	sqfs_writer_cleanup(&sqfs, status);
+	return status;
+}
diff --git a/bin/gensquashfs/mkfs.h b/bin/gensquashfs/mkfs.h
new file mode 100644
index 0000000..1b767aa
--- /dev/null
+++ b/bin/gensquashfs/mkfs.h
@@ -0,0 +1,72 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * mkfs.h
+ *
+ * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
+ */
+#ifndef MKFS_H
+#define MKFS_H
+
+#include "config.h"
+
+#include "common.h"
+#include "fstree.h"
+
+#ifdef HAVE_SYS_XATTR_H
+#include <sys/xattr.h>
+
+#if defined(__APPLE__) && defined(__MACH__)
+#define llistxattr(path, list, size) \
+	listxattr(path, list, size, XATTR_NOFOLLOW)
+
+#define lgetxattr(path, name, value, size) \
+	getxattr(path, name, value, size, 0, XATTR_NOFOLLOW)
+#endif
+#endif
+
+#ifdef WITH_SELINUX
+#include <selinux/selinux.h>
+#include <selinux/label.h>
+#endif
+
+#include <getopt.h>
+#include <assert.h>
+#include <stdlib.h>
+#include <string.h>
+#include <limits.h>
+#include <dirent.h>
+#include <stdio.h>
+#include <errno.h>
+#include <ctype.h>
+
+typedef struct {
+	sqfs_writer_cfg_t cfg;
+	unsigned int dirscan_flags;
+	const char *infile;
+	const char *packdir;
+	const char *selinux;
+	bool no_tail_packing;
+} options_t;
+
+enum {
+	DIR_SCAN_KEEP_TIME = 0x01,
+
+	DIR_SCAN_ONE_FILESYSTEM = 0x02,
+
+	DIR_SCAN_READ_XATTR = 0x04,
+};
+
+void process_command_line(options_t *opt, int argc, char **argv);
+
+int fstree_from_dir(fstree_t *fs, const char *path, void *selinux_handle,
+		    sqfs_xattr_writer_t *xwr, unsigned int flags);
+
+
+void *selinux_open_context_file(const char *filename);
+
+int selinux_relable_node(void *sehnd, sqfs_xattr_writer_t *xwr,
+			 tree_node_t *node, const char *path);
+
+void selinux_close_context_file(void *sehnd);
+
+#endif /* MKFS_H */
diff --git a/bin/gensquashfs/options.c b/bin/gensquashfs/options.c
new file mode 100644
index 0000000..2369787
--- /dev/null
+++ b/bin/gensquashfs/options.c
@@ -0,0 +1,289 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * options.c
+ *
+ * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
+ */
+#include "mkfs.h"
+
+static struct option long_opts[] = {
+	{ "compressor", required_argument, NULL, 'c' },
+	{ "block-size", required_argument, NULL, 'b' },
+	{ "dev-block-size", required_argument, NULL, 'B' },
+	{ "defaults", required_argument, NULL, 'd' },
+	{ "comp-extra", required_argument, NULL, 'X' },
+	{ "pack-file", required_argument, NULL, 'F' },
+	{ "pack-dir", required_argument, NULL, 'D' },
+	{ "num-jobs", required_argument, NULL, 'j' },
+	{ "queue-backlog", required_argument, NULL, 'Q' },
+	{ "keep-time", no_argument, NULL, 'k' },
+#ifdef HAVE_SYS_XATTR_H
+	{ "keep-xattr", no_argument, NULL, 'x' },
+#endif
+	{ "one-file-system", no_argument, NULL, 'o' },
+	{ "exportable", no_argument, NULL, 'e' },
+	{ "no-tail-packing", no_argument, NULL, 'T' },
+	{ "force", no_argument, NULL, 'f' },
+	{ "quiet", no_argument, NULL, 'q' },
+#ifdef WITH_SELINUX
+	{ "selinux", required_argument, NULL, 's' },
+#endif
+	{ "version", no_argument, NULL, 'V' },
+	{ "help", no_argument, NULL, 'h' },
+	{ NULL, 0, NULL, 0 },
+};
+
+static const char *short_opts = "F:D:X:c:b:B:d:j:Q:kxoefqThV"
+#ifdef WITH_SELINUX
+"s:"
+#endif
+#ifdef HAVE_SYS_XATTR_H
+"x"
+#endif
+;
+
+static const char *help_string =
+"Usage: gensquashfs [OPTIONS...] <squashfs-file>\n"
+"\n"
+"Possible options:\n"
+"\n"
+"  --pack-file, -F <file>      Use a `gen_init_cpio` style description file.\n"
+"                              The file format is specified below.\n"
+"                              If --pack-dir is used, input file paths are\n"
+"                              relative to the pack directory, otherwise\n"
+"                              they are relative to the directory the pack\n"
+"                              file is in.\n"
+"  --pack-dir, -D <directory>  If --pack-file is used, this is the root path\n"
+"                              relative to which to read files. If no pack\n"
+"                              file is specified, pack the contents of the\n"
+"                              given directory into a SquashFS image. The\n"
+"                              directory becomes the root of the file\n"
+"                              system.\n"
+"\n"
+"  --compressor, -c <name>     Select the compressor to use.\n"
+"                              A list of available compressors is below.\n"
+"  --comp-extra, -X <options>  A comma separated list of extra options for\n"
+"                              the selected compressor. Specify 'help' to\n"
+"                              get a list of available options.\n"
+"  --num-jobs, -j <count>      Number of compressor jobs to create.\n"
+"  --queue-backlog, -Q <count> Maximum number of data blocks in the thread\n"
+"                              worker queue before the packer starts waiting\n"
+"                              for the block processors to catch up.\n"
+"                              Defaults to 10 times the number of jobs.\n"
+"  --block-size, -b <size>     Block size to use for Squashfs image.\n"
+"                              Defaults to %u.\n"
+"  --dev-block-size, -B <size> Device block size to padd the image to.\n"
+"                              Defaults to %u.\n"
+"  --defaults, -d <options>    A comma separated list of default values for\n"
+"                              implicitly created directories.\n"
+"\n"
+"                              Possible options:\n"
+"                                 uid=<value>    0 if not set.\n"
+"                                 gid=<value>    0 if not set.\n"
+"                                 mode=<value>   0755 if not set.\n"
+"                                 mtime=<value>  0 if not set.\n"
+"\n"
+#ifdef WITH_SELINUX
+"  --selinux, -s <file>        Specify an SELinux label file to get context\n"
+"                              attributes from.\n"
+#endif
+"  --keep-time, -k             When using --pack-dir only, use the timestamps\n"
+"                              from the input files instead of setting\n"
+"                              defaults on all input paths.\n"
+"  --keep-xattr, -x            When using --pack-dir only, read and pack the\n"
+"                              extended attributes from the input files.\n"
+"  --one-file-system, -o       When using --pack-dir only, stay in local file\n"
+"                              system and do not cross mount points.\n"
+"  --exportable, -e            Generate an export table for NFS support.\n"
+"  --no-tail-packing, -T       Do not perform tail end packing on files that\n"
+"                              are larger than block size.\n"
+"  --force, -f                 Overwrite the output file if it exists.\n"
+"  --quiet, -q                 Do not print out progress reports.\n"
+"  --help, -h                  Print help text and exit.\n"
+"  --version, -V               Print version information and exit.\n"
+"\n";
+
+const char *help_details =
+"When using the pack file option, the given file is expected to contain\n"
+"newline separated entries that describe the files to be included in the\n"
+"SquashFS image. The following entry types can be specified:\n"
+"\n"
+"# a comment\n"
+"file <path> <mode> <uid> <gid> [<location>]\n"
+"dir <path> <mode> <uid> <gid>\n"
+"nod <path> <mode> <uid> <gid> <dev_type> <maj> <min>\n"
+"slink <path> <mode> <uid> <gid> <target>\n"
+"link <path> <dummy> <dummy> <dummy> <target>\n"
+"pipe <path> <mode> <uid> <gid>\n"
+"sock <path> <mode> <uid> <gid>\n"
+"\n"
+"<path>       Absolute path of the entry in the image. Can be put in quotes\n"
+"             if some components contain spaces.\n"
+"<location>   If given, location of the input file. Either absolute or relative\n"
+"             to the description file. If omitted, the image path is used,\n"
+"             relative to the description file.\n"
+"<target>     Symlink or hardlink target.\n"
+"<mode>       Mode/permissions of the entry.\n"
+"<uid>        Numeric user id.\n"
+"<gid>        Numeric group id.\n"
+"<dev_type>   Device type (b=block, c=character).\n"
+"<maj>        Major number of a device special file.\n"
+"<min>        Minor number of a device special file.\n"
+"\n"
+"Example:\n"
+"    # A simple squashfs image\n"
+"    dir /dev 0755 0 0\n"
+"    nod /dev/console 0600 0 0 c 5 1\n"
+"    dir /root 0700 0 0\n"
+"    dir /sbin 0755 0 0\n"
+"    \n"
+"    # Add a file. Input is relative to listing or pack dir.\n"
+"    file /sbin/init 0755 0 0 ../init/sbin/init\n"
+"    \n"
+"    # Read bin/bash, relative to listing or pack dir.\n"
+"    # Implicitly create /bin.\n"
+"    file /bin/bash 0755 0 0\n"
+"    \n"
+"    # file name with a space in it.\n"
+"    file \"/opt/my app/\\\"special\\\"/data\" 0600 0 0\n"
+"\n\n";
+
+void process_command_line(options_t *opt, int argc, char **argv)
+{
+	bool have_compressor;
+	int i, ret;
+
+	memset(opt, 0, sizeof(*opt));
+	sqfs_writer_cfg_init(&opt->cfg);
+
+	for (;;) {
+		i = getopt_long(argc, argv, short_opts, long_opts, NULL);
+		if (i == -1)
+			break;
+
+		switch (i) {
+		case 'T':
+			opt->no_tail_packing = true;
+			break;
+		case 'c':
+			have_compressor = true;
+			ret = sqfs_compressor_id_from_name(optarg);
+
+			if (ret < 0) {
+				have_compressor = false;
+#ifdef WITH_LZO
+				if (opt->cfg.comp_id == SQFS_COMP_LZO)
+					have_compressor = true;
+#endif
+			}
+
+			if (!have_compressor) {
+				fprintf(stderr, "Unsupported compressor '%s'\n",
+					optarg);
+				exit(EXIT_FAILURE);
+			}
+
+			opt->cfg.comp_id = ret;
+			break;
+		case 'b':
+			if (parse_size("Block size", &opt->cfg.block_size,
+				       optarg, 0)) {
+				exit(EXIT_FAILURE);
+			}
+			break;
+		case 'j':
+			opt->cfg.num_jobs = strtol(optarg, NULL, 0);
+			break;
+		case 'Q':
+			opt->cfg.max_backlog = strtol(optarg, NULL, 0);
+			break;
+		case 'B':
+			if (parse_size("Device block size",
+				       &opt->cfg.devblksize, optarg, 0)) {
+				exit(EXIT_FAILURE);
+			}
+			if (opt->cfg.devblksize < 1024) {
+				fputs("Device block size must be at "
+				      "least 1024\n", stderr);
+				exit(EXIT_FAILURE);
+			}
+			break;
+		case 'd':
+			opt->cfg.fs_defaults = optarg;
+			break;
+		case 'k':
+			opt->dirscan_flags |= DIR_SCAN_KEEP_TIME;
+			break;
+#ifdef HAVE_SYS_XATTR_H
+		case 'x':
+			opt->dirscan_flags |= DIR_SCAN_READ_XATTR;
+			break;
+#endif
+		case 'o':
+			opt->dirscan_flags |= DIR_SCAN_ONE_FILESYSTEM;
+			break;
+		case 'e':
+			opt->cfg.exportable = true;
+			break;
+		case 'f':
+			opt->cfg.outmode |= SQFS_FILE_OPEN_OVERWRITE;
+			break;
+		case 'q':
+			opt->cfg.quiet = true;
+			break;
+		case 'X':
+			opt->cfg.comp_extra = optarg;
+			break;
+		case 'F':
+			opt->infile = optarg;
+			break;
+		case 'D':
+			opt->packdir = optarg;
+			break;
+#ifdef WITH_SELINUX
+		case 's':
+			opt->selinux = optarg;
+			break;
+#endif
+		case 'h':
+			printf(help_string,
+			       SQFS_DEFAULT_BLOCK_SIZE, SQFS_DEVBLK_SIZE);
+			fputs(help_details, stdout);
+			compressor_print_available();
+			exit(EXIT_SUCCESS);
+		case 'V':
+			print_version("gensquashfs");
+			exit(EXIT_SUCCESS);
+		default:
+			goto fail_arg;
+		}
+	}
+
+	if (opt->cfg.num_jobs < 1)
+		opt->cfg.num_jobs = 1;
+
+	if (opt->cfg.max_backlog < 1)
+		opt->cfg.max_backlog = 10 * opt->cfg.num_jobs;
+
+	if (opt->cfg.comp_extra != NULL &&
+	    strcmp(opt->cfg.comp_extra, "help") == 0) {
+		compressor_print_help(opt->cfg.comp_id);
+		exit(EXIT_SUCCESS);
+	}
+
+	if (opt->infile == NULL && opt->packdir == NULL) {
+		fputs("No input file or directory specified.\n", stderr);
+		goto fail_arg;
+	}
+
+	if (optind >= argc) {
+		fputs("No output file specified.\n", stderr);
+		goto fail_arg;
+	}
+
+	opt->cfg.filename = argv[optind++];
+	return;
+fail_arg:
+	fputs("Try `gensquashfs --help' for more information.\n", stderr);
+	exit(EXIT_FAILURE);
+}
diff --git a/bin/gensquashfs/selinux.c b/bin/gensquashfs/selinux.c
new file mode 100644
index 0000000..678723b
--- /dev/null
+++ b/bin/gensquashfs/selinux.c
@@ -0,0 +1,78 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * selinux.c
+ *
+ * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
+ */
+#include "mkfs.h"
+
+#define XATTR_NAME_SELINUX "security.selinux"
+#define XATTR_VALUE_SELINUX "system_u:object_r:unlabeled_t:s0"
+
+#ifdef WITH_SELINUX
+int selinux_relable_node(void *sehnd, sqfs_xattr_writer_t *xwr,
+			 tree_node_t *node, const char *path)
+{
+	char *context = NULL;
+	int ret;
+
+	if (selabel_lookup(sehnd, &context, path, node->mode) < 0) {
+		context = strdup(XATTR_VALUE_SELINUX);
+		if (context == NULL)
+			goto fail;
+	}
+
+	ret = sqfs_xattr_writer_add(xwr, XATTR_NAME_SELINUX,
+				    context, strlen(context));
+	free(context);
+
+	if (ret)
+		sqfs_perror(node->name, "storing SELinux xattr", ret);
+
+	return ret;
+fail:
+	perror("relabeling files");
+	return -1;
+}
+
+void *selinux_open_context_file(const char *filename)
+{
+	struct selabel_handle *sehnd;
+	struct selinux_opt seopts[] = {
+		{ SELABEL_OPT_PATH, filename },
+	};
+
+	sehnd = selabel_open(SELABEL_CTX_FILE, seopts, 1);
+	if (sehnd == NULL)
+		perror(filename);
+
+	return sehnd;
+}
+
+void selinux_close_context_file(void *sehnd)
+{
+	selabel_close(sehnd);
+}
+#else
+int selinux_relable_node(void *sehnd, sqfs_xattr_writer_t *xwr,
+			 tree_node_t *node, const char *path)
+{
+	(void)sehnd; (void)xwr; (void)node; (void)path;
+	fputs("Built without SELinux support, cannot add SELinux labels\n",
+	      stderr);
+	return -1;
+}
+
+void *selinux_open_context_file(const char *filename)
+{
+	(void)filename;
+	fputs("Built without SELinux support, cannot open contexts file\n",
+	      stderr);
+	return NULL;
+}
+
+void selinux_close_context_file(void *sehnd)
+{
+	(void)sehnd;
+}
+#endif
diff --git a/bin/rdsquashfs/describe.c b/bin/rdsquashfs/describe.c
new file mode 100644
index 0000000..d30f844
--- /dev/null
+++ b/bin/rdsquashfs/describe.c
@@ -0,0 +1,125 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * describe.c
+ *
+ * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
+ */
+#include "rdsquashfs.h"
+
+static int print_name(const sqfs_tree_node_t *n)
+{
+	char *start, *ptr, *name = sqfs_tree_node_get_path(n);
+	int ret;
+
+	if (name == NULL) {
+		perror("Recovering file path of tree node");
+		return -1;
+	}
+
+	ret = canonicalize_name(name);
+	assert(ret == 0);
+
+	if (strchr(name, ' ') == NULL && strchr(name, '"') == NULL) {
+		fputs(name, stdout);
+	} else {
+		fputc('"', stdout);
+
+		ptr = strchr(name, '"');
+
+		if (ptr != NULL) {
+			start = name;
+
+			do {
+				fwrite(start, 1, ptr - start, stdout);
+				fputs("\\\"", stdout);
+				start = ptr + 1;
+				ptr = strchr(start, '"');
+			} while (ptr != NULL);
+
+			fputs(start, stdout);
+		} else {
+			fputs(name, stdout);
+		}
+
+		fputc('"', stdout);
+	}
+
+	free(name);
+	return 0;
+}
+
+static void print_perm(const sqfs_tree_node_t *n)
+{
+	printf(" 0%o %d %d", n->inode->base.mode & (~S_IFMT), n->uid, n->gid);
+}
+
+static int print_simple(const char *type, const sqfs_tree_node_t *n,
+			const char *extra)
+{
+	printf("%s ", type);
+	if (print_name(n))
+		return -1;
+	print_perm(n);
+	if (extra != NULL)
+		printf(" %s", extra);
+	fputc('\n', stdout);
+	return 0;
+}
+
+int describe_tree(const sqfs_tree_node_t *root, const char *unpack_root)
+{
+	const sqfs_tree_node_t *n;
+
+	switch (root->inode->base.mode & S_IFMT) {
+	case S_IFSOCK:
+		return print_simple("sock", root, NULL);
+	case S_IFLNK:
+		return print_simple("slink", root,
+				    (const char *)root->inode->extra);
+	case S_IFIFO:
+		return print_simple("pipe", root, NULL);
+	case S_IFREG:
+		if (unpack_root == NULL)
+			return print_simple("file", root, NULL);
+
+		fputs("file ", stdout);
+		if (print_name(root))
+			return -1;
+		print_perm(root);
+		printf(" %s/", unpack_root);
+		if (print_name(root))
+			return -1;
+		fputc('\n', stdout);
+		break;
+	case S_IFCHR:
+	case S_IFBLK: {
+		char buffer[32];
+		sqfs_u32 devno;
+
+		if (root->inode->base.type == SQFS_INODE_EXT_BDEV ||
+		    root->inode->base.type == SQFS_INODE_EXT_CDEV) {
+			devno = root->inode->data.dev_ext.devno;
+		} else {
+			devno = root->inode->data.dev.devno;
+		}
+
+		sprintf(buffer, "%c %d %d",
+			S_ISCHR(root->inode->base.mode) ? 'c' : 'b',
+			major(devno), minor(devno));
+		return print_simple("nod", root, buffer);
+	}
+	case S_IFDIR:
+		if (root->name[0] != '\0') {
+			if (print_simple("dir", root, NULL))
+				return -1;
+		}
+
+		for (n = root->children; n != NULL; n = n->next) {
+			if (describe_tree(n, unpack_root))
+				return -1;
+		}
+		break;
+	}
+
+	return 0;
+}
diff --git a/bin/rdsquashfs/dump_xattrs.c b/bin/rdsquashfs/dump_xattrs.c
new file mode 100644
index 0000000..93b0b01
--- /dev/null
+++ b/bin/rdsquashfs/dump_xattrs.c
@@ -0,0 +1,120 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * dump_xattrs.c
+ *
+ * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
+ */
+#include "rdsquashfs.h"
+
+static void print_hex(const sqfs_u8 *value, size_t len)
+{
+	printf("0x");
+
+	while (len--)
+		printf("%02X", *(value++));
+}
+
+static bool is_printable(const sqfs_u8 *value, size_t len)
+{
+	size_t utf8_cont = 0;
+	sqfs_u8 x;
+
+	while (len--) {
+		x = *(value++);
+
+		if (utf8_cont > 0) {
+			if ((x & 0xC0) != 0x80)
+				return false;
+
+			--utf8_cont;
+		} else {
+			if (x < 0x80) {
+				if (x < 0x20) {
+					if (x >= 0x07 && x <= 0x0D)
+						continue;
+					if (x == 0x00)
+						continue;
+					return false;
+				}
+
+				if (x == 0x7F)
+					return false;
+			}
+
+			if ((x & 0xE0) == 0xC0) {
+				utf8_cont = 1;
+			} else if ((x & 0xF0) == 0xE0) {
+				utf8_cont = 2;
+			} else if ((x & 0xF8) == 0xF0) {
+				utf8_cont = 3;
+			} else if ((x & 0xFC) == 0xF8) {
+				utf8_cont = 4;
+			} else if ((x & 0xFE) == 0xFC) {
+				utf8_cont = 5;
+			}
+
+			if (utf8_cont > 0 && len < utf8_cont)
+				return false;
+		}
+	}
+
+	return true;
+}
+
+int dump_xattrs(sqfs_xattr_reader_t *xattr, const sqfs_inode_generic_t *inode)
+{
+	sqfs_xattr_value_t *value;
+	sqfs_xattr_entry_t *key;
+	sqfs_xattr_id_t desc;
+	sqfs_u32 index;
+	size_t i;
+
+	if (xattr == NULL)
+		return 0;
+
+	sqfs_inode_get_xattr_index(inode, &index);
+
+	if (index == 0xFFFFFFFF)
+		return 0;
+
+	if (sqfs_xattr_reader_get_desc(xattr, index, &desc)) {
+		fputs("Error resolving xattr index\n", stderr);
+		return -1;
+	}
+
+	if (sqfs_xattr_reader_seek_kv(xattr, &desc)) {
+		fputs("Error locating xattr key-value pairs\n", stderr);
+		return -1;
+	}
+
+	for (i = 0; i < desc.count; ++i) {
+		if (sqfs_xattr_reader_read_key(xattr, &key)) {
+			fputs("Error reading xattr key\n", stderr);
+			return -1;
+		}
+
+		if (sqfs_xattr_reader_read_value(xattr, key, &value)) {
+			fputs("Error reading xattr value\n", stderr);
+			free(key);
+			return -1;
+		}
+
+		if (is_printable(key->key, key->size)) {
+			printf("%s=", key->key);
+		} else {
+			print_hex(key->key, key->size);
+		}
+
+		if (is_printable(value->value, value->size)) {
+			printf("%s\n", value->value);
+		} else {
+			print_hex(value->value, value->size);
+			printf("\n");
+		}
+
+		free(key);
+		free(value);
+	}
+
+	return 0;
+}
diff --git a/bin/rdsquashfs/fill_files.c b/bin/rdsquashfs/fill_files.c
new file mode 100644
index 0000000..b75afbf
--- /dev/null
+++ b/bin/rdsquashfs/fill_files.c
@@ -0,0 +1,183 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * fill_files.c
+ *
+ * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
+ */
+#include "config.h"
+#include "rdsquashfs.h"
+
+static struct file_ent {
+	char *path;
+	const sqfs_inode_generic_t *inode;
+} *files = NULL;
+
+static size_t num_files = 0, max_files = 0;
+static size_t block_size = 0;
+
+static int compare_files(const void *l, const void *r)
+{
+	sqfs_u32 lhs_frag_idx, lhs_frag_off, rhs_frag_idx, rhs_frag_off;
+	sqfs_u64 lhs_size, rhs_size, lhs_start, rhs_start;
+	const struct file_ent *lhs = l, *rhs = r;
+
+	sqfs_inode_get_frag_location(lhs->inode, &lhs_frag_idx, &lhs_frag_off);
+	sqfs_inode_get_file_block_start(lhs->inode, &lhs_start);
+	sqfs_inode_get_file_size(lhs->inode, &lhs_size);
+
+	sqfs_inode_get_frag_location(rhs->inode, &rhs_frag_idx, &rhs_frag_off);
+	sqfs_inode_get_file_block_start(rhs->inode, &rhs_start);
+	sqfs_inode_get_file_size(rhs->inode, &rhs_size);
+
+	/* Files with fragments come first, ordered by ID.
+	   In case of tie, files without data blocks come first,
+	   and the others are ordered by start block. */
+	if ((lhs_size % block_size) && (lhs_frag_off < block_size) &&
+	    (lhs_frag_idx != 0xFFFFFFFF)) {
+		if ((rhs_size % block_size) && (rhs_frag_off < block_size) &&
+		    (rhs_frag_idx != 0xFFFFFFFF))
+			return -1;
+
+		if (lhs_frag_idx < rhs_frag_idx)
+			return -1;
+
+		if (lhs_frag_idx > rhs_frag_idx)
+			return 1;
+
+		if (lhs_size < block_size)
+			return (rhs_size < block_size) ? 0 : -1;
+
+		if (rhs_size < block_size)
+			return 1;
+
+		goto order_by_start;
+	}
+
+	if ((rhs_size % block_size) && (rhs_frag_off < block_size) &&
+	    (rhs_frag_idx != 0xFFFFFFFF))
+		return 1;
+
+	/* order the rest by start block */
+order_by_start:
+	return lhs_start < rhs_start ? -1 : lhs_start > rhs_start ? 1 : 0;
+}
+
+static int add_file(const sqfs_tree_node_t *node)
+{
+	size_t new_sz;
+	char *path;
+	void *new;
+
+	if (num_files == max_files) {
+		new_sz = max_files ? max_files * 2 : 256;
+		new = realloc(files, sizeof(files[0]) * new_sz);
+
+		if (new == NULL) {
+			perror("expanding file list");
+			return -1;
+		}
+
+		files = new;
+		max_files = new_sz;
+	}
+
+	path = sqfs_tree_node_get_path(node);
+	if (path == NULL) {
+		perror("assembling file path");
+		return -1;
+	}
+
+	if (canonicalize_name(path)) {
+		fprintf(stderr, "Invalid file path '%s'\n", path);
+		free(path);
+		return -1;
+	}
+
+	files[num_files].path = path;
+	files[num_files].inode = node->inode;
+	num_files++;
+	return 0;
+}
+
+static void clear_file_list(void)
+{
+	size_t i;
+
+	for (i = 0; i < num_files; ++i)
+		free(files[i].path);
+
+	free(files);
+	files = NULL;
+	num_files = 0;
+	max_files = 0;
+}
+
+static int gen_file_list_dfs(const sqfs_tree_node_t *n)
+{
+	if (!is_filename_sane((const char *)n->name, true)) {
+		fprintf(stderr, "Found an entry named '%s', skipping.\n",
+			n->name);
+		return 0;
+	}
+
+	if (S_ISREG(n->inode->base.mode))
+		return add_file(n);
+
+	if (S_ISDIR(n->inode->base.mode)) {
+		for (n = n->children; n != NULL; n = n->next) {
+			if (gen_file_list_dfs(n))
+				return -1;
+		}
+	}
+
+	return 0;
+}
+
+static int fill_files(sqfs_data_reader_t *data, int flags)
+{
+	size_t i;
+	FILE *fp;
+
+	for (i = 0; i < num_files; ++i) {
+		fp = fopen(files[i].path, "wb");
+		if (fp == NULL) {
+			fprintf(stderr, "unpacking %s: %s\n",
+				files[i].path, strerror(errno));
+			return -1;
+		}
+
+		if (!(flags & UNPACK_QUIET))
+			printf("unpacking %s\n", files[i].path);
+
+		if (sqfs_data_reader_dump(files[i].path, data, files[i].inode,
+					  fp, block_size,
+					  (flags & UNPACK_NO_SPARSE) == 0)) {
+			fclose(fp);
+			return -1;
+		}
+
+		fflush(fp);
+		fclose(fp);
+	}
+
+	return 0;
+}
+
+int fill_unpacked_files(size_t blk_sz, const sqfs_tree_node_t *root,
+			sqfs_data_reader_t *data, int flags)
+{
+	int status;
+
+	block_size = blk_sz;
+
+	if (gen_file_list_dfs(root)) {
+		clear_file_list();
+		return -1;
+	}
+
+	qsort(files, num_files, sizeof(files[0]), compare_files);
+
+	status = fill_files(data, flags);
+	clear_file_list();
+	return status;
+}
diff --git a/bin/rdsquashfs/list_files.c b/bin/rdsquashfs/list_files.c
new file mode 100644
index 0000000..238ffec
--- /dev/null
+++ b/bin/rdsquashfs/list_files.c
@@ -0,0 +1,156 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * list_files.c
+ *
+ * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
+ */
+#include "rdsquashfs.h"
+
+static void mode_to_str(sqfs_u16 mode, char *p)
+{
+	switch (mode & S_IFMT) {
+	case S_IFDIR:  *(p++) = 'd'; break;
+	case S_IFCHR:  *(p++) = 'c'; break;
+	case S_IFBLK:  *(p++) = 'b'; break;
+	case S_IFREG:  *(p++) = '-'; break;
+	case S_IFLNK:  *(p++) = 'l'; break;
+	case S_IFSOCK: *(p++) = 's'; break;
+	case S_IFIFO:  *(p++) = 'p'; break;
+	default:       *(p++) = '?'; break;
+	}
+
+	*(p++) = (mode & S_IRUSR) ? 'r' : '-';
+	*(p++) = (mode & S_IWUSR) ? 'w' : '-';
+
+	switch (mode & (S_IXUSR | S_ISUID)) {
+	case S_IXUSR | S_ISUID: *(p++) = 's'; break;
+	case S_IXUSR:           *(p++) = 'x'; break;
+	case S_ISUID:           *(p++) = 'S'; break;
+	default:                *(p++) = '-'; break;
+	}
+
+	*(p++) = (mode & S_IRGRP) ? 'r' : '-';
+	*(p++) = (mode & S_IWGRP) ? 'w' : '-';
+
+	switch (mode & (S_IXGRP | S_ISGID)) {
+	case S_IXGRP | S_ISGID: *(p++) = 's'; break;
+	case S_IXGRP:           *(p++) = 'x'; break;
+	case S_ISGID:           *(p++) = 'S'; break;
+	case 0:                 *(p++) = '-'; break;
+	}
+
+	*(p++) = (mode & S_IROTH) ? 'r' : '-';
+	*(p++) = (mode & S_IWOTH) ? 'w' : '-';
+
+	switch (mode & (S_IXOTH | S_ISVTX)) {
+	case S_IXOTH | S_ISVTX: *(p++) = 't'; break;
+	case S_IXOTH:           *(p++) = 'x'; break;
+	case S_ISVTX:           *(p++) = 'T'; break;
+	case 0:                 *(p++) = '-'; break;
+	}
+
+	*p = '\0';
+}
+
+static int count_int_chars(unsigned int i)
+{
+	int count = 1;
+
+	while (i > 10) {
+		++count;
+		i /= 10;
+	}
+
+	return count;
+}
+
+static void print_node_size(const sqfs_tree_node_t *n, char *buffer)
+{
+	switch (n->inode->base.mode & S_IFMT) {
+	case S_IFLNK:
+		print_size(strlen((const char *)n->inode->extra), buffer, true);
+		break;
+	case S_IFREG: {
+		sqfs_u64 size;
+		sqfs_inode_get_file_size(n->inode, &size);
+		print_size(size, buffer, true);
+		break;
+	}
+	case S_IFDIR:
+		if (n->inode->base.type == SQFS_INODE_EXT_DIR) {
+			print_size(n->inode->data.dir_ext.size, buffer, true);
+		} else {
+			print_size(n->inode->data.dir.size, buffer, true);
+		}
+		break;
+	case S_IFBLK:
+	case S_IFCHR: {
+		sqfs_u32 devno;
+
+		if (n->inode->base.type == SQFS_INODE_EXT_BDEV ||
+		    n->inode->base.type == SQFS_INODE_EXT_CDEV) {
+			devno = n->inode->data.dev_ext.devno;
+		} else {
+			devno = n->inode->data.dev.devno;
+		}
+
+		sprintf(buffer, "%u:%u", major(devno), minor(devno));
+		break;
+	}
+	default:
+		buffer[0] = '0';
+		buffer[1] = '\0';
+		break;
+	}
+}
+
+void list_files(const sqfs_tree_node_t *node)
+{
+	int i, max_uid_chars = 0, max_gid_chars = 0, max_sz_chars = 0;
+	char modestr[12], sizestr[32];
+	const sqfs_tree_node_t *n;
+
+	if (S_ISDIR(node->inode->base.mode)) {
+		for (n = node->children; n != NULL; n = n->next) {
+			i = count_int_chars(n->uid);
+			max_uid_chars = i > max_uid_chars ? i : max_uid_chars;
+
+			i = count_int_chars(n->gid);
+			max_gid_chars = i > max_gid_chars ? i : max_gid_chars;
+
+			print_node_size(n, sizestr);
+			i = strlen(sizestr);
+			max_sz_chars = i > max_sz_chars ? i : max_sz_chars;
+		}
+
+		for (n = node->children; n != NULL; n = n->next) {
+			mode_to_str(n->inode->base.mode, modestr);
+			print_node_size(n, sizestr);
+
+			printf("%s %*u/%-*u %*s %s", modestr,
+			       max_uid_chars, n->uid,
+			       max_gid_chars, n->gid,
+			       max_sz_chars, sizestr,
+			       n->name);
+
+			if (S_ISLNK(n->inode->base.mode)) {
+				printf(" -> %s\n",
+				       (const char *)n->inode->extra);
+			} else {
+				fputc('\n', stdout);
+			}
+		}
+	} else {
+		mode_to_str(node->inode->base.mode, modestr);
+		print_node_size(node, sizestr);
+
+		printf("%s %u/%u %s %s", modestr,
+		       node->uid, node->gid, sizestr, node->name);
+
+		if (S_ISLNK(node->inode->base.mode)) {
+			printf(" -> %s\n", (const char *)node->inode->extra);
+		} else {
+			fputc('\n', stdout);
+		}
+	}
+}
diff --git a/bin/rdsquashfs/options.c b/bin/rdsquashfs/options.c
new file mode 100644
index 0000000..cdd19e1
--- /dev/null
+++ b/bin/rdsquashfs/options.c
@@ -0,0 +1,217 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * options.c
+ *
+ * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
+ */
+#include "rdsquashfs.h"
+
+static struct option long_opts[] = {
+	{ "list", required_argument, NULL, 'l' },
+	{ "cat", required_argument, NULL, 'c' },
+	{ "xattr", required_argument, NULL, 'x' },
+	{ "unpack-root", required_argument, NULL, 'p' },
+	{ "unpack-path", required_argument, NULL, 'u' },
+	{ "no-dev", no_argument, NULL, 'D' },
+	{ "no-sock", no_argument, NULL, 'S' },
+	{ "no-fifo", no_argument, NULL, 'F' },
+	{ "no-slink", no_argument, NULL, 'L' },
+	{ "no-empty-dir", no_argument, NULL, 'E' },
+	{ "no-sparse", no_argument, NULL, 'Z' },
+#ifdef HAVE_SYS_XATTR_H
+	{ "set-xattr", no_argument, NULL, 'X' },
+#endif
+	{ "set-times", no_argument, NULL, 'T' },
+	{ "describe", no_argument, NULL, 'd' },
+	{ "chmod", no_argument, NULL, 'C' },
+	{ "chown", no_argument, NULL, 'O' },
+	{ "quiet", no_argument, NULL, 'q' },
+	{ "help", no_argument, NULL, 'h' },
+	{ "version", no_argument, NULL, 'V' },
+	{ NULL, 0, NULL, 0 },
+};
+
+static const char *short_opts =
+	"l:c:u:p:x:DSFLCOEZTj:dqhV"
+#ifdef HAVE_SYS_XATTR_H
+	"X"
+#endif
+	;
+
+static const char *help_string =
+"Usage: rdsquashfs [OPTIONS] <squashfs-file>\n"
+"\n"
+"View or extract the contents of a squashfs image.\n"
+"\n"
+"Possible options:\n"
+"\n"
+"  --list, -l <path>         Produce a directory listing for a given path in\n"
+"                            the squashfs image.\n"
+"  --cat, -c <path>          If the specified path is a regular file in the,\n"
+"                            image, dump its contents to stdout.\n"
+"  --xattr, -x <path>        Enumerate extended attributes associated with\n"
+"                            an inode that the given path resolves to.\n"
+"  --unpack-path, -u <path>  Unpack this sub directory from the image. To\n"
+"                            unpack everything, simply specify /.\n"
+"  --describe, -d            Produce a file listing from the image.\n"
+"\n"
+"  --unpack-root, -p <path>  If used with --unpack-path, this is where the\n"
+"                            data unpacked to. If used with --describe, this\n"
+"                            is used as a prefix for the input path of\n"
+"                            regular files.\n"
+"\n"
+"  --no-dev, -D              Do not unpack device special files.\n"
+"  --no-sock, -S             Do not unpack socket files.\n"
+"  --no-fifo, -F             Do not unpack named pipes.\n"
+"  --no-slink, -L            Do not unpack symbolic links.\n"
+"  --no-empty-dir, -E        Do not unpack directories that would end up\n"
+"                            empty after applying the above rules.\n"
+"  --no-sparse, -Z           Do not create sparse files, always write zero\n"
+"                            blocks to disk.\n"
+#ifdef HAVE_SYS_XATTR_H
+"  --set-xattr, -X           When unpacking files to disk, set the extended\n"
+"                            attributes from the squashfs image.\n"
+#endif
+"  --set-times, -T           When unpacking files to disk, set the create\n"
+"                            and modify timestamps from the squashfs image.\n"
+"  --chmod, -C               Change permission flags of unpacked files to\n"
+"                            those store in the squashfs image.\n"
+"  --chown, -O               Change ownership of unpacked files to the\n"
+"                            UID/GID set in the squashfs image.\n"
+"  --quiet, -q               Do not print out progress while unpacking.\n"
+"\n"
+"  --help, -h                Print help text and exit.\n"
+"  --version, -V             Print version information and exit.\n"
+"\n";
+
+static char *get_path(char *old, const char *arg)
+{
+	char *path;
+
+	free(old);
+
+	path = strdup(arg);
+	if (path == NULL) {
+		perror("processing arguments");
+		exit(EXIT_FAILURE);
+	}
+
+	if (canonicalize_name(path)) {
+		fprintf(stderr, "Invalid path: %s\n", arg);
+		free(path);
+		exit(EXIT_FAILURE);
+	}
+
+	return path;
+}
+
+void process_command_line(options_t *opt, int argc, char **argv)
+{
+	int i;
+
+	opt->op = OP_NONE;
+	opt->rdtree_flags = 0;
+	opt->flags = 0;
+	opt->cmdpath = NULL;
+	opt->unpack_root = NULL;
+	opt->image_name = NULL;
+
+	for (;;) {
+		i = getopt_long(argc, argv, short_opts, long_opts, NULL);
+		if (i == -1)
+			break;
+
+		switch (i) {
+		case 'D':
+			opt->rdtree_flags |= SQFS_TREE_NO_DEVICES;
+			break;
+		case 'S':
+			opt->rdtree_flags |= SQFS_TREE_NO_SOCKETS;
+			break;
+		case 'F':
+			opt->rdtree_flags |= SQFS_TREE_NO_FIFO;
+			break;
+		case 'L':
+			opt->rdtree_flags |= SQFS_TREE_NO_SLINKS;
+			break;
+		case 'E':
+			opt->rdtree_flags |= SQFS_TREE_NO_EMPTY;
+			break;
+		case 'C':
+			opt->flags |= UNPACK_CHMOD;
+			break;
+		case 'O':
+			opt->flags |= UNPACK_CHOWN;
+			break;
+		case 'Z':
+			opt->flags |= UNPACK_NO_SPARSE;
+			break;
+#ifdef HAVE_SYS_XATTR_H
+		case 'X':
+			opt->flags |= UNPACK_SET_XATTR;
+			break;
+#endif
+		case 'T':
+			opt->flags |= UNPACK_SET_TIMES;
+			break;
+		case 'c':
+			opt->op = OP_CAT;
+			opt->cmdpath = get_path(opt->cmdpath, optarg);
+			break;
+		case 'd':
+			opt->op = OP_DESCRIBE;
+			free(opt->cmdpath);
+			opt->cmdpath = NULL;
+			break;
+		case 'x':
+			opt->op = OP_RDATTR;
+			opt->cmdpath = get_path(opt->cmdpath, optarg);
+			break;
+		case 'l':
+			opt->op = OP_LS;
+			opt->cmdpath = get_path(opt->cmdpath, optarg);
+			break;
+		case 'p':
+			opt->unpack_root = optarg;
+			break;
+		case 'u':
+			opt->op = OP_UNPACK;
+			opt->cmdpath = get_path(opt->cmdpath, optarg);
+			break;
+		case 'q':
+			opt->flags |= UNPACK_QUIET;
+			break;
+		case 'h':
+			fputs(help_string, stdout);
+			free(opt->cmdpath);
+			exit(EXIT_SUCCESS);
+		case 'V':
+			print_version("rdsquashfs");
+			free(opt->cmdpath);
+			exit(EXIT_SUCCESS);
+		default:
+			goto fail_arg;
+		}
+	}
+
+	if (opt->op == OP_NONE) {
+		fputs("No operation specified\n", stderr);
+		goto fail_arg;
+	}
+
+	if (opt->op == OP_LS || opt->op == OP_CAT || opt->op == OP_RDATTR) {
+		opt->rdtree_flags |= SQFS_TREE_NO_RECURSE;
+	}
+
+	if (optind >= argc) {
+		fputs("Missing image argument\n", stderr);
+		goto fail_arg;
+	}
+
+	opt->image_name = argv[optind++];
+	return;
+fail_arg:
+	fputs("Try `rdsquashfs --help' for more information.\n", stderr);
+	free(opt->cmdpath);
+	exit(EXIT_FAILURE);
+}
diff --git a/bin/rdsquashfs/rdsquashfs.c b/bin/rdsquashfs/rdsquashfs.c
new file mode 100644
index 0000000..fa2bbb4
--- /dev/null
+++ b/bin/rdsquashfs/rdsquashfs.c
@@ -0,0 +1,175 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * rdsquashfs.c
+ *
+ * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
+ */
+#include "rdsquashfs.h"
+
+int main(int argc, char **argv)
+{
+	sqfs_xattr_reader_t *xattr = NULL;
+	sqfs_compressor_config_t cfg;
+	int status = EXIT_FAILURE;
+	sqfs_data_reader_t *data;
+	sqfs_dir_reader_t *dirrd;
+	sqfs_compressor_t *cmp;
+	sqfs_id_table_t *idtbl;
+	sqfs_tree_node_t *n;
+	sqfs_super_t super;
+	sqfs_file_t *file;
+	options_t opt;
+	int ret;
+
+	process_command_line(&opt, argc, argv);
+
+	file = sqfs_open_file(opt.image_name, SQFS_FILE_OPEN_READ_ONLY);
+	if (file == NULL) {
+		perror(opt.image_name);
+		goto out_cmd;
+	}
+
+	ret = sqfs_super_read(&super, file);
+	if (ret) {
+		sqfs_perror(opt.image_name, "reading super block", ret);
+		goto out_file;
+	}
+
+	sqfs_compressor_config_init(&cfg, super.compression_id,
+				    super.block_size,
+				    SQFS_COMP_FLAG_UNCOMPRESS);
+
+	ret = sqfs_compressor_create(&cfg, &cmp);
+
+#ifdef WITH_LZO
+	if (super.compression_id == SQFS_COMP_LZO && ret != 0)
+		ret = lzo_compressor_create(&cfg, &cmp);
+#endif
+
+	if (ret != 0) {
+		sqfs_perror(opt.image_name, "creating compressor", ret);
+		goto out_file;
+	}
+
+	if (!(super.flags & SQFS_FLAG_NO_XATTRS)) {
+		xattr = sqfs_xattr_reader_create(0);
+		if (xattr == NULL) {
+			sqfs_perror(opt.image_name, "creating xattr reader",
+				    SQFS_ERROR_ALLOC);
+			goto out_cmp;
+		}
+
+		ret = sqfs_xattr_reader_load(xattr, &super, file, cmp);
+		if (ret) {
+			sqfs_perror(opt.image_name, "loading xattr table",
+				    ret);
+			goto out_xr;
+		}
+	}
+
+	idtbl = sqfs_id_table_create(0);
+	if (idtbl == NULL) {
+		sqfs_perror(opt.image_name, "creating ID table",
+			    SQFS_ERROR_ALLOC);
+		goto out_xr;
+	}
+
+	ret = sqfs_id_table_read(idtbl, file, &super, cmp);
+	if (ret) {
+		sqfs_perror(opt.image_name, "loading ID table", ret);
+		goto out_id;
+	}
+
+	dirrd = sqfs_dir_reader_create(&super, cmp, file);
+	if (dirrd == NULL) {
+		sqfs_perror(opt.image_name, "creating dir reader",
+			    SQFS_ERROR_ALLOC);
+		goto out_id;
+	}
+
+	data = sqfs_data_reader_create(file, super.block_size, cmp);
+	if (data == NULL) {
+		sqfs_perror(opt.image_name, "creating data reader",
+			    SQFS_ERROR_ALLOC);
+		goto out_dr;
+	}
+
+	ret = sqfs_data_reader_load_fragment_table(data, &super);
+	if (ret) {
+		sqfs_perror(opt.image_name, "loading fragment table", ret);
+		goto out_data;
+	}
+
+	ret = sqfs_dir_reader_get_full_hierarchy(dirrd, idtbl, opt.cmdpath,
+						 opt.rdtree_flags, &n);
+	if (ret) {
+		sqfs_perror(opt.image_name, "reading filesystem tree", ret);
+		goto out_data;
+	}
+
+	switch (opt.op) {
+	case OP_LS:
+		list_files(n);
+		break;
+	case OP_CAT:
+		if (!S_ISREG(n->inode->base.mode)) {
+			fprintf(stderr, "/%s: not a regular file\n",
+				opt.cmdpath);
+			goto out;
+		}
+
+		if (sqfs_data_reader_dump(opt.cmdpath, data, n->inode,
+					  stdout, super.block_size, false)) {
+			goto out;
+		}
+		break;
+	case OP_UNPACK:
+		if (opt.unpack_root != NULL) {
+			if (mkdir_p(opt.unpack_root))
+				goto out;
+
+			if (chdir(opt.unpack_root)) {
+				perror(opt.unpack_root);
+				goto out;
+			}
+		}
+
+		if (restore_fstree(n, opt.flags))
+			goto out;
+
+		if (fill_unpacked_files(super.block_size, n, data, opt.flags))
+			goto out;
+
+		if (update_tree_attribs(xattr, n, opt.flags))
+			goto out;
+		break;
+	case OP_DESCRIBE:
+		if (describe_tree(n, opt.unpack_root))
+			goto out;
+		break;
+	case OP_RDATTR:
+		if (dump_xattrs(xattr, n->inode))
+			goto out;
+		break;
+	}
+
+	status = EXIT_SUCCESS;
+out:
+	sqfs_dir_tree_destroy(n);
+out_data:
+	sqfs_destroy(data);
+out_dr:
+	sqfs_destroy(dirrd);
+out_id:
+	sqfs_destroy(idtbl);
+out_xr:
+	if (xattr != NULL)
+		sqfs_destroy(xattr);
+out_cmp:
+	sqfs_destroy(cmp);
+out_file:
+	sqfs_destroy(file);
+out_cmd:
+	free(opt.cmdpath);
+	return status;
+}
diff --git a/bin/rdsquashfs/rdsquashfs.h b/bin/rdsquashfs/rdsquashfs.h
new file mode 100644
index 0000000..17c0a85
--- /dev/null
+++ b/bin/rdsquashfs/rdsquashfs.h
@@ -0,0 +1,77 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * rdsquashfs.h
+ *
+ * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
+ */
+#ifndef RDSQUASHFS_H
+#define RDSQUASHFS_H
+
+#include "config.h"
+#include "common.h"
+#include "fstree.h"
+
+#ifdef _WIN32
+#define WIN32_LEAN_AND_MEAN
+#include <windows.h>
+#endif
+#ifdef HAVE_SYS_XATTR_H
+#include <sys/xattr.h>
+
+#if defined(__APPLE__) && defined(__MACH__)
+#define lsetxattr(path, name, value, size, flags) \
+	setxattr(path, name, value, size, 0, flags | XATTR_NOFOLLOW)
+#endif
+#endif
+#include <string.h>
+#include <stdlib.h>
+#include <getopt.h>
+#include <assert.h>
+#include <ctype.h>
+#include <errno.h>
+#include <stdio.h>
+
+enum UNPACK_FLAGS {
+	UNPACK_CHMOD = 0x01,
+	UNPACK_CHOWN = 0x02,
+	UNPACK_QUIET = 0x04,
+	UNPACK_NO_SPARSE = 0x08,
+	UNPACK_SET_XATTR = 0x10,
+	UNPACK_SET_TIMES = 0x20,
+};
+
+enum {
+	OP_NONE = 0,
+	OP_LS,
+	OP_CAT,
+	OP_UNPACK,
+	OP_DESCRIBE,
+	OP_RDATTR,
+};
+
+typedef struct {
+	int op;
+	int rdtree_flags;
+	int flags;
+	char *cmdpath;
+	const char *unpack_root;
+	const char *image_name;
+} options_t;
+
+void list_files(const sqfs_tree_node_t *node);
+
+int restore_fstree(sqfs_tree_node_t *root, int flags);
+
+int update_tree_attribs(sqfs_xattr_reader_t *xattr,
+			const sqfs_tree_node_t *root, int flags);
+
+int fill_unpacked_files(size_t blk_sz, const sqfs_tree_node_t *root,
+			sqfs_data_reader_t *data, int flags);
+
+int describe_tree(const sqfs_tree_node_t *root, const char *unpack_root);
+
+int dump_xattrs(sqfs_xattr_reader_t *xattr, const sqfs_inode_generic_t *inode);
+
+void process_command_line(options_t *opt, int argc, char **argv);
+
+#endif /* RDSQUASHFS_H */
diff --git a/bin/rdsquashfs/restore_fstree.c b/bin/rdsquashfs/restore_fstree.c
new file mode 100644
index 0000000..8f99439
--- /dev/null
+++ b/bin/rdsquashfs/restore_fstree.c
@@ -0,0 +1,320 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * restore_fstree.c
+ *
+ * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
+ */
+#include "rdsquashfs.h"
+
+#ifdef _WIN32
+static int create_node(const sqfs_tree_node_t *n, const char *name)
+{
+	WCHAR *wpath;
+	HANDLE fh;
+
+	wpath = path_to_windows(name);
+	if (wpath == NULL)
+		return -1;
+
+	switch (n->inode->base.mode & S_IFMT) {
+	case S_IFDIR:
+		if (!CreateDirectoryW(wpath, NULL))
+			goto fail;
+		break;
+	case S_IFREG:
+		fh = CreateFileW(wpath, GENERIC_READ,
+				 FILE_SHARE_READ | FILE_SHARE_WRITE,
+				 NULL, CREATE_NEW, 0, NULL);
+
+		if (fh == INVALID_HANDLE_VALUE)
+			goto fail;
+
+		CloseHandle(fh);
+		break;
+	default:
+		break;
+	}
+
+	free(wpath);
+	return 0;
+fail:
+	fprintf(stderr, "Creating %s: %ld\n", name, GetLastError());
+	free(wpath);
+	return -1;
+}
+#else
+static int create_node(const sqfs_tree_node_t *n, const char *name)
+{
+	sqfs_u32 devno;
+	int fd;
+
+	switch (n->inode->base.mode & S_IFMT) {
+	case S_IFDIR:
+		if (mkdir(name, 0755) && errno != EEXIST) {
+			fprintf(stderr, "mkdir %s: %s\n",
+				name, strerror(errno));
+			return -1;
+		}
+		break;
+	case S_IFLNK:
+		if (symlink((const char *)n->inode->extra, name)) {
+			fprintf(stderr, "ln -s %s %s: %s\n",
+				(const char *)n->inode->extra, name,
+				strerror(errno));
+			return -1;
+		}
+		break;
+	case S_IFSOCK:
+	case S_IFIFO:
+		if (mknod(name, (n->inode->base.mode & S_IFMT) | 0700, 0)) {
+			fprintf(stderr, "creating %s: %s\n",
+				name, strerror(errno));
+			return -1;
+		}
+		break;
+	case S_IFBLK:
+	case S_IFCHR:
+		if (n->inode->base.type == SQFS_INODE_EXT_BDEV ||
+		    n->inode->base.type == SQFS_INODE_EXT_CDEV) {
+			devno = n->inode->data.dev_ext.devno;
+		} else {
+			devno = n->inode->data.dev.devno;
+		}
+
+		if (mknod(name, n->inode->base.mode & S_IFMT, devno)) {
+			fprintf(stderr, "creating device %s: %s\n",
+				name, strerror(errno));
+			return -1;
+		}
+		break;
+	case S_IFREG:
+		fd = open(name, O_WRONLY | O_CREAT | O_EXCL, 0600);
+
+		if (fd < 0) {
+			fprintf(stderr, "creating %s: %s\n",
+				name, strerror(errno));
+			return -1;
+		}
+
+		close(fd);
+		break;
+	default:
+		break;
+	}
+
+	return 0;
+}
+#endif
+
+static int create_node_dfs(const sqfs_tree_node_t *n, int flags)
+{
+	const sqfs_tree_node_t *c;
+	char *name;
+	int ret;
+
+	if (!is_filename_sane((const char *)n->name, true)) {
+		fprintf(stderr, "Found an entry named '%s', skipping.\n",
+			n->name);
+		return 0;
+	}
+
+	name = sqfs_tree_node_get_path(n);
+	if (name == NULL) {
+		fprintf(stderr, "Constructing full path for '%s': %s\n",
+			(const char *)n->name, strerror(errno));
+		return -1;
+	}
+
+	ret = canonicalize_name(name);
+	assert(ret == 0);
+
+	if (!(flags & UNPACK_QUIET))
+		printf("creating %s\n", name);
+
+	ret = create_node(n, name);
+	free(name);
+	if (ret)
+		return -1;
+
+	if (S_ISDIR(n->inode->base.mode)) {
+		for (c = n->children; c != NULL; c = c->next) {
+			if (create_node_dfs(c, flags))
+				return -1;
+		}
+	}
+	return 0;
+}
+
+#ifdef HAVE_SYS_XATTR_H
+static int set_xattr(const char *path, sqfs_xattr_reader_t *xattr,
+		     const sqfs_tree_node_t *n)
+{
+	sqfs_xattr_value_t *value;
+	sqfs_xattr_entry_t *key;
+	sqfs_xattr_id_t desc;
+	sqfs_u32 index;
+	size_t i;
+	int ret;
+
+	sqfs_inode_get_xattr_index(n->inode, &index);
+
+	if (index == 0xFFFFFFFF)
+		return 0;
+
+	if (sqfs_xattr_reader_get_desc(xattr, index, &desc)) {
+		fputs("Error resolving xattr index\n", stderr);
+		return -1;
+	}
+
+	if (sqfs_xattr_reader_seek_kv(xattr, &desc)) {
+		fputs("Error locating xattr key-value pairs\n", stderr);
+		return -1;
+	}
+
+	for (i = 0; i < desc.count; ++i) {
+		if (sqfs_xattr_reader_read_key(xattr, &key)) {
+			fputs("Error reading xattr key\n", stderr);
+			return -1;
+		}
+
+		if (sqfs_xattr_reader_read_value(xattr, key, &value)) {
+			fputs("Error reading xattr value\n", stderr);
+			free(key);
+			return -1;
+		}
+
+		ret = lsetxattr(path, (const char *)key->key,
+				value->value, value->size, 0);
+		if (ret) {
+			fprintf(stderr, "setting xattr '%s' on %s: %s\n",
+				key->key, path, strerror(errno));
+		}
+
+		free(key);
+		free(value);
+		if (ret)
+			return -1;
+	}
+
+	return 0;
+}
+#endif
+
+static int set_attribs(sqfs_xattr_reader_t *xattr,
+		       const sqfs_tree_node_t *n, int flags)
+{
+	const sqfs_tree_node_t *c;
+	char *path;
+	int ret;
+
+	if (!is_filename_sane((const char *)n->name, true))
+		return 0;
+
+	if (S_ISDIR(n->inode->base.mode)) {
+		for (c = n->children; c != NULL; c = c->next) {
+			if (set_attribs(xattr, c, flags))
+				return -1;
+		}
+	}
+
+	path = sqfs_tree_node_get_path(n);
+	if (path == NULL) {
+		fprintf(stderr, "Reconstructing full path: %s\n",
+			strerror(errno));
+		return -1;
+	}
+
+	ret = canonicalize_name(path);
+	assert(ret == 0);
+
+#ifdef HAVE_SYS_XATTR_H
+	if ((flags & UNPACK_SET_XATTR) && xattr != NULL) {
+		if (set_xattr(path, xattr, n))
+			goto fail;
+	}
+#endif
+
+#ifndef _WIN32
+	if (flags & UNPACK_SET_TIMES) {
+		struct timespec times[2];
+
+		memset(times, 0, sizeof(times));
+		times[0].tv_sec = n->inode->base.mod_time;
+		times[1].tv_sec = n->inode->base.mod_time;
+
+		if (utimensat(AT_FDCWD, path, times, AT_SYMLINK_NOFOLLOW)) {
+			fprintf(stderr, "setting timestamp on %s: %s\n",
+				path, strerror(errno));
+			goto fail;
+		}
+	}
+#endif
+	if (flags & UNPACK_CHOWN) {
+		if (fchownat(AT_FDCWD, path, n->uid, n->gid,
+			     AT_SYMLINK_NOFOLLOW)) {
+			fprintf(stderr, "chown %s: %s\n",
+				path, strerror(errno));
+			goto fail;
+		}
+	}
+
+	if (flags & UNPACK_CHMOD && !S_ISLNK(n->inode->base.mode)) {
+		if (fchmodat(AT_FDCWD, path,
+			     n->inode->base.mode & ~S_IFMT, 0)) {
+			fprintf(stderr, "chmod %s: %s\n",
+				path, strerror(errno));
+			goto fail;
+		}
+	}
+
+	free(path);
+	return 0;
+fail:
+	free(path);
+	return -1;
+}
+
+int restore_fstree(sqfs_tree_node_t *root, int flags)
+{
+	sqfs_tree_node_t *n, *old_parent;
+
+	/* make sure fstree_get_path() stops at this node */
+	old_parent = root->parent;
+	root->parent = NULL;
+
+	if (S_ISDIR(root->inode->base.mode)) {
+		for (n = root->children; n != NULL; n = n->next) {
+			if (create_node_dfs(n, flags))
+				return -1;
+		}
+	} else {
+		if (create_node_dfs(root, flags))
+			return -1;
+	}
+
+	root->parent = old_parent;
+	return 0;
+}
+
+int update_tree_attribs(sqfs_xattr_reader_t *xattr,
+			const sqfs_tree_node_t *root, int flags)
+{
+	const sqfs_tree_node_t *n;
+
+	if ((flags & (UNPACK_CHOWN | UNPACK_CHMOD |
+		      UNPACK_SET_TIMES | UNPACK_SET_XATTR)) == 0) {
+		return 0;
+	}
+
+	if (S_ISDIR(root->inode->base.mode)) {
+		for (n = root->children; n != NULL; n = n->next) {
+			if (set_attribs(xattr, n, flags))
+				return -1;
+		}
+	} else {
+		if (set_attribs(xattr, root, flags))
+			return -1;
+	}
+
+	return 0;
+}
diff --git a/bin/sqfs2tar.c b/bin/sqfs2tar.c
new file mode 100644
index 0000000..6d2a51a
--- /dev/null
+++ b/bin/sqfs2tar.c
@@ -0,0 +1,688 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * sqfs2tar.c
+ *
+ * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
+ */
+#include "config.h"
+#include "common.h"
+#include "tar.h"
+
+#include <getopt.h>
+#include <string.h>
+#include <stdlib.h>
+#include <assert.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+
+static struct option long_opts[] = {
+	{ "subdir", required_argument, NULL, 'd' },
+	{ "keep-as-dir", no_argument, NULL, 'k' },
+	{ "root-becomes", required_argument, NULL, 'r' },
+	{ "no-skip", no_argument, NULL, 's' },
+	{ "no-xattr", no_argument, NULL, 'X' },
+	{ "no-hard-links", no_argument, NULL, 'L' },
+	{ "help", no_argument, NULL, 'h' },
+	{ "version", no_argument, NULL, 'V' },
+	{ NULL, 0, NULL, 0 },
+};
+
+static const char *short_opts = "d:kr:sXLhV";
+
+static const char *usagestr =
+"Usage: sqfs2tar [OPTIONS...] <sqfsfile>\n"
+"\n"
+"Read an input squashfs archive and turn it into a tar archive, written\n"
+"to stdout.\n"
+"\n"
+"Possible options:\n"
+"\n"
+"  --subdir, -d <dir>        Unpack the given sub directory instead of the\n"
+"                            filesystem root. Can be specified more than\n"
+"                            once to select multiple directories. If only\n"
+"                            one is specified, it becomes the new root of\n"
+"                            node of the archive file system tree.\n"
+"\n"
+"  --root-becomes, -r <dir>  Turn the root inode into a directory with the\n"
+"                            specified name. Everything else will be stored\n"
+"                            inside this directory. The special value '.' is\n"
+"                            allowed to prefix all tar paths with './' and\n"
+"                            add an entry named '.' for the root inode.\n"
+"                            If this option isn't used, all meta data stored\n"
+"                            in the root inode IS LOST!\n"
+"\n"
+"  --keep-as-dir, -k         If --subdir is used only once, don't make the\n"
+"                            subdir the archive root, instead keep it as\n"
+"                            prefix for all unpacked files.\n"
+"                            Using --subdir more than once implies\n"
+"                            --keep-as-dir.\n"
+"  --no-xattr, -X            Do not copy extended attributes.\n"
+"  --no-hard-links, -L       Do not generate hard links. Produce duplicate\n"
+"                            entries instead.\n"
+"\n"
+"  --no-skip, -s             Abort if a file cannot be stored in a tar\n"
+"                            archive. By default, it is simply skipped\n"
+"                            and a warning is written to stderr.\n"
+"\n"
+"  --help, -h                Print help text and exit.\n"
+"  --version, -V             Print version information and exit.\n"
+"\n"
+"Examples:\n"
+"\n"
+"\tsqfs2tar rootfs.sqfs > rootfs.tar\n"
+"\tsqfs2tar rootfs.sqfs | gzip > rootfs.tar.gz\n"
+"\tsqfs2tar rootfs.sqfs | xz > rootfs.tar.xz\n"
+"\n";
+
+static const char *filename;
+static unsigned int record_counter;
+static bool dont_skip = false;
+static bool keep_as_dir = false;
+static bool no_xattr = false;
+static bool no_links = false;
+
+static char *root_becomes = NULL;
+static char **subdirs = NULL;
+static size_t num_subdirs = 0;
+static size_t max_subdirs = 0;
+
+static sqfs_xattr_reader_t *xr;
+static sqfs_data_reader_t *data;
+static sqfs_file_t *file;
+static sqfs_super_t super;
+static sqfs_hard_link_t *links = NULL;
+
+static FILE *out_file = NULL;
+
+static void process_args(int argc, char **argv)
+{
+	size_t idx, new_count;
+	int i, ret;
+	void *new;
+
+	for (;;) {
+		i = getopt_long(argc, argv, short_opts, long_opts, NULL);
+		if (i == -1)
+			break;
+
+		switch (i) {
+		case 'd':
+			if (num_subdirs == max_subdirs) {
+				new_count = max_subdirs ? max_subdirs * 2 : 16;
+				new = realloc(subdirs,
+					      new_count * sizeof(subdirs[0]));
+				if (new == NULL)
+					goto fail_errno;
+
+				max_subdirs = new_count;
+				subdirs = new;
+			}
+
+			subdirs[num_subdirs] = strdup(optarg);
+			if (subdirs[num_subdirs] == NULL)
+				goto fail_errno;
+
+			if (canonicalize_name(subdirs[num_subdirs])) {
+				perror(optarg);
+				goto fail;
+			}
+
+			++num_subdirs;
+			break;
+		case 'r':
+			free(root_becomes);
+			root_becomes = strdup(optarg);
+			if (root_becomes == NULL)
+				goto fail_errno;
+
+			if (strcmp(root_becomes, "./") == 0)
+				root_becomes[1] = '\0';
+
+			if (strcmp(root_becomes, ".") == 0)
+				break;
+
+			if (canonicalize_name(root_becomes) != 0 ||
+			    strlen(root_becomes) == 0) {
+				fprintf(stderr,
+					"Invalid root directory '%s'.\n",
+					optarg);
+				goto fail_arg;
+			}
+			break;
+		case 'k':
+			keep_as_dir = true;
+			break;
+		case 's':
+			dont_skip = true;
+			break;
+		case 'X':
+			no_xattr = true;
+			break;
+		case 'L':
+			no_links = true;
+			break;
+		case 'h':
+			fputs(usagestr, stdout);
+			goto out_success;
+		case 'V':
+			print_version("sqfs2tar");
+			goto out_success;
+		default:
+			goto fail_arg;
+		}
+	}
+
+	if (optind >= argc) {
+		fputs("Missing argument: squashfs image\n", stderr);
+		goto fail_arg;
+	}
+
+	filename = argv[optind++];
+
+	if (optind < argc) {
+		fputs("Unknown extra arguments\n", stderr);
+		goto fail_arg;
+	}
+
+	if (num_subdirs > 1)
+		keep_as_dir = true;
+
+	return;
+fail_errno:
+	perror("parsing options");
+	goto fail;
+fail_arg:
+	fputs("Try `sqfs2tar --help' for more information.\n", stderr);
+	goto fail;
+fail:
+	ret = EXIT_FAILURE;
+	goto out_exit;
+out_success:
+	ret = EXIT_SUCCESS;
+	goto out_exit;
+out_exit:
+	for (idx = 0; idx < num_subdirs; ++idx)
+		free(subdirs[idx]);
+	free(root_becomes);
+	free(subdirs);
+	exit(ret);
+}
+
+static int terminate_archive(void)
+{
+	char buffer[1024];
+
+	memset(buffer, '\0', sizeof(buffer));
+
+	return write_retry("adding archive terminator", out_file,
+			   buffer, sizeof(buffer));
+}
+
+static int get_xattrs(const char *name, const sqfs_inode_generic_t *inode,
+		      tar_xattr_t **out)
+{
+	tar_xattr_t *list = NULL, *ent;
+	sqfs_xattr_value_t *value;
+	sqfs_xattr_entry_t *key;
+	sqfs_xattr_id_t desc;
+	sqfs_u32 index;
+	size_t i;
+	int ret;
+
+	if (xr == NULL)
+		return 0;
+
+	sqfs_inode_get_xattr_index(inode, &index);
+
+	if (index == 0xFFFFFFFF)
+		return 0;
+
+	ret = sqfs_xattr_reader_get_desc(xr, index, &desc);
+	if (ret) {
+		sqfs_perror(name, "resolving xattr index", ret);
+		return -1;
+	}
+
+	ret = sqfs_xattr_reader_seek_kv(xr, &desc);
+	if (ret) {
+		sqfs_perror(name, "locating xattr key-value pairs", ret);
+		return -1;
+	}
+
+	for (i = 0; i < desc.count; ++i) {
+		ret = sqfs_xattr_reader_read_key(xr, &key);
+		if (ret) {
+			sqfs_perror(name, "reading xattr key", ret);
+			goto fail;
+		}
+
+		ret = sqfs_xattr_reader_read_value(xr, key, &value);
+		if (ret) {
+			sqfs_perror(name, "reading xattr value", ret);
+			free(key);
+			goto fail;
+		}
+
+		ent = calloc(1, sizeof(*ent) + strlen((const char *)key->key) +
+			     value->size + 2);
+		if (ent == NULL) {
+			perror("creating xattr entry");
+			free(key);
+			free(value);
+			goto fail;
+		}
+
+		ent->key = ent->data;
+		strcpy(ent->key, (const char *)key->key);
+
+		ent->value = (sqfs_u8 *)ent->key + strlen(ent->key) + 1;
+		memcpy(ent->value, value->value, value->size + 1);
+
+		ent->value_len = value->size;
+		ent->next = list;
+		list = ent;
+
+		free(key);
+		free(value);
+	}
+
+	*out = list;
+	return 0;
+fail:
+	while (list != NULL) {
+		ent = list;
+		list = list->next;
+		free(ent);
+	}
+	return -1;
+}
+
+static char *assemble_tar_path(char *name, bool is_dir)
+{
+	size_t len, new_len;
+	char *temp;
+	(void)is_dir;
+
+	if (root_becomes == NULL && !is_dir)
+		return name;
+
+	new_len = strlen(name);
+	if (root_becomes != NULL)
+		new_len += strlen(root_becomes) + 1;
+	if (is_dir)
+		new_len += 1;
+
+	temp = realloc(name, new_len + 1);
+	if (temp == NULL) {
+		perror("assembling tar entry filename");
+		free(name);
+		return NULL;
+	}
+
+	name = temp;
+
+	if (root_becomes != NULL) {
+		len = strlen(root_becomes);
+
+		memmove(name + len + 1, name, strlen(name) + 1);
+		memcpy(name, root_becomes, len);
+		name[len] = '/';
+	}
+
+	if (is_dir) {
+		len = strlen(name);
+
+		if (len == 0 || name[len - 1] != '/') {
+			name[len++] = '/';
+			name[len] = '\0';
+		}
+	}
+
+	return name;
+}
+
+static int write_tree_dfs(const sqfs_tree_node_t *n)
+{
+	tar_xattr_t *xattr = NULL, *xit;
+	sqfs_hard_link_t *lnk = NULL;
+	char *name, *target;
+	struct stat sb;
+	size_t len;
+	int ret;
+
+	inode_stat(n, &sb);
+
+	if (n->parent == NULL) {
+		if (root_becomes == NULL)
+			goto skip_hdr;
+
+		len = strlen(root_becomes);
+		name = malloc(len + 2);
+		if (name == NULL) {
+			perror("creating root directory");
+			return -1;
+		}
+
+		memcpy(name, root_becomes, len);
+		name[len] = '/';
+		name[len + 1] = '\0';
+	} else {
+		if (!is_filename_sane((const char *)n->name, false)) {
+			fprintf(stderr, "Found a file named '%s', skipping.\n",
+				n->name);
+			if (dont_skip) {
+				fputs("Not allowed to skip files, aborting!\n",
+				      stderr);
+				return -1;
+			}
+			return 0;
+		}
+
+		name = sqfs_tree_node_get_path(n);
+		if (name == NULL) {
+			perror("resolving tree node path");
+			return -1;
+		}
+
+		if (canonicalize_name(name))
+			goto out_skip;
+
+		for (lnk = links; lnk != NULL; lnk = lnk->next) {
+			if (lnk->inode_number == n->inode->base.inode_number) {
+				if (strcmp(name, lnk->target) == 0)
+					lnk = NULL;
+				break;
+			}
+		}
+
+		name = assemble_tar_path(name, S_ISDIR(sb.st_mode));
+		if (name == NULL)
+			return -1;
+	}
+
+	if (lnk != NULL) {
+		ret = write_hard_link(out_file, &sb, name, lnk->target,
+				      record_counter++);
+		free(name);
+		return ret;
+	}
+
+	if (!no_xattr) {
+		if (get_xattrs(name, n->inode, &xattr)) {
+			free(name);
+			return -1;
+		}
+	}
+
+	target = S_ISLNK(sb.st_mode) ? (char *)n->inode->extra : NULL;
+	ret = write_tar_header(out_file, &sb, name, target, xattr,
+			       record_counter++);
+
+	while (xattr != NULL) {
+		xit = xattr;
+		xattr = xattr->next;
+		free(xit);
+	}
+
+	if (ret > 0)
+		goto out_skip;
+
+	if (ret < 0) {
+		free(name);
+		return -1;
+	}
+
+	if (S_ISREG(sb.st_mode)) {
+		if (sqfs_data_reader_dump(name, data, n->inode, out_file,
+					  super.block_size, false)) {
+			free(name);
+			return -1;
+		}
+
+		if (padd_file(out_file, sb.st_size)) {
+			free(name);
+			return -1;
+		}
+	}
+
+	free(name);
+skip_hdr:
+	for (n = n->children; n != NULL; n = n->next) {
+		if (write_tree_dfs(n))
+			return -1;
+	}
+	return 0;
+out_skip:
+	if (dont_skip) {
+		fputs("Not allowed to skip files, aborting!\n", stderr);
+		ret = -1;
+	} else {
+		fprintf(stderr, "Skipping %s\n", name);
+		ret = 0;
+	}
+	free(name);
+	return ret;
+}
+
+static sqfs_tree_node_t *tree_merge(sqfs_tree_node_t *lhs,
+				    sqfs_tree_node_t *rhs)
+{
+	sqfs_tree_node_t *head = NULL, **next_ptr = &head;
+	sqfs_tree_node_t *it, *l, *r;
+	int diff;
+
+	while (lhs->children != NULL && rhs->children != NULL) {
+		diff = strcmp((const char *)lhs->children->name,
+			      (const char *)rhs->children->name);
+
+		if (diff < 0) {
+			it = lhs->children;
+			lhs->children = lhs->children->next;
+		} else if (diff > 0) {
+			it = rhs->children;
+			rhs->children = rhs->children->next;
+		} else {
+			l = lhs->children;
+			lhs->children = lhs->children->next;
+
+			r = rhs->children;
+			rhs->children = rhs->children->next;
+
+			it = tree_merge(l, r);
+		}
+
+		*next_ptr = it;
+		next_ptr = &it->next;
+	}
+
+	it = (lhs->children != NULL ? lhs->children : rhs->children);
+	*next_ptr = it;
+
+	sqfs_dir_tree_destroy(rhs);
+	lhs->children = head;
+	return lhs;
+}
+
+int main(int argc, char **argv)
+{
+	sqfs_tree_node_t *root = NULL, *subtree;
+	int flags, ret, status = EXIT_FAILURE;
+	sqfs_compressor_config_t cfg;
+	sqfs_compressor_t *cmp;
+	sqfs_id_table_t *idtbl;
+	sqfs_dir_reader_t *dr;
+	sqfs_hard_link_t *lnk;
+	size_t i;
+
+	process_args(argc, argv);
+
+#ifdef _WIN32
+	_setmode(_fileno(stdout), _O_BINARY);
+	out_file = stdout;
+#else
+	out_file = freopen(NULL, "wb", stdout);
+#endif
+
+	if (out_file == NULL) {
+		perror("changing stdout to binary mode");
+		goto out_dirs;
+	}
+
+	file = sqfs_open_file(filename, SQFS_FILE_OPEN_READ_ONLY);
+	if (file == NULL) {
+		perror(filename);
+		goto out_dirs;
+	}
+
+	ret = sqfs_super_read(&super, file);
+	if (ret) {
+		sqfs_perror(filename, "reading super block", ret);
+		goto out_fd;
+	}
+
+	sqfs_compressor_config_init(&cfg, super.compression_id,
+				    super.block_size,
+				    SQFS_COMP_FLAG_UNCOMPRESS);
+
+	ret = sqfs_compressor_create(&cfg, &cmp);
+
+#ifdef WITH_LZO
+	if (super.compression_id == SQFS_COMP_LZO && ret != 0)
+		ret = lzo_compressor_create(&cfg, &cmp);
+#endif
+
+	if (ret != 0) {
+		sqfs_perror(filename, "creating compressor", ret);
+		goto out_fd;
+	}
+
+	idtbl = sqfs_id_table_create(0);
+
+	if (idtbl == NULL) {
+		perror("creating ID table");
+		goto out_cmp;
+	}
+
+	ret = sqfs_id_table_read(idtbl, file, &super, cmp);
+	if (ret) {
+		sqfs_perror(filename, "loading ID table", ret);
+		goto out_id;
+	}
+
+	data = sqfs_data_reader_create(file, super.block_size, cmp);
+	if (data == NULL) {
+		sqfs_perror(filename, "creating data reader",
+			    SQFS_ERROR_ALLOC);
+		goto out_id;
+	}
+
+	ret = sqfs_data_reader_load_fragment_table(data, &super);
+	if (ret) {
+		sqfs_perror(filename, "loading fragment table", ret);
+		goto out_data;
+	}
+
+	dr = sqfs_dir_reader_create(&super, cmp, file);
+	if (dr == NULL) {
+		sqfs_perror(filename, "creating dir reader",
+			    SQFS_ERROR_ALLOC);
+		goto out_data;
+	}
+
+	if (!no_xattr && !(super.flags & SQFS_FLAG_NO_XATTRS)) {
+		xr = sqfs_xattr_reader_create(0);
+		if (xr == NULL) {
+			sqfs_perror(filename, "creating xattr reader",
+				    SQFS_ERROR_ALLOC);
+			goto out_dr;
+		}
+
+		ret = sqfs_xattr_reader_load(xr, &super, file, cmp);
+		if (ret) {
+			sqfs_perror(filename, "loading xattr table", ret);
+			goto out_xr;
+		}
+	}
+
+	if (num_subdirs == 0) {
+		ret = sqfs_dir_reader_get_full_hierarchy(dr, idtbl, NULL,
+							 0, &root);
+		if (ret) {
+			sqfs_perror(filename, "loading filesystem tree", ret);
+			goto out;
+		}
+	} else {
+		flags = 0;
+
+		if (keep_as_dir || num_subdirs > 1)
+			flags = SQFS_TREE_STORE_PARENTS;
+
+		for (i = 0; i < num_subdirs; ++i) {
+			ret = sqfs_dir_reader_get_full_hierarchy(dr, idtbl,
+								 subdirs[i],
+								 flags,
+								 &subtree);
+			if (ret) {
+				sqfs_perror(subdirs[i], "loading filesystem "
+					    "tree", ret);
+				goto out;
+			}
+
+			if (root == NULL) {
+				root = subtree;
+			} else {
+				root = tree_merge(root, subtree);
+			}
+		}
+	}
+
+	if (!no_links) {
+		if (sqfs_tree_find_hard_links(root, &links))
+			goto out_tree;
+
+		for (lnk = links; lnk != NULL; lnk = lnk->next) {
+			lnk->target = assemble_tar_path(lnk->target, false);
+			if (lnk->target == NULL)
+				goto out;
+		}
+	}
+
+	if (write_tree_dfs(root))
+		goto out;
+
+	if (terminate_archive())
+		goto out;
+
+	status = EXIT_SUCCESS;
+	fflush(out_file);
+out:
+	while (links != NULL) {
+		lnk = links;
+		links = links->next;
+		free(lnk->target);
+		free(lnk);
+	}
+out_tree:
+	if (root != NULL)
+		sqfs_dir_tree_destroy(root);
+out_xr:
+	if (xr != NULL)
+		sqfs_destroy(xr);
+out_dr:
+	sqfs_destroy(dr);
+out_data:
+	sqfs_destroy(data);
+out_id:
+	sqfs_destroy(idtbl);
+out_cmp:
+	sqfs_destroy(cmp);
+out_fd:
+	sqfs_destroy(file);
+out_dirs:
+	for (i = 0; i < num_subdirs; ++i)
+		free(subdirs[i]);
+	free(subdirs);
+	free(root_becomes);
+	return status;
+}
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;
+}
diff --git a/bin/tar2sqfs.c b/bin/tar2sqfs.c
new file mode 100644
index 0000000..6025dc9
--- /dev/null
+++ b/bin/tar2sqfs.c
@@ -0,0 +1,544 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * tar2sqfs.c
+ *
+ * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
+ */
+#include "config.h"
+#include "common.h"
+#include "compat.h"
+#include "tar.h"
+
+#include <stdlib.h>
+#include <getopt.h>
+#include <string.h>
+#include <stdio.h>
+#include <fcntl.h>
+
+#ifdef _WIN32
+#include <io.h>
+#endif
+
+static struct option long_opts[] = {
+	{ "root-becomes", required_argument, NULL, 'r' },
+	{ "compressor", required_argument, NULL, 'c' },
+	{ "block-size", required_argument, NULL, 'b' },
+	{ "dev-block-size", required_argument, NULL, 'B' },
+	{ "defaults", required_argument, NULL, 'd' },
+	{ "num-jobs", required_argument, NULL, 'j' },
+	{ "queue-backlog", required_argument, NULL, 'Q' },
+	{ "comp-extra", required_argument, NULL, 'X' },
+	{ "no-skip", no_argument, NULL, 's' },
+	{ "no-xattr", no_argument, NULL, 'x' },
+	{ "no-keep-time", no_argument, NULL, 'k' },
+	{ "exportable", no_argument, NULL, 'e' },
+	{ "no-tail-packing", no_argument, NULL, 'T' },
+	{ "force", no_argument, NULL, 'f' },
+	{ "quiet", no_argument, NULL, 'q' },
+	{ "help", no_argument, NULL, 'h' },
+	{ "version", no_argument, NULL, 'V' },
+	{ NULL, 0, NULL, 0 },
+};
+
+static const char *short_opts = "r:c:b:B:d:X:j:Q:sxekfqThV";
+
+static const char *usagestr =
+"Usage: tar2sqfs [OPTIONS...] <sqfsfile>\n"
+"\n"
+"Read an uncompressed tar archive from stdin and turn it into a squashfs\n"
+"filesystem image.\n"
+"\n"
+"Possible options:\n"
+"\n"
+"  --root-becomes, -r <dir>    The specified directory becomes the root.\n"
+"                              Only its children are packed into the image\n"
+"                              and its attributes (ownership, permissions,\n"
+"                              xattrs, ...) are stored in the root inode.\n"
+"                              If not set and a tarbal has an entry for './'\n"
+"                              or '/', it becomes the root instead.\n"
+"\n"
+"  --compressor, -c <name>     Select the compressor to use.\n"
+"                              A list of available compressors is below.\n"
+"  --comp-extra, -X <options>  A comma separated list of extra options for\n"
+"                              the selected compressor. Specify 'help' to\n"
+"                              get a list of available options.\n"
+"  --num-jobs, -j <count>      Number of compressor jobs to create.\n"
+"  --queue-backlog, -Q <count> Maximum number of data blocks in the thread\n"
+"                              worker queue before the packer starts waiting\n"
+"                              for the block processors to catch up.\n"
+"                              Defaults to 10 times the number of jobs.\n"
+"  --block-size, -b <size>     Block size to use for Squashfs image.\n"
+"                              Defaults to %u.\n"
+"  --dev-block-size, -B <size> Device block size to padd the image to.\n"
+"                              Defaults to %u.\n"
+"  --defaults, -d <options>    A comma separated list of default values for\n"
+"                              implicitly created directories.\n"
+"\n"
+"                              Possible options:\n"
+"                                 uid=<value>    0 if not set.\n"
+"                                 gid=<value>    0 if not set.\n"
+"                                 mode=<value>   0755 if not set.\n"
+"                                 mtime=<value>  0 if not set.\n"
+"\n"
+"  --no-skip, -s               Abort if a tar record cannot be read instead\n"
+"                              of skipping it.\n"
+"  --no-xattr, -x              Do not copy extended attributes from archive.\n"
+"  --no-keep-time, -k          Do not keep the time stamps stored in the\n"
+"                              archive. Instead, set defaults on all files.\n"
+"  --exportable, -e            Generate an export table for NFS support.\n"
+"  --no-tail-packing, -T       Do not perform tail end packing on files that\n"
+"                              are larger than block size.\n"
+"  --force, -f                 Overwrite the output file if it exists.\n"
+"  --quiet, -q                 Do not print out progress reports.\n"
+"  --help, -h                  Print help text and exit.\n"
+"  --version, -V               Print version information and exit.\n"
+"\n"
+"Examples:\n"
+"\n"
+"\ttar2sqfs rootfs.sqfs < rootfs.tar\n"
+"\tzcat rootfs.tar.gz | tar2sqfs rootfs.sqfs\n"
+"\txzcat rootfs.tar.xz | tar2sqfs rootfs.sqfs\n"
+"\n";
+
+static bool dont_skip = false;
+static bool keep_time = true;
+static bool no_tail_pack = false;
+static sqfs_writer_cfg_t cfg;
+static sqfs_writer_t sqfs;
+static FILE *input_file = NULL;
+static char *root_becomes = NULL;
+
+static void process_args(int argc, char **argv)
+{
+	bool have_compressor;
+	int i, ret;
+
+	sqfs_writer_cfg_init(&cfg);
+
+	for (;;) {
+		i = getopt_long(argc, argv, short_opts, long_opts, NULL);
+		if (i == -1)
+			break;
+
+		switch (i) {
+		case 'T':
+			no_tail_pack = true;
+			break;
+		case 'b':
+			if (parse_size("Block size", &cfg.block_size,
+				       optarg, 0)) {
+				exit(EXIT_FAILURE);
+			}
+			break;
+		case 'B':
+			if (parse_size("Device block size", &cfg.devblksize,
+				       optarg, 0)) {
+				exit(EXIT_FAILURE);
+			}
+			if (cfg.devblksize < 1024) {
+				fputs("Device block size must be at "
+				      "least 1024\n", stderr);
+				exit(EXIT_FAILURE);
+			}
+			break;
+		case 'c':
+			have_compressor = true;
+			ret = sqfs_compressor_id_from_name(optarg);
+
+			if (ret < 0) {
+				have_compressor = false;
+#ifdef WITH_LZO
+				if (cfg.comp_id == SQFS_COMP_LZO)
+					have_compressor = true;
+#endif
+			}
+
+			if (!have_compressor) {
+				fprintf(stderr, "Unsupported compressor '%s'\n",
+					optarg);
+				exit(EXIT_FAILURE);
+			}
+
+			cfg.comp_id = ret;
+			break;
+		case 'j':
+			cfg.num_jobs = strtol(optarg, NULL, 0);
+			break;
+		case 'Q':
+			cfg.max_backlog = strtol(optarg, NULL, 0);
+			break;
+		case 'X':
+			cfg.comp_extra = optarg;
+			break;
+		case 'd':
+			cfg.fs_defaults = optarg;
+			break;
+		case 'x':
+			cfg.no_xattr = true;
+			break;
+		case 'k':
+			keep_time = false;
+			break;
+		case 'r':
+			free(root_becomes);
+			root_becomes = strdup(optarg);
+			if (root_becomes == NULL) {
+				perror("copying root directory name");
+				exit(EXIT_FAILURE);
+			}
+
+			if (canonicalize_name(root_becomes) != 0 ||
+			    strlen(root_becomes) == 0) {
+				fprintf(stderr,
+					"Invalid root directory '%s'.\n",
+					optarg);
+				goto fail_arg;
+			}
+			break;
+		case 's':
+			dont_skip = true;
+			break;
+		case 'e':
+			cfg.exportable = true;
+			break;
+		case 'f':
+			cfg.outmode |= SQFS_FILE_OPEN_OVERWRITE;
+			break;
+		case 'q':
+			cfg.quiet = true;
+			break;
+		case 'h':
+			printf(usagestr, SQFS_DEFAULT_BLOCK_SIZE,
+			       SQFS_DEVBLK_SIZE);
+			compressor_print_available();
+			exit(EXIT_SUCCESS);
+		case 'V':
+			print_version("tar2sqfs");
+			exit(EXIT_SUCCESS);
+		default:
+			goto fail_arg;
+		}
+	}
+
+	if (cfg.num_jobs < 1)
+		cfg.num_jobs = 1;
+
+	if (cfg.max_backlog < 1)
+		cfg.max_backlog = 10 * cfg.num_jobs;
+
+	if (cfg.comp_extra != NULL && strcmp(cfg.comp_extra, "help") == 0) {
+		compressor_print_help(cfg.comp_id);
+		exit(EXIT_SUCCESS);
+	}
+
+	if (optind >= argc) {
+		fputs("Missing argument: squashfs image\n", stderr);
+		goto fail_arg;
+	}
+
+	cfg.filename = argv[optind++];
+
+	if (optind < argc) {
+		fputs("Unknown extra arguments\n", stderr);
+		goto fail_arg;
+	}
+	return;
+fail_arg:
+	fputs("Try `tar2sqfs --help' for more information.\n", stderr);
+	exit(EXIT_FAILURE);
+}
+
+static int write_file(tar_header_decoded_t *hdr, file_info_t *fi,
+		      sqfs_u64 filesize)
+{
+	sqfs_file_t *file;
+	int flags;
+	int ret;
+
+	file = sqfs_get_stdin_file(input_file, hdr->sparse, filesize);
+	if (file == NULL) {
+		perror("packing files");
+		return -1;
+	}
+
+	flags = 0;
+	if (no_tail_pack && filesize > cfg.block_size)
+		flags |= SQFS_BLK_DONT_FRAGMENT;
+
+	ret = write_data_from_file(hdr->name, sqfs.data,
+				   (sqfs_inode_generic_t **)&fi->user_ptr,
+				   file, flags);
+	sqfs_destroy(file);
+
+	if (ret)
+		return -1;
+
+	return skip_padding(input_file, hdr->sparse == NULL ?
+			    filesize : hdr->record_size);
+}
+
+static int copy_xattr(tree_node_t *node, const tar_header_decoded_t *hdr)
+{
+	tar_xattr_t *xattr;
+	int ret;
+
+	ret = sqfs_xattr_writer_begin(sqfs.xwr);
+	if (ret) {
+		sqfs_perror(hdr->name, "beginning xattr block", ret);
+		return -1;
+	}
+
+	for (xattr = hdr->xattr; xattr != NULL; xattr = xattr->next) {
+		if (sqfs_get_xattr_prefix_id(xattr->key) < 0) {
+			fprintf(stderr, "%s: squashfs does not "
+				"support xattr prefix of %s\n",
+				dont_skip ? "ERROR" : "WARNING",
+				xattr->key);
+
+			if (dont_skip)
+				return -1;
+			continue;
+		}
+
+		ret = sqfs_xattr_writer_add(sqfs.xwr, xattr->key, xattr->value,
+					    xattr->value_len);
+		if (ret) {
+			sqfs_perror(hdr->name, "storing xattr key-value pair",
+				    ret);
+			return -1;
+		}
+	}
+
+	ret = sqfs_xattr_writer_end(sqfs.xwr, &node->xattr_idx);
+	if (ret) {
+		sqfs_perror(hdr->name, "completing xattr block", ret);
+		return -1;
+	}
+
+	return 0;
+}
+
+static int create_node_and_repack_data(tar_header_decoded_t *hdr)
+{
+	tree_node_t *node;
+
+	if (hdr->is_hard_link) {
+		node = fstree_add_hard_link(&sqfs.fs, hdr->name,
+					    hdr->link_target);
+		if (node == NULL)
+			goto fail_errno;
+
+		if (!cfg.quiet) {
+			printf("Hard link %s -> %s\n", hdr->name,
+			       hdr->link_target);
+		}
+		return 0;
+	}
+
+	if (!keep_time) {
+		hdr->sb.st_mtime = sqfs.fs.defaults.st_mtime;
+	}
+
+	node = fstree_add_generic(&sqfs.fs, hdr->name,
+				  &hdr->sb, hdr->link_target);
+	if (node == NULL)
+		goto fail_errno;
+
+	if (!cfg.quiet)
+		printf("Packing %s\n", hdr->name);
+
+	if (!cfg.no_xattr) {
+		if (copy_xattr(node, hdr))
+			return -1;
+	}
+
+	if (S_ISREG(hdr->sb.st_mode)) {
+		if (write_file(hdr, &node->data.file, hdr->sb.st_size))
+			return -1;
+	}
+
+	return 0;
+fail_errno:
+	perror(hdr->name);
+	return -1;
+}
+
+static int set_root_attribs(const tar_header_decoded_t *hdr)
+{
+	if (hdr->is_hard_link || !S_ISDIR(hdr->sb.st_mode)) {
+		fprintf(stderr, "'%s' is not a directory!\n", hdr->name);
+		return -1;
+	}
+
+	sqfs.fs.root->uid = hdr->sb.st_uid;
+	sqfs.fs.root->gid = hdr->sb.st_gid;
+	sqfs.fs.root->mode = hdr->sb.st_mode;
+
+	if (keep_time)
+		sqfs.fs.root->mod_time = hdr->sb.st_mtime;
+
+	if (!cfg.no_xattr) {
+		if (copy_xattr(sqfs.fs.root, hdr))
+			return -1;
+	}
+
+	return 0;
+}
+
+static int process_tar_ball(void)
+{
+	bool skip, is_root, is_prefixed;
+	tar_header_decoded_t hdr;
+	sqfs_u64 offset, count;
+	sparse_map_t *m;
+	size_t rootlen;
+	int ret;
+
+	rootlen = root_becomes == NULL ? 0 : strlen(root_becomes);
+
+	for (;;) {
+		ret = read_header(input_file, &hdr);
+		if (ret > 0)
+			break;
+		if (ret < 0)
+			return -1;
+
+		if (hdr.mtime < 0)
+			hdr.mtime = 0;
+
+		if ((sqfs_u64)hdr.mtime > 0x0FFFFFFFFUL)
+			hdr.mtime = 0x0FFFFFFFFUL;
+
+		hdr.sb.st_mtime = hdr.mtime;
+
+		skip = false;
+		is_root = false;
+		is_prefixed = true;
+
+		if (hdr.name == NULL || canonicalize_name(hdr.name) != 0) {
+			fprintf(stderr, "skipping '%s' (invalid name)\n",
+				hdr.name);
+			skip = true;
+		}
+
+		if (root_becomes != NULL) {
+			if (strncmp(hdr.name, root_becomes, rootlen) == 0) {
+				if (hdr.name[rootlen] == '\0') {
+					is_root = true;
+				} else if (hdr.name[rootlen] != '/') {
+					is_prefixed = false;
+				}
+			} else {
+				is_prefixed = false;
+			}
+
+			if (is_prefixed && !is_root) {
+				memmove(hdr.name, hdr.name + rootlen + 1,
+					strlen(hdr.name + rootlen + 1) + 1);
+			}
+
+			if (is_prefixed && hdr.name[0] == '\0') {
+				fputs("skipping entry with empty name\n",
+				      stderr);
+				skip = true;
+			}
+		} else if (hdr.name[0] == '\0') {
+			is_root = true;
+		}
+
+		if (!is_prefixed) {
+			clear_header(&hdr);
+			continue;
+		}
+
+		if (is_root) {
+			if (set_root_attribs(&hdr))
+				goto fail;
+			clear_header(&hdr);
+			continue;
+		}
+
+		if (!skip && hdr.unknown_record) {
+			fprintf(stderr, "%s: unknown entry type\n", hdr.name);
+			skip = true;
+		}
+
+		if (!skip && hdr.sparse != NULL) {
+			offset = hdr.sparse->offset;
+			count = 0;
+
+			for (m = hdr.sparse; m != NULL; m = m->next) {
+				if (m->offset < offset) {
+					skip = true;
+					break;
+				}
+				offset = m->offset + m->count;
+				count += m->count;
+			}
+
+			if (count != hdr.record_size)
+				skip = true;
+
+			if (skip) {
+				fprintf(stderr, "%s: broken sparse "
+					"file layout\n", hdr.name);
+			}
+		}
+
+		if (skip) {
+			if (dont_skip)
+				goto fail;
+			if (skip_entry(input_file, hdr.sb.st_size))
+				goto fail;
+
+			clear_header(&hdr);
+			continue;
+		}
+
+		if (create_node_and_repack_data(&hdr))
+			goto fail;
+
+		clear_header(&hdr);
+	}
+
+	return 0;
+fail:
+	clear_header(&hdr);
+	return -1;
+}
+
+int main(int argc, char **argv)
+{
+	int status = EXIT_FAILURE;
+
+	process_args(argc, argv);
+
+#ifdef _WIN32
+	_setmode(_fileno(stdin), _O_BINARY);
+	input_file = stdin;
+#else
+	input_file = freopen(NULL, "rb", stdin);
+#endif
+
+	if (input_file == NULL) {
+		perror("changing stdin to binary mode");
+		return EXIT_FAILURE;
+	}
+
+	if (sqfs_writer_init(&sqfs, &cfg))
+		return EXIT_FAILURE;
+
+	if (process_tar_ball())
+		goto out;
+
+	if (fstree_post_process(&sqfs.fs))
+		goto out;
+
+	if (sqfs_writer_finish(&sqfs, &cfg))
+		goto out;
+
+	status = EXIT_SUCCESS;
+out:
+	sqfs_writer_cleanup(&sqfs, status);
+	return status;
+}
-- 
cgit v1.2.3