// SPDX-License-Identifier: GPL-2.0+
/* Perform various tests on stat and statx output
 * Copyright (C) 2017 Red Hat, Inc. All Rights Reserved.
 * Written by David Howells (dhowells@redhat.com)
 */

#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/sysmacros.h>
#include "statx.h"

static bool failed = false;
static bool is_verbose = 0;
static const char *prog;
static const char *testfile;

/* Reference data */
static struct statx ref;
static struct statx_timestamp origin;
static bool ref_set, origin_set;

/*
 * Field IDs, sorted for bsearch() on field_list[].
 */
enum fields {
	stx_atime_tv_nsec,
	stx_atime_tv_sec,
	stx_attributes,
	stx_blksize,
	stx_blocks,
	stx_btime_tv_nsec,
	stx_btime_tv_sec,
	stx_ctime_tv_nsec,
	stx_ctime_tv_sec,
	stx_dev_major,
	stx_dev_minor,
	stx_gid,
	stx_ino,
	stx_mask,
	stx_mode,
	stx_mtime_tv_nsec,
	stx_mtime_tv_sec,
	stx_nlink,
	stx_rdev_major,
	stx_rdev_minor,
	stx_size,
	stx_type,
	stx_uid,
	nr__fields
};

struct field {
	const char *name;		/* Name on command line */
	unsigned int mask_bit;
};

/*
 * List of fields, sorted for bsearch().
 */
static const struct field field_list[nr__fields] = {
	[stx_atime_tv_nsec]	= { "stx_atime.tv_nsec",	STATX_ATIME },
	[stx_atime_tv_sec]	= { "stx_atime.tv_sec",		STATX_ATIME },
	[stx_attributes]	= { "stx_attributes",		0 },
	[stx_blksize]		= { "stx_blksize",		0 },
	[stx_blocks]		= { "stx_blocks",		STATX_BLOCKS },
	[stx_btime_tv_nsec]	= { "stx_btime.tv_nsec",	STATX_BTIME },
	[stx_btime_tv_sec]	= { "stx_btime.tv_sec",		STATX_BTIME },
	[stx_ctime_tv_nsec]	= { "stx_ctime.tv_nsec",	STATX_CTIME },
	[stx_ctime_tv_sec]	= { "stx_ctime.tv_sec",		STATX_CTIME },
	[stx_dev_major]		= { "stx_dev_major",		0 },
	[stx_dev_minor]		= { "stx_dev_minor",		0 },
	[stx_gid]		= { "stx_gid",			STATX_GID },
	[stx_ino]		= { "stx_ino",			STATX_INO },
	[stx_mask]		= { "stx_mask",			0 },
	[stx_mode]		= { "stx_mode",			STATX_MODE },
	[stx_mtime_tv_nsec]	= { "stx_mtime.tv_nsec",	STATX_MTIME },
	[stx_mtime_tv_sec]	= { "stx_mtime.tv_sec",		STATX_MTIME },
	[stx_nlink]		= { "stx_nlink",		STATX_NLINK },
	[stx_rdev_major]	= { "stx_rdev_major",		0 },
	[stx_rdev_minor]	= { "stx_rdev_minor",		0 },
	[stx_size]		= { "stx_size",			STATX_SIZE },
	[stx_type]		= { "stx_type",			STATX_TYPE },
	[stx_uid]		= { "stx_uid",			STATX_UID },
};

static int field_cmp(const void *_key, const void *_p)
{
	const char *key = _key;
	const struct field *p = _p;
	return strcmp(key, p->name);
}

/*
 * Sorted list of attribute flags for bsearch().
 */
struct attr_name {
	const char	*name;
	__u64		attr_flag;
};

static const struct attr_name attr_list[] = {
	{ "append",	STATX_ATTR_APPEND },
	{ "automount",	STATX_ATTR_AUTOMOUNT },
	{ "compressed",	STATX_ATTR_COMPRESSED },
	{ "encrypted",	STATX_ATTR_ENCRYPTED },
	{ "immutable",	STATX_ATTR_IMMUTABLE },
	{ "nodump",	STATX_ATTR_NODUMP },
};

static int attr_name_cmp(const void *_key, const void *_p)
{
	const char *key = _key;
	const struct attr_name *p = _p;
	return strcmp(key, p->name);
}

struct file_type {
	const char *name;
	mode_t mode;
};

/*
 * List of file types.
 */
static const struct file_type file_types[] = {
	{ "fifo",	S_IFIFO },
	{ "char",	S_IFCHR },
	{ "dir",	S_IFDIR },
	{ "block",	S_IFBLK },
	{ "file",	S_IFREG },
	{ "sym",	S_IFLNK },
	{ "sock",	S_IFSOCK },
	{ NULL }
};

static __attribute__((noreturn))
void format(void)
{
	fprintf(stderr, "usage: %s --check-statx\n", prog);
	fprintf(stderr, "usage: %s [-v] [-m<mask>] <testfile> [checks]\n", prog);
	fprintf(stderr, "\t<mask> can be basic, all or a number; all is the default\n");
	fprintf(stderr, "checks is a list of zero or more of:\n");
	fprintf(stderr, "\tattr=[+-]<name> -- check an attribute in stx_attributes\n");
	fprintf(stderr, "\t\tappend -- The file is marked as append only\n");
	fprintf(stderr, "\t\tautomount -- The object is an automount point\n");
	fprintf(stderr, "\t\tcompressed -- The file is marked as compressed\n");
	fprintf(stderr, "\t\tencrypted -- The file is marked as encrypted\n");
	fprintf(stderr, "\t\timmutable -- The file is marked as immutable\n");
	fprintf(stderr, "\t\tnodump -- The file is marked as no-dump\n");
	fprintf(stderr, "\tcmp_ref -- check that the reference file has identical stats\n");
	fprintf(stderr, "\tref=<file> -- get reference stats from file\n");
	fprintf(stderr, "\tstx_<field>=<val> -- statx field value check\n");
	fprintf(stderr, "\tts=<a>,<b> -- timestamp a <= b, where a and b can each be one of:\n");
	fprintf(stderr, "\t\t[abcm] -- the timestamps from testfile\n");
	fprintf(stderr, "\t\t[ABCM] -- the timestamps from the reference file\n");
	fprintf(stderr, "\t\t0 -- the origin timestamp\n");
	fprintf(stderr, "\tts_origin=<sec>.<nsec> -- set the origin timestamp\n");
	fprintf(stderr, "\tts_order -- check the timestamp order\n");
	fprintf(stderr, "\t\t(for stx_type, fifo char dir, block, file, sym, sock can be used)\n");
	exit(2);
}

static __attribute__((noreturn, format(printf, 1, 2)))
void bad_arg(const char *fmt, ...)
{
	va_list va;

	va_start(va, fmt);
	vfprintf(stderr, fmt, va);
	va_end(va);
	exit(2);
}

static __attribute__((format(printf, 1, 2)))
void verbose(const char *fmt, ...)
{
	va_list va;

	if (is_verbose) {
		va_start(va, fmt);
		fputs(" - ", stdout);
		vprintf(fmt, va);
		va_end(va);
	}
}

static __attribute__((format(printf, 2, 3)))
void check(bool good, const char *fmt, ...)
{
	va_list va;

	if (!good) {
		va_start(va, fmt);
		fputs("[!] ", stdout);
		vprintf(fmt, va);
		va_end(va);
		failed = true;
	}
}

/*
 * Compare the contents of a statx struct with that of a stat struct and check
 * that they're the same.
 */
static void cmp_statx(const struct statx *stx, const struct stat *st)
{
#define cmp(fmt, x)						      \
	do {							      \
		check(stx->stx_##x == st->st_##x,		      \
		      "stat.%s differs, "fmt" != "fmt"\n",	      \
		      #x,					      \
		      (unsigned long long)stx->stx_##x,		      \
		      (unsigned long long)st->st_##x);		      \
	} while (0)

	cmp("%llu", blksize);
	cmp("%llu", nlink);
	cmp("%llu", uid);
	cmp("%llu", gid);
	cmp("%llo", mode);
	cmp("%llu", ino);
	cmp("%llu", size);
	cmp("%llu", blocks);

#define devcmp(x) \
	do {								\
		check(stx->stx_##x##_major == major(st->st_##x),	\
		      "stat.%s.major differs, %u != %u\n",		\
		      #x,						\
		      stx->stx_##x##_major,				\
		      major(st->st_##x));				\
		check(stx->stx_##x##_minor == minor(st->st_##x),	\
		      "stat.%s.minor differs, %u != %u\n",		\
		      #x,						\
		      stx->stx_##x##_minor,				\
		      minor(st->st_##x));				\
	} while (0)

	devcmp(dev);
	devcmp(rdev);

#define timecmp(x) \
	do {								\
		check(stx->stx_##x##time.tv_sec == st->st_##x##tim.tv_sec, \
		      "stat.%stime.tv_sec differs, %lld != %lld\n",	\
		      #x,						\
		      (long long)stx->stx_##x##time.tv_sec,		\
		      (long long)st->st_##x##tim.tv_sec);		\
		check(stx->stx_##x##time.tv_nsec == st->st_##x##tim.tv_nsec, \
		      "stat.%stime.tv_nsec differs, %lld != %lld\n",	\
		      #x,						\
		      (long long)stx->stx_##x##time.tv_nsec,		\
		      (long long)st->st_##x##tim.tv_nsec);		\
	} while (0)

	timecmp(a);
	timecmp(c);
	timecmp(m);
}

/*
 * Set origin timestamp from a "<sec>.<nsec>" string.
 */
static void set_origin_timestamp(const char *arg)
{
	long long sec;
	int nsec;

	switch (sscanf(arg, "%lld.%d", &sec, &nsec)) {
	case 0:
		bad_arg("ts_origin= missing seconds value");
	case 1:
		bad_arg("ts_origin= missing nanoseconds value");
	default:
		origin.tv_sec = sec;
		origin.tv_nsec = nsec;
		origin_set = true;
		break;
	}
}

/*
 * Get reference stats from a file.
 */
static void get_reference(const char *file, unsigned int mask)
{
	int ret;

	if (!*file)
		bad_arg("ref= requires a filename\n");

	memset(&ref, 0xfb, sizeof(ref));
	ret = xfstests_statx(AT_FDCWD, file, AT_SYMLINK_NOFOLLOW, mask, &ref);
	switch (ret) {
	case 0:
		ref_set = true;
		break;
	case -1:
		perror(file);
		exit(1);
	default:
		fprintf(stderr, "Unexpected return %d from statx()\n", ret);
		exit(1);
	}
}

/*
 * Check a pair of timestamps.
 */
static void check_earlier(const struct statx_timestamp *A,
			  const struct statx_timestamp *B,
			  const char *A_name,
			  const char *B_name)
{

	check((B->tv_sec - A->tv_sec) >= 0,
	      "%s.sec is before %s.sec (%lld < %lld)\n",
	      B_name, A_name, B->tv_sec, A->tv_sec);

	if (B->tv_sec == A->tv_sec)
		check((B->tv_nsec - A->tv_nsec) >= 0,
		      "%s.nsec is before %s.nsec (%d < %d)\n",
		      B_name, A_name, B->tv_nsec, A->tv_nsec);
}

/*
 * Check that the timestamps are reasonably ordered.
 *
 * We require the following to hold true immediately after creation if the
 * relevant timestamps exist on the filesystem:
 *
 *	btime <= atime
 *	btime <= mtime <= ctime
 */
static void check_timestamp_order(const struct statx *stx)
{
	if ((stx->stx_mask & (STATX_BTIME | STATX_ATIME)) == (STATX_BTIME | STATX_ATIME))
		check_earlier(&stx->stx_btime, &stx->stx_atime, "btime", "atime");
	if ((stx->stx_mask & (STATX_BTIME | STATX_MTIME)) == (STATX_BTIME | STATX_MTIME))
		check_earlier(&stx->stx_btime, &stx->stx_mtime, "btime", "mtime");
	if ((stx->stx_mask & (STATX_BTIME | STATX_CTIME)) == (STATX_BTIME | STATX_CTIME))
		check_earlier(&stx->stx_btime, &stx->stx_ctime, "btime", "ctime");
	if ((stx->stx_mask & (STATX_MTIME | STATX_CTIME)) == (STATX_MTIME | STATX_CTIME))
		check_earlier(&stx->stx_mtime, &stx->stx_ctime, "mtime", "ctime");
}

/*
 * Check that the second timestamp is the same as or after the first timestamp.
 */
static void check_timestamp(const struct statx *stx, char *arg)
{
	const struct statx_timestamp *a, *b;
	const char *an, *bn;
	unsigned int mask;

	if (strlen(arg) != 3 || arg[1] != ',')
		bad_arg("ts= requires <a>,<b>\n");

	switch (arg[0]) {
	case 'a': a = &stx->stx_atime;	an = "atime";	mask = STATX_ATIME; break;
	case 'b': a = &stx->stx_btime;	an = "btime";	mask = STATX_BTIME; break;
	case 'c': a = &stx->stx_ctime;	an = "ctime";	mask = STATX_CTIME; break;
	case 'm': a = &stx->stx_mtime;	an = "mtime";	mask = STATX_MTIME; break;
	case 'A': a = &ref.stx_atime;	an = "ref_a";	mask = STATX_ATIME; break;
	case 'B': a = &ref.stx_btime;	an = "ref_b";	mask = STATX_BTIME; break;
	case 'C': a = &ref.stx_ctime;	an = "ref_c";	mask = STATX_CTIME; break;
	case 'M': a = &ref.stx_mtime;	an = "ref_m";	mask = STATX_MTIME; break;
	case '0': a = &origin;		an = "origin";	mask = 0; break;
	default:
		bad_arg("ts= timestamp '%c' not supported\n", arg[0]);
	}

	if (arg[0] == '0') {
		if (!origin_set)
			bad_arg("ts= timestamp '%c' requires origin= first\n", arg[0]);
	} else if (arg[0] <= 'Z') {
		if (!ref_set)
			bad_arg("ts= timestamp '%c' requires ref= first\n", arg[0]);
		if (!(ref.stx_mask & mask))
			return;
	} else {
		if (!(stx->stx_mask & mask))
			return;
	}

	switch (arg[2]) {
	case 'a': b = &stx->stx_atime;	bn = "atime";	mask = STATX_ATIME; break;
	case 'b': b = &stx->stx_btime;	bn = "btime";	mask = STATX_BTIME; break;
	case 'c': b = &stx->stx_ctime;	bn = "ctime";	mask = STATX_CTIME; break;
	case 'm': b = &stx->stx_mtime;	bn = "mtime";	mask = STATX_MTIME; break;
	case 'A': b = &ref.stx_atime;	bn = "ref_a";	mask = STATX_ATIME; break;
	case 'B': b = &ref.stx_btime;	bn = "ref_b";	mask = STATX_BTIME; break;
	case 'C': b = &ref.stx_ctime;	bn = "ref_c";	mask = STATX_CTIME; break;
	case 'M': b = &ref.stx_mtime;	bn = "ref_m";	mask = STATX_MTIME; break;
	case '0': b = &origin;		bn = "origin";	mask = 0; break;
	default:
		bad_arg("ts= timestamp '%c' not supported\n", arg[2]);
	}

	if (arg[2] == '0') {
		if (!origin_set)
			bad_arg("ts= timestamp '%c' requires origin= first\n", arg[0]);
	} else if (arg[2] <= 'Z') {
		if (!ref_set)
			bad_arg("ts= timestamp '%c' requires ref= first\n", arg[2]);
		if (!(ref.stx_mask & mask))
			return;
	} else {
		if (!(stx->stx_mask & mask))
			return;
	}

	verbose("check %s <= %s\n", an, bn);
	check_earlier(a, b, an, bn);
}

/*
 * Compare to reference file.
 */
static void cmp_ref(const struct statx *stx, unsigned int mask)
{
#undef cmp
#define cmp(fmt, x)							\
	do {								\
		check(stx->x == ref.x,					\
		      "attr '%s' differs from ref file, "fmt" != "fmt"\n", \
		      #x,						\
		      (unsigned long long)stx->x,			\
		      (unsigned long long)ref.x);			\
	} while (0)

	cmp("%llx", stx_mask);
	cmp("%llx", stx_attributes);
	cmp("%llu", stx_blksize);
	cmp("%llu", stx_attributes);
	cmp("%llu", stx_nlink);
	cmp("%llu", stx_uid);
	cmp("%llu", stx_gid);
	cmp("%llo", stx_mode);
	cmp("%llu", stx_ino);
	cmp("%llu", stx_size);
	cmp("%llu", stx_blocks);
	cmp("%lld", stx_atime.tv_sec);
	cmp("%lld", stx_atime.tv_nsec);
	cmp("%lld", stx_btime.tv_sec);
	cmp("%lld", stx_btime.tv_nsec);
	cmp("%lld", stx_ctime.tv_sec);
	cmp("%lld", stx_ctime.tv_nsec);
	cmp("%lld", stx_mtime.tv_sec);
	cmp("%lld", stx_mtime.tv_nsec);
	cmp("%llu", stx_rdev_major);
	cmp("%llu", stx_rdev_minor);
	cmp("%llu", stx_dev_major);
	cmp("%llu", stx_dev_minor);
}

/*
 * Check an field restriction.  Specified on the command line as a key=val pair
 * in the checks section.  For instance:
 *
 *	stx_type=char
 *	stx_mode=0644
 */
static void check_field(const struct statx *stx, char *arg)
{
	const struct file_type *type;
	const struct field *field;
	unsigned long long ucheck, uval = 0;
	long long scheck, sval = 0;
	char *key, *val, *p;

	verbose("check %s\n", arg);

	key = arg;
	val = strchr(key, '=');
	if (!val || !val[1])
		bad_arg("%s check requires value\n", key);
	*(val++) = 0;

	field = bsearch(key, field_list, nr__fields, sizeof(*field), field_cmp);
	if (!field)
		bad_arg("Field '%s' not supported\n", key);

	/* Read the stat information specified by the key. */
	switch ((enum fields)(field - field_list)) {
	case stx_mask:		uval = stx->stx_mask;		break;
	case stx_blksize:	uval = stx->stx_blksize;	break;
	case stx_attributes:	uval = stx->stx_attributes;	break;
	case stx_nlink:		uval = stx->stx_nlink;		break;
	case stx_uid:		uval = stx->stx_uid;		break;
	case stx_gid:		uval = stx->stx_gid;		break;
	case stx_type:		uval = stx->stx_mode & ~07777;	break;
	case stx_mode:		uval = stx->stx_mode & 07777;	break;
	case stx_ino:		uval = stx->stx_ino;		break;
	case stx_size:		uval = stx->stx_size;		break;
	case stx_blocks:	uval = stx->stx_blocks;		break;
	case stx_rdev_major:	uval = stx->stx_rdev_major;	break;
	case stx_rdev_minor:	uval = stx->stx_rdev_minor;	break;
	case stx_dev_major:	uval = stx->stx_dev_major;	break;
	case stx_dev_minor:	uval = stx->stx_dev_minor;	break;

	case stx_atime_tv_sec:	sval = stx->stx_atime.tv_sec;	break;
	case stx_atime_tv_nsec:	sval = stx->stx_atime.tv_nsec;	break;
	case stx_btime_tv_sec:	sval = stx->stx_btime.tv_sec;	break;
	case stx_btime_tv_nsec:	sval = stx->stx_btime.tv_nsec;	break;
	case stx_ctime_tv_sec:	sval = stx->stx_ctime.tv_sec;	break;
	case stx_ctime_tv_nsec:	sval = stx->stx_ctime.tv_nsec;	break;
	case stx_mtime_tv_sec:	sval = stx->stx_mtime.tv_sec;	break;
	case stx_mtime_tv_nsec:	sval = stx->stx_mtime.tv_nsec;	break;
	default:
		break;
	}

	/* Parse the specified value as signed or unsigned as
	 * appropriate and compare to the stat information.
	 */
	switch ((enum fields)(field - field_list)) {
	case stx_mask:
	case stx_attributes:
		ucheck = strtoull(val, &p, 0);
		if (*p)
			bad_arg("Field '%s' requires unsigned integer\n", key);
		check(uval == ucheck,
		      "%s differs, 0x%llx != 0x%llx\n", key, uval, ucheck);
		break;

	case stx_type:
		for (type = file_types; type->name; type++) {
			if (strcmp(type->name, val) == 0) {
				ucheck = type->mode;
				goto octal_check;
			}
		}

		/* fall through */

	case stx_mode:
		ucheck = strtoull(val, &p, 0);
		if (*p)
			bad_arg("Field '%s' requires unsigned integer\n", key);
	octal_check:
		check(uval == ucheck,
		      "%s differs, 0%llo != 0%llo\n", key, uval, ucheck);
		break;

	case stx_blksize:
	case stx_nlink:
	case stx_uid:
	case stx_gid:
	case stx_ino:
	case stx_size:
	case stx_blocks:
	case stx_rdev_major:
	case stx_rdev_minor:
	case stx_dev_major:
	case stx_dev_minor:
		ucheck = strtoull(val, &p, 0);
		if (*p)
			bad_arg("Field '%s' requires unsigned integer\n", key);
		check(uval == ucheck,
		      "%s differs, %llu != %llu\n", key, uval, ucheck);
		break;

	case stx_atime_tv_sec:
	case stx_atime_tv_nsec:
	case stx_btime_tv_sec:
	case stx_btime_tv_nsec:
	case stx_ctime_tv_sec:
	case stx_ctime_tv_nsec:
	case stx_mtime_tv_sec:
	case stx_mtime_tv_nsec:
		scheck = strtoll(val, &p, 0);
		if (*p)
			bad_arg("Field '%s' requires integer\n", key);
		check(sval == scheck,
		      "%s differs, %lld != %lld\n", key, sval, scheck);
		break;

	default:
		break;
	}
}

/*
 * Check attributes in stx_attributes.  When stx_attributes_mask gets in
 * upstream, we will need to consider that also.
 */
static void check_attribute(const struct statx *stx, char *arg)
{
	const struct attr_name *p;
	__u64 attr;
	bool set;

	verbose("check attr %s\n", arg);
	switch (arg[0]) {
	case '+': set = true;	break;
	case '-': set = false;	break;
	default:
		bad_arg("attr flag must be marked + (set) or - (unset)\n");
	}
	arg++;

	p = bsearch(arg, attr_list, sizeof(attr_list) / sizeof(attr_list[0]),
		    sizeof(attr_list[0]), attr_name_cmp);
	if (!p)
		bad_arg("Unrecognised attr name '%s'\n", arg);

	attr = p->attr_flag;
	if (set) {
		check((stx->stx_attributes & attr) == attr,
		      "Attribute %s should be set\n", arg);
	} else {
		check((stx->stx_attributes & attr) == 0,
		      "Attribute %s should be unset\n", arg);
	}
}

/*
 * Do the testing.
 */
int main(int argc, char **argv)
{
	struct statx stx;
	struct stat st;
	unsigned int mask = STATX_ALL;
	unsigned int atflags = AT_STATX_SYNC_AS_STAT;
	char *p;
	int c, ret;

	if (argc == 2 && strcmp(argv[1], "--check-statx") == 0) {
		errno = 0;
		return (xfstests_statx(AT_FDCWD, "/", 0, 0, &stx) == -1 &&
			errno == ENOSYS) ? 1 : 0;
	}

	prog = argv[0];
	while (c = getopt(argc, argv, "+DFm:v"),
	       c != -1
	       ) {
		switch (c) {
		case 'F':
			atflags &= ~AT_STATX_SYNC_TYPE;
			atflags |= AT_STATX_FORCE_SYNC;
			break;
		case 'D':
			atflags &= ~AT_STATX_SYNC_TYPE;
			atflags |= AT_STATX_DONT_SYNC;
			break;
		case 'm':
			if (strcmp(optarg, "basic") == 0) {
				mask = STATX_BASIC_STATS;
			} else if (strcmp(optarg, "all") == 0) {
				mask = STATX_ALL;
			} else {
				mask = strtoul(optarg, &p, 0);
				if (*p)
					format();
			}
			break;
		case 'v':
			is_verbose = 1;
			break;
		default:
			format();
		}
	}

	argc -= optind;
	argv += optind;
	if (argc < 1)
		format();
	testfile = argv[0];
	argv += 1;

	/* Gather the stats.  We want both statx and stat so that we can
	 * compare what's in the buffers.
	 */
	verbose("call statx %s\n", testfile);
	memset(&stx, 0xfb, sizeof(stx));
	ret = xfstests_statx(AT_FDCWD, testfile, atflags | AT_SYMLINK_NOFOLLOW,
			     mask, &stx);
	switch (ret) {
	case 0:
		break;
	case -1:
		perror(testfile);
		exit(1);
	default:
		fprintf(stderr, "Unexpected return %d from statx()\n", ret);
		exit(1);
	}

	verbose("call stat %s\n", testfile);
	ret = fstatat(AT_FDCWD, testfile, &st, AT_SYMLINK_NOFOLLOW);
	switch (ret) {
	case 0:
		break;
	case -1:
		perror(testfile);
		exit(1);
	default:
		fprintf(stderr, "Unexpected return %d from stat()\n", ret);
		exit(1);
	}

	verbose("compare statx and stat\n");
	cmp_statx(&stx, &st);

	/* Display the available timestamps */
	verbose("begin time %llu.%09u\n", origin.tv_sec, origin.tv_nsec);
	if (stx.stx_mask & STATX_BTIME)
		verbose("     btime %llu.%09u\n", stx.stx_btime.tv_sec, stx.stx_btime.tv_nsec);
	if (stx.stx_mask & STATX_ATIME)
		verbose("     atime %llu.%09u\n", stx.stx_atime.tv_sec, stx.stx_atime.tv_nsec);
	if (stx.stx_mask & STATX_MTIME)
		verbose("     mtime %llu.%09u\n", stx.stx_mtime.tv_sec, stx.stx_mtime.tv_nsec);
	if (stx.stx_mask & STATX_CTIME)
		verbose("     ctime %llu.%09u\n", stx.stx_ctime.tv_sec, stx.stx_ctime.tv_nsec);

	/* Handle additional checks the user specified */
	for (; *argv; argv++) {
		char *arg = *argv;

		if (strncmp("attr=", arg, 5) == 0) {
			/* attr=[+-]<attr> - check attribute flag */
			check_attribute(&stx, arg + 5);
			continue;
		}

		if (strcmp("cmp_ref", arg) == 0) {
			/* cmp_ref - check ref file has same stats */
			cmp_ref(&stx, mask);
			continue;
		}

		if (strncmp(arg, "stx_", 4) == 0) {
			/* stx_<field>=<n> - check field set to n */
			check_field(&stx, *argv);
			continue;
		}

		if (strncmp("ref=", arg, 4) == 0) {
			/* ref=<file> - set reference stats from file */
			get_reference(arg + 4, mask);
			continue;
		}

		if (strcmp("ts_order", arg) == 0) {
			/* ts_order - check timestamp order */
			check_timestamp_order(&stx);
			continue;
		}

		if (strncmp("ts_origin=", arg, 10) == 0) {
			/* ts_origin=<sec>.<nsec> - set origin timestamp */
			set_origin_timestamp(arg + 10);
			continue;
		}

		if (strncmp("ts=", arg, 3) == 0) {
			/* ts=<a>,<b> - check timestamp b is same as a or after */
			check_timestamp(&stx, arg + 3);
			continue;
		}

		bad_arg("check '%s' not supported\n", arg);
	}

	if (failed) {
		printf("Failed\n");
		exit(1);
	}

	verbose("Success\n");
	exit(0);
}
