// SPDX-License-Identifier: LGPL-2.1-or-later
/*
 * Copyright (c) 2019, DDN Storage Corporation.
 */
/*
 * This file is part of Lustre, http://www.lustre.org/
 */
/*
 *
 * Test for Lustre statx().
 * It uses some code in coreutils ('ls.c' and 'stat.c') for reference.
 *
 * Author: Qian Yingjin <qian@ddn.com>
 */
#define _ATFILE_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <getopt.h>
#include <limits.h>
#include <unistd.h>
#include <ctype.h>
#include <errno.h>
#include <time.h>
#include <sys/time.h>
#include <pwd.h>
#include <grp.h>
#include <dirent.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/param.h>
#include <sys/sysmacros.h>
#include <inttypes.h>
#include <fcntl.h>
#include <locale.h>
#include <linux/lustre/lustre_user.h>

#ifdef HAVE_SELINUX
#include <selinux/selinux.h>
#endif

/* Factor out some of the common --help and --version processing code. */

/* These enum values cannot possibly conflict with the option values
 * ordinarily used by commands, including CHAR_MAX + 1, etc.  Avoid
 * CHAR_MIN - 1, as it may equal -1, the getopt end-of-options value.
 */
enum {
	PRINTF_OPTION = (CHAR_MAX + 1),
	GETOPT_HELP_CHAR = (CHAR_MIN - 2),
	GETOPT_VERSION_CHAR = (CHAR_MIN - 3)
};

static bool o_quiet;

#ifdef __NR_statx
#ifndef HAVE_STATX

#define AT_STATX_SYNC_TYPE	0x6000
#define AT_STATX_FORCE_SYNC	0x2000
#define AT_STATX_DONT_SYNC	0x4000

static __attribute__((unused))
ssize_t statx(int dfd, const char *filename, int flags,
	      unsigned int mask, struct statx *buffer)
{
	return syscall(__NR_statx, dfd, filename, flags, mask, buffer);
}
#endif /* HAVE_STATX */

#define xstrdup(str) strdup(str)
static inline
char *xasprintf(const char *fmt, const char *old_fmt, const char *str)
{
	char *tmp = NULL;

	if (asprintf(&tmp, fmt, old_fmt, str) < 0) {
		fprintf(stderr, "asprintf allocation failed\n");
		exit(1);
	}

	return tmp;
}


/* coreutils/lib/intprops.h */
#define _GL_SIGNED_TYPE_OR_EXPR(t) TYPE_SIGNED(__typeof__(t))

/* Bound on length of the string representing an unsigned integer
 * value representable in B bits.  log10 (2.0) < 146/485.  The
 * smallest value of B where this bound is not tight is 2621.
 */
#define INT_BITS_STRLEN_BOUND(b) (((b) * 146 + 484) / 485)

/* The width in bits of the integer type or expression T.
 * Do not evaluate T.
 * Padding bits are not supported; this is checked at compile-time below.
 */
#define TYPE_WIDTH(t) (sizeof(t) * CHAR_BIT)

/* Bound on length of the string representing an integer type or expression T.
 * Subtract 1 for the sign bit if T is signed, and then add 1 more for
 * a minus sign if needed.
 *
 * Because _GL_SIGNED_TYPE_OR_EXPR sometimes returns 1 when its argument is
 * unsigned, this macro may overestimate the true bound by one byte when
 * applied to unsigned types of size 2, 4, 16, ... bytes.
 */
#define INT_STRLEN_BOUND(t)                                     \
	(INT_BITS_STRLEN_BOUND(TYPE_WIDTH(t) - _GL_SIGNED_TYPE_OR_EXPR(t)) \
	+ _GL_SIGNED_TYPE_OR_EXPR(t))

/* Bound on buffer size needed to represent an integer type or expression T,
 * including the terminating null.
 */
#define INT_BUFSIZE_BOUND(t) (INT_STRLEN_BOUND(t) + 1)

/* The maximum and minimum values for the integer type T.  */
#define TYPE_MINIMUM(t) ((t)~TYPE_MAXIMUM(t))
#define TYPE_MAXIMUM(t)						\
	((t) (!TYPE_SIGNED(t)					\
	? (t)-1						\
	: ((((t)1 << (TYPE_WIDTH(t) - 2)) - 1) * 2 + 1)))

static bool o_dir_list;
static bool long_format; /* use a long listing format */

/* Current time in seconds and nanoseconds since 1970, updated as
 * needed when deciding whether a file is recent.
 */
static struct timespec current_time;

/* FIXME: these are used by printf.c, too */
#define isodigit(c) ('0' <= (c) && (c) <= '7')
#define octtobin(c) ((c) - '0')
#define hextobin(c) ((c) >= 'a' && (c) <= 'f' ? (c) - 'a' + 10 : \
		     (c) >= 'A' && (c) <= 'F' ? (c) - 'A' + 10 : (c) - '0')

#define ISDIGIT(c) ((unsigned int)(c) - '0' <= 9)

/* True if the real type T is signed.  */
#define TYPE_SIGNED(t) (!((t)0 < (t)-1))

static char const digits[] = "0123456789";

/* Flags that are portable for use in printf, for at least one
 * conversion specifier; make_format removes unportable flags as
 * needed for particular specifiers.  The glibc 2.2 extension "I" is
 * listed here; it is removed by make_format because it has undefined
 * behavior elsewhere and because it is incompatible with
 * out_epoch_sec.
 */
static char const printf_flags[] = "'-+ #0I";

/* Formats for the --terse option. */
static char const fmt_terse_fs[] = "%n %i %l %t %s %S %b %f %a %c %d\n";
static char const fmt_terse_regular[] = "%n %s %b %f %u %g %D %i %h %t %T %X %Y %Z %W %o\n";
#ifdef HAVE_SELINUX
static char const fmt_terse_selinux[] = "%n %s %b %f %u %g %D %i %h %t %T %X %Y %Z %W %o %C\n";
#endif
static char *format;

/* Whether to follow symbolic links;  True for --dereference (-L).  */
static bool follow_links;

/* Whether to interpret backslash-escape sequences.
 * True for --printf=FMT, not for --format=FMT (-c).
 */
static bool interpret_backslash_escapes;

/* The trailing delimiter string:
 * "" for --printf=FMT, "\n" for --format=FMT (-c).
 */
static char const *trailing_delim = "";

/* The representation of the decimal point in the current locale.  */
static char const *decimal_point;
static size_t decimal_point_len;

/* Convert a possibly-signed character to an unsigned character.  This is
 * a bit safer than casting to unsigned char, since it catches some type
 * errors that the cast doesn't.
 */
static inline unsigned char to_uchar(char ch)
{
	return ch;
}

static void usage(char *prog)
{
	printf("Usage: %s [options] <FILE>...\n", prog);
	printf("Display file status via statx() syscall.\n"
	       "List information about the FILE "
	       "(the current diretory by default) via statx() syscall.\n"
	       "options:\n"
	       "\t-L --dereference   follow links\n"
	       "\t--cached=MODE      specify how to use cached attributes;\n"
	       "\t                     See MODE below\n"
	       "\t-c --format=FORMAT use the specified FORMAT instead of the "
	       "default;\n"
	       "\t                     output a newline after each use of "
	       "FORMAT\n"
	       "\t-t, --terse        print the information in terse form\n"
	       "\t-D --dir           list information about the FILE (ls)\n"
	       "\t-l                 use a long listing format\n"
	       "\t-q --quiet         do not display results, test only\n\n"
	       "The --cached MODE argument can be; always, never, or default.\n"
	       "`always` will use cached attributes if available, while\n"
	       "`never` will try to synchronize with the latest attributes,\n"
	       "and `default` will leave it up to the underlying file system.\n"
	       "\n"
	       "The valid format sequences for files (without --file-system):\n"
	       "\n"
	       "\t%%a  access rights in octal (note '#' and '0' printf flags)\n"
	       "\t%%A   access rights in human readable form\n"
	       "\t%%b   number of blocks allocated (see %%B)\n"
	       "\t%%B   the size in bytes of each block reported by %%b\n"
	       "\t%%C   SELinux security context string\n"
	       "\t%%d   device number in decimal\n"
	       "\t%%D   device number in hex\n"
	       "\t%%f   raw mode in hex\n"
	       "\t%%F   file type\n"
	       "\t%%g   group ID of owner\n"
	       "\t%%G   group name of owner\n"
	       "\t%%h   number of hard links\n"
	       "\t%%i   inode number\n"
	       "\t%%m   mount point\n"
	       "\t%%n   file name\n"
	       "\t%%N   quoted file name with dereference if symbolic link\n"
	       "\t%%o   optimal I/O transfer size hint\n"
	       "\t%%p   Mask to show what's supported in stx_attributes\n"
	       "\t%%r   Flags conveying information about the file: "
	       "stx_attributes\n"
	       "\t%%s   total size, in bytes\n"
	       "\t%%t   major device type in hex, for character/block device "
	       "special files\n"
	       "\t%%T   minor device type in hex, for character/block device "
	       "special files\n"
	       "\t%%u   user ID of owner\n"
	       "\t%%U   user name of owner\n"
	       "\t%%w   time of file birth, human-readable; - if unknown\n"
	       "\t%%W   time of file birth, seconds since Epoch; 0 if unknown\n"
	       "\t%%x   time of last access, human-readable\n"
	       "\t%%X   time of last access, seconds since Epoch\n"
	       "\t%%y   time of last data modification, human-readable\n"
	       "\t%%Y   time of last data modification, seconds since Epoch\n"
	       "\t%%z   time of last status change, human-readable\n"
	       "\t%%Z   time of last status change, seconds since Epoch\n");
	exit(0);
}

/* gnulib/lib/filemode.c */
/* Return a character indicating the type of file described by
 * file mode BITS:
 * '-' regular file
 * 'b' block special file
 * 'c' character special file
 * 'C' high performance ("contiguous data") file
 * 'd' directory
 * 'D' door
 * 'l' symbolic link
 * 'm' multiplexed file (7th edition Unix; obsolete)
 * 'n' network special file (HP-UX)
 * 'p' fifo (named pipe)
 * 'P' port
 * 's' socket
 * 'w' whiteout (4.4BSD)
 * '?' some other file type
 */
static char ftypelet(mode_t bits)
{
	/* These are the most common, so test for them first.*/
	if (S_ISREG(bits))
		return '-';
	if (S_ISDIR(bits))
		return 'd';

	/* Other letters standardized by POSIX 1003.1-2004.*/
	if (S_ISBLK(bits))
		return 'b';
	if (S_ISCHR(bits))
		return 'c';
	if (S_ISLNK(bits))
		return 'l';
	if (S_ISFIFO(bits))
		return 'p';

	/* Other file types (though not letters) standardized by POSIX.*/
	if (S_ISSOCK(bits))
		return 's';

	return '?';
}

/* Like filemodestring, but rely only on MODE.*/
static void strmode(mode_t mode, char *str)
{
	str[0] = ftypelet(mode);
	str[1] = mode & 0400 ? 'r' : '-';
	str[2] = mode & 0200 ? 'w' : '-';
	str[3] = (mode & S_ISUID
			? (mode & 0100 ? 's' : 'S')
			: (mode & 0100 ? 'x' : '-'));
	str[4] = mode & 0040 ? 'r' : '-';
	str[5] = mode & 0020 ? 'w' : '-';
	str[6] = (mode & S_ISGID
			? (mode & 0010 ? 's' : 'S')
			: (mode & 0010 ? 'x' : '-'));
	str[7] = mode & 0004 ? 'r' : '-';
	str[8] = mode & 0002 ? 'w' : '-';
	str[9] = (mode & S_ISVTX
			? (mode & 0001 ? 't' : 'T')
			: (mode & 0001 ? 'x' : '-'));
	str[10] = ' ';
	str[11] = '\0';
}

/* filemodestring - fill in string STR with an ls-style ASCII
 * representation of the st_mode field of file stats block STATP.
 * 12 characters are stored in STR.
 * The characters stored in STR are:
 *
 * 0    File type, as in ftypelet above, except that other letters are used
 *      for files whose type cannot be determined solely from st_mode:
 *
 *          'F' semaphore
 *          'M' migrated file (Cray DMF)
 *          'Q' message queue
 *          'S' shared memory object
 *          'T' typed memory object
 *
 * 1    'r' if the owner may read, '-' otherwise.
 *
 * 2    'w' if the owner may write, '-' otherwise.
 *
 * 3    'x' if the owner may execute, 's' if the file is
 *      set-user-id, '-' otherwise.
 *      'S' if the file is set-user-id, but the execute
 *      bit isn't set.
 *
 * 4    'r' if group members may read, '-' otherwise.
 *
 * 5    'w' if group members may write, '-' otherwise.
 *
 * 6    'x' if group members may execute, 's' if the file is
 *      set-group-id, '-' otherwise.
 *      'S' if it is set-group-id but not executable.
 *
 * 7    'r' if any user may read, '-' otherwise.
 *
 * 8    'w' if any user may write, '-' otherwise.
 *
 * 9    'x' if any user may execute, 't' if the file is "sticky"
 *      (will be retained in swap space after execution), '-'
 *      otherwise.
 *      'T' if the file is sticky but not executable.
 *
 * 10   ' ' for compatibility with 4.4BSD strmode,
 *      since this interface does not support ACLs.
 *
 * 11   '\0'.
 */
static void filemodestring(struct statx const *stxp, char *str)
{
	strmode(stxp->stx_mode, str);
}

/* gnulib/lib/file-type.c */
static char const *file_type(struct statx const *stx)
{
	/* See POSIX 1003.1-2001 XCU Table 4-8 lines 17093-17107 for some of
	 * these formats.
	 *
	 * To keep diagnostics grammatical in English, the returned string
	 * must start with a consonant.
	 */
	/* Do these three first, as they're the most common.  */
	if (S_ISREG(stx->stx_mode))
		return stx->stx_size == 0 ? "regular empty file" :
					    "regular file";

	if (S_ISDIR(stx->stx_mode))
		return "directory";

	if (S_ISLNK(stx->stx_mode))
		return "symbolic link";

	/* The remaining are in alphabetical order.  */
	if (S_ISBLK(stx->stx_mode))
		return "block special file";

	if (S_ISCHR(stx->stx_mode))
		return "character special file";

	if (S_ISFIFO(stx->stx_mode))
		return "fifo";

	if (S_ISSOCK(stx->stx_mode))
		return "socket";

	return "weird file";
}

/* gnulib/lib/areadlink-with-size.c */
/* SYMLINK_MAX is used only for an initial memory-allocation sanity
 * check, so it's OK to guess too small on hosts where there is no
 * arbitrary limit to symbolic link length.
 */
#ifndef SYMLINK_MAX
#define SYMLINK_MAX 1024
#endif

#define MAXSIZE (SIZE_MAX < SSIZE_MAX ? SIZE_MAX : SSIZE_MAX)

/* Call readlink to get the symbolic link value of FILE.
 * SIZE is a hint as to how long the link is expected to be;
 * typically it is taken from st_size.  It need not be correct.
 * Return a pointer to that NUL-terminated string in malloc'd storage.
 * If readlink fails, malloc fails, or if the link value is longer
 * than SSIZE_MAX, return NULL (caller may use errno to diagnose).
 */
static char *areadlink_with_size(char const *file, size_t size)
{
	/* Some buggy file systems report garbage in st_size.  Defend
	 * against them by ignoring outlandish st_size values in the initial
	 * memory allocation.
	 */
	size_t symlink_max = SYMLINK_MAX;
	size_t INITIAL_LIMIT_BOUND = 8 * 1024;
	size_t initial_limit = (symlink_max < INITIAL_LIMIT_BOUND ?
				symlink_max + 1 : INITIAL_LIMIT_BOUND);
	enum { stackbuf_size = 128 };
	/* The initial buffer size for the link value. */
	size_t buf_size = (size == 0 ? stackbuf_size : size < initial_limit ?
			   size + 1 : initial_limit);

	while (1) {
		ssize_t r;
		size_t link_length;
		char stackbuf[stackbuf_size];
		char *buf = stackbuf;
		char *buffer = NULL;

		if (!(size == 0 && buf_size == stackbuf_size)) {
			buf = buffer = malloc(buf_size);
			if (!buffer)
				return NULL;
		}

		r = readlink(file, buf, buf_size);
		link_length = r;

		/* On AIX 5L v5.3 and HP-UX 11i v2 04/09, readlink returns -1
		 * with errno == ERANGE if the buffer is too small.
		 */
		if (r < 0 && errno != ERANGE) {
			int saved_errno = errno;

			free(buffer);
			errno = saved_errno;
			return NULL;
		}

		if (link_length < buf_size) {
			buf[link_length] = 0;
			if (!buffer) {
				buffer = malloc(link_length + 1);
				if (buffer)
					return memcpy(buffer, buf,
						      link_length + 1);
			} else if (link_length + 1 < buf_size) {
				/* Shrink BUFFER before returning it. */
				char *shrinked_buffer;

				shrinked_buffer = realloc(buffer,
							  link_length + 1);
				if (shrinked_buffer != NULL)
					buffer = shrinked_buffer;
			}
			return buffer;
		}

		free(buffer);
		if (buf_size <= MAXSIZE / 2) {
			buf_size *= 2;
		} else if (buf_size < MAXSIZE) {
			buf_size = MAXSIZE;
		} else {
			errno = ENOMEM;
			return NULL;
		}
	}
}

/* coreutils/src/stat.c */
/* Output a single-character \ escape.  */
static void print_esc_char(char c)
{
	switch (c) {
	case 'a':			/* Alert. */
		c = '\a';
		break;
	case 'b':			/* Backspace. */
		c = '\b';
		break;
	case 'e':			/* Escape. */
		c = '\x1B';
		break;
	case 'f':			/* Form feed. */
		c = '\f';
		break;
	case 'n':			/* New line. */
		c = '\n';
		break;
	case 'r':			/* Carriage return. */
		c = '\r';
		break;
	case 't':			/* Horizontal tab. */
		c = '\t';
		break;
	case 'v':			/* Vertical tab. */
		c = '\v';
		break;
	case '"':
	case '\\':
		break;
	default:
		printf("warning: unrecognized escape '\\%c'", c);
		break;
	}
	putchar (c);
}

static size_t format_code_offset(char const *directive)
{
	size_t len = strspn(directive + 1, printf_flags);
	char const *fmt_char = directive + len + 1;

	fmt_char += strspn(fmt_char, digits);
	if (*fmt_char == '.')
		fmt_char += 1 + strspn(fmt_char + 1, digits);

	return fmt_char - directive;
}

static unsigned int fmt_to_mask(char fmt)
{
	switch (fmt) {
	case 'N':
		return STATX_MODE;
	case 'd':
	case 'D':
		return STATX_MODE;
	case 'i':
		return STATX_INO;
	case 'a':
	case 'A':
		return STATX_MODE;
	case 'f':
		return STATX_MODE|STATX_TYPE;
	case 'F':
		return STATX_TYPE;
	case 'h':
		return STATX_NLINK;
	case 'u':
	case 'U':
		return STATX_UID;
	case 'g':
	case 'G':
		return STATX_GID;
	case 'm':
		return STATX_MODE|STATX_INO;
	case 's':
		return STATX_SIZE;
	case 't':
	case 'T':
		return STATX_MODE;
	case 'b':
		return STATX_BLOCKS;
	case 'w':
	case 'W':
		return STATX_BTIME;
	case 'x':
	case 'X':
		return STATX_ATIME;
	case 'y':
	case 'Y':
		return STATX_MTIME;
	case 'z':
	case 'Z':
		return STATX_CTIME;
	}
	return 0;
}

static unsigned int format_to_mask(char const *format)
{
	unsigned int mask = 0;
	char const *b;

	for (b = format; *b; b++) {
		if (*b != '%')
			continue;

		b += format_code_offset(b);
		if (*b == '\0')
			break;
		mask |= fmt_to_mask(*b);
	}

	return mask;
}

static char *human_access(struct statx const *stxbuf)
{
	static char modebuf[12];

	filemodestring(stxbuf, modebuf);
	modebuf[10] = 0;
	return modebuf;
}

static inline struct timespec
statx_timestamp_to_timespec(struct statx_timestamp tsx)
{
	struct timespec ts;

	ts.tv_sec = tsx.tv_sec;
	ts.tv_nsec = tsx.tv_nsec;

	return ts;
}

static int timespec_cmp(struct timespec a, struct timespec b)
{
	if (a.tv_sec < b.tv_sec)
		return -1;
	if (a.tv_sec > b.tv_sec)
		return 1;

	return a.tv_nsec - b.tv_nsec;
}

static char *human_time(const struct statx_timestamp *ts)
{
	/* STR must be at least INT_BUFSIZE_BOUND (intmax_t) big, either
	 * because localtime_rz fails, or because the time zone is truly
	 * outlandish so that %z expands to a long string.
	 */
	static char str[INT_BUFSIZE_BOUND(intmax_t)
		+ INT_STRLEN_BOUND(int) /* YYYY */
		+ 1 /* because YYYY might equal INT_MAX + 1900 */
		+ sizeof "-MM-DD HH:MM:SS.NNNNNNNNN +"];
	struct tm tm;
	time_t tim;
	int len;
	int len2;

	tim = ts->tv_sec;
	if (!localtime_r(&tim, &tm)) {
		perror("localtime_r");
		exit(EXIT_FAILURE);
	}

	if (o_dir_list && long_format) {
		struct timespec when_timespec;
		struct timespec six_months_ago;
		bool recent;

		when_timespec = statx_timestamp_to_timespec(*ts);
		/* If the file appears to be in the future, update the current
		 * time, in case the file happens to have been modified since
		 * the last time we checked the clock.
		 */
		if (timespec_cmp(current_time, when_timespec) < 0) {
			struct timeval tv;

			gettimeofday(&tv, NULL);
			current_time.tv_sec = tv.tv_sec;
			current_time.tv_nsec = tv.tv_usec * 1000;
		}

		/* Consider a time to be recent if it is within the past six
		 * months.
		 * A Gregorian year has 365.2425 * 24 * 60 * 60 == 31556952
		 * seconds on the average.  Write this value as an integer
		 * constant to avoid floating point hassles.
		 */
		six_months_ago.tv_sec = current_time.tv_sec - 31556952 / 2;
		six_months_ago.tv_nsec = current_time.tv_nsec;

		recent = (timespec_cmp(six_months_ago, when_timespec) < 0 &&
			  (timespec_cmp(when_timespec, current_time) < 0));

		/* We assume here that all time zones are offset from UTC by a
		 * whole number of seconds.
		 */
		len = strftime(str, sizeof(str),
			       recent ? "%b %e %H:%M" : "%b %e %Y", &tm);
		if (len == 0) {
			perror("strftime");
			exit(EXIT_FAILURE);
		}

		return str;
	}

	len = strftime(str, sizeof(str), "%Y-%m-%d %H:%M:%S", &tm);
	if (len == 0) {
		perror("strftime");
		exit(EXIT_FAILURE);
	}

	len2 = snprintf(str + len, sizeof(str) - len, ".%09u ", ts->tv_nsec);
	len = strftime(str + len + len2, sizeof(str) - len - len2, "%z", &tm);
	if (len == 0) {
		perror("strftime2");
		exit(1);
	}

	return str;
}

/* PFORMAT points to a '%' followed by a prefix of a format, all of
 * size PREFIX_LEN.  The flags allowed for this format are
 * ALLOWED_FLAGS; remove other printf flags from the prefix, then
 * append SUFFIX.
 */
static void make_format(char *pformat, size_t prefix_len,
			char const *allowed_flags, char const *suffix)
{
	char *dst = pformat + 1;
	char const *src;
	char const *srclim = pformat + prefix_len;

	for (src = dst; src < srclim && strchr(printf_flags, *src); src++)
		if (strchr(allowed_flags, *src))
			*dst++ = *src;
	while (src < srclim)
		*dst++ = *src++;
	strcpy(dst, suffix);
}

static void out_string(char *pformat, size_t prefix_len, char const *arg)
{
	make_format(pformat, prefix_len, "-", "s");
	printf(pformat, arg);
}

static int out_int(char *pformat, size_t prefix_len, intmax_t arg)
{
	make_format(pformat, prefix_len, "'-+ 0", PRIdMAX);
	return printf(pformat, arg);
}

static int out_uint(char *pformat, size_t prefix_len, uintmax_t arg)
{
	make_format(pformat, prefix_len, "'-0", PRIuMAX);
	return printf(pformat, arg);
}

static void out_uint_o(char *pformat, size_t prefix_len, uintmax_t arg)
{
	make_format(pformat, prefix_len, "-#0", PRIoMAX);
	printf(pformat, arg);
}

static void out_uint_x(char *pformat, size_t prefix_len, uintmax_t arg)
{
	make_format(pformat, prefix_len, "-#0", PRIxMAX);
	printf(pformat, arg);
}

static int out_minus_zero(char *pformat, size_t prefix_len)
{
	make_format(pformat, prefix_len, "'-+ 0", ".0f");
	return printf(pformat, -0.25);
}

/* Output the number of seconds since the Epoch, using a format that
 * acts like printf's %f format.
 */
static void out_epoch_sec(char *pformat, size_t prefix_len,
			  struct timespec arg)
{
	char *dot = memchr(pformat, '.', prefix_len);
	size_t sec_prefix_len = prefix_len;
	int width = 0;
	int precision = 0;
	bool frac_left_adjust = false;

	if (dot) {
		sec_prefix_len = dot - pformat;
		pformat[prefix_len] = '\0';

		if (ISDIGIT(dot[1])) {
			long int lprec = strtol(dot + 1, NULL, 10);

			precision = (lprec <= INT_MAX ? lprec : INT_MAX);
		} else {
			precision = 9;
		}

		if (precision && ISDIGIT(dot[-1])) {
			/* If a nontrivial width is given, subtract the width
			 * of the decimal point and PRECISION digits that will
			 * be output later.
			 */
			char *p = dot;

			*dot = '\0';

			do
				--p;
			while (ISDIGIT(p[-1]));

			long int lwidth = strtol(p, NULL, 10);

			width = (lwidth <= INT_MAX ? lwidth : INT_MAX);
			if (width > 1) {
				p += (*p == '0');
				sec_prefix_len = p - pformat;

				int w_d = (decimal_point_len < width ?
					   width - decimal_point_len : 0);

				if (w_d > 1) {
					int w = w_d - precision;

					if (w > 1) {
						char *dst = pformat;
						char const *src = dst;

						for (; src < p; src++) {
							if (*src == '-')
								frac_left_adjust = true;
							else
								*dst++ = *src;
						}
						sec_prefix_len =
							(dst - pformat
			+ (frac_left_adjust ? 0 : sprintf(dst, "%d", w)));
					}
				}
			}
		}
	}

	int divisor = 1;
	int i;

	for (i = precision; i < 9; i++)
		divisor *= 10;

	int frac_sec = arg.tv_nsec / divisor;
	int int_len;


	if (TYPE_SIGNED(time_t)) {
		bool minus_zero = false;

		if (arg.tv_sec < 0 && arg.tv_nsec != 0) {
			int frac_sec_modulus = 1000000000 / divisor;

			frac_sec = (frac_sec_modulus - frac_sec
				    - (arg.tv_nsec % divisor != 0));
			arg.tv_sec += (frac_sec != 0);
			minus_zero = (arg.tv_sec == 0);
		}
		int_len = (minus_zero ?
			   out_minus_zero(pformat, sec_prefix_len) :
			   out_int(pformat, sec_prefix_len, arg.tv_sec));
	} else {
		int_len = out_uint(pformat, sec_prefix_len, arg.tv_sec);
	}

	if (precision) {
		int prec = (precision < 9 ? precision : 9);
		int trailing_prec = precision - prec;
		int ilen = (int_len < 0 ? 0 : int_len);
		int trailing_width = (ilen < width &&
				      decimal_point_len < width - ilen ?
				      width - ilen - decimal_point_len - prec :
				      0);

		printf("%s%.*d%-*.*d", decimal_point, prec, frac_sec,
			trailing_width, trailing_prec, 0);
	}
}

/* Print the context information of FILENAME, and return true iff the
 * context could not be obtained.
 */
static int out_file_context(char *pformat, size_t prefix_len,
			    char const *filename)
{
	char *scontext = NULL;
	int rc = 0;

#ifdef HAVE_SELINUX
	if ((follow_links ? getfilecon(filename, &scontext) :
			    lgetfilecon(filename, &scontext)) < 0) {
		printf("failed to get security context of %s: %s\n",
		       filename, strerror(errno));
		scontext = NULL;
		rc  = -errno;
	}
#endif

	strcpy(pformat + prefix_len, "s");
	printf(pformat, (scontext ? scontext : "?"));
#ifdef HAVE_SELINUX
	if (scontext)
		freecon(scontext);
#endif
	return rc;
}

/* Map a TS with negative TS.tv_nsec to {0,0}.  */
static inline struct timespec neg_to_zero(struct timespec ts)
{
	if (ts.tv_nsec >= 0)
		return ts;

	struct timespec z = {0, 0};

	return z;
}

/* All the mode bits that can be affected by chmod.  */
#define CHMOD_MODE_BITS \
	(S_ISUID | S_ISGID | S_ISVTX | 0700 | 0070 | 0007)

/* Print statx info.  Return zero upon success, nonzero upon failure.  */
static int print_statx(char *pformat, size_t prefix_len, unsigned int m,
		       int fd, char const *filename, struct statx const *stx)
{
	struct passwd *pw_ent;
	struct group *gw_ent;
	int rc = 0;
	int ret;

	switch (m) {
	case 'n':
		out_string(pformat, prefix_len,
			   o_dir_list ? strrchr(filename, '/') + 1 : filename);
		break;
	case 'N':
		out_string(pformat, prefix_len,
			   o_dir_list ? strrchr(filename, '/')  + 1 : filename);
		if (S_ISLNK(stx->stx_mode)) {
			char *linkname;

			linkname = areadlink_with_size(filename, stx->stx_size);
			if (linkname == NULL) {
				printf("cannot read symbolic link %s: %s",
				       filename, strerror(errno));
				return -errno;
			}
			printf(" -> ");
			out_string(pformat, prefix_len, linkname);
			free(linkname);
		}
		break;
	case 'd':
		out_uint(pformat, prefix_len, makedev(stx->stx_dev_major,
						      stx->stx_dev_minor));
		break;
	case 'D':
		out_uint_x(pformat, prefix_len, makedev(stx->stx_dev_major,
							stx->stx_dev_minor));
		break;
	case 'i':
		out_uint(pformat, prefix_len, stx->stx_ino);
		break;
	case 'a':
		out_uint_o(pformat, prefix_len,
			   stx->stx_mode & CHMOD_MODE_BITS);
		break;
	case 'A':
		out_string(pformat, prefix_len, human_access(stx));
		break;
	case 'f':
		out_uint_x(pformat, prefix_len, stx->stx_mode);
		break;
	case 'F':
		out_string(pformat, prefix_len, file_type(stx));
		break;
	case 'h':
		out_uint(pformat, prefix_len, stx->stx_nlink);
		break;
	case 'u':
		out_uint(pformat, prefix_len, stx->stx_uid);
		break;
	case 'U':
		pw_ent = getpwuid(stx->stx_uid);
		out_string(pformat, prefix_len,
			   pw_ent ? pw_ent->pw_name : "UNKNOWN");
		break;
	case 'g':
		out_uint(pformat, prefix_len, stx->stx_gid);
		break;
	case 'G':
		gw_ent = getgrgid(stx->stx_gid);
		out_string(pformat, prefix_len,
			   gw_ent ? gw_ent->gr_name : "UNKNOWN");
		break;
	case 'm':
		/*
		 * fail |= out_mount_point(filename, pformat, prefix_len,
		 *			   statbuf);
		 */
		if (!rc)
			rc = -ENOTSUP;
		break;
	case 's':
		out_int(pformat, prefix_len, stx->stx_size);
		break;
	case 't':
		out_uint_x(pformat, prefix_len,
			   major(makedev(stx->stx_rdev_major,
					 stx->stx_rdev_minor)));
		break;
	case 'T':
		out_uint_x(pformat, prefix_len,
			   minor(makedev(stx->stx_rdev_major,
					 stx->stx_rdev_minor)));
		break;
	case 'B':
		out_uint(pformat, prefix_len, S_BLKSIZE);
		break;
	case 'b':
		out_uint(pformat, prefix_len, stx->stx_blocks);
		break;
	case 'o':
		out_uint(pformat, prefix_len, stx->stx_blksize);
		break;
	case 'p':
		out_uint_x(pformat, prefix_len, stx->stx_attributes_mask);
		break;
	case 'r':
		out_uint_x(pformat, prefix_len, stx->stx_attributes);
		break;
	case 'w':
		if (stx->stx_btime.tv_nsec < 0)
			out_string(pformat, prefix_len, "-");
		else
			out_string(pformat, prefix_len,
				   human_time(&stx->stx_btime));
		break;
	case 'W':
		out_epoch_sec(pformat, prefix_len,
			      neg_to_zero(statx_timestamp_to_timespec(
					      stx->stx_btime)));
		break;
	case 'x':
		out_string(pformat, prefix_len,
			   human_time(&stx->stx_atime));
		break;
	case 'X':
		out_epoch_sec(pformat, prefix_len,
			      neg_to_zero(statx_timestamp_to_timespec(
					      stx->stx_atime)));
		break;
	case 'y':
		out_string(pformat, prefix_len,
			   human_time(&stx->stx_mtime));
		break;
	case 'Y':
		out_epoch_sec(pformat, prefix_len,
			      neg_to_zero(statx_timestamp_to_timespec(
					      stx->stx_mtime)));
		break;
	case 'z':
		out_string(pformat, prefix_len,
			   human_time(&stx->stx_ctime));
		break;
	case 'Z':
		out_epoch_sec(pformat, prefix_len,
			      neg_to_zero(statx_timestamp_to_timespec(
					      stx->stx_ctime)));
		break;
	case 'C':
		ret = out_file_context(pformat, prefix_len, filename);
		if (!rc && ret)
			rc = ret;
		break;
	default:
		fputc('?', stdout);
		break;
	}

	return rc;
}

static int print_it(int fd, char const *filename,
		    int (*print_func)(char *, size_t, unsigned int,
				      int, char const *, struct statx const *),
		    void const *data)
{
	/* Add 2 to accommodate our conversion of the stat '%s' format string
	 * to the longer printf '%llu' one.
	 */
	enum {
		MAX_ADDITIONAL_BYTES = (MAX(sizeof(PRIdMAX),
					MAX(sizeof(PRIoMAX),
					    MAX(sizeof(PRIuMAX),
						sizeof(PRIxMAX)))) - 1)
	};
	size_t n_alloc;
	char *dest;
	char const *b;
	int rc = 0;

	if (o_quiet)
		return 0;

	n_alloc = strlen(format) + MAX_ADDITIONAL_BYTES + 1;
	dest = malloc(n_alloc);
	if (dest == NULL)
		return -ENOMEM;

	for (b = format; *b; b++) {
		switch (*b) {
		case '%': {
			size_t len = format_code_offset(b);
			char const *fmt_char = b + len;
			int ret;

			memcpy(dest, b, len);
			b += len;

			switch (*fmt_char) {
			case '\0':
				--b;
			case '%':
				if (len > 1) {
					dest[len] = *fmt_char;
					dest[len + 1] = '\0';
					printf("%s: invalid directive", dest);
					return -EINVAL;
				}
				putchar('%');
				break;
			default:
				ret = print_func(dest, len, to_uchar(*fmt_char),
						 fd, filename, data);
				if (rc == 0 && ret)
					rc = ret;
				break;
			}
			break;
		}
		case '\\':
			if (!interpret_backslash_escapes) {
				putchar ('\\');
				break;
			}
			++b;
			if (isodigit(*b)) {
				int esc_value = octtobin(*b);
				int esc_length = 1; /* number of octal digits */

				for (++b; esc_length < 3 && isodigit(*b);
				     ++esc_length, ++b) {
					esc_value = esc_value * 8 +
						    octtobin(*b);
				}
				putchar(esc_value);
				--b;
			} else if (*b == 'x' && isxdigit(to_uchar(b[1]))) {
				/* Value of \xhh escape. */
				int esc_value = hextobin(b[1]);
				/* A hexadecimal \xhh escape sequence must have
				 * 1 or 2 hex. digits.
				 */

				++b;
				if (isxdigit(to_uchar(b[1]))) {
					++b;
					esc_value = esc_value * 16 +
						    hextobin(*b);
				}
				putchar(esc_value);
			} else if (*b == '\0') {
				printf("warning: backslash at end of format");
				putchar('\\');
				/* Arrange to exit the loop.  */
				--b;
			} else {
				print_esc_char(*b);
			}
			break;

		default:
			putchar(*b);
			break;
		}
	}
	free(dest);

	fputs(trailing_delim, stdout);

	return rc;
}

/* Return an allocated format string in static storage that
 * corresponds to whether FS and TERSE options were declared.
 */
static char *default_format(bool fs, bool terse, bool device)
{
	char *format;

	if (fs) {
		if (terse) {
			format = xstrdup(fmt_terse_fs);
		} else {
			/* TRANSLATORS: This string uses format specifiers from
			 * 'stat --help' with --file-system, and NOT from
			 * printf.
			 */
			format = xstrdup(
			"  File: \"%n\"\n"
			"    ID: %-8i Namelen: %-7l Type: %T\n"
			"Block size: %-10s Fundamental block size: %S\n"
			"Blocks: Total: %-10b Free: %-10f Available: %a\n"
			"Inodes: Total: %-10c Free: %d\n");
		}
	} else /* ! fs */ {
		if (terse) {
#ifdef HAVE_SELINUX
			if (is_selinux_enabled() > 0)
				format = xstrdup(fmt_terse_selinux);
			else
#endif
				format = xstrdup(fmt_terse_regular);
		} else {
			char *temp;

			/* TRANSLATORS: This string uses format specifiers from
			 * 'stat --help' without --file-system, and NOT from
			 * printf.
			 */
			format = xstrdup("\
  File: %N\n\
  Size: %-10s\tBlocks: %-10b IO Block: %-6o %F\n\
");

			temp = format;
			if (device) {
				/* TRANSLATORS: This string uses format
				 * specifiers from 'stat --help' without
				 * --file-system, and NOT from printf.
				 */
				format = xasprintf("%s%s", format, "\
" "Device: %Dh/%dd\tInode: %-10i  Links: %-5h Device type: %t,%T\n\
");
			} else {
				/* TRANSLATORS: This string uses format
				 * specifiers from 'stat --help' without
				 * --file-system, and NOT from printf.
				 */
				format = xasprintf("%s%s", format, "\
" "Device: %Dh/%dd\tInode: %-10i  Links: %h\n\
");
			}
			free(temp);

			temp = format;
			/* TRANSLATORS: This string uses format specifiers from
			 * 'stat --help' without --file-system, and NOT from
			 * printf.
			 */
			format = xasprintf("%s%s", format, "\
" "Access: (%04a/%10.10A)  Uid: (%5u/%8U)   Gid: (%5g/%8G)\n\
");
			free(temp);

#ifdef HAVE_SELINUX
			if (is_selinux_enabled() > 0) {
				temp = format;
				/* TRANSLATORS: This string uses format
				 * specifiers from 'stat --help' without
				 * --file-system, and NOT from printf.
				 */
				format = xasprintf("%s%s", format,
						   "Context: %C\n");
				free(temp);
			}
#endif
			temp = format;
			/* TRANSLATORS: This string uses format specifiers from
			 * 'stat --help' without --file-system, and NOT from
			 * printf.
			 */
			format = xasprintf("%s%s", format,
					   "Access: %x\n"
					   "Modify: %y\n"
					   "Change: %z\n"
					   " Birth: %w\n");
			free(temp);
		}
	}
	return format;
}

static char *list_long_format(void)
{
	char *format;

	format = xstrdup("\
" "%10.10A %h %8U %8G %-10s %y %N\
");

	return format;
}

static int do_statx(char const *filename, unsigned int request_mask, int flags)
{
	const char *pathname = filename;
	struct statx stx = { 0, };
	int fd;

	if (strcmp(filename, "-") == 0)
		fd = 0;
	else
		fd = AT_FDCWD;

	if (fd != AT_FDCWD) {
		pathname = "";
		flags |= AT_EMPTY_PATH;
	}

	fd = statx(fd, pathname, flags, request_mask, &stx);
	if (fd < 0) {
		if (flags & AT_EMPTY_PATH)
			printf("cannot stat standard input\n");
		else
			printf("cannot statx %s: %s\n",
			       filename, strerror(errno));

		return -errno;
	}

	return print_it(fd, filename, print_statx, &stx);
}

/* Return true if FILE should be ignored. */
static bool file_ignored(char const *name)
{
	return name[0] == '.';
}

static int do_dir_list(char const *dirname, unsigned int request_mask,
		       int flags)
{
	DIR *dir;
	struct dirent *ent;
	char fullname[PATH_MAX];
	size_t size = sizeof(fullname);
	int namelen;
	int rc = 0;

	dir = opendir(dirname);
	if (!dir) {
		rc = -errno;
		printf("lsx: cannot open directory '%s': %s\n",
		       dirname, strerror(errno));
		return rc;
	}

	while ((ent = readdir(dir)) != NULL) {
		int ret;

		/* skip "." and ".." */
		if (file_ignored(ent->d_name))
			continue;

		/* ls -1 */
		if (!format) {
			if (o_quiet)
				continue;

			printf("%s", ent->d_name);
			putchar('\n');
		} else {
			if (strlen(ent->d_name) + strlen(dirname) + 1 >=
			    sizeof(fullname)) {
				errno = ENAMETOOLONG;
				fprintf(stderr,
					"lsx: ignored too long path: %s/%s\n",
					dirname, ent->d_name);
				if (!rc)
					rc = -ENAMETOOLONG;
				continue;
			}
			namelen = snprintf(fullname, size, "%s/%s",
					   dirname, ent->d_name);
			if (namelen >= size)
				fullname[size - 1] = '\0';

			ret = do_statx(fullname, request_mask, flags);
			if (!ret)
				putchar('\n');
			else if (rc == 0)
				rc = ret;
		}
	}

	closedir(dir);
	return rc;
}

int main(int argc, char **argv)
{
	static struct option options[] = {
		{"dereference", no_argument, NULL, 'L'},
		{"format", required_argument, NULL, 'c'},
		{"printf", required_argument, NULL, PRINTF_OPTION},
		{"terse", no_argument, NULL, 't'},
		{"cached", required_argument, NULL, 0},
		{"dir", no_argument, NULL, 'D'},
		{"long-format", no_argument, NULL, 'l'},
		{"quiet", no_argument, NULL, 'q'},
		{"help", no_argument, NULL, GETOPT_HELP_CHAR},
		{"version", no_argument, NULL, GETOPT_VERSION_CHAR},
		{NULL, 0, NULL, 0}
	};
	bool terse = false;
	unsigned int request_mask;
	int flags = AT_SYMLINK_NOFOLLOW;
	struct lconv const *locale = localeconv();
	int c;
	int rc = 0;
	int i = 0;

	decimal_point = locale->decimal_point[0] ? locale->decimal_point : ".";
	decimal_point_len = strlen(decimal_point);
	current_time.tv_sec = TYPE_MINIMUM(time_t);
	current_time.tv_nsec = -1;

	while ((c = getopt_long(argc, argv, "c:DqLlt", options, NULL)) != EOF) {
		switch (c) {
		case 'L':
			flags &= ~AT_SYMLINK_NOFOLLOW;
			follow_links = true;
			break;
		case PRINTF_OPTION:
			format = optarg;
			interpret_backslash_escapes = true;
			trailing_delim = "";
			break;
		case 'c':
			format = optarg;
			interpret_backslash_escapes = false;
			trailing_delim = "\n";
			break;
		case 'q':
			o_quiet = true;
			break;
		case 'l':
			o_dir_list = true;
			long_format = true;
			break;
		case 'D':
			o_dir_list = true;
			break;
		case 't':
			terse = true;
			break;
		case 0:
			if (strcmp(optarg, "never") == 0) {
				flags &= ~AT_STATX_SYNC_TYPE;
				flags |= AT_STATX_FORCE_SYNC;
			} else if (strcmp(optarg, "always") == 0) {
				flags &= ~AT_STATX_SYNC_TYPE;
				flags |= AT_STATX_DONT_SYNC;
			} else if (strcmp(optarg, "default") == 0) {
				flags &= ~AT_STATX_SYNC_TYPE;
				flags |= AT_SYMLINK_NOFOLLOW;
			} else {
				printf("%s: invalid cached mode: %s\n",
				       argv[0], optarg);
				return -EINVAL;
			}
			break;
		case GETOPT_HELP_CHAR:
			usage(argv[0]);
		case GETOPT_VERSION_CHAR:
			if (!o_quiet)
				printf("Lustre statx: version 0.1\n");
			return 0;
		default:
			printf("%s: unknown option '-%c'\n",
			       argv[0], optopt);
			return -EINVAL;
		}
	}

	if (format) {
		request_mask = format_to_mask(format);
	} else {
		request_mask = STATX_ALL;
		if (o_dir_list)
			format = long_format ? list_long_format() : NULL;
		else
			format = default_format(false, terse, false);
	}

	if (optind == argc) {
		if (o_dir_list)
			return do_dir_list(".", request_mask, flags);

		printf("statx: missing operand\n"
		       "Try 'stat --help' for more information.\n");
		return 0;
	}

	for (i = optind; i < argc; i++) {
		int ret;

		if (o_dir_list)
			ret = do_dir_list(argv[i], request_mask, flags);
		else
			ret = do_statx(argv[i], request_mask, flags);

		if (rc == 0 && ret)
			rc = ret;
	}

	return rc;
}
#else
int main(int argc, char **argv)
{
	static struct option options[] = {
		{"version", no_argument, NULL, GETOPT_VERSION_CHAR},
		{"quiet", no_argument, NULL, 'q'},
		{NULL, 0, NULL, 0}
	};
	int c;

	while ((c = getopt_long(argc, argv, "q", options, NULL)) != EOF) {
		switch (c) {
		case 'q':
			o_quiet = true;
			break;
		case GETOPT_VERSION_CHAR:
			if (!o_quiet)
				printf("statx: Not support statx() syscall\n");
			return -ENOTSUP;
		}
	}
	printf("Skip: system does not support statx syscall.\n");
	return 0;
}
#endif /* __NR_statx */
