/**
 * xattr.c
 *
 * Many parts of codes are copied from Linux kernel/fs/f2fs.
 *
 * Copyright (C) 2015 Huawei Ltd.
 * Witten by:
 *   Hou Pengyang <houpengyang@huawei.com>
 *   Liu Shuoran <liushuoran@huawei.com>
 *   Jaegeuk Kim <jaegeuk@kernel.org>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 */
#include "fsck.h"
#include "node.h"
#include "xattr.h"

void *read_all_xattrs(struct f2fs_sb_info *sbi, struct f2fs_node *inode)
{
	struct f2fs_xattr_header *header;
	void *txattr_addr;
	u64 inline_size = inline_xattr_size(&inode->i);

	txattr_addr = calloc(inline_size + BLOCK_SZ, 1);
	ASSERT(txattr_addr);

	if (inline_size)
		memcpy(txattr_addr, inline_xattr_addr(&inode->i), inline_size);

	/* Read from xattr node block. */
	if (inode->i.i_xattr_nid) {
		struct node_info ni;
		int ret;

		get_node_info(sbi, le32_to_cpu(inode->i.i_xattr_nid), &ni);
		ret = dev_read_block(txattr_addr + inline_size, ni.blk_addr);
		ASSERT(ret >= 0);
	}

	header = XATTR_HDR(txattr_addr);

	/* Never been allocated xattrs */
	if (le32_to_cpu(header->h_magic) != F2FS_XATTR_MAGIC) {
		header->h_magic = cpu_to_le32(F2FS_XATTR_MAGIC);
		header->h_refcount = cpu_to_le32(1);
	}
	return txattr_addr;
}

static struct f2fs_xattr_entry *__find_xattr(void *base_addr, int index,
		size_t len, const char *name)
{
	struct f2fs_xattr_entry *entry;
	list_for_each_xattr(entry, base_addr) {
		if (entry->e_name_index != index)
			continue;
		if (entry->e_name_len != len)
			continue;
		if (!memcmp(entry->e_name, name, len))
			break;
	}
	return entry;
}

static void write_all_xattrs(struct f2fs_sb_info *sbi,
		struct f2fs_node *inode, __u32 hsize, void *txattr_addr)
{
	void *xattr_addr;
	struct dnode_of_data dn;
	struct node_info ni;
	struct f2fs_node *xattr_node;
	nid_t new_nid = 0;
	block_t blkaddr;
	nid_t xnid = le32_to_cpu(inode->i.i_xattr_nid);
	u64 inline_size = inline_xattr_size(&inode->i);
	int ret;

	memcpy(inline_xattr_addr(&inode->i), txattr_addr, inline_size);

	if (hsize <= inline_size)
		return;

	if (!xnid) {
		f2fs_alloc_nid(sbi, &new_nid, 0);

		set_new_dnode(&dn, inode, NULL, new_nid);
		/* NAT entry would be updated by new_node_page. */
		blkaddr = new_node_block(sbi, &dn, XATTR_NODE_OFFSET);
		ASSERT(dn.node_blk);
		xattr_node = dn.node_blk;
		inode->i.i_xattr_nid = cpu_to_le32(new_nid);
	} else {
		set_new_dnode(&dn, inode, NULL, xnid);
		get_node_info(sbi, xnid, &ni);
		blkaddr = ni.blk_addr;
		xattr_node = calloc(BLOCK_SZ, 1);
		ASSERT(xattr_node);
		ret = dev_read_block(xattr_node, ni.blk_addr);
		ASSERT(ret >= 0);
	}

	/* write to xattr node block */
	xattr_addr = (void *)xattr_node;
	memcpy(xattr_addr, txattr_addr + inline_size,
			PAGE_SIZE - sizeof(struct node_footer));

	ret = dev_write_block(xattr_node, blkaddr);
	ASSERT(ret >= 0);

	if (xnid)
		free(xattr_node);
}

int f2fs_setxattr(struct f2fs_sb_info *sbi, nid_t ino, int index, const char *name,
		const void *value, size_t size, int flags)
{
	struct f2fs_node *inode;
	void *base_addr;
	struct f2fs_xattr_entry *here, *last;
	struct node_info ni;
	int error = 0;
	int len;
	int found, newsize;
	__u32 new_hsize;
	int ret;

	if (name == NULL)
		return -EINVAL;

	if (value == NULL)
		return -EINVAL;

	len = strlen(name);

	if (len > F2FS_NAME_LEN || size > MAX_VALUE_LEN)
		return -ERANGE;

	if (ino < 3)
		return -EINVAL;

	/* Now We just support selinux */
	ASSERT(index == F2FS_XATTR_INDEX_SECURITY);

	get_node_info(sbi, ino, &ni);
	inode = calloc(BLOCK_SZ, 1);
	ASSERT(inode);
	ret = dev_read_block(inode, ni.blk_addr);
	ASSERT(ret >= 0);

	base_addr = read_all_xattrs(sbi, inode);
	ASSERT(base_addr);

	here = __find_xattr(base_addr, index, len, name);

	found = IS_XATTR_LAST_ENTRY(here) ? 0 : 1;

	if ((flags & XATTR_REPLACE) && !found) {
		error = -ENODATA;
		goto exit;
	} else if ((flags & XATTR_CREATE) && found) {
		error = -EEXIST;
		goto exit;
	}

	last = here;
	while (!IS_XATTR_LAST_ENTRY(last))
		last = XATTR_NEXT_ENTRY(last);

	newsize = XATTR_ALIGN(sizeof(struct f2fs_xattr_entry) + len + size);

	/* 1. Check space */
	if (value) {
		int free;
		/*
		 * If value is NULL, it is remove operation.
		 * In case of update operation, we calculate free.
		 */
		free = MIN_OFFSET - ((char *)last - (char *)base_addr);
		if (found)
			free = free + ENTRY_SIZE(here);
		if (free < newsize) {
			error = -ENOSPC;
			goto exit;
		}
	}

	/* 2. Remove old entry */
	if (found) {
		/*
		 * If entry if sound, remove old entry.
		 * If not found, remove operation is not needed
		 */
		struct f2fs_xattr_entry *next = XATTR_NEXT_ENTRY(here);
		int oldsize = ENTRY_SIZE(here);

		memmove(here, next, (char *)last - (char *)next);
		last = (struct f2fs_xattr_entry *)((char *)last - oldsize);
		memset(last, 0, oldsize);

	}

	new_hsize = (char *)last - (char *)base_addr;

	/* 3. Write new entry */
	if (value) {
		char *pval;
		/*
		 * Before we come here, old entry is removed.
		 * We just write new entry.
		 */
		memset(last, 0, newsize);
		last->e_name_index = index;
		last->e_name_len = len;
		memcpy(last->e_name, name, len);
		pval = last->e_name + len;
		memcpy(pval, value, size);
		last->e_value_size = cpu_to_le16(size);
		new_hsize += newsize;
	}

	write_all_xattrs(sbi, inode, new_hsize, base_addr);

	/* inode need update */
	ret = dev_write_block(inode, ni.blk_addr);
	ASSERT(ret >= 0);
exit:
	free(inode);
	free(base_addr);
	return error;
}

int inode_set_selinux(struct f2fs_sb_info *sbi, u32 ino, const char *secon)
{
	if (!secon)
		return 0;

	return f2fs_setxattr(sbi, ino, F2FS_XATTR_INDEX_SECURITY,
			XATTR_SELINUX_SUFFIX, secon, strlen(secon), 1);
}