[PATCH] erofs-utils: mkfs: add hot file list support

Mengdie Yan <[email protected]>
Newsgroups org.ozlabs.lists.linux-erofs
Message-ID <[email protected]>
Add --hot-file-list to let mkfs prioritize image-relative hot paths
for local directory builds. The option loads newline-separated hot paths,
resolves symlink aliases in local rootfs builds, prioritizes ancestor
directory metadata, and keeps hot regular files out of fragment packing
and cross-file dedupe so their physical layout remains stable.

Add layout coverage for hot files, hot directories, symlink aliases,
hardlinks, and large root-directory data.

Signed-off-by: Mengdie Yan <[email protected]>
---
 include/erofs/hotfile.h  |  25 ++
 include/erofs/internal.h |   9 +
 lib/Makefile.am          |   2 +
 lib/compress.c           |  20 +-
 lib/hotfile.c            | 641 +++++++++++++++++++++++++++++++++++++++
 lib/inode.c              | 313 ++++++++++++++++++-
 man/mkfs.erofs.1         |  13 +
 mkfs/main.c              |  32 ++
 tests/hotfile-layout.sh  | 321 ++++++++++++++++++++
 9 files changed, 1359 insertions(+), 17 deletions(-)
 create mode 100644 include/erofs/hotfile.h
 create mode 100644 lib/hotfile.c
 create mode 100755 tests/hotfile-layout.sh

diff --git a/include/erofs/hotfile.h b/include/erofs/hotfile.h
new file mode 100644
index 0000000..5f43e41
--- /dev/null
+++ b/include/erofs/hotfile.h
@@ -0,0 +1,25 @@
+/* SPDX-License-Identifier: GPL-2.0+ OR Apache-2.0 */
+#ifndef __EROFS_HOTFILE_H
+#define __EROFS_HOTFILE_H
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+#include <limits.h>
+#include "defs.h"
+
+#define EROFS_HOT_RANK_NONE	UINT_MAX
+
+int erofs_hotfile_load(const char *path);
+bool erofs_hotfile_enabled(void);
+unsigned int erofs_get_hot_file_rank(const char *path);
+unsigned int erofs_get_hot_dir_rank(const char *path);
+void erofs_hotfile_exit(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/include/erofs/internal.h b/include/erofs/internal.h
index 450e264..d658c36 100644
--- a/include/erofs/internal.h
+++ b/include/erofs/internal.h
@@ -265,6 +265,10 @@ struct erofs_inode {
 	bool compressed_idata;
 	bool lazy_tailblock;
 	bool opaque;
+	bool hotfile;
+	bool hotdir;
+	bool hotdir_deferred;
+	u32 hot_rank;
 	/* OVL: non-merge dir that may contain whiteout entries */
 	bool whiteouts;
 	bool dot_omitted;
@@ -574,6 +578,11 @@ static inline bool erofs_is_packed_inode(struct erofs_inode *inode)
 	return inode->i_srcpath == EROFS_PACKED_INODE;
 }
 
+static inline bool erofs_inode_is_hotfile(struct erofs_inode *inode)
+{
+	return inode->hotfile;
+}
+
 int erofs_packedfile_init(struct erofs_sb_info *sbi, bool fragments_mkfs);
 void erofs_packedfile_exit(struct erofs_sb_info *sbi);
 
diff --git a/lib/Makefile.am b/lib/Makefile.am
index 27bf710..07282e2 100644
--- a/lib/Makefile.am
+++ b/lib/Makefile.am
@@ -12,6 +12,7 @@ noinst_HEADERS = $(top_srcdir)/include/erofs_fs.h \
       $(top_srcdir)/include/erofs/exclude.h \
       $(top_srcdir)/include/erofs/flex-array.h \
       $(top_srcdir)/include/erofs/hashmap.h \
+      $(top_srcdir)/include/erofs/hotfile.h \
       $(top_srcdir)/include/erofs/inode.h \
       $(top_srcdir)/include/erofs/internal.h \
       $(top_srcdir)/include/erofs/io.h \
@@ -45,6 +46,7 @@ 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 \
+		      hotfile.c \
 		      vmdk.c metabox.c global.c importer.c base64.c
 
 liberofs_la_CFLAGS = -Wall ${libuuid_CFLAGS} -I$(top_srcdir)/include
diff --git a/lib/compress.c b/lib/compress.c
index 62d2672..9d55cbc 100644
--- a/lib/compress.c
+++ b/lib/compress.c
@@ -301,7 +301,7 @@ static int z_erofs_compress_dedupe(struct z_erofs_compress_sctx *ctx)
 	 * No need dedupe for packed inode since it is composed of
 	 * fragments which have already been deduplicated.
 	 */
-	if (erofs_is_packed_inode(inode))
+	if (erofs_is_packed_inode(inode) || erofs_inode_is_hotfile(inode))
 		goto out;
 
 	do {
@@ -573,7 +573,8 @@ static int __z_erofs_compress_one(struct z_erofs_compress_sctx *ctx,
 	bool is_packed_inode = erofs_is_packed_inode(inode);
 	bool tsg = (ctx->seg_idx + 1 >= ictx->seg_num), final = !ctx->remaining;
 	bool may_packing = (params->fragments && tsg && final && !is_packed_inode &&
-			    !erofs_is_metabox_inode(inode));
+			    !erofs_is_metabox_inode(inode) &&
+			    !erofs_inode_is_hotfile(inode));
 	bool data_unaligned = ictx->data_unaligned;
 	bool may_inline = (params->ztailpacking && !data_unaligned && tsg &&
 			   final && !may_packing);
@@ -742,7 +743,8 @@ frag_packing:
 	e->pstart = ctx->pstart;
 	if (ctx->pstart != EROFS_NULL_ADDR)
 		ctx->pstart += e->plen;
-	if (!may_inline && !may_packing && !is_packed_inode)
+	if (!may_inline && !may_packing && !is_packed_inode &&
+	    !erofs_inode_is_hotfile(inode))
 		(void)z_erofs_dedupe_insert(e, ctx->queue + ctx->head);
 	ctx->head += e->length;
 	return 0;
@@ -1256,6 +1258,7 @@ int z_erofs_compress_segment(struct z_erofs_compress_sctx *ctx,
 	struct erofs_inode *inode = ictx->inode;
 	bool frag = params->fragments && !erofs_is_packed_inode(inode) &&
 		!erofs_is_metabox_inode(inode) &&
+		!erofs_inode_is_hotfile(inode) &&
 		ctx->seg_idx >= ictx->seg_num - 1;
 	struct erofs_vfile *vf = ictx->vf;
 	int ret;
@@ -1551,7 +1554,8 @@ int z_erofs_merge_segment(struct z_erofs_compress_ictx *ictx,
 	struct z_erofs_extent_item *ei, *n;
 	const struct erofs_importer_params *params = ictx->im->params;
 	struct erofs_sb_info *sbi = ictx->inode->sbi;
-	bool dedupe_ext = params->fragments;
+	bool dedupe_ext = params->fragments &&
+		!erofs_inode_is_hotfile(ictx->inode);
 	erofs_off_t off = 0;
 	int ret = 0, ret2;
 	erofs_off_t dpo;
@@ -1802,7 +1806,8 @@ void *erofs_prepare_compressed_file(struct erofs_importer *im,
 	struct erofs_sb_info *sbi = inode->sbi;
 	struct z_erofs_compress_ictx *ictx;
 	bool frag = params->fragments && !erofs_is_packed_inode(inode) &&
-		!erofs_is_metabox_inode(inode);
+		!erofs_is_metabox_inode(inode) &&
+		!erofs_inode_is_hotfile(inode);
 	bool all_fragments = params->all_fragments && frag;
 
 	/* initialize per-file compression setting */
@@ -1888,7 +1893,7 @@ void *erofs_prepare_compressed_file(struct erofs_importer *im,
 		ictx->data_unaligned = false;
 	}
 	if (params->fragments && params->dedupe != EROFS_DEDUPE_FORCE_ON &&
-	    !ictx->data_unaligned)
+	    !ictx->data_unaligned && !erofs_inode_is_hotfile(inode))
 		inode->z_advise |= Z_EROFS_ADVISE_INTERLACED_PCLUSTER;
 
 	init_list_head(&ictx->extents);
@@ -1911,7 +1916,8 @@ int erofs_begin_compressed_file(struct z_erofs_compress_ictx *ictx)
 	const struct erofs_importer_params *params = ictx->im->params;
 	struct erofs_inode *inode = ictx->inode;
 	bool frag = params->fragments && !erofs_is_packed_inode(inode) &&
-		!erofs_is_metabox_inode(inode);
+		!erofs_is_metabox_inode(inode) &&
+		!erofs_inode_is_hotfile(inode);
 	bool all_fragments = params->all_fragments && frag;
 	int ret;
 
diff --git a/lib/hotfile.c b/lib/hotfile.c
new file mode 100644
index 0000000..9e0878a
--- /dev/null
+++ b/lib/hotfile.c
@@ -0,0 +1,641 @@
+// SPDX-License-Identifier: GPL-2.0+ OR Apache-2.0
+#define _GNU_SOURCE
+
+#include <ctype.h>
+#include <errno.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include "erofs/config.h"
+#include "erofs/hotfile.h"
+#include "erofs/print.h"
+
+struct erofs_hotfile_manifest {
+	struct erofs_hotfile_entry {
+		char *path;
+		unsigned int rank;
+	} *file_paths;
+	unsigned int nr_files;
+	unsigned int cap_files;
+	struct erofs_hotfile_entry *dir_paths;
+	unsigned int nr_dirs;
+	unsigned int cap_dirs;
+};
+
+static struct erofs_hotfile_manifest hotfile_manifest;
+
+static bool erofs_hotfile_is_local_rootfs(char **root)
+{
+	struct stat st;
+
+	if (!cfg.c_src_path)
+		return false;
+	if (stat(cfg.c_src_path, &st))
+		return false;
+	if (!S_ISDIR(st.st_mode))
+		return false;
+	if (root)
+		*root = cfg.c_src_path;
+	return true;
+}
+
+static int erofs_hotfile_cmp(const void *a, const void *b)
+{
+	const struct erofs_hotfile_entry *lhs = a;
+	const struct erofs_hotfile_entry *rhs = b;
+
+	return strcmp(lhs->path, rhs->path);
+}
+
+static int erofs_hotfile_path_cmp(const void *a, const void *b)
+{
+	const char *lhs = a;
+	const struct erofs_hotfile_entry *rhs = b;
+
+	return strcmp(lhs, rhs->path);
+}
+
+static char *erofs_hotfile_normalize(const char *path)
+{
+	const unsigned char *src = (const unsigned char *)path;
+	const unsigned char *end;
+	char *out;
+	size_t len = 0;
+	bool slash = false;
+
+	while (*src && isspace(*src))
+		++src;
+	end = src + strlen((const char *)src);
+	while (end > src && isspace(end[-1]))
+		--end;
+
+	while (src < end && *src == '/')
+		++src;
+
+	out = malloc((end - src) + 1);
+	if (!out)
+		return NULL;
+
+	while (src < end) {
+		if (*src == '/') {
+			if (!slash)
+				out[len++] = '/';
+			slash = true;
+		} else {
+			out[len++] = *src;
+			slash = false;
+		}
+		++src;
+	}
+
+	while (len > 0 && out[len - 1] == '/')
+		--len;
+	out[len] = '\0';
+	return out;
+}
+
+static bool erofs_hotfile_line_is_dir(const char *path)
+{
+	const unsigned char *src = (const unsigned char *)path;
+	const unsigned char *end;
+
+	while (*src && isspace(*src))
+		++src;
+	if (*src == '#')
+		return false;
+
+	end = src + strlen((const char *)src);
+	while (end > src && isspace(end[-1]))
+		--end;
+
+	return end > src && end[-1] == '/';
+}
+
+static int erofs_hotfile_add_file(char *path, unsigned int rank);
+static int erofs_hotfile_add_dir(char *path, unsigned int rank);
+static int erofs_hotfile_collect_parent_dirs(const char *path,
+					     unsigned int rank);
+
+static char *erofs_hotfile_clean_path(const char *path)
+{
+	char **stack;
+	char *normalized, *token, *saveptr;
+	char *out;
+	size_t len, depth = 0, i, pos = 0;
+
+	normalized = erofs_hotfile_normalize(path);
+	if (!normalized)
+		return NULL;
+
+	len = strlen(normalized);
+	stack = calloc(len + 1, sizeof(*stack));
+	if (!stack) {
+		free(normalized);
+		return NULL;
+	}
+
+	for (token = strtok_r(normalized, "/", &saveptr); token;
+	     token = strtok_r(NULL, "/", &saveptr)) {
+		if (!strcmp(token, "."))
+			continue;
+		if (!strcmp(token, "..")) {
+			if (!depth) {
+				free(stack);
+				free(normalized);
+				errno = EXDEV;
+				return NULL;
+			}
+			--depth;
+			continue;
+		}
+		stack[depth++] = token;
+	}
+
+	out = malloc(len + 1);
+	if (!out) {
+		free(stack);
+		free(normalized);
+		return NULL;
+	}
+
+	for (i = 0; i < depth; ++i) {
+		size_t n = strlen(stack[i]);
+
+		if (i)
+			out[pos++] = '/';
+		memcpy(out + pos, stack[i], n);
+		pos += n;
+	}
+	out[pos] = '\0';
+	free(stack);
+	free(normalized);
+	return out;
+}
+
+static char *erofs_hotfile_join_symlink_target(const char *linkpath,
+					       const char *target,
+					       const char *suffix)
+{
+	char *joined, *parent = NULL, *slash;
+	int ret;
+
+	if (target[0] == '/') {
+		char *cleaned;
+
+		ret = asprintf(&joined, "%s%s%s", target,
+			       suffix && suffix[0] ? "/" : "",
+			       suffix && suffix[0] ? suffix : "");
+		if (ret < 0)
+			return NULL;
+		cleaned = erofs_hotfile_clean_path(joined);
+		free(joined);
+		return cleaned;
+	}
+
+	parent = strdup(linkpath);
+	if (!parent)
+		return NULL;
+	slash = strrchr(parent, '/');
+	if (slash)
+		*slash = '\0';
+	else
+		parent[0] = '\0';
+
+	ret = asprintf(&joined, "%s%s%s%s%s",
+		       parent,
+		       parent[0] ? "/" : "",
+		       target,
+		       suffix && suffix[0] ? "/" : "",
+		       suffix && suffix[0] ? suffix : "");
+	free(parent);
+	if (ret < 0)
+		return NULL;
+	parent = erofs_hotfile_clean_path(joined);
+	free(joined);
+	return parent;
+}
+
+static int erofs_hotfile_add_file_path(const char *path, unsigned int rank)
+{
+	char *dup;
+	int err;
+
+	dup = strdup(path);
+	if (!dup)
+		return -ENOMEM;
+	err = erofs_hotfile_add_file(dup, rank);
+	if (err) {
+		free(dup);
+		return err;
+	}
+	return erofs_hotfile_collect_parent_dirs(path, rank);
+}
+
+static int erofs_hotfile_add_dir_path(const char *path, unsigned int rank)
+{
+	char *dup;
+	int err;
+
+	dup = strdup(path);
+	if (!dup)
+		return -ENOMEM;
+	err = erofs_hotfile_add_dir(dup, rank);
+	if (err) {
+		free(dup);
+		return err;
+	}
+	return erofs_hotfile_collect_parent_dirs(path, rank);
+}
+
+static int erofs_hotfile_add_plain_path(const char *path, bool dir_entry,
+					unsigned int rank)
+{
+	if (!path[0])
+		return 0;
+	return dir_entry ? erofs_hotfile_add_dir_path(path, rank) :
+		erofs_hotfile_add_file_path(path, rank);
+}
+
+static int erofs_hotfile_add_resolved_path(const char *path, bool dir_entry,
+					   unsigned int rank)
+{
+	char *root, *current;
+	int depth, err;
+
+	if (!path[0])
+		return 0;
+
+	if (!erofs_hotfile_is_local_rootfs(&root))
+		return erofs_hotfile_add_plain_path(path, dir_entry, rank);
+
+	current = erofs_hotfile_clean_path(path);
+	if (!current)
+		return erofs_hotfile_add_plain_path(path, dir_entry, rank);
+
+	for (depth = 0; depth < 32; ++depth) {
+		char *walk, *saveptr, *part, *prefix = NULL;
+		bool redirected = false;
+		size_t prefix_len = 0;
+
+		walk = strdup(current);
+		if (!walk) {
+			free(current);
+			return -ENOMEM;
+		}
+
+		for (part = strtok_r(walk, "/", &saveptr); part;
+		     part = strtok_r(NULL, "/", &saveptr)) {
+			char *fullpath, *next, *target;
+			const char *suffix = saveptr;
+			struct stat st;
+
+			if (prefix_len) {
+				if (asprintf(&next, "%s/%s", prefix, part) < 0) {
+					free(prefix);
+					free(walk);
+					free(current);
+					return -ENOMEM;
+				}
+				free(prefix);
+				prefix = next;
+			} else {
+				prefix = strdup(part);
+				if (!prefix) {
+					free(walk);
+					free(current);
+					return -ENOMEM;
+				}
+			}
+			prefix_len = strlen(prefix);
+
+			if (asprintf(&fullpath, "%s/%s", root, prefix) < 0) {
+				free(prefix);
+				free(walk);
+				free(current);
+				return -ENOMEM;
+			}
+			if (lstat(fullpath, &st)) {
+				free(fullpath);
+				free(prefix);
+				free(walk);
+				err = erofs_hotfile_add_plain_path(current,
+								   dir_entry,
+								   rank);
+				free(current);
+				return err;
+			}
+			if (!S_ISLNK(st.st_mode)) {
+				free(fullpath);
+				continue;
+			}
+
+			err = erofs_hotfile_add_file_path(prefix, rank);
+			if (err) {
+				free(fullpath);
+				free(prefix);
+				free(walk);
+				free(current);
+				return err;
+			}
+
+			target = malloc(st.st_size + 1);
+			if (!target) {
+				free(fullpath);
+				free(prefix);
+				free(walk);
+				free(current);
+				return -ENOMEM;
+			}
+			err = readlink(fullpath, target, st.st_size);
+			free(fullpath);
+			if (err < 0) {
+				err = -errno;
+				free(target);
+				free(prefix);
+				free(walk);
+				free(current);
+				return err;
+			}
+			target[err] = '\0';
+
+			next = erofs_hotfile_join_symlink_target(prefix, target,
+								 suffix);
+			free(target);
+			free(prefix);
+			free(walk);
+			free(current);
+			if (!next)
+				return -ENOMEM;
+			current = next;
+			redirected = true;
+			break;
+		}
+
+		if (!redirected) {
+			free(prefix);
+			free(walk);
+			err = erofs_hotfile_add_plain_path(current, dir_entry,
+							   rank);
+			free(current);
+			return err;
+		}
+	}
+	free(current);
+	return -ELOOP;
+}
+
+static int erofs_hotfile_add(struct erofs_hotfile_entry **paths,
+			     unsigned int *nr, unsigned int *cap,
+			     char *path, unsigned int rank)
+{
+	struct erofs_hotfile_entry *npaths;
+
+	if (*nr >= *cap) {
+		unsigned int ncap = *cap ? *cap * 2 : 64;
+
+		npaths = realloc(*paths, ncap * sizeof(*npaths));
+		if (!npaths)
+			return -ENOMEM;
+		*paths = npaths;
+		*cap = ncap;
+	}
+	(*paths)[*nr].path = path;
+	(*paths)[*nr].rank = rank;
+	++(*nr);
+	return 0;
+}
+
+static int erofs_hotfile_add_file(char *path, unsigned int rank)
+{
+	return erofs_hotfile_add(&hotfile_manifest.file_paths,
+				 &hotfile_manifest.nr_files,
+				 &hotfile_manifest.cap_files,
+				 path, rank);
+}
+
+static int erofs_hotfile_add_dir(char *path, unsigned int rank)
+{
+	return erofs_hotfile_add(&hotfile_manifest.dir_paths,
+				 &hotfile_manifest.nr_dirs,
+				 &hotfile_manifest.cap_dirs,
+				 path, rank);
+}
+
+static int erofs_hotfile_collect_parent_dirs(const char *path,
+					     unsigned int rank)
+{
+	char *dir = strdup(path);
+	int err = 0;
+
+	if (!dir)
+		return -ENOMEM;
+
+	while (1) {
+		char *slash = strrchr(dir, '/');
+
+		if (!slash)
+			break;
+		*slash = '\0';
+		if (!dir[0])
+			break;
+
+		slash = strdup(dir);
+		if (!slash) {
+			err = -ENOMEM;
+			break;
+		}
+		err = erofs_hotfile_add_dir(slash, rank);
+		if (err)
+			break;
+	}
+	free(dir);
+	return err;
+}
+
+static unsigned int erofs_hotfile_sort_dedupe(struct erofs_hotfile_entry *paths,
+					      unsigned int nr)
+{
+	unsigned int i, out = 0;
+
+	if (!nr)
+		return 0;
+
+	qsort(paths, nr, sizeof(paths[0]), erofs_hotfile_cmp);
+	for (i = 0; i < nr; ++i) {
+		if (out && !strcmp(paths[out - 1].path, paths[i].path)) {
+			if (paths[i].rank < paths[out - 1].rank) {
+				free(paths[out - 1].path);
+				paths[out - 1] = paths[i];
+			} else {
+				free(paths[i].path);
+			}
+			continue;
+		}
+		paths[out++] = paths[i];
+	}
+	return out;
+}
+
+int erofs_hotfile_load(const char *path)
+{
+	FILE *fp;
+	char *line = NULL;
+	size_t linesz = 0;
+	ssize_t nread;
+	unsigned int rank = 0;
+	int err = 0;
+
+	fp = fopen(path, "r");
+	if (!fp)
+		return -errno;
+
+	while ((nread = getline(&line, &linesz, fp)) >= 0) {
+		char *normalized;
+		bool dir_entry;
+
+		if (!nread)
+			continue;
+		dir_entry = erofs_hotfile_line_is_dir(line);
+		normalized = erofs_hotfile_normalize(line);
+		if (!normalized) {
+			err = -ENOMEM;
+			break;
+		}
+		if ((!normalized[0] && !dir_entry) || normalized[0] == '#') {
+			free(normalized);
+			continue;
+		}
+		err = erofs_hotfile_add_resolved_path(normalized, dir_entry,
+						      rank);
+		free(normalized);
+		if (err)
+			break;
+		++rank;
+	}
+	free(line);
+	fclose(fp);
+	if (err)
+		goto err_out;
+
+	if (!hotfile_manifest.nr_files && !hotfile_manifest.nr_dirs)
+		return 0;
+
+	hotfile_manifest.nr_files = erofs_hotfile_sort_dedupe(
+		hotfile_manifest.file_paths, hotfile_manifest.nr_files);
+	hotfile_manifest.nr_dirs = erofs_hotfile_sort_dedupe(
+		hotfile_manifest.dir_paths, hotfile_manifest.nr_dirs);
+	erofs_info("loaded %u hot files and %u hot ancestor dirs from %s",
+		   hotfile_manifest.nr_files, hotfile_manifest.nr_dirs, path);
+	return 0;
+
+err_out:
+	erofs_hotfile_exit();
+	return err;
+}
+
+bool erofs_hotfile_enabled(void)
+{
+	return hotfile_manifest.nr_files || hotfile_manifest.nr_dirs;
+}
+
+unsigned int erofs_get_hot_file_rank(const char *path)
+{
+	char *normalized;
+	struct erofs_hotfile_entry *found;
+	const char *fspath;
+	unsigned int rank = EROFS_HOT_RANK_NONE;
+
+	if (!erofs_hotfile_enabled())
+		return EROFS_HOT_RANK_NONE;
+
+	fspath = erofs_fspath(path);
+	normalized = erofs_hotfile_normalize(fspath);
+	if (!normalized)
+		return EROFS_HOT_RANK_NONE;
+
+	found = bsearch(normalized, hotfile_manifest.file_paths,
+			hotfile_manifest.nr_files,
+			sizeof(hotfile_manifest.file_paths[0]),
+			erofs_hotfile_path_cmp);
+	if (found)
+		rank = found->rank;
+
+	if (rank != 0 && hotfile_manifest.nr_files) {
+		char *root = NULL;
+		char *fullpath = NULL;
+		struct stat st;
+
+		if (erofs_hotfile_is_local_rootfs(&root) &&
+		    asprintf(&fullpath, "%s/%s", root, normalized) >= 0 &&
+		    lstat(fullpath, &st) == 0 && S_ISLNK(st.st_mode)) {
+			char *resolved = realpath(fullpath, NULL);
+
+			if (resolved && !strncmp(resolved, root, strlen(root)) &&
+			    resolved[strlen(root)] == '/') {
+				char *resolved_norm =
+					erofs_hotfile_normalize(resolved + strlen(root));
+
+				if (resolved_norm) {
+					struct erofs_hotfile_entry *resolved_found;
+
+					resolved_found = bsearch(resolved_norm,
+						hotfile_manifest.file_paths,
+						hotfile_manifest.nr_files,
+						sizeof(hotfile_manifest.file_paths[0]),
+						erofs_hotfile_path_cmp);
+					if (resolved_found && resolved_found->rank < rank)
+						rank = resolved_found->rank;
+					free(resolved_norm);
+				}
+			}
+			free(resolved);
+		}
+		free(fullpath);
+	}
+	free(normalized);
+	return rank;
+}
+
+unsigned int erofs_get_hot_dir_rank(const char *path)
+{
+	char *normalized;
+	struct erofs_hotfile_entry *found;
+	const char *fspath;
+
+	if (!erofs_hotfile_enabled())
+		return EROFS_HOT_RANK_NONE;
+
+	fspath = erofs_fspath(path);
+	normalized = erofs_hotfile_normalize(fspath);
+	if (!normalized)
+		return EROFS_HOT_RANK_NONE;
+
+	found = bsearch(normalized, hotfile_manifest.dir_paths,
+			hotfile_manifest.nr_dirs,
+			sizeof(hotfile_manifest.dir_paths[0]),
+			erofs_hotfile_path_cmp);
+	free(normalized);
+	return found ? found->rank : EROFS_HOT_RANK_NONE;
+}
+
+void erofs_hotfile_exit(void)
+{
+	unsigned int i;
+
+	for (i = 0; i < hotfile_manifest.nr_files; ++i)
+		free(hotfile_manifest.file_paths[i].path);
+	for (i = 0; i < hotfile_manifest.nr_dirs; ++i)
+		free(hotfile_manifest.dir_paths[i].path);
+	free(hotfile_manifest.file_paths);
+	free(hotfile_manifest.dir_paths);
+	hotfile_manifest.file_paths = NULL;
+	hotfile_manifest.nr_files = 0;
+	hotfile_manifest.cap_files = 0;
+	hotfile_manifest.dir_paths = NULL;
+	hotfile_manifest.nr_dirs = 0;
+	hotfile_manifest.cap_dirs = 0;
+}
diff --git a/lib/inode.c b/lib/inode.c
index 95fd93b..cabe085 100644
--- a/lib/inode.c
+++ b/lib/inode.c
@@ -24,6 +24,7 @@
 #include "erofs/block_list.h"
 #include "erofs/compress_hints.h"
 #include "erofs/blobchunk.h"
+#include "erofs/hotfile.h"
 #include "erofs/importer.h"
 #include "liberofs_cache.h"
 #include "liberofs_compress.h"
@@ -661,10 +662,20 @@ static int erofs_write_unencoded_data(struct erofs_inode *inode,
 	inode->idata_size = inode->i_size % erofs_blksiz(sbi);
 	remaining = inode->i_size - inode->idata_size;
 
-	ret = erofs_allocate_inode_bh_data(inode, remaining >> sbi->blkszbits,
-					   in_metazone);
-	if (ret)
-		return ret;
+	/*
+	 * Hot directories may have their main data block pre-allocated
+	 * during hot-queue processing so that its physical offset is
+	 * placed inside the hot region. Reuse it rather than re-balloc,
+	 * which would leak the pre-allocated buffer and defeat the
+	 * layout intent.
+	 */
+	if (!inode->bh_data) {
+		ret = erofs_allocate_inode_bh_data(inode,
+						   remaining >> sbi->blkszbits,
+						   in_metazone);
+		if (ret)
+			return ret;
+	}
 
 	bh = inode->bh_data;
 	if (bh) {
@@ -1008,7 +1019,8 @@ static int erofs_prepare_inode_buffer(struct erofs_importer *im,
 		goto noinline;
 
 	if (!is_inode_layout_compression(inode)) {
-		if (params->no_datainline && S_ISREG(inode->i_mode)) {
+		if (S_ISREG(inode->i_mode) &&
+		    (params->no_datainline || erofs_inode_is_hotfile(inode))) {
 			inode->datalayout = EROFS_INODE_FLAT_PLAIN;
 			goto noinline;
 		}
@@ -1351,6 +1363,15 @@ static int erofs_fill_inode(struct erofs_importer *im, struct erofs_inode *inode
 		if (!inode->i_srcpath)
 			return -ENOMEM;
 	}
+	inode->hot_rank = EROFS_HOT_RANK_NONE;
+	if (!erofs_is_special_identifier(path)) {
+		inode->hot_rank = erofs_get_hot_file_rank(path);
+		inode->hotfile = inode->hot_rank != EROFS_HOT_RANK_NONE;
+		if (!inode->hotfile && S_ISDIR(st->st_mode)) {
+			inode->hot_rank = erofs_get_hot_dir_rank(path);
+			inode->hotdir = inode->hot_rank != EROFS_HOT_RANK_NONE;
+		}
+	}
 
 	if (erofs_should_use_inode_extended(im, inode, path)) {
 		if (params->force_inodeversion == EROFS_FORCE_INODE_COMPACT) {
@@ -1410,11 +1431,28 @@ static struct erofs_inode *erofs_iget_from_local(struct erofs_importer *im,
 	 * lookup in hash table first, if it already exists we have a
 	 * hard-link, just return it. Also don't lookup for directories
 	 * since hard-link directory isn't allowed.
+	 *
+	 * When a hard-linked inode has already been created via another
+	 * name, only that first name's hotfile rank was recorded. If the
+	 * current name is listed in the hotlist with a lower rank (= hotter
+	 * priority), adopt it so the shared inode gets the hottest rank
+	 * across all of its hard-links. Without this, an inode shared by
+	 * cold + hot paths (e.g. /usr/share/zoneinfo/Asia/Chongqing shared
+	 * with .../Shanghai, only Shanghai is hot) ends up with no hot rank
+	 * and gets placed in the cold region, blowing up the hot-zone end
+	 * to near the full image size.
 	 */
 	if (!S_ISDIR(st.st_mode) && !params->hard_dereference) {
 		inode = erofs_iget(st.st_dev, st.st_ino);
-		if (inode)
+		if (inode) {
+			u32 rank = erofs_get_hot_file_rank(path);
+
+			if (rank != EROFS_HOT_RANK_NONE && rank < inode->hot_rank) {
+				inode->hot_rank = rank;
+				inode->hotfile = true;
+			}
 			return inode;
+		}
 	}
 
 	/* cannot find in the inode cache */
@@ -1612,6 +1650,42 @@ static int erofs_mkfs_create_directory(const struct erofs_mkfs_btctx *ctx,
 	if (ret)
 		return ret;
 	inode->bh->op = &erofs_skip_write_bhops;
+
+	/*
+	 * For hot directories, also pre-allocate the main dentry data
+	 * block right now (while we are still in the hot-queue stage),
+	 * so that its physical offset falls inside the hot region.
+	 * Otherwise the main dentry block would be ballocated lazily by
+	 * erofs_write_unencoded_data() during the later cold-dir dump
+	 * pass, landing well past the hot prefix and breaking path
+	 * lookups such as /usr/bin/sh when the image is head-truncated
+	 * to the hot prefix.
+	 *
+	 * A directory is treated as hot here when it has any hot rank,
+	 * either because it was listed as a hot dir (trailing slash) or
+	 * because a caller accidentally listed it as a hot file (no
+	 * trailing slash). In both cases the user signalled that the
+	 * lookup path passes through this directory, so its dentry
+	 * block must be reachable from the hot prefix.
+	 *
+	 * Only uncompressed FLAT_{INLINE,PLAIN} dirs have a separate
+	 * main data extent; compressed dirs carry all bytes via the
+	 * compression path and do not go through erofs_balloc(DIRA).
+	 */
+	if ((inode->hotdir || inode->hotfile) &&
+	    inode->hot_rank != EROFS_HOT_RANK_NONE &&
+	    (inode->datalayout == EROFS_INODE_FLAT_INLINE ||
+	     inode->datalayout == EROFS_INODE_FLAT_PLAIN)) {
+		u64 remaining = inode->i_size - inode->idata_size;
+
+		if (remaining) {
+			ret = erofs_allocate_inode_bh_data(inode,
+					remaining >> inode->sbi->blkszbits,
+					ctx->im->params->dirdata_in_metazone);
+			if (ret)
+				return ret;
+		}
+	}
 	return 0;
 }
 
@@ -2129,18 +2203,176 @@ static void erofs_mark_parent_inode(struct erofs_inode *inode,
 	inode->i_parent = (void *)((unsigned long)dir | 1);
 }
 
+struct erofs_mkfs_hot_item {
+	struct erofs_mkfs_hot_item *next;
+	struct erofs_inode *parent;
+	struct erofs_inode *inode;
+	unsigned int priority;
+	unsigned int rank;
+	unsigned int order;
+};
+
+static unsigned int erofs_mkfs_inode_rank(struct erofs_inode *inode)
+{
+	/*
+	 * A directory that the user listed without a trailing slash
+	 * ends up with hotfile=true/hotdir=false but still carries a
+	 * valid hot_rank. Treat it as a hot directory here so its
+	 * metadata (including the pre-allocated dentry data block)
+	 * participates in the hot-queue layout. Otherwise the dentry
+	 * block is emitted during the later cold-dir pass and falls
+	 * outside the hot prefix.
+	 */
+	if ((!S_ISDIR(inode->i_mode) && erofs_inode_is_hotfile(inode)) ||
+	    (S_ISDIR(inode->i_mode) &&
+	     (inode->hotdir || inode->hotfile)))
+		return inode->hot_rank;
+	return EROFS_HOT_RANK_NONE;
+}
+
+static unsigned int erofs_mkfs_hot_item_priority(struct erofs_inode *inode)
+{
+	/* Path lookup needs hot directory and symlink metadata before file data. */
+	if (S_ISDIR(inode->i_mode) || S_ISLNK(inode->i_mode))
+		return 0;
+	return 1;
+}
+
+static int erofs_mkfs_enqueue_hot_item(struct erofs_mkfs_hot_item **queue,
+				       struct erofs_inode *parent,
+				       struct erofs_inode *inode,
+				       unsigned int *order)
+{
+	struct erofs_mkfs_hot_item *item, **pos = queue;
+
+	item = malloc(sizeof(*item));
+	if (!item)
+		return -ENOMEM;
+
+	item->parent = parent;
+	item->inode = inode;
+	item->priority = erofs_mkfs_hot_item_priority(inode);
+	item->rank = erofs_mkfs_inode_rank(inode);
+	item->order = (*order)++;
+	item->next = NULL;
+
+	while (*pos) {
+		if (item->priority < (*pos)->priority)
+			break;
+		if (item->priority > (*pos)->priority) {
+			pos = &(*pos)->next;
+			continue;
+		}
+		if (item->rank < (*pos)->rank)
+			break;
+		if (item->rank == (*pos)->rank &&
+		    item->order < (*pos)->order)
+			break;
+		pos = &(*pos)->next;
+	}
+	item->next = *pos;
+	*pos = item;
+	return 0;
+}
+
+static int erofs_mkfs_enqueue_hot_children(struct erofs_inode *dir,
+					   struct erofs_mkfs_hot_item **queue,
+					   unsigned int *order)
+{
+	struct erofs_dentry *d;
+
+	list_for_each_entry(d, &dir->i_subdirs, d_child) {
+		struct erofs_inode *inode = d->inode;
+		int err;
+
+		if (is_dot_dotdot(d->name) ||
+		    (d->flags & EROFS_DENTRY_FLAG_VALIDNID))
+			continue;
+		if (erofs_mkfs_inode_rank(inode) == EROFS_HOT_RANK_NONE)
+			continue;
+		err = erofs_mkfs_enqueue_hot_item(queue, dir, inode, order);
+		if (err)
+			return err;
+	}
+	return 0;
+}
+
+static int erofs_mkfs_dump_tree_default(const struct erofs_mkfs_btctx *ctx,
+					struct erofs_inode *dumpdir,
+					struct list_head *pending_dirs,
+					bool grouped_dirdata)
+{
+	int err = 0, err2;
+
+	do {
+		struct erofs_inode *dir = dumpdir;
+		/* used for adding sub-directories in reverse order due to FIFO */
+		struct erofs_inode *head, **last = &head;
+		struct erofs_dentry *d;
+
+		dumpdir = dir->next_dirwrite;
+		list_for_each_entry(d, &dir->i_subdirs, d_child) {
+			struct erofs_inode *inode = d->inode;
+
+			if (is_dot_dotdot(d->name) ||
+			    (d->flags & EROFS_DENTRY_FLAG_VALIDNID))
+				continue;
+
+			if (!erofs_inode_visited(inode)) {
+				DBG_BUGON(ctx->rebuild && (inode->i_nlink == 1 ||
+					  S_ISDIR(inode->i_mode)) &&
+					  erofs_parent_inode(inode) != dir);
+				erofs_mark_parent_inode(inode, dir);
+
+				err = erofs_mkfs_handle_inode(ctx, inode);
+				if (err)
+					break;
+				if (S_ISDIR(inode->i_mode)) {
+					inode->next_dirwrite = NULL;
+					*last = inode;
+					last = &inode->next_dirwrite;
+					(void)erofs_igrab(inode);
+				}
+			} else if (!ctx->rebuild) {
+				++inode->i_nlink;
+			}
+		}
+		*last = dumpdir;	/* fixup the last (or the only) one */
+		dumpdir = head;
+		err2 = grouped_dirdata ?
+			erofs_mkfs_push_pending_job(pending_dirs,
+				EROFS_MKFS_JOB_DIR_BH, &dir, sizeof(dir)) :
+			erofs_mkfs_go(ctx, EROFS_MKFS_JOB_DIR_BH,
+				      &dir, sizeof(dir));
+		if (err || err2) {
+			if (!err)
+				err = err2;
+			break;
+		}
+	} while (dumpdir);
+	err2 = erofs_mkfs_flush_pending_jobs(ctx, pending_dirs);
+	return err ? err : err2;
+}
+
 static int erofs_mkfs_dump_tree(const struct erofs_mkfs_btctx *ctx)
 {
 	struct erofs_importer *im = ctx->im;
 	struct erofs_inode *root = im->root;
 	struct erofs_sb_info *sbi = root->sbi;
 	struct erofs_inode *dumpdir = erofs_igrab(root);
+	struct erofs_inode *deferdir = NULL, **deferlast = &deferdir;
+	struct erofs_mkfs_hot_item *hot_queue = NULL;
 	bool grouped_dirdata = im->params->grouped_dirdata;
 	LIST_HEAD(pending_dirs);
+	unsigned int hot_order = 0;
 	int err, err2;
 
 	erofs_mark_parent_inode(root, root);	/* rootdir mark */
 	root->next_dirwrite = NULL;
+	if (erofs_hotfile_enabled()) {
+		root->hotdir = true;
+		root->hot_rank = 0;
+	}
 	/* update dev/i_ino[1] to keep track of the base image */
 	if (ctx->incremental) {
 		root->dev = root->sbi->dev;
@@ -2168,13 +2400,46 @@ static int erofs_mkfs_dump_tree(const struct erofs_mkfs_btctx *ctx)
 		sbi->root_nid = root->nid;
 	}
 
+	if (!erofs_hotfile_enabled())
+		return erofs_mkfs_dump_tree_default(ctx, dumpdir, &pending_dirs,
+						    grouped_dirdata);
+
+	err = erofs_mkfs_enqueue_hot_children(root, &hot_queue, &hot_order);
+	if (err)
+		goto out_hot;
+
+	while (hot_queue) {
+		struct erofs_mkfs_hot_item *item = hot_queue;
+		struct erofs_inode *inode = item->inode;
+		struct erofs_inode *parent = item->parent;
+
+		hot_queue = item->next;
+		free(item);
+
+		if (!erofs_inode_visited(inode)) {
+			erofs_mark_parent_inode(inode, parent);
+			err = erofs_mkfs_handle_inode(ctx, inode);
+			if (err)
+				goto out_hot;
+			if (S_ISDIR(inode->i_mode)) {
+				err = erofs_mkfs_enqueue_hot_children(inode,
+								      &hot_queue,
+								      &hot_order);
+				if (err)
+					goto out_hot;
+				inode->hotdir_deferred = true;
+			}
+		}
+	}
+
 	do {
 		struct erofs_inode *dir = dumpdir;
-		/* used for adding sub-directories in reverse order due to FIFO */
 		struct erofs_inode *head, **last = &head;
 		struct erofs_dentry *d;
 
+		err2 = 0;
 		dumpdir = dir->next_dirwrite;
+		dir->next_dirwrite = NULL;
 		list_for_each_entry(d, &dir->i_subdirs, d_child) {
 			struct erofs_inode *inode = d->inode;
 
@@ -2192,16 +2457,37 @@ static int erofs_mkfs_dump_tree(const struct erofs_mkfs_btctx *ctx)
 				if (err)
 					break;
 				if (S_ISDIR(inode->i_mode)) {
+					inode->next_dirwrite = NULL;
 					*last = inode;
 					last = &inode->next_dirwrite;
 					(void)erofs_igrab(inode);
 				}
-			} else if (!ctx->rebuild) {
-				++inode->i_nlink;
+			} else {
+				if (S_ISDIR(inode->i_mode) &&
+				    inode->hotdir_deferred) {
+					inode->hotdir_deferred = false;
+					inode->next_dirwrite = NULL;
+					/*
+					 * A hot directory means its own metadata is hot;
+					 * it must not recursively promote all cold
+					 * children beneath it.
+					 */
+					*deferlast = inode;
+					deferlast = &inode->next_dirwrite;
+					(void)erofs_igrab(inode);
+				}
+				if (!ctx->rebuild &&
+				    erofs_parent_inode(inode) != dir)
+					++inode->i_nlink;
 			}
 		}
-		*last = dumpdir;	/* fixup the last (or the only) one */
+		*last = dumpdir;
 		dumpdir = head;
+		if (!dumpdir && deferdir) {
+			dumpdir = deferdir;
+			deferdir = NULL;
+			deferlast = &deferdir;
+		}
 		err2 = grouped_dirdata ?
 			erofs_mkfs_push_pending_job(&pending_dirs,
 				EROFS_MKFS_JOB_DIR_BH, &dir, sizeof(dir)) :
@@ -2214,6 +2500,13 @@ static int erofs_mkfs_dump_tree(const struct erofs_mkfs_btctx *ctx)
 		}
 	} while (dumpdir);
 	err2 = erofs_mkfs_flush_pending_jobs(ctx, &pending_dirs);
+out_hot:
+	while (hot_queue) {
+		struct erofs_mkfs_hot_item *item = hot_queue;
+
+		hot_queue = item->next;
+		free(item);
+	}
 	return err ? err : err2;
 }
 
diff --git a/man/mkfs.erofs.1 b/man/mkfs.erofs.1
index 65ec807..393a293 100644
--- a/man/mkfs.erofs.1
+++ b/man/mkfs.erofs.1
@@ -310,6 +310,19 @@ random gzip access. Source file must be a gzip-compressed tarball.
 .BI "\-\-hard-dereference"
 Dereference hardlinks and add links as separate inodes.
 .TP
+.BI "\-\-hot-file-list=" path
+Read a newline-delimited list of image-relative hot file paths for local
+directory sources.
+Hot regular files are scheduled before cold files while keeping compression
+enabled.  Parent directories of listed files are also prioritized so hot
+subtrees are traversed earlier than cold sibling subtrees.  Entries ending in
+\fI/\fR are treated as hot directories: the directory inode and directory data
+are prioritized, but children are not recursively marked hot.  Hot files do not
+participate in fragment packing or cross-file dedupe.  Symlink aliases in the
+source tree are resolved before matching, so merged-usr compatibility paths such
+as \fI/lib/...\fR can match their real payloads under \fI/usr/lib/...\fR while
+preserving the user-provided order.
+.TP
 .B "\-\-ignore-mtime"
 Ignore the file modification time whenever it would cause \fBmkfs.erofs\fR to
 use extended inodes over compact inodes. When not using a fixed timestamp, this
diff --git a/mkfs/main.c b/mkfs/main.c
index 31fea7c..c154247 100644
--- a/mkfs/main.c
+++ b/mkfs/main.c
@@ -23,6 +23,7 @@
 #include "erofs/xattr.h"
 #include "erofs/exclude.h"
 #include "erofs/block_list.h"
+#include "erofs/hotfile.h"
 #include "erofs/compress_hints.h"
 #include "erofs/blobchunk.h"
 #include "../lib/compressor.h"
@@ -103,9 +104,12 @@ static struct option long_options[] = {
 	{"MZ", optional_argument, NULL, 537},
 	{"xattr-prefix", required_argument, NULL, 538},
 	{"xattr-inode-digest", required_argument, NULL, 539},
+	{"hot-file-list", required_argument, NULL, 540},
 	{0, 0, 0, 0},
 };
 
+static char *hotfile_list_path;
+
 static void print_available_compressors(FILE *f, const char *delim)
 {
 	int i = 0;
@@ -208,6 +212,11 @@ static void usage(int argc, char **argv)
 		" --uid-offset=#         add offset # to all file uids (# = id offset)\n"
 		" --gid-offset=#         add offset # to all file gids (# = id offset)\n"
 		" --hard-dereference     dereference hardlinks, add links as separate inodes\n"
+		" --hot-file-list=X      specify newline-separated hot file paths for\n"
+		"                        local directory sources; local rootfs builds\n"
+		"                        resolve symlink aliases\n"
+		"                        (e.g. /lib/... -> /usr/lib/...) and prioritize\n"
+		"                        ancestor directories as well\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"
 		" --mount-point=X        X=prefix of target fs path (default: /)\n"
@@ -1487,6 +1496,12 @@ static int mkfs_parse_options_cfg(struct erofs_importer_params *params,
 				return err;
 			}
 			break;
+		case 540:
+			free(hotfile_list_path);
+			hotfile_list_path = strdup(optarg);
+			if (!hotfile_list_path)
+				return -ENOMEM;
+			break;
 		case 'V':
 			version();
 			exit(0);
@@ -1542,6 +1557,11 @@ static int mkfs_parse_options_cfg(struct erofs_importer_params *params,
 			return err;
 	}
 
+	if (hotfile_list_path && source_mode != EROFS_MKFS_SOURCE_LOCALDIR) {
+		erofs_err("--hot-file-list is only supported for local directory sources");
+		return -EOPNOTSUPP;
+	}
+
 	if (quiet) {
 		cfg.c_dbg_lvl = EROFS_ERR;
 		cfg.c_showprogress = false;
@@ -1778,6 +1798,15 @@ int main(int argc, char **argv)
 		goto exit;
 	}
 
+	if (hotfile_list_path) {
+		err = erofs_hotfile_load(hotfile_list_path);
+		if (err) {
+			erofs_err("failed to load hot-file list %s: %s",
+				  hotfile_list_path, erofs_strerror(err));
+			goto exit;
+		}
+	}
+
 	err = parse_source_date_epoch();
 	if (err) {
 		fprintf(stderr, "Try '%s --help' for more information.\n", argv[0]);
@@ -2065,6 +2094,9 @@ exit:
 		fclose(blklst);
 	erofs_cleanup_compress_hints();
 	erofs_cleanup_exclude_rules();
+	erofs_hotfile_exit();
+	free(hotfile_list_path);
+	hotfile_list_path = NULL;
 	if (cfg.c_chunkbits || source_mode == EROFS_MKFS_SOURCE_REBUILD)
 		erofs_blob_exit();
 	erofs_xattr_cleanup_name_prefixes();
diff --git a/tests/hotfile-layout.sh b/tests/hotfile-layout.sh
new file mode 100755
index 0000000..6af3f41
--- /dev/null
+++ b/tests/hotfile-layout.sh
@@ -0,0 +1,321 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0+
+
+set -eu
+
+MKFS=${MKFS:-./mkfs/mkfs.erofs}
+DUMP=${DUMP:-./dump/dump.erofs}
+
+tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/erofs-hotfile-layout.XXXXXX")
+cleanup() {
+	rm -rf "$tmpdir"
+}
+trap cleanup EXIT
+
+extent_start() {
+	"$DUMP" -e --path="$1" "$2" |
+		awk '
+			/^[[:space:]]+[0-9]+:/ {
+				line = $0
+				sub(/^.*\|[[:space:]]*[0-9]+[[:space:]]*:[[:space:]]*/, "", line)
+				sub(/\.\..*$/, "", line)
+				gsub(/[[:space:]]/, "", line)
+				print line
+				exit
+			}
+		'
+}
+
+inode_links() {
+	"$DUMP" --path="$1" "$2" |
+		awk '
+			/Links:/ {
+				for (i = 1; i <= NF; ++i) {
+					if ($i == "Links:") {
+						print $(i + 1)
+						exit
+					}
+				}
+			}
+		'
+}
+
+assert_hot_file_nonrecursive_hotdir() {
+	img="$1"
+
+	hot=$(extent_start /a/hot "$img")
+	hotdir_cold=$(extent_start /a/cold "$img")
+	outside_cold=$(extent_start /c/cold "$img")
+
+	if [ "$hot" -ge "$hotdir_cold" ]; then
+		echo "hot file was not placed before cold sibling: hot=$hot cold=$hotdir_cold" >&2
+		exit 1
+	fi
+
+	if [ "$outside_cold" -ge "$hotdir_cold" ]; then
+		echo "hot directory promoted cold child recursively: outside=$outside_cold hotdir_cold=$hotdir_cold" >&2
+		exit 1
+	fi
+}
+
+assert_explicit_hotdir_nonrecursive() {
+	img="$1"
+
+	hotdir_cold=$(extent_start /a/cold "$img")
+	outside_cold=$(extent_start /c/cold "$img")
+
+	if [ "$outside_cold" -ge "$hotdir_cold" ]; then
+		echo "explicit hot directory promoted cold child recursively: outside=$outside_cold hotdir_cold=$hotdir_cold" >&2
+		exit 1
+	fi
+}
+
+assert_symlink_chain_metadata_hot() {
+	img="$1"
+	log="$2"
+
+	parent_link=$(extent_start /lib64 "$img")
+	parent_cold=$(extent_start /aaa-cold "$img")
+	symlink=$(extent_start /usr/lib64/ld-linux-x86-64.so.2 "$img")
+	cold=$(extent_start /usr/lib64/aaa-cold "$img")
+	real=$(extent_start /usr/lib/x86_64-linux-gnu/ld-real "$img")
+
+	if ! grep -q 'loaded 3 hot files' "$log"; then
+		echo "hot symlink chain did not preserve parent link, symlink, and target as hot files" >&2
+		cat "$log" >&2
+		exit 1
+	fi
+
+	if [ "$parent_link" -ge "$parent_cold" ]; then
+		echo "hot parent symlink metadata was not placed before cold sibling: parent_link=$parent_link parent_cold=$parent_cold" >&2
+		exit 1
+	fi
+
+	if [ "$symlink" -ge "$cold" ]; then
+		echo "hot symlink metadata was not placed before cold sibling: symlink=$symlink cold=$cold" >&2
+		exit 1
+	fi
+
+	if [ "$real" -ge "$cold" ]; then
+		echo "hot symlink target was not placed before cold sibling: real=$real cold=$cold" >&2
+		exit 1
+	fi
+}
+
+assert_hot_metadata_precedes_regular_data() {
+	img="$1"
+
+	hotfile=$(extent_start /a/hot "$img")
+	hotdir=$(extent_start /z "$img")
+
+	if [ "$hotdir" -ge "$hotfile" ]; then
+		echo "hot metadata was not placed before regular hot file data: hotdir=$hotdir hotfile=$hotfile" >&2
+		exit 1
+	fi
+}
+
+assert_late_hotdirs_precede_regular_hotfile() {
+	img="$1"
+
+	sysdir=$(extent_start /sys "$img")
+	devdir=$(extent_start /dev "$img")
+	liblink=$(extent_start /lib "$img")
+	lib64link=$(extent_start /lib64 "$img")
+	node=$(extent_start /usr/local/bin/node "$img")
+
+	if [ "$liblink" -ge "$node" ]; then
+		echo "late hot /lib symlink metadata was placed after node data: lib=$liblink node=$node" >&2
+		exit 1
+	fi
+
+	if [ "$lib64link" -ge "$node" ]; then
+		echo "late hot /lib64 symlink metadata was placed after node data: lib64=$lib64link node=$node" >&2
+		exit 1
+	fi
+
+	if [ "$sysdir" -ge "$node" ]; then
+		echo "late hot /sys metadata was placed after node data: sys=$sysdir node=$node" >&2
+		exit 1
+	fi
+
+	if [ "$devdir" -ge "$node" ]; then
+		echo "late hot /dev metadata was placed after node data: dev=$devdir node=$node" >&2
+		exit 1
+	fi
+}
+
+assert_late_symlink_alias_inherits_target_rank() {
+	img="$1"
+
+	alias=$(extent_start /usr/lib/x86_64-linux-gnu/libfoo.so.1 "$img")
+	node=$(extent_start /usr/local/bin/node "$img")
+
+	if [ "$alias" -ge "$node" ]; then
+		echo "late hot symlink alias was placed after node data: alias=$alias node=$node" >&2
+		exit 1
+	fi
+}
+
+assert_hot_hardlinks_keep_real_link_count() {
+	img="$1"
+
+	links=$(inode_links /a/hot "$img")
+
+	if [ "$links" -ne 2 ]; then
+		echo "hot hardlink aliases changed link count: links=$links" >&2
+		exit 1
+	fi
+}
+
+assert_root_dirdata_precedes_hot_file() {
+	img="$1"
+
+	rootdir=$(extent_start / "$img")
+	hotfile=$(extent_start /zz/hot "$img")
+
+	if [ "$rootdir" -ge "$hotfile" ]; then
+		echo "root directory data was placed after hot file data: root=$rootdir hotfile=$hotfile" >&2
+		exit 1
+	fi
+}
+
+root="$tmpdir/root"
+mkdir -p "$root/a" "$root/c"
+printf hot > "$root/a/hot"
+dd if=/dev/zero bs=4096 count=16 of="$root/a/cold" status=none
+dd if=/dev/zero bs=4096 count=16 of="$root/c/cold" status=none
+
+printf '/a/hot\n' > "$tmpdir/hotlist.file"
+"$MKFS" -zlz4hc,level=9 --workers=1 \
+	--hot-file-list="$tmpdir/hotlist.file" \
+	"$tmpdir/img.file.erofs" "$root" >/dev/null
+assert_hot_file_nonrecursive_hotdir "$tmpdir/img.file.erofs"
+
+printf '/a/\n' > "$tmpdir/hotlist.dir"
+"$MKFS" -zlz4hc,level=9 --workers=1 \
+	--hot-file-list="$tmpdir/hotlist.dir" \
+	"$tmpdir/img.dir.erofs" "$root" >/dev/null
+assert_explicit_hotdir_nonrecursive "$tmpdir/img.dir.erofs"
+
+mkdir -p "$root/usr/lib/x86_64-linux-gnu" "$root/usr/lib64"
+printf real > "$root/usr/lib/x86_64-linux-gnu/ld-real"
+dd if=/dev/zero bs=4096 count=16 of="$root/aaa-cold" status=none
+dd if=/dev/zero bs=4096 count=16 of="$root/usr/lib64/aaa-cold" status=none
+ln -s usr/lib64 "$root/lib64"
+ln -s /lib/x86_64-linux-gnu/ld-real "$root/usr/lib64/ld-linux-x86-64.so.2"
+
+printf '/lib64/ld-linux-x86-64.so.2\n' > "$tmpdir/hotlist.symlink"
+"$MKFS" -d9 -zlz4hc,level=9 --workers=1 \
+	--hot-file-list="$tmpdir/hotlist.symlink" \
+	"$tmpdir/img.symlink.erofs" "$root" >"$tmpdir/mkfs.symlink.log" 2>&1
+assert_symlink_chain_metadata_hot "$tmpdir/img.symlink.erofs" "$tmpdir/mkfs.symlink.log"
+
+root_meta="$tmpdir/root-meta-first"
+mkdir -p "$root_meta/a" "$root_meta/z"
+dd if=/dev/urandom bs=4096 count=256 of="$root_meta/a/hot" status=none
+printf '/a/hot\n/z/\n' > "$tmpdir/hotlist.meta-first"
+"$MKFS" -d9 -zzstd,level=9 --workers=1 \
+	--hot-file-list="$tmpdir/hotlist.meta-first" \
+	"$tmpdir/img.meta-first.erofs" "$root_meta" >"$tmpdir/mkfs.meta-first.log" 2>&1
+assert_hot_metadata_precedes_regular_data "$tmpdir/img.meta-first.erofs"
+
+root_usrmerge="$tmpdir/root-usrmerge"
+mkdir -p \
+	"$root_usrmerge/dev" \
+	"$root_usrmerge/etc/ssl" \
+	"$root_usrmerge/proc" \
+	"$root_usrmerge/sys" \
+	"$root_usrmerge/usr/bin" \
+	"$root_usrmerge/usr/lib/x86_64-linux-gnu" \
+	"$root_usrmerge/usr/lib64" \
+	"$root_usrmerge/usr/local/bin" \
+	"$root_usrmerge/usr/local/sbin"
+ln -s usr/bin "$root_usrmerge/bin"
+ln -s usr/lib "$root_usrmerge/lib"
+ln -s usr/lib64 "$root_usrmerge/lib64"
+ln -s dash "$root_usrmerge/usr/bin/sh"
+ln -s /lib/x86_64-linux-gnu/ld-real "$root_usrmerge/usr/lib64/ld-linux-x86-64.so.2"
+for file in \
+	etc/passwd \
+	etc/ld.so.cache \
+	etc/ssl/openssl.cnf \
+	usr/bin/dash \
+	usr/lib/x86_64-linux-gnu/ld-real \
+	usr/lib/x86_64-linux-gnu/libc.so.6 \
+	usr/lib/x86_64-linux-gnu/libdl.so.2 \
+	usr/lib/x86_64-linux-gnu/libstdc++.so.6 \
+	usr/lib/x86_64-linux-gnu/libm.so.6 \
+	usr/lib/x86_64-linux-gnu/libgcc_s.so.1 \
+	usr/lib/x86_64-linux-gnu/libpthread.so.0 \
+	usr/local/bin/docker-entrypoint.sh \
+	usr/local/bin/node \
+	usr/local/sbin/docker-entrypoint.sh; do
+	printf x > "$root_usrmerge/$file"
+done
+cat > "$tmpdir/hotlist.usrmerge" <<'EOF'
+/etc/passwd
+/proc/
+/usr/local/sbin/docker-entrypoint.sh
+/usr/local/bin/docker-entrypoint.sh
+/bin/sh
+/lib64/ld-linux-x86-64.so.2
+/etc/ld.so.preload
+/etc/ld.so.cache
+/lib/x86_64-linux-gnu/libc.so.6
+/usr/local/sbin/node
+/usr/local/bin/node
+/lib/x86_64-linux-gnu/libdl.so.2
+/lib/x86_64-linux-gnu/libstdc++.so.6
+/lib/x86_64-linux-gnu/libm.so.6
+/lib/x86_64-linux-gnu/libgcc_s.so.1
+/lib/x86_64-linux-gnu/libpthread.so.0
+/etc/ssl/openssl.cnf
+/sys/
+/dev/
+EOF
+"$MKFS" -d9 -zzstd,level=9 --workers=1 \
+	--hot-file-list="$tmpdir/hotlist.usrmerge" \
+	"$tmpdir/img.usrmerge.erofs" "$root_usrmerge" >"$tmpdir/mkfs.usrmerge.log" 2>&1
+assert_late_hotdirs_precede_regular_hotfile "$tmpdir/img.usrmerge.erofs"
+
+root_alias="$tmpdir/root-alias-rank"
+mkdir -p "$root_alias/usr/lib/x86_64-linux-gnu" "$root_alias/usr/local/bin"
+dd if=/dev/zero bs=4096 count=64 of="$root_alias/usr/local/bin/node" status=none
+dd if=/dev/zero bs=4096 count=64 of="$root_alias/usr/lib/x86_64-linux-gnu/libfoo.so.1.2.3" status=none
+ln -s libfoo.so.1.2.3 "$root_alias/usr/lib/x86_64-linux-gnu/libfoo.so.1"
+cat > "$tmpdir/hotlist.alias-rank" <<'EOF'
+/usr/lib/x86_64-linux-gnu/libfoo.so.1.2.3
+/usr/local/bin/node
+/usr/lib/x86_64-linux-gnu/libfoo.so.1
+EOF
+"$MKFS" -d9 -zzstd,level=9 --workers=1 \
+	--hot-file-list="$tmpdir/hotlist.alias-rank" \
+	"$tmpdir/img.alias-rank.erofs" "$root_alias" >"$tmpdir/mkfs.alias-rank.log" 2>&1
+assert_late_symlink_alias_inherits_target_rank "$tmpdir/img.alias-rank.erofs"
+
+root_hardlink="$tmpdir/root-hardlink"
+mkdir -p "$root_hardlink/a" "$root_hardlink/b"
+printf data > "$root_hardlink/a/hot"
+ln "$root_hardlink/a/hot" "$root_hardlink/b/alias"
+cat > "$tmpdir/hotlist.hardlink" <<'EOF'
+/a/hot
+/b/alias
+EOF
+"$MKFS" -d9 --hot-file-list="$tmpdir/hotlist.hardlink" \
+	"$tmpdir/img.hardlink.erofs" "$root_hardlink" >"$tmpdir/mkfs.hardlink.log" 2>&1
+assert_hot_hardlinks_keep_real_link_count "$tmpdir/img.hardlink.erofs"
+
+root_wide="$tmpdir/root-wide"
+mkdir -p "$root_wide/zz"
+printf hot > "$root_wide/zz/hot"
+i=0
+while [ "$i" -lt 420 ]; do
+	dir=$(printf 'cold%04d' "$i")
+	mkdir -p "$root_wide/$dir"
+	printf x > "$root_wide/$dir/file"
+	i=$((i + 1))
+done
+printf '/zz/hot\n' > "$tmpdir/hotlist.wide-root"
+"$MKFS" -d9 --hot-file-list="$tmpdir/hotlist.wide-root" \
+	"$tmpdir/img.wide-root.erofs" "$root_wide" >"$tmpdir/mkfs.wide-root.log" 2>&1
+assert_root_dirdata_precedes_hot_file "$tmpdir/img.wide-root.erofs"

base-commit: 8a579d4d692689eee0af40df91d91d4e632d4c0e
-- 
2.43.7
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.