From cdccc69c62579b0c13b35fad0728079652b8f3c9 Mon Sep 17 00:00:00 2001
From: David Oberhollenzer <david.oberhollenzer@sigma-star.at>
Date: Tue, 31 Jan 2023 11:21:30 +0100
Subject: Move library source into src sub-directory

Signed-off-by: David Oberhollenzer <david.oberhollenzer@sigma-star.at>
---
 bin/gensquashfs/Makemodule.am          |  13 +-
 bin/gensquashfs/dirscan_xattr.c        | 220 ------------
 bin/gensquashfs/filemap_xattr.c        | 254 --------------
 bin/gensquashfs/fstree_from_dir.c      | 493 ---------------------------
 bin/gensquashfs/fstree_from_file.c     | 591 ---------------------------------
 bin/gensquashfs/mkfs.c                 | 215 ------------
 bin/gensquashfs/mkfs.h                 | 137 --------
 bin/gensquashfs/options.c              | 383 ---------------------
 bin/gensquashfs/selinux.c              |  78 -----
 bin/gensquashfs/sort_by_file.c         | 368 --------------------
 bin/gensquashfs/src/dirscan_xattr.c    | 220 ++++++++++++
 bin/gensquashfs/src/filemap_xattr.c    | 254 ++++++++++++++
 bin/gensquashfs/src/fstree_from_dir.c  | 493 +++++++++++++++++++++++++++
 bin/gensquashfs/src/fstree_from_file.c | 591 +++++++++++++++++++++++++++++++++
 bin/gensquashfs/src/mkfs.c             | 215 ++++++++++++
 bin/gensquashfs/src/mkfs.h             | 137 ++++++++
 bin/gensquashfs/src/options.c          | 383 +++++++++++++++++++++
 bin/gensquashfs/src/selinux.c          |  78 +++++
 bin/gensquashfs/src/sort_by_file.c     | 368 ++++++++++++++++++++
 bin/rdsquashfs/Makemodule.am           |  10 +-
 bin/rdsquashfs/describe.c              | 139 --------
 bin/rdsquashfs/dump_xattrs.c           | 120 -------
 bin/rdsquashfs/fill_files.c            | 186 -----------
 bin/rdsquashfs/list_files.c            | 158 ---------
 bin/rdsquashfs/options.c               | 226 -------------
 bin/rdsquashfs/rdsquashfs.c            | 275 ---------------
 bin/rdsquashfs/rdsquashfs.h            |  82 -----
 bin/rdsquashfs/restore_fstree.c        | 336 -------------------
 bin/rdsquashfs/src/describe.c          | 139 ++++++++
 bin/rdsquashfs/src/dump_xattrs.c       | 120 +++++++
 bin/rdsquashfs/src/fill_files.c        | 186 +++++++++++
 bin/rdsquashfs/src/list_files.c        | 158 +++++++++
 bin/rdsquashfs/src/options.c           | 226 +++++++++++++
 bin/rdsquashfs/src/rdsquashfs.c        | 275 +++++++++++++++
 bin/rdsquashfs/src/rdsquashfs.h        |  82 +++++
 bin/rdsquashfs/src/restore_fstree.c    | 336 +++++++++++++++++++
 bin/rdsquashfs/src/stat.c              | 187 +++++++++++
 bin/rdsquashfs/stat.c                  | 187 -----------
 bin/sqfs2tar/Makemodule.am             |   6 +-
 bin/sqfs2tar/options.c                 | 212 ------------
 bin/sqfs2tar/sqfs2tar.c                | 274 ---------------
 bin/sqfs2tar/sqfs2tar.h                |  56 ----
 bin/sqfs2tar/src/options.c             | 212 ++++++++++++
 bin/sqfs2tar/src/sqfs2tar.c            | 274 +++++++++++++++
 bin/sqfs2tar/src/sqfs2tar.h            |  56 ++++
 bin/sqfs2tar/src/write_tree.c          | 209 ++++++++++++
 bin/sqfs2tar/src/xattr.c               |  91 +++++
 bin/sqfs2tar/write_tree.c              | 209 ------------
 bin/sqfs2tar/xattr.c                   |  91 -----
 bin/sqfsdiff/Makemodule.am             |  10 +-
 bin/sqfsdiff/compare_dir.c             |  94 ------
 bin/sqfsdiff/compare_files.c           |  72 ----
 bin/sqfsdiff/extract.c                 |  58 ----
 bin/sqfsdiff/node_compare.c            | 206 ------------
 bin/sqfsdiff/options.c                 | 131 --------
 bin/sqfsdiff/sqfsdiff.c                | 167 ----------
 bin/sqfsdiff/sqfsdiff.h                |  73 ----
 bin/sqfsdiff/src/compare_dir.c         |  94 ++++++
 bin/sqfsdiff/src/compare_files.c       |  72 ++++
 bin/sqfsdiff/src/extract.c             |  58 ++++
 bin/sqfsdiff/src/node_compare.c        | 206 ++++++++++++
 bin/sqfsdiff/src/options.c             | 131 ++++++++
 bin/sqfsdiff/src/sqfsdiff.c            | 167 ++++++++++
 bin/sqfsdiff/src/sqfsdiff.h            |  73 ++++
 bin/sqfsdiff/src/super.c               | 125 +++++++
 bin/sqfsdiff/src/util.c                |  27 ++
 bin/sqfsdiff/super.c                   | 125 -------
 bin/sqfsdiff/util.c                    |  27 --
 bin/tar2sqfs/Makemodule.am             |   4 +-
 bin/tar2sqfs/options.c                 | 257 --------------
 bin/tar2sqfs/process_tarball.c         | 346 -------------------
 bin/tar2sqfs/src/options.c             | 257 ++++++++++++++
 bin/tar2sqfs/src/process_tarball.c     | 346 +++++++++++++++++++
 bin/tar2sqfs/src/tar2sqfs.c            | 104 ++++++
 bin/tar2sqfs/src/tar2sqfs.h            |  39 +++
 bin/tar2sqfs/tar2sqfs.c                | 104 ------
 bin/tar2sqfs/tar2sqfs.h                |  39 ---
 77 files changed, 7010 insertions(+), 7011 deletions(-)
 delete mode 100644 bin/gensquashfs/dirscan_xattr.c
 delete mode 100644 bin/gensquashfs/filemap_xattr.c
 delete mode 100644 bin/gensquashfs/fstree_from_dir.c
 delete mode 100644 bin/gensquashfs/fstree_from_file.c
 delete mode 100644 bin/gensquashfs/mkfs.c
 delete mode 100644 bin/gensquashfs/mkfs.h
 delete mode 100644 bin/gensquashfs/options.c
 delete mode 100644 bin/gensquashfs/selinux.c
 delete mode 100644 bin/gensquashfs/sort_by_file.c
 create mode 100644 bin/gensquashfs/src/dirscan_xattr.c
 create mode 100644 bin/gensquashfs/src/filemap_xattr.c
 create mode 100644 bin/gensquashfs/src/fstree_from_dir.c
 create mode 100644 bin/gensquashfs/src/fstree_from_file.c
 create mode 100644 bin/gensquashfs/src/mkfs.c
 create mode 100644 bin/gensquashfs/src/mkfs.h
 create mode 100644 bin/gensquashfs/src/options.c
 create mode 100644 bin/gensquashfs/src/selinux.c
 create mode 100644 bin/gensquashfs/src/sort_by_file.c
 delete mode 100644 bin/rdsquashfs/describe.c
 delete mode 100644 bin/rdsquashfs/dump_xattrs.c
 delete mode 100644 bin/rdsquashfs/fill_files.c
 delete mode 100644 bin/rdsquashfs/list_files.c
 delete mode 100644 bin/rdsquashfs/options.c
 delete mode 100644 bin/rdsquashfs/rdsquashfs.c
 delete mode 100644 bin/rdsquashfs/rdsquashfs.h
 delete mode 100644 bin/rdsquashfs/restore_fstree.c
 create mode 100644 bin/rdsquashfs/src/describe.c
 create mode 100644 bin/rdsquashfs/src/dump_xattrs.c
 create mode 100644 bin/rdsquashfs/src/fill_files.c
 create mode 100644 bin/rdsquashfs/src/list_files.c
 create mode 100644 bin/rdsquashfs/src/options.c
 create mode 100644 bin/rdsquashfs/src/rdsquashfs.c
 create mode 100644 bin/rdsquashfs/src/rdsquashfs.h
 create mode 100644 bin/rdsquashfs/src/restore_fstree.c
 create mode 100644 bin/rdsquashfs/src/stat.c
 delete mode 100644 bin/rdsquashfs/stat.c
 delete mode 100644 bin/sqfs2tar/options.c
 delete mode 100644 bin/sqfs2tar/sqfs2tar.c
 delete mode 100644 bin/sqfs2tar/sqfs2tar.h
 create mode 100644 bin/sqfs2tar/src/options.c
 create mode 100644 bin/sqfs2tar/src/sqfs2tar.c
 create mode 100644 bin/sqfs2tar/src/sqfs2tar.h
 create mode 100644 bin/sqfs2tar/src/write_tree.c
 create mode 100644 bin/sqfs2tar/src/xattr.c
 delete mode 100644 bin/sqfs2tar/write_tree.c
 delete mode 100644 bin/sqfs2tar/xattr.c
 delete mode 100644 bin/sqfsdiff/compare_dir.c
 delete mode 100644 bin/sqfsdiff/compare_files.c
 delete mode 100644 bin/sqfsdiff/extract.c
 delete mode 100644 bin/sqfsdiff/node_compare.c
 delete mode 100644 bin/sqfsdiff/options.c
 delete mode 100644 bin/sqfsdiff/sqfsdiff.c
 delete mode 100644 bin/sqfsdiff/sqfsdiff.h
 create mode 100644 bin/sqfsdiff/src/compare_dir.c
 create mode 100644 bin/sqfsdiff/src/compare_files.c
 create mode 100644 bin/sqfsdiff/src/extract.c
 create mode 100644 bin/sqfsdiff/src/node_compare.c
 create mode 100644 bin/sqfsdiff/src/options.c
 create mode 100644 bin/sqfsdiff/src/sqfsdiff.c
 create mode 100644 bin/sqfsdiff/src/sqfsdiff.h
 create mode 100644 bin/sqfsdiff/src/super.c
 create mode 100644 bin/sqfsdiff/src/util.c
 delete mode 100644 bin/sqfsdiff/super.c
 delete mode 100644 bin/sqfsdiff/util.c
 delete mode 100644 bin/tar2sqfs/options.c
 delete mode 100644 bin/tar2sqfs/process_tarball.c
 create mode 100644 bin/tar2sqfs/src/options.c
 create mode 100644 bin/tar2sqfs/src/process_tarball.c
 create mode 100644 bin/tar2sqfs/src/tar2sqfs.c
 create mode 100644 bin/tar2sqfs/src/tar2sqfs.h
 delete mode 100644 bin/tar2sqfs/tar2sqfs.c
 delete mode 100644 bin/tar2sqfs/tar2sqfs.h

(limited to 'bin')

diff --git a/bin/gensquashfs/Makemodule.am b/bin/gensquashfs/Makemodule.am
index c6a98a2..7edc39a 100644
--- a/bin/gensquashfs/Makemodule.am
+++ b/bin/gensquashfs/Makemodule.am
@@ -1,10 +1,9 @@
-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_xattr.c
-gensquashfs_SOURCES += bin/gensquashfs/filemap_xattr.c
-gensquashfs_SOURCES += bin/gensquashfs/fstree_from_file.c
-gensquashfs_SOURCES += bin/gensquashfs/fstree_from_dir.c
-gensquashfs_SOURCES += bin/gensquashfs/sort_by_file.c
+gensquashfs_SOURCES = bin/gensquashfs/src/mkfs.c bin/gensquashfs/src/mkfs.h \
+	bin/gensquashfs/src/options.c bin/gensquashfs/src/selinux.c \
+	bin/gensquashfs/src/dirscan_xattr.c bin/gensquashfs/src/filemap_xattr.c\
+	bin/gensquashfs/src/fstree_from_file.c \
+	bin/gensquashfs/src/fstree_from_dir.c \
+	bin/gensquashfs/src/sort_by_file.c
 gensquashfs_LDADD = libcommon.a libsquashfs.la libfstree.a libio.a
 gensquashfs_LDADD += libutil.a libcompat.a $(LZO_LIBS) $(PTHREAD_LIBS)
 gensquashfs_CPPFLAGS = $(AM_CPPFLAGS)
diff --git a/bin/gensquashfs/dirscan_xattr.c b/bin/gensquashfs/dirscan_xattr.c
deleted file mode 100644
index 7d4e552..0000000
--- a/bin/gensquashfs/dirscan_xattr.c
+++ /dev/null
@@ -1,220 +0,0 @@
-/* SPDX-License-Identifier: GPL-3.0-or-later */
-/*
- * dirscan_xattr.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
-
-static int xattr_xcan_dfs(const char *path_prefix, void *selinux_handle,
-			  sqfs_xattr_writer_t *xwr, bool scan_xattr, void *xattr_map,
-			  tree_node_t *node)
-{
-	char *path = NULL;
-	int ret;
-
-	ret = sqfs_xattr_writer_begin(xwr, 0);
-	if (ret) {
-		sqfs_perror(node->name, "recoding xattr key-value pairs\n",
-			    ret);
-		return -1;
-	}
-
-#ifdef HAVE_SYS_XATTR_H
-	if (scan_xattr) {
-		path = get_full_path(path_prefix, node);
-		if (path == NULL)
-			return -1;
-
-		ret = xattr_from_path(xwr, path);
-		free(path);
-		path = NULL;
-		if (ret) {
-			ret = -1;
-			goto out;
-		}
-	}
-#else
-	(void)path_prefix;
-#endif
-
-	if (selinux_handle != NULL || xattr_map != NULL) {
-		path = fstree_get_path(node);
-
-		if (path == NULL) {
-			perror("reconstructing absolute path");
-			ret = -1;
-			goto out;
-		}
-	}
-
-	if (xattr_map != NULL) {
-		ret = xattr_apply_map_file(path, xattr_map, xwr);
-
-		if (ret) {
-			ret = -1;
-			goto out;
-		}
-	}
-
-	if (selinux_handle != NULL) {
-		ret = selinux_relable_node(selinux_handle, xwr, node, path);
-
-		if (ret) {
-			ret = -1;
-			goto out;
-		}
-	}
-
-	if (sqfs_xattr_writer_end(xwr, &node->xattr_idx)) {
-		sqfs_perror(node->name, "completing xattr key-value pairs",
-			    ret);
-		ret = -1;
-		goto out;
-	}
-
-	if (S_ISDIR(node->mode)) {
-		node = node->data.dir.children;
-
-		while (node != NULL) {
-			if (xattr_xcan_dfs(path_prefix, selinux_handle, xwr,
-					   scan_xattr, xattr_map, node)) {
-				ret = -1;
-				goto out;
-			}
-
-			node = node->next;
-		}
-	}
-
-out:
-	free(path);
-	return ret;
-}
-
-int xattrs_from_dir(fstree_t *fs, const char *path, void *selinux_handle,
-		    void *xattr_map, sqfs_xattr_writer_t *xwr, bool scan_xattr)
-{
-	if (xwr == NULL)
-		return 0;
-
-	if (selinux_handle == NULL && !scan_xattr && xattr_map == NULL)
-		return 0;
-
-	return xattr_xcan_dfs(path, selinux_handle, xwr, scan_xattr, xattr_map, fs->root);
-}
diff --git a/bin/gensquashfs/filemap_xattr.c b/bin/gensquashfs/filemap_xattr.c
deleted file mode 100644
index dd76b50..0000000
--- a/bin/gensquashfs/filemap_xattr.c
+++ /dev/null
@@ -1,254 +0,0 @@
-/* SPDX-License-Identifier: GPL-3.0-or-later */
-/*
- * filemap_xattr.c
- *
- * Copyright (C) 2022 Enno Boland <mail@eboland.de>
- */
-#include "fstree.h"
-#include "mkfs.h"
-#include <stdio.h>
-
-#define NEW_FILE_START "# file: "
-
-static void print_error(const char *filename, size_t line_num, const char *err)
-{
-	fprintf(stderr, "%s: " PRI_SZ ": %s\n", filename, line_num, err);
-}
-
-// Taken from attr-2.5.1/tools/setfattr.c
-static sqfs_u8 *decode(const char *filename, size_t line_num,
-		       const char *value, size_t *size)
-{
-	sqfs_u8 *decoded = NULL;
-
-	if (*size == 0) {
-		decoded = (sqfs_u8 *)strdup("");
-		if (decoded == NULL)
-			goto fail_alloc;
-		return decoded;
-	}
-
-	if (value[0] == '0' && (value[1] == 'x' || value[1] == 'X')) {
-		*size = ((*size) - 2) / 2;
-
-		decoded = calloc(1, (*size) + 1);
-		if (decoded == NULL)
-			goto fail_alloc;
-
-		if (hex_decode(value + 2, (*size) * 2, decoded, *size))
-			goto fail_encode;
-	} else if (value[0] == '0' && (value[1] == 's' || value[1] == 'S')) {
-		size_t input_len = *size - 2;
-
-		*size = (input_len / 4) * 3;
-
-		decoded = calloc(1, (*size) + 1);
-		if (decoded == NULL)
-			goto fail_alloc;
-
-		if (base64_decode(value + 2, input_len, decoded, size))
-			goto fail_encode;
-	} else {
-		const char *v = value, *end = value + *size;
-		sqfs_u8 *d;
-
-		if (end > v + 1 && *v == '"' && *(end - 1) == '"') {
-			v++;
-			end--;
-		}
-
-		decoded = calloc(1, (*size) + 1);
-		if (decoded == NULL)
-			goto fail_alloc;
-
-		d = decoded;
-
-		while (v < end) {
-			if (v[0] == '\\') {
-				if (v[1] == '\\' || v[1] == '"') {
-					*d++ = *++v;
-					v++;
-				} else if (v[1] >= '0' && v[1] <= '7') {
-					int c = 0;
-					v++;
-					c = (*v++ - '0');
-					if (*v >= '0' && *v <= '7')
-						c = (c << 3) + (*v++ - '0');
-					if (*v >= '0' && *v <= '7')
-						c = (c << 3) + (*v++ - '0');
-					*d++ = c;
-				} else
-					*d++ = *v++;
-			} else
-				*d++ = *v++;
-		}
-		*size = d - decoded;
-	}
-	return decoded;
-fail_alloc:
-	fprintf(stderr, "out of memory\n");
-	return NULL;
-fail_encode:
-	print_error(filename, line_num, "bad input encoding");
-	free(decoded);
-	return NULL;
-}
-
-static int parse_file_name(const char *filename, size_t line_num,
-			   char *line, struct XattrMap *map)
-{
-	struct XattrMapPattern *current_file;
-	char *file_name = strdup(line + strlen(NEW_FILE_START));
-
-	if (file_name == NULL)
-		goto fail_alloc;
-
-	current_file = calloc(1, sizeof(struct XattrMapPattern));
-	if (current_file == NULL)
-		goto fail_alloc;
-
-	current_file->next = map->patterns;
-	map->patterns = current_file;
-
-	if (canonicalize_name(file_name)) {
-		print_error(filename, line_num, "invalid absolute path");
-		free(current_file);
-		free(file_name);
-		return -1;
-	}
-
-	current_file->path = file_name;
-	return 0;
-fail_alloc:
-	fprintf(stderr, "out of memory\n");
-	free(file_name);
-	return -1;
-}
-
-static int parse_xattr(const char *filename, size_t line_num, char *key_start,
-		       char *value_start, struct XattrMap *map)
-{
-	size_t len;
-	struct XattrMapPattern *current_pattern = map->patterns;
-	struct XattrMapEntry *current_entry;
-
-	if (current_pattern == NULL) {
-		print_error(filename, line_num, "no file specified yet");
-		return -1;
-	}
-
-	current_entry = calloc(1, sizeof(struct XattrMapEntry));
-	if (current_entry == NULL) {
-		return -1;
-	}
-	current_entry->next = current_pattern->entries;
-	current_pattern->entries = current_entry;
-
-	current_entry->key = strdup(key_start);
-	len = strlen(value_start);
-	current_entry->value = decode(filename, line_num, value_start, &len);
-	current_entry->value_len = len;
-
-	return 0;
-}
-
-void *
-xattr_open_map_file(const char *path) {
-	struct XattrMap *map;
-	size_t line_num = 1;
-	char *p = NULL;
-	istream_t *file = istream_open_file(path);
-	if (file == NULL) {
-		return NULL;
-	}
-
-	map = calloc(1, sizeof(struct XattrMap));
-	if (map == NULL)
-		goto fail_close;
-
-	for (;;) {
-		char *line = NULL;
-		int ret = istream_get_line(file, &line, &line_num,
-					   ISTREAM_LINE_LTRIM |
-					   ISTREAM_LINE_RTRIM |
-					   ISTREAM_LINE_SKIP_EMPTY);
-		if (ret < 0)
-			goto fail;
-		if (ret > 0)
-			break;
-
-		if (strncmp(NEW_FILE_START, line, strlen(NEW_FILE_START)) == 0) {
-			ret = parse_file_name(path, line_num, line, map);
-		} else if ((p = strchr(line, '='))) {
-			*(p++) = '\0';
-			ret = parse_xattr(path, line_num, line, p, map);
-		} else if (line[0] != '#') {
-			print_error(path, line_num, "not a key-value pair");
-			ret = -1;
-		}
-
-		++line_num;
-		free(line);
-		if (ret < 0)
-			goto fail;
-	}
-
-	sqfs_drop(file);
-	return map;
-fail:
-	xattr_close_map_file(map);
-fail_close:
-	sqfs_drop(file);
-	return NULL;
-}
-
-void
-xattr_close_map_file(void *xattr_map) {
-	struct XattrMap *map = xattr_map;
-	while (map->patterns != NULL) {
-		struct XattrMapPattern *file = map->patterns;
-		map->patterns = file->next;
-		while (file->entries != NULL) {
-			struct XattrMapEntry *entry = file->entries;
-			file->entries = entry->next;
-			free(entry->key);
-			free(entry->value);
-			free(entry);
-		}
-		free(file->path);
-		free(file);
-	}
-	free(xattr_map);
-}
-
-int
-xattr_apply_map_file(char *path, void *map, sqfs_xattr_writer_t *xwr) {
-	struct XattrMap *xattr_map = map;
-	int ret = 0;
-	const struct XattrMapPattern *pat;
-	const struct XattrMapEntry *entry;
-
-	for (pat = xattr_map->patterns; pat != NULL; pat = pat->next) {
-		char *patstr = pat->path;
-		const char *stripped = path;
-
-		if (patstr[0] != '/' && stripped[0] == '/') {
-			stripped++;
-		}
-
-		if (strcmp(patstr, stripped) == 0) {
-			printf("Applying xattrs for %s", path);
-			for (entry = pat->entries; entry != NULL; entry = entry->next) {
-				printf("  %s = \n", entry->key);
-				fwrite(entry->value, entry->value_len, 1, stdout);
-				puts("\n");
-				ret = sqfs_xattr_writer_add(
-						xwr, entry->key, entry->value, entry->value_len);
-				if (ret < 0) {
-					return ret;
-				}
-			}
-		}
-	}
-	return ret;
-}
diff --git a/bin/gensquashfs/fstree_from_dir.c b/bin/gensquashfs/fstree_from_dir.c
deleted file mode 100644
index 5b3f003..0000000
--- a/bin/gensquashfs/fstree_from_dir.c
+++ /dev/null
@@ -1,493 +0,0 @@
-/* SPDX-License-Identifier: GPL-3.0-or-later */
-/*
- * fstree_from_dir.c
- *
- * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
- */
-#include "config.h"
-#include "mkfs.h"
-
-#include <dirent.h>
-#include <stdlib.h>
-#include <string.h>
-#include <errno.h>
-
-#if defined(_WIN32) || defined(__WINDOWS__)
-#define UNIX_EPOCH_ON_W32 11644473600UL
-#define W32_TICS_PER_SEC 10000000UL
-
-static sqfs_u32 w32time_to_sqfs_time(const FILETIME *ft)
-{
-	sqfs_u64 w32ts;
-
-	w32ts = ft->dwHighDateTime;
-	w32ts <<= 32UL;
-	w32ts |= ft->dwLowDateTime;
-
-	w32ts /= W32_TICS_PER_SEC;
-
-	if (w32ts <= UNIX_EPOCH_ON_W32)
-		return 0;
-
-	w32ts -= UNIX_EPOCH_ON_W32;
-
-	return (w32ts < 0x0FFFFFFFFUL) ? w32ts : 0xFFFFFFFF;
-}
-
-static int add_node(fstree_t *fs, tree_node_t *root,
-		    scan_node_callback cb, void *user,
-		    unsigned int flags,
-		    const LPWIN32_FIND_DATAW entry)
-{
-	tree_node_t *n;
-	DWORD length;
-
-	if (entry->cFileName[0] == '.') {
-		if (entry->cFileName[1] == '\0')
-			return 0;
-
-		if (entry->cFileName[1] == '.' && entry->cFileName[2] == '\0')
-			return 0;
-	}
-
-	length = WideCharToMultiByte(CP_UTF8, 0, entry->cFileName,
-				     -1, NULL, 0, NULL, NULL);
-	if (length <= 0) {
-		w32_perror("converting path to UTF-8");
-		return -1;
-	}
-
-	n = calloc(1, sizeof(*n) + length + 1);
-	if (n == NULL) {
-		fprintf(stderr, "creating tree node: out-of-memory\n");
-		return -1;
-	}
-
-	n->name = (char *)n->payload;
-	WideCharToMultiByte(CP_UTF8, 0, entry->cFileName, -1,
-			    n->name, length + 1, NULL, NULL);
-
-	if (entry->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
-		if (flags & DIR_SCAN_NO_DIR) {
-			free(n);
-			return 0;
-		}
-
-		n->mode = S_IFDIR | 0755;
-	} else {
-		if (flags & DIR_SCAN_NO_FILE) {
-			free(n);
-			return 0;
-		}
-
-		n->mode = S_IFREG | 0644;
-	}
-
-	if (cb != NULL) {
-		int ret = cb(user, fs, n);
-
-		if (ret != 0) {
-			free(n);
-			return ret < 0 ? ret : 0;
-		}
-	}
-
-	if (flags & DIR_SCAN_KEEP_TIME) {
-		n->mod_time = w32time_to_sqfs_time(&(entry->ftLastWriteTime));
-	} else {
-		n->mod_time = fs->defaults.st_mtime;
-	}
-
-	fstree_insert_sorted(root, n);
-	return 0;
-}
-
-static int scan_dir(fstree_t *fs, tree_node_t *root,
-		    const char *path, const WCHAR *wpath,
-		    scan_node_callback cb, void *user,
-		    unsigned int flags)
-{
-	WIN32_FIND_DATAW entry;
-	HANDLE dirhnd;
-
-	dirhnd = FindFirstFileW(wpath, &entry);
-
-	if (dirhnd == INVALID_HANDLE_VALUE)
-		goto fail_perror;
-
-	do {
-		if (add_node(fs, root, cb, user, flags, &entry))
-			goto fail;
-	} while (FindNextFileW(dirhnd, &entry));
-
-	if (GetLastError() != ERROR_NO_MORE_FILES)
-		goto fail_perror;
-
-	FindClose(dirhnd);
-	return 0;
-fail_perror:
-	w32_perror(path);
-fail:
-	if (dirhnd != INVALID_HANDLE_VALUE)
-		FindClose(dirhnd);
-	return -1;
-}
-
-int fstree_from_dir(fstree_t *fs, tree_node_t *root,
-		    const char *path, scan_node_callback cb,
-		    void *user, unsigned int flags)
-{
-	WCHAR *wpath = NULL, *new = NULL;
-	size_t len, newlen;
-	tree_node_t *n;
-
-	/* path -> to_wchar(path) + L"\*" */
-	wpath = path_to_windows(path);
-	if (wpath == NULL) {
-		fprintf(stderr, "%s: allocation failure.\n", path);
-		return -1;
-	}
-
-	for (len = 0; wpath[len] != '\0'; ++len)
-		;
-
-	newlen = len + 1;
-
-	if (len > 0 && wpath[len - 1] != '\\')
-		newlen += 1;
-
-	new = realloc(wpath, sizeof(wpath[0]) * (newlen + 1));
-	if (new == NULL) {
-		fprintf(stderr, "%s: allocation failure.\n", path);
-		goto fail;
-	}
-
-	wpath = new;
-
-	if (len > 0 && wpath[len - 1] != '\\')
-		wpath[len++] = '\\';
-
-	wpath[len++] = '*';
-	wpath[len++] = '\0';
-
-	/* scan directory contents */
-	if (scan_dir(fs, root, path, wpath, cb, user, flags))
-		goto fail;
-
-	free(wpath);
-	wpath = NULL;
-
-	/* recursion step */
-	if (flags & DIR_SCAN_NO_RECURSION)
-		return 0;
-
-	for (n = root->data.dir.children; n != NULL; n = n->next) {
-		if (!S_ISDIR(n->mode))
-			continue;
-
-		if (fstree_from_subdir(fs, n, path, n->name, cb, user, flags))
-			return -1;
-	}
-
-	return 0;
-fail:
-	free(wpath);
-	return -1;
-}
-
-int fstree_from_subdir(fstree_t *fs, tree_node_t *root,
-		       const char *path, const char *subdir,
-		       scan_node_callback cb, void *user,
-		       unsigned int flags)
-{
-	size_t len, plen, slen;
-	WCHAR *wpath = NULL;
-	char *temp = NULL;
-	tree_node_t *n;
-
-	plen = strlen(path);
-	slen = subdir == NULL ? 0 : strlen(subdir);
-
-	if (slen == 0)
-		return fstree_from_dir(fs, root, path, cb, user, flags);
-
-	len = plen + 1 + slen + 2;
-
-	temp = calloc(1, len + 1);
-	if (temp == NULL) {
-		fprintf(stderr, "%s/%s: allocation failure.\n", path, subdir);
-		return -1;
-	}
-
-	memcpy(temp, path, plen);
-	temp[plen] = '/';
-	memcpy(temp + plen + 1, subdir, slen);
-	temp[plen + 1 + slen    ] = '/';
-	temp[plen + 1 + slen + 1] = '*';
-	temp[plen + 1 + slen + 2] = '\0';
-
-	wpath = path_to_windows(temp);
-	if (wpath == NULL) {
-		fprintf(stderr, "%s: allocation failure.\n", temp);
-		goto fail;
-	}
-
-	if (scan_dir(fs, root, temp, wpath, cb, user, flags))
-		goto fail;
-
-	free(wpath);
-	wpath = NULL;
-
-	if (flags & DIR_SCAN_NO_RECURSION) {
-		free(temp);
-		return 0;
-	}
-
-	temp[plen + 1 + slen] = '\0';
-
-	for (n = root->data.dir.children; n != NULL; n = n->next) {
-		if (!S_ISDIR(n->mode))
-			continue;
-
-		if (fstree_from_subdir(fs, n, temp, n->name, cb, user, flags))
-			goto fail;
-	}
-
-	free(temp);
-	return 0;
-fail:
-	free(temp);
-	free(wpath);
-	return -1;
-
-}
-#else
-static void discard_node(tree_node_t *root, tree_node_t *n)
-{
-	tree_node_t *it;
-
-	if (n == root->data.dir.children) {
-		root->data.dir.children = n->next;
-	} else {
-		it = root->data.dir.children;
-
-		while (it != NULL && it->next != n)
-			it = it->next;
-
-		if (it != NULL)
-			it->next = n->next;
-	}
-
-	free(n);
-}
-
-static int populate_dir(int dir_fd, fstree_t *fs, tree_node_t *root,
-			dev_t devstart, scan_node_callback cb,
-			void *user, unsigned int flags)
-{
-	char *extra = NULL;
-	struct dirent *ent;
-	int ret, childfd;
-	struct stat sb;
-	tree_node_t *n;
-	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;
-		}
-
-		switch (sb.st_mode & S_IFMT) {
-		case S_IFSOCK:
-			if (flags & DIR_SCAN_NO_SOCK)
-				continue;
-			break;
-		case S_IFLNK:
-			if (flags & DIR_SCAN_NO_SLINK)
-				continue;
-			break;
-		case S_IFREG:
-			if (flags & DIR_SCAN_NO_FILE)
-				continue;
-			break;
-		case S_IFBLK:
-			if (flags & DIR_SCAN_NO_BLK)
-				continue;
-			break;
-		case S_IFCHR:
-			if (flags & DIR_SCAN_NO_CHR)
-				continue;
-			break;
-		case S_IFIFO:
-			if (flags & DIR_SCAN_NO_FIFO)
-				continue;
-			break;
-		default:
-			break;
-		}
-
-		if ((flags & DIR_SCAN_ONE_FILESYSTEM) && sb.st_dev != devstart)
-			continue;
-
-		if (S_ISLNK(sb.st_mode)) {
-			size_t size;
-
-			if ((sizeof(sb.st_size) > sizeof(size_t)) &&
-			    sb.st_size > SIZE_MAX) {
-				errno = EOVERFLOW;
-				goto fail_rdlink;
-			}
-
-			if (SZ_ADD_OV((size_t)sb.st_size, 1, &size)) {
-				errno = EOVERFLOW;
-				goto fail_rdlink;
-			}
-
-			extra = calloc(1, size);
-			if (extra == NULL)
-				goto fail_rdlink;
-
-			if (readlinkat(dir_fd, ent->d_name,
-				       extra, (size_t)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;
-
-		if (S_ISDIR(sb.st_mode) && (flags & DIR_SCAN_NO_DIR)) {
-			n = fstree_get_node_by_path(fs, root, ent->d_name,
-						    false, false);
-			if (n == NULL)
-				continue;
-
-			ret = 0;
-		} else {
-			n = fstree_mknode(root, ent->d_name,
-					  strlen(ent->d_name), extra, &sb);
-			if (n == NULL) {
-				perror("creating tree node");
-				goto fail;
-			}
-
-			ret = (cb == NULL) ? 0 : cb(user, fs, n);
-		}
-
-		free(extra);
-		extra = NULL;
-
-		if (ret < 0)
-			goto fail;
-
-		if (ret > 0) {
-			discard_node(root, n);
-			continue;
-		}
-
-		if (S_ISDIR(n->mode) && !(flags & DIR_SCAN_NO_RECURSION)) {
-			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,
-					 cb, user, flags)) {
-				goto fail;
-			}
-		}
-	}
-
-	closedir(dir);
-	return 0;
-fail_rdlink:
-	perror("readlink");
-fail:
-	closedir(dir);
-	free(extra);
-	return -1;
-}
-
-int fstree_from_subdir(fstree_t *fs, tree_node_t *root,
-		       const char *path, const char *subdir,
-		       scan_node_callback cb, void *user,
-		       unsigned int flags)
-{
-	struct stat sb;
-	int fd, subfd;
-
-	if (!S_ISDIR(root->mode)) {
-		fprintf(stderr,
-			"scanning %s/%s into %s: target is not a directory\n",
-			path, subdir == NULL ? "" : subdir, root->name);
-		return -1;
-	}
-
-	fd = open(path, O_DIRECTORY | O_RDONLY | O_CLOEXEC);
-	if (fd < 0) {
-		perror(path);
-		return -1;
-	}
-
-	if (subdir != NULL) {
-		subfd = openat(fd, subdir, O_DIRECTORY | O_RDONLY | O_CLOEXEC);
-
-		if (subfd < 0) {
-			fprintf(stderr, "%s/%s: %s\n", path, subdir,
-				strerror(errno));
-			close(fd);
-			return -1;
-		}
-
-		close(fd);
-		fd = subfd;
-	}
-
-	if (fstat(fd, &sb)) {
-		fprintf(stderr, "%s/%s: %s\n", path,
-			subdir == NULL ? "" : subdir,
-			strerror(errno));
-		close(fd);
-		return -1;
-	}
-
-	return populate_dir(fd, fs, root, sb.st_dev, cb, user, flags);
-}
-
-int fstree_from_dir(fstree_t *fs, tree_node_t *root,
-		    const char *path, scan_node_callback cb,
-		    void *user, unsigned int flags)
-{
-	return fstree_from_subdir(fs, root, path, NULL, cb, user, flags);
-}
-#endif
diff --git a/bin/gensquashfs/fstree_from_file.c b/bin/gensquashfs/fstree_from_file.c
deleted file mode 100644
index e26d4b1..0000000
--- a/bin/gensquashfs/fstree_from_file.c
+++ /dev/null
@@ -1,591 +0,0 @@
-/* SPDX-License-Identifier: GPL-3.0-or-later */
-/*
- * fstree_from_file.c
- *
- * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
- */
-#include "config.h"
-
-#include "util/util.h"
-#include "io/file.h"
-#include "compat.h"
-#include "mkfs.h"
-
-#include <stdlib.h>
-#include <string.h>
-#include <assert.h>
-#include <errno.h>
-#include <ctype.h>
-
-struct glob_context {
-	const char *filename;
-	size_t line_num;
-
-	struct stat *basic;
-	unsigned int glob_flags;
-
-	char *name_pattern;
-};
-
-enum {
-	GLOB_MODE_FROM_SRC = 0x01,
-	GLOB_UID_FROM_SRC = 0x02,
-	GLOB_GID_FROM_SRC = 0x04,
-	GLOB_FLAG_PATH = 0x08,
-};
-
-static const struct {
-	const char *name;
-	unsigned int clear_flag;
-	unsigned int set_flag;
-} glob_scan_flags[] = {
-	{ "-type b", DIR_SCAN_NO_BLK, 0 },
-	{ "-type c", DIR_SCAN_NO_CHR, 0 },
-	{ "-type d", DIR_SCAN_NO_DIR, 0 },
-	{ "-type p", DIR_SCAN_NO_FIFO, 0 },
-	{ "-type f", DIR_SCAN_NO_FILE, 0 },
-	{ "-type l", DIR_SCAN_NO_SLINK, 0 },
-	{ "-type s", DIR_SCAN_NO_SOCK, 0 },
-	{ "-xdev", 0, DIR_SCAN_ONE_FILESYSTEM },
-	{ "-mount", 0, DIR_SCAN_ONE_FILESYSTEM },
-	{ "-keeptime", 0, DIR_SCAN_KEEP_TIME },
-	{ "-nonrecursive", 0, DIR_SCAN_NO_RECURSION },
-};
-
-static int add_generic(fstree_t *fs, const char *filename, size_t line_num,
-		       const char *path, struct stat *sb,
-		       const char *basepath, unsigned int glob_flags,
-		       const char *extra)
-{
-	(void)basepath;
-	(void)glob_flags;
-
-	if (fstree_add_generic(fs, path, sb, extra) == NULL) {
-		fprintf(stderr, "%s: " PRI_SZ ": %s: %s\n",
-			filename, line_num, path, strerror(errno));
-		return -1;
-	}
-
-	return 0;
-}
-
-static int add_device(fstree_t *fs, const char *filename, size_t line_num,
-		      const char *path, struct stat *sb, const char *basepath,
-		      unsigned int glob_flags, const char *extra)
-{
-	unsigned int maj, min;
-	char c;
-
-	if (sscanf(extra, "%c %u %u", &c, &maj, &min) != 3) {
-		fprintf(stderr, "%s: " PRI_SZ ": "
-			"expected '<c|b> major minor'\n",
-			filename, line_num);
-		return -1;
-	}
-
-	if (c == 'c' || c == 'C') {
-		sb->st_mode |= S_IFCHR;
-	} else if (c == 'b' || c == 'B') {
-		sb->st_mode |= S_IFBLK;
-	} else {
-		fprintf(stderr, "%s: " PRI_SZ ": unknown device type '%c'\n",
-			filename, line_num, c);
-		return -1;
-	}
-
-	sb->st_rdev = makedev(maj, min);
-	return add_generic(fs, filename, line_num, path, sb, basepath,
-			   glob_flags, NULL);
-}
-
-static int add_file(fstree_t *fs, const char *filename, size_t line_num,
-		    const char *path, struct stat *basic, const char *basepath,
-		    unsigned int glob_flags, const char *extra)
-{
-	if (extra == NULL || *extra == '\0')
-		extra = path;
-
-	return add_generic(fs, filename, line_num, path, basic,
-			   basepath, glob_flags, extra);
-}
-
-static int add_hard_link(fstree_t *fs, const char *filename, size_t line_num,
-			 const char *path, struct stat *basic,
-			 const char *basepath, unsigned int glob_flags,
-			 const char *extra)
-{
-	(void)basepath;
-	(void)glob_flags;
-	(void)basic;
-
-	if (fstree_add_hard_link(fs, path, extra) == NULL) {
-		fprintf(stderr, "%s: " PRI_SZ ": %s\n",
-			filename, line_num, strerror(errno));
-		return -1;
-	}
-	return 0;
-}
-
-static int glob_node_callback(void *user, fstree_t *fs, tree_node_t *node)
-{
-	struct glob_context *ctx = user;
-	char *path;
-	int ret;
-	(void)fs;
-
-	if (!(ctx->glob_flags & GLOB_MODE_FROM_SRC)) {
-		node->mode &= ~(07777);
-		node->mode |= ctx->basic->st_mode & 07777;
-	}
-
-	if (!(ctx->glob_flags & GLOB_UID_FROM_SRC))
-		node->uid = ctx->basic->st_uid;
-
-	if (!(ctx->glob_flags & GLOB_GID_FROM_SRC))
-		node->gid = ctx->basic->st_gid;
-
-	if (ctx->name_pattern != NULL) {
-		if (ctx->glob_flags & GLOB_FLAG_PATH) {
-			path = fstree_get_path(node);
-			if (path == NULL) {
-				fprintf(stderr, "%s: " PRI_SZ ": %s\n",
-					ctx->filename, ctx->line_num,
-					strerror(errno));
-				return -1;
-			}
-
-			ret = canonicalize_name(path);
-			assert(ret == 0);
-
-			ret = fnmatch(ctx->name_pattern, path, FNM_PATHNAME);
-			free(path);
-		} else {
-			ret = fnmatch(ctx->name_pattern, node->name, 0);
-		}
-
-		if (ret != 0)
-			return 1;
-	}
-
-	return 0;
-}
-
-static size_t name_string_length(const char *str)
-{
-	size_t len = 0;
-	int start;
-
-	if (*str == '"' || *str == '\'') {
-		start = *str;
-		++len;
-
-		while (str[len] != '\0' && str[len] != start)
-			++len;
-
-		if (str[len] == start)
-			++len;
-	} else {
-		while (str[len] != '\0' && !isspace(str[len]))
-			++len;
-	}
-
-	return len;
-}
-
-static void quote_remove(char *str)
-{
-	char *dst = str;
-	int start = *(str++);
-
-	if (start != '\'' && start != '"')
-		return;
-
-	while (*str != start && *str != '\0')
-		*(dst++) = *(str++);
-
-	*(dst++) = '\0';
-}
-
-static int glob_files(fstree_t *fs, const char *filename, size_t line_num,
-		      const char *path, struct stat *basic,
-		      const char *basepath, unsigned int glob_flags,
-		      const char *extra)
-{
-	unsigned int scan_flags = 0, all_flags;
-	struct glob_context ctx;
-	bool first_clear_flag;
-	size_t i, count, len;
-	tree_node_t *root;
-	int ret;
-
-	memset(&ctx, 0, sizeof(ctx));
-	ctx.filename = filename;
-	ctx.line_num = line_num;
-	ctx.basic = basic;
-	ctx.glob_flags = glob_flags;
-
-	/* fetch the actual target node */
-	root = fstree_get_node_by_path(fs, fs->root, path, true, false);
-	if (root == NULL) {
-		fprintf(stderr, "%s: " PRI_SZ ": %s: %s\n",
-			filename, line_num, path, strerror(errno));
-		return -1;
-	}
-
-	/* process options */
-	first_clear_flag = true;
-
-	all_flags = DIR_SCAN_NO_BLK | DIR_SCAN_NO_CHR | DIR_SCAN_NO_DIR |
-		DIR_SCAN_NO_FIFO | DIR_SCAN_NO_FILE | DIR_SCAN_NO_SLINK |
-		DIR_SCAN_NO_SOCK;
-
-	while (extra != NULL && *extra != '\0') {
-		count = sizeof(glob_scan_flags) / sizeof(glob_scan_flags[0]);
-
-		for (i = 0; i < count; ++i) {
-			len = strlen(glob_scan_flags[i].name);
-			if (strncmp(extra, glob_scan_flags[i].name, len) != 0)
-				continue;
-
-			if (isspace(extra[len])) {
-				extra += len;
-				while (isspace(*extra))
-					++extra;
-				break;
-			}
-		}
-
-		if (i < count) {
-			if (glob_scan_flags[i].clear_flag != 0 &&
-			    first_clear_flag) {
-				scan_flags |= all_flags;
-				first_clear_flag = false;
-			}
-
-			scan_flags &= ~(glob_scan_flags[i].clear_flag);
-			scan_flags |= glob_scan_flags[i].set_flag;
-			continue;
-		}
-
-		if (strncmp(extra, "-name", 5) == 0 && isspace(extra[5])) {
-			for (extra += 5; isspace(*extra); ++extra)
-				;
-
-			len = name_string_length(extra);
-
-			free(ctx.name_pattern);
-			ctx.name_pattern = strndup(extra, len);
-			extra += len;
-
-			while (isspace(*extra))
-				++extra;
-
-			quote_remove(ctx.name_pattern);
-			continue;
-		}
-
-		if (strncmp(extra, "-path", 5) == 0 && isspace(extra[5])) {
-			for (extra += 5; isspace(*extra); ++extra)
-				;
-
-			len = name_string_length(extra);
-
-			free(ctx.name_pattern);
-			ctx.name_pattern = strndup(extra, len);
-			extra += len;
-
-			while (isspace(*extra))
-				++extra;
-
-			quote_remove(ctx.name_pattern);
-			ctx.glob_flags |= GLOB_FLAG_PATH;
-			continue;
-		}
-
-		if (extra[0] == '-') {
-			if (extra[1] == '-' && isspace(extra[2])) {
-				extra += 2;
-				while (isspace(*extra))
-					++extra;
-				break;
-			}
-
-			fprintf(stderr, "%s: " PRI_SZ ": unknown option.\n",
-				filename, line_num);
-			free(ctx.name_pattern);
-			return -1;
-		} else {
-			break;
-		}
-	}
-
-	if (extra != NULL && *extra == '\0')
-		extra = NULL;
-
-	/* do the scan */
-	if (basepath == NULL) {
-		if (extra == NULL) {
-			ret = fstree_from_dir(fs, root, ".", glob_node_callback,
-					      &ctx, scan_flags);
-		} else {
-			ret = fstree_from_dir(fs, root, extra,
-					      glob_node_callback,
-					      &ctx, scan_flags);
-		}
-	} else {
-		ret = fstree_from_subdir(fs, root, basepath, extra,
-					 glob_node_callback, &ctx,
-					 scan_flags);
-	}
-
-	free(ctx.name_pattern);
-	return ret;
-}
-
-static const struct callback_t {
-	const char *keyword;
-	unsigned int mode;
-	bool need_extra;
-	bool is_glob;
-	bool allow_root;
-	int (*callback)(fstree_t *fs, const char *filename, size_t line_num,
-			const char *path, struct stat *sb,
-			const char *basepath, unsigned int glob_flags,
-			const char *extra);
-} file_list_hooks[] = {
-	{ "dir", S_IFDIR, false, false, true, add_generic },
-	{ "slink", S_IFLNK, true, false, false, add_generic },
-	{ "link", 0, true, false, false, add_hard_link },
-	{ "nod", 0, true, false, false, add_device },
-	{ "pipe", S_IFIFO, false, false, false, add_generic },
-	{ "sock", S_IFSOCK, false, false, false, add_generic },
-	{ "file", S_IFREG, false, false, false, add_file },
-	{ "glob", 0, false, true, true, glob_files },
-};
-
-#define NUM_HOOKS (sizeof(file_list_hooks) / sizeof(file_list_hooks[0]))
-
-static char *skip_space(char *str)
-{
-	if (!isspace(*str))
-		return NULL;
-	while (isspace(*str))
-		++str;
-	return str;
-}
-
-static char *read_u32(char *str, sqfs_u32 *out, sqfs_u32 base)
-{
-	*out = 0;
-
-	if (!isdigit(*str))
-		return NULL;
-
-	while (isdigit(*str)) {
-		sqfs_u32 x = *(str++) - '0';
-
-		if (x >= base || (*out) > (0xFFFFFFFF - x) / base)
-			return NULL;
-
-		(*out) = (*out) * base + x;
-	}
-
-	return str;
-}
-
-static char *read_str(char *str, char **out)
-{
-	*out = str;
-
-	if (*str == '"') {
-		char *ptr = str++;
-
-		while (*str != '\0' && *str != '"') {
-			if (str[0] == '\\' &&
-			    (str[1] == '"' || str[1] == '\\')) {
-				*(ptr++) = str[1];
-				str += 2;
-			} else {
-				*(ptr++) = *(str++);
-			}
-		}
-
-		if (str[0] != '"' || !isspace(str[1]))
-			return NULL;
-
-		*ptr = '\0';
-		++str;
-	} else {
-		while (*str != '\0' && !isspace(*str))
-			++str;
-
-		if (!isspace(*str))
-			return NULL;
-
-		*(str++) = '\0';
-	}
-
-	while (isspace(*str))
-		++str;
-
-	return str;
-}
-
-static int handle_line(fstree_t *fs, const char *filename,
-		       size_t line_num, char *line,
-		       const char *basepath)
-{
-	const char *extra = NULL, *msg = NULL;
-	const struct callback_t *cb = NULL;
-	unsigned int glob_flags = 0;
-	sqfs_u32 uid, gid, mode;
-	struct stat sb;
-	char *path;
-
-	for (size_t i = 0; i < NUM_HOOKS; ++i) {
-		size_t len = strlen(file_list_hooks[i].keyword);
-		if (strncmp(file_list_hooks[i].keyword, line, len) != 0)
-			continue;
-
-		if (isspace(line[len])) {
-			cb = file_list_hooks + i;
-			line = skip_space(line + len);
-			break;
-		}
-	}
-
-	if (cb == NULL)
-		goto fail_kw;
-
-	if ((line = read_str(line, &path)) == NULL)
-		goto fail_ent;
-
-	if (canonicalize_name(path))
-		goto fail_ent;
-
-	if (*path == '\0' && !cb->allow_root)
-		goto fail_root;
-
-	if (cb->is_glob && *line == '*') {
-		++line;
-		mode = 0;
-		glob_flags |= GLOB_MODE_FROM_SRC;
-	} else {
-		if ((line = read_u32(line, &mode, 8)) == NULL || mode > 07777)
-			goto fail_mode;
-	}
-
-	if ((line = skip_space(line)) == NULL)
-		goto fail_ent;
-
-	if (cb->is_glob && *line == '*') {
-		++line;
-		uid = 0;
-		glob_flags |= GLOB_UID_FROM_SRC;
-	} else {
-		if ((line = read_u32(line, &uid, 10)) == NULL)
-			goto fail_uid_gid;
-	}
-
-	if ((line = skip_space(line)) == NULL)
-		goto fail_ent;
-
-	if (cb->is_glob && *line == '*') {
-		++line;
-		gid = 0;
-		glob_flags |= GLOB_GID_FROM_SRC;
-	} else {
-		if ((line = read_u32(line, &gid, 10)) == NULL)
-			goto fail_uid_gid;
-	}
-
-	if ((line = skip_space(line)) != NULL && *line != '\0')
-		extra = line;
-
-	if (cb->need_extra && extra == NULL)
-		goto fail_no_extra;
-
-	/* forward to callback */
-	memset(&sb, 0, sizeof(sb));
-	sb.st_mtime = fs->defaults.st_mtime;
-	sb.st_mode = mode | cb->mode;
-	sb.st_uid = uid;
-	sb.st_gid = gid;
-
-	return cb->callback(fs, filename, line_num, path,
-			    &sb, basepath, glob_flags, extra);
-fail_root:
-	fprintf(stderr, "%s: " PRI_SZ ": cannot use / as argument for %s.\n",
-		filename, line_num, cb->keyword);
-	return -1;
-fail_no_extra:
-	fprintf(stderr, "%s: " PRI_SZ ": missing argument for %s.\n",
-		filename, line_num, cb->keyword);
-	return -1;
-fail_uid_gid:
-	msg = "uid & gid must be decimal numbers < 2^32";
-	goto out_desc;
-fail_mode:
-	msg = "mode must be an octal number <= 07777";
-	goto out_desc;
-fail_kw:
-	msg = "unknown entry type";
-	goto out_desc;
-fail_ent:
-	msg = "error in entry description";
-	goto out_desc;
-out_desc:
-	fprintf(stderr, "%s: " PRI_SZ ": %s.\n", filename, line_num, msg);
-	fputs("expected: <type> <path> <mode> <uid> <gid> [<extra>]\n",
-	      stderr);
-	return -1;
-}
-
-int fstree_from_file_stream(fstree_t *fs, istream_t *fp, const char *basepath)
-{
-	const char *filename;
-	size_t line_num = 1;
-	char *line;
-	int ret;
-
-	filename = istream_get_filename(fp);
-
-	for (;;) {
-		ret = istream_get_line(fp, &line, &line_num,
-				       ISTREAM_LINE_LTRIM | ISTREAM_LINE_SKIP_EMPTY);
-		if (ret < 0)
-			return -1;
-		if (ret > 0)
-			break;
-
-		if (line[0] != '#') {
-			if (handle_line(fs, filename, line_num,
-					line, basepath)) {
-				goto fail_line;
-			}
-		}
-
-		free(line);
-		++line_num;
-	}
-
-	return 0;
-fail_line:
-	free(line);
-	return -1;
-}
-
-int fstree_from_file(fstree_t *fs, const char *filename, const char *basepath)
-{
-	istream_t *fp;
-	int ret;
-
-	fp = istream_open_file(filename);
-	if (fp == NULL)
-		return -1;
-
-	ret = fstree_from_file_stream(fs, fp, basepath);
-
-	sqfs_drop(fp);
-	return ret;
-}
diff --git a/bin/gensquashfs/mkfs.c b/bin/gensquashfs/mkfs.c
deleted file mode 100644
index c773dd7..0000000
--- a/bin/gensquashfs/mkfs.c
+++ /dev/null
@@ -1,215 +0,0 @@
-/* SPDX-License-Identifier: GPL-3.0-or-later */
-/*
- * mkfs.c
- *
- * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
- */
-#include "mkfs.h"
-
-static int pack_files(sqfs_block_processor_t *data, fstree_t *fs,
-		      options_t *opt)
-{
-	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 (opt->packdir != NULL && chdir(opt->packdir) != 0) {
-		perror(opt->packdir);
-		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 = fi->flags;
-		filesize = file->get_size(file);
-
-		if (opt->no_tail_packing && filesize > opt->cfg.block_size)
-			flags |= SQFS_BLK_DONT_FRAGMENT;
-
-		ret = write_data_from_file(path, data, &fi->inode, file, flags);
-		sqfs_drop(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, 0);
-	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)
-{
-	int ret;
-
-	ret = fstree_from_file(fs, opt->infile, opt->packdir);
-
-	if (ret == 0 && selinux_handle != NULL)
-		ret = relabel_tree_dfs(opt->cfg.filename, xwr,
-				       fs->root, selinux_handle);
-
-	return ret;
-}
-
-static void override_owner_dfs(const options_t *opt, tree_node_t *n)
-{
-	if (opt->force_uid)
-		n->uid = opt->force_uid_value;
-
-	if (opt->force_gid)
-		n->gid = opt->force_gid_value;
-
-	if (S_ISDIR(n->mode)) {
-		for (n = n->data.dir.children; n != NULL; n = n->next)
-			override_owner_dfs(opt, n);
-	}
-}
-
-int main(int argc, char **argv)
-{
-	int status = EXIT_FAILURE;
-	istream_t *sortfile = NULL;
-	void *sehnd = NULL;
-	void *xattrmap = 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 (opt.xattr_file != NULL) {
-		xattrmap = xattr_open_map_file(opt.xattr_file);
-		if (xattrmap == NULL)
-			goto out;
-	}
-
-	if (opt.sortfile != NULL) {
-		sortfile = istream_open_file(opt.sortfile);
-		if (sortfile == NULL)
-			goto out;
-	}
-
-	if (opt.infile == NULL) {
-		if (fstree_from_dir(&sqfs.fs, sqfs.fs.root, opt.packdir,
-				    NULL, NULL, opt.dirscan_flags)) {
-			goto out;
-		}
-	} else {
-		if (read_fstree(&sqfs.fs, &opt, sqfs.xwr, sehnd))
-			goto out;
-	}
-
-	if (opt.force_uid || opt.force_gid)
-		override_owner_dfs(&opt, sqfs.fs.root);
-
-	if (fstree_post_process(&sqfs.fs))
-		goto out;
-
-	if (opt.infile == NULL) {
-		if (xattrs_from_dir(&sqfs.fs, opt.packdir, sehnd, xattrmap,
-				    sqfs.xwr, opt.scan_xattr)) {
-			goto out;
-		}
-	}
-
-	if (sortfile != NULL) {
-		if (fstree_sort_files(&sqfs.fs, sortfile))
-			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);
-	if (sehnd != NULL)
-		selinux_close_context_file(sehnd);
-	if (sortfile != NULL)
-		sqfs_drop(sortfile);
-	free(opt.packdir);
-	return status;
-}
diff --git a/bin/gensquashfs/mkfs.h b/bin/gensquashfs/mkfs.h
deleted file mode 100644
index 53fb018..0000000
--- a/bin/gensquashfs/mkfs.h
+++ /dev/null
@@ -1,137 +0,0 @@
-/* SPDX-License-Identifier: GPL-3.0-or-later */
-/*
- * mkfs.h
- *
- * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
- * Copyright (C) 2022 Enno Boland <mail@eboland.de>
- */
-#ifndef MKFS_H
-#define MKFS_H
-
-#include "config.h"
-
-#include "common.h"
-#include "fstree.h"
-#include "util/util.h"
-#include "io/file.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 <stdio.h>
-#include <errno.h>
-#include <ctype.h>
-
-typedef struct {
-	sqfs_writer_cfg_t cfg;
-	unsigned int dirscan_flags;
-	const char *infile;
-	const char *selinux;
-	const char *xattr_file;
-	const char *sortfile;
-	bool no_tail_packing;
-
-	/* copied from command line or constructed from infile argument
-	   if not specified. Must be free'd. */
-	char *packdir;
-
-	unsigned int force_uid_value;
-	unsigned int force_gid_value;
-	bool force_uid;
-	bool force_gid;
-
-	bool scan_xattr;
-} options_t;
-
-struct XattrMapEntry {
-	char *key;
-	sqfs_u8 *value;
-	size_t value_len;
-	struct XattrMapEntry *next;
-};
-
-struct XattrMapPattern {
-	char *path;
-	struct XattrMapEntry *entries;
-	struct XattrMapPattern *next;
-};
-
-struct XattrMap {
-	struct XattrMapPattern *patterns;
-};
-
-void process_command_line(options_t *opt, int argc, char **argv);
-
-int xattrs_from_dir(fstree_t *fs, const char *path, void *selinux_handle,
-		    void *xattr_map, sqfs_xattr_writer_t *xwr, bool scan_xattr);
-
-void *xattr_open_map_file(const char *path);
-
-int
-xattr_apply_map_file(char *path, void *map, sqfs_xattr_writer_t *xwr);
-
-void xattr_close_map_file(void *xattr_map);
-
-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);
-
-/*
-  Parses the file format accepted by gensquashfs and produce a file system
-  tree from it. File input paths are interpreted as relative to the current
-  working directory.
-
-  On failure, an error report with filename and line number is written
-  to stderr.
-
-  Returns 0 on success.
- */
-int fstree_from_file(fstree_t *fs, const char *filename,
-		     const char *basepath);
-
-int fstree_from_file_stream(fstree_t *fs, istream_t *file,
-			    const char *basepath);
-
-/*
-  Recursively scan a directory to build a file system tree.
-
-  Returns 0 on success, prints to stderr on failure.
- */
-int fstree_from_dir(fstree_t *fs, tree_node_t *root,
-		    const char *path, scan_node_callback cb, void *user,
-		    unsigned int flags);
-
-/*
-  Same as fstree_from_dir, but scans a sub-directory inside the specified path.
-
-  Returns 0 on success, prints to stderr on failure.
- */
-int fstree_from_subdir(fstree_t *fs, tree_node_t *root,
-		       const char *path, const char *subdir,
-		       scan_node_callback cb, void *user, unsigned int flags);
-
-int fstree_sort_files(fstree_t *fs, istream_t *sortfile);
-
-#endif /* MKFS_H */
diff --git a/bin/gensquashfs/options.c b/bin/gensquashfs/options.c
deleted file mode 100644
index f263bce..0000000
--- a/bin/gensquashfs/options.c
+++ /dev/null
@@ -1,383 +0,0 @@
-/* SPDX-License-Identifier: GPL-3.0-or-later */
-/*
- * options.c
- *
- * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
- */
-#include "mkfs.h"
-
-enum {
-	ALL_ROOT_OPTION = 1,
-};
-
-static struct option long_opts[] = {
-	{ "all-root", no_argument, NULL, ALL_ROOT_OPTION },
-	{ "set-uid", required_argument, NULL, 'u' },
-	{ "set-gid", required_argument, NULL, 'g' },
-	{ "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
-	{ "xattr-file", required_argument, NULL, 'A' },
-	{ "sort-file", required_argument, NULL, 'S' },
-	{ "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:u:g:j:Q:S:A: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";
-
-static const char *pack_options =
-"  --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. The directory becomes the\n"
-"                              file system root.\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"
-"  --set-uid, -u <number>      Force the owners user ID for ALL inodes to\n"
-"                              this value, no matter what the pack file or\n"
-"                              directory entries actually specify.\n"
-"  --set-gid, -g <number>      Force the owners group ID for ALL inodes to\n"
-"                              this value, no matter what the pack file or\n"
-"                              directory entries actually specify.\n"
-"  --all-root                  A short hand for `--set-uid 0 --set-gid 0`.\n"
-"\n";
-
-const char *extra_options =
-"  --sort-file, -S <file>      Specify a \"sort file\" that can be used to\n"
-"                              micro manage the order of files during packing\n"
-"                              and behaviour (compression, fragmentation, ..)\n"
-"\n"
-#ifdef WITH_SELINUX
-"  --selinux, -s <file>        Specify an SELinux label file to get context\n"
-"                              attributes from.\n"
-#endif
-"  --xattr-file, -A <file>     Specify an Xattr file to get extended attributes\n"
-"                              for loading xattrs\n"
-"  --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 *pack_details =
-"Example of a pack file:\n"
-"\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"
-"    \n"
-"    # `slink` for symlink, `link` for hard links\n"
-"    slink /lib 0777 0 0 /usr/lib\n"
-"    link /init 0777 0 0 /sbin/init\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"
-"    # collect the contents of ./lib and put it under /usr/lib\n"
-"    glob /usr/lib 0755 0 0 -type d ./lib\n"
-"    glob /usr/lib 0755 0 0 -type f -name \"*.so.*\" ./lib\n"
-"    glob /usr/lib 0777 0 0 -type l -name \"*.so.*\" ./lib\n"
-"\n\n";
-
-const char *sort_details =
-"When using a sort file, the specified paths are within the SquashFS image.\n"
-"Files with lower priority are packed first, default priority is 0.\n"
-"The sorting is stable, files with the same priority do not change place\n"
-"relative to each other.\n"
-"\n"
-"Example:\n"
-"    # Specify a packing order with file globbing\n"
-"    -8000  [glob]          bin/*\n"
-"    -5000  [glob]          lib/*\n"
-"\n"
-"    # glob_no_path means * is allowed to match /\n"
-"    -1000  [glob_no_path]  share/*\n"
-"\n"
-"    # Our boot loader needs this\n"
-"    -100000  [dont_compress,dont_fragment,nosparse]  boot/vmlinuz\n"
-"\n"
-"    # For demonstration, a quoted filename and no flags\n"
-"    1337  \"usr/share/my \\\"special\\\" file  \"\n"
-"\n\n";
-
-static const char *xattr_details =
-"The format of xattr files tries to immitate the format generated\n"
-"by `getfattr --dump`.\n"
-"\n"
-"Example:\n"
-"    # file: dev/\n"
-"    security.selinux=\"system_u:object_r:device_t:s0\"\n"
-"    user.beverage_preference=0xCAFECAFEDECAFBAD\n"
-"\n"
-"    # file: dev/rfkill\n"
-"    security.selinux=\"system_u:object_r:wireless_device_t:s0\"\n"
-"    system.posix_acl_access=0sSGVsbG8gdGhlcmUgOi0pCg==\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 ALL_ROOT_OPTION:
-			opt->force_uid_value = 0;
-			opt->force_gid_value = 0;
-			opt->force_uid = true;
-			opt->force_gid = true;
-			break;
-		case 'u':
-			opt->force_uid_value = strtol(optarg, NULL, 0);
-			opt->force_uid = true;
-			break;
-		case 'g':
-			opt->force_gid_value = strtol(optarg, NULL, 0);
-			opt->force_gid = true;
-			break;
-		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->scan_xattr = true;
-			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':
-			free(opt->packdir);
-			opt->packdir = strdup(optarg);
-			if (opt->packdir == NULL) {
-				perror(optarg);
-				exit(EXIT_FAILURE);
-			}
-			break;
-#ifdef WITH_SELINUX
-		case 's':
-			opt->selinux = optarg;
-			break;
-#endif
-		case 'A':
-			opt->xattr_file = optarg;
-			break;
-		case 'S':
-			opt->sortfile = optarg;
-			break;
-		case 'h':
-			fputs(help_string, stdout);
-			printf(pack_options, SQFS_DEFAULT_BLOCK_SIZE,
-			       SQFS_DEVBLK_SIZE);
-			fputs(extra_options, stdout);
-			fputs(pack_details, stdout);
-			fputs(sort_details, stdout);
-			fputs(xattr_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++];
-
-	if (optind < argc) {
-		fputs("Unknown extra arguments specified.\n", stderr);
-		goto fail_arg;
-	}
-
-	/* construct packdir if not specified */
-	if (opt->packdir == NULL && opt->infile != NULL) {
-		const char *split = strrchr(opt->infile, '/');
-
-		if (split != NULL) {
-			opt->packdir = strndup(opt->infile,
-					       split - opt->infile);
-
-			if (opt->packdir == NULL) {
-				perror("constructing input directory path");
-				exit(EXIT_FAILURE);
-			}
-		}
-	}
-	return;
-fail_arg:
-	fputs("Try `gensquashfs --help' for more information.\n", stderr);
-	free(opt->packdir);
-	exit(EXIT_FAILURE);
-}
diff --git a/bin/gensquashfs/selinux.c b/bin/gensquashfs/selinux.c
deleted file mode 100644
index 678723b..0000000
--- a/bin/gensquashfs/selinux.c
+++ /dev/null
@@ -1,78 +0,0 @@
-/* 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/gensquashfs/sort_by_file.c b/bin/gensquashfs/sort_by_file.c
deleted file mode 100644
index a555718..0000000
--- a/bin/gensquashfs/sort_by_file.c
+++ /dev/null
@@ -1,368 +0,0 @@
-/* SPDX-License-Identifier: GPL-3.0-or-later */
-/*
- * sort_by_file.c
- *
- * Copyright (C) 2021 David Oberhollenzer <goliath@infraroot.at>
- */
-#include "config.h"
-
-#include "util/util.h"
-#include "fstree.h"
-#include "mkfs.h"
-
-#include "sqfs/block.h"
-
-#include <string.h>
-#include <stdlib.h>
-#include <ctype.h>
-
-static int decode_priority(const char *filename, size_t line_no,
-			   char *line, sqfs_s64 *priority)
-{
-	bool negative = false;
-	size_t i = 0;
-
-	if (line[0] == '-') {
-		negative = true;
-		i = 1;
-	}
-
-	if (!isdigit(line[i]))
-		goto fail_number;
-
-	*priority = 0;
-
-	for (; isdigit(line[i]); ++i) {
-		sqfs_s64 x = line[i] - '0';
-
-		if ((*priority) >= ((0x7FFFFFFFFFFFFFFFL - x) / 10L))
-			goto fail_ov;
-
-		(*priority) = (*priority) * 10 + x;
-	}
-
-	if (!isspace(line[i]))
-		goto fail_filename;
-
-	while (isspace(line[i]))
-		++i;
-
-	if (line[i] == '\0')
-		goto fail_filename;
-
-	if (negative)
-		(*priority) = -(*priority);
-
-	memmove(line, line + i, strlen(line + i) + 1);
-	return 0;
-fail_number:
-	fprintf(stderr, "%s: " PRI_SZ ": Line must start with "
-		"numeric sort priority.\n",
-		filename, line_no);
-	return -1;
-fail_ov:
-	fprintf(stderr, "%s: " PRI_SZ ": Numeric overflow in sort priority.\n",
-		filename, line_no);
-	return -1;
-fail_filename:
-	fprintf(stderr, "%s: " PRI_SZ ": Expacted `<space> <filename>` "
-		"after sort priority.\n",
-		filename, line_no);
-	return -1;
-}
-
-static int decode_filename(const char *filename, size_t line_no, char *buffer)
-{
-	char *src, *dst;
-
-	if (buffer[0] == '"') {
-		src = buffer + 1;
-		dst = buffer;
-
-		for (;;) {
-			if (src[0] == '\0')
-				goto fail_match;
-
-			if (src[0] == '"') {
-				++src;
-				break;
-			}
-
-			if (src[0] == '\\') {
-				switch (src[1]) {
-				case '\\':
-					*(dst++) = '\\';
-					src += 2;
-					break;
-				case '"':
-					*(dst++) = '"';
-					src += 2;
-					break;
-				default:
-					goto fail_escape;
-				}
-			} else {
-				*(dst++) = *(src++);
-			}
-		}
-
-		if (*src != '\0')
-			return -1;
-	}
-
-	if (canonicalize_name(buffer))
-		goto fail_canon;
-	return 0;
-fail_canon:
-	fprintf(stderr, "%s: " PRI_SZ ": Malformed filename.\n",
-		filename, line_no);
-	return -1;
-fail_escape:
-	fprintf(stderr, "%s: " PRI_SZ ": Unknown escape sequence `\\%c` "
-		"in filename.\n", filename, line_no, src[1]);
-	return -1;
-fail_match:
-	fprintf(stderr, "%s: " PRI_SZ ": Unmatched '\"' in filename.\n",
-		filename, line_no);
-	return -1;
-}
-
-static int decode_flags(const char *filename, size_t line_no, bool *do_glob,
-			bool *path_glob, int *flags, char *line)
-{
-	char *start = line;
-
-	*do_glob = false;
-	*path_glob = false;
-	*flags = 0;
-
-	if (*(line++) != '[')
-		return 0;
-
-	for (;;) {
-		while (isspace(*line))
-			++line;
-
-		if (*line == ']') {
-			++line;
-			break;
-		}
-
-		if (strncmp(line, "glob_no_path", 12) == 0) {
-			line += 12;
-			*do_glob = true;
-			*path_glob = false;
-		} else if (strncmp(line, "glob", 4) == 0) {
-			line += 4;
-			*do_glob = true;
-			*path_glob = true;
-		} else if (strncmp(line, "dont_fragment", 13) == 0) {
-			line += 13;
-			(*flags) |= SQFS_BLK_DONT_FRAGMENT;
-		} else if (strncmp(line, "align", 5) == 0) {
-			line += 5;
-			(*flags) |= SQFS_BLK_ALIGN;
-		} else if (strncmp(line, "dont_compress", 13) == 0) {
-			line += 13;
-			(*flags) |= SQFS_BLK_DONT_COMPRESS;
-		} else if (strncmp(line, "dont_deduplicate", 16) == 0) {
-			line += 16;
-			(*flags) |= SQFS_BLK_DONT_DEDUPLICATE;
-		} else if (strncmp(line, "nosparse", 8) == 0) {
-			line += 8;
-			(*flags) |= SQFS_BLK_IGNORE_SPARSE;
-		} else {
-			goto fail_flag;
-		}
-
-		while (isspace(*line))
-			++line;
-
-		if (*line == ']') {
-			++line;
-			break;
-		}
-
-		if (*(line++) != ',')
-			goto fail_sep;
-	}
-
-	if (!isspace(*line))
-		goto fail_fname;
-
-	while (isspace(*line))
-		++line;
-
-	memmove(start, line, strlen(line) + 1);
-	return 0;
-fail_fname:
-	fprintf(stderr, "%s: " PRI_SZ ": Expected `<space> <filename>` "
-		"after flag list.\n", filename, line_no);
-	return -1;
-fail_sep:
-	fprintf(stderr, "%s: " PRI_SZ ": Unexpected '%c' after flag.\n",
-		filename, line_no, *line);
-	return -1;
-fail_flag:
-	fprintf(stderr, "%s: " PRI_SZ ": Unknown flag `%.3s...`.\n",
-		filename, line_no, line);
-	return -1;
-}
-
-static void sort_file_list(fstree_t *fs)
-{
-	file_info_t *out = NULL, *out_last = NULL;
-
-	while (fs->files != NULL) {
-		sqfs_s64 lowest = fs->files->priority;
-		file_info_t *it, *prev;
-
-		for (it = fs->files; it != NULL; it = it->next) {
-			if (it->priority < lowest)
-				lowest = it->priority;
-		}
-
-		it = fs->files;
-		prev = NULL;
-
-		while (it != NULL) {
-			if (it->priority != lowest) {
-				prev = it;
-				it = it->next;
-				continue;
-			}
-
-			if (prev == NULL) {
-				fs->files = it->next;
-			} else {
-				prev->next = it->next;
-			}
-
-			if (out == NULL) {
-				out = it;
-			} else {
-				out_last->next = it;
-			}
-
-			out_last = it;
-			it = it->next;
-			out_last->next = NULL;
-		}
-	}
-
-	fs->files = out;
-}
-
-int fstree_sort_files(fstree_t *fs, istream_t *sortfile)
-{
-	const char *filename;
-	size_t line_num = 1;
-	file_info_t *it;
-
-	for (it = fs->files; it != NULL; it = it->next) {
-		it->priority = 0;
-		it->flags = 0;
-		it->already_matched = false;
-	}
-
-	filename = istream_get_filename(sortfile);
-
-	for (;;) {
-		bool do_glob, path_glob, have_match;
-		char *line = NULL;
-		sqfs_s64 priority;
-		int ret, flags;
-
-		ret = istream_get_line(sortfile, &line, &line_num,
-				       ISTREAM_LINE_LTRIM |
-				       ISTREAM_LINE_RTRIM |
-				       ISTREAM_LINE_SKIP_EMPTY);
-		if (ret != 0) {
-			free(line);
-			if (ret < 0)
-				return -1;
-			break;
-		}
-
-		if (line[0] == '#') {
-			free(line);
-			continue;
-		}
-
-		if (decode_priority(filename, line_num, line, &priority)) {
-			free(line);
-			return -1;
-		}
-
-		if (decode_flags(filename, line_num, &do_glob, &path_glob,
-				 &flags, line)) {
-			free(line);
-			return -1;
-		}
-
-		if (decode_filename(filename, line_num, line)) {
-			free(line);
-			return -1;
-		}
-
-		have_match = false;
-
-		for (it = fs->files; it != NULL; it = it->next) {
-			tree_node_t *node;
-			char *path;
-
-			if (it->already_matched)
-				continue;
-
-			node = container_of(it, tree_node_t, data.file);
-			path = fstree_get_path(node);
-			if (path == NULL) {
-				fprintf(stderr, "%s: " PRI_SZ ": out-of-memory\n",
-					filename, line_num);
-				free(line);
-				return -1;
-			}
-
-			if (canonicalize_name(path)) {
-				fprintf(stderr,
-					"%s: " PRI_SZ ": [BUG] error "
-					"reconstructing node path\n",
-					filename, line_num);
-				free(line);
-				free(path);
-				return -1;
-			}
-
-			if (do_glob) {
-				ret = fnmatch(line, path,
-					      path_glob ? FNM_PATHNAME : 0);
-
-			} else {
-				ret = strcmp(path, line);
-			}
-
-			free(path);
-
-			if (ret == 0) {
-				have_match = true;
-				it->flags = flags;
-				it->priority = priority;
-				it->already_matched = true;
-
-				if (!do_glob)
-					break;
-			}
-		}
-
-		if (!have_match) {
-			fprintf(stderr, "WARNING: %s: " PRI_SZ ": no match "
-				"for '%s'.\n",
-				filename, line_num, line);
-		}
-
-		free(line);
-	}
-
-	sort_file_list(fs);
-	return 0;
-}
diff --git a/bin/gensquashfs/src/dirscan_xattr.c b/bin/gensquashfs/src/dirscan_xattr.c
new file mode 100644
index 0000000..7d4e552
--- /dev/null
+++ b/bin/gensquashfs/src/dirscan_xattr.c
@@ -0,0 +1,220 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * dirscan_xattr.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
+
+static int xattr_xcan_dfs(const char *path_prefix, void *selinux_handle,
+			  sqfs_xattr_writer_t *xwr, bool scan_xattr, void *xattr_map,
+			  tree_node_t *node)
+{
+	char *path = NULL;
+	int ret;
+
+	ret = sqfs_xattr_writer_begin(xwr, 0);
+	if (ret) {
+		sqfs_perror(node->name, "recoding xattr key-value pairs\n",
+			    ret);
+		return -1;
+	}
+
+#ifdef HAVE_SYS_XATTR_H
+	if (scan_xattr) {
+		path = get_full_path(path_prefix, node);
+		if (path == NULL)
+			return -1;
+
+		ret = xattr_from_path(xwr, path);
+		free(path);
+		path = NULL;
+		if (ret) {
+			ret = -1;
+			goto out;
+		}
+	}
+#else
+	(void)path_prefix;
+#endif
+
+	if (selinux_handle != NULL || xattr_map != NULL) {
+		path = fstree_get_path(node);
+
+		if (path == NULL) {
+			perror("reconstructing absolute path");
+			ret = -1;
+			goto out;
+		}
+	}
+
+	if (xattr_map != NULL) {
+		ret = xattr_apply_map_file(path, xattr_map, xwr);
+
+		if (ret) {
+			ret = -1;
+			goto out;
+		}
+	}
+
+	if (selinux_handle != NULL) {
+		ret = selinux_relable_node(selinux_handle, xwr, node, path);
+
+		if (ret) {
+			ret = -1;
+			goto out;
+		}
+	}
+
+	if (sqfs_xattr_writer_end(xwr, &node->xattr_idx)) {
+		sqfs_perror(node->name, "completing xattr key-value pairs",
+			    ret);
+		ret = -1;
+		goto out;
+	}
+
+	if (S_ISDIR(node->mode)) {
+		node = node->data.dir.children;
+
+		while (node != NULL) {
+			if (xattr_xcan_dfs(path_prefix, selinux_handle, xwr,
+					   scan_xattr, xattr_map, node)) {
+				ret = -1;
+				goto out;
+			}
+
+			node = node->next;
+		}
+	}
+
+out:
+	free(path);
+	return ret;
+}
+
+int xattrs_from_dir(fstree_t *fs, const char *path, void *selinux_handle,
+		    void *xattr_map, sqfs_xattr_writer_t *xwr, bool scan_xattr)
+{
+	if (xwr == NULL)
+		return 0;
+
+	if (selinux_handle == NULL && !scan_xattr && xattr_map == NULL)
+		return 0;
+
+	return xattr_xcan_dfs(path, selinux_handle, xwr, scan_xattr, xattr_map, fs->root);
+}
diff --git a/bin/gensquashfs/src/filemap_xattr.c b/bin/gensquashfs/src/filemap_xattr.c
new file mode 100644
index 0000000..dd76b50
--- /dev/null
+++ b/bin/gensquashfs/src/filemap_xattr.c
@@ -0,0 +1,254 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * filemap_xattr.c
+ *
+ * Copyright (C) 2022 Enno Boland <mail@eboland.de>
+ */
+#include "fstree.h"
+#include "mkfs.h"
+#include <stdio.h>
+
+#define NEW_FILE_START "# file: "
+
+static void print_error(const char *filename, size_t line_num, const char *err)
+{
+	fprintf(stderr, "%s: " PRI_SZ ": %s\n", filename, line_num, err);
+}
+
+// Taken from attr-2.5.1/tools/setfattr.c
+static sqfs_u8 *decode(const char *filename, size_t line_num,
+		       const char *value, size_t *size)
+{
+	sqfs_u8 *decoded = NULL;
+
+	if (*size == 0) {
+		decoded = (sqfs_u8 *)strdup("");
+		if (decoded == NULL)
+			goto fail_alloc;
+		return decoded;
+	}
+
+	if (value[0] == '0' && (value[1] == 'x' || value[1] == 'X')) {
+		*size = ((*size) - 2) / 2;
+
+		decoded = calloc(1, (*size) + 1);
+		if (decoded == NULL)
+			goto fail_alloc;
+
+		if (hex_decode(value + 2, (*size) * 2, decoded, *size))
+			goto fail_encode;
+	} else if (value[0] == '0' && (value[1] == 's' || value[1] == 'S')) {
+		size_t input_len = *size - 2;
+
+		*size = (input_len / 4) * 3;
+
+		decoded = calloc(1, (*size) + 1);
+		if (decoded == NULL)
+			goto fail_alloc;
+
+		if (base64_decode(value + 2, input_len, decoded, size))
+			goto fail_encode;
+	} else {
+		const char *v = value, *end = value + *size;
+		sqfs_u8 *d;
+
+		if (end > v + 1 && *v == '"' && *(end - 1) == '"') {
+			v++;
+			end--;
+		}
+
+		decoded = calloc(1, (*size) + 1);
+		if (decoded == NULL)
+			goto fail_alloc;
+
+		d = decoded;
+
+		while (v < end) {
+			if (v[0] == '\\') {
+				if (v[1] == '\\' || v[1] == '"') {
+					*d++ = *++v;
+					v++;
+				} else if (v[1] >= '0' && v[1] <= '7') {
+					int c = 0;
+					v++;
+					c = (*v++ - '0');
+					if (*v >= '0' && *v <= '7')
+						c = (c << 3) + (*v++ - '0');
+					if (*v >= '0' && *v <= '7')
+						c = (c << 3) + (*v++ - '0');
+					*d++ = c;
+				} else
+					*d++ = *v++;
+			} else
+				*d++ = *v++;
+		}
+		*size = d - decoded;
+	}
+	return decoded;
+fail_alloc:
+	fprintf(stderr, "out of memory\n");
+	return NULL;
+fail_encode:
+	print_error(filename, line_num, "bad input encoding");
+	free(decoded);
+	return NULL;
+}
+
+static int parse_file_name(const char *filename, size_t line_num,
+			   char *line, struct XattrMap *map)
+{
+	struct XattrMapPattern *current_file;
+	char *file_name = strdup(line + strlen(NEW_FILE_START));
+
+	if (file_name == NULL)
+		goto fail_alloc;
+
+	current_file = calloc(1, sizeof(struct XattrMapPattern));
+	if (current_file == NULL)
+		goto fail_alloc;
+
+	current_file->next = map->patterns;
+	map->patterns = current_file;
+
+	if (canonicalize_name(file_name)) {
+		print_error(filename, line_num, "invalid absolute path");
+		free(current_file);
+		free(file_name);
+		return -1;
+	}
+
+	current_file->path = file_name;
+	return 0;
+fail_alloc:
+	fprintf(stderr, "out of memory\n");
+	free(file_name);
+	return -1;
+}
+
+static int parse_xattr(const char *filename, size_t line_num, char *key_start,
+		       char *value_start, struct XattrMap *map)
+{
+	size_t len;
+	struct XattrMapPattern *current_pattern = map->patterns;
+	struct XattrMapEntry *current_entry;
+
+	if (current_pattern == NULL) {
+		print_error(filename, line_num, "no file specified yet");
+		return -1;
+	}
+
+	current_entry = calloc(1, sizeof(struct XattrMapEntry));
+	if (current_entry == NULL) {
+		return -1;
+	}
+	current_entry->next = current_pattern->entries;
+	current_pattern->entries = current_entry;
+
+	current_entry->key = strdup(key_start);
+	len = strlen(value_start);
+	current_entry->value = decode(filename, line_num, value_start, &len);
+	current_entry->value_len = len;
+
+	return 0;
+}
+
+void *
+xattr_open_map_file(const char *path) {
+	struct XattrMap *map;
+	size_t line_num = 1;
+	char *p = NULL;
+	istream_t *file = istream_open_file(path);
+	if (file == NULL) {
+		return NULL;
+	}
+
+	map = calloc(1, sizeof(struct XattrMap));
+	if (map == NULL)
+		goto fail_close;
+
+	for (;;) {
+		char *line = NULL;
+		int ret = istream_get_line(file, &line, &line_num,
+					   ISTREAM_LINE_LTRIM |
+					   ISTREAM_LINE_RTRIM |
+					   ISTREAM_LINE_SKIP_EMPTY);
+		if (ret < 0)
+			goto fail;
+		if (ret > 0)
+			break;
+
+		if (strncmp(NEW_FILE_START, line, strlen(NEW_FILE_START)) == 0) {
+			ret = parse_file_name(path, line_num, line, map);
+		} else if ((p = strchr(line, '='))) {
+			*(p++) = '\0';
+			ret = parse_xattr(path, line_num, line, p, map);
+		} else if (line[0] != '#') {
+			print_error(path, line_num, "not a key-value pair");
+			ret = -1;
+		}
+
+		++line_num;
+		free(line);
+		if (ret < 0)
+			goto fail;
+	}
+
+	sqfs_drop(file);
+	return map;
+fail:
+	xattr_close_map_file(map);
+fail_close:
+	sqfs_drop(file);
+	return NULL;
+}
+
+void
+xattr_close_map_file(void *xattr_map) {
+	struct XattrMap *map = xattr_map;
+	while (map->patterns != NULL) {
+		struct XattrMapPattern *file = map->patterns;
+		map->patterns = file->next;
+		while (file->entries != NULL) {
+			struct XattrMapEntry *entry = file->entries;
+			file->entries = entry->next;
+			free(entry->key);
+			free(entry->value);
+			free(entry);
+		}
+		free(file->path);
+		free(file);
+	}
+	free(xattr_map);
+}
+
+int
+xattr_apply_map_file(char *path, void *map, sqfs_xattr_writer_t *xwr) {
+	struct XattrMap *xattr_map = map;
+	int ret = 0;
+	const struct XattrMapPattern *pat;
+	const struct XattrMapEntry *entry;
+
+	for (pat = xattr_map->patterns; pat != NULL; pat = pat->next) {
+		char *patstr = pat->path;
+		const char *stripped = path;
+
+		if (patstr[0] != '/' && stripped[0] == '/') {
+			stripped++;
+		}
+
+		if (strcmp(patstr, stripped) == 0) {
+			printf("Applying xattrs for %s", path);
+			for (entry = pat->entries; entry != NULL; entry = entry->next) {
+				printf("  %s = \n", entry->key);
+				fwrite(entry->value, entry->value_len, 1, stdout);
+				puts("\n");
+				ret = sqfs_xattr_writer_add(
+						xwr, entry->key, entry->value, entry->value_len);
+				if (ret < 0) {
+					return ret;
+				}
+			}
+		}
+	}
+	return ret;
+}
diff --git a/bin/gensquashfs/src/fstree_from_dir.c b/bin/gensquashfs/src/fstree_from_dir.c
new file mode 100644
index 0000000..5b3f003
--- /dev/null
+++ b/bin/gensquashfs/src/fstree_from_dir.c
@@ -0,0 +1,493 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * fstree_from_dir.c
+ *
+ * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
+ */
+#include "config.h"
+#include "mkfs.h"
+
+#include <dirent.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+
+#if defined(_WIN32) || defined(__WINDOWS__)
+#define UNIX_EPOCH_ON_W32 11644473600UL
+#define W32_TICS_PER_SEC 10000000UL
+
+static sqfs_u32 w32time_to_sqfs_time(const FILETIME *ft)
+{
+	sqfs_u64 w32ts;
+
+	w32ts = ft->dwHighDateTime;
+	w32ts <<= 32UL;
+	w32ts |= ft->dwLowDateTime;
+
+	w32ts /= W32_TICS_PER_SEC;
+
+	if (w32ts <= UNIX_EPOCH_ON_W32)
+		return 0;
+
+	w32ts -= UNIX_EPOCH_ON_W32;
+
+	return (w32ts < 0x0FFFFFFFFUL) ? w32ts : 0xFFFFFFFF;
+}
+
+static int add_node(fstree_t *fs, tree_node_t *root,
+		    scan_node_callback cb, void *user,
+		    unsigned int flags,
+		    const LPWIN32_FIND_DATAW entry)
+{
+	tree_node_t *n;
+	DWORD length;
+
+	if (entry->cFileName[0] == '.') {
+		if (entry->cFileName[1] == '\0')
+			return 0;
+
+		if (entry->cFileName[1] == '.' && entry->cFileName[2] == '\0')
+			return 0;
+	}
+
+	length = WideCharToMultiByte(CP_UTF8, 0, entry->cFileName,
+				     -1, NULL, 0, NULL, NULL);
+	if (length <= 0) {
+		w32_perror("converting path to UTF-8");
+		return -1;
+	}
+
+	n = calloc(1, sizeof(*n) + length + 1);
+	if (n == NULL) {
+		fprintf(stderr, "creating tree node: out-of-memory\n");
+		return -1;
+	}
+
+	n->name = (char *)n->payload;
+	WideCharToMultiByte(CP_UTF8, 0, entry->cFileName, -1,
+			    n->name, length + 1, NULL, NULL);
+
+	if (entry->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
+		if (flags & DIR_SCAN_NO_DIR) {
+			free(n);
+			return 0;
+		}
+
+		n->mode = S_IFDIR | 0755;
+	} else {
+		if (flags & DIR_SCAN_NO_FILE) {
+			free(n);
+			return 0;
+		}
+
+		n->mode = S_IFREG | 0644;
+	}
+
+	if (cb != NULL) {
+		int ret = cb(user, fs, n);
+
+		if (ret != 0) {
+			free(n);
+			return ret < 0 ? ret : 0;
+		}
+	}
+
+	if (flags & DIR_SCAN_KEEP_TIME) {
+		n->mod_time = w32time_to_sqfs_time(&(entry->ftLastWriteTime));
+	} else {
+		n->mod_time = fs->defaults.st_mtime;
+	}
+
+	fstree_insert_sorted(root, n);
+	return 0;
+}
+
+static int scan_dir(fstree_t *fs, tree_node_t *root,
+		    const char *path, const WCHAR *wpath,
+		    scan_node_callback cb, void *user,
+		    unsigned int flags)
+{
+	WIN32_FIND_DATAW entry;
+	HANDLE dirhnd;
+
+	dirhnd = FindFirstFileW(wpath, &entry);
+
+	if (dirhnd == INVALID_HANDLE_VALUE)
+		goto fail_perror;
+
+	do {
+		if (add_node(fs, root, cb, user, flags, &entry))
+			goto fail;
+	} while (FindNextFileW(dirhnd, &entry));
+
+	if (GetLastError() != ERROR_NO_MORE_FILES)
+		goto fail_perror;
+
+	FindClose(dirhnd);
+	return 0;
+fail_perror:
+	w32_perror(path);
+fail:
+	if (dirhnd != INVALID_HANDLE_VALUE)
+		FindClose(dirhnd);
+	return -1;
+}
+
+int fstree_from_dir(fstree_t *fs, tree_node_t *root,
+		    const char *path, scan_node_callback cb,
+		    void *user, unsigned int flags)
+{
+	WCHAR *wpath = NULL, *new = NULL;
+	size_t len, newlen;
+	tree_node_t *n;
+
+	/* path -> to_wchar(path) + L"\*" */
+	wpath = path_to_windows(path);
+	if (wpath == NULL) {
+		fprintf(stderr, "%s: allocation failure.\n", path);
+		return -1;
+	}
+
+	for (len = 0; wpath[len] != '\0'; ++len)
+		;
+
+	newlen = len + 1;
+
+	if (len > 0 && wpath[len - 1] != '\\')
+		newlen += 1;
+
+	new = realloc(wpath, sizeof(wpath[0]) * (newlen + 1));
+	if (new == NULL) {
+		fprintf(stderr, "%s: allocation failure.\n", path);
+		goto fail;
+	}
+
+	wpath = new;
+
+	if (len > 0 && wpath[len - 1] != '\\')
+		wpath[len++] = '\\';
+
+	wpath[len++] = '*';
+	wpath[len++] = '\0';
+
+	/* scan directory contents */
+	if (scan_dir(fs, root, path, wpath, cb, user, flags))
+		goto fail;
+
+	free(wpath);
+	wpath = NULL;
+
+	/* recursion step */
+	if (flags & DIR_SCAN_NO_RECURSION)
+		return 0;
+
+	for (n = root->data.dir.children; n != NULL; n = n->next) {
+		if (!S_ISDIR(n->mode))
+			continue;
+
+		if (fstree_from_subdir(fs, n, path, n->name, cb, user, flags))
+			return -1;
+	}
+
+	return 0;
+fail:
+	free(wpath);
+	return -1;
+}
+
+int fstree_from_subdir(fstree_t *fs, tree_node_t *root,
+		       const char *path, const char *subdir,
+		       scan_node_callback cb, void *user,
+		       unsigned int flags)
+{
+	size_t len, plen, slen;
+	WCHAR *wpath = NULL;
+	char *temp = NULL;
+	tree_node_t *n;
+
+	plen = strlen(path);
+	slen = subdir == NULL ? 0 : strlen(subdir);
+
+	if (slen == 0)
+		return fstree_from_dir(fs, root, path, cb, user, flags);
+
+	len = plen + 1 + slen + 2;
+
+	temp = calloc(1, len + 1);
+	if (temp == NULL) {
+		fprintf(stderr, "%s/%s: allocation failure.\n", path, subdir);
+		return -1;
+	}
+
+	memcpy(temp, path, plen);
+	temp[plen] = '/';
+	memcpy(temp + plen + 1, subdir, slen);
+	temp[plen + 1 + slen    ] = '/';
+	temp[plen + 1 + slen + 1] = '*';
+	temp[plen + 1 + slen + 2] = '\0';
+
+	wpath = path_to_windows(temp);
+	if (wpath == NULL) {
+		fprintf(stderr, "%s: allocation failure.\n", temp);
+		goto fail;
+	}
+
+	if (scan_dir(fs, root, temp, wpath, cb, user, flags))
+		goto fail;
+
+	free(wpath);
+	wpath = NULL;
+
+	if (flags & DIR_SCAN_NO_RECURSION) {
+		free(temp);
+		return 0;
+	}
+
+	temp[plen + 1 + slen] = '\0';
+
+	for (n = root->data.dir.children; n != NULL; n = n->next) {
+		if (!S_ISDIR(n->mode))
+			continue;
+
+		if (fstree_from_subdir(fs, n, temp, n->name, cb, user, flags))
+			goto fail;
+	}
+
+	free(temp);
+	return 0;
+fail:
+	free(temp);
+	free(wpath);
+	return -1;
+
+}
+#else
+static void discard_node(tree_node_t *root, tree_node_t *n)
+{
+	tree_node_t *it;
+
+	if (n == root->data.dir.children) {
+		root->data.dir.children = n->next;
+	} else {
+		it = root->data.dir.children;
+
+		while (it != NULL && it->next != n)
+			it = it->next;
+
+		if (it != NULL)
+			it->next = n->next;
+	}
+
+	free(n);
+}
+
+static int populate_dir(int dir_fd, fstree_t *fs, tree_node_t *root,
+			dev_t devstart, scan_node_callback cb,
+			void *user, unsigned int flags)
+{
+	char *extra = NULL;
+	struct dirent *ent;
+	int ret, childfd;
+	struct stat sb;
+	tree_node_t *n;
+	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;
+		}
+
+		switch (sb.st_mode & S_IFMT) {
+		case S_IFSOCK:
+			if (flags & DIR_SCAN_NO_SOCK)
+				continue;
+			break;
+		case S_IFLNK:
+			if (flags & DIR_SCAN_NO_SLINK)
+				continue;
+			break;
+		case S_IFREG:
+			if (flags & DIR_SCAN_NO_FILE)
+				continue;
+			break;
+		case S_IFBLK:
+			if (flags & DIR_SCAN_NO_BLK)
+				continue;
+			break;
+		case S_IFCHR:
+			if (flags & DIR_SCAN_NO_CHR)
+				continue;
+			break;
+		case S_IFIFO:
+			if (flags & DIR_SCAN_NO_FIFO)
+				continue;
+			break;
+		default:
+			break;
+		}
+
+		if ((flags & DIR_SCAN_ONE_FILESYSTEM) && sb.st_dev != devstart)
+			continue;
+
+		if (S_ISLNK(sb.st_mode)) {
+			size_t size;
+
+			if ((sizeof(sb.st_size) > sizeof(size_t)) &&
+			    sb.st_size > SIZE_MAX) {
+				errno = EOVERFLOW;
+				goto fail_rdlink;
+			}
+
+			if (SZ_ADD_OV((size_t)sb.st_size, 1, &size)) {
+				errno = EOVERFLOW;
+				goto fail_rdlink;
+			}
+
+			extra = calloc(1, size);
+			if (extra == NULL)
+				goto fail_rdlink;
+
+			if (readlinkat(dir_fd, ent->d_name,
+				       extra, (size_t)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;
+
+		if (S_ISDIR(sb.st_mode) && (flags & DIR_SCAN_NO_DIR)) {
+			n = fstree_get_node_by_path(fs, root, ent->d_name,
+						    false, false);
+			if (n == NULL)
+				continue;
+
+			ret = 0;
+		} else {
+			n = fstree_mknode(root, ent->d_name,
+					  strlen(ent->d_name), extra, &sb);
+			if (n == NULL) {
+				perror("creating tree node");
+				goto fail;
+			}
+
+			ret = (cb == NULL) ? 0 : cb(user, fs, n);
+		}
+
+		free(extra);
+		extra = NULL;
+
+		if (ret < 0)
+			goto fail;
+
+		if (ret > 0) {
+			discard_node(root, n);
+			continue;
+		}
+
+		if (S_ISDIR(n->mode) && !(flags & DIR_SCAN_NO_RECURSION)) {
+			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,
+					 cb, user, flags)) {
+				goto fail;
+			}
+		}
+	}
+
+	closedir(dir);
+	return 0;
+fail_rdlink:
+	perror("readlink");
+fail:
+	closedir(dir);
+	free(extra);
+	return -1;
+}
+
+int fstree_from_subdir(fstree_t *fs, tree_node_t *root,
+		       const char *path, const char *subdir,
+		       scan_node_callback cb, void *user,
+		       unsigned int flags)
+{
+	struct stat sb;
+	int fd, subfd;
+
+	if (!S_ISDIR(root->mode)) {
+		fprintf(stderr,
+			"scanning %s/%s into %s: target is not a directory\n",
+			path, subdir == NULL ? "" : subdir, root->name);
+		return -1;
+	}
+
+	fd = open(path, O_DIRECTORY | O_RDONLY | O_CLOEXEC);
+	if (fd < 0) {
+		perror(path);
+		return -1;
+	}
+
+	if (subdir != NULL) {
+		subfd = openat(fd, subdir, O_DIRECTORY | O_RDONLY | O_CLOEXEC);
+
+		if (subfd < 0) {
+			fprintf(stderr, "%s/%s: %s\n", path, subdir,
+				strerror(errno));
+			close(fd);
+			return -1;
+		}
+
+		close(fd);
+		fd = subfd;
+	}
+
+	if (fstat(fd, &sb)) {
+		fprintf(stderr, "%s/%s: %s\n", path,
+			subdir == NULL ? "" : subdir,
+			strerror(errno));
+		close(fd);
+		return -1;
+	}
+
+	return populate_dir(fd, fs, root, sb.st_dev, cb, user, flags);
+}
+
+int fstree_from_dir(fstree_t *fs, tree_node_t *root,
+		    const char *path, scan_node_callback cb,
+		    void *user, unsigned int flags)
+{
+	return fstree_from_subdir(fs, root, path, NULL, cb, user, flags);
+}
+#endif
diff --git a/bin/gensquashfs/src/fstree_from_file.c b/bin/gensquashfs/src/fstree_from_file.c
new file mode 100644
index 0000000..e26d4b1
--- /dev/null
+++ b/bin/gensquashfs/src/fstree_from_file.c
@@ -0,0 +1,591 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * fstree_from_file.c
+ *
+ * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
+ */
+#include "config.h"
+
+#include "util/util.h"
+#include "io/file.h"
+#include "compat.h"
+#include "mkfs.h"
+
+#include <stdlib.h>
+#include <string.h>
+#include <assert.h>
+#include <errno.h>
+#include <ctype.h>
+
+struct glob_context {
+	const char *filename;
+	size_t line_num;
+
+	struct stat *basic;
+	unsigned int glob_flags;
+
+	char *name_pattern;
+};
+
+enum {
+	GLOB_MODE_FROM_SRC = 0x01,
+	GLOB_UID_FROM_SRC = 0x02,
+	GLOB_GID_FROM_SRC = 0x04,
+	GLOB_FLAG_PATH = 0x08,
+};
+
+static const struct {
+	const char *name;
+	unsigned int clear_flag;
+	unsigned int set_flag;
+} glob_scan_flags[] = {
+	{ "-type b", DIR_SCAN_NO_BLK, 0 },
+	{ "-type c", DIR_SCAN_NO_CHR, 0 },
+	{ "-type d", DIR_SCAN_NO_DIR, 0 },
+	{ "-type p", DIR_SCAN_NO_FIFO, 0 },
+	{ "-type f", DIR_SCAN_NO_FILE, 0 },
+	{ "-type l", DIR_SCAN_NO_SLINK, 0 },
+	{ "-type s", DIR_SCAN_NO_SOCK, 0 },
+	{ "-xdev", 0, DIR_SCAN_ONE_FILESYSTEM },
+	{ "-mount", 0, DIR_SCAN_ONE_FILESYSTEM },
+	{ "-keeptime", 0, DIR_SCAN_KEEP_TIME },
+	{ "-nonrecursive", 0, DIR_SCAN_NO_RECURSION },
+};
+
+static int add_generic(fstree_t *fs, const char *filename, size_t line_num,
+		       const char *path, struct stat *sb,
+		       const char *basepath, unsigned int glob_flags,
+		       const char *extra)
+{
+	(void)basepath;
+	(void)glob_flags;
+
+	if (fstree_add_generic(fs, path, sb, extra) == NULL) {
+		fprintf(stderr, "%s: " PRI_SZ ": %s: %s\n",
+			filename, line_num, path, strerror(errno));
+		return -1;
+	}
+
+	return 0;
+}
+
+static int add_device(fstree_t *fs, const char *filename, size_t line_num,
+		      const char *path, struct stat *sb, const char *basepath,
+		      unsigned int glob_flags, const char *extra)
+{
+	unsigned int maj, min;
+	char c;
+
+	if (sscanf(extra, "%c %u %u", &c, &maj, &min) != 3) {
+		fprintf(stderr, "%s: " PRI_SZ ": "
+			"expected '<c|b> major minor'\n",
+			filename, line_num);
+		return -1;
+	}
+
+	if (c == 'c' || c == 'C') {
+		sb->st_mode |= S_IFCHR;
+	} else if (c == 'b' || c == 'B') {
+		sb->st_mode |= S_IFBLK;
+	} else {
+		fprintf(stderr, "%s: " PRI_SZ ": unknown device type '%c'\n",
+			filename, line_num, c);
+		return -1;
+	}
+
+	sb->st_rdev = makedev(maj, min);
+	return add_generic(fs, filename, line_num, path, sb, basepath,
+			   glob_flags, NULL);
+}
+
+static int add_file(fstree_t *fs, const char *filename, size_t line_num,
+		    const char *path, struct stat *basic, const char *basepath,
+		    unsigned int glob_flags, const char *extra)
+{
+	if (extra == NULL || *extra == '\0')
+		extra = path;
+
+	return add_generic(fs, filename, line_num, path, basic,
+			   basepath, glob_flags, extra);
+}
+
+static int add_hard_link(fstree_t *fs, const char *filename, size_t line_num,
+			 const char *path, struct stat *basic,
+			 const char *basepath, unsigned int glob_flags,
+			 const char *extra)
+{
+	(void)basepath;
+	(void)glob_flags;
+	(void)basic;
+
+	if (fstree_add_hard_link(fs, path, extra) == NULL) {
+		fprintf(stderr, "%s: " PRI_SZ ": %s\n",
+			filename, line_num, strerror(errno));
+		return -1;
+	}
+	return 0;
+}
+
+static int glob_node_callback(void *user, fstree_t *fs, tree_node_t *node)
+{
+	struct glob_context *ctx = user;
+	char *path;
+	int ret;
+	(void)fs;
+
+	if (!(ctx->glob_flags & GLOB_MODE_FROM_SRC)) {
+		node->mode &= ~(07777);
+		node->mode |= ctx->basic->st_mode & 07777;
+	}
+
+	if (!(ctx->glob_flags & GLOB_UID_FROM_SRC))
+		node->uid = ctx->basic->st_uid;
+
+	if (!(ctx->glob_flags & GLOB_GID_FROM_SRC))
+		node->gid = ctx->basic->st_gid;
+
+	if (ctx->name_pattern != NULL) {
+		if (ctx->glob_flags & GLOB_FLAG_PATH) {
+			path = fstree_get_path(node);
+			if (path == NULL) {
+				fprintf(stderr, "%s: " PRI_SZ ": %s\n",
+					ctx->filename, ctx->line_num,
+					strerror(errno));
+				return -1;
+			}
+
+			ret = canonicalize_name(path);
+			assert(ret == 0);
+
+			ret = fnmatch(ctx->name_pattern, path, FNM_PATHNAME);
+			free(path);
+		} else {
+			ret = fnmatch(ctx->name_pattern, node->name, 0);
+		}
+
+		if (ret != 0)
+			return 1;
+	}
+
+	return 0;
+}
+
+static size_t name_string_length(const char *str)
+{
+	size_t len = 0;
+	int start;
+
+	if (*str == '"' || *str == '\'') {
+		start = *str;
+		++len;
+
+		while (str[len] != '\0' && str[len] != start)
+			++len;
+
+		if (str[len] == start)
+			++len;
+	} else {
+		while (str[len] != '\0' && !isspace(str[len]))
+			++len;
+	}
+
+	return len;
+}
+
+static void quote_remove(char *str)
+{
+	char *dst = str;
+	int start = *(str++);
+
+	if (start != '\'' && start != '"')
+		return;
+
+	while (*str != start && *str != '\0')
+		*(dst++) = *(str++);
+
+	*(dst++) = '\0';
+}
+
+static int glob_files(fstree_t *fs, const char *filename, size_t line_num,
+		      const char *path, struct stat *basic,
+		      const char *basepath, unsigned int glob_flags,
+		      const char *extra)
+{
+	unsigned int scan_flags = 0, all_flags;
+	struct glob_context ctx;
+	bool first_clear_flag;
+	size_t i, count, len;
+	tree_node_t *root;
+	int ret;
+
+	memset(&ctx, 0, sizeof(ctx));
+	ctx.filename = filename;
+	ctx.line_num = line_num;
+	ctx.basic = basic;
+	ctx.glob_flags = glob_flags;
+
+	/* fetch the actual target node */
+	root = fstree_get_node_by_path(fs, fs->root, path, true, false);
+	if (root == NULL) {
+		fprintf(stderr, "%s: " PRI_SZ ": %s: %s\n",
+			filename, line_num, path, strerror(errno));
+		return -1;
+	}
+
+	/* process options */
+	first_clear_flag = true;
+
+	all_flags = DIR_SCAN_NO_BLK | DIR_SCAN_NO_CHR | DIR_SCAN_NO_DIR |
+		DIR_SCAN_NO_FIFO | DIR_SCAN_NO_FILE | DIR_SCAN_NO_SLINK |
+		DIR_SCAN_NO_SOCK;
+
+	while (extra != NULL && *extra != '\0') {
+		count = sizeof(glob_scan_flags) / sizeof(glob_scan_flags[0]);
+
+		for (i = 0; i < count; ++i) {
+			len = strlen(glob_scan_flags[i].name);
+			if (strncmp(extra, glob_scan_flags[i].name, len) != 0)
+				continue;
+
+			if (isspace(extra[len])) {
+				extra += len;
+				while (isspace(*extra))
+					++extra;
+				break;
+			}
+		}
+
+		if (i < count) {
+			if (glob_scan_flags[i].clear_flag != 0 &&
+			    first_clear_flag) {
+				scan_flags |= all_flags;
+				first_clear_flag = false;
+			}
+
+			scan_flags &= ~(glob_scan_flags[i].clear_flag);
+			scan_flags |= glob_scan_flags[i].set_flag;
+			continue;
+		}
+
+		if (strncmp(extra, "-name", 5) == 0 && isspace(extra[5])) {
+			for (extra += 5; isspace(*extra); ++extra)
+				;
+
+			len = name_string_length(extra);
+
+			free(ctx.name_pattern);
+			ctx.name_pattern = strndup(extra, len);
+			extra += len;
+
+			while (isspace(*extra))
+				++extra;
+
+			quote_remove(ctx.name_pattern);
+			continue;
+		}
+
+		if (strncmp(extra, "-path", 5) == 0 && isspace(extra[5])) {
+			for (extra += 5; isspace(*extra); ++extra)
+				;
+
+			len = name_string_length(extra);
+
+			free(ctx.name_pattern);
+			ctx.name_pattern = strndup(extra, len);
+			extra += len;
+
+			while (isspace(*extra))
+				++extra;
+
+			quote_remove(ctx.name_pattern);
+			ctx.glob_flags |= GLOB_FLAG_PATH;
+			continue;
+		}
+
+		if (extra[0] == '-') {
+			if (extra[1] == '-' && isspace(extra[2])) {
+				extra += 2;
+				while (isspace(*extra))
+					++extra;
+				break;
+			}
+
+			fprintf(stderr, "%s: " PRI_SZ ": unknown option.\n",
+				filename, line_num);
+			free(ctx.name_pattern);
+			return -1;
+		} else {
+			break;
+		}
+	}
+
+	if (extra != NULL && *extra == '\0')
+		extra = NULL;
+
+	/* do the scan */
+	if (basepath == NULL) {
+		if (extra == NULL) {
+			ret = fstree_from_dir(fs, root, ".", glob_node_callback,
+					      &ctx, scan_flags);
+		} else {
+			ret = fstree_from_dir(fs, root, extra,
+					      glob_node_callback,
+					      &ctx, scan_flags);
+		}
+	} else {
+		ret = fstree_from_subdir(fs, root, basepath, extra,
+					 glob_node_callback, &ctx,
+					 scan_flags);
+	}
+
+	free(ctx.name_pattern);
+	return ret;
+}
+
+static const struct callback_t {
+	const char *keyword;
+	unsigned int mode;
+	bool need_extra;
+	bool is_glob;
+	bool allow_root;
+	int (*callback)(fstree_t *fs, const char *filename, size_t line_num,
+			const char *path, struct stat *sb,
+			const char *basepath, unsigned int glob_flags,
+			const char *extra);
+} file_list_hooks[] = {
+	{ "dir", S_IFDIR, false, false, true, add_generic },
+	{ "slink", S_IFLNK, true, false, false, add_generic },
+	{ "link", 0, true, false, false, add_hard_link },
+	{ "nod", 0, true, false, false, add_device },
+	{ "pipe", S_IFIFO, false, false, false, add_generic },
+	{ "sock", S_IFSOCK, false, false, false, add_generic },
+	{ "file", S_IFREG, false, false, false, add_file },
+	{ "glob", 0, false, true, true, glob_files },
+};
+
+#define NUM_HOOKS (sizeof(file_list_hooks) / sizeof(file_list_hooks[0]))
+
+static char *skip_space(char *str)
+{
+	if (!isspace(*str))
+		return NULL;
+	while (isspace(*str))
+		++str;
+	return str;
+}
+
+static char *read_u32(char *str, sqfs_u32 *out, sqfs_u32 base)
+{
+	*out = 0;
+
+	if (!isdigit(*str))
+		return NULL;
+
+	while (isdigit(*str)) {
+		sqfs_u32 x = *(str++) - '0';
+
+		if (x >= base || (*out) > (0xFFFFFFFF - x) / base)
+			return NULL;
+
+		(*out) = (*out) * base + x;
+	}
+
+	return str;
+}
+
+static char *read_str(char *str, char **out)
+{
+	*out = str;
+
+	if (*str == '"') {
+		char *ptr = str++;
+
+		while (*str != '\0' && *str != '"') {
+			if (str[0] == '\\' &&
+			    (str[1] == '"' || str[1] == '\\')) {
+				*(ptr++) = str[1];
+				str += 2;
+			} else {
+				*(ptr++) = *(str++);
+			}
+		}
+
+		if (str[0] != '"' || !isspace(str[1]))
+			return NULL;
+
+		*ptr = '\0';
+		++str;
+	} else {
+		while (*str != '\0' && !isspace(*str))
+			++str;
+
+		if (!isspace(*str))
+			return NULL;
+
+		*(str++) = '\0';
+	}
+
+	while (isspace(*str))
+		++str;
+
+	return str;
+}
+
+static int handle_line(fstree_t *fs, const char *filename,
+		       size_t line_num, char *line,
+		       const char *basepath)
+{
+	const char *extra = NULL, *msg = NULL;
+	const struct callback_t *cb = NULL;
+	unsigned int glob_flags = 0;
+	sqfs_u32 uid, gid, mode;
+	struct stat sb;
+	char *path;
+
+	for (size_t i = 0; i < NUM_HOOKS; ++i) {
+		size_t len = strlen(file_list_hooks[i].keyword);
+		if (strncmp(file_list_hooks[i].keyword, line, len) != 0)
+			continue;
+
+		if (isspace(line[len])) {
+			cb = file_list_hooks + i;
+			line = skip_space(line + len);
+			break;
+		}
+	}
+
+	if (cb == NULL)
+		goto fail_kw;
+
+	if ((line = read_str(line, &path)) == NULL)
+		goto fail_ent;
+
+	if (canonicalize_name(path))
+		goto fail_ent;
+
+	if (*path == '\0' && !cb->allow_root)
+		goto fail_root;
+
+	if (cb->is_glob && *line == '*') {
+		++line;
+		mode = 0;
+		glob_flags |= GLOB_MODE_FROM_SRC;
+	} else {
+		if ((line = read_u32(line, &mode, 8)) == NULL || mode > 07777)
+			goto fail_mode;
+	}
+
+	if ((line = skip_space(line)) == NULL)
+		goto fail_ent;
+
+	if (cb->is_glob && *line == '*') {
+		++line;
+		uid = 0;
+		glob_flags |= GLOB_UID_FROM_SRC;
+	} else {
+		if ((line = read_u32(line, &uid, 10)) == NULL)
+			goto fail_uid_gid;
+	}
+
+	if ((line = skip_space(line)) == NULL)
+		goto fail_ent;
+
+	if (cb->is_glob && *line == '*') {
+		++line;
+		gid = 0;
+		glob_flags |= GLOB_GID_FROM_SRC;
+	} else {
+		if ((line = read_u32(line, &gid, 10)) == NULL)
+			goto fail_uid_gid;
+	}
+
+	if ((line = skip_space(line)) != NULL && *line != '\0')
+		extra = line;
+
+	if (cb->need_extra && extra == NULL)
+		goto fail_no_extra;
+
+	/* forward to callback */
+	memset(&sb, 0, sizeof(sb));
+	sb.st_mtime = fs->defaults.st_mtime;
+	sb.st_mode = mode | cb->mode;
+	sb.st_uid = uid;
+	sb.st_gid = gid;
+
+	return cb->callback(fs, filename, line_num, path,
+			    &sb, basepath, glob_flags, extra);
+fail_root:
+	fprintf(stderr, "%s: " PRI_SZ ": cannot use / as argument for %s.\n",
+		filename, line_num, cb->keyword);
+	return -1;
+fail_no_extra:
+	fprintf(stderr, "%s: " PRI_SZ ": missing argument for %s.\n",
+		filename, line_num, cb->keyword);
+	return -1;
+fail_uid_gid:
+	msg = "uid & gid must be decimal numbers < 2^32";
+	goto out_desc;
+fail_mode:
+	msg = "mode must be an octal number <= 07777";
+	goto out_desc;
+fail_kw:
+	msg = "unknown entry type";
+	goto out_desc;
+fail_ent:
+	msg = "error in entry description";
+	goto out_desc;
+out_desc:
+	fprintf(stderr, "%s: " PRI_SZ ": %s.\n", filename, line_num, msg);
+	fputs("expected: <type> <path> <mode> <uid> <gid> [<extra>]\n",
+	      stderr);
+	return -1;
+}
+
+int fstree_from_file_stream(fstree_t *fs, istream_t *fp, const char *basepath)
+{
+	const char *filename;
+	size_t line_num = 1;
+	char *line;
+	int ret;
+
+	filename = istream_get_filename(fp);
+
+	for (;;) {
+		ret = istream_get_line(fp, &line, &line_num,
+				       ISTREAM_LINE_LTRIM | ISTREAM_LINE_SKIP_EMPTY);
+		if (ret < 0)
+			return -1;
+		if (ret > 0)
+			break;
+
+		if (line[0] != '#') {
+			if (handle_line(fs, filename, line_num,
+					line, basepath)) {
+				goto fail_line;
+			}
+		}
+
+		free(line);
+		++line_num;
+	}
+
+	return 0;
+fail_line:
+	free(line);
+	return -1;
+}
+
+int fstree_from_file(fstree_t *fs, const char *filename, const char *basepath)
+{
+	istream_t *fp;
+	int ret;
+
+	fp = istream_open_file(filename);
+	if (fp == NULL)
+		return -1;
+
+	ret = fstree_from_file_stream(fs, fp, basepath);
+
+	sqfs_drop(fp);
+	return ret;
+}
diff --git a/bin/gensquashfs/src/mkfs.c b/bin/gensquashfs/src/mkfs.c
new file mode 100644
index 0000000..c773dd7
--- /dev/null
+++ b/bin/gensquashfs/src/mkfs.c
@@ -0,0 +1,215 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * mkfs.c
+ *
+ * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
+ */
+#include "mkfs.h"
+
+static int pack_files(sqfs_block_processor_t *data, fstree_t *fs,
+		      options_t *opt)
+{
+	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 (opt->packdir != NULL && chdir(opt->packdir) != 0) {
+		perror(opt->packdir);
+		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 = fi->flags;
+		filesize = file->get_size(file);
+
+		if (opt->no_tail_packing && filesize > opt->cfg.block_size)
+			flags |= SQFS_BLK_DONT_FRAGMENT;
+
+		ret = write_data_from_file(path, data, &fi->inode, file, flags);
+		sqfs_drop(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, 0);
+	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)
+{
+	int ret;
+
+	ret = fstree_from_file(fs, opt->infile, opt->packdir);
+
+	if (ret == 0 && selinux_handle != NULL)
+		ret = relabel_tree_dfs(opt->cfg.filename, xwr,
+				       fs->root, selinux_handle);
+
+	return ret;
+}
+
+static void override_owner_dfs(const options_t *opt, tree_node_t *n)
+{
+	if (opt->force_uid)
+		n->uid = opt->force_uid_value;
+
+	if (opt->force_gid)
+		n->gid = opt->force_gid_value;
+
+	if (S_ISDIR(n->mode)) {
+		for (n = n->data.dir.children; n != NULL; n = n->next)
+			override_owner_dfs(opt, n);
+	}
+}
+
+int main(int argc, char **argv)
+{
+	int status = EXIT_FAILURE;
+	istream_t *sortfile = NULL;
+	void *sehnd = NULL;
+	void *xattrmap = 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 (opt.xattr_file != NULL) {
+		xattrmap = xattr_open_map_file(opt.xattr_file);
+		if (xattrmap == NULL)
+			goto out;
+	}
+
+	if (opt.sortfile != NULL) {
+		sortfile = istream_open_file(opt.sortfile);
+		if (sortfile == NULL)
+			goto out;
+	}
+
+	if (opt.infile == NULL) {
+		if (fstree_from_dir(&sqfs.fs, sqfs.fs.root, opt.packdir,
+				    NULL, NULL, opt.dirscan_flags)) {
+			goto out;
+		}
+	} else {
+		if (read_fstree(&sqfs.fs, &opt, sqfs.xwr, sehnd))
+			goto out;
+	}
+
+	if (opt.force_uid || opt.force_gid)
+		override_owner_dfs(&opt, sqfs.fs.root);
+
+	if (fstree_post_process(&sqfs.fs))
+		goto out;
+
+	if (opt.infile == NULL) {
+		if (xattrs_from_dir(&sqfs.fs, opt.packdir, sehnd, xattrmap,
+				    sqfs.xwr, opt.scan_xattr)) {
+			goto out;
+		}
+	}
+
+	if (sortfile != NULL) {
+		if (fstree_sort_files(&sqfs.fs, sortfile))
+			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);
+	if (sehnd != NULL)
+		selinux_close_context_file(sehnd);
+	if (sortfile != NULL)
+		sqfs_drop(sortfile);
+	free(opt.packdir);
+	return status;
+}
diff --git a/bin/gensquashfs/src/mkfs.h b/bin/gensquashfs/src/mkfs.h
new file mode 100644
index 0000000..53fb018
--- /dev/null
+++ b/bin/gensquashfs/src/mkfs.h
@@ -0,0 +1,137 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * mkfs.h
+ *
+ * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
+ * Copyright (C) 2022 Enno Boland <mail@eboland.de>
+ */
+#ifndef MKFS_H
+#define MKFS_H
+
+#include "config.h"
+
+#include "common.h"
+#include "fstree.h"
+#include "util/util.h"
+#include "io/file.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 <stdio.h>
+#include <errno.h>
+#include <ctype.h>
+
+typedef struct {
+	sqfs_writer_cfg_t cfg;
+	unsigned int dirscan_flags;
+	const char *infile;
+	const char *selinux;
+	const char *xattr_file;
+	const char *sortfile;
+	bool no_tail_packing;
+
+	/* copied from command line or constructed from infile argument
+	   if not specified. Must be free'd. */
+	char *packdir;
+
+	unsigned int force_uid_value;
+	unsigned int force_gid_value;
+	bool force_uid;
+	bool force_gid;
+
+	bool scan_xattr;
+} options_t;
+
+struct XattrMapEntry {
+	char *key;
+	sqfs_u8 *value;
+	size_t value_len;
+	struct XattrMapEntry *next;
+};
+
+struct XattrMapPattern {
+	char *path;
+	struct XattrMapEntry *entries;
+	struct XattrMapPattern *next;
+};
+
+struct XattrMap {
+	struct XattrMapPattern *patterns;
+};
+
+void process_command_line(options_t *opt, int argc, char **argv);
+
+int xattrs_from_dir(fstree_t *fs, const char *path, void *selinux_handle,
+		    void *xattr_map, sqfs_xattr_writer_t *xwr, bool scan_xattr);
+
+void *xattr_open_map_file(const char *path);
+
+int
+xattr_apply_map_file(char *path, void *map, sqfs_xattr_writer_t *xwr);
+
+void xattr_close_map_file(void *xattr_map);
+
+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);
+
+/*
+  Parses the file format accepted by gensquashfs and produce a file system
+  tree from it. File input paths are interpreted as relative to the current
+  working directory.
+
+  On failure, an error report with filename and line number is written
+  to stderr.
+
+  Returns 0 on success.
+ */
+int fstree_from_file(fstree_t *fs, const char *filename,
+		     const char *basepath);
+
+int fstree_from_file_stream(fstree_t *fs, istream_t *file,
+			    const char *basepath);
+
+/*
+  Recursively scan a directory to build a file system tree.
+
+  Returns 0 on success, prints to stderr on failure.
+ */
+int fstree_from_dir(fstree_t *fs, tree_node_t *root,
+		    const char *path, scan_node_callback cb, void *user,
+		    unsigned int flags);
+
+/*
+  Same as fstree_from_dir, but scans a sub-directory inside the specified path.
+
+  Returns 0 on success, prints to stderr on failure.
+ */
+int fstree_from_subdir(fstree_t *fs, tree_node_t *root,
+		       const char *path, const char *subdir,
+		       scan_node_callback cb, void *user, unsigned int flags);
+
+int fstree_sort_files(fstree_t *fs, istream_t *sortfile);
+
+#endif /* MKFS_H */
diff --git a/bin/gensquashfs/src/options.c b/bin/gensquashfs/src/options.c
new file mode 100644
index 0000000..f263bce
--- /dev/null
+++ b/bin/gensquashfs/src/options.c
@@ -0,0 +1,383 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * options.c
+ *
+ * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
+ */
+#include "mkfs.h"
+
+enum {
+	ALL_ROOT_OPTION = 1,
+};
+
+static struct option long_opts[] = {
+	{ "all-root", no_argument, NULL, ALL_ROOT_OPTION },
+	{ "set-uid", required_argument, NULL, 'u' },
+	{ "set-gid", required_argument, NULL, 'g' },
+	{ "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
+	{ "xattr-file", required_argument, NULL, 'A' },
+	{ "sort-file", required_argument, NULL, 'S' },
+	{ "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:u:g:j:Q:S:A: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";
+
+static const char *pack_options =
+"  --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. The directory becomes the\n"
+"                              file system root.\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"
+"  --set-uid, -u <number>      Force the owners user ID for ALL inodes to\n"
+"                              this value, no matter what the pack file or\n"
+"                              directory entries actually specify.\n"
+"  --set-gid, -g <number>      Force the owners group ID for ALL inodes to\n"
+"                              this value, no matter what the pack file or\n"
+"                              directory entries actually specify.\n"
+"  --all-root                  A short hand for `--set-uid 0 --set-gid 0`.\n"
+"\n";
+
+const char *extra_options =
+"  --sort-file, -S <file>      Specify a \"sort file\" that can be used to\n"
+"                              micro manage the order of files during packing\n"
+"                              and behaviour (compression, fragmentation, ..)\n"
+"\n"
+#ifdef WITH_SELINUX
+"  --selinux, -s <file>        Specify an SELinux label file to get context\n"
+"                              attributes from.\n"
+#endif
+"  --xattr-file, -A <file>     Specify an Xattr file to get extended attributes\n"
+"                              for loading xattrs\n"
+"  --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 *pack_details =
+"Example of a pack file:\n"
+"\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"
+"    \n"
+"    # `slink` for symlink, `link` for hard links\n"
+"    slink /lib 0777 0 0 /usr/lib\n"
+"    link /init 0777 0 0 /sbin/init\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"
+"    # collect the contents of ./lib and put it under /usr/lib\n"
+"    glob /usr/lib 0755 0 0 -type d ./lib\n"
+"    glob /usr/lib 0755 0 0 -type f -name \"*.so.*\" ./lib\n"
+"    glob /usr/lib 0777 0 0 -type l -name \"*.so.*\" ./lib\n"
+"\n\n";
+
+const char *sort_details =
+"When using a sort file, the specified paths are within the SquashFS image.\n"
+"Files with lower priority are packed first, default priority is 0.\n"
+"The sorting is stable, files with the same priority do not change place\n"
+"relative to each other.\n"
+"\n"
+"Example:\n"
+"    # Specify a packing order with file globbing\n"
+"    -8000  [glob]          bin/*\n"
+"    -5000  [glob]          lib/*\n"
+"\n"
+"    # glob_no_path means * is allowed to match /\n"
+"    -1000  [glob_no_path]  share/*\n"
+"\n"
+"    # Our boot loader needs this\n"
+"    -100000  [dont_compress,dont_fragment,nosparse]  boot/vmlinuz\n"
+"\n"
+"    # For demonstration, a quoted filename and no flags\n"
+"    1337  \"usr/share/my \\\"special\\\" file  \"\n"
+"\n\n";
+
+static const char *xattr_details =
+"The format of xattr files tries to immitate the format generated\n"
+"by `getfattr --dump`.\n"
+"\n"
+"Example:\n"
+"    # file: dev/\n"
+"    security.selinux=\"system_u:object_r:device_t:s0\"\n"
+"    user.beverage_preference=0xCAFECAFEDECAFBAD\n"
+"\n"
+"    # file: dev/rfkill\n"
+"    security.selinux=\"system_u:object_r:wireless_device_t:s0\"\n"
+"    system.posix_acl_access=0sSGVsbG8gdGhlcmUgOi0pCg==\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 ALL_ROOT_OPTION:
+			opt->force_uid_value = 0;
+			opt->force_gid_value = 0;
+			opt->force_uid = true;
+			opt->force_gid = true;
+			break;
+		case 'u':
+			opt->force_uid_value = strtol(optarg, NULL, 0);
+			opt->force_uid = true;
+			break;
+		case 'g':
+			opt->force_gid_value = strtol(optarg, NULL, 0);
+			opt->force_gid = true;
+			break;
+		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->scan_xattr = true;
+			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':
+			free(opt->packdir);
+			opt->packdir = strdup(optarg);
+			if (opt->packdir == NULL) {
+				perror(optarg);
+				exit(EXIT_FAILURE);
+			}
+			break;
+#ifdef WITH_SELINUX
+		case 's':
+			opt->selinux = optarg;
+			break;
+#endif
+		case 'A':
+			opt->xattr_file = optarg;
+			break;
+		case 'S':
+			opt->sortfile = optarg;
+			break;
+		case 'h':
+			fputs(help_string, stdout);
+			printf(pack_options, SQFS_DEFAULT_BLOCK_SIZE,
+			       SQFS_DEVBLK_SIZE);
+			fputs(extra_options, stdout);
+			fputs(pack_details, stdout);
+			fputs(sort_details, stdout);
+			fputs(xattr_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++];
+
+	if (optind < argc) {
+		fputs("Unknown extra arguments specified.\n", stderr);
+		goto fail_arg;
+	}
+
+	/* construct packdir if not specified */
+	if (opt->packdir == NULL && opt->infile != NULL) {
+		const char *split = strrchr(opt->infile, '/');
+
+		if (split != NULL) {
+			opt->packdir = strndup(opt->infile,
+					       split - opt->infile);
+
+			if (opt->packdir == NULL) {
+				perror("constructing input directory path");
+				exit(EXIT_FAILURE);
+			}
+		}
+	}
+	return;
+fail_arg:
+	fputs("Try `gensquashfs --help' for more information.\n", stderr);
+	free(opt->packdir);
+	exit(EXIT_FAILURE);
+}
diff --git a/bin/gensquashfs/src/selinux.c b/bin/gensquashfs/src/selinux.c
new file mode 100644
index 0000000..678723b
--- /dev/null
+++ b/bin/gensquashfs/src/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/gensquashfs/src/sort_by_file.c b/bin/gensquashfs/src/sort_by_file.c
new file mode 100644
index 0000000..a555718
--- /dev/null
+++ b/bin/gensquashfs/src/sort_by_file.c
@@ -0,0 +1,368 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * sort_by_file.c
+ *
+ * Copyright (C) 2021 David Oberhollenzer <goliath@infraroot.at>
+ */
+#include "config.h"
+
+#include "util/util.h"
+#include "fstree.h"
+#include "mkfs.h"
+
+#include "sqfs/block.h"
+
+#include <string.h>
+#include <stdlib.h>
+#include <ctype.h>
+
+static int decode_priority(const char *filename, size_t line_no,
+			   char *line, sqfs_s64 *priority)
+{
+	bool negative = false;
+	size_t i = 0;
+
+	if (line[0] == '-') {
+		negative = true;
+		i = 1;
+	}
+
+	if (!isdigit(line[i]))
+		goto fail_number;
+
+	*priority = 0;
+
+	for (; isdigit(line[i]); ++i) {
+		sqfs_s64 x = line[i] - '0';
+
+		if ((*priority) >= ((0x7FFFFFFFFFFFFFFFL - x) / 10L))
+			goto fail_ov;
+
+		(*priority) = (*priority) * 10 + x;
+	}
+
+	if (!isspace(line[i]))
+		goto fail_filename;
+
+	while (isspace(line[i]))
+		++i;
+
+	if (line[i] == '\0')
+		goto fail_filename;
+
+	if (negative)
+		(*priority) = -(*priority);
+
+	memmove(line, line + i, strlen(line + i) + 1);
+	return 0;
+fail_number:
+	fprintf(stderr, "%s: " PRI_SZ ": Line must start with "
+		"numeric sort priority.\n",
+		filename, line_no);
+	return -1;
+fail_ov:
+	fprintf(stderr, "%s: " PRI_SZ ": Numeric overflow in sort priority.\n",
+		filename, line_no);
+	return -1;
+fail_filename:
+	fprintf(stderr, "%s: " PRI_SZ ": Expacted `<space> <filename>` "
+		"after sort priority.\n",
+		filename, line_no);
+	return -1;
+}
+
+static int decode_filename(const char *filename, size_t line_no, char *buffer)
+{
+	char *src, *dst;
+
+	if (buffer[0] == '"') {
+		src = buffer + 1;
+		dst = buffer;
+
+		for (;;) {
+			if (src[0] == '\0')
+				goto fail_match;
+
+			if (src[0] == '"') {
+				++src;
+				break;
+			}
+
+			if (src[0] == '\\') {
+				switch (src[1]) {
+				case '\\':
+					*(dst++) = '\\';
+					src += 2;
+					break;
+				case '"':
+					*(dst++) = '"';
+					src += 2;
+					break;
+				default:
+					goto fail_escape;
+				}
+			} else {
+				*(dst++) = *(src++);
+			}
+		}
+
+		if (*src != '\0')
+			return -1;
+	}
+
+	if (canonicalize_name(buffer))
+		goto fail_canon;
+	return 0;
+fail_canon:
+	fprintf(stderr, "%s: " PRI_SZ ": Malformed filename.\n",
+		filename, line_no);
+	return -1;
+fail_escape:
+	fprintf(stderr, "%s: " PRI_SZ ": Unknown escape sequence `\\%c` "
+		"in filename.\n", filename, line_no, src[1]);
+	return -1;
+fail_match:
+	fprintf(stderr, "%s: " PRI_SZ ": Unmatched '\"' in filename.\n",
+		filename, line_no);
+	return -1;
+}
+
+static int decode_flags(const char *filename, size_t line_no, bool *do_glob,
+			bool *path_glob, int *flags, char *line)
+{
+	char *start = line;
+
+	*do_glob = false;
+	*path_glob = false;
+	*flags = 0;
+
+	if (*(line++) != '[')
+		return 0;
+
+	for (;;) {
+		while (isspace(*line))
+			++line;
+
+		if (*line == ']') {
+			++line;
+			break;
+		}
+
+		if (strncmp(line, "glob_no_path", 12) == 0) {
+			line += 12;
+			*do_glob = true;
+			*path_glob = false;
+		} else if (strncmp(line, "glob", 4) == 0) {
+			line += 4;
+			*do_glob = true;
+			*path_glob = true;
+		} else if (strncmp(line, "dont_fragment", 13) == 0) {
+			line += 13;
+			(*flags) |= SQFS_BLK_DONT_FRAGMENT;
+		} else if (strncmp(line, "align", 5) == 0) {
+			line += 5;
+			(*flags) |= SQFS_BLK_ALIGN;
+		} else if (strncmp(line, "dont_compress", 13) == 0) {
+			line += 13;
+			(*flags) |= SQFS_BLK_DONT_COMPRESS;
+		} else if (strncmp(line, "dont_deduplicate", 16) == 0) {
+			line += 16;
+			(*flags) |= SQFS_BLK_DONT_DEDUPLICATE;
+		} else if (strncmp(line, "nosparse", 8) == 0) {
+			line += 8;
+			(*flags) |= SQFS_BLK_IGNORE_SPARSE;
+		} else {
+			goto fail_flag;
+		}
+
+		while (isspace(*line))
+			++line;
+
+		if (*line == ']') {
+			++line;
+			break;
+		}
+
+		if (*(line++) != ',')
+			goto fail_sep;
+	}
+
+	if (!isspace(*line))
+		goto fail_fname;
+
+	while (isspace(*line))
+		++line;
+
+	memmove(start, line, strlen(line) + 1);
+	return 0;
+fail_fname:
+	fprintf(stderr, "%s: " PRI_SZ ": Expected `<space> <filename>` "
+		"after flag list.\n", filename, line_no);
+	return -1;
+fail_sep:
+	fprintf(stderr, "%s: " PRI_SZ ": Unexpected '%c' after flag.\n",
+		filename, line_no, *line);
+	return -1;
+fail_flag:
+	fprintf(stderr, "%s: " PRI_SZ ": Unknown flag `%.3s...`.\n",
+		filename, line_no, line);
+	return -1;
+}
+
+static void sort_file_list(fstree_t *fs)
+{
+	file_info_t *out = NULL, *out_last = NULL;
+
+	while (fs->files != NULL) {
+		sqfs_s64 lowest = fs->files->priority;
+		file_info_t *it, *prev;
+
+		for (it = fs->files; it != NULL; it = it->next) {
+			if (it->priority < lowest)
+				lowest = it->priority;
+		}
+
+		it = fs->files;
+		prev = NULL;
+
+		while (it != NULL) {
+			if (it->priority != lowest) {
+				prev = it;
+				it = it->next;
+				continue;
+			}
+
+			if (prev == NULL) {
+				fs->files = it->next;
+			} else {
+				prev->next = it->next;
+			}
+
+			if (out == NULL) {
+				out = it;
+			} else {
+				out_last->next = it;
+			}
+
+			out_last = it;
+			it = it->next;
+			out_last->next = NULL;
+		}
+	}
+
+	fs->files = out;
+}
+
+int fstree_sort_files(fstree_t *fs, istream_t *sortfile)
+{
+	const char *filename;
+	size_t line_num = 1;
+	file_info_t *it;
+
+	for (it = fs->files; it != NULL; it = it->next) {
+		it->priority = 0;
+		it->flags = 0;
+		it->already_matched = false;
+	}
+
+	filename = istream_get_filename(sortfile);
+
+	for (;;) {
+		bool do_glob, path_glob, have_match;
+		char *line = NULL;
+		sqfs_s64 priority;
+		int ret, flags;
+
+		ret = istream_get_line(sortfile, &line, &line_num,
+				       ISTREAM_LINE_LTRIM |
+				       ISTREAM_LINE_RTRIM |
+				       ISTREAM_LINE_SKIP_EMPTY);
+		if (ret != 0) {
+			free(line);
+			if (ret < 0)
+				return -1;
+			break;
+		}
+
+		if (line[0] == '#') {
+			free(line);
+			continue;
+		}
+
+		if (decode_priority(filename, line_num, line, &priority)) {
+			free(line);
+			return -1;
+		}
+
+		if (decode_flags(filename, line_num, &do_glob, &path_glob,
+				 &flags, line)) {
+			free(line);
+			return -1;
+		}
+
+		if (decode_filename(filename, line_num, line)) {
+			free(line);
+			return -1;
+		}
+
+		have_match = false;
+
+		for (it = fs->files; it != NULL; it = it->next) {
+			tree_node_t *node;
+			char *path;
+
+			if (it->already_matched)
+				continue;
+
+			node = container_of(it, tree_node_t, data.file);
+			path = fstree_get_path(node);
+			if (path == NULL) {
+				fprintf(stderr, "%s: " PRI_SZ ": out-of-memory\n",
+					filename, line_num);
+				free(line);
+				return -1;
+			}
+
+			if (canonicalize_name(path)) {
+				fprintf(stderr,
+					"%s: " PRI_SZ ": [BUG] error "
+					"reconstructing node path\n",
+					filename, line_num);
+				free(line);
+				free(path);
+				return -1;
+			}
+
+			if (do_glob) {
+				ret = fnmatch(line, path,
+					      path_glob ? FNM_PATHNAME : 0);
+
+			} else {
+				ret = strcmp(path, line);
+			}
+
+			free(path);
+
+			if (ret == 0) {
+				have_match = true;
+				it->flags = flags;
+				it->priority = priority;
+				it->already_matched = true;
+
+				if (!do_glob)
+					break;
+			}
+		}
+
+		if (!have_match) {
+			fprintf(stderr, "WARNING: %s: " PRI_SZ ": no match "
+				"for '%s'.\n",
+				filename, line_num, line);
+		}
+
+		free(line);
+	}
+
+	sort_file_list(fs);
+	return 0;
+}
diff --git a/bin/rdsquashfs/Makemodule.am b/bin/rdsquashfs/Makemodule.am
index 1ff9c60..f8f9d3d 100644
--- a/bin/rdsquashfs/Makemodule.am
+++ b/bin/rdsquashfs/Makemodule.am
@@ -1,8 +1,8 @@
-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_SOURCES += bin/rdsquashfs/stat.c
+rdsquashfs_SOURCES = bin/rdsquashfs/src/rdsquashfs.c \
+	bin/rdsquashfs/src/rdsquashfs.h bin/rdsquashfs/src/list_files.c \
+	bin/rdsquashfs/src/options.c bin/rdsquashfs/src/restore_fstree.c \
+	bin/rdsquashfs/src/describe.c bin/rdsquashfs/src/fill_files.c \
+	bin/rdsquashfs/src/dump_xattrs.c bin/rdsquashfs/src/stat.c
 rdsquashfs_CFLAGS = $(AM_CFLAGS) $(PTHREAD_CFLAGS)
 rdsquashfs_LDADD = libcommon.a libio.a libcompat.a libutil.a libsquashfs.la
 rdsquashfs_LDADD += libfstree.a $(LZO_LIBS) $(PTHREAD_LIBS)
diff --git a/bin/rdsquashfs/describe.c b/bin/rdsquashfs/describe.c
deleted file mode 100644
index 540b126..0000000
--- a/bin/rdsquashfs/describe.c
+++ /dev/null
@@ -1,139 +0,0 @@
-/* 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, bool dont_escape)
-{
-	char *start, *ptr, *name;
-	int ret;
-
-	ret = sqfs_tree_node_get_path(n, &name);
-	if (ret != 0) {
-		sqfs_perror(NULL, "Recovering file path of tree node", ret);
-		return -1;
-	}
-
-	if (canonicalize_name(name) != 0) {
-		fprintf(stderr, "Error sanitizing file path '%s'\n", name);
-		sqfs_free(name);
-		return -1;
-	}
-
-	if (dont_escape || (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);
-	}
-
-	sqfs_free(name);
-	return 0;
-}
-
-static void print_perm(const sqfs_tree_node_t *n)
-{
-	printf(" 0%o %u %u", (unsigned int)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, false))
-		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;
-
-	if (!is_filename_sane((const char *)root->name, false)) {
-		fprintf(stderr, "Encountered illegal file name '%s'\n",
-			root->name);
-		return -1;
-	}
-
-	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, false))
-			return -1;
-		print_perm(root);
-		printf(" %s/", unpack_root);
-		if (print_name(root, true))
-			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 %u %u",
-			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;
-	default:
-		break;
-	}
-
-	return 0;
-}
diff --git a/bin/rdsquashfs/dump_xattrs.c b/bin/rdsquashfs/dump_xattrs.c
deleted file mode 100644
index 9dbe437..0000000
--- a/bin/rdsquashfs/dump_xattrs.c
+++ /dev/null
@@ -1,120 +0,0 @@
-/* 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);
-			sqfs_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");
-		}
-
-		sqfs_free(key);
-		sqfs_free(value);
-	}
-
-	return 0;
-}
diff --git a/bin/rdsquashfs/fill_files.c b/bin/rdsquashfs/fill_files.c
deleted file mode 100644
index 3104146..0000000
--- a/bin/rdsquashfs/fill_files.c
+++ /dev/null
@@ -1,186 +0,0 @@
-/* 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)
-{
-	struct file_ent *new;
-	size_t new_sz;
-	char *path;
-	int ret;
-
-	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;
-	}
-
-	ret = sqfs_tree_node_get_path(node, &path);
-	if (ret != 0) {
-		sqfs_perror(NULL, "assembling file path", ret);
-		return -1;
-	}
-
-	if (canonicalize_name(path)) {
-		fprintf(stderr, "Invalid file path '%s'\n", path);
-		sqfs_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)
-		sqfs_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)
-{
-	int ret, openflags;
-	ostream_t *fp;
-	size_t i;
-
-	openflags = OSTREAM_OPEN_OVERWRITE;
-
-	if (flags & UNPACK_NO_SPARSE)
-		openflags |= OSTREAM_OPEN_SPARSE;
-
-	for (i = 0; i < num_files; ++i) {
-		fp = ostream_open_file(files[i].path, openflags);
-		if (fp == NULL)
-			return -1;
-
-		if (!(flags & UNPACK_QUIET))
-			printf("unpacking %s\n", files[i].path);
-
-		ret = sqfs_data_reader_dump(files[i].path, data, files[i].inode,
-					    fp, block_size);
-		if (ret == 0)
-			ret = ostream_flush(fp);
-
-		sqfs_drop(fp);
-		if (ret)
-			return -1;
-	}
-
-	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
deleted file mode 100644
index b1a0102..0000000
--- a/bin/rdsquashfs/list_files.c
+++ /dev/null
@@ -1,158 +0,0 @@
-/* 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;
-	default:                              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;
-	default:                              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
deleted file mode 100644
index dbb5e40..0000000
--- a/bin/rdsquashfs/options.c
+++ /dev/null
@@ -1,226 +0,0 @@
-/* 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' },
-	{ "stat", required_argument, NULL, 's' },
-	{ "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:s: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"
-"  --stat, -s <path>         Dump all information that can be extracted from\n"
-"                            the inode coresponding to a path, including\n"
-"                            SquashFS specific internals.\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 's':
-			opt->op = OP_STAT;
-			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->op == OP_STAT) {
-		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
deleted file mode 100644
index bdcc5a0..0000000
--- a/bin/rdsquashfs/rdsquashfs.c
+++ /dev/null
@@ -1,275 +0,0 @@
-/* SPDX-License-Identifier: GPL-3.0-or-later */
-/*
- * rdsquashfs.c
- *
- * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
- */
-#include "rdsquashfs.h"
-
-static sqfs_tree_node_t *list_merge(sqfs_tree_node_t *lhs,
-				    sqfs_tree_node_t *rhs)
-{
-	sqfs_tree_node_t *it, *head = NULL, **next_ptr = &head;
-
-	while (lhs != NULL && rhs != NULL) {
-		if (strcmp((const char *)lhs->name,
-			   (const char *)rhs->name) <= 0) {
-			it = lhs;
-			lhs = lhs->next;
-		} else {
-			it = rhs;
-			rhs = rhs->next;
-		}
-
-		*next_ptr = it;
-		next_ptr = &it->next;
-	}
-
-	it = (lhs != NULL ? lhs : rhs);
-	*next_ptr = it;
-	return head;
-}
-
-static sqfs_tree_node_t *list_sort(sqfs_tree_node_t *head)
-{
-	sqfs_tree_node_t *it, *half, *prev;
-
-	it = half = prev = head;
-
-	while (it != NULL) {
-		prev = half;
-		half = half->next;
-		it = it->next;
-
-		if (it != NULL)
-			it = it->next;
-	}
-
-	if (half == NULL)
-		return head;
-
-	prev->next = NULL;
-
-	return list_merge(list_sort(head), list_sort(half));
-}
-
-static int tree_sort(sqfs_tree_node_t *root)
-{
-	sqfs_tree_node_t *it;
-
-	if (root->children == NULL)
-		return 0;
-
-	root->children = list_sort(root->children);
-
-	/*
-	  XXX: not only an inconvenience but a security issue: e.g. we unpack a
-	  SquashFS image that has a symlink pointing somewhere, and then a
-	  sub-directory or file with the same name, the unpacker can be tricked
-	  to follow the symlink and write anything, anywhere on the filesystem.
-	 */
-	for (it = root->children; it->next != NULL; it = it->next) {
-		if (strcmp((const char *)it->name,
-			   (const char *)it->next->name) == 0) {
-			char *path;
-			int ret;
-
-			ret = sqfs_tree_node_get_path(it, &path);
-
-			if (ret == 0) {
-				fprintf(stderr,
-					"Entry '%s' found more than once!\n",
-					path);
-			} else {
-				fputs("Entry found more than once!\n", stderr);
-			}
-
-			sqfs_free(path);
-			return -1;
-		}
-	}
-
-	for (it = root->children; it != NULL; it = it->next) {
-		if (tree_sort(it))
-			return -1;
-	}
-
-	return 0;
-}
-
-int main(int argc, char **argv)
-{
-	sqfs_xattr_reader_t *xattr = NULL;
-	sqfs_data_reader_t *data = NULL;
-	sqfs_dir_reader_t *dirrd = NULL;
-	sqfs_compressor_t *cmp = NULL;
-	sqfs_id_table_t *idtbl = NULL;
-	sqfs_compressor_config_t cfg;
-	sqfs_tree_node_t *n = NULL;
-	int status = EXIT_FAILURE;
-	sqfs_file_t *file = NULL;
-	sqfs_super_t super;
-	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;
-	}
-
-	ret = sqfs_super_read(&super, file);
-	if (ret) {
-		sqfs_perror(opt.image_name, "reading super block", ret);
-		goto out;
-	}
-
-	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;
-	}
-
-	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;
-		}
-
-		ret = sqfs_xattr_reader_load(xattr, &super, file, cmp);
-		if (ret) {
-			sqfs_perror(opt.image_name, "loading xattr table",
-				    ret);
-			goto out;
-		}
-	}
-
-	idtbl = sqfs_id_table_create(0);
-	if (idtbl == NULL) {
-		sqfs_perror(opt.image_name, "creating ID table",
-			    SQFS_ERROR_ALLOC);
-		goto out;
-	}
-
-	ret = sqfs_id_table_read(idtbl, file, &super, cmp);
-	if (ret) {
-		sqfs_perror(opt.image_name, "loading ID table", ret);
-		goto out;
-	}
-
-	dirrd = sqfs_dir_reader_create(&super, cmp, file, 0);
-	if (dirrd == NULL) {
-		sqfs_perror(opt.image_name, "creating dir reader",
-			    SQFS_ERROR_ALLOC);
-		goto out;
-	}
-
-	data = sqfs_data_reader_create(file, super.block_size, cmp, 0);
-	if (data == NULL) {
-		sqfs_perror(opt.image_name, "creating data reader",
-			    SQFS_ERROR_ALLOC);
-		goto out;
-	}
-
-	ret = sqfs_data_reader_load_fragment_table(data, &super);
-	if (ret) {
-		sqfs_perror(opt.image_name, "loading fragment table", ret);
-		goto out;
-	}
-
-	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;
-	}
-
-	switch (opt.op) {
-	case OP_LS:
-		list_files(n);
-		break;
-	case OP_STAT:
-		if (stat_file(n))
-			goto out;
-		break;
-	case OP_CAT: {
-		ostream_t *fp;
-
-		if (!S_ISREG(n->inode->base.mode)) {
-			fprintf(stderr, "/%s: not a regular file\n",
-				opt.cmdpath);
-			goto out;
-		}
-
-		fp = ostream_open_stdout();
-		if (fp == NULL)
-			goto out;
-
-		ret = sqfs_data_reader_dump(opt.cmdpath, data, n->inode,
-					    fp, super.block_size);
-		sqfs_drop(fp);
-		if (ret)
-			goto out;
-		break;
-	}
-	case OP_UNPACK:
-		if (tree_sort(n))
-			goto out;
-
-		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;
-	default:
-		break;
-	}
-
-	status = EXIT_SUCCESS;
-out:
-	sqfs_dir_tree_destroy(n);
-	sqfs_drop(data);
-	sqfs_drop(dirrd);
-	sqfs_drop(idtbl);
-	sqfs_drop(xattr);
-	sqfs_drop(cmp);
-	sqfs_drop(file);
-	free(opt.cmdpath);
-	return status;
-}
diff --git a/bin/rdsquashfs/rdsquashfs.h b/bin/rdsquashfs/rdsquashfs.h
deleted file mode 100644
index 56bb836..0000000
--- a/bin/rdsquashfs/rdsquashfs.h
+++ /dev/null
@@ -1,82 +0,0 @@
-/* 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"
-#include "util/util.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>
-#include <time.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,
-	OP_STAT,
-};
-
-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 stat_file(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
deleted file mode 100644
index ea9d4f1..0000000
--- a/bin/rdsquashfs/restore_fstree.c
+++ /dev/null
@@ -1,336 +0,0 @@
-/* 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, int flags)
-{
-	WCHAR *wpath;
-	HANDLE fh;
-	(void)flags;
-
-	wpath = path_to_windows(name);
-	if (wpath == NULL)
-		return -1;
-
-	switch (n->inode->base.mode & S_IFMT) {
-	case S_IFDIR:
-		if (!CreateDirectoryW(wpath, NULL)) {
-			if (GetLastError() != ERROR_ALREADY_EXISTS)
-				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: {
-	DWORD err = GetLastError();
-	free(wpath);
-	SetLastError(err);
-	w32_perror(name);
-
-	if (err == ERROR_FILE_EXISTS) {
-		fputs("\nHINT: this could be caused by case "
-		      "sensitivity on Windows.\n", stderr);
-	}
-	return -1;
-}
-}
-#else
-static int create_node(const sqfs_tree_node_t *n, const char *name, int flags)
-{
-	sqfs_u32 devno;
-	int fd, mode;
-
-	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:
-		if (flags & UNPACK_CHMOD) {
-			mode = (n->inode->base.mode & ~S_IFMT) | 0200;
-		} else {
-			mode = 0644;
-		}
-
-		fd = open(name, O_WRONLY | O_CREAT | O_EXCL, mode);
-
-		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;
-	}
-
-	ret = sqfs_tree_node_get_path(n, &name);
-	if (ret != 0) {
-		sqfs_perror((const char *)n->name,
-			    "constructing full path", ret);
-		return -1;
-	}
-
-	ret = canonicalize_name(name);
-	assert(ret == 0);
-
-	if (!(flags & UNPACK_QUIET))
-		printf("creating %s\n", name);
-
-	ret = create_node(n, name, flags);
-	sqfs_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);
-			sqfs_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));
-		}
-
-		sqfs_free(key);
-		sqfs_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;
-		}
-	}
-
-	ret = sqfs_tree_node_get_path(n, &path);
-	if (ret != 0) {
-		sqfs_perror(NULL, "reconstructing full path", ret);
-		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;
-		}
-	}
-
-	sqfs_free(path);
-	return 0;
-fail:
-	sqfs_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/rdsquashfs/src/describe.c b/bin/rdsquashfs/src/describe.c
new file mode 100644
index 0000000..540b126
--- /dev/null
+++ b/bin/rdsquashfs/src/describe.c
@@ -0,0 +1,139 @@
+/* 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, bool dont_escape)
+{
+	char *start, *ptr, *name;
+	int ret;
+
+	ret = sqfs_tree_node_get_path(n, &name);
+	if (ret != 0) {
+		sqfs_perror(NULL, "Recovering file path of tree node", ret);
+		return -1;
+	}
+
+	if (canonicalize_name(name) != 0) {
+		fprintf(stderr, "Error sanitizing file path '%s'\n", name);
+		sqfs_free(name);
+		return -1;
+	}
+
+	if (dont_escape || (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);
+	}
+
+	sqfs_free(name);
+	return 0;
+}
+
+static void print_perm(const sqfs_tree_node_t *n)
+{
+	printf(" 0%o %u %u", (unsigned int)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, false))
+		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;
+
+	if (!is_filename_sane((const char *)root->name, false)) {
+		fprintf(stderr, "Encountered illegal file name '%s'\n",
+			root->name);
+		return -1;
+	}
+
+	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, false))
+			return -1;
+		print_perm(root);
+		printf(" %s/", unpack_root);
+		if (print_name(root, true))
+			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 %u %u",
+			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;
+	default:
+		break;
+	}
+
+	return 0;
+}
diff --git a/bin/rdsquashfs/src/dump_xattrs.c b/bin/rdsquashfs/src/dump_xattrs.c
new file mode 100644
index 0000000..9dbe437
--- /dev/null
+++ b/bin/rdsquashfs/src/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);
+			sqfs_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");
+		}
+
+		sqfs_free(key);
+		sqfs_free(value);
+	}
+
+	return 0;
+}
diff --git a/bin/rdsquashfs/src/fill_files.c b/bin/rdsquashfs/src/fill_files.c
new file mode 100644
index 0000000..3104146
--- /dev/null
+++ b/bin/rdsquashfs/src/fill_files.c
@@ -0,0 +1,186 @@
+/* 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)
+{
+	struct file_ent *new;
+	size_t new_sz;
+	char *path;
+	int ret;
+
+	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;
+	}
+
+	ret = sqfs_tree_node_get_path(node, &path);
+	if (ret != 0) {
+		sqfs_perror(NULL, "assembling file path", ret);
+		return -1;
+	}
+
+	if (canonicalize_name(path)) {
+		fprintf(stderr, "Invalid file path '%s'\n", path);
+		sqfs_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)
+		sqfs_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)
+{
+	int ret, openflags;
+	ostream_t *fp;
+	size_t i;
+
+	openflags = OSTREAM_OPEN_OVERWRITE;
+
+	if (flags & UNPACK_NO_SPARSE)
+		openflags |= OSTREAM_OPEN_SPARSE;
+
+	for (i = 0; i < num_files; ++i) {
+		fp = ostream_open_file(files[i].path, openflags);
+		if (fp == NULL)
+			return -1;
+
+		if (!(flags & UNPACK_QUIET))
+			printf("unpacking %s\n", files[i].path);
+
+		ret = sqfs_data_reader_dump(files[i].path, data, files[i].inode,
+					    fp, block_size);
+		if (ret == 0)
+			ret = ostream_flush(fp);
+
+		sqfs_drop(fp);
+		if (ret)
+			return -1;
+	}
+
+	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/src/list_files.c b/bin/rdsquashfs/src/list_files.c
new file mode 100644
index 0000000..b1a0102
--- /dev/null
+++ b/bin/rdsquashfs/src/list_files.c
@@ -0,0 +1,158 @@
+/* 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;
+	default:                              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;
+	default:                              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/src/options.c b/bin/rdsquashfs/src/options.c
new file mode 100644
index 0000000..dbb5e40
--- /dev/null
+++ b/bin/rdsquashfs/src/options.c
@@ -0,0 +1,226 @@
+/* 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' },
+	{ "stat", required_argument, NULL, 's' },
+	{ "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:s: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"
+"  --stat, -s <path>         Dump all information that can be extracted from\n"
+"                            the inode coresponding to a path, including\n"
+"                            SquashFS specific internals.\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 's':
+			opt->op = OP_STAT;
+			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->op == OP_STAT) {
+		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/src/rdsquashfs.c b/bin/rdsquashfs/src/rdsquashfs.c
new file mode 100644
index 0000000..bdcc5a0
--- /dev/null
+++ b/bin/rdsquashfs/src/rdsquashfs.c
@@ -0,0 +1,275 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * rdsquashfs.c
+ *
+ * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
+ */
+#include "rdsquashfs.h"
+
+static sqfs_tree_node_t *list_merge(sqfs_tree_node_t *lhs,
+				    sqfs_tree_node_t *rhs)
+{
+	sqfs_tree_node_t *it, *head = NULL, **next_ptr = &head;
+
+	while (lhs != NULL && rhs != NULL) {
+		if (strcmp((const char *)lhs->name,
+			   (const char *)rhs->name) <= 0) {
+			it = lhs;
+			lhs = lhs->next;
+		} else {
+			it = rhs;
+			rhs = rhs->next;
+		}
+
+		*next_ptr = it;
+		next_ptr = &it->next;
+	}
+
+	it = (lhs != NULL ? lhs : rhs);
+	*next_ptr = it;
+	return head;
+}
+
+static sqfs_tree_node_t *list_sort(sqfs_tree_node_t *head)
+{
+	sqfs_tree_node_t *it, *half, *prev;
+
+	it = half = prev = head;
+
+	while (it != NULL) {
+		prev = half;
+		half = half->next;
+		it = it->next;
+
+		if (it != NULL)
+			it = it->next;
+	}
+
+	if (half == NULL)
+		return head;
+
+	prev->next = NULL;
+
+	return list_merge(list_sort(head), list_sort(half));
+}
+
+static int tree_sort(sqfs_tree_node_t *root)
+{
+	sqfs_tree_node_t *it;
+
+	if (root->children == NULL)
+		return 0;
+
+	root->children = list_sort(root->children);
+
+	/*
+	  XXX: not only an inconvenience but a security issue: e.g. we unpack a
+	  SquashFS image that has a symlink pointing somewhere, and then a
+	  sub-directory or file with the same name, the unpacker can be tricked
+	  to follow the symlink and write anything, anywhere on the filesystem.
+	 */
+	for (it = root->children; it->next != NULL; it = it->next) {
+		if (strcmp((const char *)it->name,
+			   (const char *)it->next->name) == 0) {
+			char *path;
+			int ret;
+
+			ret = sqfs_tree_node_get_path(it, &path);
+
+			if (ret == 0) {
+				fprintf(stderr,
+					"Entry '%s' found more than once!\n",
+					path);
+			} else {
+				fputs("Entry found more than once!\n", stderr);
+			}
+
+			sqfs_free(path);
+			return -1;
+		}
+	}
+
+	for (it = root->children; it != NULL; it = it->next) {
+		if (tree_sort(it))
+			return -1;
+	}
+
+	return 0;
+}
+
+int main(int argc, char **argv)
+{
+	sqfs_xattr_reader_t *xattr = NULL;
+	sqfs_data_reader_t *data = NULL;
+	sqfs_dir_reader_t *dirrd = NULL;
+	sqfs_compressor_t *cmp = NULL;
+	sqfs_id_table_t *idtbl = NULL;
+	sqfs_compressor_config_t cfg;
+	sqfs_tree_node_t *n = NULL;
+	int status = EXIT_FAILURE;
+	sqfs_file_t *file = NULL;
+	sqfs_super_t super;
+	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;
+	}
+
+	ret = sqfs_super_read(&super, file);
+	if (ret) {
+		sqfs_perror(opt.image_name, "reading super block", ret);
+		goto out;
+	}
+
+	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;
+	}
+
+	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;
+		}
+
+		ret = sqfs_xattr_reader_load(xattr, &super, file, cmp);
+		if (ret) {
+			sqfs_perror(opt.image_name, "loading xattr table",
+				    ret);
+			goto out;
+		}
+	}
+
+	idtbl = sqfs_id_table_create(0);
+	if (idtbl == NULL) {
+		sqfs_perror(opt.image_name, "creating ID table",
+			    SQFS_ERROR_ALLOC);
+		goto out;
+	}
+
+	ret = sqfs_id_table_read(idtbl, file, &super, cmp);
+	if (ret) {
+		sqfs_perror(opt.image_name, "loading ID table", ret);
+		goto out;
+	}
+
+	dirrd = sqfs_dir_reader_create(&super, cmp, file, 0);
+	if (dirrd == NULL) {
+		sqfs_perror(opt.image_name, "creating dir reader",
+			    SQFS_ERROR_ALLOC);
+		goto out;
+	}
+
+	data = sqfs_data_reader_create(file, super.block_size, cmp, 0);
+	if (data == NULL) {
+		sqfs_perror(opt.image_name, "creating data reader",
+			    SQFS_ERROR_ALLOC);
+		goto out;
+	}
+
+	ret = sqfs_data_reader_load_fragment_table(data, &super);
+	if (ret) {
+		sqfs_perror(opt.image_name, "loading fragment table", ret);
+		goto out;
+	}
+
+	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;
+	}
+
+	switch (opt.op) {
+	case OP_LS:
+		list_files(n);
+		break;
+	case OP_STAT:
+		if (stat_file(n))
+			goto out;
+		break;
+	case OP_CAT: {
+		ostream_t *fp;
+
+		if (!S_ISREG(n->inode->base.mode)) {
+			fprintf(stderr, "/%s: not a regular file\n",
+				opt.cmdpath);
+			goto out;
+		}
+
+		fp = ostream_open_stdout();
+		if (fp == NULL)
+			goto out;
+
+		ret = sqfs_data_reader_dump(opt.cmdpath, data, n->inode,
+					    fp, super.block_size);
+		sqfs_drop(fp);
+		if (ret)
+			goto out;
+		break;
+	}
+	case OP_UNPACK:
+		if (tree_sort(n))
+			goto out;
+
+		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;
+	default:
+		break;
+	}
+
+	status = EXIT_SUCCESS;
+out:
+	sqfs_dir_tree_destroy(n);
+	sqfs_drop(data);
+	sqfs_drop(dirrd);
+	sqfs_drop(idtbl);
+	sqfs_drop(xattr);
+	sqfs_drop(cmp);
+	sqfs_drop(file);
+	free(opt.cmdpath);
+	return status;
+}
diff --git a/bin/rdsquashfs/src/rdsquashfs.h b/bin/rdsquashfs/src/rdsquashfs.h
new file mode 100644
index 0000000..56bb836
--- /dev/null
+++ b/bin/rdsquashfs/src/rdsquashfs.h
@@ -0,0 +1,82 @@
+/* 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"
+#include "util/util.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>
+#include <time.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,
+	OP_STAT,
+};
+
+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 stat_file(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/src/restore_fstree.c b/bin/rdsquashfs/src/restore_fstree.c
new file mode 100644
index 0000000..ea9d4f1
--- /dev/null
+++ b/bin/rdsquashfs/src/restore_fstree.c
@@ -0,0 +1,336 @@
+/* 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, int flags)
+{
+	WCHAR *wpath;
+	HANDLE fh;
+	(void)flags;
+
+	wpath = path_to_windows(name);
+	if (wpath == NULL)
+		return -1;
+
+	switch (n->inode->base.mode & S_IFMT) {
+	case S_IFDIR:
+		if (!CreateDirectoryW(wpath, NULL)) {
+			if (GetLastError() != ERROR_ALREADY_EXISTS)
+				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: {
+	DWORD err = GetLastError();
+	free(wpath);
+	SetLastError(err);
+	w32_perror(name);
+
+	if (err == ERROR_FILE_EXISTS) {
+		fputs("\nHINT: this could be caused by case "
+		      "sensitivity on Windows.\n", stderr);
+	}
+	return -1;
+}
+}
+#else
+static int create_node(const sqfs_tree_node_t *n, const char *name, int flags)
+{
+	sqfs_u32 devno;
+	int fd, mode;
+
+	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:
+		if (flags & UNPACK_CHMOD) {
+			mode = (n->inode->base.mode & ~S_IFMT) | 0200;
+		} else {
+			mode = 0644;
+		}
+
+		fd = open(name, O_WRONLY | O_CREAT | O_EXCL, mode);
+
+		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;
+	}
+
+	ret = sqfs_tree_node_get_path(n, &name);
+	if (ret != 0) {
+		sqfs_perror((const char *)n->name,
+			    "constructing full path", ret);
+		return -1;
+	}
+
+	ret = canonicalize_name(name);
+	assert(ret == 0);
+
+	if (!(flags & UNPACK_QUIET))
+		printf("creating %s\n", name);
+
+	ret = create_node(n, name, flags);
+	sqfs_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);
+			sqfs_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));
+		}
+
+		sqfs_free(key);
+		sqfs_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;
+		}
+	}
+
+	ret = sqfs_tree_node_get_path(n, &path);
+	if (ret != 0) {
+		sqfs_perror(NULL, "reconstructing full path", ret);
+		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;
+		}
+	}
+
+	sqfs_free(path);
+	return 0;
+fail:
+	sqfs_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/rdsquashfs/src/stat.c b/bin/rdsquashfs/src/stat.c
new file mode 100644
index 0000000..8b4581f
--- /dev/null
+++ b/bin/rdsquashfs/src/stat.c
@@ -0,0 +1,187 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * stat.c
+ *
+ * Copyright (C) 2020 David Oberhollenzer <goliath@infraroot.at>
+ */
+#include "rdsquashfs.h"
+
+static const char *inode_types[] = {
+	[SQFS_INODE_DIR] = "directory",
+	[SQFS_INODE_FILE] = "file",
+	[SQFS_INODE_SLINK] = "symbolic link",
+	[SQFS_INODE_BDEV] = "block device",
+	[SQFS_INODE_CDEV] = "character device",
+	[SQFS_INODE_FIFO] = "named pipe",
+	[SQFS_INODE_SOCKET] = "socket",
+	[SQFS_INODE_EXT_DIR] = "extended directory",
+	[SQFS_INODE_EXT_FILE] = "extended file",
+	[SQFS_INODE_EXT_SLINK] = "extended symbolic link",
+	[SQFS_INODE_EXT_BDEV] = "extended block device",
+	[SQFS_INODE_EXT_CDEV] = "extended character device",
+	[SQFS_INODE_EXT_FIFO] = "extended named pipe",
+	[SQFS_INODE_EXT_SOCKET] = "extended socket",
+};
+
+int stat_file(const sqfs_tree_node_t *node)
+{
+	sqfs_u32 xattr_idx = 0xFFFFFFFF, devno = 0, link_size = 0;
+	const sqfs_inode_generic_t *inode = node->inode;
+	const char *type = NULL, *link_target = NULL;
+	sqfs_u32 frag_idx, frag_offset;
+	bool have_devno = false;
+	sqfs_u64 location, size;
+	unsigned int nlinks = 0;
+	sqfs_dir_index_t *idx;
+	char buffer[64];
+	time_t timeval;
+	struct tm *tm;
+	size_t i;
+	int ret;
+
+	/* decode */
+	if ((size_t)inode->base.type <
+	    sizeof(inode_types) / sizeof(inode_types[0])) {
+		type = inode_types[inode->base.type];
+	}
+
+	sqfs_inode_get_xattr_index(inode, &xattr_idx);
+
+	switch (inode->base.type) {
+	case SQFS_INODE_DIR:
+		nlinks = inode->data.dir.nlink;
+		break;
+	case SQFS_INODE_SLINK:
+		nlinks = inode->data.slink.nlink;
+		link_target = (const char *)inode->extra;
+		link_size = inode->data.slink.target_size;
+		break;
+	case SQFS_INODE_BDEV:
+	case SQFS_INODE_CDEV:
+		nlinks = inode->data.dev.nlink;
+		devno = inode->data.dev.devno;
+		have_devno = true;
+		break;
+	case SQFS_INODE_FIFO:
+	case SQFS_INODE_SOCKET:
+		nlinks = inode->data.ipc.nlink;
+		break;
+	case SQFS_INODE_EXT_DIR:
+		nlinks = inode->data.dir_ext.nlink;
+		break;
+	case SQFS_INODE_EXT_FILE:
+		nlinks = inode->data.file_ext.nlink;
+		break;
+	case SQFS_INODE_EXT_SLINK:
+		nlinks = inode->data.slink_ext.nlink;
+		link_target = (const char *)inode->extra;
+		link_size = inode->data.slink_ext.target_size;
+		break;
+	case SQFS_INODE_EXT_BDEV:
+	case SQFS_INODE_EXT_CDEV:
+		nlinks = inode->data.dev_ext.nlink;
+		devno = inode->data.dev_ext.devno;
+		have_devno = true;
+		break;
+	case SQFS_INODE_EXT_FIFO:
+	case SQFS_INODE_EXT_SOCKET:
+		nlinks = inode->data.ipc_ext.nlink;
+		break;
+	default:
+		break;
+	}
+
+	timeval = inode->base.mod_time;
+	tm = gmtime(&timeval);
+	strftime(buffer, sizeof(buffer), "%a, %d %b %Y %T %z", tm);
+
+	/* info dump */
+	printf("Name: %s\n", (const char *)node->name);
+	printf("Inode type: %s\n", type == NULL ? "UNKNOWN" : type);
+	printf("Inode number: %u\n", inode->base.inode_number);
+	printf("Access: 0%o\n",
+	       (unsigned int)inode->base.mode & ~SQFS_INODE_MODE_MASK);
+	printf("UID: %u (index = %u)\n", node->uid, inode->base.uid_idx);
+	printf("GID: %u (index = %u)\n", node->gid, inode->base.gid_idx);
+	printf("Last modified: %s (%u)\n", buffer, inode->base.mod_time);
+
+	if (type != NULL && inode->base.type != SQFS_INODE_FILE)
+		printf("Hard link count: %u\n", nlinks);
+
+	if (type != NULL && inode->base.type >= SQFS_INODE_EXT_DIR)
+		printf("Xattr index: 0x%X\n", xattr_idx);
+
+	if (link_target != NULL)
+		printf("Link target: %.*s\n", (int)link_size, link_target);
+
+	if (have_devno) {
+		printf("Device number: %u:%u (%u)\n",
+		       major(devno), minor(devno), devno);
+	}
+
+	switch (inode->base.type) {
+	case SQFS_INODE_FILE:
+	case SQFS_INODE_EXT_FILE:
+		sqfs_inode_get_file_block_start(inode, &location);
+		sqfs_inode_get_file_size(inode, &size);
+		sqfs_inode_get_frag_location(inode, &frag_idx, &frag_offset);
+
+		printf("Fragment index: 0x%X\n", frag_idx);
+		printf("Fragment offset: %u\n", frag_offset);
+		printf("File size: %lu\n", (unsigned long)size);
+
+		if (inode->base.type == SQFS_INODE_EXT_FILE) {
+			printf("Sparse: " PRI_U64 "\n",
+			       inode->data.file_ext.sparse);
+		}
+
+		printf("Blocks start: %lu\n", (unsigned long)location);
+		printf("Block count: %lu\n",
+		       (unsigned long)sqfs_inode_get_file_block_count(inode));
+
+		for (i = 0; i < sqfs_inode_get_file_block_count(inode); ++i) {
+			printf("\tBlock #%lu size: %u (%s)\n", (unsigned long)i,
+			       SQFS_ON_DISK_BLOCK_SIZE(inode->extra[i]),
+			       SQFS_IS_BLOCK_COMPRESSED(inode->extra[i]) ?
+			       "compressed" : "uncompressed");
+		}
+		break;
+	case SQFS_INODE_DIR:
+		printf("Start block: %u\n", inode->data.dir.start_block);
+		printf("Offset: %u\n", inode->data.dir.offset);
+		printf("Listing size: %u\n", inode->data.dir.size);
+		printf("Parent inode: %u\n", inode->data.dir.parent_inode);
+		break;
+	case SQFS_INODE_EXT_DIR:
+		printf("Start block: %u\n", inode->data.dir_ext.start_block);
+		printf("Offset: %u\n", inode->data.dir_ext.offset);
+		printf("Listing size: %u\n", inode->data.dir_ext.size);
+		printf("Parent inode: %u\n", inode->data.dir_ext.parent_inode);
+		printf("Directory index entries: %u\n",
+		       inode->data.dir_ext.inodex_count);
+
+		if (inode->data.dir_ext.size == 0)
+			break;
+
+		for (i = 0; ; ++i) {
+			ret = sqfs_inode_unpack_dir_index_entry(inode, &idx, i);
+			if (ret == SQFS_ERROR_OUT_OF_BOUNDS)
+				break;
+			if (ret < 0) {
+				sqfs_perror(NULL, "reading directory index",
+					    ret);
+				return -1;
+			}
+
+			printf("\t'%.*s' -> block %u, header offset %u\n",
+			       (int)(idx->size + 1), idx->name,
+			       idx->start_block, idx->index);
+
+			sqfs_free(idx);
+		}
+		break;
+	default:
+		break;
+	}
+	return 0;
+}
diff --git a/bin/rdsquashfs/stat.c b/bin/rdsquashfs/stat.c
deleted file mode 100644
index 8b4581f..0000000
--- a/bin/rdsquashfs/stat.c
+++ /dev/null
@@ -1,187 +0,0 @@
-/* SPDX-License-Identifier: GPL-3.0-or-later */
-/*
- * stat.c
- *
- * Copyright (C) 2020 David Oberhollenzer <goliath@infraroot.at>
- */
-#include "rdsquashfs.h"
-
-static const char *inode_types[] = {
-	[SQFS_INODE_DIR] = "directory",
-	[SQFS_INODE_FILE] = "file",
-	[SQFS_INODE_SLINK] = "symbolic link",
-	[SQFS_INODE_BDEV] = "block device",
-	[SQFS_INODE_CDEV] = "character device",
-	[SQFS_INODE_FIFO] = "named pipe",
-	[SQFS_INODE_SOCKET] = "socket",
-	[SQFS_INODE_EXT_DIR] = "extended directory",
-	[SQFS_INODE_EXT_FILE] = "extended file",
-	[SQFS_INODE_EXT_SLINK] = "extended symbolic link",
-	[SQFS_INODE_EXT_BDEV] = "extended block device",
-	[SQFS_INODE_EXT_CDEV] = "extended character device",
-	[SQFS_INODE_EXT_FIFO] = "extended named pipe",
-	[SQFS_INODE_EXT_SOCKET] = "extended socket",
-};
-
-int stat_file(const sqfs_tree_node_t *node)
-{
-	sqfs_u32 xattr_idx = 0xFFFFFFFF, devno = 0, link_size = 0;
-	const sqfs_inode_generic_t *inode = node->inode;
-	const char *type = NULL, *link_target = NULL;
-	sqfs_u32 frag_idx, frag_offset;
-	bool have_devno = false;
-	sqfs_u64 location, size;
-	unsigned int nlinks = 0;
-	sqfs_dir_index_t *idx;
-	char buffer[64];
-	time_t timeval;
-	struct tm *tm;
-	size_t i;
-	int ret;
-
-	/* decode */
-	if ((size_t)inode->base.type <
-	    sizeof(inode_types) / sizeof(inode_types[0])) {
-		type = inode_types[inode->base.type];
-	}
-
-	sqfs_inode_get_xattr_index(inode, &xattr_idx);
-
-	switch (inode->base.type) {
-	case SQFS_INODE_DIR:
-		nlinks = inode->data.dir.nlink;
-		break;
-	case SQFS_INODE_SLINK:
-		nlinks = inode->data.slink.nlink;
-		link_target = (const char *)inode->extra;
-		link_size = inode->data.slink.target_size;
-		break;
-	case SQFS_INODE_BDEV:
-	case SQFS_INODE_CDEV:
-		nlinks = inode->data.dev.nlink;
-		devno = inode->data.dev.devno;
-		have_devno = true;
-		break;
-	case SQFS_INODE_FIFO:
-	case SQFS_INODE_SOCKET:
-		nlinks = inode->data.ipc.nlink;
-		break;
-	case SQFS_INODE_EXT_DIR:
-		nlinks = inode->data.dir_ext.nlink;
-		break;
-	case SQFS_INODE_EXT_FILE:
-		nlinks = inode->data.file_ext.nlink;
-		break;
-	case SQFS_INODE_EXT_SLINK:
-		nlinks = inode->data.slink_ext.nlink;
-		link_target = (const char *)inode->extra;
-		link_size = inode->data.slink_ext.target_size;
-		break;
-	case SQFS_INODE_EXT_BDEV:
-	case SQFS_INODE_EXT_CDEV:
-		nlinks = inode->data.dev_ext.nlink;
-		devno = inode->data.dev_ext.devno;
-		have_devno = true;
-		break;
-	case SQFS_INODE_EXT_FIFO:
-	case SQFS_INODE_EXT_SOCKET:
-		nlinks = inode->data.ipc_ext.nlink;
-		break;
-	default:
-		break;
-	}
-
-	timeval = inode->base.mod_time;
-	tm = gmtime(&timeval);
-	strftime(buffer, sizeof(buffer), "%a, %d %b %Y %T %z", tm);
-
-	/* info dump */
-	printf("Name: %s\n", (const char *)node->name);
-	printf("Inode type: %s\n", type == NULL ? "UNKNOWN" : type);
-	printf("Inode number: %u\n", inode->base.inode_number);
-	printf("Access: 0%o\n",
-	       (unsigned int)inode->base.mode & ~SQFS_INODE_MODE_MASK);
-	printf("UID: %u (index = %u)\n", node->uid, inode->base.uid_idx);
-	printf("GID: %u (index = %u)\n", node->gid, inode->base.gid_idx);
-	printf("Last modified: %s (%u)\n", buffer, inode->base.mod_time);
-
-	if (type != NULL && inode->base.type != SQFS_INODE_FILE)
-		printf("Hard link count: %u\n", nlinks);
-
-	if (type != NULL && inode->base.type >= SQFS_INODE_EXT_DIR)
-		printf("Xattr index: 0x%X\n", xattr_idx);
-
-	if (link_target != NULL)
-		printf("Link target: %.*s\n", (int)link_size, link_target);
-
-	if (have_devno) {
-		printf("Device number: %u:%u (%u)\n",
-		       major(devno), minor(devno), devno);
-	}
-
-	switch (inode->base.type) {
-	case SQFS_INODE_FILE:
-	case SQFS_INODE_EXT_FILE:
-		sqfs_inode_get_file_block_start(inode, &location);
-		sqfs_inode_get_file_size(inode, &size);
-		sqfs_inode_get_frag_location(inode, &frag_idx, &frag_offset);
-
-		printf("Fragment index: 0x%X\n", frag_idx);
-		printf("Fragment offset: %u\n", frag_offset);
-		printf("File size: %lu\n", (unsigned long)size);
-
-		if (inode->base.type == SQFS_INODE_EXT_FILE) {
-			printf("Sparse: " PRI_U64 "\n",
-			       inode->data.file_ext.sparse);
-		}
-
-		printf("Blocks start: %lu\n", (unsigned long)location);
-		printf("Block count: %lu\n",
-		       (unsigned long)sqfs_inode_get_file_block_count(inode));
-
-		for (i = 0; i < sqfs_inode_get_file_block_count(inode); ++i) {
-			printf("\tBlock #%lu size: %u (%s)\n", (unsigned long)i,
-			       SQFS_ON_DISK_BLOCK_SIZE(inode->extra[i]),
-			       SQFS_IS_BLOCK_COMPRESSED(inode->extra[i]) ?
-			       "compressed" : "uncompressed");
-		}
-		break;
-	case SQFS_INODE_DIR:
-		printf("Start block: %u\n", inode->data.dir.start_block);
-		printf("Offset: %u\n", inode->data.dir.offset);
-		printf("Listing size: %u\n", inode->data.dir.size);
-		printf("Parent inode: %u\n", inode->data.dir.parent_inode);
-		break;
-	case SQFS_INODE_EXT_DIR:
-		printf("Start block: %u\n", inode->data.dir_ext.start_block);
-		printf("Offset: %u\n", inode->data.dir_ext.offset);
-		printf("Listing size: %u\n", inode->data.dir_ext.size);
-		printf("Parent inode: %u\n", inode->data.dir_ext.parent_inode);
-		printf("Directory index entries: %u\n",
-		       inode->data.dir_ext.inodex_count);
-
-		if (inode->data.dir_ext.size == 0)
-			break;
-
-		for (i = 0; ; ++i) {
-			ret = sqfs_inode_unpack_dir_index_entry(inode, &idx, i);
-			if (ret == SQFS_ERROR_OUT_OF_BOUNDS)
-				break;
-			if (ret < 0) {
-				sqfs_perror(NULL, "reading directory index",
-					    ret);
-				return -1;
-			}
-
-			printf("\t'%.*s' -> block %u, header offset %u\n",
-			       (int)(idx->size + 1), idx->name,
-			       idx->start_block, idx->index);
-
-			sqfs_free(idx);
-		}
-		break;
-	default:
-		break;
-	}
-	return 0;
-}
diff --git a/bin/sqfs2tar/Makemodule.am b/bin/sqfs2tar/Makemodule.am
index 05cee5b..2e6c411 100644
--- a/bin/sqfs2tar/Makemodule.am
+++ b/bin/sqfs2tar/Makemodule.am
@@ -1,6 +1,6 @@
-sqfs2tar_SOURCES = bin/sqfs2tar/sqfs2tar.c bin/sqfs2tar/sqfs2tar.h
-sqfs2tar_SOURCES += bin/sqfs2tar/options.c bin/sqfs2tar/write_tree.c
-sqfs2tar_SOURCES += bin/sqfs2tar/xattr.c
+sqfs2tar_SOURCES = bin/sqfs2tar/src/sqfs2tar.c bin/sqfs2tar/src/sqfs2tar.h \
+	bin/sqfs2tar/src/options.c bin/sqfs2tar/src/write_tree.c \
+	bin/sqfs2tar/src/xattr.c
 sqfs2tar_CFLAGS = $(AM_CFLAGS) $(PTHREAD_CFLAGS)
 sqfs2tar_LDADD = libcommon.a libutil.a libsquashfs.la libtar.a
 sqfs2tar_LDADD += libio.a libxfrm.a libcompat.a libfstree.a
diff --git a/bin/sqfs2tar/options.c b/bin/sqfs2tar/options.c
deleted file mode 100644
index ba1588d..0000000
--- a/bin/sqfs2tar/options.c
+++ /dev/null
@@ -1,212 +0,0 @@
-/* SPDX-License-Identifier: GPL-3.0-or-later */
-/*
- * options.c
- *
- * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
- */
-#include "sqfs2tar.h"
-
-static struct option long_opts[] = {
-	{ "compressor", required_argument, NULL, 'c' },
-	{ "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 = "c: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"
-"  --compressor, -c <name>   If set, stream compress the resulting tarball.\n"
-"                            By default, the tarball is uncompressed.\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"
-"Supported tar compression formats:\n";
-
-bool dont_skip = false;
-bool keep_as_dir = false;
-bool no_xattr = false;
-bool no_links = false;
-
-char *root_becomes = NULL;
-char **subdirs = NULL;
-size_t num_subdirs = 0;
-static size_t max_subdirs = 0;
-int compressor = 0;
-
-const char *filename = NULL;
-
-void process_args(int argc, char **argv)
-{
-	size_t idx, new_count;
-	const char *name;
-	int i, ret;
-	char **new;
-
-	for (;;) {
-		i = getopt_long(argc, argv, short_opts, long_opts, NULL);
-		if (i == -1)
-			break;
-
-		switch (i) {
-		case 'c':
-			compressor = xfrm_compressor_id_from_name(optarg);
-			if (compressor <= 0) {
-				fprintf(stderr, "unknown compressor '%s'.\n",
-					optarg);
-				goto fail;
-			}
-			break;
-		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);
-
-			i = XFRM_COMPRESSOR_MIN;
-
-			while (i <= XFRM_COMPRESSOR_MAX) {
-				name = xfrm_compressor_name_from_id(i);
-				if (name != NULL)
-					printf("\t%s\n", name);
-				++i;
-			}
-
-			fputc('\n', 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);
-}
diff --git a/bin/sqfs2tar/sqfs2tar.c b/bin/sqfs2tar/sqfs2tar.c
deleted file mode 100644
index 43f9e78..0000000
--- a/bin/sqfs2tar/sqfs2tar.c
+++ /dev/null
@@ -1,274 +0,0 @@
-/* SPDX-License-Identifier: GPL-3.0-or-later */
-/*
- * sqfs2tar.c
- *
- * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
- */
-#include "sqfs2tar.h"
-
-sqfs_xattr_reader_t *xr;
-sqfs_data_reader_t *data;
-sqfs_super_t super;
-ostream_t *out_file = NULL;
-
-static sqfs_file_t *file;
-
-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 terminate_archive(void)
-{
-	char buffer[1024];
-
-	memset(buffer, '\0', sizeof(buffer));
-
-	return ostream_append(out_file, buffer, sizeof(buffer));
-}
-
-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 = NULL;
-	int flags, ret, status = EXIT_FAILURE;
-	sqfs_compressor_t *cmp = NULL;
-	sqfs_id_table_t *idtbl = NULL;
-	sqfs_dir_reader_t *dr = NULL;
-	sqfs_compressor_config_t cfg;
-	size_t i;
-
-	process_args(argc, argv);
-
-	out_file = ostream_open_stdout();
-	if (out_file == NULL) {
-		perror("changing stdout to binary mode");
-		goto out;
-	}
-
-	if (compressor > 0) {
-		xfrm_stream_t *xfrm = compressor_stream_create(compressor,NULL);
-		ostream_t *strm;
-
-		if (xfrm == NULL)
-			goto out;
-
-		strm = ostream_xfrm_create(out_file, xfrm);
-		sqfs_drop(out_file);
-		sqfs_drop(xfrm);
-		out_file = strm;
-
-		if (out_file == NULL)
-			goto out;
-	}
-
-	file = sqfs_open_file(filename, SQFS_FILE_OPEN_READ_ONLY);
-	if (file == NULL) {
-		perror(filename);
-		goto out;
-	}
-
-	ret = sqfs_super_read(&super, file);
-	if (ret) {
-		sqfs_perror(filename, "reading super block", ret);
-		goto out;
-	}
-
-	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;
-	}
-
-	idtbl = sqfs_id_table_create(0);
-
-	if (idtbl == NULL) {
-		perror("creating ID table");
-		goto out;
-	}
-
-	ret = sqfs_id_table_read(idtbl, file, &super, cmp);
-	if (ret) {
-		sqfs_perror(filename, "loading ID table", ret);
-		goto out;
-	}
-
-	data = sqfs_data_reader_create(file, super.block_size, cmp, 0);
-	if (data == NULL) {
-		sqfs_perror(filename, "creating data reader",
-			    SQFS_ERROR_ALLOC);
-		goto out;
-	}
-
-	ret = sqfs_data_reader_load_fragment_table(data, &super);
-	if (ret) {
-		sqfs_perror(filename, "loading fragment table", ret);
-		goto out;
-	}
-
-	dr = sqfs_dir_reader_create(&super, cmp, file, 0);
-	if (dr == NULL) {
-		sqfs_perror(filename, "creating dir reader",
-			    SQFS_ERROR_ALLOC);
-		goto out;
-	}
-
-	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;
-		}
-
-		ret = sqfs_xattr_reader_load(xr, &super, file, cmp);
-		if (ret) {
-			sqfs_perror(filename, "loading xattr table", ret);
-			goto out;
-		}
-	}
-
-	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 (write_tree(root))
-		goto out;
-
-	if (terminate_archive())
-		goto out;
-
-	if (ostream_flush(out_file))
-		goto out;
-
-	status = EXIT_SUCCESS;
-out:
-	sqfs_dir_tree_destroy(root);
-	sqfs_drop(xr);
-	sqfs_drop(dr);
-	sqfs_drop(data);
-	sqfs_drop(idtbl);
-	sqfs_drop(cmp);
-	sqfs_drop(file);
-	sqfs_drop(out_file);
-	for (i = 0; i < num_subdirs; ++i)
-		free(subdirs[i]);
-	free(subdirs);
-	free(root_becomes);
-	return status;
-}
diff --git a/bin/sqfs2tar/sqfs2tar.h b/bin/sqfs2tar/sqfs2tar.h
deleted file mode 100644
index 4bf5428..0000000
--- a/bin/sqfs2tar/sqfs2tar.h
+++ /dev/null
@@ -1,56 +0,0 @@
-/* SPDX-License-Identifier: GPL-3.0-or-later */
-/*
- * sqfs2tar.h
- *
- * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
- */
-#ifndef SQFS2TAR_H
-#define SQFS2TAR_H
-
-#include "config.h"
-#include "common.h"
-
-#include "util/util.h"
-#include "tar/tar.h"
-#include "xfrm/compress.h"
-#include "io/xfrm.h"
-
-#include <getopt.h>
-#include <string.h>
-#include <stdlib.h>
-#include <assert.h>
-#include <errno.h>
-#include <fcntl.h>
-#include <stdio.h>
-
-/* options.c */
-extern bool dont_skip;
-extern bool keep_as_dir;
-extern bool no_xattr;
-extern bool no_links;
-
-extern char *root_becomes;
-extern char **subdirs;
-extern size_t num_subdirs;
-extern int compressor;
-
-extern const char *filename;
-
-void process_args(int argc, char **argv);
-
-/* tar2sqfs.c */
-extern sqfs_xattr_reader_t *xr;
-extern sqfs_data_reader_t *data;
-extern sqfs_super_t super;
-extern ostream_t *out_file;
-
-char *assemble_tar_path(char *name, bool is_dir);
-
-/* xattr.c */
-int get_xattrs(const char *name, const sqfs_inode_generic_t *inode,
-	       tar_xattr_t **out);
-
-/* write_tree.c */
-int write_tree(const sqfs_tree_node_t *n);
-
-#endif /* SQFS2TAR_H */
diff --git a/bin/sqfs2tar/src/options.c b/bin/sqfs2tar/src/options.c
new file mode 100644
index 0000000..ba1588d
--- /dev/null
+++ b/bin/sqfs2tar/src/options.c
@@ -0,0 +1,212 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * options.c
+ *
+ * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
+ */
+#include "sqfs2tar.h"
+
+static struct option long_opts[] = {
+	{ "compressor", required_argument, NULL, 'c' },
+	{ "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 = "c: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"
+"  --compressor, -c <name>   If set, stream compress the resulting tarball.\n"
+"                            By default, the tarball is uncompressed.\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"
+"Supported tar compression formats:\n";
+
+bool dont_skip = false;
+bool keep_as_dir = false;
+bool no_xattr = false;
+bool no_links = false;
+
+char *root_becomes = NULL;
+char **subdirs = NULL;
+size_t num_subdirs = 0;
+static size_t max_subdirs = 0;
+int compressor = 0;
+
+const char *filename = NULL;
+
+void process_args(int argc, char **argv)
+{
+	size_t idx, new_count;
+	const char *name;
+	int i, ret;
+	char **new;
+
+	for (;;) {
+		i = getopt_long(argc, argv, short_opts, long_opts, NULL);
+		if (i == -1)
+			break;
+
+		switch (i) {
+		case 'c':
+			compressor = xfrm_compressor_id_from_name(optarg);
+			if (compressor <= 0) {
+				fprintf(stderr, "unknown compressor '%s'.\n",
+					optarg);
+				goto fail;
+			}
+			break;
+		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);
+
+			i = XFRM_COMPRESSOR_MIN;
+
+			while (i <= XFRM_COMPRESSOR_MAX) {
+				name = xfrm_compressor_name_from_id(i);
+				if (name != NULL)
+					printf("\t%s\n", name);
+				++i;
+			}
+
+			fputc('\n', 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);
+}
diff --git a/bin/sqfs2tar/src/sqfs2tar.c b/bin/sqfs2tar/src/sqfs2tar.c
new file mode 100644
index 0000000..43f9e78
--- /dev/null
+++ b/bin/sqfs2tar/src/sqfs2tar.c
@@ -0,0 +1,274 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * sqfs2tar.c
+ *
+ * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
+ */
+#include "sqfs2tar.h"
+
+sqfs_xattr_reader_t *xr;
+sqfs_data_reader_t *data;
+sqfs_super_t super;
+ostream_t *out_file = NULL;
+
+static sqfs_file_t *file;
+
+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 terminate_archive(void)
+{
+	char buffer[1024];
+
+	memset(buffer, '\0', sizeof(buffer));
+
+	return ostream_append(out_file, buffer, sizeof(buffer));
+}
+
+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 = NULL;
+	int flags, ret, status = EXIT_FAILURE;
+	sqfs_compressor_t *cmp = NULL;
+	sqfs_id_table_t *idtbl = NULL;
+	sqfs_dir_reader_t *dr = NULL;
+	sqfs_compressor_config_t cfg;
+	size_t i;
+
+	process_args(argc, argv);
+
+	out_file = ostream_open_stdout();
+	if (out_file == NULL) {
+		perror("changing stdout to binary mode");
+		goto out;
+	}
+
+	if (compressor > 0) {
+		xfrm_stream_t *xfrm = compressor_stream_create(compressor,NULL);
+		ostream_t *strm;
+
+		if (xfrm == NULL)
+			goto out;
+
+		strm = ostream_xfrm_create(out_file, xfrm);
+		sqfs_drop(out_file);
+		sqfs_drop(xfrm);
+		out_file = strm;
+
+		if (out_file == NULL)
+			goto out;
+	}
+
+	file = sqfs_open_file(filename, SQFS_FILE_OPEN_READ_ONLY);
+	if (file == NULL) {
+		perror(filename);
+		goto out;
+	}
+
+	ret = sqfs_super_read(&super, file);
+	if (ret) {
+		sqfs_perror(filename, "reading super block", ret);
+		goto out;
+	}
+
+	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;
+	}
+
+	idtbl = sqfs_id_table_create(0);
+
+	if (idtbl == NULL) {
+		perror("creating ID table");
+		goto out;
+	}
+
+	ret = sqfs_id_table_read(idtbl, file, &super, cmp);
+	if (ret) {
+		sqfs_perror(filename, "loading ID table", ret);
+		goto out;
+	}
+
+	data = sqfs_data_reader_create(file, super.block_size, cmp, 0);
+	if (data == NULL) {
+		sqfs_perror(filename, "creating data reader",
+			    SQFS_ERROR_ALLOC);
+		goto out;
+	}
+
+	ret = sqfs_data_reader_load_fragment_table(data, &super);
+	if (ret) {
+		sqfs_perror(filename, "loading fragment table", ret);
+		goto out;
+	}
+
+	dr = sqfs_dir_reader_create(&super, cmp, file, 0);
+	if (dr == NULL) {
+		sqfs_perror(filename, "creating dir reader",
+			    SQFS_ERROR_ALLOC);
+		goto out;
+	}
+
+	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;
+		}
+
+		ret = sqfs_xattr_reader_load(xr, &super, file, cmp);
+		if (ret) {
+			sqfs_perror(filename, "loading xattr table", ret);
+			goto out;
+		}
+	}
+
+	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 (write_tree(root))
+		goto out;
+
+	if (terminate_archive())
+		goto out;
+
+	if (ostream_flush(out_file))
+		goto out;
+
+	status = EXIT_SUCCESS;
+out:
+	sqfs_dir_tree_destroy(root);
+	sqfs_drop(xr);
+	sqfs_drop(dr);
+	sqfs_drop(data);
+	sqfs_drop(idtbl);
+	sqfs_drop(cmp);
+	sqfs_drop(file);
+	sqfs_drop(out_file);
+	for (i = 0; i < num_subdirs; ++i)
+		free(subdirs[i]);
+	free(subdirs);
+	free(root_becomes);
+	return status;
+}
diff --git a/bin/sqfs2tar/src/sqfs2tar.h b/bin/sqfs2tar/src/sqfs2tar.h
new file mode 100644
index 0000000..4bf5428
--- /dev/null
+++ b/bin/sqfs2tar/src/sqfs2tar.h
@@ -0,0 +1,56 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * sqfs2tar.h
+ *
+ * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
+ */
+#ifndef SQFS2TAR_H
+#define SQFS2TAR_H
+
+#include "config.h"
+#include "common.h"
+
+#include "util/util.h"
+#include "tar/tar.h"
+#include "xfrm/compress.h"
+#include "io/xfrm.h"
+
+#include <getopt.h>
+#include <string.h>
+#include <stdlib.h>
+#include <assert.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+
+/* options.c */
+extern bool dont_skip;
+extern bool keep_as_dir;
+extern bool no_xattr;
+extern bool no_links;
+
+extern char *root_becomes;
+extern char **subdirs;
+extern size_t num_subdirs;
+extern int compressor;
+
+extern const char *filename;
+
+void process_args(int argc, char **argv);
+
+/* tar2sqfs.c */
+extern sqfs_xattr_reader_t *xr;
+extern sqfs_data_reader_t *data;
+extern sqfs_super_t super;
+extern ostream_t *out_file;
+
+char *assemble_tar_path(char *name, bool is_dir);
+
+/* xattr.c */
+int get_xattrs(const char *name, const sqfs_inode_generic_t *inode,
+	       tar_xattr_t **out);
+
+/* write_tree.c */
+int write_tree(const sqfs_tree_node_t *n);
+
+#endif /* SQFS2TAR_H */
diff --git a/bin/sqfs2tar/src/write_tree.c b/bin/sqfs2tar/src/write_tree.c
new file mode 100644
index 0000000..354ec21
--- /dev/null
+++ b/bin/sqfs2tar/src/write_tree.c
@@ -0,0 +1,209 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * write_tree.c
+ *
+ * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
+ */
+#include "sqfs2tar.h"
+
+static sqfs_hard_link_t *links = NULL;
+static unsigned int record_counter;
+
+static sqfs_hard_link_t *find_hard_link(const char *name, sqfs_u32 inum)
+{
+	sqfs_hard_link_t *lnk = NULL;
+
+	for (lnk = links; lnk != NULL; lnk = lnk->next) {
+		if (lnk->inode_number == inum) {
+			if (strcmp(name, lnk->target) == 0)
+				lnk = NULL;
+			break;
+		}
+	}
+
+	return lnk;
+}
+
+static void inode_stat(const sqfs_tree_node_t *node, struct stat *sb)
+{
+	memset(sb, 0, sizeof(*sb));
+
+	sb->st_mode = node->inode->base.mode;
+	sb->st_uid = node->uid;
+	sb->st_gid = node->gid;
+	sb->st_mtime = node->inode->base.mod_time;
+
+	switch (node->inode->base.type) {
+	case SQFS_INODE_BDEV:
+	case SQFS_INODE_CDEV:
+		sb->st_rdev = node->inode->data.dev.devno;
+		break;
+	case SQFS_INODE_EXT_BDEV:
+	case SQFS_INODE_EXT_CDEV:
+		sb->st_rdev = node->inode->data.dev_ext.devno;
+		break;
+	case SQFS_INODE_SLINK:
+		sb->st_size = node->inode->data.slink.target_size;
+		break;
+	case SQFS_INODE_EXT_SLINK:
+		sb->st_size = node->inode->data.slink_ext.target_size;
+		break;
+	case SQFS_INODE_FILE:
+		sb->st_size = node->inode->data.file.file_size;
+		break;
+	case SQFS_INODE_EXT_FILE:
+		sb->st_size = node->inode->data.file_ext.file_size;
+		break;
+	case SQFS_INODE_DIR:
+		sb->st_size = node->inode->data.dir.size;
+		break;
+	case SQFS_INODE_EXT_DIR:
+		sb->st_size = node->inode->data.dir_ext.size;
+		break;
+	default:
+		break;
+	}
+}
+
+static int write_tree_dfs(const sqfs_tree_node_t *n)
+{
+	sqfs_hard_link_t *lnk = NULL;
+	tar_xattr_t *xattr = 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;
+		}
+
+		ret = sqfs_tree_node_get_path(n, &name);
+		if (ret != 0) {
+			sqfs_perror(NULL, "resolving tree node path", ret);
+			return -1;
+		}
+
+		if (canonicalize_name(name))
+			goto out_skip;
+
+		name = assemble_tar_path(name, S_ISDIR(sb.st_mode));
+		if (name == NULL)
+			return -1;
+
+		lnk = find_hard_link(name, n->inode->base.inode_number);
+		if (lnk != NULL) {
+			ret = write_hard_link(out_file, &sb, name, lnk->target,
+					      record_counter++);
+			sqfs_free(name);
+			return ret;
+		}
+	}
+
+	if (!no_xattr) {
+		if (get_xattrs(name, n->inode, &xattr)) {
+			sqfs_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++);
+	free_xattr_list(xattr);
+
+	if (ret > 0)
+		goto out_skip;
+
+	if (ret < 0) {
+		sqfs_free(name);
+		return -1;
+	}
+
+	if (S_ISREG(sb.st_mode)) {
+		if (sqfs_data_reader_dump(name, data, n->inode, out_file,
+					  super.block_size)) {
+			sqfs_free(name);
+			return -1;
+		}
+
+		if (padd_file(out_file, sb.st_size)) {
+			sqfs_free(name);
+			return -1;
+		}
+	}
+
+	sqfs_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;
+	}
+	sqfs_free(name);
+	return ret;
+}
+
+int write_tree(const sqfs_tree_node_t *n)
+{
+	sqfs_hard_link_t *lnk;
+	int status = -1;
+
+	if (!no_links) {
+		int ret = sqfs_tree_find_hard_links(n, &links);
+		if (ret) {
+			sqfs_perror(NULL, "detecting hard links in "
+				    "file system tree", ret);
+			return -1;
+		}
+
+		for (lnk = links; lnk != NULL; lnk = lnk->next) {
+			lnk->target = assemble_tar_path(lnk->target, false);
+
+			if (lnk->target == NULL)
+				goto out_links;
+		}
+	}
+
+	status = write_tree_dfs(n);
+out_links:
+	while (links != NULL) {
+		lnk = links;
+		links = links->next;
+		sqfs_free(lnk->target);
+		free(lnk);
+	}
+	return status;
+}
diff --git a/bin/sqfs2tar/src/xattr.c b/bin/sqfs2tar/src/xattr.c
new file mode 100644
index 0000000..abec4fb
--- /dev/null
+++ b/bin/sqfs2tar/src/xattr.c
@@ -0,0 +1,91 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * xattr.c
+ *
+ * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
+ */
+#include "sqfs2tar.h"
+
+static tar_xattr_t *mkxattr(const sqfs_xattr_entry_t *key,
+			    const sqfs_xattr_value_t *value)
+{
+	tar_xattr_t *ent;
+
+	ent = calloc(1, sizeof(*ent) + strlen((const char *)key->key) +
+		     value->size + 2);
+
+	if (ent == NULL) {
+		perror("creating xattr entry");
+		return NULL;
+	}
+
+	ent->key = ent->data;
+	ent->value = (sqfs_u8 *)ent->key + strlen((const char *)key->key) + 1;
+	ent->value_len = value->size;
+
+	strcpy(ent->key, (const char *)key->key);
+	memcpy(ent->value, value->value, value->size + 1);
+	return ent;
+}
+
+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);
+			sqfs_free(key);
+			goto fail;
+		}
+
+		ent = mkxattr(key, value);
+		sqfs_free(key);
+		sqfs_free(value);
+
+		if (ent == NULL)
+			goto fail;
+
+		ent->next = list;
+		list = ent;
+	}
+
+	*out = list;
+	return 0;
+fail:
+	free_xattr_list(list);
+	return -1;
+}
diff --git a/bin/sqfs2tar/write_tree.c b/bin/sqfs2tar/write_tree.c
deleted file mode 100644
index 354ec21..0000000
--- a/bin/sqfs2tar/write_tree.c
+++ /dev/null
@@ -1,209 +0,0 @@
-/* SPDX-License-Identifier: GPL-3.0-or-later */
-/*
- * write_tree.c
- *
- * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
- */
-#include "sqfs2tar.h"
-
-static sqfs_hard_link_t *links = NULL;
-static unsigned int record_counter;
-
-static sqfs_hard_link_t *find_hard_link(const char *name, sqfs_u32 inum)
-{
-	sqfs_hard_link_t *lnk = NULL;
-
-	for (lnk = links; lnk != NULL; lnk = lnk->next) {
-		if (lnk->inode_number == inum) {
-			if (strcmp(name, lnk->target) == 0)
-				lnk = NULL;
-			break;
-		}
-	}
-
-	return lnk;
-}
-
-static void inode_stat(const sqfs_tree_node_t *node, struct stat *sb)
-{
-	memset(sb, 0, sizeof(*sb));
-
-	sb->st_mode = node->inode->base.mode;
-	sb->st_uid = node->uid;
-	sb->st_gid = node->gid;
-	sb->st_mtime = node->inode->base.mod_time;
-
-	switch (node->inode->base.type) {
-	case SQFS_INODE_BDEV:
-	case SQFS_INODE_CDEV:
-		sb->st_rdev = node->inode->data.dev.devno;
-		break;
-	case SQFS_INODE_EXT_BDEV:
-	case SQFS_INODE_EXT_CDEV:
-		sb->st_rdev = node->inode->data.dev_ext.devno;
-		break;
-	case SQFS_INODE_SLINK:
-		sb->st_size = node->inode->data.slink.target_size;
-		break;
-	case SQFS_INODE_EXT_SLINK:
-		sb->st_size = node->inode->data.slink_ext.target_size;
-		break;
-	case SQFS_INODE_FILE:
-		sb->st_size = node->inode->data.file.file_size;
-		break;
-	case SQFS_INODE_EXT_FILE:
-		sb->st_size = node->inode->data.file_ext.file_size;
-		break;
-	case SQFS_INODE_DIR:
-		sb->st_size = node->inode->data.dir.size;
-		break;
-	case SQFS_INODE_EXT_DIR:
-		sb->st_size = node->inode->data.dir_ext.size;
-		break;
-	default:
-		break;
-	}
-}
-
-static int write_tree_dfs(const sqfs_tree_node_t *n)
-{
-	sqfs_hard_link_t *lnk = NULL;
-	tar_xattr_t *xattr = 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;
-		}
-
-		ret = sqfs_tree_node_get_path(n, &name);
-		if (ret != 0) {
-			sqfs_perror(NULL, "resolving tree node path", ret);
-			return -1;
-		}
-
-		if (canonicalize_name(name))
-			goto out_skip;
-
-		name = assemble_tar_path(name, S_ISDIR(sb.st_mode));
-		if (name == NULL)
-			return -1;
-
-		lnk = find_hard_link(name, n->inode->base.inode_number);
-		if (lnk != NULL) {
-			ret = write_hard_link(out_file, &sb, name, lnk->target,
-					      record_counter++);
-			sqfs_free(name);
-			return ret;
-		}
-	}
-
-	if (!no_xattr) {
-		if (get_xattrs(name, n->inode, &xattr)) {
-			sqfs_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++);
-	free_xattr_list(xattr);
-
-	if (ret > 0)
-		goto out_skip;
-
-	if (ret < 0) {
-		sqfs_free(name);
-		return -1;
-	}
-
-	if (S_ISREG(sb.st_mode)) {
-		if (sqfs_data_reader_dump(name, data, n->inode, out_file,
-					  super.block_size)) {
-			sqfs_free(name);
-			return -1;
-		}
-
-		if (padd_file(out_file, sb.st_size)) {
-			sqfs_free(name);
-			return -1;
-		}
-	}
-
-	sqfs_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;
-	}
-	sqfs_free(name);
-	return ret;
-}
-
-int write_tree(const sqfs_tree_node_t *n)
-{
-	sqfs_hard_link_t *lnk;
-	int status = -1;
-
-	if (!no_links) {
-		int ret = sqfs_tree_find_hard_links(n, &links);
-		if (ret) {
-			sqfs_perror(NULL, "detecting hard links in "
-				    "file system tree", ret);
-			return -1;
-		}
-
-		for (lnk = links; lnk != NULL; lnk = lnk->next) {
-			lnk->target = assemble_tar_path(lnk->target, false);
-
-			if (lnk->target == NULL)
-				goto out_links;
-		}
-	}
-
-	status = write_tree_dfs(n);
-out_links:
-	while (links != NULL) {
-		lnk = links;
-		links = links->next;
-		sqfs_free(lnk->target);
-		free(lnk);
-	}
-	return status;
-}
diff --git a/bin/sqfs2tar/xattr.c b/bin/sqfs2tar/xattr.c
deleted file mode 100644
index abec4fb..0000000
--- a/bin/sqfs2tar/xattr.c
+++ /dev/null
@@ -1,91 +0,0 @@
-/* SPDX-License-Identifier: GPL-3.0-or-later */
-/*
- * xattr.c
- *
- * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
- */
-#include "sqfs2tar.h"
-
-static tar_xattr_t *mkxattr(const sqfs_xattr_entry_t *key,
-			    const sqfs_xattr_value_t *value)
-{
-	tar_xattr_t *ent;
-
-	ent = calloc(1, sizeof(*ent) + strlen((const char *)key->key) +
-		     value->size + 2);
-
-	if (ent == NULL) {
-		perror("creating xattr entry");
-		return NULL;
-	}
-
-	ent->key = ent->data;
-	ent->value = (sqfs_u8 *)ent->key + strlen((const char *)key->key) + 1;
-	ent->value_len = value->size;
-
-	strcpy(ent->key, (const char *)key->key);
-	memcpy(ent->value, value->value, value->size + 1);
-	return ent;
-}
-
-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);
-			sqfs_free(key);
-			goto fail;
-		}
-
-		ent = mkxattr(key, value);
-		sqfs_free(key);
-		sqfs_free(value);
-
-		if (ent == NULL)
-			goto fail;
-
-		ent->next = list;
-		list = ent;
-	}
-
-	*out = list;
-	return 0;
-fail:
-	free_xattr_list(list);
-	return -1;
-}
diff --git a/bin/sqfsdiff/Makemodule.am b/bin/sqfsdiff/Makemodule.am
index ff08c7a..4f21901 100644
--- a/bin/sqfsdiff/Makemodule.am
+++ b/bin/sqfsdiff/Makemodule.am
@@ -1,8 +1,8 @@
-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_SOURCES = bin/sqfsdiff/src/sqfsdiff.c bin/sqfsdiff/src/sqfsdiff.h \
+	bin/sqfsdiff/src/util.c bin/sqfsdiff/src/options.c \
+	bin/sqfsdiff/src/compare_dir.c bin/sqfsdiff/src/node_compare.c \
+	bin/sqfsdiff/src/compare_files.c bin/sqfsdiff/src/super.c \
+	bin/sqfsdiff/src/extract.c
 sqfsdiff_CFLAGS = $(AM_CFLAGS) $(PTHREAD_CFLAGS)
 sqfsdiff_LDADD = libcommon.a libsquashfs.la libio.a libcompat.a libutil.a
 sqfsdiff_LDADD += $(LZO_LIBS) libfstree.a $(PTHREAD_LIBS)
diff --git a/bin/sqfsdiff/compare_dir.c b/bin/sqfsdiff/compare_dir.c
deleted file mode 100644
index 1a4c800..0000000
--- a/bin/sqfsdiff/compare_dir.c
+++ /dev/null
@@ -1,94 +0,0 @@
-/* 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
deleted file mode 100644
index 51b66bb..0000000
--- a/bin/sqfsdiff/compare_files.c
+++ /dev/null
@@ -1,72 +0,0 @@
-/* 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
deleted file mode 100644
index f2072d4..0000000
--- a/bin/sqfsdiff/extract.c
+++ /dev/null
@@ -1,58 +0,0 @@
-/* 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;
-	ostream_t *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 = ostream_open_file(temp, OSTREAM_OPEN_OVERWRITE |
-			       OSTREAM_OPEN_SPARSE);
-	if (fp == NULL) {
-		perror(temp);
-		return -1;
-	}
-
-	if (sqfs_data_reader_dump(path, data, inode, fp, block_size)) {
-		sqfs_drop(fp);
-		return -1;
-	}
-
-	ostream_flush(fp);
-	sqfs_drop(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
deleted file mode 100644
index a0c99c7..0000000
--- a/bin/sqfsdiff/node_compare.c
+++ /dev/null
@@ -1,206 +0,0 @@
-/* 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)
-{
-	sqfs_tree_node_t *ait, *bit;
-	bool promoted, demoted;
-	int ret, status = 0;
-	char *path;
-
-	ret = sqfs_tree_node_get_path(a, &path);
-	if (ret != 0) {
-		sqfs_perror(NULL, "constructing absolute file path", ret);
-		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;
-		default:
-			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);
-			sqfs_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;
-
-		sqfs_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;
-	}
-
-	sqfs_free(path);
-	return status;
-}
diff --git a/bin/sqfsdiff/options.c b/bin/sqfsdiff/options.c
deleted file mode 100644
index b8ce7f0..0000000
--- a/bin/sqfsdiff/options.c
+++ /dev/null
@@ -1,131 +0,0 @@
-/* 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
deleted file mode 100644
index d789fe1..0000000
--- a/bin/sqfsdiff/sqfsdiff.c
+++ /dev/null
@@ -1,167 +0,0 @@
-/* SPDX-License-Identifier: GPL-3.0-or-later */
-/*
- * sqfsdiff.c
- *
- * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
- */
-#include "sqfsdiff.h"
-
-static void close_sfqs(sqfs_state_t *state)
-{
-	sqfs_drop(state->data);
-	sqfs_dir_tree_destroy(state->root);
-	sqfs_drop(state->dr);
-	sqfs_drop(state->idtbl);
-	sqfs_drop(state->cmp);
-	sqfs_drop(state->file);
-}
-
-static int open_sfqs(sqfs_state_t *state, const char *path)
-{
-	int ret;
-
-	memset(state, 0, sizeof(*state));
-
-	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;
-	}
-
-	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;
-	}
-
-	if (state->super.flags & SQFS_FLAG_COMPRESSOR_OPTIONS) {
-		ret = state->cmp->read_options(state->cmp, state->file);
-
-		if (ret == 0) {
-			state->cmp->get_configuration(state->cmp,
-						      &state->options);
-			state->have_options = true;
-		} else {
-			sqfs_perror(path, "reading compressor options", ret);
-			state->have_options = false;
-		}
-	} else {
-		state->have_options = false;
-	}
-
-	state->idtbl = sqfs_id_table_create(0);
-	if (state->idtbl == NULL) {
-		sqfs_perror(path, "creating ID table", SQFS_ERROR_ALLOC);
-		goto fail;
-	}
-
-	ret = sqfs_id_table_read(state->idtbl, state->file,
-				 &state->super, state->cmp);
-	if (ret) {
-		sqfs_perror(path, "loading ID table", ret);
-		goto fail;
-	}
-
-	state->dr = sqfs_dir_reader_create(&state->super, state->cmp,
-					   state->file, 0);
-	if (state->dr == NULL) {
-		sqfs_perror(path, "creating directory reader",
-			    SQFS_ERROR_ALLOC);
-		goto fail;
-	}
-
-	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;
-	}
-
-	state->data = sqfs_data_reader_create(state->file,
-					      state->super.block_size,
-					      state->cmp, 0);
-	if (state->data == NULL) {
-		sqfs_perror(path, "creating data reader", SQFS_ERROR_ALLOC);
-		goto fail;
-	}
-
-	ret = sqfs_data_reader_load_fragment_table(state->data, &state->super);
-	if (ret) {
-		sqfs_perror(path, "loading fragment table", ret);
-		goto fail;
-	}
-
-	return 0;
-fail:
-	close_sfqs(state);
-	return -1;
-}
-
-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
deleted file mode 100644
index 65e8120..0000000
--- a/bin/sqfsdiff/sqfsdiff.h
+++ /dev/null
@@ -1,73 +0,0 @@
-/* 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 "util/util.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_compressor_config_t options;
-	bool have_options;
-} 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/src/compare_dir.c b/bin/sqfsdiff/src/compare_dir.c
new file mode 100644
index 0000000..1a4c800
--- /dev/null
+++ b/bin/sqfsdiff/src/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/src/compare_files.c b/bin/sqfsdiff/src/compare_files.c
new file mode 100644
index 0000000..51b66bb
--- /dev/null
+++ b/bin/sqfsdiff/src/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/src/extract.c b/bin/sqfsdiff/src/extract.c
new file mode 100644
index 0000000..f2072d4
--- /dev/null
+++ b/bin/sqfsdiff/src/extract.c
@@ -0,0 +1,58 @@
+/* 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;
+	ostream_t *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 = ostream_open_file(temp, OSTREAM_OPEN_OVERWRITE |
+			       OSTREAM_OPEN_SPARSE);
+	if (fp == NULL) {
+		perror(temp);
+		return -1;
+	}
+
+	if (sqfs_data_reader_dump(path, data, inode, fp, block_size)) {
+		sqfs_drop(fp);
+		return -1;
+	}
+
+	ostream_flush(fp);
+	sqfs_drop(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/src/node_compare.c b/bin/sqfsdiff/src/node_compare.c
new file mode 100644
index 0000000..a0c99c7
--- /dev/null
+++ b/bin/sqfsdiff/src/node_compare.c
@@ -0,0 +1,206 @@
+/* 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)
+{
+	sqfs_tree_node_t *ait, *bit;
+	bool promoted, demoted;
+	int ret, status = 0;
+	char *path;
+
+	ret = sqfs_tree_node_get_path(a, &path);
+	if (ret != 0) {
+		sqfs_perror(NULL, "constructing absolute file path", ret);
+		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;
+		default:
+			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);
+			sqfs_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;
+
+		sqfs_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;
+	}
+
+	sqfs_free(path);
+	return status;
+}
diff --git a/bin/sqfsdiff/src/options.c b/bin/sqfsdiff/src/options.c
new file mode 100644
index 0000000..b8ce7f0
--- /dev/null
+++ b/bin/sqfsdiff/src/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/src/sqfsdiff.c b/bin/sqfsdiff/src/sqfsdiff.c
new file mode 100644
index 0000000..d789fe1
--- /dev/null
+++ b/bin/sqfsdiff/src/sqfsdiff.c
@@ -0,0 +1,167 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * sqfsdiff.c
+ *
+ * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
+ */
+#include "sqfsdiff.h"
+
+static void close_sfqs(sqfs_state_t *state)
+{
+	sqfs_drop(state->data);
+	sqfs_dir_tree_destroy(state->root);
+	sqfs_drop(state->dr);
+	sqfs_drop(state->idtbl);
+	sqfs_drop(state->cmp);
+	sqfs_drop(state->file);
+}
+
+static int open_sfqs(sqfs_state_t *state, const char *path)
+{
+	int ret;
+
+	memset(state, 0, sizeof(*state));
+
+	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;
+	}
+
+	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;
+	}
+
+	if (state->super.flags & SQFS_FLAG_COMPRESSOR_OPTIONS) {
+		ret = state->cmp->read_options(state->cmp, state->file);
+
+		if (ret == 0) {
+			state->cmp->get_configuration(state->cmp,
+						      &state->options);
+			state->have_options = true;
+		} else {
+			sqfs_perror(path, "reading compressor options", ret);
+			state->have_options = false;
+		}
+	} else {
+		state->have_options = false;
+	}
+
+	state->idtbl = sqfs_id_table_create(0);
+	if (state->idtbl == NULL) {
+		sqfs_perror(path, "creating ID table", SQFS_ERROR_ALLOC);
+		goto fail;
+	}
+
+	ret = sqfs_id_table_read(state->idtbl, state->file,
+				 &state->super, state->cmp);
+	if (ret) {
+		sqfs_perror(path, "loading ID table", ret);
+		goto fail;
+	}
+
+	state->dr = sqfs_dir_reader_create(&state->super, state->cmp,
+					   state->file, 0);
+	if (state->dr == NULL) {
+		sqfs_perror(path, "creating directory reader",
+			    SQFS_ERROR_ALLOC);
+		goto fail;
+	}
+
+	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;
+	}
+
+	state->data = sqfs_data_reader_create(state->file,
+					      state->super.block_size,
+					      state->cmp, 0);
+	if (state->data == NULL) {
+		sqfs_perror(path, "creating data reader", SQFS_ERROR_ALLOC);
+		goto fail;
+	}
+
+	ret = sqfs_data_reader_load_fragment_table(state->data, &state->super);
+	if (ret) {
+		sqfs_perror(path, "loading fragment table", ret);
+		goto fail;
+	}
+
+	return 0;
+fail:
+	close_sfqs(state);
+	return -1;
+}
+
+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/src/sqfsdiff.h b/bin/sqfsdiff/src/sqfsdiff.h
new file mode 100644
index 0000000..65e8120
--- /dev/null
+++ b/bin/sqfsdiff/src/sqfsdiff.h
@@ -0,0 +1,73 @@
+/* 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 "util/util.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_compressor_config_t options;
+	bool have_options;
+} 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/src/super.c b/bin/sqfsdiff/src/super.c
new file mode 100644
index 0000000..0cf18e0
--- /dev/null
+++ b/bin/sqfsdiff/src/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_NO_DUPLICATES, "no 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/src/util.c b/bin/sqfsdiff/src/util.c
new file mode 100644
index 0000000..a11770f
--- /dev/null
+++ b/bin/sqfsdiff/src/util.c
@@ -0,0 +1,27 @@
+/* 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;
+	int ret;
+
+	ret = sqfs_tree_node_get_path(n, &path);
+	if (ret != 0) {
+		sqfs_perror(NULL, "get path", ret);
+		return NULL;
+	}
+
+	if (canonicalize_name(path)) {
+		fprintf(stderr, "failed to canonicalization '%s'\n", path);
+		sqfs_free(path);
+		return NULL;
+	}
+
+	return path;
+}
diff --git a/bin/sqfsdiff/super.c b/bin/sqfsdiff/super.c
deleted file mode 100644
index 0cf18e0..0000000
--- a/bin/sqfsdiff/super.c
+++ /dev/null
@@ -1,125 +0,0 @@
-/* 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_NO_DUPLICATES, "no 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
deleted file mode 100644
index a11770f..0000000
--- a/bin/sqfsdiff/util.c
+++ /dev/null
@@ -1,27 +0,0 @@
-/* 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;
-	int ret;
-
-	ret = sqfs_tree_node_get_path(n, &path);
-	if (ret != 0) {
-		sqfs_perror(NULL, "get path", ret);
-		return NULL;
-	}
-
-	if (canonicalize_name(path)) {
-		fprintf(stderr, "failed to canonicalization '%s'\n", path);
-		sqfs_free(path);
-		return NULL;
-	}
-
-	return path;
-}
diff --git a/bin/tar2sqfs/Makemodule.am b/bin/tar2sqfs/Makemodule.am
index faa2948..c8f52ea 100644
--- a/bin/tar2sqfs/Makemodule.am
+++ b/bin/tar2sqfs/Makemodule.am
@@ -1,5 +1,5 @@
-tar2sqfs_SOURCES = bin/tar2sqfs/tar2sqfs.c bin/tar2sqfs/tar2sqfs.h
-tar2sqfs_SOURCES += bin/tar2sqfs/options.c bin/tar2sqfs/process_tarball.c
+tar2sqfs_SOURCES = bin/tar2sqfs/src/tar2sqfs.c bin/tar2sqfs/src/tar2sqfs.h \
+	bin/tar2sqfs/src/options.c bin/tar2sqfs/src/process_tarball.c
 tar2sqfs_CFLAGS = $(AM_CFLAGS) $(PTHREAD_CFLAGS)
 tar2sqfs_LDADD = libcommon.a libsquashfs.la libtar.a libio.a libxfrm.a
 tar2sqfs_LDADD += libfstree.a libcompat.a libfstree.a libutil.a $(LZO_LIBS)
diff --git a/bin/tar2sqfs/options.c b/bin/tar2sqfs/options.c
deleted file mode 100644
index f2185a6..0000000
--- a/bin/tar2sqfs/options.c
+++ /dev/null
@@ -1,257 +0,0 @@
-/* SPDX-License-Identifier: GPL-3.0-or-later */
-/*
- * options.c
- *
- * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
- */
-#include "tar2sqfs.h"
-
-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-symlink-retarget", no_argument, NULL, 'S' },
-	{ "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:sxekfqSThV";
-
-static const char *usagestr =
-"Usage: tar2sqfs [OPTIONS...] <sqfsfile>\n"
-"\n"
-"Read a tar archive from stdin and turn it into a squashfs 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"
-"  --no-symlink-retarget, -S   If --root-becomes is used, link targets are\n"
-"                              adjusted if they are prefixed by the root\n"
-"                              path. If this flag is set, symlinks are left\n"
-"                              untouched and only hard links are changed.\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";
-
-bool dont_skip = false;
-bool keep_time = true;
-bool no_tail_pack = false;
-bool no_symlink_retarget = false;
-sqfs_writer_cfg_t cfg;
-char *root_becomes = NULL;
-
-static void input_compressor_print_available(void)
-{
-	int i = XFRM_COMPRESSOR_MIN;
-	const char *name;
-
-	fputs("\nSupported tar compression formats:\n", stdout);
-
-	while (i <= XFRM_COMPRESSOR_MAX) {
-		name = xfrm_compressor_name_from_id(i);
-
-		if (name != NULL)
-			printf("\t%s\n", name);
-
-		++i;
-	}
-
-	fputs("\tuncompressed\n", stdout);
-	fputc('\n', stdout);
-}
-
-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 'S':
-			no_symlink_retarget = true;
-			break;
-		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();
-			input_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 specified.\n", stderr);
-		goto fail_arg;
-	}
-	return;
-fail_arg:
-	fputs("Try `tar2sqfs --help' for more information.\n", stderr);
-	exit(EXIT_FAILURE);
-}
diff --git a/bin/tar2sqfs/process_tarball.c b/bin/tar2sqfs/process_tarball.c
deleted file mode 100644
index 6aaa24b..0000000
--- a/bin/tar2sqfs/process_tarball.c
+++ /dev/null
@@ -1,346 +0,0 @@
-/* SPDX-License-Identifier: GPL-3.0-or-later */
-/*
- * process_tarball.c
- *
- * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
- */
-#include "tar2sqfs.h"
-
-static int write_file(istream_t *input_file, sqfs_writer_t *sqfs,
-		      const tar_header_decoded_t *hdr,
-		      file_info_t *fi, sqfs_u64 filesize)
-{
-	const sparse_map_t *list;
-	int flags = 0, ret = 0;
-	sqfs_u64 offset, diff;
-	bool sparse_region;
-	ostream_t *out;
-
-	if (no_tail_pack && filesize > cfg.block_size)
-		flags |= SQFS_BLK_DONT_FRAGMENT;
-
-	out = data_writer_ostream_create(hdr->name, sqfs->data, &fi->inode,
-					 flags);
-
-	if (out == NULL)
-		return -1;
-
-	list = hdr->sparse;
-
-	for (offset = 0; offset < filesize; offset += diff) {
-		if (hdr->sparse != NULL) {
-			if (list == NULL) {
-				sparse_region = true;
-				diff = filesize - offset;
-			} else if (offset < list->offset) {
-				sparse_region = true;
-				diff = list->offset - offset;
-			} else if (offset - list->offset >= list->count) {
-				list = list->next;
-				diff = 0;
-				continue;
-			} else {
-				sparse_region = false;
-				diff = list->count - (offset - list->offset);
-			}
-		} else {
-			sparse_region = false;
-			diff = filesize - offset;
-		}
-
-		if (diff > 0x7FFFFFFFUL)
-			diff = 0x7FFFFFFFUL;
-
-		if (sparse_region) {
-			ret = ostream_append_sparse(out, diff);
-		} else {
-			ret = ostream_append_from_istream(out, input_file,
-							  diff);
-
-			if (ret == 0) {
-				fprintf(stderr, "%s: unexpected end-of-file\n",
-					hdr->name);
-				ret = -1;
-			} else if (ret > 0) {
-				diff = ret;
-				ret = 0;
-			}
-		}
-
-		if (ret < 0)
-			break;
-	}
-
-	ostream_flush(out);
-	sqfs_drop(out);
-
-	if (ret)
-		return -1;
-
-	return skip_padding(input_file, hdr->sparse == NULL ?
-			    filesize : hdr->record_size);
-}
-
-static int copy_xattr(sqfs_writer_t *sqfs, tree_node_t *node,
-		      const tar_header_decoded_t *hdr)
-{
-	tar_xattr_t *xattr;
-	int ret;
-
-	ret = sqfs_xattr_writer_begin(sqfs->xwr, 0);
-	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(istream_t *input_file,
-				       sqfs_writer_t *sqfs,
-				       tar_header_decoded_t *hdr)
-{
-	tree_node_t *node;
-	struct stat sb;
-
-	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->mtime = sqfs->fs.defaults.st_mtime;
-	}
-
-	memset(&sb, 0, sizeof(sb));
-	sb.st_mode = hdr->mode;
-	sb.st_uid = hdr->uid;
-	sb.st_gid = hdr->gid;
-	sb.st_rdev = hdr->devno;
-	sb.st_size = hdr->actual_size;
-	sb.st_mtime = hdr->mtime;
-
-	node = fstree_add_generic(&sqfs->fs, hdr->name,
-				  &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(sqfs, node, hdr))
-			return -1;
-	}
-
-	if (S_ISREG(hdr->mode)) {
-		if (write_file(input_file, sqfs, hdr, &node->data.file,
-			       hdr->actual_size)) {
-			return -1;
-		}
-	}
-
-	return 0;
-fail_errno:
-	perror(hdr->name);
-	return -1;
-}
-
-static int set_root_attribs(sqfs_writer_t *sqfs,
-			    const tar_header_decoded_t *hdr)
-{
-	if (hdr->is_hard_link || !S_ISDIR(hdr->mode)) {
-		fprintf(stderr, "'%s' is not a directory!\n", hdr->name);
-		return -1;
-	}
-
-	sqfs->fs.root->uid = hdr->uid;
-	sqfs->fs.root->gid = hdr->gid;
-	sqfs->fs.root->mode = hdr->mode;
-
-	if (keep_time)
-		sqfs->fs.root->mod_time = hdr->mtime;
-
-	if (!cfg.no_xattr) {
-		if (copy_xattr(sqfs, sqfs->fs.root, hdr))
-			return -1;
-	}
-
-	return 0;
-}
-
-int process_tarball(istream_t *input_file, sqfs_writer_t *sqfs)
-{
-	bool skip, is_root, is_prefixed;
-	tar_header_decoded_t hdr;
-	sqfs_u64 offset, count;
-	sparse_map_t *m;
-	size_t rootlen;
-	char *target;
-	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;
-
-		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;
-		} else 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;
-			}
-
-			if (hdr.link_target != NULL &&
-			    (hdr.is_hard_link || !no_symlink_retarget)) {
-				target = strdup(hdr.link_target);
-				if (target == NULL) {
-					fprintf(stderr, "packing '%s': %s\n",
-						hdr.name, strerror(errno));
-					goto fail;
-				}
-
-				if (canonicalize_name(target) == 0 &&
-				    !strncmp(target, root_becomes, rootlen) &&
-				    target[rootlen] == '/') {
-					memmove(hdr.link_target,
-						target + rootlen,
-						strlen(target + rootlen) + 1);
-				}
-
-				free(target);
-			}
-		} else if (hdr.name[0] == '\0') {
-			is_root = true;
-		}
-
-		if (!is_prefixed) {
-			if (skip_entry(input_file, hdr.record_size))
-				goto fail;
-			clear_header(&hdr);
-			continue;
-		}
-
-		if (is_root) {
-			if (set_root_attribs(sqfs, &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.record_size))
-				goto fail;
-
-			clear_header(&hdr);
-			continue;
-		}
-
-		if (create_node_and_repack_data(input_file, sqfs, &hdr))
-			goto fail;
-
-		clear_header(&hdr);
-	}
-
-	return 0;
-fail:
-	clear_header(&hdr);
-	return -1;
-}
diff --git a/bin/tar2sqfs/src/options.c b/bin/tar2sqfs/src/options.c
new file mode 100644
index 0000000..f2185a6
--- /dev/null
+++ b/bin/tar2sqfs/src/options.c
@@ -0,0 +1,257 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * options.c
+ *
+ * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
+ */
+#include "tar2sqfs.h"
+
+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-symlink-retarget", no_argument, NULL, 'S' },
+	{ "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:sxekfqSThV";
+
+static const char *usagestr =
+"Usage: tar2sqfs [OPTIONS...] <sqfsfile>\n"
+"\n"
+"Read a tar archive from stdin and turn it into a squashfs 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"
+"  --no-symlink-retarget, -S   If --root-becomes is used, link targets are\n"
+"                              adjusted if they are prefixed by the root\n"
+"                              path. If this flag is set, symlinks are left\n"
+"                              untouched and only hard links are changed.\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";
+
+bool dont_skip = false;
+bool keep_time = true;
+bool no_tail_pack = false;
+bool no_symlink_retarget = false;
+sqfs_writer_cfg_t cfg;
+char *root_becomes = NULL;
+
+static void input_compressor_print_available(void)
+{
+	int i = XFRM_COMPRESSOR_MIN;
+	const char *name;
+
+	fputs("\nSupported tar compression formats:\n", stdout);
+
+	while (i <= XFRM_COMPRESSOR_MAX) {
+		name = xfrm_compressor_name_from_id(i);
+
+		if (name != NULL)
+			printf("\t%s\n", name);
+
+		++i;
+	}
+
+	fputs("\tuncompressed\n", stdout);
+	fputc('\n', stdout);
+}
+
+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 'S':
+			no_symlink_retarget = true;
+			break;
+		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();
+			input_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 specified.\n", stderr);
+		goto fail_arg;
+	}
+	return;
+fail_arg:
+	fputs("Try `tar2sqfs --help' for more information.\n", stderr);
+	exit(EXIT_FAILURE);
+}
diff --git a/bin/tar2sqfs/src/process_tarball.c b/bin/tar2sqfs/src/process_tarball.c
new file mode 100644
index 0000000..6aaa24b
--- /dev/null
+++ b/bin/tar2sqfs/src/process_tarball.c
@@ -0,0 +1,346 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * process_tarball.c
+ *
+ * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
+ */
+#include "tar2sqfs.h"
+
+static int write_file(istream_t *input_file, sqfs_writer_t *sqfs,
+		      const tar_header_decoded_t *hdr,
+		      file_info_t *fi, sqfs_u64 filesize)
+{
+	const sparse_map_t *list;
+	int flags = 0, ret = 0;
+	sqfs_u64 offset, diff;
+	bool sparse_region;
+	ostream_t *out;
+
+	if (no_tail_pack && filesize > cfg.block_size)
+		flags |= SQFS_BLK_DONT_FRAGMENT;
+
+	out = data_writer_ostream_create(hdr->name, sqfs->data, &fi->inode,
+					 flags);
+
+	if (out == NULL)
+		return -1;
+
+	list = hdr->sparse;
+
+	for (offset = 0; offset < filesize; offset += diff) {
+		if (hdr->sparse != NULL) {
+			if (list == NULL) {
+				sparse_region = true;
+				diff = filesize - offset;
+			} else if (offset < list->offset) {
+				sparse_region = true;
+				diff = list->offset - offset;
+			} else if (offset - list->offset >= list->count) {
+				list = list->next;
+				diff = 0;
+				continue;
+			} else {
+				sparse_region = false;
+				diff = list->count - (offset - list->offset);
+			}
+		} else {
+			sparse_region = false;
+			diff = filesize - offset;
+		}
+
+		if (diff > 0x7FFFFFFFUL)
+			diff = 0x7FFFFFFFUL;
+
+		if (sparse_region) {
+			ret = ostream_append_sparse(out, diff);
+		} else {
+			ret = ostream_append_from_istream(out, input_file,
+							  diff);
+
+			if (ret == 0) {
+				fprintf(stderr, "%s: unexpected end-of-file\n",
+					hdr->name);
+				ret = -1;
+			} else if (ret > 0) {
+				diff = ret;
+				ret = 0;
+			}
+		}
+
+		if (ret < 0)
+			break;
+	}
+
+	ostream_flush(out);
+	sqfs_drop(out);
+
+	if (ret)
+		return -1;
+
+	return skip_padding(input_file, hdr->sparse == NULL ?
+			    filesize : hdr->record_size);
+}
+
+static int copy_xattr(sqfs_writer_t *sqfs, tree_node_t *node,
+		      const tar_header_decoded_t *hdr)
+{
+	tar_xattr_t *xattr;
+	int ret;
+
+	ret = sqfs_xattr_writer_begin(sqfs->xwr, 0);
+	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(istream_t *input_file,
+				       sqfs_writer_t *sqfs,
+				       tar_header_decoded_t *hdr)
+{
+	tree_node_t *node;
+	struct stat sb;
+
+	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->mtime = sqfs->fs.defaults.st_mtime;
+	}
+
+	memset(&sb, 0, sizeof(sb));
+	sb.st_mode = hdr->mode;
+	sb.st_uid = hdr->uid;
+	sb.st_gid = hdr->gid;
+	sb.st_rdev = hdr->devno;
+	sb.st_size = hdr->actual_size;
+	sb.st_mtime = hdr->mtime;
+
+	node = fstree_add_generic(&sqfs->fs, hdr->name,
+				  &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(sqfs, node, hdr))
+			return -1;
+	}
+
+	if (S_ISREG(hdr->mode)) {
+		if (write_file(input_file, sqfs, hdr, &node->data.file,
+			       hdr->actual_size)) {
+			return -1;
+		}
+	}
+
+	return 0;
+fail_errno:
+	perror(hdr->name);
+	return -1;
+}
+
+static int set_root_attribs(sqfs_writer_t *sqfs,
+			    const tar_header_decoded_t *hdr)
+{
+	if (hdr->is_hard_link || !S_ISDIR(hdr->mode)) {
+		fprintf(stderr, "'%s' is not a directory!\n", hdr->name);
+		return -1;
+	}
+
+	sqfs->fs.root->uid = hdr->uid;
+	sqfs->fs.root->gid = hdr->gid;
+	sqfs->fs.root->mode = hdr->mode;
+
+	if (keep_time)
+		sqfs->fs.root->mod_time = hdr->mtime;
+
+	if (!cfg.no_xattr) {
+		if (copy_xattr(sqfs, sqfs->fs.root, hdr))
+			return -1;
+	}
+
+	return 0;
+}
+
+int process_tarball(istream_t *input_file, sqfs_writer_t *sqfs)
+{
+	bool skip, is_root, is_prefixed;
+	tar_header_decoded_t hdr;
+	sqfs_u64 offset, count;
+	sparse_map_t *m;
+	size_t rootlen;
+	char *target;
+	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;
+
+		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;
+		} else 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;
+			}
+
+			if (hdr.link_target != NULL &&
+			    (hdr.is_hard_link || !no_symlink_retarget)) {
+				target = strdup(hdr.link_target);
+				if (target == NULL) {
+					fprintf(stderr, "packing '%s': %s\n",
+						hdr.name, strerror(errno));
+					goto fail;
+				}
+
+				if (canonicalize_name(target) == 0 &&
+				    !strncmp(target, root_becomes, rootlen) &&
+				    target[rootlen] == '/') {
+					memmove(hdr.link_target,
+						target + rootlen,
+						strlen(target + rootlen) + 1);
+				}
+
+				free(target);
+			}
+		} else if (hdr.name[0] == '\0') {
+			is_root = true;
+		}
+
+		if (!is_prefixed) {
+			if (skip_entry(input_file, hdr.record_size))
+				goto fail;
+			clear_header(&hdr);
+			continue;
+		}
+
+		if (is_root) {
+			if (set_root_attribs(sqfs, &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.record_size))
+				goto fail;
+
+			clear_header(&hdr);
+			continue;
+		}
+
+		if (create_node_and_repack_data(input_file, sqfs, &hdr))
+			goto fail;
+
+		clear_header(&hdr);
+	}
+
+	return 0;
+fail:
+	clear_header(&hdr);
+	return -1;
+}
diff --git a/bin/tar2sqfs/src/tar2sqfs.c b/bin/tar2sqfs/src/tar2sqfs.c
new file mode 100644
index 0000000..9257fed
--- /dev/null
+++ b/bin/tar2sqfs/src/tar2sqfs.c
@@ -0,0 +1,104 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * tar2sqfs.c
+ *
+ * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
+ */
+#include "tar2sqfs.h"
+
+static int tar_probe(const sqfs_u8 *data, size_t size)
+{
+	size_t i, offset;
+
+	if (size >= TAR_RECORD_SIZE) {
+		for (i = 0; i < TAR_RECORD_SIZE; ++i) {
+			if (data[i] != 0x00)
+				break;
+		}
+
+		if (i == TAR_RECORD_SIZE) {
+			data += TAR_RECORD_SIZE;
+			size -= TAR_RECORD_SIZE;
+		}
+	}
+
+	offset = offsetof(tar_header_t, magic);
+
+	if (offset + 5 <= size) {
+		if (memcmp(data + offset, "ustar", 5) == 0)
+			return 1;
+	}
+
+	return 0;
+}
+
+static istream_t *magic_autowrap(istream_t *strm)
+{
+	xfrm_stream_t *xfrm = NULL;
+	istream_t *wrapper = NULL;
+	const sqfs_u8 *data;
+	size_t avail;
+	int ret;
+
+	ret = istream_precache(strm);
+	if (ret != 0)
+		goto out;
+
+	data = strm->buffer + strm->buffer_offset;
+	avail = strm->buffer_used - strm->buffer_offset;
+
+	ret = tar_probe(data, avail);
+	if (ret > 0)
+		return strm;
+
+	ret = xfrm_compressor_id_from_magic(data, avail);
+	if (ret <= 0)
+		return strm;
+
+	xfrm = decompressor_stream_create(ret);
+	if (xfrm == NULL)
+		goto out;
+
+	wrapper = istream_xfrm_create(strm, xfrm);
+out:
+	sqfs_drop(strm);
+	sqfs_drop(xfrm);
+	return wrapper;
+}
+
+int main(int argc, char **argv)
+{
+	int status = EXIT_FAILURE;
+	istream_t *input_file = NULL;
+	sqfs_writer_t sqfs;
+
+	process_args(argc, argv);
+
+	input_file = istream_open_stdin();
+	if (input_file == NULL)
+		return EXIT_FAILURE;
+
+	input_file = magic_autowrap(input_file);
+	if (input_file == NULL)
+		return EXIT_FAILURE;
+
+	memset(&sqfs, 0, sizeof(sqfs));
+	if (sqfs_writer_init(&sqfs, &cfg))
+		goto out_if;
+
+	if (process_tarball(input_file, &sqfs))
+		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);
+out_if:
+	sqfs_drop(input_file);
+	return status;
+}
diff --git a/bin/tar2sqfs/src/tar2sqfs.h b/bin/tar2sqfs/src/tar2sqfs.h
new file mode 100644
index 0000000..a21774b
--- /dev/null
+++ b/bin/tar2sqfs/src/tar2sqfs.h
@@ -0,0 +1,39 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * tar2sqfs.h
+ *
+ * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
+ */
+#ifndef TAR2SQFS_H
+#define TAR2SQFS_H
+
+#include "config.h"
+#include "common.h"
+#include "compat.h"
+
+#include "util/util.h"
+#include "tar/tar.h"
+#include "tar/format.h"
+#include "xfrm/compress.h"
+#include "io/xfrm.h"
+
+#include <stdlib.h>
+#include <getopt.h>
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+
+/* options.c */
+extern bool dont_skip;
+extern bool keep_time;
+extern bool no_tail_pack;
+extern bool no_symlink_retarget;
+extern sqfs_writer_cfg_t cfg;
+extern char *root_becomes;
+
+void process_args(int argc, char **argv);
+
+/* process_tarball.c */
+int process_tarball(istream_t *input_file, sqfs_writer_t *sqfs);
+
+#endif /* TAR2SQFS_H */
diff --git a/bin/tar2sqfs/tar2sqfs.c b/bin/tar2sqfs/tar2sqfs.c
deleted file mode 100644
index 9257fed..0000000
--- a/bin/tar2sqfs/tar2sqfs.c
+++ /dev/null
@@ -1,104 +0,0 @@
-/* SPDX-License-Identifier: GPL-3.0-or-later */
-/*
- * tar2sqfs.c
- *
- * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
- */
-#include "tar2sqfs.h"
-
-static int tar_probe(const sqfs_u8 *data, size_t size)
-{
-	size_t i, offset;
-
-	if (size >= TAR_RECORD_SIZE) {
-		for (i = 0; i < TAR_RECORD_SIZE; ++i) {
-			if (data[i] != 0x00)
-				break;
-		}
-
-		if (i == TAR_RECORD_SIZE) {
-			data += TAR_RECORD_SIZE;
-			size -= TAR_RECORD_SIZE;
-		}
-	}
-
-	offset = offsetof(tar_header_t, magic);
-
-	if (offset + 5 <= size) {
-		if (memcmp(data + offset, "ustar", 5) == 0)
-			return 1;
-	}
-
-	return 0;
-}
-
-static istream_t *magic_autowrap(istream_t *strm)
-{
-	xfrm_stream_t *xfrm = NULL;
-	istream_t *wrapper = NULL;
-	const sqfs_u8 *data;
-	size_t avail;
-	int ret;
-
-	ret = istream_precache(strm);
-	if (ret != 0)
-		goto out;
-
-	data = strm->buffer + strm->buffer_offset;
-	avail = strm->buffer_used - strm->buffer_offset;
-
-	ret = tar_probe(data, avail);
-	if (ret > 0)
-		return strm;
-
-	ret = xfrm_compressor_id_from_magic(data, avail);
-	if (ret <= 0)
-		return strm;
-
-	xfrm = decompressor_stream_create(ret);
-	if (xfrm == NULL)
-		goto out;
-
-	wrapper = istream_xfrm_create(strm, xfrm);
-out:
-	sqfs_drop(strm);
-	sqfs_drop(xfrm);
-	return wrapper;
-}
-
-int main(int argc, char **argv)
-{
-	int status = EXIT_FAILURE;
-	istream_t *input_file = NULL;
-	sqfs_writer_t sqfs;
-
-	process_args(argc, argv);
-
-	input_file = istream_open_stdin();
-	if (input_file == NULL)
-		return EXIT_FAILURE;
-
-	input_file = magic_autowrap(input_file);
-	if (input_file == NULL)
-		return EXIT_FAILURE;
-
-	memset(&sqfs, 0, sizeof(sqfs));
-	if (sqfs_writer_init(&sqfs, &cfg))
-		goto out_if;
-
-	if (process_tarball(input_file, &sqfs))
-		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);
-out_if:
-	sqfs_drop(input_file);
-	return status;
-}
diff --git a/bin/tar2sqfs/tar2sqfs.h b/bin/tar2sqfs/tar2sqfs.h
deleted file mode 100644
index a21774b..0000000
--- a/bin/tar2sqfs/tar2sqfs.h
+++ /dev/null
@@ -1,39 +0,0 @@
-/* SPDX-License-Identifier: GPL-3.0-or-later */
-/*
- * tar2sqfs.h
- *
- * Copyright (C) 2019 David Oberhollenzer <goliath@infraroot.at>
- */
-#ifndef TAR2SQFS_H
-#define TAR2SQFS_H
-
-#include "config.h"
-#include "common.h"
-#include "compat.h"
-
-#include "util/util.h"
-#include "tar/tar.h"
-#include "tar/format.h"
-#include "xfrm/compress.h"
-#include "io/xfrm.h"
-
-#include <stdlib.h>
-#include <getopt.h>
-#include <string.h>
-#include <stdio.h>
-#include <errno.h>
-
-/* options.c */
-extern bool dont_skip;
-extern bool keep_time;
-extern bool no_tail_pack;
-extern bool no_symlink_retarget;
-extern sqfs_writer_cfg_t cfg;
-extern char *root_becomes;
-
-void process_args(int argc, char **argv);
-
-/* process_tarball.c */
-int process_tarball(istream_t *input_file, sqfs_writer_t *sqfs);
-
-#endif /* TAR2SQFS_H */
-- 
cgit v1.2.3