#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <stdarg.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <libmount.h>
#include <blkid.h>
#include <sys/utsname.h>
#include <stdbool.h>

#include "nls.h"
#include "c.h"
#include "strutils.h"
#include "xalloc.h"
#include "pathnames.h"
#include "match.h"
#include "mountutils.h"

#include "findmnt.h"

struct verify_context {
	struct libmnt_fs	*fs;
	struct libmnt_table	*tb;

	char	**fs_ary;
	size_t	fs_num;
	size_t  fs_alloc;

	int	nwarnings;
	int	nerrors;

	unsigned int	target_printed : 1,
			no_fsck : 1;
};

static void __attribute__ ((__format__ (__printf__, 3, 0)))
	verify_mesg(struct verify_context *vfy, char type, const char *fmt, va_list ap)
{
	if (!vfy->target_printed && vfy->fs) {
		fprintf(stdout, "%s\n", mnt_fs_get_target(vfy->fs));
		vfy->target_printed = 1;
	}

	fprintf(stdout, "   [%c] ", type);
	vfprintf(stdout, fmt, ap);
	fputc('\n', stdout);
}

static int __attribute__ ((__format__ (__printf__, 2, 3)))
	verify_warn(struct verify_context *vfy, const char *fmt, ...)
{
	va_list ap;
	vfy->nwarnings++;
	va_start(ap, fmt);
	verify_mesg(vfy, 'W', fmt, ap);
	va_end(ap);
	return 0;
}

static int __attribute__ ((__format__ (__printf__, 2, 3)))
	verify_err(struct verify_context *vfy, const char *fmt, ...)
{
	va_list ap;
	vfy->nerrors++;
	va_start(ap, fmt);
	verify_mesg(vfy, 'E', fmt, ap);
	va_end(ap);
	return 0;
}

static int __attribute__ ((__format__ (__printf__, 3, 4)))
	verify_ok(struct verify_context *vfy __attribute__((unused)),
		      bool verbose,
		      const char *fmt, ...)
{
	va_list ap;

	if (!verbose)
		return 0;

	va_start(ap, fmt);
	verify_mesg(vfy, ' ', fmt, ap);
	va_end(ap);
	return 0;
}

static int verify_order(struct verify_context *vfy, struct findmnt *findmnt)
{
	struct libmnt_iter *itr = NULL;
	struct libmnt_fs *next;
	const char *tgt;

	tgt = mnt_fs_get_target(vfy->fs);
	if (tgt && !(findmnt->flags & FL_NOCACHE))
		tgt  = mnt_resolve_target(tgt, findmnt->cache);
	else if (!tgt)
		return 0;

	itr = mnt_new_iter(MNT_ITER_FORWARD);
	if (!itr) {
		warn(_("failed to initialize libmount iterator"));
		goto done;
	}

	/* set iterator position to 'fs' */
	mnt_table_set_iter(vfy->tb, itr, vfy->fs);

	if (mnt_table_next_fs(vfy->tb, itr, &next) != 0)
		goto done;

	/* scan all next filesystems */
	while (mnt_table_next_fs(vfy->tb, itr, &next) == 0) {
		const char *n_tgt;
		size_t len;

		n_tgt = mnt_fs_get_target(next);
		if (n_tgt && !(findmnt->flags & FL_NOCACHE))
			n_tgt  = mnt_resolve_target(n_tgt, findmnt->cache);
		else if (!n_tgt)
			continue;
		len = strlen(n_tgt);

		if (strncmp(n_tgt, tgt, len) == 0) {
			if (*(tgt + len) == '\0')
				verify_warn(vfy, _("target specified more than once"));
			else if (*(tgt + len) == '/')
				verify_err(vfy, _("wrong order: %s specified before %s"), tgt, n_tgt);
		}
	}
done:
	mnt_free_iter(itr);
	return 0;
}

static int verify_target(struct verify_context *vfy, struct findmnt *findmnt)
{
	const char *tgt = mnt_fs_get_target(vfy->fs);
	struct stat sb;

	if (!tgt)
		return verify_err(vfy, _("undefined target (fs_file)"));

	if (!(findmnt->flags & FL_NOCACHE)) {
		const char *cn = mnt_resolve_target(tgt, findmnt->cache);
		if (!cn)
			return -ENOMEM;
		if (strcmp(cn, tgt) != 0)
			verify_warn(vfy, _("non-canonical target path (real: %s)"), cn);
		tgt = cn;
	}
	if (stat(tgt, &sb) != 0) {
		if (mnt_fs_get_option(vfy->fs, "noauto", NULL, NULL) == 1)
			verify_err(vfy, _("unreachable on boot required target: %m"));
		else
			verify_warn(vfy, _("unreachable target: %m"));

	} else if (!S_ISDIR(sb.st_mode)
		 && mnt_fs_get_option(vfy->fs, "bind", NULL, NULL) == 1) {
		verify_err(vfy, _("target is not a directory"));
	} else
		verify_ok(vfy, findmnt->flags & FL_VERBOSE, _("target exists"));

	return 0;
}

static char *verify_tag(struct verify_context *vfy, const char *name,
		      const char *value, struct findmnt *findmnt)
{
	char *src = NULL;

	if (!(findmnt->flags & FL_NOCACHE))
		src = mnt_resolve_tag(name, value, findmnt->cache);

	if (!src) {
		if (mnt_fs_get_option(vfy->fs, "noauto", NULL, NULL) == 1)
			verify_err(vfy, _("unreachable on boot required source: %s=%s"), name, value);
		else
			verify_warn(vfy, _("unreachable: %s=%s"), name, value);
	} else
		verify_ok(vfy, findmnt->flags & FL_VERBOSE, _("%s=%s translated to %s"), name, value, src);

	return src;
}

/* Note that mount source is very FS specific and we should not
 * interpret unreachable source as error. The exception is only
 * NAME=value, this has to be convertible to device name.
 */
static int verify_source(struct verify_context *vfy, struct findmnt *findmnt)
{
	const char *src = mnt_fs_get_srcpath(vfy->fs);
	char *t = NULL, *v = NULL;
	struct stat sb;
	int isbind, rc = 0;

	/* source is NAME=value tag */
	if (!src) {
		const char *tag = NULL, *val = NULL;

		if (mnt_fs_get_tag(vfy->fs, &tag, &val) != 0)
			return verify_err(vfy, _("undefined source (fs_spec)"));

		src = verify_tag(vfy, tag, val, findmnt);
		if (!src)
			goto done;

	/* blkid is able to parse it, but libmount does not see it as a tag --
	 * it means unsupported tag */
	} else if (blkid_parse_tag_string(src, &t, &v) == 0 && stat(src, &sb) != 0) {
		rc = verify_err(vfy, _("unsupported source tag: %s"), src);
		goto done;
	}
	isbind = mnt_fs_get_option(vfy->fs, "bind", NULL, NULL) == 0;

	/* source is path */
	if (mnt_fs_is_pseudofs(vfy->fs) || mnt_fs_is_netfs(vfy->fs))
		verify_ok(vfy, findmnt->flags & FL_VERBOSE, _("do not check %s source (pseudo/net)"), src);

	else if (stat(src, &sb) != 0)
		verify_warn(vfy, _("unreachable source: %s: %m"), src);

	else if ((S_ISDIR(sb.st_mode) || S_ISREG(sb.st_mode)) && !isbind)
		verify_warn(vfy, _("non-bind mount source %s is a directory or regular file"), src);

	else if (!S_ISBLK(sb.st_mode) && !isbind)
		verify_warn(vfy, _("source %s is not a block device"), src);
	else
		verify_ok(vfy, findmnt->flags & FL_VERBOSE, _("source %s exists"), src);
done:
	free(t);
	free(v);
	return rc;
}

static int verify_options(struct verify_context *vfy, bool verbose)
{
	const char *opts;

	opts = mnt_fs_get_vfs_options(vfy->fs);
	if (opts)
		verify_ok(vfy, verbose, _("VFS options: %s"), opts);

	opts = mnt_fs_get_fs_options(vfy->fs);
	if (opts)
		verify_ok(vfy, verbose, _("FS options: %s"), opts);

	opts = mnt_fs_get_user_options(vfy->fs);
	if (opts)
		verify_ok(vfy, verbose, _("userspace options: %s"), opts);

	return 0;
}

static int verify_swaparea(struct verify_context *vfy)
{
	char *arg;
	size_t argsz = 0;

	if (mnt_fs_get_option(vfy->fs, "discard", &arg, &argsz) == 0
	    && arg
	    && strncmp(arg, "once", argsz) != 0
	    && strncmp(arg, "pages", argsz) != 0)
		verify_err(vfy, _("unsupported swaparea discard policy: %s"), arg);

	if (mnt_fs_get_option(vfy->fs, "pri", &arg, &argsz) == 0 && arg) {
		char *p = arg;
		if (*p == '-')
			p++;
		for (; p < arg + argsz; p++) {
			if (!isdigit((unsigned char) *p)) {
				verify_err(vfy, _("failed to parse swaparea priority option"));
				break;
			}
		}
	}

	return 0;
}

static int is_supported_filesystem(struct verify_context *vfy, const char *name)
{
	size_t n;

	if (!vfy->fs_num)
		return 0;

	for (n = 0; n < vfy->fs_num; n++ ) {
		if (match_fstype(vfy->fs_ary[n], name))
			return 1;
	}

	return 0;
}

static int add_filesystem(struct verify_context *vfy, const char *name)
{
	#define MYCHUNK	16

	if (is_supported_filesystem(vfy, name))
		return 0;

	if (vfy->fs_alloc == 0 || vfy->fs_num + 1 <= vfy->fs_alloc) {
		vfy->fs_alloc = ((vfy->fs_alloc + 1 + MYCHUNK) / MYCHUNK) * MYCHUNK;
		vfy->fs_ary = xreallocarray(vfy->fs_ary, vfy->fs_alloc, sizeof(char *));
	}

	vfy->fs_ary[vfy->fs_num] = xstrdup(name);
	vfy->fs_num++;

	return 0;
}

static int read_proc_filesystems(struct verify_context *vfy)
{
	int rc = 0;
	FILE *f;
	char buf[80], *cp, *t;

	f = fopen("/proc/filesystems", "r");
	if (!f)
		return -errno;

	while (!feof(f)) {
		if (!fgets(buf, sizeof(buf), f))
			break;
		cp = buf;
		if (!isspace(*cp)) {
			while (*cp && !isspace(*cp))
				cp++;
		}
		while (*cp && isspace(*cp))
			cp++;
		if ((t = strchr(cp, '\n')) != NULL)
			*t = 0;
		if ((t = strchr(cp, '\t')) != NULL)
			*t = 0;
		if ((t = strchr(cp, ' ')) != NULL)
			*t = 0;

		rc = add_filesystem(vfy, cp);
		if (rc)
			break;
	}
	fclose(f);
	return rc;
}

static void free_proc_filesystems(struct verify_context *vfy)
{
	size_t n;

	if (!vfy->fs_ary)
		return;

	for (n = 0; n < vfy->fs_num; n++ )
		free(vfy->fs_ary[n]);
	free(vfy->fs_ary);
}

static int read_kernel_filesystems(struct verify_context *vfy)
{
	int rc = 0;
#ifdef __linux__
	struct utsname uts;
	FILE *f;
	char buf[1024];

	if (uname(&uts))
		return 0;
	snprintf(buf, sizeof(buf), "/lib/modules/%s/modules.dep", uts.release);

	f = fopen(buf, "r");
	if (!f)
		return 0;

	while (!feof(f)) {
		char *p, *name;

		if (!fgets(buf, sizeof(buf), f))
			break;

		if (strncmp("kernel/fs/", buf, 10) != 0 ||
		    strncmp("kernel/fs/nls/", buf, 14) == 0)
			continue;

		p = strchr(buf, ':');
		if (!p)
			continue;
		*p = '\0';

		name = strrchr(buf, '/');
		if (!name)
			continue;
		name++;

		p = strstr(name, ".ko");
		if (!p)
			continue;
		*p = '\0';

		rc = add_filesystem(vfy, name);
		if (rc)
			break;
	}
	fclose(f);
#endif /* __linux__ */
	return rc;
}

static int verify_fstype(struct verify_context *vfy, struct findmnt *findmnt)
{
	char *src = mnt_resolve_spec(mnt_fs_get_source(vfy->fs), findmnt->cache);
	char *realtype = NULL;
	const char *type;
	int ambi = 0, isauto = 0, isswap = 0;

	if (!src)
		return 0;

	if (mnt_fs_is_pseudofs(vfy->fs) || mnt_fs_is_netfs(vfy->fs)) {
		verify_ok(vfy, findmnt->flags & FL_VERBOSE, _("do not check %s FS type (pseudo/net)"), src);
		goto done;
	}

	type = mnt_fs_get_fstype(vfy->fs);

	if (type) {
		int none = strcmp(type, "none") == 0;

		if (none
		    && mnt_fs_get_option(vfy->fs, "bind", NULL, NULL) == 1
		    && mnt_fs_get_option(vfy->fs, "move", NULL, NULL) == 1) {
			verify_warn(vfy, _("\"none\" FS type is recommended for bind or move operations only"));
			goto done;
		}

		if (strcmp(type, "auto") == 0)
			isauto = 1;
		else if (strcmp(type, "swap") == 0)
			isswap = 1;
		else if (strcmp(type, "xfs") == 0 || strcmp(type, "btrfs") == 0)
			vfy->no_fsck = 1;

		if (!isswap && !isauto && !none && !is_supported_filesystem(vfy, type))
			verify_warn(vfy, _("%s seems unsupported by the current kernel"), type);
	}

	errno = 0;
	realtype = mnt_get_fstype(src, &ambi, findmnt->cache);

	if (!realtype) {
		const char *reason = errno ? strerror(errno) : _("reason unknown");

		if (isauto)
			verify_err(vfy, _("cannot detect on-disk filesystem type (%s)"), reason);
		else
			verify_warn(vfy, _("cannot detect on-disk filesystem type (%s)"), reason);
		goto done;
	}

	if (realtype) {
		const char *mounttype;

		isswap = strcmp(realtype, "swap") == 0;
		vfy->no_fsck = strcmp(realtype, "xfs") == 0
				|| strcmp(realtype, "btrfs") == 0;

		/* libblkid always reports "ntfs"; remap possible different
		 * names from fstab to "ntfs" as well */
		if (type && strcmp(type, "ntfs3") == 0)
			type = "ntfs";

		if (type && !isauto && strcmp(type, realtype) != 0) {
			verify_warn(vfy, _("%s does not match with on-disk %s"), type, realtype);
			goto done;
		}

		mounttype = ul_fstype_to_mounttype(realtype) ? : realtype;

		if (!isswap
		    && (!type || isauto)
		    && !is_supported_filesystem(vfy, mounttype)) {
			verify_warn(vfy, _("on-disk %s seems unsupported by the current kernel"), mounttype);
			goto done;
		}

		verify_ok(vfy, findmnt->flags & FL_VERBOSE, _("FS type is %s"), realtype);
	}

done:
	if (!findmnt->cache) {
		free(src);
		free(realtype);
	}
	return 0;
}

static int verify_passno(struct verify_context *vfy)
{
	int passno = mnt_fs_get_passno(vfy->fs);
	const char *tgt = mnt_fs_get_target(vfy->fs);

	if (tgt && strcmp("/", tgt) == 0 && passno != 1 && !vfy->no_fsck)
		return verify_warn(vfy, _("recommended root FS passno is 1 (current is %d)"), passno);

	return 0;
}

static int verify_filesystem(struct verify_context *vfy, struct findmnt *findmnt)
{
	int rc = 0;

	if (mnt_fs_is_swaparea(vfy->fs))
		rc = verify_swaparea(vfy);
	else {
		rc = verify_target(vfy, findmnt);
		if (!rc)
			rc = verify_options(vfy, findmnt->flags & FL_VERBOSE);
	}

	if (!rc)
		rc = verify_source(vfy, findmnt);
	if (!rc)
		rc = verify_fstype(vfy, findmnt);
	if (!rc)
		rc = verify_passno(vfy);	/* depends on verify_fstype() */

	return rc;
}

int verify_table(struct libmnt_table *tb, struct findmnt *findmnt)
{
	struct verify_context vfy = { .nerrors = 0 };
	struct libmnt_iter *itr;
	int rc = 0;		/* overall return code (alloc errors, etc.) */
	int check_order = is_listall_mode(findmnt->flags);
	static int has_read_fs = 0;

	itr = mnt_new_iter(MNT_ITER_FORWARD);
	if (!itr) {
		warn(_("failed to initialize libmount iterator"));
		goto done;
	}

	vfy.tb = tb;

	if (has_read_fs == 0) {
		read_proc_filesystems(&vfy);
		read_kernel_filesystems(&vfy);
		has_read_fs = 1;
	}

	while (rc == 0 && (vfy.fs = get_next_fs(tb, itr, findmnt))) {
		vfy.target_printed = 0;
		vfy.no_fsck = 0;

		if (check_order)
			rc = verify_order(&vfy, findmnt);
		if (!rc)
			rc = verify_filesystem(&vfy, findmnt);

		if (findmnt->flags & FL_FIRSTONLY)
			break;
		findmnt->flags |= FL_NOSWAPMATCH;
	}

#ifdef USE_SYSTEMD
	{
		struct stat a, b;

		if (stat(_PATH_SD_UNITSLOAD, &a) == 0 &&
		    stat(_PATH_MNTTAB, &b) == 0 &&
		    cmp_stat_mtime(&a, &b, <))
			verify_warn(&vfy, _(
	"your fstab has been modified, but systemd still uses the old version;\n"
	"       use 'systemctl daemon-reload' to reload"));
	}
#endif

done:
	mnt_free_iter(itr);

	/* summary */
	if (vfy.nerrors || findmnt->parse_nerrors || vfy.nwarnings) {
		fputc('\n', stderr);
		fprintf(stderr, P_("%d parse error", "%d parse errors", findmnt->parse_nerrors), findmnt->parse_nerrors);
		fprintf(stderr, P_(", %d error",     ", %d errors", vfy.nerrors), vfy.nerrors);
		fprintf(stderr, P_(", %d warning",   ", %d warnings", vfy.nwarnings), vfy.nwarnings);
		fputc('\n', stderr);
	} else
		fprintf(stdout, _("Success, no errors or warnings detected\n"));


	free_proc_filesystems(&vfy);

	return rc != 0 ? rc : vfy.nerrors + findmnt->parse_nerrors;
}
