[RFC PATCH 3/3] erofs-utils: support incremental file merge

Yifan Zhao <[email protected]>
Newsgroups org.ozlabs.lists.linux-erofs
Message-ID <[email protected]>
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
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.