Add --incremental=file-merge for local directory incremental builds.
The mode targets overlayfs-style copy-up outputs, where a sparse upper
file contains only bytes dirtied by the upper layer, while holes mean
that data from the lower image is still valid.

The desired merge looks like this:

                0            a            b            c            EOF
  result:       | inherit    | promote    | inherit    | promote      |
  upper source: | hole       | dirty      | hole       | dirty        |
  lower image:  |<-------- compressed extent A -------->|<- extent B->|

For compressed files, walk the sparse upper source and lower file
mappings together.  If a lower compressed extent is unchanged in the
upper source, inherit it directly.  If upper data appears inside a
lower extent, inherit only the clean prefix and recompress the dirty
suffix from the upper source.

                0            d                                  end
  result:       | inherit    | recompress promoted suffix         |
  upper source: | hole       | dirty bytes ...                    |
  lower extent: |<-------------- compressed extent -------------->|

Assisted-by: Codex:GPT-5.5
Signed-off-by: Yifan Zhao <[email protected]>
---
 include/erofs/importer.h  |   1 +
 include/erofs/internal.h  |   1 +
 lib/Makefile.am           |   4 +-
 lib/compress.c            | 211 +++++++++++++++++++++++++++++++++++++-
 lib/file_merge.c          | 118 +++++++++++++++++++++
 lib/inode.c               | 107 +++++++++++++------
 lib/liberofs_compress.h   |   3 +
 lib/liberofs_file_merge.h |  22 ++++
 lib/liberofs_rebuild.h    |   3 +-
 lib/rebuild.c             |  77 +++++++++++++-
 man/mkfs.erofs.1          |  11 +-
 mkfs/main.c               |  17 ++-
 12 files changed, 533 insertions(+), 42 deletions(-)
 create mode 100644 lib/file_merge.c
 create mode 100644 lib/liberofs_file_merge.h

diff --git a/include/erofs/importer.h b/include/erofs/importer.h
index 07e40b4..e194743 100644
--- a/include/erofs/importer.h
+++ b/include/erofs/importer.h
@@ -57,6 +57,7 @@ struct erofs_importer_params {
        bool dot_omitted;
        bool no_xattrs;                 /* don't store extended attributes */
        bool no_zcompact;
+       bool incremental_file_merge;
        bool ztailpacking;
        char dedupe;
        bool fragments;
diff --git a/include/erofs/internal.h b/include/erofs/internal.h
index cba1348..e7cb041 100644
--- a/include/erofs/internal.h
+++ b/include/erofs/internal.h
@@ -214,6 +214,7 @@ enum erofs_inode_data_source_type {
 
 struct erofs_inode_datasource {
        unsigned char type;
+       struct erofs_inode *lower_inode;
        union {
                struct erofs_diskbuf *diskbuf;
                struct {
diff --git a/lib/Makefile.am b/lib/Makefile.am
index cecbe36..780df25 100644
--- a/lib/Makefile.am
+++ b/lib/Makefile.am
@@ -26,6 +26,7 @@ noinst_HEADERS = $(top_srcdir)/include/erofs_fs.h \
       $(top_srcdir)/lib/liberofs_base64.h \
       $(top_srcdir)/lib/liberofs_cache.h \
       $(top_srcdir)/lib/liberofs_compress.h \
+      $(top_srcdir)/lib/liberofs_file_merge.h \
       $(top_srcdir)/lib/liberofs_fragments.h \
       $(top_srcdir)/lib/liberofs_private.h \
       $(top_srcdir)/lib/liberofs_rebuild.h \
@@ -46,7 +47,8 @@ liberofs_la_SOURCES = config.c io.c cache.c super.c inode.c 
xattr.c exclude.c \
                      compress_hints.c hashmap.c sha256.c blobchunk.c dir.c \
                      fragments.c dedupe.c uuid_unparse.c uuid.c tar.c \
                      block_list.c rebuild.c diskbuf.c bitops.c dedupe_ext.c \
-                     vmdk.c metabox.c global.c importer.c base64.c
+                     vmdk.c metabox.c global.c importer.c base64.c \
+                     file_merge.c
 
 liberofs_la_CFLAGS = -Wall ${libuuid_CFLAGS} -I$(top_srcdir)/include
 liberofs_la_LDFLAGS = ${libselinux_LIBS} ${libuuid_LIBS} ${liblz4_LIBS} \
diff --git a/lib/compress.c b/lib/compress.c
index 633b69a..b6ea3ba 100644
--- a/lib/compress.c
+++ b/lib/compress.c
@@ -1398,7 +1398,8 @@ int erofs_commit_compressed_file(struct 
z_erofs_compress_ictx *ictx,
 
        if (ptotal)
                (void)erofs_bh_balloon(bh, ptotal);
-       else if (!params->fragments && params->dedupe != EROFS_DEDUPE_FORCE_ON)
+       else if (!params->fragments && params->dedupe != EROFS_DEDUPE_FORCE_ON 
&&
+                !ictx->dedupe)
                DBG_BUGON(!inode->idata_size);
 
        erofs_info("compressed %s (%llu bytes) into %llu bytes",
@@ -1820,6 +1821,8 @@ void *erofs_prepare_compressed_file(struct erofs_importer 
*im,
        bool frag = params->fragments && !erofs_is_packed_inode(inode) &&
                !erofs_is_metabox_inode(inode);
        bool all_fragments = params->all_fragments && frag;
+       bool file_merge = params->incremental_file_merge &&
+                         inode->datasource.lower_inode;
 
        /* initialize per-file compression setting */
        inode->z_advise = 0;
@@ -1837,7 +1840,7 @@ void *erofs_prepare_compressed_file(struct erofs_importer 
*im,
        inode->idata_size = 0;
        inode->fragment_size = 0;
 
-       if (!z_erofs_mt_enabled || all_fragments) {
+       if (!z_erofs_mt_enabled || all_fragments || file_merge) {
 #ifdef EROFS_MT_ENABLED
                pthread_mutex_lock(&g_ictx.mutex);
                while (g_ictx.seg_num) {
@@ -2047,6 +2050,210 @@ out:
        return ret;
 }
 
+static erofs_off_t z_erofs_file_merge_next_data(struct erofs_vfile *dirty_vf,
+                                               erofs_off_t start,
+                                               erofs_off_t end)
+{
+       off_t dataoff;
+
+       dataoff = erofs_io_lseek(dirty_vf, start, SEEK_DATA);
+       if (dataoff < 0)
+               return dataoff == -ENXIO ? end : start;
+       if ((erofs_off_t)dataoff >= end)
+               return end;
+       return max_t(erofs_off_t, dataoff, start);
+}
+
+static bool z_erofs_file_merge_can_inherit(struct z_erofs_compress_ictx *ictx,
+                                          struct erofs_map_blocks *map)
+{
+       struct erofs_sb_info *sbi = ictx->inode->sbi;
+       unsigned int blksz = erofs_blksiz(sbi);
+
+       if (!(map->m_flags & EROFS_MAP_MAPPED) ||
+           !(map->m_flags & EROFS_MAP_ENCODED))
+               return false;
+       if (map->m_flags & (EROFS_MAP_META | __EROFS_MAP_FRAGMENT |
+                           EROFS_MAP_PARTIAL_REF))
+               return false;
+       if (map->m_deviceid)
+               return false;
+       if ((map->m_pa | map->m_plen) & (blksz - 1))
+               return false;
+       if (map->m_algorithmformat != ictx->inode->z_algorithmtype[0])
+               return false;
+       if (map->m_llen > UINT_MAX)
+               return false;
+       return true;
+}
+
+static int z_erofs_file_merge_inherit_extent(struct z_erofs_compress_ictx 
*ictx,
+                                            struct erofs_map_blocks *map,
+                                            erofs_off_t length, bool partial)
+{
+       struct z_erofs_extent_item *ei;
+
+       if (length > UINT_MAX)
+               return -EOPNOTSUPP;
+       ei = malloc(sizeof(*ei));
+       if (!ei)
+               return -ENOMEM;
+       init_list_head(&ei->list);
+       ei->e = (struct z_erofs_inmem_extent) {
+               .pstart = map->m_pa,
+               .plen = map->m_plen,
+               .length = length,
+               .raw = false,
+               .partial = partial,
+       };
+       list_add_tail(&ei->list, &ictx->extents);
+       ictx->dedupe = true;
+       erofs_sb_set_dedupe(ictx->inode->sbi);
+       ictx->inode->sbi->saved_by_deduplication += map->m_plen;
+       return 0;
+}
+
+static int z_erofs_file_merge_promote_extent(struct z_erofs_compress_ictx 
*ictx,
+                                            erofs_off_t lstart,
+                                            erofs_off_t length,
+                                            erofs_off_t *pstart)
+{
+       static u8 g_queue[Z_EROFS_COMPR_QUEUE_SZ];
+       struct z_erofs_compress_sctx sctx;
+       int ret;
+
+       if (length > UINT_MAX)
+               return -EOPNOTSUPP;
+       sctx = (struct z_erofs_compress_sctx) {
+               .ictx = ictx,
+               .queue = g_queue,
+               .chandle = &ictx->ccfg->handle,
+               .remaining = length,
+               .seg_idx = 0,
+               .pivot = &dummy_pivot,
+               .pclustersize = z_erofs_get_pclustersize(ictx),
+       };
+       init_list_head(&sctx.extents);
+
+       ret = z_erofs_compress_segment(&sctx, lstart, *pstart);
+       if (ret)
+               return ret;
+
+       list_splice_tail(&sctx.extents, &ictx->extents);
+       *pstart = sctx.pstart;
+       return 0;
+}
+
+int erofs_write_compressed_file_file_merge(struct z_erofs_compress_ictx *ictx,
+                                          struct erofs_vfile *dirty_vf,
+                                          struct erofs_inode *lower)
+{
+       struct erofs_buffer_head *bh;
+       struct erofs_inode *inode = ictx->inode;
+       struct erofs_sb_info *sbi = inode->sbi;
+       erofs_off_t pstart, pcur, pos;
+       int ret;
+
+#ifdef EROFS_MT_ENABLED
+       if (ictx != &g_ictx)
+               return erofs_write_compressed_file(ictx);
+#endif
+       if (ictx->im->params->fragments || ictx->im->params->ztailpacking ||
+           ictx->data_unaligned)
+               return erofs_write_compressed_file(ictx);
+
+       bh = erofs_balloc(sbi->bmgr, DATA, 0, 0);
+       if (IS_ERR(bh)) {
+               ret = PTR_ERR(bh);
+               goto err_free_idata;
+       }
+       pstart = erofs_pos(sbi, erofs_mapbh(NULL, bh->block));
+       pcur = pstart;
+
+       for (pos = 0; pos < inode->i_size; ) {
+               struct erofs_map_blocks map = {
+                       .buf = __EROFS_BUF_INITIALIZER,
+                       .m_la = pos,
+               };
+               erofs_off_t end, promote_start;
+               bool can_inherit;
+
+               ret = erofs_map_blocks(lower, &map, EROFS_GET_BLOCKS_FIEMAP);
+               if (ret)
+                       goto err_bdrop;
+               if (!map.m_llen) {
+                       ret = -EIO;
+                       goto err_bdrop;
+               }
+               if (map.m_la < pos) {
+                       if (map.m_la + map.m_llen <= pos) {
+                               ret = -EIO;
+                               goto err_bdrop;
+                       }
+                       map.m_llen -= pos - map.m_la;
+                       map.m_la = pos;
+               }
+               if (map.m_la != pos) {
+                       ret = -EIO;
+                       goto err_bdrop;
+               }
+               if (map.m_la + map.m_llen > inode->i_size)
+                       map.m_llen = inode->i_size - map.m_la;
+
+               end = map.m_la + map.m_llen;
+               can_inherit = z_erofs_file_merge_can_inherit(ictx, &map);
+               promote_start = map.m_la;
+               if (can_inherit) {
+                       erofs_off_t dataoff;
+
+                       dataoff = z_erofs_file_merge_next_data(dirty_vf,
+                                                              map.m_la, end);
+                       if (dataoff >= end) {
+                               ret = z_erofs_file_merge_inherit_extent(ictx,
+                                               &map, map.m_llen, false);
+                               if (ret)
+                                       goto err_bdrop;
+                               pos = end;
+                               continue;
+                       }
+
+                       promote_start = round_down(dataoff, erofs_blksiz(sbi));
+                       if (promote_start > map.m_la) {
+                               ret = z_erofs_file_merge_inherit_extent(ictx,
+                                               &map, promote_start - map.m_la,
+                                               true);
+                               if (ret)
+                                       goto err_bdrop;
+                       }
+               }
+               ret = z_erofs_file_merge_promote_extent(ictx, promote_start,
+                                                       end - promote_start,
+                                                       &pcur);
+               if (ret)
+                       goto err_bdrop;
+               pos = end;
+       }
+
+       ret = erofs_commit_compressed_file(ictx, bh, pstart, pcur - pstart);
+       goto out;
+
+err_bdrop:
+       erofs_bdrop(bh, true);
+err_free_idata:
+       if (inode->idata) {
+               free(inode->idata);
+               inode->idata = NULL;
+       }
+out:
+#ifdef EROFS_MT_ENABLED
+       pthread_mutex_lock(&ictx->mutex);
+       ictx->seg_num = ret < 0 ? INT_MAX : 0;
+       pthread_cond_signal(&ictx->cond);
+       pthread_mutex_unlock(&ictx->mutex);
+#endif
+       return ret;
+}
+
 int erofs_begin_compress_dir(struct erofs_importer *im,
                             struct erofs_inode *inode)
 
diff --git a/lib/file_merge.c b/lib/file_merge.c
new file mode 100644
index 0000000..169947f
--- /dev/null
+++ b/lib/file_merge.c
@@ -0,0 +1,118 @@
+// SPDX-License-Identifier: GPL-2.0+ OR MIT
+#define _GNU_SOURCE
+#include <errno.h>
+#include <config.h>
+#include "liberofs_file_merge.h"
+
+bool erofs_inode_dvf_is_file_merge(struct erofs_inode_datasource_vfile *dvf)
+{
+       return dvf->inode->datasource.type == EROFS_INODE_DATA_SOURCE_LOCALPATH 
&&
+              dvf->inode->datasource.lower_inode;
+}
+
+bool erofs_inode_vfile_is_file_merge(struct erofs_vfile *vf,
+                                    const struct erofs_vfops *inode_vfops)
+{
+       if (vf->ops != inode_vfops)
+               return false;
+       return erofs_inode_dvf_is_file_merge(
+                       container_of(vf, struct erofs_inode_datasource_vfile,
+                                    vf));
+}
+
+static bool erofs_inode_vfseek_fallback(off_t ret)
+{
+       return ret == -ENXIO || ret == -EOPNOTSUPP || ret == -EINVAL;
+}
+
+static int erofs_inode_vfnext_data(struct erofs_vfile *vf, u64 pos, u64 end,
+                                  u64 *next, bool *data)
+{
+       off_t dataoff, holeoff;
+
+       dataoff = erofs_io_lseek(&(struct erofs_vfile){ .fd = vf->fd },
+                                pos, SEEK_DATA);
+       if (dataoff < 0) {
+               if (dataoff == -ENXIO) {
+                       *data = false;
+                       *next = end;
+                       return 0;
+               }
+               if (erofs_inode_vfseek_fallback(dataoff)) {
+                       *data = true;
+                       *next = end;
+                       return 0;
+               }
+               return dataoff;
+       }
+
+       if ((u64)dataoff > pos) {
+               *data = false;
+               *next = min_t(u64, dataoff, end);
+               return 0;
+       }
+
+       holeoff = erofs_io_lseek(&(struct erofs_vfile){ .fd = vf->fd },
+                                pos, SEEK_HOLE);
+       if (holeoff < 0) {
+               if (erofs_inode_vfseek_fallback(holeoff)) {
+                       *data = true;
+                       *next = end;
+                       return 0;
+               }
+               return holeoff;
+       }
+       *data = true;
+       *next = min_t(u64, holeoff, end);
+       return 0;
+}
+
+ssize_t erofs_inode_dvf_pread_file_merge(
+               struct erofs_inode_datasource_vfile *dvf, void *buf,
+               size_t len, u64 offset)
+{
+       struct erofs_inode *inode = dvf->inode;
+       struct erofs_vfile *vf = &dvf->vf;
+       struct erofs_vfile lower = {};
+       u64 end;
+       size_t done = 0;
+       int err;
+
+       if (offset >= inode->i_size)
+               return 0;
+       len = min_t(u64, len, inode->i_size - offset);
+       end = offset + len;
+
+       if (!inode->datasource.lower_inode)
+               return -EINVAL;
+       err = erofs_iopen(&lower, inode->datasource.lower_inode);
+       if (err)
+               return err;
+
+       while (offset < end) {
+               bool data = false;
+               u64 next = offset;
+               ssize_t ret;
+
+               ret = erofs_inode_vfnext_data(vf, offset, end, &next, &data);
+               if (ret < 0)
+                       return ret;
+               if (next <= offset)
+                       return -EIO;
+
+               if (data)
+                       ret = erofs_io_pread(&(struct erofs_vfile){ .fd = 
vf->fd },
+                                            (char *)buf + done, next - offset,
+                                            offset);
+               else
+                       ret = erofs_io_pread(&lower, (char *)buf + done,
+                                            next - offset, offset);
+               if (ret < 0)
+                       return ret;
+               if (!ret)
+                       break;
+               done += ret;
+               offset += ret;
+       }
+       return done;
+}
diff --git a/lib/inode.c b/lib/inode.c
index 328215b..b4abef0 100644
--- a/lib/inode.c
+++ b/lib/inode.c
@@ -28,6 +28,7 @@
 #include "liberofs_cache.h"
 #include "liberofs_compress.h"
 #include "liberofs_fragments.h"
+#include "liberofs_file_merge.h"
 #include "liberofs_metabox.h"
 #include "liberofs_private.h"
 #include "liberofs_rebuild.h"
@@ -76,13 +77,6 @@ static const umode_t erofs_dtype_by_umode[EROFS_FT_MAX] = {
        [EROFS_FT_SYMLINK]      = S_IFLNK
 };
 
-struct erofs_inode_datasource_vfile {
-       struct erofs_vfile vf;
-       struct erofs_inode *inode;
-       u64 base;
-       u64 pos;
-};
-
 umode_t erofs_ftype_to_mode(unsigned int ftype, unsigned int perm)
 {
        if (ftype >= EROFS_FT_MAX)
@@ -147,6 +141,11 @@ struct erofs_inode *erofs_iget(dev_t dev, ino_t ino)
 
 static void erofs_inode_free_datasource(struct erofs_inode *inode)
 {
+       if (inode->datasource.lower_inode) {
+               erofs_iput(inode->datasource.lower_inode);
+               inode->datasource.lower_inode = NULL;
+       }
+
        switch (inode->datasource.type) {
        case EROFS_INODE_DATA_SOURCE_DISKBUF:
                erofs_diskbuf_close(inode->datasource.diskbuf);
@@ -169,6 +168,9 @@ static ssize_t erofs_inode_vfpread(struct erofs_vfile *vf, 
void *buf,
        struct erofs_inode_datasource_vfile *dvf =
                container_of(vf, struct erofs_inode_datasource_vfile, vf);
 
+       if (erofs_inode_dvf_is_file_merge(dvf))
+               return erofs_inode_dvf_pread_file_merge(dvf, buf, len, offset);
+
        return erofs_io_pread(&(struct erofs_vfile){ .fd = vf->fd },
                              buf, len, dvf->base + offset);
 }
@@ -213,6 +215,8 @@ static off_t erofs_inode_vflseek(struct erofs_vfile *vf, 
u64 offset,
                break;
        case SEEK_DATA:
        case SEEK_HOLE:
+               if (erofs_inode_dvf_is_file_merge(dvf))
+                       return -EOPNOTSUPP;
                if (offset > dvf->inode->i_size)
                        return -ENXIO;
                if (offset == dvf->inode->i_size) {
@@ -305,10 +309,12 @@ static struct erofs_inode_datasource_vfile 
*erofs_inode_open_data_vfile(
        if (dvf->vf.fd < 0) {
                ret = inode->datasource.type == EROFS_INODE_DATA_SOURCE_DISKBUF 
?
                      dvf->vf.fd : -errno;
-               free(dvf);
-               return ERR_PTR(ret);
+               goto err_free;
        }
        return dvf;
+err_free:
+       free(dvf);
+       return ERR_PTR(ret);
 }
 
 unsigned int erofs_iput(struct erofs_inode *inode)
@@ -868,31 +874,38 @@ static int erofs_write_unencoded_data(struct erofs_inode 
*inode,
 }
 
 static int erofs_write_unencoded_file(struct erofs_inode *inode,
-                                     struct erofs_inode_datasource_vfile *src)
+                                     struct erofs_vfile *vf)
 {
-       int ret;
+       bool rebuild_blob = inode->datasource.type ==
+                           EROFS_INODE_DATA_SOURCE_REBUILD_BLOB;
+       off_t off;
 
-       ret = erofs_io_lseek(&src->vf, 0, SEEK_SET);
-       if (ret)
-               return ret;
+       off = erofs_io_lseek(vf, 0, SEEK_SET);
+       if (off < 0)
+               return off;
+       if (off)
+               return -EIO;
 
        if (cfg.c_chunkbits &&
-           inode->datasource.type != EROFS_INODE_DATA_SOURCE_REBUILD_BLOB) {
+           vf->fd >= 0 &&
+           !erofs_inode_vfile_is_file_merge(vf, &erofs_inode_vfops) &&
+           !rebuild_blob) {
                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, &src->vf);
+               return erofs_blob_write_chunked_file(inode, vf);
        }
 
-       if (inode->datasource.type != EROFS_INODE_DATA_SOURCE_REBUILD_BLOB)
+       if (!rebuild_blob)
                inode->datalayout = EROFS_INODE_FLAT_INLINE;
        /* fallback to all data uncompressed */
-       return erofs_write_unencoded_data(inode, &src->vf, 0,
+       return erofs_write_unencoded_data(inode, vf, 0,
+                       vf->fd < 0 ||
+                       erofs_inode_vfile_is_file_merge(vf, &erofs_inode_vfops) 
||
                        inode->datasource.type == 
EROFS_INODE_DATA_SOURCE_DISKBUF ||
-                       inode->datasource.type == 
EROFS_INODE_DATA_SOURCE_REBUILD_BLOB,
-                       false);
+                       rebuild_blob, false);
 }
 
 static int erofs_write_dir_file(const struct erofs_importer *im,
@@ -1665,22 +1678,37 @@ struct erofs_mkfs_job_ndir_ctx {
 static int erofs_mkfs_job_write_file(struct erofs_mkfs_job_ndir_ctx *ctx)
 {
        struct erofs_inode *inode = ctx->inode;
+       struct erofs_vfile *vf = &ctx->src->vf;
        int ret;
 
        if (ctx->ictx) {
-               ret = erofs_io_lseek(&ctx->src->vf, 0, SEEK_SET);
-               if (ret)
+               off_t off;
+
+               off = erofs_io_lseek(vf, 0, SEEK_SET);
+               if (off < 0) {
+                       ret = off;
+                       goto out;
+               }
+               if (off) {
+                       ret = -EIO;
                        goto out;
-               ret = erofs_write_compressed_file(ctx->ictx);
+               }
+               if (erofs_inode_dvf_is_file_merge(ctx->src))
+                       ret = erofs_write_compressed_file_file_merge(ctx->ictx,
+                               &(struct erofs_vfile){ .fd = ctx->src->vf.fd },
+                               inode->datasource.lower_inode);
+               else
+                       ret = erofs_write_compressed_file(ctx->ictx);
                if (ret != -ENOSPC)
                        goto out;
        }
        /* fallback to all data uncompressed */
-       ret = erofs_write_unencoded_file(inode, ctx->src);
+       ret = erofs_write_unencoded_file(inode, vf);
 out:
-       DBG_BUGON(!ctx->src || ctx->src->vf.fd < 0);
-       erofs_io_close(&ctx->src->vf);
-       ctx->src = NULL;
+       if (ctx->src) {
+               erofs_io_close(&ctx->src->vf);
+               ctx->src = NULL;
+       }
        return ret;
 }
 
@@ -2101,7 +2129,9 @@ static int erofs_prepare_dir_inode(const struct 
erofs_mkfs_btctx *ctx,
        }
 
        if (ctx->incremental && dir->dev == sbi->dev && !dir->opaque) {
-               ret = erofs_rebuild_load_basedir(dir, &nr_subdirs, &i_nlink);
+               ret = erofs_rebuild_load_basedir(dir, &nr_subdirs, &i_nlink,
+                                                
im->params->incremental_file_merge,
+                                                im->params->hard_dereference);
                if (ret)
                        return ret;
        }
@@ -2178,17 +2208,23 @@ static int erofs_mkfs_begin_nondirectory(const struct 
erofs_mkfs_btctx *btctx,
        int ret;
 
        if (S_ISREG(inode->i_mode) && inode->i_size) {
+               bool rebuild_blob;
+               struct erofs_vfile *vf;
+
                ctx.src = erofs_inode_open_data_vfile(inode);
                if (IS_ERR(ctx.src))
                        return PTR_ERR(ctx.src);
                if (!ctx.src)
                        goto out;
+               vf = &ctx.src->vf;
 
-               ret = erofs_set_inode_fingerprint(inode, &ctx.src->vf);
+               ret = erofs_set_inode_fingerprint(inode, vf);
                if (ret < 0)
                        goto err_close;
 
-               if (inode->datasource.type != 
EROFS_INODE_DATA_SOURCE_REBUILD_BLOB &&
+               rebuild_blob = inode->datasource.type ==
+                       EROFS_INODE_DATA_SOURCE_REBUILD_BLOB;
+               if (!rebuild_blob &&
                    inode->sbi->available_compr_algs &&
                    erofs_file_is_compressible(im, inode)) {
                        ctx.ictx = erofs_prepare_compressed_file(im, inode);
@@ -2197,7 +2233,7 @@ static int erofs_mkfs_begin_nondirectory(const struct 
erofs_mkfs_btctx *btctx,
                                goto err_close;
                        }
                        erofs_bind_compressed_file_with_vfile(ctx.ictx,
-                                                             &ctx.src->vf);
+                                                             vf);
                        ret = erofs_begin_compressed_file(ctx.ictx);
                        if (ret)
                                goto err_close;
@@ -2206,12 +2242,15 @@ static int erofs_mkfs_begin_nondirectory(const struct 
erofs_mkfs_btctx *btctx,
 out:
        ret = erofs_mkfs_go(btctx, EROFS_MKFS_JOB_NDIR, &ctx, sizeof(ctx));
 #ifdef EROFS_MT_ENABLED
-       if (ret && ctx.src)
-               erofs_io_close(&ctx.src->vf);
+       if (ret) {
+               if (ctx.src)
+                       erofs_io_close(&ctx.src->vf);
+       }
 #endif
        return ret;
 err_close:
-       erofs_io_close(&ctx.src->vf);
+       if (ctx.src)
+               erofs_io_close(&ctx.src->vf);
        return ret;
 }
 
diff --git a/lib/liberofs_compress.h b/lib/liberofs_compress.h
index 55a8943..73cdfbe 100644
--- a/lib/liberofs_compress.h
+++ b/lib/liberofs_compress.h
@@ -23,6 +23,9 @@ void erofs_bind_compressed_file_with_fd(struct 
z_erofs_compress_ictx *ictx,
                                        int fd, u64 fpos);
 int erofs_begin_compressed_file(struct z_erofs_compress_ictx *ictx);
 int erofs_write_compressed_file(struct z_erofs_compress_ictx *ictx);
+int erofs_write_compressed_file_file_merge(struct z_erofs_compress_ictx *ictx,
+                                          struct erofs_vfile *dirty_vf,
+                                          struct erofs_inode *lower);
 
 int erofs_begin_compress_dir(struct erofs_importer *im,
                             struct erofs_inode *inode);
diff --git a/lib/liberofs_file_merge.h b/lib/liberofs_file_merge.h
new file mode 100644
index 0000000..6652d4e
--- /dev/null
+++ b/lib/liberofs_file_merge.h
@@ -0,0 +1,22 @@
+/* SPDX-License-Identifier: GPL-2.0+ OR MIT */
+#ifndef __EROFS_LIB_LIBEROFS_FILE_MERGE_H
+#define __EROFS_LIB_LIBEROFS_FILE_MERGE_H
+
+#include "erofs/internal.h"
+#include "erofs/io.h"
+
+struct erofs_inode_datasource_vfile {
+       struct erofs_vfile vf;
+       struct erofs_inode *inode;
+       u64 base;
+       u64 pos;
+};
+
+bool erofs_inode_dvf_is_file_merge(struct erofs_inode_datasource_vfile *dvf);
+bool erofs_inode_vfile_is_file_merge(struct erofs_vfile *vf,
+                                    const struct erofs_vfops *inode_vfops);
+ssize_t erofs_inode_dvf_pread_file_merge(
+               struct erofs_inode_datasource_vfile *dvf, void *buf,
+               size_t len, u64 offset);
+
+#endif
diff --git a/lib/liberofs_rebuild.h b/lib/liberofs_rebuild.h
index 6459dbd..c1c5916 100644
--- a/lib/liberofs_rebuild.h
+++ b/lib/liberofs_rebuild.h
@@ -17,5 +17,6 @@ int erofs_rebuild_load_tree(struct erofs_inode *root, struct 
erofs_sb_info *sbi,
                            enum erofs_rebuild_datamode mode);
 
 int erofs_rebuild_load_basedir(struct erofs_inode *dir, u64 *nr_subdirs,
-                              unsigned int *i_nlink);
+                              unsigned int *i_nlink, bool file_merge,
+                              bool hard_dereference);
 #endif
diff --git a/lib/rebuild.c b/lib/rebuild.c
index abe3b74..a4ea2ec 100644
--- a/lib/rebuild.c
+++ b/lib/rebuild.c
@@ -26,6 +26,9 @@
 #define AUFS_WH_DIROPQ         AUFS_WH_PFX AUFS_DIROPQ_NAME
 #endif
 
+static struct erofs_inode *erofs_rebuild_read_inode(struct erofs_sb_info *sbi,
+                                                   erofs_nid_t nid);
+
 /*
  * These non-existent parent directories are created with the same permissions
  * as their parent directories.  It is expected that a call to create these
@@ -356,8 +359,45 @@ struct erofs_rebuild_dir_context {
                        unsigned int *i_nlink;
                };
        };
+       bool file_merge;
+       bool hard_dereference;
 };
 
+static struct erofs_inode *erofs_rebuild_read_inode(struct erofs_sb_info *sbi,
+                                                   erofs_nid_t nid)
+{
+       struct erofs_inode *inode;
+       int ret;
+
+       inode = erofs_new_inode(sbi);
+       if (IS_ERR(inode))
+               return inode;
+       inode->nid = nid;
+       ret = erofs_read_inode_from_disk(inode);
+       if (ret) {
+               erofs_iput(inode);
+               return ERR_PTR(ret);
+       }
+       return inode;
+}
+
+static int erofs_rebuild_source_is_hardlink(struct erofs_inode *inode,
+                                           bool hard_dereference,
+                                           bool *is_hardlink)
+{
+       struct stat st;
+
+       *is_hardlink = false;
+       if (hard_dereference ||
+           inode->datasource.type != EROFS_INODE_DATA_SOURCE_LOCALPATH)
+               return 0;
+
+       if (lstat(inode->i_srcpath, &st))
+               return -errno;
+       *is_hardlink = S_ISREG(st.st_mode) && st.st_nlink > 1;
+       return 0;
+}
+
 static int erofs_rebuild_dirent_iter(struct erofs_dir_context *ctx)
 {
        struct erofs_rebuild_dir_context *rctx = (void *)ctx;
@@ -571,6 +611,38 @@ static int erofs_rebuild_basedir_dirent_iter(struct 
erofs_dir_context *ctx)
        } else {
                struct erofs_inode *inode = d->inode;
 
+               if (rctx->file_merge && S_ISREG(inode->i_mode) &&
+                   ctx->de_ftype == EROFS_FT_REG_FILE) {
+                       struct erofs_inode *lower;
+                       bool source_is_hardlink;
+
+                       ret = erofs_rebuild_source_is_hardlink(inode,
+                                       rctx->hard_dereference,
+                                       &source_is_hardlink);
+                       if (ret)
+                               goto out;
+                       if (!source_is_hardlink) {
+                               lower = erofs_rebuild_read_inode(dir->sbi,
+                                                                ctx->de_nid);
+                               if (IS_ERR(lower)) {
+                                       ret = PTR_ERR(lower);
+                                       goto out;
+                               }
+                               if (!S_ISREG(lower->i_mode)) {
+                                       erofs_iput(lower);
+                                       ret = -EOPNOTSUPP;
+                                       goto out;
+                               }
+                               if (lower->i_size == inode->i_size) {
+                                       if (inode->datasource.lower_inode)
+                                               
erofs_iput(inode->datasource.lower_inode);
+                                       inode->datasource.lower_inode = lower;
+                               } else {
+                                       erofs_iput(lower);
+                               }
+                       }
+               }
+
                /* update sub-directories only for recursively loading */
                if (S_ISDIR(inode->i_mode) &&
                    (ctx->de_ftype == EROFS_FT_DIR ||
@@ -588,7 +660,8 @@ out:
 }
 
 int erofs_rebuild_load_basedir(struct erofs_inode *dir, u64 *nr_subdirs,
-                              unsigned int *i_nlink)
+                              unsigned int *i_nlink, bool file_merge,
+                              bool hard_dereference)
 {
        struct erofs_inode fakeinode = {
                .sbi = dir->sbi,
@@ -622,6 +695,8 @@ int erofs_rebuild_load_basedir(struct erofs_inode *dir, u64 
*nr_subdirs,
                .mergedir = dir,
                .nr_subdirs = nr_subdirs,
                .i_nlink = i_nlink,
+               .file_merge = file_merge,
+               .hard_dereference = hard_dereference,
        };
        return erofs_iterate_dir(&ctx.ctx, false);
 }
diff --git a/man/mkfs.erofs.1 b/man/mkfs.erofs.1
index 65ec807..f7adec6 100644
--- a/man/mkfs.erofs.1
+++ b/man/mkfs.erofs.1
@@ -319,7 +319,8 @@ can reduce total metadata size. Implied by
 .BI "\-\-incremental=" MODE
 Run an incremental build where DESTINATION is an existing EROFS image,
 and the data specified by SOURCE will be incrementally appended to the image.
-\fIMODE\fR has the same meaning as in \fB\-\-clean\fR above.
+\fIMODE\fR generally has the same meaning as in \fB\-\-clean\fR above.
+The special \fBfile-merge\fR mode is only valid for \fB\-\-incremental\fR.
 Incremental build is unsupported for \fB\-\-s3\fR and \fB\-\-oci\fR sources.
 
 If \fB\-\-incremental\fR is specified without an explicit value, it is treated
@@ -329,7 +330,13 @@ The current source-specific support for \fIMODE\fR:
 .RS 1.2i
 .TP
 .I Local directory source
-Only \fBdata\fR is supported. \fBrvsp\fR and \fB0\fR will be ignored.
+\fBdata\fR and \fBfile-merge\fR are supported. \fBrvsp\fR and \fB0\fR will be 
ignored.
+\fBfile-merge\fR keeps the normal incremental append behavior, but when a
+regular file from SOURCE has the same path and size as an existing lower
+regular file, sparse holes in the SOURCE file are inherited from the lower
+file while allocated data ranges replace the lower content.  An all-zero
+overwrite must therefore be represented as allocated zero data rather than a
+sparse hole.
 .TP
 .I Tar source (\fB\-\-tar\fR)
 \fBdata\fR and \fBrvsp\fR are supported. \fB0\fR will be ignored.
diff --git a/mkfs/main.c b/mkfs/main.c
index 4fadb56..9054ca3 100644
--- a/mkfs/main.c
+++ b/mkfs/main.c
@@ -193,7 +193,8 @@ static void usage(int argc, char **argv)
                " --chunksize=#          generate chunk-based files with #-byte 
chunks\n"
                " --clean=X              run full clean build (default) or:\n"
                " --incremental=X        run incremental build\n"
-               "                        X = data|rvsp|0 (data: full data, 
rvsp: space fallocated\n"
+               "                        X = data|rvsp|0|file-merge (data: full 
data,\n"
+               "                                         rvsp: space 
fallocated,\n"
                "                                         0: inodes zeroed)\n"
                " --compress-hints=X     specify a file to configure per-file 
compression strategy\n"
                " --dsunit=#             align all data block addresses to 
multiples of #\n"
@@ -1352,12 +1353,20 @@ static int mkfs_parse_options_cfg(struct 
erofs_importer_params *params,
                        break;
                case 522:
                case 523:
+                       params->incremental_file_merge = false;
                        if (!optarg || !strcmp(optarg, "data")) {
                                dataimport_mode = 
EROFS_MKFS_DATA_IMPORT_FULLDATA;
                        } else if (!strcmp(optarg, "rvsp")) {
                                dataimport_mode = EROFS_MKFS_DATA_IMPORT_RVSP;
                        } else if (!strcmp(optarg, "0")) {
                                dataimport_mode = 
EROFS_MKFS_DATA_IMPORT_ZEROFILL;
+                       } else if (!strcmp(optarg, "file-merge")) {
+                               if (opt != 523) {
+                                       erofs_err("--clean=file-merge is 
invalid");
+                                       return -EINVAL;
+                               }
+                               dataimport_mode = 
EROFS_MKFS_DATA_IMPORT_FULLDATA;
+                               params->incremental_file_merge = true;
                        } else {
                                errno = 0;
                                dataimport_mode = strtol(optarg, &endptr, 0);
@@ -1550,6 +1559,12 @@ static int mkfs_parse_options_cfg(struct 
erofs_importer_params *params,
                cfg.c_showprogress = false;
        }
 
+       if (params->incremental_file_merge &&
+           source_mode != EROFS_MKFS_SOURCE_LOCALDIR) {
+               erofs_err("--incremental=file-merge requires a local directory 
source");
+               return -EINVAL;
+       }
+
        if (pclustersize_max) {
                if (pclustersize_max < (1U << mkfs_blkszbits) ||
                    pclustersize_max % (1U << mkfs_blkszbits)) {
-- 
2.47.3


Reply via email to