* [RFC 0/7] erofs-utils: add manifest input support
@ 2026-11-04 4:07 Wenyuan Xu
2026-11-04 4:07 ` [RFC 1/7] erofs-utils: mkfs: init root inode in local source mode Wenyuan Xu
` (6 more replies)
0 siblings, 7 replies; 8+ messages in thread
From: Wenyuan Xu @ 2026-11-04 4:07 UTC (permalink / raw)
To: xiang, linux-erofs; +Cc: sereno.xu
Hi,
This series adds manifest input support to mkfs.erofs. It lets callers
specify the image tree and metadata without first creating a matching
staging directory, while reading regular file contents from a backing
directory. Only entries listed in the manifest are imported; source
directories are not scanned recursively.
This series is based on origin/experimental-tests.
Usage:
mkfs.erofs --manifest=manifest.txt out.erofs basedir/
The manifest accepts two record formats, which can be mixed:
PATH PAYLOAD SIZE MODE UID GID RDEV MTIME [KEY=VALUE ...]
PATH PAYLOAD
The complete record syntax is inspired by composefs's
"mkcomposefs --from-file" format, documented in composefs-dump(5) [1].
This series uses eight fixed fields with PAYLOAD immediately after PATH,
omitting the NLINK, CONTENT and DIGEST fields from the composefs format.
Complete records specify metadata and xattrs explicitly. Regular file
payloads identify backing files relative to basedir, symlink payloads
provide link contents, and a MODE beginning with @ creates a hard link
to an earlier entry. Fields unused for a file type are not parsed and
can be written as '-'.
Two-field records import metadata, contents and xattrs from the backing
file using local import policies. Symlinks are imported without
following their targets, and source hard links are preserved unless
--hard-dereference is specified.
PATH must start with '/', with the root directory defined first and
parents preceding their children. Duplicate paths and xattr keys are
rejected. String fields support \xXY byte escapes, including binary
xattr values. MTIME uses seconds.nanoseconds, where 1.1 means one second
and one nanosecond.
Both formats support regular file compression, xattr name prefixes and
shared xattrs. They also work with --incremental=data: the manifest
provides the update tree, matching directories are merged, and directory
metadata comes from the new records. No on-disk format changes are
required.
Patch 1 moves local root inode initialization to the local source path.
Patches 2 and 3 add helpers for duplicate xattr detection and building
shared xattrs from already loaded inodes. Patches 4 and 5 implement the
complete and two-field formats. Patches 6 and 7 add documentation and
regression coverage.
Test 033 covers file types, metadata, hard links, escaping, binary and
shared xattrs, name prefixes, malformed records, compression and
incremental updates. It checks images using dump.erofs and fsck.erofs.
Please apply the fix in [2] before running test 033.
[1] https://man.archlinux.org/man/composefs-dump.5.en
[2] https://lore.kernel.org/all/20261104035208.2471090-1-sereno.xu@deepseek.com/
Feedback on the record formats and importer integration is welcome.
Assisted-by: LLM
Signed-off-by: Wenyuan Xu <sereno.xu@deepseek.com>
Wenyuan Xu (7):
erofs-utils: mkfs: init root inode in local source mode
erofs-utils: lib: add erofs_inode_has_xattr helper
erofs-utils: lib: add erofs_build_shared_xattrs() helper
erofs-utils: mkfs: add manifest input mode
erofs-utils: mkfs: support two-field manifest records
erofs-utils: man: document manifest input formats
erofs-utils: tests: cover manifest input
include/erofs/inode.h | 2 +
include/erofs/manifest.h | 17 ++
include/erofs/xattr.h | 3 +
lib/Makefile.am | 3 +-
lib/inode.c | 30 +-
lib/manifest.c | 527 ++++++++++++++++++++++++++++++++
lib/xattr.c | 62 +++-
man/mkfs.erofs.1 | 114 +++++++
mkfs/main.c | 43 ++-
tests/Makefile.am | 3 +
tests/erofs/033 | 637 +++++++++++++++++++++++++++++++++++++++
tests/erofs/033.out | 2 +
12 files changed, 1403 insertions(+), 40 deletions(-)
create mode 100644 include/erofs/manifest.h
create mode 100644 lib/manifest.c
create mode 100755 tests/erofs/033
create mode 100644 tests/erofs/033.out
--
2.43.0
^ permalink raw reply [flat|nested] 8+ messages in thread
* [RFC 1/7] erofs-utils: mkfs: init root inode in local source mode
2026-11-04 4:07 [RFC 0/7] erofs-utils: add manifest input support Wenyuan Xu
@ 2026-11-04 4:07 ` Wenyuan Xu
2026-11-04 4:07 ` [RFC 2/7] erofs-utils: lib: add erofs_inode_has_xattr helper Wenyuan Xu
` (5 subsequent siblings)
6 siblings, 0 replies; 8+ messages in thread
From: Wenyuan Xu @ 2026-11-04 4:07 UTC (permalink / raw)
To: xiang, linux-erofs; +Cc: sereno.xu
Signed-off-by: Wenyuan Xu <sereno.xu@deepseek.com>
---
include/erofs/inode.h | 2 ++
lib/inode.c | 30 ++++--------------------------
mkfs/main.c | 8 +++++++-
3 files changed, 13 insertions(+), 27 deletions(-)
diff --git a/include/erofs/inode.h b/include/erofs/inode.h
index 5b67b95..88c413b 100644
--- a/include/erofs/inode.h
+++ b/include/erofs/inode.h
@@ -42,6 +42,8 @@ int erofs_allocate_inode_bh_data(struct erofs_inode *inode, erofs_blk_t nblocks,
bool erofs_dentry_is_wht(struct erofs_sb_info *sbi, struct erofs_dentry *d);
int __erofs_fill_inode(struct erofs_importer *im, struct erofs_inode *inode,
struct stat *st, const char *path);
+int erofs_fill_inode(struct erofs_importer *im, struct erofs_inode *inode,
+ struct stat *st, const char *path);
struct erofs_inode *erofs_new_inode(struct erofs_sb_info *sbi);
int erofs_importer_load_tree(struct erofs_importer *im, bool rebuild,
bool incremental);
diff --git a/lib/inode.c b/lib/inode.c
index da29b5a..2a3d022 100644
--- a/lib/inode.c
+++ b/lib/inode.c
@@ -1334,7 +1334,7 @@ int __erofs_fill_inode(struct erofs_importer *im, struct erofs_inode *inode,
return 0;
}
-static int erofs_fill_inode(struct erofs_importer *im, struct erofs_inode *inode,
+int erofs_fill_inode(struct erofs_importer *im, struct erofs_inode *inode,
struct stat *st, const char *path)
{
const struct erofs_importer_params *params = im->params;
@@ -2251,30 +2251,6 @@ struct erofs_mkfs_buildtree_ctx {
struct erofs_importer *im;
bool rebuild, incremental;
};
-#ifndef EROFS_MT_ENABLED
-#define __erofs_mkfs_build_tree erofs_mkfs_build_tree
-#endif
-
-static int __erofs_mkfs_build_tree(const struct erofs_mkfs_btctx *ctx)
-{
- struct erofs_importer *im = ctx->im;
-
- if (!ctx->rebuild) {
- struct erofs_importer_params *params = im->params;
- struct stat st;
- int err;
-
- err = lstat(params->source, &st);
- if (err)
- return -errno;
-
- err = erofs_fill_inode(im, im->root, &st, params->source);
- if (err)
- return err;
- }
- return erofs_mkfs_dump_tree(ctx);
-}
-
#ifdef EROFS_MT_ENABLED
#ifdef HAVE_SYS_RESOURCE_H
@@ -2339,7 +2315,7 @@ static int erofs_mkfs_build_tree(struct erofs_mkfs_btctx *ctx)
if (err)
goto fail;
- err = __erofs_mkfs_build_tree(ctx);
+ err = erofs_mkfs_dump_tree(ctx);
erofs_mkfs_go(ctx, ~0, NULL, 0);
err2 = pthread_join(sbi->dfops_worker, &retval);
DBG_BUGON(!q->exited);
@@ -2358,6 +2334,8 @@ fail:
free(q);
return err;
}
+#else
+#define erofs_mkfs_build_tree erofs_mkfs_dump_tree
#endif
int erofs_importer_load_tree(struct erofs_importer *im, bool rebuild,
diff --git a/mkfs/main.c b/mkfs/main.c
index 93123c1..d8706d7 100644
--- a/mkfs/main.c
+++ b/mkfs/main.c
@@ -1987,7 +1987,13 @@ int main(int argc, char **argv)
}
importer.root = root;
- if (source_mode == EROFS_MKFS_SOURCE_TAR) {
+ if (source_mode == EROFS_MKFS_SOURCE_LOCALDIR) {
+ struct stat st;
+
+ err = lstat(cfg.c_src_path, &st);
+ if (!err)
+ err = erofs_fill_inode(&importer, importer.root, &st, cfg.c_src_path);
+ } else if (source_mode == EROFS_MKFS_SOURCE_TAR) {
while (!(err = tarerofs_parse_tar(&importer, &erofstar)))
;
} else if (source_mode == EROFS_MKFS_SOURCE_REBUILD) {
--
2.43.0
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [RFC 2/7] erofs-utils: lib: add erofs_inode_has_xattr helper
2026-11-04 4:07 [RFC 0/7] erofs-utils: add manifest input support Wenyuan Xu
2026-11-04 4:07 ` [RFC 1/7] erofs-utils: mkfs: init root inode in local source mode Wenyuan Xu
@ 2026-11-04 4:07 ` Wenyuan Xu
2026-11-04 4:07 ` [RFC 3/7] erofs-utils: lib: add erofs_build_shared_xattrs() helper Wenyuan Xu
` (4 subsequent siblings)
6 siblings, 0 replies; 8+ messages in thread
From: Wenyuan Xu @ 2026-11-04 4:07 UTC (permalink / raw)
To: xiang, linux-erofs; +Cc: sereno.xu
Allow callers to check whether an inode already has an in-memory xattr
with a given name without exposing the internal xattr structures.
Assisted-by: LLM
Signed-off-by: Wenyuan Xu <sereno.xu@deepseek.com>
---
include/erofs/xattr.h | 1 +
lib/xattr.c | 10 ++++++++++
2 files changed, 11 insertions(+)
diff --git a/include/erofs/xattr.h b/include/erofs/xattr.h
index d12c292..0016ad2 100644
--- a/include/erofs/xattr.h
+++ b/include/erofs/xattr.h
@@ -45,6 +45,7 @@ int erofs_setxattr(struct erofs_inode *inode, int index, const char *name,
const void *value, size_t size);
int erofs_vfs_setxattr(struct erofs_inode *inode, const char *name,
const void *value, size_t size);
+bool erofs_inode_has_xattr(struct erofs_inode *inode, const char *name);
int erofs_set_opaque_xattr(struct erofs_inode *inode);
void erofs_clear_opaque_xattr(struct erofs_inode *inode);
bool erofs_get_opaque_from_disk(struct erofs_inode *inode);
diff --git a/lib/xattr.c b/lib/xattr.c
index d86d94a..a0ac76f 100644
--- a/lib/xattr.c
+++ b/lib/xattr.c
@@ -579,6 +579,16 @@ int erofs_vfs_setxattr(struct erofs_inode *inode, const char *name,
return erofs_setxattr(inode, 0, name, value, size);
}
+bool erofs_inode_has_xattr(struct erofs_inode *inode, const char *name)
+{
+ struct erofs_inode_xattr_node *node;
+
+ list_for_each_entry(node, &inode->i_xattrs, list)
+ if (!strcmp(node->item->kvbuf, name))
+ return true;
+ return false;
+}
+
static void erofs_removexattr(struct erofs_inode *inode, const char *key)
{
struct erofs_inode_xattr_node *node, *n;
--
2.43.0
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [RFC 3/7] erofs-utils: lib: add erofs_build_shared_xattrs() helper
2026-11-04 4:07 [RFC 0/7] erofs-utils: add manifest input support Wenyuan Xu
2026-11-04 4:07 ` [RFC 1/7] erofs-utils: mkfs: init root inode in local source mode Wenyuan Xu
2026-11-04 4:07 ` [RFC 2/7] erofs-utils: lib: add erofs_inode_has_xattr helper Wenyuan Xu
@ 2026-11-04 4:07 ` Wenyuan Xu
2026-11-04 4:07 ` [RFC 4/7] erofs-utils: mkfs: add manifest input mode Wenyuan Xu
` (3 subsequent siblings)
6 siblings, 0 replies; 8+ messages in thread
From: Wenyuan Xu @ 2026-11-04 4:07 UTC (permalink / raw)
To: xiang, linux-erofs; +Cc: sereno.xu
Extract shared xattr serialization from the local-directory scan so it
can also be used with xattrs already attached to inodes.
Add erofs_build_shared_xattrs() to select shared entries without
freeing inline entries. Retain an extra reference to shared entries
until xattr manager cleanup.
Assisted-by: LLM
Signed-off-by: Wenyuan Xu <sereno.xu@deepseek.com>
---
include/erofs/xattr.h | 2 ++
lib/xattr.c | 52 +++++++++++++++++++++++++++++++++----------
2 files changed, 42 insertions(+), 12 deletions(-)
diff --git a/include/erofs/xattr.h b/include/erofs/xattr.h
index 0016ad2..1388a6f 100644
--- a/include/erofs/xattr.h
+++ b/include/erofs/xattr.h
@@ -32,6 +32,8 @@ int erofs_prepare_xattr_ibody(struct erofs_inode *inode, bool noroom);
char *erofs_export_xattr_ibody(struct erofs_inode *inode);
int erofs_load_shared_xattrs_from_path(struct erofs_sb_info *sbi, const char *path,
long inlinexattr_tolerance);
+int erofs_build_shared_xattrs(struct erofs_sb_info *sbi,
+ long inlinexattr_tolerance);
int erofs_xattr_insert_name_prefix(const char *prefix);
int erofs_xattr_set_ishare_prefix(struct erofs_sb_info *sbi,
const char *prefix);
diff --git a/lib/xattr.c b/lib/xattr.c
index a0ac76f..4e7ac8b 100644
--- a/lib/xattr.c
+++ b/lib/xattr.c
@@ -995,27 +995,18 @@ static void erofs_write_xattr_entry(char *buf, struct erofs_xattritem *item)
item->prefix, item->kvbuf + item->prefix_len);
}
-int erofs_load_shared_xattrs_from_path(struct erofs_sb_info *sbi, const char *path,
- long inlinexattr_tolerance)
+static int erofs_write_shared_xattrs(struct erofs_sb_info *sbi,
+ unsigned int sharedxattr_count)
{
struct erofs_xattrmgr *xamgr = sbi->xamgr;
struct erofs_xattritem *item, *n, **sorted_n;
- unsigned int sharedxattr_count, p, i;
+ unsigned int p, i;
struct erofs_buffer_head *bh;
char *buf;
int ret;
erofs_off_t off;
erofs_off_t shared_xattrs_size = 0;
- /* check if xattr or shared xattr is disabled */
- if (inlinexattr_tolerance < 0 || inlinexattr_tolerance >= INT_MAX)
- return 0;
-
- ret = erofs_count_all_xattrs_from_path(sbi, path);
- if (ret)
- return ret;
-
- sharedxattr_count = erofs_cleanxattrs(xamgr, inlinexattr_tolerance);
if (!sharedxattr_count)
return 0;
@@ -1071,6 +1062,43 @@ int erofs_load_shared_xattrs_from_path(struct erofs_sb_info *sbi, const char *pa
return ret;
}
+int erofs_load_shared_xattrs_from_path(struct erofs_sb_info *sbi, const char *path,
+ long inlinexattr_tolerance)
+{
+ int ret;
+
+ if (inlinexattr_tolerance < 0 || inlinexattr_tolerance >= INT_MAX)
+ return 0;
+ ret = erofs_count_all_xattrs_from_path(sbi, path);
+ if (ret)
+ return ret;
+ return erofs_write_shared_xattrs(sbi,
+ erofs_cleanxattrs(sbi->xamgr, inlinexattr_tolerance));
+}
+
+int erofs_build_shared_xattrs(struct erofs_sb_info *sbi,
+ long inlinexattr_tolerance)
+{
+ struct erofs_xattrmgr *xamgr = sbi->xamgr;
+ struct erofs_xattritem *item;
+ unsigned int i, count = 0;
+
+ if (inlinexattr_tolerance < 0 || inlinexattr_tolerance >= INT_MAX)
+ return 0;
+ for (i = 0; i < ARRAY_SIZE(xamgr->hash); ++i) {
+ list_for_each_entry(item, xamgr->hash + i, node) {
+ if (item->count > inlinexattr_tolerance) {
+ /* keep shared items alive after inode references drop */
+ ++item->count;
+ item->next_shared_xattr = xamgr->shared_xattrs;
+ xamgr->shared_xattrs = item;
+ ++count;
+ }
+ }
+ }
+ return erofs_write_shared_xattrs(sbi, count);
+}
+
char *erofs_export_xattr_ibody(struct erofs_inode *inode)
{
struct list_head *ixattrs = &inode->i_xattrs;
--
2.43.0
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [RFC 4/7] erofs-utils: mkfs: add manifest input mode
2026-11-04 4:07 [RFC 0/7] erofs-utils: add manifest input support Wenyuan Xu
` (2 preceding siblings ...)
2026-11-04 4:07 ` [RFC 3/7] erofs-utils: lib: add erofs_build_shared_xattrs() helper Wenyuan Xu
@ 2026-11-04 4:07 ` Wenyuan Xu
2026-11-04 4:07 ` [RFC 5/7] erofs-utils: mkfs: support two-field manifest records Wenyuan Xu
` (2 subsequent siblings)
6 siblings, 0 replies; 8+ messages in thread
From: Wenyuan Xu @ 2026-11-04 4:07 UTC (permalink / raw)
To: xiang, linux-erofs; +Cc: sereno.xu
Add --manifest=FILE to build an inode tree from explicit path, payload,
metadata and xattr records without scanning the source directory.
Regular file payloads are resolved relative to the source directory.
Support escaped strings, binary xattr values, hard links and special
files. Reject duplicate paths and xattr keys, require parents before
children, and report malformed records with their line and field.
Build shared xattrs from the loaded tree and mark directories containing
whiteouts before passing the tree to the importer.
Assisted-by: LLM
Signed-off-by: Wenyuan Xu <sereno.xu@deepseek.com>
---
include/erofs/manifest.h | 17 ++
lib/Makefile.am | 3 +-
lib/manifest.c | 443 +++++++++++++++++++++++++++++++++++++++
mkfs/main.c | 35 ++++
4 files changed, 497 insertions(+), 1 deletion(-)
create mode 100644 include/erofs/manifest.h
create mode 100644 lib/manifest.c
diff --git a/include/erofs/manifest.h b/include/erofs/manifest.h
new file mode 100644
index 0000000..dce87d3
--- /dev/null
+++ b/include/erofs/manifest.h
@@ -0,0 +1,17 @@
+/* SPDX-License-Identifier: GPL-2.0+ OR MIT */
+#ifndef __EROFS_MANIFEST_H
+#define __EROFS_MANIFEST_H
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+struct erofs_importer;
+int erofs_manifest_load(struct erofs_importer *im, const char *filename);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/lib/Makefile.am b/lib/Makefile.am
index 0a82915..eb44d67 100644
--- a/lib/Makefile.am
+++ b/lib/Makefile.am
@@ -20,6 +20,7 @@ noinst_HEADERS = $(top_srcdir)/include/erofs_fs.h \
$(top_srcdir)/include/erofs/xattr.h \
$(top_srcdir)/include/erofs/compress_hints.h \
$(top_srcdir)/include/erofs/importer.h \
+ $(top_srcdir)/include/erofs/manifest.h \
$(top_srcdir)/lib/liberofs_base64.h \
$(top_srcdir)/lib/liberofs_cache.h \
$(top_srcdir)/lib/liberofs_chunk.h \
@@ -44,7 +45,7 @@ liberofs_la_SOURCES = config.c io.c cache.c super.c inode.c xattr.c exclude.c \
compress_hints.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 manifest.c
liberofs_la_CFLAGS = -Wall ${libuuid_CFLAGS} -I$(top_srcdir)/include
liberofs_la_LIBADD = $(LIBEROFS_LIBS)
diff --git a/lib/manifest.c b/lib/manifest.c
new file mode 100644
index 0000000..bf8eb8c
--- /dev/null
+++ b/lib/manifest.c
@@ -0,0 +1,443 @@
+// SPDX-License-Identifier: GPL-2.0+ OR MIT
+#define _GNU_SOURCE
+#include <config.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <limits.h>
+#ifdef HAVE_SYS_SYSMACROS_H
+#include <sys/sysmacros.h>
+#endif
+#include "erofs/importer.h"
+#include "erofs/inode.h"
+#include "erofs/xattr.h"
+#include "erofs/print.h"
+#include "erofs/manifest.h"
+
+static int erofs_manifest_hex(unsigned char c)
+{
+ if (c >= '0' && c <= '9')
+ return c - '0';
+ if (c >= 'a' && c <= 'f')
+ return c - 'a' + 10;
+ if (c >= 'A' && c <= 'F')
+ return c - 'A' + 10;
+ return -1;
+}
+
+/* Decode in place, retaining the length for binary xattr values. */
+static int erofs_manifest_decode(char *s, bool binary, size_t *len)
+{
+ char *p = s, *q = s;
+ int a, b;
+
+ while (*p) {
+ unsigned char c = *p++;
+
+ if (c == '\\') {
+ /*
+ * A space terminates the token before decoding, so
+ * \x or \xX immediately followed by a space is invalid.
+ */
+ if (p[0] != 'x' || !p[1] || !p[2])
+ return -EINVAL;
+ a = erofs_manifest_hex(p[1]);
+ b = erofs_manifest_hex(p[2]);
+ if (a < 0 || b < 0)
+ return -EINVAL;
+ c = (a << 4) | b;
+ p += 3;
+ } else if (c <= ' ' || c == 127) {
+ return -EINVAL;
+ }
+ /* \x00 is invalid except in binary xattr values. */
+ if (!c && !binary)
+ return -EINVAL;
+ *q++ = c;
+ }
+ *len = q - s;
+ *q = '\0';
+ return 0;
+}
+
+static int erofs_manifest_number(const char *s, unsigned int base,
+ u64 max, u64 *v)
+{
+ u64 n = 0;
+
+ if (!*s)
+ return -EINVAL;
+ for (; *s; ++s) {
+ unsigned int d = (unsigned char)*s - '0';
+
+ if (d >= base || n > (max - d) / base)
+ return -EINVAL;
+ n = n * base + d;
+ }
+ *v = n;
+ return 0;
+}
+
+/*
+ * Validate and resolve each component once, without modifying the path.
+ * Return NULL for a missing final entry, or an error for a missing parent.
+ * The empty component under sentinel represents the root directory.
+ */
+static struct erofs_inode *erofs_manifest_lookup(struct erofs_inode *sentinel,
+ const char *path, struct erofs_inode **parent, char *name)
+{
+ struct erofs_inode *dir = sentinel;
+ const char *p = path, *component = "";
+ size_t len = 0;
+
+ if (*p != '/')
+ return ERR_PTR(-EINVAL);
+ while (*p == '/')
+ ++p;
+ for (;;) {
+ struct erofs_dentry *d;
+ struct erofs_inode *next = NULL;
+
+ list_for_each_entry(d, &dir->i_subdirs, d_child) {
+ if (d->namelen == len && !memcmp(d->name, component, len)) {
+ next = d->inode;
+ break;
+ }
+ }
+ if (!*p) {
+ if (parent)
+ *parent = dir;
+ if (name) {
+ memcpy(name, component, len);
+ name[len] = '\0';
+ }
+ return next;
+ }
+ if (!next)
+ return ERR_PTR(-ENOENT);
+ if (!S_ISDIR(next->i_mode))
+ return ERR_PTR(-ENOTDIR);
+ dir = next;
+ component = p;
+ while (*p && *p != '/')
+ ++p;
+ len = p - component;
+ if (len > EROFS_NAME_LEN ||
+ (len == 1 && component[0] == '.') ||
+ (len == 2 && component[0] == '.' && component[1] == '.'))
+ return ERR_PTR(-EINVAL);
+ while (*p == '/')
+ ++p;
+ }
+}
+
+static int erofs_manifest_xattrs(struct erofs_inode *inode, char *rest)
+{
+ char *token;
+ int ret;
+
+ while ((token = strsep(&rest, " "))) {
+ char *eq;
+ size_t len, size;
+ unsigned int index, prefixlen;
+
+ if (!*token)
+ continue;
+ eq = strchr(token, '=');
+ if (!eq || strchr(eq + 1, '='))
+ return -EINVAL;
+ *eq++ = '\0';
+ if (erofs_manifest_decode(token, false, &len) || !len || len > 255 ||
+ erofs_manifest_decode(eq, true, &size) || size > UINT16_MAX)
+ return -EINVAL;
+ if (erofs_inode_has_xattr(inode, token))
+ return -EEXIST;
+ if (!erofs_xattr_prefix_matches(token, &index, &prefixlen))
+ return -EOPNOTSUPP;
+ ret = erofs_vfs_setxattr(inode, token, eq, size);
+ if (ret)
+ return ret;
+ }
+ return 0;
+}
+
+enum erofs_manifest_field {
+ EROFS_MANIFEST_PATH,
+ EROFS_MANIFEST_PAYLOAD,
+ EROFS_MANIFEST_SIZE,
+ EROFS_MANIFEST_MODE,
+ EROFS_MANIFEST_UID,
+ EROFS_MANIFEST_GID,
+ EROFS_MANIFEST_RDEV,
+ EROFS_MANIFEST_MTIME,
+ EROFS_MANIFEST_FIELD_COUNT,
+};
+
+static int erofs_manifest_parse_record(struct erofs_importer *im,
+ struct erofs_inode *sentinel, char *line)
+{
+ const char *basedir = im->params->source;
+ struct erofs_inode *inode = NULL, *parent = NULL;
+ struct erofs_dentry *d;
+ char *f[EROFS_MANIFEST_FIELD_COUNT], *token, *dot;
+ char name[EROFS_NAME_LEN + 1];
+ bool hardlink, is_root;
+ u64 mode, size = 0, uid, gid, rdev = 0, sec, nsec;
+ size_t len;
+ int i, ret = -EINVAL;
+
+ for (i = 0; i < EROFS_MANIFEST_FIELD_COUNT; ++i) {
+ /*
+ * strsep() yields empty tokens for leading or repeated spaces.
+ * skip in this case.
+ */
+ do {
+ token = strsep(&line, " ");
+ } while (token && !*token);
+ if (!token)
+ return -EINVAL;
+ f[i] = token;
+ }
+
+ /* PATH */
+ if (erofs_manifest_decode(f[EROFS_MANIFEST_PATH], false, &len))
+ return -EINVAL;
+ inode = erofs_manifest_lookup(sentinel, f[EROFS_MANIFEST_PATH],
+ &parent, name);
+ if (IS_ERR(inode))
+ return PTR_ERR(inode);
+ if (inode)
+ return -EEXIST;
+ is_root = parent == sentinel;
+
+ /* MODE */
+ hardlink = f[EROFS_MANIFEST_MODE][0] == '@';
+ if (hardlink) {
+ if (is_root)
+ return -EINVAL;
+
+ /* PAYLOAD */
+ if (erofs_manifest_decode(f[EROFS_MANIFEST_PAYLOAD], false, &len))
+ return -EINVAL;
+
+ inode = erofs_manifest_lookup(sentinel, f[EROFS_MANIFEST_PAYLOAD],
+ NULL, NULL);
+ if (IS_ERR(inode))
+ return PTR_ERR(inode);
+ if (!inode || S_ISDIR(inode->i_mode))
+ return -EINVAL;
+ if (inode->i_nlink == UINT_MAX)
+ return -EOVERFLOW;
+
+ inode = erofs_igrab(inode);
+ ++inode->i_nlink;
+ goto link;
+ }
+
+ /* MODE */
+ if (erofs_manifest_number(f[EROFS_MANIFEST_MODE], 8, UINT16_MAX, &mode))
+ return -EINVAL;
+ if (is_root && !S_ISDIR(mode))
+ return -EINVAL;
+ switch (mode & S_IFMT) {
+ case S_IFREG:
+ case S_IFLNK:
+ /* SIZE */
+ if (erofs_manifest_number(f[EROFS_MANIFEST_SIZE], 10,
+ INT64_MAX, &size))
+ return -EINVAL;
+ break;
+ case S_IFCHR:
+ case S_IFBLK:
+ case S_IFIFO:
+ case S_IFSOCK:
+ /* RDEV */
+ if (erofs_manifest_number(f[EROFS_MANIFEST_RDEV], 10,
+ UINT64_MAX, &rdev) ||
+ (u64)(dev_t)rdev != rdev ||
+ major((dev_t)rdev) > 0xfff || minor((dev_t)rdev) > 0xfffff)
+ return -EINVAL;
+ break;
+ case S_IFDIR:
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ /* UID */
+ if (erofs_manifest_number(f[EROFS_MANIFEST_UID], 10, UINT32_MAX, &uid))
+ return -EINVAL;
+
+ /* GID */
+ if (erofs_manifest_number(f[EROFS_MANIFEST_GID], 10, UINT32_MAX, &gid))
+ return -EINVAL;
+
+ /* MTIME */
+ dot = strchr(f[EROFS_MANIFEST_MTIME], '.');
+ if (!dot)
+ return -EINVAL;
+ *dot++ = '\0';
+ if (erofs_manifest_number(f[EROFS_MANIFEST_MTIME], 10, INT64_MAX, &sec) ||
+ erofs_manifest_number(dot, 10, 999999999, &nsec))
+ return -EINVAL;
+
+ inode = erofs_new_inode(im->sbi);
+ if (IS_ERR(inode))
+ return PTR_ERR(inode);
+ inode->dev = sentinel->dev;
+ inode->i_mode = mode;
+ inode->i_uid = uid;
+ inode->i_gid = gid;
+ inode->i_size = size;
+ inode->i_mtime = sec;
+ inode->i_mtime_nsec = nsec;
+ inode->i_nlink = S_ISDIR(mode) ? 2 : 1;
+ inode->i_parent = parent;
+ if (S_ISCHR(mode) || S_ISBLK(mode) || S_ISFIFO(mode) || S_ISSOCK(mode))
+ inode->u.i_rdev = erofs_new_encode_dev(rdev);
+
+ /* PAYLOAD */
+ if (S_ISREG(mode)) {
+ struct stat st;
+ int fd;
+
+ if (erofs_manifest_decode(f[EROFS_MANIFEST_PAYLOAD], false, &len))
+ goto out;
+ if (f[EROFS_MANIFEST_PAYLOAD][0] != '/')
+ goto out;
+ if (asprintf(&inode->i_srcpath, "%s%s", basedir,
+ f[EROFS_MANIFEST_PAYLOAD]) < 0) {
+ ret = -ENOMEM;
+ goto out;
+ }
+ fd = open(inode->i_srcpath, O_RDONLY | O_NONBLOCK | O_BINARY);
+ if (fd < 0) {
+ ret = -errno;
+ goto out;
+ }
+ ret = fstat(fd, &st) ? -errno : 0;
+ close(fd);
+ if (ret)
+ goto out;
+ ret = -EINVAL;
+ if (!S_ISREG(st.st_mode))
+ goto out;
+ if (st.st_size != size)
+ goto out;
+ inode->datasource = EROFS_INODE_DATA_SOURCE_LOCALPATH;
+ } else {
+ /* Use the image path for progress reporting and diagnostics. */
+ if (asprintf(&inode->i_srcpath, "%s%s", basedir,
+ f[EROFS_MANIFEST_PATH]) < 0) {
+ ret = -ENOMEM;
+ goto out;
+ }
+
+ if (S_ISLNK(mode)) {
+ if (erofs_manifest_decode(f[EROFS_MANIFEST_PAYLOAD], false, &len))
+ goto out;
+ if (len != size)
+ goto out;
+ inode->i_link = strdup(f[EROFS_MANIFEST_PAYLOAD]);
+ if (!inode->i_link) {
+ ret = -ENOMEM;
+ goto out;
+ }
+ }
+ }
+
+ /* XATTR */
+ ret = erofs_manifest_xattrs(inode, line);
+ if (ret)
+ goto out;
+link:
+ /* PATH */
+ d = erofs_d_alloc(parent, name);
+ if (IS_ERR(d)) {
+ ret = PTR_ERR(d);
+ goto out;
+ }
+ d->inode = inode;
+ d->type = erofs_mode_to_ftype(inode->i_mode);
+ if (erofs_inode_is_whiteout(inode))
+ parent->whiteouts = true;
+ return 0;
+out:
+ erofs_iput(inode);
+ return ret;
+}
+
+/* The importer has not taken ownership of the tree on a parse error. */
+static void erofs_manifest_drop_children(struct erofs_inode *dir)
+{
+ struct erofs_dentry *d, *next;
+
+ list_for_each_entry_safe(d, next, &dir->i_subdirs, d_child) {
+ if (S_ISDIR(d->inode->i_mode))
+ erofs_manifest_drop_children(d->inode);
+ erofs_iput(d->inode);
+ list_del(&d->d_child);
+ free(d);
+ }
+}
+
+int erofs_manifest_load(struct erofs_importer *im, const char *filename)
+{
+ struct erofs_inode sentinel = { .i_mode = S_IFDIR };
+ struct erofs_dentry *d;
+ struct stat st;
+ FILE *fp = fopen(filename, "r");
+ char *line = NULL;
+ size_t capacity = 0;
+ unsigned long lineno = 0;
+ ssize_t len;
+ int ret = 0;
+
+ if (!fp) {
+ ret = -errno;
+ erofs_err("%s: cannot open manifest: %s", filename, strerror(errno));
+ return ret;
+ }
+ if (stat(im->params->source, &st)) {
+ ret = -errno;
+ erofs_err("%s: cannot stat base directory: %s",
+ im->params->source, erofs_strerror(ret));
+ fclose(fp);
+ return ret;
+ }
+ sentinel.dev = st.st_dev;
+ init_list_head(&sentinel.i_subdirs);
+
+ while ((len = getline(&line, &capacity, fp)) >= 0) {
+ ++lineno;
+ if (memchr(line, '\0', len)) {
+ ret = -EINVAL;
+ break;
+ }
+ if (len && line[len - 1] == '\n')
+ line[--len] = '\0';
+ ret = erofs_manifest_parse_record(im, &sentinel, line);
+ if (ret)
+ break;
+ }
+ if (!ret && ferror(fp))
+ ret = -EIO;
+ if (!ret && !lineno)
+ ret = -EINVAL;
+ if (ret) {
+ erofs_manifest_drop_children(&sentinel);
+ erofs_err("%s:%lu: %s", filename, lineno, erofs_strerror(ret));
+ } else {
+ /* Transfer the root out of sentinel before it leaves scope. */
+ d = list_first_entry(&sentinel.i_subdirs, struct erofs_dentry, d_child);
+ im->root = d->inode;
+ im->root->i_parent = im->root;
+ list_del(&d->d_child);
+ free(d);
+ }
+ free(line);
+ fclose(fp);
+ return ret;
+}
diff --git a/mkfs/main.c b/mkfs/main.c
index d8706d7..6686c5a 100644
--- a/mkfs/main.c
+++ b/mkfs/main.c
@@ -24,6 +24,7 @@
#include "erofs/exclude.h"
#include "erofs/block_list.h"
#include "erofs/compress_hints.h"
+#include "erofs/manifest.h"
#include "../lib/compressor.h"
#include "../lib/liberofs_chunk.h"
#include "../lib/liberofs_gzran.h"
@@ -105,6 +106,7 @@ static struct option long_options[] = {
{"MZ", optional_argument, NULL, 537},
{"xattr-prefix", required_argument, NULL, 538},
{"xattr-inode-digest", optional_argument, NULL, 539},
+ {"manifest", required_argument, NULL, 540},
{0, 0, 0, 0},
};
@@ -212,6 +214,7 @@ static void usage(int argc, char **argv)
" --hard-dereference dereference hardlinks, add links as separate inodes\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"
+ " --manifest=FILE build an uncompressed image from a manifest and basedir\n"
" --mount-point=X X=prefix of target fs path (default: /)\n"
" --preserve-mtime keep per-file modification time strictly\n"
" --offset=# skip # bytes at the beginning of IMAGE.\n"
@@ -324,8 +327,10 @@ static enum {
EROFS_MKFS_SOURCE_S3,
EROFS_MKFS_SOURCE_OCI,
EROFS_MKFS_SOURCE_REBUILD,
+ EROFS_MKFS_SOURCE_MANIFEST,
} source_mode;
+static const char *manifest_file;
static unsigned int rebuild_src_count;
static LIST_HEAD(rebuild_src_list);
static u8 fixeduuid[16];
@@ -971,6 +976,7 @@ static int mkfs_parse_sources(int argc, char *argv[], int optind)
char *s;
switch (source_mode) {
+ case EROFS_MKFS_SOURCE_MANIFEST:
case EROFS_MKFS_SOURCE_LOCALDIR:
err = lstat((s = argv[optind++]), &st);
if (err) {
@@ -986,6 +992,8 @@ static int mkfs_parse_sources(int argc, char *argv[], int optind)
return -ENOENT;
}
erofs_set_fs_root(cfg.c_src_path);
+ } else if (source_mode == EROFS_MKFS_SOURCE_MANIFEST) {
+ return -ENOTDIR;
} else {
cfg.c_src_path = strdup(s);
if (!cfg.c_src_path)
@@ -1497,6 +1505,14 @@ static int mkfs_parse_options_cfg(struct erofs_importer_params *params,
return err;
}
break;
+ case 540:
+ if (manifest_file) {
+ erofs_err("duplicate manifest file");
+ return -EINVAL;
+ }
+ manifest_file = optarg;
+ source_mode = EROFS_MKFS_SOURCE_MANIFEST;
+ break;
case 'V':
version();
exit(0);
@@ -1970,6 +1986,14 @@ int main(int argc, char **argv)
err = PTR_ERR(root);
goto exit;
}
+ } else if (source_mode == EROFS_MKFS_SOURCE_MANIFEST) {
+ err = erofs_xattr_flush_name_prefixes(&importer,
+ mkfs_plain_xattr_pfx);
+ if (err) {
+ erofs_err("failed to flush long xattr prefixes: %s",
+ erofs_strerror(err));
+ goto exit;
+ }
} else {
err = erofs_xattr_flush_name_prefixes(&importer,
mkfs_plain_xattr_pfx);
@@ -1993,6 +2017,17 @@ int main(int argc, char **argv)
err = lstat(cfg.c_src_path, &st);
if (!err)
err = erofs_fill_inode(&importer, importer.root, &st, cfg.c_src_path);
+ } else if (source_mode == EROFS_MKFS_SOURCE_MANIFEST) {
+ err = erofs_manifest_load(&importer, manifest_file);
+ if (err)
+ goto exit;
+ root = importer.root;
+
+ err = erofs_build_shared_xattrs(&g_sbi, mkfscfg.inlinexattr_tolerance);
+ if (err) {
+ erofs_err("failed to build shared xattrs: %s", erofs_strerror(err));
+ goto exit;
+ }
} else if (source_mode == EROFS_MKFS_SOURCE_TAR) {
while (!(err = tarerofs_parse_tar(&importer, &erofstar)))
;
--
2.43.0
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [RFC 5/7] erofs-utils: mkfs: support two-field manifest records
2026-11-04 4:07 [RFC 0/7] erofs-utils: add manifest input support Wenyuan Xu
` (3 preceding siblings ...)
2026-11-04 4:07 ` [RFC 4/7] erofs-utils: mkfs: add manifest input mode Wenyuan Xu
@ 2026-11-04 4:07 ` Wenyuan Xu
2026-11-04 4:07 ` [RFC 6/7] erofs-utils: man: document manifest input formats Wenyuan Xu
2026-11-04 4:07 ` [RFC 7/7] erofs-utils: tests: cover manifest input Wenyuan Xu
6 siblings, 0 replies; 8+ messages in thread
From: Wenyuan Xu @ 2026-11-04 4:07 UTC (permalink / raw)
To: xiang, linux-erofs; +Cc: sereno.xu
Accept PATH PAYLOAD records alongside complete manifest records. Import
metadata and xattrs from the backing file using local import policies,
without recursively scanning directories. Read symbolic links without
following their targets.
Preserve source hard links unless hard dereferencing is requested, and
count only entries imported into the image. Allow explicit hard links
to refer to entries imported from backing files.
Assisted-by: LLM
Signed-off-by: Wenyuan Xu <sereno.xu@deepseek.com>
---
lib/manifest.c | 88 ++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 86 insertions(+), 2 deletions(-)
diff --git a/lib/manifest.c b/lib/manifest.c
index bf8eb8c..244c1a0 100644
--- a/lib/manifest.c
+++ b/lib/manifest.c
@@ -175,6 +175,75 @@ enum erofs_manifest_field {
EROFS_MANIFEST_FIELD_COUNT,
};
+static struct erofs_inode *erofs_manifest_load_local(struct erofs_importer *im,
+ struct erofs_inode *parent, char *payload)
+{
+ struct erofs_inode *inode;
+ struct stat st;
+ char *path;
+ size_t len;
+ int ret;
+
+ /* PAYLOAD */
+ if (erofs_manifest_decode(payload, false, &len) || *payload != '/')
+ return ERR_PTR(-EINVAL);
+ if (asprintf(&path, "%s%s", im->params->source, payload) < 0)
+ return ERR_PTR(-ENOMEM);
+
+ ret = lstat(path, &st) ? -errno : 0;
+ if (ret)
+ goto out_path;
+ if (!S_ISDIR(st.st_mode) && !im->params->hard_dereference) {
+ inode = erofs_iget(st.st_dev, st.st_ino);
+ if (inode) {
+ free(path);
+ if (inode->i_nlink == UINT_MAX) {
+ erofs_iput(inode);
+ return ERR_PTR(-EOVERFLOW);
+ }
+ ++inode->i_nlink;
+ return inode;
+ }
+ }
+
+ inode = erofs_new_inode(im->sbi);
+ if (IS_ERR(inode)) {
+ ret = PTR_ERR(inode);
+ goto out_path;
+ }
+ ret = erofs_fill_inode(im, inode, &st, path);
+ if (ret)
+ goto out_inode;
+ inode->i_parent = parent;
+ inode->datasource = EROFS_INODE_DATA_SOURCE_LOCALPATH;
+ if (S_ISLNK(inode->i_mode)) {
+ inode->i_link = malloc(st.st_size);
+ if (!inode->i_link) {
+ ret = -ENOMEM;
+ goto out_inode;
+ }
+ if (readlink(path, inode->i_link, st.st_size) < 0) {
+ ret = -errno;
+ goto out_inode;
+ }
+ }
+
+ if (!im->params->no_xattrs) {
+ /* XATTR */
+ ret = erofs_scan_file_xattrs(inode);
+ if (ret)
+ goto out_inode;
+ }
+
+ free(path);
+ return inode;
+out_inode:
+ erofs_iput(inode);
+out_path:
+ free(path);
+ return ERR_PTR(ret);
+}
+
static int erofs_manifest_parse_record(struct erofs_importer *im,
struct erofs_inode *sentinel, char *line)
{
@@ -183,7 +252,7 @@ static int erofs_manifest_parse_record(struct erofs_importer *im,
struct erofs_dentry *d;
char *f[EROFS_MANIFEST_FIELD_COUNT], *token, *dot;
char name[EROFS_NAME_LEN + 1];
- bool hardlink, is_root;
+ bool hardlink, is_root, short_record = false;
u64 mode, size = 0, uid, gid, rdev = 0, sec, nsec;
size_t len;
int i, ret = -EINVAL;
@@ -196,8 +265,13 @@ static int erofs_manifest_parse_record(struct erofs_importer *im,
do {
token = strsep(&line, " ");
} while (token && !*token);
- if (!token)
+ if (!token) {
+ if (i == EROFS_MANIFEST_PAYLOAD + 1) {
+ short_record = true;
+ break;
+ }
return -EINVAL;
+ }
f[i] = token;
}
@@ -212,6 +286,16 @@ static int erofs_manifest_parse_record(struct erofs_importer *im,
return -EEXIST;
is_root = parent == sentinel;
+ if (short_record) {
+ inode = erofs_manifest_load_local(im, parent,
+ f[EROFS_MANIFEST_PAYLOAD]);
+ if (IS_ERR(inode))
+ return PTR_ERR(inode);
+ if (is_root && !S_ISDIR(inode->i_mode))
+ goto out;
+ goto link;
+ }
+
/* MODE */
hardlink = f[EROFS_MANIFEST_MODE][0] == '@';
if (hardlink) {
--
2.43.0
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [RFC 6/7] erofs-utils: man: document manifest input formats
2026-11-04 4:07 [RFC 0/7] erofs-utils: add manifest input support Wenyuan Xu
` (4 preceding siblings ...)
2026-11-04 4:07 ` [RFC 5/7] erofs-utils: mkfs: support two-field manifest records Wenyuan Xu
@ 2026-11-04 4:07 ` Wenyuan Xu
2026-11-04 4:07 ` [RFC 7/7] erofs-utils: tests: cover manifest input Wenyuan Xu
6 siblings, 0 replies; 8+ messages in thread
From: Wenyuan Xu @ 2026-11-04 4:07 UTC (permalink / raw)
To: xiang, linux-erofs; +Cc: sereno.xu
Document complete and two-field manifest records, path handling,
metadata policies, hard links and shared xattrs.
Assisted-by: LLM
Signed-off-by: Wenyuan Xu <sereno.xu@deepseek.com>
---
man/mkfs.erofs.1 | 114 +++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 114 insertions(+)
diff --git a/man/mkfs.erofs.1 b/man/mkfs.erofs.1
index 65ec807..a694676 100644
--- a/man/mkfs.erofs.1
+++ b/man/mkfs.erofs.1
@@ -32,6 +32,10 @@ other EROFS image(s), see \fIREBUILD MODE\fR below
.RE
.SH OPTIONS
.TP
+.BI "\-\-manifest=" FILE
+Build an image from a manifest and a base directory.
+See \fIMANIFEST INPUT MODE\fR below.
+.TP
.BI "\-b " block-size
Set the fundamental block size of the filesystem in bytes. In other words,
specify the smallest amount of data that can be accessed at a time. The
@@ -224,6 +228,9 @@ The current source-specific support for \fIMODE\fR:
.I Local directory source
Only \fBdata\fR is supported. \fBrvsp\fR and \fB0\fR will be ignored.
.TP
+.I Manifest source (\fB\-\-manifest\fR)
+\fBdata\fR is supported with both complete and short records.
+.TP
.I Tar source (\fB\-\-tar\fR)
\fBdata\fR and \fBrvsp\fR are supported. \fB0\fR will be ignored.
Note that \fBrvsp\fR takes precedence over \fB--tar=i\fR or \fB--tar=headerball\fR.
@@ -331,6 +338,11 @@ The current source-specific support for \fIMODE\fR:
.I Local directory source
Only \fBdata\fR is supported. \fBrvsp\fR and \fB0\fR will be ignored.
.TP
+.I Manifest source (\fB\-\-manifest\fR)
+\fBdata\fR is supported with both complete and short records.
+See \fIMANIFEST INPUT MODE\fR below for incremental usage and parent
+directory requirements.
+.TP
.I Tar source (\fB\-\-tar\fR)
\fBdata\fR and \fBrvsp\fR are supported. \fB0\fR will be ignored.
Note that \fBrvsp\fR takes precedence over \fB--tar=i\fR or \fB--tar=headerball\fR.
@@ -497,6 +509,108 @@ Display help string and exit.
.TP
\fB\-V\fR, \fB\-\-version\fR
Print the version number and exit.
+.SH MANIFEST INPUT MODE
+With \fB--manifest=FILE\fR, \fISOURCE\fR is a base directory supplying backing
+files. The manifest defines the image tree for a clean build, or the update
+tree for an incremental build; source directories are not recursively scanned.
+Two record formats may be mixed in the same file:
+.PP
+.nf
+PATH PAYLOAD
+PATH PAYLOAD SIZE MODE UID GID RDEV MTIME [KEY=VALUE ...]
+.fi
+.PP
+Exactly two fields select the short format. Three through seven fields are
+invalid. Eight or more fields select the complete format.
+.PP
+Both formats support regular file compression with \fB-z\fR, using the same
+compression options as local-directory input. Without \fB-z\fR, regular file
+data is stored uncompressed.
+.PP
+In the short format, PAYLOAD is a backing path beginning with /, appended
+directly to SOURCE after decoding. Repeated slashes and . or .. components
+are resolved by the host filesystem. A directory record imports only that
+directory, not its children.
+.PP
+The field descriptions below apply to complete records unless noted.
+.PP
+Records are separated by newlines; the last newline is optional. Blank records
+and comments are not supported. Unused fields require a nonempty placeholder
+('-' is preferred as a placeholder) but are not parsed.
+.PP
+In both formats, PATH names an image entry and must begin with /. The first record
+must define the root directory. Every parent directory must precede its children,
+and duplicate paths are errors. Repeated and trailing slashes in PATH are skipped.
+Individual . and .. components are forbidden.
+.TP
+.B Regular files
+PAYLOAD begins with / and is interpreted relative to the base directory, not
+the host root. The decoded PAYLOAD is appended directly to the base directory;
+repeated slashes and . or .. components are resolved by the host filesystem,
+as are symlinks. The source must be a regular file whose size
+exactly equals SIZE, including for empty files. RDEV is unused.
+.TP
+.B Directories
+SIZE, PAYLOAD and RDEV are unused. A record describes only the directory itself;
+its size is computed from the generated directory entries.
+.TP
+.B Symbolic links
+PAYLOAD is the link content; repeated slashes are preserved.
+SIZE must equal its decoded byte length. RDEV is unused.
+.TP
+.B Hard links
+A MODE beginning with @ denotes a hard link. PATH creates the new entry and
+PAYLOAD names an already defined non-directory entry in the image. Only the
+leading @ in MODE is examined; all other fields and xattrs are ignored.
+Hard links to earlier hard links are supported.
+Repeated and trailing slashes in PAYLOAD are skipped.
+An explicit @ hard link may refer to a previously defined short record.
+.TP
+.B Character/block devices, sockets and FIFOs
+SIZE and PAYLOAD are unused. RDEV is the decimal value of stat.st_rdev;
+use zero for sockets and FIFOs.
+.PP
+MODE is an octal stat.st_mode, including file type bits; arbitrary leading zeros
+are accepted. SIZE, UID, GID and RDEV are unsigned decimal integers within the
+supported ranges. MTIME is nonnegative seconds.nanoseconds since the Unix
+epoch. It is not a floating-point value: 1.1 means one second plus one nanosecond.
+Nanoseconds must be between 0 and 999999999.
+.PP
+PATH, used PAYLOAD fields and xattrs encode special bytes as \fB\exXY\fR,
+where XY is a two-digit hexadecimal byte, with either letter case accepted.
+For example, a space is \fB\ex20\fR and a backslash is \fB\ex5c\fR.
+An equals sign within an xattr key or value must be encoded as \fB\ex3d\fR;
+it may appear literally in PATH and PAYLOAD.
+NUL is forbidden in paths, symlink contents and xattr keys, but is permitted in
+xattr values. Empty xattr values are supported. Repeated keys within one record
+are rejected after decoding the keys. Keys must use a supported EROFS xattr
+namespace and be at most 255 bytes; values must fit the on-disk 16-bit length field.
+Identical keys and values used by more than the \fB-x\fR threshold of distinct
+inodes are stored as shared xattrs.
+.PP
+For example, using data from \fIbasedir/source\fR:
+.PP
+.nf
+/ - - 40755 0 0 - 1.0
+/file /source 3 100644 1000 1000 - 1.1 user.note=hello\ex20world
+/link /file - @ - - - -
+/symlink file 4 120777 1000 1000 - 1.0
+.fi
+.PP
+.nf
+mkfs.erofs --manifest=manifest.txt out.erofs basedir
+.fi
+.PP
+Both formats support \fB--incremental=data\fR (or \fB--incremental\fR) with
+an existing image as DESTINATION and a backing directory as SOURCE:
+.PP
+.nf
+mkfs.erofs --incremental=data --manifest=updates.txt base.erofs updates/
+.fi
+.PP
+The manifest must still define the root directory first and each parent before
+its children, even when those directories already exist in the image.
+.PP
.SH NOTES
.TP
.B REBUILD MODE
--
2.43.0
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [RFC 7/7] erofs-utils: tests: cover manifest input
2026-11-04 4:07 [RFC 0/7] erofs-utils: add manifest input support Wenyuan Xu
` (5 preceding siblings ...)
2026-11-04 4:07 ` [RFC 6/7] erofs-utils: man: document manifest input formats Wenyuan Xu
@ 2026-11-04 4:07 ` Wenyuan Xu
6 siblings, 0 replies; 8+ messages in thread
From: Wenyuan Xu @ 2026-11-04 4:07 UTC (permalink / raw)
To: xiang, linux-erofs; +Cc: sereno.xu
Add test 033 for complete and two-field records, metadata, path
validation, hard links, symbolic links and whiteouts.
Check binary xattrs, name prefixes, shared storage and local import
options using the existing dump and fsck tools.
Assisted-by: LLM
Signed-off-by: Wenyuan Xu <sereno.xu@deepseek.com>
---
tests/Makefile.am | 3 +
tests/erofs/033 | 637 ++++++++++++++++++++++++++++++++++++++++++++
tests/erofs/033.out | 2 +
3 files changed, 642 insertions(+)
create mode 100755 tests/erofs/033
create mode 100644 tests/erofs/033.out
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 363b93d..5bb96ed 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -135,6 +135,9 @@ TESTS += erofs/031
# 032 - check that the xattr layout does not depend on listxattr(2) order
TESTS += erofs/032
+# 033 - manifest rebuild input and validation
+TESTS += erofs/033
+
# NEW TEST CASE HERE
# TESTS += erofs/999
diff --git a/tests/erofs/033 b/tests/erofs/033
new file mode 100755
index 0000000..5f427bd
--- /dev/null
+++ b/tests/erofs/033
@@ -0,0 +1,637 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0+
+# Manifest rebuild: metadata, binary escapes, hardlinks and rejected inputs.
+seq=`basename "$0"`
+seqres=$RESULT_DIR/$(echo "$0" | awk '{print $((NF-1))"/"$NF}' FS="/")
+. "${srcdir}/common/rc"
+cleanup() { :; }
+_require_xattr
+command -v getfattr >/dev/null 2>&1 || _notrun "getfattr isn't installed"
+export TZ=UTC
+export LC_ALL=C
+echo "QA output created by $seq"
+
+base=$tmp/base
+manifest=$tmp/manifest
+image=$tmp/image
+mkdir -p "$base/sub"
+printf abc > "$base/data =file"
+printf hidden > "$base/sub/unlisted"
+printf outside > "$tmp/outside"
+dd if=/dev/zero of="$base/large" bs=4096 count=20 >> "$seqres.full" 2>&1
+: > "$base/empty"
+if command -v setfattr >/dev/null 2>&1; then
+ setfattr -n user.host -v hidden "$base/data =file" 2>/dev/null || :
+fi
+cat > "$manifest" <<'MANIFEST'
+ / - - 040755 123 456 - 1.1 user.root=root
+//dir/// - - 0040750 123 456 - 2.999999999
+/sub - - 40755 123 456 - 4.0
+///dir//file/// /data\x20=file 3 00100644 123 456 - 1.1 user.text=new user.bin=\x00\xff\x3d\x5c\x20\x0a user.empty= user.a\x3db=v\x3d1
+/hard/// //dir///file/// - @unparsed - - - - -
+/chain /hard/ - @ - - - -
+/symlink/// ../dir//file 12 120777 123 456 - 3.000000007
+/char - - 20600 123 456 259 4.0
+/block - - 60600 123 456 2048 4.0
+/fifo - - 10600 123 456 0 4.0
+/socket - - 140600 123 456 0 4.0
+/large /large 81920 100600 123 456 - 4.0
+/empty /empty 0 100644 123 456 - 4.0
+/payload-slashes /\x2fsub//../data\x20=file 3 100644 123 456 - 4.0
+/payload-dot /./data\x20=file 3 100644 123 456 - 4.0
+/payload-parent /../outside 7 100644 123 456 - 4.0
+/space\x20\x5c\xff= /data\x20=file 3 100644 123 456 - 4.0
+MANIFEST
+# Accept a final record without a newline and a single directory trailing slash.
+printf '/last/ - - 40755 123 456 - 4.0' >> "$manifest"
+SOURCE_DATE_EPOCH=2000000000 "$MKFS_EROFS_PROG" --manifest="$manifest" \
+ "$image" "$base" >> "$seqres.full" 2>&1 || _fail "manifest build failed"
+"$FSCK_EROFS_PROG" "$image" >> "$seqres.full" 2>&1 || _fail "fsck failed"
+
+dump_inode()
+{
+ "$DUMP_EROFS_PROG" --path="$1" "$image" > "$tmp/dump" || return 1
+ # dump.erofs may report a lookup failure without a nonzero exit status.
+ grep -q '^NID: ' "$tmp/dump" || return 1
+ cat "$tmp/dump"
+}
+expect_stat()
+{
+ dump_inode "$1" > "$tmp/inode" || _fail "lookup $1 failed"
+ path=$1
+ set -- $2
+ mode=$1 uid=$2 gid=$3 timestamp=$4
+ access=$(printf '%04o' "$((0$mode & 0777))")
+ case "$mode" in
+ 100*) type='regular file' ;;
+ 120*) type='symlink file' ;;
+ 40*) type='directory' ;;
+ 20*) type='char dev' ;;
+ 60*) type='block dev' ;;
+ 10*) type='FIFO file' ;;
+ 140*) type='SOCK file' ;;
+ esac
+ grep -q " $type\$" "$tmp/inode" || _fail "unexpected type for $path"
+ grep -q "Uid: $uid Gid: $gid Access: $access/" "$tmp/inode" ||
+ _fail "unexpected ownership or permissions for $path"
+ stamp=$(date -u -d "@${timestamp%.*}" '+%Y-%m-%d %H:%M:%S')
+ nsec=$(printf '%09d' "${timestamp#*.}")
+ grep -Fx "Timestamp: $stamp.$nsec" "$tmp/inode" >/dev/null ||
+ _fail "unexpected timestamp for $path"
+ if [ "$#" -eq 6 ]; then
+ grep -q "^Size: $5 " "$tmp/inode" || _fail "unexpected size for $path"
+ grep -q " Links: $6 " "$tmp/inode" || _fail "unexpected links for $path"
+ fi
+}
+expect_stat / '40755 123 456 1.1'
+expect_stat /dir '40750 123 456 2.999999999'
+# Check the entry containing a space, literal backslash and byte 255.
+escaped_path=$(printf '/space \\%b=' '\377')
+dump_inode "$escaped_path" >> "$seqres.full" || _fail 'escaped path missing'
+expect_stat /dir/file '100644 123 456 1.1 3 3'
+expect_stat /symlink '120777 123 456 3.7 12 1'
+expect_stat /large '100600 123 456 4.0 81920 1'
+expect_stat /empty '100644 123 456 4.0 0 1'
+expect_stat /char '20600 123 456 4.0'
+expect_stat /block '60600 123 456 4.0'
+expect_stat /fifo '10600 123 456 4.0'
+expect_stat /socket '140600 123 456 4.0'
+expect_stat /last '40755 123 456 4.0'
+nid=$(dump_inode /dir/file | sed -n 's/^NID: \([0-9]*\).*/\1/p')
+[ -n "$nid" ] || _fail 'missing inode number'
+for path in /hard /chain; do
+ [ "$(dump_inode "$path" | sed -n 's/^NID: \([0-9]*\).*/\1/p')" = "$nid" ] ||
+ _fail 'hardlink inode differs'
+done
+dump_inode /sub/unlisted >> "$seqres.full" 2>&1 && _fail 'unlisted file imported'
+"$FSCK_EROFS_PROG" --extract="$tmp/extract" --no-preserve-owner --xattrs \
+ "$image" >> "$seqres.full" 2>&1 || _fail 'extraction failed'
+expect_xattr()
+{
+ getfattr --only-values -n "$1" "$tmp/extract/dir/file" > "$tmp/value" \
+ 2>> "$seqres.full" || _fail "missing xattr $1"
+ actual=$(od -An -tx1 -v "$tmp/value" | tr -d ' \n')
+ [ "$actual" = "$2" ] || _fail "unexpected value for $1"
+}
+expect_xattr user.text 6e6577
+expect_xattr user.bin 00ff3d5c200a
+expect_xattr 'user.a=b' 763d31
+expect_xattr user.empty ''
+getfattr -n user.host "$tmp/extract/dir/file" >> "$seqres.full" 2>&1 &&
+ _fail 'source xattr imported'
+# Unprivileged extraction may skip device nodes.
+if [ -c "$tmp/extract/char" ]; then
+ [ "$(stat -c '%t:%T' "$tmp/extract/char")" = '1:3' ] || _fail 'char device number'
+fi
+if [ -b "$tmp/extract/block" ]; then
+ [ "$(stat -c '%t:%T' "$tmp/extract/block")" = '8:0' ] || _fail 'block device number'
+fi
+cmp "$base/data =file" "$tmp/extract/dir/file" || _fail 'file contents differ'
+cmp "$base/large" "$tmp/extract/large" || _fail 'large file contents differ'
+for path in payload-slashes payload-dot; do
+ cmp "$base/data =file" "$tmp/extract/$path" || _fail "payload resolution failed: $path"
+done
+cmp "$tmp/outside" "$tmp/extract/payload-parent" || _fail 'parent payload resolution failed'
+[ "$(readlink "$tmp/extract/symlink")" = ../dir//file ] || _fail 'symlink contents differ'
+
+bad_record()
+{
+ printf '/ - - 40755 0 0 - 1.1\n%s\n' "$1" > "$tmp/bad"
+ "$MKFS_EROFS_PROG" --manifest="$tmp/bad" "$tmp/badimage" "$base" \
+ >> "$seqres.full" 2>&1
+ rc=$?
+ [ "$rc" -eq 0 ] && _fail "accepted invalid record: $1"
+ [ "$rc" -ge 128 ] && _fail "crashed on invalid record: $1"
+ grep -F "$tmp/bad:2:" "$seqres.full" >/dev/null || _fail 'missing line diagnostic'
+}
+bad_record '/ - - 40755 0 0 - 1.1'
+bad_record '//// - - 40755 0 0 - 1.1'
+bad_record '/bad//./file /empty 0 100644 0 0 - 1.1'
+bad_record '/missing/file /empty 0 100644 0 0 - 1.1'
+bad_record '/hard /later - @ - - - -'
+bad_record '/hard / - @ - - - -'
+bad_record '/bad /empty 1 100644 0 0 - 1.1'
+bad_record '/bad /large 0 100644 0 0 - 1.1'
+bad_record '/bad /sub 0 100644 0 0 - 1.1'
+bad_record '/bad abc 2 120777 0 0 - 1.1'
+bad_record '/bad//file /empty 0 100644 0 0 - 1.1'
+bad_record '/bad /empty// 0 100644 0 0 - 1.1'
+bad_record '/bad //target - @ - - - -'
+bad_record '/bad\x2f/file /empty 0 100644 0 0 - 1.1'
+bad_record '/bad/../file /empty 0 100644 0 0 - 1.1'
+bad_record '/bad/\x2e/file /empty 0 100644 0 0 - 1.1'
+bad_record '/bad\x00 /empty 0 100644 0 0 - 1.1'
+bad_record '/bad\xGG /empty 0 100644 0 0 - 1.1'
+bad_record '/bad /empty 0 100888 0 0 - 1.1'
+bad_record '/bad /empty 0 100644 4294967296 0 - 1.1'
+bad_record '/bad /empty 18446744073709551616 100644 0 0 - 1.1'
+bad_record '/bad /empty 0 100644 0 0 - 1.1000000000'
+bad_record '/bad /empty 0 100644 0 0 - 1.1 user.key=a=b'
+bad_record '/bad /empty 0 100644 0 0 - 1.1 =value'
+bad_record '/bad /empty 0 100644 0 0 - 1.1 user.dup=old user.dup=new'
+bad_record '/bad /empty 0 100644 0 0 - 1.1 user.dup=same user.dup=same'
+bad_record '/bad /empty 0 100644 0 0 - 1.1 user.dup=old user.\x64up=new'
+bad_record '/bad /empty 0 100644 0 0 - 1.1 user.key=\x0'
+bad_record '/bad /empty 0 100644 0 0 - 1.1 user.key=\x'
+bad_record '/bad abc\x00 4 120777 0 0 - 1.1'
+bad_record '/bad /empty\x00 0 100644 0 0 - 1.1'
+bad_record 'relative /empty 0 100644 0 0 - 1.1'
+bad_record '/bad relative 0 100644 0 0 - 1.1'
+bad_record '/bad /empty 0 100644 0 0 - 1.1 user.key\x00=v'
+bad_record '/bad /empty 0 100644 0 0 x'
+bad_record ''
+: > "$tmp/bad"
+"$MKFS_EROFS_PROG" --manifest="$tmp/bad" "$tmp/badimage" "$base" \
+ >> "$seqres.full" 2>&1 && _fail 'accepted empty manifest'
+printf '/file /empty 0 100644 0 0 - 1.1\n' > "$tmp/bad"
+"$MKFS_EROFS_PROG" --manifest="$tmp/bad" "$tmp/badimage" "$base" \
+ >> "$seqres.full" 2>&1 && _fail 'accepted missing root'
+# Root creation uses the same record path as every other inode.
+for record in '/ /empty 0 100644 0 0 - 1.1' \
+ '/ target 6 120777 0 0 - 1.1' \
+ '/ /target - @ - - - -' \
+ '/ - - 40755 0 0 - 1.1 user.bad=\x'; do
+ printf '%s\n' "$record" > "$tmp/bad"
+ "$MKFS_EROFS_PROG" --manifest="$tmp/bad" "$tmp/badimage" "$base" \
+ >> "$seqres.full" 2>&1
+ rc=$?
+ [ "$rc" -eq 0 ] && _fail "accepted invalid root: $record"
+ [ "$rc" -ge 128 ] && _fail "crashed on invalid root: $record"
+done
+# An error after a hardlink must clean up the shared inode safely.
+printf '/ - - 40755 0 0 - 1.1\n/file /empty 0 100644 0 0 - 1.1\n/hard /file - @ - - - -\n/ - - 40755 0 0 - 1.1\n' > "$tmp/bad"
+"$MKFS_EROFS_PROG" --manifest="$tmp/bad" "$tmp/badimage" "$base" \
+ >> "$seqres.full" 2>&1
+rc=$?
+[ "$rc" -eq 0 ] && _fail 'accepted duplicate root after hardlink'
+[ "$rc" -ge 128 ] && _fail 'crashed cleaning up duplicate root'
+
+# Build time does not replace manifest metadata; ignore-mtime permits compact inodes.
+"$MKFS_EROFS_PROG" -T1 --all-root --force-uid=1 --force-gid=1 \
+ --uid-offset=1 --gid-offset=1 --ignore-mtime --ovlfs-strip=0 \
+ -x-1 -x2 --manifest="$manifest" "$image" "$base" \
+ >> "$seqres.full" 2>&1 || _fail 'rejected permitted options'
+expect_stat /dir/file '100644 123 456 1.0 3 3'
+
+# Time option behavior must not depend on where --manifest appears.
+for position in before after; do
+ for options in '-T10' '-T10 --all-time' '-T10 --mkfs-time' \
+ '-T10 --mkfs-time --preserve-mtime' \
+ '-T10 --mkfs-time --ignore-mtime' \
+ '-T10 --mkfs-time -E force-inode-compact' \
+ '-T10 --mkfs-time -E force-inode-extended'; do
+ case "$position" in
+ before) set -- --manifest="$manifest" $options ;;
+ after) set -- $options --manifest="$manifest" ;;
+ esac
+ "$MKFS_EROFS_PROG" "$@" "$image" "$base" >> "$seqres.full" 2>&1 ||
+ _fail "time options rejected: $position $options"
+ case "$options" in
+ *ignore-mtime*|*force-inode-compact*) stamp=10.0; isize=32 ;;
+ *) stamp=1.1; isize=64 ;;
+ esac
+ expect_stat /dir/file "100644 123 456 $stamp 3 3"
+ grep -q "^Inode size: $isize " "$tmp/inode" ||
+ _fail "unexpected inode format: $options"
+ "$FSCK_EROFS_PROG" "$image" >> "$seqres.full" 2>&1 ||
+ _fail "fsck failed: $options"
+ done
+done
+
+# Each directory containing a whiteout, including a hardlink, must be marked.
+cat > "$tmp/whiteouts" <<'MANIFEST'
+/ - - 40755 0 0 - 1.1
+/a - - 40755 0 0 - 1.1
+/b - - 40755 0 0 - 1.1
+/a/deleted - - 20000 0 0 0 1.1
+/b/deleted /a/deleted - @ - - - -
+/b/device - - 20600 0 0 259 1.1
+MANIFEST
+for strip in 0 1; do
+ "$MKFS_EROFS_PROG" --manifest="$tmp/whiteouts" --ovlfs-strip="$strip" \
+ "$image" "$base" >> "$seqres.full" 2>&1 || _fail 'whiteout build failed'
+ for path in /a/deleted /b/deleted; do
+ if [ "$strip" -eq 0 ]; then
+ dump_inode "$path" >> "$seqres.full" || _fail "missing whiteout $path"
+ else
+ dump_inode "$path" >> "$seqres.full" 2>&1 && _fail "whiteout not stripped: $path"
+ fi
+ done
+ dump_inode /b/device >> "$seqres.full" || _fail 'ordinary device stripped'
+ "$FSCK_EROFS_PROG" "$image" >> "$seqres.full" 2>&1 || _fail 'whiteout fsck failed'
+done
+
+# Read h_shared_count using the inode location reported by dump.erofs.
+expect_shared_count()
+{
+ dump_inode "$1" > "$tmp/inode" || _fail "lookup $1 failed"
+ nid=$(sed -n 's/^NID: \([0-9]*\).*/\1/p' "$tmp/inode")
+ isz=$(sed -n 's/^Inode size: \([0-9]*\).*/\1/p' "$tmp/inode")
+ meta=$("$DUMP_EROFS_PROG" -s "$image" | awk '/inode metadata start block:/ {print $NF}')
+ # These fixtures use 4096-byte blocks and 32-byte inode slots.
+ offset=$((meta * 4096 + nid * 32 + isz + 4))
+ count=$(od -An -tu1 -j "$offset" -N1 "$image" | tr -d ' ')
+ [ "$count" = "$2" ] || _fail "unexpected shared count for $1: $count"
+}
+cat > "$tmp/shared" <<'MANIFEST'
+/ - - 40755 0 0 - 1.1
+/one /empty 0 100644 0 0 - 1.1 user.common=\x00\xff user.diff=a user.single=only
+/two /empty 0 100644 0 0 - 1.1 user.common=\x00\xff user.diff=b
+/three /empty 0 100644 0 0 - 1.1 user.common=\x00\xff user.diff=c
+/link1 /one - @ - - - -
+/link2 /one - @ - - - -
+MANIFEST
+for tolerance in default 0 2147483647 -1; do
+ set --
+ [ "$tolerance" = default ] || set -- -x"$tolerance"
+ "$MKFS_EROFS_PROG" -b4096 "$@" --manifest="$tmp/shared" "$image" "$base" \
+ >> "$seqres.full" 2>&1 || _fail "shared build failed: $tolerance"
+ case "$tolerance" in
+ default) one=1; two=1 ;;
+ 0) one=3; two=2 ;;
+ *) one=0; two=0 ;;
+ esac
+ expect_shared_count /one "$one"
+ expect_shared_count /link1 "$one"
+ expect_shared_count /two "$two"
+ "$FSCK_EROFS_PROG" --extract="$tmp/shared-$tolerance" --no-preserve-owner --xattrs \
+ "$image" >> "$seqres.full" 2>&1 || _fail 'shared extraction failed'
+ for file in one two three; do
+ getfattr --only-values -n user.common "$tmp/shared-$tolerance/$file" \
+ > "$tmp/value" 2>> "$seqres.full" || _fail 'missing shared value'
+ [ "$(od -An -tx1 -v "$tmp/value" | tr -d ' \n')" = 00ff ] || _fail 'shared binary value'
+ done
+ [ "$(getfattr --only-values -n user.diff "$tmp/shared-$tolerance/two" 2>> "$seqres.full")" = b ] ||
+ _fail 'different xattr values merged'
+done
+
+# Manifest xattrs use custom name prefixes in inline and shared entries.
+cat > "$tmp/prefixes" <<'MANIFEST'
+/ - - 40755 0 0 - 1.1
+/one /empty 0 100644 0 0 - 1.1 user.manifest.long.bin=\x00\xff user.second.long.empty= user.manifest.long.unique=one user.plain=one
+/two /empty 0 100644 0 0 - 1.1 user.manifest.long.bin=\x00\xff user.second.long.empty= user.manifest.long.unique=two user.plain=two
+/three /empty 0 100644 0 0 - 1.1 user.manifest.long.bin=\x00\xff user.second.long.empty= user.manifest.long.unique=three user.plain=three
+MANIFEST
+for tolerance in 2147483647 2 0; do
+ "$MKFS_EROFS_PROG" -b4096 -x"$tolerance" --manifest="$tmp/prefixes" \
+ --xattr-prefix=user.manifest.long. --xattr-prefix=user.second.long. \
+ "$image" "$base" >> "$seqres.full" 2>&1 || _fail 'manifest name prefix build failed'
+ "$DUMP_EROFS_PROG" -s "$image" > "$tmp/super" || _fail 'prefix superblock dump failed'
+ grep -q 'xattr_prefixes' "$tmp/super" || _fail 'missing xattr prefix feature'
+ case "$tolerance" in
+ 2147483647) shared_count=0 ;;
+ 2) shared_count=2 ;;
+ 0) shared_count=4 ;;
+ esac
+ for file in one two three; do
+ expect_shared_count /"$file" "$shared_count"
+ done
+ if [ "$tolerance" -eq 2147483647 ]; then
+ dump_inode /one > "$tmp/inode" || _fail 'prefix inode dump failed'
+ # Header (12), shortened entries (12+12+16), plain entry (12).
+ grep -q 'Xattr size: 64$' "$tmp/inode" || _fail 'name prefixes not used inline'
+ fi
+ "$FSCK_EROFS_PROG" --extract="$tmp/prefix-out-$tolerance" --no-preserve-owner --xattrs \
+ "$image" >> "$seqres.full" 2>&1 || _fail 'manifest name prefix extraction failed'
+ for file in one two three; do
+ for key in user.manifest.long.bin user.second.long.empty user.manifest.long.unique user.plain; do
+ getfattr --only-values -n "$key" "$tmp/prefix-out-$tolerance/$file" \
+ > "$tmp/value" 2>> "$seqres.full" || _fail "missing prefix xattr: $file $key"
+ case "$key" in
+ user.manifest.long.bin) printf '\000\377' > "$tmp/expected-value" ;;
+ user.second.long.empty) : > "$tmp/expected-value" ;;
+ *) printf '%s' "$file" > "$tmp/expected-value" ;;
+ esac
+ cmp "$tmp/expected-value" "$tmp/value" || _fail "prefix xattr differs: $file $key"
+ done
+ done
+done
+
+# The 256th candidate remains inline after 255 shared references.
+awk 'BEGIN {
+ print "/ - - 40755 0 0 - 1.1";
+ for (f = 1; f <= 3; ++f) {
+ printf "/f%d /empty 0 100644 0 0 - 1.1", f;
+ for (i = 0; i < 256; ++i) printf " user.k%03d=value", i;
+ printf "\n";
+ }
+}' > "$tmp/many"
+"$MKFS_EROFS_PROG" -b4096 --manifest="$tmp/many" "$image" "$base" \
+ >> "$seqres.full" 2>&1 || _fail 'many shared xattrs build failed'
+expect_shared_count /f1 255
+"$FSCK_EROFS_PROG" --extract --xattrs "$image" >> "$seqres.full" 2>&1 ||
+ _fail 'many shared xattrs fsck failed'
+# The final sorted entry is inline, after the header and 255 shared IDs.
+offset=$((meta * 4096 + nid * 32 + isz + 12 + 255 * 4 + 8))
+[ "$(od -An -tx1 -j "$offset" -N5 "$image" | tr -d ' \n')" = 76616c7565 ] ||
+ _fail 'inline overflow xattr value'
+
+# More than 256 KiB before sharing, but only five shared references per inode.
+awk 'BEGIN {
+ print "/ - - 40755 0 0 - 1.1";
+ for (i = 0; i < 60000; ++i) value = value "a";
+ for (f = 1; f <= 3; ++f) {
+ printf "/f%d /empty 0 100644 0 0 - 1.1", f;
+ for (i = 0; i < 5; ++i) printf " user.big%d=%s", i, value;
+ printf "\n";
+ }
+}' > "$tmp/bigattrs"
+"$MKFS_EROFS_PROG" -b4096 --manifest="$tmp/bigattrs" "$image" "$base" \
+ >> "$seqres.full" 2>&1 || _fail 'large shared xattrs build failed'
+expect_shared_count /f1 5
+# Read all xattrs without extracting values exceeding host filesystem limits.
+"$FSCK_EROFS_PROG" --extract --xattrs "$image" >> "$seqres.full" 2>&1 ||
+ _fail 'large shared xattrs fsck failed'
+shared=$("$DUMP_EROFS_PROG" -s "$image" | awk '/shared xattr metadata start block:/ {print $NF}')
+awk 'BEGIN { for (i = 0; i < 60000; ++i) printf "a"; }' > "$tmp/expected-value"
+for i in 0 1 2 3 4; do
+ # Four-byte entry header, four-byte suffix, then the value.
+ dd if="$image" of="$tmp/value" bs=1 skip=$((shared * 4096 + i * 60008 + 8)) \
+ count=60000 2>> "$seqres.full" || _fail 'shared value read failed'
+ cmp "$tmp/expected-value" "$tmp/value" || _fail 'large shared value differs'
+done
+
+# Short records import backing metadata and xattrs without scanning directories.
+mkdir -p "$base/short/sub"
+printf backing > "$base/short/data file"
+printf hidden > "$base/short/sub/unlisted"
+ln "$base/short/data file" "$base/short/alias"
+ln "$base/short/data file" "$base/short/not-listed"
+cp "$base/short/data file" "$base/short/copy1"
+cp "$base/short/data file" "$base/short/copy2"
+ln -s 'data file' "$base/short/link"
+ln -s missing "$base/short/dangling"
+mkfifo "$base/short/fifo"
+chmod 640 "$base/short/data file" "$base/short/copy1" "$base/short/copy2" "$base/short/fifo"
+chmod 750 "$base/short/sub"
+for file in 'data file' copy1 copy2; do
+ setfattr -n user.short.common -v 0x00ff "$base/short/$file" || _fail 'set short xattr'
+ setfattr -n user.short.empty -v '' "$base/short/$file" || _fail 'set empty short xattr'
+done
+setfattr -n user.short.single -v only "$base/short/data file" || _fail 'set single short xattr'
+setfattr -n user.short.dir -v directory "$base/short/sub" || _fail 'set directory xattr'
+touch -h -d '@5.000000009' "$base/short/"*
+cat > "$tmp/short" <<'MANIFEST'
+ / /short
+/dir /short/sub
+/data /short/data\x20file
+/alias /short/alias
+/copy1 /short/copy1
+/copy2 /short/copy2
+/link /short/link
+/dangling /short/dangling
+/fifo /short/fifo
+/explicit /data - @ - - - -
+/full /short/data\x20file 7 100644 123 456 - 1.1
+/full-dir - - 40755 0 0 - 1.1
+/full-dir/child /short/copy1
+MANIFEST
+# Exercise host path resolution and a final short record without a newline.
+printf ' /dot /short/sub/../data\x20file ' >> "$tmp/short"
+"$MKFS_EROFS_PROG" -b4096 -T10 --mkfs-time --xattr-prefix=user.short. \
+ --manifest="$tmp/short" "$image" "$base" >> "$seqres.full" 2>&1 || _fail 'short build failed'
+short_uid=$(id -u) short_gid=$(id -g)
+expect_stat /data "100640 $short_uid $short_gid 5.9 7 4"
+expect_stat /dir "40750 $short_uid $short_gid 5.9"
+expect_stat /link "120777 $short_uid $short_gid 5.9 9 1"
+expect_stat /dangling "120777 $short_uid $short_gid 5.9 7 1"
+expect_stat /fifo "10640 $short_uid $short_gid 5.9"
+expect_stat /full '100644 123 456 1.1 7 1'
+expect_shared_count /data 2
+nid=$(dump_inode /data | sed -n 's/^NID: \([0-9]*\).*/\1/p')
+for file in alias explicit dot; do
+ [ "$(dump_inode /"$file" | sed -n 's/^NID: \([0-9]*\).*/\1/p')" = "$nid" ] ||
+ _fail 'short hardlink inode differs'
+done
+"$FSCK_EROFS_PROG" --extract="$tmp/short-out" --no-preserve-owner --xattrs "$image" \
+ >> "$seqres.full" 2>&1 || _fail 'short extraction failed'
+[ ! -e "$tmp/short-out/dir/unlisted" ] || _fail 'short directory recursively imported'
+[ ! -e "$tmp/short-out/not-listed" ] || _fail 'unlisted source hardlink imported'
+[ "$(readlink "$tmp/short-out/link")" = 'data file' ] || _fail 'short symlink followed'
+[ "$(readlink "$tmp/short-out/dangling")" = missing ] || _fail 'dangling symlink changed'
+cmp "$base/short/data file" "$tmp/short-out/data" || _fail 'short data differs'
+getfattr --only-values -n user.short.common "$tmp/short-out/data" > "$tmp/value" 2>> "$seqres.full" ||
+ _fail 'short binary xattr missing'
+[ "$(od -An -tx1 -v "$tmp/value" | tr -d ' \n')" = 00ff ] || _fail 'short binary xattr differs'
+getfattr --only-values -n user.short.empty "$tmp/short-out/data" > "$tmp/value" 2>> "$seqres.full" ||
+ _fail 'short empty xattr missing'
+[ ! -s "$tmp/value" ] || _fail 'short empty xattr differs'
+[ "$(getfattr --only-values -n user.short.dir "$tmp/short-out/dir" 2>> "$seqres.full")" = directory ] ||
+ _fail 'short directory xattr differs'
+getfattr -n user.short.common "$tmp/short-out/full" >> "$seqres.full" 2>&1 &&
+ _fail 'full record imported backing xattr'
+
+# Local metadata options affect short records, but not complete records.
+"$MKFS_EROFS_PROG" -T20 --force-uid=42 --force-gid=43 --uid-offset=1 --gid-offset=2 \
+ --hard-dereference -x-1 --manifest="$tmp/short" "$image" "$base" \
+ >> "$seqres.full" 2>&1 || _fail 'short local options failed'
+expect_stat /data '100640 43 45 20.0 7 2'
+expect_stat /alias '100640 43 45 20.0 7 1'
+expect_stat /full '100644 123 456 1.1 7 1'
+dump_inode /data > "$tmp/inode" || _fail 'short data lookup failed'
+grep -q 'Xattr size: 0$' "$tmp/inode" || _fail 'short xattrs not disabled'
+"$FSCK_EROFS_PROG" "$image" >> "$seqres.full" 2>&1 || _fail 'short options fsck failed'
+SOURCE_DATE_EPOCH=3 "$MKFS_EROFS_PROG" --manifest="$tmp/short" "$image" "$base" \
+ >> "$seqres.full" 2>&1 || _fail 'short timestamp clamping failed'
+expect_stat /data "100640 $short_uid $short_gid 3.0 7 4"
+expect_stat /full '100644 123 456 1.1 7 1'
+
+# Special backing files are optional where creating device nodes is forbidden.
+for device in char block whiteout; do
+ case "$device" in
+ char) set -- c 1 3 ;;
+ block) set -- b 8 0 ;;
+ whiteout) set -- c 0 0 ;;
+ esac
+ if mknod "$base/short/$device" "$@" 2>> "$seqres.full"; then
+ printf '/ - - 40755 0 0 - 1.1\n/device /short/%s\n/hard /device - @ - - - -\n' "$device" > "$tmp/special"
+ for strip in 0 1; do
+ "$MKFS_EROFS_PROG" --ovlfs-strip="$strip" --manifest="$tmp/special" "$image" "$base" \
+ >> "$seqres.full" 2>&1 || _fail 'short device build failed'
+ if [ "$device:$strip" = whiteout:1 ]; then
+ dump_inode /device >> "$seqres.full" 2>&1 && _fail 'short whiteout not stripped'
+ dump_inode /hard >> "$seqres.full" 2>&1 && _fail 'short whiteout link not stripped'
+ else
+ dump_inode /device >> "$seqres.full" || _fail 'short device missing'
+ fi
+ "$FSCK_EROFS_PROG" "$image" >> "$seqres.full" 2>&1 || _fail 'short device fsck failed'
+ done
+ fi
+done
+if command -v python3 >/dev/null 2>&1; then
+ python3 -c 'import socket, sys; s = socket.socket(socket.AF_UNIX); s.bind(sys.argv[1]); s.close()' \
+ "$base/short/socket" || _fail 'socket fixture failed'
+ printf '/ /short\n/socket /short/socket\n' > "$tmp/special"
+ "$MKFS_EROFS_PROG" --manifest="$tmp/special" "$image" "$base" \
+ >> "$seqres.full" 2>&1 || _fail 'short socket build failed'
+ dump_inode /socket > "$tmp/inode" || _fail 'short socket missing'
+ grep -q 'SOCK file$' "$tmp/inode" || _fail 'short socket type differs'
+ "$FSCK_EROFS_PROG" "$image" >> "$seqres.full" 2>&1 || _fail 'short socket fsck failed'
+fi
+
+bad_record '/bad /short/missing'
+bad_record '/bad relative'
+bad_record '/bad /short/data\x00file'
+bad_record '/bad /short/data\x'
+bad_record '/bad /short/copy1 user.extra=value'
+bad_record '/missing/child /short/copy1'
+for fields in '/bad /short/copy1 7' '/bad /short/copy1 7 100644' \
+ '/bad /short/copy1 7 100644 0' '/bad /short/copy1 7 100644 0 0' \
+ '/bad /short/copy1 7 100644 0 0 -'; do
+ bad_record "$fields"
+done
+printf '/ /short/copy1\n' > "$tmp/bad"
+"$MKFS_EROFS_PROG" --manifest="$tmp/bad" "$image" "$base" >> "$seqres.full" 2>&1 &&
+ _fail 'accepted short nondirectory root'
+printf '/ /short\n/one /short/data\x20file\n/two /short/alias\n/one /short/copy1\n' > "$tmp/bad"
+"$MKFS_EROFS_PROG" --manifest="$tmp/bad" "$image" "$base" >> "$seqres.full" 2>&1
+rc=$?
+[ "$rc" -eq 0 ] && _fail 'accepted duplicate short path'
+[ "$rc" -ge 128 ] && _fail 'crashed cleaning up short hardlinks'
+
+# Both record formats enter the normal file compression pipeline.
+awk 'BEGIN { for (i = 0; i < 4096; ++i) printf "manifest payload\n"; }' > "$base/compress"
+compressed_size=$(stat -c %s "$base/compress")
+chmod 640 "$base/compress"
+setfattr -n user.compressed -v 0x00ff "$base/compress" || _fail 'set compression xattr'
+touch -d '@5.000000009' "$base/compress"
+cat > "$tmp/compressed" <<MANIFEST
+/ - - 40755 0 0 - 1.1
+/full /compress $compressed_size 100600 123 456 - 1.1 user.compressed=\x00\xff
+/short /compress
+/alias /compress
+/hard /full - @ - - - -
+MANIFEST
+for algorithm in lz4 lz4hc lzma deflate zstd; do
+ case "$algorithm" in
+ lz4) [ -n "$lz4_on" ] || continue ;;
+ lz4hc) [ -n "$lz4hc_on" ] || continue ;;
+ lzma) [ -n "$lzma_on" ] || continue ;;
+ zstd) [ -n "$zstd_on" ] || continue ;;
+ esac
+ "$MKFS_EROFS_PROG" -b4096 -T10 --mkfs-time -x0 "-z$algorithm" \
+ --manifest="$tmp/compressed" "$image" "$base" \
+ >> "$seqres.full" 2>&1 || _fail "compressed build failed: $algorithm"
+ expect_stat /full "100600 123 456 1.1 $compressed_size 2"
+ expect_stat /short "100640 $short_uid $short_gid 5.9 $compressed_size 2"
+ for file in full short; do
+ dump_inode /"$file" > "$tmp/inode" || _fail 'compressed inode missing'
+ grep -Eq 'Layout: (1|3) ' "$tmp/inode" ||
+ _fail "file was not compressed: $algorithm $file"
+ expect_shared_count /"$file" 1
+ done
+ for pair in 'full hard' 'short alias'; do
+ set -- $pair
+ nid=$(dump_inode /"$1" | sed -n 's/^NID: \([0-9]*\).*/\1/p')
+ [ "$(dump_inode /"$2" | sed -n 's/^NID: \([0-9]*\).*/\1/p')" = "$nid" ] ||
+ _fail 'compressed hardlink inode differs'
+ done
+ out="$tmp/compressed-$algorithm"
+ "$FSCK_EROFS_PROG" --extract="$out" --no-preserve-owner --xattrs "$image" \
+ >> "$seqres.full" 2>&1 || _fail "compressed extraction failed: $algorithm"
+ for file in full short hard alias; do
+ cmp "$base/compress" "$out/$file" || _fail 'decompressed content differs'
+ getfattr --only-values -n user.compressed "$out/$file" > "$tmp/value" \
+ 2>> "$seqres.full" || _fail 'compressed xattr missing'
+ [ "$(od -An -tx1 -v "$tmp/value" | tr -d ' \n')" = 00ff ] ||
+ _fail 'compressed binary xattr differs'
+ done
+done
+
+# Incremental complete records associate only matching old directories.
+incr_base="$tmp/incr-base"
+incr_source="$tmp/incr-source"
+mkdir -p "$incr_base/existing" "$incr_base/to-file" "$incr_source/local"
+printf keep > "$incr_base/existing/keep"
+printf old > "$incr_base/existing/change"
+printf old > "$incr_base/to-dir"
+printf old > "$incr_base/to-file/child"
+printf new > "$incr_source/data"
+ln "$incr_source/data" "$incr_source/alias"
+setfattr -n user.old -v old "$incr_base/existing" || _fail 'set old directory xattr'
+"$MKFS_EROFS_PROG" -T10 "$image" "$incr_base" >> "$seqres.full" 2>&1 ||
+ _fail 'incremental base build failed'
+cat > "$tmp/incr-manifest" <<'MANIFEST'
+/ - - 40755 123 456 - 1.1
+/existing - - 40750 123 456 - 2.3 user.new=new
+/existing/change /data 3 100640 123 456 - 2.3
+/newdir - - 40750 123 456 - 2.3
+/newdir/nested - - 40700 123 456 - 2.3
+/newdir/nested/file /data 3 100640 123 456 - 2.3
+/to-dir - - 40750 123 456 - 2.3
+/to-dir/file /data 3 100640 123 456 - 2.3
+/to-file /data 3 100640 123 456 - 2.3
+/local /local
+/local/full-child - - 40750 123 456 - 2.3
+/local/full-child/short /data
+/short-alias /alias
+/explicit /local/full-child/short - @ - - - -
+MANIFEST
+"$MKFS_EROFS_PROG" --incremental=data --manifest="$tmp/incr-manifest" \
+ "$image" "$incr_source" >> "$seqres.full" 2>&1 || _fail 'incremental manifest build failed'
+expect_stat / '40755 123 456 1.1'
+expect_stat /existing '40750 123 456 2.3'
+expect_stat /newdir/nested '40700 123 456 2.3'
+expect_stat /to-dir '40750 123 456 2.3'
+expect_stat /local/full-child '40750 123 456 2.3'
+expect_stat /existing/change '100640 123 456 2.3 3 1'
+expect_stat /to-file '100640 123 456 2.3 3 1'
+nid=$(dump_inode /local/full-child/short | sed -n 's/^NID: \([0-9]*\).*/\1/p')
+for entry in /short-alias /explicit; do
+ [ "$(dump_inode "$entry" | sed -n 's/^NID: \([0-9]*\).*/\1/p')" = "$nid" ] ||
+ _fail 'incremental hardlink inode differs'
+done
+"$FSCK_EROFS_PROG" --extract="$tmp/incr-out" --no-preserve-owner --xattrs "$image" \
+ >> "$seqres.full" 2>&1 || _fail 'incremental extraction failed'
+cmp "$incr_base/existing/keep" "$tmp/incr-out/existing/keep" || _fail 'old file not retained'
+for entry in existing/change newdir/nested/file to-dir/file to-file local/full-child/short; do
+ cmp "$incr_source/data" "$tmp/incr-out/$entry" || _fail 'incremental content differs'
+done
+[ "$(getfattr --only-values -n user.new "$tmp/incr-out/existing" 2>> "$seqres.full")" = new ] ||
+ _fail 'new directory xattr missing'
+getfattr -n user.old "$tmp/incr-out/existing" >> "$seqres.full" 2>&1 &&
+ _fail 'old directory xattr retained'
+
+echo 'Silence is golden'
+status=0
+exit 0
diff --git a/tests/erofs/033.out b/tests/erofs/033.out
new file mode 100644
index 0000000..c5d2914
--- /dev/null
+++ b/tests/erofs/033.out
@@ -0,0 +1,2 @@
+QA output created by 033
+Silence is golden
--
2.43.0
^ permalink raw reply related [flat|nested] 8+ messages in thread
end of thread, other threads:[~2026-11-04 4:13 UTC | newest]
Thread overview: 8+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-04 4:07 [RFC 0/7] erofs-utils: add manifest input support Wenyuan Xu
2026-11-04 4:07 ` [RFC 1/7] erofs-utils: mkfs: init root inode in local source mode Wenyuan Xu
2026-11-04 4:07 ` [RFC 2/7] erofs-utils: lib: add erofs_inode_has_xattr helper Wenyuan Xu
2026-11-04 4:07 ` [RFC 3/7] erofs-utils: lib: add erofs_build_shared_xattrs() helper Wenyuan Xu
2026-11-04 4:07 ` [RFC 4/7] erofs-utils: mkfs: add manifest input mode Wenyuan Xu
2026-11-04 4:07 ` [RFC 5/7] erofs-utils: mkfs: support two-field manifest records Wenyuan Xu
2026-11-04 4:07 ` [RFC 6/7] erofs-utils: man: document manifest input formats Wenyuan Xu
2026-11-04 4:07 ` [RFC 7/7] erofs-utils: tests: cover manifest input Wenyuan Xu
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox