// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * Copyright (c) 2017-2024 Oracle.  All Rights Reserved.
 * Author: Darrick J. Wong <djwong@kernel.org>
 */
#include "platform_defs.h"
#include "xfs.h"
#include "xfs_arch.h"
#include "list.h"
#include "paths.h"
#include "handle.h"
#include "libfrog/getparents.h"

/* Allocate a buffer for the xfs_getparent_rec array. */
static void *
alloc_records(
	struct xfs_getparents	*gp,
	size_t			bufsize)
{
	void			*buf;

	if (bufsize >= UINT32_MAX) {
		errno = ENOMEM;
		return NULL;
	} else if (!bufsize) {
		bufsize = XFS_XATTR_LIST_MAX;
	}

	buf = malloc(bufsize);
	if (!buf)
		return NULL;

	gp->gp_buffer = (uintptr_t)buf;
	gp->gp_bufsize = bufsize;
	return buf;
}

/* Copy a file handle. */
static inline void
copy_handle(
	struct xfs_handle	*dest,
	const struct xfs_handle	*src)
{
	memcpy(dest, src, sizeof(struct xfs_handle));
}

/* Initiate a callback for each parent pointer. */
static int
walk_parent_records(
	struct xfs_getparents	*gp,
	walk_parent_fn		fn,
	void			*arg)
{
	struct xfs_getparents_rec *gpr;
	int			ret;

	if (gp->gp_oflags & XFS_GETPARENTS_OFLAG_ROOT) {
		struct parent_rec	rec = {
			.p_flags	= PARENTREC_FILE_IS_ROOT,
		};

		return fn(&rec, arg);
	}

	for (gpr = xfs_getparents_first_rec(gp);
	     gpr != NULL;
	     gpr = xfs_getparents_next_rec(gp, gpr)) {
		struct parent_rec	rec = { };

		if (gpr->gpr_name[0] == 0)
			break;

		copy_handle(&rec.p_handle, &gpr->gpr_parent);
		rec.p_name = gpr->gpr_name;

		ret = fn(&rec, arg);
		if (ret)
			return ret;
	}

	return 0;
}

/* Walk all parent pointers of this fd.  Returns 0 or positive errno. */
int
fd_walk_parents(
	int			fd,
	size_t			bufsize,
	walk_parent_fn		fn,
	void			*arg)
{
	struct xfs_getparents	gp = { };
	void			*buf;
	int			ret;

	buf = alloc_records(&gp, bufsize);
	if (!buf)
		return errno;

	while ((ret = ioctl(fd, XFS_IOC_GETPARENTS, &gp)) == 0) {
		ret = walk_parent_records(&gp, fn, arg);
		if (ret)
			goto out_buf;
		if (gp.gp_oflags & XFS_GETPARENTS_OFLAG_DONE)
			break;
	}
	if (ret)
		ret = errno;

out_buf:
	free(buf);
	return ret;
}

/*
 * Walk all parent pointers of this handle using the given fd to query the
 * filesystem.  Returns 0 or positive errno.
 */
static int
handle_walk_parents_fd(
	int			fd,
	const void		*hanp,
	size_t			hlen,
	size_t			bufsize,
	walk_parent_fn		fn,
	void			*arg)
{
	struct xfs_getparents_by_handle	gph = { };
	void			*buf;
	int			ret;

	if (hlen != sizeof(struct xfs_handle))
		return EINVAL;

	buf = alloc_records(&gph.gph_request, bufsize);
	if (!buf)
		return errno;

	copy_handle(&gph.gph_handle, hanp);
	while ((ret = ioctl(fd, XFS_IOC_GETPARENTS_BY_HANDLE, &gph)) == 0) {
		ret = walk_parent_records(&gph.gph_request, fn, arg);
		if (ret)
			goto out_buf;
		if (gph.gph_request.gp_oflags & XFS_GETPARENTS_OFLAG_DONE)
			break;
	}
	if (ret)
		ret = errno;

out_buf:
	free(buf);
	return ret;
}

/* Walk all parent pointers of this handle.  Returns 0 or positive errno. */
int
handle_walk_parents(
	const void		*hanp,
	size_t			hlen,
	size_t			bufsize,
	walk_parent_fn		fn,
	void			*arg)
{
	char			*mntpt;
	int			fd;

	/*
	 * This function doesn't modify the handle, but we don't want to have
	 * to bump the libhandle major version just to change that.
	 */
	fd = handle_to_fsfd((void *)hanp, &mntpt);
	if (fd < 0)
		return errno;

	return handle_walk_parents_fd(fd, hanp, hlen, bufsize, fn, arg);
}

struct walk_ppaths_info {
	/* Callback */
	walk_path_fn		fn;
	void			*arg;

	/* Mountpoint of this filesystem. */
	char			*mntpt;

	/* Path that we're constructing. */
	struct path_list	*path;

	/* Use this much memory per call. */
	size_t			ioctl_bufsize;

	/* Use this fd for calling the getparents ioctl. */
	int			mntfd;
};

/*
 * Recursively walk upwards through the directory tree, changing out the path
 * components as needed.  Call the callback when we have a complete path.
 */
static int
find_parent_component(
	const struct parent_rec	*rec,
	void			*arg)
{
	struct walk_ppaths_info	*wpi = arg;
	struct path_component	*pc;
	int			ret;

	if (rec->p_flags & PARENTREC_FILE_IS_ROOT)
		return wpi->fn(wpi->mntpt, wpi->path, wpi->arg);

	/*
	 * If we detect a directory tree cycle, give up.  We never made any
	 * guarantees about concurrent tree updates.
	 */
	if (path_will_loop(wpi->path, rec->p_handle.ha_fid.fid_ino))
		return 0;

	pc = path_component_init(rec->p_name, rec->p_handle.ha_fid.fid_ino);
	if (!pc)
		return errno;
	path_list_add_parent_component(wpi->path, pc);

	if (wpi->mntfd >= 0)
		ret = handle_walk_parents_fd(wpi->mntfd, &rec->p_handle,
				sizeof(rec->p_handle), wpi->ioctl_bufsize,
				find_parent_component, wpi);
	else
		ret = handle_walk_parents(&rec->p_handle,
				sizeof(rec->p_handle), wpi->ioctl_bufsize,
				find_parent_component, wpi);

	path_list_del_component(wpi->path, pc);
	path_component_free(pc);
	return ret;
}

/*
 * Call the given function on all known paths from the vfs root to the inode
 * described in the handle.  Returns 0 for success or positive errno.
 */
int
handle_walk_paths(
	const void		*hanp,
	size_t			hlen,
	size_t			ioctl_bufsize,
	walk_path_fn		fn,
	void			*arg)
{
	struct walk_ppaths_info	wpi = {
		.ioctl_bufsize	= ioctl_bufsize,
		.mntfd		= -1,
	};
	int			ret;

	/*
	 * This function doesn't modify the handle, but we don't want to have
	 * to bump the libhandle major version just to change that.
	 */
	ret = handle_to_fsfd((void *)hanp, &wpi.mntpt);
	if (ret < 0)
		return errno;

	wpi.path = path_list_init();
	if (!wpi.path)
		return errno;
	wpi.fn = fn;
	wpi.arg = arg;

	ret = handle_walk_parents(hanp, hlen, ioctl_bufsize,
			find_parent_component, &wpi);

	path_list_free(wpi.path);
	return ret;
}

/*
 * Call the given function on all known paths from the vfs root to the inode
 * described in the handle using an already open mountpoint and fd.  Returns 0
 * for success or positive errno.
 */
int
handle_walk_paths_fd(
	const char		*mntpt,
	int			mntfd,
	const void		*hanp,
	size_t			hlen,
	size_t			ioctl_bufsize,
	walk_path_fn		fn,
	void			*arg)
{
	struct walk_ppaths_info	wpi = {
		.ioctl_bufsize	= ioctl_bufsize,
		.mntfd		= mntfd,
		.mntpt		= (char *)mntpt,
	};
	int			ret;

	wpi.path = path_list_init();
	if (!wpi.path)
		return errno;
	wpi.fn = fn;
	wpi.arg = arg;

	ret = handle_walk_parents_fd(mntfd, hanp, hlen, ioctl_bufsize,
			find_parent_component, &wpi);

	path_list_free(wpi.path);
	return ret;
}

/*
 * Call the given function on all known paths from the vfs root to the inode
 * referred to by the file description.  Returns 0 or positive errno.
 */
int
fd_walk_paths(
	int			fd,
	size_t			ioctl_bufsize,
	walk_path_fn		fn,
	void			*arg)
{
	void			*hanp;
	size_t			hlen;
	int			ret;

	ret = fd_to_handle(fd, &hanp, &hlen);
	if (ret)
		return errno;

	ret = handle_walk_paths(hanp, hlen, ioctl_bufsize, fn, arg);
	free_handle(hanp, hlen);
	return ret;
}

struct gather_path_info {
	char			*buf;
	size_t			len;
	size_t			written;
};

/* Helper that stringifies the first full path that we find. */
static int
path_to_string(
	const char		*mntpt,
	const struct path_list	*path,
	void			*arg)
{
	struct gather_path_info	*gpi = arg;
	int			mntpt_len = strlen(mntpt);
	int			ret;

	/* Trim trailing slashes from the mountpoint */
	while (mntpt_len > 0 && mntpt[mntpt_len - 1] == '/')
		mntpt_len--;

	ret = snprintf(gpi->buf, gpi->len, "%.*s", mntpt_len, mntpt);
	if (ret != mntpt_len)
		return ENAMETOOLONG;
	gpi->written += ret;

	ret = path_list_to_string(path, gpi->buf + ret, gpi->len - ret);
	if (ret < 0)
		return ENAMETOOLONG;

	gpi->written += ret;
	return ECANCELED;
}

/*
 * Return any eligible path to this file handle.  Returns 0 for success or
 * positive errno.
 */
int
handle_to_path(
	const void		*hanp,
	size_t			hlen,
	size_t			ioctl_bufsize,
	char			*path,
	size_t			pathlen)
{
	struct gather_path_info	gpi = { .buf = path, .len = pathlen };
	int			ret;

	ret = handle_walk_paths(hanp, hlen, ioctl_bufsize, path_to_string,
			&gpi);
	if (ret && ret != ECANCELED)
		return ret;
	if (!gpi.written)
		return ENODATA;

	path[gpi.written] = 0;
	return 0;
}

/*
 * Return any eligible path to this file description.  Returns 0 for success
 * or positive errno.
 */
int
fd_to_path(
	int			fd,
	size_t			ioctl_bufsize,
	char			*path,
	size_t			pathlen)
{
	struct gather_path_info	gpi = { .buf = path, .len = pathlen };
	int			ret;

	ret = fd_walk_paths(fd, ioctl_bufsize, path_to_string, &gpi);
	if (ret && ret != ECANCELED)
		return ret;
	if (!gpi.written)
		return ENODATA;

	path[gpi.written] = 0;
	return 0;
}
