/*
 * Copyright (C) 2005-2013 Junjiro R. Okajima
 *
 * This program, aufs is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

/*
 * readdir in userspace.
 */

#include <linux/compat.h>
#include <linux/fs_stack.h>
#include <linux/security.h>
#include "aufs.h"

/* bits for struct aufs_rdu.flags */
#define	AuRdu_CALLED	1
#define	AuRdu_CONT	(1 << 1)
#define	AuRdu_FULL	(1 << 2)
#define au_ftest_rdu(flags, name)	((flags) & AuRdu_##name)
#define au_fset_rdu(flags, name) \
	do { (flags) |= AuRdu_##name; } while (0)
#define au_fclr_rdu(flags, name) \
	do { (flags) &= ~AuRdu_##name; } while (0)

struct au_rdu_arg {
	struct aufs_rdu			*rdu;
	union au_rdu_ent_ul		ent;
	unsigned long			end;

	struct super_block		*sb;
	int				err;
};

static int au_rdu_fill(void *__arg, const char *name, int nlen,
		       loff_t offset, u64 h_ino, unsigned int d_type)
{
	int err, len;
	struct au_rdu_arg *arg = __arg;
	struct aufs_rdu *rdu = arg->rdu;
	struct au_rdu_ent ent;

	err = 0;
	arg->err = 0;
	au_fset_rdu(rdu->cookie.flags, CALLED);
	len = au_rdu_len(nlen);
	if (arg->ent.ul + len  < arg->end) {
		ent.ino = h_ino;
		ent.bindex = rdu->cookie.bindex;
		ent.type = d_type;
		ent.nlen = nlen;
		if (unlikely(nlen > AUFS_MAX_NAMELEN))
			ent.type = DT_UNKNOWN;

		/* unnecessary to support mmap_sem since this is a dir */
		err = -EFAULT;
		if (copy_to_user(arg->ent.e, &ent, sizeof(ent)))
			goto out;
		if (copy_to_user(arg->ent.e->name, name, nlen))
			goto out;
		/* the terminating NULL */
		if (__put_user(0, arg->ent.e->name + nlen))
			goto out;
		err = 0;
		/* AuDbg("%p, %.*s\n", arg->ent.p, nlen, name); */
		arg->ent.ul += len;
		rdu->rent++;
	} else {
		err = -EFAULT;
		au_fset_rdu(rdu->cookie.flags, FULL);
		rdu->full = 1;
		rdu->tail = arg->ent;
	}

out:
	/* AuTraceErr(err); */
	return err;
}

static int au_rdu_do(struct file *h_file, struct au_rdu_arg *arg)
{
	int err;
	loff_t offset;
	struct au_rdu_cookie *cookie = &arg->rdu->cookie;

	/* we don't have to care (FMODE_32BITHASH | FMODE_64BITHASH) for ext4 */
	offset = vfsub_llseek(h_file, cookie->h_pos, SEEK_SET);
	err = offset;
	if (unlikely(offset != cookie->h_pos))
		goto out;

	err = 0;
	do {
		arg->err = 0;
		au_fclr_rdu(cookie->flags, CALLED);
		/* smp_mb(); */
		err = vfsub_readdir(h_file, au_rdu_fill, arg);
		if (err >= 0)
			err = arg->err;
	} while (!err
		 && au_ftest_rdu(cookie->flags, CALLED)
		 && !au_ftest_rdu(cookie->flags, FULL));
	cookie->h_pos = h_file->f_pos;

out:
	AuTraceErr(err);
	return err;
}

static int au_rdu(struct file *file, struct aufs_rdu *rdu)
{
	int err;
	aufs_bindex_t bend;
	struct au_rdu_arg arg;
	struct dentry *dentry;
	struct inode *inode;
	struct file *h_file;
	struct au_rdu_cookie *cookie = &rdu->cookie;

	err = !access_ok(VERIFY_WRITE, rdu->ent.e, rdu->sz);
	if (unlikely(err)) {
		err = -EFAULT;
		AuTraceErr(err);
		goto out;
	}
	rdu->rent = 0;
	rdu->tail = rdu->ent;
	rdu->full = 0;
	arg.rdu = rdu;
	arg.ent = rdu->ent;
	arg.end = arg.ent.ul;
	arg.end += rdu->sz;

	err = -ENOTDIR;
	if (unlikely(!file->f_op || !file->f_op->readdir))
		goto out;

	err = security_file_permission(file, MAY_READ);
	AuTraceErr(err);
	if (unlikely(err))
		goto out;

	dentry = file->f_dentry;
	inode = dentry->d_inode;
#if 1
	mutex_lock(&inode->i_mutex);
#else
	err = mutex_lock_killable(&inode->i_mutex);
	AuTraceErr(err);
	if (unlikely(err))
		goto out;
#endif

	arg.sb = inode->i_sb;
	err = si_read_lock(arg.sb, AuLock_FLUSH | AuLock_NOPLM);
	if (unlikely(err))
		goto out_mtx;
	err = au_alive_dir(dentry);
	if (unlikely(err))
		goto out_si;
	/* todo: reval? */
	fi_read_lock(file);

	err = -EAGAIN;
	if (unlikely(au_ftest_rdu(cookie->flags, CONT)
		     && cookie->generation != au_figen(file)))
		goto out_unlock;

	err = 0;
	if (!rdu->blk) {
		rdu->blk = au_sbi(arg.sb)->si_rdblk;
		if (!rdu->blk)
			rdu->blk = au_dir_size(file, /*dentry*/NULL);
	}
	bend = au_fbstart(file);
	if (cookie->bindex < bend)
		cookie->bindex = bend;
	bend = au_fbend_dir(file);
	/* AuDbg("b%d, b%d\n", cookie->bindex, bend); */
	for (; !err && cookie->bindex <= bend;
	     cookie->bindex++, cookie->h_pos = 0) {
		h_file = au_hf_dir(file, cookie->bindex);
		if (!h_file)
			continue;

		au_fclr_rdu(cookie->flags, FULL);
		err = au_rdu_do(h_file, &arg);
		AuTraceErr(err);
		if (unlikely(au_ftest_rdu(cookie->flags, FULL) || err))
			break;
	}
	AuDbg("rent %llu\n", rdu->rent);

	if (!err && !au_ftest_rdu(cookie->flags, CONT)) {
		rdu->shwh = !!au_opt_test(au_sbi(arg.sb)->si_mntflags, SHWH);
		au_fset_rdu(cookie->flags, CONT);
		cookie->generation = au_figen(file);
	}

	ii_read_lock_child(inode);
	fsstack_copy_attr_atime(inode, au_h_iptr(inode, au_ibstart(inode)));
	ii_read_unlock(inode);

out_unlock:
	fi_read_unlock(file);
out_si:
	si_read_unlock(arg.sb);
out_mtx:
	mutex_unlock(&inode->i_mutex);
out:
	AuTraceErr(err);
	return err;
}

static int au_rdu_ino(struct file *file, struct aufs_rdu *rdu)
{
	int err;
	ino_t ino;
	unsigned long long nent;
	union au_rdu_ent_ul *u;
	struct au_rdu_ent ent;
	struct super_block *sb;

	err = 0;
	nent = rdu->nent;
	u = &rdu->ent;
	sb = file->f_dentry->d_sb;
	si_read_lock(sb, AuLock_FLUSH);
	while (nent-- > 0) {
		/* unnecessary to support mmap_sem since this is a dir */
		err = copy_from_user(&ent, u->e, sizeof(ent));
		if (!err)
			err = !access_ok(VERIFY_WRITE, &u->e->ino, sizeof(ino));
		if (unlikely(err)) {
			err = -EFAULT;
			AuTraceErr(err);
			break;
		}

		/* AuDbg("b%d, i%llu\n", ent.bindex, ent.ino); */
		if (!ent.wh)
			err = au_ino(sb, ent.bindex, ent.ino, ent.type, &ino);
		else
			err = au_wh_ino(sb, ent.bindex, ent.ino, ent.type,
					&ino);
		if (unlikely(err)) {
			AuTraceErr(err);
			break;
		}

		err = __put_user(ino, &u->e->ino);
		if (unlikely(err)) {
			err = -EFAULT;
			AuTraceErr(err);
			break;
		}
		u->ul += au_rdu_len(ent.nlen);
	}
	si_read_unlock(sb);

	return err;
}

/* ---------------------------------------------------------------------- */

static int au_rdu_verify(struct aufs_rdu *rdu)
{
	AuDbg("rdu{%llu, %p, %u | %u | %llu, %u, %u | "
	      "%llu, b%d, 0x%x, g%u}\n",
	      rdu->sz, rdu->ent.e, rdu->verify[AufsCtlRduV_SZ],
	      rdu->blk,
	      rdu->rent, rdu->shwh, rdu->full,
	      rdu->cookie.h_pos, rdu->cookie.bindex, rdu->cookie.flags,
	      rdu->cookie.generation);

	if (rdu->verify[AufsCtlRduV_SZ] == sizeof(*rdu))
		return 0;

	AuDbg("%u:%u\n",
	      rdu->verify[AufsCtlRduV_SZ], (unsigned int)sizeof(*rdu));
	return -EINVAL;
}

long au_rdu_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
	long err, e;
	struct aufs_rdu rdu;
	void __user *p = (void __user *)arg;

	err = copy_from_user(&rdu, p, sizeof(rdu));
	if (unlikely(err)) {
		err = -EFAULT;
		AuTraceErr(err);
		goto out;
	}
	err = au_rdu_verify(&rdu);
	if (unlikely(err))
		goto out;

	switch (cmd) {
	case AUFS_CTL_RDU:
		err = au_rdu(file, &rdu);
		if (unlikely(err))
			break;

		e = copy_to_user(p, &rdu, sizeof(rdu));
		if (unlikely(e)) {
			err = -EFAULT;
			AuTraceErr(err);
		}
		break;
	case AUFS_CTL_RDU_INO:
		err = au_rdu_ino(file, &rdu);
		break;

	default:
		/* err = -ENOTTY; */
		err = -EINVAL;
	}

out:
	AuTraceErr(err);
	return err;
}

#ifdef CONFIG_COMPAT
long au_rdu_compat_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
	long err, e;
	struct aufs_rdu rdu;
	void __user *p = compat_ptr(arg);

	/* todo: get_user()? */
	err = copy_from_user(&rdu, p, sizeof(rdu));
	if (unlikely(err)) {
		err = -EFAULT;
		AuTraceErr(err);
		goto out;
	}
	rdu.ent.e = compat_ptr(rdu.ent.ul);
	err = au_rdu_verify(&rdu);
	if (unlikely(err))
		goto out;

	switch (cmd) {
	case AUFS_CTL_RDU:
		err = au_rdu(file, &rdu);
		if (unlikely(err))
			break;

		rdu.ent.ul = ptr_to_compat(rdu.ent.e);
		rdu.tail.ul = ptr_to_compat(rdu.tail.e);
		e = copy_to_user(p, &rdu, sizeof(rdu));
		if (unlikely(e)) {
			err = -EFAULT;
			AuTraceErr(err);
		}
		break;
	case AUFS_CTL_RDU_INO:
		err = au_rdu_ino(file, &rdu);
		break;

	default:
		/* err = -ENOTTY; */
		err = -EINVAL;
	}

out:
	AuTraceErr(err);
	return err;
}
#endif
