// 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);
}
