diff --git a/src/Makefile b/src/Makefile
index f7ebb66b1bcc9fcac65a267af6e3329c3cf9af75..0540cd83aaff10780c1ff75f4b8a5d40244bff4b 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -21,7 +21,7 @@ libapk_so		:= $(obj)/libapk.so.$(libapk_soname)
 libapk.so.$(libapk_soname)-objs := \
 	adb.o adb_comp.o adb_walk_adb.o adb_walk_genadb.o adb_walk_gentext.o adb_walk_text.o apk_adb.o \
 	atom.o blob.o commit.o common.o context.o crypto_openssl.o database.o hash.o \
-	extract.o extract_v2.o extract_v3.o io.o io_gunzip.o io_url.o tar.o \
+	extract_v2.o extract_v3.o fs_fsys.o fs_uvol.o io.o io_gunzip.o io_url.o tar.o \
 	package.o pathbuilder.o print.o solver.o trust.o version.o
 
 libapk.so.$(libapk_soname)-libs := libfetch/libfetch.a
diff --git a/src/apk_context.h b/src/apk_context.h
index 1bec2b965bdc5722c3040b3e78fb512511280818..7aae61b0fdf95d09005b5a73955e5a316cae37f1 100644
--- a/src/apk_context.h
+++ b/src/apk_context.h
@@ -9,9 +9,11 @@
 #ifndef APK_CONTEXT_H
 #define APK_CONTEXT_H
 
+#include "apk_blob.h"
 #include "apk_print.h"
 #include "apk_trust.h"
 #include "apk_io.h"
+#include "apk_crypto.h"
 #include "adb.h"
 
 #define APK_SIMULATE			BIT(0)
@@ -34,8 +36,6 @@
 #define APK_FORCE_NON_REPOSITORY	BIT(4)
 #define APK_FORCE_BINARY_STDOUT		BIT(5)
 
-struct apk_database;
-
 #define APK_OPENF_READ			0x0001
 #define APK_OPENF_WRITE			0x0002
 #define APK_OPENF_CREATE		0x0004
@@ -53,6 +53,8 @@ struct apk_database;
 				 APK_OPENF_NO_SCRIPTS |		\
 				 APK_OPENF_NO_WORLD)
 
+struct apk_database;
+
 struct apk_ctx {
 	unsigned int flags, force, lock_wait;
 	struct apk_out out;
@@ -70,7 +72,9 @@ struct apk_ctx {
 	struct apk_trust trust;
 	struct apk_id_cache id_cache;
 	struct apk_database *db;
-	int root_fd;
+	int root_fd, dest_fd;
+
+	struct apk_digest_ctx dctx;
 };
 
 void apk_ctx_init(struct apk_ctx *ac);
@@ -81,6 +85,7 @@ struct apk_trust *apk_ctx_get_trust(struct apk_ctx *ac);
 struct apk_id_cache *apk_ctx_get_id_cache(struct apk_ctx *ac);
 
 static inline int apk_ctx_fd_root(struct apk_ctx *ac) { return ac->root_fd; }
+static inline int apk_ctx_fd_dest(struct apk_ctx *ac) { return ac->dest_fd; }
 static inline time_t apk_ctx_since(struct apk_ctx *ac, time_t since) {
 	return (ac->force & APK_FORCE_REFRESH) ? APK_ISTREAM_FORCE_REFRESH : since;
 }
diff --git a/src/apk_crypto.h b/src/apk_crypto.h
index 03306940feeae752bd61a74160d3804ad2e273d3..18bf3b545c5ea575069c59b0948dc026bc4912ca 100644
--- a/src/apk_crypto.h
+++ b/src/apk_crypto.h
@@ -90,10 +90,16 @@ static inline int apk_digest_ctx_init(struct apk_digest_ctx *dctx, uint8_t alg)
 #ifdef EVP_MD_CTX_FLAG_FINALISE
 	EVP_MD_CTX_set_flags(dctx->mdctx, EVP_MD_CTX_FLAG_FINALISE);
 #endif
-	EVP_DigestInit_ex(dctx->mdctx, apk_digest_alg_to_evp(alg), 0);
+	if (alg != APK_DIGEST_NONE) EVP_DigestInit_ex(dctx->mdctx, apk_digest_alg_to_evp(alg), 0);
 	return 0;
 }
 
+static inline void apk_digest_ctx_reset(struct apk_digest_ctx *dctx, uint8_t alg)
+{
+	dctx->alg = alg;
+	EVP_DigestInit_ex(dctx->mdctx, apk_digest_alg_to_evp(alg), 0);
+}
+
 static inline void apk_digest_ctx_free(struct apk_digest_ctx *dctx) {
 	EVP_MD_CTX_free(dctx->mdctx);
 	dctx->mdctx = 0;
diff --git a/src/apk_defines.h b/src/apk_defines.h
index 395958b130ed1658e494400d266a12e5483e4005..0a502841a86829cda55fcf1bb26a333bb543df1c 100644
--- a/src/apk_defines.h
+++ b/src/apk_defines.h
@@ -62,7 +62,9 @@ enum {
 	APKE_INDEX_STALE,
 	APKE_FILE_INTEGRITY,
 	APKE_CACHE_NOT_AVAILABLE,
-	APKE_UVOL,
+	APKE_UVOL_NOT_AVAILABLE,
+	APKE_UVOL_ERROR,
+	APKE_UVOL_ROOT,
 };
 
 static inline void *ERR_PTR(long error) { return (void*) error; }
diff --git a/src/apk_extract.h b/src/apk_extract.h
index ecb39ed366c1ad5b2c02ee248c95091b2ae488a6..e3fabad29f27612a0892b13e633b3435e70b204f 100644
--- a/src/apk_extract.h
+++ b/src/apk_extract.h
@@ -18,16 +18,6 @@ struct adb_obj;
 struct apk_ctx;
 struct apk_extract_ctx;
 
-#define APK_EXTRACT_SKIP_FILE		0x111
-
-#define APK_EXTRACTF_NO_CHOWN		0x0001
-#define APK_EXTRACTF_NO_OVERWRITE	0x0002
-
-int apk_extract_file(int atfd, const struct apk_file_info *ae,
-		const char *extract_name, const char *hardlink_name,
-		struct apk_istream *is, apk_progress_cb cb, void *cb_ctx,
-		unsigned int extract_flags, struct apk_ctx *ac);
-
 struct apk_extract_ops {
 	int (*v2index)(struct apk_extract_ctx *, apk_blob_t *desc, struct apk_istream *is);
 	int (*v2meta)(struct apk_extract_ctx *, struct apk_istream *is);
diff --git a/src/apk_fs.h b/src/apk_fs.h
new file mode 100644
index 0000000000000000000000000000000000000000..6a2a285b29bd43ce24819acf351919e807009c2b
--- /dev/null
+++ b/src/apk_fs.h
@@ -0,0 +1,70 @@
+/* apk_fs.h - Alpine Package Keeper (APK)
+ *
+ * Copyright (C) 2021 Timo Teräs <timo.teras@iki.fi>
+ * All rights reserved.
+ *
+ * SPDX-License-Identifier: GPL-2.0-only
+ */
+
+#ifndef APK_FS_H
+#define APK_FS_H
+
+#include "apk_context.h"
+#include "apk_io.h"
+#include "apk_pathbuilder.h"
+
+#define APK_FS_CTRL_COMMIT	1
+#define APK_FS_CTRL_APKNEW	2
+#define APK_FS_CTRL_CANCEL	3
+#define APK_FS_CTRL_DELETE	4
+
+#define APK_FS_DIR_MODIFIED	1
+
+struct apk_fsdir_ops;
+
+struct apk_fsdir {
+	struct apk_ctx *ac;
+	const struct apk_fsdir_ops *ops;
+	struct apk_pathbuilder pb;
+	apk_blob_t pkgctx;
+};
+
+struct apk_fsdir_ops {
+	int (*dir_create)(struct apk_fsdir *, mode_t);
+	int (*dir_delete)(struct apk_fsdir *);
+	int (*dir_check)(struct apk_fsdir *, mode_t, uid_t, gid_t);
+	int (*dir_update_perms)(struct apk_fsdir *, mode_t, uid_t, gid_t);
+
+	int (*file_extract)(struct apk_ctx *, const struct apk_file_info *, struct apk_istream *, apk_progress_cb, void *, unsigned int, apk_blob_t);
+	int (*file_control)(struct apk_fsdir *, apk_blob_t, int);
+	int (*file_digest)(struct apk_fsdir *, apk_blob_t, uint8_t alg, struct apk_digest *);
+};
+
+#define APK_FSEXTRACTF_NO_CHOWN		0x0001
+#define APK_FSEXTRACTF_NO_OVERWRITE	0x0002
+
+int apk_fs_extract(struct apk_ctx *, const struct apk_file_info *, struct apk_istream *, apk_progress_cb, void *, unsigned int, apk_blob_t);
+
+void apk_fsdir_get(struct apk_fsdir *, apk_blob_t dir, struct apk_ctx *, apk_blob_t);
+
+static inline int apk_fsdir_create(struct apk_fsdir *fs, mode_t mode) {
+	return fs->ops->dir_create(fs, mode);
+}
+static inline int apk_fsdir_delete(struct apk_fsdir *fs) {
+	return fs->ops->dir_delete(fs);
+}
+static inline int apk_fsdir_check(struct apk_fsdir *fs, mode_t mode, uid_t uid, gid_t gid) {
+	return fs->ops->dir_check(fs, mode, uid, gid);
+}
+static inline int apk_fsdir_update_perms(struct apk_fsdir *fs, mode_t mode, uid_t uid, gid_t gid) {
+	return fs->ops->dir_update_perms(fs, mode, uid, gid);
+}
+
+static inline int apk_fsdir_file_control(struct apk_fsdir *fs, apk_blob_t filename, int ctrl) {
+	return fs->ops->file_control(fs, filename, ctrl);
+}
+static inline int apk_fsdir_file_digest(struct apk_fsdir *fs, apk_blob_t filename, uint8_t alg, struct apk_digest *dgst) {
+	return fs->ops->file_digest(fs, filename, alg, dgst);
+}
+
+#endif
diff --git a/src/apk_io.h b/src/apk_io.h
index 762f9e0fd2bf91f2803fae4548aa3e4a67a10e8f..d04638a5726220b1a19ed552d9b2c07d6ab60240 100644
--- a/src/apk_io.h
+++ b/src/apk_io.h
@@ -44,7 +44,6 @@ struct apk_file_meta {
 struct apk_file_info {
 	const char *name;
 	const char *link_target;
-	const char *uvol_name;
 	const char *uname;
 	const char *gname;
 	off_t size;
@@ -140,8 +139,9 @@ struct apk_digest_istream {
 	struct apk_istream *pis;
 	struct apk_digest *digest;
 	struct apk_digest_ctx dctx;
+	off_t size_left;
 };
-struct apk_istream *apk_istream_verify(struct apk_digest_istream *dis, struct apk_istream *is, struct apk_digest *d);
+struct apk_istream *apk_istream_verify(struct apk_digest_istream *dis, struct apk_istream *is, off_t size, struct apk_digest *d);
 
 #define APK_ISTREAM_TEE_COPY_META 1
 #define APK_ISTREAM_TEE_OPTIONAL  2
diff --git a/src/app_add.c b/src/app_add.c
index 1d20e81eeb05835fa12efec6df5f09ccdf3ca99c..614dfb479fbacf0f44ade086edd21bf039a62537 100644
--- a/src/app_add.c
+++ b/src/app_add.c
@@ -10,11 +10,13 @@
 #include <errno.h>
 #include <stdio.h>
 #include <unistd.h>
+
 #include "apk_applet.h"
 #include "apk_database.h"
 #include "apk_print.h"
 #include "apk_solver.h"
 #include "apk_extract.h"
+#include "apk_fs.h"
 
 struct add_ctx {
 	const char *virtpkg;
@@ -43,7 +45,7 @@ static int option_parse_applet(void *ctx, struct apk_ctx *ac, int opt, const cha
 		actx->solver_flags |= APK_SOLVERF_LATEST;
 		break;
 	case OPT_ADD_no_chown:
-		actx->extract_flags |= APK_EXTRACTF_NO_CHOWN;
+		actx->extract_flags |= APK_FSEXTRACTF_NO_CHOWN;
 		break;
 	case OPT_ADD_upgrade:
 		actx->solver_flags |= APK_SOLVERF_UPGRADE;
@@ -124,8 +126,8 @@ static int add_main(void *ctx, struct apk_ctx *ac, struct apk_string_array *args
 
 	apk_dependency_array_copy(&world, db->world);
 
-	if (getuid() != 0 || (actx->extract_flags & APK_EXTRACTF_NO_CHOWN))
-		db->extract_flags |= APK_EXTRACTF_NO_CHOWN;
+	if (getuid() != 0 || (actx->extract_flags & APK_FSEXTRACTF_NO_CHOWN))
+		db->extract_flags |= APK_FSEXTRACTF_NO_CHOWN;
 
 	if (actx->virtpkg) {
 		apk_blob_t b = APK_BLOB_STR(actx->virtpkg);
diff --git a/src/app_extract.c b/src/app_extract.c
index 2dd310bf2c30779b2ec199d4153a96ead70c4add..fea49246f2551fb882f3ec891b67bdefcafa2a27 100644
--- a/src/app_extract.c
+++ b/src/app_extract.c
@@ -15,6 +15,7 @@
 #include "apk_applet.h"
 #include "apk_print.h"
 #include "apk_extract.h"
+#include "apk_fs.h"
 
 struct extract_ctx {
 	const char *destination;
@@ -22,7 +23,6 @@ struct extract_ctx {
 
 	struct apk_extract_ctx ectx;
 	struct apk_ctx *ac;
-	int root_fd;
 };
 
 
@@ -41,7 +41,7 @@ static int option_parse_applet(void *pctx, struct apk_ctx *ac, int opt, const ch
 		ctx->destination = optarg;
 		break;
 	case OPT_EXTRACT_no_chown:
-		ctx->extract_flags |= APK_EXTRACTF_NO_CHOWN;
+		ctx->extract_flags |= APK_FSEXTRACTF_NO_CHOWN;
 		break;
 	default:
 		return -ENOTSUP;
@@ -66,8 +66,7 @@ static int extract_file(struct apk_extract_ctx *ectx, const struct apk_file_info
 
 	apk_dbg2(out, "%s", fi->name);
 
-	return apk_extract_file(ctx->root_fd, fi, 0, 0, is, 0, 0,
-		ctx->extract_flags, ectx->ac);
+	return apk_fs_extract(ctx->ac, fi, is, 0, 0, ctx->extract_flags, APK_BLOB_NULL);
 }
 
 static const struct apk_extract_ops extract_ops = {
@@ -84,10 +83,11 @@ static int extract_main(void *pctx, struct apk_ctx *ac, struct apk_string_array
 	int r = 0;
 
 	ctx->ac = ac;
-	if (!(ac->force & APK_FORCE_OVERWRITE)) ctx->extract_flags |= APK_EXTRACTF_NO_OVERWRITE;
+	if (!(ac->force & APK_FORCE_OVERWRITE)) ctx->extract_flags |= APK_FSEXTRACTF_NO_OVERWRITE;
 	if (!ctx->destination) ctx->destination = ".";
-	ctx->root_fd = openat(AT_FDCWD, ctx->destination, O_RDONLY);
-	if (ctx->root_fd < 0) {
+
+	ac->dest_fd = openat(AT_FDCWD, ctx->destination, O_RDONLY);
+	if (ac->dest_fd < 0) {
 		r = -errno;
 		apk_err(out, "Error opening destination '%s': %s",
 			ctx->destination, apk_error_str(r));
@@ -103,7 +103,7 @@ static int extract_main(void *pctx, struct apk_ctx *ac, struct apk_string_array
 			break;
 		}
 	}
-	close(ctx->root_fd);
+	close(ac->dest_fd);
 	return r;
 }
 
diff --git a/src/context.c b/src/context.c
index 4ad2bbf1ea0cc377f1511751f246143e3f3a6612..7afb579f4e2cb92892003bc6dae7d9dd908c56ef 100644
--- a/src/context.c
+++ b/src/context.c
@@ -21,6 +21,7 @@ void apk_ctx_init(struct apk_ctx *ac)
 	ac->out.out = stdout;
 	ac->out.err = stderr;
 	ac->out.verbosity = 1;
+	apk_digest_ctx_init(&ac->dctx, APK_DIGEST_SHA256);
 }
 
 void apk_ctx_free(struct apk_ctx *ac)
@@ -43,8 +44,19 @@ int apk_ctx_prepare(struct apk_ctx *ac)
 	if (!ac->keys_dir) ac->keys_dir = "etc/apk/keys";
 	if (!ac->root) ac->root = "/";
 	if (!ac->cache_max_age) ac->cache_max_age = 4*60*60; /* 4 hours default */
-	if (!strcmp(ac->root, "/")) ac->flags |= APK_NO_CHROOT; /* skip chroot if root is default */
-	ac->uvol = getenv("APK_UVOL") ?: "/usr/bin/uvol";
+
+	if (!strcmp(ac->root, "/")) {
+		// No chroot needed if using system root
+		ac->flags |= APK_NO_CHROOT;
+
+		// Check uvol availability
+		ac->uvol = getenv("APK_UVOL") ?: "/usr/bin/uvol";
+		if (access(ac->uvol, X_OK) != 0)
+			ac->uvol = ERR_PTR(-APKE_UVOL_NOT_AVAILABLE);
+	} else {
+		ac->uvol = ERR_PTR(-APKE_UVOL_ROOT);
+	}
+
 
 	ac->root_fd = openat(AT_FDCWD, ac->root, O_RDONLY | O_CLOEXEC);
 	if (ac->root_fd < 0 && (ac->open_flags & APK_OPENF_CREATE)) {
@@ -55,13 +67,15 @@ int apk_ctx_prepare(struct apk_ctx *ac)
 		apk_err(&ac->out, "Unable to open root: %s", apk_error_str(errno));
 		return -errno;
 	}
+	ac->dest_fd = ac->root_fd;
 
 	if (ac->open_flags & APK_OPENF_WRITE) {
-		const char *log_path = "var/log/apk.log", *log_dir = "var/log";
+		const char *log_path = "var/log/apk.log";
 		const int lflags = O_WRONLY | O_APPEND | O_CREAT | O_CLOEXEC;
 		int fd = openat(ac->root_fd, log_path, lflags, 0644);
 		if (fd < 0 && (ac->open_flags & APK_OPENF_CREATE)) {
-			mkdirat(ac->root_fd, log_dir, 0755);
+			mkdirat(ac->root_fd, "var", 0755);
+			mkdirat(ac->root_fd, "var/log", 0755);
 			fd = openat(ac->root_fd, log_path, lflags, 0644);
 		}
 		if (fd < 0) {
diff --git a/src/database.c b/src/database.c
index f5c8d8cfe79810f5864fe709b9a65602c6f36150..a3a5800d0ba0a8ce21ee76eac873596fafd51512 100644
--- a/src/database.c
+++ b/src/database.c
@@ -37,6 +37,7 @@
 #include "apk_openssl.h"
 #include "apk_tar.h"
 #include "apk_adb.h"
+#include "apk_fs.h"
 
 static const apk_spn_match_def apk_spn_repo_separators = {
 	[1] = (1<<1) /* tab */,
@@ -83,6 +84,11 @@ struct install_ctx {
 	struct hlist_node **file_diri_node;
 };
 
+static apk_blob_t apk_pkg_ctx(struct apk_package *pkg)
+{
+	return APK_BLOB_PTR_LEN(pkg->name->name, strlen(pkg->name->name)+1);
+}
+
 static apk_blob_t pkg_name_get_key(apk_hash_item item)
 {
 	return APK_BLOB_STR(((struct apk_name *) item)->name);
@@ -244,23 +250,21 @@ static struct apk_db_acl *apk_db_acl_atomize_digest(struct apk_database *db, mod
 
 static void apk_db_dir_prepare(struct apk_database *db, struct apk_db_dir *dir, mode_t newmode)
 {
-	struct stat st;
+	struct apk_fsdir d;
 
 	if (dir->namelen == 0) return;
 	if (dir->created) return;
 
-	if (fstatat(db->root_fd, dir->name, &st, AT_SYMLINK_NOFOLLOW) == 0) {
-		/* If directory exists and stats match what we expect,
-		 * then we can allow auto updating the permissions */
-		dir->created = 1;
-		dir->update_permissions |=
-			(st.st_mode & 07777) == (dir->mode & 07777) &&
-			st.st_uid == dir->uid && st.st_gid == dir->gid;
-	} else if (newmode) {
+	apk_fsdir_get(&d, APK_BLOB_PTR_LEN(dir->name, dir->namelen), db->ctx, APK_BLOB_NULL);
+	switch (apk_fsdir_check(&d, dir->mode, dir->uid, dir->gid)) {
+	default:
 		if (!(db->ctx->flags & APK_SIMULATE))
-			mkdirat(db->root_fd, dir->name, newmode);
-		dir->created = 1;
+			apk_fsdir_create(&d, dir->mode);
+	case 0:
 		dir->update_permissions = 1;
+	case APK_FS_DIR_MODIFIED:
+		dir->created = 1;
+		break;
 	}
 }
 
@@ -272,9 +276,11 @@ void apk_db_dir_unref(struct apk_database *db, struct apk_db_dir *dir, int rmdir
 	if (dir->namelen != 0) {
 		if (rmdir_mode == APK_DIR_REMOVE) {
 			dir->modified = 1;
-			if (!(db->ctx->flags & APK_SIMULATE) &&
-			    unlinkat(db->root_fd, dir->name, AT_REMOVEDIR) != 0)
-				;
+			if (!(db->ctx->flags & APK_SIMULATE)) {
+				struct apk_fsdir d;
+				apk_fsdir_get(&d, APK_BLOB_PTR_LEN(dir->name, dir->namelen), db->ctx, APK_BLOB_NULL);
+				apk_fsdir_delete(&d);
+			}
 		}
 		apk_db_dir_unref(db, dir->parent, rmdir_mode);
 		dir->parent = NULL;
@@ -1953,20 +1959,15 @@ static int update_permissions(apk_hash_item item, void *pctx)
 	struct update_permissions_ctx *ctx = pctx;
 	struct apk_database *db = ctx->db;
 	struct apk_db_dir *dir = (struct apk_db_dir *) item;
-	struct stat st;
-	int r;
+	struct apk_fsdir d;
 
 	if (dir->refs == 0) return 0;
 	if (!dir->update_permissions) return 0;
 	dir->seen = 0;
 
-	r = fstatat(db->root_fd, dir->name, &st, AT_SYMLINK_NOFOLLOW);
-	if (r < 0 || (st.st_mode & 07777) != (dir->mode & 07777))
-		if (fchmodat(db->root_fd, dir->name, dir->mode, 0) < 0)
-			ctx->errors++;
-	if (r < 0 || st.st_uid != dir->uid || st.st_gid != dir->gid)
-		if (fchownat(db->root_fd, dir->name, dir->uid, dir->gid, 0) < 0)
-			ctx->errors++;
+	apk_fsdir_get(&d, APK_BLOB_PTR_LEN(dir->name, dir->namelen), db->ctx, APK_BLOB_NULL);
+	if (apk_fsdir_update_perms(&d, dir->mode, dir->uid, dir->gid) != 0)
+		ctx->errors++;
 
 	return 0;
 }
@@ -2330,37 +2331,6 @@ static struct apk_db_dir_instance *apk_db_install_directory_entry(struct install
 	return diri;
 }
 
-#define TMPNAME_MAX	(PATH_MAX + 64)
-
-static const char *format_tmpname(struct apk_package *pkg, struct apk_db_file *f, char tmpname[static TMPNAME_MAX])
-{
-	struct apk_digest_ctx dctx;
-	struct apk_digest d;
-	apk_blob_t b = APK_BLOB_PTR_LEN(tmpname, TMPNAME_MAX);
-
-	if (!f) return NULL;
-
-	if (apk_digest_ctx_init(&dctx, APK_DIGEST_SHA256) != 0) return NULL;
-
-	apk_digest_ctx_update(&dctx, pkg->name->name, strlen(pkg->name->name) + 1);
-	apk_digest_ctx_update(&dctx, f->diri->dir->name, f->diri->dir->namelen);
-	apk_digest_ctx_update(&dctx, "/", 1);
-	apk_digest_ctx_update(&dctx, f->name, f->namelen);
-	apk_digest_ctx_final(&dctx, &d);
-	apk_digest_ctx_free(&dctx);
-
-	apk_blob_push_blob(&b, APK_BLOB_PTR_LEN(f->diri->dir->name, f->diri->dir->namelen));
-	if (f->diri->dir->namelen > 0) {
-		apk_blob_push_blob(&b, APK_BLOB_STR("/.apk."));
-	} else {
-		apk_blob_push_blob(&b, APK_BLOB_STR(".apk."));
-	}
-	apk_blob_push_hexdump(&b, APK_BLOB_PTR_LEN((char *)d.data, 24));
-	apk_blob_push_blob(&b, APK_BLOB_PTR_LEN("", 1));
-
-	return tmpname;
-}
-
 static int contains_control_character(const char *str)
 {
 	for (const uint8_t *p = (const uint8_t *) str; *p; p++) {
@@ -2386,7 +2356,7 @@ static int apk_db_install_v3meta(struct apk_extract_ctx *ectx, struct adb_obj *p
 	struct adb_obj triggers, pkginfo, obj;
 	int i;
 
-	apk_pkg_from_adb(db, ctx->pkg, pkg);
+	// Extract the information not available in index
 	adb_ro_obj(pkg, ADBI_PKG_PKGINFO, &pkginfo);
 	apk_deps_from_adb(&ipkg->replaces, db, adb_ro_obj(&pkginfo, ADBI_PI_REPLACES, &obj));
 	ipkg->replaces_priority = adb_ro_int(&pkginfo, ADBI_PI_PRIORITY);
@@ -2424,7 +2394,6 @@ static int apk_db_install_file(struct apk_extract_ctx *ectx, const struct apk_fi
 	struct apk_db_dir_instance *diri = ctx->diri;
 	struct apk_db_file *file, *link_target_file = NULL;
 	int ret = 0, r;
-	char tmpname_file[TMPNAME_MAX], tmpname_link_target[TMPNAME_MAX];
 
 	apk_db_run_pending_script(ctx);
 	if (ae->name[0] == '.') return 0;
@@ -2440,13 +2409,6 @@ static int apk_db_install_file(struct apk_extract_ctx *ectx, const struct apk_fi
 		return 0;
 	}
 
-	if (ae->uvol_name) {
-		apk_warn(out, PKG_VER_FMT": %s: uvol not supported yet",
-			PKG_VER_PRINTF(pkg), ae->name);
-		ipkg->broken_files = 1;
-		return APK_EXTRACT_SKIP_FILE;
-	}
-
 	/* Installable entry */
 	ctx->current_file_size = apk_calc_installed_size(ae->size);
 	if (!S_ISDIR(ae->mode)) {
@@ -2563,12 +2525,7 @@ static int apk_db_install_file(struct apk_extract_ctx *ectx, const struct apk_fi
 
 		/* Extract the file with temporary name */
 		file->acl = apk_db_acl_atomize_digest(db, ae->mode, ae->uid, ae->gid, &ae->xattr_digest);
-		r = apk_extract_file(
-				db->root_fd, ae,
-				format_tmpname(pkg, file, tmpname_file),
-				format_tmpname(pkg, link_target_file, tmpname_link_target),
-				is, extract_cb, ctx, db->extract_flags, ac);
-
+		r = apk_fs_extract(ac, ae, is, extract_cb, ctx, db->extract_flags, apk_pkg_ctx(pkg));
 		switch (r) {
 		case 0:
 			/* Hardlinks need special care for checksum */
@@ -2596,6 +2553,10 @@ static int apk_db_install_file(struct apk_extract_ctx *ectx, const struct apk_fi
 				ctx->missing_checksum = 1;
 			}
 			break;
+		case -APKE_UVOL_ROOT:
+		case -APKE_UVOL_NOT_AVAILABLE:
+			ipkg->broken_files = 1;
+			break;
 		case -ENOTSUP:
 			ipkg->broken_xattr = 1;
 			break;
@@ -2638,22 +2599,20 @@ static void apk_db_purge_pkg(struct apk_database *db,
 	struct apk_db_dir_instance *diri;
 	struct apk_db_file *file;
 	struct apk_db_file_hash_key key;
-	struct apk_file_info fi;
+	struct apk_fsdir d;
+	struct apk_digest dgst;
 	struct hlist_node *dc, *dn, *fc, *fn;
 	unsigned long hash;
-	char name[TMPNAME_MAX];
+	int ctrl = is_installed ? APK_FS_CTRL_DELETE : APK_FS_CTRL_CANCEL;
 
 	hlist_for_each_entry_safe(diri, dc, dn, &ipkg->owned_dirs, pkg_dirs_list) {
+		apk_blob_t dirname = APK_BLOB_PTR_LEN(diri->dir->name, diri->dir->namelen);
 		if (is_installed) diri->dir->modified = 1;
+		apk_fsdir_get(&d, dirname, db->ctx, apk_pkg_ctx(ipkg->pkg));
 
 		hlist_for_each_entry_safe(file, fc, fn, &diri->owned_files, diri_files_list) {
-			if (is_installed)
-				snprintf(name, sizeof name, DIR_FILE_FMT, DIR_FILE_PRINTF(diri->dir, file));
-			else
-				format_tmpname(ipkg->pkg, file, name);
-
 			key = (struct apk_db_file_hash_key) {
-				.dirname = APK_BLOB_PTR_LEN(diri->dir->name, diri->dir->namelen),
+				.dirname = dirname,
 				.filename = APK_BLOB_PTR_LEN(file->name, file->namelen),
 			};
 			hash = apk_blob_hash_seed(key.filename, diri->dir->hash);
@@ -2661,10 +2620,11 @@ static void apk_db_purge_pkg(struct apk_database *db,
 			    (diri->dir->protect_mode == APK_PROTECT_NONE) ||
 			    (db->ctx->flags & APK_PURGE) ||
 			    (file->csum.type != APK_CHECKSUM_NONE &&
-			     apk_fileinfo_get(db->root_fd, name, APK_FI_NOFOLLOW | APK_FI_DIGEST(apk_dbf_digest(file)), &fi, &db->atoms) == 0 &&
-			     apk_digest_cmp_csum(&fi.digest, &file->csum) == 0))
-				unlinkat(db->root_fd, name, 0);
-			apk_dbg2(out, "%s", name);
+			     apk_fsdir_file_digest(&d, key.filename, apk_dbf_digest(file), &dgst) == 0 &&
+			     apk_digest_cmp_csum(&dgst, &file->csum) == 0))
+				apk_fsdir_file_control(&d, key.filename, ctrl);
+
+			apk_dbg2(out, DIR_FILE_FMT, DIR_FILE_PRINTF(diri->dir, file));
 			__hlist_del(fc, &diri->owned_files.first);
 			if (is_installed) {
 				apk_hash_delete_hashed(&db->installed.files, APK_BLOB_BUF(&key), hash);
@@ -2685,22 +2645,22 @@ static void apk_db_migrate_files(struct apk_database *db,
 	struct apk_db_dir *dir;
 	struct apk_db_file *file, *ofile;
 	struct apk_db_file_hash_key key;
-	struct apk_file_info fi;
 	struct hlist_node *dc, *dn, *fc, *fn;
+	struct apk_fsdir d;
+	struct apk_digest dgst;
 	unsigned long hash;
-	char name[PATH_MAX], tmpname[TMPNAME_MAX];
-	int cstype, r;
+	apk_blob_t dirname;
+	int r, ctrl;
 
 	hlist_for_each_entry_safe(diri, dc, dn, &ipkg->owned_dirs, pkg_dirs_list) {
 		dir = diri->dir;
 		dir->modified = 1;
+		dirname = APK_BLOB_PTR_LEN(dir->name, dir->namelen);
+		apk_fsdir_get(&d, dirname, db->ctx, apk_pkg_ctx(ipkg->pkg));
 
 		hlist_for_each_entry_safe(file, fc, fn, &diri->owned_files, diri_files_list) {
-			snprintf(name, sizeof(name), DIR_FILE_FMT, DIR_FILE_PRINTF(diri->dir, file));
-			format_tmpname(ipkg->pkg, file, tmpname);
-
 			key = (struct apk_db_file_hash_key) {
-				.dirname = APK_BLOB_PTR_LEN(dir->name, dir->namelen),
+				.dirname = dirname,
 				.filename = APK_BLOB_PTR_LEN(file->name, file->namelen),
 			};
 
@@ -2710,61 +2670,41 @@ static void apk_db_migrate_files(struct apk_database *db,
 			ofile = (struct apk_db_file *) apk_hash_get_hashed(
 				&db->installed.files, APK_BLOB_BUF(&key), hash);
 
-			/* We want to compare checksums only if one exists
-			 * in db, and the file is in a protected path */
-			cstype = APK_CHECKSUM_NONE;
-			if (ofile != NULL && diri->dir->protect_mode != APK_PROTECT_NONE)
-				cstype = APK_FI_DIGEST(apk_dbf_digest(ofile));
-			cstype |= APK_FI_NOFOLLOW;
-
-			r = apk_fileinfo_get(db->root_fd, name, cstype, &fi, &db->atoms);
+			ctrl = APK_FS_CTRL_COMMIT;
 			if (ofile && ofile->diri->pkg->name == NULL) {
-				/* File was from overlay, delete the
-				 * packages version */
-				unlinkat(db->root_fd, tmpname, 0);
+				// File was from overlay, delete the package's version
+				ctrl = APK_FS_CTRL_CANCEL;
 			} else if ((diri->dir->protect_mode != APK_PROTECT_NONE) &&
-				   (r == 0) &&
-				   (ofile == NULL ||
-				    ofile->csum.type == APK_CHECKSUM_NONE ||
-				    apk_digest_cmp_csum(&fi.digest, &ofile->csum) != 0)) {
-				/* Protected directory, with file without
-				 * db entry, or local modifications.
-				 *
-				 * Delete the apk-new if it's identical with the
-				 * existing file */
-				if (ofile == NULL ||
-				    ofile->csum.type != file->csum.type)
-					apk_fileinfo_get(db->root_fd, name,
-						APK_FI_NOFOLLOW |APK_FI_DIGEST(apk_dbf_digest(file)),
-						&fi, &db->atoms);
+				   (!ofile || ofile->csum.type == APK_CHECKSUM_NONE ||
+				    (apk_fsdir_file_digest(&d, key.filename, apk_dbf_digest(ofile), &dgst) == 0 &&
+				     apk_digest_cmp_csum(&dgst, &ofile->csum) != 0))) {
+				// Protected directory, and a file without db entry
+				// or with local modifications. Keep the filesystem file.
+				// Determine if the package's file should be kept as .apk-new
 				if ((db->ctx->flags & APK_CLEAN_PROTECTED) ||
 				    (file->csum.type != APK_CHECKSUM_NONE &&
-				     apk_digest_cmp_csum(&fi.digest, &file->csum) == 0)) {
-					unlinkat(db->root_fd, tmpname, 0);
+				     (apk_fsdir_file_digest(&d, key.filename, apk_dbf_digest(file), &dgst) == 0 &&
+				      apk_digest_cmp_csum(&dgst, &file->csum) == 0))) {
+					// No .apk-new files allowed, or the file on disk has the same
+					// hash as the file from new package. Keep the on disk one.
+					ctrl = APK_FS_CTRL_CANCEL;
 				} else {
-					snprintf(name, sizeof name,
-						 DIR_FILE_FMT ".apk-new",
-						 DIR_FILE_PRINTF(diri->dir, file));
-					if (renameat(db->root_fd, tmpname,
-						     db->root_fd, name) != 0) {
-						apk_err(out, PKG_VER_FMT": failed to rename %s to %s.",
-							PKG_VER_PRINTF(ipkg->pkg),
-							tmpname, name);
-						ipkg->broken_files = 1;
-					}
+					// All files difference. Use the package's file as .apk-new.
+					ctrl = APK_FS_CTRL_APKNEW;
 				}
+			}
 
-			} else {
-				/* Overwrite the old file */
-				if (renameat(db->root_fd, tmpname,
-					     db->root_fd, name) != 0) {
-					apk_err(out, PKG_VER_FMT": failed to rename %s to %s.",
-						PKG_VER_PRINTF(ipkg->pkg), tmpname, name);
-					ipkg->broken_files = 1;
-				}
+			// Commit changes
+			r = apk_fsdir_file_control(&d, key.filename, ctrl);
+			if (r < 0) {
+				apk_err(out, PKG_VER_FMT": failed to commit " DIR_FILE_FMT ": %s",
+					PKG_VER_PRINTF(ipkg->pkg),
+					DIR_FILE_PRINTF(diri->dir, file),
+					apk_error_str(r));
+				ipkg->broken_files = 1;
 			}
 
-			/* Claim ownership of the file in db */
+			// Claim ownership of the file in db
 			if (ofile != file) {
 				if (ofile != NULL) {
 					hlist_del(&ofile->diri_files_list,
diff --git a/src/extract.c b/src/extract.c
deleted file mode 100644
index 00d871f3fbb4d5772375030513b56eb9d88062b4..0000000000000000000000000000000000000000
--- a/src/extract.c
+++ /dev/null
@@ -1,235 +0,0 @@
-/* extract.c - Alpine Package Keeper (APK)
- *
- * Copyright (C) 2005-2008 Natanael Copa <n@tanael.org>
- * Copyright (C) 2008-2011 Timo Teräs <timo.teras@iki.fi>
- * All rights reserved.
- *
- * SPDX-License-Identifier: GPL-2.0-only
- */
-
-#include <spawn.h>
-#include <unistd.h>
-#include <sys/stat.h>
-#include <sys/wait.h>
-#include <sys/xattr.h>
-
-#include "apk_context.h"
-#include "apk_extract.h"
-
-static int uvol_run(struct apk_ctx *ac, char *action, const char *volname, char *arg1, char *arg2)
-{
-	struct apk_out *out = &ac->out;
-	pid_t pid;
-	int r, status;
-	char *argv[] = { (char*)apk_ctx_get_uvol(ac), action, (char*) volname, arg1, arg2, 0 };
-	posix_spawn_file_actions_t act;
-
-	posix_spawn_file_actions_init(&act);
-	posix_spawn_file_actions_addclose(&act, STDIN_FILENO);
-	r = posix_spawn(&pid, apk_ctx_get_uvol(ac), &act, 0, argv, environ);
-	posix_spawn_file_actions_destroy(&act);
-	if (r != 0) {
-		apk_err(out, "%s: uvol exec error: %s", volname, apk_error_str(r));
-		return r;
-	}
-	while (waitpid(pid, &status, 0) < 0 && errno == EINTR);
-	if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
-		apk_err(out, "%s: uvol exited with error %d", volname, WEXITSTATUS(status));
-		return -APKE_UVOL;
-	}
-	return 0;
-}
-
-static int uvol_extract(struct apk_ctx *ac, const char *volname, char *arg1, off_t sz,
-	struct apk_istream *is, apk_progress_cb cb, void *cb_ctx)
-{
-	struct apk_out *out = &ac->out;
-	struct apk_ostream *os;
-	pid_t pid;
-	int r, status, pipefds[2];
-	char *argv[] = { (char*)apk_ctx_get_uvol(ac), "write", (char*) volname, arg1, 0 };
-	posix_spawn_file_actions_t act;
-
-	if (pipe2(pipefds, O_CLOEXEC) != 0) return -errno;
-
-	posix_spawn_file_actions_init(&act);
-	posix_spawn_file_actions_adddup2(&act, pipefds[0], STDIN_FILENO);
-	r = posix_spawn(&pid, apk_ctx_get_uvol(ac), &act, 0, argv, environ);
-	posix_spawn_file_actions_destroy(&act);
-	if (r != 0) {
-		apk_err(out, "%s: uvol exec error: %s", volname, apk_error_str(r));
-		return r;
-	}
-	close(pipefds[0]);
-	os = apk_ostream_to_fd(pipefds[1]);
-	apk_stream_copy(is, os, sz, cb, cb_ctx, 0);
-	r = apk_ostream_close(os);
-	if (r != 0) {
-		if (r >= 0) r = -APKE_UVOL;
-		apk_err(out, "%s: uvol write error: %s", volname, apk_error_str(r));
-		return r;
-	}
-
-	while (waitpid(pid, &status, 0) < 0 && errno == EINTR);
-	if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
-		apk_err(out, "%s: uvol exited with error %d", volname, WEXITSTATUS(status));
-		return -APKE_UVOL;
-	}
-
-	return 0;
-}
-
-static int apk_extract_volume(struct apk_ctx *ac, const struct apk_file_info *fi,
-	struct apk_istream *is, apk_progress_cb cb, void *cb_ctx)
-{
-	char size[64];
-	int r;
-
-	snprintf(size, sizeof size, "%ju", fi->size);
-	r = uvol_run(ac, "create", fi->uvol_name, size, "ro");
-	if (r != 0) return r;
-
-	r = uvol_extract(ac, fi->uvol_name, size, fi->size, is, cb, cb_ctx);
-	if (r == 0) r = uvol_run(ac, "up", fi->uvol_name, 0, 0);
-	if (r != 0) uvol_run(ac, "remove", fi->uvol_name, 0, 0);
-	return r;
-}
-
-int apk_extract_file(int atfd, const struct apk_file_info *ae,
-		const char *extract_name, const char *link_target,
-		struct apk_istream *is,
-		apk_progress_cb cb, void *cb_ctx,
-		unsigned int extract_flags, struct apk_ctx *ac)
-{
-	struct apk_out *out = &ac->out;
-	struct apk_xattr *xattr;
-	const char *fn = extract_name ?: ae->name;
-	int fd, r = -1, atflags = 0, ret = 0;
-
-	if (ae->uvol_name && is) {
-		if (extract_name || link_target) return -APKE_UVOL;
-		return apk_extract_volume(ac, ae, is, cb, cb_ctx);
-	}
-
-	if (!S_ISDIR(ae->mode) && !(extract_flags & APK_EXTRACTF_NO_OVERWRITE)) {
-		if (unlinkat(atfd, fn, 0) != 0 && errno != ENOENT) return -errno;
-	}
-
-	switch (ae->mode & S_IFMT) {
-	case S_IFDIR:
-		r = mkdirat(atfd, fn, ae->mode & 07777);
-		if (r < 0 && errno != EEXIST)
-			ret = -errno;
-		break;
-	case S_IFREG:
-		if (ae->link_target == NULL) {
-			int flags = O_RDWR | O_CREAT | O_TRUNC | O_CLOEXEC | O_EXCL;
-			int fd = openat(atfd, fn, flags, ae->mode & 07777);
-			if (fd < 0) {
-				ret = -errno;
-				break;
-			}
-			struct apk_ostream *os = apk_ostream_to_fd(fd);
-			if (IS_ERR(os)) {
-				ret = PTR_ERR(os);
-				break;
-			}
-			apk_stream_copy(is, os, ae->size, cb, cb_ctx, 0);
-			r = apk_ostream_close(os);
-			if (r < 0) {
-				unlinkat(atfd, fn, 0);
-				ret = r;
-			}
-		} else {
-			r = linkat(atfd, link_target ?: ae->link_target, atfd, fn, 0);
-			if (r < 0) ret = -errno;
-		}
-		break;
-	case S_IFLNK:
-		r = symlinkat(link_target ?: ae->link_target, atfd, fn);
-		if (r < 0) ret = -errno;
-		atflags |= AT_SYMLINK_NOFOLLOW;
-		break;
-	case S_IFBLK:
-	case S_IFCHR:
-	case S_IFIFO:
-		r = mknodat(atfd, fn, ae->mode, ae->device);
-		if (r < 0) ret = -errno;
-		break;
-	}
-	if (ret) {
-		apk_err(out, "Failed to create %s: %s", ae->name, strerror(-ret));
-		return ret;
-	}
-
-	if (!(extract_flags & APK_EXTRACTF_NO_CHOWN)) {
-		r = fchownat(atfd, fn, ae->uid, ae->gid, atflags);
-		if (r < 0) {
-			apk_err(out, "Failed to set ownership on %s: %s",
-				fn, strerror(errno));
-			if (!ret) ret = -errno;
-		}
-
-		/* chown resets suid bit so we need set it again */
-		if (ae->mode & 07000) {
-			r = fchmodat(atfd, fn, ae->mode & 07777, atflags);
-			if (r < 0) {
-				apk_err(out, "Failed to set file permissions on %s: %s",
-					fn, strerror(errno));
-				if (!ret) ret = -errno;
-			}
-		}
-	}
-
-	/* extract xattrs */
-	if (!S_ISLNK(ae->mode) && ae->xattrs && ae->xattrs->num) {
-		r = 0;
-		fd = openat(atfd, fn, O_RDWR);
-		if (fd >= 0) {
-			foreach_array_item(xattr, ae->xattrs) {
-				if (fsetxattr(fd, xattr->name, xattr->value.ptr, xattr->value.len, 0) < 0) {
-					r = -errno;
-					if (r != -ENOTSUP) break;
-				}
-			}
-			close(fd);
-		} else {
-			r = -errno;
-		}
-		if (r) {
-			if (r != -ENOTSUP)
-				apk_err(out, "Failed to set xattrs on %s: %s",
-					fn, strerror(-r));
-			if (!ret) ret = r;
-		}
-	}
-
-	if (!S_ISLNK(ae->mode)) {
-		/* preserve modification time */
-		struct timespec times[2];
-
-		times[0].tv_sec  = times[1].tv_sec  = ae->mtime;
-		times[0].tv_nsec = times[1].tv_nsec = 0;
-		r = utimensat(atfd, fn, times, atflags);
-		if (r < 0) {
-			apk_err(out, "Failed to preserve modification time on %s: %s",
-				fn, strerror(errno));
-			if (!ret || ret == -ENOTSUP) ret = -errno;
-		}
-	}
-
-	return ret;
-}
-
-int apk_extract(struct apk_extract_ctx *ectx, struct apk_istream *is)
-{
-	void *sig;
-
-	if (IS_ERR(is)) return PTR_ERR(is);
-
-	sig = apk_istream_peek(is, 4);
-	if (IS_ERR(sig)) return apk_istream_close_error(is, PTR_ERR(sig));
-
-	if (memcmp(sig, "ADB", 3) == 0) return apk_extract_v3(ectx, is);
-	return apk_extract_v2(ectx, is);
-}
diff --git a/src/extract_v3.c b/src/extract_v3.c
index dcc5c24adf6f9c87497dbaee2be1c86502786ced..c25b2aee9ec98734c29be038fc26bd750cbfd424 100644
--- a/src/extract_v3.c
+++ b/src/extract_v3.c
@@ -22,15 +22,6 @@ struct apk_extract_v3_ctx {
 	struct apk_pathbuilder pb;
 };
 
-static const char *uvol_detect(struct apk_ctx *ac, const char *path)
-{
-	if (!apk_ctx_get_uvol(ac)) return 0;
-	if (strncmp(path, "uvol", 4) != 0) return 0;
-	if (path[4] == 0) return path;
-	if (path[4] == '/') return &path[5];
-	return 0;
-}
-
 static void apk_extract_v3_acl(struct apk_file_info *fi, struct adb_obj *o, struct apk_id_cache *idc)
 {
 	fi->mode = adb_ro_int(o, ADBI_ACL_MODE);
@@ -41,11 +32,9 @@ static void apk_extract_v3_acl(struct apk_file_info *fi, struct adb_obj *o, stru
 static int apk_extract_v3_file(struct apk_extract_ctx *ectx, off_t sz, struct apk_istream *is)
 {
 	struct apk_extract_v3_ctx *ctx = ectx->pctx;
-	struct apk_ctx *ac = ectx->ac;
 	const char *path_name = apk_pathbuilder_cstr(&ctx->pb);
 	struct apk_file_info fi = {
 		.name = path_name,
-		.uvol_name = uvol_detect(ac, path_name),
 		.size = adb_ro_int(&ctx->file, ADBI_FI_SIZE),
 		.mtime = adb_ro_int(&ctx->file, ADBI_FI_MTIME),
 	};
@@ -92,23 +81,18 @@ static int apk_extract_v3_file(struct apk_extract_ctx *ectx, off_t sz, struct ap
 	if (fi.digest.alg == APK_DIGEST_NONE) return -APKE_ADB_SCHEMA;
 
 	fi.mode |= S_IFREG;
-	r = ectx->ops->file(ectx, &fi, apk_istream_verify(&dis, is, &fi.digest));
-	if (r == APK_EXTRACT_SKIP_FILE)
-		return 0;
+	r = ectx->ops->file(ectx, &fi, apk_istream_verify(&dis, is, fi.size, &fi.digest));
 	return apk_istream_close_error(&dis.is, r);
 }
 
 static int apk_extract_v3_directory(struct apk_extract_ctx *ectx)
 {
 	struct apk_extract_v3_ctx *ctx = ectx->pctx;
-	struct apk_ctx *ac = ectx->ac;
 	struct apk_file_info fi = {
 		.name = apk_pathbuilder_cstr(&ctx->pb),
 	};
 	struct adb_obj acl;
 
-	if (uvol_detect(ac, fi.name)) return 0;
-
 	apk_extract_v3_acl(&fi, adb_ro_obj(&ctx->path, ADBI_DI_ACL, &acl), apk_ctx_get_id_cache(ectx->ac));
 	fi.mode |= S_IFDIR;
 
@@ -263,3 +247,16 @@ int apk_extract_v3(struct apk_extract_ctx *ectx, struct apk_istream *is)
 
 	return r;
 }
+
+int apk_extract(struct apk_extract_ctx *ectx, struct apk_istream *is)
+{
+	void *sig;
+
+	if (IS_ERR(is)) return PTR_ERR(is);
+
+	sig = apk_istream_peek(is, 4);
+	if (IS_ERR(sig)) return apk_istream_close_error(is, PTR_ERR(sig));
+
+	if (memcmp(sig, "ADB", 3) == 0) return apk_extract_v3(ectx, is);
+	return apk_extract_v2(ectx, is);
+}
diff --git a/src/fs_fsys.c b/src/fs_fsys.c
new file mode 100644
index 0000000000000000000000000000000000000000..7615b79187e5302e5d9d29e2d6b47ddc0dfe54aa
--- /dev/null
+++ b/src/fs_fsys.c
@@ -0,0 +1,316 @@
+/* fsops_sys.c - Alpine Package Keeper (APK)
+ *
+ * Copyright (C) 2005-2008 Natanael Copa <n@tanael.org>
+ * Copyright (C) 2008-2011 Timo Teräs <timo.teras@iki.fi>
+ * All rights reserved.
+ *
+ * SPDX-License-Identifier: GPL-2.0-only
+ */
+
+#include <unistd.h>
+#include <sys/stat.h>
+#include <sys/xattr.h>
+
+#include "apk_fs.h"
+
+#define TMPNAME_MAX (PATH_MAX + 64)
+
+static int fsys_dir_create(struct apk_fsdir *d, mode_t mode)
+{
+	if (mkdirat(apk_ctx_fd_dest(d->ac), apk_pathbuilder_cstr(&d->pb), mode) < 0)
+		return -errno;
+	return 0;
+}
+
+static int fsys_dir_delete(struct apk_fsdir *d)
+{
+	if (unlinkat(apk_ctx_fd_dest(d->ac), apk_pathbuilder_cstr(&d->pb), AT_REMOVEDIR) < 0)
+		return -errno;
+	return 0;
+}
+
+static int fsys_dir_check(struct apk_fsdir *d, mode_t mode, uid_t uid, gid_t gid)
+{
+	struct stat st;
+
+	if (fstatat(apk_ctx_fd_dest(d->ac), apk_pathbuilder_cstr(&d->pb), &st, AT_SYMLINK_NOFOLLOW) != 0)
+		return -errno;
+
+	if ((st.st_mode & 07777) != (mode & 07777) || st.st_uid != uid || st.st_gid != gid)
+		return APK_FS_DIR_MODIFIED;
+
+	return 0;
+}
+
+static int fsys_dir_update_perms(struct apk_fsdir *d, mode_t mode, uid_t uid, gid_t gid)
+{
+	struct stat st;
+	int fd = apk_ctx_fd_dest(d->ac), rc = 0;
+	const char *dirname = apk_pathbuilder_cstr(&d->pb);
+
+	if (fstatat(fd, dirname, &st, AT_SYMLINK_NOFOLLOW) != 0)
+		return -errno;
+
+	if ((st.st_mode & 07777) != (mode & 07777)) {
+		if (fchmodat(fd, dirname, mode, 0) < 0)
+			rc = -errno;
+	}
+	if (st.st_uid != uid || st.st_gid != gid) {
+		if (fchownat(fd, dirname, uid, gid, 0) < 0)
+			rc = -errno;
+	}
+	return rc;
+}
+
+static const char *format_tmpname(struct apk_digest_ctx *dctx, apk_blob_t pkgctx,
+	apk_blob_t dirname, apk_blob_t fullname, char tmpname[static TMPNAME_MAX])
+{
+	struct apk_digest d;
+	apk_blob_t b = APK_BLOB_PTR_LEN(tmpname, TMPNAME_MAX);
+
+	apk_digest_ctx_reset(dctx, APK_DIGEST_SHA256);
+	apk_digest_ctx_update(dctx, pkgctx.ptr, pkgctx.len);
+	apk_digest_ctx_update(dctx, fullname.ptr, fullname.len);
+	apk_digest_ctx_final(dctx, &d);
+
+	apk_blob_push_blob(&b, dirname);
+	if (dirname.len > 0) {
+		apk_blob_push_blob(&b, APK_BLOB_STR("/.apk."));
+	} else {
+		apk_blob_push_blob(&b, APK_BLOB_STR(".apk."));
+	}
+	apk_blob_push_hexdump(&b, APK_BLOB_PTR_LEN((char *)d.data, 24));
+	apk_blob_push_blob(&b, APK_BLOB_PTR_LEN("", 1));
+
+	return tmpname;
+}
+
+static apk_blob_t get_dirname(const char *fullname)
+{
+	char *slash = strrchr(fullname, '/');
+	if (!slash) return APK_BLOB_NULL;
+	return APK_BLOB_PTR_PTR((char*)fullname, slash);
+}
+
+static int fsys_file_extract(struct apk_ctx *ac, const struct apk_file_info *fi, struct apk_istream *is,
+	apk_progress_cb cb, void *cb_ctx, unsigned int extract_flags, apk_blob_t pkgctx)
+{
+	char tmpname_file[TMPNAME_MAX], tmpname_linktarget[TMPNAME_MAX];
+	struct apk_out *out = &ac->out;
+	struct apk_xattr *xattr;
+	int fd, r = -1, atflags = 0, ret = 0;
+	int atfd = apk_ctx_fd_dest(ac);
+	const char *fn = fi->name, *link_target = fi->link_target;
+
+	if (pkgctx.ptr) {
+		fn = format_tmpname(&ac->dctx, pkgctx, get_dirname(fn), APK_BLOB_STR(fn), tmpname_file);
+		if (link_target)
+			link_target = format_tmpname(&ac->dctx, pkgctx, get_dirname(link_target), APK_BLOB_STR(link_target), tmpname_linktarget);
+	}
+
+	if (!S_ISDIR(fi->mode) && !(extract_flags & APK_FSEXTRACTF_NO_OVERWRITE)) {
+		if (unlinkat(atfd, fn, 0) != 0 && errno != ENOENT) return -errno;
+	}
+
+	switch (fi->mode & S_IFMT) {
+	case S_IFDIR:
+		r = mkdirat(atfd, fn, fi->mode & 07777);
+		if (r < 0 && errno != EEXIST)
+			ret = -errno;
+		break;
+	case S_IFREG:
+		if (fi->link_target == NULL) {
+			int flags = O_RDWR | O_CREAT | O_TRUNC | O_CLOEXEC | O_EXCL;
+			int fd = openat(atfd, fn, flags, fi->mode & 07777);
+			if (fd < 0) {
+				ret = -errno;
+				break;
+			}
+			struct apk_ostream *os = apk_ostream_to_fd(fd);
+			if (IS_ERR(os)) {
+				ret = PTR_ERR(os);
+				break;
+			}
+			apk_stream_copy(is, os, fi->size, cb, cb_ctx, 0);
+			r = apk_ostream_close(os);
+			if (r < 0) {
+				unlinkat(atfd, fn, 0);
+				ret = r;
+			}
+		} else {
+			r = linkat(atfd, link_target, atfd, fn, 0);
+			if (r < 0) ret = -errno;
+		}
+		break;
+	case S_IFLNK:
+		r = symlinkat(link_target, atfd, fn);
+		if (r < 0) ret = -errno;
+		atflags |= AT_SYMLINK_NOFOLLOW;
+		break;
+	case S_IFBLK:
+	case S_IFCHR:
+	case S_IFIFO:
+		r = mknodat(atfd, fn, fi->mode, fi->device);
+		if (r < 0) ret = -errno;
+		break;
+	}
+	if (ret) {
+		apk_err(out, "Failed to create %s: %s", fi->name, strerror(-ret));
+		return ret;
+	}
+
+	if (!(extract_flags & APK_FSEXTRACTF_NO_CHOWN)) {
+		r = fchownat(atfd, fn, fi->uid, fi->gid, atflags);
+		if (r < 0) {
+			apk_err(out, "Failed to set ownership on %s: %s",
+				fn, strerror(errno));
+			if (!ret) ret = -errno;
+		}
+
+		/* chown resets suid bit so we need set it again */
+		if (fi->mode & 07000) {
+			r = fchmodat(atfd, fn, fi->mode & 07777, atflags);
+			if (r < 0) {
+				apk_err(out, "Failed to set file permissions on %s: %s",
+					fn, strerror(errno));
+				if (!ret) ret = -errno;
+			}
+		}
+	}
+
+	/* extract xattrs */
+	if (!S_ISLNK(fi->mode) && fi->xattrs && fi->xattrs->num) {
+		r = 0;
+		fd = openat(atfd, fn, O_RDWR);
+		if (fd >= 0) {
+			foreach_array_item(xattr, fi->xattrs) {
+				if (fsetxattr(fd, xattr->name, xattr->value.ptr, xattr->value.len, 0) < 0) {
+					r = -errno;
+					if (r != -ENOTSUP) break;
+				}
+			}
+			close(fd);
+		} else {
+			r = -errno;
+		}
+		if (r) {
+			if (r != -ENOTSUP)
+				apk_err(out, "Failed to set xattrs on %s: %s",
+					fn, strerror(-r));
+			if (!ret) ret = r;
+		}
+	}
+
+	if (!S_ISLNK(fi->mode)) {
+		/* preserve modification time */
+		struct timespec times[2];
+
+		times[0].tv_sec  = times[1].tv_sec  = fi->mtime;
+		times[0].tv_nsec = times[1].tv_nsec = 0;
+		r = utimensat(atfd, fn, times, atflags);
+		if (r < 0) {
+			apk_err(out, "Failed to preserve modification time on %s: %s",
+				fn, strerror(errno));
+			if (!ret || ret == -ENOTSUP) ret = -errno;
+		}
+	}
+
+	return ret;
+}
+
+static int fsys_file_control(struct apk_fsdir *d, apk_blob_t filename, int ctrl)
+{
+	struct apk_ctx *ac = d->ac;
+	char tmpname[TMPNAME_MAX], apknewname[TMPNAME_MAX];
+	const char *fn;
+	int rc = 0, atfd = apk_ctx_fd_dest(d->ac);
+	apk_blob_t dirname = apk_pathbuilder_get(&d->pb);
+
+	apk_pathbuilder_pushb(&d->pb, filename);
+	fn = apk_pathbuilder_cstr(&d->pb);
+
+	switch (ctrl) {
+	case APK_FS_CTRL_COMMIT:
+		// rename tmpname -> realname
+		if (renameat(atfd, format_tmpname(&ac->dctx, d->pkgctx, dirname, apk_pathbuilder_get(&d->pb), tmpname),
+			     atfd, fn) < 0)
+			rc = -errno;
+		break;
+	case APK_FS_CTRL_APKNEW:
+		// rename tmpname -> realname.apk-new
+		snprintf(apknewname, sizeof apknewname, "%s%s", fn, ".apk-new");
+		if (renameat(atfd, format_tmpname(&ac->dctx, d->pkgctx, dirname, apk_pathbuilder_get(&d->pb), tmpname),
+			     atfd, apknewname) < 0)
+			rc = -errno;
+		break;
+	case APK_FS_CTRL_CANCEL:
+		// unlink tmpname
+		if (unlinkat(atfd, format_tmpname(&ac->dctx, d->pkgctx, dirname, apk_pathbuilder_get(&d->pb), tmpname), 0) < 0)
+			rc = -errno;
+		break;
+	case APK_FS_CTRL_DELETE:
+		// unlink realname
+		if (unlinkat(atfd, fn, 0) < 0)
+			rc = -errno;
+		break;
+	default:
+		rc = -ENOSYS;
+		break;
+	}
+
+	apk_pathbuilder_pop(&d->pb);
+	return rc;
+}
+
+static int fsys_file_digest(struct apk_fsdir *d, apk_blob_t filename, uint8_t alg, struct apk_digest *dgst)
+{
+	struct apk_ctx *ac = d->ac;
+	struct apk_istream *is;
+	apk_blob_t blob;
+
+	apk_pathbuilder_pushb(&d->pb, filename);
+	is = apk_istream_from_file(apk_ctx_fd_dest(ac), apk_pathbuilder_cstr(&d->pb));
+	apk_pathbuilder_pop(&d->pb);
+	if (IS_ERR(is)) return PTR_ERR(is);
+
+	apk_digest_ctx_reset(&ac->dctx, alg);
+	while (apk_istream_get_all(is, &blob) == 0)
+		apk_digest_ctx_update(&ac->dctx, blob.ptr, blob.len);
+	apk_digest_ctx_final(&ac->dctx, dgst);
+	return apk_istream_close(is);
+}
+
+static const struct apk_fsdir_ops fsdir_ops_fsys = {
+	.dir_create = fsys_dir_create,
+	.dir_delete = fsys_dir_delete,
+	.dir_check = fsys_dir_check,
+	.dir_update_perms = fsys_dir_update_perms,
+	.file_extract = fsys_file_extract,
+	.file_control = fsys_file_control,
+	.file_digest = fsys_file_digest,
+};
+
+static const struct apk_fsdir_ops *apk_fsops_get(apk_blob_t dir)
+{
+	if (dir.len >= 4 && memcmp(dir.ptr, "uvol", 4) == 0 && (dir.len == 4 || dir.ptr[4] == '/')) {
+		extern const struct apk_fsdir_ops fsdir_ops_uvol;
+		return &fsdir_ops_uvol;
+	}
+
+	return &fsdir_ops_fsys;
+}
+
+int apk_fs_extract(struct apk_ctx *ac, const struct apk_file_info *fi, struct apk_istream *is,
+	apk_progress_cb cb, void *cb_ctx, unsigned int extract_flags, apk_blob_t pkgctx)
+{
+	const struct apk_fsdir_ops *ops = apk_fsops_get(APK_BLOB_PTR_LEN((char*)fi->name, strnlen(fi->name, 5)));
+	return ops->file_extract(ac, fi, is, cb, cb_ctx, extract_flags, pkgctx);
+}
+
+void apk_fsdir_get(struct apk_fsdir *d, apk_blob_t dir, struct apk_ctx *ac, apk_blob_t pkgctx)
+{
+	d->ac = ac;
+	d->pkgctx = pkgctx;
+	d->ops = apk_fsops_get(dir);
+	apk_pathbuilder_setb(&d->pb, dir);
+}
diff --git a/src/fs_uvol.c b/src/fs_uvol.c
new file mode 100644
index 0000000000000000000000000000000000000000..f2ba3f1619f9abeedad7b4fd08e2333f3f845020
--- /dev/null
+++ b/src/fs_uvol.c
@@ -0,0 +1,162 @@
+/* fsops_uvol.c - Alpine Package Keeper (APK)
+ *
+ * Copyright (C) 2005-2008 Natanael Copa <n@tanael.org>
+ * Copyright (C) 2008-2011 Timo Teräs <timo.teras@iki.fi>
+ * All rights reserved.
+ *
+ * SPDX-License-Identifier: GPL-2.0-only
+ */
+
+#include <spawn.h>
+#include <unistd.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+
+#include "apk_context.h"
+#include "apk_fs.h"
+
+static int uvol_run(struct apk_ctx *ac, char *action, const char *volname, char *arg1, char *arg2)
+{
+	struct apk_out *out = &ac->out;
+	pid_t pid;
+	int r, status;
+	char *argv[] = { (char*)apk_ctx_get_uvol(ac), action, (char*) volname, arg1, arg2, 0 };
+	posix_spawn_file_actions_t act;
+
+	posix_spawn_file_actions_init(&act);
+	posix_spawn_file_actions_addclose(&act, STDIN_FILENO);
+	r = posix_spawn(&pid, apk_ctx_get_uvol(ac), &act, 0, argv, environ);
+	posix_spawn_file_actions_destroy(&act);
+	if (r != 0) {
+		apk_err(out, "%s: uvol exec error: %s", volname, apk_error_str(r));
+		return r;
+	}
+	while (waitpid(pid, &status, 0) < 0 && errno == EINTR);
+	if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
+		apk_err(out, "%s: uvol exited with error %d", volname, WEXITSTATUS(status));
+		return -APKE_UVOL_ERROR;
+	}
+	return 0;
+}
+
+static int uvol_extract(struct apk_ctx *ac, const char *volname, char *arg1, off_t sz,
+	struct apk_istream *is, apk_progress_cb cb, void *cb_ctx)
+{
+	struct apk_out *out = &ac->out;
+	struct apk_ostream *os;
+	pid_t pid;
+	int r, status, pipefds[2];
+	char *argv[] = { (char*)apk_ctx_get_uvol(ac), "write", (char*) volname, arg1, 0 };
+	posix_spawn_file_actions_t act;
+
+	if (pipe2(pipefds, O_CLOEXEC) != 0) return -errno;
+
+	posix_spawn_file_actions_init(&act);
+	posix_spawn_file_actions_adddup2(&act, pipefds[0], STDIN_FILENO);
+	r = posix_spawn(&pid, apk_ctx_get_uvol(ac), &act, 0, argv, environ);
+	posix_spawn_file_actions_destroy(&act);
+	if (r != 0) {
+		apk_err(out, "%s: uvol exec error: %s", volname, apk_error_str(r));
+		return r;
+	}
+	close(pipefds[0]);
+	os = apk_ostream_to_fd(pipefds[1]);
+	apk_stream_copy(is, os, sz, cb, cb_ctx, 0);
+	r = apk_ostream_close(os);
+	if (r != 0) {
+		if (r >= 0) r = -APKE_UVOL_ERROR;
+		apk_err(out, "%s: uvol write error: %s", volname, apk_error_str(r));
+		return r;
+	}
+
+	while (waitpid(pid, &status, 0) < 0 && errno == EINTR);
+	if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
+		apk_err(out, "%s: uvol exited with error %d", volname, WEXITSTATUS(status));
+		return -APKE_UVOL_ERROR;
+	}
+
+	return 0;
+}
+
+static int uvol_dir_create(struct apk_fsdir *d, mode_t mode)
+{
+	return 0;
+}
+
+static int uvol_dir_delete(struct apk_fsdir *d)
+{
+	return 0;
+}
+
+static int uvol_dir_check(struct apk_fsdir *d, mode_t mode, uid_t uid, gid_t gid)
+{
+	return 0;
+}
+
+static int uvol_dir_update_perms(struct apk_fsdir *d, mode_t mode, uid_t uid, gid_t gid)
+{
+	return 0;
+}
+
+static int uvol_file_extract(struct apk_ctx *ac, const struct apk_file_info *fi, struct apk_istream *is,
+	apk_progress_cb cb, void *cb_ctx, unsigned int extract_flags, apk_blob_t pkgctx)
+{
+	char size[64];
+	const char *uvol_name;
+	int r;
+
+	if (IS_ERR(ac->uvol)) return PTR_ERR(ac->uvol);
+
+	uvol_name = strrchr(fi->name, '/');
+	uvol_name = uvol_name ? uvol_name + 1 : fi->name;
+
+	snprintf(size, sizeof size, "%ju", fi->size);
+	r = uvol_run(ac, "create", uvol_name, size, "ro");
+	if (r != 0) return r;
+
+	r = uvol_extract(ac, uvol_name, size, fi->size, is, cb, cb_ctx);
+	if (r == 0 && !pkgctx.ptr)
+		r = uvol_run(ac, "up", uvol_name, 0, 0);
+
+	if (r != 0) uvol_run(ac, "remove", uvol_name, 0, 0);
+
+	return r;
+}
+
+static int uvol_file_control(struct apk_fsdir *d, apk_blob_t filename, int ctrl)
+{
+	struct apk_ctx *ac = d->ac;
+	struct apk_pathbuilder pb;
+	const char *uvol_name;
+
+	if (IS_ERR(ac->uvol)) return PTR_ERR(ac->uvol);
+
+	apk_pathbuilder_setb(&pb, filename);
+	uvol_name = apk_pathbuilder_cstr(&pb);
+
+	switch (ctrl) {
+	case APK_FS_CTRL_COMMIT:
+		return uvol_run(ac, "up", uvol_name, 0, 0);
+	case APK_FS_CTRL_APKNEW:
+	case APK_FS_CTRL_CANCEL:
+	case APK_FS_CTRL_DELETE:
+		return uvol_run(ac, "remove", uvol_name, 0, 0);
+	default:
+		return -APKE_UVOL_ERROR;
+	}
+}
+
+static int uvol_file_digest(struct apk_fsdir *d, apk_blob_t filename, uint8_t alg, struct apk_digest *dgst)
+{
+	return -APKE_UVOL_ERROR;
+}
+
+const struct apk_fsdir_ops fsdir_ops_uvol = {
+	.dir_create = uvol_dir_create,
+	.dir_delete = uvol_dir_delete,
+	.dir_check = uvol_dir_check,
+	.dir_update_perms = uvol_dir_update_perms,
+	.file_extract = uvol_file_extract,
+	.file_control = uvol_file_control,
+	.file_digest = uvol_file_digest,
+};
diff --git a/src/io.c b/src/io.c
index a8821e47af8d2451131763fb552a15d32ee71c3b..cf30bb6eed00316c0a3fa57dd01b15d173b98ae2 100644
--- a/src/io.c
+++ b/src/io.c
@@ -314,7 +314,10 @@ static ssize_t digest_read(struct apk_istream *is, void *ptr, size_t size)
 	ssize_t r;
 
 	r = dis->pis->ops->read(dis->pis, ptr, size);
-	if (r > 0) apk_digest_ctx_update(&dis->dctx, ptr, r);
+	if (r > 0) {
+		apk_digest_ctx_update(&dis->dctx, ptr, r);
+		dis->size_left -= r;
+	}
 	return r;
 }
 
@@ -322,7 +325,7 @@ static int digest_close(struct apk_istream *is)
 {
 	struct apk_digest_istream *dis = container_of(is, struct apk_digest_istream, is);
 
-	if (dis->digest) {
+	if (dis->digest && dis->size_left == 0) {
 		struct apk_digest res;
 		apk_digest_ctx_final(&dis->dctx, &res);
 		if (apk_digest_cmp(&res, dis->digest) != 0)
@@ -340,7 +343,7 @@ static const struct apk_istream_ops digest_istream_ops = {
 	.close = digest_close,
 };
 
-struct apk_istream *apk_istream_verify(struct apk_digest_istream *dis, struct apk_istream *is, struct apk_digest *d)
+struct apk_istream *apk_istream_verify(struct apk_digest_istream *dis, struct apk_istream *is, off_t size, struct apk_digest *d)
 {
 	*dis = (struct apk_digest_istream) {
 		.is.ops = &digest_istream_ops,
@@ -350,10 +353,13 @@ struct apk_istream *apk_istream_verify(struct apk_digest_istream *dis, struct ap
 		.is.end = is->end,
 		.pis = is,
 		.digest = d,
+		.size_left = size,
 	};
 	apk_digest_ctx_init(&dis->dctx, d->alg);
-	if (dis->is.ptr != dis->is.end)
+	if (dis->is.ptr != dis->is.end) {
 		apk_digest_ctx_update(&dis->dctx, dis->is.ptr, dis->is.end - dis->is.ptr);
+		dis->size_left -= dis->is.end - dis->is.ptr;
+	}
 	return &dis->is;
 }
 
diff --git a/src/meson.build b/src/meson.build
index 3bdb477c855968375d087370a3ccd702d44684ba..9422ca6f6ed078af163ac419231264fa15d23187 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -14,9 +14,10 @@ libapk_src = [
 	'context.c',
 	'crypto_openssl.c',
 	'database.c',
-	'extract.c',
 	'extract_v2.c',
 	'extract_v3.c',
+	'fs_fsys.c',
+	'fs_uvol.c',
 	'hash.c',
 	'io.c',
 	'io_url.c',
@@ -38,6 +39,7 @@ libapk_headers = [
 	'apk_database.h',
 	'apk_defines.h',
 	'apk_extract.h',
+	'apk_fs.h',
 	'apk_hash.h',
 	'apk_io.h',
 	'apk_openssl.h',
diff --git a/src/package.c b/src/package.c
index cba999fe57fa5eb555fa595263c8f42f299144a7..f08101ff834540d42f85285617f90a5c324742df 100644
--- a/src/package.c
+++ b/src/package.c
@@ -591,7 +591,7 @@ void apk_pkg_from_adb(struct apk_database *db, struct apk_package *pkg, struct a
 	adb_ro_obj(pkgo, ADBI_PKG_PKGINFO, &pkginfo);
 
 	uid = adb_ro_blob(&pkginfo, ADBI_PI_UNIQUE_ID);
-	if (uid.len == APK_CHECKSUM_SHA1) {
+	if (uid.len >= APK_CHECKSUM_SHA1) {
 		pkg->csum.type = APK_CHECKSUM_SHA1;
 		memcpy(pkg->csum.data, uid.ptr, uid.len);
 	}
@@ -723,19 +723,16 @@ err:
 
 void apk_pkg_free(struct apk_package *pkg)
 {
-	if (pkg == NULL)
-		return;
+	if (!pkg) return;
 
 	apk_pkg_uninstall(NULL, pkg);
 	apk_dependency_array_free(&pkg->depends);
 	apk_dependency_array_free(&pkg->provides);
 	apk_dependency_array_free(&pkg->install_if);
-	if (pkg->url)
-		free(pkg->url);
-	if (pkg->description)
-		free(pkg->description);
-	if (pkg->commit)
-		free(pkg->commit);
+	if (pkg->url) free(pkg->url);
+	if (pkg->description) free(pkg->description);
+	if (pkg->commit) free(pkg->commit);
+	if (pkg->filename) free(pkg->filename);
 	free(pkg);
 }
 
diff --git a/src/print.c b/src/print.c
index ea66b90de701190e681db404bad8aba650a1ef81..31a9fbb0362177afaa7b99ef630f3068217c4744 100644
--- a/src/print.c
+++ b/src/print.c
@@ -58,7 +58,9 @@ const char *apk_error_str(int error)
 	case APKE_INDEX_STALE:			return "package mentioned in index not found (try 'apk update')";
 	case APKE_FILE_INTEGRITY:		return "file integrity error";
 	case APKE_CACHE_NOT_AVAILABLE:		return "cache not available";
-	case APKE_UVOL:				return "uvol error";
+	case APKE_UVOL_NOT_AVAILABLE:		return "uvol manager not available";
+	case APKE_UVOL_ERROR:			return "uvol error";
+	case APKE_UVOL_ROOT:			return "uvol not supported with --root";
 	default:
 		return strerror(error);
 	}