// SPDX-License-Identifier: GPL-2.0
/*
 * Copyright (c) 2005 Silicon Graphics, Inc.
 * All Rights Reserved.
 */
#include <stdbool.h>
#include "command.h"
#include "init.h"
#include "quota.h"

static cmdinfo_t off_cmd;
static cmdinfo_t state_cmd;
static cmdinfo_t enable_cmd;
static cmdinfo_t disable_cmd;
static cmdinfo_t remove_cmd;

static void
off_help(void)
{
	printf(_(
"\n"
" turn filesystem quota off, both accounting and enforcement\n"
"\n"
" Example:\n"
" 'off -uv'  (switch off user quota on the current filesystem)\n"
" This command is the equivalent of the traditional quotaoff command,\n"
" which disables quota completely on a mounted filesystem.\n"
" Note that there is no 'on' command - for XFS filesystems (with the\n"
" exception of the root filesystem on IRIX) quota can only be enabled\n"
" at mount time, through the use of one of the quota mount options.\n"
"\n"
" The state command is useful for displaying the current state.  Using\n"
" the -v (verbose) option with the 'off' command will display the quota\n"
" state for the affected filesystem once the operation is complete.\n"
" The affected quota type is -g (groups), -p (projects) or -u (users)\n"
" and defaults to user quota (multiple types can be specified).\n"
"\n"));
}

static void
state_help(void)
{
	printf(_(
"\n"
" query the state of quota on the current filesystem\n"
"\n"
" This is a verbose status command, reporting whether or not accounting\n"
" and/or enforcement are enabled for a filesystem, which inodes are in\n"
" use as the quota state inodes, and how many extents and blocks are\n"
" presently being used to hold that information.\n"
" The quota type is specified via -g (groups), -p (projects) or -u (users)\n"
" and defaults to user quota (multiple types can be specified).\n"
"\n"));
}

static void
enable_help(void)
{
	printf(_(
"\n"
" enable quota enforcement on a filesystem\n"
"\n"
" If a filesystem is mounted and has quota accounting enabled, but not\n"
" quota enforcement, enforcement can be enabled with this command.\n"
" With the -v (verbose) option, the status of the filesystem will be\n"
" reported after the operation is complete.\n"
" The affected quota type is -g (groups), -p (projects) or -u (users)\n"
" and defaults to user quota (multiple types can be specified).\n"
"\n"));
}

static void
disable_help(void)
{
	printf(_(
"\n"
" disable quota enforcement on a filesystem\n"
"\n"
" If a filesystem is mounted and is currently enforcing quota, this\n"
" provides a mechanism to switch off the enforcement, but continue to\n"
" perform used space (and used inodes) accounting.\n"
" The affected quota type is -g (groups), -p (projects) or -u (users).\n"
"\n"));
}

static void
remove_help(void)
{
	printf(_(
"\n"
" remove any space being used by the quota subsystem\n"
"\n"
" Once quota has been switched 'off' on a filesystem, the space that\n"
" was allocated to holding quota metadata can be freed via this command.\n"
" The affected quota type is -g (groups), -p (projects) or -u (users)\n"
" and defaults to user quota (multiple types can be specified).\n"
"\n"));
}

static void
state_qfilestat(
	FILE			*fp,
	struct fs_path		*mount,
	uint			type,
	struct fs_qfilestatv	*qfs,
	int			accounting,
	int			enforcing)
{
	fprintf(fp, _("%s quota state on %s (%s)\n"), type_to_string(type),
		mount->fs_dir, mount->fs_name);
	fprintf(fp, _("  Accounting: %s\n"), accounting ? _("ON") : _("OFF"));
	fprintf(fp, _("  Enforcement: %s\n"), enforcing ? _("ON") : _("OFF"));
	if (qfs->qfs_ino != (__u64) -1)
		fprintf(fp, _("  Inode: #%llu (%llu blocks, %lu extents)\n"),
			(unsigned long long)qfs->qfs_ino,
			(unsigned long long)qfs->qfs_nblks,
			(unsigned long)qfs->qfs_nextents);
	else
		fprintf(fp, _("  Inode: N/A\n"));
}

static void
state_timelimit(
	FILE		*fp,
	uint		form,
	uint32_t	timelimit)
{
	fprintf(fp, _("%s grace time: %s\n"),
		form_to_string(form),
		time_to_string(timelimit, VERBOSE_FLAG | ABSOLUTE_FLAG));
}

static void
state_warnlimit(
	FILE		*fp,
	uint		form,
	uint16_t	warnlimit)
{
	fprintf(fp, _("%s max warnings: %u\n"),
		form_to_string(form), warnlimit);
}

/*
 * fs_quota_stat holds a subset of fs_quota_statv; this copies
 * the smaller into the larger, leaving any not-present fields
 * empty.  This is so the same reporting function can be used
 * for both XFS_GETQSTAT and XFS_GETQSTATV results.
 */
static void
state_stat_to_statv(
	struct fs_quota_stat	*s,
	struct fs_quota_statv	*sv)
{
	memset(sv, 0, sizeof(struct fs_quota_statv));

	/* shared information */
	sv->qs_version = s->qs_version;
	sv->qs_flags = s->qs_flags;
	sv->qs_incoredqs = s->qs_incoredqs;
	sv->qs_btimelimit = s->qs_btimelimit;
	sv->qs_itimelimit = s->qs_itimelimit;
	sv->qs_rtbtimelimit = s->qs_rtbtimelimit;
	sv->qs_bwarnlimit = s->qs_bwarnlimit;
	sv->qs_iwarnlimit = s->qs_iwarnlimit;

	/* Always room for uquota */
	sv->qs_uquota.qfs_ino = s->qs_uquota.qfs_ino;
	sv->qs_uquota.qfs_nblks = s->qs_uquota.qfs_nblks;
	sv->qs_uquota.qfs_nextents = s->qs_uquota.qfs_nextents;

	/*
	 * If we are here, XFS_GETQSTATV failed and XFS_GETQSTAT passed;
	 * that is a very strong hint that we're on a kernel which predates
	 * the on-disk pquota inode; both were added in v3.12.  So, we do
	 * some tricksy determination here.
	 * gs_gquota may hold either group quota inode info, or project
	 * quota if that is used instead; which one it actually holds depends
	 * on the quota flags.  (If neither is set, neither is used)
	 */
	if (s->qs_flags & XFS_QUOTA_GDQ_ACCT) {
		/* gs_gquota holds group quota info */
		sv->qs_gquota.qfs_ino = s->qs_gquota.qfs_ino;
		sv->qs_gquota.qfs_nblks = s->qs_gquota.qfs_nblks;
		sv->qs_gquota.qfs_nextents = s->qs_gquota.qfs_nextents;
	} else if (s->qs_flags & XFS_QUOTA_PDQ_ACCT) {
		/* gs_gquota actually holds project quota info */
		sv->qs_pquota.qfs_ino = s->qs_gquota.qfs_ino;
		sv->qs_pquota.qfs_nblks = s->qs_gquota.qfs_nblks;
		sv->qs_pquota.qfs_nextents = s->qs_gquota.qfs_nextents;
	}
}

static void
state_quotafile_stat(
	FILE			*fp,
	uint			type,
	struct fs_path          *mount,
	struct fs_quota_statv	*sv,
	struct fs_quota_stat	*s,
	uint			flags)
{
	bool			accounting, enforcing;
	struct fs_qfilestatv	*qsv;
	char			*dev = mount->fs_name;

	if (xfsquotactl(XFS_GETQSTATV, dev, type, 0, (void *)sv) < 0) {
		if (xfsquotactl(XFS_GETQSTAT, dev, type, 0, (void *)s) < 0) {
			if (flags & VERBOSE_FLAG)
				fprintf(fp,
					_("%s quota are not enabled on %s\n"),
					type_to_string(type), dev);
			return;
		}
		state_stat_to_statv(s, sv);
	}

	switch(type) {
	case XFS_USER_QUOTA:
		qsv = &sv->qs_uquota;
		accounting = sv->qs_flags & XFS_QUOTA_UDQ_ACCT;
		enforcing = sv->qs_flags & XFS_QUOTA_UDQ_ENFD;
		break;
	case XFS_GROUP_QUOTA:
		qsv = &sv->qs_gquota;
		accounting = sv->qs_flags & XFS_QUOTA_GDQ_ACCT;
		enforcing = sv->qs_flags & XFS_QUOTA_GDQ_ENFD;
		break;
	case XFS_PROJ_QUOTA:
		qsv = &sv->qs_pquota;
		accounting = sv->qs_flags & XFS_QUOTA_PDQ_ACCT;
		enforcing = sv->qs_flags & XFS_QUOTA_PDQ_ENFD;
		break;
	default:
		return;
	}


	state_qfilestat(fp, mount, type, qsv, accounting, enforcing);

	state_timelimit(fp, XFS_BLOCK_QUOTA, sv->qs_btimelimit);
	state_warnlimit(fp, XFS_BLOCK_QUOTA, sv->qs_bwarnlimit);

	state_timelimit(fp, XFS_INODE_QUOTA, sv->qs_itimelimit);
	state_warnlimit(fp, XFS_INODE_QUOTA, sv->qs_iwarnlimit);

	state_timelimit(fp, XFS_RTBLOCK_QUOTA, sv->qs_rtbtimelimit);
}

static void
state_quotafile_mount(
	FILE			*fp,
	uint			type,
	struct fs_path		*mount,
	uint			flags)
{
	struct fs_quota_stat	s;
	struct fs_quota_statv	sv;

	sv.qs_version = FS_QSTATV_VERSION1;

	if (type & XFS_USER_QUOTA) {
		state_quotafile_stat(fp, XFS_USER_QUOTA, mount,
				     &sv, &s, flags);
	}

	if (type & XFS_GROUP_QUOTA) {
		state_quotafile_stat(fp, XFS_GROUP_QUOTA, mount,
				     &sv, &s, flags);
	}

	if (type & XFS_PROJ_QUOTA) {
		state_quotafile_stat(fp, XFS_PROJ_QUOTA, mount,
				     &sv, &s, flags);
	}
}

static void
state_quotafile(
	FILE		*fp,
	uint		type,
	char		*dir,
	uint		flags)
{
	fs_cursor_t	cursor;
	fs_path_t	*mount;

	fs_cursor_initialise(dir, FS_MOUNT_POINT, &cursor);
	while ((mount = fs_cursor_next_entry(&cursor)))
		state_quotafile_mount(fp, type, mount, flags);
}

static int
state_f(
	int		argc,
	char		**argv)
{
	FILE		*fp = NULL;
	char		*fname = NULL;
	int		c, flags = 0, type = 0;

	while ((c = getopt(argc, argv, "af:gpuv")) != EOF) {
		switch (c) {
		case 'a':
			flags |= ALL_MOUNTS_FLAG;
			break;
		case 'f':
			fname = optarg;
			break;
		case 'g':
			type |= XFS_GROUP_QUOTA;
			break;
		case 'p':
			type |= XFS_PROJ_QUOTA;
			break;
		case 'u':
			type |= XFS_USER_QUOTA;
			break;
		case 'v':
			flags |= VERBOSE_FLAG;
			break;
		default:
			return command_usage(&state_cmd);
		}
	}

	if (argc != optind)
		return command_usage(&state_cmd);

	if ((fp = fopen_write_secure(fname)) == NULL)
		return 0;

	if (!type)
		type = XFS_USER_QUOTA | XFS_GROUP_QUOTA | XFS_PROJ_QUOTA;

	if (flags & ALL_MOUNTS_FLAG)
		state_quotafile(fp, type, NULL, flags);
	else if (fs_path && fs_path->fs_flags & FS_MOUNT_POINT)
		state_quotafile(fp, type, fs_path->fs_dir, flags);

	if (fname)
		fclose(fp);
	return 0;
}

static void
enable_enforcement(
	char		*dir,
	uint		type,
	uint		qflags,
	uint		flags)
{
	fs_path_t	*mount;

	mount = fs_table_lookup(dir, FS_MOUNT_POINT);
	if (!mount) {
		exitcode = 1;
		fprintf(stderr, "%s: unknown mount point %s\n", progname, dir);
		return;
	}
	dir = mount->fs_name;
	if (xfsquotactl(XFS_QUOTAON, dir, type, 0, (void *)&qflags) < 0) {
		if (errno == EEXIST)
			fprintf(stderr,
				_("Quota enforcement already enabled.\n"));
		else if (errno == EINVAL || errno == ENOSYS)
			fprintf(stderr,
				_("Can't enable enforcement when quota off.\n"));
		else
			perror("XFS_QUOTAON");
	}
	else if (flags & VERBOSE_FLAG)
		state_quotafile_mount(stdout, type, mount, flags);
}

static void
disable_enforcement(
	char		*dir,
	uint		type,
	uint		qflags,
	uint		flags)
{
	fs_path_t	*mount;

	mount = fs_table_lookup(dir, FS_MOUNT_POINT);
	if (!mount) {
		exitcode = 1;
		fprintf(stderr, "%s: unknown mount point %s\n", progname, dir);
		return;
	}
	dir = mount->fs_name;
	if (xfsquotactl(XFS_QUOTAOFF, dir, type, 0, (void *)&qflags) < 0) {
		if (errno == EEXIST)
			fprintf(stderr,
				_("Quota enforcement already disabled.\n"));
		else if (errno == EINVAL || errno == ENOSYS)
			fprintf(stderr,
				_("Can't disable enforcement when quota off.\n"));
		else
			perror("XFS_QUOTAOFF");
	}
	else if (flags & VERBOSE_FLAG)
		state_quotafile_mount(stdout, type, mount, flags);
}

static void
quotaoff(
	char		*dir,
	uint		type,
	uint		qflags,
	uint		flags)
{
	fs_path_t	*mount;

	mount = fs_table_lookup(dir, FS_MOUNT_POINT);
	if (!mount) {
		exitcode = 1;
		fprintf(stderr, "%s: unknown mount point %s\n", progname, dir);
		return;
	}
	dir = mount->fs_name;
	if (xfsquotactl(XFS_QUOTAOFF, dir, type, 0, (void *)&qflags) < 0) {
		if (errno == EEXIST || errno == ENOSYS)
			fprintf(stderr, _("Quota already off.\n"));
		else
			perror("XFS_QUOTAOFF");
	}
	else if (flags & VERBOSE_FLAG)
		state_quotafile_mount(stdout, type, mount, flags);
}

static int
remove_qtype_extents(
	char		*dir,
	uint		type)
{
	int	error = 0;

	if ((error = xfsquotactl(XFS_QUOTARM, dir, type, 0, (void *)&type)) < 0)
		perror("XFS_QUOTARM");
	return error;
}

static void
remove_extents(
	char		*dir,
	uint		type,
	uint		flags)
{
	fs_path_t	*mount;

	mount = fs_table_lookup(dir, FS_MOUNT_POINT);
	if (!mount) {
		exitcode = 1;
		fprintf(stderr, "%s: unknown mount point %s\n", progname, dir);
		return;
	}
	dir = mount->fs_name;
	if (type & XFS_USER_QUOTA) {
		if (remove_qtype_extents(dir, XFS_USER_QUOTA) < 0)
			return;
	}
	if (type & XFS_GROUP_QUOTA) {
		if (remove_qtype_extents(dir, XFS_GROUP_QUOTA) < 0)
			return;
	}
	if (type & XFS_PROJ_QUOTA) {
		if (remove_qtype_extents(dir, XFS_PROJ_QUOTA) < 0)
			return;
	}
	if (flags & VERBOSE_FLAG)
		state_quotafile_mount(stdout, type, mount, flags);
}

static int
enable_f(
	int		argc,
	char		**argv)
{
	int		c, flags = 0, qflags = 0, type = 0;

	while ((c = getopt(argc, argv, "gpuv")) != EOF) {
		switch (c) {
		case 'g':
			type |= XFS_GROUP_QUOTA;
			qflags |= XFS_QUOTA_GDQ_ACCT | XFS_QUOTA_GDQ_ENFD;
			break;
		case 'p':
			type |= XFS_PROJ_QUOTA;
			qflags |= XFS_QUOTA_PDQ_ACCT | XFS_QUOTA_PDQ_ENFD;
			break;
		case 'u':
			type |= XFS_USER_QUOTA;
			qflags |= XFS_QUOTA_UDQ_ACCT | XFS_QUOTA_UDQ_ENFD;
			break;
		case 'v':
			flags |= VERBOSE_FLAG;
			break;
		default:
			return command_usage(&enable_cmd);
		}
	}

	if (argc != optind)
		return command_usage(&enable_cmd);

	if (!type) {
		type |= XFS_USER_QUOTA;
		qflags |= XFS_QUOTA_UDQ_ACCT | XFS_QUOTA_UDQ_ENFD;
	}

	if (fs_path->fs_flags & FS_MOUNT_POINT)
		enable_enforcement(fs_path->fs_dir, type, qflags, flags);
	return 0;
}

static int
disable_f(
	int		argc,
	char		**argv)
{
	int		c, flags = 0, qflags = 0, type = 0;

	while ((c = getopt(argc, argv, "gpuv")) != EOF) {
		switch (c) {
		case 'g':
			type |= XFS_GROUP_QUOTA;
			qflags |= XFS_QUOTA_GDQ_ENFD;
			break;
		case 'p':
			type |= XFS_PROJ_QUOTA;
			qflags |= XFS_QUOTA_PDQ_ENFD;
			break;
		case 'u':
			type |= XFS_USER_QUOTA;
			qflags |= XFS_QUOTA_UDQ_ENFD;
			break;
		case 'v':
			flags |= VERBOSE_FLAG;
			break;
		default:
			return command_usage(&disable_cmd);
		}
	}

	if (argc != optind)
		return command_usage(&disable_cmd);

	if (!type) {
		type |= XFS_USER_QUOTA;
		qflags |= XFS_QUOTA_UDQ_ENFD;
	}

	if (fs_path->fs_flags & FS_MOUNT_POINT)
		disable_enforcement(fs_path->fs_dir, type, qflags, flags);
	return 0;
}

static int
off_f(
	int		argc,
	char		**argv)
{
	int		c, flags = 0, qflags = 0, type = 0;

	while ((c = getopt(argc, argv, "gpuv")) != EOF) {
		switch (c) {
		case 'g':
			type |= XFS_GROUP_QUOTA;
			qflags |= XFS_QUOTA_GDQ_ACCT | XFS_QUOTA_GDQ_ENFD;
			break;
		case 'p':
			type |= XFS_PROJ_QUOTA;
			qflags |= XFS_QUOTA_PDQ_ACCT | XFS_QUOTA_PDQ_ENFD;
			break;
		case 'u':
			type |= XFS_USER_QUOTA;
			qflags |= XFS_QUOTA_UDQ_ACCT | XFS_QUOTA_UDQ_ENFD;
			break;
		case 'v':
			flags |= VERBOSE_FLAG;
			break;
		default:
			return command_usage(&off_cmd);
		}
	}

	if (argc != optind)
		return command_usage(&off_cmd);

	if (!type) {
		type |= XFS_USER_QUOTA;
		qflags |= XFS_QUOTA_UDQ_ACCT | XFS_QUOTA_UDQ_ENFD;
	}

	if (fs_path->fs_flags & FS_MOUNT_POINT)
		quotaoff(fs_path->fs_dir, type, qflags, flags);
	return 0;
}

static int
remove_f(
	int		argc,
	char		**argv)
{
	int		c, flags = 0, type = 0;

	while ((c = getopt(argc, argv, "gpuv")) != EOF) {
		switch (c) {
		case 'g':
			type |= XFS_GROUP_QUOTA;
			break;
		case 'p':
			type |= XFS_PROJ_QUOTA;
			break;
		case 'u':
			type |= XFS_USER_QUOTA;
			break;
		case 'v':
			flags |= VERBOSE_FLAG;
			break;
		default:
			return command_usage(&remove_cmd);
		}
	}

	if (argc != optind)
		return command_usage(&remove_cmd);

	if (!type) {
		type |= XFS_USER_QUOTA;
	}

	if (fs_path->fs_flags & FS_MOUNT_POINT)
		remove_extents(fs_path->fs_dir, type, flags);
	return 0;
}

void
state_init(void)
{
	off_cmd.name = "off";
	off_cmd.cfunc = off_f;
	off_cmd.argmin = 0;
	off_cmd.argmax = -1;
	off_cmd.args = _("[-gpu] [-v]");
	off_cmd.oneline = _("permanently switch quota off for a path");
	off_cmd.help = off_help;

	state_cmd.name = "state";
	state_cmd.cfunc = state_f;
	state_cmd.argmin = 0;
	state_cmd.argmax = -1;
	state_cmd.args = _("[-gpu] [-a] [-v] [-f file]");
	state_cmd.oneline = _("get overall quota state information");
	state_cmd.help = state_help;
	state_cmd.flags = CMD_FLAG_FOREIGN_OK;

	enable_cmd.name = "enable";
	enable_cmd.cfunc = enable_f;
	enable_cmd.argmin = 0;
	enable_cmd.argmax = -1;
	enable_cmd.args = _("[-gpu] [-v]");
	enable_cmd.oneline = _("enable quota enforcement");
	enable_cmd.help = enable_help;

	disable_cmd.name = "disable";
	disable_cmd.cfunc = disable_f;
	disable_cmd.argmin = 0;
	disable_cmd.argmax = -1;
	disable_cmd.args = _("[-gpu] [-v]");
	disable_cmd.oneline = _("disable quota enforcement");
	disable_cmd.help = disable_help;

	remove_cmd.name = "remove";
	remove_cmd.cfunc = remove_f;
	remove_cmd.argmin = 0;
	remove_cmd.argmax = -1;
	remove_cmd.args = _("[-gpu] [-v]");
	remove_cmd.oneline = _("remove quota extents from a filesystem");
	remove_cmd.help = remove_help;

	if (expert) {
		add_command(&off_cmd);
		add_command(&state_cmd);
		add_command(&enable_cmd);
		add_command(&disable_cmd);
		add_command(&remove_cmd);
	}
}
