diff --git a/libcomposefs/lcfs-internal.h b/libcomposefs/lcfs-internal.h index 98e59d23..3e4f32fe 100644 --- a/libcomposefs/lcfs-internal.h +++ b/libcomposefs/lcfs-internal.h @@ -23,6 +23,12 @@ #include "lcfs-fsverity.h" #include "hash.h" +/* When using LCFS_BUILD_INLINE_SMALL in lcfs_load_node_from_file() inline files below this size + * We pick 64 which is the size of a sha256 digest that would otherwise be used as a redirect + * xattr, so the inlined file is smaller. + */ +#define LCFS_BUILD_INLINE_FILE_SIZE_LIMIT 64 + #define ALIGN_TO(_offset, _align_size) \ (((_offset) + _align_size - 1) & ~(_align_size - 1)) @@ -98,6 +104,8 @@ struct lcfs_node_s { char *name; char *payload; /* backing file or symlink target */ + uint8_t *content; + struct lcfs_xattr_s *xattrs; size_t n_xattrs; diff --git a/libcomposefs/lcfs-utils.h b/libcomposefs/lcfs-utils.h index 64eeb83d..22a24dfc 100644 --- a/libcomposefs/lcfs-utils.h +++ b/libcomposefs/lcfs-utils.h @@ -23,6 +23,7 @@ #include #define max(a, b) ((a > b) ? (a) : (b)) +#define min(a, b) (((a) < (b)) ? (a) : (b)) static inline void _lcfs_reset_errno_(int *saved_errno) { diff --git a/libcomposefs/lcfs-writer-erofs.c b/libcomposefs/lcfs-writer-erofs.c index 6f28ac4a..f6039f13 100644 --- a/libcomposefs/lcfs-writer-erofs.c +++ b/libcomposefs/lcfs-writer-erofs.c @@ -23,6 +23,7 @@ #include "lcfs-writer.h" #include "lcfs-fsverity.h" #include "lcfs-erofs.h" +#include "lcfs-utils.h" #include "hash.h" #include @@ -517,12 +518,21 @@ static void compute_erofs_inode_size(struct lcfs_node_s *node) node->erofs_n_blocks = 0; node->erofs_tailsize = strlen(node->payload); } else if (type == S_IFREG && file_size > 0) { - uint32_t chunkbits = compute_erofs_chunk_bitsize(node); - uint64_t chunksize = 1ULL << chunkbits; - uint32_t chunk_count = DIV_ROUND_UP(file_size, chunksize); + if (node->content != NULL) { + node->erofs_n_blocks = file_size / EROFS_BLKSIZ; + node->erofs_tailsize = file_size % EROFS_BLKSIZ; + if (node->erofs_tailsize > EROFS_BLKSIZ / 2) { + node->erofs_n_blocks++; + node->erofs_tailsize = 0; + } + } else { + uint32_t chunkbits = compute_erofs_chunk_bitsize(node); + uint64_t chunksize = 1ULL << chunkbits; + uint32_t chunk_count = DIV_ROUND_UP(file_size, chunksize); - node->erofs_n_blocks = 0; - node->erofs_tailsize = chunk_count * sizeof(uint32_t); + node->erofs_n_blocks = 0; + node->erofs_tailsize = chunk_count * sizeof(uint32_t); + } } else { node->erofs_n_blocks = 0; node->erofs_tailsize = 0; @@ -823,7 +833,7 @@ static int write_erofs_inode_data(struct lcfs_ctx_s *ctx, struct lcfs_node_s *no } else if (type == S_IFREG) { size = node->inode.st_size; - if (size > 0) { + if (size > 0 && node->content == NULL) { uint32_t chunkbits = compute_erofs_chunk_bitsize(node); uint64_t chunksize = 1ULL << chunkbits; @@ -889,6 +899,12 @@ static int write_erofs_inode_data(struct lcfs_ctx_s *ctx, struct lcfs_node_s *no } else if (type == S_IFCHR || type == S_IFBLK) { i.i_u.rdev = lcfs_u32_to_file(node->inode.st_rdev); } else if (type == S_IFREG) { + if (node->erofs_n_blocks > 0) { + i.i_u.raw_blkaddr = lcfs_u32_to_file( + ctx_erofs->current_end / EROFS_BLKSIZ); + ctx_erofs->current_end += + EROFS_BLKSIZ * node->erofs_n_blocks; + } if (datalayout == EROFS_INODE_CHUNK_BASED) { i.i_u.c.format = lcfs_u16_to_file(chunk_format); } @@ -944,11 +960,24 @@ static int write_erofs_inode_data(struct lcfs_ctx_s *ctx, struct lcfs_node_s *no if (ret < 0) return ret; } else if (type == S_IFREG) { - for (size_t i = 0; i < chunk_count; i++) { - uint32_t empty_chunk = 0xFFFFFFFF; - ret = lcfs_write(ctx, &empty_chunk, sizeof(empty_chunk)); - if (ret < 0) - return ret; + if (node->content != NULL) { + if (node->erofs_tailsize) { + uint64_t file_size = node->inode.st_size; + ret = lcfs_write(ctx, + node->content + file_size - + node->erofs_tailsize, + node->erofs_tailsize); + if (ret < 0) + return ret; + } + } else { + for (size_t i = 0; i < chunk_count; i++) { + uint32_t empty_chunk = 0xFFFFFFFF; + ret = lcfs_write(ctx, &empty_chunk, + sizeof(empty_chunk)); + if (ret < 0) + return ret; + } } } @@ -976,7 +1005,31 @@ static int write_erofs_inodes(struct lcfs_ctx_s *ctx) return 0; } -static int write_erofs_dirent_blocks(struct lcfs_ctx_s *ctx) +/* Writes the non-tailpacked file data, if any */ +static int write_erofs_file_content(struct lcfs_ctx_s *ctx, struct lcfs_node_s *node) +{ + int type = node->inode.st_mode & S_IFMT; + off_t size = node->inode.st_size; + + if (type != S_IFREG || node->erofs_n_blocks == 0) + return 0; + + assert(node->content != NULL); + + for (size_t i = 0; i < node->erofs_n_blocks; i++) { + off_t offset = i * EROFS_BLKSIZ; + off_t len = min(size - offset, EROFS_BLKSIZ); + int ret; + + ret = lcfs_write(ctx, node->content + offset, len); + if (ret < 0) + return ret; + } + + return lcfs_write_align(ctx, EROFS_BLKSIZ); +} + +static int write_erofs_data_blocks(struct lcfs_ctx_s *ctx) { struct lcfs_node_s *node; int ret; @@ -985,6 +1038,9 @@ static int write_erofs_dirent_blocks(struct lcfs_ctx_s *ctx) ret = write_erofs_dentries(ctx, node, true, false); if (ret < 0) return ret; + ret = write_erofs_file_content(ctx, node); + if (ret < 0) + return ret; } return 0; @@ -1042,7 +1098,7 @@ static int add_overlayfs_xattrs(struct lcfs_node_s *node) } } - if (type == S_IFREG && node->inode.st_size > 0) { + if (type == S_IFREG && node->inode.st_size > 0 && node->content == NULL) { uint8_t xattr_data[4 + LCFS_DIGEST_SIZE]; size_t xattr_len = 0; @@ -1060,7 +1116,7 @@ static int add_overlayfs_xattrs(struct lcfs_node_s *node) if (ret < 0) return ret; - if (strlen(node->payload) > 0) { + if (node->payload && strlen(node->payload) > 0) { char *path = maybe_join_path("/", node->payload); if (path == NULL) { errno = ENOMEM; @@ -1351,7 +1407,7 @@ int lcfs_write_erofs_to(struct lcfs_ctx_s *ctx) assert(data_block_start == (uint64_t)ctx->bytes_written); - ret = write_erofs_dirent_blocks(ctx); + ret = write_erofs_data_blocks(ctx); if (ret < 0) return ret; diff --git a/libcomposefs/lcfs-writer.c b/libcomposefs/lcfs-writer.c index c0ed7fc0..caada52f 100644 --- a/libcomposefs/lcfs-writer.c +++ b/libcomposefs/lcfs-writer.c @@ -540,6 +540,30 @@ int lcfs_node_set_fsverity_from_fd(struct lcfs_node_s *node, int fd) return lcfs_node_set_fsverity_from_content(node, &_fd, fsverity_read_cb); } +static int read_content(int fd, size_t size, uint8_t *buf) +{ + int bytes_read; + + while (size > 0) { + do + bytes_read = read(fd, buf, size); + while (bytes_read < 0 && errno == EINTR); + + if (bytes_read == 0) + break; + + size -= bytes_read; + buf += bytes_read; + } + + if (size > 0) { + errno = ENODATA; + return -1; + } + + return 0; +} + struct lcfs_node_s *lcfs_load_node_from_file(int dirfd, const char *fname, int buildflags) { @@ -548,7 +572,8 @@ struct lcfs_node_s *lcfs_load_node_from_file(int dirfd, const char *fname, int r; if (buildflags & ~(LCFS_BUILD_SKIP_XATTRS | LCFS_BUILD_USE_EPOCH | - LCFS_BUILD_SKIP_DEVICES | LCFS_BUILD_COMPUTE_DIGEST)) { + LCFS_BUILD_SKIP_DEVICES | LCFS_BUILD_COMPUTE_DIGEST | + LCFS_BUILD_NO_INLINE)) { errno = EINVAL; return NULL; } @@ -568,17 +593,42 @@ struct lcfs_node_s *lcfs_load_node_from_file(int dirfd, const char *fname, ret->inode.st_size = sb.st_size; if ((sb.st_mode & S_IFMT) == S_IFREG) { - if (sb.st_size != 0 && (buildflags & LCFS_BUILD_COMPUTE_DIGEST) != 0) { - int fd = openat(dirfd, fname, O_RDONLY | O_CLOEXEC); + bool compute_digest = (buildflags & LCFS_BUILD_COMPUTE_DIGEST) != 0; + bool no_inline = (buildflags & LCFS_BUILD_NO_INLINE) != 0; + bool is_zerosized = sb.st_size == 0; + bool do_digest = !is_zerosized && compute_digest; + bool do_inline = !is_zerosized && !no_inline && + sb.st_size <= LCFS_BUILD_INLINE_FILE_SIZE_LIMIT; + + if (do_digest || do_inline) { + cleanup_fd int fd = + openat(dirfd, fname, O_RDONLY | O_CLOEXEC); if (fd < 0) { lcfs_node_unref(ret); return NULL; } - r = lcfs_node_set_fsverity_from_fd(ret, fd); - close(fd); - if (r < 0) { - lcfs_node_unref(ret); - return NULL; + if (do_digest) { + r = lcfs_node_set_fsverity_from_fd(ret, fd); + if (r < 0) { + lcfs_node_unref(ret); + return NULL; + } + /* In case we re-read below */ + lseek(fd, 0, SEEK_SET); + } + if (do_inline) { + uint8_t buf[LCFS_BUILD_INLINE_FILE_SIZE_LIMIT]; + + r = read_content(fd, sb.st_size, buf); + if (r < 0) { + lcfs_node_unref(ret); + return NULL; + } + r = lcfs_node_set_content(ret, buf, sb.st_size); + if (r < 0) { + lcfs_node_unref(ret); + return NULL; + } } } } @@ -627,6 +677,31 @@ void lcfs_node_set_fsverity_digest(struct lcfs_node_s *node, memcpy(node->digest, digest, LCFS_DIGEST_SIZE); } +int lcfs_node_set_content(struct lcfs_node_s *node, const uint8_t *data, + size_t data_size) +{ + uint8_t *dup = NULL; + + if (data && data_size != 0) { + dup = malloc(data_size); + if (dup == NULL) { + errno = ENOMEM; + return -1; + } + memcpy(dup, data, data_size); + } + free(node->content); + node->content = dup; + node->inode.st_size = data_size; + + return 0; +} + +const uint8_t *lcfs_node_get_content(struct lcfs_node_s *node) +{ + return node->content; +} + const char *lcfs_node_get_name(struct lcfs_node_s *node) { return node->name; @@ -699,8 +774,14 @@ uint64_t lcfs_node_get_size(struct lcfs_node_s *node) return node->inode.st_size; } +/* Clears content if size changes */ void lcfs_node_set_size(struct lcfs_node_s *node, uint64_t size) { + if (size == node->inode.st_size) + return; + + free(node->content); + node->content = NULL; node->inode.st_size = size; } @@ -824,6 +905,7 @@ void lcfs_node_unref(struct lcfs_node_s *node) free(node->name); free(node->payload); + free(node->content); for (i = 0; i < node->n_xattrs; i++) { free(node->xattrs[i].key); @@ -874,6 +956,13 @@ struct lcfs_node_s *lcfs_node_clone(struct lcfs_node_s *node) goto fail; } + if (node->content) { + new->content = malloc(node->inode.st_size); + if (new->content == NULL) + goto fail; + memcpy(new->content, node->content, node->inode.st_size); + } + if (node->n_xattrs > 0) { new->xattrs = malloc(sizeof(struct lcfs_xattr_s) * node->n_xattrs); if (new->xattrs == NULL) diff --git a/libcomposefs/lcfs-writer.h b/libcomposefs/lcfs-writer.h index daf8d6ca..8fc60b9d 100644 --- a/libcomposefs/lcfs-writer.h +++ b/libcomposefs/lcfs-writer.h @@ -34,6 +34,7 @@ enum { LCFS_BUILD_USE_EPOCH = (1 << 1), LCFS_BUILD_SKIP_DEVICES = (1 << 2), LCFS_BUILD_COMPUTE_DIGEST = (1 << 3), + LCFS_BUILD_NO_INLINE = (1 << 4), }; enum lcfs_format_t { @@ -78,6 +79,10 @@ LCFS_EXTERN const char *lcfs_node_get_xattr_name(struct lcfs_node_s *node, LCFS_EXTERN int lcfs_node_set_payload(struct lcfs_node_s *node, const char *payload); +LCFS_EXTERN int lcfs_node_set_content(struct lcfs_node_s *node, + const uint8_t *data, size_t data_size); +LCFS_EXTERN const uint8_t *lcfs_node_get_content(struct lcfs_node_s *node); + LCFS_EXTERN struct lcfs_node_s *lcfs_node_lookup_child(struct lcfs_node_s *node, const char *name); LCFS_EXTERN struct lcfs_node_s *lcfs_node_get_parent(struct lcfs_node_s *node); diff --git a/tests/dumpdir b/tests/dumpdir new file mode 100755 index 00000000..52cf357d --- /dev/null +++ b/tests/dumpdir @@ -0,0 +1,47 @@ +#!/usr/bin/python3 + +import os +import sys +import shlex +import hashlib +import stat +import argparse + +def log_error(error): + print("Readdir error: " + error) + +def dumpfile(file, root): + rel = os.path.relpath(file, root) + s = os.lstat(file) + nlink = s.st_nlink; + if args.no_nlink: + nlink = 1 + print(f"{shlex.quote(rel)} {s.st_mode} {nlink} {s.st_uid}:{s.st_gid} {s.st_rdev} {s.st_mtime_ns}",end="") + if stat.S_ISREG(s.st_mode): + digest = hashlib.sha256(open(file,'rb').read()).hexdigest() + print(f" {s.st_size} sha256:{digest}",end="") + elif stat.S_ISLNK(s.st_mode): + link = os.readlink(file) + print(f" ->{shlex.quote(link)}",end="") + + for attr in sorted(os.listxattr(file, follow_symlinks=False)): + v = os.getxattr(file, attr, follow_symlinks=False) + print(f" {attr}={v}", end="") + + print() + + + +def dumpdir(root): + dumpfile(root, root) + for parent, dirs, files in os.walk(root, topdown=True, onerror=log_error): + for file in sorted(dirs + files): + dumpfile(os.path.join(parent, file), root) + +argParser = argparse.ArgumentParser() +argParser.add_argument("--no-nlink", action='store_true') +argParser.add_argument('path') + +args = argParser.parse_args() + +dumpdir(args.path) diff --git a/tests/integration.sh b/tests/integration.sh index 5a174f5c..074dd912 100755 --- a/tests/integration.sh +++ b/tests/integration.sh @@ -4,11 +4,7 @@ # the output of ls -lR (without hardlink counts). set -xeuo pipefail -# ls -l but without hardlinks -nonhardlink_ls() { - ls "$@" | sed -e 's,^\([^ ]*\) *\([0-9][0-9]*\)\(.*\)$,\1\3,' -} - +orig=$(pwd) cfsroot=${cfsroot:-/composefs} rm ${cfsroot}/tmp -rf mkdir -p ${cfsroot}/{objects,roots,tmp} @@ -22,13 +18,18 @@ test "$prev_digest" = "$new_digest" mkdir -p mnt mount.composefs -o basedir=${cfsroot}/objects ${cfsroot}/roots/test.cfs mnt -(cd ${testsrc} && nonhardlink_ls -lR .) > src-ls.txt -(cd mnt && nonhardlink_ls -lR .) > mnt-ls.txt +$orig/tests/dumpdir --no-nlink ${testsrc} > src-dump.txt +$orig/tests/dumpdir --no-nlink mnt > mnt-dump.txt failed= -if ! diff -u src-ls.txt mnt-ls.txt; then +if ! diff -u src-dump.txt mnt-dump.txt; then failed=1 fi -umount mnt if test -n "${failed}"; then + umount mnt exit 1 fi + +new_digest=$(mkcomposefs --by-digest --print-digest-only mnt) +test "$prev_digest" = "$new_digest" + +umount mnt diff --git a/tools/mkcomposefs.c b/tools/mkcomposefs.c index 7fb7487a..1e88e7ba 100644 --- a/tools/mkcomposefs.c +++ b/tools/mkcomposefs.c @@ -323,7 +323,8 @@ static int fill_payload(struct lcfs_node_s *node, const char *path, size_t len, ret = lcfs_node_set_payload(node, target); if (ret < 0) return ret; - } else if ((lcfs_node_get_mode(node) & S_IFMT) == S_IFREG) { + } else if ((lcfs_node_get_mode(node) & S_IFMT) == S_IFREG && + lcfs_node_get_content(node) == NULL) { const uint8_t *digest = NULL; if (by_digest)