// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * Copyright (C) 2021 Oracle.  All Rights Reserved.
 * Author: Darrick J. Wong <djwong@kernel.org>
 */
#include "libxfs.h"
#include "command.h"
#include "output.h"
#include "init.h"
#include "io.h"
#include "type.h"
#include "input.h"
#include "faddr.h"
#include "fprint.h"
#include "field.h"
#include "inode.h"
#include "namei.h"

/* Path lookup */

/* Key for looking up metadata inodes. */
struct dirpath {
	/* Array of string pointers. */
	char		**path;

	/* Number of strings in path. */
	unsigned int	depth;
};

static void
path_free(
	struct dirpath	*dirpath)
{
	unsigned int	i;

	for (i = 0; i < dirpath->depth; i++)
		free(dirpath->path[i]);
	free(dirpath->path);
	free(dirpath);
}

/* Chop a freeform string path into a structured path. */
static struct dirpath *
path_parse(
	const char	*path)
{
	struct dirpath	*dirpath;
	const char	*p = path;
	const char	*endp = path + strlen(path);

	dirpath = calloc(1, sizeof(*dirpath));
	if (!dirpath)
		return NULL;

	while (p < endp) {
		char		**new_path;
		const char	*next_slash;

		next_slash = strchr(p, '/');
		if (next_slash == p) {
			p++;
			continue;
		}
		if (!next_slash)
			next_slash = endp;

		new_path = realloc(dirpath->path,
				(dirpath->depth + 1) * sizeof(char *));
		if (!new_path) {
			path_free(dirpath);
			return NULL;
		}

		dirpath->path = new_path;
		dirpath->path[dirpath->depth] = strndup(p, next_slash - p);
		dirpath->depth++;

		p = next_slash + 1;
	}

	return dirpath;
}

/* Given a directory and a structured path, walk the path and set the cursor. */
static int
path_navigate(
	struct xfs_mount	*mp,
	xfs_ino_t		rootino,
	struct dirpath		*dirpath)
{
	struct xfs_trans	*tp;
	struct xfs_inode	*dp;
	xfs_ino_t		ino = rootino;
	unsigned int		i;
	int			error;

	tp = libxfs_trans_alloc_empty(mp);

	error = -libxfs_iget(mp, tp, ino, 0, &dp);
	if (error)
		goto out_trans;

	for (i = 0; i < dirpath->depth; i++) {
		struct xfs_name	xname = {
			.name	= (unsigned char *)dirpath->path[i],
			.len	= strlen(dirpath->path[i]),
		};

		if (!S_ISDIR(VFS_I(dp)->i_mode)) {
			error = ENOTDIR;
			goto out_rele;
		}

		error = -libxfs_dir_lookup(tp, dp, &xname, &ino, NULL);
		if (error)
			goto out_rele;
		if (!xfs_verify_ino(mp, ino)) {
			error = EFSCORRUPTED;
			goto out_rele;
		}

		libxfs_irele(dp);
		dp = NULL;

		error = -libxfs_iget(mp, tp, ino, 0, &dp);
		switch (error) {
		case EFSCORRUPTED:
		case EFSBADCRC:
		case 0:
			break;
		default:
			goto out_trans;
		}
	}

	set_cur_inode(ino);
out_rele:
	if (dp)
		libxfs_irele(dp);
out_trans:
	libxfs_trans_cancel(tp);
	return error;
}

/* Walk a directory path to an inode and set the io cursor to that inode. */
int
path_walk(
	xfs_ino_t	rootino,
	const char	*path)
{
	struct dirpath	*dirpath;
	const char	*p = path;
	int		error = 0;

	if (*p == '/') {
		/* Absolute path, start from the root inode. */
		p++;
	} else {
		/* Relative path, start from current dir. */
		if (iocur_top->typ != &typtab[TYP_INODE] ||
		    !S_ISDIR(iocur_top->mode))
			return ENOTDIR;

		rootino = iocur_top->ino;
	}

	dirpath = path_parse(p);
	if (!dirpath)
		return ENOMEM;

	error = path_navigate(mp, rootino, dirpath);
	path_free(dirpath);
	return error;
}

static void
path_help(void)
{
	dbprintf(_(
"\n"
" Navigate to an inode via directory path.\n"
"\n"
" Options:\n"
"   -m -- Walk an absolute path down the metadata directory tree.\n"
	));
}

static int
path_f(
	int		argc,
	char		**argv)
{
	xfs_ino_t	rootino = mp->m_sb.sb_rootino;
	int		c;
	int		error;

	while ((c = getopt(argc, argv, "m")) != -1) {
		switch (c) {
		case 'm':
			/* Absolute path, start from metadata rootdir. */
			if (!xfs_has_metadir(mp)) {
				dbprintf(
	_("filesystem does not support metadata directories.\n"));
				exitcode = 1;
				return 0;
			}
			rootino = mp->m_sb.sb_metadirino;
			break;
		default:
			path_help();
			return 0;
		}
	}

	if (argc == optind || argc > optind + 1) {
		dbprintf(_("Only supply one path.\n"));
		return -1;
	}

	error = path_walk(rootino, argv[optind]);
	if (error) {
		dbprintf("%s: %s\n", argv[optind], strerror(error));
		exitcode = 1;
	}

	return 0;
}

static struct cmdinfo path_cmd = {
	.name		= "path",
	.altname	= NULL,
	.cfunc		= path_f,
	.argmin		= 1,
	.argmax		= -1,
	.canpush	= 0,
	.args		= "",
	.help		= path_help,
};

/* List a directory's entries. */

static const char *filetype_strings[XFS_DIR3_FT_MAX] = {
	[XFS_DIR3_FT_UNKNOWN]	= "unknown",
	[XFS_DIR3_FT_REG_FILE]	= "regular",
	[XFS_DIR3_FT_DIR]	= "directory",
	[XFS_DIR3_FT_CHRDEV]	= "chardev",
	[XFS_DIR3_FT_BLKDEV]	= "blkdev",
	[XFS_DIR3_FT_FIFO]	= "fifo",
	[XFS_DIR3_FT_SOCK]	= "socket",
	[XFS_DIR3_FT_SYMLINK]	= "symlink",
	[XFS_DIR3_FT_WHT]	= "whiteout",
};

static const char *
get_dstr(
	struct xfs_mount	*mp,
	uint8_t			filetype)
{
	if (!xfs_has_ftype(mp))
		return filetype_strings[XFS_DIR3_FT_UNKNOWN];

	if (filetype >= XFS_DIR3_FT_MAX)
		return filetype_strings[XFS_DIR3_FT_UNKNOWN];

	return filetype_strings[filetype];
}

static int
print_dirent(
	struct xfs_trans	*tp,
	struct xfs_inode	*dp,
	xfs_dir2_dataptr_t	off,
	char			*name,
	ssize_t			namelen,
	xfs_ino_t		ino,
	uint8_t			dtype,
	void			*private)
{
	struct xfs_mount	*mp = dp->i_mount;
	char			*display_name;
	struct xfs_name		xname = { .name = (unsigned char *)name };
	const char		*dstr = get_dstr(mp, dtype);
	xfs_dahash_t		hash;
	bool			good;

	if (namelen < 0) {
		/* Negative length means that name is null-terminated. */
		display_name = name;
		xname.len = strlen(name);
		good = true;
	} else {
		/*
		 * Otherwise, name came from a directory entry, so we have to
		 * copy the string to a buffer so that we can add the null
		 * terminator.
		 */
		display_name = malloc(namelen + 1);
		memcpy(display_name, name, namelen);
		display_name[namelen] = 0;
		xname.len = namelen;
		good = libxfs_dir2_namecheck(name, namelen);
	}
	hash = libxfs_dir2_hashname(mp, &xname);

	dbprintf("%-10u %-18llu %-14s 0x%08llx %3d %s %s\n", off & 0xFFFFFFFF,
			ino, dstr, hash, xname.len,
			display_name, good ? _("(good)") : _("(corrupt)"));

	if (display_name != name)
		free(display_name);
	return 0;
}

static int
list_sfdir(
	struct xfs_da_args		*args,
	dir_emit_t			dir_emit,
	void				*private)
{
	struct xfs_inode		*dp = args->dp;
	struct xfs_mount		*mp = dp->i_mount;
	struct xfs_da_geometry		*geo = args->geo;
	struct xfs_dir2_sf_entry	*sfep;
	struct xfs_dir2_sf_hdr		*sfp = dp->i_df.if_data;
	xfs_ino_t			ino;
	xfs_dir2_dataptr_t		off;
	unsigned int			i;
	uint8_t				filetype;
	int				error;

	/* . and .. entries */
	off = xfs_dir2_db_off_to_dataptr(geo, geo->datablk,
			geo->data_entry_offset);
	error = dir_emit(args->trans, args->dp, off, ".", -1, dp->i_ino,
			XFS_DIR3_FT_DIR, private);
	if (error)
		return error;

	ino = libxfs_dir2_sf_get_parent_ino(sfp);
	off = xfs_dir2_db_off_to_dataptr(geo, geo->datablk,
			geo->data_entry_offset +
			libxfs_dir2_data_entsize(mp, sizeof(".") - 1));
	error = dir_emit(args->trans, args->dp, off, "..", -1, ino,
			XFS_DIR3_FT_DIR, private);
	if (error)
		return error;

	/* Walk everything else. */
	sfep = xfs_dir2_sf_firstentry(sfp);
	for (i = 0; i < sfp->count; i++) {
		ino = libxfs_dir2_sf_get_ino(mp, sfp, sfep);
		filetype = libxfs_dir2_sf_get_ftype(mp, sfep);
		off = xfs_dir2_db_off_to_dataptr(geo, geo->datablk,
				xfs_dir2_sf_get_offset(sfep));

		error = dir_emit(args->trans, args->dp, off,
				(char *)sfep->name, sfep->namelen, ino,
				filetype, private);
		if (error)
			return error;
		sfep = libxfs_dir2_sf_nextentry(mp, sfp, sfep);
	}

	return 0;
}

/* List entries in block format directory. */
static int
list_blockdir(
	struct xfs_da_args	*args,
	dir_emit_t		dir_emit,
	void			*private)
{
	struct xfs_inode	*dp = args->dp;
	struct xfs_mount	*mp = dp->i_mount;
	struct xfs_buf		*bp;
	struct xfs_da_geometry	*geo = mp->m_dir_geo;
	xfs_dir2_dataptr_t	diroff;
	unsigned int		offset;
	unsigned int		end;
	int			error;

	error = xfs_dir3_block_read(args->trans, dp, args->owner, &bp);
	if (error)
		return error;

	end = xfs_dir3_data_end_offset(geo, bp->b_addr);
	for (offset = geo->data_entry_offset; offset < end;) {
		struct xfs_dir2_data_unused	*dup = bp->b_addr + offset;
		struct xfs_dir2_data_entry	*dep = bp->b_addr + offset;
		uint8_t				filetype;

		if (be16_to_cpu(dup->freetag) == XFS_DIR2_DATA_FREE_TAG) {
			/* Unused entry */
			offset += be16_to_cpu(dup->length);
			continue;
		}

		/* Real entry */
		diroff = xfs_dir2_db_off_to_dataptr(geo, geo->datablk, offset);
		offset += libxfs_dir2_data_entsize(mp, dep->namelen);
		filetype = libxfs_dir2_data_get_ftype(dp->i_mount, dep);
		error = dir_emit(args->trans, args->dp, diroff,
				(char *)dep->name, dep->namelen,
				be64_to_cpu(dep->inumber), filetype, private);
		if (error)
			break;
	}

	libxfs_trans_brelse(args->trans, bp);
	return error;
}

/* List entries in leaf format directory. */
static int
list_leafdir(
	struct xfs_da_args	*args,
	dir_emit_t		dir_emit,
	void			*private)
{
	struct xfs_bmbt_irec	map;
	struct xfs_iext_cursor	icur;
	struct xfs_inode	*dp = args->dp;
	struct xfs_mount	*mp = dp->i_mount;
	struct xfs_buf		*bp = NULL;
	struct xfs_ifork	*ifp = xfs_ifork_ptr(dp, XFS_DATA_FORK);
	struct xfs_da_geometry	*geo = mp->m_dir_geo;
	xfs_dir2_off_t		dirboff;
	xfs_dablk_t		dabno = 0;
	int			error = 0;

	/* Read extent map. */
	error = -libxfs_iread_extents(args->trans, dp, XFS_DATA_FORK);
	if (error)
		return error;

	while (dabno < geo->leafblk) {
		unsigned int	offset;
		unsigned int	length;

		/* Find mapping for leaf block. */
		if (!xfs_iext_lookup_extent(dp, ifp, dabno, &icur, &map))
			break;
		if (map.br_startoff >= geo->leafblk)
			break;
		libxfs_trim_extent(&map, dabno, geo->leafblk - dabno);

		/* Read the directory block of that first mapping. */
		error = xfs_dir3_data_read(args->trans, dp, args->owner,
				map.br_startoff, 0, &bp);
		if (error)
			break;

		dirboff = xfs_dir2_da_to_byte(geo, map.br_startoff);
		for (offset = geo->data_entry_offset; offset < geo->blksize;) {
			struct xfs_dir2_data_entry	*dep;
			struct xfs_dir2_data_unused	*dup;
			uint8_t				filetype;

			dup = bp->b_addr + offset;
			dep = bp->b_addr + offset;

			if (be16_to_cpu(dup->freetag) ==
			    XFS_DIR2_DATA_FREE_TAG) {
				/* Skip unused entry */
				length = be16_to_cpu(dup->length);
				offset += length;
				continue;
			}

			offset += libxfs_dir2_data_entsize(mp, dep->namelen);
			filetype = libxfs_dir2_data_get_ftype(mp, dep);

			error = dir_emit(args->trans, args->dp,
					xfs_dir2_byte_to_dataptr(dirboff + offset),
					(char *)dep->name, dep->namelen,
					be64_to_cpu(dep->inumber), filetype,
					private);
			if (error)
				break;
		}

		dabno += XFS_DADDR_TO_FSB(mp, bp->b_length);
		libxfs_trans_brelse(args->trans, bp);
		bp = NULL;
	}

	if (bp)
		libxfs_trans_brelse(args->trans, bp);

	return error;
}

/* Read the directory, display contents. */
int
listdir(
	struct xfs_trans	*tp,
	struct xfs_inode	*dp,
	dir_emit_t		dir_emit,
	void			*private)
{
	struct xfs_da_args	args = {
		.trans		= tp,
		.dp		= dp,
		.geo		= dp->i_mount->m_dir_geo,
		.owner		= dp->i_ino,
	};
	int			error;

	switch (libxfs_dir2_format(&args, &error)) {
	case XFS_DIR2_FMT_SF:
		return list_sfdir(&args, dir_emit, private);
	case XFS_DIR2_FMT_BLOCK:
		return list_blockdir(&args, dir_emit, private);
	case XFS_DIR2_FMT_LEAF:
	case XFS_DIR2_FMT_NODE:
		return list_leafdir(&args, dir_emit, private);
	default:
		return EFSCORRUPTED;
	}
}

/* List the inode number of the currently selected inode. */
static int
inum_cur(void)
{
	if (iocur_top->typ != &typtab[TYP_INODE])
		return ENOENT;

	dbprintf("%llu\n", iocur_top->ino);
	return 0;
}

/* If the io cursor points to a directory, list its contents. */
static int
ls_cur(
	char			*tag)
{
	struct xfs_inode	*dp;
	int			error = 0;

	if (iocur_top->typ != &typtab[TYP_INODE] ||
	    !S_ISDIR(iocur_top->mode))
		return ENOTDIR;

	error = -libxfs_iget(mp, NULL, iocur_top->ino, 0, &dp);
	if (error)
		return error;

	if (!S_ISDIR(VFS_I(dp)->i_mode)) {
		error = ENOTDIR;
		goto rele;
	}

	/* List the contents of a directory. */
	if (tag)
		dbprintf(_("%s:\n"), tag);

	error = listdir(NULL, dp, print_dirent, NULL);
	if (error)
		goto rele;

rele:
	libxfs_irele(dp);
	return error;
}

static void
ls_help(void)
{
	dbprintf(_(
"\n"
" List the contents of the currently selected directory inode.\n"
"\n"
" Options:\n"
"   -i -- Resolve the given paths to their corresponding inode numbers.\n"
"         If no paths are given, display the current inode number.\n"
"   -m -- Walk an absolute path down the metadata directory tree.\n"
"\n"
" Directory contents will be listed in the format:\n"
" dir_cookie	inode_number	type	hash	name_length	name\n"
	));
}

static int
ls_f(
	int			argc,
	char			**argv)
{
	xfs_ino_t		rootino = mp->m_sb.sb_rootino;
	bool			inum_only = false;
	int			c;
	int			error = 0;

	while ((c = getopt(argc, argv, "im")) != -1) {
		switch (c) {
		case 'i':
			inum_only = true;
			break;
		case 'm':
			/* Absolute path, start from metadata rootdir. */
			if (!xfs_has_metadir(mp)) {
				dbprintf(
	_("filesystem does not support metadata directories.\n"));
				exitcode = 1;
				return 0;
			}
			rootino = mp->m_sb.sb_metadirino;
			break;
		default:
			ls_help();
			return 0;
		}
	}

	if (optind == argc) {
		if (inum_only)
			error = inum_cur();
		else
			error = ls_cur(NULL);
		if (error) {
			dbprintf("%s\n", strerror(error));
			exitcode = 1;
		}

		return 0;
	}

	for (c = optind; c < argc; c++) {
		push_cur();

		error = path_walk(rootino, argv[c]);
		if (error)
			goto err_cur;

		if (inum_only)
			error = inum_cur();
		else
			error = ls_cur(argv[c]);
		if (error)
			goto err_cur;

		pop_cur();
	}

	return 0;
err_cur:
	pop_cur();
	if (error) {
		dbprintf("%s: %s\n", argv[c], strerror(error));
		exitcode = 1;
	}
	return 0;
}

static struct cmdinfo ls_cmd = {
	.name		= "ls",
	.altname	= "l",
	.cfunc		= ls_f,
	.argmin		= 0,
	.argmax		= -1,
	.canpush	= 0,
	.args		= "[-i] [paths...]",
	.help		= ls_help,
};

static void
pptr_emit(
	struct xfs_inode	*ip,
	unsigned int		attr_flags,
	const uint8_t		*name,
	unsigned int		namelen,
	const void		*value,
	unsigned int		valuelen)
{
	struct xfs_mount	*mp = ip->i_mount;
	xfs_ino_t		parent_ino;
	uint32_t		parent_gen;
	int			error;

	if (!(attr_flags & XFS_ATTR_PARENT))
		return;

	error = -libxfs_parent_from_attr(mp, attr_flags, name, namelen, value,
			valuelen, &parent_ino, &parent_gen);
	if (error)
		return;

	dbprintf("%18llu:0x%08x %3d %.*s\n", parent_ino, parent_gen, namelen,
			namelen, name);
}

static int
list_sf_pptrs(
	struct xfs_inode		*ip)
{
	struct xfs_attr_sf_hdr		*hdr = ip->i_af.if_data;
	struct xfs_attr_sf_entry	*sfe;
	unsigned int			i;

	sfe = libxfs_attr_sf_firstentry(hdr);
	for (i = 0; i < hdr->count; i++) {
		pptr_emit(ip, sfe->flags, sfe->nameval, sfe->namelen,
				sfe->nameval + sfe->valuelen, sfe->valuelen);

		sfe = xfs_attr_sf_nextentry(sfe);
	}

	return 0;
}

static void
list_leaf_pptr_entries(
	struct xfs_inode		*ip,
	struct xfs_buf			*bp)
{
	struct xfs_attr3_icleaf_hdr	ichdr;
	struct xfs_mount		*mp = ip->i_mount;
	struct xfs_attr_leafblock	*leaf = bp->b_addr;
	struct xfs_attr_leaf_entry	*entry;
	unsigned int			i;

	libxfs_attr3_leaf_hdr_from_disk(mp->m_attr_geo, &ichdr, leaf);
	entry = xfs_attr3_leaf_entryp(leaf);

	for (i = 0; i < ichdr.count; entry++, i++) {
		struct xfs_attr_leaf_name_local	*name_loc;

		/*
		 * Parent pointers cannot be remote values; don't bother
		 * decoding this xattr name.
		 */
		if (!(entry->flags & XFS_ATTR_LOCAL))
			continue;

		name_loc = xfs_attr3_leaf_name_local(leaf, i);
		pptr_emit(ip, entry->flags, name_loc->nameval,
				name_loc->namelen,
				name_loc->nameval + name_loc->namelen,
				be16_to_cpu(name_loc->valuelen));
	}
}

static int
list_leaf_pptrs(
	struct xfs_inode		*ip)
{
	struct xfs_buf			*leaf_bp;
	int				error;

	error = -libxfs_attr3_leaf_read(NULL, ip, ip->i_ino, 0, &leaf_bp);
	if (error)
		return error;

	list_leaf_pptr_entries(ip, leaf_bp);
	libxfs_trans_brelse(NULL, leaf_bp);
	return 0;
}

static int
find_leftmost_attr_leaf(
	struct xfs_inode		*ip,
	struct xfs_buf			**leaf_bpp)
{
	struct xfs_da3_icnode_hdr	nodehdr;
	struct xfs_mount		*mp = ip->i_mount;
	struct xfs_da_intnode		*node;
	struct xfs_da_node_entry	*btree;
	struct xfs_buf			*bp;
	xfs_dablk_t			blkno = 0;
	unsigned int			expected_level = 0;
	int				error;

	for (;;) {
		uint16_t		magic;

		error = -libxfs_da3_node_read(NULL, ip, blkno, &bp,
				XFS_ATTR_FORK);
		if (error)
			return error;

		node = bp->b_addr;
		magic = be16_to_cpu(node->hdr.info.magic);
		if (magic == XFS_ATTR_LEAF_MAGIC ||
		    magic == XFS_ATTR3_LEAF_MAGIC)
			break;

		error = EFSCORRUPTED;
		if (magic != XFS_DA_NODE_MAGIC &&
		    magic != XFS_DA3_NODE_MAGIC)
			goto out_buf;

		libxfs_da3_node_hdr_from_disk(mp, &nodehdr, node);

		if (nodehdr.count == 0 || nodehdr.level >= XFS_DA_NODE_MAXDEPTH)
			goto out_buf;

		/* Check the level from the root node. */
		if (blkno == 0)
			expected_level = nodehdr.level - 1;
		else if (expected_level != nodehdr.level)
			goto out_buf;
		else
			expected_level--;

		/* Find the next level towards the leaves of the dabtree. */
		btree = nodehdr.btree;
		blkno = be32_to_cpu(btree->before);
		libxfs_trans_brelse(NULL, bp);
	}

	error = EFSCORRUPTED;
	if (expected_level != 0)
		goto out_buf;

	*leaf_bpp = bp;
	return 0;

out_buf:
	libxfs_trans_brelse(NULL, bp);
	return error;
}

static int
list_node_pptrs(
	struct xfs_inode		*ip)
{
	struct xfs_attr3_icleaf_hdr	leafhdr;
	struct xfs_mount		*mp = ip->i_mount;
	struct xfs_attr_leafblock	*leaf;
	struct xfs_buf			*leaf_bp;
	int				error;

	error = find_leftmost_attr_leaf(ip, &leaf_bp);
	if (error)
		return error;

	for (;;) {
		list_leaf_pptr_entries(ip, leaf_bp);

		/* Find the right sibling of this leaf block. */
		leaf = leaf_bp->b_addr;
		libxfs_attr3_leaf_hdr_from_disk(mp->m_attr_geo, &leafhdr, leaf);
		if (leafhdr.forw == 0)
			goto out_leaf;

		libxfs_trans_brelse(NULL, leaf_bp);

		error = -libxfs_attr3_leaf_read(NULL, ip, ip->i_ino,
				leafhdr.forw, &leaf_bp);
		if (error)
			return error;
	}

out_leaf:
	libxfs_trans_brelse(NULL, leaf_bp);
	return error;
}

static int
list_pptrs(
	struct xfs_inode	*ip)
{
	int			error;

	if (!libxfs_inode_hasattr(ip))
		return 0;

	if (ip->i_af.if_format == XFS_DINODE_FMT_LOCAL)
		return list_sf_pptrs(ip);

	/* attr functions require that the attr fork is loaded */
	error = -libxfs_iread_extents(NULL, ip, XFS_ATTR_FORK);
	if (error)
		return error;

	if (libxfs_attr_is_leaf(ip))
		return list_leaf_pptrs(ip);

	return list_node_pptrs(ip);
}

/* If the io cursor points to a file, list its parents. */
static int
parent_cur(
	char			*tag)
{
	struct xfs_inode	*ip;
	int			error = 0;

	if (!xfs_has_parent(mp))
		return 0;

	if (iocur_top->typ != &typtab[TYP_INODE])
		return ENOTDIR;

	error = -libxfs_iget(mp, NULL, iocur_top->ino, 0, &ip);
	if (error)
		return error;

	/* List the parents of a file. */
	if (tag)
		dbprintf(_("%s:\n"), tag);

	error = list_pptrs(ip);
	if (error)
		goto rele;

rele:
	libxfs_irele(ip);
	return error;
}

static void
parent_help(void)
{
	dbprintf(_(
"\n"
" List the parents of the currently selected file.\n"
"\n"
" Parent pointers will be listed in the format:\n"
" inode_number:inode_gen	ondisk_namehash:namehash	name_length	name\n"
	));
}

static int
parent_f(
	int			argc,
	char			**argv)
{
	xfs_ino_t		rootino = mp->m_sb.sb_rootino;
	int			c;
	int			error = 0;

	while ((c = getopt(argc, argv, "m")) != -1) {
		switch (c) {
		case 'm':
			/* Absolute path, start from metadata rootdir. */
			if (!xfs_has_metadir(mp)) {
				dbprintf(
	_("filesystem does not support metadata directories.\n"));
				exitcode = 1;
				return 0;
			}
			rootino = mp->m_sb.sb_metadirino;
			break;
		default:
			ls_help();
			return 0;
		}
	}

	if (optind == argc) {
		error = parent_cur(NULL);
		if (error) {
			dbprintf("%s\n", strerror(error));
			exitcode = 1;
		}

		return 0;
	}

	for (c = optind; c < argc; c++) {
		push_cur();

		error = path_walk(rootino, argv[c]);
		if (error)
			goto err_cur;

		error = parent_cur(argv[c]);
		if (error)
			goto err_cur;

		pop_cur();
	}

	return 0;
err_cur:
	pop_cur();
	if (error) {
		dbprintf("%s: %s\n", argv[c], strerror(error));
		exitcode = 1;
	}
	return 0;
}

static struct cmdinfo parent_cmd = {
	.name		= "parent",
	.altname	= "pptr",
	.cfunc		= parent_f,
	.argmin		= 0,
	.argmax		= -1,
	.canpush	= 0,
	.args		= "[-m] [paths...]",
	.help		= parent_help,
};

static void
link_help(void)
{
	dbprintf(_(
"\n"
" Create a directory entry in the current directory that points to the\n"
" specified file.\n"
"\n"
" Options:\n"
"   -i   -- Point to this specific inode number.\n"
"   -m   -- Select the metadata directory tree.\n"
"   -p   -- Point to the inode given by this path.\n"
"   -t   -- Set the file type to this value.\n"
"   name -- Create this directory entry with this name.\n"
	));
}

static int
create_child(
	struct xfs_mount	*mp,
	xfs_ino_t		parent_ino,
	const char		*name,
	unsigned int		ftype,
	xfs_ino_t		child_ino)
{
	struct xfs_name		xname = {
		.name		= (const unsigned char *)name,
		.len		= strlen(name),
		.type		= ftype,
	};
	struct xfs_parent_args	*ppargs = NULL;
	struct xfs_trans	*tp;
	struct xfs_inode	*dp, *ip;
	unsigned int		resblks;
	bool			isdir;
	int			error;

	error = -libxfs_iget(mp, NULL, parent_ino, 0, &dp);
	if (error)
		return error;

	if (!S_ISDIR(VFS_I(dp)->i_mode)) {
		error = -ENOTDIR;
		goto out_dp;
	}

	error = -libxfs_iget(mp, NULL, child_ino, 0, &ip);
	if (error)
		goto out_dp;
	isdir = S_ISDIR(VFS_I(ip)->i_mode);

	if (xname.type == XFS_DIR3_FT_UNKNOWN)
		xname.type = libxfs_mode_to_ftype(VFS_I(ip)->i_mode);

	error = -libxfs_parent_start(mp, &ppargs);
	if (error)
		goto out_ip;

	resblks = libxfs_link_space_res(mp, MAXNAMELEN);
	error = -libxfs_trans_alloc(mp, &M_RES(mp)->tr_link, resblks, 0, 0,
			&tp);
	if (error)
		goto out_parent;

	libxfs_trans_ijoin(tp, dp, 0);
	libxfs_trans_ijoin(tp, ip, 0);

	error = -libxfs_dir_createname(tp, dp, &xname, ip->i_ino, resblks);
	if (error)
		goto out_trans;

	/* bump dp's link to ip */
	libxfs_bumplink(tp, ip);

	/* bump ip's dotdot link to dp */
	if (isdir)
		libxfs_bumplink(tp, dp);

	/* Replace the dotdot entry in the child directory. */
	if (isdir) {
		error = -libxfs_dir_replace(tp, ip, &xfs_name_dotdot,
				dp->i_ino, resblks);
		if (error)
			goto out_trans;
	}

	if (ppargs) {
		error = -libxfs_parent_addname(tp, ppargs, dp, &xname, ip);
		if (error)
			goto out_trans;
	}

	error = -libxfs_trans_commit(tp);
	goto out_parent;

out_trans:
	libxfs_trans_cancel(tp);
out_parent:
	libxfs_parent_finish(mp, ppargs);
out_ip:
	libxfs_irele(ip);
out_dp:
	libxfs_irele(dp);
	return error;
}

static const char *ftype_map[] = {
	[XFS_DIR3_FT_REG_FILE]	= "reg",
	[XFS_DIR3_FT_DIR]	= "dir",
	[XFS_DIR3_FT_CHRDEV]	= "cdev",
	[XFS_DIR3_FT_BLKDEV]	= "bdev",
	[XFS_DIR3_FT_FIFO]	= "fifo",
	[XFS_DIR3_FT_SOCK]	= "sock",
	[XFS_DIR3_FT_SYMLINK]	= "symlink",
	[XFS_DIR3_FT_WHT]	= "whiteout",
};

static int
link_f(
	int			argc,
	char			**argv)
{
	xfs_ino_t		child_ino = NULLFSINO;
	int			ftype = XFS_DIR3_FT_UNKNOWN;
	xfs_ino_t		rootino = mp->m_sb.sb_rootino;
	unsigned int		i;
	int			c;
	int			error = 0;

	while ((c = getopt(argc, argv, "i:mp:t:")) != -1) {
		switch (c) {
		case 'i':
			errno = 0;
			child_ino = strtoull(optarg, NULL, 0);
			if (errno == ERANGE) {
				printf("%s: unknown inode number\n", optarg);
				exitcode = 1;
				return 0;
			}
			break;
		case 'm':
			rootino = mp->m_sb.sb_metadirino;
			break;
		case 'p':
			push_cur();
			error = path_walk(rootino, optarg);
			if (error) {
				printf("%s: %s\n", optarg, strerror(error));
				exitcode = 1;
				return 0;
			} else if (iocur_top->typ != &typtab[TYP_INODE]) {
				printf("%s: does not point to an inode\n",
						optarg);
				exitcode = 1;
				return 0;
			} else {
				child_ino = iocur_top->ino;
			}
			pop_cur();
			break;
		case 't':
			for (i = 0; i < ARRAY_SIZE(ftype_map); i++) {
				if (ftype_map[i] &&
				    !strcmp(ftype_map[i], optarg)) {
					ftype = i;
					break;
				}
			}
			if (i == ARRAY_SIZE(ftype_map)) {
				printf("%s: unknown file type\n", optarg);
				exitcode = 1;
				return 0;
			}
			break;
		default:
			link_help();
			return 0;
		}
	}

	if (child_ino == NULLFSINO) {
		printf("link: need to specify child via -i or -p\n");
		exitcode = 1;
		return 0;
	}

	if (iocur_top->typ != &typtab[TYP_INODE]) {
		printf("io cursor does not point to an inode.\n");
		exitcode = 1;
		return 0;
	}

	if (optind + 1 != argc) {
		printf("link: need directory entry name");
		exitcode = 1;
		return 0;
	}

	error = create_child(mp, iocur_top->ino, argv[optind], ftype,
			child_ino);
	if (error) {
		printf("link failed: %s\n", strerror(error));
		exitcode = 1;
		return 0;
	}

	return 0;
}

static struct cmdinfo link_cmd = {
	.name		= "link",
	.cfunc		= link_f,
	.argmin		= 0,
	.argmax		= -1,
	.canpush	= 0,
	.args		= "[-i ino] [-m] [-p path] [-t ftype] name",
	.help		= link_help,
};

static void
unlink_help(void)
{
	dbprintf(_(
"\n"
" Remove a directory entry from the current directory.\n"
"\n"
" Options:\n"
"   name -- Remove the directory entry with this name.\n"
	));
}

static int
remove_child(
	struct xfs_mount	*mp,
	xfs_ino_t		parent_ino,
	const char		*name)
{
	struct xfs_name		xname = {
		.name		= (const unsigned char *)name,
		.len		= strlen(name),
	};
	struct xfs_parent_args	*ppargs;
	struct xfs_trans	*tp;
	struct xfs_inode	*dp, *ip;
	xfs_ino_t		child_ino;
	unsigned int		resblks;
	int			error;

	error = -libxfs_iget(mp, NULL, parent_ino, 0, &dp);
	if (error)
		return error;

	if (!S_ISDIR(VFS_I(dp)->i_mode)) {
		error = -ENOTDIR;
		goto out_dp;
	}

	error = -libxfs_dir_lookup(NULL, dp, &xname, &child_ino, NULL);
	if (error)
		goto out_dp;

	error = -libxfs_iget(mp, NULL, child_ino, 0, &ip);
	if (error)
		goto out_dp;

	error = -libxfs_parent_start(mp, &ppargs);
	if (error)
		goto out_ip;

	resblks = libxfs_remove_space_res(mp, MAXNAMELEN);
	error = -libxfs_trans_alloc(mp, &M_RES(mp)->tr_remove, resblks, 0, 0,
			&tp);
	if (error)
		goto out_parent;

	libxfs_trans_ijoin(tp, dp, 0);
	libxfs_trans_ijoin(tp, ip, 0);

	if (S_ISDIR(VFS_I(ip)->i_mode)) {
		/* drop ip's dotdot link to dp */
		error = -libxfs_droplink(tp, dp);
		if (error)
			goto out_trans;
	} else {
		libxfs_trans_log_inode(tp, dp, XFS_ILOG_CORE);
	}

	/* drop dp's link to ip */
	error = -libxfs_droplink(tp, ip);
	if (error)
		goto out_trans;

	error = -libxfs_dir_removename(tp, dp, &xname, ip->i_ino, resblks);
	if (error)
		goto out_trans;

	if (ppargs) {
		error = -libxfs_parent_removename(tp, ppargs, dp, &xname, ip);
		if (error)
			goto out_trans;
	}

	error = -libxfs_trans_commit(tp);
	goto out_parent;

out_trans:
	libxfs_trans_cancel(tp);
out_parent:
	libxfs_parent_finish(mp, ppargs);
out_ip:
	libxfs_irele(ip);
out_dp:
	libxfs_irele(dp);
	return error;
}

static int
unlink_f(
	int			argc,
	char			**argv)
{
	int			c;
	int			error = 0;

	while ((c = getopt(argc, argv, "")) != -1) {
		switch (c) {
		default:
			unlink_help();
			return 0;
		}
	}

	if (iocur_top->typ != &typtab[TYP_INODE]) {
		printf("io cursor does not point to an inode.\n");
		exitcode = 1;
		return 0;
	}

	if (optind + 1 != argc) {
		printf("unlink: need directory entry name");
		exitcode = 1;
		return 0;
	}

	error = remove_child(mp, iocur_top->ino, argv[optind]);
	if (error) {
		printf("unlink failed: %s\n", strerror(error));
		exitcode = 1;
		return 0;
	}

	return 0;
}

static struct cmdinfo unlink_cmd = {
	.name		= "unlink",
	.cfunc		= unlink_f,
	.argmin		= 0,
	.argmax		= -1,
	.canpush	= 0,
	.args		= "name",
	.help		= unlink_help,
};

void
namei_init(void)
{
	path_cmd.oneline = _("navigate to an inode by path");
	add_command(&path_cmd);

	ls_cmd.oneline = _("list directory contents");
	add_command(&ls_cmd);

	parent_cmd.oneline = _("list parent pointers");
	add_command(&parent_cmd);

	if (expert_mode) {
		link_cmd.oneline = _("create directory link");
		add_command(&link_cmd);

		unlink_cmd.oneline = _("remove directory link");
		add_command(&unlink_cmd);
	}
}
