// SPDX-License-Identifier: GPL-2.0-or-later
/* CacheFiles extended attribute management
 *
 * Copyright (C) 2021 Red Hat, Inc. All Rights Reserved.
 * Written by David Howells (dhowells@redhat.com)
 */

#include <linux/module.h>
#include <linux/sched.h>
#include <linux/file.h>
#include <linux/fs.h>
#include <linux/fsnotify.h>
#include <linux/quotaops.h>
#include <linux/xattr.h>
#include <linux/slab.h>
#include "internal.h"

#define CACHEFILES_COOKIE_TYPE_DATA 1

struct cachefiles_xattr {
	__be64	object_size;	/* Actual size of the object */
	__be64	zero_point;	/* Size after which server has no data not written by us */
	__u8	type;		/* Type of object */
	__u8	content;	/* Content presence (enum cachefiles_content) */
	__u8	data[];		/* netfs coherency data */
} __packed;

static const char cachefiles_xattr_cache[] =
	XATTR_USER_PREFIX "CacheFiles.cache";

struct cachefiles_vol_xattr {
	__be32	reserved;	/* Reserved, should be 0 */
	__u8	data[];		/* netfs volume coherency data */
} __packed;

/*
 * set the state xattr on a cache file
 */
int cachefiles_set_object_xattr(struct cachefiles_object *object)
{
	struct cachefiles_xattr *buf;
	struct dentry *dentry;
	struct file *file = object->file;
	unsigned int len = object->cookie->aux_len;
	int ret;

	if (!file)
		return -ESTALE;
	dentry = file->f_path.dentry;

	_enter("%x,#%d", object->debug_id, len);

	buf = kmalloc(sizeof(struct cachefiles_xattr) + len, GFP_KERNEL);
	if (!buf)
		return -ENOMEM;

	buf->object_size	= cpu_to_be64(object->cookie->object_size);
	buf->zero_point		= 0;
	buf->type		= CACHEFILES_COOKIE_TYPE_DATA;
	buf->content		= object->content_info;
	if (test_bit(FSCACHE_COOKIE_LOCAL_WRITE, &object->cookie->flags))
		buf->content	= CACHEFILES_CONTENT_DIRTY;
	if (len > 0)
		memcpy(buf->data, fscache_get_aux(object->cookie), len);

	ret = cachefiles_inject_write_error();
	if (ret == 0) {
		ret = mnt_want_write_file(file);
		if (ret == 0) {
			ret = vfs_setxattr(&nop_mnt_idmap, dentry,
					   cachefiles_xattr_cache, buf,
					   sizeof(struct cachefiles_xattr) + len, 0);
			mnt_drop_write_file(file);
		}
	}
	if (ret < 0) {
		trace_cachefiles_vfs_error(object, file_inode(file), ret,
					   cachefiles_trace_setxattr_error);
		trace_cachefiles_coherency(object, file_inode(file)->i_ino,
					   be64_to_cpup((__be64 *)buf->data),
					   buf->content,
					   cachefiles_coherency_set_fail);
		if (ret != -ENOMEM)
			cachefiles_io_error_obj(
				object,
				"Failed to set xattr with error %d", ret);
	} else {
		trace_cachefiles_coherency(object, file_inode(file)->i_ino,
					   be64_to_cpup((__be64 *)buf->data),
					   buf->content,
					   cachefiles_coherency_set_ok);
	}

	kfree(buf);
	_leave(" = %d", ret);
	return ret;
}

/*
 * check the consistency between the backing cache and the FS-Cache cookie
 */
int cachefiles_check_auxdata(struct cachefiles_object *object, struct file *file)
{
	struct cachefiles_xattr *buf;
	struct dentry *dentry = file->f_path.dentry;
	unsigned int len = object->cookie->aux_len, tlen;
	const void *p = fscache_get_aux(object->cookie);
	enum cachefiles_coherency_trace why;
	ssize_t xlen;
	int ret = -ESTALE;

	tlen = sizeof(struct cachefiles_xattr) + len;
	buf = kmalloc(tlen, GFP_KERNEL);
	if (!buf)
		return -ENOMEM;

	xlen = cachefiles_inject_read_error();
	if (xlen == 0)
		xlen = vfs_getxattr(&nop_mnt_idmap, dentry, cachefiles_xattr_cache, buf, tlen);
	if (xlen != tlen) {
		if (xlen < 0) {
			ret = xlen;
			trace_cachefiles_vfs_error(object, file_inode(file), xlen,
						   cachefiles_trace_getxattr_error);
		}
		if (xlen == -EIO)
			cachefiles_io_error_obj(
				object,
				"Failed to read aux with error %zd", xlen);
		why = cachefiles_coherency_check_xattr;
		goto out;
	}

	if (buf->type != CACHEFILES_COOKIE_TYPE_DATA) {
		why = cachefiles_coherency_check_type;
	} else if (memcmp(buf->data, p, len) != 0) {
		why = cachefiles_coherency_check_aux;
	} else if (be64_to_cpu(buf->object_size) != object->cookie->object_size) {
		why = cachefiles_coherency_check_objsize;
	} else if (buf->content == CACHEFILES_CONTENT_DIRTY) {
		// TODO: Begin conflict resolution
		pr_warn("Dirty object in cache\n");
		why = cachefiles_coherency_check_dirty;
	} else {
		why = cachefiles_coherency_check_ok;
		ret = 0;
	}

out:
	trace_cachefiles_coherency(object, file_inode(file)->i_ino,
				   be64_to_cpup((__be64 *)buf->data),
				   buf->content, why);
	kfree(buf);
	return ret;
}

/*
 * remove the object's xattr to mark it stale
 */
int cachefiles_remove_object_xattr(struct cachefiles_cache *cache,
				   struct cachefiles_object *object,
				   struct dentry *dentry)
{
	int ret;

	ret = cachefiles_inject_remove_error();
	if (ret == 0) {
		ret = mnt_want_write(cache->mnt);
		if (ret == 0) {
			ret = vfs_removexattr(&nop_mnt_idmap, dentry,
					      cachefiles_xattr_cache);
			mnt_drop_write(cache->mnt);
		}
	}
	if (ret < 0) {
		trace_cachefiles_vfs_error(object, d_inode(dentry), ret,
					   cachefiles_trace_remxattr_error);
		if (ret == -ENOENT || ret == -ENODATA)
			ret = 0;
		else if (ret != -ENOMEM)
			cachefiles_io_error(cache,
					    "Can't remove xattr from %lu"
					    " (error %d)",
					    d_backing_inode(dentry)->i_ino, -ret);
	}

	_leave(" = %d", ret);
	return ret;
}

/*
 * Stick a marker on the cache object to indicate that it's dirty.
 */
void cachefiles_prepare_to_write(struct fscache_cookie *cookie)
{
	const struct cred *saved_cred;
	struct cachefiles_object *object = cookie->cache_priv;
	struct cachefiles_cache *cache = object->volume->cache;

	_enter("c=%08x", object->cookie->debug_id);

	if (!test_bit(CACHEFILES_OBJECT_USING_TMPFILE, &object->flags)) {
		cachefiles_begin_secure(cache, &saved_cred);
		cachefiles_set_object_xattr(object);
		cachefiles_end_secure(cache, saved_cred);
	}
}

/*
 * Set the state xattr on a volume directory.
 */
bool cachefiles_set_volume_xattr(struct cachefiles_volume *volume)
{
	struct cachefiles_vol_xattr *buf;
	unsigned int len = volume->vcookie->coherency_len;
	const void *p = volume->vcookie->coherency;
	struct dentry *dentry = volume->dentry;
	int ret;

	_enter("%x,#%d", volume->vcookie->debug_id, len);

	len += sizeof(*buf);
	buf = kmalloc(len, GFP_KERNEL);
	if (!buf)
		return false;
	buf->reserved = cpu_to_be32(0);
	memcpy(buf->data, p, volume->vcookie->coherency_len);

	ret = cachefiles_inject_write_error();
	if (ret == 0) {
		ret = mnt_want_write(volume->cache->mnt);
		if (ret == 0) {
			ret = vfs_setxattr(&nop_mnt_idmap, dentry,
					   cachefiles_xattr_cache,
					   buf, len, 0);
			mnt_drop_write(volume->cache->mnt);
		}
	}
	if (ret < 0) {
		trace_cachefiles_vfs_error(NULL, d_inode(dentry), ret,
					   cachefiles_trace_setxattr_error);
		trace_cachefiles_vol_coherency(volume, d_inode(dentry)->i_ino,
					       cachefiles_coherency_vol_set_fail);
		if (ret != -ENOMEM)
			cachefiles_io_error(
				volume->cache, "Failed to set xattr with error %d", ret);
	} else {
		trace_cachefiles_vol_coherency(volume, d_inode(dentry)->i_ino,
					       cachefiles_coherency_vol_set_ok);
	}

	kfree(buf);
	_leave(" = %d", ret);
	return ret == 0;
}

/*
 * Check the consistency between the backing cache and the volume cookie.
 */
int cachefiles_check_volume_xattr(struct cachefiles_volume *volume)
{
	struct cachefiles_vol_xattr *buf;
	struct dentry *dentry = volume->dentry;
	unsigned int len = volume->vcookie->coherency_len;
	const void *p = volume->vcookie->coherency;
	enum cachefiles_coherency_trace why;
	ssize_t xlen;
	int ret = -ESTALE;

	_enter("");

	len += sizeof(*buf);
	buf = kmalloc(len, GFP_KERNEL);
	if (!buf)
		return -ENOMEM;

	xlen = cachefiles_inject_read_error();
	if (xlen == 0)
		xlen = vfs_getxattr(&nop_mnt_idmap, dentry, cachefiles_xattr_cache, buf, len);
	if (xlen != len) {
		if (xlen < 0) {
			ret = xlen;
			trace_cachefiles_vfs_error(NULL, d_inode(dentry), xlen,
						   cachefiles_trace_getxattr_error);
			if (xlen == -EIO)
				cachefiles_io_error(
					volume->cache,
					"Failed to read xattr with error %zd", xlen);
		}
		why = cachefiles_coherency_vol_check_xattr;
	} else if (buf->reserved != cpu_to_be32(0)) {
		why = cachefiles_coherency_vol_check_resv;
	} else if (memcmp(buf->data, p, len - sizeof(*buf)) != 0) {
		why = cachefiles_coherency_vol_check_cmp;
	} else {
		why = cachefiles_coherency_vol_check_ok;
		ret = 0;
	}

	trace_cachefiles_vol_coherency(volume, d_inode(dentry)->i_ino, why);
	kfree(buf);
	_leave(" = %d", ret);
	return ret;
}
