// SPDX-License-Identifier: GPL-2.0+
/*
 * Copyright (C) 2018-2019 HUAWEI, Inc.
 *             http://www.huawei.com/
 * Created by Li Guifu <bluce.liguifu@huawei.com>
 */
#define _GNU_SOURCE
#include <time.h>
#include <sys/time.h>
#include <stdlib.h>
#include <limits.h>
#include <libgen.h>
#include <sys/stat.h>
#include <getopt.h>
#include "erofs/config.h"
#include "erofs/print.h"
#include "erofs/cache.h"
#include "erofs/diskbuf.h"
#include "erofs/inode.h"
#include "erofs/tar.h"
#include "erofs/io.h"
#include "erofs/compress.h"
#include "erofs/dedupe.h"
#include "erofs/xattr.h"
#include "erofs/exclude.h"
#include "erofs/block_list.h"
#include "erofs/compress_hints.h"
#include "erofs/blobchunk.h"
#include "erofs/fragments.h"
#include "erofs/rebuild.h"
#include "../lib/liberofs_private.h"
#include "../lib/liberofs_uuid.h"

#define EROFS_SUPER_END (EROFS_SUPER_OFFSET + sizeof(struct erofs_super_block))

static struct option long_options[] = {
	{"help", no_argument, 0, 1},
	{"exclude-path", required_argument, NULL, 2},
	{"exclude-regex", required_argument, NULL, 3},
#ifdef HAVE_LIBSELINUX
	{"file-contexts", required_argument, NULL, 4},
#endif
	{"force-uid", required_argument, NULL, 5},
	{"force-gid", required_argument, NULL, 6},
	{"all-root", no_argument, NULL, 7},
#ifndef NDEBUG
	{"random-pclusterblks", no_argument, NULL, 8},
	{"random-algorithms", no_argument, NULL, 18},
#endif
	{"max-extent-bytes", required_argument, NULL, 9},
	{"compress-hints", required_argument, NULL, 10},
	{"chunksize", required_argument, NULL, 11},
	{"quiet", no_argument, 0, 12},
	{"blobdev", required_argument, NULL, 13},
	{"ignore-mtime", no_argument, NULL, 14},
	{"preserve-mtime", no_argument, NULL, 15},
	{"uid-offset", required_argument, NULL, 16},
	{"gid-offset", required_argument, NULL, 17},
	{"tar", optional_argument, NULL, 20},
	{"aufs", no_argument, NULL, 21},
	{"mount-point", required_argument, NULL, 512},
	{"xattr-prefix", required_argument, NULL, 19},
#ifdef WITH_ANDROID
	{"product-out", required_argument, NULL, 513},
	{"fs-config-file", required_argument, NULL, 514},
	{"block-list-file", required_argument, NULL, 515},
#endif
	{"ovlfs-strip", optional_argument, NULL, 516},
#ifdef HAVE_ZLIB
	{"gzip", no_argument, NULL, 517},
#endif
	{0, 0, 0, 0},
};

static void print_available_compressors(FILE *f, const char *delim)
{
	int i = 0;
	bool comma = false;
	const char *s;

	while ((s = z_erofs_list_available_compressors(&i)) != NULL) {
		if (comma)
			fputs(delim, f);
		fputs(s, f);
		comma = true;
	}
	fputc('\n', f);
}

static void usage(void)
{
	fputs("usage: [options] FILE SOURCE(s)\n"
	      "Generate EROFS image (FILE) from DIRECTORY, TARBALL and/or EROFS images.  And [options] are:\n"
	      " -b#                   set block size to # (# = page size by default)\n"
	      " -d#                   set output message level to # (maximum 9)\n"
	      " -x#                   set xattr tolerance to # (< 0, disable xattrs; default 2)\n"
	      " -zX[,Y][:..]          X=compressor (Y=compression level, optional)\n"
	      "                       alternative algorithms can be separated by colons(:)\n"
	      " -C#                   specify the size of compress physical cluster in bytes\n"
	      " -EX[,...]             X=extended options\n"
	      " -L volume-label       set the volume label (maximum 16)\n"
	      " -T#                   set a fixed UNIX timestamp # to all files\n"
	      " -UX                   use a given filesystem UUID\n"
	      " --all-root            make all files owned by root\n"
	      " --blobdev=X           specify an extra device X to store chunked data\n"
	      " --chunksize=#         generate chunk-based files with #-byte chunks\n"
	      " --compress-hints=X    specify a file to configure per-file compression strategy\n"
	      " --exclude-path=X      avoid including file X (X = exact literal path)\n"
	      " --exclude-regex=X     avoid including files that match X (X = regular expression)\n"
#ifdef HAVE_LIBSELINUX
	      " --file-contexts=X     specify a file contexts file to setup selinux labels\n"
#endif
	      " --force-uid=#         set all file uids to # (# = UID)\n"
	      " --force-gid=#         set all file gids to # (# = GID)\n"
	      " --uid-offset=#        add offset # to all file uids (# = id offset)\n"
	      " --gid-offset=#        add offset # to all file gids (# = id offset)\n"
#ifdef HAVE_ZLIB
	      " --gzip                try to filter the tarball stream through gzip\n"
#endif
	      " --help                display this help and exit\n"
	      " --ignore-mtime        use build time instead of strict per-file modification time\n"
	      " --max-extent-bytes=#  set maximum decompressed extent size # in bytes\n"
	      " --preserve-mtime      keep per-file modification time strictly\n"
	      " --aufs                replace aufs special files with overlayfs metadata\n"
	      " --tar=[fi]            generate an image from tarball(s)\n"
	      " --ovlfs-strip=[01]    strip overlayfs metadata in the target image (e.g. whiteouts)\n"
	      " --quiet               quiet execution (do not write anything to standard output.)\n"
#ifndef NDEBUG
	      " --random-pclusterblks randomize pclusterblks for big pcluster (debugging only)\n"
	      " --random-algorithms   randomize per-file algorithms (debugging only)\n"
#endif
	      " --xattr-prefix=X      X=extra xattr name prefix\n"
	      " --mount-point=X       X=prefix of target fs path (default: /)\n"
#ifdef WITH_ANDROID
	      "\nwith following android-specific options:\n"
	      " --product-out=X       X=product_out directory\n"
	      " --fs-config-file=X    X=fs_config file\n"
	      " --block-list-file=X   X=block_list file\n"
#endif
	      "\nAvailable compressors are: ", stderr);
	print_available_compressors(stderr, ", ");
}

static unsigned int pclustersize_packed, pclustersize_max;
static struct erofs_tarfile erofstar = {
	.global.xattrs = LIST_HEAD_INIT(erofstar.global.xattrs)
};
static bool tar_mode, rebuild_mode, gzip_supported;

static unsigned int rebuild_src_count;
static LIST_HEAD(rebuild_src_list);

static int parse_extended_opts(const char *opts)
{
#define MATCH_EXTENTED_OPT(opt, token, keylen) \
	(keylen == sizeof(opt) - 1 && !memcmp(token, opt, sizeof(opt) - 1))

	const char *token, *next, *tokenend, *value __maybe_unused;
	unsigned int keylen, vallen;

	value = NULL;
	for (token = opts; *token != '\0'; token = next) {
		bool clear = false;
		const char *p = strchr(token, ',');

		next = NULL;
		if (p) {
			next = p + 1;
		} else {
			p = token + strlen(token);
			next = p;
		}

		tokenend = memchr(token, '=', p - token);
		if (tokenend) {
			keylen = tokenend - token;
			vallen = p - tokenend - 1;
			if (!vallen)
				return -EINVAL;

			value = tokenend + 1;
		} else {
			keylen = p - token;
			vallen = 0;
		}

		if (token[0] == '^') {
			if (keylen < 2)
				return -EINVAL;
			++token;
			--keylen;
			clear = true;
		}

		if (MATCH_EXTENTED_OPT("legacy-compress", token, keylen)) {
			if (vallen)
				return -EINVAL;
			/* disable compacted indexes and 0padding */
			cfg.c_legacy_compress = true;
		} else if (MATCH_EXTENTED_OPT("force-inode-compact", token, keylen)) {
			if (vallen)
				return -EINVAL;
			cfg.c_force_inodeversion = FORCE_INODE_COMPACT;
			cfg.c_ignore_mtime = true;
		} else if (MATCH_EXTENTED_OPT("force-inode-extended", token, keylen)) {
			if (vallen)
				return -EINVAL;
			cfg.c_force_inodeversion = FORCE_INODE_EXTENDED;
		} else if (MATCH_EXTENTED_OPT("nosbcrc", token, keylen)) {
			if (vallen)
				return -EINVAL;
			erofs_sb_clear_sb_chksum(&sbi);
		} else if (MATCH_EXTENTED_OPT("noinline_data", token, keylen)) {
			if (vallen)
				return -EINVAL;
			cfg.c_inline_data = false;
		} else if (MATCH_EXTENTED_OPT("inline_data", token, keylen)) {
			if (vallen)
				return -EINVAL;
			cfg.c_inline_data = !clear;
		} else if (MATCH_EXTENTED_OPT("force-inode-blockmap", token, keylen)) {
			if (vallen)
				return -EINVAL;
			cfg.c_force_chunkformat = FORCE_INODE_BLOCK_MAP;
		} else if (MATCH_EXTENTED_OPT("force-chunk-indexes", token, keylen)) {
			if (vallen)
				return -EINVAL;
			cfg.c_force_chunkformat = FORCE_INODE_CHUNK_INDEXES;
		} else if (MATCH_EXTENTED_OPT("ztailpacking", token, keylen)) {
			if (vallen)
				return -EINVAL;
			cfg.c_ztailpacking = !clear;
		} else if (MATCH_EXTENTED_OPT("all-fragments", token, keylen)) {
			cfg.c_all_fragments = true;
			goto handle_fragment;
		} else if (MATCH_EXTENTED_OPT("fragments", token, keylen)) {
			char *endptr;
			u64 i;

handle_fragment:
			cfg.c_fragments = true;
			if (vallen) {
				i = strtoull(value, &endptr, 0);
				if (endptr - value != vallen) {
					erofs_err("invalid pcluster size for the packed file %s",
						  next);
					return -EINVAL;
				}
				pclustersize_packed = i;
			}
		} else if (MATCH_EXTENTED_OPT("dedupe", token, keylen)) {
			if (vallen)
				return -EINVAL;
			cfg.c_dedupe = !clear;
		} else if (MATCH_EXTENTED_OPT("xattr-name-filter", token, keylen)) {
			if (vallen)
				return -EINVAL;
			cfg.c_xattr_name_filter = !clear;
		} else {
			erofs_err("unknown extended option %.*s",
				  p - token, token);
			return -EINVAL;
		}
	}
	return 0;
}

static int mkfs_parse_compress_algs(char *algs)
{
	unsigned int i;
	char *s;

	for (s = strtok(algs, ":"), i = 0; s; s = strtok(NULL, ":"), ++i) {
		const char *lv;

		if (i >= EROFS_MAX_COMPR_CFGS - 1) {
			erofs_err("too many algorithm types");
			return -EINVAL;
		}

		lv = strchr(s, ',');
		if (lv) {
			cfg.c_compr_level[i] = atoi(lv + 1);
			cfg.c_compr_alg[i] = strndup(s, lv - s);
		} else {
			cfg.c_compr_level[i] = -1;
			cfg.c_compr_alg[i] = strdup(s);
		}
	}
	return 0;
}

static void erofs_rebuild_cleanup(void)
{
	struct erofs_sb_info *src, *n;

	list_for_each_entry_safe(src, n, &rebuild_src_list, list) {
		list_del(&src->list);
		erofs_put_super(src);
		dev_close(src);
		free(src);
	}
	rebuild_src_count = 0;
}

static int mkfs_parse_options_cfg(int argc, char *argv[])
{
	char *endptr;
	int opt, i, err;
	bool quiet = false;

	while ((opt = getopt_long(argc, argv, "C:E:L:T:U:b:d:x:z:",
				  long_options, NULL)) != -1) {
		switch (opt) {
		case 'z':
			i = mkfs_parse_compress_algs(optarg);
			if (i)
				return i;
			break;

		case 'b':
			i = atoi(optarg);
			if (i < 512 || i > EROFS_MAX_BLOCK_SIZE) {
				erofs_err("invalid block size %s", optarg);
				return -EINVAL;
			}
			sbi.blkszbits = ilog2(i);
			break;

		case 'd':
			i = atoi(optarg);
			if (i < EROFS_MSG_MIN || i > EROFS_MSG_MAX) {
				erofs_err("invalid debug level %d", i);
				return -EINVAL;
			}
			cfg.c_dbg_lvl = i;
			break;

		case 'x':
			i = strtol(optarg, &endptr, 0);
			if (*endptr != '\0') {
				erofs_err("invalid xattr tolerance %s", optarg);
				return -EINVAL;
			}
			cfg.c_inline_xattr_tolerance = i;
			break;

		case 'E':
			opt = parse_extended_opts(optarg);
			if (opt)
				return opt;
			break;

		case 'L':
			if (optarg == NULL ||
			    strlen(optarg) > sizeof(sbi.volume_name)) {
				erofs_err("invalid volume label");
				return -EINVAL;
			}
			strncpy(sbi.volume_name, optarg,
				sizeof(sbi.volume_name));
			break;

		case 'T':
			cfg.c_unix_timestamp = strtoull(optarg, &endptr, 0);
			if (cfg.c_unix_timestamp == -1 || *endptr != '\0') {
				erofs_err("invalid UNIX timestamp %s", optarg);
				return -EINVAL;
			}
			cfg.c_timeinherit = TIMESTAMP_FIXED;
			break;
		case 'U':
			if (erofs_uuid_parse(optarg, sbi.uuid)) {
				erofs_err("invalid UUID %s", optarg);
				return -EINVAL;
			}
			break;
		case 2:
			opt = erofs_parse_exclude_path(optarg, false);
			if (opt) {
				erofs_err("failed to parse exclude path: %s",
					  erofs_strerror(opt));
				return opt;
			}
			break;
		case 3:
			opt = erofs_parse_exclude_path(optarg, true);
			if (opt) {
				erofs_err("failed to parse exclude regex: %s",
					  erofs_strerror(opt));
				return opt;
			}
			break;

		case 4:
			opt = erofs_selabel_open(optarg);
			if (opt && opt != -EBUSY)
				return opt;
			break;
		case 5:
			cfg.c_uid = strtoul(optarg, &endptr, 0);
			if (cfg.c_uid == -1 || *endptr != '\0') {
				erofs_err("invalid uid %s", optarg);
				return -EINVAL;
			}
			break;
		case 6:
			cfg.c_gid = strtoul(optarg, &endptr, 0);
			if (cfg.c_gid == -1 || *endptr != '\0') {
				erofs_err("invalid gid %s", optarg);
				return -EINVAL;
			}
			break;
		case 7:
			cfg.c_uid = cfg.c_gid = 0;
			break;
#ifndef NDEBUG
		case 8:
			cfg.c_random_pclusterblks = true;
			break;
		case 18:
			cfg.c_random_algorithms = true;
			break;
#endif
		case 9:
			cfg.c_max_decompressed_extent_bytes =
				strtoul(optarg, &endptr, 0);
			if (*endptr != '\0') {
				erofs_err("invalid maximum uncompressed extent size %s",
					  optarg);
				return -EINVAL;
			}
			break;
		case 10:
			cfg.c_compress_hints_file = optarg;
			break;
		case 512:
			cfg.mount_point = optarg;
			/* all trailing '/' should be deleted */
			opt = strlen(cfg.mount_point);
			if (opt && optarg[opt - 1] == '/')
				optarg[opt - 1] = '\0';
			break;
#ifdef WITH_ANDROID
		case 513:
			cfg.target_out_path = optarg;
			break;
		case 514:
			cfg.fs_config_file = optarg;
			break;
		case 515:
			cfg.block_list_file = optarg;
			break;
#endif
		case 'C':
			i = strtoull(optarg, &endptr, 0);
			if (*endptr != '\0') {
				erofs_err("invalid physical clustersize %s",
					  optarg);
				return -EINVAL;
			}
			pclustersize_max = i;
			break;
		case 11:
			i = strtol(optarg, &endptr, 0);
			if (*endptr != '\0') {
				erofs_err("invalid chunksize %s", optarg);
				return -EINVAL;
			}
			cfg.c_chunkbits = ilog2(i);
			if ((1 << cfg.c_chunkbits) != i) {
				erofs_err("chunksize %s must be a power of two",
					  optarg);
				return -EINVAL;
			}
			erofs_sb_set_chunked_file(&sbi);
			break;
		case 12:
			quiet = true;
			break;
		case 13:
			cfg.c_blobdev_path = optarg;
			break;
		case 14:
			cfg.c_ignore_mtime = true;
			break;
		case 15:
			cfg.c_ignore_mtime = false;
			break;
		case 16:
			errno = 0;
			cfg.c_uid_offset = strtoll(optarg, &endptr, 0);
			if (errno || *endptr != '\0') {
				erofs_err("invalid uid offset %s", optarg);
				return -EINVAL;
			}
			break;
		case 17:
			errno = 0;
			cfg.c_gid_offset = strtoll(optarg, &endptr, 0);
			if (errno || *endptr != '\0') {
				erofs_err("invalid gid offset %s", optarg);
				return -EINVAL;
			}
			break;
		case 19:
			errno = 0;
			opt = erofs_xattr_insert_name_prefix(optarg);
			if (opt) {
				erofs_err("failed to parse xattr name prefix: %s",
					  erofs_strerror(opt));
				return opt;
			}
			cfg.c_extra_ea_name_prefixes = true;
			break;
		case 20:
			if (optarg && (!strcmp(optarg, "i") ||
				!strcmp(optarg, "0") || !memcmp(optarg, "0,", 2))) {
				erofstar.index_mode = true;
				if (!memcmp(optarg, "0,", 2))
					erofstar.mapfile = strdup(optarg + 2);
			}
			tar_mode = true;
			break;
		case 21:
			erofstar.aufs = true;
			break;
		case 516:
			if (!optarg || !strcmp(optarg, "1"))
				cfg.c_ovlfs_strip = true;
			else
				cfg.c_ovlfs_strip = false;
			break;
		case 517:
			gzip_supported = true;
			break;
		case 1:
			usage();
			exit(0);

		default: /* '?' */
			return -EINVAL;
		}
	}

	if (cfg.c_blobdev_path && cfg.c_chunkbits < sbi.blkszbits) {
		erofs_err("--blobdev must be used together with --chunksize");
		return -EINVAL;
	}

	/* TODO: can be implemented with (deviceslot) mapped_blkaddr */
	if (cfg.c_blobdev_path &&
	    cfg.c_force_chunkformat == FORCE_INODE_BLOCK_MAP) {
		erofs_err("--blobdev cannot work with block map currently");
		return -EINVAL;
	}

	if (optind >= argc) {
		erofs_err("missing argument: FILE");
		return -EINVAL;
	}

	cfg.c_img_path = strdup(argv[optind++]);
	if (!cfg.c_img_path)
		return -ENOMEM;

	if (optind >= argc) {
		if (!tar_mode) {
			erofs_err("missing argument: SOURCE(s)");
			return -EINVAL;
		} else {
			int dupfd;

			dupfd = dup(STDIN_FILENO);
			if (dupfd < 0) {
				erofs_err("failed to duplicate STDIN_FILENO: %s",
					  strerror(errno));
				return -errno;
			}
			err = erofs_iostream_open(&erofstar.ios, dupfd, gzip_supported);
			if (err)
				return err;
		}
	} else {
		struct stat st;

		cfg.c_src_path = realpath(argv[optind++], NULL);
		if (!cfg.c_src_path) {
			erofs_err("failed to parse source directory: %s",
				  erofs_strerror(-errno));
			return -ENOENT;
		}

		if (tar_mode) {
			int fd = open(cfg.c_src_path, O_RDONLY);

			if (fd < 0) {
				erofs_err("failed to open file: %s", cfg.c_src_path);
				return -errno;
			}
			err = erofs_iostream_open(&erofstar.ios, fd, gzip_supported);
			if (err)
				return err;
		} else {
			err = lstat(cfg.c_src_path, &st);
			if (err)
				return -errno;
			if (S_ISDIR(st.st_mode))
				erofs_set_fs_root(cfg.c_src_path);
			else
				rebuild_mode = true;
		}

		if (rebuild_mode) {
			char *srcpath = cfg.c_src_path;
			struct erofs_sb_info *src;

			do {
				src = calloc(1, sizeof(struct erofs_sb_info));
				if (!src) {
					erofs_rebuild_cleanup();
					return -ENOMEM;
				}

				err = dev_open_ro(src, srcpath);
				if (err) {
					free(src);
					erofs_rebuild_cleanup();
					return err;
				}

				/* extra device index starts from 1 */
				src->dev = ++rebuild_src_count;
				list_add(&src->list, &rebuild_src_list);
			} while (optind < argc && (srcpath = argv[optind++]));
		} else if (optind < argc) {
			erofs_err("unexpected argument: %s\n", argv[optind]);
			return -EINVAL;
		}
	}
	if (quiet) {
		cfg.c_dbg_lvl = EROFS_ERR;
		cfg.c_showprogress = false;
	}

	if (cfg.c_compr_alg[0] && erofs_blksiz(&sbi) != getpagesize())
		erofs_warn("Please note that subpage blocksize with compression isn't yet supported in kernel. "
			   "This compressed image will only work with bs = ps = %u bytes",
			   erofs_blksiz(&sbi));

	if (pclustersize_max) {
		if (pclustersize_max < erofs_blksiz(&sbi) ||
		    pclustersize_max % erofs_blksiz(&sbi)) {
			erofs_err("invalid physical clustersize %u",
				  pclustersize_max);
			return -EINVAL;
		}
		cfg.c_pclusterblks_max = pclustersize_max >> sbi.blkszbits;
		cfg.c_pclusterblks_def = cfg.c_pclusterblks_max;
	}
	if (cfg.c_chunkbits && cfg.c_chunkbits < sbi.blkszbits) {
		erofs_err("chunksize %u must be larger than block size",
			  1u << cfg.c_chunkbits);
		return -EINVAL;
	}

	if (pclustersize_packed) {
		if (pclustersize_max < erofs_blksiz(&sbi) ||
		    pclustersize_max % erofs_blksiz(&sbi)) {
			erofs_err("invalid pcluster size for the packed file %u",
				  pclustersize_packed);
			return -EINVAL;
		}
		cfg.c_pclusterblks_packed = pclustersize_packed >> sbi.blkszbits;
	}
	return 0;
}

int erofs_mkfs_update_super_block(struct erofs_buffer_head *bh,
				  erofs_nid_t root_nid,
				  erofs_blk_t *blocks,
				  erofs_nid_t packed_nid)
{
	struct erofs_super_block sb = {
		.magic     = cpu_to_le32(EROFS_SUPER_MAGIC_V1),
		.blkszbits = sbi.blkszbits,
		.inos   = cpu_to_le64(sbi.inos),
		.build_time = cpu_to_le64(sbi.build_time),
		.build_time_nsec = cpu_to_le32(sbi.build_time_nsec),
		.blocks = 0,
		.meta_blkaddr  = cpu_to_le32(sbi.meta_blkaddr),
		.xattr_blkaddr = cpu_to_le32(sbi.xattr_blkaddr),
		.xattr_prefix_count = sbi.xattr_prefix_count,
		.xattr_prefix_start = cpu_to_le32(sbi.xattr_prefix_start),
		.feature_incompat = cpu_to_le32(sbi.feature_incompat),
		.feature_compat = cpu_to_le32(sbi.feature_compat &
					      ~EROFS_FEATURE_COMPAT_SB_CHKSUM),
		.extra_devices = cpu_to_le16(sbi.extra_devices),
		.devt_slotoff = cpu_to_le16(sbi.devt_slotoff),
	};
	const u32 sb_blksize = round_up(EROFS_SUPER_END, erofs_blksiz(&sbi));
	char *buf;
	int ret;

	*blocks         = erofs_mapbh(NULL);
	sb.blocks       = cpu_to_le32(*blocks);
	sb.root_nid     = cpu_to_le16(root_nid);
	sb.packed_nid    = cpu_to_le64(packed_nid);
	memcpy(sb.uuid, sbi.uuid, sizeof(sb.uuid));
	memcpy(sb.volume_name, sbi.volume_name, sizeof(sb.volume_name));

	if (erofs_sb_has_compr_cfgs(&sbi))
		sb.u1.available_compr_algs = cpu_to_le16(sbi.available_compr_algs);
	else
		sb.u1.lz4_max_distance = cpu_to_le16(sbi.lz4_max_distance);

	buf = calloc(sb_blksize, 1);
	if (!buf) {
		erofs_err("failed to allocate memory for sb: %s",
			  erofs_strerror(-errno));
		return -ENOMEM;
	}
	memcpy(buf + EROFS_SUPER_OFFSET, &sb, sizeof(sb));

	ret = dev_write(&sbi, buf, erofs_btell(bh, false), EROFS_SUPER_END);
	free(buf);
	erofs_bdrop(bh, false);
	return ret;
}

static int erofs_mkfs_superblock_csum_set(void)
{
	int ret;
	u8 buf[EROFS_MAX_BLOCK_SIZE];
	u32 crc;
	unsigned int len;
	struct erofs_super_block *sb;

	ret = blk_read(&sbi, 0, buf, 0, erofs_blknr(&sbi, EROFS_SUPER_END) + 1);
	if (ret) {
		erofs_err("failed to read superblock to set checksum: %s",
			  erofs_strerror(ret));
		return ret;
	}

	/*
	 * skip the first 1024 bytes, to allow for the installation
	 * of x86 boot sectors and other oddities.
	 */
	sb = (struct erofs_super_block *)(buf + EROFS_SUPER_OFFSET);

	if (le32_to_cpu(sb->magic) != EROFS_SUPER_MAGIC_V1) {
		erofs_err("internal error: not an erofs valid image");
		return -EFAULT;
	}

	/* turn on checksum feature */
	sb->feature_compat = cpu_to_le32(le32_to_cpu(sb->feature_compat) |
					 EROFS_FEATURE_COMPAT_SB_CHKSUM);
	if (erofs_blksiz(&sbi) > EROFS_SUPER_OFFSET)
		len = erofs_blksiz(&sbi) - EROFS_SUPER_OFFSET;
	else
		len = erofs_blksiz(&sbi);
	crc = erofs_crc32c(~0, (u8 *)sb, len);

	/* set up checksum field to erofs_super_block */
	sb->checksum = cpu_to_le32(crc);

	ret = blk_write(&sbi, buf, 0, 1);
	if (ret) {
		erofs_err("failed to write checksummed superblock: %s",
			  erofs_strerror(ret));
		return ret;
	}

	erofs_info("superblock checksum 0x%08x written", crc);
	return 0;
}

static void erofs_mkfs_default_options(void)
{
	cfg.c_showprogress = true;
	cfg.c_legacy_compress = false;
	cfg.c_inline_data = true;
	cfg.c_xattr_name_filter = true;
	sbi.blkszbits = ilog2(min_t(u32, getpagesize(), EROFS_MAX_BLOCK_SIZE));
	sbi.feature_incompat = EROFS_FEATURE_INCOMPAT_ZERO_PADDING;
	sbi.feature_compat = EROFS_FEATURE_COMPAT_SB_CHKSUM |
			     EROFS_FEATURE_COMPAT_MTIME;

	/* generate a default uuid first */
	erofs_uuid_generate(sbi.uuid);
}

/* https://reproducible-builds.org/specs/source-date-epoch/ for more details */
int parse_source_date_epoch(void)
{
	char *source_date_epoch;
	unsigned long long epoch = -1ULL;
	char *endptr;

	source_date_epoch = getenv("SOURCE_DATE_EPOCH");
	if (!source_date_epoch)
		return 0;

	epoch = strtoull(source_date_epoch, &endptr, 10);
	if (epoch == -1ULL || *endptr != '\0') {
		erofs_err("environment variable $SOURCE_DATE_EPOCH %s is invalid",
			  source_date_epoch);
		return -EINVAL;
	}

	if (cfg.c_force_inodeversion != FORCE_INODE_EXTENDED)
		erofs_info("SOURCE_DATE_EPOCH is set, forcely generate extended inodes instead");

	cfg.c_force_inodeversion = FORCE_INODE_EXTENDED;
	cfg.c_unix_timestamp = epoch;
	cfg.c_timeinherit = TIMESTAMP_CLAMPING;
	return 0;
}

void erofs_show_progs(int argc, char *argv[])
{
	if (cfg.c_dbg_lvl >= EROFS_WARN)
		printf("%s %s\n", basename(argv[0]), cfg.c_version);
}
static struct erofs_inode *erofs_alloc_root_inode(void)
{
	struct erofs_inode *root;

	root = erofs_new_inode();
	if (IS_ERR(root))
		return root;
	root->i_srcpath = strdup("/");
	root->i_mode = S_IFDIR | 0777;
	root->i_parent = root;
	root->i_mtime = root->sbi->build_time;
	root->i_mtime_nsec = root->sbi->build_time_nsec;
	erofs_init_empty_dir(root);
	return root;
}

static int erofs_rebuild_load_trees(struct erofs_inode *root)
{
	struct erofs_sb_info *src;
	unsigned int extra_devices = 0;
	erofs_blk_t nblocks;
	int ret, idx;

	list_for_each_entry(src, &rebuild_src_list, list) {
		ret = erofs_rebuild_load_tree(root, src);
		if (ret) {
			erofs_err("failed to load %s", src->devname);
			return ret;
		}
		if (src->extra_devices > 1) {
			erofs_err("%s: unsupported number of extra devices",
				  src->devname, src->extra_devices);
			return -EOPNOTSUPP;
		}
		extra_devices += src->extra_devices;
	}

	if (extra_devices && extra_devices != rebuild_src_count) {
		erofs_err("extra_devices(%u) is mismatched with source images(%u)",
			  extra_devices, rebuild_src_count);
		return -EOPNOTSUPP;
	}

	ret = erofs_mkfs_init_devices(&sbi, rebuild_src_count);
	if (ret)
		return ret;

	list_for_each_entry(src, &rebuild_src_list, list) {
		u8 *tag = NULL;

		if (extra_devices) {
			nblocks = src->devs[0].blocks;
			tag = src->devs[0].tag;
		} else {
			nblocks = src->primarydevice_blocks;
		}
		DBG_BUGON(src->dev < 1);
		idx = src->dev - 1;
		sbi.devs[idx].blocks = nblocks;
		if (tag && *tag)
			memcpy(sbi.devs[idx].tag, tag, sizeof(sbi.devs[0].tag));
		else
			/* convert UUID of the source image to a hex string */
			sprintf((char *)sbi.devs[idx].tag,
				"%04x%04x%04x%04x%04x%04x%04x%04x",
				(src->uuid[0] << 8) | src->uuid[1],
				(src->uuid[2] << 8) | src->uuid[3],
				(src->uuid[4] << 8) | src->uuid[5],
				(src->uuid[6] << 8) | src->uuid[7],
				(src->uuid[8] << 8) | src->uuid[9],
				(src->uuid[10] << 8) | src->uuid[11],
				(src->uuid[12] << 8) | src->uuid[13],
				(src->uuid[14] << 8) | src->uuid[15]);
	}
	return 0;
}

static void erofs_mkfs_showsummaries(erofs_blk_t nblocks)
{
	char uuid_str[37] = {};

	if (!(cfg.c_dbg_lvl > EROFS_ERR && cfg.c_showprogress))
		return;

	erofs_uuid_unparse_lower(sbi.uuid, uuid_str);

	fprintf(stdout, "------\nFilesystem UUID: %s\n"
		"Filesystem total blocks: %u (of %u-byte blocks)\n"
		"Filesystem total inodes: %llu\n"
		"Filesystem total metadata blocks: %u\n"
		"Filesystem total deduplicated bytes (of source files): %llu\n",
		uuid_str, nblocks, 1U << sbi.blkszbits, sbi.inos | 0ULL,
		erofs_total_metablocks(),
		sbi.saved_by_deduplication | 0ULL);
}

int main(int argc, char **argv)
{
	int err = 0;
	struct erofs_buffer_head *sb_bh;
	struct erofs_inode *root_inode, *packed_inode;
	erofs_nid_t root_nid, packed_nid;
	erofs_blk_t nblocks;
	struct timeval t;
	FILE *packedfile = NULL;

	erofs_init_configure();
	erofs_mkfs_default_options();

	err = mkfs_parse_options_cfg(argc, argv);
	erofs_show_progs(argc, argv);
	if (err) {
		if (err == -EINVAL)
			usage();
		return 1;
	}

	err = parse_source_date_epoch();
	if (err) {
		usage();
		return 1;
	}

	if (cfg.c_unix_timestamp != -1) {
		sbi.build_time      = cfg.c_unix_timestamp;
		sbi.build_time_nsec = 0;
	} else if (!gettimeofday(&t, NULL)) {
		sbi.build_time      = t.tv_sec;
		sbi.build_time_nsec = t.tv_usec;
	}

	err = dev_open(&sbi, cfg.c_img_path);
	if (err) {
		usage();
		return 1;
	}

	if (tar_mode && !erofstar.index_mode) {
		err = erofs_diskbuf_init(1);
		if (err) {
			erofs_err("failed to initialize diskbuf: %s",
				   strerror(-err));
			goto exit;
		}
	}
#ifdef WITH_ANDROID
	if (cfg.fs_config_file &&
	    load_canned_fs_config(cfg.fs_config_file) < 0) {
		erofs_err("failed to load fs config %s", cfg.fs_config_file);
		return 1;
	}

	if (cfg.block_list_file &&
	    erofs_blocklist_open(cfg.block_list_file, false)) {
		erofs_err("failed to open %s", cfg.block_list_file);
		return 1;
	}
#endif
	erofs_show_config();
	if (cfg.c_fragments || cfg.c_extra_ea_name_prefixes) {
		if (!cfg.c_pclusterblks_packed)
			cfg.c_pclusterblks_packed = cfg.c_pclusterblks_def;

		packedfile = erofs_packedfile_init();
		if (IS_ERR(packedfile)) {
			erofs_err("failed to initialize packedfile");
			return 1;
		}
	}

	if (cfg.c_fragments) {
		err = z_erofs_fragments_init();
		if (err) {
			erofs_err("failed to initialize fragments");
			return 1;
		}
	}

#ifndef NDEBUG
	if (cfg.c_random_pclusterblks)
		srand(time(NULL));
#endif
	if (tar_mode && erofstar.index_mode) {
		if (erofstar.mapfile) {
			err = erofs_blocklist_open(erofstar.mapfile, true);
			if (err) {
				erofs_err("failed to open %s", erofstar.mapfile);
				goto exit;
			}
		} else {
			sbi.blkszbits = 9;
		}
	}

	if (rebuild_mode) {
		struct erofs_sb_info *src;

		erofs_warn("EXPERIMENTAL rebuild mode in use. Use at your own risk!");

		src = list_first_entry(&rebuild_src_list, struct erofs_sb_info, list);
		if (!src)
			goto exit;
		err = erofs_read_superblock(src);
		if (err) {
			erofs_err("failed to read superblock of %s", src->devname);
			goto exit;
		}
		sbi.blkszbits = src->blkszbits;
	}

	sb_bh = erofs_buffer_init();
	if (IS_ERR(sb_bh)) {
		err = PTR_ERR(sb_bh);
		erofs_err("failed to initialize buffers: %s",
			  erofs_strerror(err));
		goto exit;
	}
	err = erofs_bh_balloon(sb_bh, EROFS_SUPER_END);
	if (err < 0) {
		erofs_err("failed to balloon erofs_super_block: %s",
			  erofs_strerror(err));
		goto exit;
	}

	/* make sure that the super block should be the very first blocks */
	(void)erofs_mapbh(sb_bh->block);
	if (erofs_btell(sb_bh, false) != 0) {
		erofs_err("failed to reserve erofs_super_block");
		goto exit;
	}

	err = erofs_load_compress_hints(&sbi);
	if (err) {
		erofs_err("failed to load compress hints %s",
			  cfg.c_compress_hints_file);
		goto exit;
	}

	err = z_erofs_compress_init(&sbi, sb_bh);
	if (err) {
		erofs_err("failed to initialize compressor: %s",
			  erofs_strerror(err));
		goto exit;
	}

	if (cfg.c_dedupe) {
		if (!cfg.c_compr_alg[0]) {
			erofs_err("Compression is not enabled.  Turn on chunk-based data deduplication instead.");
			cfg.c_chunkbits = sbi.blkszbits;
		} else {
			err = z_erofs_dedupe_init(erofs_blksiz(&sbi));
			if (err) {
				erofs_err("failed to initialize deduplication: %s",
					  erofs_strerror(err));
				goto exit;
			}
		}
	}

	if (cfg.c_chunkbits) {
		err = erofs_blob_init(cfg.c_blobdev_path);
		if (err)
			return 1;
	}

	if ((erofstar.index_mode && !erofstar.mapfile) || cfg.c_blobdev_path)
		err = erofs_mkfs_init_devices(&sbi, 1);
	if (err) {
		erofs_err("failed to generate device table: %s",
			  erofs_strerror(err));
		goto exit;
	}

	erofs_inode_manager_init();

	if (tar_mode) {
		root_inode = erofs_alloc_root_inode();
		if (IS_ERR(root_inode)) {
			err = PTR_ERR(root_inode);
			goto exit;
		}

		while (!(err = tarerofs_parse_tar(root_inode, &erofstar)));

		if (err < 0)
			goto exit;

		err = erofs_rebuild_dump_tree(root_inode);
		if (err < 0)
			goto exit;
	} else if (rebuild_mode) {
		root_inode = erofs_alloc_root_inode();
		if (IS_ERR(root_inode)) {
			err = PTR_ERR(root_inode);
			goto exit;
		}

		err = erofs_rebuild_load_trees(root_inode);
		if (err)
			goto exit;
		err = erofs_rebuild_dump_tree(root_inode);
		if (err)
			goto exit;
	} else {
		err = erofs_build_shared_xattrs_from_path(&sbi, cfg.c_src_path);
		if (err) {
			erofs_err("failed to build shared xattrs: %s",
				  erofs_strerror(err));
			goto exit;
		}

		if (cfg.c_extra_ea_name_prefixes)
			erofs_xattr_write_name_prefixes(&sbi, packedfile);

		root_inode = erofs_mkfs_build_tree_from_path(cfg.c_src_path);
		if (IS_ERR(root_inode)) {
			err = PTR_ERR(root_inode);
			goto exit;
		}
	}
	root_nid = erofs_lookupnid(root_inode);
	erofs_iput(root_inode);

	if (erofstar.index_mode || cfg.c_chunkbits || sbi.extra_devices) {
		if (erofstar.index_mode && !erofstar.mapfile)
			sbi.devs[0].blocks =
				BLK_ROUND_UP(&sbi, erofstar.offset);
		err = erofs_mkfs_dump_blobs(&sbi);
		if (err)
			goto exit;
	}

	packed_nid = 0;
	if ((cfg.c_fragments || cfg.c_extra_ea_name_prefixes) &&
	    erofs_sb_has_fragments(&sbi)) {
		erofs_update_progressinfo("Handling packed_file ...");
		packed_inode = erofs_mkfs_build_packedfile();
		if (IS_ERR(packed_inode)) {
			err = PTR_ERR(packed_inode);
			goto exit;
		}
		packed_nid = erofs_lookupnid(packed_inode);
		erofs_iput(packed_inode);
	}

	/* flush all buffers except for the superblock */
	if (!erofs_bflush(NULL)) {
		err = -EIO;
		goto exit;
	}

	err = erofs_mkfs_update_super_block(sb_bh, root_nid, &nblocks,
					    packed_nid);
	if (err)
		goto exit;

	/* flush all remaining buffers */
	if (!erofs_bflush(NULL))
		err = -EIO;
	else
		err = dev_resize(&sbi, nblocks);

	if (!err && erofs_sb_has_sb_chksum(&sbi))
		err = erofs_mkfs_superblock_csum_set();
exit:
	z_erofs_compress_exit();
	z_erofs_dedupe_exit();
	erofs_blocklist_close();
	dev_close(&sbi);
	erofs_cleanup_compress_hints();
	erofs_cleanup_exclude_rules();
	if (cfg.c_chunkbits)
		erofs_blob_exit();
	if (cfg.c_fragments)
		z_erofs_fragments_exit();
	erofs_packedfile_exit();
	erofs_xattr_cleanup_name_prefixes();
	erofs_rebuild_cleanup();
	erofs_diskbuf_exit();
	erofs_exit_configure();
	if (tar_mode)
		erofs_iostream_close(&erofstar.ios);

	if (err) {
		erofs_err("\tCould not format the device : %s\n",
			  erofs_strerror(err));
		return 1;
	}
	erofs_update_progressinfo("Build completed.\n");
	erofs_mkfs_showsummaries(nblocks);
	return 0;
}
