diff --git a/Makefile.am b/Makefile.am index 31463d7d..55580dc2 100644 --- a/Makefile.am +++ b/Makefile.am @@ -1,4 +1,4 @@ -SUBDIRS=libcomposefs tools +SUBDIRS=libcomposefs tools tests EXTRA_DIST=\ composefs.pc.in \ diff --git a/configure.ac b/configure.ac index 9fcbfce8..67fa2ded 100644 --- a/configure.ac +++ b/configure.ac @@ -206,6 +206,7 @@ AC_CONFIG_FILES([ Makefile libcomposefs/Makefile tools/Makefile +tests/Makefile composefs.spec composefs.pc ]) diff --git a/libcomposefs/Makefile-lib.am b/libcomposefs/Makefile-lib.am index 0eaae482..4b4d4010 100644 --- a/libcomposefs/Makefile-lib.am +++ b/libcomposefs/Makefile-lib.am @@ -8,6 +8,7 @@ libcomposefs_la_SOURCES = \ $(COMPOSEFSDIR)/hash.h \ $(COMPOSEFSDIR)/lcfs-internal.h \ $(COMPOSEFSDIR)/lcfs-erofs.h \ + $(COMPOSEFSDIR)/lcfs-erofs-internal.h \ $(COMPOSEFSDIR)/lcfs-fsverity.c \ $(COMPOSEFSDIR)/lcfs-fsverity.h \ $(COMPOSEFSDIR)/lcfs-writer-erofs.c \ diff --git a/libcomposefs/erofs_fs_wrapper.h b/libcomposefs/erofs_fs_wrapper.h index 5e5cb400..b7ed2aa8 100644 --- a/libcomposefs/erofs_fs_wrapper.h +++ b/libcomposefs/erofs_fs_wrapper.h @@ -1,3 +1,4 @@ +#include #include #define __packed __attribute__((__packed__)) @@ -33,6 +34,14 @@ static inline __u64 le64_to_cpu(__u64 val) return le64toh(val); } +/* Note: These only do power of 2 */ +#define __round_mask(x, y) ((__typeof__(x))((y)-1)) +#define round_up(x, y) ((((x)-1) | __round_mask(x, y)) + 1) +#define round_down(x, y) ((x) & ~__round_mask(x, y)) + +#define ALIGN_TO(_offset, _align_size) \ + (((_offset) + _align_size - 1) & ~(_align_size - 1)) + #define BIT(nr) (((uint64_t) 1) << (nr)) #define BUILD_BUG_ON(condition) ((void)sizeof(char[1 - 2 * !!(condition)])) #define DIV_ROUND_UP(n, d) (((n) + (d) - 1) / (d)) diff --git a/libcomposefs/lcfs-erofs-internal.h b/libcomposefs/lcfs-erofs-internal.h new file mode 100644 index 00000000..f6f7a979 --- /dev/null +++ b/libcomposefs/lcfs-erofs-internal.h @@ -0,0 +1,134 @@ +/* lcfs + Copyright (C) 2023 Alexander Larsson + + This file is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as + published by the Free Software Foundation; either version 2.1 of the + License, or (at your option) any later version. + + This file is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program. If not, see . */ + +#ifndef _LCFS_EROFS_INTERNAL_H +#define _LCFS_EROFS_INTERNAL_H + +#include + +#include "lcfs-internal.h" +#include "lcfs-erofs.h" +#include "erofs_fs_wrapper.h" + +typedef union { + __le16 i_format; + struct erofs_inode_compact compact; + struct erofs_inode_extended extended; +} erofs_inode; + +static const char *erofs_xattr_prefixes[] = { + "", + "user.", + "system.posix_acl_access", + "system.posix_acl_default", + "trusted.", + "lustre.", + "security.", +}; + +static inline uint16_t erofs_inode_version(const erofs_inode *cino) +{ + uint16_t i_format = lcfs_u16_from_file(cino->i_format); + return (i_format >> EROFS_I_VERSION_BIT) & EROFS_I_VERSION_MASK; +} + +static inline bool erofs_inode_is_compact(const erofs_inode *cino) +{ + return erofs_inode_version(cino) == 0; +} + +static inline uint16_t erofs_inode_datalayout(const erofs_inode *cino) +{ + uint16_t i_format = lcfs_u16_from_file(cino->i_format); + return (i_format >> EROFS_I_DATALAYOUT_BIT) & EROFS_I_DATALAYOUT_MASK; +} + +static inline bool erofs_inode_is_tailpacked(const erofs_inode *cino) +{ + return erofs_inode_datalayout(cino) == EROFS_INODE_FLAT_INLINE; +} + +static inline bool erofs_inode_is_flat(const erofs_inode *cino) +{ + return erofs_inode_datalayout(cino) == EROFS_INODE_FLAT_INLINE || + erofs_inode_datalayout(cino) == EROFS_INODE_FLAT_PLAIN; +} + +static inline size_t erofs_xattr_inode_size(uint16_t xattr_icount) +{ + size_t xattr_size = 0; + if (xattr_icount > 0) + xattr_size = sizeof(struct erofs_xattr_ibody_header) + + (xattr_icount - 1) * 4; + return xattr_size; +} + +#define EROFS_N_XATTR_PREFIXES (sizeof(erofs_xattr_prefixes) / sizeof(char *)) + +static inline bool erofs_is_acl_xattr(int prefix, const char *name, size_t name_len) +{ + const char *const nfs_acl = "system.nfs4_acl"; + + if ((prefix == EROFS_XATTR_INDEX_POSIX_ACL_ACCESS || + prefix == EROFS_XATTR_INDEX_POSIX_ACL_DEFAULT) && + name_len == 0) + return true; + if (prefix == 0 && name_len == strlen(nfs_acl) && + memcmp(name, nfs_acl, strlen(nfs_acl)) == 0) + return true; + return false; +} + +static inline int erofs_get_xattr_prefix(const char *str) +{ + for (int i = 1; i < EROFS_N_XATTR_PREFIXES; i++) { + const char *prefix = erofs_xattr_prefixes[i]; + if (strlen(str) >= strlen(prefix) && + memcmp(str, prefix, strlen(prefix)) == 0) { + return i; + } + } + return 0; +} + +static inline char *erofs_get_xattr_name(uint8_t index, const char *name, + size_t name_len) +{ + char *res; + const char *prefix; + size_t prefix_len; + + if (index >= EROFS_N_XATTR_PREFIXES) { + errno = EINVAL; + return NULL; + } + + prefix = erofs_xattr_prefixes[index]; + prefix_len = strlen(prefix); + + res = malloc(prefix_len + name_len + 1); + if (res == NULL) { + errno = ENOMEM; + return NULL; + } + memcpy(res, prefix, prefix_len); + memcpy(res + prefix_len, name, name_len); + res[prefix_len + name_len] = 0; + + return res; +} + +#endif diff --git a/libcomposefs/lcfs-internal.h b/libcomposefs/lcfs-internal.h index 3e4f32fe..f9cab47b 100644 --- a/libcomposefs/lcfs-internal.h +++ b/libcomposefs/lcfs-internal.h @@ -29,6 +29,28 @@ */ #define LCFS_BUILD_INLINE_FILE_SIZE_LIMIT 64 +#define OVERLAY_XATTR_USER_PREFIX "user." +#define OVERLAY_XATTR_TRUSTED_PREFIX "trusted." +#define OVERLAY_XATTR_PARTIAL_PREFIX "overlay." +#define OVERLAY_XATTR_PREFIX \ + OVERLAY_XATTR_TRUSTED_PREFIX OVERLAY_XATTR_PARTIAL_PREFIX +#define OVERLAY_XATTR_USERXATTR_PREFIX \ + OVERLAY_XATTR_USER_PREFIX OVERLAY_XATTR_PARTIAL_PREFIX +#define OVERLAY_XATTR_ESCAPE_PREFIX OVERLAY_XATTR_PREFIX "overlay." +#define OVERLAY_XATTR_METACOPY OVERLAY_XATTR_PREFIX "metacopy" +#define OVERLAY_XATTR_REDIRECT OVERLAY_XATTR_PREFIX "redirect" +#define OVERLAY_XATTR_WHITEOUT OVERLAY_XATTR_PREFIX "whiteout" +#define OVERLAY_XATTR_WHITEOUTS OVERLAY_XATTR_PREFIX "whiteouts" +#define OVERLAY_XATTR_OPAQUE OVERLAY_XATTR_PREFIX "opaque" + +#define OVERLAY_XATTR_ESCAPED_WHITEOUT OVERLAY_XATTR_ESCAPE_PREFIX "whiteout" +#define OVERLAY_XATTR_ESCAPED_WHITEOUTS OVERLAY_XATTR_ESCAPE_PREFIX "whiteouts" + +#define OVERLAY_XATTR_USERXATTR_WHITEOUT \ + OVERLAY_XATTR_USERXATTR_PREFIX "whiteout" +#define OVERLAY_XATTR_USERXATTR_WHITEOUTS \ + OVERLAY_XATTR_USERXATTR_PREFIX "whiteouts" + #define ALIGN_TO(_offset, _align_size) \ (((_offset) + _align_size - 1) & ~(_align_size - 1)) @@ -147,6 +169,15 @@ struct lcfs_ctx_s { void (*finalize)(struct lcfs_ctx_s *ctx); }; +static inline void lcfs_node_unrefp(struct lcfs_node_s **nodep) +{ + if (*nodep != NULL) { + lcfs_node_unref(*nodep); + *nodep = NULL; + } +} +#define cleanup_node __attribute__((cleanup(lcfs_node_unrefp))) + /* lcfs-writer.c */ size_t hash_memory(const char *string, size_t len, size_t n_buckets); int lcfs_write(struct lcfs_ctx_s *ctx, void *_data, size_t data_len); diff --git a/libcomposefs/lcfs-utils.h b/libcomposefs/lcfs-utils.h index 22a24dfc..e493d6d7 100644 --- a/libcomposefs/lcfs-utils.h +++ b/libcomposefs/lcfs-utils.h @@ -19,12 +19,41 @@ #include #include +#include #include #include #define max(a, b) ((a > b) ? (a) : (b)) #define min(a, b) (((a) < (b)) ? (a) : (b)) +static inline bool str_has_prefix(const char *str, const char *prefix) +{ + return strncmp(str, prefix, strlen(prefix)) == 0; +} + +static inline char *memdup(const char *s, size_t len) +{ + char *s2 = malloc(len); + if (s2 == NULL) { + errno = ENOMEM; + return NULL; + } + memcpy(s2, s, len); + return s2; +} + +static inline char *str_join(const char *a, const char *b) +{ + size_t a_len = strlen(a); + size_t b_len = strlen(b); + char *res = malloc(a_len + b_len + 1); + if (res) { + memcpy(res, a, a_len); + memcpy(res + a_len, b, b_len + 1); + } + return res; +} + static inline void _lcfs_reset_errno_(int *saved_errno) { if (*saved_errno < 0) diff --git a/libcomposefs/lcfs-writer-erofs.c b/libcomposefs/lcfs-writer-erofs.c index f6039f13..c0617039 100644 --- a/libcomposefs/lcfs-writer-erofs.c +++ b/libcomposefs/lcfs-writer-erofs.c @@ -22,7 +22,7 @@ #include "lcfs-utils.h" #include "lcfs-writer.h" #include "lcfs-fsverity.h" -#include "lcfs-erofs.h" +#include "lcfs-erofs-internal.h" #include "lcfs-utils.h" #include "hash.h" @@ -187,8 +187,6 @@ struct lcfs_ctx_s *lcfs_ctx_erofs_new(void) return &ret->base; } -#include "erofs_fs_wrapper.h" - static int erofs_make_file_type(int regular) { switch (regular) { @@ -258,11 +256,6 @@ static int xattrs_ht_sort(const void *d1, const void *d2) return memcmp(v2->xattr->value, v1->xattr->value, v1->xattr->value_len); } -static bool str_has_prefix(const char *str, const char *prefix) -{ - return strncmp(str, prefix, strlen(prefix)) == 0; -} - static uint8_t xattr_erofs_entry_index(struct lcfs_xattr_s *xattr, char **rest) { char *key = xattr->key; @@ -323,11 +316,6 @@ static bool erofs_xattr_should_be_shared(struct hasher_xattr_s *ent) if (ent->count > 1) return true; - /* Also share verity overlay xattrs, as they are kind - of large to have inline, and not always accessed. */ - if (strcmp(ent->xattr->key, "trusted.overlay.verity") == 0) - return true; - return false; } @@ -868,6 +856,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); } @@ -1061,18 +1055,6 @@ static int write_erofs_shared_xattrs(struct lcfs_ctx_s *ctx) return 0; } -static char *str_join(const char *a, const char *b) -{ - size_t a_len = strlen(a); - size_t b_len = strlen(b); - char *res = malloc(a_len + b_len + 1); - if (res) { - memcpy(res, a, a_len); - memcpy(res + a_len, b, b_len + 1); - } - return res; -} - static int add_overlayfs_xattrs(struct lcfs_node_s *node) { int type = node->inode.st_mode & S_IFMT; @@ -1082,10 +1064,10 @@ static int add_overlayfs_xattrs(struct lcfs_node_s *node) for (size_t i = 0; i < lcfs_node_get_n_xattr(node); i++) { const char *name = lcfs_node_get_xattr_name(node, i); - if (str_has_prefix(name, "trusted.overlay.")) { + if (str_has_prefix(name, OVERLAY_XATTR_PREFIX)) { cleanup_free char *renamed = - str_join("trusted.overlay.overlay.", - name + strlen("trusted.overlay.")); + str_join(OVERLAY_XATTR_ESCAPE_PREFIX, + name + strlen(OVERLAY_XATTR_PREFIX)); if (renamed == NULL) { errno = ENOMEM; return -1; @@ -1111,7 +1093,7 @@ static int add_overlayfs_xattrs(struct lcfs_node_s *node) memcpy(xattr_data + 4, node->digest, LCFS_DIGEST_SIZE); } - ret = lcfs_node_set_xattr(node, "trusted.overlay.metacopy", + ret = lcfs_node_set_xattr(node, OVERLAY_XATTR_METACOPY, (const char *)xattr_data, xattr_len); if (ret < 0) return ret; @@ -1122,7 +1104,7 @@ static int add_overlayfs_xattrs(struct lcfs_node_s *node) errno = ENOMEM; return -1; } - ret = lcfs_node_set_xattr(node, "trusted.overlay.redirect", + ret = lcfs_node_set_xattr(node, OVERLAY_XATTR_REDIRECT, path, strlen(path)); free(path); if (ret < 0) @@ -1136,14 +1118,22 @@ static int add_overlayfs_xattrs(struct lcfs_node_s *node) lcfs_node_set_mode(node, S_IFREG | (lcfs_node_get_mode(node) & ~S_IFMT)); - ret = lcfs_node_set_xattr(node, "trusted.overlay.overlay.whiteout", + ret = lcfs_node_set_xattr(node, OVERLAY_XATTR_ESCAPED_WHITEOUT, + "", 0); + if (ret < 0) + return ret; + ret = lcfs_node_set_xattr(node, OVERLAY_XATTR_USERXATTR_WHITEOUT, "", 0); if (ret < 0) return ret; /* Mark parent dir containing whiteouts */ - ret = lcfs_node_set_xattr( - parent, "trusted.overlay.overlay.whiteouts", "", 0); + ret = lcfs_node_set_xattr(parent, + OVERLAY_XATTR_ESCAPED_WHITEOUTS, "", 0); + if (ret < 0) + return ret; + ret = lcfs_node_set_xattr(parent, OVERLAY_XATTR_USERXATTR_WHITEOUTS, + "", 0); if (ret < 0) return ret; } @@ -1268,7 +1258,7 @@ static int set_overlay_opaque(struct lcfs_node_s *node) { int ret; - ret = lcfs_node_set_xattr(node, "trusted.overlay.opaque", "y", 1); + ret = lcfs_node_set_xattr(node, OVERLAY_XATTR_OPAQUE, "y", 1); if (ret < 0) return ret; @@ -1417,3 +1407,474 @@ int lcfs_write_erofs_to(struct lcfs_ctx_s *ctx) return 0; } + +struct hasher_node_s { + uint64_t nid; + struct lcfs_node_s *node; +}; + +struct lcfs_image_data { + const uint8_t *erofs_data; + size_t erofs_data_size; + const uint8_t *erofs_metadata; + const uint8_t *erofs_metadata_end; + const uint8_t *erofs_xattrdata; + const uint8_t *erofs_xattrdata_end; + uint64_t erofs_build_time; + uint32_t erofs_build_time_nsec; + Hash_table *node_hash; +}; + +static const erofs_inode *lcfs_image_get_erofs_inode(struct lcfs_image_data *data, + uint64_t nid) +{ + const uint8_t *inode_data = data->erofs_metadata + (nid << EROFS_ISLOTBITS); + + if (inode_data >= data->erofs_metadata_end) + return NULL; + + return (const erofs_inode *)inode_data; +} + +static struct lcfs_node_s *lcfs_build_node_from_image(struct lcfs_image_data *data, + uint64_t nid); + +static int erofs_readdir_block(struct lcfs_image_data *data, + struct lcfs_node_s *parent, const uint8_t *block, + size_t block_size) +{ + const struct erofs_dirent *dirents = (struct erofs_dirent *)block; + size_t dirents_size = lcfs_u16_from_file(dirents[0].nameoff); + size_t n_dirents, i; + + if (dirents_size % sizeof(struct erofs_dirent) != 0) { + /* This should not happen for valid filesystems */ + errno = EINVAL; + return -1; + } + + n_dirents = dirents_size / sizeof(struct erofs_dirent); + + for (i = 0; i < n_dirents; i++) { + char name_buf[PATH_MAX]; + uint64_t nid = lcfs_u64_from_file(dirents[i].nid); + uint16_t nameoff = lcfs_u16_from_file(dirents[i].nameoff); + const char *child_name; + uint16_t child_name_len; + cleanup_node struct lcfs_node_s *child = NULL; + + /* Compute length of the name, which is a bit weird for the last dirent */ + child_name = (char *)(block + nameoff); + if (i + 1 < n_dirents) + child_name_len = + lcfs_u16_from_file(dirents[i + 1].nameoff) - nameoff; + else + child_name_len = strnlen(child_name, block_size - nameoff); + + if ((child_name_len == 1 && child_name[0] == '.') || + (child_name_len == 2 && child_name[0] == '.' && + child_name[1] == '.')) + continue; + + /* Copy to null terminate */ + child_name_len = min(child_name_len, PATH_MAX - 1); + memcpy(name_buf, child_name, child_name_len); + name_buf[child_name_len] = 0; + + child = lcfs_build_node_from_image(data, nid); + if (child == NULL) { + if (errno == ENOTSUP) + continue; /* Skip real whiteouts (00-ff) */ + } + + if (lcfs_node_add_child(parent, child, /* Takes ownership on success */ + name_buf) < 0) + return -1; + steal_pointer(&child); + } + + return 0; +} + +static int lcfs_build_node_erofs_xattr(struct lcfs_node_s *node, uint8_t name_index, + const char *entry_name, uint8_t name_len, + const char *value, uint16_t value_size) +{ + cleanup_free char *name = + erofs_get_xattr_name(name_index, entry_name, name_len); + if (name == NULL) + return -1; + + if (strcmp(name, OVERLAY_XATTR_REDIRECT) == 0) { + if ((node->inode.st_mode & S_IFMT) == S_IFREG) { + if (value_size > 1 && value[0] == '/') { + value_size++; + value++; + } + node->payload = strndup(value, value_size); + if (node->payload == NULL) { + errno = EINVAL; + return -1; + } + } + return 0; + } + + if (strcmp(name, OVERLAY_XATTR_METACOPY) == 0) { + if ((node->inode.st_mode & S_IFMT) == S_IFREG && + value_size == 4 + LCFS_DIGEST_SIZE) + lcfs_node_set_fsverity_digest(node, (uint8_t *)value + 4); + return 0; + } + + if (strcmp(name, OVERLAY_XATTR_ESCAPED_WHITEOUT) == 0 && + (node->inode.st_mode & S_IFMT) == S_IFREG) { + /* Rewrite to regular whiteout */ + node->inode.st_mode = (node->inode.st_mode & ~S_IFMT) | S_IFCHR; + node->inode.st_rdev = makedev(0, 0); + node->inode.st_size = 0; + return 0; + } + if (strcmp(name, OVERLAY_XATTR_ESCAPED_WHITEOUTS) == 0 || + strcmp(name, OVERLAY_XATTR_USERXATTR_WHITEOUT) == 0 || + strcmp(name, OVERLAY_XATTR_USERXATTR_WHITEOUTS) == 0) { + /* skip */ + return 0; + } + + if (str_has_prefix(name, OVERLAY_XATTR_PREFIX)) { + if (str_has_prefix(name, OVERLAY_XATTR_ESCAPE_PREFIX)) { + /* Unescape */ + memmove(name + strlen(OVERLAY_XATTR_TRUSTED_PREFIX), + name + strlen(OVERLAY_XATTR_PREFIX), + strlen(name) - strlen(OVERLAY_XATTR_PREFIX) + 1); + } else { + /* skip */ + return 0; + } + } + + if (lcfs_node_set_xattr(node, name, value, value_size) < 0) + return -1; + + return 0; +} + +static struct lcfs_node_s *lcfs_build_node_from_image(struct lcfs_image_data *data, + uint64_t nid) +{ + const erofs_inode *cino; + cleanup_node struct lcfs_node_s *node = NULL; + uint64_t file_size; + uint16_t xattr_icount; + uint32_t raw_blkaddr; + int type; + size_t isize; + bool tailpacked; + size_t xattr_size; + struct hasher_node_s ht_entry = { nid }; + struct hasher_node_s *new_ht_entry; + struct hasher_node_s *existing; + uint64_t n_blocks; + uint64_t last_oob_block; + size_t tail_size; + const uint8_t *tail_data; + const uint8_t *oob_data; + + cino = lcfs_image_get_erofs_inode(data, nid); + if (cino == NULL) + return NULL; + + node = lcfs_node_new(); + if (node == NULL) { + errno = ENOMEM; + return NULL; + } + + existing = hash_lookup(data->node_hash, &ht_entry); + if (existing) { + node->link_to = lcfs_node_ref(existing->node); + return steal_pointer(&node); + } + + new_ht_entry = malloc(sizeof(struct hasher_node_s)); + if (new_ht_entry == NULL) { + errno = ENOMEM; + return NULL; + } + new_ht_entry->nid = nid; + new_ht_entry->node = node; + if (hash_insert(data->node_hash, new_ht_entry) == NULL) { + errno = ENOMEM; + return NULL; + } + + if (erofs_inode_is_compact(cino)) { + const struct erofs_inode_compact *c = &cino->compact; + + node->inode.st_mode = lcfs_u16_from_file(c->i_mode); + node->inode.st_nlink = lcfs_u16_from_file(c->i_nlink); + node->inode.st_size = lcfs_u32_from_file(c->i_size); + node->inode.st_uid = lcfs_u16_from_file(c->i_uid); + node->inode.st_gid = lcfs_u16_from_file(c->i_gid); + + node->inode.st_mtim_sec = data->erofs_build_time; + node->inode.st_mtim_nsec = data->erofs_build_time_nsec; + + type = node->inode.st_mode & S_IFMT; + + if (type == S_IFCHR || type == S_IFBLK) + node->inode.st_rdev = lcfs_u32_from_file(c->i_u.rdev); + + file_size = lcfs_u32_from_file(c->i_size); + xattr_icount = lcfs_u16_from_file(c->i_xattr_icount); + raw_blkaddr = lcfs_u32_from_file(c->i_u.raw_blkaddr); + isize = sizeof(struct erofs_inode_compact); + + } else { + const struct erofs_inode_extended *e = &cino->extended; + + node->inode.st_mode = lcfs_u16_from_file(e->i_mode); + node->inode.st_size = lcfs_u64_from_file(e->i_size); + node->inode.st_uid = lcfs_u32_from_file(e->i_uid); + node->inode.st_gid = lcfs_u32_from_file(e->i_gid); + node->inode.st_mtim_sec = lcfs_u64_from_file(e->i_mtime); + node->inode.st_mtim_nsec = lcfs_u32_from_file(e->i_mtime_nsec); + node->inode.st_nlink = lcfs_u32_from_file(e->i_nlink); + + type = node->inode.st_mode & S_IFMT; + + if (type == S_IFCHR || type == S_IFBLK) + node->inode.st_rdev = lcfs_u32_from_file(e->i_u.rdev); + + file_size = lcfs_u64_from_file(e->i_size); + xattr_icount = lcfs_u16_from_file(e->i_xattr_icount); + raw_blkaddr = lcfs_u32_from_file(e->i_u.raw_blkaddr); + isize = sizeof(struct erofs_inode_extended); + } + + if (type == S_IFCHR && node->inode.st_rdev == 0) { + errno = ENOTSUP; /* Use this to signal that we found a whiteout */ + return NULL; + } + + xattr_size = erofs_xattr_inode_size(xattr_icount); + + tailpacked = erofs_inode_is_tailpacked(cino); + tail_size = tailpacked ? file_size % EROFS_BLKSIZ : 0; + tail_data = ((uint8_t *)cino) + isize + xattr_size; + oob_data = data->erofs_data + raw_blkaddr * EROFS_BLKSIZ; + + n_blocks = round_up(file_size, EROFS_BLKSIZ) / EROFS_BLKSIZ; + last_oob_block = tailpacked ? n_blocks - 1 : n_blocks; + + if (type == S_IFDIR) { + /* First read the out-of-band blocks */ + for (uint64_t block = 0; block < last_oob_block; block++) { + const uint8_t *block_data = oob_data + block * EROFS_BLKSIZ; + size_t block_size = EROFS_BLKSIZ; + + if (!tailpacked && block + 1 == last_oob_block) { + block_size = file_size % EROFS_BLKSIZ; + if (block_size == 0) { + block_size = EROFS_BLKSIZ; + } + } + + if (erofs_readdir_block(data, node, block_data, block_size) < 0) + return NULL; + } + + /* Then inline */ + if (tailpacked) { + if (erofs_readdir_block(data, node, tail_data, tail_size) < 0) + return NULL; + } + + } else if (type == S_IFLNK) { + char name_buf[PATH_MAX]; + + if (file_size >= PATH_MAX || !tailpacked) { + errno = -EINVAL; + return NULL; + } + + memcpy(name_buf, tail_data, file_size); + name_buf[file_size] = 0; + if (lcfs_node_set_payload(node, name_buf) < 0) + return NULL; + + } else if (type == S_IFREG && file_size != 0 && erofs_inode_is_flat(cino)) { + cleanup_free uint8_t *content = NULL; + size_t oob_size; + + content = malloc(file_size); + if (content == NULL) { + errno = ENOMEM; + return NULL; + } + + oob_size = tailpacked ? last_oob_block * EROFS_BLKSIZ : file_size; + memcpy(content, data->erofs_data + raw_blkaddr * EROFS_BLKSIZ, + oob_size); + if (tailpacked) + memcpy(content + oob_size, tail_data, tail_size); + + lcfs_node_set_content(node, content, file_size); + } + + if (xattr_icount > 0) { + const struct erofs_xattr_ibody_header *xattr_header; + const uint8_t *xattrs_inline; + const uint8_t *xattrs_start; + const uint8_t *xattrs_end; + uint8_t shared_count; + + xattrs_start = ((uint8_t *)cino) + isize; + xattrs_end = ((uint8_t *)cino) + isize + xattr_size; + xattr_header = (struct erofs_xattr_ibody_header *)xattrs_start; + shared_count = xattr_header->h_shared_count; + + xattrs_inline = xattrs_start + + sizeof(struct erofs_xattr_ibody_header) + + shared_count * 4; + + /* Inline xattrs */ + while (xattrs_inline + sizeof(struct erofs_xattr_entry) < xattrs_end) { + const struct erofs_xattr_entry *entry = + (const struct erofs_xattr_entry *)xattrs_inline; + const char *entry_data = (const char *)entry + + sizeof(struct erofs_xattr_entry); + const char *entry_name = entry_data; + uint8_t name_len = entry->e_name_len; + uint8_t name_index = entry->e_name_index; + const char *value = entry_data + name_len; + uint16_t value_size = + lcfs_u16_from_file(entry->e_value_size); + size_t el_size = round_up(sizeof(struct erofs_xattr_entry) + + name_len + value_size, + 4); + + if (lcfs_build_node_erofs_xattr(node, name_index, + entry_name, name_len, + value, value_size) < 0) + return NULL; + + xattrs_inline += el_size; + } + + /* Shared xattrs */ + for (int i = 0; i < shared_count; i++) { + uint32_t idx = lcfs_u32_from_file( + xattr_header->h_shared_xattrs[i]); + const struct erofs_xattr_entry *entry = + (const struct erofs_xattr_entry *)(data->erofs_xattrdata + + idx * 4); + const char *entry_data = (const char *)entry + + sizeof(struct erofs_xattr_entry); + const char *entry_name = entry_data; + uint8_t name_len = entry->e_name_len; + uint8_t name_index = entry->e_name_index; + const char *value = entry_data + name_len; + uint16_t value_size = + lcfs_u16_from_file(entry->e_value_size); + + if (lcfs_build_node_erofs_xattr(node, name_index, + entry_name, name_len, + value, value_size) < 0) + return NULL; + } + } + + return steal_pointer(&node); +} + +static size_t node_ht_hasher(const void *d, size_t n) +{ + const struct hasher_node_s *v = d; + return v->nid % n; +} + +static bool node_ht_comparator(const void *d1, const void *d2) +{ + const struct hasher_node_s *v1 = d1; + const struct hasher_node_s *v2 = d2; + + return v1->nid == v2->nid; +} + +struct lcfs_node_s *lcfs_load_node_from_image(const uint8_t *image_data, + size_t image_data_size) +{ + const uint8_t *image_data_end; + struct lcfs_image_data data = { image_data, image_data_size }; + const struct lcfs_erofs_header_s *cfs_header; + const struct erofs_super_block *erofs_super; + uint64_t erofs_root_nid; + struct lcfs_node_s *root; + + if (image_data_size < EROFS_BLKSIZ) { + errno = EINVAL; + return NULL; + } + + /* Avoid wrapping */ + image_data_end = image_data + image_data_size; + if (image_data_end < image_data) { + errno = EINVAL; + return NULL; + } + + cfs_header = (struct lcfs_erofs_header_s *)(image_data); + if (lcfs_u32_from_file(cfs_header->magic) != LCFS_EROFS_MAGIC) { + errno = EINVAL; /* Wrong cfs magic */ + return NULL; + } + + if (lcfs_u32_from_file(cfs_header->version) != LCFS_EROFS_VERSION) { + errno = ENOTSUP; /* Wrong cfs version */ + return NULL; + } + + erofs_super = (struct erofs_super_block *)(image_data + EROFS_SUPER_OFFSET); + + if (lcfs_u32_from_file(erofs_super->magic) != EROFS_SUPER_MAGIC_V1) { + errno = EINVAL; /* Wrong erofs magic */ + return NULL; + } + + data.erofs_metadata = + image_data + + lcfs_u32_from_file(erofs_super->meta_blkaddr) * EROFS_BLKSIZ; + data.erofs_xattrdata = + image_data + + lcfs_u32_from_file(erofs_super->xattr_blkaddr) * EROFS_BLKSIZ; + + if (data.erofs_metadata >= image_data_end || + data.erofs_xattrdata >= image_data_end) { + errno = EINVAL; + return NULL; + } + + data.erofs_metadata_end = image_data_end; + data.erofs_xattrdata_end = image_data_end; + + data.erofs_build_time = lcfs_u64_from_file(erofs_super->build_time); + data.erofs_build_time_nsec = + lcfs_u32_from_file(erofs_super->build_time_nsec); + + erofs_root_nid = lcfs_u16_from_file(erofs_super->root_nid); + + data.node_hash = + hash_initialize(0, NULL, node_ht_hasher, node_ht_comparator, free); + if (data.node_hash == NULL) { + errno = ENOMEM; + return NULL; + } + + root = lcfs_build_node_from_image(&data, erofs_root_nid); + + hash_free(data.node_hash); + + return root; +} diff --git a/libcomposefs/lcfs-writer.c b/libcomposefs/lcfs-writer.c index caada52f..13a8e06a 100644 --- a/libcomposefs/lcfs-writer.c +++ b/libcomposefs/lcfs-writer.c @@ -34,6 +34,7 @@ #include #include #include +#include static void lcfs_node_remove_all_children(struct lcfs_node_s *node); static void lcfs_node_destroy(struct lcfs_node_s *node); @@ -61,17 +62,6 @@ char *maybe_join_path(const char *a, const char *b) return res; } -static char *memdup(const char *s, size_t len) -{ - char *s2 = malloc(len); - if (s2 == NULL) { - errno = ENOMEM; - return NULL; - } - memcpy(s2, s, len); - return s2; -} - size_t hash_memory(const char *string, size_t len, size_t n_buckets) { size_t i, value = 0; @@ -567,7 +557,7 @@ static int read_content(int fd, size_t size, uint8_t *buf) struct lcfs_node_s *lcfs_load_node_from_file(int dirfd, const char *fname, int buildflags) { - struct lcfs_node_s *ret; + cleanup_node struct lcfs_node_s *ret = NULL; struct stat sb; int r; @@ -603,16 +593,12 @@ struct lcfs_node_s *lcfs_load_node_from_file(int dirfd, const char *fname, if (do_digest || do_inline) { cleanup_fd int fd = openat(dirfd, fname, O_RDONLY | O_CLOEXEC); - if (fd < 0) { - lcfs_node_unref(ret); + if (fd < 0) return NULL; - } if (do_digest) { r = lcfs_node_set_fsverity_from_fd(ret, fd); - if (r < 0) { - lcfs_node_unref(ret); + if (r < 0) return NULL; - } /* In case we re-read below */ lseek(fd, 0, SEEK_SET); } @@ -620,15 +606,11 @@ struct lcfs_node_s *lcfs_load_node_from_file(int dirfd, const char *fname, uint8_t buf[LCFS_BUILD_INLINE_FILE_SIZE_LIMIT]; r = read_content(fd, sb.st_size, buf); - if (r < 0) { - lcfs_node_unref(ret); + if (r < 0) return NULL; - } r = lcfs_node_set_content(ret, buf, sb.st_size); - if (r < 0) { - lcfs_node_unref(ret); + if (r < 0) return NULL; - } } } } @@ -640,13 +622,45 @@ struct lcfs_node_s *lcfs_load_node_from_file(int dirfd, const char *fname, if ((buildflags & LCFS_BUILD_SKIP_XATTRS) == 0) { r = read_xattrs(ret, dirfd, fname); - if (r < 0) { - lcfs_node_unref(ret); + if (r < 0) return NULL; - } } - return ret; + return steal_pointer(&ret); +} + +struct lcfs_node_s *lcfs_load_node_from_fd(int fd) +{ + struct lcfs_node_s *node; + uint8_t *image_data; + size_t image_data_size; + struct stat s; + int errsv; + int r; + + r = fstat(fd, &s); + if (r < 0) { + return NULL; + } + + image_data_size = s.st_size; + + image_data = mmap(0, image_data_size, PROT_READ, MAP_PRIVATE, fd, 0); + if (image_data == MAP_FAILED) { + return NULL; + } + + node = lcfs_load_node_from_image(image_data, image_data_size); + if (node == NULL) { + errsv = errno; + munmap(image_data, image_data_size); + errno = errsv; + return NULL; + } + + munmap(image_data, image_data_size); + + return node; } int lcfs_node_set_payload(struct lcfs_node_s *node, const char *payload) @@ -662,6 +676,11 @@ int lcfs_node_set_payload(struct lcfs_node_s *node, const char *payload) return 0; } +const char *lcfs_node_get_payload(struct lcfs_node_s *node) +{ + return node->payload; +} + const uint8_t *lcfs_node_get_fsverity_digest(struct lcfs_node_s *node) { if (node->digest_set) @@ -823,6 +842,11 @@ void lcfs_node_make_hardlink(struct lcfs_node_s *node, struct lcfs_node_s *targe target->inode.st_nlink++; } +struct lcfs_node_s *lcfs_node_get_hardlink_target(struct lcfs_node_s *node) +{ + return node->link_to; +} + int lcfs_node_add_child(struct lcfs_node_s *parent, struct lcfs_node_s *child, const char *name) { @@ -939,7 +963,7 @@ static void lcfs_node_destroy(struct lcfs_node_s *node) struct lcfs_node_s *lcfs_node_clone(struct lcfs_node_s *node) { - struct lcfs_node_s *new = lcfs_node_new(); + cleanup_node struct lcfs_node_s *new = lcfs_node_new(); if (new == NULL) return NULL; @@ -953,20 +977,22 @@ struct lcfs_node_s *lcfs_node_clone(struct lcfs_node_s *node) if (node->payload) { new->payload = strdup(node->payload); if (new->payload == NULL) - goto fail; + return NULL; + ; } if (node->content) { new->content = malloc(node->inode.st_size); if (new->content == NULL) - goto fail; + return NULL; + ; 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) - goto fail; + return NULL; for (size_t i = 0; i < node->n_xattrs; i++) { char *key = strdup(node->xattrs[i].key); char *value = memdup(node->xattrs[i].value, @@ -974,7 +1000,7 @@ struct lcfs_node_s *lcfs_node_clone(struct lcfs_node_s *node) if (key == NULL || value == NULL) { free(key); free(value); - goto fail; + return NULL; } new->xattrs[i].key = key; new->xattrs[i].value = value; @@ -987,11 +1013,7 @@ struct lcfs_node_s *lcfs_node_clone(struct lcfs_node_s *node) memcpy(new->digest, node->digest, LCFS_DIGEST_SIZE); new->inode = node->inode; - return new; - -fail: - lcfs_node_unref(new); - return NULL; + return steal_pointer(&new); } struct lcfs_node_mapping_s { @@ -1008,7 +1030,7 @@ struct lcfs_clone_data { static struct lcfs_node_s *_lcfs_node_clone_deep(struct lcfs_node_s *node, struct lcfs_clone_data *data) { - struct lcfs_node_s *new = lcfs_node_clone(node); + cleanup_node struct lcfs_node_s *new = lcfs_node_clone(node); if (new == NULL) return NULL; @@ -1021,7 +1043,7 @@ static struct lcfs_node_s *_lcfs_node_clone_deep(struct lcfs_node_s *node, sizeof(struct lcfs_node_mapping_s), data->allocated_mappings); if (new_mapping == NULL) - goto fail; + return NULL; data->mapping = new_mapping; } @@ -1033,17 +1055,13 @@ static struct lcfs_node_s *_lcfs_node_clone_deep(struct lcfs_node_s *node, struct lcfs_node_s *child = node->children[i]; struct lcfs_node_s *new_child = _lcfs_node_clone_deep(child, data); if (new_child == NULL) - goto fail; + return NULL; if (lcfs_node_add_child(new, new_child, child->name) < 0) - goto fail; + return NULL; } - return new; - -fail: - lcfs_node_unref(new); - return NULL; + return steal_pointer(&new); } /* Rewrite all hardlinks according to mapping */ diff --git a/libcomposefs/lcfs-writer.h b/libcomposefs/lcfs-writer.h index 8fc60b9d..136de48b 100644 --- a/libcomposefs/lcfs-writer.h +++ b/libcomposefs/lcfs-writer.h @@ -67,6 +67,9 @@ LCFS_EXTERN struct lcfs_node_s *lcfs_node_clone(struct lcfs_node_s *node); LCFS_EXTERN struct lcfs_node_s *lcfs_node_clone_deep(struct lcfs_node_s *node); LCFS_EXTERN struct lcfs_node_s *lcfs_load_node_from_file(int dirfd, const char *fname, int buildflags); +LCFS_EXTERN struct lcfs_node_s *lcfs_load_node_from_image(const uint8_t *image_data, + size_t image_data_size); +LCFS_EXTERN struct lcfs_node_s *lcfs_load_node_from_fd(int fd); LCFS_EXTERN const char *lcfs_node_get_xattr(struct lcfs_node_s *node, const char *name, size_t *length); @@ -78,6 +81,7 @@ LCFS_EXTERN const char *lcfs_node_get_xattr_name(struct lcfs_node_s *node, size_t index); LCFS_EXTERN int lcfs_node_set_payload(struct lcfs_node_s *node, const char *payload); +LCFS_EXTERN const char *lcfs_node_get_payload(struct lcfs_node_s *node); LCFS_EXTERN int lcfs_node_set_content(struct lcfs_node_s *node, const uint8_t *data, size_t data_size); @@ -95,6 +99,7 @@ LCFS_EXTERN struct lcfs_node_s *lcfs_node_get_child(struct lcfs_node_s *node, size_t i); LCFS_EXTERN void lcfs_node_make_hardlink(struct lcfs_node_s *node, struct lcfs_node_s *target); +LCFS_EXTERN struct lcfs_node_s *lcfs_node_get_hardlink_target(struct lcfs_node_s *node); LCFS_EXTERN bool lcfs_node_dirp(struct lcfs_node_s *node); LCFS_EXTERN uint32_t lcfs_node_get_mode(struct lcfs_node_s *node); diff --git a/tests/Makefile.am b/tests/Makefile.am new file mode 100644 index 00000000..127d39cf --- /dev/null +++ b/tests/Makefile.am @@ -0,0 +1,20 @@ +TEST_ASSETS_SMALL = \ + config.json.gz config-with-hard-link.json.gz special.json + +TEST_ASSETS = ${TEST_ASSETS_SMALL} \ + cs9-x86_64-developer.json.gz cs9-x86_64-minimal.json.gz \ + f36-x86_64-silverblue.json.gz + +if ENABLE_VALGRIND +VALGRIND_PREFIX=libtool --mode=execute ${VALGRIND} --quiet --leak-check=yes --error-exitcode=42 +endif + +EXTRA_DIST = test-checksums.sh $(patsubst %,assets/%,${TEST_ASSETS_SMALL}) $(patsubst %,assets/%.sha256_erofs,${TEST_ASSETS_SMALL}) + +check-checksums: + $(srcdir)/test-checksums.sh "${VALGRIND_PREFIX} $(builddir)/../tools/" "$(srcdir)/assets" "${TEST_ASSETS}" + +check-units: + $(srcdir)/test-units.sh "${VALGRIND_PREFIX} $(builddir)/../tools/" + +check: check-units check-checksums diff --git a/tools/test-assets/config-with-hard-link.json.gz b/tests/assets/config-with-hard-link.json.gz similarity index 100% rename from tools/test-assets/config-with-hard-link.json.gz rename to tests/assets/config-with-hard-link.json.gz diff --git a/tools/test-assets/config-with-hard-link.json.gz.sha256_erofs b/tests/assets/config-with-hard-link.json.gz.sha256_erofs similarity index 100% rename from tools/test-assets/config-with-hard-link.json.gz.sha256_erofs rename to tests/assets/config-with-hard-link.json.gz.sha256_erofs diff --git a/tools/test-assets/config.json.gz b/tests/assets/config.json.gz similarity index 100% rename from tools/test-assets/config.json.gz rename to tests/assets/config.json.gz diff --git a/tools/test-assets/config.json.gz.sha256_erofs b/tests/assets/config.json.gz.sha256_erofs similarity index 100% rename from tools/test-assets/config.json.gz.sha256_erofs rename to tests/assets/config.json.gz.sha256_erofs diff --git a/tools/test-assets/cs9-x86_64-developer.json.gz b/tests/assets/cs9-x86_64-developer.json.gz similarity index 100% rename from tools/test-assets/cs9-x86_64-developer.json.gz rename to tests/assets/cs9-x86_64-developer.json.gz diff --git a/tools/test-assets/cs9-x86_64-developer.json.gz.sha256_erofs b/tests/assets/cs9-x86_64-developer.json.gz.sha256_erofs similarity index 100% rename from tools/test-assets/cs9-x86_64-developer.json.gz.sha256_erofs rename to tests/assets/cs9-x86_64-developer.json.gz.sha256_erofs diff --git a/tools/test-assets/cs9-x86_64-minimal.json.gz b/tests/assets/cs9-x86_64-minimal.json.gz similarity index 100% rename from tools/test-assets/cs9-x86_64-minimal.json.gz rename to tests/assets/cs9-x86_64-minimal.json.gz diff --git a/tools/test-assets/cs9-x86_64-minimal.json.gz.sha256_erofs b/tests/assets/cs9-x86_64-minimal.json.gz.sha256_erofs similarity index 100% rename from tools/test-assets/cs9-x86_64-minimal.json.gz.sha256_erofs rename to tests/assets/cs9-x86_64-minimal.json.gz.sha256_erofs diff --git a/tools/test-assets/f36-x86_64-silverblue.json.gz b/tests/assets/f36-x86_64-silverblue.json.gz similarity index 100% rename from tools/test-assets/f36-x86_64-silverblue.json.gz rename to tests/assets/f36-x86_64-silverblue.json.gz diff --git a/tools/test-assets/f36-x86_64-silverblue.json.gz.sha256_erofs b/tests/assets/f36-x86_64-silverblue.json.gz.sha256_erofs similarity index 100% rename from tools/test-assets/f36-x86_64-silverblue.json.gz.sha256_erofs rename to tests/assets/f36-x86_64-silverblue.json.gz.sha256_erofs diff --git a/tests/assets/special.json b/tests/assets/special.json new file mode 100644 index 00000000..025d5034 --- /dev/null +++ b/tests/assets/special.json @@ -0,0 +1,139 @@ +{ + "version": 1, + "entries": [ + { + "type": "dir", + "name": "/", + "mode": 365, + "size": 0, + "uid": 0, + "gid": 0, + "modtime": "2021-10-11T13:06:16+02:00", + "devMajor": 0, + "devMinor": 0, + "xattrs": { + "trusted.foo1": "YmFyLTE=", + "user.foo2": "YmFyLTI=" + } + }, + { + "type": "char", + "name": "chardev", + "mode": 511, + "size": 0, + "uid": 0, + "gid": 0, + "modtime": "2021-10-11T13:06:16+02:00", + "devMajor": 42, + "devMinor": 17, + "xattrs": { + "trusted.foo": "YmFyLTI=" + } + }, + { + "type": "block", + "name": "blockdev", + "mode": 511, + "size": 0, + "uid": 0, + "gid": 0, + "modtime": "2021-10-11T13:06:16+02:00", + "devMajor": 420, + "devMinor": 170, + "xattrs": { + "trusted.bar": "YmFyLTI=" + } + }, + { + "type": "fifo", + "name": "fifo", + "mode": 511, + "size": 0, + "uid": 0, + "gid": 0, + "modtime": "2021-10-11T13:06:16+02:00", + "devMajor": 0, + "devMinor": 0, + "xattrs": { + "trusted.bar": "YmFyLTI=" + } + }, + { + "type": "reg", + "name": "escaped-xattr", + "mode": 511, + "size": 0, + "uid": 0, + "gid": 0, + "modtime": "2021-10-11T13:06:16+02:00", + "devMajor": 0, + "devMinor": 0, + "xattrs": { + "trusted.overlay.redirect": "L2Zvbwo=", + "user.overlay.redirect": "L2Zvbwo=", + "user.foo": "YmFyLTI=" + } + }, + { + "type": "reg", + "name": "inline", + "mode": 511, + "uid": 0, + "gid": 0, + "modtime": "2021-10-11T13:06:16+02:00", + "devMajor": 0, + "devMinor": 0, + "x-content": "Rk9PQkFSCklOQUZJTEUK", + "xattrs": { + "user.foo": "YmFyLTI=" + } + }, + { + "type": "reg", + "name": "inline-large1", + "mode": 511, + "uid": 0, + "gid": 0, + "modtime": "2021-10-11T13:06:16+02:00", + "devMajor": 0, + "devMinor": 0, + "x-content": "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" + }, + { + "type": "reg", + "name": "inline-large2", + "mode": 511, + "uid": 0, + "gid": 0, + "modtime": "2021-10-11T13:06:16+02:00", + "devMajor": 0, + "devMinor": 0, + "x-content": "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" + }, + { + "type": "reg", + "name": "inline-large3", + "mode": 511, + "uid": 0, + "gid": 0, + "modtime": "2022-10-11T13:06:16+02:00", + "devMajor": 0, + "devMinor": 0, + "x-content": "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" + }, + { + "type": "char", + "name": "whiteout", + "mode": 511, + "size": 0, + "uid": 0, + "gid": 0, + "modtime": "2021-10-11T13:06:16+02:00", + "devMajor": 0, + "devMinor": 0, + "xattrs": { + "trusted.foo": "YmFyLTI=" + } + } + ] +} diff --git a/tests/assets/special.json.sha256_erofs b/tests/assets/special.json.sha256_erofs new file mode 100644 index 00000000..ff898dc7 --- /dev/null +++ b/tests/assets/special.json.sha256_erofs @@ -0,0 +1 @@ +d344782ff962ab7d1c5533755c19c5bf3c3485f55bfaf228fc89524c7e2ba61e \ No newline at end of file diff --git a/tests/dumpdir b/tests/dumpdir index 52cf357d..4ad80775 100755 --- a/tests/dumpdir +++ b/tests/dumpdir @@ -16,7 +16,7 @@ def dumpfile(file, root): 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="") + print(f"{shlex.quote(rel)} {oct(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="") diff --git a/tests/integration.sh b/tests/integration.sh index 074dd912..6d1dfa83 100755 --- a/tests/integration.sh +++ b/tests/integration.sh @@ -16,6 +16,10 @@ prev_digest=$(cat digest.txt) new_digest=$(mkcomposefs --by-digest --print-digest-only ${testsrc}) test "$prev_digest" = "$new_digest" +if which fsck.erofs &>/dev/null; then + fsck.erofs ${cfsroot}/roots/test.cfs +fi + mkdir -p mnt mount.composefs -o basedir=${cfsroot}/objects ${cfsroot}/roots/test.cfs mnt $orig/tests/dumpdir --no-nlink ${testsrc} > src-dump.txt diff --git a/tools/test-checksums.sh b/tests/test-checksums.sh similarity index 52% rename from tools/test-checksums.sh rename to tests/test-checksums.sh index 52d542ab..47e5a0ab 100755 --- a/tools/test-checksums.sh +++ b/tests/test-checksums.sh @@ -1,12 +1,18 @@ #!/usr/bin/bash -WRITER_JSON="$1" +BINDIR="$1" ASSET_DIR="$2" TEST_ASSETS="$3" +has_fsck=n +if which fsck.erofs &>/dev/null; then + has_fsck=y +fi + set -e tmpfile=$(mktemp /tmp/lcfs-test.XXXXXX) -trap 'rm -rf -- "$tmpfile"' EXIT +tmpfile2=$(mktemp /tmp/lcfs-test.XXXXXX) +trap 'rm -rf -- "$tmpfile" "$tmpfile2"' EXIT for format in erofs ; do for file in ${TEST_ASSETS} ; do @@ -21,12 +27,24 @@ for format in erofs ; do CAT=cat fi - $CAT $ASSET_DIR/$file | $WRITER_JSON --format=$format --out=$tmpfile - + $CAT $ASSET_DIR/$file | ${BINDIR}/composefs-from-json --format=$format --out=$tmpfile - SHA=$(sha256sum $tmpfile | awk "{print \$1}") + # Run fsck.erofs to make sure we're not generating anything weird + if [ $has_fsck == y ]; then + fsck.erofs $tmpfile + fi + if [ $SHA != $EXPECTED_SHA ]; then echo Invalid $format checksum of file generated from $file: $SHA, expected $EXPECTED_SHA exit 1 fi + + # Ensure dump reproduces the same file + ${BINDIR}/composefs-dump $tmpfile $tmpfile2 + if ! cmp $tmpfile $tmpfile2; then + echo Dump is not reproducible + exit 1 + fi done done diff --git a/tests/test-units.sh b/tests/test-units.sh new file mode 100755 index 00000000..5372d9b1 --- /dev/null +++ b/tests/test-units.sh @@ -0,0 +1,63 @@ +#!/usr/bin/bash + +BINDIR="$1" + +set -e + +workdir=$(mktemp -d /var/tmp/lcfs-test.XXXXXX) +trap 'rm -rf -- "$workdir"' EXIT + +function makeimage () { + local dir=$1 + $BINDIR/mkcomposefs --digest-store=$dir/objects $dir/root $dir/test.cfs +} + +function countobjects () { + local dir=$1 + find $dir/objects -type f | wc -l +} + +# Ensure small files are inlined +function test_inline () { + local dir=$1 + + echo foo > $dir/root/a-file + + makeimage $dir + + objects=$(countobjects $dir) + if [ $objects != 0 ]; then + return 1 + fi +} + +# Ensure we generate objects for large files +function test_objects () { + local dir=$1 + dd if=/dev/zero bs=1 count=1024 2>/dev/null > $dir/root/a-file + + makeimage $dir + + objects=$(countobjects $dir) + if [ $objects != 1 ]; then + return 1 + fi +} + +TESTS="test_inline test_objects" +res=0 +for i in $TESTS; do + testdir=$(mktemp -d $workdir/$i.XXXXXX) + mkdir $testdir/root + mkdir $testdir/objects + if $i $testdir ; then + echo "Test $i: OK" + else + res=1 + echo "Test $i Failed" + fi + + rm -rf $testdir +done + +exit $res diff --git a/tools/Makefile.am b/tools/Makefile.am index 4bd8cc0f..871e359f 100644 --- a/tools/Makefile.am +++ b/tools/Makefile.am @@ -1,6 +1,8 @@ -bin_PROGRAMS = mkcomposefs +COMPOSEFS_HASH_CFLAGS = -DUSE_OBSTACK=0 -DTESTING=0 -DUSE_DIFF_HASH=0 + +bin_PROGRAMS = mkcomposefs composefs-info sbin_PROGRAMS = mount.composefs -noinst_PROGRAMS = +noinst_PROGRAMS = composefs-dump if USE_YAJL noinst_PROGRAMS += composefs-from-json @@ -21,24 +23,13 @@ mount_composefs_LDADD = ../libcomposefs/libcomposefs.la $(LIBCRYPTO_LIBS) composefs_from_json_SOURCES = composefs-from-json.c read-file.c read-file.h composefs_from_json_LDADD = ../libcomposefs/libcomposefs.la $(LIBS_YAJL) $(LIBCRYPTO_LIBS) $(LIBS_SECCOMP) -composefs_fuse_SOURCES = cfs-fuse.c -composefs_fuse_LDADD = ../libcomposefs/libcomposefs.la $(FUSE3_LIBS) -composefs_fuse_CFLAGS = $(FUSE3_CFLAGS) - -TEST_ASSETS_SMALL = \ - config.json.gz config-with-hard-link.json.gz - -TEST_ASSETS = ${TEST_ASSETS_SMALL} \ - cs9-x86_64-developer.json.gz cs9-x86_64-minimal.json.gz \ - f36-x86_64-silverblue.json.gz +composefs_info_SOURCES = composefs-info.c ../libcomposefs/hash.c +composefs_info_CFLAGS = $(AM_CFLAGS) $(COMPOSEFS_HASH_CFLAGS) +composefs_info_LDADD = ../libcomposefs/libcomposefs.la -if ENABLE_VALGRIND -WRITER_JSON_PREFIX=libtool --mode=execute ${VALGRIND} --quiet --leak-check=yes --error-exitcode=42 -endif - -EXTRA_DIST = test-checksums.sh $(patsubst %,test-assets/%,${TEST_ASSETS_SMALL}) $(patsubst %,test-assets/%.sha256_erofs,${TEST_ASSETS_SMALL}) - -check-checksums: - $(srcdir)/test-checksums.sh "${WRITER_JSON_PREFIX} $(builddir)/composefs-from-json" "$(srcdir)/test-assets" "${TEST_ASSETS}" +composefs_dump_SOURCES = composefs-dump.c +composefs_dump_LDADD = ../libcomposefs/libcomposefs.la -check: check-checksums +composefs_fuse_SOURCES = cfs-fuse.c +composefs_fuse_LDADD = ../libcomposefs/libcomposefs.la $(FUSE3_LIBS) +composefs_fuse_CFLAGS = $(AM_CFLAGS) $(FUSE3_CFLAGS) diff --git a/tools/cfs-fuse.c b/tools/cfs-fuse.c index 9a2040a7..9e87555f 100644 --- a/tools/cfs-fuse.c +++ b/tools/cfs-fuse.c @@ -24,8 +24,9 @@ #include #include -#include "libcomposefs/lcfs-erofs.h" +#include "libcomposefs/lcfs-erofs-internal.h" #include "libcomposefs/lcfs-internal.h" +#include "libcomposefs/lcfs-utils.h" /* TODO: * Do we want to user ther negative_timeout=T option? @@ -34,16 +35,6 @@ #define CFS_ENTRY_TIMEOUT 3600.0 #define CFS_ATTR_TIMEOUT 3600.0 -#define ALIGN_TO(_offset, _align_size) \ - (((_offset) + _align_size - 1) & ~(_align_size - 1)) - -/* Note: These only do power of 2 */ -#define __round_mask(x, y) ((__typeof__(x))((y)-1)) -#define round_up(x, y) ((((x)-1) | __round_mask(x, y)) + 1) -#define round_down(x, y) ((x) & ~__round_mask(x, y)) - -#include "libcomposefs/erofs_fs_wrapper.h" - const uint8_t *erofs_data; size_t erofs_data_size; uint64_t erofs_root_nid; @@ -81,12 +72,6 @@ static const struct fuse_opt cfs_opts[] = { FUSE_OPT_END }; -typedef union { - __le16 i_format; - struct erofs_inode_compact compact; - struct erofs_inode_extended extended; -} erofs_inode; - static uint64_t cfs_nid_from_ino(fuse_ino_t ino) { if (ino == FUSE_ROOT_ID) { @@ -112,28 +97,6 @@ static const erofs_inode *cfs_get_erofs_inode(fuse_ino_t ino) return (const erofs_inode *)(erofs_metadata + (nid << EROFS_ISLOTBITS)); } -static uint16_t erofs_inode_version(const erofs_inode *cino) -{ - uint16_t i_format = lcfs_u16_from_file(cino->i_format); - return (i_format >> EROFS_I_VERSION_BIT) & EROFS_I_VERSION_MASK; -} - -static bool erofs_inode_is_compact(const erofs_inode *cino) -{ - return erofs_inode_version(cino) == 0; -} - -static uint16_t erofs_inode_datalayout(const erofs_inode *cino) -{ - uint16_t i_format = lcfs_u16_from_file(cino->i_format); - return (i_format >> EROFS_I_DATALAYOUT_BIT) & EROFS_I_DATALAYOUT_MASK; -} - -static bool erofs_inode_is_tailpacked(const erofs_inode *cino) -{ - return erofs_inode_datalayout(cino) == EROFS_INODE_FLAT_INLINE; -} - static int cfs_stat(fuse_ino_t ino, const erofs_inode *cino, struct stat *stbuf) { stbuf->st_ino = ino; @@ -211,8 +174,6 @@ static void erofs_inode_get_info(const erofs_inode *cino, uint32_t *mode, } } -#define min(a, b) (((a) < (b)) ? (a) : (b)) - /* This is essentially strcmp() for non-null-terminated strings */ static inline int memcmp2(const void *a, const size_t a_size, const void *b, size_t b_size) @@ -298,10 +259,12 @@ static void cfs_lookup(fuse_req_t req, fuse_ino_t parent, const char *name) size_t xattr_size; size_t isize; uint64_t n_blocks; - uint64_t last_block; + uint64_t last_oob_block; bool tailpacked; + size_t tail_size; + const uint8_t *tail_data; + const uint8_t *oob_data; int start_block, end_block; - int cmp; if (parent_cino == NULL) { fuse_reply_err(req, ENOENT); @@ -316,26 +279,25 @@ static void cfs_lookup(fuse_req_t req, fuse_ino_t parent, const char *name) return; } - xattr_size = 0; - if (xattr_icount > 0) - xattr_size = sizeof(struct erofs_xattr_ibody_header) + - (xattr_icount - 1) * 4; + xattr_size = erofs_xattr_inode_size(xattr_icount); tailpacked = erofs_inode_is_tailpacked(parent_cino); + tail_size = tailpacked ? file_size % EROFS_BLKSIZ : 0; + tail_data = ((uint8_t *)parent_cino) + isize + xattr_size; n_blocks = round_up(file_size, EROFS_BLKSIZ) / EROFS_BLKSIZ; - last_block = tailpacked ? n_blocks - 1 : n_blocks; + last_oob_block = tailpacked ? n_blocks - 1 : n_blocks; + oob_data = erofs_data + raw_blkaddr * EROFS_BLKSIZ; /* First read the out-of-band blocks */ start_block = 0; - end_block = last_block - 1; + end_block = last_oob_block - 1; while (start_block <= end_block) { int mid_block = start_block + (end_block - start_block) / 2; - const uint8_t *block_data = - erofs_data + ((raw_blkaddr + mid_block) * EROFS_BLKSIZ); + const uint8_t *block_data = oob_data + mid_block * EROFS_BLKSIZ; size_t block_size = EROFS_BLKSIZ; int cmp; - if (!tailpacked && mid_block + 1 == last_block) { + if (!tailpacked && mid_block + 1 == last_oob_block) { block_size = file_size % EROFS_BLKSIZ; if (block_size == 0) { block_size = EROFS_BLKSIZ; @@ -357,10 +319,8 @@ static void cfs_lookup(fuse_req_t req, fuse_ino_t parent, const char *name) } if (tailpacked && start_block > end_block) { - const uint8_t *block_data = - ((uint8_t *)parent_cino) + isize + xattr_size; - if (cfs_lookup_block(req, block_data, file_size % EROFS_BLKSIZ, - name, &cmp)) + int cmp; + if (cfs_lookup_block(req, tail_data, tail_size, name, &cmp)) return; } @@ -510,9 +470,12 @@ static void _cfs_readdir(fuse_req_t req, fuse_ino_t ino, size_t max_size, size_t xattr_size; size_t isize; uint64_t n_blocks; - uint64_t last_block; + uint64_t last_oob_block; size_t first_block; bool tailpacked; + size_t tail_size; + const uint8_t *tail_data; + const uint8_t *oob_data; uint32_t raw_blkaddr; uint8_t bufdata[max_size]; bool done; @@ -526,15 +489,15 @@ static void _cfs_readdir(fuse_req_t req, fuse_ino_t ino, size_t max_size, erofs_inode_get_info(cino, &mode, &file_size, &xattr_icount, &raw_blkaddr, &isize); - xattr_size = 0; - if (xattr_icount > 0) - xattr_size = sizeof(struct erofs_xattr_ibody_header) + - (xattr_icount - 1) * 4; + xattr_size = erofs_xattr_inode_size(xattr_icount); tailpacked = erofs_inode_is_tailpacked(cino); + tail_size = tailpacked ? file_size % EROFS_BLKSIZ : 0; + tail_data = ((uint8_t *)cino) + isize + xattr_size; n_blocks = round_up(file_size, EROFS_BLKSIZ) / EROFS_BLKSIZ; - last_block = tailpacked ? n_blocks - 1 : n_blocks; + last_oob_block = tailpacked ? n_blocks - 1 : n_blocks; first_block = buf.offset / EROFS_BLKSIZ; + oob_data = erofs_data + raw_blkaddr * EROFS_BLKSIZ; if (first_block >= n_blocks) { goto out; @@ -542,11 +505,11 @@ static void _cfs_readdir(fuse_req_t req, fuse_ino_t ino, size_t max_size, /* First read the out-of-band blocks */ done = false; - for (uint64_t block = first_block; block < last_block; block++) { + for (uint64_t block = first_block; block < last_oob_block; block++) { size_t block_start = block * EROFS_BLKSIZ; size_t block_size = EROFS_BLKSIZ; - if (!tailpacked && block + 1 == last_block) { + if (!tailpacked && block + 1 == last_oob_block) { block_size = file_size % EROFS_BLKSIZ; if (block_size == 0) { block_size = EROFS_BLKSIZ; @@ -555,8 +518,7 @@ static void _cfs_readdir(fuse_req_t req, fuse_ino_t ino, size_t max_size, if (buf.offset >= block_start && buf.offset < block_start + block_size) { - const uint8_t *block_data = - erofs_data + raw_blkaddr * EROFS_BLKSIZ + block_start; + const uint8_t *block_data = oob_data + block_start; if (cfs_readdir_block(req, &buf, block_data, block_size, block_start, use_plus)) { done = true; @@ -566,14 +528,10 @@ static void _cfs_readdir(fuse_req_t req, fuse_ino_t ino, size_t max_size, } if (!done && tailpacked) { - size_t block_start = last_block * EROFS_BLKSIZ; - size_t block_size = file_size % EROFS_BLKSIZ; + size_t block_start = last_oob_block * EROFS_BLKSIZ; - if (buf.offset >= block_start && - buf.offset < block_start + block_size) { - const uint8_t *block_data = - ((uint8_t *)cino) + isize + xattr_size; - cfs_readdir_block(req, &buf, block_data, block_size, + if (buf.offset >= block_start && buf.offset < block_start + tail_size) { + cfs_readdir_block(req, &buf, tail_data, tail_size, block_start, use_plus); } } @@ -648,10 +606,7 @@ static void cfs_readlink(fuse_req_t req, fuse_ino_t ino) return; } - xattr_size = 0; - if (xattr_icount > 0) - xattr_size = sizeof(struct erofs_xattr_ibody_header) + - (xattr_icount - 1) * 4; + xattr_size = erofs_xattr_inode_size(xattr_icount); tailpacked = erofs_inode_is_tailpacked(cino); if (!tailpacked) { @@ -684,53 +639,16 @@ static void cfs_init(void *userdata, struct fuse_conn_info *conn) conn->want |= FUSE_CAP_SPLICE_READ; } -const char *erofs_xattr_prefixes[] = { - "", - "user.", - "system.posix_acl_access", - "system.posix_acl_default", - "trusted.", - "lustre.", - "security.", -}; - -#define EROFS_N_PREFIXES (sizeof(erofs_xattr_prefixes) / sizeof(char *)) - -static bool is_acl_xattr(int prefix, const char *name, size_t name_len) -{ - const char *const nfs_acl = "system.nfs4_acl"; - - if ((prefix == EROFS_XATTR_INDEX_POSIX_ACL_ACCESS || - prefix == EROFS_XATTR_INDEX_POSIX_ACL_DEFAULT) && - name_len == 0) - return true; - if (prefix == 0 && name_len == strlen(nfs_acl) && - memcmp(name, nfs_acl, strlen(nfs_acl)) == 0) - return true; - return false; -} - -static int erofs_get_xattr_prefix(const char *str) -{ - for (int i = 1; i < EROFS_N_PREFIXES; i++) { - const char *prefix = erofs_xattr_prefixes[i]; - if (strlen(str) >= strlen(prefix) && - memcmp(str, prefix, strlen(prefix)) == 0) { - return i; - } - } - return 0; -} - -#define OVERLAY_PREFIX "overlay." +#define OVERLAY_XATTR_PARTIAL_PREFIX "overlay." static int cfs_rewrite_xattr_prefix_from_image(int name_index, const char *name, size_t name_len) { /* We rewrite trusted.overlay.* to user.overlay.* */ if (name_index == EROFS_XATTR_INDEX_TRUSTED && - name_len > strlen(OVERLAY_PREFIX) && - memcmp(name, OVERLAY_PREFIX, strlen(OVERLAY_PREFIX)) == 0) + name_len > strlen(OVERLAY_XATTR_PARTIAL_PREFIX) && + memcmp(name, OVERLAY_XATTR_PARTIAL_PREFIX, + strlen(OVERLAY_XATTR_PARTIAL_PREFIX)) == 0) return EROFS_XATTR_INDEX_USER; return name_index; @@ -741,8 +659,9 @@ static int cfs_rewrite_xattr_prefix_to_image(int name_index, const char *name, { /* We rewrite trusted.overlay.* to user.overlay.* */ if (name_index == EROFS_XATTR_INDEX_USER && - name_len > strlen(OVERLAY_PREFIX) && - memcmp(name, OVERLAY_PREFIX, strlen(OVERLAY_PREFIX)) == 0) + name_len > strlen(OVERLAY_XATTR_PARTIAL_PREFIX) && + memcmp(name, OVERLAY_XATTR_PARTIAL_PREFIX, + strlen(OVERLAY_XATTR_PARTIAL_PREFIX)) == 0) return EROFS_XATTR_INDEX_TRUSTED; return name_index; @@ -815,10 +734,7 @@ static void cfs_listxattr(fuse_req_t req, fuse_ino_t ino, size_t max_size) return; } - xattr_size = 0; - if (xattr_icount > 0) - xattr_size = sizeof(struct erofs_xattr_ibody_header) + - (xattr_icount - 1) * 4; + xattr_size = erofs_xattr_inode_size(xattr_icount); xattrs_start = ((uint8_t *)cino) + isize; xattrs_end = ((uint8_t *)cino) + isize + xattr_size; @@ -900,10 +816,7 @@ static const char *do_getxattr(const erofs_inode *cino, int name_prefix, return NULL; } - xattr_size = 0; - if (xattr_icount > 0) - xattr_size = sizeof(struct erofs_xattr_ibody_header) + - (xattr_icount - 1) * 4; + xattr_size = erofs_xattr_inode_size(xattr_icount); xattrs_start = ((uint8_t *)cino) + isize; xattrs_end = ((uint8_t *)cino) + isize + xattr_size; @@ -978,7 +891,7 @@ static void cfs_getxattr(fuse_req_t req, fuse_ino_t ino, const char *name, /* When acls are not used, send EOPTNOTSUPP, as this informs userspace to stop constantly looking for acls */ - if (!erofs_use_acl && is_acl_xattr(name_prefix, name, name_len)) { + if (!erofs_use_acl && erofs_is_acl_xattr(name_prefix, name, name_len)) { fuse_reply_err(req, EOPNOTSUPP); return; } @@ -1048,16 +961,72 @@ static void cfs_release(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info *f fuse_reply_err(req, 0); } +static void cfs_read_inline(fuse_req_t req, fuse_ino_t ino, size_t size, + off_t offset, struct fuse_file_info *fi) +{ + const erofs_inode *cino = cfs_get_erofs_inode(ino); + uint32_t mode; + uint64_t file_size; + uint16_t xattr_icount; + size_t xattr_size; + size_t isize; + uint64_t n_blocks; + uint64_t last_oob_block; + bool tailpacked; + size_t tail_size; + uint32_t raw_blkaddr; + off_t oob_size; + const uint8_t *tail_data; + const uint8_t *oob_data; + struct iovec iov[2]; + int i; + + if (!erofs_inode_is_flat(cino)) { + fuse_reply_err(req, ENXIO); + return; + } + + erofs_inode_get_info(cino, &mode, &file_size, &xattr_icount, + &raw_blkaddr, &isize); + + xattr_size = erofs_xattr_inode_size(xattr_icount); + + tailpacked = erofs_inode_is_tailpacked(cino); + tail_size = tailpacked ? file_size % EROFS_BLKSIZ : 0; + tail_data = ((uint8_t *)cino) + isize + xattr_size; + + n_blocks = round_up(file_size, EROFS_BLKSIZ) / EROFS_BLKSIZ; + last_oob_block = tailpacked ? n_blocks - 1 : n_blocks; + + oob_data = erofs_data + raw_blkaddr * EROFS_BLKSIZ; + oob_size = tailpacked ? last_oob_block * EROFS_BLKSIZ : file_size; + + i = 0; + if (offset < oob_size) { + size_t oob_send = min(size, oob_size); + iov[i].iov_base = (uint8_t *)oob_data; + iov[i++].iov_len = oob_send; + size -= oob_send; + } + + if (size > 0 && tail_size > 0) { + size_t tail_send = min(size, tail_size); + iov[i].iov_base = (uint8_t *)tail_data; + iov[i++].iov_len = tail_send; + size -= tail_send; + } + + fuse_reply_iov(req, iov, i); +} + static void cfs_read(fuse_req_t req, fuse_ino_t ino, size_t size, off_t offset, struct fuse_file_info *fi) { struct fuse_bufvec buf = FUSE_BUFVEC_INIT(size); int fd = fi->fh; - char c; if (fd < 0) { - c = 0; - fuse_reply_buf(req, &c, 0); + cfs_read_inline(req, ino, size, offset, fi); } else { buf.buf[0].flags = FUSE_BUF_IS_FD | FUSE_BUF_FD_SEEK; buf.buf[0].fd = fd; diff --git a/tools/composefs-dump.c b/tools/composefs-dump.c new file mode 100644 index 00000000..980ec975 --- /dev/null +++ b/tools/composefs-dump.c @@ -0,0 +1,94 @@ +/* lcfs + Copyright (C) 2023 Alexander Larsson + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#define _GNU_SOURCE + +#include "config.h" + +#include "libcomposefs/lcfs-writer.h" +#include "libcomposefs/lcfs-utils.h" + +#include +#include +#include +#include +#include +#include + +static void usage(const char *argv0) +{ + fprintf(stderr, "usage: %s SRC DEST\n", argv0); +} + +static ssize_t write_cb(void *_file, void *buf, size_t count) +{ + FILE *file = _file; + + return fwrite(buf, 1, count, file); +} + +int main(int argc, char **argv) +{ + const char *bin = argv[0]; + int fd; + struct lcfs_node_s *root; + const char *src_path = NULL; + const char *dst_path = NULL; + struct lcfs_write_options_s options = { 0 }; + + if (argc <= 1) { + fprintf(stderr, "No source path specified\n"); + usage(bin); + exit(1); + } + src_path = argv[1]; + + if (argc <= 2) { + fprintf(stderr, "No destination path specified\n"); + usage(bin); + exit(1); + } + dst_path = argv[2]; + + fd = open(src_path, O_RDONLY | O_CLOEXEC); + if (fd < 0) { + err(EXIT_FAILURE, "Failed to open '%s'", src_path); + } + + root = lcfs_load_node_from_fd(fd); + if (root == NULL) { + err(EXIT_FAILURE, "Failed to load '%s'", src_path); + } + + close(fd); + + options.format = LCFS_FORMAT_EROFS; + + FILE *out_file = fopen(dst_path, "we"); + if (out_file == NULL) + error(EXIT_FAILURE, errno, "failed to open '%s'", dst_path); + + options.file = out_file; + options.file_write_cb = write_cb; + + if (lcfs_write_to(root, &options) < 0) + error(EXIT_FAILURE, errno, "cannot write file"); + + lcfs_node_unref(root); + + return 0; +} diff --git a/tools/composefs-from-json.c b/tools/composefs-from-json.c index de7ba432..caa67ad1 100644 --- a/tools/composefs-from-json.c +++ b/tools/composefs-from-json.c @@ -405,6 +405,7 @@ static int fill_file(const char *typ, struct lcfs_node_s *root, struct lcfs_node_s *node, yajl_val entry) { const char *payload = NULL; + const char *content = NULL; char payload_buffer[128]; uint16_t min = 0, maj = 0; mode_t mode = 0; @@ -524,6 +525,35 @@ static int fill_file(const char *typ, struct lcfs_node_s *root, } } + /* custom extension to the CRFS format. */ + v = get_child(entry, "x-content", yajl_t_string); + if (v) + content = YAJL_GET_STRING(v); + if (content) { + int r; + size_t buf_size = strlen(content); /* Enough to fit base64 decoded value */ + size_t written; + cleanup_free uint8_t *buf = malloc(buf_size); + + if (buf == NULL) { + error(0, 0, "malloc"); + return -1; + } + + r = base64_decode(content, strlen(content), (char *)buf, + buf_size, &written); + if (r < 0) { + error(0, 0, "x-content value is not valid b64"); + return -1; + } + + r = lcfs_node_set_content(node, buf, written); + if (r < 0) { + error(0, 0, "set_content"); + return -1; + } + } + v = get_child(entry, "xattrs", yajl_t_object); if (v) { res = fill_xattrs(node, v); diff --git a/tools/composefs-info.c b/tools/composefs-info.c new file mode 100644 index 00000000..89622a37 --- /dev/null +++ b/tools/composefs-info.c @@ -0,0 +1,294 @@ +/* lcfs + Copyright (C) 2023 Alexander Larsson + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#define _GNU_SOURCE + +#include "config.h" + +#include "libcomposefs/lcfs-writer.h" +#include "libcomposefs/lcfs-utils.h" +#include "libcomposefs/lcfs-internal.h" +#include "libcomposefs/hash.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#define ESCAPE_STANDARD 0 +#define NOESCAPE_SPACE (1 << 0) +#define ESCAPE_EQUAL (1 << 1) +#define ESCAPE_LONE_DASH (1 << 2) + +static void print_escaped(const char *val, ssize_t len, int escape) +{ + bool noescape_space = (escape & NOESCAPE_SPACE) != 0; + bool escape_equal = (escape & ESCAPE_EQUAL) != 0; + bool escape_lone_dash = (escape & ESCAPE_LONE_DASH) != 0; + + if (len < 0) + len = strlen(val); + + if (escape_lone_dash && len == 1 && val[0] == '-') { + printf("\\x%.2x", val[0]); + return; + } + + for (size_t i = 0; i < len; i++) { + uint8_t c = val[i]; + bool hex_escape = false; + const char *special = NULL; + switch (c) { + case '\\': + special = "\\\\"; + break; + case '\n': + special = "\\n"; + break; + case '\r': + special = "\\r"; + break; + case '\t': + special = "\\t"; + break; + case '=': + hex_escape = escape_equal; + break; + default: + if (noescape_space) + hex_escape = !isprint(c); + else + hex_escape = !isgraph(c); + break; + } + + if (special != NULL) + printf("%s", special); + else if (hex_escape) + printf("\\x%.2x", c); + else + printf("%c", c); + } +} + +static void print_node(struct lcfs_node_s *node, char *parent_path) +{ + for (size_t i = 0; i < lcfs_node_get_n_children(node); i++) { + struct lcfs_node_s *child = lcfs_node_get_child(node, i); + cleanup_free char *path = NULL; + + asprintf(&path, "%s/%s", parent_path, lcfs_node_get_name(child)); + + uint32_t mode = lcfs_node_get_mode(child); + uint32_t type = mode & S_IFMT; + const char *payload = lcfs_node_get_payload(child); + + print_escaped(path, -1, NOESCAPE_SPACE); + + if (type == S_IFDIR) { + printf("/\t"); + } else if (type == S_IFLNK) { + printf("\t-> "); + print_escaped(payload, -1, ESCAPE_STANDARD); + } else if (type == S_IFREG && payload) { + printf("\t@ "); + print_escaped(payload, -1, ESCAPE_STANDARD); + } + printf("\n"); + + print_node(child, path); + } +} + +static void digest_to_string(const uint8_t *csum, char *buf) +{ + static const char hexchars[] = "0123456789abcdef"; + uint32_t i, j; + + for (i = 0, j = 0; i < LCFS_DIGEST_SIZE; i++, j += 2) { + uint8_t byte = csum[i]; + buf[j] = hexchars[byte >> 4]; + buf[j + 1] = hexchars[byte & 0xF]; + } + buf[j] = '\0'; +} + +static void dump_node(struct lcfs_node_s *node, char *path) +{ + struct lcfs_node_s *target; + struct timespec mtime; + const char *payload; + const uint8_t *digest; + + target = lcfs_node_get_hardlink_target(node); + if (target == NULL) + target = node; + + lcfs_node_get_mtime(target, &mtime); + payload = lcfs_node_get_payload(target); + digest = lcfs_node_get_fsverity_digest(target); + + print_escaped(*path == 0 ? "/" : path, -1, ESCAPE_STANDARD); + printf(" %" PRIu64 " %s%o %u %u %u %u %" PRIi64 ".%u ", + lcfs_node_get_size(target), target == node ? "" : "@", + lcfs_node_get_mode(target), lcfs_node_get_nlink(target), + lcfs_node_get_uid(target), lcfs_node_get_gid(target), + lcfs_node_get_rdev(target), (int64_t)mtime.tv_sec, + (unsigned int)mtime.tv_nsec); + print_escaped(payload ? payload : "-", -1, ESCAPE_LONE_DASH); + + if (digest) { + char digest_str[LCFS_DIGEST_SIZE * 2 + 1] = { 0 }; + digest_to_string(digest, digest_str); + printf(" %s", digest_str); + } else { + printf(" -"); + } + + size_t n_xattr = lcfs_node_get_n_xattr(target); + for (size_t i = 0; i < n_xattr; i++) { + const char *name = lcfs_node_get_xattr_name(target, i); + size_t value_len; + const char *value = lcfs_node_get_xattr(target, name, &value_len); + + printf(" "); + print_escaped(name, -1, ESCAPE_EQUAL); + printf("="); + print_escaped(value, value_len, ESCAPE_EQUAL); + } + + printf("\n"); + + for (size_t i = 0; i < lcfs_node_get_n_children(node); i++) { + struct lcfs_node_s *child = lcfs_node_get_child(node, i); + cleanup_free char *child_path = NULL; + + asprintf(&child_path, "%s/%s", path, lcfs_node_get_name(child)); + + dump_node(child, child_path); + } +} + +static void get_objects(struct lcfs_node_s *node, Hash_table *ht) +{ + uint32_t mode = lcfs_node_get_mode(node); + uint32_t type = mode & S_IFMT; + const char *payload = lcfs_node_get_payload(node); + + if (type == S_IFREG && payload) { + if (hash_insert(ht, payload) == NULL) { + errx(EXIT_FAILURE, "Out of memory"); + } + } + + for (size_t i = 0; i < lcfs_node_get_n_children(node); i++) { + struct lcfs_node_s *child = lcfs_node_get_child(node, i); + get_objects(child, ht); + } +} + +static size_t str_ht_hash(const void *entry, size_t table_size) +{ + return hash_string(entry, table_size); +} + +static bool str_ht_eq(const void *entry1, const void *entry2) +{ + return strcmp(entry1, entry2) == 0; +} + +static int cmp_obj(const void *_a, const void *_b) +{ + const char *const *a = _a; + const char *const *b = _b; + return strcmp(*a, *b); +} + +static void print_objects(struct lcfs_node_s *node) +{ + Hash_table *ht = hash_initialize(0, NULL, str_ht_hash, str_ht_eq, NULL); + if (ht == NULL) + errx(EXIT_FAILURE, "Out of memory"); + + get_objects(node, ht); + + size_t n_objects = hash_get_n_entries(ht); + char **objects = calloc(n_objects, sizeof(char *)); + + hash_get_entries(ht, (void **)objects, n_objects); + + qsort(objects, n_objects, sizeof(char *), cmp_obj); + + for (size_t i = 0; i < n_objects; i++) + printf("%s\n", objects[i]); + + hash_free(ht); +} + +static void usage(const char *argv0) +{ + fprintf(stderr, "usage: %s [ls|objects|dump] IMAGE\n", argv0); +} + +int main(int argc, char **argv) +{ + const char *bin = argv[0]; + int fd; + cleanup_node struct lcfs_node_s *root = NULL; + const char *image_path = NULL; + const char *command; + + if (argc <= 1) { + fprintf(stderr, "No command specified\n"); + usage(bin); + exit(1); + } + command = argv[1]; + + if (argc <= 2) { + fprintf(stderr, "No image path specified\n"); + usage(bin); + exit(1); + } + image_path = argv[2]; + + fd = open(image_path, O_RDONLY | O_CLOEXEC); + if (fd < 0) { + err(EXIT_FAILURE, "Failed to open '%s'", image_path); + } + + root = lcfs_load_node_from_fd(fd); + if (root == NULL) { + err(EXIT_FAILURE, "Failed to load '%s'", image_path); + } + + if (strcmp(command, "ls") == 0) { + print_node(root, ""); + } else if (strcmp(command, "dump") == 0) { + dump_node(root, ""); + } else if (strcmp(command, "objects") == 0) { + print_objects(root); + } else { + errx(EXIT_FAILURE, "Unknown command '%s'\n", command); + } + + return 0; +}