#include <sys/param.h>
#include <sys/mount.h>
#include <sys/stat.h>

#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <limits.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

struct erofs_test_fid {
	uint16_t len;
	uint16_t pad;
	uint32_t nid_hi;
	uint32_t nid_lo;
	uint32_t gen;
};

_Static_assert(sizeof(struct erofs_test_fid) == 16,
    "unexpected EROFS test file handle size");
_Static_assert(sizeof(struct erofs_test_fid) <= sizeof(struct fid),
    "EROFS test file handle does not fit struct fid");

static void
usage(void)
{
	fprintf(stderr,
	    "usage:\n"
	    "  nfs_fh_tool capture path handle\n"
	    "  nfs_fh_tool lcapture path handle\n"
	    "  nfs_fh_tool describe handle\n"
	    "  nfs_fh_tool compare handle1 handle2\n"
	    "  nfs_fh_tool stat handle\n"
	    "  nfs_fh_tool cat handle output\n"
	    "  nfs_fh_tool mutate input output field value\n"
	    "  nfs_fh_tool expect-stat handle errno\n"
	    "  nfs_fh_tool expect-open handle errno\n");
	exit(2);
}

static void
read_handle(const char *path, fhandle_t *fh)
{
	FILE *fp;

	fp = fopen(path, "rb");
	if (fp == NULL)
		err(1, "fopen %s", path);
	if (fread(fh, sizeof(*fh), 1, fp) != 1)
		err(1, "fread %s", path);
	if (fgetc(fp) != EOF)
		errx(1, "%s has trailing data", path);
	if (fclose(fp) != 0)
		err(1, "fclose %s", path);
}

static void
write_handle(const char *path, const fhandle_t *fh)
{
	FILE *fp;

	fp = fopen(path, "wb");
	if (fp == NULL)
		err(1, "fopen %s", path);
	if (fwrite(fh, sizeof(*fh), 1, fp) != 1)
		err(1, "fwrite %s", path);
	if (fclose(fp) != 0)
		err(1, "fclose %s", path);
}

static int
parse_errno(const char *name)
{
	char *end;
	long value;

	if (strcmp(name, "EINVAL") == 0)
		return (EINVAL);
	if (strcmp(name, "ESTALE") == 0)
		return (ESTALE);
	errno = 0;
	value = strtol(name, &end, 0);
	if (errno != 0 || *end != '\0' || value <= 0 || value > INT_MAX)
		errx(2, "invalid errno: %s", name);
	return ((int)value);
}

static uint64_t
parse_value(const char *text)
{
	char *end;
	uintmax_t value;

	errno = 0;
	value = strtoumax(text, &end, 0);
	if (errno != 0 || *end != '\0' || value > UINT64_MAX)
		errx(2, "invalid value: %s", text);
	return ((uint64_t)value);
}

static void
capture(const char *path, const char *output, int nofollow)
{
	fhandle_t fh;
	int error;

	bzero(&fh, sizeof(fh));
	error = nofollow ? lgetfh(path, &fh) : getfh(path, &fh);
	if (error != 0)
		err(1, "%s %s", nofollow ? "lgetfh" : "getfh", path);
	write_handle(output, &fh);
}

static void
describe(const char *path)
{
	struct erofs_test_fid efid;
	fhandle_t fh;
	uint64_t nid;

	read_handle(path, &fh);
	bzero(&efid, sizeof(efid));
	memcpy(&efid, &fh.fh_fid, sizeof(efid));
	nid = ((uint64_t)efid.nid_hi << 32) | efid.nid_lo;
	printf("fsid=%08x:%08x len=%u pad=%u nid=%016" PRIx64
	    " gen=%u\n", (unsigned int)fh.fh_fsid.val[0],
	    (unsigned int)fh.fh_fsid.val[1], efid.len, efid.pad, nid, efid.gen);
}

static void
compare(const char *left, const char *right)
{
	fhandle_t a, b;

	read_handle(left, &a);
	read_handle(right, &b);
	if (memcmp(&a, &b, sizeof(a)) != 0)
		errx(1, "file handles differ");
}

static void
stat_handle(const char *path)
{
	fhandle_t fh;
	struct stat sb;

	read_handle(path, &fh);
	if (fhstat(&fh, &sb) != 0)
		err(1, "fhstat %s", path);
	printf("mode=%#o ino=%ju gen=%u size=%jd\n", (unsigned int)sb.st_mode,
	    (uintmax_t)sb.st_ino, (unsigned int)sb.st_gen,
	    (intmax_t)sb.st_size);
}

static void
cat_handle(const char *handle, const char *output)
{
	char buf[65536];
	fhandle_t fh;
	ssize_t nr, nw;
	int fd, outfd;

	read_handle(handle, &fh);
	fd = fhopen(&fh, O_RDONLY);
	if (fd < 0)
		err(1, "fhopen %s", handle);
	outfd = open(output, O_WRONLY | O_CREAT | O_TRUNC, 0600);
	if (outfd < 0)
		err(1, "open %s", output);
	while ((nr = read(fd, buf, sizeof(buf))) > 0) {
		for (ssize_t done = 0; done < nr; done += nw) {
			nw = write(outfd, buf + done, nr - done);
			if (nw < 0)
				err(1, "write %s", output);
		}
	}
	if (nr < 0)
		err(1, "read %s", handle);
	if (close(outfd) != 0)
		err(1, "close %s", output);
	if (close(fd) != 0)
		err(1, "close fhopen");
}

static void
mutate(const char *input, const char *output, const char *field,
    const char *text)
{
	struct erofs_test_fid efid;
	fhandle_t fh;
	uint64_t value;

	read_handle(input, &fh);
	bzero(&efid, sizeof(efid));
	memcpy(&efid, &fh.fh_fid, sizeof(efid));
	value = parse_value(text);
	if (strcmp(field, "len") == 0) {
		if (value > UINT16_MAX)
			errx(2, "len is too large");
		efid.len = value;
	} else if (strcmp(field, "pad") == 0) {
		if (value > UINT16_MAX)
			errx(2, "pad is too large");
		efid.pad = value;
	} else if (strcmp(field, "nid_hi") == 0) {
		if (value > UINT32_MAX)
			errx(2, "nid_hi is too large");
		efid.nid_hi = value;
	} else if (strcmp(field, "nid_lo") == 0) {
		if (value > UINT32_MAX)
			errx(2, "nid_lo is too large");
		efid.nid_lo = value;
	} else if (strcmp(field, "gen") == 0) {
		if (value > UINT32_MAX)
			errx(2, "gen is too large");
		efid.gen = value;
	} else if (strcmp(field, "gen_xor") == 0) {
		if (value == 0 || value > UINT32_MAX)
			errx(2, "gen_xor must be a non-zero uint32_t");
		efid.gen ^= value;
	} else {
		errx(2, "unknown field: %s", field);
	}
	memcpy(&fh.fh_fid, &efid, sizeof(efid));
	write_handle(output, &fh);
}

static void
expect_failure(const char *path, const char *error_name, int use_open)
{
	fhandle_t fh;
	struct stat sb;
	int expected, fd, result;

	read_handle(path, &fh);
	expected = parse_errno(error_name);
	errno = 0;
	if (use_open) {
		fd = fhopen(&fh, O_RDONLY);
		result = fd;
		if (fd >= 0)
			close(fd);
	} else {
		result = fhstat(&fh, &sb);
	}
	if (result != -1)
		errx(1, "%s unexpectedly succeeded", use_open ? "fhopen" : "fhstat");
	if (errno != expected)
		errx(1, "%s returned %s, expected %s",
		    use_open ? "fhopen" : "fhstat", strerror(errno),
		    strerror(expected));
}

int
main(int argc, char **argv)
{
	if (argc == 4 && strcmp(argv[1], "capture") == 0)
		capture(argv[2], argv[3], 0);
	else if (argc == 4 && strcmp(argv[1], "lcapture") == 0)
		capture(argv[2], argv[3], 1);
	else if (argc == 3 && strcmp(argv[1], "describe") == 0)
		describe(argv[2]);
	else if (argc == 4 && strcmp(argv[1], "compare") == 0)
		compare(argv[2], argv[3]);
	else if (argc == 3 && strcmp(argv[1], "stat") == 0)
		stat_handle(argv[2]);
	else if (argc == 4 && strcmp(argv[1], "cat") == 0)
		cat_handle(argv[2], argv[3]);
	else if (argc == 6 && strcmp(argv[1], "mutate") == 0)
		mutate(argv[2], argv[3], argv[4], argv[5]);
	else if (argc == 4 && strcmp(argv[1], "expect-stat") == 0)
		expect_failure(argv[2], argv[3], 0);
	else if (argc == 4 && strcmp(argv[1], "expect-open") == 0)
		expect_failure(argv[2], argv[3], 1);
	else
		usage();
	return (0);
}
