| // SPDX-License-Identifier: GPL-2.0+ OR Apache-2.0 |
| #define _GNU_SOURCE |
| #include <unistd.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <sys/stat.h> |
| #include <config.h> |
| #if defined(HAVE_SYS_SYSMACROS_H) |
| #include <sys/sysmacros.h> |
| #endif |
| #include "erofs/print.h" |
| #include "erofs/inode.h" |
| #include "erofs/rebuild.h" |
| #include "erofs/dir.h" |
| #include "erofs/xattr.h" |
| #include "erofs/blobchunk.h" |
| #include "erofs/internal.h" |
| #include "liberofs_uuid.h" |
| |
| #ifdef HAVE_LINUX_AUFS_TYPE_H |
| #include <linux/aufs_type.h> |
| #else |
| #define AUFS_WH_PFX ".wh." |
| #define AUFS_DIROPQ_NAME AUFS_WH_PFX ".opq" |
| #define AUFS_WH_DIROPQ AUFS_WH_PFX AUFS_DIROPQ_NAME |
| #endif |
| |
| static struct erofs_dentry *erofs_rebuild_mkdir(struct erofs_inode *dir, |
| const char *s) |
| { |
| struct erofs_inode *inode; |
| struct erofs_dentry *d; |
| |
| inode = erofs_new_inode(dir->sbi); |
| if (IS_ERR(inode)) |
| return ERR_CAST(inode); |
| |
| if (asprintf(&inode->i_srcpath, "%s/%s", |
| dir->i_srcpath ? : "", s) < 0) { |
| erofs_iput(inode); |
| return ERR_PTR(-ENOMEM); |
| } |
| inode->i_mode = S_IFDIR | 0755; |
| inode->i_parent = dir; |
| inode->i_uid = getuid(); |
| inode->i_gid = getgid(); |
| inode->i_mtime = inode->sbi->build_time; |
| inode->i_mtime_nsec = inode->sbi->build_time_nsec; |
| erofs_init_empty_dir(inode); |
| |
| d = erofs_d_alloc(dir, s); |
| if (IS_ERR(d)) { |
| erofs_iput(inode); |
| } else { |
| d->type = EROFS_FT_DIR; |
| d->inode = inode; |
| } |
| return d; |
| } |
| |
| struct erofs_dentry *erofs_rebuild_get_dentry(struct erofs_inode *pwd, |
| char *path, bool aufs, bool *whout, bool *opq, bool to_head) |
| { |
| struct erofs_dentry *d = NULL; |
| unsigned int len = strlen(path); |
| char *s = path; |
| |
| *whout = false; |
| *opq = false; |
| |
| while (s < path + len) { |
| char *slash = memchr(s, '/', path + len - s); |
| |
| if (slash) { |
| if (s == slash) { |
| while (*++s == '/'); /* skip '//...' */ |
| continue; |
| } |
| *slash = '\0'; |
| } |
| |
| if (!memcmp(s, ".", 2)) { |
| /* null */ |
| } else if (!memcmp(s, "..", 3)) { |
| pwd = pwd->i_parent; |
| } else { |
| struct erofs_inode *inode = NULL; |
| |
| if (aufs && !slash) { |
| if (!memcmp(s, AUFS_WH_DIROPQ, sizeof(AUFS_WH_DIROPQ))) { |
| *opq = true; |
| break; |
| } |
| if (!memcmp(s, AUFS_WH_PFX, sizeof(AUFS_WH_PFX) - 1)) { |
| s += sizeof(AUFS_WH_PFX) - 1; |
| *whout = true; |
| } |
| } |
| |
| list_for_each_entry(d, &pwd->i_subdirs, d_child) { |
| if (!strcmp(d->name, s)) { |
| if (d->type != EROFS_FT_DIR && slash) |
| return ERR_PTR(-EIO); |
| inode = d->inode; |
| break; |
| } |
| } |
| |
| if (inode) { |
| if (to_head) { |
| list_del(&d->d_child); |
| list_add(&d->d_child, &pwd->i_subdirs); |
| } |
| pwd = inode; |
| } else if (!slash) { |
| d = erofs_d_alloc(pwd, s); |
| if (IS_ERR(d)) |
| return d; |
| d->type = EROFS_FT_UNKNOWN; |
| d->inode = pwd; |
| } else { |
| d = erofs_rebuild_mkdir(pwd, s); |
| if (IS_ERR(d)) |
| return d; |
| pwd = d->inode; |
| } |
| } |
| if (slash) { |
| *slash = '/'; |
| s = slash + 1; |
| } else { |
| break; |
| } |
| } |
| return d; |
| } |
| |
| static int erofs_rebuild_write_blob_index(struct erofs_sb_info *dst_sb, |
| struct erofs_inode *inode) |
| { |
| int ret; |
| unsigned int count, unit, chunkbits, i; |
| struct erofs_inode_chunk_index *idx; |
| erofs_off_t chunksize; |
| erofs_blk_t blkaddr; |
| |
| /* TODO: fill data map in other layouts */ |
| if (inode->datalayout == EROFS_INODE_CHUNK_BASED) { |
| chunkbits = inode->u.chunkbits; |
| if (chunkbits < dst_sb->blkszbits) { |
| erofs_err("%s: chunk size %u is smaller than the target block size %u", |
| inode->i_srcpath, 1U << chunkbits, |
| 1U << dst_sb->blkszbits); |
| return -EINVAL; |
| } |
| } else if (inode->datalayout == EROFS_INODE_FLAT_PLAIN) { |
| chunkbits = ilog2(inode->i_size - 1) + 1; |
| if (chunkbits < dst_sb->blkszbits) |
| chunkbits = dst_sb->blkszbits; |
| if (chunkbits - dst_sb->blkszbits > EROFS_CHUNK_FORMAT_BLKBITS_MASK) |
| chunkbits = EROFS_CHUNK_FORMAT_BLKBITS_MASK + dst_sb->blkszbits; |
| } else { |
| erofs_err("%s: unsupported datalayout %d ", inode->i_srcpath, |
| inode->datalayout); |
| return -EOPNOTSUPP; |
| } |
| |
| chunksize = 1ULL << chunkbits; |
| count = DIV_ROUND_UP(inode->i_size, chunksize); |
| |
| unit = sizeof(struct erofs_inode_chunk_index); |
| inode->extent_isize = count * unit; |
| idx = malloc(max(sizeof(*idx), sizeof(void *))); |
| if (!idx) |
| return -ENOMEM; |
| inode->chunkindexes = idx; |
| |
| for (i = 0; i < count; i++) { |
| struct erofs_blobchunk *chunk; |
| struct erofs_map_blocks map = { |
| .index = UINT_MAX, |
| }; |
| |
| map.m_la = i << chunkbits; |
| ret = erofs_map_blocks(inode, &map, 0); |
| if (ret) |
| goto err; |
| |
| blkaddr = erofs_blknr(dst_sb, map.m_pa); |
| chunk = erofs_get_unhashed_chunk(inode->dev, blkaddr, 0); |
| if (IS_ERR(chunk)) { |
| ret = PTR_ERR(chunk); |
| goto err; |
| } |
| *(void **)idx++ = chunk; |
| |
| } |
| inode->datalayout = EROFS_INODE_CHUNK_BASED; |
| inode->u.chunkformat = EROFS_CHUNK_FORMAT_INDEXES; |
| inode->u.chunkformat |= chunkbits - dst_sb->blkszbits; |
| return 0; |
| err: |
| free(inode->chunkindexes); |
| inode->chunkindexes = NULL; |
| return ret; |
| } |
| |
| static int erofs_rebuild_update_inode(struct erofs_sb_info *dst_sb, |
| struct erofs_inode *inode, |
| enum erofs_rebuild_datamode datamode) |
| { |
| int err = 0; |
| |
| switch (inode->i_mode & S_IFMT) { |
| case S_IFCHR: |
| if (erofs_inode_is_whiteout(inode)) |
| inode->i_parent->whiteouts = true; |
| /* fallthrough */ |
| case S_IFBLK: |
| case S_IFIFO: |
| case S_IFSOCK: |
| inode->i_size = 0; |
| erofs_dbg("\tdev: %d %d", major(inode->u.i_rdev), |
| minor(inode->u.i_rdev)); |
| inode->u.i_rdev = erofs_new_encode_dev(inode->u.i_rdev); |
| break; |
| case S_IFDIR: |
| err = erofs_init_empty_dir(inode); |
| break; |
| case S_IFLNK: |
| inode->i_link = malloc(inode->i_size + 1); |
| if (!inode->i_link) |
| return -ENOMEM; |
| err = erofs_pread(inode, inode->i_link, inode->i_size, 0); |
| erofs_dbg("\tsymlink: %s -> %s", inode->i_srcpath, inode->i_link); |
| break; |
| case S_IFREG: |
| if (!inode->i_size) { |
| inode->u.i_blkaddr = NULL_ADDR; |
| break; |
| } |
| if (datamode == EROFS_REBUILD_DATA_BLOB_INDEX) |
| err = erofs_rebuild_write_blob_index(dst_sb, inode); |
| else if (datamode == EROFS_REBUILD_DATA_RESVSP) |
| inode->datasource = EROFS_INODE_DATA_SOURCE_RESVSP; |
| else |
| err = -EOPNOTSUPP; |
| break; |
| default: |
| return -EINVAL; |
| } |
| return err; |
| } |
| |
| /* |
| * @mergedir: parent directory in the merged tree |
| * @ctx.dir: parent directory when itering erofs_iterate_dir() |
| * @datamode: indicate how to import inode data |
| */ |
| struct erofs_rebuild_dir_context { |
| struct erofs_dir_context ctx; |
| struct erofs_inode *mergedir; |
| enum erofs_rebuild_datamode datamode; |
| }; |
| |
| static int erofs_rebuild_dirent_iter(struct erofs_dir_context *ctx) |
| { |
| struct erofs_rebuild_dir_context *rctx = (void *)ctx; |
| struct erofs_inode *mergedir = rctx->mergedir; |
| struct erofs_inode *dir = ctx->dir; |
| struct erofs_inode *inode, *candidate; |
| struct erofs_inode src; |
| struct erofs_dentry *d; |
| char *path, *dname; |
| bool dumb; |
| int ret; |
| |
| if (ctx->dot_dotdot) |
| return 0; |
| |
| ret = asprintf(&path, "%s/%.*s", rctx->mergedir->i_srcpath, |
| ctx->de_namelen, ctx->dname); |
| if (ret < 0) |
| return ret; |
| |
| erofs_dbg("parsing %s", path); |
| dname = path + strlen(mergedir->i_srcpath) + 1; |
| |
| d = erofs_rebuild_get_dentry(mergedir, dname, false, |
| &dumb, &dumb, false); |
| if (IS_ERR(d)) { |
| ret = PTR_ERR(d); |
| goto out; |
| } |
| |
| ret = 0; |
| if (d->type != EROFS_FT_UNKNOWN) { |
| /* |
| * bail out if the file exists in the upper layers. (Note that |
| * extended attributes won't be merged too even for dirs.) |
| */ |
| if (!S_ISDIR(d->inode->i_mode) || d->inode->opaque) |
| goto out; |
| |
| /* merge directory entries */ |
| src = (struct erofs_inode) { |
| .sbi = dir->sbi, |
| .nid = ctx->de_nid |
| }; |
| ret = erofs_read_inode_from_disk(&src); |
| if (ret || !S_ISDIR(src.i_mode)) |
| goto out; |
| mergedir = d->inode; |
| inode = dir = &src; |
| } else { |
| u64 nid; |
| |
| DBG_BUGON(mergedir != d->inode); |
| inode = erofs_new_inode(dir->sbi); |
| if (IS_ERR(inode)) { |
| ret = PTR_ERR(inode); |
| goto out; |
| } |
| |
| /* reuse i_ino[0] to read nid in source fs */ |
| nid = inode->i_ino[0]; |
| inode->sbi = dir->sbi; |
| inode->nid = ctx->de_nid; |
| ret = erofs_read_inode_from_disk(inode); |
| if (ret) |
| goto out; |
| |
| /* restore nid in new generated fs */ |
| inode->i_ino[1] = inode->i_ino[0]; |
| inode->i_ino[0] = nid; |
| inode->dev = inode->sbi->dev; |
| |
| if (S_ISREG(inode->i_mode) && inode->i_nlink > 1 && |
| (candidate = erofs_iget(inode->dev, ctx->de_nid))) { |
| /* hardlink file */ |
| erofs_iput(inode); |
| inode = candidate; |
| if (S_ISDIR(inode->i_mode)) { |
| erofs_err("hardlink directory not supported"); |
| ret = -EISDIR; |
| goto out; |
| } |
| inode->i_nlink++; |
| erofs_dbg("\thardlink: %s -> %s", path, inode->i_srcpath); |
| } else { |
| ret = erofs_read_xattrs_from_disk(inode); |
| if (ret) { |
| erofs_iput(inode); |
| goto out; |
| } |
| |
| inode->i_parent = d->inode; |
| inode->i_srcpath = path; |
| path = NULL; |
| inode->i_ino[1] = inode->nid; |
| inode->i_nlink = 1; |
| |
| ret = erofs_rebuild_update_inode(&g_sbi, inode, |
| rctx->datamode); |
| if (ret) { |
| erofs_iput(inode); |
| goto out; |
| } |
| |
| erofs_insert_ihash(inode); |
| mergedir = dir = inode; |
| } |
| |
| d->inode = inode; |
| d->type = erofs_mode_to_ftype(inode->i_mode); |
| } |
| |
| if (S_ISDIR(inode->i_mode)) { |
| struct erofs_rebuild_dir_context nctx = *rctx; |
| |
| nctx.mergedir = mergedir; |
| nctx.ctx.dir = dir; |
| ret = erofs_iterate_dir(&nctx.ctx, false); |
| if (ret) |
| goto out; |
| } |
| |
| /* reset sbi, nid after subdirs are all loaded for the final dump */ |
| inode->sbi = &g_sbi; |
| inode->nid = 0; |
| out: |
| free(path); |
| return ret; |
| } |
| |
| int erofs_rebuild_load_tree(struct erofs_inode *root, struct erofs_sb_info *sbi, |
| enum erofs_rebuild_datamode mode) |
| { |
| struct erofs_inode inode = {}; |
| struct erofs_rebuild_dir_context ctx; |
| char uuid_str[37]; |
| char *fsid = sbi->devname; |
| int ret; |
| |
| if (!fsid) { |
| erofs_uuid_unparse_lower(sbi->uuid, uuid_str); |
| fsid = uuid_str; |
| } |
| ret = erofs_read_superblock(sbi); |
| if (ret) { |
| erofs_err("failed to read superblock of %s", fsid); |
| return ret; |
| } |
| |
| inode.nid = sbi->root_nid; |
| inode.sbi = sbi; |
| ret = erofs_read_inode_from_disk(&inode); |
| if (ret) { |
| erofs_err("failed to read root inode of %s", fsid); |
| return ret; |
| } |
| inode.i_srcpath = strdup("/"); |
| |
| ctx = (struct erofs_rebuild_dir_context) { |
| .ctx.dir = &inode, |
| .ctx.cb = erofs_rebuild_dirent_iter, |
| .mergedir = root, |
| .datamode = mode, |
| }; |
| ret = erofs_iterate_dir(&ctx.ctx, false); |
| free(inode.i_srcpath); |
| return ret; |
| } |
| |
| static int erofs_rebuild_basedir_dirent_iter(struct erofs_dir_context *ctx) |
| { |
| struct erofs_rebuild_dir_context *rctx = (void *)ctx; |
| struct erofs_inode *dir = ctx->dir; |
| struct erofs_inode *mergedir = rctx->mergedir; |
| struct erofs_dentry *d; |
| char *dname; |
| bool dumb; |
| int ret; |
| |
| if (ctx->dot_dotdot) |
| return 0; |
| |
| dname = strndup(ctx->dname, ctx->de_namelen); |
| if (!dname) |
| return -ENOMEM; |
| d = erofs_rebuild_get_dentry(mergedir, dname, false, |
| &dumb, &dumb, false); |
| if (IS_ERR(d)) { |
| ret = PTR_ERR(d); |
| goto out; |
| } |
| |
| if (d->type == EROFS_FT_UNKNOWN) { |
| d->nid = ctx->de_nid; |
| d->type = ctx->de_ftype; |
| d->validnid = true; |
| if (!mergedir->whiteouts && erofs_dentry_is_wht(dir->sbi, d)) |
| mergedir->whiteouts = true; |
| } else { |
| struct erofs_inode *inode = d->inode; |
| |
| /* update sub-directories only for recursively loading */ |
| if (S_ISDIR(inode->i_mode)) { |
| list_del(&inode->i_hash); |
| inode->dev = dir->sbi->dev; |
| inode->i_ino[1] = ctx->de_nid; |
| erofs_insert_ihash(inode); |
| } |
| } |
| ret = 0; |
| out: |
| free(dname); |
| return ret; |
| } |
| |
| int erofs_rebuild_load_basedir(struct erofs_inode *dir) |
| { |
| struct erofs_inode fakeinode = { |
| .sbi = dir->sbi, |
| .nid = dir->i_ino[1], |
| }; |
| struct erofs_rebuild_dir_context ctx; |
| int ret; |
| |
| ret = erofs_read_inode_from_disk(&fakeinode); |
| if (ret) { |
| erofs_err("failed to read inode @ %llu", fakeinode.nid); |
| return ret; |
| } |
| |
| /* Inherit the maximum xattr size for the root directory */ |
| if (__erofs_unlikely(IS_ROOT(dir))) |
| dir->xattr_isize = fakeinode.xattr_isize; |
| |
| ctx = (struct erofs_rebuild_dir_context) { |
| .ctx.dir = &fakeinode, |
| .ctx.cb = erofs_rebuild_basedir_dirent_iter, |
| .mergedir = dir, |
| }; |
| return erofs_iterate_dir(&ctx.ctx, false); |
| } |