/*
 * Copyright 2016 Jakub Klama <jceel@FreeBSD.org>
 * All rights reserved
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted providing that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
 * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 */

#include <errno.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <assert.h>
#include <inttypes.h>
#include <sys/param.h>
#include <sys/stat.h>
#include <sys/uio.h>
#if defined(__FreeBSD__)
#include <sys/sbuf.h>
#else
#include "sbuf/sbuf.h"
#endif
#include "lib9p.h"
#include "fcall.h"
#include "linux_errno.h"

#ifdef __APPLE__
  #define GETGROUPS_GROUP_TYPE_IS_INT
#endif

#define N(ary)          (sizeof(ary) / sizeof(*ary))

/* See l9p_describe_bits() below. */
struct descbits {
	uint64_t	db_mask;	/* mask value */
	uint64_t	db_match;	/* match value */
	const char	*db_name;	/* name for matched value */
};


static bool l9p_describe_bits(const char *, uint64_t, const char *,
    const struct descbits *, struct sbuf *);
static void l9p_describe_fid(const char *, uint32_t, struct sbuf *);
static void l9p_describe_mode(const char *, uint32_t, struct sbuf *);
static void l9p_describe_name(const char *, char *, struct sbuf *);
static void l9p_describe_perm(const char *, uint32_t, struct sbuf *);
static void l9p_describe_lperm(const char *, uint32_t, struct sbuf *);
static void l9p_describe_qid(const char *, struct l9p_qid *, struct sbuf *);
static void l9p_describe_l9stat(const char *, struct l9p_stat *,
    enum l9p_version, struct sbuf *);
static void l9p_describe_statfs(const char *, struct l9p_statfs *,
    struct sbuf *);
static void l9p_describe_time(struct sbuf *, const char *, uint64_t, uint64_t);
static void l9p_describe_readdir(struct sbuf *, struct l9p_f_io *);
static void l9p_describe_size(const char *, uint64_t, struct sbuf *);
static void l9p_describe_ugid(const char *, uint32_t, struct sbuf *);
static void l9p_describe_getattr_mask(uint64_t, struct sbuf *);
static void l9p_describe_unlinkat_flags(const char *, uint32_t, struct sbuf *);
static const char *lookup_linux_errno(uint32_t);

/*
 * Using indexed initializers, we can have these occur in any order.
 * Using adjacent-string concatenation ("T" #name, "R" #name), we
 * get both Tfoo and Rfoo strings with one copy of the name.
 * Alas, there is no stupid cpp trick to lowercase-ify, so we
 * have to write each name twice.  In which case we might as well
 * make the second one a string in the first place and not bother
 * with the stringizing.
 *
 * This table should have entries for each enum value in fcall.h.
 */
#define X(NAME, name)	[L9P_T##NAME - L9P__FIRST] = "T" name, \
			[L9P_R##NAME - L9P__FIRST] = "R" name
static const char *ftype_names[] = {
	X(VERSION,	"version"),
	X(AUTH,		"auth"),
	X(ATTACH,	"attach"),
	X(ERROR,	"error"),
	X(LERROR,	"lerror"),
	X(FLUSH,	"flush"),
	X(WALK,		"walk"),
	X(OPEN,		"open"),
	X(CREATE,	"create"),
	X(READ,		"read"),
	X(WRITE,	"write"),
	X(CLUNK,	"clunk"),
	X(REMOVE,	"remove"),
	X(STAT,		"stat"),
	X(WSTAT,	"wstat"),
	X(STATFS,	"statfs"),
	X(LOPEN,	"lopen"),
	X(LCREATE,	"lcreate"),
	X(SYMLINK,	"symlink"),
	X(MKNOD,	"mknod"),
	X(RENAME,	"rename"),
	X(READLINK,	"readlink"),
	X(GETATTR,	"getattr"),
	X(SETATTR,	"setattr"),
	X(XATTRWALK,	"xattrwalk"),
	X(XATTRCREATE,	"xattrcreate"),
	X(READDIR,	"readdir"),
	X(FSYNC,	"fsync"),
	X(LOCK,		"lock"),
	X(GETLOCK,	"getlock"),
	X(LINK,		"link"),
	X(MKDIR,	"mkdir"),
	X(RENAMEAT,	"renameat"),
	X(UNLINKAT,	"unlinkat"),
};
#undef X

void
l9p_seek_iov(struct iovec *iov1, size_t niov1, struct iovec *iov2,
    size_t *niov2, size_t seek)
{
	size_t remainder = 0;
	size_t left = seek;
	size_t i, j;

	for (i = 0; i < niov1; i++) {
		size_t toseek = MIN(left, iov1[i].iov_len);
		left -= toseek;

		if (toseek == iov1[i].iov_len)
			continue;

		if (left == 0) {
			remainder = toseek;
			break;
		}
	}

	for (j = i; j < niov1; j++) {
		iov2[j - i].iov_base = (char *)iov1[j].iov_base + remainder;
		iov2[j - i].iov_len = iov1[j].iov_len - remainder;
		remainder = 0;
	}

	*niov2 = j - i;
}

size_t
l9p_truncate_iov(struct iovec *iov, size_t niov, size_t length)
{
	size_t i, done = 0;

	for (i = 0; i < niov; i++) {
		size_t toseek = MIN(length - done, iov[i].iov_len);
		done += toseek;

		if (toseek < iov[i].iov_len) {
			iov[i].iov_len = toseek;
			return (i + 1);
		}
	}

	return (niov);
}

/*
 * This wrapper for getgrouplist() that malloc'ed memory, and
 * papers over FreeBSD vs Mac differences in the getgrouplist()
 * argument types.
 *
 * Note that this function guarantees that *either*:
 *     return value != NULL and *angroups has been set
 * or: return value == NULL and *angroups is 0
 */
gid_t *
l9p_getgrlist(const char *name, gid_t basegid, int *angroups)
{
#ifdef GETGROUPS_GROUP_TYPE_IS_INT
	int i, *int_groups;
#endif
	gid_t *groups;
	int ngroups;

	/*
	 * Todo, perhaps: while getgrouplist() returns -1, expand.
	 * For now just use NGROUPS_MAX.
	 */
	ngroups = NGROUPS_MAX;
	groups = malloc((size_t)ngroups * sizeof(*groups));
#ifdef GETGROUPS_GROUP_TYPE_IS_INT
	int_groups = groups ? malloc((size_t)ngroups * sizeof(*int_groups)) :
	    NULL;
	if (int_groups == NULL) {
		free(groups);
		groups = NULL;
	}
#endif
	if (groups == NULL) {
		*angroups = 0;
		return (NULL);
	}
#ifdef GETGROUPS_GROUP_TYPE_IS_INT
	(void) getgrouplist(name, (int)basegid, int_groups, &ngroups);
	for (i = 0; i < ngroups; i++)
		groups[i] = (gid_t)int_groups[i];
#else
	(void) getgrouplist(name, basegid, groups, &ngroups);
#endif
	*angroups = ngroups;
	return (groups);
}

/*
 * For the various debug describe ops: decode bits in a bit-field-y
 * value.  For example, we might produce:
 *     value=0x3c[FOO,BAR,QUUX,?0x20]
 * when FOO is bit 0x10, BAR is 0x08, and QUUX is 0x04 (as defined
 * by the table).  This leaves 0x20 (bit 5) as a mystery, while bits
 * 4, 3, and 2 were decoded.  (Bits 0 and 1 were 0 on input hence
 * were not attempted here.)
 *
 * For general use we take a uint64_t <value>.  The bit description
 * table <db> is an array of {mask, match, str} values ending with
 * {0, 0, NULL}.
 *
 * If <str> is non-NULL we'll print it and the mask as well (if
 * str is NULL we'll print neither).  The mask is always printed in
 * hex at the moment.  See undec description too.
 *
 * For convenience, you can use a mask-and-match value, e.g., to
 * decode a 2-bit field in bits 0 and 1 you can mask against 3 and
 * match the values 0, 1, 2, and 3.  To handle this, make sure that
 * all masks-with-same-match are sequential.
 *
 * If there are any nonzero undecoded bits, print them after
 * all the decode-able bits have been handled.
 *
 * The <oc> argument defines the open and close bracket characters,
 * typically "[]", that surround the entire string.  If NULL, no
 * brackets are added, else oc[0] goes in the front and oc[1] at
 * the end, after printing any <str><value> part.
 *
 * Returns true if it printed anything (other than the implied
 * str-and-value, that is).
 */
static bool
l9p_describe_bits(const char *str, uint64_t value, const char *oc,
    const struct descbits *db, struct sbuf *sb)
{
	const char *sep;
	char bracketbuf[2] = "";
	bool printed = false;

	if (str != NULL)
		sbuf_printf(sb, "%s0x%" PRIx64, str, value);

	if (oc != NULL)
		bracketbuf[0] = oc[0];
	sep = bracketbuf;
	for (; db->db_name != NULL; db++) {
		if ((value & db->db_mask) == db->db_match) {
			sbuf_printf(sb, "%s%s", sep, db->db_name);
			sep = ",";
			printed = true;

			/*
			 * Clear the field, and make sure we
			 * won't match a zero-valued field with
			 * this same mask.
			 */
			value &= ~db->db_mask;
			while (db[1].db_mask == db->db_mask &&
			    db[1].db_name != NULL)
				db++;
		}
	}
	if (value != 0) {
		sbuf_printf(sb, "%s?0x%" PRIx64, sep, value);
		printed = true;
	}
	if (printed && oc != NULL) {
		bracketbuf[0] = oc[1];
		sbuf_cat(sb, bracketbuf);
	}
	return (printed);
}

/*
 * Show file ID.
 */
static void
l9p_describe_fid(const char *str, uint32_t fid, struct sbuf *sb)
{

	sbuf_printf(sb, "%s%" PRIu32, str, fid);
}

/*
 * Show user or group ID.
 */
static void
l9p_describe_ugid(const char *str, uint32_t ugid, struct sbuf *sb)
{

	sbuf_printf(sb, "%s%" PRIu32, str, ugid);
}

/*
 * Show file mode (O_RDWR, O_RDONLY, etc).  The argument is
 * an l9p_omode, not a Linux flags mode.  Linux flags are
 * decoded with l9p_describe_lflags.
 */
static void
l9p_describe_mode(const char *str, uint32_t mode, struct sbuf *sb)
{
	static const struct descbits bits[] = {
		{ L9P_OACCMODE,	L9P_OREAD,	"OREAD" },
		{ L9P_OACCMODE,	L9P_OWRITE,	"OWRITE" },
		{ L9P_OACCMODE,	L9P_ORDWR,	"ORDWR" },
		{ L9P_OACCMODE,	L9P_OEXEC,	"OEXEC" },

		{ L9P_OCEXEC,	L9P_OCEXEC,	"OCEXEC" },
		{ L9P_ODIRECT,	L9P_ODIRECT,	"ODIRECT" },
		{ L9P_ORCLOSE,	L9P_ORCLOSE,	"ORCLOSE" },
		{ L9P_OTRUNC,	L9P_OTRUNC,	"OTRUNC" },
		{ 0, 0, NULL }
	};

	(void) l9p_describe_bits(str, mode, "[]", bits, sb);
}

/*
 * Show Linux mode/flags.
 */
static void
l9p_describe_lflags(const char *str, uint32_t flags, struct sbuf *sb)
{
	static const struct descbits bits[] = {
	    { L9P_OACCMODE,	L9P_OREAD,		"O_READ" },
	    { L9P_OACCMODE,	L9P_OWRITE,		"O_WRITE" },
	    { L9P_OACCMODE,	L9P_ORDWR,		"O_RDWR" },
	    { L9P_OACCMODE,	L9P_OEXEC,		"O_EXEC" },

	    { L9P_L_O_APPEND,	L9P_L_O_APPEND,		"O_APPEND" },
	    { L9P_L_O_CLOEXEC,	L9P_L_O_CLOEXEC,	"O_CLOEXEC" },
	    { L9P_L_O_CREAT,	L9P_L_O_CREAT,		"O_CREAT" },
	    { L9P_L_O_DIRECT,	L9P_L_O_DIRECT,		"O_DIRECT" },
	    { L9P_L_O_DIRECTORY, L9P_L_O_DIRECTORY,	"O_DIRECTORY" },
	    { L9P_L_O_DSYNC,	L9P_L_O_DSYNC,		"O_DSYNC" },
	    { L9P_L_O_EXCL,	L9P_L_O_EXCL,		"O_EXCL" },
	    { L9P_L_O_FASYNC,	L9P_L_O_FASYNC,		"O_FASYNC" },
	    { L9P_L_O_LARGEFILE, L9P_L_O_LARGEFILE,	"O_LARGEFILE" },
	    { L9P_L_O_NOATIME,	L9P_L_O_NOATIME,	"O_NOATIME" },
	    { L9P_L_O_NOCTTY,	L9P_L_O_NOCTTY,		"O_NOCTTY" },
	    { L9P_L_O_NOFOLLOW,	L9P_L_O_NOFOLLOW,	"O_NOFOLLOW" },
	    { L9P_L_O_NONBLOCK,	L9P_L_O_NONBLOCK,	"O_NONBLOCK" },
	    { L9P_L_O_PATH,	L9P_L_O_PATH,		"O_PATH" },
	    { L9P_L_O_SYNC,	L9P_L_O_SYNC,		"O_SYNC" },
	    { L9P_L_O_TMPFILE,	L9P_L_O_TMPFILE,	"O_TMPFILE" },
	    { L9P_L_O_TMPFILE,	L9P_L_O_TMPFILE,	"O_TMPFILE" },
	    { L9P_L_O_TRUNC,	L9P_L_O_TRUNC,		"O_TRUNC" },
	    { 0, 0, NULL }
	};

	(void) l9p_describe_bits(str, flags, "[]", bits, sb);
}

/*
 * Show file name or other similar, potentially-very-long string.
 * Actual strings get quotes, a NULL name (if it occurs) gets
 * <null> (no quotes), so you can tell the difference.
 */
static void
l9p_describe_name(const char *str, char *name, struct sbuf *sb)
{
	size_t len;

	if (name == NULL) {
		sbuf_printf(sb, "%s<null>", str);
		return;
	}

	len = strlen(name);

	if (len > 32)
		sbuf_printf(sb, "%s\"%.*s...\"", str, 32 - 3, name);
	else
		sbuf_printf(sb, "%s\"%.*s\"", str, (int)len, name);
}

/*
 * Show permissions (rwx etc).  Prints the value in hex only if
 * the rwx bits do not cover the entire value.
 */
static void
l9p_describe_perm(const char *str, uint32_t mode, struct sbuf *sb)
{
	char pbuf[12];

	strmode(mode & 0777, pbuf);
	if ((mode & ~(uint32_t)0777) != 0)
		sbuf_printf(sb, "%s0x%" PRIx32 "<%.9s>", str, mode, pbuf + 1);
	else
		sbuf_printf(sb, "%s<%.9s>", str, pbuf + 1);
}

/*
 * Show "extended" permissions: regular permissions, but also the
 * various DM* extension bits from 9P2000.u.
 */
static void
l9p_describe_ext_perm(const char *str, uint32_t mode, struct sbuf *sb)
{
	static const struct descbits bits[] = {
		{ L9P_DMDIR,	L9P_DMDIR,	"DMDIR" },
		{ L9P_DMAPPEND,	L9P_DMAPPEND,	"DMAPPEND" },
		{ L9P_DMEXCL,	L9P_DMEXCL,	"DMEXCL" },
		{ L9P_DMMOUNT,	L9P_DMMOUNT,	"DMMOUNT" },
		{ L9P_DMAUTH,	L9P_DMAUTH,	"DMAUTH" },
		{ L9P_DMTMP,	L9P_DMTMP,	"DMTMP" },
		{ L9P_DMSYMLINK, L9P_DMSYMLINK,	"DMSYMLINK" },
		{ L9P_DMDEVICE,	L9P_DMDEVICE,	"DMDEVICE" },
		{ L9P_DMNAMEDPIPE, L9P_DMNAMEDPIPE, "DMNAMEDPIPE" },
		{ L9P_DMSOCKET,	L9P_DMSOCKET,	"DMSOCKET" },
		{ L9P_DMSETUID,	L9P_DMSETUID,	"DMSETUID" },
		{ L9P_DMSETGID,	L9P_DMSETGID,	"DMSETGID" },
		{ 0, 0, NULL }
	};
	bool need_sep;

	sbuf_printf(sb, "%s[", str);
	need_sep = l9p_describe_bits(NULL, mode & ~(uint32_t)0777, NULL,
	    bits, sb);
	l9p_describe_perm(need_sep ? "," : "", mode & 0777, sb);
	sbuf_cat(sb, "]");
}

/*
 * Show Linux-specific permissions: regular permissions, but also
 * the S_IFMT field.
 */
static void
l9p_describe_lperm(const char *str, uint32_t mode, struct sbuf *sb)
{
	static const struct descbits bits[] = {
		{ S_IFMT,	S_IFIFO,	"S_IFIFO" },
		{ S_IFMT,	S_IFCHR,	"S_IFCHR" },
		{ S_IFMT,	S_IFDIR,	"S_IFDIR" },
		{ S_IFMT,	S_IFBLK,	"S_IFBLK" },
		{ S_IFMT,	S_IFREG,	"S_IFREG" },
		{ S_IFMT,	S_IFLNK,	"S_IFLNK" },
		{ S_IFMT,	S_IFSOCK,	"S_IFSOCK" },
		{ 0, 0, NULL }
	};
	bool need_sep;

	sbuf_printf(sb, "%s[", str);
	need_sep = l9p_describe_bits(NULL, mode & ~(uint32_t)0777, NULL,
	    bits, sb);
	l9p_describe_perm(need_sep ? "," : "", mode & 0777, sb);
	sbuf_cat(sb, "]");
}

/*
 * Show qid (<type, version, path> tuple).
 */
static void
l9p_describe_qid(const char *str, struct l9p_qid *qid, struct sbuf *sb)
{
	static const struct descbits bits[] = {
		/*
		 * NB: L9P_QTFILE is 0, i.e., is implied by no
		 * other bits being set.  We get this produced
		 * when we mask against 0xff and compare for
		 * L9P_QTFILE, but we must do it first so that
		 * we mask against the original (not-adjusted)
		 * value.
		 */
		{ 0xff,		L9P_QTFILE,	"FILE" },
		{ L9P_QTDIR,	L9P_QTDIR,	"DIR" },
		{ L9P_QTAPPEND,	L9P_QTAPPEND,	"APPEND" },
		{ L9P_QTEXCL,	L9P_QTEXCL,	"EXCL" },
		{ L9P_QTMOUNT,	L9P_QTMOUNT,	"MOUNT" },
		{ L9P_QTAUTH,	L9P_QTAUTH,	"AUTH" },
		{ L9P_QTTMP,	L9P_QTTMP,	"TMP" },
		{ L9P_QTSYMLINK, L9P_QTSYMLINK,	"SYMLINK" },
		{ 0, 0, NULL }
	};

	assert(qid != NULL);

	sbuf_cat(sb, str);
	(void) l9p_describe_bits("<", qid->type, "[]", bits, sb);
	sbuf_printf(sb, ",%" PRIu32 ",0x%016" PRIx64 ">",
	    qid->version, qid->path);
}

/*
 * Show size.
 */
static void
l9p_describe_size(const char *str, uint64_t size, struct sbuf *sb)
{

	sbuf_printf(sb, "%s%" PRIu64, str, size);
}

/*
 * Show l9stat (including 9P2000.u extensions if appropriate).
 */
static void
l9p_describe_l9stat(const char *str, struct l9p_stat *st,
    enum l9p_version version, struct sbuf *sb)
{
	bool dotu = version >= L9P_2000U;

	assert(st != NULL);

	sbuf_printf(sb, "%stype=0x%04" PRIx32 " dev=0x%08" PRIx32, str,
	    st->type, st->dev);
	l9p_describe_qid(" qid=", &st->qid, sb);
	l9p_describe_ext_perm(" mode=", st->mode, sb);
	if (st->atime != (uint32_t)-1)
		sbuf_printf(sb, " atime=%" PRIu32, st->atime);
	if (st->mtime != (uint32_t)-1)
		sbuf_printf(sb, " mtime=%" PRIu32, st->mtime);
	if (st->length != (uint64_t)-1)
		sbuf_printf(sb, " length=%" PRIu64, st->length);
	l9p_describe_name(" name=", st->name, sb);
	/*
	 * It's pretty common to have NULL name+gid+muid.  They're
	 * just noise if NULL *and* dot-u; decode only if non-null
	 * or not-dot-u.
	 */
	if (st->uid != NULL || !dotu)
		l9p_describe_name(" uid=", st->uid, sb);
	if (st->gid != NULL || !dotu)
		l9p_describe_name(" gid=", st->gid, sb);
	if (st->muid != NULL || !dotu)
		l9p_describe_name(" muid=", st->muid, sb);
	if (dotu) {
		if (st->extension != NULL)
			l9p_describe_name(" extension=", st->extension, sb);
		sbuf_printf(sb,
		    " n_uid=%" PRIu32 " n_gid=%" PRIu32 " n_muid=%" PRIu32,
		    st->n_uid, st->n_gid, st->n_muid);
	}
}

static void
l9p_describe_statfs(const char *str, struct l9p_statfs *st, struct sbuf *sb)
{

	assert(st != NULL);

	sbuf_printf(sb, "%stype=0x%04lx bsize=%lu blocks=%" PRIu64
	    " bfree=%" PRIu64 " bavail=%" PRIu64 " files=%" PRIu64
	    " ffree=%" PRIu64 " fsid=0x%" PRIx64 " namelen=%" PRIu32 ">",
	    str, (u_long)st->type, (u_long)st->bsize, st->blocks,
	    st->bfree, st->bavail, st->files,
	    st->ffree, st->fsid, st->namelen);
}

/*
 * Decode a <seconds,nsec> timestamp.
 *
 * Perhaps should use asctime_r.  For now, raw values.
 */
static void
l9p_describe_time(struct sbuf *sb, const char *s, uint64_t sec, uint64_t nsec)
{

	sbuf_cat(sb, s);
	if (nsec > 999999999)
		sbuf_printf(sb, "%" PRIu64 ".<invalid nsec %" PRIu64 ">)",
		    sec, nsec);
	else
		sbuf_printf(sb, "%" PRIu64 ".%09" PRIu64, sec, nsec);
}

/*
 * Decode readdir data (.L format, variable length names).
 */
static void
l9p_describe_readdir(struct sbuf *sb, struct l9p_f_io *io)
{
	uint32_t count;
#ifdef notyet
	int i;
	struct l9p_message msg;
	struct l9p_dirent de;
#endif

	if ((count = io->count) == 0) {
		sbuf_printf(sb, " EOF (count=0)");
		return;
	}

	/*
	 * Can't do this yet because we do not have the original
	 * req.
	 */
#ifdef notyet
	sbuf_printf(sb, " count=%" PRIu32 " [", count);

	l9p_init_msg(&msg, req, L9P_UNPACK);
	for (i = 0; msg.lm_size < count; i++) {
		if (l9p_pudirent(&msg, &de) < 0) {
			sbuf_printf(sb, " bad count");
			break;
		}

		sbuf_printf(sb, i ? ", " : " ");
		l9p_describe_qid(" qid=", &de.qid, sb);
		sbuf_printf(sb, " offset=%" PRIu64 " type=%d",
		    de.offset, de.type);
		l9p_describe_name(" name=", de.name);
		free(de.name);
	}
	sbuf_printf(sb, "]=%d dir entries", i);
#else /* notyet */
	sbuf_printf(sb, " count=%" PRIu32, count);
#endif
}

/*
 * Decode Tgetattr request_mask field.
 */
static void
l9p_describe_getattr_mask(uint64_t request_mask, struct sbuf *sb)
{
	static const struct descbits bits[] = {
		/*
		 * Note: ALL and BASIC must occur first and second.
		 * This is a little dirty: it depends on the way the
		 * describe_bits code clears the values.  If we
		 * match ALL, we clear all those bits and do not
		 * match BASIC; if we match BASIC, we clear all
		 * those bits and do not match individual bits.  Thus
		 * if we have BASIC but not all the additional bits,
		 * we'll see, e.g., [BASIC,BTIME,GEN]; if we have
		 * all the additional bits too, we'll see [ALL].
		 *
		 * Since <undec> is true below, we'll also spot any
		 * bits added to the protocol since we made this table.
		 */
		{ L9PL_GETATTR_ALL,	L9PL_GETATTR_ALL,	"ALL" },
		{ L9PL_GETATTR_BASIC,	L9PL_GETATTR_BASIC,	"BASIC" },

		/* individual bits in BASIC */
		{ L9PL_GETATTR_MODE,	L9PL_GETATTR_MODE,	"MODE" },
		{ L9PL_GETATTR_NLINK,	L9PL_GETATTR_NLINK,	"NLINK" },
		{ L9PL_GETATTR_UID,	L9PL_GETATTR_UID,	"UID" },
		{ L9PL_GETATTR_GID,	L9PL_GETATTR_GID,	"GID" },
		{ L9PL_GETATTR_RDEV,	L9PL_GETATTR_RDEV,	"RDEV" },
		{ L9PL_GETATTR_ATIME,	L9PL_GETATTR_ATIME,	"ATIME" },
		{ L9PL_GETATTR_MTIME,	L9PL_GETATTR_MTIME,	"MTIME" },
		{ L9PL_GETATTR_CTIME,	L9PL_GETATTR_CTIME,	"CTIME" },
		{ L9PL_GETATTR_INO,	L9PL_GETATTR_INO,	"INO" },
		{ L9PL_GETATTR_SIZE,	L9PL_GETATTR_SIZE,	"SIZE" },
		{ L9PL_GETATTR_BLOCKS,	L9PL_GETATTR_BLOCKS,	"BLOCKS" },

		/* additional bits in ALL */
		{ L9PL_GETATTR_BTIME,	L9PL_GETATTR_BTIME,	"BTIME" },
		{ L9PL_GETATTR_GEN,	L9PL_GETATTR_GEN,	"GEN" },
		{ L9PL_GETATTR_DATA_VERSION, L9PL_GETATTR_DATA_VERSION,
							"DATA_VERSION" },
		{ 0, 0, NULL }
	};

	(void) l9p_describe_bits(" request_mask=", request_mask, "[]", bits,
	    sb);
}

/*
 * Decode Tunlinkat flags.
 */
static void
l9p_describe_unlinkat_flags(const char *str, uint32_t flags, struct sbuf *sb)
{
	static const struct descbits bits[] = {
		{ L9PL_AT_REMOVEDIR, L9PL_AT_REMOVEDIR, "AT_REMOVEDIR" },
		{ 0, 0, NULL }
	};

	(void) l9p_describe_bits(str, flags, "[]", bits, sb);
}

static const char *
lookup_linux_errno(uint32_t linux_errno)
{
	static char unknown[50];

	/*
	 * Error numbers in the "base" range (1..ERANGE) are common
	 * across BSD, MacOS, Linux, and Plan 9.
	 *
	 * Error numbers outside that range require translation.
	 */
	const char *const table[] = {
#define X0(name) [name] = name ## _STR
#define	X(name) [name] = name ## _STR
		X(LINUX_EAGAIN),
		X(LINUX_EDEADLK),
		X(LINUX_ENAMETOOLONG),
		X(LINUX_ENOLCK),
		X(LINUX_ENOSYS),
		X(LINUX_ENOTEMPTY),
		X(LINUX_ELOOP),
		X(LINUX_ENOMSG),
		X(LINUX_EIDRM),
		X(LINUX_ECHRNG),
		X(LINUX_EL2NSYNC),
		X(LINUX_EL3HLT),
		X(LINUX_EL3RST),
		X(LINUX_ELNRNG),
		X(LINUX_EUNATCH),
		X(LINUX_ENOCSI),
		X(LINUX_EL2HLT),
		X(LINUX_EBADE),
		X(LINUX_EBADR),
		X(LINUX_EXFULL),
		X(LINUX_ENOANO),
		X(LINUX_EBADRQC),
		X(LINUX_EBADSLT),
		X(LINUX_EBFONT),
		X(LINUX_ENOSTR),
		X(LINUX_ENODATA),
		X(LINUX_ETIME),
		X(LINUX_ENOSR),
		X(LINUX_ENONET),
		X(LINUX_ENOPKG),
		X(LINUX_EREMOTE),
		X(LINUX_ENOLINK),
		X(LINUX_EADV),
		X(LINUX_ESRMNT),
		X(LINUX_ECOMM),
		X(LINUX_EPROTO),
		X(LINUX_EMULTIHOP),
		X(LINUX_EDOTDOT),
		X(LINUX_EBADMSG),
		X(LINUX_EOVERFLOW),
		X(LINUX_ENOTUNIQ),
		X(LINUX_EBADFD),
		X(LINUX_EREMCHG),
		X(LINUX_ELIBACC),
		X(LINUX_ELIBBAD),
		X(LINUX_ELIBSCN),
		X(LINUX_ELIBMAX),
		X(LINUX_ELIBEXEC),
		X(LINUX_EILSEQ),
		X(LINUX_ERESTART),
		X(LINUX_ESTRPIPE),
		X(LINUX_EUSERS),
		X(LINUX_ENOTSOCK),
		X(LINUX_EDESTADDRREQ),
		X(LINUX_EMSGSIZE),
		X(LINUX_EPROTOTYPE),
		X(LINUX_ENOPROTOOPT),
		X(LINUX_EPROTONOSUPPORT),
		X(LINUX_ESOCKTNOSUPPORT),
		X(LINUX_EOPNOTSUPP),
		X(LINUX_EPFNOSUPPORT),
		X(LINUX_EAFNOSUPPORT),
		X(LINUX_EADDRINUSE),
		X(LINUX_EADDRNOTAVAIL),
		X(LINUX_ENETDOWN),
		X(LINUX_ENETUNREACH),
		X(LINUX_ENETRESET),
		X(LINUX_ECONNABORTED),
		X(LINUX_ECONNRESET),
		X(LINUX_ENOBUFS),
		X(LINUX_EISCONN),
		X(LINUX_ENOTCONN),
		X(LINUX_ESHUTDOWN),
		X(LINUX_ETOOMANYREFS),
		X(LINUX_ETIMEDOUT),
		X(LINUX_ECONNREFUSED),
		X(LINUX_EHOSTDOWN),
		X(LINUX_EHOSTUNREACH),
		X(LINUX_EALREADY),
		X(LINUX_EINPROGRESS),
		X(LINUX_ESTALE),
		X(LINUX_EUCLEAN),
		X(LINUX_ENOTNAM),
		X(LINUX_ENAVAIL),
		X(LINUX_EISNAM),
		X(LINUX_EREMOTEIO),
		X(LINUX_EDQUOT),
		X(LINUX_ENOMEDIUM),
		X(LINUX_EMEDIUMTYPE),
		X(LINUX_ECANCELED),
		X(LINUX_ENOKEY),
		X(LINUX_EKEYEXPIRED),
		X(LINUX_EKEYREVOKED),
		X(LINUX_EKEYREJECTED),
		X(LINUX_EOWNERDEAD),
		X(LINUX_ENOTRECOVERABLE),
		X(LINUX_ERFKILL),
		X(LINUX_EHWPOISON),
#undef X0
#undef X
	};
	if ((size_t)linux_errno < N(table) && table[linux_errno] != NULL)
		return (table[linux_errno]);
	if (linux_errno <= ERANGE)
		return (strerror((int)linux_errno));
	(void) snprintf(unknown, sizeof(unknown),
	    "Unknown error %d", linux_errno);
	return (unknown);
}

void
l9p_describe_fcall(union l9p_fcall *fcall, enum l9p_version version,
    struct sbuf *sb)
{
	uint64_t mask;
	uint8_t type;
	int i;

	assert(fcall != NULL);
	assert(sb != NULL);
	assert(version <= L9P_2000L && version >= L9P_INVALID_VERSION);

	type = fcall->hdr.type;

	if (type < L9P__FIRST || type >= L9P__LAST_PLUS_1 ||
	    ftype_names[type - L9P__FIRST] == NULL) {
		const char *rr;

		/*
		 * Can't say for sure that this distinction --
		 * an even number is a request, an odd one is
		 * a response -- will be maintained forever,
		 * but it's good enough for now.
		 */
		rr = (type & 1) != 0 ? "response" : "request";
		sbuf_printf(sb, "<unknown %s %d> tag=%d", rr, type,
		    fcall->hdr.tag);
	} else {
		sbuf_printf(sb, "%s tag=%d", ftype_names[type - L9P__FIRST],
		    fcall->hdr.tag);
	}

	switch (type) {
	case L9P_TVERSION:
	case L9P_RVERSION:
		sbuf_printf(sb, " version=\"%s\" msize=%d", fcall->version.version,
		    fcall->version.msize);
		return;

	case L9P_TAUTH:
		l9p_describe_fid(" afid=", fcall->hdr.fid, sb);
		sbuf_printf(sb, " uname=\"%s\" aname=\"%s\"",
		    fcall->tauth.uname, fcall->tauth.aname);
		return;

	case L9P_TATTACH:
		l9p_describe_fid(" fid=", fcall->hdr.fid, sb);
		l9p_describe_fid(" afid=", fcall->tattach.afid, sb);
		sbuf_printf(sb, " uname=\"%s\" aname=\"%s\"",
		    fcall->tattach.uname, fcall->tattach.aname);
		if (version >= L9P_2000U)
			sbuf_printf(sb, " n_uname=%d", fcall->tattach.n_uname);
		return;

	case L9P_RATTACH:
		l9p_describe_qid(" ", &fcall->rattach.qid, sb);
		return;

	case L9P_RERROR:
		sbuf_printf(sb, " ename=\"%s\" errnum=%d", fcall->error.ename,
		    fcall->error.errnum);
		return;

	case L9P_RLERROR:
		sbuf_printf(sb, " errnum=%d (%s)", fcall->error.errnum,
		    lookup_linux_errno(fcall->error.errnum));
		return;

	case L9P_TFLUSH:
		sbuf_printf(sb, " oldtag=%d", fcall->tflush.oldtag);
		return;

	case L9P_RFLUSH:
		return;

	case L9P_TWALK:
		l9p_describe_fid(" fid=", fcall->hdr.fid, sb);
		l9p_describe_fid(" newfid=", fcall->twalk.newfid, sb);
		if (fcall->twalk.nwname) {
			sbuf_cat(sb, " wname=\"");
			for (i = 0; i < fcall->twalk.nwname; i++)
				sbuf_printf(sb, "%s%s", i == 0 ? "" : "/",
				    fcall->twalk.wname[i]);
			sbuf_cat(sb, "\"");
		}
		return;

	case L9P_RWALK:
		sbuf_printf(sb, " wqid=[");
		for (i = 0; i < fcall->rwalk.nwqid; i++)
			l9p_describe_qid(i == 0 ? "" : ",",
			    &fcall->rwalk.wqid[i], sb);
		sbuf_cat(sb, "]");
		return;

	case L9P_TOPEN:
		l9p_describe_fid(" fid=", fcall->hdr.fid, sb);
		l9p_describe_mode(" mode=", fcall->tcreate.mode, sb);
		return;

	case L9P_ROPEN:
		l9p_describe_qid(" qid=", &fcall->ropen.qid, sb);
		sbuf_printf(sb, " iounit=%d", fcall->ropen.iounit);
		return;

	case L9P_TCREATE:
		l9p_describe_fid(" fid=", fcall->hdr.fid, sb);
		l9p_describe_name(" name=", fcall->tcreate.name, sb);
		l9p_describe_ext_perm(" perm=", fcall->tcreate.perm, sb);
		l9p_describe_mode(" mode=", fcall->tcreate.mode, sb);
		if (version >= L9P_2000U && fcall->tcreate.extension != NULL)
			l9p_describe_name(" extension=",
			    fcall->tcreate.extension, sb);
		return;

	case L9P_RCREATE:
		l9p_describe_qid(" qid=", &fcall->rcreate.qid, sb);
		sbuf_printf(sb, " iounit=%d", fcall->rcreate.iounit);
		return;

	case L9P_TREAD:
		l9p_describe_fid(" fid=", fcall->hdr.fid, sb);
		sbuf_printf(sb, " offset=%" PRIu64 " count=%" PRIu32,
		    fcall->io.offset, fcall->io.count);
		return;

	case L9P_RREAD:
	case L9P_RWRITE:
		sbuf_printf(sb, " count=%" PRIu32, fcall->io.count);
		return;

	case L9P_TWRITE:
	case L9P_TREADDIR:
		l9p_describe_fid(" fid=", fcall->hdr.fid, sb);
		sbuf_printf(sb, " offset=%" PRIu64 " count=%" PRIu32,
		    fcall->io.offset, fcall->io.count);
		return;

	case L9P_TCLUNK:
		l9p_describe_fid(" fid=", fcall->hdr.fid, sb);
		return;

	case L9P_RCLUNK:
		return;

	case L9P_TREMOVE:
		l9p_describe_fid(" fid=", fcall->hdr.fid, sb);
		return;

	case L9P_RREMOVE:
		return;

	case L9P_TSTAT:
		l9p_describe_fid(" fid=", fcall->hdr.fid, sb);
		return;

	case L9P_RSTAT:
		l9p_describe_l9stat(" ", &fcall->rstat.stat, version, sb);
		return;

	case L9P_TWSTAT:
		l9p_describe_fid(" fid=", fcall->hdr.fid, sb);
		l9p_describe_l9stat(" ", &fcall->twstat.stat, version, sb);
		return;

	case L9P_RWSTAT:
		return;

	case L9P_TSTATFS:
		l9p_describe_fid(" fid=", fcall->hdr.fid, sb);
		return;

	case L9P_RSTATFS:
		l9p_describe_statfs(" ", &fcall->rstatfs.statfs, sb);
		return;

	case L9P_TLOPEN:
		l9p_describe_fid(" fid=", fcall->hdr.fid, sb);
		l9p_describe_lflags(" flags=", fcall->tlcreate.flags, sb);
		return;

	case L9P_RLOPEN:
		l9p_describe_qid(" qid=", &fcall->rlopen.qid, sb);
		sbuf_printf(sb, " iounit=%d", fcall->rlopen.iounit);
		return;

	case L9P_TLCREATE:
		l9p_describe_fid(" fid=", fcall->hdr.fid, sb);
		l9p_describe_name(" name=", fcall->tlcreate.name, sb);
		/* confusing: "flags" is open-mode, "mode" is permissions */
		l9p_describe_lflags(" flags=", fcall->tlcreate.flags, sb);
		/* TLCREATE mode/permissions have S_IFREG (0x8000) set */
		l9p_describe_lperm(" mode=", fcall->tlcreate.mode, sb);
		l9p_describe_ugid(" gid=", fcall->tlcreate.gid, sb);
		return;

	case L9P_RLCREATE:
		l9p_describe_qid(" qid=", &fcall->rlcreate.qid, sb);
		sbuf_printf(sb, " iounit=%d", fcall->rlcreate.iounit);
		return;

	case L9P_TSYMLINK:
		l9p_describe_fid(" fid=", fcall->hdr.fid, sb);
		l9p_describe_name(" name=", fcall->tsymlink.name, sb);
		l9p_describe_name(" symtgt=", fcall->tsymlink.symtgt, sb);
		l9p_describe_ugid(" gid=", fcall->tsymlink.gid, sb);
		return;

	case L9P_RSYMLINK:
		l9p_describe_qid(" qid=", &fcall->ropen.qid, sb);
		return;

	case L9P_TMKNOD:
		l9p_describe_fid(" dfid=", fcall->hdr.fid, sb);
		l9p_describe_name(" name=", fcall->tmknod.name, sb);
		/*
		 * TMKNOD mode/permissions have S_IFBLK/S_IFCHR/S_IFIFO
		 * bits.  The major and minor values are only meaningful
		 * for S_IFBLK and S_IFCHR, but just decode always here.
		 */
		l9p_describe_lperm(" mode=", fcall->tmknod.mode, sb);
		sbuf_printf(sb, " major=%u minor=%u",
		    fcall->tmknod.major, fcall->tmknod.minor);
		l9p_describe_ugid(" gid=", fcall->tmknod.gid, sb);
		return;

	case L9P_RMKNOD:
		l9p_describe_qid(" qid=", &fcall->rmknod.qid, sb);
		return;

	case L9P_TRENAME:
		l9p_describe_fid(" fid=", fcall->hdr.fid, sb);
		l9p_describe_fid(" dfid=", fcall->trename.dfid, sb);
		l9p_describe_name(" name=", fcall->trename.name, sb);
		return;

	case L9P_RRENAME:
		return;

	case L9P_TREADLINK:
		l9p_describe_fid(" fid=", fcall->hdr.fid, sb);
		return;

	case L9P_RREADLINK:
		l9p_describe_name(" target=", fcall->rreadlink.target, sb);
		return;

	case L9P_TGETATTR:
		l9p_describe_fid(" fid=", fcall->hdr.fid, sb);
		l9p_describe_getattr_mask(fcall->tgetattr.request_mask, sb);
		return;

	case L9P_RGETATTR:
		/* Don't need to decode bits: they're implied by the output */
		mask = fcall->rgetattr.valid;
		sbuf_printf(sb, " valid=0x%016" PRIx64, mask);
		l9p_describe_qid(" qid=", &fcall->rgetattr.qid, sb);
		if (mask & L9PL_GETATTR_MODE)
			l9p_describe_lperm(" mode=", fcall->rgetattr.mode, sb);
		if (mask & L9PL_GETATTR_UID)
			l9p_describe_ugid(" uid=", fcall->rgetattr.uid, sb);
		if (mask & L9PL_GETATTR_GID)
			l9p_describe_ugid(" gid=", fcall->rgetattr.gid, sb);
		if (mask & L9PL_GETATTR_NLINK)
			sbuf_printf(sb, " nlink=%" PRIu64,
			    fcall->rgetattr.nlink);
		if (mask & L9PL_GETATTR_RDEV)
			sbuf_printf(sb, " rdev=0x%" PRIx64,
			    fcall->rgetattr.rdev);
		if (mask & L9PL_GETATTR_SIZE)
			l9p_describe_size(" size=", fcall->rgetattr.size, sb);
		if (mask & L9PL_GETATTR_BLOCKS)
			sbuf_printf(sb, " blksize=%" PRIu64 " blocks=%" PRIu64,
			    fcall->rgetattr.blksize, fcall->rgetattr.blocks);
		if (mask & L9PL_GETATTR_ATIME)
			l9p_describe_time(sb, " atime=",
			    fcall->rgetattr.atime_sec,
			    fcall->rgetattr.atime_nsec);
		if (mask & L9PL_GETATTR_MTIME)
			l9p_describe_time(sb, " mtime=",
			    fcall->rgetattr.mtime_sec,
			    fcall->rgetattr.mtime_nsec);
		if (mask & L9PL_GETATTR_CTIME)
			l9p_describe_time(sb, " ctime=",
			    fcall->rgetattr.ctime_sec,
			    fcall->rgetattr.ctime_nsec);
		if (mask & L9PL_GETATTR_BTIME)
			l9p_describe_time(sb, " btime=",
			    fcall->rgetattr.btime_sec,
			    fcall->rgetattr.btime_nsec);
		if (mask & L9PL_GETATTR_GEN)
			sbuf_printf(sb, " gen=0x%" PRIx64, fcall->rgetattr.gen);
		if (mask & L9PL_GETATTR_DATA_VERSION)
			sbuf_printf(sb, " data_version=0x%" PRIx64,
			    fcall->rgetattr.data_version);
		return;

	case L9P_TSETATTR:
		/* As with RGETATTR, we'll imply decode via output. */
		l9p_describe_fid(" fid=", fcall->hdr.fid, sb);
		mask = fcall->tsetattr.valid;
		/* NB: tsetattr valid mask is only 32 bits, hence %08x */
		sbuf_printf(sb, " valid=0x%08" PRIx64, mask);
		if (mask & L9PL_SETATTR_MODE)
			l9p_describe_lperm(" mode=", fcall->tsetattr.mode, sb);
		if (mask & L9PL_SETATTR_UID)
			l9p_describe_ugid(" uid=", fcall->tsetattr.uid, sb);
		if (mask & L9PL_SETATTR_GID)
			l9p_describe_ugid(" uid=", fcall->tsetattr.gid, sb);
		if (mask & L9PL_SETATTR_SIZE)
			l9p_describe_size(" size=", fcall->tsetattr.size, sb);
		if (mask & L9PL_SETATTR_ATIME) {
			if (mask & L9PL_SETATTR_ATIME_SET)
				l9p_describe_time(sb, " atime=",
				    fcall->tsetattr.atime_sec,
				    fcall->tsetattr.atime_nsec);
			else
				sbuf_cat(sb, " atime=now");
		}
		if (mask & L9PL_SETATTR_MTIME) {
			if (mask & L9PL_SETATTR_MTIME_SET)
				l9p_describe_time(sb, " mtime=",
				    fcall->tsetattr.mtime_sec,
				    fcall->tsetattr.mtime_nsec);
			else
				sbuf_cat(sb, " mtime=now");
		}
		if (mask & L9PL_SETATTR_CTIME)
			sbuf_cat(sb, " ctime=now");
		return;

	case L9P_RSETATTR:
		return;

	case L9P_TXATTRWALK:
		l9p_describe_fid(" fid=", fcall->hdr.fid, sb);
		l9p_describe_fid(" newfid=", fcall->txattrwalk.newfid, sb);
		l9p_describe_name(" name=", fcall->txattrwalk.name, sb);
		return;

	case L9P_RXATTRWALK:
		l9p_describe_size(" size=", fcall->rxattrwalk.size, sb);
		return;

	case L9P_TXATTRCREATE:
		l9p_describe_fid(" fid=", fcall->hdr.fid, sb);
		l9p_describe_name(" name=", fcall->txattrcreate.name, sb);
		l9p_describe_size(" size=", fcall->txattrcreate.attr_size, sb);
		sbuf_printf(sb, " flags=%" PRIu32, fcall->txattrcreate.flags);
		return;

	case L9P_RXATTRCREATE:
		return;

	case L9P_RREADDIR:
		l9p_describe_readdir(sb, &fcall->io);
		return;

	case L9P_TFSYNC:
		l9p_describe_fid(" fid=", fcall->hdr.fid, sb);
		return;

	case L9P_RFSYNC:
		return;

	case L9P_TLOCK:
		l9p_describe_fid(" fid=", fcall->hdr.fid, sb);
		/* decode better later */
		sbuf_printf(sb, " type=%d flags=0x%" PRIx32
		    " start=%" PRIu64 " length=%" PRIu64
		    " proc_id=0x%" PRIx32 " client_id=\"%s\"",
		    fcall->tlock.type, fcall->tlock.flags,
		    fcall->tlock.start, fcall->tlock.length,
		    fcall->tlock.proc_id, fcall->tlock.client_id);
		return;

	case L9P_RLOCK:
		sbuf_printf(sb, " status=%d", fcall->rlock.status);
		return;

	case L9P_TGETLOCK:
		l9p_describe_fid(" fid=", fcall->hdr.fid, sb);
		/* FALLTHROUGH */

	case L9P_RGETLOCK:
		/* decode better later */
		sbuf_printf(sb, " type=%d "
		    " start=%" PRIu64 " length=%" PRIu64
		    " proc_id=0x%" PRIx32 " client_id=\"%s\"",
		    fcall->getlock.type,
		    fcall->getlock.start, fcall->getlock.length,
		    fcall->getlock.proc_id, fcall->getlock.client_id);
		return;

	case L9P_TLINK:
		l9p_describe_fid(" dfid=", fcall->tlink.dfid, sb);
		l9p_describe_fid(" fid=", fcall->hdr.fid, sb);
		l9p_describe_name(" name=", fcall->tlink.name, sb);
		return;

	case L9P_RLINK:
		return;

	case L9P_TMKDIR:
		l9p_describe_fid(" fid=", fcall->hdr.fid, sb);
		l9p_describe_name(" name=", fcall->tmkdir.name, sb);
		/* TMKDIR mode/permissions have S_IFDIR set */
		l9p_describe_lperm(" mode=", fcall->tmkdir.mode, sb);
		l9p_describe_ugid(" gid=", fcall->tmkdir.gid, sb);
		return;

	case L9P_RMKDIR:
		l9p_describe_qid(" qid=", &fcall->rmkdir.qid, sb);
		return;

	case L9P_TRENAMEAT:
		l9p_describe_fid(" olddirfid=", fcall->hdr.fid, sb);
		l9p_describe_name(" oldname=", fcall->trenameat.oldname,
		    sb);
		l9p_describe_fid(" newdirfid=", fcall->trenameat.newdirfid, sb);
		l9p_describe_name(" newname=", fcall->trenameat.newname,
		    sb);
		return;

	case L9P_RRENAMEAT:
		return;

	case L9P_TUNLINKAT:
		l9p_describe_fid(" dirfd=", fcall->hdr.fid, sb);
		l9p_describe_name(" name=", fcall->tunlinkat.name, sb);
		l9p_describe_unlinkat_flags(" flags=",
		    fcall->tunlinkat.flags, sb);
		return;

	case L9P_RUNLINKAT:
		return;

	default:
		sbuf_printf(sb, " <missing case in %s()>", __func__);
	}
}