// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * Copyright (C) 2018-2024 Oracle.  All Rights Reserved.
 * Author: Darrick J. Wong <djwong@kernel.org>
 */
#include "xfs.h"
#include <pthread.h>
#include <sys/statvfs.h>
#include <syslog.h>
#include "platform_defs.h"
#include "libfrog/paths.h"
#include "libfrog/getparents.h"
#include "libfrog/handle_priv.h"
#include "xfs_scrub.h"
#include "common.h"
#include "progress.h"

extern char		*progname;

/*
 * Reporting Status to the Console
 *
 * We aim for a roughly standard reporting format -- the severity of the
 * status being reported, a textual description of the object being
 * reported, and whatever the status happens to be.
 *
 * Errors are the most severe and reflect filesystem corruption.
 * Warnings indicate that something is amiss and needs the attention of
 * the administrator, but does not constitute a corruption.  Information
 * is merely advisory.
 */

/* Too many errors? Bail out. */
bool
scrub_excessive_errors(
	struct scrub_ctx	*ctx)
{
	unsigned long long	errors_seen;

	/*
	 * We only set max_errors at the start of the program, so it's safe to
	 * access it locklessly.
	 */
	if (ctx->max_errors == 0)
		return false;

	pthread_mutex_lock(&ctx->lock);
	errors_seen = ctx->corruptions_found + ctx->unfixable_errors;
	pthread_mutex_unlock(&ctx->lock);

	return errors_seen >= ctx->max_errors;
}

static struct {
	const char *string;
	int loglevel;
} err_levels[] = {
	[S_ERROR]  = {
		.string = "Error",
		.loglevel = LOG_ERR,
	},
	[S_CORRUPT] = {
		.string = "Corruption",
		.loglevel = LOG_ERR,
	},
	[S_UNFIXABLE] = {
		.string = "Unfixable Error",
		.loglevel = LOG_ERR,
	},
	[S_WARN]   = {
		.string = "Warning",
		.loglevel = LOG_WARNING,
	},
	[S_INFO]   = {
		.string = "Info",
		.loglevel = LOG_INFO,
	},
	[S_REPAIR] = {
		.string = "Repaired",
		.loglevel = LOG_INFO,
	},
	[S_PREEN]  = {
		.string = "Optimized",
		.loglevel = LOG_INFO,
	},
};

/* If stream is a tty, clear to end of line to clean up progress bar. */
static inline const char *stream_start(FILE *stream)
{
	if (stream == stderr)
		return stderr_isatty ? CLEAR_EOL : "";
	return stdout_isatty ? CLEAR_EOL : "";
}

/* Print a warning string and some warning text. */
void
__str_out(
	struct scrub_ctx	*ctx,
	const char		*descr,
	enum error_level	level,
	int			error,
	const char		*file,
	int			line,
	const char		*format,
	...)
{
	FILE			*stream = stderr;
	va_list			args;
	char			buf[DESCR_BUFSZ];

	/* print strerror or format of choice but not both */
	assert(!(error && format));

	if (level == S_INFO && info_is_warning)
		level = S_WARN;
	if (level >= S_INFO)
		stream = stdout;

	pthread_mutex_lock(&ctx->lock);

	/* We only want to hear about optimizing when in debug/verbose mode. */
	if (level == S_PREEN && !debug && !verbose)
		goto out_record;

	fprintf(stream, "%s%s: %s: ", stream_start(stream),
			_(err_levels[level].string), descr);
	if (error) {
		fprintf(stream, _("%s."), strerror_r(error, buf, DESCR_BUFSZ));
	} else {
		va_start(args, format);
		vfprintf(stream, format, args);
		va_end(args);
	}

	if (debug)
		fprintf(stream, _(" (%s line %d)"), file, line);
	fprintf(stream, "\n");
	if (stream == stdout)
		fflush(stream);

out_record:
	if (error || level == S_ERROR)      /* A syscall failed */
		ctx->runtime_errors++;
	else if (level == S_CORRUPT)
		ctx->corruptions_found++;
	else if (level == S_UNFIXABLE)
		ctx->unfixable_errors++;
	else if (level == S_WARN)
		ctx->warnings_found++;
	else if (level == S_REPAIR)
		ctx->repairs++;
	else if (level == S_PREEN)
		ctx->preens++;

	pthread_mutex_unlock(&ctx->lock);
}

/* Log a message to syslog. */
#define LOG_BUFSZ	4096
#define LOGNAME_BUFSZ	256
void
__str_log(
	struct scrub_ctx	*ctx,
	enum error_level	level,
	const char		*format,
	...)
{
	va_list			args;
	char			logname[LOGNAME_BUFSZ];
	char			buf[LOG_BUFSZ];
	int			sz;

	/* We only want to hear about optimizing when in debug/verbose mode. */
	if (level == S_PREEN && !debug && !verbose)
		return;

	/*
	 * Skip logging if we're being run as a service (presumably the
	 * service will log stdout/stderr); if we're being run in a non
	 * interactive manner (assume we're a service); or if we're in
	 * debug mode.
	 */
	if (is_service || !isatty(fileno(stdin)) || debug)
		return;

	snprintf(logname, LOGNAME_BUFSZ, "%s@%s", progname, ctx->mntpoint);
	openlog(logname, LOG_PID, LOG_DAEMON);

	sz = snprintf(buf, LOG_BUFSZ, "%s: ", _(err_levels[level].string));
	va_start(args, format);
	vsnprintf(buf + sz, LOG_BUFSZ - sz, format, args);
	va_end(args);
	syslog(err_levels[level].loglevel, "%s", buf);

	closelog();
}

double
timeval_subtract(
	struct timeval		*tv1,
	struct timeval		*tv2)
{
	return ((tv1->tv_sec - tv2->tv_sec) +
		((float) (tv1->tv_usec - tv2->tv_usec)) / 1000000);
}

/* Produce human readable disk space output. */
double
auto_space_units(
	unsigned long long	bytes,
	char			**units)
{
	if (debug > 1)
		goto no_prefix;
	if (bytes > (1ULL << 40)) {
		*units = "TiB";
		return (double)bytes / (1ULL << 40);
	} else if (bytes > (1ULL << 30)) {
		*units = "GiB";
		return (double)bytes / (1ULL << 30);
	} else if (bytes > (1ULL << 20)) {
		*units = "MiB";
		return (double)bytes / (1ULL << 20);
	} else if (bytes > (1ULL << 10)) {
		*units = "KiB";
		return (double)bytes / (1ULL << 10);
	}

no_prefix:
	*units = "B";
	return bytes;
}

/* Produce human readable discrete number output. */
double
auto_units(
	unsigned long long	number,
	char			**units,
	int			*precision)
{
	if (debug > 1)
		goto no_prefix;
	*precision = 1;
	if (number > 1000000000000ULL) {
		*units = "T";
		return number / 1000000000000.0;
	} else if (number > 1000000000ULL) {
		*units = "G";
		return number / 1000000000.0;
	} else if (number > 1000000ULL) {
		*units = "M";
		return number / 1000000.0;
	} else if (number > 1000ULL) {
		*units = "K";
		return number / 1000.0;
	}

no_prefix:
	*units = "";
	*precision = 0;
	return number;
}

/* How many threads to kick off? */
unsigned int
scrub_nproc(
	struct scrub_ctx	*ctx)
{
	if (force_nr_threads)
		return force_nr_threads;
	return ctx->nr_io_threads;
}

/*
 * How many threads to kick off for a workqueue?  If we only want one
 * thread, save ourselves the overhead and just run it in the main thread.
 */
unsigned int
scrub_nproc_workqueue(
	struct scrub_ctx	*ctx)
{
	unsigned int		x;

	x = scrub_nproc(ctx);
	if (x == 1)
		x = 0;
	return x;
}

/*
 * Sleep for 100us * however many -b we got past the initial one.
 * This is an (albeit clumsy) way to throttle scrub activity.
 */
void
background_sleep(void)
{
	unsigned long long	time_ns;
	struct timespec		tv;

	if (bg_mode < 2)
		return;

	time_ns =  100 * NSEC_PER_USEC * (bg_mode - 1);
	tv.tv_sec = time_ns / NSEC_PER_SEC;
	tv.tv_nsec = time_ns % NSEC_PER_SEC;
	nanosleep(&tv, NULL);
}

/*
 * Return the input string with non-printing bytes escaped.
 * Caller must free the buffer.
 */
char *
string_escape(
	const char		*in)
{
	char			*str;
	const char		*p;
	char			*q;
	int			x;

	/*
	 * Each non-printing byte renders as a four-byte escape sequence, so
	 * allocate 4x the input length, plus a byte for the null terminator.
	 */
	str = malloc(strlen(in) * 4 + 1);
	if (!str)
		return NULL;
	for (p = in, q = str; *p != '\0'; p++) {
		if (isprint(*p)) {
			*q = *p;
			q++;
		} else {
			x = sprintf(q, "\\x%02x", *p);
			q += x;
		}
	}
	*q = '\0';
	return str;
}

/*
 * Record another naming warning, and decide if it's worth
 * complaining about.
 */
bool
should_warn_about_name(
	struct scrub_ctx	*ctx)
{
	bool			whine;
	bool			res;

	pthread_mutex_lock(&ctx->lock);
	ctx->naming_warnings++;
	whine = ctx->naming_warnings == TOO_MANY_NAME_WARNINGS;
	res = ctx->naming_warnings < TOO_MANY_NAME_WARNINGS;
	pthread_mutex_unlock(&ctx->lock);

	if (whine && !(debug || verbose))
		str_info(ctx, ctx->mntpoint,
_("More than %u naming warnings, shutting up."),
				TOO_MANY_NAME_WARNINGS);

	return debug || verbose || res;
}

/* Decide if a value is within +/- (n/d) of a desired value. */
bool
within_range(
	struct scrub_ctx	*ctx,
	unsigned long long	value,
	unsigned long long	desired,
	unsigned long long	abs_threshold,
	unsigned int		n,
	unsigned int		d,
	const char		*descr)
{
	assert(n < d);

	/* Don't complain if difference does not exceed an absolute value. */
	if (value < desired && desired - value < abs_threshold)
		return true;
	if (value > desired && value - desired < abs_threshold)
		return true;

	/* Complain if the difference exceeds a certain percentage. */
	if (value < desired * (d - n) / d)
		return false;
	if (value > desired * (d + n) / d)
		return false;

	return true;
}

/*
 * Render an inode number into a buffer in a format suitable for use in
 * log messages. The buffer will be filled with:
 * 	"inode <inode number> (<ag number>/<ag inode number>)"
 * If the @format argument is non-NULL, it will be rendered into the buffer
 * after the inode representation and a single space.
 */
int
scrub_render_ino_descr(
	const struct scrub_ctx	*ctx,
	char			*buf,
	size_t			buflen,
	uint64_t		ino,
	uint32_t		gen,
	const char		*format,
	...)
{
	va_list			args;
	size_t			pathlen = 0;
	uint32_t		agno;
	uint32_t		agino;
	int			ret;

	if (ctx->mnt.fsgeom.flags & XFS_FSOP_GEOM_FLAGS_PARENT) {
		struct xfs_handle handle;
		char		*pathbuf = buf;
		size_t		used = 0;

		handle_from_fshandle(&handle, ctx->fshandle, ctx->fshandle_len);
		handle_from_inogen(&handle, ino, gen);

		/*
		 * @actual_mntpoint is the path we used to open the filesystem,
		 * and @mntpoint is the path we use for display purposes.  If
		 * these aren't the same string, then for reporting purposes
		 * we must fix the start of the path string.  Start by copying
		 * the display mountpoint into buf, except for trailing
		 * slashes.  At this point buf will not be null-terminated.
		 */
		if (ctx->actual_mntpoint != ctx->mntpoint) {
			used = strlen(ctx->mntpoint);
			while (used && ctx->mntpoint[used - 1] == '/')
				used--;

			/* If it doesn't fit, report the handle instead. */
			if (used >= buflen) {
				used = 0;
				goto report_inum;
			}

			memcpy(buf, ctx->mntpoint, used);
			pathbuf += used;
		}

		ret = handle_to_path(&handle, sizeof(struct xfs_handle), 4096,
				pathbuf, buflen - used);
		if (ret)
			goto report_inum;

		/*
		 * Now that handle_to_path formatted the full path (including
		 * the actual mount point, stripped of any trailing slashes)
		 * into the rest of pathbuf, slide down the contents by the
		 * length of the actual mount point.  Don't count any trailing
		 * slashes because handle_to_path uses libhandle, which strips
		 * trailing slashes.  Copy one more byte to ensure we get the
		 * terminating null.
		 */
		if (ctx->actual_mntpoint != ctx->mntpoint) {
			size_t	len = strlen(ctx->actual_mntpoint);

			while (len && ctx->actual_mntpoint[len - 1] == '/')
				len--;

			pathlen = strlen(pathbuf);
			memmove(pathbuf, pathbuf + len, pathlen - len + 1);
		}

		/*
		 * Leave at least 16 bytes for the description of what went
		 * wrong.  If we can't do that, we'll use the inode number.
		 */
		pathlen = strlen(buf);
		if (pathlen >= buflen - 16)
			goto report_inum;

		if (format) {
			buf[pathlen] = ' ';
			buf[pathlen + 1] = 0;
			pathlen++;
		}

		goto report_format;
	}

report_inum:
	agno = cvt_ino_to_agno(&ctx->mnt, ino);
	agino = cvt_ino_to_agino(&ctx->mnt, ino);
	ret = snprintf(buf, buflen, _("inode %"PRIu64" (%"PRIu32"/%"PRIu32")%s"),
			ino, agno, agino, format ? " " : "");
	if (ret < 0 || ret >= buflen || format == NULL)
		return ret;
	pathlen = ret;

report_format:
	va_start(args, format);
	pathlen += vsnprintf(buf + pathlen, buflen - pathlen, format, args);
	va_end(args);
	return pathlen;
}
