// SPDX-License-Identifier: GPL-2.0+
/*
 * Copyright (C) 2018 Oracle.  All Rights Reserved.
 * Author: Darrick J. Wong <darrick.wong@oracle.com>
 */
#include "xfs.h"
#include <stdint.h>
#include <dirent.h>
#include <sys/types.h>
#include <sys/statvfs.h>
#ifdef HAVE_LIBATTR
# include <attr/attributes.h>
#endif
#include <linux/fs.h>
#include "handle.h"
#include "list.h"
#include "libfrog/paths.h"
#include "libfrog/workqueue.h"
#include "xfs_scrub.h"
#include "common.h"
#include "inodes.h"
#include "progress.h"
#include "scrub.h"
#include "descr.h"
#include "unicrash.h"

/* Phase 5: Check directory connectivity. */

/*
 * Warn about problematic bytes in a directory/attribute name.  That means
 * terminal control characters and escape sequences, since that could be used
 * to do something naughty to the user's computer and/or break scripts.  XFS
 * doesn't consider any byte sequence invalid, so don't flag these as errors.
 *
 * Returns 0 for success or -1 for error.  This function logs errors.
 */
static int
simple_check_name(
	struct scrub_ctx	*ctx,
	struct descr		*dsc,
	const char		*namedescr,
	const char		*name)
{
	const char		*p;
	bool			bad = false;
	char			*errname;

	/* Complain about zero length names. */
	if (*name == '\0' && should_warn_about_name(ctx)) {
		str_warn(ctx, descr_render(dsc), _("Zero length name found."));
		return 0;
	}

	/* control characters */
	for (p = name; *p; p++) {
		if ((*p >= 1 && *p <= 31) || *p == 127) {
			bad = true;
			break;
		}
	}

	if (bad && should_warn_about_name(ctx)) {
		errname = string_escape(name);
		if (!errname) {
			str_errno(ctx, descr_render(dsc));
			return -1;
		}
		str_info(ctx, descr_render(dsc),
_("Control character found in %s name \"%s\"."),
				namedescr, errname);
		free(errname);
	}

	return 0;
}

/*
 * Iterate a directory looking for filenames with problematic
 * characters.
 */
static int
check_dirent_names(
	struct scrub_ctx	*ctx,
	struct descr		*dsc,
	int			*fd,
	struct xfs_bulkstat	*bstat)
{
	struct unicrash		*uc = NULL;
	DIR			*dir;
	struct dirent		*dentry;
	int			ret;

	dir = fdopendir(*fd);
	if (!dir) {
		str_errno(ctx, descr_render(dsc));
		return errno;
	}
	*fd = -1; /* closedir will close *fd for us */

	ret = unicrash_dir_init(&uc, ctx, bstat);
	if (ret) {
		str_liberror(ctx, ret, descr_render(dsc));
		goto out_unicrash;
	}

	errno = 0;
	dentry = readdir(dir);
	while (dentry) {
		if (uc)
			ret = unicrash_check_dir_name(uc, dsc, dentry);
		else
			ret = simple_check_name(ctx, dsc, _("directory"),
					dentry->d_name);
		if (ret) {
			str_liberror(ctx, ret, descr_render(dsc));
			break;
		}
		errno = 0;
		dentry = readdir(dir);
	}
	if (errno) {
		ret = errno;
		str_liberror(ctx, ret, descr_render(dsc));
	}
	unicrash_free(uc);

out_unicrash:
	closedir(dir);
	return ret;
}

#ifdef HAVE_LIBATTR
/* Routines to scan all of an inode's xattrs for name problems. */
struct attrns_decode {
	int			flags;
	const char		*name;
};

static const struct attrns_decode attr_ns[] = {
	{0,			"user"},
	{ATTR_ROOT,		"system"},
	{ATTR_SECURE,		"secure"},
	{0, NULL},
};

/*
 * Check all the xattr names in a particular namespace of a file handle
 * for Unicode normalization problems or collisions.
 */
static int
check_xattr_ns_names(
	struct scrub_ctx		*ctx,
	struct descr			*dsc,
	struct xfs_handle		*handle,
	struct xfs_bulkstat		*bstat,
	const struct attrns_decode	*attr_ns)
{
	struct attrlist_cursor		cur;
	char				attrbuf[XFS_XATTR_LIST_MAX];
	char				keybuf[XATTR_NAME_MAX + 1];
	struct attrlist			*attrlist = (struct attrlist *)attrbuf;
	struct attrlist_ent		*ent;
	struct unicrash			*uc = NULL;
	int				i;
	int				error;

	error = unicrash_xattr_init(&uc, ctx, bstat);
	if (error) {
		str_liberror(ctx, error, descr_render(dsc));
		return error;
	}

	memset(attrbuf, 0, XFS_XATTR_LIST_MAX);
	memset(&cur, 0, sizeof(cur));
	memset(keybuf, 0, XATTR_NAME_MAX + 1);
	error = attr_list_by_handle(handle, sizeof(*handle), attrbuf,
			XFS_XATTR_LIST_MAX, attr_ns->flags, &cur);
	while (!error) {
		/* Examine the xattrs. */
		for (i = 0; i < attrlist->al_count; i++) {
			ent = ATTR_ENTRY(attrlist, i);
			snprintf(keybuf, XATTR_NAME_MAX, "%s.%s", attr_ns->name,
					ent->a_name);
			if (uc)
				error = unicrash_check_xattr_name(uc, dsc,
						keybuf);
			else
				error = simple_check_name(ctx, dsc,
						_("extended attribute"),
						keybuf);
			if (error) {
				str_liberror(ctx, error, descr_render(dsc));
				goto out;
			}
		}

		if (!attrlist->al_more)
			break;
		error = attr_list_by_handle(handle, sizeof(*handle), attrbuf,
				XFS_XATTR_LIST_MAX, attr_ns->flags, &cur);
	}
	if (error) {
		if (errno == ESTALE)
			errno = 0;
		if (errno)
			str_errno(ctx, descr_render(dsc));
	}
out:
	unicrash_free(uc);
	return error;
}

/*
 * Check all the xattr names in all the xattr namespaces for problematic
 * characters.
 */
static int
check_xattr_names(
	struct scrub_ctx		*ctx,
	struct descr			*dsc,
	struct xfs_handle		*handle,
	struct xfs_bulkstat		*bstat)
{
	const struct attrns_decode	*ns;
	int				ret;

	for (ns = attr_ns; ns->name; ns++) {
		ret = check_xattr_ns_names(ctx, dsc, handle, bstat, ns);
		if (ret)
			break;
	}
	return ret;
}
#else
# define check_xattr_names(c, d, h, b)	(0)
#endif /* HAVE_LIBATTR */

static int
render_ino_from_handle(
	struct scrub_ctx	*ctx,
	char			*buf,
	size_t			buflen,
	void			*data)
{
	struct xfs_bulkstat	*bstat = data;

	return scrub_render_ino_descr(ctx, buf, buflen, bstat->bs_ino,
			bstat->bs_gen, NULL);
}

/*
 * Verify the connectivity of the directory tree.
 * We know that the kernel's open-by-handle function will try to reconnect
 * parents of an opened directory, so we'll accept that as sufficient.
 *
 * Check for potential Unicode collisions in names.
 */
static int
check_inode_names(
	struct scrub_ctx	*ctx,
	struct xfs_handle	*handle,
	struct xfs_bulkstat	*bstat,
	void			*arg)
{
	DEFINE_DESCR(dsc, ctx, render_ino_from_handle);
	bool			*aborted = arg;
	int			fd = -1;
	int			error = 0;
	int			err2;

	descr_set(&dsc, bstat);
	background_sleep();

	/* Warn about naming problems in xattrs. */
	if (bstat->bs_xflags & FS_XFLAG_HASATTR) {
		error = check_xattr_names(ctx, &dsc, handle, bstat);
		if (error)
			goto out;
	}

	/*
	 * Warn about naming problems in the directory entries.  Opening the
	 * dir by handle means the kernel will try to reconnect it to the root.
	 * If the reconnection fails due to corruption in the parents we get
	 * ESTALE, which is why we skip phase 5 if we found corruption.
	 */
	if (S_ISDIR(bstat->bs_mode)) {
		fd = scrub_open_handle(handle);
		if (fd < 0) {
			error = errno;
			if (error == ESTALE)
				return ESTALE;
			str_errno(ctx, descr_render(&dsc));
			goto out;
		}

		error = check_dirent_names(ctx, &dsc, &fd, bstat);
		if (error)
			goto out;
	}

out:
	progress_add(1);
	if (fd >= 0) {
		err2 = close(fd);
		if (err2)
			str_errno(ctx, descr_render(&dsc));
		if (!error && err2)
			error = err2;
	}

	if (error)
		*aborted = true;
	if (!error && *aborted)
		error = ECANCELED;

	return error;
}

#ifndef FS_IOC_GETFSLABEL
# define FSLABEL_MAX		256
# define FS_IOC_GETFSLABEL	_IOR(0x94, 49, char[FSLABEL_MAX])
#endif /* FS_IOC_GETFSLABEL */

static int
scrub_render_mountpoint(
	struct scrub_ctx	*ctx,
	char			*buf,
	size_t			buflen,
	void			*data)
{
	return snprintf(buf, buflen, _("%s"), ctx->mntpoint);
}

/*
 * Check the filesystem label for Unicode normalization problems or misleading
 * sequences.
 */
static int
check_fs_label(
	struct scrub_ctx		*ctx)
{
	DEFINE_DESCR(dsc, ctx, scrub_render_mountpoint);
	char				label[FSLABEL_MAX];
	struct unicrash			*uc = NULL;
	int				error;

	error = unicrash_fs_label_init(&uc, ctx);
	if (error) {
		str_liberror(ctx, error, descr_render(&dsc));
		return error;
	}

	descr_set(&dsc, NULL);

	/* Retrieve label; quietly bail if we don't support that. */
	error = ioctl(ctx->mnt.fd, FS_IOC_GETFSLABEL, &label);
	if (error) {
		if (errno != EOPNOTSUPP && errno != ENOTTY) {
			error = errno;
			perror(ctx->mntpoint);
		}
		goto out;
	}

	/* Ignore empty labels. */
	if (label[0] == 0)
		goto out;

	/* Otherwise check for weirdness. */
	if (uc)
		error = unicrash_check_fs_label(uc, &dsc, label);
	else
		error = simple_check_name(ctx, &dsc, _("filesystem label"),
				label);
	if (error)
		str_liberror(ctx, error, descr_render(&dsc));
out:
	unicrash_free(uc);
	return error;
}

/* Check directory connectivity. */
int
phase5_func(
	struct scrub_ctx	*ctx)
{
	bool			aborted = false;
	int			ret;

	if (ctx->corruptions_found || ctx->unfixable_errors) {
		str_info(ctx, ctx->mntpoint,
_("Filesystem has errors, skipping connectivity checks."));
		return 0;
	}

	ret = check_fs_label(ctx);
	if (ret)
		return ret;

	ret = scrub_scan_all_inodes(ctx, check_inode_names, &aborted);
	if (ret)
		return ret;
	if (aborted)
		return ECANCELED;

	scrub_report_preen_triggers(ctx);
	return 0;
}

/* Estimate how much work we're going to do. */
int
phase5_estimate(
	struct scrub_ctx	*ctx,
	uint64_t		*items,
	unsigned int		*nr_threads,
	int			*rshift)
{
	*items = ctx->mnt_sv.f_files - ctx->mnt_sv.f_ffree;
	*nr_threads = scrub_nproc(ctx);
	*rshift = 0;
	return 0;
}
