// SPDX-License-Identifier: GPL-2.0
/*
 * Copyright (C) 2022-2023 Oracle.  All Rights Reserved.
 * Author: Darrick J. Wong <djwong@kernel.org>
 */
#include "libxfs.h"
#include "command.h"
#include "output.h"
#include "init.h"

static xfs_filblks_t
count_rtblocks(
	struct xfs_inode	*ip)
{
	struct xfs_iext_cursor	icur;
	struct xfs_bmbt_irec	got;
	xfs_filblks_t		count = 0;
	struct xfs_ifork	*ifp = xfs_ifork_ptr(ip, XFS_DATA_FORK);
	int			error;

	error = -libxfs_iread_extents(NULL, ip, XFS_DATA_FORK);
	if (error) {
		dbprintf(
_("could not read AG %u agino %u extents, err=%d\n"),
				XFS_INO_TO_AGNO(ip->i_mount, ip->i_ino),
				XFS_INO_TO_AGINO(ip->i_mount, ip->i_ino),
				error);
		return 0;
	}

	for_each_xfs_iext(ifp, &icur, &got)
		if (!isnullstartblock(got.br_startblock))
			count += got.br_blockcount;
	return count;
}

static xfs_agino_t
get_next_unlinked(
	xfs_agnumber_t		agno,
	xfs_agino_t		agino,
	bool			verbose)
{
	struct xfs_buf		*ino_bp;
	struct xfs_dinode	*dip;
	struct xfs_inode	*ip;
	xfs_ino_t		ino;
	xfs_agino_t		ret;
	int			error;

	ino = XFS_AGINO_TO_INO(mp, agno, agino);
	error = -libxfs_iget(mp, NULL, ino, 0, &ip);
	if (error)
		goto bad;

	if (verbose) {
		xfs_filblks_t	blocks, rtblks = 0;

		if (XFS_IS_REALTIME_INODE(ip))
			rtblks = count_rtblocks(ip);
		blocks = ip->i_nblocks - rtblks;

		dbprintf(_(" blocks %llu rtblocks %llu\n"),
				blocks, rtblks);
	} else {
		dbprintf("\n");
	}

	error = -libxfs_imap_to_bp(mp, NULL, &ip->i_imap, &ino_bp);
	if (error)
		goto bad;

	dip = xfs_buf_offset(ino_bp, ip->i_imap.im_boffset);
	ret = be32_to_cpu(dip->di_next_unlinked);
	libxfs_buf_relse(ino_bp);

	return ret;
bad:
	dbprintf(_("AG %u agino %u: %s\n"), agno, agino, strerror(error));
	return NULLAGINO;
}

static void
dump_unlinked_bucket(
	xfs_agnumber_t	agno,
	struct xfs_buf	*agi_bp,
	unsigned int	bucket,
	bool		quiet,
	bool		verbose)
{
	struct xfs_agi	*agi = agi_bp->b_addr;
	xfs_agino_t	agino;
	unsigned int	i = 0;

	agino = be32_to_cpu(agi->agi_unlinked[bucket]);
	if (agino != NULLAGINO)
		dbprintf(_("AG %u bucket %u agino %u"), agno, bucket, agino);
	else if (!quiet && agino == NULLAGINO)
		dbprintf(_("AG %u bucket %u agino NULL\n"), agno, bucket);

	while (agino != NULLAGINO) {
		agino = get_next_unlinked(agno, agino, verbose);
		if (agino != NULLAGINO)
			dbprintf(_("    [%u] agino %u"), i++, agino);
		else if (!quiet && agino == NULLAGINO)
			dbprintf(_("    [%u] agino NULL\n"), i++);
	}
}

static void
dump_unlinked(
	struct xfs_perag	*pag,
	unsigned int		bucket,
	bool			quiet,
	bool			verbose)
{
	struct xfs_buf		*agi_bp;
	xfs_agnumber_t		agno = pag->pag_agno;
	int			error;

	error = -libxfs_ialloc_read_agi(pag, NULL, 0, &agi_bp);
	if (error) {
		dbprintf(_("AGI %u: %s\n"), agno, strerror(errno));
		return;
	}

	if (bucket != -1U) {
		dump_unlinked_bucket(agno, agi_bp, bucket, quiet, verbose);
		goto relse;
	}

	for (bucket = 0; bucket < XFS_AGI_UNLINKED_BUCKETS; bucket++) {
		dump_unlinked_bucket(agno, agi_bp, bucket, quiet, verbose);
	}

relse:
	libxfs_buf_relse(agi_bp);
}

static int
dump_iunlinked_f(
	int			argc,
	char			**argv)
{
	struct xfs_perag	*pag;
	xfs_agnumber_t		agno = NULLAGNUMBER;
	unsigned int		bucket = -1U;
	bool			quiet = false;
	bool			verbose = false;
	int			c;

	while ((c = getopt(argc, argv, "a:b:qv")) != EOF) {
		switch (c) {
		case 'a':
			agno = atoi(optarg);
			if (agno >= mp->m_sb.sb_agcount) {
				dbprintf(_("Unknown AG %u, agcount is %u.\n"),
						agno, mp->m_sb.sb_agcount);
				return 0;
			}
			break;
		case 'b':
			bucket = atoi(optarg);
			if (bucket >= XFS_AGI_UNLINKED_BUCKETS) {
				dbprintf(_("Unknown bucket %u, max is 63.\n"),
						bucket);
				return 0;
			}
			break;
		case 'q':
			quiet = true;
			break;
		case 'v':
			verbose = true;
			break;
		default:
			dbprintf(_("Bad option for dump_iunlinked command.\n"));
			return 0;
		}
	}

	if (agno != NULLAGNUMBER) {
		struct xfs_perag	*pag = libxfs_perag_get(mp, agno);

		dump_unlinked(pag, bucket, quiet, verbose);
		libxfs_perag_put(pag);
		return 0;
	}

	for_each_perag(mp, agno, pag)
		dump_unlinked(pag, bucket, quiet, verbose);

	return 0;
}

static const cmdinfo_t	dump_iunlinked_cmd =
	{ "dump_iunlinked", NULL, dump_iunlinked_f, 0, -1, 0,
	  N_("[-a agno] [-b bucket] [-q] [-v]"),
	  N_("dump chain of unlinked inode buckets"), NULL };

/*
 * Look up the inode cluster buffer and log the on-disk unlinked inode change
 * we need to make.
 */
static int
iunlink_log_dinode(
	struct xfs_trans	*tp,
	struct xfs_inode	*ip,
	struct xfs_perag	*pag,
	xfs_agino_t		next_agino)
{
	struct xfs_mount	*mp = tp->t_mountp;
	struct xfs_dinode	*dip;
	struct xfs_buf		*ibp;
	int			offset;
	int			error;

	error = -libxfs_imap_to_bp(mp, tp, &ip->i_imap, &ibp);
	if (error)
		return error;

	dip = xfs_buf_offset(ibp, ip->i_imap.im_boffset);

	dip->di_next_unlinked = cpu_to_be32(next_agino);
	offset = ip->i_imap.im_boffset +
			offsetof(struct xfs_dinode, di_next_unlinked);

	libxfs_dinode_calc_crc(mp, dip);
	libxfs_trans_log_buf(tp, ibp, offset, offset + sizeof(xfs_agino_t) - 1);
	return 0;
}

static int
iunlink_insert_inode(
	struct xfs_trans	*tp,
	struct xfs_perag	*pag,
	struct xfs_buf		*agibp,
	struct xfs_inode	*ip)
{
	struct xfs_mount	*mp = tp->t_mountp;
	struct xfs_agi		*agi = agibp->b_addr;
	xfs_agino_t		next_agino;
	xfs_agino_t		agino = XFS_INO_TO_AGINO(mp, ip->i_ino);
	short			bucket_index = agino % XFS_AGI_UNLINKED_BUCKETS;
	int			offset;
	int			error;

	/*
	 * Get the index into the agi hash table for the list this inode will
	 * go on.  Make sure the pointer isn't garbage and that this inode
	 * isn't already on the list.
	 */
	next_agino = be32_to_cpu(agi->agi_unlinked[bucket_index]);
	if (next_agino == agino || !xfs_verify_agino_or_null(pag, next_agino))
		return EFSCORRUPTED;

	if (next_agino != NULLAGINO) {
		/*
		 * There is already another inode in the bucket, so point this
		 * inode to the current head of the list.
		 */
		error = iunlink_log_dinode(tp, ip, pag, next_agino);
		if (error)
			return error;
	}

	/* Update the bucket. */
	agi->agi_unlinked[bucket_index] = cpu_to_be32(agino);
	offset = offsetof(struct xfs_agi, agi_unlinked) +
			(sizeof(xfs_agino_t) * bucket_index);
	libxfs_trans_log_buf(tp, agibp, offset,
			offset + sizeof(xfs_agino_t) - 1);
	return 0;
}

/*
 * This is called when the inode's link count has gone to 0 or we are creating
 * a tmpfile via O_TMPFILE.  The inode @ip must have nlink == 0.
 *
 * We place the on-disk inode on a list in the AGI.  It will be pulled from this
 * list when the inode is freed.
 */
static int
iunlink(
	struct xfs_trans	*tp,
	struct xfs_inode	*ip)
{
	struct xfs_mount	*mp = tp->t_mountp;
	struct xfs_perag	*pag;
	struct xfs_buf		*agibp;
	int			error;

	ASSERT(VFS_I(ip)->i_nlink == 0);
	ASSERT(VFS_I(ip)->i_mode != 0);

	pag = libxfs_perag_get(mp, XFS_INO_TO_AGNO(mp, ip->i_ino));

	/* Get the agi buffer first.  It ensures lock ordering on the list. */
	error = -libxfs_read_agi(pag, tp, 0, &agibp);
	if (error)
		goto out;

	error = iunlink_insert_inode(tp, pag, agibp, ip);
out:
	libxfs_perag_put(pag);
	return error;
}

static int
create_unlinked(
	struct xfs_mount	*mp)
{
	struct cred		cr = { };
	struct fsxattr		fsx = { };
	struct xfs_inode	*ip;
	struct xfs_trans	*tp;
	unsigned int		resblks;
	int			error;

	resblks = XFS_IALLOC_SPACE_RES(mp);
	error = -libxfs_trans_alloc(mp, &M_RES(mp)->tr_create_tmpfile, resblks,
			0, 0, &tp);
	if (error) {
		dbprintf(_("alloc trans: %s\n"), strerror(error));
		return error;
	}

	error = -libxfs_dir_ialloc(&tp, NULL, S_IFREG | 0600, 0, 0, &cr, &fsx,
			&ip);
	if (error) {
		dbprintf(_("create inode: %s\n"), strerror(error));
		goto out_cancel;
	}

	error = iunlink(tp, ip);
	if (error) {
		dbprintf(_("unlink inode: %s\n"), strerror(error));
		goto out_rele;
	}

	error = -libxfs_trans_commit(tp);
	if (error)
		dbprintf(_("commit inode: %s\n"), strerror(error));

	dbprintf(_("Created unlinked inode %llu in agno %u\n"),
			(unsigned long long)ip->i_ino,
			XFS_INO_TO_AGNO(mp, ip->i_ino));
	libxfs_irele(ip);
	return error;
out_rele:
	libxfs_irele(ip);
out_cancel:
	libxfs_trans_cancel(tp);
	return error;
}

static int
iunlink_f(
	int		argc,
	char		**argv)
{
	int		nr = 1;
	int		c;
	int		error;

	while ((c = getopt(argc, argv, "n:")) != EOF) {
		switch (c) {
		case 'n':
			nr = atoi(optarg);
			if (nr <= 0) {
				dbprintf(_("%s: need positive number\n"));
				return 0;
			}
			break;
		default:
			dbprintf(_("Bad option for iunlink command.\n"));
			return 0;
		}
	}

	for (c = 0; c < nr; c++) {
		error = create_unlinked(mp);
		if (error)
			return 1;
	}

	return 0;
}

static const cmdinfo_t	iunlink_cmd =
	{ "iunlink", NULL, iunlink_f, 0, -1, 0,
	  N_("[-n nr]"),
	  N_("allocate inodes and put them on the unlinked list"), NULL };

void
iunlink_init(void)
{
	add_command(&dump_iunlinked_cmd);
	if (expert_mode)
		add_command(&iunlink_cmd);
}
