Snap for 8570526 from a1c0e6162f259e6cbb27432707ae9c4ca569cbda to mainline-scheduling-release

Change-Id: I7ccf8ed0c2b9fec196db4edf2bebcd1c0b6b7a14
diff --git a/.gitignore b/.gitignore
index 8bdd505..33e5d30 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,4 +27,7 @@
 libtool
 stamp-h
 stamp-h1
-
+/mkfs/mkfs.erofs
+/fuse/erofsfuse
+/dump/dump.erofs
+/fsck/fsck.erofs
diff --git a/AUTHORS b/AUTHORS
index 3487d44..812042a 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -1,9 +1,8 @@
 EROFS USERSPACE UTILITIES
-M: Li Guifu <bluce.liguifu@huawei.com>
-M: Miao Xie <miaoxie@huawei.com>
-M: Fang Wei <fangwei1@huawei.com>
-R: Gao Xiang <xiang@kernel.org>
-R: Chao Yu <yuchao0@huawei.com>
+M: Li Guifu <bluce.lee@aliyun.com>
+M: Gao Xiang <xiang@kernel.org>
+R: Chao Yu <chao@kernel.org>
+R: Miao Xie <miaoxie@huawei.com>
+R: Fang Wei <fangwei1@huawei.com>
 S: Maintained
 L: linux-erofs@lists.ozlabs.org
-
diff --git a/Android.bp b/Android.bp
index 4494013..a622074 100644
--- a/Android.bp
+++ b/Android.bp
@@ -52,7 +52,7 @@
 }
 
 cc_defaults {
-    name: "erofs-utils_defaults",
+    name: "erofs-utils_export_defaults",
 
     cflags: [
         "-Wall",
@@ -60,7 +60,7 @@
         "-Wno-ignored-qualifiers",
         "-Wno-pointer-arith",
         "-Wno-unused-parameter",
-        "-include erofs-utils-version.h",
+        "-Wno-unused-function",
         "-DHAVE_FALLOCATE",
         "-DHAVE_LINUX_TYPES_H",
         "-DHAVE_LIBSELINUX",
@@ -69,6 +69,15 @@
         "-DLZ4HC_ENABLED",
         "-DWITH_ANDROID",
     ],
+}
+
+cc_defaults {
+    name: "erofs-utils_defaults",
+    defaults: ["erofs-utils_export_defaults"],
+
+    cflags: [
+        "-include erofs-utils-version.h",
+    ],
     local_include_dirs: [
         "include",
     ],
@@ -76,18 +85,35 @@
         "external/e2fsprogs/lib/",
     ],
     generated_headers: ["erofs-utils-version.h"],
-    shared_libs: [
+    static_libs: [
+        "libbase",
         "libcutils",
         "libext2_uuid",
-        "libselinux",
-    ],
-    static_libs: [
+        "liblog",
         "liblz4",
+        "libselinux",
     ],
 }
 
-cc_binary_host {
-    name: "mkfs.erofs",
+cc_library {
+    host_supported: true,
+    recovery_available: true,
+    name: "liberofs",
+    defaults: ["erofs-utils_defaults"],
+    srcs: [
+        "lib/*.c",
+    ],
+    export_include_dirs: ["include"],
+
+    target: {
+        darwin: {
+            enabled: false,
+        },
+    },
+}
+
+cc_defaults {
+    name: "mkfs-erofs_defaults",
 
     defaults: ["erofs-utils_defaults"],
 
@@ -95,6 +121,9 @@
         "lib/*.c",
         "mkfs/*.c",
     ],
+    static_libs: [
+        "liberofs",
+    ],
 
     target: {
         darwin: {
@@ -103,14 +132,51 @@
     },
 }
 
-sh_binary_host {
-    name: "mkerofsimage.sh",
-    src: "mkerofsimage.sh",
-    required: [
-        "img2simg",
-        "mkfs.erofs",
-    ],
+cc_binary {
+    name: "mkfs.erofs",
 
+    defaults: ["mkfs-erofs_defaults"],
+    host_supported: true,
+    recovery_available: true,
+}
+
+cc_binary_host {
+    name: "make_erofs",
+
+    defaults: ["mkfs-erofs_defaults"],
+}
+
+cc_binary {
+    name: "dump.erofs",
+    defaults: ["erofs-utils_defaults"],
+    host_supported: true,
+    recovery_available: true,
+    srcs: [
+        "lib/*.c",
+        "dump/*.c",
+    ],
+    static_libs: [
+        "liberofs",
+    ],
+    target: {
+        darwin: {
+            enabled: false,
+        },
+    },
+}
+
+cc_binary {
+    name: "fsck.erofs",
+    defaults: ["erofs-utils_defaults"],
+    host_supported: true,
+    recovery_available: true,
+    srcs: [
+        "lib/*.c",
+        "fsck/*.c",
+    ],
+    static_libs: [
+        "liberofs",
+    ],
     target: {
         darwin: {
             enabled: false,
diff --git a/ChangeLog b/ChangeLog
index d8e89d9..6b53d24 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,34 @@
+erofs-utils 1.4
+
+ * This release includes the following updates:
+   - (experimental) introduce preliminary dump.erofs (Wang Qi, Guo Xuenan);
+   - (experimental) introduce preliminary fsck.erofs (Daeho Jeong);
+   - introduce MicroLZMA compression support (thanks to Lasse Collin);
+   - support chunk-based uncompressed files for deduplication;
+   - support multiple devices for multi-blob CAS container images;
+   - (mkfs.erofs, AOSP) add block list support (Yue Hu, David Anderson);
+   - (mkfs.erofs) support per-inode compress pcluster hints (Huang Jianan);
+   - (mkfs.erofs) add "noinline_data" extended option for DAX;
+   - (mkfs.erofs) introduce --quiet option (suggested by nl6720);
+   - complete MacOS build & functionality;
+   - various bugfixes and cleanups;
+
+ -- Gao Xiang <xiang@kernel.org>  Mon, 22 Nov 2021 00:00:00 +0800
+
+erofs-utils 1.3
+
+ * This release includes the following updates:
+   - support new big pcluster feature together with Linux 5.13+;
+   - optimize buffer allocation logic (Hu Weiwen);
+   - optimize build performance for large directories (Hu Weiwen);
+   - add support to override uid / gid (Hu Weiwen);
+   - add support to adjust lz4 history window size (Huang Jianan);
+   - add a manual for erofsfuse;
+   - add support to limit max decompressed extent size;
+   - various bugfixes and cleanups;
+
+ -- Gao Xiang <xiang@kernel.org>  Tue, 01 Jun 2021 00:00:00 +0800
+
 erofs-utils (1.2.1-1) unstable; urgency=medium
 
  * A quick maintenance release includes the following updates:
@@ -44,4 +75,3 @@
    - (mkfs.erofs) Posix ACL support;
 
  -- Gao Xiang <xiang@kernel.org>  Thu, 24 Oct 2019 00:00:00 +0800
-
diff --git a/METADATA b/METADATA
index a1390e0..1e3e1a0 100644
--- a/METADATA
+++ b/METADATA
@@ -5,11 +5,11 @@
     type: GIT
     value: "https://git.kernel.org/pub/scm/linux/kernel/git/xiang/erofs-utils.git"
   }
-  version: "v1.2.1"
+  version: "1.4"
   license_type: RESTRICTED
   last_upgrade_date {
-    year: 2020
-    month: 12
-    day: 7
+    year: 2021
+    month: 11
+    day: 22
   }
 }
diff --git a/Makefile.am b/Makefile.am
index b804aa9..fc464e8 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -1,10 +1,8 @@
 # SPDX-License-Identifier: GPL-2.0+
-# Makefile.am
 
 ACLOCAL_AMFLAGS = -I m4
 
-SUBDIRS = man lib mkfs
+SUBDIRS = man lib mkfs dump fsck
 if ENABLE_FUSE
 SUBDIRS += fuse
 endif
-
diff --git a/OWNERS b/OWNERS
new file mode 100644
index 0000000..e281cd2
--- /dev/null
+++ b/OWNERS
@@ -0,0 +1,3 @@
+dvander@google.com
+jaegeuk@google.com
+daehojeong@google.com
diff --git a/README b/README
index b57550b..aadd880 100644
--- a/README
+++ b/README
@@ -1,18 +1,20 @@
 erofs-utils
 ===========
 
-erofs-utils includes user-space tools for erofs filesystem.
-Currently mkfs.erofs and erofsfuse (experimental) are available.
+erofs-utils includes user-space tools for EROFS filesystem.
+Currently mkfs.erofs, (experimental) erofsfuse, dump.erofs, fsck.erofs
+are available.
 
 Dependencies & build
 --------------------
 
  lz4 1.8.0+ for lz4 enabled [2], lz4 1.9.3+ highly recommended [4][5].
+ XZ Utils 5.3.2alpha [6] or later versions for MicroLZMA enabled.
 
  libfuse 2.6+ for erofsfuse enabled as a plus.
 
-How to build for lz4-1.9.0 or above
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+How to build with lz4-1.9.0 or above
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 To build, you can run the following commands in order:
 
@@ -31,8 +33,8 @@
    LZ4_decompress_safe_partial() [5], which impacts erofsfuse
    functionality for legacy images (without 0PADDING).
 
-How to build for lz4-1.8.0~1.8.3
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+How to build with lz4-1.8.0~1.8.3
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 For these old lz4 versions, lz4hc algorithm cannot be supported
 without lz4-static installed due to LZ4_compress_HC_destSize()
@@ -47,40 +49,80 @@
 since there are serious bugs in these compressors, see [2] [3] [4]
 as well.
 
+How to build with liblzma
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+In order to enable LZMA support, build with the following commands:
+	$ ./configure --enable-lzma
+	$ make
+
+Additionally, you could specify liblzma build paths with:
+	--with-liblzma-incdir and --with-liblzma-libdir
+
 mkfs.erofs
 ----------
 
-two main kinds of erofs images can be generated: (un)compressed.
+two main kinds of EROFS images can be generated: (un)compressed.
 
  - For uncompressed images, there will be none of compression
    files in these images. However, it can decide whether the tail
    block of a file should be inlined or not properly [1].
 
- - For compressed images, it will try to use lz4(hc) algorithm
+ - For compressed images, it'll try to use specific algorithms
    first for each regular file and see if storage space can be
    saved with compression. If not, fallback to an uncompressed
    file.
 
-How to generate erofs images
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+How to generate EROFS images (lz4 for Linux 5.3+, lzma for Linux 5.16+)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-Currently lz4 and lz4hc are available for compression, e.g.
+Currently lz4(hc) and lzma are available for compression, e.g.
  $ mkfs.erofs -zlz4hc foo.erofs.img foo/
 
 Or leave all files uncompressed as an option:
  $ mkfs.erofs foo.erofs.img foo/
 
-How to generate legacy erofs images
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+In addition, you could specify a higher compression level to get a
+(slightly) better compression ratio than the default level, e.g.
+ $ mkfs.erofs -zlz4hc,12 foo.erofs.img foo/
+
+Note that all compressors are still single-threaded for now, thus it
+could take more time on the multiprocessor platform. Multi-threaded
+approach is already in our TODO list.
+
+How to generate EROFS big pcluster images (Linux 5.13+)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+In order to get much better compression ratios (thus better sequential
+read performance for common storage devices), big pluster feature has
+been introduced since linux-5.13, which is not forward-compatible with
+old kernels.
+
+In details, -C is used to specify the maximum size of each big pcluster
+in bytes, e.g.
+ $ mkfs.erofs -zlz4hc -C65536 foo.erofs.img foo/
+
+So in that case, pcluster size can be 64KiB at most.
+
+Note that large pcluster size can cause bad random performance, so
+please evaluate carefully in advance. Or make your own per-(sub)file
+compression strategies according to file access patterns if needed.
+
+How to generate legacy EROFS images (Linux 4.19+)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 Decompression inplace and compacted indexes have been introduced in
-linux-5.3, which are not forward-compatible with older kernels.
+Linux upstream v5.3, which are not forward-compatible with older
+kernels.
 
-In order to generate _legacy_ erofs images for old kernels,
+In order to generate _legacy_ EROFS images for old kernels,
 consider adding "-E legacy-compress" to the command line, e.g.
 
  $ mkfs.erofs -E legacy-compress -zlz4hc foo.erofs.img foo/
 
+For Linux kernel >= 5.3, legacy EROFS images are _NOT recommended_
+due to runtime performance loss compared with non-legacy images.
+
 Obsoleted erofs.mkfs
 ~~~~~~~~~~~~~~~~~~~~
 
@@ -91,10 +133,10 @@
 
 PLEASE NOTE: This version is highly _NOT recommended_ now.
 
-erofsfuse (experimental, unstable)
-----------------------------------
+erofsfuse (experimental)
+------------------------
 
-erofsfuse is introduced to support erofs format for various platforms
+erofsfuse is introduced to support EROFS format for various platforms
 (including older linux kernels) and new on-disk features iteration.
 It can also be used as an unpacking tool for unprivileged users.
 
@@ -112,15 +154,15 @@
 How to build erofsfuse
 ~~~~~~~~~~~~~~~~~~~~~~
 
-It's disabled by default as an experimental feature for now, to enable
-and build it manually:
+It's disabled by default as an experimental feature for now due to
+the extra libfuse dependency, to enable and build it manually:
 
 	$ ./configure --enable-fuse
 	$ make
 
 erofsfuse binary will be generated under fuse folder.
 
-How to mount an erofs image with erofsfuse
+How to mount an EROFS image with erofsfuse
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 As the other FUSE implementations, it's quite simple to mount with
@@ -136,26 +178,28 @@
 To unmount an erofsfuse mountpoint as a non-root user:
  $ fusermount -u foo/
 
+dump.erofs and fsck.erofs (experimental)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+dump.erofs and fsck.erofs are two new experimental tools to analyse
+and check EROFS file systems.
+
+They are still incomplete and actively under development by the
+community. But you could check them out if needed in advance.
+
+Report, feedback and/or contribution are welcomed.
+
 Contribution
 ------------
 
-erofs-utils is under GPLv2+ as a part of erofs project,
-feel free to send patches or feedback to us.
-
-To:
+erofs-utils is under GPLv2+ as a part of EROFS filesystem project,
+feel free to send patches or feedback to:
   linux-erofs mailing list   <linux-erofs@lists.ozlabs.org>
-  Li Guifu                   <bluce.liguifu@huawei.com>
-  Miao Xie                   <miaoxie@huawei.com>
-  Fang Wei                   <fangwei1@huawei.com>
-
-Cc:
-  Gao Xiang                  <xiang@kernel.org>
-  Chao Yu                    <yuchao0@huawei.com>
 
 Comments
 --------
 
-[1] According to the erofs on-disk format, the tail block of files
+[1] According to the EROFS on-disk format, the tail block of files
     could be inlined aggressively with its metadata in order to reduce
     the I/O overhead and save the storage space (called tail-packing).
 
@@ -206,3 +250,4 @@
 
     which is also resolved in lz4-1.9.3.
 
+[6] https://tukaani.org/xz/xz-5.3.2alpha.tar.xz
diff --git a/VERSION b/VERSION
index 203a5b8..babe602 100644
--- a/VERSION
+++ b/VERSION
@@ -1,2 +1,2 @@
-1.2.1
-2021-01-10
+1.4
+2021-11-22
diff --git a/autogen.sh b/autogen.sh
index fdda7e1..fd81db4 100755
--- a/autogen.sh
+++ b/autogen.sh
@@ -4,6 +4,6 @@
 aclocal && \
 autoheader && \
 autoconf && \
-libtoolize && \
+case `uname` in Darwin*) glibtoolize --copy ;; \
+  *) libtoolize --copy ;; esac && \
 automake -a -c
-
diff --git a/configure.ac b/configure.ac
index 28926c3..6f7e271 100644
--- a/configure.ac
+++ b/configure.ac
@@ -3,7 +3,7 @@
 
 AC_PREREQ([2.69])
 
-m4_define([erofs_utils_version], m4_esyscmd([sed -n '1p' VERSION | tr -d '\n']))
+m4_define([erofs_utils_version], m4_esyscmd_s([scripts/get-version-number]))
 m4_define([erofs_utils_date], m4_esyscmd([sed -n '2p' VERSION | tr -d '\n']))
 
 AC_INIT([erofs-utils], [erofs_utils_version], [linux-erofs@lists.ozlabs.org])
@@ -59,10 +59,20 @@
  fi
 ])
 
+AC_ARG_ENABLE([debug],
+    [AS_HELP_STRING([--enable-debug],
+                    [enable debugging mode @<:@default=no@:>@])],
+    [enable_debug="$enableval"],
+    [enable_debug="no"])
+
 AC_ARG_ENABLE(lz4,
    [AS_HELP_STRING([--disable-lz4], [disable LZ4 compression support @<:@default=enabled@:>@])],
    [enable_lz4="$enableval"], [enable_lz4="yes"])
 
+AC_ARG_ENABLE(lzma,
+   [AS_HELP_STRING([--enable-lzma], [enable LZMA compression support @<:@default=no@:>@])],
+   [enable_lzma="$enableval"], [enable_lzma="no"])
+
 AC_ARG_ENABLE(fuse,
    [AS_HELP_STRING([--enable-fuse], [enable erofsfuse @<:@default=no@:>@])],
    [enable_fuse="$enableval"], [enable_fuse="no"])
@@ -93,6 +103,14 @@
 AC_ARG_VAR([LZ4_CFLAGS], [C compiler flags for lz4])
 AC_ARG_VAR([LZ4_LIBS], [linker flags for lz4])
 
+AC_ARG_WITH(liblzma-incdir,
+   [AS_HELP_STRING([--with-liblzma-incdir=DIR], [liblzma include directory])], [
+   EROFS_UTILS_PARSE_DIRECTORY(["$withval"],[withval])])
+
+AC_ARG_WITH(liblzma-libdir,
+   [AS_HELP_STRING([--with-liblzma-libdir=DIR], [liblzma lib directory])], [
+   EROFS_UTILS_PARSE_DIRECTORY(["$withval"],[withval])])
+
 # Checks for header files.
 AC_CHECK_HEADERS(m4_flatten([
 	dirent.h
@@ -122,6 +140,8 @@
 AC_TYPE_SIZE_T
 AC_TYPE_SSIZE_T
 AC_CHECK_MEMBERS([struct stat.st_rdev])
+AC_CHECK_MEMBERS([struct stat.st_atim])
+AC_CHECK_MEMBERS([struct stat.st_atimensec])
 AC_TYPE_UINT64_T
 
 #
@@ -148,7 +168,29 @@
    #include <unistd.h>])
 
 # Checks for library functions.
-AC_CHECK_FUNCS([backtrace fallocate gettimeofday memset realpath strdup strerror strrchr strtoull])
+AC_CHECK_FUNCS(m4_flatten([
+	backtrace
+	copy_file_range
+	fallocate
+	gettimeofday
+	lgetxattr
+	llistxattr
+	memset
+	realpath
+	pread64
+	pwrite64
+	strdup
+	strerror
+	strrchr
+	strtoull
+	tmpfile64
+	utimensat]))
+
+# Configure debug mode
+AS_IF([test "x$enable_debug" != "xno"], [], [
+  dnl Turn off all assert checking.
+  CPPFLAGS="$CPPFLAGS -DNDEBUG"
+])
 
 # Configure libuuid
 AS_IF([test "x$with_uuid" != "xno"], [
@@ -234,10 +276,36 @@
   CPPFLAGS=${saved_CPPFLAGS}
 fi
 
+if test "x$enable_lzma" = "xyes"; then
+  saved_CPPFLAGS=${CPPFLAGS}
+  test -z "${with_liblzma_incdir}" ||
+    CPPFLAGS="-I$with_liblzma_incdir $CPPFLAGS"
+  AC_CHECK_HEADERS([lzma.h],[have_lzmah="yes"], [])
+
+  if test "x${have_lzmah}" = "xyes" ; then
+    saved_LIBS="$LIBS"
+    saved_LDFLAGS="$LDFLAGS"
+
+    test -z "${with_liblzma_libdir}" ||
+      LDFLAGS="-L$with_liblzma_libdir ${LDFLAGS}"
+    AC_CHECK_LIB(lzma, lzma_microlzma_encoder, [],
+      [AC_MSG_ERROR([Cannot find proper liblzma])])
+
+    AC_CHECK_DECL(lzma_microlzma_encoder, [have_liblzma="yes"],
+      [AC_MSG_ERROR([Cannot find proper liblzma])], [[
+#include <lzma.h>
+    ]])
+    LDFLAGS="${saved_LDFLAGS}"
+    LIBS="${saved_LIBS}"
+  fi
+  CPPFLAGS="${saved_CPPFLAGS}"
+fi
+
 # Set up needed symbols, conditionals and compiler/linker flags
 AM_CONDITIONAL([ENABLE_LZ4], [test "x${have_lz4}" = "xyes"])
 AM_CONDITIONAL([ENABLE_LZ4HC], [test "x${have_lz4hc}" = "xyes"])
 AM_CONDITIONAL([ENABLE_FUSE], [test "x${have_fuse}" = "xyes"])
+AM_CONDITIONAL([ENABLE_LIBLZMA], [test "x${have_liblzma}" = "xyes"])
 
 if test "x$have_uuid" = "xyes"; then
   AC_DEFINE([HAVE_LIBUUID], 1, [Define to 1 if libuuid is found])
@@ -258,16 +326,28 @@
     LZ4_LIBS="-Wl,-Bstatic -Wl,-whole-archive -Xlinker ${LZ4_LIBS} -Wl,-no-whole-archive -Wl,-Bdynamic"
     test -z "${with_lz4_libdir}" || LZ4_LIBS="-L${with_lz4_libdir} $LZ4_LIBS"
   else
-    test -z "${with_lz4_libdir}" || LZ4_LIBS="-R${with_lz4_libdir} $LZ4_LIBS"
+    test -z "${with_lz4_libdir}" || LZ4_LIBS="-L${with_lz4_libdir} -R${with_lz4_libdir} $LZ4_LIBS"
   fi
   liblz4_LIBS="${LZ4_LIBS}"
 fi
 AC_SUBST([liblz4_LIBS])
 
+if test "x${have_liblzma}" = "xyes"; then
+  AC_DEFINE([HAVE_LIBLZMA], [1], [Define to 1 if liblzma is enabled.])
+  liblzma_LIBS="-llzma"
+  test -z "${with_liblzma_libdir}" ||
+    liblzma_LIBS="-L${with_liblzma_libdir} $liblzma_LIBS"
+  test -z "${with_liblzma_incdir}" ||
+    liblzma_CFLAGS="-I${with_liblzma_incdir}"
+  AC_SUBST([liblzma_LIBS])
+  AC_SUBST([liblzma_CFLAGS])
+fi
+
 AC_CONFIG_FILES([Makefile
 		 man/Makefile
 		 lib/Makefile
 		 mkfs/Makefile
-		 fuse/Makefile])
+		 dump/Makefile
+		 fuse/Makefile
+		 fsck/Makefile])
 AC_OUTPUT
-
diff --git a/dump/Makefile.am b/dump/Makefile.am
new file mode 100644
index 0000000..9f0cd3f
--- /dev/null
+++ b/dump/Makefile.am
@@ -0,0 +1,10 @@
+# SPDX-License-Identifier: GPL-2.0+
+# Makefile.am
+
+AUTOMAKE_OPTIONS = foreign
+bin_PROGRAMS     = dump.erofs
+AM_CPPFLAGS = ${libuuid_CFLAGS}
+dump_erofs_SOURCES = main.c
+dump_erofs_CFLAGS = -Wall -Werror -I$(top_srcdir)/include
+dump_erofs_LDADD = $(top_builddir)/lib/liberofs.la ${libselinux_LIBS} \
+	${libuuid_LIBS} ${liblz4_LIBS} ${liblzma_LIBS}
diff --git a/dump/main.c b/dump/main.c
new file mode 100644
index 0000000..72761bd
--- /dev/null
+++ b/dump/main.c
@@ -0,0 +1,734 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright (C) 2021-2022 HUAWEI, Inc.
+ *             http://www.huawei.com/
+ * Created by Wang Qi <mpiglet@outlook.com>
+ *            Guo Xuenan <guoxuenan@huawei.com>
+ */
+#include <stdlib.h>
+#include <getopt.h>
+#include <time.h>
+#include "erofs/print.h"
+#include "erofs/inode.h"
+#include "erofs/io.h"
+
+#ifdef HAVE_LIBUUID
+#include <uuid.h>
+#endif
+
+struct erofsdump_cfg {
+	unsigned int totalshow;
+	bool show_inode;
+	bool show_extent;
+	bool show_superblock;
+	bool show_statistics;
+	erofs_nid_t nid;
+	const char *inode_path;
+};
+static struct erofsdump_cfg dumpcfg;
+
+static const char chart_format[] = "%-16s	%-11d %8.2f%% |%-50s|\n";
+static const char header_format[] = "%-16s %11s %16s |%-50s|\n";
+static char *file_types[] = {
+	".txt", ".so", ".xml", ".apk",
+	".odex", ".vdex", ".oat", ".rc",
+	".otf", ".txt", "others",
+};
+#define OTHERFILETYPE	ARRAY_SIZE(file_types)
+/* (1 << FILE_MAX_SIZE_BITS)KB */
+#define	FILE_MAX_SIZE_BITS	16
+
+static const char * const file_category_types[] = {
+	[EROFS_FT_UNKNOWN] = "unknown type",
+	[EROFS_FT_REG_FILE] = "regular file",
+	[EROFS_FT_DIR] = "directory",
+	[EROFS_FT_CHRDEV] = "char dev",
+	[EROFS_FT_BLKDEV] = "block dev",
+	[EROFS_FT_FIFO] = "FIFO file",
+	[EROFS_FT_SOCK] = "SOCK file",
+	[EROFS_FT_SYMLINK] = "symlink file",
+};
+
+struct erofs_statistics {
+	unsigned long files;
+	unsigned long compressed_files;
+	unsigned long uncompressed_files;
+	unsigned long files_total_size;
+	unsigned long files_total_origin_size;
+	double compress_rate;
+
+	/* [statistics] # of files based on inode_info->flags */
+	unsigned long file_category_stat[EROFS_FT_MAX];
+	/* [statistics] # of files based on file name extensions */
+	unsigned int file_type_stat[OTHERFILETYPE];
+	/* [statistics] # of files based on the original size of files */
+	unsigned int file_original_size[FILE_MAX_SIZE_BITS + 1];
+	/* [statistics] # of files based on the compressed size of files */
+	unsigned int file_comp_size[FILE_MAX_SIZE_BITS + 1];
+};
+static struct erofs_statistics stats;
+
+static struct option long_options[] = {
+	{"help", no_argument, NULL, 1},
+	{"nid", required_argument, NULL, 2},
+	{"device", required_argument, NULL, 3},
+	{"path", required_argument, NULL, 4},
+	{0, 0, 0, 0},
+};
+
+struct erofsdump_feature {
+	bool compat;
+	u32 flag;
+	const char *name;
+};
+
+static struct erofsdump_feature feature_lists[] = {
+	{ true, EROFS_FEATURE_COMPAT_SB_CHKSUM, "sb_csum" },
+	{ true, EROFS_FEATURE_COMPAT_MTIME, "mtime" },
+	{ false, EROFS_FEATURE_INCOMPAT_LZ4_0PADDING, "0padding" },
+	{ false, EROFS_FEATURE_INCOMPAT_BIG_PCLUSTER, "big_pcluster" },
+	{ false, EROFS_FEATURE_INCOMPAT_CHUNKED_FILE, "chunked_file" },
+	{ false, EROFS_FEATURE_INCOMPAT_DEVICE_TABLE, "device_table" },
+};
+
+static int erofs_read_dir(erofs_nid_t nid, erofs_nid_t parent_nid);
+static inline int erofs_checkdirent(struct erofs_dirent *de,
+		struct erofs_dirent *last_de,
+		u32 maxsize, const char *dname);
+
+static void usage(void)
+{
+	fputs("usage: [options] IMAGE\n\n"
+	      "Dump erofs layout from IMAGE, and [options] are:\n"
+	      " -S              show statistic information of the image\n"
+	      " -V              print the version number of dump.erofs and exit.\n"
+	      " -e              show extent info (--nid is required)\n"
+	      " -s              show information about superblock\n"
+	      " --device=X      specify an extra device to be used together\n"
+	      " --nid=#         show the target inode info of nid #\n"
+	      " --path=X        show the target inode info of path X\n"
+	      " --help          display this help and exit.\n",
+	      stderr);
+}
+
+static void erofsdump_print_version(void)
+{
+	fprintf(stderr, "dump.erofs %s\n", cfg.c_version);
+}
+
+static int erofsdump_parse_options_cfg(int argc, char **argv)
+{
+	int opt, err;
+
+	while ((opt = getopt_long(argc, argv, "SVes",
+				  long_options, NULL)) != -1) {
+		switch (opt) {
+		case 'e':
+			dumpcfg.show_extent = true;
+			++dumpcfg.totalshow;
+			break;
+		case 's':
+			dumpcfg.show_superblock = true;
+			++dumpcfg.totalshow;
+			break;
+		case 'S':
+			dumpcfg.show_statistics = true;
+			++dumpcfg.totalshow;
+			break;
+		case 'V':
+			erofsdump_print_version();
+			exit(0);
+		case 2:
+			dumpcfg.show_inode = true;
+			dumpcfg.nid = (erofs_nid_t)atoll(optarg);
+			++dumpcfg.totalshow;
+			break;
+		case 1:
+			usage();
+			exit(0);
+		case 3:
+			err = blob_open_ro(optarg);
+			if (err)
+				return err;
+			++sbi.extra_devices;
+			break;
+		case 4:
+			dumpcfg.inode_path = optarg;
+			dumpcfg.show_inode = true;
+			++dumpcfg.totalshow;
+			break;
+		default:
+			return -EINVAL;
+		}
+	}
+
+	if (optind >= argc) {
+		erofs_err("missing argument: IMAGE");
+		return -EINVAL;
+	}
+
+	cfg.c_img_path = strdup(argv[optind++]);
+	if (!cfg.c_img_path)
+		return -ENOMEM;
+
+	if (optind < argc) {
+		erofs_err("unexpected argument: %s\n", argv[optind]);
+		return -EINVAL;
+	}
+	return 0;
+}
+
+static int erofs_get_occupied_size(struct erofs_inode *inode,
+		erofs_off_t *size)
+{
+	*size = 0;
+	switch (inode->datalayout) {
+	case EROFS_INODE_FLAT_INLINE:
+	case EROFS_INODE_FLAT_PLAIN:
+	case EROFS_INODE_CHUNK_BASED:
+		stats.uncompressed_files++;
+		*size = inode->i_size;
+		break;
+	case EROFS_INODE_FLAT_COMPRESSION_LEGACY:
+	case EROFS_INODE_FLAT_COMPRESSION:
+		stats.compressed_files++;
+		*size = inode->u.i_blocks * EROFS_BLKSIZ;
+		break;
+	default:
+		erofs_err("unknown datalayout");
+		return -1;
+	}
+	return 0;
+}
+
+static int erofs_getfile_extension(const char *filename)
+{
+	char *postfix = strrchr(filename, '.');
+	int type = 0;
+
+	if (!postfix)
+		return OTHERFILETYPE - 1;
+	for (type = 0; type < OTHERFILETYPE - 1; ++type) {
+		if (strcmp(postfix, file_types[type]) == 0)
+			break;
+	}
+	return type;
+}
+
+static void update_file_size_statatics(erofs_off_t occupied_size,
+		erofs_off_t original_size)
+{
+	int occupied_size_mark, original_size_mark;
+
+	original_size_mark = 0;
+	occupied_size_mark = 0;
+	occupied_size >>= 10;
+	original_size >>= 10;
+
+	while (occupied_size || original_size) {
+		if (occupied_size) {
+			occupied_size >>= 1;
+			occupied_size_mark++;
+		}
+		if (original_size) {
+			original_size >>= 1;
+			original_size_mark++;
+		}
+	}
+
+	if (original_size_mark >= FILE_MAX_SIZE_BITS)
+		stats.file_original_size[FILE_MAX_SIZE_BITS]++;
+	else
+		stats.file_original_size[original_size_mark]++;
+
+	if (occupied_size_mark >= FILE_MAX_SIZE_BITS)
+		stats.file_comp_size[FILE_MAX_SIZE_BITS]++;
+	else
+		stats.file_comp_size[occupied_size_mark]++;
+}
+
+static inline int erofs_checkdirent(struct erofs_dirent *de,
+		struct erofs_dirent *last_de,
+		u32 maxsize, const char *dname)
+{
+	int dname_len;
+	unsigned int nameoff = le16_to_cpu(de->nameoff);
+
+	if (nameoff < sizeof(struct erofs_dirent) ||
+			nameoff >= PAGE_SIZE) {
+		erofs_err("invalid de[0].nameoff %u @ nid %llu",
+				nameoff, de->nid | 0ULL);
+		return -EFSCORRUPTED;
+	}
+
+	dname_len = (de + 1 >= last_de) ? strnlen(dname, maxsize - nameoff) :
+				le16_to_cpu(de[1].nameoff) - nameoff;
+	/* a corrupted entry is found */
+	if (nameoff + dname_len > maxsize ||
+			dname_len > EROFS_NAME_LEN) {
+		erofs_err("bogus dirent @ nid %llu",
+				le64_to_cpu(de->nid) | 0ULL);
+		DBG_BUGON(1);
+		return -EFSCORRUPTED;
+	}
+	if (de->file_type >= EROFS_FT_MAX) {
+		erofs_err("invalid file type %llu", de->nid);
+		return -EFSCORRUPTED;
+	}
+	return dname_len;
+}
+
+static int erofs_read_dirent(struct erofs_dirent *de,
+		erofs_nid_t nid, erofs_nid_t parent_nid,
+		const char *dname)
+{
+	int err;
+	erofs_off_t occupied_size = 0;
+	struct erofs_inode inode = { .nid = de->nid };
+
+	stats.files++;
+	stats.file_category_stat[de->file_type]++;
+	err = erofs_read_inode_from_disk(&inode);
+	if (err) {
+		erofs_err("read file inode from disk failed!");
+		return err;
+	}
+
+	err = erofs_get_occupied_size(&inode, &occupied_size);
+	if (err) {
+		erofs_err("get file size failed\n");
+		return err;
+	}
+
+	if (de->file_type == EROFS_FT_REG_FILE) {
+		stats.files_total_origin_size += inode.i_size;
+		stats.file_type_stat[erofs_getfile_extension(dname)]++;
+		stats.files_total_size += occupied_size;
+		update_file_size_statatics(occupied_size, inode.i_size);
+	}
+
+	if ((de->file_type == EROFS_FT_DIR)
+			&& de->nid != nid && de->nid != parent_nid) {
+		err = erofs_read_dir(de->nid, nid);
+		if (err) {
+			erofs_err("parse dir nid %llu error occurred\n",
+					de->nid);
+			return err;
+		}
+	}
+	return 0;
+}
+
+static int erofs_read_dir(erofs_nid_t nid, erofs_nid_t parent_nid)
+{
+	int err;
+	erofs_off_t offset;
+	char buf[EROFS_BLKSIZ];
+	struct erofs_inode vi = { .nid = nid };
+
+	err = erofs_read_inode_from_disk(&vi);
+	if (err)
+		return err;
+
+	offset = 0;
+	while (offset < vi.i_size) {
+		erofs_off_t maxsize = min_t(erofs_off_t,
+						vi.i_size - offset, EROFS_BLKSIZ);
+		struct erofs_dirent *de = (void *)buf;
+		struct erofs_dirent *end;
+		unsigned int nameoff;
+
+		err = erofs_pread(&vi, buf, maxsize, offset);
+		if (err)
+			return err;
+
+		nameoff = le16_to_cpu(de->nameoff);
+		end = (void *)buf + nameoff;
+		while (de < end) {
+			const char *dname;
+			int ret;
+
+			/* skip "." and ".." dentry */
+			if (de->nid == nid || de->nid == parent_nid) {
+				de++;
+				continue;
+			}
+
+			dname = (char *)buf + nameoff;
+			ret = erofs_checkdirent(de, end, maxsize, dname);
+			if (ret < 0)
+				return ret;
+			ret = erofs_read_dirent(de, nid, parent_nid, dname);
+			if (ret < 0)
+				return ret;
+			++de;
+		}
+		offset += maxsize;
+	}
+	return 0;
+}
+
+static int erofs_get_pathname(erofs_nid_t nid, erofs_nid_t parent_nid,
+		erofs_nid_t target, char *path, unsigned int pos)
+{
+	int err;
+	erofs_off_t offset;
+	char buf[EROFS_BLKSIZ];
+	struct erofs_inode inode = { .nid = nid };
+
+	path[pos++] = '/';
+	if (target == sbi.root_nid)
+		return 0;
+
+	err = erofs_read_inode_from_disk(&inode);
+	if (err) {
+		erofs_err("read inode failed @ nid %llu", nid | 0ULL);
+		return err;
+	}
+
+	offset = 0;
+	while (offset < inode.i_size) {
+		erofs_off_t maxsize = min_t(erofs_off_t,
+					inode.i_size - offset, EROFS_BLKSIZ);
+		struct erofs_dirent *de = (void *)buf;
+		struct erofs_dirent *end;
+		unsigned int nameoff;
+
+		err = erofs_pread(&inode, buf, maxsize, offset);
+		if (err)
+			return err;
+
+		nameoff = le16_to_cpu(de->nameoff);
+		end = (void *)buf + nameoff;
+		while (de < end) {
+			const char *dname;
+			int len;
+
+			nameoff = le16_to_cpu(de->nameoff);
+			dname = (char *)buf + nameoff;
+			len = erofs_checkdirent(de, end, maxsize, dname);
+			if (len < 0)
+				return len;
+
+			if (de->nid == target) {
+				memcpy(path + pos, dname, len);
+				path[pos + len] = '\0';
+				return 0;
+			}
+
+			if (de->file_type == EROFS_FT_DIR &&
+					de->nid != parent_nid &&
+					de->nid != nid) {
+				memcpy(path + pos, dname, len);
+				err = erofs_get_pathname(de->nid, nid,
+						target, path, pos + len);
+				if (!err)
+					return 0;
+				memset(path + pos, 0, len);
+			}
+			++de;
+		}
+		offset += maxsize;
+	}
+	return -1;
+}
+
+static int erofsdump_map_blocks(struct erofs_inode *inode,
+		struct erofs_map_blocks *map, int flags)
+{
+	if (erofs_inode_is_data_compressed(inode->datalayout))
+		return z_erofs_map_blocks_iter(inode, map, flags);
+	return erofs_map_blocks(inode, map, flags);
+}
+
+static void erofsdump_show_fileinfo(bool show_extent)
+{
+	const char *ext_fmt[] = {
+		"%4d: %8" PRIu64 "..%8" PRIu64 " | %7" PRIu64 " : %10" PRIu64 "..%10" PRIu64 " | %7" PRIu64 "\n",
+		"%4d: %8" PRIu64 "..%8" PRIu64 " | %7" PRIu64 " : %10" PRIu64 "..%10" PRIu64 " | %7" PRIu64 "  # device %u\n"
+	};
+	int err, i;
+	erofs_off_t size;
+	u16 access_mode;
+	struct erofs_inode inode = { .nid = dumpcfg.nid };
+	char path[PATH_MAX + 1] = {0};
+	char access_mode_str[] = "rwxrwxrwx";
+	char timebuf[128] = {0};
+	unsigned int extent_count = 0;
+	struct erofs_map_blocks map = {
+		.index = UINT_MAX,
+		.m_la = 0,
+	};
+
+	if (dumpcfg.inode_path) {
+		err = erofs_ilookup(dumpcfg.inode_path, &inode);
+		if (err) {
+			erofs_err("read inode failed @ %s", dumpcfg.inode_path);
+			return;
+		}
+	} else {
+		err = erofs_read_inode_from_disk(&inode);
+		if (err) {
+			erofs_err("read inode failed @ nid %llu",
+				  inode.nid | 0ULL);
+			return;
+		}
+	}
+
+	err = erofs_get_occupied_size(&inode, &size);
+	if (err) {
+		erofs_err("get file size failed @ nid %llu", inode.nid | 0ULL);
+		return;
+	}
+
+	err = erofs_get_pathname(sbi.root_nid, sbi.root_nid,
+				 inode.nid, path, 0);
+	if (err < 0) {
+		erofs_err("file path not found @ nid %llu", inode.nid | 0ULL);
+		return;
+	}
+
+	strftime(timebuf, sizeof(timebuf),
+		 "%Y-%m-%d %H:%M:%S", localtime((time_t *)&inode.i_mtime));
+	access_mode = inode.i_mode & 0777;
+	for (i = 8; i >= 0; i--)
+		if (((access_mode >> i) & 1) == 0)
+			access_mode_str[8 - i] = '-';
+	fprintf(stdout, "File : %s\n", path);
+	fprintf(stdout, "Size: %" PRIu64"  On-disk size: %" PRIu64 "  %s\n",
+		inode.i_size, size,
+		file_category_types[erofs_mode_to_ftype(inode.i_mode)]);
+	fprintf(stdout, "NID: %" PRIu64 "   ", inode.nid);
+	fprintf(stdout, "Links: %u   ", inode.i_nlink);
+	fprintf(stdout, "Layout: %d   Compression ratio: %.2f%%\n",
+		inode.datalayout,
+		(double)(100 * size) / (double)(inode.i_size));
+	fprintf(stdout, "Inode size: %d   ", inode.inode_isize);
+	fprintf(stdout, "Extent size: %u   ", inode.extent_isize);
+	fprintf(stdout,	"Xattr size: %u\n", inode.xattr_isize);
+	fprintf(stdout, "Uid: %u   Gid: %u  ", inode.i_uid, inode.i_gid);
+	fprintf(stdout, "Access: %04o/%s\n", access_mode, access_mode_str);
+	fprintf(stdout, "Timestamp: %s.%09d\n", timebuf, inode.i_mtime_nsec);
+
+	if (!dumpcfg.show_extent)
+		return;
+
+	fprintf(stdout, "\n Ext:   logical offset   |  length :     physical offset    |  length \n");
+	while (map.m_la < inode.i_size) {
+		struct erofs_map_dev mdev;
+
+		err = erofsdump_map_blocks(&inode, &map,
+				EROFS_GET_BLOCKS_FIEMAP);
+		if (err) {
+			erofs_err("failed to get file blocks range");
+			return;
+		}
+
+		mdev = (struct erofs_map_dev) {
+			.m_deviceid = map.m_deviceid,
+			.m_pa = map.m_pa,
+		};
+		err = erofs_map_dev(&sbi, &mdev);
+		if (err) {
+			erofs_err("failed to map device");
+			return;
+		}
+
+		fprintf(stdout, ext_fmt[!!mdev.m_deviceid], extent_count++,
+			map.m_la, map.m_la + map.m_llen, map.m_llen,
+			mdev.m_pa, mdev.m_pa + map.m_plen, map.m_plen,
+			mdev.m_deviceid);
+		map.m_la += map.m_llen;
+	}
+	fprintf(stdout, "%s: %d extents found\n", path, extent_count);
+}
+
+static void erofsdump_filesize_distribution(const char *title,
+		unsigned int *file_counts, unsigned int len)
+{
+	char col1[30];
+	unsigned int col2, i, lowerbound, upperbound;
+	double col3;
+	char col4[400];
+
+	lowerbound = 0;
+	upperbound = 1;
+	fprintf(stdout, "\n%s file size distribution:\n", title);
+	fprintf(stdout, header_format, ">=(KB) .. <(KB) ", "count",
+			"ratio", "distribution");
+	for (i = 0; i < len; i++) {
+		memset(col1, 0, sizeof(col1));
+		memset(col4, 0, sizeof(col4));
+		if (i == len - 1)
+			sprintf(col1, "%6d ..", lowerbound);
+		else if (i <= 6)
+			sprintf(col1, "%6d .. %-6d", lowerbound, upperbound);
+		else
+
+			sprintf(col1, "%6d .. %-6d", lowerbound, upperbound);
+		col2 = file_counts[i];
+		if (stats.file_category_stat[EROFS_FT_REG_FILE])
+			col3 = (double)(100 * col2) /
+				stats.file_category_stat[EROFS_FT_REG_FILE];
+		else
+			col3 = 0.0;
+		memset(col4, '#', col3 / 2);
+		fprintf(stdout, chart_format, col1, col2, col3, col4);
+		lowerbound = upperbound;
+		upperbound <<= 1;
+	}
+}
+
+static void erofsdump_filetype_distribution(char **file_types, unsigned int len)
+{
+	char col1[30];
+	unsigned int col2, i;
+	double col3;
+	char col4[401];
+
+	fprintf(stdout, "\nFile type distribution:\n");
+	fprintf(stdout, header_format, "type", "count", "ratio",
+			"distribution");
+	for (i = 0; i < len; i++) {
+		memset(col1, 0, sizeof(col1));
+		memset(col4, 0, sizeof(col4));
+		sprintf(col1, "%-17s", file_types[i]);
+		col2 = stats.file_type_stat[i];
+		if (stats.file_category_stat[EROFS_FT_REG_FILE])
+			col3 = (double)(100 * col2) /
+				stats.file_category_stat[EROFS_FT_REG_FILE];
+		else
+			col3 = 0.0;
+		memset(col4, '#', col3 / 2);
+		fprintf(stdout, chart_format, col1, col2, col3, col4);
+	}
+}
+
+static void erofsdump_file_statistic(void)
+{
+	unsigned int i;
+
+	fprintf(stdout, "Filesystem total file count:		%lu\n",
+			stats.files);
+	for (i = 0; i < EROFS_FT_MAX; i++)
+		fprintf(stdout, "Filesystem %s count:		%lu\n",
+			file_category_types[i], stats.file_category_stat[i]);
+
+	stats.compress_rate = (double)(100 * stats.files_total_size) /
+		(double)(stats.files_total_origin_size);
+	fprintf(stdout, "Filesystem compressed files:            %lu\n",
+			stats.compressed_files);
+	fprintf(stdout, "Filesystem uncompressed files:          %lu\n",
+			stats.uncompressed_files);
+	fprintf(stdout, "Filesystem total original file size:    %lu Bytes\n",
+			stats.files_total_origin_size);
+	fprintf(stdout, "Filesystem total file size:             %lu Bytes\n",
+			stats.files_total_size);
+	fprintf(stdout, "Filesystem compress rate:               %.2f%%\n",
+			stats.compress_rate);
+}
+
+static void erofsdump_print_statistic(void)
+{
+	int err;
+
+	err = erofs_read_dir(sbi.root_nid, sbi.root_nid);
+	if (err) {
+		erofs_err("read dir failed");
+		return;
+	}
+
+	erofsdump_file_statistic();
+	erofsdump_filesize_distribution("Original",
+			stats.file_original_size,
+			ARRAY_SIZE(stats.file_original_size));
+	erofsdump_filesize_distribution("On-disk",
+			stats.file_comp_size,
+			ARRAY_SIZE(stats.file_comp_size));
+	erofsdump_filetype_distribution(file_types, OTHERFILETYPE);
+}
+
+static void erofsdump_show_superblock(void)
+{
+	time_t time = sbi.build_time;
+	char uuid_str[37] = "not available";
+	int i = 0;
+
+	fprintf(stdout, "Filesystem magic number:                      0x%04X\n",
+			EROFS_SUPER_MAGIC_V1);
+	fprintf(stdout, "Filesystem blocks:                            %llu\n",
+			sbi.total_blocks | 0ULL);
+	fprintf(stdout, "Filesystem inode metadata start block:        %u\n",
+			sbi.meta_blkaddr);
+	fprintf(stdout, "Filesystem shared xattr metadata start block: %u\n",
+			sbi.xattr_blkaddr);
+	fprintf(stdout, "Filesystem root nid:                          %llu\n",
+			sbi.root_nid | 0ULL);
+	fprintf(stdout, "Filesystem inode count:                       %llu\n",
+			sbi.inos | 0ULL);
+	fprintf(stdout, "Filesystem created:                           %s",
+			ctime(&time));
+	fprintf(stdout, "Filesystem features:                          ");
+	for (; i < ARRAY_SIZE(feature_lists); i++) {
+		u32 feat = le32_to_cpu(feature_lists[i].compat ?
+				       sbi.feature_compat :
+				       sbi.feature_incompat);
+		if (feat & feature_lists[i].flag)
+			fprintf(stdout, "%s ", feature_lists[i].name);
+	}
+#ifdef HAVE_LIBUUID
+	uuid_unparse_lower(sbi.uuid, uuid_str);
+#endif
+	fprintf(stdout, "\nFilesystem UUID:                              %s\n",
+			uuid_str);
+}
+
+int main(int argc, char **argv)
+{
+	int err;
+
+	erofs_init_configure();
+	err = erofsdump_parse_options_cfg(argc, argv);
+	if (err) {
+		if (err == -EINVAL)
+			usage();
+		goto exit;
+	}
+
+	err = dev_open_ro(cfg.c_img_path);
+	if (err) {
+		erofs_err("failed to open image file");
+		goto exit;
+	}
+
+	err = erofs_read_superblock();
+	if (err) {
+		erofs_err("failed to read superblock");
+		goto exit_dev_close;
+	}
+
+	if (!dumpcfg.totalshow) {
+		dumpcfg.show_superblock = true;
+		dumpcfg.totalshow = 1;
+	}
+	if (dumpcfg.show_superblock)
+		erofsdump_show_superblock();
+
+	if (dumpcfg.show_statistics)
+		erofsdump_print_statistic();
+
+	if (dumpcfg.show_extent && !dumpcfg.show_inode) {
+		usage();
+		goto exit_dev_close;
+	}
+
+	if (dumpcfg.show_inode)
+		erofsdump_show_fileinfo(dumpcfg.show_extent);
+
+exit_dev_close:
+	dev_close();
+exit:
+	blob_closeall();
+	erofs_exit_configure();
+	return err;
+}
diff --git a/fsck/Makefile.am b/fsck/Makefile.am
new file mode 100644
index 0000000..55b31ea
--- /dev/null
+++ b/fsck/Makefile.am
@@ -0,0 +1,10 @@
+# SPDX-License-Identifier: GPL-2.0+
+# Makefile.am
+
+AUTOMAKE_OPTIONS = foreign
+bin_PROGRAMS     = fsck.erofs
+AM_CPPFLAGS = ${libuuid_CFLAGS}
+fsck_erofs_SOURCES = main.c
+fsck_erofs_CFLAGS = -Wall -Werror -I$(top_srcdir)/include
+fsck_erofs_LDADD = $(top_builddir)/lib/liberofs.la ${libselinux_LIBS} \
+	${libuuid_LIBS} ${liblz4_LIBS} ${liblzma_LIBS}
diff --git a/fsck/main.c b/fsck/main.c
new file mode 100644
index 0000000..0af15b4
--- /dev/null
+++ b/fsck/main.c
@@ -0,0 +1,806 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright 2021 Google LLC
+ * Author: Daeho Jeong <daehojeong@google.com>
+ */
+#include <stdlib.h>
+#include <getopt.h>
+#include <time.h>
+#include <utime.h>
+#include <unistd.h>
+#include <sys/stat.h>
+#include "erofs/print.h"
+#include "erofs/io.h"
+#include "erofs/compress.h"
+#include "erofs/decompress.h"
+#include "erofs/dir.h"
+
+static int erofsfsck_check_inode(erofs_nid_t pnid, erofs_nid_t nid);
+
+struct erofsfsck_cfg {
+	u64 physical_blocks;
+	u64 logical_blocks;
+	char *extract_path;
+	size_t extract_pos;
+	mode_t umask;
+	bool superuser;
+	bool corrupted;
+	bool print_comp_ratio;
+	bool check_decomp;
+	bool force;
+	bool overwrite;
+	bool preserve_owner;
+	bool preserve_perms;
+};
+static struct erofsfsck_cfg fsckcfg;
+
+static struct option long_options[] = {
+	{"help", no_argument, 0, 1},
+	{"extract", optional_argument, 0, 2},
+	{"device", required_argument, 0, 3},
+	{"force", no_argument, 0, 4},
+	{"overwrite", no_argument, 0, 5},
+	{"preserve", no_argument, 0, 6},
+	{"preserve-owner", no_argument, 0, 7},
+	{"preserve-perms", no_argument, 0, 8},
+	{"no-preserve", no_argument, 0, 9},
+	{"no-preserve-owner", no_argument, 0, 10},
+	{"no-preserve-perms", no_argument, 0, 11},
+	{0, 0, 0, 0},
+};
+
+static void print_available_decompressors(FILE *f, const char *delim)
+{
+	unsigned int i = 0;
+	const char *s;
+
+	while ((s = z_erofs_list_available_compressors(i)) != NULL) {
+		if (i++)
+			fputs(delim, f);
+		fputs(s, f);
+	}
+	fputc('\n', f);
+}
+
+static void usage(void)
+{
+	fputs("usage: [options] IMAGE\n\n"
+	      "Check erofs filesystem compatibility and integrity of IMAGE, and [options] are:\n"
+	      " -V                     print the version number of fsck.erofs and exit\n"
+	      " -d#                    set output message level to # (maximum 9)\n"
+	      " -p                     print total compression ratio of all files\n"
+	      " --device=X             specify an extra device to be used together\n"
+	      " --extract[=X]          check if all files are well encoded, optionally extract to X\n"
+	      " --help                 display this help and exit\n"
+	      "\nExtraction options (--extract=X is required):\n"
+	      " --force                allow extracting to root\n"
+	      " --overwrite            overwrite files that already exist\n"
+	      " --preserve             extract with the same ownership and permissions as on the filesystem\n"
+	      "                        (default for superuser)\n"
+	      " --preserve-owner       extract with the same ownership as on the filesystem\n"
+	      " --preserve-perms       extract with the same permissions as on the filesystem\n"
+	      " --no-preserve          extract as yourself and apply user's umask on permissions\n"
+	      "                        (default for ordinary users)\n"
+	      " --no-preserve-owner    extract as yourself\n"
+	      " --no-preserve-perms    apply user's umask when extracting permissions\n"
+	      "\nSupported algorithms are: ", stderr);
+	print_available_decompressors(stderr, ", ");
+}
+
+static void erofsfsck_print_version(void)
+{
+	fprintf(stderr, "fsck.erofs %s\n", cfg.c_version);
+}
+
+static int erofsfsck_parse_options_cfg(int argc, char **argv)
+{
+	int opt, ret;
+	bool has_opt_preserve = false;
+
+	while ((opt = getopt_long(argc, argv, "Vd:p",
+				  long_options, NULL)) != -1) {
+		switch (opt) {
+		case 'V':
+			erofsfsck_print_version();
+			exit(0);
+		case 'd':
+			ret = atoi(optarg);
+			if (ret < EROFS_MSG_MIN || ret > EROFS_MSG_MAX) {
+				erofs_err("invalid debug level %d", ret);
+				return -EINVAL;
+			}
+			cfg.c_dbg_lvl = ret;
+			break;
+		case 'p':
+			fsckcfg.print_comp_ratio = true;
+			break;
+		case 1:
+			usage();
+			exit(0);
+		case 2:
+			fsckcfg.check_decomp = true;
+			if (optarg) {
+				size_t len = strlen(optarg);
+
+				if (len == 0) {
+					erofs_err("empty value given for --extract=X");
+					return -EINVAL;
+				}
+
+				/* remove trailing slashes except root */
+				while (len > 1 && optarg[len - 1] == '/')
+					len--;
+
+				fsckcfg.extract_path = malloc(PATH_MAX);
+				if (!fsckcfg.extract_path)
+					return -ENOMEM;
+				strncpy(fsckcfg.extract_path, optarg, len);
+				fsckcfg.extract_path[len] = '\0';
+				/* if path is root, start writing from position 0 */
+				if (len == 1 && fsckcfg.extract_path[0] == '/')
+					len = 0;
+				fsckcfg.extract_pos = len;
+			}
+			break;
+		case 3:
+			ret = blob_open_ro(optarg);
+			if (ret)
+				return ret;
+			++sbi.extra_devices;
+			break;
+		case 4:
+			fsckcfg.force = true;
+			break;
+		case 5:
+			fsckcfg.overwrite = true;
+			break;
+		case 6:
+			fsckcfg.preserve_owner = fsckcfg.preserve_perms = true;
+			has_opt_preserve = true;
+			break;
+		case 7:
+			fsckcfg.preserve_owner = true;
+			has_opt_preserve = true;
+			break;
+		case 8:
+			fsckcfg.preserve_perms = true;
+			has_opt_preserve = true;
+			break;
+		case 9:
+			fsckcfg.preserve_owner = fsckcfg.preserve_perms = false;
+			has_opt_preserve = true;
+			break;
+		case 10:
+			fsckcfg.preserve_owner = false;
+			has_opt_preserve = true;
+			break;
+		case 11:
+			fsckcfg.preserve_perms = false;
+			has_opt_preserve = true;
+			break;
+		default:
+			return -EINVAL;
+		}
+	}
+
+	if (fsckcfg.extract_path) {
+		if (!fsckcfg.extract_pos && !fsckcfg.force) {
+			erofs_err("--extract=/ must be used together with --force");
+			return -EINVAL;
+		}
+	} else {
+		if (fsckcfg.force) {
+			erofs_err("--force must be used together with --extract=X");
+			return -EINVAL;
+		}
+		if (fsckcfg.overwrite) {
+			erofs_err("--overwrite must be used together with --extract=X");
+			return -EINVAL;
+		}
+		if (has_opt_preserve) {
+			erofs_err("--[no-]preserve[-owner/-perms] must be used together with --extract=X");
+			return -EINVAL;
+		}
+	}
+
+	if (optind >= argc) {
+		erofs_err("missing argument: IMAGE");
+		return -EINVAL;
+	}
+
+	cfg.c_img_path = strdup(argv[optind++]);
+	if (!cfg.c_img_path)
+		return -ENOMEM;
+
+	if (optind < argc) {
+		erofs_err("unexpected argument: %s", argv[optind]);
+		return -EINVAL;
+	}
+	return 0;
+}
+
+static void erofsfsck_set_attributes(struct erofs_inode *inode, char *path)
+{
+	int ret;
+
+	/* don't apply attributes when fsck is used without extraction */
+	if (!fsckcfg.extract_path)
+		return;
+
+#ifdef HAVE_UTIMENSAT
+	if (utimensat(AT_FDCWD, path, (struct timespec []) {
+				[0] = { .tv_sec = inode->i_mtime,
+					.tv_nsec = inode->i_mtime_nsec },
+				[1] = { .tv_sec = inode->i_mtime,
+					.tv_nsec = inode->i_mtime_nsec },
+			}, AT_SYMLINK_NOFOLLOW) < 0)
+#else
+	if (utime(path, &((struct utimbuf){.actime = inode->i_mtime,
+					   .modtime = inode->i_mtime})) < 0)
+#endif
+		erofs_warn("failed to set times: %s", path);
+
+	if (!S_ISLNK(inode->i_mode)) {
+		if (fsckcfg.preserve_perms)
+			ret = chmod(path, inode->i_mode);
+		else
+			ret = chmod(path, inode->i_mode & ~fsckcfg.umask);
+		if (ret < 0)
+			erofs_warn("failed to set permissions: %s", path);
+	}
+
+	if (fsckcfg.preserve_owner) {
+		ret = lchown(path, inode->i_uid, inode->i_gid);
+		if (ret < 0)
+			erofs_warn("failed to change ownership: %s", path);
+	}
+}
+
+static int erofs_check_sb_chksum(void)
+{
+	int ret;
+	u8 buf[EROFS_BLKSIZ];
+	u32 crc;
+	struct erofs_super_block *sb;
+
+	ret = blk_read(0, buf, 0, 1);
+	if (ret) {
+		erofs_err("failed to read superblock to check checksum: %d",
+			  ret);
+		return -1;
+	}
+
+	sb = (struct erofs_super_block *)(buf + EROFS_SUPER_OFFSET);
+	sb->checksum = 0;
+
+	crc = erofs_crc32c(~0, (u8 *)sb, EROFS_BLKSIZ - EROFS_SUPER_OFFSET);
+	if (crc != sbi.checksum) {
+		erofs_err("superblock chksum doesn't match: saved(%08xh) calculated(%08xh)",
+			  sbi.checksum, crc);
+		fsckcfg.corrupted = true;
+		return -1;
+	}
+	return 0;
+}
+
+static int erofs_verify_xattr(struct erofs_inode *inode)
+{
+	unsigned int xattr_hdr_size = sizeof(struct erofs_xattr_ibody_header);
+	unsigned int xattr_entry_size = sizeof(struct erofs_xattr_entry);
+	erofs_off_t addr;
+	unsigned int ofs, xattr_shared_count;
+	struct erofs_xattr_ibody_header *ih;
+	struct erofs_xattr_entry *entry;
+	int i, remaining = inode->xattr_isize, ret = 0;
+	char buf[EROFS_BLKSIZ];
+
+	if (inode->xattr_isize == xattr_hdr_size) {
+		erofs_err("xattr_isize %d of nid %llu is not supported yet",
+			  inode->xattr_isize, inode->nid | 0ULL);
+		ret = -EFSCORRUPTED;
+		goto out;
+	} else if (inode->xattr_isize < xattr_hdr_size) {
+		if (inode->xattr_isize) {
+			erofs_err("bogus xattr ibody @ nid %llu",
+				  inode->nid | 0ULL);
+			ret = -EFSCORRUPTED;
+			goto out;
+		}
+	}
+
+	addr = iloc(inode->nid) + inode->inode_isize;
+	ret = dev_read(0, buf, addr, xattr_hdr_size);
+	if (ret < 0) {
+		erofs_err("failed to read xattr header @ nid %llu: %d",
+			  inode->nid | 0ULL, ret);
+		goto out;
+	}
+	ih = (struct erofs_xattr_ibody_header *)buf;
+	xattr_shared_count = ih->h_shared_count;
+
+	ofs = erofs_blkoff(addr) + xattr_hdr_size;
+	addr += xattr_hdr_size;
+	remaining -= xattr_hdr_size;
+	for (i = 0; i < xattr_shared_count; ++i) {
+		if (ofs >= EROFS_BLKSIZ) {
+			if (ofs != EROFS_BLKSIZ) {
+				erofs_err("unaligned xattr entry in xattr shared area @ nid %llu",
+					  inode->nid | 0ULL);
+				ret = -EFSCORRUPTED;
+				goto out;
+			}
+			ofs = 0;
+		}
+		ofs += xattr_entry_size;
+		addr += xattr_entry_size;
+		remaining -= xattr_entry_size;
+	}
+
+	while (remaining > 0) {
+		unsigned int entry_sz;
+
+		ret = dev_read(0, buf, addr, xattr_entry_size);
+		if (ret) {
+			erofs_err("failed to read xattr entry @ nid %llu: %d",
+				  inode->nid | 0ULL, ret);
+			goto out;
+		}
+
+		entry = (struct erofs_xattr_entry *)buf;
+		entry_sz = erofs_xattr_entry_size(entry);
+		if (remaining < entry_sz) {
+			erofs_err("xattr on-disk corruption: xattr entry beyond xattr_isize @ nid %llu",
+				  inode->nid | 0ULL);
+			ret = -EFSCORRUPTED;
+			goto out;
+		}
+		addr += entry_sz;
+		remaining -= entry_sz;
+	}
+out:
+	return ret;
+}
+
+static int erofs_verify_inode_data(struct erofs_inode *inode, int outfd)
+{
+	struct erofs_map_blocks map = {
+		.index = UINT_MAX,
+	};
+	struct erofs_map_dev mdev;
+	int ret = 0;
+	bool compressed;
+	erofs_off_t pos = 0;
+	u64 pchunk_len = 0;
+	unsigned int raw_size = 0, buffer_size = 0;
+	char *raw = NULL, *buffer = NULL;
+
+	erofs_dbg("verify data chunk of nid(%llu): type(%d)",
+		  inode->nid | 0ULL, inode->datalayout);
+
+	switch (inode->datalayout) {
+	case EROFS_INODE_FLAT_PLAIN:
+	case EROFS_INODE_FLAT_INLINE:
+	case EROFS_INODE_CHUNK_BASED:
+		compressed = false;
+		break;
+	case EROFS_INODE_FLAT_COMPRESSION_LEGACY:
+	case EROFS_INODE_FLAT_COMPRESSION:
+		compressed = true;
+		break;
+	default:
+		erofs_err("unknown datalayout");
+		return -EINVAL;
+	}
+
+	while (pos < inode->i_size) {
+		map.m_la = pos;
+		if (compressed)
+			ret = z_erofs_map_blocks_iter(inode, &map,
+					EROFS_GET_BLOCKS_FIEMAP);
+		else
+			ret = erofs_map_blocks(inode, &map,
+					EROFS_GET_BLOCKS_FIEMAP);
+		if (ret)
+			goto out;
+
+		if (!compressed && map.m_llen != map.m_plen) {
+			erofs_err("broken chunk length m_la %" PRIu64 " m_llen %" PRIu64 " m_plen %" PRIu64,
+				  map.m_la, map.m_llen, map.m_plen);
+			ret = -EFSCORRUPTED;
+			goto out;
+		}
+
+		/* the last lcluster can be divided into 3 parts */
+		if (map.m_la + map.m_llen > inode->i_size)
+			map.m_llen = inode->i_size - map.m_la;
+
+		pchunk_len += map.m_plen;
+		pos += map.m_llen;
+
+		/* should skip decomp? */
+		if (!(map.m_flags & EROFS_MAP_MAPPED) || !fsckcfg.check_decomp)
+			continue;
+
+		if (map.m_plen > raw_size) {
+			raw_size = map.m_plen;
+			raw = realloc(raw, raw_size);
+			BUG_ON(!raw);
+		}
+
+		mdev = (struct erofs_map_dev) {
+			.m_deviceid = map.m_deviceid,
+			.m_pa = map.m_pa,
+		};
+		ret = erofs_map_dev(&sbi, &mdev);
+		if (ret) {
+			erofs_err("failed to map device of m_pa %" PRIu64 ", m_deviceid %u @ nid %llu: %d",
+				  map.m_pa, map.m_deviceid, inode->nid | 0ULL,
+				  ret);
+			goto out;
+		}
+
+		if (compressed && map.m_llen > buffer_size) {
+			buffer_size = map.m_llen;
+			buffer = realloc(buffer, buffer_size);
+			BUG_ON(!buffer);
+		}
+
+		ret = dev_read(mdev.m_deviceid, raw, mdev.m_pa, map.m_plen);
+		if (ret < 0) {
+			erofs_err("failed to read data of m_pa %" PRIu64 ", m_plen %" PRIu64 " @ nid %llu: %d",
+				  mdev.m_pa, map.m_plen, inode->nid | 0ULL,
+				  ret);
+			goto out;
+		}
+
+		if (compressed) {
+			struct z_erofs_decompress_req rq = {
+				.in = raw,
+				.out = buffer,
+				.decodedskip = 0,
+				.inputsize = map.m_plen,
+				.decodedlength = map.m_llen,
+				.alg = map.m_algorithmformat,
+				.partial_decoding = 0
+			};
+
+			ret = z_erofs_decompress(&rq);
+			if (ret < 0) {
+				erofs_err("failed to decompress data of m_pa %" PRIu64 ", m_plen %" PRIu64 " @ nid %llu: %s",
+					  mdev.m_pa, map.m_plen,
+					  inode->nid | 0ULL, strerror(-ret));
+				goto out;
+			}
+		}
+
+		if (outfd >= 0 && write(outfd, compressed ? buffer : raw,
+					map.m_llen) < 0) {
+			erofs_err("I/O error occurred when verifying data chunk @ nid %llu",
+				  inode->nid | 0ULL);
+			ret = -EIO;
+			goto out;
+		}
+	}
+
+	if (fsckcfg.print_comp_ratio) {
+		fsckcfg.logical_blocks +=
+			DIV_ROUND_UP(inode->i_size, EROFS_BLKSIZ);
+		fsckcfg.physical_blocks +=
+			DIV_ROUND_UP(pchunk_len, EROFS_BLKSIZ);
+	}
+out:
+	if (raw)
+		free(raw);
+	if (buffer)
+		free(buffer);
+	return ret < 0 ? ret : 0;
+}
+
+static inline int erofs_extract_dir(struct erofs_inode *inode)
+{
+	int ret;
+
+	erofs_dbg("create directory %s", fsckcfg.extract_path);
+
+	/* verify data chunk layout */
+	ret = erofs_verify_inode_data(inode, -1);
+	if (ret)
+		return ret;
+
+	/*
+	 * Make directory with default user rwx permissions rather than
+	 * the permissions from the filesystem, as these may not have
+	 * write/execute permission.  These are fixed up later in
+	 * erofsfsck_set_attributes().
+	 */
+	if (mkdir(fsckcfg.extract_path, 0700) < 0) {
+		struct stat st;
+
+		if (errno != EEXIST) {
+			erofs_err("failed to create directory: %s (%s)",
+				  fsckcfg.extract_path, strerror(errno));
+			return -errno;
+		}
+
+		if (lstat(fsckcfg.extract_path, &st) ||
+		    !S_ISDIR(st.st_mode)) {
+			erofs_err("path is not a directory: %s",
+				  fsckcfg.extract_path);
+			return -ENOTDIR;
+		}
+
+		/*
+		 * Try to change permissions of existing directory so
+		 * that we can write to it
+		 */
+		if (chmod(fsckcfg.extract_path, 0700) < 0) {
+			erofs_err("failed to set permissions: %s (%s)",
+				  fsckcfg.extract_path, strerror(errno));
+			return -errno;
+		}
+	}
+	return 0;
+}
+
+static inline int erofs_extract_file(struct erofs_inode *inode)
+{
+	bool tryagain = true;
+	int ret, fd;
+
+	erofs_dbg("extract file to path: %s", fsckcfg.extract_path);
+
+again:
+	fd = open(fsckcfg.extract_path,
+		  O_WRONLY | O_CREAT | O_NOFOLLOW |
+			(fsckcfg.overwrite ? O_TRUNC : O_EXCL), 0700);
+	if (fd < 0) {
+		if (fsckcfg.overwrite && tryagain) {
+			if (errno == EISDIR) {
+				erofs_warn("try to forcely remove directory %s",
+					   fsckcfg.extract_path);
+				if (rmdir(fsckcfg.extract_path) < 0) {
+					erofs_err("failed to remove: %s (%s)",
+						  fsckcfg.extract_path, strerror(errno));
+					return -EISDIR;
+				}
+			} else if (errno == EACCES &&
+				   chmod(fsckcfg.extract_path, 0700) < 0) {
+				erofs_err("failed to set permissions: %s (%s)",
+					  fsckcfg.extract_path, strerror(errno));
+				return -errno;
+			}
+			tryagain = false;
+			goto again;
+		}
+		erofs_err("failed to open: %s (%s)", fsckcfg.extract_path,
+			  strerror(errno));
+		return -errno;
+	}
+
+	/* verify data chunk layout */
+	ret = erofs_verify_inode_data(inode, fd);
+	if (ret)
+		return ret;
+
+	if (close(fd))
+		return -errno;
+	return ret;
+}
+
+static inline int erofs_extract_symlink(struct erofs_inode *inode)
+{
+	bool tryagain = true;
+	int ret;
+	char *buf = NULL;
+
+	erofs_dbg("extract symlink to path: %s", fsckcfg.extract_path);
+
+	/* verify data chunk layout */
+	ret = erofs_verify_inode_data(inode, -1);
+	if (ret)
+		return ret;
+
+	buf = malloc(inode->i_size + 1);
+	if (!buf) {
+		ret = -ENOMEM;
+		goto out;
+	}
+
+	ret = erofs_pread(inode, buf, inode->i_size, 0);
+	if (ret) {
+		erofs_err("I/O error occurred when reading symlink @ nid %llu: %d",
+			  inode->nid | 0ULL, ret);
+		goto out;
+	}
+
+	buf[inode->i_size] = '\0';
+again:
+	if (symlink(buf, fsckcfg.extract_path) < 0) {
+		if (errno == EEXIST && fsckcfg.overwrite && tryagain) {
+			erofs_warn("try to forcely remove file %s",
+				   fsckcfg.extract_path);
+			if (unlink(fsckcfg.extract_path) < 0) {
+				erofs_err("failed to remove: %s",
+					  fsckcfg.extract_path);
+				ret = -errno;
+				goto out;
+			}
+			tryagain = false;
+			goto again;
+		}
+		erofs_err("failed to create symlink: %s",
+			  fsckcfg.extract_path);
+		ret = -errno;
+	}
+out:
+	if (buf)
+		free(buf);
+	return ret;
+}
+
+static int erofsfsck_dirent_iter(struct erofs_dir_context *ctx)
+{
+	int ret;
+	size_t prev_pos = fsckcfg.extract_pos;
+
+	if (ctx->dot_dotdot)
+		return 0;
+
+	if (fsckcfg.extract_path) {
+		size_t curr_pos = prev_pos;
+
+		fsckcfg.extract_path[curr_pos++] = '/';
+		strncpy(fsckcfg.extract_path + curr_pos, ctx->dname,
+			ctx->de_namelen);
+		curr_pos += ctx->de_namelen;
+		fsckcfg.extract_path[curr_pos] = '\0';
+		fsckcfg.extract_pos = curr_pos;
+	}
+
+	ret = erofsfsck_check_inode(ctx->dir->nid, ctx->de_nid);
+
+	if (fsckcfg.extract_path) {
+		fsckcfg.extract_path[prev_pos] = '\0';
+		fsckcfg.extract_pos = prev_pos;
+	}
+	return ret;
+}
+
+static int erofsfsck_check_inode(erofs_nid_t pnid, erofs_nid_t nid)
+{
+	int ret;
+	struct erofs_inode inode;
+
+	erofs_dbg("check inode: nid(%llu)", nid | 0ULL);
+
+	inode.nid = nid;
+	ret = erofs_read_inode_from_disk(&inode);
+	if (ret) {
+		if (ret == -EIO)
+			erofs_err("I/O error occurred when reading nid(%llu)",
+				  nid | 0ULL);
+		goto out;
+	}
+
+	/* verify xattr field */
+	ret = erofs_verify_xattr(&inode);
+	if (ret)
+		goto out;
+
+	if (fsckcfg.extract_path) {
+		switch (inode.i_mode & S_IFMT) {
+		case S_IFDIR:
+			ret = erofs_extract_dir(&inode);
+			break;
+		case S_IFREG:
+			ret = erofs_extract_file(&inode);
+			break;
+		case S_IFLNK:
+			ret = erofs_extract_symlink(&inode);
+			break;
+		default:
+			/* TODO */
+			goto verify;
+		}
+	} else {
+verify:
+		/* verify data chunk layout */
+		ret = erofs_verify_inode_data(&inode, -1);
+	}
+	if (ret)
+		goto out;
+
+	/* XXXX: the dir depth should be restricted in order to avoid loops */
+	if (S_ISDIR(inode.i_mode)) {
+		struct erofs_dir_context ctx = {
+			.flags = EROFS_READDIR_VALID_PNID,
+			.pnid = pnid,
+			.dir = &inode,
+			.cb = erofsfsck_dirent_iter,
+		};
+
+		ret = erofs_iterate_dir(&ctx, true);
+	}
+
+	if (!ret)
+		erofsfsck_set_attributes(&inode, fsckcfg.extract_path);
+
+out:
+	if (ret && ret != -EIO)
+		fsckcfg.corrupted = true;
+	return ret;
+}
+
+int main(int argc, char **argv)
+{
+	int err;
+
+	erofs_init_configure();
+
+	fsckcfg.physical_blocks = 0;
+	fsckcfg.logical_blocks = 0;
+	fsckcfg.extract_path = NULL;
+	fsckcfg.extract_pos = 0;
+	fsckcfg.umask = umask(0);
+	fsckcfg.superuser = geteuid() == 0;
+	fsckcfg.corrupted = false;
+	fsckcfg.print_comp_ratio = false;
+	fsckcfg.check_decomp = false;
+	fsckcfg.force = false;
+	fsckcfg.overwrite = false;
+	fsckcfg.preserve_owner = fsckcfg.superuser;
+	fsckcfg.preserve_perms = fsckcfg.superuser;
+
+	err = erofsfsck_parse_options_cfg(argc, argv);
+	if (err) {
+		if (err == -EINVAL)
+			usage();
+		goto exit;
+	}
+
+	err = dev_open_ro(cfg.c_img_path);
+	if (err) {
+		erofs_err("failed to open image file");
+		goto exit;
+	}
+
+	err = erofs_read_superblock();
+	if (err) {
+		erofs_err("failed to read superblock");
+		goto exit_dev_close;
+	}
+
+	if (erofs_sb_has_sb_chksum() && erofs_check_sb_chksum()) {
+		erofs_err("failed to verify superblock checksum");
+		goto exit_dev_close;
+	}
+
+	err = erofsfsck_check_inode(sbi.root_nid, sbi.root_nid);
+	if (fsckcfg.corrupted) {
+		if (!fsckcfg.extract_path)
+			erofs_err("Found some filesystem corruption");
+		else
+			erofs_err("Failed to extract filesystem");
+		err = -EFSCORRUPTED;
+	} else if (!err) {
+		if (!fsckcfg.extract_path)
+			erofs_info("No errors found");
+		else
+			erofs_info("Extracted filesystem successfully");
+
+		if (fsckcfg.print_comp_ratio) {
+			double comp_ratio =
+				(double)fsckcfg.physical_blocks * 100 /
+				(double)fsckcfg.logical_blocks;
+
+			erofs_info("Compression ratio: %.2f(%%)", comp_ratio);
+		}
+	}
+
+exit_dev_close:
+	dev_close();
+exit:
+	blob_closeall();
+	erofs_exit_configure();
+	return err ? 1 : 0;
+}
diff --git a/fuse/Makefile.am b/fuse/Makefile.am
index e7757bc..0a78c0a 100644
--- a/fuse/Makefile.am
+++ b/fuse/Makefile.am
@@ -1,10 +1,9 @@
 # SPDX-License-Identifier: GPL-2.0+
-# Makefile.am
 
 AUTOMAKE_OPTIONS = foreign
 bin_PROGRAMS     = erofsfuse
-erofsfuse_SOURCES = dir.c main.c
+erofsfuse_SOURCES = main.c
 erofsfuse_CFLAGS = -Wall -Werror -I$(top_srcdir)/include
 erofsfuse_CFLAGS += -DFUSE_USE_VERSION=26 ${libfuse_CFLAGS} ${libselinux_CFLAGS}
-erofsfuse_LDADD = $(top_builddir)/lib/liberofs.la ${libfuse_LIBS} ${liblz4_LIBS} ${libselinux_LIBS}
-
+erofsfuse_LDADD = $(top_builddir)/lib/liberofs.la ${libfuse_LIBS} ${liblz4_LIBS} \
+	${libselinux_LIBS} ${liblzma_LIBS}
diff --git a/fuse/dir.c b/fuse/dir.c
deleted file mode 100644
index f8fa0f6..0000000
--- a/fuse/dir.c
+++ /dev/null
@@ -1,103 +0,0 @@
-// SPDX-License-Identifier: GPL-2.0+
-/*
- * erofs-utils/fuse/dir.c
- *
- * Created by Li Guifu <blucerlee@gmail.com>
- */
-#include <fuse.h>
-#include <fuse_opt.h>
-
-#include "erofs/internal.h"
-#include "erofs/print.h"
-
-static int erofs_fill_dentries(struct erofs_inode *dir,
-			       fuse_fill_dir_t filler, void *buf,
-			       void *dblk, unsigned int nameoff,
-			       unsigned int maxsize)
-{
-	struct erofs_dirent *de = dblk;
-	const struct erofs_dirent *end = dblk + nameoff;
-	char namebuf[EROFS_NAME_LEN + 1];
-
-	while (de < end) {
-		const char *de_name;
-		unsigned int de_namelen;
-
-		nameoff = le16_to_cpu(de->nameoff);
-		de_name = (char *)dblk + nameoff;
-
-		/* the last dirent in the block? */
-		if (de + 1 >= end)
-			de_namelen = strnlen(de_name, maxsize - nameoff);
-		else
-			de_namelen = le16_to_cpu(de[1].nameoff) - nameoff;
-
-		/* a corrupted entry is found */
-		if (nameoff + de_namelen > maxsize ||
-		    de_namelen > EROFS_NAME_LEN) {
-			erofs_err("bogus dirent @ nid %llu", dir->nid | 0ULL);
-			DBG_BUGON(1);
-			return -EFSCORRUPTED;
-		}
-
-		memcpy(namebuf, de_name, de_namelen);
-		namebuf[de_namelen] = '\0';
-
-		filler(buf, namebuf, NULL, 0);
-		++de;
-	}
-	return 0;
-}
-
-int erofsfuse_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
-		      off_t offset, struct fuse_file_info *fi)
-{
-	int ret;
-	struct erofs_inode dir;
-	char dblk[EROFS_BLKSIZ];
-	erofs_off_t pos;
-
-	erofs_dbg("readdir:%s offset=%llu", path, (long long)offset);
-
-	ret = erofs_ilookup(path, &dir);
-	if (ret)
-		return ret;
-
-	erofs_dbg("path=%s nid = %llu", path, dir.nid | 0ULL);
-
-	if (!S_ISDIR(dir.i_mode))
-		return -ENOTDIR;
-
-	if (!dir.i_size)
-		return 0;
-
-	pos = 0;
-	while (pos < dir.i_size) {
-		unsigned int nameoff, maxsize;
-		struct erofs_dirent *de;
-
-		maxsize = min_t(unsigned int, EROFS_BLKSIZ,
-				dir.i_size - pos);
-		ret = erofs_pread(&dir, dblk, maxsize, pos);
-		if (ret)
-			return ret;
-
-		de = (struct erofs_dirent *)dblk;
-		nameoff = le16_to_cpu(de->nameoff);
-		if (nameoff < sizeof(struct erofs_dirent) ||
-		    nameoff >= PAGE_SIZE) {
-			erofs_err("invalid de[0].nameoff %u @ nid %llu",
-				  nameoff, dir.nid | 0ULL);
-			ret = -EFSCORRUPTED;
-			break;
-		}
-
-		ret = erofs_fill_dentries(&dir, filler, buf,
-					  dblk, nameoff, maxsize);
-		if (ret)
-			break;
-		pos += maxsize;
-	}
-	return 0;
-}
-
diff --git a/fuse/macosx.h b/fuse/macosx.h
new file mode 100644
index 0000000..81ac47f
--- /dev/null
+++ b/fuse/macosx.h
@@ -0,0 +1,4 @@
+/* SPDX-License-Identifier: GPL-2.0+ */
+#ifdef __APPLE__
+#undef LIST_HEAD
+#endif
diff --git a/fuse/main.c b/fuse/main.c
index c162912..ae377ae 100644
--- a/fuse/main.c
+++ b/fuse/main.c
@@ -1,7 +1,5 @@
 // SPDX-License-Identifier: GPL-2.0+
 /*
- * erofs-utils/fuse/main.c
- *
  * Created by Li Guifu <blucerlee@gmail.com>
  */
 #include <stdlib.h>
@@ -10,13 +8,61 @@
 #include <libgen.h>
 #include <fuse.h>
 #include <fuse_opt.h>
-
+#include "macosx.h"
 #include "erofs/config.h"
 #include "erofs/print.h"
 #include "erofs/io.h"
+#include "erofs/dir.h"
 
-int erofsfuse_readdir(const char *path, void *buffer, fuse_fill_dir_t filler,
-		      off_t offset, struct fuse_file_info *fi);
+struct erofsfuse_dir_context {
+	struct erofs_dir_context ctx;
+	fuse_fill_dir_t filler;
+	struct fuse_file_info *fi;
+	void *buf;
+};
+
+static int erofsfuse_fill_dentries(struct erofs_dir_context *ctx)
+{
+	struct erofsfuse_dir_context *fusectx = (void *)ctx;
+	char dname[EROFS_NAME_LEN + 1];
+
+	strncpy(dname, ctx->dname, ctx->de_namelen);
+	dname[ctx->de_namelen] = '\0';
+	fusectx->filler(fusectx->buf, dname, NULL, 0);
+	return 0;
+}
+
+int erofsfuse_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
+		      off_t offset, struct fuse_file_info *fi)
+{
+	int ret;
+	struct erofs_inode dir;
+	struct erofsfuse_dir_context ctx = {
+		.ctx.dir = &dir,
+		.ctx.cb = erofsfuse_fill_dentries,
+		.filler = filler,
+		.fi = fi,
+		.buf = buf,
+	};
+	erofs_dbg("readdir:%s offset=%llu", path, (long long)offset);
+
+	ret = erofs_ilookup(path, &dir);
+	if (ret)
+		return ret;
+
+	erofs_dbg("path=%s nid = %llu", path, dir.nid | 0ULL);
+	if (!S_ISDIR(dir.i_mode))
+		return -ENOTDIR;
+
+	if (!dir.i_size)
+		return 0;
+#ifdef NDEBUG
+	return erofs_iterate_dir(&ctx.ctx, false);
+#else
+	return erofs_iterate_dir(&ctx.ctx, true);
+#endif
+
+}
 
 static void *erofsfuse_init(struct fuse_conn_info *info)
 {
@@ -52,7 +98,7 @@
 	stbuf->st_gid = vi.i_gid;
 	if (S_ISBLK(vi.i_mode) || S_ISCHR(vi.i_mode))
 		stbuf->st_rdev = vi.u.i_rdev;
-	stbuf->st_ctime = vi.i_ctime;
+	stbuf->st_ctime = vi.i_mtime;
 	stbuf->st_mtime = stbuf->st_ctime;
 	stbuf->st_atime = stbuf->st_ctime;
 	return 0;
@@ -74,6 +120,10 @@
 	ret = erofs_pread(&vi, buffer, size, offset);
 	if (ret)
 		return ret;
+	if (offset >= vi.i_size)
+		return 0;
+	if (offset + size > vi.i_size)
+		return vi.i_size - offset;
 	return size;
 }
 
@@ -83,6 +133,10 @@
 
 	if (ret < 0)
 		return ret;
+	DBG_BUGON(ret > size);
+	if (ret == size)
+		buffer[size - 1] = '\0';
+	erofs_dbg("readlink(%s): %s", path, buffer);
 	return 0;
 }
 
@@ -103,16 +157,14 @@
 	bool odebug;
 } fusecfg;
 
-#define OPTION(t, p)                           \
-    { t, offsetof(struct options, p), 1 }
+#define OPTION(t, p) { t, offsetof(struct options, p), 1 }
 static const struct fuse_opt option_spec[] = {
 	OPTION("--dbglevel=%u", debug_lvl),
 	OPTION("--help", show_help),
+	FUSE_OPT_KEY("--device=", 1),
 	FUSE_OPT_END
 };
 
-#define OPTION(t, p)    { t, offsetof(struct options, p), 1 }
-
 static void usage(void)
 {
 	struct fuse_args args = FUSE_ARGS_INIT(0, NULL);
@@ -120,6 +172,7 @@
 	fputs("usage: [options] IMAGE MOUNTPOINT\n\n"
 	      "Options:\n"
 	      "    --dbglevel=#           set output message level to # (maximum 9)\n"
+	      "    --device=#             specify an extra device to be used together\n"
 #if FUSE_MAJOR_VERSION < 3
 	      "    --help                 display this help and exit\n"
 #endif
@@ -145,7 +198,15 @@
 static int optional_opt_func(void *data, const char *arg, int key,
 			     struct fuse_args *outargs)
 {
+	int ret;
+
 	switch (key) {
+	case 1:
+		ret = blob_open_ro(arg + sizeof("--device=") - 1);
+		if (ret)
+			return -1;
+		++sbi.extra_devices;
+		return 0;
 	case FUSE_OPT_KEY_NONOPT:
 		if (fusecfg.mountpoint)
 			return -1; /* Too many args */
@@ -234,6 +295,7 @@
 
 	ret = fuse_main(args.argc, args.argv, &erofs_ops, NULL);
 err_dev_close:
+	blob_closeall();
 	dev_close();
 err_fuse_free_args:
 	fuse_opt_free_args(&args);
@@ -241,4 +303,3 @@
 	erofs_exit_configure();
 	return ret ? EXIT_FAILURE : EXIT_SUCCESS;
 }
-
diff --git a/include/erofs/blobchunk.h b/include/erofs/blobchunk.h
new file mode 100644
index 0000000..4e1ae79
--- /dev/null
+++ b/include/erofs/blobchunk.h
@@ -0,0 +1,28 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * erofs-utils/lib/blobchunk.h
+ *
+ * Copyright (C) 2021, Alibaba Cloud
+ */
+#ifndef __EROFS_BLOBCHUNK_H
+#define __EROFS_BLOBCHUNK_H
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+#include "erofs/internal.h"
+
+int erofs_blob_write_chunk_indexes(struct erofs_inode *inode, erofs_off_t off);
+int erofs_blob_write_chunked_file(struct erofs_inode *inode);
+int erofs_blob_remap(void);
+void erofs_blob_exit(void);
+int erofs_blob_init(const char *blobfile_path);
+int erofs_generate_devtable(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/include/erofs/block_list.h b/include/erofs/block_list.h
new file mode 100644
index 0000000..ca8053e
--- /dev/null
+++ b/include/erofs/block_list.h
@@ -0,0 +1,42 @@
+/* SPDX-License-Identifier: GPL-2.0+ */
+/*
+ * Copyright (C), 2021, Coolpad Group Limited.
+ * Created by Yue Hu <huyue2@yulong.com>
+ */
+#ifndef __EROFS_BLOCK_LIST_H
+#define __EROFS_BLOCK_LIST_H
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+#include "internal.h"
+
+#ifdef WITH_ANDROID
+int erofs_droid_blocklist_fopen(void);
+void erofs_droid_blocklist_fclose(void);
+void erofs_droid_blocklist_write(struct erofs_inode *inode,
+				 erofs_blk_t blk_start, erofs_blk_t nblocks);
+void erofs_droid_blocklist_write_tail_end(struct erofs_inode *inode,
+					  erofs_blk_t blkaddr);
+void erofs_droid_blocklist_write_extent(struct erofs_inode *inode,
+					erofs_blk_t blk_start, erofs_blk_t nblocks,
+					bool first_extent, bool last_extent);
+#else
+static inline void erofs_droid_blocklist_write(struct erofs_inode *inode,
+				 erofs_blk_t blk_start, erofs_blk_t nblocks) {}
+static inline void
+erofs_droid_blocklist_write_tail_end(struct erofs_inode *inode,
+					  erofs_blk_t blkaddr) {}
+static inline void
+erofs_droid_blocklist_write_extent(struct erofs_inode *inode,
+				   erofs_blk_t blk_start, erofs_blk_t nblocks,
+				   bool first_extent, bool last_extent) {}
+#endif
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/include/erofs/cache.h b/include/erofs/cache.h
index 8c171f5..72b849d 100644
--- a/include/erofs/cache.h
+++ b/include/erofs/cache.h
@@ -1,7 +1,5 @@
 /* SPDX-License-Identifier: GPL-2.0+ */
 /*
- * erofs-utils/include/erofs/cache.h
- *
  * Copyright (C) 2018 HUAWEI, Inc.
  *             http://www.huawei.com/
  * Created by Miao Xie <miaoxie@huawei.com>
@@ -10,6 +8,11 @@
 #ifndef __EROFS_CACHE_H
 #define __EROFS_CACHE_H
 
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
 #include "internal.h"
 
 struct erofs_buffer_head;
@@ -21,6 +24,8 @@
 #define INODE		2
 /* shared xattrs */
 #define XATTR		3
+/* device table */
+#define DEVT		4
 
 struct erofs_bhops {
 	bool (*preflush)(struct erofs_buffer_head *bh);
@@ -39,6 +44,7 @@
 
 struct erofs_buffer_block {
 	struct list_head list;
+	struct list_head mapped_list;
 
 	erofs_blk_t blkaddr;
 	int type;
@@ -57,6 +63,9 @@
 	} else if (type == XATTR) {
 		*type_ret = META;
 		return sizeof(struct erofs_xattr_entry);
+	} else if (type == DEVT) {
+		*type_ret = META;
+		return EROFS_DEVT_SLOT_SIZE;
 	}
 
 	if (type == META)
@@ -95,10 +104,13 @@
 struct erofs_buffer_head *erofs_battach(struct erofs_buffer_head *bh,
 					int type, unsigned int size);
 
-erofs_blk_t erofs_mapbh(struct erofs_buffer_block *bb, bool end);
+erofs_blk_t erofs_mapbh(struct erofs_buffer_block *bb);
 bool erofs_bflush(struct erofs_buffer_block *bb);
 
 void erofs_bdrop(struct erofs_buffer_head *bh, bool tryrevoke);
 
+#ifdef __cplusplus
+}
 #endif
 
+#endif
diff --git a/include/erofs/compress.h b/include/erofs/compress.h
index 952f287..fdbf5ff 100644
--- a/include/erofs/compress.h
+++ b/include/erofs/compress.h
@@ -1,7 +1,5 @@
 /* SPDX-License-Identifier: GPL-2.0+ */
 /*
- * erofs-utils/include/erofs/compress.h
- *
  * Copyright (C) 2019 HUAWEI, Inc.
  *             http://www.huawei.com/
  * Created by Gao Xiang <gaoxiang25@huawei.com>
@@ -9,6 +7,11 @@
 #ifndef __EROFS_COMPRESS_H
 #define __EROFS_COMPRESS_H
 
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
 #include "internal.h"
 
 /* workaround for an upstream lz4 compression issue, which can crash us */
@@ -18,10 +21,13 @@
 
 int erofs_write_compressed_file(struct erofs_inode *inode);
 
-int z_erofs_compress_init(void);
+int z_erofs_compress_init(struct erofs_buffer_head *bh);
 int z_erofs_compress_exit(void);
 
 const char *z_erofs_list_available_compressors(unsigned int i);
 
+#ifdef __cplusplus
+}
 #endif
 
+#endif
diff --git a/include/erofs/compress_hints.h b/include/erofs/compress_hints.h
new file mode 100644
index 0000000..43f80e1
--- /dev/null
+++ b/include/erofs/compress_hints.h
@@ -0,0 +1,33 @@
+/* SPDX-License-Identifier: GPL-2.0+ */
+/*
+ * Copyright (C), 2008-2021, OPPO Mobile Comm Corp., Ltd.
+ * Created by Huang Jianan <huangjianan@oppo.com>
+ */
+#ifndef __EROFS_COMPRESS_HINTS_H
+#define __EROFS_COMPRESS_HINTS_H
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+#include "erofs/internal.h"
+#include <sys/types.h>
+#include <regex.h>
+
+struct erofs_compress_hints {
+	struct list_head list;
+
+	regex_t reg;
+	unsigned int physical_clusterblks;
+};
+
+bool z_erofs_apply_compress_hints(struct erofs_inode *inode);
+void erofs_cleanup_compress_hints(void);
+int erofs_load_compress_hints(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/include/erofs/config.h b/include/erofs/config.h
index 02ddf59..0a1b18b 100644
--- a/include/erofs/config.h
+++ b/include/erofs/config.h
@@ -1,7 +1,5 @@
 /* SPDX-License-Identifier: GPL-2.0+ */
 /*
- * erofs-utils/include/erofs/config.h
- *
  * Copyright (C) 2018-2019 HUAWEI, Inc.
  *             http://www.huawei.com/
  * Created by Li Guifu <bluce.liguifu@huawei.com>
@@ -9,20 +7,14 @@
 #ifndef __EROFS_CONFIG_H
 #define __EROFS_CONFIG_H
 
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
 #include "defs.h"
 #include "err.h"
 
-#ifdef HAVE_LIBSELINUX
-#include <selinux/selinux.h>
-#include <selinux/label.h>
-#endif
-
-#ifdef WITH_ANDROID
-#include <selinux/android.h>
-#include <private/android_filesystem_config.h>
-#include <private/canned_fs_config.h>
-#include <private/fs_config.h>
-#endif
 
 enum {
 	FORCE_INODE_COMPACT = 1,
@@ -30,6 +22,11 @@
 };
 
 enum {
+	FORCE_INODE_BLOCK_MAP = 1,
+	FORCE_INODE_CHUNK_INDEXES,
+};
+
+enum {
 	TIMESTAMP_NONE,
 	TIMESTAMP_FIXED,
 	TIMESTAMP_CLAMPING,
@@ -40,7 +37,13 @@
 	int c_dbg_lvl;
 	bool c_dry_run;
 	bool c_legacy_compress;
+#ifndef NDEBUG
+	bool c_random_pclusterblks;
+#endif
 	char c_timeinherit;
+	char c_chunkbits;
+	bool c_noinline_data;
+	bool c_ignore_mtime;
 
 #ifdef HAVE_LIBSELINUX
 	struct selabel_handle *sehnd;
@@ -48,16 +51,25 @@
 	/* related arguments for mkfs.erofs */
 	char *c_img_path;
 	char *c_src_path;
+	char *c_blobdev_path;
+	char *c_compress_hints_file;
 	char *c_compr_alg_master;
 	int c_compr_level_master;
-	int c_force_inodeversion;
+	char c_force_inodeversion;
+	char c_force_chunkformat;
 	/* < 0, xattr disabled and INT_MAX, always use inline xattrs */
 	int c_inline_xattr_tolerance;
+
+	u32 c_pclusterblks_max, c_pclusterblks_def;
+	u32 c_max_decompressed_extent_bytes;
+	u32 c_dict_size;
 	u64 c_unix_timestamp;
+	u32 c_uid, c_gid;
 #ifdef WITH_ANDROID
 	char *mount_point;
 	char *target_out_path;
 	char *fs_config_file;
+	char *block_list_file;
 #endif
 };
 
@@ -79,5 +91,8 @@
 }
 #endif
 
+#ifdef __cplusplus
+}
 #endif
 
+#endif
diff --git a/include/erofs/decompress.h b/include/erofs/decompress.h
index beaac35..e649c80 100644
--- a/include/erofs/decompress.h
+++ b/include/erofs/decompress.h
@@ -1,19 +1,17 @@
 /* SPDX-License-Identifier: GPL-2.0+ */
 /*
- * erofs-utils/include/erofs/decompress.h
- *
  * Copyright (C), 2008-2020, OPPO Mobile Comm Corp., Ltd.
  * Created by Huang Jianan <huangjianan@oppo.com>
  */
 #ifndef __EROFS_DECOMPRESS_H
 #define __EROFS_DECOMPRESS_H
 
-#include "internal.h"
+#ifdef __cplusplus
+extern "C"
+{
+#endif
 
-enum {
-	Z_EROFS_COMPRESSION_SHIFTED = Z_EROFS_COMPRESSION_MAX,
-	Z_EROFS_COMPRESSION_RUNTIME_MAX
-};
+#include "internal.h"
 
 struct z_erofs_decompress_req {
 	char *in, *out;
@@ -32,4 +30,8 @@
 
 int z_erofs_decompress(struct z_erofs_decompress_req *rq);
 
+#ifdef __cplusplus
+}
+#endif
+
 #endif
diff --git a/include/erofs/defs.h b/include/erofs/defs.h
index b54cd9d..4db237f 100644
--- a/include/erofs/defs.h
+++ b/include/erofs/defs.h
@@ -1,7 +1,5 @@
 /* SPDX-License-Identifier: GPL-2.0+ */
 /*
- * erofs-utils/include/erofs/defs.h
- *
  * Copyright (C) 2018 HUAWEI, Inc.
  *             http://www.huawei.com/
  * Created by Li Guifu <bluce.liguifu@huawei.com>
@@ -10,6 +8,11 @@
 #ifndef __EROFS_DEFS_H
 #define __EROFS_DEFS_H
 
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
 #include <stddef.h>
 #include <stdint.h>
 #include <assert.h>
@@ -84,10 +87,12 @@
 #endif
 #endif
 
-#ifndef __OPTIMIZE__
+#ifdef __cplusplus
+#define BUILD_BUG_ON(condition) static_assert(!(condition))
+#elif !defined(__OPTIMIZE__)
 #define BUILD_BUG_ON(condition) ((void)sizeof(char[1 - 2 * !!(condition)]))
 #else
-#define BUILD_BUG_ON(condition) assert(condition)
+#define BUILD_BUG_ON(condition) assert(!(condition))
 #endif
 
 #define DIV_ROUND_UP(n, d) (((n) + (d) - 1) / (d))
@@ -96,6 +101,7 @@
 #define round_up(x, y)          ((((x)-1) | __round_mask(x, y))+1)
 #define round_down(x, y)        ((x) & ~__round_mask(x, y))
 
+#ifndef roundup
 /* The `const' in roundup() prevents gcc-3.3 from calling __divdi3 */
 #define roundup(x, y) (					\
 {							\
@@ -103,6 +109,7 @@
 	(((x) + (__y - 1)) / __y) * __y;		\
 }							\
 )
+#endif
 #define rounddown(x, y) (				\
 {							\
 	typeof(x) __x = (x);				\
@@ -110,6 +117,8 @@
 }							\
 )
 
+/* Can easily conflict with C++'s std::min */
+#ifndef __cplusplus
 #define min(x, y) ({				\
 	typeof(x) _min1 = (x);			\
 	typeof(y) _min2 = (y);			\
@@ -121,6 +130,7 @@
 	typeof(y) _max2 = (y);			\
 	(void) (&_max1 == &_max2);		\
 	_max1 > _max2 ? _max1 : _max2; })
+#endif
 
 /*
  * ..and if you can't take the strict types, you can specify one yourself.
@@ -175,5 +185,142 @@
 	return p[0] | p[1] << 8 | p[2] << 16 | p[3] << 24;
 }
 
+/**
+ * ilog2 - log of base 2 of 32-bit or a 64-bit unsigned value
+ * @n - parameter
+ *
+ * constant-capable log of base 2 calculation
+ * - this can be used to initialise global variables from constant data, hence
+ *   the massive ternary operator construction
+ *
+ * selects the appropriately-sized optimised version depending on sizeof(n)
+ */
+#define ilog2(n)			\
+(					\
+	(n) & (1ULL << 63) ? 63 :	\
+	(n) & (1ULL << 62) ? 62 :	\
+	(n) & (1ULL << 61) ? 61 :	\
+	(n) & (1ULL << 60) ? 60 :	\
+	(n) & (1ULL << 59) ? 59 :	\
+	(n) & (1ULL << 58) ? 58 :	\
+	(n) & (1ULL << 57) ? 57 :	\
+	(n) & (1ULL << 56) ? 56 :	\
+	(n) & (1ULL << 55) ? 55 :	\
+	(n) & (1ULL << 54) ? 54 :	\
+	(n) & (1ULL << 53) ? 53 :	\
+	(n) & (1ULL << 52) ? 52 :	\
+	(n) & (1ULL << 51) ? 51 :	\
+	(n) & (1ULL << 50) ? 50 :	\
+	(n) & (1ULL << 49) ? 49 :	\
+	(n) & (1ULL << 48) ? 48 :	\
+	(n) & (1ULL << 47) ? 47 :	\
+	(n) & (1ULL << 46) ? 46 :	\
+	(n) & (1ULL << 45) ? 45 :	\
+	(n) & (1ULL << 44) ? 44 :	\
+	(n) & (1ULL << 43) ? 43 :	\
+	(n) & (1ULL << 42) ? 42 :	\
+	(n) & (1ULL << 41) ? 41 :	\
+	(n) & (1ULL << 40) ? 40 :	\
+	(n) & (1ULL << 39) ? 39 :	\
+	(n) & (1ULL << 38) ? 38 :	\
+	(n) & (1ULL << 37) ? 37 :	\
+	(n) & (1ULL << 36) ? 36 :	\
+	(n) & (1ULL << 35) ? 35 :	\
+	(n) & (1ULL << 34) ? 34 :	\
+	(n) & (1ULL << 33) ? 33 :	\
+	(n) & (1ULL << 32) ? 32 :	\
+	(n) & (1ULL << 31) ? 31 :	\
+	(n) & (1ULL << 30) ? 30 :	\
+	(n) & (1ULL << 29) ? 29 :	\
+	(n) & (1ULL << 28) ? 28 :	\
+	(n) & (1ULL << 27) ? 27 :	\
+	(n) & (1ULL << 26) ? 26 :	\
+	(n) & (1ULL << 25) ? 25 :	\
+	(n) & (1ULL << 24) ? 24 :	\
+	(n) & (1ULL << 23) ? 23 :	\
+	(n) & (1ULL << 22) ? 22 :	\
+	(n) & (1ULL << 21) ? 21 :	\
+	(n) & (1ULL << 20) ? 20 :	\
+	(n) & (1ULL << 19) ? 19 :	\
+	(n) & (1ULL << 18) ? 18 :	\
+	(n) & (1ULL << 17) ? 17 :	\
+	(n) & (1ULL << 16) ? 16 :	\
+	(n) & (1ULL << 15) ? 15 :	\
+	(n) & (1ULL << 14) ? 14 :	\
+	(n) & (1ULL << 13) ? 13 :	\
+	(n) & (1ULL << 12) ? 12 :	\
+	(n) & (1ULL << 11) ? 11 :	\
+	(n) & (1ULL << 10) ? 10 :	\
+	(n) & (1ULL <<  9) ?  9 :	\
+	(n) & (1ULL <<  8) ?  8 :	\
+	(n) & (1ULL <<  7) ?  7 :	\
+	(n) & (1ULL <<  6) ?  6 :	\
+	(n) & (1ULL <<  5) ?  5 :	\
+	(n) & (1ULL <<  4) ?  4 :	\
+	(n) & (1ULL <<  3) ?  3 :	\
+	(n) & (1ULL <<  2) ?  2 :	\
+	(n) & (1ULL <<  1) ?  1 : 0	\
+)
+
+static inline unsigned int fls_long(unsigned long x)
+{
+	return x ? sizeof(x) * 8 - __builtin_clz(x) : 0;
+}
+
+/**
+ * __roundup_pow_of_two() - round up to nearest power of two
+ * @n: value to round up
+ */
+static inline __attribute__((const))
+unsigned long __roundup_pow_of_two(unsigned long n)
+{
+	return 1UL << fls_long(n - 1);
+}
+
+/**
+ * roundup_pow_of_two - round the given value up to nearest power of two
+ * @n: parameter
+ *
+ * round the given value up to the nearest power of two
+ * - the result is undefined when n == 0
+ * - this can be used to initialise global variables from constant data
+ */
+#define roundup_pow_of_two(n)			\
+(						\
+	__builtin_constant_p(n) ? (		\
+		((n) == 1) ? 1 :		\
+		(1UL << (ilog2((n) - 1) + 1))	\
+				   ) :		\
+	__roundup_pow_of_two(n)			\
+)
+
+#ifndef __always_inline
+#define __always_inline	inline
 #endif
 
+#ifdef HAVE_STRUCT_STAT_ST_ATIM
+/* Linux */
+#define ST_ATIM_NSEC(stbuf) ((stbuf)->st_atim.tv_nsec)
+#define ST_CTIM_NSEC(stbuf) ((stbuf)->st_ctim.tv_nsec)
+#define ST_MTIM_NSEC(stbuf) ((stbuf)->st_mtim.tv_nsec)
+#elif defined(HAVE_STRUCT_STAT_ST_ATIMENSEC)
+/* macOS */
+#define ST_ATIM_NSEC(stbuf) ((stbuf)->st_atimensec)
+#define ST_CTIM_NSEC(stbuf) ((stbuf)->st_ctimensec)
+#define ST_MTIM_NSEC(stbuf) ((stbuf)->st_mtimensec)
+#else
+#define ST_ATIM_NSEC(stbuf) 0
+#define ST_CTIM_NSEC(stbuf) 0
+#define ST_MTIM_NSEC(stbuf) 0
+#endif
+
+#ifdef __APPLE__
+#define stat64		stat
+#define lstat64		lstat
+#endif
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/include/erofs/dir.h b/include/erofs/dir.h
new file mode 100644
index 0000000..77656ca
--- /dev/null
+++ b/include/erofs/dir.h
@@ -0,0 +1,60 @@
+/* SPDX-License-Identifier: GPL-2.0+ OR Apache-2.0 */
+#ifndef __EROFS_DIR_H
+#define __EROFS_DIR_H
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+#include "internal.h"
+
+#define EROFS_READDIR_VALID_PNID	0x0001
+#define EROFS_READDIR_DOTDOT_FOUND	0x0002
+#define EROFS_READDIR_DOT_FOUND		0x0004
+
+#define EROFS_READDIR_ALL_SPECIAL_FOUND	\
+	(EROFS_READDIR_DOTDOT_FOUND | EROFS_READDIR_DOT_FOUND)
+
+struct erofs_dir_context;
+
+/* callback function for iterating over inodes of EROFS */
+typedef int (*erofs_readdir_cb)(struct erofs_dir_context *);
+
+/*
+ * Callers could use a wrapper to contain extra information.
+ *
+ * Note that callback can reuse `struct erofs_dir_context' with care
+ * to avoid stack overflow due to deep recursion:
+ *  - if fsck is true, |pnid|, |flags|, (optional)|cb| SHOULD be saved
+ *    to ensure the original state;
+ *  - if fsck is false, EROFS_READDIR_VALID_PNID SHOULD NOT be
+ *    set if |pnid| is inaccurate.
+ *
+ * Another way is to allocate a `struct erofs_dir_context' wraper
+ * with `struct inode' on heap, and chain them together for
+ * multi-level traversal to completely avoid recursion.
+ *
+ * |dname| may be WITHOUT the trailing '\0' and it's ONLY valid in
+ * the callback context. |de_namelen| is the exact dirent name length.
+ */
+struct erofs_dir_context {
+	struct erofs_inode *dir;
+	erofs_readdir_cb cb;
+	erofs_nid_t pnid;		/* optional */
+
+	/* [OUT] the dirent which is under processing */
+	const char *dname;		/* please see the comment above */
+	erofs_nid_t de_nid;
+	u8 de_namelen, de_ftype, flags;
+	bool dot_dotdot;
+};
+
+/* Iterate over inodes that are in directory */
+int erofs_iterate_dir(struct erofs_dir_context *ctx, bool fsck);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/include/erofs/err.h b/include/erofs/err.h
index da3b681..18f152a 100644
--- a/include/erofs/err.h
+++ b/include/erofs/err.h
@@ -1,7 +1,5 @@
 /* SPDX-License-Identifier: GPL-2.0+ */
 /*
- * erofs-utils/include/erofs/err.h
- *
  * Copyright (C) 2018 HUAWEI, Inc.
  *             http://www.huawei.com/
  * Created by Li Guifu <bluce.liguifu@huawei.com>
@@ -9,6 +7,11 @@
 #ifndef __EROFS_ERR_H
 #define __EROFS_ERR_H
 
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
 #include <errno.h>
 
 #define MAX_ERRNO (4095)
@@ -30,5 +33,8 @@
 	return (long) ptr;
 }
 
+#ifdef __cplusplus
+}
 #endif
 
+#endif
diff --git a/include/erofs/exclude.h b/include/erofs/exclude.h
index 88c55d7..599f018 100644
--- a/include/erofs/exclude.h
+++ b/include/erofs/exclude.h
@@ -1,12 +1,15 @@
 /* SPDX-License-Identifier: GPL-2.0+ */
 /*
- * erofs-utils/include/erofs/exclude.h
- *
  * Created by Li Guifu <bluce.lee@aliyun.com>
  */
 #ifndef __EROFS_EXCLUDE_H
 #define __EROFS_EXCLUDE_H
 
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
 #include <sys/types.h>
 #include <regex.h>
 
@@ -23,5 +26,9 @@
 int erofs_parse_exclude_path(const char *args, bool is_regex);
 struct erofs_exclude_rule *erofs_is_exclude_path(const char *dir,
 						 const char *name);
+
+#ifdef __cplusplus
+}
 #endif
 
+#endif
diff --git a/include/erofs/flex-array.h b/include/erofs/flex-array.h
new file mode 100644
index 0000000..9b1642f
--- /dev/null
+++ b/include/erofs/flex-array.h
@@ -0,0 +1,156 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef __EROFS_FLEX_ARRAY_H
+#define __EROFS_FLEX_ARRAY_H
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <limits.h>
+#include <stdint.h>
+
+#include "defs.h"
+#include "print.h"
+
+/*
+ * flex-array.h
+ *
+ * Some notes to make sense of the code.
+ *
+ * Flex-arrays:
+ *   - Flex-arrays became standard in C99 and are defined by "array[]" (at the
+ *     end of a struct)
+ *   - Pre-C99 flex-arrays can be accomplished by "array[1]"
+ *   - There is a GNU extension where they are defined using "array[0]"
+ *     Allegedly there is/was a bug in gcc whereby foo[1] generated incorrect
+ *     code, so it's safest to use [0] (https://lkml.org/lkml/2015/2/18/407).
+ *
+ * For C89 and C90, __STDC__ is 1
+ * For later standards, __STDC_VERSION__ is defined according to the standard.
+ * For example: 199901L or 201112L
+ *
+ * Whilst we're on the subject, in version 5 of gcc, the default std was
+ * changed from gnu89 to gnu11. In jgmenu, CFLAGS therefore contains -std=gnu89
+ * You can check your default gcc std by doing:
+ * gcc -dM -E - </dev/null | grep '__STDC_VERSION__\|__STDC__'
+ *
+ * The code below is copied from git's git-compat-util.h in support of
+ * hashmap.c
+ */
+
+#ifndef FLEX_ARRAY
+#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) && \
+	(!defined(__SUNPRO_C) || (__SUNPRO_C > 0x580))
+# define FLEX_ARRAY /* empty */
+#elif defined(__GNUC__)
+# if (__GNUC__ >= 3)
+#  define FLEX_ARRAY /* empty */
+# else
+#  define FLEX_ARRAY 0 /* older GNU extension */
+# endif
+#endif
+
+/* Otherwise, default to safer but a bit wasteful traditional style */
+#ifndef FLEX_ARRAY
+# define FLEX_ARRAY 1
+#endif
+#endif
+
+#define bitsizeof(x) (CHAR_BIT * sizeof(x))
+
+#define maximum_signed_value_of_type(a) \
+	(INTMAX_MAX >> (bitsizeof(intmax_t) - bitsizeof(a)))
+
+#define maximum_unsigned_value_of_type(a) \
+	(UINTMAX_MAX >> (bitsizeof(uintmax_t) - bitsizeof(a)))
+
+/*
+ * Signed integer overflow is undefined in C, so here's a helper macro
+ * to detect if the sum of two integers will overflow.
+ * Requires: a >= 0, typeof(a) equals typeof(b)
+ */
+#define signed_add_overflows(a, b) \
+	((b) > maximum_signed_value_of_type(a) - (a))
+
+#define unsigned_add_overflows(a, b) \
+	((b) > maximum_unsigned_value_of_type(a) - (a))
+
+static inline size_t st_add(size_t a, size_t b)
+{
+	if (unsigned_add_overflows(a, b)) {
+		erofs_err("size_t overflow: %llu + %llu", a | 0ULL, b | 0ULL);
+		BUG_ON(1);
+		return -1;
+	}
+	return a + b;
+}
+
+#define st_add3(a, b, c) st_add(st_add((a), (b)), (c))
+#define st_add4(a, b, c, d) st_add(st_add3((a), (b), (c)), (d))
+
+/*
+ * These functions help you allocate structs with flex arrays, and copy
+ * the data directly into the array. For example, if you had:
+ *
+ *   struct foo {
+ *     int bar;
+ *     char name[FLEX_ARRAY];
+ *   };
+ *
+ * you can do:
+ *
+ *   struct foo *f;
+ *   FLEX_ALLOC_MEM(f, name, src, len);
+ *
+ * to allocate a "foo" with the contents of "src" in the "name" field.
+ * The resulting struct is automatically zero'd, and the flex-array field
+ * is NUL-terminated (whether the incoming src buffer was or not).
+ *
+ * The FLEXPTR_* variants operate on structs that don't use flex-arrays,
+ * but do want to store a pointer to some extra data in the same allocated
+ * block. For example, if you have:
+ *
+ *   struct foo {
+ *     char *name;
+ *     int bar;
+ *   };
+ *
+ * you can do:
+ *
+ *   struct foo *f;
+ *   FLEXPTR_ALLOC_STR(f, name, src);
+ *
+ * and "name" will point to a block of memory after the struct, which will be
+ * freed along with the struct (but the pointer can be repointed anywhere).
+ *
+ * The *_STR variants accept a string parameter rather than a ptr/len
+ * combination.
+ *
+ * Note that these macros will evaluate the first parameter multiple
+ * times, and it must be assignable as an lvalue.
+ */
+#define FLEX_ALLOC_MEM(x, flexname, buf, len) do { \
+	size_t flex_array_len_ = (len); \
+	(x) = calloc(1, st_add3(sizeof(*(x)), flex_array_len_, 1)); \
+	BUG_ON(!(x)); \
+	memcpy((void *)(x)->flexname, (buf), flex_array_len_); \
+} while (0)
+#define FLEXPTR_ALLOC_MEM(x, ptrname, buf, len) do { \
+	size_t flex_array_len_ = (len); \
+	(x) = xcalloc(1, st_add3(sizeof(*(x)), flex_array_len_, 1)); \
+	memcpy((x) + 1, (buf), flex_array_len_); \
+	(x)->ptrname = (void *)((x) + 1); \
+} while (0)
+#define FLEX_ALLOC_STR(x, flexname, str) \
+	FLEX_ALLOC_MEM((x), flexname, (str), strlen(str))
+#define FLEXPTR_ALLOC_STR(x, ptrname, str) \
+	FLEXPTR_ALLOC_MEM((x), ptrname, (str), strlen(str))
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/include/erofs/hashmap.h b/include/erofs/hashmap.h
new file mode 100644
index 0000000..3d38578
--- /dev/null
+++ b/include/erofs/hashmap.h
@@ -0,0 +1,112 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef __EROFS_HASHMAP_H
+#define __EROFS_HASHMAP_H
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+/* Copied from https://github.com/git/git.git */
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "flex-array.h"
+
+/*
+ * Generic implementation of hash-based key-value mappings.
+ * See Documentation/technical/api-hashmap.txt.
+ */
+
+/* FNV-1 functions */
+unsigned int strhash(const char *str);
+unsigned int strihash(const char *str);
+unsigned int memhash(const void *buf, size_t len);
+unsigned int memihash(const void *buf, size_t len);
+
+static inline unsigned int sha1hash(const unsigned char *sha1)
+{
+	/*
+	 * Equivalent to 'return *(unsigned int *)sha1;', but safe on
+	 * platforms that don't support unaligned reads.
+	 */
+	unsigned int hash;
+
+	memcpy(&hash, sha1, sizeof(hash));
+	return hash;
+}
+
+/* data structures */
+struct hashmap_entry {
+	struct hashmap_entry *next;
+	unsigned int hash;
+};
+
+typedef int (*hashmap_cmp_fn)(const void *entry, const void *entry_or_key,
+		const void *keydata);
+
+struct hashmap {
+	struct hashmap_entry **table;
+	hashmap_cmp_fn cmpfn;
+	unsigned int size, tablesize, grow_at, shrink_at;
+};
+
+struct hashmap_iter {
+	struct hashmap *map;
+	struct hashmap_entry *next;
+	unsigned int tablepos;
+};
+
+/* hashmap functions */
+void hashmap_init(struct hashmap *map, hashmap_cmp_fn equals_function,
+		  size_t initial_size);
+void hashmap_free(struct hashmap *map, int free_entries);
+
+/* hashmap_entry functions */
+static inline void hashmap_entry_init(void *entry, unsigned int hash)
+{
+	struct hashmap_entry *e = entry;
+
+	e->hash = hash;
+	e->next = NULL;
+}
+
+void *hashmap_get(const struct hashmap *map, const void *key, const void *keydata);
+void *hashmap_get_next(const struct hashmap *map, const void *entry);
+void hashmap_add(struct hashmap *map, void *entry);
+void *hashmap_put(struct hashmap *map, void *entry);
+void *hashmap_remove(struct hashmap *map, const void *key, const void *keydata);
+
+static inline void *hashmap_get_from_hash(const struct hashmap *map,
+					  unsigned int hash,
+					  const void *keydata)
+{
+	struct hashmap_entry key;
+
+	hashmap_entry_init(&key, hash);
+	return hashmap_get(map, &key, keydata);
+}
+
+/* hashmap_iter functions */
+void hashmap_iter_init(struct hashmap *map, struct hashmap_iter *iter);
+void *hashmap_iter_next(struct hashmap_iter *iter);
+static inline void *hashmap_iter_first(struct hashmap *map,
+				       struct hashmap_iter *iter)
+{
+	hashmap_iter_init(map, iter);
+	return hashmap_iter_next(iter);
+}
+
+/* string interning */
+const void *memintern(const void *data, size_t len);
+static inline const char *strintern(const char *string)
+{
+	return memintern(string, strlen(string));
+}
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/include/erofs/hashtable.h b/include/erofs/hashtable.h
index 7e47189..f4eca8d 100644
--- a/include/erofs/hashtable.h
+++ b/include/erofs/hashtable.h
@@ -1,12 +1,15 @@
 /* SPDX-License-Identifier: GPL-2.0 */
 /*
- * erofs-utils/include/erofs/hashtable.h
- *
  * Original code taken from 'linux/include/linux/hash{,table}.h'
  */
 #ifndef __EROFS_HASHTABLE_H
 #define __EROFS_HASHTABLE_H
 
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
 /*
  * Fast hashing routine for ints,  longs and pointers.
  * (C) 2002 Nadia Yvette Chambers, IBM
@@ -253,7 +256,7 @@
 	return __hash_32(val) >> (32 - bits);
 }
 
-static __always_inline u32 hash_64(u64 val, unsigned int bits)
+static inline u32 hash_64(u64 val, unsigned int bits)
 {
 #if BITS_PER_LONG == 64
 	/* 64x64-bit multiply is efficient on all 64-bit processors */
@@ -264,83 +267,6 @@
 #endif
 }
 
-/**
- * ilog2 - log of base 2 of 32-bit or a 64-bit unsigned value
- * @n - parameter
- *
- * constant-capable log of base 2 calculation
- * - this can be used to initialise global variables from constant data, hence
- *   the massive ternary operator construction
- *
- * selects the appropriately-sized optimised version depending on sizeof(n)
- */
-#define ilog2(n)				\
-(								\
-	(n) & (1ULL << 63) ? 63 :	\
-	(n) & (1ULL << 62) ? 62 :	\
-	(n) & (1ULL << 61) ? 61 :	\
-	(n) & (1ULL << 60) ? 60 :	\
-	(n) & (1ULL << 59) ? 59 :	\
-	(n) & (1ULL << 58) ? 58 :	\
-	(n) & (1ULL << 57) ? 57 :	\
-	(n) & (1ULL << 56) ? 56 :	\
-	(n) & (1ULL << 55) ? 55 :	\
-	(n) & (1ULL << 54) ? 54 :	\
-	(n) & (1ULL << 53) ? 53 :	\
-	(n) & (1ULL << 52) ? 52 :	\
-	(n) & (1ULL << 51) ? 51 :	\
-	(n) & (1ULL << 50) ? 50 :	\
-	(n) & (1ULL << 49) ? 49 :	\
-	(n) & (1ULL << 48) ? 48 :	\
-	(n) & (1ULL << 47) ? 47 :	\
-	(n) & (1ULL << 46) ? 46 :	\
-	(n) & (1ULL << 45) ? 45 :	\
-	(n) & (1ULL << 44) ? 44 :	\
-	(n) & (1ULL << 43) ? 43 :	\
-	(n) & (1ULL << 42) ? 42 :	\
-	(n) & (1ULL << 41) ? 41 :	\
-	(n) & (1ULL << 40) ? 40 :	\
-	(n) & (1ULL << 39) ? 39 :	\
-	(n) & (1ULL << 38) ? 38 :	\
-	(n) & (1ULL << 37) ? 37 :	\
-	(n) & (1ULL << 36) ? 36 :	\
-	(n) & (1ULL << 35) ? 35 :	\
-	(n) & (1ULL << 34) ? 34 :	\
-	(n) & (1ULL << 33) ? 33 :	\
-	(n) & (1ULL << 32) ? 32 :	\
-	(n) & (1ULL << 31) ? 31 :	\
-	(n) & (1ULL << 30) ? 30 :	\
-	(n) & (1ULL << 29) ? 29 :	\
-	(n) & (1ULL << 28) ? 28 :	\
-	(n) & (1ULL << 27) ? 27 :	\
-	(n) & (1ULL << 26) ? 26 :	\
-	(n) & (1ULL << 25) ? 25 :	\
-	(n) & (1ULL << 24) ? 24 :	\
-	(n) & (1ULL << 23) ? 23 :	\
-	(n) & (1ULL << 22) ? 22 :	\
-	(n) & (1ULL << 21) ? 21 :	\
-	(n) & (1ULL << 20) ? 20 :	\
-	(n) & (1ULL << 19) ? 19 :	\
-	(n) & (1ULL << 18) ? 18 :	\
-	(n) & (1ULL << 17) ? 17 :	\
-	(n) & (1ULL << 16) ? 16 :	\
-	(n) & (1ULL << 15) ? 15 :	\
-	(n) & (1ULL << 14) ? 14 :	\
-	(n) & (1ULL << 13) ? 13 :	\
-	(n) & (1ULL << 12) ? 12 :	\
-	(n) & (1ULL << 11) ? 11 :	\
-	(n) & (1ULL << 10) ? 10 :	\
-	(n) & (1ULL <<  9) ?  9 :	\
-	(n) & (1ULL <<  8) ?  8 :	\
-	(n) & (1ULL <<  7) ?  7 :	\
-	(n) & (1ULL <<  6) ?  6 :	\
-	(n) & (1ULL <<  5) ?  5 :	\
-	(n) & (1ULL <<  4) ?  4 :	\
-	(n) & (1ULL <<  3) ?  3 :	\
-	(n) & (1ULL <<  2) ?  2 :	\
-	(n) & (1ULL <<  1) ?  1 : 0	\
-)
-
 #define DEFINE_HASHTABLE(name, bits)					\
 	struct hlist_head name[1 << (bits)] =				\
 			{ [0 ... ((1 << (bits)) - 1)] = HLIST_HEAD_INIT }
@@ -459,4 +385,8 @@
 #define hash_for_each_possible(name, obj, member, key)			\
 	hlist_for_each_entry(obj, &name[hash_min(key, HASH_BITS(name))], member)
 
+#ifdef __cplusplus
+}
+#endif
+
 #endif
diff --git a/include/erofs/inode.h b/include/erofs/inode.h
index 5a7f5f1..e23d65f 100644
--- a/include/erofs/inode.h
+++ b/include/erofs/inode.h
@@ -1,7 +1,5 @@
 /* SPDX-License-Identifier: GPL-2.0+ */
 /*
- * erofs-utils/include/erofs/inode.h
- *
  * Copyright (C) 2018-2019 HUAWEI, Inc.
  *             http://www.huawei.com/
  * Created by Li Guifu <bluce.liguifu@huawei.com>
@@ -10,12 +8,22 @@
 #ifndef __EROFS_INODE_H
 #define __EROFS_INODE_H
 
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
 #include "erofs/internal.h"
 
+unsigned char erofs_mode_to_ftype(umode_t mode);
 void erofs_inode_manager_init(void);
 unsigned int erofs_iput(struct erofs_inode *inode);
 erofs_nid_t erofs_lookupnid(struct erofs_inode *inode);
 struct erofs_inode *erofs_mkfs_build_tree_from_path(struct erofs_inode *parent,
 						    const char *path);
 
+#ifdef __cplusplus
+}
+#endif
+
 #endif
diff --git a/include/erofs/internal.h b/include/erofs/internal.h
index ac5b270..56627e9 100644
--- a/include/erofs/internal.h
+++ b/include/erofs/internal.h
@@ -1,7 +1,5 @@
 /* SPDX-License-Identifier: GPL-2.0+ */
 /*
- * erofs-utils/include/erofs/internal.h
- *
  * Copyright (C) 2019 HUAWEI, Inc.
  *             http://www.huawei.com/
  * Created by Gao Xiang <gaoxiang25@huawei.com>
@@ -9,6 +7,11 @@
 #ifndef __EROFS_INTERNAL_H
 #define __EROFS_INTERNAL_H
 
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
 #include "list.h"
 #include "err.h"
 
@@ -36,7 +39,9 @@
 #error incompatible PAGE_SIZE is already defined
 #endif
 
+#ifndef PAGE_MASK
 #define PAGE_MASK		(~(PAGE_SIZE-1))
+#endif
 
 #define LOG_BLOCK_SIZE          (12)
 #define EROFS_BLKSIZ            (1U << LOG_BLOCK_SIZE)
@@ -60,8 +65,16 @@
 
 struct erofs_buffer_head;
 
+struct erofs_device_info {
+	u32 blocks;
+	u32 mapped_blkaddr;
+};
+
 struct erofs_sb_info {
-	u64 blocks;
+	struct erofs_device_info *devs;
+
+	u64 total_blocks;
+	u64 primarydevice_blocks;
 
 	erofs_blk_t meta_blkaddr;
 	erofs_blk_t xattr_blkaddr;
@@ -79,6 +92,16 @@
 	u64 inos;
 
 	u8 uuid[16];
+
+	u16 available_compr_algs;
+	u16 lz4_max_distance;
+
+	u32 checksum;
+	u16 extra_devices;
+	union {
+		u16 devt_slotoff;		/* used for mkfs */
+		u16 device_id_mask;		/* used for others */
+	};
 };
 
 /* global sbi */
@@ -104,6 +127,10 @@
 }
 
 EROFS_FEATURE_FUNCS(lz4_0padding, incompat, INCOMPAT_LZ4_0PADDING)
+EROFS_FEATURE_FUNCS(compr_cfgs, incompat, INCOMPAT_COMPR_CFGS)
+EROFS_FEATURE_FUNCS(big_pcluster, incompat, INCOMPAT_BIG_PCLUSTER)
+EROFS_FEATURE_FUNCS(chunked_file, incompat, INCOMPAT_CHUNKED_FILE)
+EROFS_FEATURE_FUNCS(device_table, incompat, INCOMPAT_DEVICE_TABLE)
 EROFS_FEATURE_FUNCS(sb_chksum, compat, COMPAT_SB_CHKSUM)
 
 #define EROFS_I_EA_INITED	(1 << 0)
@@ -127,14 +154,18 @@
 	u64 i_ino[2];
 	u32 i_uid;
 	u32 i_gid;
-	u64 i_ctime;
-	u32 i_ctime_nsec;
+	u64 i_mtime;
+	u32 i_mtime_nsec;
 	u32 i_nlink;
 
 	union {
 		u32 i_blkaddr;
 		u32 i_blocks;
 		u32 i_rdev;
+		struct {
+			unsigned short	chunkformat;
+			unsigned char	chunkbits;
+		};
 	} u;
 
 	char i_srcpath[PATH_MAX + 1];
@@ -155,11 +186,12 @@
 
 	union {
 		void *compressmeta;
+		void *chunkindexes;
 		struct {
 			uint16_t z_advise;
 			uint8_t  z_algorithmtype[2];
 			uint8_t  z_logical_clusterbits;
-			uint8_t  z_physical_clusterbits[2];
+			uint8_t  z_physical_clusterblks;
 		};
 	};
 #ifdef WITH_ANDROID
@@ -203,6 +235,14 @@
 	};
 };
 
+static inline bool is_dot_dotdot_len(const char *name, unsigned int len)
+{
+	if (len >= 1 && name[0] != '.')
+		return false;
+
+	return len == 1 || (len == 2 && name[1] == '.');
+}
+
 static inline bool is_dot_dotdot(const char *name)
 {
 	if (name[0] != '.')
@@ -225,7 +265,7 @@
 enum {
 	BH_Meta,
 	BH_Mapped,
-	BH_Zipped,
+	BH_Encoded,
 	BH_FullMapped,
 };
 
@@ -233,8 +273,8 @@
 #define EROFS_MAP_MAPPED	(1 << BH_Mapped)
 /* Located in metadata (could be copied from bd_inode) */
 #define EROFS_MAP_META		(1 << BH_Meta)
-/* The extent has been compressed */
-#define EROFS_MAP_ZIPPED	(1 << BH_Zipped)
+/* The extent is encoded */
+#define EROFS_MAP_ENCODED	(1 << BH_Encoded)
 /* The length of extent is full */
 #define EROFS_MAP_FULL_MAPPED	(1 << BH_FullMapped)
 
@@ -244,25 +284,68 @@
 	erofs_off_t m_pa, m_la;
 	u64 m_plen, m_llen;
 
+	unsigned short m_deviceid;
+	char m_algorithmformat;
 	unsigned int m_flags;
 	erofs_blk_t index;
 };
 
+/*
+ * Used to get the exact decompressed length, e.g. fiemap (consider lookback
+ * approach instead if possible since it's more metadata lightweight.)
+ */
+#define EROFS_GET_BLOCKS_FIEMAP	0x0002
+
+enum {
+	Z_EROFS_COMPRESSION_SHIFTED = Z_EROFS_COMPRESSION_MAX,
+	Z_EROFS_COMPRESSION_RUNTIME_MAX
+};
+
+struct erofs_map_dev {
+	erofs_off_t m_pa;
+	unsigned int m_deviceid;
+};
+
 /* super.c */
 int erofs_read_superblock(void);
 
 /* namei.c */
+int erofs_read_inode_from_disk(struct erofs_inode *vi);
 int erofs_ilookup(const char *path, struct erofs_inode *vi);
+int erofs_read_inode_from_disk(struct erofs_inode *vi);
 
 /* data.c */
 int erofs_pread(struct erofs_inode *inode, char *buf,
 		erofs_off_t count, erofs_off_t offset);
+int erofs_map_blocks(struct erofs_inode *inode,
+		struct erofs_map_blocks *map, int flags);
+int erofs_map_dev(struct erofs_sb_info *sbi, struct erofs_map_dev *map);
 /* zmap.c */
 int z_erofs_fill_inode(struct erofs_inode *vi);
 int z_erofs_map_blocks_iter(struct erofs_inode *vi,
-			    struct erofs_map_blocks *map);
+			    struct erofs_map_blocks *map, int flags);
 
+#ifdef EUCLEAN
 #define EFSCORRUPTED	EUCLEAN		/* Filesystem is corrupted */
-
+#else
+#define EFSCORRUPTED	EIO
 #endif
 
+#define CRC32C_POLY_LE	0x82F63B78
+static inline u32 erofs_crc32c(u32 crc, const u8 *in, size_t len)
+{
+	int i;
+
+	while (len--) {
+		crc ^= *in++;
+		for (i = 0; i < 8; i++)
+			crc = (crc >> 1) ^ ((crc & 1) ? CRC32C_POLY_LE : 0);
+	}
+	return crc;
+}
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/include/erofs/io.h b/include/erofs/io.h
index 5574245..6f51e06 100644
--- a/include/erofs/io.h
+++ b/include/erofs/io.h
@@ -1,7 +1,5 @@
 /* SPDX-License-Identifier: GPL-2.0+ */
 /*
- * erofs-utils/include/erofs/io.h
- *
  * Copyright (C) 2018-2019 HUAWEI, Inc.
  *             http://www.huawei.com/
  * Created by Li Guifu <bluce.liguifu@huawei.com>
@@ -9,6 +7,14 @@
 #ifndef __EROFS_IO_H
 #define __EROFS_IO_H
 
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#endif
 #include <unistd.h>
 #include "internal.h"
 
@@ -16,16 +22,24 @@
 #define O_BINARY	0
 #endif
 
+void blob_closeall(void);
+int blob_open_ro(const char *dev);
 int dev_open(const char *devname);
 int dev_open_ro(const char *dev);
 void dev_close(void);
 int dev_write(const void *buf, u64 offset, size_t len);
-int dev_read(void *buf, u64 offset, size_t len);
+int dev_read(int device_id, void *buf, u64 offset, size_t len);
 int dev_fillzero(u64 offset, size_t len, bool padding);
 int dev_fsync(void);
 int dev_resize(erofs_blk_t nblocks);
 u64 dev_length(void);
 
+extern int erofs_devfd;
+
+ssize_t erofs_copy_file_range(int fd_in, erofs_off_t *off_in,
+			      int fd_out, erofs_off_t *off_out,
+			      size_t length);
+
 static inline int blk_write(const void *buf, erofs_blk_t blkaddr,
 			    u32 nblocks)
 {
@@ -33,12 +47,15 @@
 			 blknr_to_addr(nblocks));
 }
 
-static inline int blk_read(void *buf, erofs_blk_t start,
-			    u32 nblocks)
+static inline int blk_read(int device_id, void *buf,
+			   erofs_blk_t start, u32 nblocks)
 {
-	return dev_read(buf, blknr_to_addr(start),
+	return dev_read(device_id, buf, blknr_to_addr(start),
 			 blknr_to_addr(nblocks));
 }
 
+#ifdef __cplusplus
+}
 #endif
 
+#endif // EROFS_IO_H_
diff --git a/include/erofs/list.h b/include/erofs/list.h
index 3572726..fd5358d 100644
--- a/include/erofs/list.h
+++ b/include/erofs/list.h
@@ -1,7 +1,5 @@
 /* SPDX-License-Identifier: GPL-2.0+ */
 /*
- * erofs-utils/include/erofs/list.h
- *
  * Copyright (C) 2018 HUAWEI, Inc.
  *             http://www.huawei.com/
  * Created by Li Guifu <bluce.liguifu@huawei.com>
@@ -9,6 +7,11 @@
 #ifndef __EROFS_LIST_HEAD_H
 #define __EROFS_LIST_HEAD_H
 
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
 #include "defs.h"
 
 struct list_head {
@@ -107,4 +110,9 @@
 	     &pos->member != (head);                                           \
 	     pos = n, n = list_next_entry(n, member))
 
+
+#ifdef __cplusplus
+}
+#endif
+
 #endif
diff --git a/include/erofs/print.h b/include/erofs/print.h
index 6b79074..2213d1d 100644
--- a/include/erofs/print.h
+++ b/include/erofs/print.h
@@ -1,7 +1,5 @@
 /* SPDX-License-Identifier: GPL-2.0+ */
 /*
- * erofs-utils/include/erofs/print.h
- *
  * Copyright (C) 2018-2019 HUAWEI, Inc.
  *             http://www.huawei.com/
  * Created by Li Guifu <bluce.liguifu@huawei.com>
@@ -9,6 +7,11 @@
 #ifndef __EROFS_PRINT_H
 #define __EROFS_PRINT_H
 
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
 #include "config.h"
 #include <stdio.h>
 
@@ -21,56 +24,61 @@
 	EROFS_MSG_MAX = 9
 };
 
+#ifndef EROFS_MODNAME
+#define EROFS_MODNAME	"erofs"
+#endif
 #define FUNC_LINE_FMT "%s() Line[%d] "
 
+#ifdef NDEBUG
 #ifndef pr_fmt
-#define pr_fmt(fmt) "EROFS: " FUNC_LINE_FMT fmt "\n"
+#define pr_fmt(fmt)	EROFS_MODNAME ": " fmt "\n"
+#endif
+#define PR_FMT_FUNC_LINE(fmt)	pr_fmt(fmt)
+#else
+#ifndef pr_fmt
+#define pr_fmt(fmt)	EROFS_MODNAME ": " FUNC_LINE_FMT fmt "\n"
+#endif
+#define PR_FMT_FUNC_LINE(fmt)	pr_fmt(fmt), __func__, __LINE__
 #endif
 
-#define erofs_dbg(fmt, ...) do {				\
-	if (cfg.c_dbg_lvl >= EROFS_DBG) {			\
-		fprintf(stdout,					\
-			pr_fmt(fmt),				\
-			__func__,				\
-			__LINE__,				\
-			##__VA_ARGS__);				\
-	}							\
+#define erofs_dbg(fmt, ...) do {			\
+	if (cfg.c_dbg_lvl >= EROFS_DBG) {		\
+		fprintf(stdout,				\
+			"<D> " PR_FMT_FUNC_LINE(fmt),	\
+			##__VA_ARGS__);			\
+	}						\
 } while (0)
 
-#define erofs_info(fmt, ...) do {				\
-	if (cfg.c_dbg_lvl >= EROFS_INFO) {			\
-		fprintf(stdout,					\
-			pr_fmt(fmt),				\
-			__func__,				\
-			__LINE__,				\
-			##__VA_ARGS__);				\
-		fflush(stdout);					\
-	}							\
+#define erofs_info(fmt, ...) do {			\
+	if (cfg.c_dbg_lvl >= EROFS_INFO) {		\
+		fprintf(stdout,				\
+			"<I> " PR_FMT_FUNC_LINE(fmt),	\
+			##__VA_ARGS__);			\
+		fflush(stdout);				\
+	}						\
 } while (0)
 
-#define erofs_warn(fmt, ...) do {				\
-	if (cfg.c_dbg_lvl >= EROFS_WARN) {			\
-		fprintf(stdout,					\
-			pr_fmt(fmt),				\
-			__func__,				\
-			__LINE__,				\
-			##__VA_ARGS__);				\
-		fflush(stdout);					\
-	}							\
+#define erofs_warn(fmt, ...) do {			\
+	if (cfg.c_dbg_lvl >= EROFS_WARN) {		\
+		fprintf(stdout,				\
+			"<W> " PR_FMT_FUNC_LINE(fmt),	\
+			##__VA_ARGS__);			\
+		fflush(stdout);				\
+	}						\
 } while (0)
 
-#define erofs_err(fmt, ...) do {				\
-	if (cfg.c_dbg_lvl >= EROFS_ERR) {			\
-		fprintf(stderr,					\
-			"Err: " pr_fmt(fmt),			\
-			__func__,				\
-			__LINE__,				\
-			##__VA_ARGS__);				\
-	}							\
+#define erofs_err(fmt, ...) do {			\
+	if (cfg.c_dbg_lvl >= EROFS_ERR) {		\
+		fprintf(stderr,				\
+			"<E> " PR_FMT_FUNC_LINE(fmt),	\
+			##__VA_ARGS__);			\
+	}						\
 } while (0)
 
 #define erofs_dump(fmt, ...) fprintf(stderr, fmt, ##__VA_ARGS__)
 
-
+#ifdef __cplusplus
+}
 #endif
 
+#endif
diff --git a/include/erofs/trace.h b/include/erofs/trace.h
index 5a12da7..893e16c 100644
--- a/include/erofs/trace.h
+++ b/include/erofs/trace.h
@@ -1,14 +1,20 @@
 /* SPDX-License-Identifier: GPL-2.0+ */
 /*
- * erofs-utils/include/erofs/trace.h
- *
  * Copyright (C) 2020 Gao Xiang <hsiangkao@aol.com>
  */
 #ifndef __EROFS_TRACE_H
 #define __EROFS_TRACE_H
 
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
 #define trace_erofs_map_blocks_flatmode_enter(inode, map, flags) ((void)0)
 #define trace_erofs_map_blocks_flatmode_exit(inode, map, flags, ret) ((void)0)
 
+#ifdef __cplusplus
+}
 #endif
 
+#endif
diff --git a/include/erofs/xattr.h b/include/erofs/xattr.h
index 197fe25..8e68812 100644
--- a/include/erofs/xattr.h
+++ b/include/erofs/xattr.h
@@ -1,7 +1,5 @@
-// SPDX-License-Identifier: GPL-2.0+
+/* SPDX-License-Identifier: GPL-2.0+ */
 /*
- * erofs-utils/include/erofs/xattr.h
- *
  * Originally contributed by an anonymous person,
  * heavily changed by Li Guifu <blucerlee@gmail.com>
  *                and Gao Xiang <xiang@kernel.org>
@@ -9,6 +7,11 @@
 #ifndef __EROFS_XATTR_H
 #define __EROFS_XATTR_H
 
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
 #include "internal.h"
 
 #define EROFS_INODE_XATTR_ICOUNT(_size)	({\
@@ -46,4 +49,8 @@
 char *erofs_export_xattr_ibody(struct list_head *ixattrs, unsigned int size);
 int erofs_build_shared_xattrs_from_path(const char *path);
 
+#ifdef __cplusplus
+}
+#endif
+
 #endif
diff --git a/include/erofs_fs.h b/include/erofs_fs.h
index a69f179..7956a62 100644
--- a/include/erofs_fs.h
+++ b/include/erofs_fs.h
@@ -1,11 +1,10 @@
 /* SPDX-License-Identifier: GPL-2.0-only OR Apache-2.0 */
 /*
- * erofs-utils/include/erofs_fs.h
  * EROFS (Enhanced ROM File System) on-disk format definition
  *
  * Copyright (C) 2017-2018 HUAWEI, Inc.
  *             http://www.huawei.com/
- * Created by Gao Xiang <gaoxiang25@huawei.com>
+ * Copyright (C) 2021, Alibaba Cloud
  */
 #ifndef __EROFS_FS_H
 #define __EROFS_FS_H
@@ -14,21 +13,44 @@
 #define EROFS_SUPER_OFFSET      1024
 
 #define EROFS_FEATURE_COMPAT_SB_CHKSUM		0x00000001
+#define EROFS_FEATURE_COMPAT_MTIME		0x00000002
 
 /*
  * Any bits that aren't in EROFS_ALL_FEATURE_INCOMPAT should
  * be incompatible with this kernel version.
  */
 #define EROFS_FEATURE_INCOMPAT_LZ4_0PADDING	0x00000001
-#define EROFS_ALL_FEATURE_INCOMPAT		EROFS_FEATURE_INCOMPAT_LZ4_0PADDING
+#define EROFS_FEATURE_INCOMPAT_COMPR_CFGS	0x00000002
+#define EROFS_FEATURE_INCOMPAT_BIG_PCLUSTER	0x00000002
+#define EROFS_FEATURE_INCOMPAT_CHUNKED_FILE	0x00000004
+#define EROFS_FEATURE_INCOMPAT_DEVICE_TABLE	0x00000008
+#define EROFS_ALL_FEATURE_INCOMPAT		\
+	(EROFS_FEATURE_INCOMPAT_LZ4_0PADDING | \
+	 EROFS_FEATURE_INCOMPAT_COMPR_CFGS | \
+	 EROFS_FEATURE_INCOMPAT_BIG_PCLUSTER | \
+	 EROFS_FEATURE_INCOMPAT_CHUNKED_FILE | \
+	 EROFS_FEATURE_INCOMPAT_DEVICE_TABLE)
 
-/* 128-byte erofs on-disk super block */
+#define EROFS_SB_EXTSLOT_SIZE	16
+
+struct erofs_deviceslot {
+	union {
+		u8 uuid[16];		/* used for device manager later */
+		u8 userdata[64];	/* digest(sha256), etc. */
+	} u;
+	__le32 blocks;			/* total fs blocks of this device */
+	__le32 mapped_blkaddr;		/* map starting at mapped_blkaddr */
+	u8 reserved[56];
+};
+#define EROFS_DEVT_SLOT_SIZE	sizeof(struct erofs_deviceslot)
+
+/* erofs on-disk super block (currently 128 bytes) */
 struct erofs_super_block {
 	__le32 magic;           /* file system magic number */
 	__le32 checksum;        /* crc32c(super_block) */
 	__le32 feature_compat;
 	__u8 blkszbits;         /* support block_size == PAGE_SIZE only */
-	__u8 reserved;
+	__u8 sb_extslots;	/* superblock size = 128 + sb_extslots * 16 */
 
 	__le16 root_nid;	/* nid of root directory */
 	__le64 inos;            /* total valid ino # (== f_files - f_favail) */
@@ -41,7 +63,15 @@
 	__u8 uuid[16];          /* 128-bit uuid for volume */
 	__u8 volume_name[16];   /* volume name */
 	__le32 feature_incompat;
-	__u8 reserved2[44];
+	union {
+		/* bitmap for available compression algorithms */
+		__le16 available_compr_algs;
+		/* customized sliding window size instead of 64k by default */
+		__le16 lz4_max_distance;
+	} __packed u1;
+	__le16 extra_devices;	/* # of devices besides the primary device */
+	__le16 devt_slotoff;	/* startoff = devt_slotoff * devt_slotsize */
+	__u8 reserved2[38];
 };
 
 /*
@@ -54,13 +84,16 @@
  * inode, [xattrs], last_inline_data, ... | ... | no-holed data
  * 3 - inode compression D:
  * inode, [xattrs], map_header, extents ... | ...
- * 4~7 - reserved
+ * 4 - inode chunk-based E:
+ * inode, [xattrs], chunk indexes ... | ...
+ * 5~7 - reserved
  */
 enum {
 	EROFS_INODE_FLAT_PLAIN			= 0,
 	EROFS_INODE_FLAT_COMPRESSION_LEGACY	= 1,
 	EROFS_INODE_FLAT_INLINE			= 2,
 	EROFS_INODE_FLAT_COMPRESSION		= 3,
+	EROFS_INODE_CHUNK_BASED			= 4,
 	EROFS_INODE_DATALAYOUT_MAX
 };
 
@@ -77,6 +110,22 @@
 #define EROFS_I_VERSION_BIT             0
 #define EROFS_I_DATALAYOUT_BIT          1
 
+#define EROFS_I_ALL	\
+	((1 << (EROFS_I_DATALAYOUT_BIT + EROFS_I_DATALAYOUT_BITS)) - 1)
+
+/* indicate chunk blkbits, thus 'chunksize = blocksize << chunk blkbits' */
+#define EROFS_CHUNK_FORMAT_BLKBITS_MASK		0x001F
+/* with chunk indexes or just a 4-byte blkaddr array */
+#define EROFS_CHUNK_FORMAT_INDEXES		0x0020
+
+#define EROFS_CHUNK_FORMAT_ALL	\
+	(EROFS_CHUNK_FORMAT_BLKBITS_MASK | EROFS_CHUNK_FORMAT_INDEXES)
+
+struct erofs_inode_chunk_info {
+	__le16 format;		/* chunk blkbits, etc. */
+	__le16 reserved;
+};
+
 /* 32-byte reduced form of an ondisk inode */
 struct erofs_inode_compact {
 	__le16 i_format;	/* inode format hints */
@@ -94,6 +143,9 @@
 
 		/* for device files, used to indicate old/new device # */
 		__le32 rdev;
+
+		/* for chunk-based files, it contains the summary info */
+		struct erofs_inode_chunk_info c;
 	} i_u;
 	__le32 i_ino;           /* only used for 32-bit stat compatibility */
 	__le16 i_uid;
@@ -122,6 +174,9 @@
 
 		/* for device files, used to indicate old/new device # */
 		__le32 rdev;
+
+		/* for chunk-based files, it contains the summary info */
+		struct erofs_inode_chunk_info c;
 	} i_u;
 
 	/* only used for 32-bit stat compatibility */
@@ -129,8 +184,8 @@
 
 	__le32 i_uid;
 	__le32 i_gid;
-	__le64 i_ctime;
-	__le32 i_ctime_nsec;
+	__le64 i_mtime;
+	__le32 i_mtime_nsec;
 	__le32 i_nlink;
 	__u8   i_reserved2[16];
 };
@@ -191,20 +246,55 @@
 				 e->e_name_len + le16_to_cpu(e->e_value_size));
 }
 
+/* represent a zeroed chunk (hole) */
+#define EROFS_NULL_ADDR			-1
+
+/* 4-byte block address array */
+#define EROFS_BLOCK_MAP_ENTRY_SIZE	sizeof(__le32)
+
+/* 8-byte inode chunk indexes */
+struct erofs_inode_chunk_index {
+	__le16 advise;		/* always 0, don't care for now */
+	__le16 device_id;	/* back-end storage id (with bits masked) */
+	__le32 blkaddr;		/* start block address of this inode chunk */
+};
+
+/* maximum supported size of a physical compression cluster */
+#define Z_EROFS_PCLUSTER_MAX_SIZE	(1024 * 1024)
+
 /* available compression algorithm types (for h_algorithmtype) */
 enum {
-	Z_EROFS_COMPRESSION_LZ4	= 0,
+	Z_EROFS_COMPRESSION_LZ4		= 0,
+	Z_EROFS_COMPRESSION_LZMA	= 1,
 	Z_EROFS_COMPRESSION_MAX
 };
+#define Z_EROFS_ALL_COMPR_ALGS		(1 << (Z_EROFS_COMPRESSION_MAX - 1))
+
+/* 14 bytes (+ length field = 16 bytes) */
+struct z_erofs_lz4_cfgs {
+	__le16 max_distance;
+	__le16 max_pclusterblks;
+	u8 reserved[10];
+} __packed;
+
+/* 14 bytes (+ length field = 16 bytes) */
+struct z_erofs_lzma_cfgs {
+	__le32 dict_size;
+	__le16 format;
+	u8 reserved[8];
+} __packed;
+#define Z_EROFS_LZMA_MAX_DICT_SIZE	(8 * Z_EROFS_PCLUSTER_MAX_SIZE)
 
 /*
  * bit 0 : COMPACTED_2B indexes (0 - off; 1 - on)
  *  e.g. for 4k logical cluster size,      4B        if compacted 2B is off;
  *                                  (4B) + 2B + (4B) if compacted 2B is on.
+ * bit 1 : HEAD1 big pcluster (0 - off; 1 - on)
+ * bit 2 : HEAD2 big pcluster (0 - off; 1 - on)
  */
-#define Z_EROFS_ADVISE_COMPACTED_2B_BIT         0
-
-#define Z_EROFS_ADVISE_COMPACTED_2B     (1 << Z_EROFS_ADVISE_COMPACTED_2B_BIT)
+#define Z_EROFS_ADVISE_COMPACTED_2B		0x0001
+#define Z_EROFS_ADVISE_BIG_PCLUSTER_1		0x0002
+#define Z_EROFS_ADVISE_BIG_PCLUSTER_2		0x0004
 
 struct z_erofs_map_header {
 	__le32	h_reserved1;
@@ -216,9 +306,7 @@
 	__u8	h_algorithmtype;
 	/*
 	 * bit 0-2 : logical cluster bits - 12, e.g. 0 for 4096;
-	 * bit 3-4 : (physical - logical) cluster bits of head 1:
-	 *       For example, if logical clustersize = 4096, 1 for 8192.
-	 * bit 5-7 : (physical - logical) cluster bits of head 2.
+	 * bit 3-7 : reserved.
 	 */
 	__u8	h_clusterbits;
 };
@@ -261,6 +349,13 @@
 #define Z_EROFS_VLE_DI_CLUSTER_TYPE_BITS        2
 #define Z_EROFS_VLE_DI_CLUSTER_TYPE_BIT         0
 
+/*
+ * D0_CBLKCNT will be marked _only_ at the 1st non-head lcluster to store the
+ * compressed block count of a compressed extent (in logical clusters, aka.
+ * block count of a pcluster).
+ */
+#define Z_EROFS_VLE_DI_D0_CBLKCNT		(1 << 11)
+
 struct z_erofs_vle_decompressed_index {
 	__le16 di_advise;
 	/* where to decompress in the head cluster */
@@ -317,13 +412,18 @@
 	BUILD_BUG_ON(sizeof(struct erofs_inode_extended) != 64);
 	BUILD_BUG_ON(sizeof(struct erofs_xattr_ibody_header) != 12);
 	BUILD_BUG_ON(sizeof(struct erofs_xattr_entry) != 4);
+	BUILD_BUG_ON(sizeof(struct erofs_inode_chunk_info) != 4);
+	BUILD_BUG_ON(sizeof(struct erofs_inode_chunk_index) != 8);
 	BUILD_BUG_ON(sizeof(struct z_erofs_map_header) != 8);
 	BUILD_BUG_ON(sizeof(struct z_erofs_vle_decompressed_index) != 8);
 	BUILD_BUG_ON(sizeof(struct erofs_dirent) != 12);
+	/* keep in sync between 2 index structures for better extendibility */
+	BUILD_BUG_ON(sizeof(struct erofs_inode_chunk_index) !=
+		     sizeof(struct z_erofs_vle_decompressed_index));
+	BUILD_BUG_ON(sizeof(struct erofs_deviceslot) != 128);
 
 	BUILD_BUG_ON(BIT(Z_EROFS_VLE_DI_CLUSTER_TYPE_BITS) <
 		     Z_EROFS_VLE_CLUSTER_TYPE_MAX - 1);
 }
 
 #endif
-
diff --git a/lib/Makefile.am b/lib/Makefile.am
index f21dc35..25a4a2b 100644
--- a/lib/Makefile.am
+++ b/lib/Makefile.am
@@ -1,9 +1,33 @@
 # SPDX-License-Identifier: GPL-2.0+
-# Makefile.am
 
 noinst_LTLIBRARIES = liberofs.la
+noinst_HEADERS = $(top_srcdir)/include/erofs_fs.h \
+      $(top_srcdir)/include/erofs/blobchunk.h \
+      $(top_srcdir)/include/erofs/block_list.h \
+      $(top_srcdir)/include/erofs/cache.h \
+      $(top_srcdir)/include/erofs/compress.h \
+      $(top_srcdir)/include/erofs/config.h \
+      $(top_srcdir)/include/erofs/decompress.h \
+      $(top_srcdir)/include/erofs/defs.h \
+      $(top_srcdir)/include/erofs/err.h \
+      $(top_srcdir)/include/erofs/exclude.h \
+      $(top_srcdir)/include/erofs/flex-array.h \
+      $(top_srcdir)/include/erofs/hashmap.h \
+      $(top_srcdir)/include/erofs/hashtable.h \
+      $(top_srcdir)/include/erofs/inode.h \
+      $(top_srcdir)/include/erofs/internal.h \
+      $(top_srcdir)/include/erofs/io.h \
+      $(top_srcdir)/include/erofs/list.h \
+      $(top_srcdir)/include/erofs/print.h \
+      $(top_srcdir)/include/erofs/trace.h \
+      $(top_srcdir)/include/erofs/xattr.h \
+      $(top_srcdir)/include/erofs/compress_hints.h \
+      $(top_srcdir)/lib/liberofs_private.h
+
+noinst_HEADERS += compressor.h
 liberofs_la_SOURCES = config.c io.c cache.c super.c inode.c xattr.c exclude.c \
-		      namei.c data.c compress.c compressor.c zmap.c decompress.c
+		      namei.c data.c compress.c compressor.c zmap.c decompress.c \
+		      compress_hints.c hashmap.c sha256.c blobchunk.c dir.c
 liberofs_la_CFLAGS = -Wall -Werror -I$(top_srcdir)/include
 if ENABLE_LZ4
 liberofs_la_CFLAGS += ${LZ4_CFLAGS}
@@ -12,4 +36,7 @@
 liberofs_la_SOURCES += compressor_lz4hc.c
 endif
 endif
-
+if ENABLE_LIBLZMA
+liberofs_la_CFLAGS += ${liblzma_CFLAGS}
+liberofs_la_SOURCES += compressor_liblzma.c
+endif
diff --git a/lib/blobchunk.c b/lib/blobchunk.c
new file mode 100644
index 0000000..b605b0b
--- /dev/null
+++ b/lib/blobchunk.c
@@ -0,0 +1,297 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * erofs-utils/lib/blobchunk.c
+ *
+ * Copyright (C) 2021, Alibaba Cloud
+ */
+#define _GNU_SOURCE
+#include "erofs/hashmap.h"
+#include "erofs/blobchunk.h"
+#include "erofs/block_list.h"
+#include "erofs/cache.h"
+#include "erofs/io.h"
+#include <unistd.h>
+
+void erofs_sha256(const unsigned char *in, unsigned long in_size,
+		  unsigned char out[32]);
+
+struct erofs_blobchunk {
+	struct hashmap_entry ent;
+	char		sha256[32];
+	unsigned int	chunksize;
+	erofs_blk_t	blkaddr;
+};
+
+static struct hashmap blob_hashmap;
+static FILE *blobfile;
+static erofs_blk_t remapped_base;
+static bool multidev;
+static struct erofs_buffer_head *bh_devt;
+
+static struct erofs_blobchunk *erofs_blob_getchunk(int fd,
+		unsigned int chunksize)
+{
+	static u8 zeroed[EROFS_BLKSIZ];
+	u8 *chunkdata, sha256[32];
+	int ret;
+	unsigned int hash;
+	erofs_off_t blkpos;
+	struct erofs_blobchunk *chunk;
+
+	chunkdata = malloc(chunksize);
+	if (!chunkdata)
+		return ERR_PTR(-ENOMEM);
+
+	ret = read(fd, chunkdata, chunksize);
+	if (ret < chunksize) {
+		chunk = ERR_PTR(-EIO);
+		goto out;
+	}
+	erofs_sha256(chunkdata, chunksize, sha256);
+	hash = memhash(sha256, sizeof(sha256));
+	chunk = hashmap_get_from_hash(&blob_hashmap, hash, sha256);
+	if (chunk) {
+		DBG_BUGON(chunksize != chunk->chunksize);
+		goto out;
+	}
+	chunk = malloc(sizeof(struct erofs_blobchunk));
+	if (!chunk) {
+		chunk = ERR_PTR(-ENOMEM);
+		goto out;
+	}
+
+	chunk->chunksize = chunksize;
+	blkpos = ftell(blobfile);
+	DBG_BUGON(erofs_blkoff(blkpos));
+	chunk->blkaddr = erofs_blknr(blkpos);
+	memcpy(chunk->sha256, sha256, sizeof(sha256));
+	hashmap_entry_init(&chunk->ent, hash);
+	hashmap_add(&blob_hashmap, chunk);
+
+	erofs_dbg("Writing chunk (%u bytes) to %u", chunksize, chunk->blkaddr);
+	ret = fwrite(chunkdata, chunksize, 1, blobfile);
+	if (ret == 1 && erofs_blkoff(chunksize))
+		ret = fwrite(zeroed, EROFS_BLKSIZ - erofs_blkoff(chunksize),
+			     1, blobfile);
+	if (ret < 1) {
+		struct hashmap_entry key;
+
+		hashmap_entry_init(&key, hash);
+		hashmap_remove(&blob_hashmap, &key, sha256);
+		free(chunk);
+		chunk = ERR_PTR(-ENOSPC);
+		goto out;
+	}
+out:
+	free(chunkdata);
+	return chunk;
+}
+
+static int erofs_blob_hashmap_cmp(const void *a, const void *b,
+				  const void *key)
+{
+	const struct erofs_blobchunk *ec1 =
+			container_of((struct hashmap_entry *)a,
+				     struct erofs_blobchunk, ent);
+	const struct erofs_blobchunk *ec2 =
+			container_of((struct hashmap_entry *)b,
+				     struct erofs_blobchunk, ent);
+
+	return memcmp(ec1->sha256, key ? key : ec2->sha256,
+		      sizeof(ec1->sha256));
+}
+
+int erofs_blob_write_chunk_indexes(struct erofs_inode *inode,
+				   erofs_off_t off)
+{
+	struct erofs_inode_chunk_index idx = {0};
+	erofs_blk_t extent_start = EROFS_NULL_ADDR;
+	erofs_blk_t extent_end, extents_blks;
+	unsigned int dst, src, unit;
+	bool first_extent = true;
+	erofs_blk_t base_blkaddr = 0;
+
+	if (multidev) {
+		idx.device_id = 1;
+		inode->u.chunkformat |= EROFS_CHUNK_FORMAT_INDEXES;
+	} else {
+		base_blkaddr = remapped_base;
+	}
+
+	if (inode->u.chunkformat & EROFS_CHUNK_FORMAT_INDEXES)
+		unit = sizeof(struct erofs_inode_chunk_index);
+	else
+		unit = EROFS_BLOCK_MAP_ENTRY_SIZE;
+
+	for (dst = src = 0; dst < inode->extent_isize;
+	     src += sizeof(void *), dst += unit) {
+		struct erofs_blobchunk *chunk;
+
+		chunk = *(void **)(inode->chunkindexes + src);
+
+		idx.blkaddr = base_blkaddr + chunk->blkaddr;
+		if (extent_start != EROFS_NULL_ADDR &&
+		    idx.blkaddr == extent_end + 1) {
+			extent_end = idx.blkaddr;
+		} else {
+			if (extent_start != EROFS_NULL_ADDR) {
+				erofs_droid_blocklist_write_extent(inode,
+					extent_start,
+					(extent_end - extent_start) + 1,
+					first_extent, false);
+				first_extent = false;
+			}
+			extent_start = idx.blkaddr;
+			extent_end = idx.blkaddr;
+		}
+		if (unit == EROFS_BLOCK_MAP_ENTRY_SIZE)
+			memcpy(inode->chunkindexes + dst, &idx.blkaddr, unit);
+		else
+			memcpy(inode->chunkindexes + dst, &idx, sizeof(idx));
+	}
+	off = roundup(off, unit);
+
+	if (extent_start == EROFS_NULL_ADDR)
+		extents_blks = 0;
+	else
+		extents_blks = (extent_end - extent_start) + 1;
+	erofs_droid_blocklist_write_extent(inode, extent_start, extents_blks,
+					   first_extent, true);
+
+	return dev_write(inode->chunkindexes, off, inode->extent_isize);
+}
+
+int erofs_blob_write_chunked_file(struct erofs_inode *inode)
+{
+	unsigned int chunksize = 1 << cfg.c_chunkbits;
+	unsigned int count = DIV_ROUND_UP(inode->i_size, chunksize);
+	struct erofs_inode_chunk_index *idx;
+	erofs_off_t pos, len;
+	unsigned int unit;
+	int fd, ret;
+
+	inode->u.chunkformat |= inode->u.chunkbits - LOG_BLOCK_SIZE;
+
+	if (inode->u.chunkformat & EROFS_CHUNK_FORMAT_INDEXES)
+		unit = sizeof(struct erofs_inode_chunk_index);
+	else
+		unit = EROFS_BLOCK_MAP_ENTRY_SIZE;
+
+	inode->extent_isize = count * unit;
+	idx = malloc(count * max(sizeof(*idx), sizeof(void *)));
+	if (!idx)
+		return -ENOMEM;
+	inode->chunkindexes = idx;
+
+	fd = open(inode->i_srcpath, O_RDONLY | O_BINARY);
+	if (fd < 0) {
+		ret = -errno;
+		goto err;
+	}
+
+	for (pos = 0; pos < inode->i_size; pos += len) {
+		struct erofs_blobchunk *chunk;
+
+		len = min_t(u64, inode->i_size - pos, chunksize);
+		chunk = erofs_blob_getchunk(fd, len);
+		if (IS_ERR(chunk)) {
+			ret = PTR_ERR(chunk);
+			close(fd);
+			goto err;
+		}
+		*(void **)idx++ = chunk;
+	}
+	inode->datalayout = EROFS_INODE_CHUNK_BASED;
+	close(fd);
+	return 0;
+err:
+	free(inode->chunkindexes);
+	inode->chunkindexes = NULL;
+	return ret;
+}
+
+int erofs_blob_remap(void)
+{
+	struct erofs_buffer_head *bh;
+	ssize_t length;
+	erofs_off_t pos_in, pos_out;
+	ssize_t ret;
+
+	fflush(blobfile);
+	length = ftell(blobfile);
+	if (multidev) {
+		struct erofs_deviceslot dis = {
+			.blocks = erofs_blknr(length),
+		};
+
+		pos_out = erofs_btell(bh_devt, false);
+		ret = dev_write(&dis, pos_out, sizeof(dis));
+		if (ret)
+			return ret;
+
+		bh_devt->op = &erofs_drop_directly_bhops;
+		erofs_bdrop(bh_devt, false);
+		return 0;
+	}
+	bh = erofs_balloc(DATA, length, 0, 0);
+	if (IS_ERR(bh))
+		return PTR_ERR(bh);
+
+	erofs_mapbh(bh->block);
+	pos_out = erofs_btell(bh, false);
+	pos_in = 0;
+	remapped_base = erofs_blknr(pos_out);
+	ret = erofs_copy_file_range(fileno(blobfile), &pos_in,
+				    erofs_devfd, &pos_out, length);
+	bh->op = &erofs_drop_directly_bhops;
+	erofs_bdrop(bh, false);
+	return ret < length ? -EIO : 0;
+}
+
+void erofs_blob_exit(void)
+{
+	if (blobfile)
+		fclose(blobfile);
+
+	hashmap_free(&blob_hashmap, 1);
+}
+
+int erofs_blob_init(const char *blobfile_path)
+{
+	if (!blobfile_path) {
+#ifdef HAVE_TMPFILE64
+		blobfile = tmpfile64();
+#else
+		blobfile = tmpfile();
+#endif
+		multidev = false;
+	} else {
+		blobfile = fopen(blobfile_path, "wb");
+		multidev = true;
+	}
+	if (!blobfile)
+		return -EACCES;
+
+	hashmap_init(&blob_hashmap, erofs_blob_hashmap_cmp, 0);
+	return 0;
+}
+
+int erofs_generate_devtable(void)
+{
+	struct erofs_deviceslot dis;
+
+	if (!multidev)
+		return 0;
+
+	bh_devt = erofs_balloc(DEVT, sizeof(dis), 0, 0);
+	if (IS_ERR(bh_devt))
+		return PTR_ERR(bh_devt);
+
+	dis = (struct erofs_deviceslot) {};
+	erofs_mapbh(bh_devt->block);
+	bh_devt->op = &erofs_skip_write_bhops;
+	sbi.devt_slotoff = erofs_btell(bh_devt, false) / EROFS_DEVT_SLOT_SIZE;
+	sbi.extra_devices = 1;
+	erofs_sb_set_device_table();
+	return 0;
+}
diff --git a/lib/block_list.c b/lib/block_list.c
new file mode 100644
index 0000000..87609a9
--- /dev/null
+++ b/lib/block_list.c
@@ -0,0 +1,108 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright (C), 2021, Coolpad Group Limited.
+ * Created by Yue Hu <huyue2@yulong.com>
+ */
+#ifdef WITH_ANDROID
+#include <stdio.h>
+#include <sys/stat.h>
+#include "erofs/block_list.h"
+
+#define EROFS_MODNAME	"erofs block_list"
+#include "erofs/print.h"
+
+static FILE *block_list_fp;
+
+int erofs_droid_blocklist_fopen(void)
+{
+	block_list_fp = fopen(cfg.block_list_file, "w");
+
+	if (!block_list_fp)
+		return -1;
+	return 0;
+}
+
+void erofs_droid_blocklist_fclose(void)
+{
+	if (!block_list_fp)
+		return;
+
+	fclose(block_list_fp);
+	block_list_fp = NULL;
+}
+
+static void blocklist_write(const char *path, erofs_blk_t blk_start,
+			    erofs_blk_t nblocks, bool first_extent,
+			    bool last_extent)
+{
+	const char *fspath = erofs_fspath(path);
+
+	if (first_extent) {
+		fprintf(block_list_fp, "/%s", cfg.mount_point);
+
+		if (fspath[0] != '/')
+			fprintf(block_list_fp, "/");
+
+		fprintf(block_list_fp, "%s", fspath);
+	}
+
+	if (nblocks == 1)
+		fprintf(block_list_fp, " %u", blk_start);
+	else
+		fprintf(block_list_fp, " %u-%u", blk_start,
+			blk_start + nblocks - 1);
+
+	if (last_extent)
+		fprintf(block_list_fp, "\n");
+}
+
+void erofs_droid_blocklist_write_extent(struct erofs_inode *inode,
+					erofs_blk_t blk_start,
+					erofs_blk_t nblocks, bool first_extent,
+					bool last_extent)
+{
+	if (!block_list_fp || !cfg.mount_point)
+		return;
+
+	if (!nblocks) {
+		if (last_extent)
+			fprintf(block_list_fp, "\n");
+		return;
+	}
+
+	blocklist_write(inode->i_srcpath, blk_start, nblocks, first_extent,
+			last_extent);
+}
+
+void erofs_droid_blocklist_write(struct erofs_inode *inode,
+				 erofs_blk_t blk_start, erofs_blk_t nblocks)
+{
+	if (!block_list_fp || !cfg.mount_point || !nblocks)
+		return;
+
+	blocklist_write(inode->i_srcpath, blk_start, nblocks,
+			true, !inode->idata_size);
+}
+
+void erofs_droid_blocklist_write_tail_end(struct erofs_inode *inode,
+					  erofs_blk_t blkaddr)
+{
+	if (!block_list_fp || !cfg.mount_point)
+		return;
+
+	/* XXX: a bit hacky.. may need a better approach */
+	if (S_ISDIR(inode->i_mode) || S_ISLNK(inode->i_mode))
+		return;
+
+	/* XXX: another hack, which means it has been outputed before */
+	if (erofs_blknr(inode->i_size)) {
+		if (blkaddr == NULL_ADDR)
+			fprintf(block_list_fp, "\n");
+		else
+			fprintf(block_list_fp, " %u\n", blkaddr);
+		return;
+	}
+	if (blkaddr != NULL_ADDR)
+		blocklist_write(inode->i_srcpath, blkaddr, 1, true, true);
+}
+#endif
diff --git a/lib/cache.c b/lib/cache.c
index 0d5c4a5..8016e38 100644
--- a/lib/cache.c
+++ b/lib/cache.c
@@ -1,7 +1,5 @@
 // SPDX-License-Identifier: GPL-2.0+
 /*
- * erofs-utils/lib/cache.c
- *
  * Copyright (C) 2018-2019 HUAWEI, Inc.
  *             http://www.huawei.com/
  * Created by Miao Xie <miaoxie@huawei.com>
@@ -18,6 +16,11 @@
 };
 static erofs_blk_t tail_blkaddr;
 
+/* buckets for all mapped buffer blocks to boost up allocation */
+static struct list_head mapped_buckets[META + 1][EROFS_BLKSIZ];
+/* last mapped buffer block to accelerate erofs_mapbh() */
+static struct erofs_buffer_block *last_mapped_block = &blkh;
+
 static bool erofs_bh_flush_drop_directly(struct erofs_buffer_head *bh)
 {
 	return erofs_bh_flush_generic_end(bh);
@@ -62,15 +65,32 @@
 /* return buffer_head of erofs super block (with size 0) */
 struct erofs_buffer_head *erofs_buffer_init(void)
 {
+	int i, j;
 	struct erofs_buffer_head *bh = erofs_balloc(META, 0, 0, 0);
 
 	if (IS_ERR(bh))
 		return bh;
 
 	bh->op = &erofs_skip_write_bhops;
+
+	for (i = 0; i < ARRAY_SIZE(mapped_buckets); i++)
+		for (j = 0; j < ARRAY_SIZE(mapped_buckets[0]); j++)
+			init_list_head(&mapped_buckets[i][j]);
 	return bh;
 }
 
+static void erofs_bupdate_mapped(struct erofs_buffer_block *bb)
+{
+	struct list_head *bkt;
+
+	if (bb->blkaddr == NULL_ADDR)
+		return;
+
+	bkt = mapped_buckets[bb->type] + bb->buffers.off % EROFS_BLKSIZ;
+	list_del(&bb->mapped_list);
+	list_add_tail(&bb->mapped_list, bkt);
+}
+
 /* return occupied bytes in specific buffer block if succeed */
 static int __erofs_battach(struct erofs_buffer_block *bb,
 			   struct erofs_buffer_head *bh,
@@ -80,7 +100,7 @@
 			   bool dryrun)
 {
 	const erofs_off_t alignedoffset = roundup(bb->buffers.off, alignsize);
-	const int oob = cmpsgn(roundup(bb->buffers.off % EROFS_BLKSIZ,
+	const int oob = cmpsgn(roundup((bb->buffers.off - 1) % EROFS_BLKSIZ + 1,
 				       alignsize) + incr + extrasize,
 			       EROFS_BLKSIZ);
 	bool tailupdate = false;
@@ -110,8 +130,9 @@
 		/* need to update the tail_blkaddr */
 		if (tailupdate)
 			tail_blkaddr = blkaddr + BLK_ROUND_UP(bb->buffers.off);
+		erofs_bupdate_mapped(bb);
 	}
-	return (alignedoffset + incr) % EROFS_BLKSIZ;
+	return (alignedoffset + incr - 1) % EROFS_BLKSIZ + 1;
 }
 
 int erofs_bh_balloon(struct erofs_buffer_head *bh, erofs_off_t incr)
@@ -125,27 +146,75 @@
 	return __erofs_battach(bb, NULL, incr, 1, 0, false);
 }
 
-struct erofs_buffer_head *erofs_balloc(int type, erofs_off_t size,
-				       unsigned int required_ext,
-				       unsigned int inline_ext)
+static int erofs_bfind_for_attach(int type, erofs_off_t size,
+				  unsigned int required_ext,
+				  unsigned int inline_ext,
+				  unsigned int alignsize,
+				  struct erofs_buffer_block **bbp)
 {
 	struct erofs_buffer_block *cur, *bb;
-	struct erofs_buffer_head *bh;
-	unsigned int alignsize, used0, usedmax;
-
-	int ret = get_alignsize(type, &type);
-
-	if (ret < 0)
-		return ERR_PTR(ret);
-	alignsize = ret;
+	unsigned int used0, used_before, usedmax, used;
+	int ret;
 
 	used0 = (size + required_ext) % EROFS_BLKSIZ + inline_ext;
+	/* inline data should be in the same fs block */
+	if (used0 > EROFS_BLKSIZ)
+		return -ENOSPC;
+
+	if (!used0 || alignsize == EROFS_BLKSIZ) {
+		*bbp = NULL;
+		return 0;
+	}
+
 	usedmax = 0;
 	bb = NULL;
 
-	list_for_each_entry(cur, &blkh.list, list) {
-		unsigned int used_before, used;
+	/* try to find a most-fit mapped buffer block first */
+	if (size + required_ext + inline_ext >= EROFS_BLKSIZ)
+		goto skip_mapped;
 
+	used_before = rounddown(EROFS_BLKSIZ -
+				(size + required_ext + inline_ext), alignsize);
+	for (; used_before; --used_before) {
+		struct list_head *bt = mapped_buckets[type] + used_before;
+
+		if (list_empty(bt))
+			continue;
+		cur = list_first_entry(bt, struct erofs_buffer_block,
+				       mapped_list);
+
+		/* last mapped block can be expended, don't handle it here */
+		if (list_next_entry(cur, list)->blkaddr == NULL_ADDR) {
+			DBG_BUGON(cur != last_mapped_block);
+			continue;
+		}
+
+		DBG_BUGON(cur->type != type);
+		DBG_BUGON(cur->blkaddr == NULL_ADDR);
+		DBG_BUGON(used_before != cur->buffers.off % EROFS_BLKSIZ);
+
+		ret = __erofs_battach(cur, NULL, size, alignsize,
+				      required_ext + inline_ext, true);
+		if (ret < 0) {
+			DBG_BUGON(1);
+			continue;
+		}
+
+		/* should contain all data in the current block */
+		used = ret + required_ext + inline_ext;
+		DBG_BUGON(used > EROFS_BLKSIZ);
+
+		bb = cur;
+		usedmax = used;
+		break;
+	}
+
+skip_mapped:
+	/* try to start from the last mapped one, which can be expended */
+	cur = last_mapped_block;
+	if (cur == &blkh)
+		cur = list_next_entry(cur, list);
+	for (; cur != &blkh; cur = list_next_entry(cur, list)) {
 		used_before = cur->buffers.off % EROFS_BLKSIZ;
 
 		/* skip if buffer block is just full */
@@ -179,34 +248,56 @@
 			usedmax = used;
 		}
 	}
+	*bbp = bb;
+	return 0;
+}
+
+struct erofs_buffer_head *erofs_balloc(int type, erofs_off_t size,
+				       unsigned int required_ext,
+				       unsigned int inline_ext)
+{
+	struct erofs_buffer_block *bb;
+	struct erofs_buffer_head *bh;
+	unsigned int alignsize;
+
+	int ret = get_alignsize(type, &type);
+
+	if (ret < 0)
+		return ERR_PTR(ret);
+
+	DBG_BUGON(type < 0 || type > META);
+	alignsize = ret;
+
+	/* try to find if we could reuse an allocated buffer block */
+	ret = erofs_bfind_for_attach(type, size, required_ext, inline_ext,
+				     alignsize, &bb);
+	if (ret)
+		return ERR_PTR(ret);
 
 	if (bb) {
 		bh = malloc(sizeof(struct erofs_buffer_head));
 		if (!bh)
 			return ERR_PTR(-ENOMEM);
-		goto found;
+	} else {
+		/* get a new buffer block instead */
+		bb = malloc(sizeof(struct erofs_buffer_block));
+		if (!bb)
+			return ERR_PTR(-ENOMEM);
+
+		bb->type = type;
+		bb->blkaddr = NULL_ADDR;
+		bb->buffers.off = 0;
+		init_list_head(&bb->buffers.list);
+		list_add_tail(&bb->list, &blkh.list);
+		init_list_head(&bb->mapped_list);
+
+		bh = malloc(sizeof(struct erofs_buffer_head));
+		if (!bh) {
+			free(bb);
+			return ERR_PTR(-ENOMEM);
+		}
 	}
 
-	/* allocate a new buffer block */
-	if (used0 > EROFS_BLKSIZ)
-		return ERR_PTR(-ENOSPC);
-
-	bb = malloc(sizeof(struct erofs_buffer_block));
-	if (!bb)
-		return ERR_PTR(-ENOMEM);
-
-	bb->type = type;
-	bb->blkaddr = NULL_ADDR;
-	bb->buffers.off = 0;
-	init_list_head(&bb->buffers.list);
-	list_add_tail(&bb->list, &blkh.list);
-
-	bh = malloc(sizeof(struct erofs_buffer_head));
-	if (!bh) {
-		free(bb);
-		return ERR_PTR(-ENOMEM);
-	}
-found:
 	ret = __erofs_battach(bb, bh, size, alignsize,
 			      required_ext + inline_ext, false);
 	if (ret < 0)
@@ -247,8 +338,11 @@
 {
 	erofs_blk_t blkaddr;
 
-	if (bb->blkaddr == NULL_ADDR)
+	if (bb->blkaddr == NULL_ADDR) {
 		bb->blkaddr = tail_blkaddr;
+		last_mapped_block = bb;
+		erofs_bupdate_mapped(bb);
+	}
 
 	blkaddr = bb->blkaddr + BLK_ROUND_UP(bb->buffers.off);
 	if (blkaddr > tail_blkaddr)
@@ -257,19 +351,20 @@
 	return blkaddr;
 }
 
-erofs_blk_t erofs_mapbh(struct erofs_buffer_block *bb, bool end)
+erofs_blk_t erofs_mapbh(struct erofs_buffer_block *bb)
 {
-	struct erofs_buffer_block *t, *nt;
+	struct erofs_buffer_block *t = last_mapped_block;
 
-	if (!bb || bb->blkaddr == NULL_ADDR) {
-		list_for_each_entry_safe(t, nt, &blkh.list, list) {
-			if (!end && (t == bb || nt == &blkh))
-				break;
-			(void)__erofs_mapbh(t);
-			if (end && t == bb)
-				break;
-		}
-	}
+	if (bb && bb->blkaddr != NULL_ADDR)
+		return bb->blkaddr;
+	do {
+		t = list_next_entry(t, list);
+		if (t == &blkh)
+			break;
+
+		DBG_BUGON(t->blkaddr != NULL_ADDR);
+		(void)__erofs_mapbh(t);
+	} while (t != bb);
 	return tail_blkaddr;
 }
 
@@ -311,6 +406,7 @@
 
 		erofs_dbg("block %u to %u flushed", p->blkaddr, blkaddr - 1);
 
+		list_del(&p->mapped_list);
 		list_del(&p->list);
 		free(p);
 	}
@@ -334,10 +430,13 @@
 	if (!list_empty(&bb->buffers.list))
 		return;
 
+	if (bb == last_mapped_block)
+		last_mapped_block = list_prev_entry(bb, list);
+
+	list_del(&bb->mapped_list);
 	list_del(&bb->list);
 	free(bb);
 
 	if (rollback)
 		tail_blkaddr = blkaddr;
 }
-
diff --git a/lib/compress.c b/lib/compress.c
index 86db940..98be7a2 100644
--- a/lib/compress.c
+++ b/lib/compress.c
@@ -1,7 +1,5 @@
 // SPDX-License-Identifier: GPL-2.0+
 /*
- * erofs-utils/lib/compress.c
- *
  * Copyright (C) 2018-2019 HUAWEI, Inc.
  *             http://www.huawei.com/
  * Created by Miao Xie <miaoxie@huawei.com>
@@ -18,19 +16,19 @@
 #include "erofs/cache.h"
 #include "erofs/compress.h"
 #include "compressor.h"
+#include "erofs/block_list.h"
+#include "erofs/compress_hints.h"
 
 static struct erofs_compress compresshandle;
-static int compressionlevel;
-
-static struct z_erofs_map_header mapheader;
+static unsigned int algorithmtype[2];
 
 struct z_erofs_vle_compress_ctx {
 	u8 *metacur;
 
 	u8 queue[EROFS_CONFIG_COMPR_MAX_SZ * 2];
 	unsigned int head, tail;
-
-	erofs_blk_t blkaddr;	/* pointing to the next blkaddr */
+	unsigned int compressedblks;
+	erofs_blk_t blkaddr;		/* pointing to the next blkaddr */
 	u16 clusterofs;
 };
 
@@ -72,10 +70,11 @@
 
 	di.di_clusterofs = cpu_to_le16(ctx->clusterofs);
 
-	/* whether the tail-end (un)compressed block or not */
+	/* whether the tail-end uncompressed block or not */
 	if (!d1) {
-		type = raw ? Z_EROFS_VLE_CLUSTER_TYPE_PLAIN :
-			Z_EROFS_VLE_CLUSTER_TYPE_HEAD;
+		/* TODO: tail-packing inline compressed data */
+		DBG_BUGON(!raw);
+		type = Z_EROFS_VLE_CLUSTER_TYPE_PLAIN;
 		advise = cpu_to_le16(type << Z_EROFS_VLE_DI_CLUSTER_TYPE_BIT);
 
 		di.di_advise = advise;
@@ -89,7 +88,13 @@
 	}
 
 	do {
-		if (d0) {
+		/* XXX: big pcluster feature should be per-inode */
+		if (d0 == 1 && erofs_sb_has_big_pcluster()) {
+			type = Z_EROFS_VLE_CLUSTER_TYPE_NONHEAD;
+			di.di_u.delta[0] = cpu_to_le16(ctx->compressedblks |
+					Z_EROFS_VLE_DI_D0_CBLKCNT);
+			di.di_u.delta[1] = cpu_to_le16(d1);
+		} else if (d0) {
 			type = Z_EROFS_VLE_CLUSTER_TYPE_NONHEAD;
 
 			di.di_u.delta[0] = cpu_to_le16(d0);
@@ -115,15 +120,14 @@
 	ctx->clusterofs = clusterofs + count;
 }
 
-static int write_uncompressed_block(struct z_erofs_vle_compress_ctx *ctx,
-				    unsigned int *len,
-				    char *dst)
+static int write_uncompressed_extent(struct z_erofs_vle_compress_ctx *ctx,
+				     unsigned int *len, char *dst)
 {
 	int ret;
 	unsigned int count;
 
 	/* reset clusterofs to 0 if permitted */
-	if (!erofs_sb_has_lz4_0padding() &&
+	if (!erofs_sb_has_lz4_0padding() && ctx->clusterofs &&
 	    ctx->head >= ctx->clusterofs) {
 		ctx->head -= ctx->clusterofs;
 		*len += ctx->clusterofs;
@@ -144,6 +148,20 @@
 	return count;
 }
 
+static unsigned int z_erofs_get_max_pclusterblks(struct erofs_inode *inode)
+{
+#ifndef NDEBUG
+	if (cfg.c_random_pclusterblks)
+		return 1 + rand() % cfg.c_pclusterblks_max;
+#endif
+	if (cfg.c_compress_hints_file) {
+		z_erofs_apply_compress_hints(inode);
+		DBG_BUGON(!inode->z_physical_clusterblks);
+		return inode->z_physical_clusterblks;
+	}
+	return cfg.c_pclusterblks_def;
+}
+
 static int vle_compress_one(struct erofs_inode *inode,
 			    struct z_erofs_vle_compress_ctx *ctx,
 			    bool final)
@@ -152,22 +170,26 @@
 	unsigned int len = ctx->tail - ctx->head;
 	unsigned int count;
 	int ret;
-	static char dstbuf[EROFS_BLKSIZ * 2];
+	static char dstbuf[EROFS_CONFIG_COMPR_MAX_SZ + EROFS_BLKSIZ];
 	char *const dst = dstbuf + EROFS_BLKSIZ;
 
 	while (len) {
+		const unsigned int pclustersize =
+			z_erofs_get_max_pclusterblks(inode) * EROFS_BLKSIZ;
 		bool raw;
 
-		if (len <= EROFS_BLKSIZ) {
-			if (final)
-				goto nocompression;
-			break;
+		if (len <= pclustersize) {
+			if (final) {
+				if (len <= EROFS_BLKSIZ)
+					goto nocompression;
+			} else {
+				break;
+			}
 		}
 
-		count = len;
-		ret = erofs_compress_destsize(h, compressionlevel,
-					      ctx->queue + ctx->head,
-					      &count, dst, EROFS_BLKSIZ);
+		count = min(len, cfg.c_max_decompressed_extent_bytes);
+		ret = erofs_compress_destsize(h, ctx->queue + ctx->head,
+					      &count, dst, pclustersize);
 		if (ret <= 0) {
 			if (ret != -EAGAIN) {
 				erofs_err("failed to compress %s: %s",
@@ -175,32 +197,42 @@
 					  erofs_strerror(ret));
 			}
 nocompression:
-			ret = write_uncompressed_block(ctx, &len, dst);
+			ret = write_uncompressed_extent(ctx, &len, dst);
 			if (ret < 0)
 				return ret;
 			count = ret;
+			ctx->compressedblks = 1;
 			raw = true;
 		} else {
+			const unsigned int tailused = ret & (EROFS_BLKSIZ - 1);
+			const unsigned int padding =
+				erofs_sb_has_lz4_0padding() && tailused ?
+					EROFS_BLKSIZ - tailused : 0;
+
+			ctx->compressedblks = DIV_ROUND_UP(ret, EROFS_BLKSIZ);
+			DBG_BUGON(ctx->compressedblks * EROFS_BLKSIZ >= count);
+
+			/* zero out garbage trailing data for non-0padding */
+			if (!erofs_sb_has_lz4_0padding())
+				memset(dst + ret, 0,
+				       roundup(ret, EROFS_BLKSIZ) - ret);
+
 			/* write compressed data */
-			erofs_dbg("Writing %u compressed data to block %u",
-				  count, ctx->blkaddr);
+			erofs_dbg("Writing %u compressed data to %u of %u blocks",
+				  count, ctx->blkaddr, ctx->compressedblks);
 
-			if (erofs_sb_has_lz4_0padding())
-				ret = blk_write(dst - (EROFS_BLKSIZ - ret),
-						ctx->blkaddr, 1);
-			else
-				ret = blk_write(dst, ctx->blkaddr, 1);
-
+			ret = blk_write(dst - padding, ctx->blkaddr,
+					ctx->compressedblks);
 			if (ret)
 				return ret;
 			raw = false;
 		}
 
 		ctx->head += count;
-		/* write compression indexes for this blkaddr */
+		/* write compression indexes for this pcluster */
 		vle_write_indexes(ctx, count, raw);
 
-		++ctx->blkaddr;
+		ctx->blkaddr += ctx->compressedblks;
 		len -= count;
 
 		if (!final && ctx->head >= EROFS_CONFIG_COMPR_MAX_SZ) {
@@ -256,20 +288,21 @@
 				     erofs_blk_t *blkaddr_ret,
 				     unsigned int destsize,
 				     unsigned int logical_clusterbits,
-				     bool final)
+				     bool final, bool *dummy_head)
 {
-	unsigned int vcnt, encodebits, pos, i;
+	unsigned int vcnt, encodebits, pos, i, cblks;
+	bool update_blkaddr;
 	erofs_blk_t blkaddr;
 
-	if (destsize == 4) {
+	if (destsize == 4)
 		vcnt = 2;
-	} else if (destsize == 2 && logical_clusterbits == 12) {
+	else if (destsize == 2 && logical_clusterbits == 12)
 		vcnt = 16;
-	} else {
+	else
 		return ERR_PTR(-EINVAL);
-	}
 	encodebits = (vcnt * destsize * 8 - 32) / vcnt;
 	blkaddr = *blkaddr_ret;
+	update_blkaddr = erofs_sb_has_big_pcluster();
 
 	pos = 0;
 	for (i = 0; i < vcnt; ++i) {
@@ -277,13 +310,26 @@
 		u8 ch, rem;
 
 		if (cv[i].clustertype == Z_EROFS_VLE_CLUSTER_TYPE_NONHEAD) {
-			if (i + 1 == vcnt)
-				offset = cv[i].u.delta[1];
-			else
+			if (cv[i].u.delta[0] & Z_EROFS_VLE_DI_D0_CBLKCNT) {
+				cblks = cv[i].u.delta[0] & ~Z_EROFS_VLE_DI_D0_CBLKCNT;
 				offset = cv[i].u.delta[0];
+				blkaddr += cblks;
+				*dummy_head = false;
+			} else if (i + 1 == vcnt) {
+				offset = cv[i].u.delta[1];
+			} else {
+				offset = cv[i].u.delta[0];
+			}
 		} else {
 			offset = cv[i].clusterofs;
-			++blkaddr;
+			if (*dummy_head) {
+				++blkaddr;
+				if (update_blkaddr)
+					*blkaddr_ret = blkaddr;
+			}
+			*dummy_head = true;
+			update_blkaddr = false;
+
 			if (cv[i].u.blkaddr != blkaddr) {
 				if (i + 1 != vcnt)
 					DBG_BUGON(!final);
@@ -307,18 +353,20 @@
 int z_erofs_convert_to_compacted_format(struct erofs_inode *inode,
 					erofs_blk_t blkaddr,
 					unsigned int legacymetasize,
-					unsigned int logical_clusterbits)
+					void *compressmeta)
 {
 	const unsigned int mpos = Z_EROFS_VLE_EXTENT_ALIGN(inode->inode_isize +
 							   inode->xattr_isize) +
 				  sizeof(struct z_erofs_map_header);
 	const unsigned int totalidx = (legacymetasize -
 				       Z_EROFS_LEGACY_MAP_HEADER_SIZE) / 8;
+	const unsigned int logical_clusterbits = inode->z_logical_clusterbits;
 	u8 *out, *in;
 	struct z_erofs_compressindex_vec cv[16];
 	/* # of 8-byte units so that it can be aligned with 32 bytes */
 	unsigned int compacted_4b_initial, compacted_4b_end;
 	unsigned int compacted_2b;
+	bool dummy_head;
 
 	if (logical_clusterbits < LOG_BLOCK_SIZE || LOG_BLOCK_SIZE < 12)
 		return -EINVAL;
@@ -343,18 +391,24 @@
 		compacted_4b_end = totalidx;
 	}
 
-	out = in = inode->compressmeta;
+	out = in = compressmeta;
 
-	/* write out compacted header */
-	memcpy(out, &mapheader, sizeof(mapheader));
-	out += sizeof(mapheader);
+	out += sizeof(struct z_erofs_map_header);
 	in += Z_EROFS_LEGACY_MAP_HEADER_SIZE;
 
+	dummy_head = false;
+	/* prior to bigpcluster, blkaddr was bumped up once coming into HEAD */
+	if (!erofs_sb_has_big_pcluster()) {
+		--blkaddr;
+		dummy_head = true;
+	}
+
 	/* generate compacted_4b_initial */
 	while (compacted_4b_initial) {
 		in = parse_legacy_indexes(cv, 2, in);
 		out = write_compacted_indexes(out, cv, &blkaddr,
-					      4, logical_clusterbits, false);
+					      4, logical_clusterbits, false,
+					      &dummy_head);
 		compacted_4b_initial -= 2;
 	}
 	DBG_BUGON(compacted_4b_initial);
@@ -363,7 +417,8 @@
 	while (compacted_2b) {
 		in = parse_legacy_indexes(cv, 16, in);
 		out = write_compacted_indexes(out, cv, &blkaddr,
-					      2, logical_clusterbits, false);
+					      2, logical_clusterbits, false,
+					      &dummy_head);
 		compacted_2b -= 16;
 	}
 	DBG_BUGON(compacted_2b);
@@ -372,7 +427,8 @@
 	while (compacted_4b_end > 1) {
 		in = parse_legacy_indexes(cv, 2, in);
 		out = write_compacted_indexes(out, cv, &blkaddr,
-					      4, logical_clusterbits, false);
+					      4, logical_clusterbits, false,
+					      &dummy_head);
 		compacted_4b_end -= 2;
 	}
 
@@ -381,13 +437,29 @@
 		memset(cv, 0, sizeof(cv));
 		in = parse_legacy_indexes(cv, 1, in);
 		out = write_compacted_indexes(out, cv, &blkaddr,
-					      4, logical_clusterbits, true);
+					      4, logical_clusterbits, true,
+					      &dummy_head);
 	}
-	inode->extent_isize = out - (u8 *)inode->compressmeta;
-	inode->datalayout = EROFS_INODE_FLAT_COMPRESSION;
+	inode->extent_isize = out - (u8 *)compressmeta;
 	return 0;
 }
 
+static void z_erofs_write_mapheader(struct erofs_inode *inode,
+				    void *compressmeta)
+{
+	struct z_erofs_map_header h = {
+		.h_advise = cpu_to_le16(inode->z_advise),
+		.h_algorithmtype = inode->z_algorithmtype[1] << 4 |
+				   inode->z_algorithmtype[0],
+		/* lclustersize */
+		.h_clusterbits = inode->z_logical_clusterbits - 12,
+	};
+
+	memset(compressmeta, 0, Z_EROFS_LEGACY_MAP_HEADER_SIZE);
+	/* write out map header */
+	memcpy(compressmeta, &h, sizeof(struct z_erofs_map_header));
+}
+
 int erofs_write_compressed_file(struct erofs_inode *inode)
 {
 	struct erofs_buffer_head *bh;
@@ -396,8 +468,8 @@
 	erofs_blk_t blkaddr, compressed_blocks;
 	unsigned int legacymetasize;
 	int ret, fd;
-
 	u8 *compressmeta = malloc(vle_compressmeta_capacity(inode->i_size));
+
 	if (!compressmeta)
 		return -ENOMEM;
 
@@ -414,9 +486,27 @@
 		goto err_close;
 	}
 
-	memset(compressmeta, 0, Z_EROFS_LEGACY_MAP_HEADER_SIZE);
+	/* initialize per-file compression setting */
+	inode->z_advise = 0;
+	if (!cfg.c_legacy_compress) {
+		inode->z_advise |= Z_EROFS_ADVISE_COMPACTED_2B;
+		inode->datalayout = EROFS_INODE_FLAT_COMPRESSION;
+	} else {
+		inode->datalayout = EROFS_INODE_FLAT_COMPRESSION_LEGACY;
+	}
 
-	blkaddr = erofs_mapbh(bh->block, true);	/* start_blkaddr */
+	if (erofs_sb_has_big_pcluster()) {
+		inode->z_advise |= Z_EROFS_ADVISE_BIG_PCLUSTER_1;
+		if (inode->datalayout == EROFS_INODE_FLAT_COMPRESSION)
+			inode->z_advise |= Z_EROFS_ADVISE_BIG_PCLUSTER_2;
+	}
+	inode->z_algorithmtype[0] = algorithmtype[0];
+	inode->z_algorithmtype[1] = algorithmtype[1];
+	inode->z_logical_clusterbits = LOG_BLOCK_SIZE;
+
+	z_erofs_write_mapheader(inode, compressmeta);
+
+	blkaddr = erofs_mapbh(bh->block);	/* start_blkaddr */
 	ctx.blkaddr = blkaddr;
 	ctx.metacur = compressmeta + Z_EROFS_LEGACY_MAP_HEADER_SIZE;
 	ctx.head = ctx.tail = 0;
@@ -456,8 +546,9 @@
 	vle_write_indexes_final(&ctx);
 
 	close(fd);
+	DBG_BUGON(!compressed_blocks);
 	ret = erofs_bh_balloon(bh, blknr_to_addr(compressed_blocks));
-	DBG_BUGON(ret);
+	DBG_BUGON(ret != EROFS_BLKSIZ);
 
 	erofs_info("compressed %s (%llu bytes) into %u blocks",
 		   inode->i_srcpath, (unsigned long long)inode->i_size,
@@ -468,19 +559,20 @@
 	 *       when both mkfs & kernel support compression inline.
 	 */
 	erofs_bdrop(bh, false);
-	inode->compressmeta = compressmeta;
 	inode->idata_size = 0;
 	inode->u.i_blocks = compressed_blocks;
 
 	legacymetasize = ctx.metacur - compressmeta;
-	if (cfg.c_legacy_compress) {
+	if (inode->datalayout == EROFS_INODE_FLAT_COMPRESSION_LEGACY) {
 		inode->extent_isize = legacymetasize;
-		inode->datalayout = EROFS_INODE_FLAT_COMPRESSION_LEGACY;
 	} else {
-		ret = z_erofs_convert_to_compacted_format(inode, blkaddr - 1,
-							  legacymetasize, 12);
+		ret = z_erofs_convert_to_compacted_format(inode, blkaddr,
+							  legacymetasize,
+							  compressmeta);
 		DBG_BUGON(ret);
 	}
+	inode->compressmeta = compressmeta;
+	erofs_droid_blocklist_write(inode, blkaddr, compressed_blocks);
 	return 0;
 
 err_bdrop:
@@ -496,12 +588,67 @@
 {
 	if (!strcmp(name, "lz4") || !strcmp(name, "lz4hc"))
 		return Z_EROFS_COMPRESSION_LZ4;
+	if (!strcmp(name, "lzma"))
+		return Z_EROFS_COMPRESSION_LZMA;
 	return -ENOTSUP;
 }
 
-int z_erofs_compress_init(void)
+int z_erofs_build_compr_cfgs(struct erofs_buffer_head *sb_bh)
 {
-	unsigned int algorithmtype[2];
+	struct erofs_buffer_head *bh = sb_bh;
+	int ret = 0;
+
+	if (sbi.available_compr_algs & (1 << Z_EROFS_COMPRESSION_LZ4)) {
+		struct {
+			__le16 size;
+			struct z_erofs_lz4_cfgs lz4;
+		} __packed lz4alg = {
+			.size = cpu_to_le16(sizeof(struct z_erofs_lz4_cfgs)),
+			.lz4 = {
+				.max_distance =
+					cpu_to_le16(sbi.lz4_max_distance),
+				.max_pclusterblks = cfg.c_pclusterblks_max,
+			}
+		};
+
+		bh = erofs_battach(bh, META, sizeof(lz4alg));
+		if (IS_ERR(bh)) {
+			DBG_BUGON(1);
+			return PTR_ERR(bh);
+		}
+		erofs_mapbh(bh->block);
+		ret = dev_write(&lz4alg, erofs_btell(bh, false),
+				sizeof(lz4alg));
+		bh->op = &erofs_drop_directly_bhops;
+	}
+#ifdef HAVE_LIBLZMA
+	if (sbi.available_compr_algs & (1 << Z_EROFS_COMPRESSION_LZMA)) {
+		struct {
+			__le16 size;
+			struct z_erofs_lzma_cfgs lzma;
+		} __packed lzmaalg = {
+			.size = cpu_to_le16(sizeof(struct z_erofs_lzma_cfgs)),
+			.lzma = {
+				.dict_size = cpu_to_le32(cfg.c_dict_size),
+			}
+		};
+
+		bh = erofs_battach(bh, META, sizeof(lzmaalg));
+		if (IS_ERR(bh)) {
+			DBG_BUGON(1);
+			return PTR_ERR(bh);
+		}
+		erofs_mapbh(bh->block);
+		ret = dev_write(&lzmaalg, erofs_btell(bh, false),
+				sizeof(lzmaalg));
+		bh->op = &erofs_drop_directly_bhops;
+	}
+#endif
+	return ret;
+}
+
+int z_erofs_compress_init(struct erofs_buffer_head *sb_bh)
+{
 	/* initialize for primary compression algorithm */
 	int ret = erofs_compressor_init(&compresshandle,
 					cfg.c_compr_alg_master);
@@ -510,31 +657,50 @@
 		return ret;
 
 	/*
-	 * if primary algorithm is not lz4* (e.g. compression off),
-	 * clear LZ4_0PADDING feature for old kernel compatibility.
+	 * if primary algorithm is empty (e.g. compression off),
+	 * clear 0PADDING feature for old kernel compatibility.
 	 */
 	if (!cfg.c_compr_alg_master ||
-	    strncmp(cfg.c_compr_alg_master, "lz4", 3))
+	    (cfg.c_legacy_compress && !strcmp(cfg.c_compr_alg_master, "lz4")))
 		erofs_sb_clear_lz4_0padding();
 
 	if (!cfg.c_compr_alg_master)
 		return 0;
 
-	compressionlevel = cfg.c_compr_level_master < 0 ?
-		compresshandle.alg->default_level :
-		cfg.c_compr_level_master;
+	ret = erofs_compressor_setlevel(&compresshandle,
+					cfg.c_compr_level_master);
+	if (ret)
+		return ret;
 
-	/* figure out mapheader */
+	/* figure out primary algorithm */
 	ret = erofs_get_compress_algorithm_id(cfg.c_compr_alg_master);
 	if (ret < 0)
 		return ret;
 
 	algorithmtype[0] = ret;	/* primary algorithm (head 0) */
 	algorithmtype[1] = 0;	/* secondary algorithm (head 1) */
-	mapheader.h_advise |= Z_EROFS_ADVISE_COMPACTED_2B;
-	mapheader.h_algorithmtype = algorithmtype[1] << 4 |
-					  algorithmtype[0];
-	mapheader.h_clusterbits = LOG_BLOCK_SIZE - 12;
+	/*
+	 * if big pcluster is enabled, an extra CBLKCNT lcluster index needs
+	 * to be loaded in order to get those compressed block counts.
+	 */
+	if (cfg.c_pclusterblks_max > 1) {
+		if (cfg.c_pclusterblks_max >
+		    Z_EROFS_PCLUSTER_MAX_SIZE / EROFS_BLKSIZ) {
+			erofs_err("unsupported clusterblks %u (too large)",
+				  cfg.c_pclusterblks_max);
+			return -EINVAL;
+		}
+		erofs_sb_set_big_pcluster();
+		erofs_warn("EXPERIMENTAL big pcluster feature in use. Use at your own risk!");
+	}
+
+	if (ret != Z_EROFS_COMPRESSION_LZ4)
+		erofs_sb_set_compr_cfgs();
+
+	if (erofs_sb_has_compr_cfgs()) {
+		sbi.available_compr_algs |= 1 << ret;
+		return z_erofs_build_compr_cfgs(sb_bh);
+	}
 	return 0;
 }
 
@@ -542,4 +708,3 @@
 {
 	return erofs_compressor_exit(&compresshandle);
 }
-
diff --git a/lib/compress_hints.c b/lib/compress_hints.c
new file mode 100644
index 0000000..81a8ac9
--- /dev/null
+++ b/lib/compress_hints.c
@@ -0,0 +1,128 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright (C), 2008-2021, OPPO Mobile Comm Corp., Ltd.
+ * Created by Huang Jianan <huangjianan@oppo.com>
+ */
+#include <string.h>
+#include <stdlib.h>
+#include "erofs/err.h"
+#include "erofs/list.h"
+#include "erofs/print.h"
+#include "erofs/compress_hints.h"
+
+static LIST_HEAD(compress_hints_head);
+
+static void dump_regerror(int errcode, const char *s, const regex_t *preg)
+{
+	char str[512];
+
+	regerror(errcode, preg, str, sizeof(str));
+	erofs_err("invalid regex %s (%s)\n", s, str);
+}
+
+static int erofs_insert_compress_hints(const char *s, unsigned int blks)
+{
+	struct erofs_compress_hints *r;
+	int ret;
+
+	r = malloc(sizeof(struct erofs_compress_hints));
+	if (!r)
+		return -ENOMEM;
+
+	ret = regcomp(&r->reg, s, REG_EXTENDED|REG_NOSUB);
+	if (ret) {
+		dump_regerror(ret, s, &r->reg);
+		goto err_out;
+	}
+	r->physical_clusterblks = blks;
+
+	list_add_tail(&r->list, &compress_hints_head);
+	erofs_info("compress hint %s (%u) is inserted", s, blks);
+	return ret;
+
+err_out:
+	free(r);
+	return ret;
+}
+
+bool z_erofs_apply_compress_hints(struct erofs_inode *inode)
+{
+	const char *s;
+	struct erofs_compress_hints *r;
+	unsigned int pclusterblks;
+
+	if (inode->z_physical_clusterblks)
+		return true;
+
+	s = erofs_fspath(inode->i_srcpath);
+	pclusterblks = cfg.c_pclusterblks_def;
+
+	list_for_each_entry(r, &compress_hints_head, list) {
+		int ret = regexec(&r->reg, s, (size_t)0, NULL, 0);
+
+		if (!ret) {
+			pclusterblks = r->physical_clusterblks;
+			break;
+		}
+		if (ret != REG_NOMATCH)
+			dump_regerror(ret, s, &r->reg);
+	}
+	inode->z_physical_clusterblks = pclusterblks;
+
+	/* pclusterblks is 0 means this file shouldn't be compressed */
+	return !!pclusterblks;
+}
+
+void erofs_cleanup_compress_hints(void)
+{
+	struct erofs_compress_hints *r, *n;
+
+	list_for_each_entry_safe(r, n, &compress_hints_head, list) {
+		list_del(&r->list);
+		free(r);
+	}
+}
+
+int erofs_load_compress_hints(void)
+{
+	char buf[PATH_MAX + 100];
+	FILE *f;
+	unsigned int line, max_pclustersize = 0;
+
+	if (!cfg.c_compress_hints_file)
+		return 0;
+
+	f = fopen(cfg.c_compress_hints_file, "r");
+	if (!f)
+		return -errno;
+
+	for (line = 1; fgets(buf, sizeof(buf), f); ++line) {
+		unsigned int pclustersize;
+		char *pattern;
+
+		pclustersize = atoi(strtok(buf, "\t "));
+		pattern = strtok(NULL, "\n");
+		if (!pattern || *pattern == '\0') {
+			erofs_err("cannot find a match pattern at line %u",
+				  line);
+			return -EINVAL;
+		}
+		if (pclustersize % EROFS_BLKSIZ) {
+			erofs_warn("invalid physical clustersize %u, "
+				   "use default pclusterblks %u",
+				   pclustersize, cfg.c_pclusterblks_def);
+			continue;
+		}
+		erofs_insert_compress_hints(pattern,
+					    pclustersize / EROFS_BLKSIZ);
+
+		if (pclustersize > max_pclustersize)
+			max_pclustersize = pclustersize;
+	}
+	fclose(f);
+	if (cfg.c_pclusterblks_max * EROFS_BLKSIZ < max_pclustersize) {
+		cfg.c_pclusterblks_max = max_pclustersize / EROFS_BLKSIZ;
+		erofs_warn("update max pclusterblks to %u", cfg.c_pclusterblks_max);
+	}
+	return 0;
+}
diff --git a/lib/compressor.c b/lib/compressor.c
index b2434e0..ad12cdf 100644
--- a/lib/compressor.c
+++ b/lib/compressor.c
@@ -1,7 +1,5 @@
 // SPDX-License-Identifier: GPL-2.0+
 /*
- * erofs-utils/lib/compressor.c
- *
  * Copyright (C) 2018-2019 HUAWEI, Inc.
  *             http://www.huawei.com/
  * Created by Gao Xiang <gaoxiang25@huawei.com>
@@ -19,28 +17,30 @@
 #endif
 		&erofs_compressor_lz4,
 #endif
+#if HAVE_LIBLZMA
+		&erofs_compressor_lzma,
+#endif
 };
 
 int erofs_compress_destsize(struct erofs_compress *c,
-			    int compression_level,
-			    void *src,
-			    unsigned int *srcsize,
-			    void *dst,
-			    unsigned int dstsize)
+			    void *src, unsigned int *srcsize,
+			    void *dst, unsigned int dstsize)
 {
+	unsigned int uncompressed_size;
 	int ret;
 
 	DBG_BUGON(!c->alg);
 	if (!c->alg->compress_destsize)
 		return -ENOTSUP;
 
-	ret = c->alg->compress_destsize(c, compression_level,
-					src, srcsize, dst, dstsize);
+	ret = c->alg->compress_destsize(c, src, srcsize, dst, dstsize);
 	if (ret < 0)
 		return ret;
 
 	/* check if there is enough gains to compress */
-	if (*srcsize <= dstsize * c->compress_threshold / 100)
+	uncompressed_size = *srcsize;
+	if (roundup(ret, EROFS_BLKSIZ) >= uncompressed_size *
+	    c->compress_threshold / 100)
 		return -EAGAIN;
 	return ret;
 }
@@ -50,6 +50,18 @@
 	return i >= ARRAY_SIZE(compressors) ? NULL : compressors[i]->name;
 }
 
+int erofs_compressor_setlevel(struct erofs_compress *c, int compression_level)
+{
+	DBG_BUGON(!c->alg);
+	if (c->alg->setlevel)
+		return c->alg->setlevel(c, compression_level);
+
+	if (compression_level >= 0)
+		return -EINVAL;
+	c->compression_level = 0;
+	return 0;
+}
+
 int erofs_compressor_init(struct erofs_compress *c, char *alg_name)
 {
 	int ret, i;
@@ -88,4 +100,3 @@
 		return c->alg->exit(c);
 	return 0;
 }
-
diff --git a/lib/compressor.h b/lib/compressor.h
index b2471c4..aa85ae0 100644
--- a/lib/compressor.h
+++ b/lib/compressor.h
@@ -1,7 +1,5 @@
 /* SPDX-License-Identifier: GPL-2.0+ */
 /*
- * erofs-utils/lib/compressor.h
- *
  * Copyright (C) 2018-2019 HUAWEI, Inc.
  *             http://www.huawei.com/
  * Created by Gao Xiang <gaoxiang25@huawei.com>
@@ -21,9 +19,9 @@
 
 	int (*init)(struct erofs_compress *c);
 	int (*exit)(struct erofs_compress *c);
+	int (*setlevel)(struct erofs_compress *c, int compression_level);
 
 	int (*compress_destsize)(struct erofs_compress *c,
-				 int compress_level,
 				 void *src, unsigned int *srcsize,
 				 void *dst, unsigned int dstsize);
 };
@@ -32,6 +30,7 @@
 	struct erofs_compressor *alg;
 
 	unsigned int compress_threshold;
+	unsigned int compression_level;
 
 	/* *_destsize specific */
 	unsigned int destsize_alignsize;
@@ -44,13 +43,14 @@
 /* list of compression algorithms */
 extern struct erofs_compressor erofs_compressor_lz4;
 extern struct erofs_compressor erofs_compressor_lz4hc;
+extern struct erofs_compressor erofs_compressor_lzma;
 
-int erofs_compress_destsize(struct erofs_compress *c, int compression_level,
+int erofs_compress_destsize(struct erofs_compress *c,
 			    void *src, unsigned int *srcsize,
 			    void *dst, unsigned int dstsize);
 
+int erofs_compressor_setlevel(struct erofs_compress *c, int compression_level);
 int erofs_compressor_init(struct erofs_compress *c, char *alg_name);
 int erofs_compressor_exit(struct erofs_compress *c);
 
 #endif
-
diff --git a/lib/compressor_liblzma.c b/lib/compressor_liblzma.c
new file mode 100644
index 0000000..40a05ef
--- /dev/null
+++ b/lib/compressor_liblzma.c
@@ -0,0 +1,108 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * erofs-utils/lib/compressor_liblzma.c
+ *
+ * Copyright (C) 2021 Gao Xiang <xiang@kernel.org>
+ */
+#include <stdlib.h>
+#include "config.h"
+#ifdef HAVE_LIBLZMA
+#include <lzma.h>
+#include "erofs/config.h"
+#include "erofs/print.h"
+#include "erofs/internal.h"
+#include "compressor.h"
+
+struct erofs_liblzma_context {
+	lzma_options_lzma opt;
+	lzma_stream strm;
+};
+
+static int erofs_liblzma_compress_destsize(struct erofs_compress *c,
+					   void *src, unsigned int *srcsize,
+					   void *dst, unsigned int dstsize)
+{
+	struct erofs_liblzma_context *ctx = c->private_data;
+	lzma_stream *strm = &ctx->strm;
+
+	lzma_ret ret = lzma_microlzma_encoder(strm, &ctx->opt);
+	if (ret != LZMA_OK)
+		return -EFAULT;
+
+	strm->next_in = src;
+	strm->avail_in = *srcsize;
+	strm->next_out = dst;
+	strm->avail_out = dstsize;
+
+	ret = lzma_code(strm, LZMA_FINISH);
+	if (ret != LZMA_STREAM_END)
+		return -EBADMSG;
+
+	*srcsize = strm->total_in;
+	return strm->total_out;
+}
+
+static int erofs_compressor_liblzma_exit(struct erofs_compress *c)
+{
+	struct erofs_liblzma_context *ctx = c->private_data;
+
+	if (!ctx)
+		return -EINVAL;
+
+	lzma_end(&ctx->strm);
+	free(ctx);
+	return 0;
+}
+
+static int erofs_compressor_liblzma_setlevel(struct erofs_compress *c,
+					     int compression_level)
+{
+	struct erofs_liblzma_context *ctx = c->private_data;
+
+	if (compression_level < 0)
+		compression_level = LZMA_PRESET_DEFAULT;
+
+	if (lzma_lzma_preset(&ctx->opt, compression_level))
+		return -EINVAL;
+
+	/* XXX: temporary hack */
+	if (cfg.c_dict_size) {
+		if (cfg.c_dict_size > Z_EROFS_LZMA_MAX_DICT_SIZE) {
+			erofs_err("dict size %u is too large", cfg.c_dict_size);
+			return -EINVAL;
+		}
+		ctx->opt.dict_size = cfg.c_dict_size;
+	} else {
+		if (ctx->opt.dict_size > Z_EROFS_LZMA_MAX_DICT_SIZE)
+			ctx->opt.dict_size = Z_EROFS_LZMA_MAX_DICT_SIZE;
+		cfg.c_dict_size = ctx->opt.dict_size;
+	}
+	c->compression_level = compression_level;
+	return 0;
+}
+
+static int erofs_compressor_liblzma_init(struct erofs_compress *c)
+{
+	struct erofs_liblzma_context *ctx;
+
+	c->alg = &erofs_compressor_lzma;
+	ctx = malloc(sizeof(*ctx));
+	if (!ctx)
+		return -ENOMEM;
+	ctx->strm = (lzma_stream)LZMA_STREAM_INIT;
+	c->private_data = ctx;
+	erofs_warn("EXPERIMENTAL MicroLZMA feature in use. Use at your own risk!");
+	erofs_warn("Note that it may take more time since the compressor is still single-threaded for now.");
+	return 0;
+}
+
+struct erofs_compressor erofs_compressor_lzma = {
+	.name = "lzma",
+	.default_level = LZMA_PRESET_DEFAULT,
+	.best_level = LZMA_PRESET_EXTREME,
+	.init = erofs_compressor_liblzma_init,
+	.exit = erofs_compressor_liblzma_exit,
+	.setlevel = erofs_compressor_liblzma_setlevel,
+	.compress_destsize = erofs_liblzma_compress_destsize,
+};
+#endif
diff --git a/lib/compressor_lz4.c b/lib/compressor_lz4.c
index 8540a0d..f6832be 100644
--- a/lib/compressor_lz4.c
+++ b/lib/compressor_lz4.c
@@ -1,7 +1,5 @@
 // SPDX-License-Identifier: GPL-2.0+
 /*
- * erofs-utils/lib/compressor-lz4.c
- *
  * Copyright (C) 2018-2019 HUAWEI, Inc.
  *             http://www.huawei.com/
  * Created by Gao Xiang <gaoxiang25@huawei.com>
@@ -10,8 +8,11 @@
 #include "erofs/internal.h"
 #include "compressor.h"
 
+#ifndef LZ4_DISTANCE_MAX	/* history window size */
+#define LZ4_DISTANCE_MAX 65535	/* set to maximum value by default */
+#endif
+
 static int lz4_compress_destsize(struct erofs_compress *c,
-				 int compression_level,
 				 void *src, unsigned int *srcsize,
 				 void *dst, unsigned int dstsize)
 {
@@ -32,6 +33,7 @@
 static int compressor_lz4_init(struct erofs_compress *c)
 {
 	c->alg = &erofs_compressor_lz4;
+	sbi.lz4_max_distance = LZ4_DISTANCE_MAX;
 	return 0;
 }
 
@@ -43,4 +45,3 @@
 	.exit = compressor_lz4_exit,
 	.compress_destsize = lz4_compress_destsize,
 };
-
diff --git a/lib/compressor_lz4hc.c b/lib/compressor_lz4hc.c
index 6680563..fd801ab 100644
--- a/lib/compressor_lz4hc.c
+++ b/lib/compressor_lz4hc.c
@@ -1,7 +1,5 @@
 // SPDX-License-Identifier: GPL-2.0+
 /*
- * erofs-utils/lib/compressor-lz4hc.c
- *
  * Copyright (C) 2018-2019 HUAWEI, Inc.
  *             http://www.huawei.com/
  * Created by Gao Xiang <gaoxiang25@huawei.com>
@@ -11,17 +9,18 @@
 #include "erofs/internal.h"
 #include "compressor.h"
 
+#ifndef LZ4_DISTANCE_MAX	/* history window size */
+#define LZ4_DISTANCE_MAX 65535	/* set to maximum value by default */
+#endif
+
 static int lz4hc_compress_destsize(struct erofs_compress *c,
-				   int compression_level,
-				   void *src,
-				   unsigned int *srcsize,
-				   void *dst,
-				   unsigned int dstsize)
+				   void *src, unsigned int *srcsize,
+				   void *dst, unsigned int dstsize)
 {
 	int srcSize = (int)*srcsize;
 	int rc = LZ4_compress_HC_destSize(c->private_data, src, dst,
 					  &srcSize, (int)dstsize,
-					  compression_level);
+					  c->compression_level);
 	if (!rc)
 		return -EFAULT;
 	*srcsize = srcSize;
@@ -44,6 +43,19 @@
 	c->private_data = LZ4_createStreamHC();
 	if (!c->private_data)
 		return -ENOMEM;
+
+	sbi.lz4_max_distance = LZ4_DISTANCE_MAX;
+	return 0;
+}
+
+static int compressor_lz4hc_setlevel(struct erofs_compress *c,
+				     int compression_level)
+{
+	if (compression_level > LZ4HC_CLEVEL_MAX)
+		return -EINVAL;
+
+	c->compression_level = compression_level < 0 ?
+		LZ4HC_CLEVEL_DEFAULT : compression_level;
 	return 0;
 }
 
@@ -53,6 +65,6 @@
 	.best_level = LZ4HC_CLEVEL_MAX,
 	.init = compressor_lz4hc_init,
 	.exit = compressor_lz4hc_exit,
+	.setlevel = compressor_lz4hc_setlevel,
 	.compress_destsize = lz4hc_compress_destsize,
 };
-
diff --git a/lib/config.c b/lib/config.c
index 3ecd481..cc15e57 100644
--- a/lib/config.c
+++ b/lib/config.c
@@ -1,14 +1,14 @@
 // SPDX-License-Identifier: GPL-2.0+
 /*
- * erofs-utils/lib/config.c
- *
  * Copyright (C) 2018-2019 HUAWEI, Inc.
  *             http://www.huawei.com/
  * Created by Li Guifu <bluce.liguifu@huawei.com>
  */
 #include <string.h>
+#include <stdlib.h>
 #include "erofs/print.h"
 #include "erofs/internal.h"
+#include "liberofs_private.h"
 
 struct erofs_configure cfg;
 struct erofs_sb_info sbi;
@@ -17,19 +17,27 @@
 {
 	memset(&cfg, 0, sizeof(cfg));
 
-	cfg.c_dbg_lvl  = 0;
+	cfg.c_dbg_lvl  = EROFS_WARN;
 	cfg.c_version  = PACKAGE_VERSION;
 	cfg.c_dry_run  = false;
+	cfg.c_ignore_mtime = false;
 	cfg.c_compr_level_master = -1;
 	cfg.c_force_inodeversion = 0;
 	cfg.c_inline_xattr_tolerance = 2;
 	cfg.c_unix_timestamp = -1;
+	cfg.c_uid = -1;
+	cfg.c_gid = -1;
+	cfg.c_pclusterblks_max = 1;
+	cfg.c_pclusterblks_def = 1;
+	cfg.c_max_decompressed_extent_bytes = -1;
 }
 
 void erofs_show_config(void)
 {
 	const struct erofs_configure *c = &cfg;
 
+	if (c->c_dbg_lvl < EROFS_WARN)
+		return;
 	erofs_dump("\tc_version:           [%8s]\n", c->c_version);
 	erofs_dump("\tc_dbg_lvl:           [%8d]\n", c->c_dbg_lvl);
 	erofs_dump("\tc_dry_run:           [%8d]\n", c->c_dry_run);
@@ -41,6 +49,8 @@
 	if (cfg.sehnd)
 		selabel_close(cfg.sehnd);
 #endif
+	if (cfg.c_img_path)
+		free(cfg.c_img_path);
 }
 
 static unsigned int fullpath_prefix;	/* root directory prefix length */
@@ -81,4 +91,3 @@
 	return 0;
 }
 #endif
-
diff --git a/lib/data.c b/lib/data.c
index 3781846..27710f9 100644
--- a/lib/data.c
+++ b/lib/data.c
@@ -1,10 +1,9 @@
 // SPDX-License-Identifier: GPL-2.0+
 /*
- * erofs-utils/lib/data.c
- *
  * Copyright (C) 2020 Gao Xiang <hsiangkao@aol.com>
  * Compression support by Huang Jianan <huangjianan@oppo.com>
  */
+#include <stdlib.h>
 #include "erofs/print.h"
 #include "erofs/internal.h"
 #include "erofs/io.h"
@@ -26,12 +25,6 @@
 	nblocks = DIV_ROUND_UP(inode->i_size, PAGE_SIZE);
 	lastblk = nblocks - tailendpacking;
 
-	if (offset >= inode->i_size) {
-		/* leave out-of-bound access unmapped */
-		map->m_flags = 0;
-		goto out;
-	}
-
 	/* there is no hole in flatmode */
 	map->m_flags = EROFS_MAP_MAPPED;
 
@@ -62,56 +55,163 @@
 		goto err_out;
 	}
 
-out:
 	map->m_llen = map->m_plen;
-
 err_out:
 	trace_erofs_map_blocks_flatmode_exit(inode, map, flags, 0);
 	return err;
 }
 
+int erofs_map_blocks(struct erofs_inode *inode,
+		struct erofs_map_blocks *map, int flags)
+{
+	struct erofs_inode *vi = inode;
+	struct erofs_inode_chunk_index *idx;
+	u8 buf[EROFS_BLKSIZ];
+	u64 chunknr;
+	unsigned int unit;
+	erofs_off_t pos;
+	int err = 0;
+
+	map->m_deviceid = 0;
+	if (map->m_la >= inode->i_size) {
+		/* leave out-of-bound access unmapped */
+		map->m_flags = 0;
+		map->m_plen = 0;
+		goto out;
+	}
+
+	if (vi->datalayout != EROFS_INODE_CHUNK_BASED)
+		return erofs_map_blocks_flatmode(inode, map, flags);
+
+	if (vi->u.chunkformat & EROFS_CHUNK_FORMAT_INDEXES)
+		unit = sizeof(*idx);			/* chunk index */
+	else
+		unit = EROFS_BLOCK_MAP_ENTRY_SIZE;	/* block map */
+
+	chunknr = map->m_la >> vi->u.chunkbits;
+	pos = roundup(iloc(vi->nid) + vi->inode_isize +
+		      vi->xattr_isize, unit) + unit * chunknr;
+
+	err = blk_read(0, buf, erofs_blknr(pos), 1);
+	if (err < 0)
+		return -EIO;
+
+	map->m_la = chunknr << vi->u.chunkbits;
+	map->m_plen = min_t(erofs_off_t, 1UL << vi->u.chunkbits,
+			    roundup(inode->i_size - map->m_la, EROFS_BLKSIZ));
+
+	/* handle block map */
+	if (!(vi->u.chunkformat & EROFS_CHUNK_FORMAT_INDEXES)) {
+		__le32 *blkaddr = (void *)buf + erofs_blkoff(pos);
+
+		if (le32_to_cpu(*blkaddr) == EROFS_NULL_ADDR) {
+			map->m_flags = 0;
+		} else {
+			map->m_pa = blknr_to_addr(le32_to_cpu(*blkaddr));
+			map->m_flags = EROFS_MAP_MAPPED;
+		}
+		goto out;
+	}
+	/* parse chunk indexes */
+	idx = (void *)buf + erofs_blkoff(pos);
+	switch (le32_to_cpu(idx->blkaddr)) {
+	case EROFS_NULL_ADDR:
+		map->m_flags = 0;
+		break;
+	default:
+		map->m_deviceid = le16_to_cpu(idx->device_id) &
+			sbi.device_id_mask;
+		map->m_pa = blknr_to_addr(le32_to_cpu(idx->blkaddr));
+		map->m_flags = EROFS_MAP_MAPPED;
+		break;
+	}
+out:
+	map->m_llen = map->m_plen;
+	return err;
+}
+
+int erofs_map_dev(struct erofs_sb_info *sbi, struct erofs_map_dev *map)
+{
+	struct erofs_device_info *dif;
+	int id;
+
+	if (map->m_deviceid) {
+		if (sbi->extra_devices < map->m_deviceid)
+			return -ENODEV;
+	} else if (sbi->extra_devices) {
+		for (id = 0; id < sbi->extra_devices; ++id) {
+			erofs_off_t startoff, length;
+
+			dif = sbi->devs + id;
+			if (!dif->mapped_blkaddr)
+				continue;
+			startoff = blknr_to_addr(dif->mapped_blkaddr);
+			length = blknr_to_addr(dif->blocks);
+
+			if (map->m_pa >= startoff &&
+			    map->m_pa < startoff + length) {
+				map->m_pa -= startoff;
+				break;
+			}
+		}
+	}
+	return 0;
+}
+
 static int erofs_read_raw_data(struct erofs_inode *inode, char *buffer,
 			       erofs_off_t size, erofs_off_t offset)
 {
 	struct erofs_map_blocks map = {
 		.index = UINT_MAX,
 	};
+	struct erofs_map_dev mdev;
 	int ret;
 	erofs_off_t ptr = offset;
 
 	while (ptr < offset + size) {
+		char *const estart = buffer + ptr - offset;
 		erofs_off_t eend;
 
 		map.m_la = ptr;
-		ret = erofs_map_blocks_flatmode(inode, &map, 0);
+		ret = erofs_map_blocks(inode, &map, 0);
 		if (ret)
 			return ret;
 
 		DBG_BUGON(map.m_plen != map.m_llen);
 
-		if (!(map.m_flags & EROFS_MAP_MAPPED)) {
-			if (!map.m_llen) {
-				ptr = offset + size;
-				continue;
-			}
-			ptr = map.m_la + map.m_llen;
-			continue;
-		}
+		mdev = (struct erofs_map_dev) {
+			.m_deviceid = map.m_deviceid,
+			.m_pa = map.m_pa,
+		};
+		ret = erofs_map_dev(&sbi, &mdev);
+		if (ret)
+			return ret;
 
 		/* trim extent */
 		eend = min(offset + size, map.m_la + map.m_llen);
 		DBG_BUGON(ptr < map.m_la);
 
+		if (!(map.m_flags & EROFS_MAP_MAPPED)) {
+			if (!map.m_llen) {
+				/* reached EOF */
+				memset(estart, 0, offset + size - ptr);
+				ptr = offset + size;
+				continue;
+			}
+			memset(estart, 0, eend - ptr);
+			ptr = eend;
+			continue;
+		}
+
 		if (ptr > map.m_la) {
-			map.m_pa += ptr - map.m_la;
+			mdev.m_pa += ptr - map.m_la;
 			map.m_la = ptr;
 		}
 
-		ret = dev_read(buffer + ptr - offset,
-			       map.m_pa, eend - map.m_la);
+		ret = dev_read(mdev.m_deviceid, estart, mdev.m_pa,
+			       eend - map.m_la);
 		if (ret < 0)
 			return -EIO;
-
 		ptr = eend;
 	}
 	return 0;
@@ -120,36 +220,34 @@
 static int z_erofs_read_data(struct erofs_inode *inode, char *buffer,
 			     erofs_off_t size, erofs_off_t offset)
 {
-	int ret;
 	erofs_off_t end, length, skip;
 	struct erofs_map_blocks map = {
 		.index = UINT_MAX,
 	};
+	struct erofs_map_dev mdev;
 	bool partial;
-	unsigned int algorithmformat;
-	char raw[EROFS_BLKSIZ];
+	unsigned int bufsize = 0;
+	char *raw = NULL;
+	int ret = 0;
 
 	end = offset + size;
 	while (end > offset) {
 		map.m_la = end - 1;
 
-		ret = z_erofs_map_blocks_iter(inode, &map);
+		ret = z_erofs_map_blocks_iter(inode, &map, 0);
 		if (ret)
-			return ret;
+			break;
 
-		if (!(map.m_flags & EROFS_MAP_MAPPED)) {
-			end = map.m_la;
-			continue;
+		/* no device id here, thus it will always succeed */
+		mdev = (struct erofs_map_dev) {
+			.m_pa = map.m_pa,
+		};
+		ret = erofs_map_dev(&sbi, &mdev);
+		if (ret) {
+			DBG_BUGON(1);
+			break;
 		}
 
-		ret = dev_read(raw, map.m_pa, EROFS_BLKSIZ);
-		if (ret < 0)
-			return -EIO;
-
-		algorithmformat = map.m_flags & EROFS_MAP_ZIPPED ?
-						Z_EROFS_COMPRESSION_LZ4 :
-						Z_EROFS_COMPRESSION_SHIFTED;
-
 		/*
 		 * trim to the needed size if the returned extent is quite
 		 * larger than requested, and set up partial flag as well.
@@ -171,19 +269,39 @@
 			end = map.m_la;
 		}
 
+		if (!(map.m_flags & EROFS_MAP_MAPPED)) {
+			memset(buffer + end - offset, 0, length);
+			end = map.m_la;
+			continue;
+		}
+
+		if (map.m_plen > bufsize) {
+			bufsize = map.m_plen;
+			raw = realloc(raw, bufsize);
+			if (!raw) {
+				ret = -ENOMEM;
+				break;
+			}
+		}
+		ret = dev_read(mdev.m_deviceid, raw, mdev.m_pa, map.m_plen);
+		if (ret < 0)
+			break;
+
 		ret = z_erofs_decompress(&(struct z_erofs_decompress_req) {
 					.in = raw,
 					.out = buffer + end - offset,
 					.decodedskip = skip,
 					.inputsize = map.m_plen,
 					.decodedlength = length,
-					.alg = algorithmformat,
+					.alg = map.m_algorithmformat,
 					.partial_decoding = partial
 					 });
 		if (ret < 0)
-			return ret;
+			break;
 	}
-	return 0;
+	if (raw)
+		free(raw);
+	return ret < 0 ? ret : 0;
 }
 
 int erofs_pread(struct erofs_inode *inode, char *buf,
@@ -192,6 +310,7 @@
 	switch (inode->datalayout) {
 	case EROFS_INODE_FLAT_PLAIN:
 	case EROFS_INODE_FLAT_INLINE:
+	case EROFS_INODE_CHUNK_BASED:
 		return erofs_read_raw_data(inode, buf, count, offset);
 	case EROFS_INODE_FLAT_COMPRESSION_LEGACY:
 	case EROFS_INODE_FLAT_COMPRESSION:
@@ -201,4 +320,3 @@
 	}
 	return -EINVAL;
 }
-
diff --git a/lib/decompress.c b/lib/decompress.c
index 490c4bc..f313e41 100644
--- a/lib/decompress.c
+++ b/lib/decompress.c
@@ -1,7 +1,5 @@
 // SPDX-License-Identifier: GPL-2.0+
 /*
- * erofs-utils/lib/decompress.c
- *
  * Copyright (C), 2008-2020, OPPO Mobile Comm Corp., Ltd.
  * Created by Huang Jianan <huangjianan@oppo.com>
  */
@@ -9,6 +7,68 @@
 
 #include "erofs/decompress.h"
 #include "erofs/err.h"
+#include "erofs/print.h"
+
+#ifdef HAVE_LIBLZMA
+#include <lzma.h>
+
+static int z_erofs_decompress_lzma(struct z_erofs_decompress_req *rq)
+{
+	int ret = 0;
+	u8 *dest = (u8 *)rq->out;
+	u8 *src = (u8 *)rq->in;
+	u8 *buff = NULL;
+	unsigned int inputmargin = 0;
+	lzma_stream strm;
+	lzma_ret ret2;
+
+	while (!src[inputmargin & ~PAGE_MASK])
+		if (!(++inputmargin & ~PAGE_MASK))
+			break;
+
+	if (inputmargin >= rq->inputsize)
+		return -EFSCORRUPTED;
+
+	if (rq->decodedskip) {
+		buff = malloc(rq->decodedlength);
+		if (!buff)
+			return -ENOMEM;
+		dest = buff;
+	}
+
+	strm = (lzma_stream)LZMA_STREAM_INIT;
+	strm.next_in = src + inputmargin;
+	strm.avail_in = rq->inputsize - inputmargin;
+	strm.next_out = dest;
+	strm.avail_out = rq->decodedlength;
+
+	ret2 = lzma_microlzma_decoder(&strm, strm.avail_in, rq->decodedlength,
+				      !rq->partial_decoding,
+				      Z_EROFS_LZMA_MAX_DICT_SIZE);
+	if (ret2 != LZMA_OK) {
+		erofs_err("fail to initialize lzma decoder %u", ret2 | 0U);
+		ret = -EFAULT;
+		goto out;
+	}
+
+	ret2 = lzma_code(&strm, LZMA_FINISH);
+	if (ret2 != LZMA_STREAM_END) {
+		ret = -EFSCORRUPTED;
+		goto out_lzma_end;
+	}
+
+	if (rq->decodedskip)
+		memcpy(rq->out, dest + rq->decodedskip,
+		       rq->decodedlength - rq->decodedskip);
+
+out_lzma_end:
+	lzma_end(&strm);
+out:
+	if (buff)
+		free(buff);
+	return ret;
+}
+#endif
 
 #ifdef LZ4_ENABLED
 #include <lz4.h>
@@ -84,5 +144,9 @@
 	if (rq->alg == Z_EROFS_COMPRESSION_LZ4)
 		return z_erofs_decompress_lz4(rq);
 #endif
+#ifdef HAVE_LIBLZMA
+	if (rq->alg == Z_EROFS_COMPRESSION_LZMA)
+		return z_erofs_decompress_lzma(rq);
+#endif
 	return -EOPNOTSUPP;
 }
diff --git a/lib/dir.c b/lib/dir.c
new file mode 100644
index 0000000..46805b4
--- /dev/null
+++ b/lib/dir.c
@@ -0,0 +1,169 @@
+// SPDX-License-Identifier: GPL-2.0+ OR Apache-2.0
+#include "erofs/print.h"
+#include "erofs/dir.h"
+#include <stdlib.h>
+#include <sys/stat.h>
+
+static int traverse_dirents(struct erofs_dir_context *ctx,
+			    void *dentry_blk, unsigned int lblk,
+			    unsigned int next_nameoff, unsigned int maxsize,
+			    bool fsck)
+{
+	struct erofs_dirent *de = dentry_blk;
+	const struct erofs_dirent *end = dentry_blk + next_nameoff;
+	const char *prev_name = NULL;
+	const char *errmsg;
+	unsigned int prev_namelen = 0;
+	int ret = 0;
+	bool silent = false;
+
+	while (de < end) {
+		const char *de_name;
+		unsigned int de_namelen;
+		unsigned int nameoff;
+
+		nameoff = le16_to_cpu(de->nameoff);
+		de_name = (char *)dentry_blk + nameoff;
+
+		/* the last dirent check */
+		if (de + 1 >= end)
+			de_namelen = strnlen(de_name, maxsize - nameoff);
+		else
+			de_namelen = le16_to_cpu(de[1].nameoff) - nameoff;
+
+		ctx->de_nid = le64_to_cpu(de->nid);
+		erofs_dbg("traversed nid (%llu)", ctx->de_nid | 0ULL);
+
+		ret = -EFSCORRUPTED;
+		/* corrupted entry check */
+		if (nameoff != next_nameoff) {
+			errmsg = "bogus dirent nameoff";
+			break;
+		}
+
+		if (nameoff + de_namelen > maxsize ||
+				de_namelen > EROFS_NAME_LEN) {
+			errmsg = "bogus dirent namelen";
+			break;
+		}
+
+		if (fsck && prev_name) {
+			int cmp = strncmp(prev_name, de_name,
+					  min(prev_namelen, de_namelen));
+
+			if (cmp > 0 || (cmp == 0 &&
+					prev_namelen >= de_namelen)) {
+				errmsg = "wrong dirent name order";
+				break;
+			}
+		}
+
+		if (fsck && de->file_type >= EROFS_FT_MAX) {
+			errmsg = "invalid file type %u";
+			break;
+		}
+
+		ctx->dname = de_name;
+		ctx->de_namelen = de_namelen;
+		ctx->de_ftype = de->file_type;
+		ctx->dot_dotdot = is_dot_dotdot_len(de_name, de_namelen);
+		if (ctx->dot_dotdot) {
+			switch (de_namelen) {
+			case 2:
+				if (fsck &&
+				    (ctx->flags & EROFS_READDIR_DOTDOT_FOUND)) {
+					errmsg = "duplicated `..' dirent";
+					goto out;
+				}
+				ctx->flags |= EROFS_READDIR_DOTDOT_FOUND;
+				if (sbi.root_nid == ctx->dir->nid) {
+					ctx->pnid = sbi.root_nid;
+					ctx->flags |= EROFS_READDIR_VALID_PNID;
+				}
+				if (fsck &&
+				    (ctx->flags & EROFS_READDIR_VALID_PNID) &&
+				    ctx->de_nid != ctx->pnid) {
+					errmsg = "corrupted `..' dirent";
+					goto out;
+				}
+				break;
+			case 1:
+				if (fsck &&
+				    (ctx->flags & EROFS_READDIR_DOT_FOUND)) {
+					errmsg = "duplicated `.' dirent";
+					goto out;
+				}
+
+				ctx->flags |= EROFS_READDIR_DOT_FOUND;
+				if (fsck && ctx->de_nid != ctx->dir->nid) {
+					errmsg = "corrupted `.' dirent";
+					goto out;
+				}
+				break;
+			}
+		}
+		ret = ctx->cb(ctx);
+		if (ret) {
+			silent = true;
+			break;
+		}
+		prev_name = de_name;
+		prev_namelen = de_namelen;
+		next_nameoff += de_namelen;
+		++de;
+	}
+out:
+	if (ret && !silent)
+		erofs_err("%s @ nid %llu, lblk %u, index %lu",
+			  errmsg, ctx->dir->nid | 0ULL, lblk,
+			  (de - (struct erofs_dirent *)dentry_blk) | 0UL);
+	return ret;
+}
+
+int erofs_iterate_dir(struct erofs_dir_context *ctx, bool fsck)
+{
+	struct erofs_inode *dir = ctx->dir;
+	int err = 0;
+	erofs_off_t pos;
+	char buf[EROFS_BLKSIZ];
+
+	if ((dir->i_mode & S_IFMT) != S_IFDIR)
+		return -ENOTDIR;
+
+	ctx->flags &= ~EROFS_READDIR_ALL_SPECIAL_FOUND;
+	pos = 0;
+	while (pos < dir->i_size) {
+		erofs_blk_t lblk = erofs_blknr(pos);
+		erofs_off_t maxsize = min_t(erofs_off_t,
+					dir->i_size - pos, EROFS_BLKSIZ);
+		const struct erofs_dirent *de = (const void *)buf;
+		unsigned int nameoff;
+
+		err = erofs_pread(dir, buf, maxsize, pos);
+		if (err) {
+			erofs_err("I/O error occurred when reading dirents @ nid %llu, lblk %u: %d",
+				  dir->nid | 0ULL, lblk, err);
+			return err;
+		}
+
+		nameoff = le16_to_cpu(de->nameoff);
+		if (nameoff < sizeof(struct erofs_dirent) ||
+		    nameoff >= PAGE_SIZE) {
+			erofs_err("invalid de[0].nameoff %u @ nid %llu, lblk %u",
+				  nameoff, dir->nid | 0ULL, lblk);
+			return -EFSCORRUPTED;
+		}
+		err = traverse_dirents(ctx, buf, lblk, nameoff, maxsize, fsck);
+		if (err)
+			break;
+		pos += maxsize;
+	}
+
+	if (fsck && (ctx->flags & EROFS_READDIR_ALL_SPECIAL_FOUND) !=
+			EROFS_READDIR_ALL_SPECIAL_FOUND) {
+		erofs_err("`.' or `..' dirent is missing @ nid %llu",
+			  dir->nid | 0ULL);
+		return -EFSCORRUPTED;
+	}
+	return err;
+}
diff --git a/lib/exclude.c b/lib/exclude.c
index 73b3720..2f980a3 100644
--- a/lib/exclude.c
+++ b/lib/exclude.c
@@ -1,7 +1,5 @@
 // SPDX-License-Identifier: GPL-2.0+
 /*
- * erofs-utils/lib/exclude.c
- *
  * Created by Li Guifu <bluce.lee@aliyun.com>
  */
 #include <string.h>
@@ -129,4 +127,3 @@
 	}
 	return NULL;
 }
-
diff --git a/lib/hashmap.c b/lib/hashmap.c
new file mode 100644
index 0000000..e11bd8d
--- /dev/null
+++ b/lib/hashmap.c
@@ -0,0 +1,284 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copied from https://github.com/git/git.git
+ * Generic implementation of hash-based key value mappings.
+ */
+#include "erofs/hashmap.h"
+
+#define FNV32_BASE ((unsigned int)0x811c9dc5)
+#define FNV32_PRIME ((unsigned int)0x01000193)
+
+unsigned int strhash(const char *str)
+{
+	unsigned int c, hash = FNV32_BASE;
+
+	while ((c = (unsigned char)*str++))
+		hash = (hash * FNV32_PRIME) ^ c;
+	return hash;
+}
+
+unsigned int strihash(const char *str)
+{
+	unsigned int c, hash = FNV32_BASE;
+
+	while ((c = (unsigned char)*str++)) {
+		if (c >= 'a' && c <= 'z')
+			c -= 'a' - 'A';
+		hash = (hash * FNV32_PRIME) ^ c;
+	}
+	return hash;
+}
+
+unsigned int memhash(const void *buf, size_t len)
+{
+	unsigned int hash = FNV32_BASE;
+	unsigned char *ucbuf = (unsigned char *)buf;
+
+	while (len--) {
+		unsigned int c = *ucbuf++;
+
+		hash = (hash * FNV32_PRIME) ^ c;
+	}
+	return hash;
+}
+
+unsigned int memihash(const void *buf, size_t len)
+{
+	unsigned int hash = FNV32_BASE;
+	unsigned char *ucbuf = (unsigned char *)buf;
+
+	while (len--) {
+		unsigned int c = *ucbuf++;
+
+		if (c >= 'a' && c <= 'z')
+			c -= 'a' - 'A';
+		hash = (hash * FNV32_PRIME) ^ c;
+	}
+	return hash;
+}
+
+#define HASHMAP_INITIAL_SIZE 64
+/* grow / shrink by 2^2 */
+#define HASHMAP_RESIZE_BITS 2
+/* load factor in percent */
+#define HASHMAP_LOAD_FACTOR 80
+
+static void alloc_table(struct hashmap *map, unsigned int size)
+{
+	map->tablesize = size;
+	map->table = calloc(size, sizeof(struct hashmap_entry *));
+	BUG_ON(!map->table);
+
+	/* calculate resize thresholds for new size */
+	map->grow_at = (unsigned int)((uint64_t)size * HASHMAP_LOAD_FACTOR / 100);
+	if (size <= HASHMAP_INITIAL_SIZE)
+		map->shrink_at = 0;
+	else
+		/*
+		 * The shrink-threshold must be slightly smaller than
+		 * (grow-threshold / resize-factor) to prevent erratic resizing,
+		 * thus we divide by (resize-factor + 1).
+		 */
+		map->shrink_at = map->grow_at / ((1 << HASHMAP_RESIZE_BITS) + 1);
+}
+
+static inline int entry_equals(const struct hashmap *map,
+			       const struct hashmap_entry *e1,
+			       const struct hashmap_entry *e2,
+			       const void *keydata)
+{
+	return (e1 == e2) || (e1->hash == e2->hash && !map->cmpfn(e1, e2, keydata));
+}
+
+static inline unsigned int bucket(const struct hashmap *map,
+				  const struct hashmap_entry *key)
+{
+	return key->hash & (map->tablesize - 1);
+}
+
+static void rehash(struct hashmap *map, unsigned int newsize)
+{
+	unsigned int i, oldsize = map->tablesize;
+	struct hashmap_entry **oldtable = map->table;
+
+	alloc_table(map, newsize);
+	for (i = 0; i < oldsize; i++) {
+		struct hashmap_entry *e = oldtable[i];
+
+		while (e) {
+			struct hashmap_entry *next = e->next;
+			unsigned int b = bucket(map, e);
+
+			e->next = map->table[b];
+			map->table[b] = e;
+			e = next;
+		}
+	}
+	free(oldtable);
+}
+
+static inline struct hashmap_entry **find_entry_ptr(const struct hashmap *map,
+						    const struct hashmap_entry *key,
+						    const void *keydata)
+{
+	struct hashmap_entry **e = &map->table[bucket(map, key)];
+
+	while (*e && !entry_equals(map, *e, key, keydata))
+		e = &(*e)->next;
+	return e;
+}
+
+static int always_equal(const void *unused1, const void *unused2, const void *unused3)
+{
+	return 0;
+}
+
+void hashmap_init(struct hashmap *map, hashmap_cmp_fn equals_function,
+		  size_t initial_size)
+{
+	unsigned int size = HASHMAP_INITIAL_SIZE;
+
+	map->size = 0;
+	map->cmpfn = equals_function ? equals_function : always_equal;
+
+	/* calculate initial table size and allocate the table */
+	initial_size = (unsigned int)((uint64_t)initial_size * 100
+			/ HASHMAP_LOAD_FACTOR);
+	while (initial_size > size)
+		size <<= HASHMAP_RESIZE_BITS;
+	alloc_table(map, size);
+}
+
+void hashmap_free(struct hashmap *map, int free_entries)
+{
+	if (!map || !map->table)
+		return;
+	if (free_entries) {
+		struct hashmap_iter iter;
+		struct hashmap_entry *e;
+
+		hashmap_iter_init(map, &iter);
+		while ((e = hashmap_iter_next(&iter)))
+			free(e);
+	}
+	free(map->table);
+	memset(map, 0, sizeof(*map));
+}
+
+void *hashmap_get(const struct hashmap *map, const void *key, const void *keydata)
+{
+	return *find_entry_ptr(map, key, keydata);
+}
+
+void *hashmap_get_next(const struct hashmap *map, const void *entry)
+{
+	struct hashmap_entry *e = ((struct hashmap_entry *)entry)->next;
+
+	for (; e; e = e->next)
+		if (entry_equals(map, entry, e, NULL))
+			return e;
+	return NULL;
+}
+
+void hashmap_add(struct hashmap *map, void *entry)
+{
+	unsigned int b = bucket(map, entry);
+
+	/* add entry */
+	((struct hashmap_entry *)entry)->next = map->table[b];
+	map->table[b] = entry;
+
+	/* fix size and rehash if appropriate */
+	map->size++;
+	if (map->size > map->grow_at)
+		rehash(map, map->tablesize << HASHMAP_RESIZE_BITS);
+}
+
+void *hashmap_remove(struct hashmap *map, const void *key, const void *keydata)
+{
+	struct hashmap_entry *old;
+	struct hashmap_entry **e = find_entry_ptr(map, key, keydata);
+
+	if (!*e)
+		return NULL;
+
+	/* remove existing entry */
+	old = *e;
+	*e = old->next;
+	old->next = NULL;
+
+	/* fix size and rehash if appropriate */
+	map->size--;
+	if (map->size < map->shrink_at)
+		rehash(map, map->tablesize >> HASHMAP_RESIZE_BITS);
+	return old;
+}
+
+void *hashmap_put(struct hashmap *map, void *entry)
+{
+	struct hashmap_entry *old = hashmap_remove(map, entry, NULL);
+
+	hashmap_add(map, entry);
+	return old;
+}
+
+void hashmap_iter_init(struct hashmap *map, struct hashmap_iter *iter)
+{
+	iter->map = map;
+	iter->tablepos = 0;
+	iter->next = NULL;
+}
+
+void *hashmap_iter_next(struct hashmap_iter *iter)
+{
+	struct hashmap_entry *current = iter->next;
+
+	for (;;) {
+		if (current) {
+			iter->next = current->next;
+			return current;
+		}
+
+		if (iter->tablepos >= iter->map->tablesize)
+			return NULL;
+
+		current = iter->map->table[iter->tablepos++];
+	}
+}
+
+struct pool_entry {
+	struct hashmap_entry ent;
+	size_t len;
+	unsigned char data[FLEX_ARRAY];
+};
+
+static int pool_entry_cmp(const struct pool_entry *e1,
+			  const struct pool_entry *e2,
+			  const unsigned char *keydata)
+{
+	return e1->data != keydata &&
+	       (e1->len != e2->len || memcmp(e1->data, keydata, e1->len));
+}
+
+const void *memintern(const void *data, size_t len)
+{
+	static struct hashmap map;
+	struct pool_entry key, *e;
+
+	/* initialize string pool hashmap */
+	if (!map.tablesize)
+		hashmap_init(&map, (hashmap_cmp_fn)pool_entry_cmp, 0);
+
+	/* lookup interned string in pool */
+	hashmap_entry_init(&key, memhash(data, len));
+	key.len = len;
+	e = hashmap_get(&map, &key, data);
+	if (!e) {
+		/* not found: create it */
+		FLEX_ALLOC_MEM(e, data, data, len);
+		hashmap_entry_init(e, key.ent.hash);
+		e->len = len;
+		hashmap_add(&map, e);
+	}
+	return e->data;
+}
diff --git a/lib/inode.c b/lib/inode.c
index d0b4d51..77ea8bf 100644
--- a/lib/inode.c
+++ b/lib/inode.c
@@ -1,7 +1,5 @@
 // SPDX-License-Identifier: GPL-2.0+
 /*
- * erofs-utils/lib/inode.c
- *
  * Copyright (C) 2018-2019 HUAWEI, Inc.
  *             http://www.huawei.com/
  * Created by Li Guifu <bluce.liguifu@huawei.com>
@@ -12,7 +10,10 @@
 #include <stdlib.h>
 #include <stdio.h>
 #include <sys/stat.h>
+#include <config.h>
+#if defined(HAVE_SYS_SYSMACROS_H)
 #include <sys/sysmacros.h>
+#endif
 #include <dirent.h>
 #include "erofs/print.h"
 #include "erofs/inode.h"
@@ -21,6 +22,10 @@
 #include "erofs/compress.h"
 #include "erofs/xattr.h"
 #include "erofs/exclude.h"
+#include "erofs/block_list.h"
+#include "erofs/compress_hints.h"
+#include "erofs/blobchunk.h"
+#include "liberofs_private.h"
 
 #define S_SHIFT                 12
 static unsigned char erofs_ftype_by_mode[S_IFMT >> S_SHIFT] = {
@@ -33,7 +38,7 @@
 	[S_IFLNK >> S_SHIFT]  = EROFS_FT_SYMLINK,
 };
 
-static unsigned char erofs_mode_to_ftype(umode_t mode)
+unsigned char erofs_mode_to_ftype(umode_t mode)
 {
 	return erofs_ftype_by_mode[(mode & S_IFMT) >> S_SHIFT];
 }
@@ -96,21 +101,6 @@
 	return 0;
 }
 
-static int dentry_add_sorted(struct erofs_dentry *d, struct list_head *head)
-{
-	struct list_head *pos;
-
-	list_for_each(pos, head) {
-		struct erofs_dentry *d2 =
-			container_of(pos, struct erofs_dentry, d_child);
-
-		if (strcmp(d->name, d2->name) < 0)
-			break;
-	}
-	list_add_tail(&d->d_child, pos);
-	return 0;
-}
-
 struct erofs_dentry *erofs_d_alloc(struct erofs_inode *parent,
 				   const char *name)
 {
@@ -122,7 +112,7 @@
 	strncpy(d->name, name, EROFS_NAME_LEN - 1);
 	d->name[EROFS_NAME_LEN - 1] = '\0';
 
-	dentry_add_sorted(d, &parent->i_subdirs);
+	list_add_tail(&d->d_child, &parent->i_subdirs);
 	return d;
 }
 
@@ -134,7 +124,7 @@
 	int ret;
 
 	if (!nblocks) {
-		/* it has only tail-end inlined data */
+		/* it has only tail-end data */
 		inode->u.i_blkaddr = NULL_ADDR;
 		return 0;
 	}
@@ -148,7 +138,7 @@
 	inode->bh_data = bh;
 
 	/* get blkaddr of the bh */
-	ret = erofs_mapbh(bh->block, true);
+	ret = erofs_mapbh(bh->block);
 	DBG_BUGON(ret < 0);
 
 	/* write blocks except for the tail-end block */
@@ -156,22 +146,51 @@
 	return 0;
 }
 
-int erofs_prepare_dir_file(struct erofs_inode *dir)
+static int comp_subdir(const void *a, const void *b)
 {
-	struct erofs_dentry *d;
-	unsigned int d_size, i_nlink;
+	const struct erofs_dentry *da, *db;
+
+	da = *((const struct erofs_dentry **)a);
+	db = *((const struct erofs_dentry **)b);
+	return strcmp(da->name, db->name);
+}
+
+int erofs_prepare_dir_file(struct erofs_inode *dir, unsigned int nr_subdirs)
+{
+	struct erofs_dentry *d, *n, **sorted_d;
+	unsigned int d_size, i_nlink, i;
 	int ret;
 
 	/* dot is pointed to the current dir inode */
 	d = erofs_d_alloc(dir, ".");
+	if (IS_ERR(d))
+		return PTR_ERR(d);
 	d->inode = erofs_igrab(dir);
 	d->type = EROFS_FT_DIR;
 
 	/* dotdot is pointed to the parent dir */
 	d = erofs_d_alloc(dir, "..");
+	if (IS_ERR(d))
+		return PTR_ERR(d);
 	d->inode = erofs_igrab(dir->i_parent);
 	d->type = EROFS_FT_DIR;
 
+	/* sort subdirs */
+	nr_subdirs += 2;
+	sorted_d = malloc(nr_subdirs * sizeof(d));
+	if (!sorted_d)
+		return -ENOMEM;
+	i = 0;
+	list_for_each_entry_safe(d, n, &dir->i_subdirs, d_child) {
+		list_del(&d->d_child);
+		sorted_d[i++] = d;
+	}
+	DBG_BUGON(i != nr_subdirs);
+	qsort(sorted_d, nr_subdirs, sizeof(d), comp_subdir);
+	for (i = 0; i < nr_subdirs; i++)
+		list_add_tail(&sorted_d[i]->d_child, &dir->i_subdirs);
+	free(sorted_d);
+
 	/* let's calculate dir size and update i_nlink */
 	d_size = 0;
 	i_nlink = 0;
@@ -241,7 +260,7 @@
 	return blk_write(buf, blkaddr, 1);
 }
 
-int erofs_write_dir_file(struct erofs_inode *dir)
+static int erofs_write_dir_file(struct erofs_inode *dir)
 {
 	struct erofs_dentry *head = list_first_entry(&dir->i_subdirs,
 						     struct erofs_dentry,
@@ -288,7 +307,7 @@
 	return 0;
 }
 
-int erofs_write_file_from_buffer(struct erofs_inode *inode, char *buf)
+static int erofs_write_file_from_buffer(struct erofs_inode *inode, char *buf)
 {
 	const unsigned int nblocks = erofs_blknr(inode->i_size);
 	int ret;
@@ -315,6 +334,8 @@
 /* rules to decide whether a file could be compressed or not */
 static bool erofs_file_is_compressible(struct erofs_inode *inode)
 {
+	if (cfg.c_compress_hints_file)
+		return z_erofs_apply_compress_hints(inode);
 	return true;
 }
 
@@ -359,6 +380,7 @@
 			return -EIO;
 		}
 	}
+	erofs_droid_blocklist_write(inode, inode->u.i_blkaddr, nblocks);
 	return 0;
 }
 
@@ -371,6 +393,15 @@
 		return 0;
 	}
 
+	if (cfg.c_chunkbits) {
+		inode->u.chunkbits = cfg.c_chunkbits;
+		/* chunk indexes when explicitly specified */
+		inode->u.chunkformat = 0;
+		if (cfg.c_force_chunkformat == FORCE_INODE_CHUNK_INDEXES)
+			inode->u.chunkformat = EROFS_CHUNK_FORMAT_INDEXES;
+		return erofs_blob_write_chunked_file(inode);
+	}
+
 	if (cfg.c_compr_alg_master && erofs_file_is_compressible(inode)) {
 		ret = erofs_write_compressed_file(inode);
 
@@ -412,7 +443,7 @@
 		u.dic.i_uid = cpu_to_le16((u16)inode->i_uid);
 		u.dic.i_gid = cpu_to_le16((u16)inode->i_gid);
 
-		switch ((inode->i_mode) >> S_SHIFT) {
+		switch (inode->i_mode & S_IFMT) {
 		case S_IFCHR:
 		case S_IFBLK:
 		case S_IFIFO:
@@ -424,6 +455,10 @@
 			if (is_inode_layout_compression(inode))
 				u.dic.i_u.compressed_blocks =
 					cpu_to_le32(inode->u.i_blocks);
+			else if (inode->datalayout ==
+					EROFS_INODE_CHUNK_BASED)
+				u.dic.i_u.c.format =
+					cpu_to_le16(inode->u.chunkformat);
 			else
 				u.dic.i_u.raw_blkaddr =
 					cpu_to_le32(inode->u.i_blkaddr);
@@ -439,13 +474,13 @@
 
 		u.die.i_ino = cpu_to_le32(inode->i_ino[0]);
 
-		u.die.i_uid = cpu_to_le16(inode->i_uid);
-		u.die.i_gid = cpu_to_le16(inode->i_gid);
+		u.die.i_uid = cpu_to_le32(inode->i_uid);
+		u.die.i_gid = cpu_to_le32(inode->i_gid);
 
-		u.die.i_ctime = cpu_to_le64(inode->i_ctime);
-		u.die.i_ctime_nsec = cpu_to_le32(inode->i_ctime_nsec);
+		u.die.i_mtime = cpu_to_le64(inode->i_mtime);
+		u.die.i_mtime_nsec = cpu_to_le32(inode->i_mtime_nsec);
 
-		switch ((inode->i_mode) >> S_SHIFT) {
+		switch (inode->i_mode & S_IFMT) {
 		case S_IFCHR:
 		case S_IFBLK:
 		case S_IFIFO:
@@ -457,6 +492,10 @@
 			if (is_inode_layout_compression(inode))
 				u.die.i_u.compressed_blocks =
 					cpu_to_le32(inode->u.i_blocks);
+			else if (inode->datalayout ==
+					EROFS_INODE_CHUNK_BASED)
+				u.die.i_u.c.format =
+					cpu_to_le16(inode->u.chunkformat);
 			else
 				u.die.i_u.raw_blkaddr =
 					cpu_to_le32(inode->u.i_blkaddr);
@@ -489,12 +528,19 @@
 	}
 
 	if (inode->extent_isize) {
-		/* write compression metadata */
-		off = Z_EROFS_VLE_EXTENT_ALIGN(off);
-		ret = dev_write(inode->compressmeta, off, inode->extent_isize);
-		if (ret)
-			return false;
-		free(inode->compressmeta);
+		if (inode->datalayout == EROFS_INODE_CHUNK_BASED) {
+			ret = erofs_blob_write_chunk_indexes(inode, off);
+			if (ret)
+				return false;
+		} else {
+			/* write compression metadata */
+			off = Z_EROFS_VLE_EXTENT_ALIGN(off);
+			ret = dev_write(inode->compressmeta, off,
+					inode->extent_isize);
+			if (ret)
+				return false;
+			free(inode->compressmeta);
+		}
 	}
 
 	inode->bh = NULL;
@@ -506,7 +552,7 @@
 	.flush = erofs_bh_flush_write_inode,
 };
 
-int erofs_prepare_tail_block(struct erofs_inode *inode)
+static int erofs_prepare_tail_block(struct erofs_inode *inode)
 {
 	struct erofs_buffer_head *bh;
 	int ret;
@@ -522,7 +568,7 @@
 		bh->op = &erofs_skip_write_bhops;
 
 		/* get blkaddr of bh */
-		ret = erofs_mapbh(bh->block, true);
+		ret = erofs_mapbh(bh->block);
 		DBG_BUGON(ret < 0);
 		inode->u.i_blkaddr = bh->block->blkaddr;
 
@@ -531,11 +577,11 @@
 	}
 	/* expend a block as the tail block (should be successful) */
 	ret = erofs_bh_balloon(bh, EROFS_BLKSIZ);
-	DBG_BUGON(ret);
+	DBG_BUGON(ret != EROFS_BLKSIZ);
 	return 0;
 }
 
-int erofs_prepare_inode_buffer(struct erofs_inode *inode)
+static int erofs_prepare_inode_buffer(struct erofs_inode *inode)
 {
 	unsigned int inodesize;
 	struct erofs_buffer_head *bh, *ibh;
@@ -549,6 +595,13 @@
 
 	if (is_inode_layout_compression(inode))
 		goto noinline;
+	if (inode->datalayout == EROFS_INODE_CHUNK_BASED)
+		goto noinline;
+
+	if (cfg.c_noinline_data && S_ISREG(inode->i_mode)) {
+		inode->datalayout = EROFS_INODE_FLAT_PLAIN;
+		goto noinline;
+	}
 
 	/*
 	 * if the file size is block-aligned for uncompressed files,
@@ -613,7 +666,7 @@
 	.flush = erofs_bh_flush_write_inline,
 };
 
-int erofs_write_tail_end(struct erofs_inode *inode)
+static int erofs_write_tail_end(struct erofs_inode *inode)
 {
 	struct erofs_buffer_head *bh, *ibh;
 
@@ -628,11 +681,13 @@
 
 		ibh->fsprivate = erofs_igrab(inode);
 		ibh->op = &erofs_write_inline_bhops;
+
+		erofs_droid_blocklist_write_tail_end(inode, NULL_ADDR);
 	} else {
 		int ret;
 		erofs_off_t pos;
 
-		erofs_mapbh(bh->block, true);
+		erofs_mapbh(bh->block);
 		pos = erofs_btell(bh, true) - EROFS_BLKSIZ;
 		ret = dev_write(inode->idata, pos, inode->idata_size);
 		if (ret)
@@ -647,6 +702,8 @@
 		inode->idata_size = 0;
 		free(inode->idata);
 		inode->idata = NULL;
+
+		erofs_droid_blocklist_write_tail_end(inode, erofs_blknr(pos));
 	}
 out:
 	/* now bh_data can drop directly */
@@ -655,11 +712,7 @@
 		 * Don't leave DATA buffers which were written in the global
 		 * buffer list. It will make balloc() slowly.
 		 */
-#if 0
-		bh->op = &erofs_drop_directly_bhops;
-#else
 		erofs_bdrop(bh, false);
-#endif
 		inode->bh_data = NULL;
 	}
 	return 0;
@@ -677,6 +730,10 @@
 		return true;
 	if (inode->i_nlink > USHRT_MAX)
 		return true;
+	if ((inode->i_mtime != sbi.build_time ||
+	     inode->i_mtime_nsec != sbi.build_time_nsec) &&
+	    !cfg.c_ignore_mtime)
+		return true;
 	return false;
 }
 
@@ -723,8 +780,7 @@
 			  cfg.target_out_path,
 			  &uid, &gid, &mode, &inode->capabilities);
 
-	erofs_dbg("/%s -> mode = 0x%x, uid = 0x%x, gid = 0x%x, "
-		  "capabilities = 0x%" PRIx64 "\n",
+	erofs_dbg("/%s -> mode = 0x%x, uid = 0x%x, gid = 0x%x, capabilities = 0x%" PRIx64,
 		  fspath, mode, uid, gid, inode->capabilities);
 
 	if (decorated)
@@ -743,27 +799,27 @@
 }
 #endif
 
-int erofs_fill_inode(struct erofs_inode *inode,
-		     struct stat64 *st,
-		     const char *path)
+static int erofs_fill_inode(struct erofs_inode *inode,
+			    struct stat64 *st,
+			    const char *path)
 {
 	int err = erofs_droid_inode_fsconfig(inode, st, path);
 
 	if (err)
 		return err;
 	inode->i_mode = st->st_mode;
-	inode->i_uid = st->st_uid;
-	inode->i_gid = st->st_gid;
-	inode->i_ctime = st->st_ctime;
-	inode->i_ctime_nsec = st->st_ctim.tv_nsec;
+	inode->i_uid = cfg.c_uid == -1 ? st->st_uid : cfg.c_uid;
+	inode->i_gid = cfg.c_gid == -1 ? st->st_gid : cfg.c_gid;
+	inode->i_mtime = st->st_mtime;
+	inode->i_mtime_nsec = ST_MTIM_NSEC(st);
 
 	switch (cfg.c_timeinherit) {
 	case TIMESTAMP_CLAMPING:
-		if (st->st_ctime < sbi.build_time)
+		if (inode->i_mtime < sbi.build_time)
 			break;
 	case TIMESTAMP_FIXED:
-		inode->i_ctime = sbi.build_time;
-		inode->i_ctime_nsec = sbi.build_time_nsec;
+		inode->i_mtime = sbi.build_time;
+		inode->i_mtime_nsec = sbi.build_time_nsec;
 	default:
 		break;
 	}
@@ -809,34 +865,24 @@
 	return 0;
 }
 
-struct erofs_inode *erofs_new_inode(void)
+static struct erofs_inode *erofs_new_inode(void)
 {
-	static unsigned int counter;
 	struct erofs_inode *inode;
 
-	inode = malloc(sizeof(struct erofs_inode));
+	inode = calloc(1, sizeof(struct erofs_inode));
 	if (!inode)
 		return ERR_PTR(-ENOMEM);
 
-	inode->i_parent = NULL;	/* also used to indicate a new inode */
-
-	inode->i_ino[0] = counter++;	/* inode serial number */
+	inode->i_ino[0] = sbi.inos++;	/* inode serial number */
 	inode->i_count = 1;
 
 	init_list_head(&inode->i_subdirs);
 	init_list_head(&inode->i_xattrs);
-
-	inode->idata_size = 0;
-	inode->xattr_isize = 0;
-	inode->extent_isize = 0;
-
-	inode->bh = inode->bh_inline = inode->bh_data = NULL;
-	inode->idata = NULL;
 	return inode;
 }
 
 /* get the inode from the (source) path */
-struct erofs_inode *erofs_iget_from_path(const char *path, bool is_src)
+static struct erofs_inode *erofs_iget_from_path(const char *path, bool is_src)
 {
 	struct stat64 st;
 	struct erofs_inode *inode;
@@ -867,19 +913,21 @@
 		return inode;
 
 	ret = erofs_fill_inode(inode, &st, path);
-	if (ret)
+	if (ret) {
+		free(inode);
 		return ERR_PTR(ret);
+	}
 
 	return inode;
 }
 
-void erofs_fixup_meta_blkaddr(struct erofs_inode *rootdir)
+static void erofs_fixup_meta_blkaddr(struct erofs_inode *rootdir)
 {
 	const erofs_off_t rootnid_maxoffset = 0xffff << EROFS_ISLOTBITS;
 	struct erofs_buffer_head *const bh = rootdir->bh;
 	erofs_off_t off, meta_offset;
 
-	erofs_mapbh(bh->block, true);
+	erofs_mapbh(bh->block);
 	off = erofs_btell(bh, false);
 
 	if (off > rootnid_maxoffset)
@@ -898,7 +946,7 @@
 	if (!bh)
 		return inode->nid;
 
-	erofs_mapbh(bh->block, true);
+	erofs_mapbh(bh->block);
 	off = erofs_btell(bh, false);
 
 	meta_offset = blknr_to_addr(sbi.meta_blkaddr);
@@ -906,7 +954,7 @@
 	return inode->nid = (off - meta_offset) >> EROFS_ISLOTBITS;
 }
 
-void erofs_d_invalidate(struct erofs_dentry *d)
+static void erofs_d_invalidate(struct erofs_dentry *d)
 {
 	struct erofs_inode *const inode = d->inode;
 
@@ -914,12 +962,13 @@
 	erofs_iput(inode);
 }
 
-struct erofs_inode *erofs_mkfs_build_tree(struct erofs_inode *dir)
+static struct erofs_inode *erofs_mkfs_build_tree(struct erofs_inode *dir)
 {
 	int ret;
 	DIR *_dir;
 	struct dirent *dp;
 	struct erofs_dentry *d;
+	unsigned int nr_subdirs;
 
 	ret = erofs_prepare_xattr_ibody(dir);
 	if (ret < 0)
@@ -954,11 +1003,12 @@
 
 	_dir = opendir(dir->i_srcpath);
 	if (!_dir) {
-		erofs_err("%s, failed to opendir at %s: %s",
-			  __func__, dir->i_srcpath, erofs_strerror(errno));
+		erofs_err("failed to opendir at %s: %s",
+			  dir->i_srcpath, erofs_strerror(errno));
 		return ERR_PTR(-errno);
 	}
 
+	nr_subdirs = 0;
 	while (1) {
 		/*
 		 * set errno to 0 before calling readdir() in order to
@@ -982,6 +1032,7 @@
 			ret = PTR_ERR(d);
 			goto err_closedir;
 		}
+		nr_subdirs++;
 
 		/* to count i_nlink for directories */
 		d->type = (dp->d_type == DT_DIR ?
@@ -994,7 +1045,7 @@
 	}
 	closedir(_dir);
 
-	ret = erofs_prepare_dir_file(dir);
+	ret = erofs_prepare_dir_file(dir, nr_subdirs);
 	if (ret)
 		goto err;
 
@@ -1071,4 +1122,3 @@
 
 	return erofs_mkfs_build_tree(inode);
 }
-
diff --git a/lib/io.c b/lib/io.c
index d835f34..a0d366a 100644
--- a/lib/io.c
+++ b/lib/io.c
@@ -1,7 +1,5 @@
 // SPDX-License-Identifier: GPL-2.0+
 /*
- * erofs-utils/lib/io.c
- *
  * Copyright (C) 2018 HUAWEI, Inc.
  *             http://www.huawei.com/
  * Created by Li Guifu <bluce.liguifu@huawei.com>
@@ -22,12 +20,13 @@
 #include <linux/falloc.h>
 #endif
 
-#define pr_fmt(fmt) "EROFS IO: " FUNC_LINE_FMT fmt "\n"
+#define EROFS_MODNAME	"erofs_io"
 #include "erofs/print.h"
 
 static const char *erofs_devname;
-static int erofs_devfd = -1;
+int erofs_devfd = -1;
 static u64 erofs_devsz;
+static unsigned int erofs_nblobs, erofs_blobfd[256];
 
 int dev_get_blkdev_size(int fd, u64 *bytes)
 {
@@ -108,6 +107,30 @@
 	return 0;
 }
 
+void blob_closeall(void)
+{
+	unsigned int i;
+
+	for (i = 0; i < erofs_nblobs; ++i)
+		close(erofs_blobfd[i]);
+	erofs_nblobs = 0;
+}
+
+int blob_open_ro(const char *dev)
+{
+	int fd = open(dev, O_RDONLY | O_BINARY);
+
+	if (fd < 0) {
+		erofs_err("failed to open(%s).", dev);
+		return -errno;
+	}
+
+	erofs_blobfd[erofs_nblobs] = fd;
+	erofs_info("successfully to open blob%u %s", erofs_nblobs, dev);
+	++erofs_nblobs;
+	return 0;
+}
+
 /* XXX: temporary soluation. Disk I/O implementation needs to be refactored. */
 int dev_open_ro(const char *dev)
 {
@@ -148,7 +171,11 @@
 		return -EINVAL;
 	}
 
+#ifdef HAVE_PWRITE64
 	ret = pwrite64(erofs_devfd, buf, len, (off64_t)offset);
+#else
+	ret = pwrite(erofs_devfd, buf, len, (off_t)offset);
+#endif
 	if (ret != (int)len) {
 		if (ret < 0) {
 			erofs_err("Failed to write data into device - %s:[%" PRIu64 ", %zd].",
@@ -227,9 +254,9 @@
 	return dev_fillzero(st.st_size, length, true);
 }
 
-int dev_read(void *buf, u64 offset, size_t len)
+int dev_read(int device_id, void *buf, u64 offset, size_t len)
 {
-	int ret;
+	int ret, fd;
 
 	if (cfg.c_dry_run)
 		return 0;
@@ -238,15 +265,22 @@
 		erofs_err("buf is NULL");
 		return -EINVAL;
 	}
-	if (offset >= erofs_devsz || len > erofs_devsz ||
-	    offset > erofs_devsz - len) {
-		erofs_err("read posion[%" PRIu64 ", %zd] is too large beyond"
-			  "the end of device(%" PRIu64 ").",
-			  offset, len, erofs_devsz);
-		return -EINVAL;
+
+	if (!device_id) {
+		fd = erofs_devfd;
+	} else {
+		if (device_id > erofs_nblobs) {
+			erofs_err("invalid device id %d", device_id);
+			return -ENODEV;
+		}
+		fd = erofs_blobfd[device_id - 1];
 	}
 
-	ret = pread64(erofs_devfd, buf, len, (off64_t)offset);
+#ifdef HAVE_PREAD64
+	ret = pread64(fd, buf, len, (off64_t)offset);
+#else
+	ret = pread(fd, buf, len, (off_t)offset);
+#endif
 	if (ret != (int)len) {
 		erofs_err("Failed to read data from device - %s:[%" PRIu64 ", %zd].",
 			  erofs_devname, offset, len);
@@ -254,3 +288,98 @@
 	}
 	return 0;
 }
+
+static ssize_t __erofs_copy_file_range(int fd_in, erofs_off_t *off_in,
+				       int fd_out, erofs_off_t *off_out,
+				       size_t length)
+{
+	size_t copied = 0;
+	char buf[8192];
+
+	/*
+	 * Main copying loop.  The buffer size is arbitrary and is a
+	 * trade-off between stack size consumption, cache usage, and
+	 * amortization of system call overhead.
+	 */
+	while (length > 0) {
+		size_t to_read;
+		ssize_t read_count;
+		char *end, *p;
+
+		to_read = min_t(size_t, length, sizeof(buf));
+#ifdef HAVE_PREAD64
+		read_count = pread64(fd_in, buf, to_read, *off_in);
+#else
+		read_count = pread(fd_in, buf, to_read, *off_in);
+#endif
+		if (read_count == 0)
+			/* End of file reached prematurely. */
+			return copied;
+		if (read_count < 0) {
+			/* Report the number of bytes copied so far. */
+			if (copied > 0)
+				return copied;
+			return -1;
+		}
+		*off_in += read_count;
+
+		/* Write the buffer part which was read to the destination. */
+		end = buf + read_count;
+		for (p = buf; p < end; ) {
+			ssize_t write_count;
+
+#ifdef HAVE_PWRITE64
+			write_count = pwrite64(fd_out, p, end - p, *off_out);
+#else
+			write_count = pwrite(fd_out, p, end - p, *off_out);
+#endif
+			if (write_count < 0) {
+				/*
+				 * Adjust the input read position to match what
+				 * we have written, so that the caller can pick
+				 * up after the error.
+				 */
+				size_t written = p - buf;
+				/*
+				 * NB: This needs to be signed so that we can
+				 * form the negative value below.
+				 */
+				ssize_t overread = read_count - written;
+
+				*off_in -= overread;
+				/* Report the number of bytes copied so far. */
+				if (copied + written > 0)
+					return copied + written;
+				return -1;
+			}
+			p += write_count;
+			*off_out += write_count;
+		} /* Write loop.  */
+		copied += read_count;
+		length -= read_count;
+	}
+	return copied;
+}
+
+ssize_t erofs_copy_file_range(int fd_in, erofs_off_t *off_in,
+			      int fd_out, erofs_off_t *off_out,
+			      size_t length)
+{
+#ifdef HAVE_COPY_FILE_RANGE
+	off64_t off64_in = *off_in, off64_out = *off_out;
+	ssize_t ret;
+
+	ret = copy_file_range(fd_in, &off64_in, fd_out, &off64_out,
+                              length, 0);
+	if (ret >= 0)
+		goto out;
+	if (errno != ENOSYS) {
+		ret = -errno;
+out:
+		*off_in = off64_in;
+		*off_out = off64_out;
+		return ret;
+	}
+#endif
+	return __erofs_copy_file_range(fd_in, off_in, fd_out, off_out, length);
+}
diff --git a/lib/liberofs_private.h b/lib/liberofs_private.h
new file mode 100644
index 0000000..c2312e8
--- /dev/null
+++ b/lib/liberofs_private.h
@@ -0,0 +1,13 @@
+/* SPDX-License-Identifier: GPL-2.0-only OR Apache-2.0 */
+
+#ifdef HAVE_LIBSELINUX
+#include <selinux/selinux.h>
+#include <selinux/label.h>
+#endif
+
+#ifdef WITH_ANDROID
+#include <selinux/android.h>
+#include <private/android_filesystem_config.h>
+#include <private/canned_fs_config.h>
+#include <private/fs_config.h>
+#endif
diff --git a/lib/namei.c b/lib/namei.c
index 4e06ba4..2c8891a 100644
--- a/lib/namei.c
+++ b/lib/namei.c
@@ -1,17 +1,16 @@
 // SPDX-License-Identifier: GPL-2.0+
 /*
- * erofs-utils/lib/namei.c
- *
  * Created by Li Guifu <blucerlee@gmail.com>
  */
-#include <linux/kdev_t.h>
 #include <sys/types.h>
 #include <unistd.h>
 #include <stdio.h>
 #include <errno.h>
 #include <sys/stat.h>
+#include <config.h>
+#if defined(HAVE_SYS_SYSMACROS_H)
 #include <sys/sysmacros.h>
-
+#endif
 #include "erofs/print.h"
 #include "erofs/io.h"
 
@@ -23,7 +22,7 @@
 	return makedev(major, minor);
 }
 
-static int erofs_read_inode_from_disk(struct erofs_inode *vi)
+int erofs_read_inode_from_disk(struct erofs_inode *vi)
 {
 	int ret, ifmt;
 	char buf[sizeof(struct erofs_inode_extended)];
@@ -31,7 +30,7 @@
 	struct erofs_inode_extended *die;
 	const erofs_off_t inode_loc = iloc(vi->nid);
 
-	ret = dev_read(buf, inode_loc, sizeof(*dic));
+	ret = dev_read(0, buf, inode_loc, sizeof(*dic));
 	if (ret < 0)
 		return -EIO;
 
@@ -48,7 +47,7 @@
 	case EROFS_INODE_LAYOUT_EXTENDED:
 		vi->inode_isize = sizeof(struct erofs_inode_extended);
 
-		ret = dev_read(buf + sizeof(*dic), inode_loc + sizeof(*dic),
+		ret = dev_read(0, buf + sizeof(*dic), inode_loc + sizeof(*dic),
 			       sizeof(*die) - sizeof(*dic));
 		if (ret < 0)
 			return -EIO;
@@ -80,9 +79,12 @@
 		vi->i_gid = le32_to_cpu(die->i_gid);
 		vi->i_nlink = le32_to_cpu(die->i_nlink);
 
-		vi->i_ctime = le64_to_cpu(die->i_ctime);
-		vi->i_ctime_nsec = le64_to_cpu(die->i_ctime_nsec);
+		vi->i_mtime = le64_to_cpu(die->i_mtime);
+		vi->i_mtime_nsec = le64_to_cpu(die->i_mtime_nsec);
 		vi->i_size = le64_to_cpu(die->i_size);
+		if (vi->datalayout == EROFS_INODE_CHUNK_BASED)
+			/* fill chunked inode summary info */
+			vi->u.chunkformat = le16_to_cpu(die->i_u.c.format);
 		break;
 	case EROFS_INODE_LAYOUT_COMPACT:
 		vi->inode_isize = sizeof(struct erofs_inode_compact);
@@ -112,10 +114,12 @@
 		vi->i_gid = le16_to_cpu(dic->i_gid);
 		vi->i_nlink = le16_to_cpu(dic->i_nlink);
 
-		vi->i_ctime = sbi.build_time;
-		vi->i_ctime_nsec = sbi.build_time_nsec;
+		vi->i_mtime = sbi.build_time;
+		vi->i_mtime_nsec = sbi.build_time_nsec;
 
 		vi->i_size = le32_to_cpu(dic->i_size);
+		if (vi->datalayout == EROFS_INODE_CHUNK_BASED)
+			vi->u.chunkformat = le16_to_cpu(dic->i_u.c.format);
 		break;
 	default:
 		erofs_err("unsupported on-disk inode version %u of nid %llu",
@@ -124,7 +128,15 @@
 	}
 
 	vi->flags = 0;
-	if (erofs_inode_is_data_compressed(vi->datalayout))
+	if (vi->datalayout == EROFS_INODE_CHUNK_BASED) {
+		if (vi->u.chunkformat & ~EROFS_CHUNK_FORMAT_ALL) {
+			erofs_err("unsupported chunk format %x of nid %llu",
+				  vi->u.chunkformat, vi->nid | 0ULL);
+			return -EOPNOTSUPP;
+		}
+		vi->u.chunkbits = LOG_BLOCK_SIZE +
+			(vi->u.chunkformat & EROFS_CHUNK_FORMAT_BLKBITS_MASK);
+	} else if (erofs_inode_is_data_compressed(vi->datalayout))
 		z_erofs_fill_inode(vi);
 	return 0;
 bogusimode:
@@ -244,7 +256,8 @@
 
 		name = p;
 		/* Skip until no more slashes. */
-		for (name = p; *name == '/'; ++name);
+		for (name = p; *name == '/'; ++name)
+			;
 	}
 	return 0;
 }
@@ -261,4 +274,3 @@
 	vi->nid = nd.nid;
 	return erofs_read_inode_from_disk(vi);
 }
-
diff --git a/lib/sha256.c b/lib/sha256.c
new file mode 100644
index 0000000..dd0e058
--- /dev/null
+++ b/lib/sha256.c
@@ -0,0 +1,248 @@
+/*
+ * sha256.c --- The sha256 algorithm
+ *
+ * Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
+ * (copied from libtomcrypt and then relicensed under GPLv2)
+ *
+ * %Begin-Header%
+ * This file may be redistributed under the terms of the GNU Library
+ * General Public License, version 2.
+ * %End-Header%
+ */
+#include "erofs/defs.h"
+#include <string.h>
+
+static const __u32 K[64] = {
+    0x428a2f98UL, 0x71374491UL, 0xb5c0fbcfUL, 0xe9b5dba5UL, 0x3956c25bUL,
+    0x59f111f1UL, 0x923f82a4UL, 0xab1c5ed5UL, 0xd807aa98UL, 0x12835b01UL,
+    0x243185beUL, 0x550c7dc3UL, 0x72be5d74UL, 0x80deb1feUL, 0x9bdc06a7UL,
+    0xc19bf174UL, 0xe49b69c1UL, 0xefbe4786UL, 0x0fc19dc6UL, 0x240ca1ccUL,
+    0x2de92c6fUL, 0x4a7484aaUL, 0x5cb0a9dcUL, 0x76f988daUL, 0x983e5152UL,
+    0xa831c66dUL, 0xb00327c8UL, 0xbf597fc7UL, 0xc6e00bf3UL, 0xd5a79147UL,
+    0x06ca6351UL, 0x14292967UL, 0x27b70a85UL, 0x2e1b2138UL, 0x4d2c6dfcUL,
+    0x53380d13UL, 0x650a7354UL, 0x766a0abbUL, 0x81c2c92eUL, 0x92722c85UL,
+    0xa2bfe8a1UL, 0xa81a664bUL, 0xc24b8b70UL, 0xc76c51a3UL, 0xd192e819UL,
+    0xd6990624UL, 0xf40e3585UL, 0x106aa070UL, 0x19a4c116UL, 0x1e376c08UL,
+    0x2748774cUL, 0x34b0bcb5UL, 0x391c0cb3UL, 0x4ed8aa4aUL, 0x5b9cca4fUL,
+    0x682e6ff3UL, 0x748f82eeUL, 0x78a5636fUL, 0x84c87814UL, 0x8cc70208UL,
+    0x90befffaUL, 0xa4506cebUL, 0xbef9a3f7UL, 0xc67178f2UL
+};
+
+/* Various logical functions */
+#define Ch(x,y,z)       (z ^ (x & (y ^ z)))
+#define Maj(x,y,z)      (((x | y) & z) | (x & y))
+#define S(x, n)         RORc((x),(n))
+#define R(x, n)         (((x)&0xFFFFFFFFUL)>>(n))
+#define Sigma0(x)       (S(x, 2) ^ S(x, 13) ^ S(x, 22))
+#define Sigma1(x)       (S(x, 6) ^ S(x, 11) ^ S(x, 25))
+#define Gamma0(x)       (S(x, 7) ^ S(x, 18) ^ R(x, 3))
+#define Gamma1(x)       (S(x, 17) ^ S(x, 19) ^ R(x, 10))
+#define RORc(x, y) ( ((((__u32)(x)&0xFFFFFFFFUL)>>(__u32)((y)&31)) | ((__u32)(x)<<(__u32)(32-((y)&31)))) & 0xFFFFFFFFUL)
+
+#define RND(a,b,c,d,e,f,g,h,i)                         \
+     t0 = h + Sigma1(e) + Ch(e, f, g) + K[i] + W[i];   \
+     t1 = Sigma0(a) + Maj(a, b, c);                    \
+     d += t0;                                          \
+     h  = t0 + t1;
+
+#define STORE64H(x, y) \
+	do { \
+		(y)[0] = (unsigned char)(((x)>>56)&255);\
+		(y)[1] = (unsigned char)(((x)>>48)&255);\
+		(y)[2] = (unsigned char)(((x)>>40)&255);\
+		(y)[3] = (unsigned char)(((x)>>32)&255);\
+		(y)[4] = (unsigned char)(((x)>>24)&255);\
+		(y)[5] = (unsigned char)(((x)>>16)&255);\
+		(y)[6] = (unsigned char)(((x)>>8)&255);\
+		(y)[7] = (unsigned char)((x)&255); } while(0)
+
+#define STORE32H(x, y)                                                                     \
+  do { (y)[0] = (unsigned char)(((x)>>24)&255); (y)[1] = (unsigned char)(((x)>>16)&255);   \
+       (y)[2] = (unsigned char)(((x)>>8)&255); (y)[3] = (unsigned char)((x)&255); } while(0)
+
+#define LOAD32H(x, y)                            \
+  do { x = ((__u32)((y)[0] & 255)<<24) | \
+           ((__u32)((y)[1] & 255)<<16) | \
+           ((__u32)((y)[2] & 255)<<8)  | \
+           ((__u32)((y)[3] & 255)); } while(0)
+
+struct sha256_state {
+    __u64 length;
+    __u32 state[8], curlen;
+    unsigned char buf[64];
+};
+
+/* This is a highly simplified version from libtomcrypt */
+struct hash_state {
+	struct sha256_state sha256;
+};
+
+static void sha256_compress(struct hash_state * md, const unsigned char *buf)
+{
+    __u32 S[8], W[64], t0, t1;
+    __u32 t;
+    int i;
+
+    /* copy state into S */
+    for (i = 0; i < 8; i++) {
+        S[i] = md->sha256.state[i];
+    }
+
+    /* copy the state into 512-bits into W[0..15] */
+    for (i = 0; i < 16; i++) {
+        LOAD32H(W[i], buf + (4*i));
+    }
+
+    /* fill W[16..63] */
+    for (i = 16; i < 64; i++) {
+        W[i] = Gamma1(W[i - 2]) + W[i - 7] + Gamma0(W[i - 15]) + W[i - 16];
+    }
+
+    /* Compress */
+     for (i = 0; i < 64; ++i) {
+         RND(S[0],S[1],S[2],S[3],S[4],S[5],S[6],S[7],i);
+         t = S[7]; S[7] = S[6]; S[6] = S[5]; S[5] = S[4];
+         S[4] = S[3]; S[3] = S[2]; S[2] = S[1]; S[1] = S[0]; S[0] = t;
+     }
+
+    /* feedback */
+    for (i = 0; i < 8; i++) {
+        md->sha256.state[i] = md->sha256.state[i] + S[i];
+    }
+}
+
+static void sha256_init(struct hash_state * md)
+{
+    md->sha256.curlen = 0;
+    md->sha256.length = 0;
+    md->sha256.state[0] = 0x6A09E667UL;
+    md->sha256.state[1] = 0xBB67AE85UL;
+    md->sha256.state[2] = 0x3C6EF372UL;
+    md->sha256.state[3] = 0xA54FF53AUL;
+    md->sha256.state[4] = 0x510E527FUL;
+    md->sha256.state[5] = 0x9B05688CUL;
+    md->sha256.state[6] = 0x1F83D9ABUL;
+    md->sha256.state[7] = 0x5BE0CD19UL;
+}
+
+#define MIN(x, y) ( ((x)<(y))?(x):(y) )
+#define SHA256_BLOCKSIZE 64
+static void sha256_process(struct hash_state * md, const unsigned char *in, unsigned long inlen)
+{
+    unsigned long n;
+
+    while (inlen > 0) {
+	    if (md->sha256.curlen == 0 && inlen >= SHA256_BLOCKSIZE) {
+		    sha256_compress(md, in);
+		    md->sha256.length += SHA256_BLOCKSIZE * 8;
+		    in += SHA256_BLOCKSIZE;
+		    inlen -= SHA256_BLOCKSIZE;
+	    } else {
+		    n = MIN(inlen, (SHA256_BLOCKSIZE - md->sha256.curlen));
+		    memcpy(md->sha256.buf + md->sha256.curlen, in, (size_t)n);
+		    md->sha256.curlen += n;
+		    in += n;
+		    inlen -= n;
+		    if (md->sha256.curlen == SHA256_BLOCKSIZE) {
+			    sha256_compress(md, md->sha256.buf);
+			    md->sha256.length += 8*SHA256_BLOCKSIZE;
+			    md->sha256.curlen = 0;
+		    }
+	    }
+    }
+}
+
+static void sha256_done(struct hash_state * md, unsigned char *out)
+{
+    int i;
+
+    /* increase the length of the message */
+    md->sha256.length += md->sha256.curlen * 8;
+
+    /* append the '1' bit */
+    md->sha256.buf[md->sha256.curlen++] = (unsigned char)0x80;
+
+    /* if the length is currently above 56 bytes we append zeros
+     * then compress.  Then we can fall back to padding zeros and length
+     * encoding like normal.
+     */
+    if (md->sha256.curlen > 56) {
+        while (md->sha256.curlen < 64) {
+            md->sha256.buf[md->sha256.curlen++] = (unsigned char)0;
+        }
+        sha256_compress(md, md->sha256.buf);
+        md->sha256.curlen = 0;
+    }
+
+    /* pad upto 56 bytes of zeroes */
+    while (md->sha256.curlen < 56) {
+        md->sha256.buf[md->sha256.curlen++] = (unsigned char)0;
+    }
+
+    /* store length */
+    STORE64H(md->sha256.length, md->sha256.buf+56);
+    sha256_compress(md, md->sha256.buf);
+
+    /* copy output */
+    for (i = 0; i < 8; i++) {
+        STORE32H(md->sha256.state[i], out+(4*i));
+    }
+}
+
+void erofs_sha256(const unsigned char *in, unsigned long in_size,
+		  unsigned char out[32])
+{
+	struct hash_state md;
+
+	sha256_init(&md);
+	sha256_process(&md, in, in_size);
+	sha256_done(&md, out);
+}
+
+#ifdef UNITTEST
+static const struct {
+	char *msg;
+	unsigned char hash[32];
+} tests[] = {
+	{ "",
+	  { 0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14,
+	    0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24,
+	    0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c,
+	    0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55 }
+	},
+	{ "abc",
+	  { 0xba, 0x78, 0x16, 0xbf, 0x8f, 0x01, 0xcf, 0xea,
+	    0x41, 0x41, 0x40, 0xde, 0x5d, 0xae, 0x22, 0x23,
+	    0xb0, 0x03, 0x61, 0xa3, 0x96, 0x17, 0x7a, 0x9c,
+	    0xb4, 0x10, 0xff, 0x61, 0xf2, 0x00, 0x15, 0xad }
+	},
+	{ "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq",
+	  { 0x24, 0x8d, 0x6a, 0x61, 0xd2, 0x06, 0x38, 0xb8,
+	    0xe5, 0xc0, 0x26, 0x93, 0x0c, 0x3e, 0x60, 0x39,
+	    0xa3, 0x3c, 0xe4, 0x59, 0x64, 0xff, 0x21, 0x67,
+	    0xf6, 0xec, 0xed, 0xd4, 0x19, 0xdb, 0x06, 0xc1 }
+	},
+};
+
+int main(int argc, char **argv)
+{
+	int i;
+	int errors = 0;
+	unsigned char tmp[32];
+
+	for (i = 0; i < (int)(sizeof(tests) / sizeof(tests[0])); i++) {
+		unsigned char *msg = (unsigned char *) tests[i].msg;
+		int len = strlen(tests[i].msg);
+
+		erofs_sha256(msg, len, tmp);
+		printf("SHA256 test message %d: ", i);
+		if (memcmp(tmp, tests[i].hash, 32) != 0) {
+			printf("FAILED\n");
+			errors++;
+		} else
+			printf("OK\n");
+	}
+	return errors;
+}
+
+#endif /* UNITTEST */
diff --git a/lib/super.c b/lib/super.c
index 025cefe..3ccc551 100644
--- a/lib/super.c
+++ b/lib/super.c
@@ -1,13 +1,9 @@
 // SPDX-License-Identifier: GPL-2.0+
 /*
- * erofs-utils/lib/super.c
- *
  * Created by Li Guifu <blucerlee@gmail.com>
  */
 #include <string.h>
 #include <stdlib.h>
-#include <asm-generic/errno-base.h>
-
 #include "erofs/io.h"
 #include "erofs/print.h"
 
@@ -27,6 +23,45 @@
 	return true;
 }
 
+static int erofs_init_devices(struct erofs_sb_info *sbi,
+			      struct erofs_super_block *dsb)
+{
+	unsigned int ondisk_extradevs, i;
+	erofs_off_t pos;
+
+	sbi->total_blocks = sbi->primarydevice_blocks;
+
+	if (!erofs_sb_has_device_table())
+		ondisk_extradevs = 0;
+	else
+		ondisk_extradevs = le16_to_cpu(dsb->extra_devices);
+
+	if (ondisk_extradevs != sbi->extra_devices) {
+		erofs_err("extra devices don't match (ondisk %u, given %u)",
+			  ondisk_extradevs, sbi->extra_devices);
+		return -EINVAL;
+	}
+	if (!ondisk_extradevs)
+		return 0;
+
+	sbi->device_id_mask = roundup_pow_of_two(ondisk_extradevs + 1) - 1;
+	sbi->devs = calloc(ondisk_extradevs, sizeof(*sbi->devs));
+	pos = le16_to_cpu(dsb->devt_slotoff) * EROFS_DEVT_SLOT_SIZE;
+	for (i = 0; i < ondisk_extradevs; ++i) {
+		struct erofs_deviceslot dis;
+		int ret;
+
+		ret = dev_read(0, &dis, pos, sizeof(dis));
+		if (ret < 0)
+			return ret;
+
+		sbi->devs[i].mapped_blkaddr = dis.mapped_blkaddr;
+		sbi->total_blocks += dis.blocks;
+		pos += EROFS_DEVT_SLOT_SIZE;
+	}
+	return 0;
+}
+
 int erofs_read_superblock(void)
 {
 	char data[EROFS_BLKSIZ];
@@ -34,7 +69,7 @@
 	unsigned int blkszbits;
 	int ret;
 
-	ret = blk_read(data, 0, 1);
+	ret = blk_read(0, data, 0, 1);
 	if (ret < 0) {
 		erofs_err("cannot read erofs superblock: %d", ret);
 		return -EIO;
@@ -60,17 +95,17 @@
 	if (!check_layout_compatibility(&sbi, dsb))
 		return ret;
 
-	sbi.blocks = le32_to_cpu(dsb->blocks);
+	sbi.primarydevice_blocks = le32_to_cpu(dsb->blocks);
 	sbi.meta_blkaddr = le32_to_cpu(dsb->meta_blkaddr);
 	sbi.xattr_blkaddr = le32_to_cpu(dsb->xattr_blkaddr);
 	sbi.islotbits = EROFS_ISLOTBITS;
 	sbi.root_nid = le16_to_cpu(dsb->root_nid);
 	sbi.inos = le64_to_cpu(dsb->inos);
+	sbi.checksum = le32_to_cpu(dsb->checksum);
 
 	sbi.build_time = le64_to_cpu(dsb->build_time);
 	sbi.build_time_nsec = le32_to_cpu(dsb->build_time_nsec);
 
 	memcpy(&sbi.uuid, dsb->uuid, sizeof(dsb->uuid));
-	return 0;
+	return erofs_init_devices(&sbi, dsb);
 }
-
diff --git a/lib/xattr.c b/lib/xattr.c
index 49ebb9c..00fb963 100644
--- a/lib/xattr.c
+++ b/lib/xattr.c
@@ -1,7 +1,5 @@
 // SPDX-License-Identifier: GPL-2.0+
 /*
- * erofs-utils/lib/xattr.c
- *
  * Originally contributed by an anonymous person,
  * heavily changed by Li Guifu <blucerlee@gmail.com>
  *                and Gao Xiang <hsiangkao@aol.com>
@@ -19,6 +17,7 @@
 #include "erofs/xattr.h"
 #include "erofs/cache.h"
 #include "erofs/io.h"
+#include "liberofs_private.h"
 
 #define EA_HASHTABLE_BITS 16
 
@@ -159,7 +158,13 @@
 	DBG_BUGON(keylen < prefixlen);
 
 	/* determine length of the value */
+#ifdef HAVE_LGETXATTR
 	ret = lgetxattr(path, key, NULL, 0);
+#elif defined(__APPLE__)
+	ret = getxattr(path, key, NULL, 0, 0, XATTR_NOFOLLOW);
+#else
+	return ERR_PTR(-EOPNOTSUPP);
+#endif
 	if (ret < 0)
 		return ERR_PTR(-errno);
 	len[1] = ret;
@@ -173,7 +178,15 @@
 	memcpy(kvbuf, key + prefixlen, len[0]);
 	if (len[1]) {
 		/* copy value to buffer */
+#ifdef HAVE_LGETXATTR
 		ret = lgetxattr(path, key, kvbuf + len[0], len[1]);
+#elif defined(__APPLE__)
+		ret = getxattr(path, key, kvbuf + len[0], len[1], 0,
+			       XATTR_NOFOLLOW);
+#else
+		free(kvbuf);
+		return ERR_PTR(-EOPNOTSUPP);
+#endif
 		if (ret < 0) {
 			free(kvbuf);
 			return ERR_PTR(-errno);
@@ -203,7 +216,7 @@
 				       erofs_fspath(srcpath));
 		else
 #endif
-		ret = asprintf(&fspath, "/%s", erofs_fspath(srcpath));
+			ret = asprintf(&fspath, "/%s", erofs_fspath(srcpath));
 		if (ret <= 0)
 			return ERR_PTR(-ENOMEM);
 
@@ -292,7 +305,13 @@
 static int read_xattrs_from_file(const char *path, mode_t mode,
 				 struct list_head *ixattrs)
 {
+#ifdef HAVE_LLISTXATTR
 	ssize_t kllen = llistxattr(path, NULL, 0);
+#elif defined(__APPLE__)
+	ssize_t kllen = listxattr(path, NULL, 0, XATTR_NOFOLLOW);
+#else
+	ssize_t kllen = 0;
+#endif
 	int ret;
 	char *keylst, *key, *klend;
 	unsigned int keylen;
@@ -313,13 +332,19 @@
 		return -ENOMEM;
 
 	/* copy the list of attribute keys to the buffer.*/
+#ifdef HAVE_LLISTXATTR
 	kllen = llistxattr(path, keylst, kllen);
+#elif defined(__APPLE__)
+	kllen = listxattr(path, keylst, kllen, XATTR_NOFOLLOW);
 	if (kllen < 0) {
 		erofs_err("llistxattr to get names for %s failed", path);
 		ret = -errno;
 		goto err;
 	}
-
+#else
+	ret = -EOPNOTSUPP;
+	goto err;
+#endif
 	/*
 	 * loop over the list of zero terminated strings with the
 	 * attribute keys. Use the remaining buffer length to determine
@@ -446,8 +471,8 @@
 
 	_dir = opendir(path);
 	if (!_dir) {
-		erofs_err("%s, failed to opendir at %s: %s",
-			  __func__, path, erofs_strerror(errno));
+		erofs_err("failed to opendir at %s: %s",
+			  path, erofs_strerror(errno));
 		return -errno;
 	}
 
@@ -575,7 +600,7 @@
 	}
 	bh->op = &erofs_skip_write_bhops;
 
-	erofs_mapbh(bh->block, true);
+	erofs_mapbh(bh->block);
 	off = erofs_btell(bh, false);
 
 	sbi.xattr_blkaddr = off / EROFS_BLKSIZ;
@@ -661,4 +686,3 @@
 	DBG_BUGON(p > size);
 	return buf;
 }
-
diff --git a/lib/zmap.c b/lib/zmap.c
index ee63de7..3715c47 100644
--- a/lib/zmap.c
+++ b/lib/zmap.c
@@ -1,7 +1,5 @@
 // SPDX-License-Identifier: GPL-2.0+
 /*
- * erofs-utils/lib/zmap.c
- *
  * (a large amount of code was adapted from Linux kernel. )
  *
  * Copyright (C) 2018-2019 HUAWEI, Inc.
@@ -14,13 +12,12 @@
 
 int z_erofs_fill_inode(struct erofs_inode *vi)
 {
-	if (vi->datalayout == EROFS_INODE_FLAT_COMPRESSION_LEGACY) {
+	if (!erofs_sb_has_big_pcluster() &&
+	    vi->datalayout == EROFS_INODE_FLAT_COMPRESSION_LEGACY) {
 		vi->z_advise = 0;
 		vi->z_algorithmtype[0] = 0;
 		vi->z_algorithmtype[1] = 0;
 		vi->z_logical_clusterbits = LOG_BLOCK_SIZE;
-		vi->z_physical_clusterbits[0] = vi->z_logical_clusterbits;
-		vi->z_physical_clusterbits[1] = vi->z_logical_clusterbits;
 
 		vi->flags |= EROFS_I_Z_INITED;
 	}
@@ -37,10 +34,11 @@
 	if (vi->flags & EROFS_I_Z_INITED)
 		return 0;
 
-	DBG_BUGON(vi->datalayout == EROFS_INODE_FLAT_COMPRESSION_LEGACY);
+	DBG_BUGON(!erofs_sb_has_big_pcluster() &&
+		  vi->datalayout == EROFS_INODE_FLAT_COMPRESSION_LEGACY);
 	pos = round_up(iloc(vi->nid) + vi->inode_isize + vi->xattr_isize, 8);
 
-	ret = dev_read(buf, pos, sizeof(buf));
+	ret = dev_read(0, buf, pos, sizeof(buf));
 	if (ret < 0)
 		return -EIO;
 
@@ -56,17 +54,13 @@
 	}
 
 	vi->z_logical_clusterbits = LOG_BLOCK_SIZE + (h->h_clusterbits & 7);
-	vi->z_physical_clusterbits[0] = vi->z_logical_clusterbits +
-					((h->h_clusterbits >> 3) & 3);
-
-	if (vi->z_physical_clusterbits[0] != LOG_BLOCK_SIZE) {
-		erofs_err("unsupported physical clusterbits %u for nid %llu",
-			  vi->z_physical_clusterbits[0], (unsigned long long)vi->nid);
-		return -EOPNOTSUPP;
+	if (vi->datalayout == EROFS_INODE_FLAT_COMPRESSION &&
+	    !(vi->z_advise & Z_EROFS_ADVISE_BIG_PCLUSTER_1) ^
+	    !(vi->z_advise & Z_EROFS_ADVISE_BIG_PCLUSTER_2)) {
+		erofs_err("big pcluster head1/2 of compact indexes should be consistent for nid %llu",
+			  vi->nid * 1ULL);
+		return -EFSCORRUPTED;
 	}
-
-	vi->z_physical_clusterbits[1] = vi->z_logical_clusterbits +
-					((h->h_clusterbits >> 5) & 7);
 	vi->flags |= EROFS_I_Z_INITED;
 	return 0;
 }
@@ -78,10 +72,10 @@
 
 	unsigned long lcn;
 	/* compression extent information gathered */
-	u8  type;
+	u8  type, headtype;
 	u16 clusterofs;
 	u16 delta[2];
-	erofs_blk_t pblk;
+	erofs_blk_t pblk, compressedlcs;
 };
 
 static int z_erofs_reload_indexes(struct z_erofs_maprecorder *m,
@@ -94,7 +88,7 @@
 	if (map->index == eblk)
 		return 0;
 
-	ret = blk_read(mpage, eblk, 1);
+	ret = blk_read(0, mpage, eblk, 1);
 	if (ret < 0)
 		return -EIO;
 
@@ -130,6 +124,15 @@
 	case Z_EROFS_VLE_CLUSTER_TYPE_NONHEAD:
 		m->clusterofs = 1 << vi->z_logical_clusterbits;
 		m->delta[0] = le16_to_cpu(di->di_u.delta[0]);
+		if (m->delta[0] & Z_EROFS_VLE_DI_D0_CBLKCNT) {
+			if (!(vi->z_advise & Z_EROFS_ADVISE_BIG_PCLUSTER_1)) {
+				DBG_BUGON(1);
+				return -EFSCORRUPTED;
+			}
+			m->compressedlcs = m->delta[0] &
+				~Z_EROFS_VLE_DI_D0_CBLKCNT;
+			m->delta[0] = 1;
+		}
 		m->delta[1] = le16_to_cpu(di->di_u.delta[1]);
 		break;
 	case Z_EROFS_VLE_CLUSTER_TYPE_PLAIN:
@@ -156,9 +159,34 @@
 	return lo;
 }
 
+static int get_compacted_la_distance(unsigned int lclusterbits,
+				     unsigned int encodebits,
+				     unsigned int vcnt, u8 *in, int i)
+{
+	const unsigned int lomask = (1 << lclusterbits) - 1;
+	unsigned int lo, d1 = 0;
+	u8 type;
+
+	DBG_BUGON(i >= vcnt);
+
+	do {
+		lo = decode_compactedbits(lclusterbits, lomask,
+					  in, encodebits * i, &type);
+
+		if (type != Z_EROFS_VLE_CLUSTER_TYPE_NONHEAD)
+			return d1;
+		++d1;
+	} while (++i < vcnt);
+
+	/* vcnt - 1 (Z_EROFS_VLE_CLUSTER_TYPE_NONHEAD) item */
+	if (!(lo & Z_EROFS_VLE_DI_D0_CBLKCNT))
+		d1 += lo - 1;
+	return d1;
+}
+
 static int unpack_compacted_index(struct z_erofs_maprecorder *m,
 				  unsigned int amortizedshift,
-				  unsigned int eofs)
+				  unsigned int eofs, bool lookahead)
 {
 	struct erofs_inode *const vi = m->inode;
 	const unsigned int lclusterbits = vi->z_logical_clusterbits;
@@ -166,6 +194,7 @@
 	unsigned int vcnt, base, lo, encodebits, nblk;
 	int i;
 	u8 *in, type;
+	bool big_pcluster;
 
 	if (1 << amortizedshift == 4)
 		vcnt = 2;
@@ -174,6 +203,7 @@
 	else
 		return -EOPNOTSUPP;
 
+	big_pcluster = vi->z_advise & Z_EROFS_ADVISE_BIG_PCLUSTER_1;
 	encodebits = ((vcnt << amortizedshift) - sizeof(__le32)) * 8 / vcnt;
 	base = round_down(eofs, vcnt << amortizedshift);
 	in = m->kaddr + base;
@@ -185,7 +215,20 @@
 	m->type = type;
 	if (type == Z_EROFS_VLE_CLUSTER_TYPE_NONHEAD) {
 		m->clusterofs = 1 << lclusterbits;
-		if (i + 1 != (int)vcnt) {
+
+		/* figure out lookahead_distance: delta[1] if needed */
+		if (lookahead)
+			m->delta[1] = get_compacted_la_distance(lclusterbits,
+						encodebits, vcnt, in, i);
+		if (lo & Z_EROFS_VLE_DI_D0_CBLKCNT) {
+			if (!big_pcluster) {
+				DBG_BUGON(1);
+				return -EFSCORRUPTED;
+			}
+			m->compressedlcs = lo & ~Z_EROFS_VLE_DI_D0_CBLKCNT;
+			m->delta[0] = 1;
+			return 0;
+		} else if (i + 1 != (int)vcnt) {
 			m->delta[0] = lo;
 			return 0;
 		}
@@ -198,22 +241,48 @@
 					  in, encodebits * (i - 1), &type);
 		if (type != Z_EROFS_VLE_CLUSTER_TYPE_NONHEAD)
 			lo = 0;
+		else if (lo & Z_EROFS_VLE_DI_D0_CBLKCNT)
+			lo = 1;
 		m->delta[0] = lo + 1;
 		return 0;
 	}
 	m->clusterofs = lo;
 	m->delta[0] = 0;
 	/* figout out blkaddr (pblk) for HEAD lclusters */
-	nblk = 1;
-	while (i > 0) {
-		--i;
-		lo = decode_compactedbits(lclusterbits, lomask,
-					  in, encodebits * i, &type);
-		if (type == Z_EROFS_VLE_CLUSTER_TYPE_NONHEAD)
-			i -= lo;
+	if (!big_pcluster) {
+		nblk = 1;
+		while (i > 0) {
+			--i;
+			lo = decode_compactedbits(lclusterbits, lomask,
+						  in, encodebits * i, &type);
+			if (type == Z_EROFS_VLE_CLUSTER_TYPE_NONHEAD)
+				i -= lo;
 
-		if (i >= 0)
+			if (i >= 0)
+				++nblk;
+		}
+	} else {
+		nblk = 0;
+		while (i > 0) {
+			--i;
+			lo = decode_compactedbits(lclusterbits, lomask,
+						  in, encodebits * i, &type);
+			if (type == Z_EROFS_VLE_CLUSTER_TYPE_NONHEAD) {
+				if (lo & Z_EROFS_VLE_DI_D0_CBLKCNT) {
+					--i;
+					nblk += lo & ~Z_EROFS_VLE_DI_D0_CBLKCNT;
+					continue;
+				}
+				if (lo == 1) {
+					DBG_BUGON(1);
+					/* --i; ++nblk;	continue; */
+					return -EFSCORRUPTED;
+				}
+				i -= lo - 2;
+				continue;
+			}
 			++nblk;
+		}
 	}
 	in += (vcnt << amortizedshift) - sizeof(__le32);
 	m->pblk = le32_to_cpu(*(__le32 *)in) + nblk;
@@ -221,7 +290,7 @@
 }
 
 static int compacted_load_cluster_from_disk(struct z_erofs_maprecorder *m,
-					    unsigned long lcn)
+					    unsigned long lcn, bool lookahead)
 {
 	struct erofs_inode *const vi = m->inode;
 	const unsigned int lclusterbits = vi->z_logical_clusterbits;
@@ -271,11 +340,12 @@
 	err = z_erofs_reload_indexes(m, erofs_blknr(pos));
 	if (err)
 		return err;
-	return unpack_compacted_index(m, amortizedshift, erofs_blkoff(pos));
+	return unpack_compacted_index(m, amortizedshift, erofs_blkoff(pos),
+				      lookahead);
 }
 
 static int z_erofs_load_cluster_from_disk(struct z_erofs_maprecorder *m,
-					  unsigned int lcn)
+					  unsigned int lcn, bool lookahead)
 {
 	const unsigned int datamode = m->inode->datalayout;
 
@@ -283,7 +353,7 @@
 		return legacy_load_cluster_from_disk(m, lcn);
 
 	if (datamode == EROFS_INODE_FLAT_COMPRESSION)
-		return compacted_load_cluster_from_disk(m, lcn);
+		return compacted_load_cluster_from_disk(m, lcn, lookahead);
 
 	return -EINVAL;
 }
@@ -306,7 +376,7 @@
 
 	/* load extent head logical cluster if needed */
 	lcn -= lookback_distance;
-	err = z_erofs_load_cluster_from_disk(m, lcn);
+	err = z_erofs_load_cluster_from_disk(m, lcn, false);
 	if (err)
 		return err;
 
@@ -320,8 +390,8 @@
 		}
 		return z_erofs_extent_lookback(m, m->delta[0]);
 	case Z_EROFS_VLE_CLUSTER_TYPE_PLAIN:
-		map->m_flags &= ~EROFS_MAP_ZIPPED;
 	case Z_EROFS_VLE_CLUSTER_TYPE_HEAD:
+		m->headtype = m->type;
 		map->m_la = (lcn << lclusterbits) | m->clusterofs;
 		break;
 	default:
@@ -333,8 +403,118 @@
 	return 0;
 }
 
+static int z_erofs_get_extent_compressedlen(struct z_erofs_maprecorder *m,
+					    unsigned int initial_lcn)
+{
+	struct erofs_inode *const vi = m->inode;
+	struct erofs_map_blocks *const map = m->map;
+	const unsigned int lclusterbits = vi->z_logical_clusterbits;
+	unsigned long lcn;
+	int err;
+
+	DBG_BUGON(m->type != Z_EROFS_VLE_CLUSTER_TYPE_PLAIN &&
+		  m->type != Z_EROFS_VLE_CLUSTER_TYPE_HEAD);
+	if (m->headtype == Z_EROFS_VLE_CLUSTER_TYPE_PLAIN ||
+	    !(vi->z_advise & Z_EROFS_ADVISE_BIG_PCLUSTER_1)) {
+		map->m_plen = 1 << lclusterbits;
+		return 0;
+	}
+
+	lcn = m->lcn + 1;
+	if (m->compressedlcs)
+		goto out;
+
+	err = z_erofs_load_cluster_from_disk(m, lcn, false);
+	if (err)
+		return err;
+
+	/*
+	 * If the 1st NONHEAD lcluster has already been handled initially w/o
+	 * valid compressedlcs, which means at least it mustn't be CBLKCNT, or
+	 * an internal implemenatation error is detected.
+	 *
+	 * The following code can also handle it properly anyway, but let's
+	 * BUG_ON in the debugging mode only for developers to notice that.
+	 */
+	DBG_BUGON(lcn == initial_lcn &&
+		  m->type == Z_EROFS_VLE_CLUSTER_TYPE_NONHEAD);
+
+	switch (m->type) {
+	case Z_EROFS_VLE_CLUSTER_TYPE_PLAIN:
+	case Z_EROFS_VLE_CLUSTER_TYPE_HEAD:
+		/*
+		 * if the 1st NONHEAD lcluster is actually PLAIN or HEAD type
+		 * rather than CBLKCNT, it's a 1 lcluster-sized pcluster.
+		 */
+		m->compressedlcs = 1;
+		break;
+	case Z_EROFS_VLE_CLUSTER_TYPE_NONHEAD:
+		if (m->delta[0] != 1)
+			goto err_bonus_cblkcnt;
+		if (m->compressedlcs)
+			break;
+		/* fallthrough */
+	default:
+		erofs_err("cannot found CBLKCNT @ lcn %lu of nid %llu",
+			  lcn, vi->nid | 0ULL);
+		DBG_BUGON(1);
+		return -EFSCORRUPTED;
+	}
+out:
+	map->m_plen = m->compressedlcs << lclusterbits;
+	return 0;
+err_bonus_cblkcnt:
+	erofs_err("bogus CBLKCNT @ lcn %lu of nid %llu",
+		  lcn, vi->nid | 0ULL);
+	DBG_BUGON(1);
+	return -EFSCORRUPTED;
+}
+
+static int z_erofs_get_extent_decompressedlen(struct z_erofs_maprecorder *m)
+{
+	struct erofs_inode *const vi = m->inode;
+	struct erofs_map_blocks *map = m->map;
+	unsigned int lclusterbits = vi->z_logical_clusterbits;
+	u64 lcn = m->lcn, headlcn = map->m_la >> lclusterbits;
+	int err;
+
+	do {
+		/* handle the last EOF pcluster (no next HEAD lcluster) */
+		if ((lcn << lclusterbits) >= vi->i_size) {
+			map->m_llen = vi->i_size - map->m_la;
+			return 0;
+		}
+
+		err = z_erofs_load_cluster_from_disk(m, lcn, true);
+		if (err)
+			return err;
+
+		if (m->type == Z_EROFS_VLE_CLUSTER_TYPE_NONHEAD) {
+			DBG_BUGON(!m->delta[1] &&
+				  m->clusterofs != 1 << lclusterbits);
+		} else if (m->type == Z_EROFS_VLE_CLUSTER_TYPE_PLAIN ||
+			   m->type == Z_EROFS_VLE_CLUSTER_TYPE_HEAD) {
+			/* go on until the next HEAD lcluster */
+			if (lcn != headlcn)
+				break;
+			m->delta[1] = 1;
+		} else {
+			erofs_err("unknown type %u @ lcn %llu of nid %llu",
+				  m->type, lcn | 0ULL,
+				  (unsigned long long)vi->nid);
+			DBG_BUGON(1);
+			return -EOPNOTSUPP;
+		}
+		lcn += m->delta[1];
+	} while (m->delta[1]);
+
+	map->m_llen = (lcn << lclusterbits) + m->clusterofs - map->m_la;
+	return 0;
+}
+
 int z_erofs_map_blocks_iter(struct erofs_inode *vi,
-			    struct erofs_map_blocks *map)
+			    struct erofs_map_blocks *map,
+			    int flags)
 {
 	struct z_erofs_maprecorder m = {
 		.inode = vi,
@@ -343,6 +523,7 @@
 	};
 	int err = 0;
 	unsigned int lclusterbits, endoff;
+	unsigned long initial_lcn;
 	unsigned long long ofs, end;
 
 	/* when trying to read beyond EOF, leave it unmapped */
@@ -359,21 +540,20 @@
 
 	lclusterbits = vi->z_logical_clusterbits;
 	ofs = map->m_la;
-	m.lcn = ofs >> lclusterbits;
+	initial_lcn = ofs >> lclusterbits;
 	endoff = ofs & ((1 << lclusterbits) - 1);
 
-	err = z_erofs_load_cluster_from_disk(&m, m.lcn);
+	err = z_erofs_load_cluster_from_disk(&m, initial_lcn, false);
 	if (err)
 		goto out;
 
-	map->m_flags = EROFS_MAP_ZIPPED;	/* by default, compressed */
+	map->m_flags = EROFS_MAP_MAPPED | EROFS_MAP_ENCODED;
 	end = (m.lcn + 1ULL) << lclusterbits;
 	switch (m.type) {
 	case Z_EROFS_VLE_CLUSTER_TYPE_PLAIN:
-		if (endoff >= m.clusterofs)
-			map->m_flags &= ~EROFS_MAP_ZIPPED;
 	case Z_EROFS_VLE_CLUSTER_TYPE_HEAD:
 		if (endoff >= m.clusterofs) {
+			m.headtype = m.type;
 			map->m_la = (m.lcn << lclusterbits) | m.clusterofs;
 			break;
 		}
@@ -387,6 +567,7 @@
 		end = (m.lcn << lclusterbits) | m.clusterofs;
 		map->m_flags |= EROFS_MAP_FULL_MAPPED;
 		m.delta[0] = 1;
+		/* fallthrough */
 	case Z_EROFS_VLE_CLUSTER_TYPE_NONHEAD:
 		/* get the correspoinding first chunk */
 		err = z_erofs_extent_lookback(&m, m.delta[0]);
@@ -401,9 +582,22 @@
 	}
 
 	map->m_llen = end - map->m_la;
-	map->m_plen = 1 << lclusterbits;
 	map->m_pa = blknr_to_addr(m.pblk);
-	map->m_flags |= EROFS_MAP_MAPPED;
+
+	err = z_erofs_get_extent_compressedlen(&m, initial_lcn);
+	if (err)
+		goto out;
+
+	if (m.headtype == Z_EROFS_VLE_CLUSTER_TYPE_PLAIN)
+		map->m_algorithmformat = Z_EROFS_COMPRESSION_SHIFTED;
+	else
+		map->m_algorithmformat = vi->z_algorithmtype[0];
+
+	if (flags & EROFS_GET_BLOCKS_FIEMAP) {
+		err = z_erofs_get_extent_decompressedlen(&m);
+		if (!err)
+			map->m_flags |= EROFS_MAP_FULL_MAPPED;
+	}
 
 out:
 	erofs_dbg("m_la %" PRIu64 " m_pa %" PRIu64 " m_llen %" PRIu64 " m_plen %" PRIu64 " m_flags 0%o",
diff --git a/man/Makefile.am b/man/Makefile.am
index dcdbb35..769b557 100644
--- a/man/Makefile.am
+++ b/man/Makefile.am
@@ -1,5 +1,8 @@
 # SPDX-License-Identifier: GPL-2.0+
-# Makefile.am
 
-dist_man_MANS = mkfs.erofs.1
+dist_man_MANS = mkfs.erofs.1 dump.erofs.1
 
+EXTRA_DIST = erofsfuse.1
+if ENABLE_FUSE
+man_MANS = erofsfuse.1
+endif
diff --git a/man/dump.erofs.1 b/man/dump.erofs.1
new file mode 100644
index 0000000..fd437cf
--- /dev/null
+++ b/man/dump.erofs.1
@@ -0,0 +1,48 @@
+.\" Copyright (c) 2021 Guo Xuenan <guoxuenan@huawei.com>
+.\"
+.TH DUMP.EROFS 1
+.SH NAME
+dump.erofs \- retrieve directory and file entries, show specific file
+or overall disk statistics information from an EROFS-formatted image.
+.SH SYNOPSIS
+\fBdump.erofs\fR [\fIOPTIONS\fR] \fIIMAGE\fR
+.SH DESCRIPTION
+.B dump.erofs
+is used to retrieve erofs metadata from \fIIMAGE\fP and demonstrate
+1) overall disk statistics,
+2) superblock information,
+3) file information of the given inode NID,
+4) file extent information of the given inode NID.
+.SH OPTIONS
+.TP
+.BI "\-\-device=" path
+Specify an extra device to be used together.
+You may give multiple `--device' options in the correct order.
+.TP
+.BI "\-\-nid=" NID
+Specify an inode NID in order to print its file information.
+.TP
+.BI "\-\-path=" path
+Specify an inode path in order to print its file information.
+.TP
+.BI \-e
+Show the file extent information. The option depends on option --nid to specify NID.
+.TP
+.BI \-V
+Print the version number and exit.
+.TP
+.BI \-s
+Show superblock information of the an EROFS-formatted image.
+.TP
+.BI \-S
+Show EROFS disk statistics, including file type/size distribution, number of (un)compressed files, compression ratio of the whole image, etc.
+.SH AUTHOR
+Initial code was written by Wang Qi <mpiglet@outlook.com>, Guo Xuenan <guoxuenan@huawei.com>.
+.PP
+This manual page was written by Guo Xuenan <guoxuenan@huawei.com>
+.SH AVAILABILITY
+.B dump.erofs
+is part of erofs-utils package and is available from git://git.kernel.org/pub/scm/linux/kernel/git/xiang/erofs-utils.git.
+.SH SEE ALSO
+.BR mkfs.erofs(1),
+.BR fsck.erofs(1)
diff --git a/man/erofsfuse.1 b/man/erofsfuse.1
new file mode 100644
index 0000000..9db6827
--- /dev/null
+++ b/man/erofsfuse.1
@@ -0,0 +1,48 @@
+.\" Copyright (c) 2021 Gao Xiang <xiang@kernel.org>
+.\"
+.TH EROFSFUSE 1
+.SH NAME
+erofsfuse \- FUSE file system client for erofs file system
+.SH SYNOPSIS
+\fBerofsfuse\fR [\fIOPTIONS\fR] \fIDEVICE\fR \fIMOUNTPOINT\fR
+.SH DESCRIPTION
+.B erofsfuse
+is a FUSE file system client that supports reading from devices or image files
+containing erofs file system.
+.SH OPTIONS
+.SS "general options:"
+.TP
+\fB\-o\fR opt,[opt...]
+mount options
+.TP
+\fB\-h\fR   \fB\-\-help\fR
+display help and exit
+.SS "erofsfuse options:"
+.TP
+.BI "\-\-dbglevel=" #
+Specify the level of debugging messages. The default is 2, which shows basic
+warning messages.
+.TP
+.BI "\-\-device=" path
+Specify an extra device to be used together.
+You may give multiple `--device' options in the correct order.
+.SS "FUSE options:"
+.TP
+\fB-d -o\fR debug
+enable debug output (implies -f)
+.TP
+\fB-f\fR
+foreground operation
+.TP
+\fB-s\fR
+disable multi-threaded operation
+.P
+For other FUSE options please see
+.BR mount.fuse (8)
+or see the output of
+.I erofsfuse \-\-help
+.SH AVAILABILITY
+\fBerofsfuse\fR is part of erofs-utils package and is available from
+git://git.kernel.org/pub/scm/linux/kernel/git/xiang/erofs-utils.git.
+.SH SEE ALSO
+.BR mount.fuse (8)
diff --git a/man/fsck.erofs.1 b/man/fsck.erofs.1
new file mode 100644
index 0000000..f3e9c3b
--- /dev/null
+++ b/man/fsck.erofs.1
@@ -0,0 +1,41 @@
+.\" Copyright (c) 2021 Daeho Jeong <daehojeong@google.com>
+.\"
+.TH FSCK.EROFS 1
+.SH NAME
+fsck.erofs \- tool to check the EROFS filesystem's integrity
+.SH SYNOPSIS
+\fBfsck.erofs\fR [\fIOPTIONS\fR] \fIIMAGE\fR
+.SH DESCRIPTION
+fsck.erofs is used to scan an EROFS filesystem \fIIMAGE\fR and check the
+integrity of it.
+.SH OPTIONS
+.TP
+.B \-V
+Print the version number of fsck.erofs and exit.
+.TP
+.BI "\-d " #
+Specify the level of debugging messages. The default is 2, which shows basic
+warning messages.
+.TP
+.B \-p
+Print total compression ratio of all files including compressed and
+non-compressed files.
+.TP
+.BI "\-\-device=" path
+Specify an extra device to be used together.
+You may give multiple `--device' options in the correct order.
+.TP
+.B \-\-extract
+Check if all files are well encoded. This will induce more I/Os to read
+compressed file data, so it might take too much time depending on the image.
+.TP
+.B \-\-help
+Display this help and exit.
+.SH AUTHOR
+This version of \fBfsck.erofs\fR is written by
+Daeho Jeong <daehojeong@google.com>.
+.SH AVAILABILITY
+\fBfsck.erofs\fR is part of erofs-utils package and is available from
+git://git.kernel.org/pub/scm/linux/kernel/git/xiang/erofs-utils.git.
+.SH SEE ALSO
+.BR fsck (8).
diff --git a/man/mkfs.erofs.1 b/man/mkfs.erofs.1
index dcaf9d7..d61e33e 100644
--- a/man/mkfs.erofs.1
+++ b/man/mkfs.erofs.1
@@ -24,8 +24,13 @@
 Set an algorithm for file compression, which can be set with an optional
 compression level separated by a comma.
 .TP
+.BI "\-C " max-pcluster-size
+Specify the maximum size of compress physical cluster in bytes. It may enable
+big pcluster feature if needed (Linux v5.13+).
+.TP
 .BI "\-d " #
-Specify the level of debugging messages. The default is 0.
+Specify the level of debugging messages. The default is 2, which shows basic
+warning messages.
 .TP
 .BI "\-x " #
 Specify the upper limit of an xattr which is still inlined. The default is 2.
@@ -46,6 +51,15 @@
 .TP
 .BI force-inode-extended
 Forcely generate extended inodes (64-byte inodes) to output.
+.TP
+.BI noinline_data
+Don't inline regular files for FSDAX support (Linux v5.15+).
+.TP
+.BI force-inode-blockmap
+Forcely generate inode chunk format in 4-byte block address array.
+.TP
+.BI force-chunk-indexes
+Forcely generate inode chunk format in 8-byte chunk indexes (with device id).
 .RE
 .TP
 .BI "\-T " #
@@ -58,6 +72,26 @@
 The format of the UUID is a series of hex digits separated by hyphens,
 like this: "c1b9d5a2-f162-11cf-9ece-0020afc76f16".
 .TP
+.B \-\-all-root
+Make all files owned by root.
+.TP
+.BI "\-\-blobdev " file
+Specify another extra blob device to store chunk-based data.
+.TP
+.BI "\-\-chunksize " #
+Generate chunk-based files with #-byte chunks.
+.TP
+.BI "\-\-compress-hints " file
+If the optional
+.BI "\-\-compress-hints " file
+argument is given,
+.B mkfs.erofs
+uses it to apply the per-file compression strategy. Each line is defined by
+tokens separated by spaces in the following form:
+.RS 1.2i
+<pcluster-in-bytes> <match-pattern>
+.RE
+.TP
 .BI "\-\-exclude-path=" path
 Ignore file that matches the exact literal path.
 You may give multiple `--exclude-path' options.
@@ -69,8 +103,22 @@
 .BI "\-\-file-contexts=" file
 Specify a \fIfile_contexts\fR file to setup / override selinux labels.
 .TP
+.BI "\-\-force-uid=" UID
+Set all file uids to \fIUID\fR.
+.TP
+.BI "\-\-force-gid=" GID
+Set all file gids to \fIGID\fR.
+.TP
 .B \-\-help
 Display this help and exit.
+.TP
+.B "\-\-ignore-mtime"
+File modification time is ignored whenever it would cause \fBmkfs.erofs\fR to
+use extended inodes over compact inodes. When not using a fixed timestamp, this
+can reduce total metadata size.
+.TP
+.BI "\-\-max-extent-bytes " #
+Specify maximum decompressed extent size # in bytes.
 .SH AUTHOR
 This version of \fBmkfs.erofs\fR is written by Li Guifu <blucerlee@gmail.com>,
 Miao Xie <miaoxie@huawei.com> and Gao Xiang <xiang@kernel.org> with
@@ -82,4 +130,3 @@
 git://git.kernel.org/pub/scm/linux/kernel/git/xiang/erofs-utils.git.
 .SH SEE ALSO
 .BR mkfs (8).
-
diff --git a/mkerofsimage.sh b/mkerofsimage.sh
deleted file mode 100755
index 89bf40e..0000000
--- a/mkerofsimage.sh
+++ /dev/null
@@ -1,111 +0,0 @@
-#!/bin/bash
-#
-# To call this script, make sure mkfs.erofs is somewhere in PATH
-
-function usage() {
-cat<<EOT
-Usage:
-${0##*/} SRC_DIR OUTPUT_FILE [-s] [-m MOUNT_POINT] [-d PRODUCT_OUT] [-C FS_CONFIG ] [-c FILE_CONTEXTS] [-z COMPRESSOR] [-T TIMESTAMP] [-U UUID]
-EOT
-}
-
-echo "in mkerofsimage.sh PATH=$PATH"
-
-if [ $# -lt 2 ]; then
-    usage
-    exit 1
-fi
-
-SRC_DIR=$1
-if [ ! -d $SRC_DIR ]; then
-  echo "Can not find directory $SRC_DIR!"
-  exit 2
-fi
-OUTPUT_FILE=$2
-shift; shift
-
-SPARSE=false
-if [[ "$1" == "-s" ]]; then
-    SPARSE=true
-    shift;
-fi
-
-MOUNT_POINT=
-if [[ "$1" == "-m" ]]; then
-    MOUNT_POINT=$2
-    shift; shift
-fi
-
-PRODUCT_OUT=
-if [[ "$1" == "-d" ]]; then
-    PRODUCT_OUT=$2
-    shift; shift
-fi
-
-FS_CONFIG=
-if [[ "$1" == "-C" ]]; then
-    FS_CONFIG=$2
-    shift; shift
-fi
-
-FILE_CONTEXTS=
-if [[ "$1" == "-c" ]]; then
-    FILE_CONTEXTS=$2
-    shift; shift
-fi
-
-COMPRESSOR="lz4hc"
-if [[ "$1" == "-z" ]]; then
-    COMPRESSOR=$2
-    shift; shift
-fi
-
-TIMESTAMP=
-if [[ "$1" == "-T" ]]; then
-    TIMESTAMP=$2
-    shift; shift
-fi
-
-UUID=
-if [[ "$1" == "-U" ]]; then
-    UUID=$2
-    shift; shift
-fi
-
-OPT=""
-if [ -n "$MOUNT_POINT" ]; then
-  OPT="$OPT --mount-point $MOUNT_POINT"
-fi
-if [ -n "$PRODUCT_OUT" ]; then
-  OPT="$OPT --product-out $PRODUCT_OUT"
-fi
-if [ -n "$FS_CONFIG" ]; then
-  OPT="$OPT --fs-config-file $FS_CONFIG"
-fi
-if [ -n "$FILE_CONTEXTS" ]; then
-  OPT="$OPT --file-contexts $FILE_CONTEXTS"
-fi
-if [ -n "$TIMESTAMP" ]; then
-  OPT="$OPT -T $TIMESTAMP"
-fi
-if [ -n "$UUID" ]; then
-  OPT="$OPT -U $UUID"
-fi
-
-MAKE_EROFS_CMD="mkfs.erofs -z $COMPRESSOR $OPT $OUTPUT_FILE $SRC_DIR"
-echo $MAKE_EROFS_CMD
-$MAKE_EROFS_CMD
-
-if [ $? -ne 0 ]; then
-    exit 4
-fi
-
-SPARSE_SUFFIX=".sparse"
-if [ "$SPARSE" = true ]; then
-    img2simg $OUTPUT_FILE $OUTPUT_FILE$SPARSE_SUFFIX
-    if [ $? -ne 0 ]; then
-        exit 4
-    fi
-    mv $OUTPUT_FILE$SPARSE_SUFFIX $OUTPUT_FILE
-fi
-
diff --git a/mkfs/Makefile.am b/mkfs/Makefile.am
index 8b8e051..2a4bc1d 100644
--- a/mkfs/Makefile.am
+++ b/mkfs/Makefile.am
@@ -1,10 +1,9 @@
 # SPDX-License-Identifier: GPL-2.0+
-# Makefile.am
 
 AUTOMAKE_OPTIONS = foreign
 bin_PROGRAMS     = mkfs.erofs
 AM_CPPFLAGS = ${libuuid_CFLAGS} ${libselinux_CFLAGS}
 mkfs_erofs_SOURCES = main.c
 mkfs_erofs_CFLAGS = -Wall -Werror -I$(top_srcdir)/include
-mkfs_erofs_LDADD = ${libuuid_LIBS} $(top_builddir)/lib/liberofs.la ${libselinux_LIBS} ${liblz4_LIBS}
-
+mkfs_erofs_LDADD = ${libuuid_LIBS} $(top_builddir)/lib/liberofs.la ${libselinux_LIBS} \
+	${liblz4_LIBS} ${liblzma_LIBS}
diff --git a/mkfs/main.c b/mkfs/main.c
index abd48be..b62a8aa 100644
--- a/mkfs/main.c
+++ b/mkfs/main.c
@@ -1,7 +1,5 @@
 // SPDX-License-Identifier: GPL-2.0+
 /*
- * mkfs/main.c
- *
  * Copyright (C) 2018-2019 HUAWEI, Inc.
  *             http://www.huawei.com/
  * Created by Li Guifu <bluce.liguifu@huawei.com>
@@ -22,6 +20,10 @@
 #include "erofs/compress.h"
 #include "erofs/xattr.h"
 #include "erofs/exclude.h"
+#include "erofs/block_list.h"
+#include "erofs/compress_hints.h"
+#include "erofs/blobchunk.h"
+#include "../lib/liberofs_private.h"
 
 #ifdef HAVE_LIBUUID
 #include <uuid.h>
@@ -36,10 +38,23 @@
 #ifdef HAVE_LIBSELINUX
 	{"file-contexts", required_argument, NULL, 4},
 #endif
+	{"force-uid", required_argument, NULL, 5},
+	{"force-gid", required_argument, NULL, 6},
+	{"all-root", no_argument, NULL, 7},
+#ifndef NDEBUG
+	{"random-pclusterblks", no_argument, NULL, 8},
+#endif
+	{"max-extent-bytes", required_argument, NULL, 9},
+	{"compress-hints", required_argument, NULL, 10},
+	{"chunksize", required_argument, NULL, 11},
+	{"quiet", no_argument, 0, 12},
+	{"blobdev", required_argument, NULL, 13},
+	{"ignore-mtime", no_argument, NULL, 14},
 #ifdef WITH_ANDROID
-	{"mount-point", required_argument, NULL, 10},
-	{"product-out", required_argument, NULL, 11},
-	{"fs-config-file", required_argument, NULL, 12},
+	{"mount-point", required_argument, NULL, 512},
+	{"product-out", required_argument, NULL, 513},
+	{"fs-config-file", required_argument, NULL, 514},
+	{"block-list-file", required_argument, NULL, 515},
 #endif
 	{0, 0, 0, 0},
 };
@@ -61,25 +76,39 @@
 {
 	fputs("usage: [options] FILE DIRECTORY\n\n"
 	      "Generate erofs image from DIRECTORY to FILE, and [options] are:\n"
-	      " -zX[,Y]            X=compressor (Y=compression level, optional)\n"
-	      " -d#                set output message level to # (maximum 9)\n"
-	      " -x#                set xattr tolerance to # (< 0, disable xattrs; default 2)\n"
-	      " -EX[,...]          X=extended options\n"
-	      " -T#                set a fixed UNIX timestamp # to all files\n"
+	      " -d#                   set output message level to # (maximum 9)\n"
+	      " -x#                   set xattr tolerance to # (< 0, disable xattrs; default 2)\n"
+	      " -zX[,Y]               X=compressor (Y=compression level, optional)\n"
+	      " -C#                   specify the size of compress physical cluster in bytes\n"
+	      " -EX[,...]             X=extended options\n"
+	      " -T#                   set a fixed UNIX timestamp # to all files\n"
 #ifdef HAVE_LIBUUID
-	      " -UX                use a given filesystem UUID\n"
+	      " -UX                   use a given filesystem UUID\n"
 #endif
-	      " --exclude-path=X   avoid including file X (X = exact literal path)\n"
-	      " --exclude-regex=X  avoid including files that match X (X = regular expression)\n"
+	      " --all-root            make all files owned by root\n"
+	      " --blobdev=X           specify an extra device X to store chunked data\n"
+	      " --chunksize=#         generate chunk-based files with #-byte chunks\n"
+	      " --compress-hints=X    specify a file to configure per-file compression strategy\n"
+	      " --exclude-path=X      avoid including file X (X = exact literal path)\n"
+	      " --exclude-regex=X     avoid including files that match X (X = regular expression)\n"
 #ifdef HAVE_LIBSELINUX
-	      " --file-contexts=X  specify a file contexts file to setup selinux labels\n"
+	      " --file-contexts=X     specify a file contexts file to setup selinux labels\n"
 #endif
-	      " --help             display this help and exit\n"
+	      " --force-uid=#         set all file uids to # (# = UID)\n"
+	      " --force-gid=#         set all file gids to # (# = GID)\n"
+	      " --help                display this help and exit\n"
+	      " --ignore-mtime        use build time instead of strict per-file modification time\n"
+	      " --max-extent-bytes=#  set maximum decompressed extent size # in bytes\n"
+	      " --quiet               quiet execution (do not write anything to standard output.)\n"
+#ifndef NDEBUG
+	      " --random-pclusterblks randomize pclusterblks for big pcluster (debugging only)\n"
+#endif
 #ifdef WITH_ANDROID
 	      "\nwith following android-specific options:\n"
-	      " --mount-point=X    X=prefix of target fs path (default: /)\n"
-	      " --product-out=X    X=product_out directory\n"
-	      " --fs-config-file=X X=fs_config file\n"
+	      " --mount-point=X       X=prefix of target fs path (default: /)\n"
+	      " --product-out=X       X=product_out directory\n"
+	      " --fs-config-file=X    X=fs_config file\n"
+	      " --block-list-file=X   X=block_list file\n"
 #endif
 	      "\nAvailable compressors are: ", stderr);
 	print_available_compressors(stderr, ", ");
@@ -123,7 +152,6 @@
 				return -EINVAL;
 			/* disable compacted indexes and 0padding */
 			cfg.c_legacy_compress = true;
-			erofs_sb_clear_lz4_0padding();
 		}
 
 		if (MATCH_EXTENTED_OPT("force-inode-compact", token, keylen)) {
@@ -143,6 +171,24 @@
 				return -EINVAL;
 			erofs_sb_clear_sb_chksum();
 		}
+
+		if (MATCH_EXTENTED_OPT("noinline_data", token, keylen)) {
+			if (vallen)
+				return -EINVAL;
+			cfg.c_noinline_data = true;
+		}
+
+		if (MATCH_EXTENTED_OPT("force-inode-blockmap", token, keylen)) {
+			if (vallen)
+				return -EINVAL;
+			cfg.c_force_chunkformat = FORCE_INODE_BLOCK_MAP;
+		}
+
+		if (MATCH_EXTENTED_OPT("force-chunk-indexes", token, keylen)) {
+			if (vallen)
+				return -EINVAL;
+			cfg.c_force_chunkformat = FORCE_INODE_CHUNK_INDEXES;
+		}
 	}
 	return 0;
 }
@@ -151,8 +197,9 @@
 {
 	char *endptr;
 	int opt, i;
+	bool quiet = false;
 
-	while((opt = getopt_long(argc, argv, "d:x:z:E:T:U:",
+	while ((opt = getopt_long(argc, argv, "C:E:T:U:d:x:z:",
 				 long_options, NULL)) != -1) {
 		switch (opt) {
 		case 'z':
@@ -233,21 +280,97 @@
 			if (opt && opt != -EBUSY)
 				return opt;
 			break;
-#ifdef WITH_ANDROID
+		case 5:
+			cfg.c_uid = strtoul(optarg, &endptr, 0);
+			if (cfg.c_uid == -1 || *endptr != '\0') {
+				erofs_err("invalid uid %s", optarg);
+				return -EINVAL;
+			}
+			break;
+		case 6:
+			cfg.c_gid = strtoul(optarg, &endptr, 0);
+			if (cfg.c_gid == -1 || *endptr != '\0') {
+				erofs_err("invalid gid %s", optarg);
+				return -EINVAL;
+			}
+			break;
+		case 7:
+			cfg.c_uid = cfg.c_gid = 0;
+			break;
+#ifndef NDEBUG
+		case 8:
+			cfg.c_random_pclusterblks = true;
+			break;
+#endif
+		case 9:
+			cfg.c_max_decompressed_extent_bytes =
+				strtoul(optarg, &endptr, 0);
+			if (*endptr != '\0') {
+				erofs_err("invalid maximum uncompressed extent size %s",
+					  optarg);
+				return -EINVAL;
+			}
+			break;
 		case 10:
+			cfg.c_compress_hints_file = optarg;
+			break;
+#ifdef WITH_ANDROID
+		case 512:
 			cfg.mount_point = optarg;
 			/* all trailing '/' should be deleted */
 			opt = strlen(cfg.mount_point);
 			if (opt && optarg[opt - 1] == '/')
 				optarg[opt - 1] = '\0';
 			break;
-		case 11:
+		case 513:
 			cfg.target_out_path = optarg;
 			break;
-		case 12:
+		case 514:
 			cfg.fs_config_file = optarg;
 			break;
+		case 515:
+			cfg.block_list_file = optarg;
+			break;
 #endif
+		case 'C':
+			i = strtoull(optarg, &endptr, 0);
+			if (*endptr != '\0' ||
+			    i < EROFS_BLKSIZ || i % EROFS_BLKSIZ) {
+				erofs_err("invalid physical clustersize %s",
+					  optarg);
+				return -EINVAL;
+			}
+			cfg.c_pclusterblks_max = i / EROFS_BLKSIZ;
+			cfg.c_pclusterblks_def = cfg.c_pclusterblks_max;
+			break;
+		case 11:
+			i = strtol(optarg, &endptr, 0);
+			if (*endptr != '\0') {
+				erofs_err("invalid chunksize %s", optarg);
+				return -EINVAL;
+			}
+			cfg.c_chunkbits = ilog2(i);
+			if ((1 << cfg.c_chunkbits) != i) {
+				erofs_err("chunksize %s must be a power of two",
+					  optarg);
+				return -EINVAL;
+			}
+			if (i < EROFS_BLKSIZ) {
+				erofs_err("chunksize %s must be larger than block size",
+					  optarg);
+				return -EINVAL;
+			}
+			erofs_sb_set_chunked_file();
+			break;
+		case 12:
+			quiet = true;
+			break;
+		case 13:
+			cfg.c_blobdev_path = optarg;
+			break;
+		case 14:
+			cfg.c_ignore_mtime = true;
+			break;
 		case 1:
 			usage();
 			exit(0);
@@ -257,29 +380,45 @@
 		}
 	}
 
-	if (optind >= argc)
+	if (cfg.c_blobdev_path && cfg.c_chunkbits < LOG_BLOCK_SIZE) {
+		erofs_err("--blobdev must be used together with --chunksize");
 		return -EINVAL;
+	}
+
+	/* TODO: can be implemented with (deviceslot) mapped_blkaddr */
+	if (cfg.c_blobdev_path &&
+	    cfg.c_force_chunkformat == FORCE_INODE_BLOCK_MAP) {
+		erofs_err("--blobdev cannot work with block map currently");
+		return -EINVAL;
+	}
+
+	if (optind >= argc) {
+		erofs_err("missing argument: FILE");
+		return -EINVAL;
+	}
 
 	cfg.c_img_path = strdup(argv[optind++]);
 	if (!cfg.c_img_path)
 		return -ENOMEM;
 
 	if (optind >= argc) {
-		erofs_err("Source directory is missing");
+		erofs_err("missing argument: DIRECTORY");
 		return -EINVAL;
 	}
 
 	cfg.c_src_path = realpath(argv[optind++], NULL);
 	if (!cfg.c_src_path) {
-		erofs_err("Failed to parse source directory: %s",
+		erofs_err("failed to parse source directory: %s",
 			  erofs_strerror(-errno));
 		return -ENOENT;
 	}
 
 	if (optind < argc) {
-		erofs_err("Unexpected argument: %s\n", argv[optind]);
+		erofs_err("unexpected argument: %s\n", argv[optind]);
 		return -EINVAL;
 	}
+	if (quiet)
+		cfg.c_dbg_lvl = EROFS_ERR;
 	return 0;
 }
 
@@ -290,7 +429,7 @@
 	struct erofs_super_block sb = {
 		.magic     = cpu_to_le32(EROFS_SUPER_MAGIC_V1),
 		.blkszbits = LOG_BLOCK_SIZE,
-		.inos   = 0,
+		.inos   = cpu_to_le64(sbi.inos),
 		.build_time = cpu_to_le64(sbi.build_time),
 		.build_time_nsec = cpu_to_le32(sbi.build_time_nsec),
 		.blocks = 0,
@@ -299,19 +438,26 @@
 		.feature_incompat = cpu_to_le32(sbi.feature_incompat),
 		.feature_compat = cpu_to_le32(sbi.feature_compat &
 					      ~EROFS_FEATURE_COMPAT_SB_CHKSUM),
+		.extra_devices = cpu_to_le16(sbi.extra_devices),
+		.devt_slotoff = cpu_to_le16(sbi.devt_slotoff),
 	};
 	const unsigned int sb_blksize =
 		round_up(EROFS_SUPER_END, EROFS_BLKSIZ);
 	char *buf;
 
-	*blocks         = erofs_mapbh(NULL, true);
+	*blocks         = erofs_mapbh(NULL);
 	sb.blocks       = cpu_to_le32(*blocks);
 	sb.root_nid     = cpu_to_le16(root_nid);
 	memcpy(sb.uuid, sbi.uuid, sizeof(sb.uuid));
 
+	if (erofs_sb_has_compr_cfgs())
+		sb.u1.available_compr_algs = sbi.available_compr_algs;
+	else
+		sb.u1.lz4_max_distance = cpu_to_le16(sbi.lz4_max_distance);
+
 	buf = calloc(sb_blksize, 1);
 	if (!buf) {
-		erofs_err("Failed to allocate memory for sb: %s",
+		erofs_err("failed to allocate memory for sb: %s",
 			  erofs_strerror(-errno));
 		return -ENOMEM;
 	}
@@ -322,19 +468,6 @@
 	return 0;
 }
 
-#define CRC32C_POLY_LE	0x82F63B78
-static inline u32 crc32c(u32 crc, const u8 *in, size_t len)
-{
-	int i;
-
-	while (len--) {
-		crc ^= *in++;
-		for (i = 0; i < 8; i++)
-			crc = (crc >> 1) ^ ((crc & 1) ? CRC32C_POLY_LE : 0);
-	}
-	return crc;
-}
-
 static int erofs_mkfs_superblock_csum_set(void)
 {
 	int ret;
@@ -342,7 +475,7 @@
 	u32 crc;
 	struct erofs_super_block *sb;
 
-	ret = blk_read(buf, 0, 1);
+	ret = blk_read(0, buf, 0, 1);
 	if (ret) {
 		erofs_err("failed to read superblock to set checksum: %s",
 			  erofs_strerror(ret));
@@ -363,7 +496,7 @@
 	/* turn on checksum feature */
 	sb->feature_compat = cpu_to_le32(le32_to_cpu(sb->feature_compat) |
 					 EROFS_FEATURE_COMPAT_SB_CHKSUM);
-	crc = crc32c(~0, (u8 *)sb, EROFS_BLKSIZ - EROFS_SUPER_OFFSET);
+	crc = erofs_crc32c(~0, (u8 *)sb, EROFS_BLKSIZ - EROFS_SUPER_OFFSET);
 
 	/* set up checksum field to erofs_super_block */
 	sb->checksum = cpu_to_le32(crc);
@@ -383,7 +516,8 @@
 {
 	cfg.c_legacy_compress = false;
 	sbi.feature_incompat = EROFS_FEATURE_INCOMPAT_LZ4_0PADDING;
-	sbi.feature_compat = EROFS_FEATURE_COMPAT_SB_CHKSUM;
+	sbi.feature_compat = EROFS_FEATURE_COMPAT_SB_CHKSUM |
+			     EROFS_FEATURE_COMPAT_MTIME;
 
 	/* generate a default uuid first */
 #ifdef HAVE_LIBUUID
@@ -406,7 +540,7 @@
 
 	epoch = strtoull(source_date_epoch, &endptr, 10);
 	if (epoch == -1ULL || *endptr != '\0') {
-		erofs_err("Environment variable $SOURCE_DATE_EPOCH %s is invalid",
+		erofs_err("environment variable $SOURCE_DATE_EPOCH %s is invalid",
 			  source_date_epoch);
 		return -EINVAL;
 	}
@@ -420,6 +554,12 @@
 	return 0;
 }
 
+void erofs_show_progs(int argc, char *argv[])
+{
+	if (cfg.c_dbg_lvl >= EROFS_WARN)
+		fprintf(stderr, "%s %s\n", basename(argv[0]), cfg.c_version);
+}
+
 int main(int argc, char **argv)
 {
 	int err = 0;
@@ -432,11 +572,10 @@
 	char uuid_str[37] = "not available";
 
 	erofs_init_configure();
-	fprintf(stderr, "%s %s\n", basename(argv[0]), cfg.c_version);
-
 	erofs_mkfs_default_options();
 
 	err = mkfs_parse_options_cfg(argc, argv);
+	erofs_show_progs(argc, argv);
 	if (err) {
 		if (err == -EINVAL)
 			usage();
@@ -449,10 +588,16 @@
 		return 1;
 	}
 
+	if (cfg.c_chunkbits) {
+		err = erofs_blob_init(cfg.c_blobdev_path);
+		if (err)
+			return 1;
+	}
+
 	err = lstat64(cfg.c_src_path, &st);
 	if (err)
 		return 1;
-	if ((st.st_mode & S_IFMT) != S_IFDIR) {
+	if (!S_ISDIR(st.st_mode)) {
 		erofs_err("root of the filesystem is not a directory - %s",
 			  cfg.c_src_path);
 		usage();
@@ -479,32 +624,54 @@
 		erofs_err("failed to load fs config %s", cfg.fs_config_file);
 		return 1;
 	}
+
+	if (cfg.block_list_file && erofs_droid_blocklist_fopen() < 0) {
+		erofs_err("failed to open %s", cfg.block_list_file);
+		return 1;
+	}
 #endif
-
 	erofs_show_config();
+	if (erofs_sb_has_chunked_file())
+		erofs_warn("EXPERIMENTAL chunked file feature in use. Use at your own risk!");
 	erofs_set_fs_root(cfg.c_src_path);
-
+#ifndef NDEBUG
+	if (cfg.c_random_pclusterblks)
+		srand(time(NULL));
+#endif
 	sb_bh = erofs_buffer_init();
 	if (IS_ERR(sb_bh)) {
 		err = PTR_ERR(sb_bh);
-		erofs_err("Failed to initialize buffers: %s",
+		erofs_err("failed to initialize buffers: %s",
 			  erofs_strerror(err));
 		goto exit;
 	}
 	err = erofs_bh_balloon(sb_bh, EROFS_SUPER_END);
 	if (err < 0) {
-		erofs_err("Failed to balloon erofs_super_block: %s",
+		erofs_err("failed to balloon erofs_super_block: %s",
 			  erofs_strerror(err));
 		goto exit;
 	}
 
-	err = z_erofs_compress_init();
+	err = erofs_load_compress_hints();
 	if (err) {
-		erofs_err("Failed to initialize compressor: %s",
+		erofs_err("failed to load compress hints %s",
+			  cfg.c_compress_hints_file);
+		goto exit;
+	}
+
+	err = z_erofs_compress_init(sb_bh);
+	if (err) {
+		erofs_err("failed to initialize compressor: %s",
 			  erofs_strerror(err));
 		goto exit;
 	}
 
+	err = erofs_generate_devtable();
+	if (err) {
+		erofs_err("failed to generate device table: %s",
+			  erofs_strerror(err));
+		goto exit;
+	}
 #ifdef HAVE_LIBUUID
 	uuid_unparse_lower(sbi.uuid, uuid_str);
 #endif
@@ -514,7 +681,7 @@
 
 	err = erofs_build_shared_xattrs_from_path(cfg.c_src_path);
 	if (err) {
-		erofs_err("Failed to build shared xattrs: %s",
+		erofs_err("failed to build shared xattrs: %s",
 			  erofs_strerror(err));
 		goto exit;
 	}
@@ -528,6 +695,13 @@
 	root_nid = erofs_lookupnid(root_inode);
 	erofs_iput(root_inode);
 
+	if (cfg.c_chunkbits) {
+		erofs_info("total metadata: %u blocks", erofs_mapbh(NULL));
+		err = erofs_blob_remap();
+		if (err)
+			goto exit;
+	}
+
 	err = erofs_mkfs_update_super_block(sb_bh, root_nid, &nblocks);
 	if (err)
 		goto exit;
@@ -542,8 +716,14 @@
 		err = erofs_mkfs_superblock_csum_set();
 exit:
 	z_erofs_compress_exit();
+#ifdef WITH_ANDROID
+	erofs_droid_blocklist_fclose();
+#endif
 	dev_close();
+	erofs_cleanup_compress_hints();
 	erofs_cleanup_exclude_rules();
+	if (cfg.c_chunkbits)
+		erofs_blob_exit();
 	erofs_exit_configure();
 
 	if (err) {
diff --git a/scripts/get-version-number b/scripts/get-version-number
new file mode 100755
index 0000000..26f0b5a
--- /dev/null
+++ b/scripts/get-version-number
@@ -0,0 +1,33 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0
+
+scm_version()
+{
+	# Check for git and a git repo.
+	if test -z "$(git rev-parse --show-cdup 2>/dev/null)" &&
+	   head="$(git rev-parse --verify HEAD 2>/dev/null)"; then
+		# If we are at a tagged commit, we ignore it.
+		if [ -z "$(git describe --exact-match 2>/dev/null)" ]; then
+			# Add -g and 8 hex chars.
+			printf '%s%s' -g "$(echo $head | cut -c1-8)"
+		fi
+		# Check for uncommitted changes.
+		# This script must avoid any write attempt to the source tree,
+		# which might be read-only.
+		# You cannot use 'git describe --dirty' because it tries to
+		# create .git/index.lock .
+		# First, with git-status, but --no-optional-locks is only
+		# supported in git >= 2.14, so fall back to git-diff-index if
+		# it fails. Note that git-diff-index does not refresh the
+		# index, so it may give misleading results. See
+		# git-update-index(1), git-diff-index(1), and git-status(1).
+		if {
+			git --no-optional-locks status -uno --porcelain 2>/dev/null ||
+			git diff-index --name-only HEAD
+		} | read dummy; then
+			printf '%s' -dirty
+		fi
+	fi
+}
+
+echo $(sed -n '1p' VERSION | tr -d '\n')$(scm_version)