/*
 * Copyright (C) 2014-2015 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, see <http://www.gnu.org/licenses/>.
 */

/*
 * handling xattr functions
 */

#include <linux/xattr.h>
#include "aufs.h"

static int au_xattr_ignore(int err, char *name, unsigned int ignore_flags)
{
	if (!ignore_flags)
		goto out;
	switch (err) {
	case -ENOMEM:
	case -EDQUOT:
		goto out;
	}

	if ((ignore_flags & AuBrAttr_ICEX) == AuBrAttr_ICEX) {
		err = 0;
		goto out;
	}

#define cmp(brattr, prefix) do {					\
		if (!strncmp(name, XATTR_##prefix##_PREFIX,		\
			     XATTR_##prefix##_PREFIX_LEN)) {		\
			if (ignore_flags & AuBrAttr_ICEX_##brattr)	\
				err = 0;				\
			goto out;					\
		}							\
	} while (0)

	cmp(SEC, SECURITY);
	cmp(SYS, SYSTEM);
	cmp(TR, TRUSTED);
	cmp(USR, USER);
#undef cmp

	if (ignore_flags & AuBrAttr_ICEX_OTH)
		err = 0;

out:
	return err;
}

static const int au_xattr_out_of_list = AuBrAttr_ICEX_OTH << 1;

static int au_do_cpup_xattr(struct dentry *h_dst, struct dentry *h_src,
			    char *name, char **buf, unsigned int ignore_flags,
			    unsigned int verbose)
{
	int err;
	ssize_t ssz;
	struct inode *h_idst;

	ssz = vfs_getxattr_alloc(h_src, name, buf, 0, GFP_NOFS);
	err = ssz;
	if (unlikely(err <= 0)) {
		if (err == -ENODATA
		    || (err == -EOPNOTSUPP
			&& ((ignore_flags & au_xattr_out_of_list)
			    || (au_test_nfs_noacl(d_inode(h_src))
				&& (!strcmp(name, XATTR_NAME_POSIX_ACL_ACCESS)
				    || !strcmp(name,
					       XATTR_NAME_POSIX_ACL_DEFAULT))))
			    ))
			err = 0;
		if (err && (verbose || au_debug_test()))
			pr_err("%s, err %d\n", name, err);
		goto out;
	}

	/* unlock it temporary */
	h_idst = d_inode(h_dst);
	mutex_unlock(&h_idst->i_mutex);
	err = vfsub_setxattr(h_dst, name, *buf, ssz, /*flags*/0);
	mutex_lock_nested(&h_idst->i_mutex, AuLsc_I_CHILD2);
	if (unlikely(err)) {
		if (verbose || au_debug_test())
			pr_err("%s, err %d\n", name, err);
		err = au_xattr_ignore(err, name, ignore_flags);
	}

out:
	return err;
}

int au_cpup_xattr(struct dentry *h_dst, struct dentry *h_src, int ignore_flags,
		  unsigned int verbose)
{
	int err, unlocked, acl_access, acl_default;
	ssize_t ssz;
	struct inode *h_isrc, *h_idst;
	char *value, *p, *o, *e;

	/* try stopping to update the source inode while we are referencing */
	/* there should not be the parent-child relationship between them */
	h_isrc = d_inode(h_src);
	h_idst = d_inode(h_dst);
	mutex_unlock(&h_idst->i_mutex);
	mutex_lock_nested(&h_isrc->i_mutex, AuLsc_I_CHILD);
	mutex_lock_nested(&h_idst->i_mutex, AuLsc_I_CHILD2);
	unlocked = 0;

	/* some filesystems don't list POSIX ACL, for example tmpfs */
	ssz = vfs_listxattr(h_src, NULL, 0);
	err = ssz;
	if (unlikely(err < 0)) {
		AuTraceErr(err);
		if (err == -ENODATA
		    || err == -EOPNOTSUPP)
			err = 0;	/* ignore */
		goto out;
	}

	err = 0;
	p = NULL;
	o = NULL;
	if (ssz) {
		err = -ENOMEM;
		p = kmalloc(ssz, GFP_NOFS);
		o = p;
		if (unlikely(!p))
			goto out;
		err = vfs_listxattr(h_src, p, ssz);
	}
	mutex_unlock(&h_isrc->i_mutex);
	unlocked = 1;
	AuDbg("err %d, ssz %zd\n", err, ssz);
	if (unlikely(err < 0))
		goto out_free;

	err = 0;
	e = p + ssz;
	value = NULL;
	acl_access = 0;
	acl_default = 0;
	while (!err && p < e) {
		acl_access |= !strncmp(p, XATTR_NAME_POSIX_ACL_ACCESS,
				       sizeof(XATTR_NAME_POSIX_ACL_ACCESS) - 1);
		acl_default |= !strncmp(p, XATTR_NAME_POSIX_ACL_DEFAULT,
					sizeof(XATTR_NAME_POSIX_ACL_DEFAULT)
					- 1);
		err = au_do_cpup_xattr(h_dst, h_src, p, &value, ignore_flags,
				       verbose);
		p += strlen(p) + 1;
	}
	AuTraceErr(err);
	ignore_flags |= au_xattr_out_of_list;
	if (!err && !acl_access) {
		err = au_do_cpup_xattr(h_dst, h_src,
				       XATTR_NAME_POSIX_ACL_ACCESS, &value,
				       ignore_flags, verbose);
		AuTraceErr(err);
	}
	if (!err && !acl_default) {
		err = au_do_cpup_xattr(h_dst, h_src,
				       XATTR_NAME_POSIX_ACL_DEFAULT, &value,
				       ignore_flags, verbose);
		AuTraceErr(err);
	}

	kfree(value);

out_free:
	kfree(o);
out:
	if (!unlocked)
		mutex_unlock(&h_isrc->i_mutex);
	AuTraceErr(err);
	return err;
}

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

enum {
	AU_XATTR_LIST,
	AU_XATTR_GET
};

struct au_lgxattr {
	int type;
	union {
		struct {
			char	*list;
			size_t	size;
		} list;
		struct {
			const char	*name;
			void		*value;
			size_t		size;
		} get;
	} u;
};

static ssize_t au_lgxattr(struct dentry *dentry, struct au_lgxattr *arg)
{
	ssize_t err;
	struct path h_path;
	struct super_block *sb;

	sb = dentry->d_sb;
	err = si_read_lock(sb, AuLock_FLUSH | AuLock_NOPLM);
	if (unlikely(err))
		goto out;
	err = au_h_path_getattr(dentry, /*force*/1, &h_path);
	if (unlikely(err))
		goto out_si;
	if (unlikely(!h_path.dentry))
		/* illegally overlapped or something */
		goto out_di; /* pretending success */

	/* always topmost entry only */
	switch (arg->type) {
	case AU_XATTR_LIST:
		err = vfs_listxattr(h_path.dentry,
				    arg->u.list.list, arg->u.list.size);
		break;
	case AU_XATTR_GET:
		err = vfs_getxattr(h_path.dentry,
				   arg->u.get.name, arg->u.get.value,
				   arg->u.get.size);
		break;
	}

out_di:
	di_read_unlock(dentry, AuLock_IR);
out_si:
	si_read_unlock(sb);
out:
	AuTraceErr(err);
	return err;
}

ssize_t aufs_listxattr(struct dentry *dentry, char *list, size_t size)
{
	struct au_lgxattr arg = {
		.type = AU_XATTR_LIST,
		.u.list = {
			.list	= list,
			.size	= size
		},
	};

	return au_lgxattr(dentry, &arg);
}

ssize_t aufs_getxattr(struct dentry *dentry, const char *name, void *value,
		      size_t size)
{
	struct au_lgxattr arg = {
		.type = AU_XATTR_GET,
		.u.get = {
			.name	= name,
			.value	= value,
			.size	= size
		},
	};

	return au_lgxattr(dentry, &arg);
}

int aufs_setxattr(struct dentry *dentry, const char *name, const void *value,
		  size_t size, int flags)
{
	struct au_srxattr arg = {
		.type = AU_XATTR_SET,
		.u.set = {
			.name	= name,
			.value	= value,
			.size	= size,
			.flags	= flags
		},
	};

	return au_srxattr(dentry, &arg);
}

int aufs_removexattr(struct dentry *dentry, const char *name)
{
	struct au_srxattr arg = {
		.type = AU_XATTR_REMOVE,
		.u.remove = {
			.name	= name
		},
	};

	return au_srxattr(dentry, &arg);
}

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

#if 0
static size_t au_xattr_list(struct dentry *dentry, char *list, size_t list_size,
			    const char *name, size_t name_len, int type)
{
	return aufs_listxattr(dentry, list, list_size);
}

static int au_xattr_get(struct dentry *dentry, const char *name, void *buffer,
			size_t size, int type)
{
	return aufs_getxattr(dentry, name, buffer, size);
}

static int au_xattr_set(struct dentry *dentry, const char *name,
			const void *value, size_t size, int flags, int type)
{
	return aufs_setxattr(dentry, name, value, size, flags);
}

static const struct xattr_handler au_xattr_handler = {
	/* no prefix, no flags */
	.list	= au_xattr_list,
	.get	= au_xattr_get,
	.set	= au_xattr_set
	/* why no remove? */
};

static const struct xattr_handler *au_xattr_handlers[] = {
	&au_xattr_handler
};

void au_xattr_init(struct super_block *sb)
{
	/* sb->s_xattr = au_xattr_handlers; */
}
#endif