// Copyright © Tavian Barnes <tavianator@tavianator.com>
// SPDX-License-Identifier: 0BSD

#include "printf.h"

#include "alloc.h"
#include "bfs.h"
#include "bfstd.h"
#include "bftw.h"
#include "color.h"
#include "ctx.h"
#include "diag.h"
#include "dir.h"
#include "dstring.h"
#include "expr.h"
#include "fsade.h"
#include "mtab.h"
#include "pwcache.h"
#include "stat.h"

#include <errno.h>
#include <grp.h>
#include <pwd.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

struct bfs_fmt;

/**
 * A function implementing a printf directive.
 */
typedef int bfs_printf_fn(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf);

/**
 * A single formatting directive like %f or %#4m.
 */
struct bfs_fmt {
	/** The printing function to invoke. */
	bfs_printf_fn *fn;
	/** String data associated with this directive. */
	dchar *str;
	/** The stat field to print. */
	enum bfs_stat_field stat_field;
	/** Character data associated with this directive. */
	char c;
	/** Some data used by the directive. */
	void *ptr;
};

/**
 * An entire format string.
 */
struct bfs_printf {
	/** An array of formatting directives. */
	struct bfs_fmt *fmts;
	/** The number of directives. */
	size_t nfmts;
};

/** Print some text as-is. */
static int bfs_printf_literal(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
	size_t len = dstrlen(fmt->str);
	if (fwrite(fmt->str, 1, len, cfile->file) == len) {
		return 0;
	} else {
		return -1;
	}
}

/** \c: flush */
static int bfs_printf_flush(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
	return fflush(cfile->file);
}

/** Check if we can safely colorize this directive. */
static bool should_color(CFILE *cfile, const struct bfs_fmt *fmt) {
	return cfile->colors && strcmp(fmt->str, "%s") == 0;
}

/**
 * Print a value to a temporary buffer before formatting it.
 */
#define BFS_PRINTF_BUF(buf, format, ...) \
	char buf[256]; \
	int ret = snprintf(buf, sizeof(buf), format, __VA_ARGS__); \
	bfs_assert(ret >= 0 && (size_t)ret < sizeof(buf)); \
	(void)ret

/** Return a dynamic format string. */
[[_format_arg(2)]]
static const char *dyn_fmt(const char *str, const char *fake) {
	bfs_assert(strcmp(str + strlen(str) - strlen(fake) + 1, fake + 1) == 0,
		"Mismatched format specifiers: '%s' vs. '%s'", str, fake);
	return str;
}

/** Wrapper for fprintf(). */
[[_printf(3, 4)]]
static int bfs_fprintf(CFILE *cfile, const struct bfs_fmt *fmt, const char *fake, ...) {
	va_list args;
	va_start(args, fake);
	int ret = vfprintf(cfile->file, dyn_fmt(fmt->str, fake), args);
	va_end(args);
	return ret;
}

/** %a, %c, %t: ctime() */
static int bfs_printf_ctime(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
	// Not using ctime() itself because GNU find adds nanoseconds
	static const char *days[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
	static const char *months[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};

	const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
	if (!statbuf) {
		return -1;
	}

	const struct timespec *ts = bfs_stat_time(statbuf, fmt->stat_field);
	if (!ts) {
		return -1;
	}

	struct tm tm;
	if (!localtime_r(&ts->tv_sec, &tm)) {
		return -1;
	}

	BFS_PRINTF_BUF(buf, "%s %s %2d %.2d:%.2d:%.2d.%09ld0 %4d",
		days[tm.tm_wday],
		months[tm.tm_mon],
		tm.tm_mday,
		tm.tm_hour,
		tm.tm_min,
		tm.tm_sec,
		(long)ts->tv_nsec,
		1900 + tm.tm_year);

	return bfs_fprintf(cfile, fmt, "%s", buf);
}

/** %A, %B/%W, %C, %T: strftime() */
static int bfs_printf_strftime(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
	const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
	if (!statbuf) {
		return -1;
	}

	const struct timespec *ts = bfs_stat_time(statbuf, fmt->stat_field);
	if (!ts) {
		return -1;
	}

	struct tm tm;
	if (!localtime_r(&ts->tv_sec, &tm)) {
		return -1;
	}

	int ret;
	char buf[256];
	char format[] = "% ";
	switch (fmt->c) {
	// Non-POSIX strftime() features
	case '@':
		ret = snprintf(buf, sizeof(buf), "%lld.%09ld0", (long long)ts->tv_sec, (long)ts->tv_nsec);
		break;
	case '+':
		ret = snprintf(buf, sizeof(buf), "%4d-%.2d-%.2d+%.2d:%.2d:%.2d.%09ld0",
			1900 + tm.tm_year,
			tm.tm_mon + 1,
			tm.tm_mday,
			tm.tm_hour,
			tm.tm_min,
			tm.tm_sec,
			(long)ts->tv_nsec);
		break;
	case 'k':
		ret = snprintf(buf, sizeof(buf), "%2d", tm.tm_hour);
		break;
	case 'l':
		ret = snprintf(buf, sizeof(buf), "%2d", (tm.tm_hour + 11) % 12 + 1);
		break;
	case 's':
		ret = snprintf(buf, sizeof(buf), "%lld", (long long)ts->tv_sec);
		break;
	case 'S':
		ret = snprintf(buf, sizeof(buf), "%.2d.%09ld0", tm.tm_sec, (long)ts->tv_nsec);
		break;
	case 'T':
		ret = snprintf(buf, sizeof(buf), "%.2d:%.2d:%.2d.%09ld0",
			tm.tm_hour,
			tm.tm_min,
			tm.tm_sec,
			(long)ts->tv_nsec);
		break;

	// POSIX strftime() features
	default:
		format[1] = fmt->c;
#if __GNUC__
#  pragma GCC diagnostic push
#  pragma GCC diagnostic ignored "-Wformat-nonliteral"
#endif
		ret = strftime(buf, sizeof(buf), format, &tm);
#if __GNUC__
#  pragma GCC diagnostic pop
#endif
		break;
	}

	bfs_assert(ret >= 0 && (size_t)ret < sizeof(buf));
	(void)ret;

	return bfs_fprintf(cfile, fmt, "%s", buf);
}

/** %b: blocks */
static int bfs_printf_b(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
	const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
	if (!statbuf) {
		return -1;
	}

	uintmax_t blocks = ((uintmax_t)statbuf->blocks * BFS_STAT_BLKSIZE + 511) / 512;
	BFS_PRINTF_BUF(buf, "%ju", blocks);
	return bfs_fprintf(cfile, fmt, "%s", buf);
}

/** %d: depth */
static int bfs_printf_d(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
	return bfs_fprintf(cfile, fmt, "%jd", (intmax_t)ftwbuf->depth);
}

/** %D: device */
static int bfs_printf_D(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
	const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
	if (!statbuf) {
		return -1;
	}

	BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->dev);
	return bfs_fprintf(cfile, fmt, "%s", buf);
}

/** %f: file name */
static int bfs_printf_f(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
	if (should_color(cfile, fmt)) {
		return cfprintf(cfile, "%pF", ftwbuf);
	} else {
		return bfs_fprintf(cfile, fmt, "%s", ftwbuf->path + ftwbuf->nameoff);
	}
}

/** %F: file system type */
static int bfs_printf_F(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
	const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
	if (!statbuf) {
		return -1;
	}

	const char *type = bfs_fstype(fmt->ptr, statbuf);
	if (!type) {
		return -1;
	}

	return bfs_fprintf(cfile, fmt, "%s", type);
}

/** %G: gid */
static int bfs_printf_G(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
	const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
	if (!statbuf) {
		return -1;
	}

	BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->gid);
	return bfs_fprintf(cfile, fmt, "%s", buf);
}

/** %g: group name */
static int bfs_printf_g(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
	const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
	if (!statbuf) {
		return -1;
	}

	struct bfs_groups *groups = fmt->ptr;
	const struct group *grp = bfs_getgrgid(groups, statbuf->gid);
	if (!grp) {
		return bfs_printf_G(cfile, fmt, ftwbuf);
	}

	return bfs_fprintf(cfile, fmt, "%s", grp->gr_name);
}

/** %h: leading directories */
static int bfs_printf_h(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
	char *copy = NULL;
	const char *buf;

	if (ftwbuf->nameoff > 0) {
		size_t len = ftwbuf->nameoff;
		if (len > 1) {
			--len;
		}

		buf = copy = strndup(ftwbuf->path, len);
	} else if (ftwbuf->path[0] == '/') {
		buf = "/";
	} else {
		buf = ".";
	}

	if (!buf) {
		return -1;
	}

	int ret;
	if (should_color(cfile, fmt)) {
		ret = cfprintf(cfile, "${di}%pQ${rs}", buf);
	} else {
		ret = bfs_fprintf(cfile, fmt, "%s", buf);
	}

	free(copy);
	return ret;
}

/** %H: current root */
static int bfs_printf_H(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
	if (should_color(cfile, fmt)) {
		if (ftwbuf->depth == 0) {
			return cfprintf(cfile, "%pP", ftwbuf);
		} else {
			return cfprintf(cfile, "${di}%pQ${rs}", ftwbuf->root);
		}
	} else {
		return bfs_fprintf(cfile, fmt, "%s", ftwbuf->root);
	}
}

/** %i: inode */
static int bfs_printf_i(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
	const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
	if (!statbuf) {
		return -1;
	}

	BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->ino);
	return bfs_fprintf(cfile, fmt, "%s", buf);
}

/** %k: 1K blocks */
static int bfs_printf_k(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
	const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
	if (!statbuf) {
		return -1;
	}

	uintmax_t blocks = ((uintmax_t)statbuf->blocks * BFS_STAT_BLKSIZE + 1023) / 1024;
	BFS_PRINTF_BUF(buf, "%ju", blocks);
	return bfs_fprintf(cfile, fmt, "%s", buf);
}

/** %l: link target */
static int bfs_printf_l(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
	char *buf = NULL;
	const char *target = "";

	if (ftwbuf->type == BFS_LNK) {
		if (should_color(cfile, fmt)) {
			return cfprintf(cfile, "%pL", ftwbuf);
		}

		const struct bfs_stat *statbuf = bftw_cached_stat(ftwbuf, BFS_STAT_NOFOLLOW);
		size_t len = statbuf ? statbuf->size : 0;

		target = buf = xreadlinkat(ftwbuf->at_fd, ftwbuf->at_path, len);
		if (!target) {
			return -1;
		}
	}

	int ret = bfs_fprintf(cfile, fmt, "%s", target);
	free(buf);
	return ret;
}

/** %m: mode */
static int bfs_printf_m(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
	const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
	if (!statbuf) {
		return -1;
	}

	return bfs_fprintf(cfile, fmt, "%o", (unsigned int)(statbuf->mode & 07777));
}

/** %M: symbolic mode */
static int bfs_printf_M(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
	const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
	if (!statbuf) {
		return -1;
	}

	char buf[11];
	xstrmode(statbuf->mode, buf);
	return bfs_fprintf(cfile, fmt, "%s", buf);
}

/** %n: link count */
static int bfs_printf_n(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
	const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
	if (!statbuf) {
		return -1;
	}

	BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->nlink);
	return bfs_fprintf(cfile, fmt, "%s", buf);
}

/** %p: full path */
static int bfs_printf_p(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
	if (should_color(cfile, fmt)) {
		return cfprintf(cfile, "%pP", ftwbuf);
	} else {
		return bfs_fprintf(cfile, fmt, "%s", ftwbuf->path);
	}
}

/** %P: path after root */
static int bfs_printf_P(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
	size_t offset = strlen(ftwbuf->root);
	if (ftwbuf->path[offset] == '/') {
		++offset;
	}

	if (should_color(cfile, fmt)) {
		if (ftwbuf->depth == 0) {
			return 0;
		}

		struct BFTW copybuf = *ftwbuf;
		copybuf.path += offset;
		copybuf.nameoff -= offset;
		return cfprintf(cfile, "%pP", &copybuf);
	} else {
		return bfs_fprintf(cfile, fmt, "%s", ftwbuf->path + offset);
	}
}

/** %s: size */
static int bfs_printf_s(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
	const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
	if (!statbuf) {
		return -1;
	}

	BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->size);
	return bfs_fprintf(cfile, fmt, "%s", buf);
}

/** %S: sparseness */
static int bfs_printf_S(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
	const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
	if (!statbuf) {
		return -1;
	}

	double sparsity;
	if (statbuf->size == 0 && statbuf->blocks == 0) {
		sparsity = 1.0;
	} else {
		sparsity = (double)BFS_STAT_BLKSIZE * statbuf->blocks / statbuf->size;
	}
	return bfs_fprintf(cfile, fmt, "%g", sparsity);
}

/** %U: uid */
static int bfs_printf_U(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
	const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
	if (!statbuf) {
		return -1;
	}

	BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->uid);
	return bfs_fprintf(cfile, fmt, "%s", buf);
}

/** %u: user name */
static int bfs_printf_u(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
	const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
	if (!statbuf) {
		return -1;
	}

	struct bfs_users *users = fmt->ptr;
	const struct passwd *pwd = bfs_getpwuid(users, statbuf->uid);
	if (!pwd) {
		return bfs_printf_U(cfile, fmt, ftwbuf);
	}

	return bfs_fprintf(cfile, fmt, "%s", pwd->pw_name);
}

static const char *bfs_printf_type(enum bfs_type type) {
	const char *const names[] = {
		[BFS_BLK] = "b",
		[BFS_CHR] = "c",
		[BFS_DIR] = "d",
		[BFS_DOOR] = "D",
		[BFS_FIFO] = "p",
		[BFS_LNK] = "l",
		[BFS_PORT] = "P",
		[BFS_REG] = "f",
		[BFS_SOCK] = "s",
		[BFS_WHT] = "w",
	};

	const char *name = NULL;
	if ((size_t)type < countof(names)) {
		name = names[type];
	}

	return name ? name : "U";
}

/** %y: type */
static int bfs_printf_y(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
	const char *type = bfs_printf_type(ftwbuf->type);
	return bfs_fprintf(cfile, fmt, "%s", type);
}

/** %Y: target type */
static int bfs_printf_Y(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
	enum bfs_type type = bftw_type(ftwbuf, BFS_STAT_FOLLOW);
	const char *str;

	int error = 0;
	if (type == BFS_ERROR) {
		if (errno == ELOOP) {
			str = "L";
		} else if (errno_is_like(ENOENT)) {
			str = "N";
		} else {
			str = "?";
			error = errno;
		}
	} else {
		str = bfs_printf_type(type);
	}

	int ret = bfs_fprintf(cfile, fmt, "%s", str);
	if (error != 0) {
		ret = -1;
		errno = error;
	}
	return ret;
}

/** %Z: SELinux context */
[[_maybe_unused]]
static int bfs_printf_Z(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
	char *con = bfs_getfilecon(ftwbuf);
	if (!con) {
		return -1;
	}

	int ret = bfs_fprintf(cfile, fmt, "%s", con);
	bfs_freecon(con);
	return ret;
}

/**
 * Append a literal string to the chain.
 */
static int append_literal(const struct bfs_ctx *ctx, struct bfs_printf *format, dchar **literal) {
	if (dstrlen(*literal) == 0) {
		return 0;
	}

	struct bfs_fmt *fmt = RESERVE(struct bfs_fmt, &format->fmts, &format->nfmts);
	if (!fmt) {
		bfs_perror(ctx, "RESERVE()");
		return -1;
	}

	fmt->fn = bfs_printf_literal;
	fmt->str = *literal;

	*literal = dstralloc(0);
	if (!*literal) {
		bfs_perror(ctx, "dstralloc()");
		return -1;
	}

	return 0;
}

/**
 * Append a printf directive to the chain.
 */
static int append_directive(const struct bfs_ctx *ctx, struct bfs_printf *format, dchar **literal, struct bfs_fmt *fmt) {
	if (append_literal(ctx, format, literal) != 0) {
		return -1;
	}

	struct bfs_fmt *dest = RESERVE(struct bfs_fmt, &format->fmts, &format->nfmts);
	if (!dest) {
		bfs_perror(ctx, "RESERVE()");
		return -1;
	}

	*dest = *fmt;
	return 0;
}

int bfs_printf_parse(const struct bfs_ctx *ctx, struct bfs_expr *expr, const char *format) {
	expr->printf = ZALLOC(struct bfs_printf);
	if (!expr->printf) {
		bfs_perror(ctx, "zalloc()");
		return -1;
	}

	dchar *literal = dstralloc(0);
	if (!literal) {
		bfs_perror(ctx, "dstralloc()");
		goto error;
	}

	for (const char *i = format; *i; ++i) {
		char c = *i;

		if (c == '\\') {
			c = *++i;

			if (c >= '0' && c < '8') {
				c = 0;
				for (int j = 0; j < 3 && *i >= '0' && *i < '8'; ++i, ++j) {
					c *= 8;
					c += *i - '0';
				}
				--i;
				goto one_char;
			}

			switch (c) {
			case 'a':  c = '\a'; break;
			case 'b':  c = '\b'; break;
			case 'f':  c = '\f'; break;
			case 'n':  c = '\n'; break;
			case 'r':  c = '\r'; break;
			case 't':  c = '\t'; break;
			case 'v':  c = '\v'; break;
			case '\\': c = '\\'; break;

			case 'c':
				{
					struct bfs_fmt fmt = {
						.fn = bfs_printf_flush,
					};
					if (append_directive(ctx, expr->printf, &literal, &fmt) != 0) {
						goto error;
					}
					goto done;
				}

			case '\0':
				bfs_expr_error(ctx, expr);
				bfs_error(ctx, "Incomplete escape sequence '\\'.\n");
				goto error;

			default:
				bfs_expr_error(ctx, expr);
				bfs_error(ctx, "Unrecognized escape sequence '\\%c'.\n", c);
				goto error;
			}
		} else if (c == '%') {
			if (i[1] == '%') {
				c = *++i;
				goto one_char;
			}

			struct bfs_fmt fmt = {
				.str = dstralloc(2),
			};
			if (!fmt.str) {
				goto fmt_error;
			}
			if (dstrapp(&fmt.str, c) != 0) {
				bfs_perror(ctx, "dstrapp()");
				goto fmt_error;
			}

			const char *specifier = "s";

			// Parse any flags
			bool must_be_numeric = false;
			while (true) {
				c = *++i;

				switch (c) {
				case '#':
				case '0':
				case '+':
				case ' ':
					must_be_numeric = true;
					[[fallthrough]];
				case '-':
					if (strchr(fmt.str, c)) {
						bfs_expr_error(ctx, expr);
						bfs_error(ctx, "Duplicate flag '%c'.\n", c);
						goto fmt_error;
					}
					if (dstrapp(&fmt.str, c) != 0) {
						bfs_perror(ctx, "dstrapp()");
						goto fmt_error;
					}
					continue;
				}

				break;
			}

			// Parse the field width
			while (c >= '0' && c <= '9') {
				if (dstrapp(&fmt.str, c) != 0) {
					bfs_perror(ctx, "dstrapp()");
					goto fmt_error;
				}
				c = *++i;
			}

			// Parse the precision
			if (c == '.') {
				do {
					if (dstrapp(&fmt.str, c) != 0) {
						bfs_perror(ctx, "dstrapp()");
						goto fmt_error;
					}
					c = *++i;
				} while (c >= '0' && c <= '9');
			}

			switch (c) {
			case 'a':
				fmt.fn = bfs_printf_ctime;
				fmt.stat_field = BFS_STAT_ATIME;
				break;
			case 'b':
				fmt.fn = bfs_printf_b;
				break;
			case 'c':
				fmt.fn = bfs_printf_ctime;
				fmt.stat_field = BFS_STAT_CTIME;
				break;
			case 'd':
				fmt.fn = bfs_printf_d;
				specifier = "jd";
				break;
			case 'D':
				fmt.fn = bfs_printf_D;
				break;
			case 'f':
				fmt.fn = bfs_printf_f;
				break;
			case 'F':
				fmt.fn = bfs_printf_F;
				fmt.ptr = (void *)bfs_ctx_mtab(ctx);
				if (!fmt.ptr) {
					int error = errno;
					bfs_expr_error(ctx, expr);
					bfs_error(ctx, "Couldn't parse the mount table: %s.\n", xstrerror(error));
					goto fmt_error;
				}
				break;
			case 'g':
				fmt.fn = bfs_printf_g;
				fmt.ptr = ctx->groups;
				break;
			case 'G':
				fmt.fn = bfs_printf_G;
				break;
			case 'h':
				fmt.fn = bfs_printf_h;
				break;
			case 'H':
				fmt.fn = bfs_printf_H;
				break;
			case 'i':
				fmt.fn = bfs_printf_i;
				break;
			case 'k':
				fmt.fn = bfs_printf_k;
				break;
			case 'l':
				fmt.fn = bfs_printf_l;
				break;
			case 'm':
				fmt.fn = bfs_printf_m;
				specifier = "o";
				break;
			case 'M':
				fmt.fn = bfs_printf_M;
				break;
			case 'n':
				fmt.fn = bfs_printf_n;
				break;
			case 'p':
				fmt.fn = bfs_printf_p;
				break;
			case 'P':
				fmt.fn = bfs_printf_P;
				break;
			case 's':
				fmt.fn = bfs_printf_s;
				break;
			case 'S':
				fmt.fn = bfs_printf_S;
				specifier = "g";
				break;
			case 't':
				fmt.fn = bfs_printf_ctime;
				fmt.stat_field = BFS_STAT_MTIME;
				break;
			case 'u':
				fmt.fn = bfs_printf_u;
				fmt.ptr = ctx->users;
				break;
			case 'U':
				fmt.fn = bfs_printf_U;
				break;
			case 'w':
				fmt.fn = bfs_printf_ctime;
				fmt.stat_field = BFS_STAT_BTIME;
				break;
			case 'y':
				fmt.fn = bfs_printf_y;
				break;
			case 'Y':
				fmt.fn = bfs_printf_Y;
				break;
			case 'Z':
#if BFS_CAN_CHECK_CONTEXT
				fmt.fn = bfs_printf_Z;
				break;
#else
				bfs_expr_error(ctx, expr);
				bfs_error(ctx, "Missing platform support for '%%%c'.\n", c);
				goto fmt_error;
#endif

			case 'A':
				fmt.stat_field = BFS_STAT_ATIME;
				goto fmt_strftime;
			case 'B':
			case 'W':
				fmt.stat_field = BFS_STAT_BTIME;
				goto fmt_strftime;
			case 'C':
				fmt.stat_field = BFS_STAT_CTIME;
				goto fmt_strftime;
			case 'T':
				fmt.stat_field = BFS_STAT_MTIME;
				goto fmt_strftime;

			fmt_strftime:
				fmt.fn = bfs_printf_strftime;
				c = *++i;
				if (!c) {
					bfs_expr_error(ctx, expr);
					bfs_error(ctx, "Incomplete time specifier '%s%c'.\n", fmt.str, i[-1]);
					goto fmt_error;
				} else if (strchr("%+@aAbBcCdDeFgGhHIjklmMnprRsStTuUVwWxXyYzZ", c)) {
					fmt.c = c;
				} else {
					bfs_expr_error(ctx, expr);
					bfs_error(ctx, "Unrecognized time specifier '%%%c%c'.\n", i[-1], c);
					goto fmt_error;
				}
				break;

			case '\0':
				bfs_expr_error(ctx, expr);
				bfs_error(ctx, "Incomplete format specifier '%s'.\n", fmt.str);
				goto fmt_error;

			default:
				bfs_expr_error(ctx, expr);
				bfs_error(ctx, "Unrecognized format specifier '%%%c'.\n", c);
				goto fmt_error;
			}

			if (must_be_numeric && strcmp(specifier, "s") == 0) {
				bfs_expr_error(ctx, expr);
				bfs_error(ctx, "Invalid flags '%s' for string format '%%%c'.\n", fmt.str + 1, c);
				goto fmt_error;
			}

			if (dstrcat(&fmt.str, specifier) != 0) {
				bfs_perror(ctx, "dstrcat()");
				goto fmt_error;
			}

			if (append_directive(ctx, expr->printf, &literal, &fmt) != 0) {
				goto fmt_error;
			}

			continue;

		fmt_error:
			dstrfree(fmt.str);
			goto error;
		}

	one_char:
		if (dstrapp(&literal, c) != 0) {
			bfs_perror(ctx, "dstrapp()");
			goto error;
		}
	}

done:
	if (append_literal(ctx, expr->printf, &literal) != 0) {
		goto error;
	}
	dstrfree(literal);
	return 0;

error:
	dstrfree(literal);
	bfs_printf_free(expr->printf);
	expr->printf = NULL;
	return -1;
}

int bfs_printf(CFILE *cfile, const struct bfs_printf *format, const struct BFTW *ftwbuf) {
	int ret = 0, error = 0;

	for (size_t i = 0; i < format->nfmts; ++i) {
		const struct bfs_fmt *fmt = &format->fmts[i];
		if (fmt->fn(cfile, fmt, ftwbuf) < 0) {
			ret = -1;
			error = errno;
		}
	}

	errno = error;
	return ret;
}

void bfs_printf_free(struct bfs_printf *format) {
	if (!format) {
		return;
	}

	for (size_t i = 0; i < format->nfmts; ++i) {
		dstrfree(format->fmts[i].str);
	}
	free(format->fmts);
	free(format);
}
