/**
 * ea.c - Processing of EA's
 *
 *      This module is part of ntfs-3g library
 *
 * Copyright (c) 2014-2021 Jean-Pierre Andre
 *
 * This program/include file 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/include file 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 (in the main directory of the NTFS-3G
 * distribution in the file COPYING); if not, write to the Free Software
 * Foundation,Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#ifdef HAVE_STDIO_H
#include <stdio.h>
#endif
#ifdef HAVE_STDLIB_H
#include <stdlib.h>
#endif
#ifdef HAVE_STRING_H
#include <string.h>
#endif
#ifdef HAVE_FCNTL_H
#include <fcntl.h>
#endif
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#ifdef HAVE_ERRNO_H
#include <errno.h>
#endif
#ifdef MAJOR_IN_MKDEV
#include <sys/mkdev.h>
#endif
#ifdef MAJOR_IN_SYSMACROS
#include <sys/sysmacros.h>
#endif

#include "types.h"
#include "param.h"
#include "layout.h"
#include "attrib.h"
#include "index.h"
#include "dir.h"
#include "ea.h"
#include "misc.h"
#include "logging.h"
#include "xattrs.h"

static const char lxdev[] = "$LXDEV";
static const char lxmod[] = "$LXMOD";


/*
 *		Create a needed attribute (EA or EA_INFORMATION)
 *
 *	Returns 0 if successful,
 *		-1 otherwise, with errno indicating why it failed.
 */

static int ntfs_need_ea(ntfs_inode *ni, ATTR_TYPES type, int size, int flags)
{
	u8 dummy = 0;
	int res;

	res = 0;
	if (!ntfs_attr_exist(ni,type, AT_UNNAMED,0)) {
		if (!(flags & XATTR_REPLACE)) {
			/*
			 * no needed attribute : add one,
			 * apparently, this does not feed the new value in
			 * Note : NTFS version must be >= 3
			 */
			if (ni->vol->major_ver >= 3) {
				res = ntfs_attr_add(ni,	type,
						AT_UNNAMED,0,&dummy,(s64)size);
				if (!res) {
					NInoFileNameSetDirty(ni);
				}
				NInoSetDirty(ni);
			} else {
				errno = EOPNOTSUPP;
				res = -1;
			}
		} else {
			errno = ENODATA;
			res = -1;
		}
	}
	return (res);
}

/*
 *		Restore the old EA_INFORMATION or delete the current one,
 *	 when EA cannot be updated.
 *
 *	As this is used in the context of some other error, the caller
 *	is responsible for returning the proper error, and errno is
 *	left unchanged.
 *	Only double errors are logged here.
 */

static void restore_ea_info(ntfs_attr *nai, const EA_INFORMATION *old_ea_info)
{
	s64 written;
	int olderrno;

	olderrno = errno;
	if (old_ea_info) {
		written = ntfs_attr_pwrite(nai,	0, sizeof(EA_INFORMATION),
				old_ea_info);
		if ((size_t)written != sizeof(EA_INFORMATION)) {
			ntfs_log_error("Could not restore the EA_INFORMATION,"
					" possible inconsistency in inode %lld\n",
					(long long)nai->ni->mft_no);
		}
	} else {
		if (ntfs_attr_rm(nai)) {
			ntfs_log_error("Could not delete the EA_INFORMATION,"
					" possible inconsistency in inode %lld\n",
					(long long)nai->ni->mft_no);
		}
	}
	errno = olderrno;
}

/*
 *		Update both EA and EA_INFORMATION
 */

static int ntfs_update_ea(ntfs_inode *ni, const char *value, size_t size,
		const EA_INFORMATION *ea_info,
		const EA_INFORMATION *old_ea_info)
{
	ntfs_attr *na;
	ntfs_attr *nai;
	int res;

	res = 0;
	nai = ntfs_attr_open(ni, AT_EA_INFORMATION, AT_UNNAMED, 0);
	if (nai) {
		na = ntfs_attr_open(ni, AT_EA, AT_UNNAMED, 0);
		if (na) {
			/*
			 * Set EA_INFORMATION first, it is easier to
			 * restore the old value, if setting EA fails.
			 */
			if (ntfs_attr_pwrite(nai, 0, sizeof(EA_INFORMATION),
						ea_info)
					!= (s64)sizeof(EA_INFORMATION)) {
				res = -errno;
			} else {
				if (((na->data_size > (s64)size)
							&& ntfs_attr_truncate(na, size))
						|| (ntfs_attr_pwrite(na, 0, size, value)
							!= (s64)size)) {
					res = -errno;
					if (old_ea_info)
						restore_ea_info(nai,
								old_ea_info);
				}
			}
			ntfs_attr_close(na);
		}
		ntfs_attr_close(nai);
	} else {
		res = -errno;
	}
	return (res);
}

/*
 *		Return the existing EA
 *
 *	The EA_INFORMATION is not examined and the consistency of the
 *	existing EA is not checked.
 *
 *	If successful, the full attribute is returned unchanged
 *		and its size is returned.
 *	If the designated buffer is too small, the needed size is
 *		returned, and the buffer is left unchanged.
 *	If there is an error, a negative value is returned and errno
 *		is set according to the error.
 */

int ntfs_get_ntfs_ea(ntfs_inode *ni, char *value, size_t size)
{
	s64 ea_size;
	void *ea_buf;
	int res = 0;

	if (ntfs_attr_exist(ni, AT_EA, AT_UNNAMED, 0)) {
		ea_buf = ntfs_attr_readall(ni, AT_EA, (ntfschar*)NULL, 0,
				&ea_size);
		if (ea_buf) {
			if (value && (ea_size <= (s64)size))
				memcpy(value, ea_buf, ea_size);
			free(ea_buf);
			res = ea_size;
		} else {
			ntfs_log_error("Failed to read EA from inode %lld\n",
					(long long)ni->mft_no);
			errno = ENODATA;
			res = -errno;
		}
	} else {
		errno = ENODATA;
		res = -errno;
	}
	return (res);
}

/*
 *		Set a new EA, and set EA_INFORMATION accordingly
 *
 *	This is roughly the same as ZwSetEaFile() on Windows, however
 *	the "offset to next" of the last EA should not be cleared.
 *
 *	Consistency of the new EA is first checked.
 *
 *	EA_INFORMATION is set first, and it is restored to its former
 *	state if setting EA fails.
 *
 *	Returns 0 if successful
 *		a negative value if an error occurred.
 */

int ntfs_set_ntfs_ea(ntfs_inode *ni, const char *value, size_t size, int flags)
{
	EA_INFORMATION ea_info;
	EA_INFORMATION *old_ea_info;
	s64 old_ea_size;
	int res;
	size_t offs;
	size_t nextoffs;
	BOOL ok;
	int ea_count;
	int ea_packed;
	const EA_ATTR *p_ea;

	res = -1;
	if (value && (size > 0)) {
		/* do consistency checks */
		offs = 0;
		ok = TRUE;
		ea_count = 0;
		ea_packed = 0;
		nextoffs = 0;
		while (ok && (offs < size)) {
			p_ea = (const EA_ATTR*)&value[offs];
			nextoffs = offs + le32_to_cpu(p_ea->next_entry_offset);
			/* null offset to next not allowed */
			ok = (nextoffs > offs)
				&& (nextoffs <= size)
				&& !(nextoffs & 3)
				&& p_ea->name_length
				/* zero sized value are allowed */
				&& ((offs + offsetof(EA_ATTR,name)
							+ p_ea->name_length + 1
							+ le16_to_cpu(p_ea->value_length))
						<= nextoffs)
				&& ((offs + offsetof(EA_ATTR,name)
							+ p_ea->name_length + 1
							+ le16_to_cpu(p_ea->value_length))
						>= (nextoffs - 3))
				&& !p_ea->name[p_ea->name_length];
			/* name not checked, as chkdsk accepts any chars */
			if (ok) {
				if (p_ea->flags & NEED_EA)
					ea_count++;
				/*
				 * Assume ea_packed includes :
				 * 4 bytes for header (flags and lengths)
				 * + name length + 1
				 * + value length
				 */
				ea_packed += 5 + p_ea->name_length
					+ le16_to_cpu(p_ea->value_length);
				offs = nextoffs;
			}
		}
		/*
		 * EA and REPARSE_POINT compatibility not checked any more,
		 * required by Windows 10, but having both may lead to
		 * problems with earlier versions.
		 */
		if (ok) {
			ea_info.ea_length = cpu_to_le16(ea_packed);
			ea_info.need_ea_count = cpu_to_le16(ea_count);
			ea_info.ea_query_length = cpu_to_le32(nextoffs);

			old_ea_size = 0;
			old_ea_info = NULL;
			/* Try to save the old EA_INFORMATION */
			if (ntfs_attr_exist(ni, AT_EA_INFORMATION,
						AT_UNNAMED, 0)) {
				old_ea_info = ntfs_attr_readall(ni,
						AT_EA_INFORMATION,
						(ntfschar*)NULL, 0, &old_ea_size);
			}
			/*
			 * no EA or EA_INFORMATION : add them
			 */
			if (!ntfs_need_ea(ni, AT_EA_INFORMATION,
						sizeof(EA_INFORMATION), flags)
					&& !ntfs_need_ea(ni, AT_EA, 0, flags)) {
				res = ntfs_update_ea(ni, value, size,
						&ea_info, old_ea_info);
			} else {
				res = -errno;
			}
			if (old_ea_info)
				free(old_ea_info);
		} else {
			errno = EINVAL;
			res = -errno;
		}
	} else {
		errno = EINVAL;
		res = -errno;
	}
	return (res);
}

/*
 *		Remove the EA (including EA_INFORMATION)
 *
 *	EA_INFORMATION is removed first, and it is restored to its former
 *	state if removing EA fails.
 *
 *	Returns 0, or -1 if there is a problem
 */

int ntfs_remove_ntfs_ea(ntfs_inode *ni)
{
	EA_INFORMATION *old_ea_info;
	s64 old_ea_size;
	int res;
	ntfs_attr *na;
	ntfs_attr *nai;

	res = 0;
	if (ni) {
		/*
		 * open and delete the EA_INFORMATION and the EA
		 */
		nai = ntfs_attr_open(ni, AT_EA_INFORMATION, AT_UNNAMED, 0);
		if (nai) {
			na = ntfs_attr_open(ni, AT_EA, AT_UNNAMED, 0);
			if (na) {
				/* Try to save the old EA_INFORMATION */
				old_ea_info = ntfs_attr_readall(ni,
						AT_EA_INFORMATION,
						(ntfschar*)NULL, 0, &old_ea_size);
				res = ntfs_attr_rm(na);
				NInoFileNameSetDirty(ni);
				if (!res) {
					res = ntfs_attr_rm(nai);
					if (res && old_ea_info) {
						/*
						 * Failed to remove the EA, try to
						 * restore the EA_INFORMATION
						 */
						restore_ea_info(nai,
								old_ea_info);
					}
				} else {
					ntfs_log_error("Failed to remove the"
							" EA_INFORMATION from inode %lld\n",
							(long long)ni->mft_no);
				}
				free(old_ea_info);
				ntfs_attr_close(na);
			} else {
				/* EA_INFORMATION present, but no EA */
				res = ntfs_attr_rm(nai);
				NInoFileNameSetDirty(ni);
			}
			ntfs_attr_close(nai);
		} else {
			errno = ENODATA;
			res = -1;
		}
		NInoSetDirty(ni);
	} else {
		errno = EINVAL;
		res = -1;
	}
	return (res ? -1 : 0);
}

/*
 *		Check for the presence of an EA "$LXDEV" (used by WSL)
 *	and return its value as a device address
 *
 *	Returns zero if successful
 *		-1 if failed, with errno set
 */

int ntfs_ea_check_wsldev(ntfs_inode *ni, dev_t *rdevp)
{
	const EA_ATTR *p_ea;
	int bufsize;
	char *buf;
	int lth;
	int res;
	int offset;
	int next;
	BOOL found;
	struct {
		le32 major;
		le32 minor;
	} device;

	res = -EOPNOTSUPP;
	bufsize = 256; /* expected to be enough */
	buf = (char*)malloc(bufsize);
	if (!buf) {
		ntfs_log_error("Failed to allocate buffer for ea\n");
		return -ENOMEM;
	}

	lth = ntfs_get_ntfs_ea(ni, buf, bufsize);
	/* retry if short buf */

	if (lth > bufsize) {
		free(buf);
		bufsize = lth;
		buf = (char*)malloc(bufsize);
		if (buf)
			lth = ntfs_get_ntfs_ea(ni, buf, bufsize);
	}

	if ((lth > 0) && (lth <= bufsize)) {
		offset = 0;
		found = FALSE;
		do {
			p_ea = (const EA_ATTR*)&buf[offset];
			next = le32_to_cpu(p_ea->next_entry_offset);
			found = ((next > (int)(sizeof(lxdev) + sizeof(device)))
					&& (p_ea->name_length == (sizeof(lxdev) - 1))
					&& (p_ea->value_length
						== const_cpu_to_le16(sizeof(device)))
					&& !memcmp(p_ea->name, lxdev, sizeof(lxdev)));
			if (!found)
				offset += next;
		} while (!found && (next > 0) && (offset < lth));
		if (found) {
			/* beware of alignment */
			memcpy(&device, &p_ea->name[p_ea->name_length + 1],
							sizeof(device));
			*rdevp = makedev(le32_to_cpu(device.major),
					le32_to_cpu(device.minor));
			res = 0;
		}
	}
	free(buf);
	return (res);
}

int ntfs_ea_set_wsl_not_symlink(ntfs_inode *ni, mode_t type, dev_t dev)
{
	le32 mode;
	struct {
		le32 major;
		le32 minor;
	} device;
	struct EA_WSL {
		struct EA_LXMOD {	/* always inserted */
			EA_ATTR base;
			char name[sizeof(lxmod)];
			char value[sizeof(mode)];
			char stuff[3 & -(sizeof(lxmod) + sizeof(mode))];
		} mod;
		struct EA_LXDEV {	/* char or block devices only */
			EA_ATTR base;
			char name[sizeof(lxdev)];
			char value[sizeof(device)];
			char stuff[3 & -(sizeof(lxdev) + sizeof(device))];
		} dev;
	} attr;
	int len;
	int res;

	memset(&attr, 0, sizeof(attr));
	mode = cpu_to_le32((u32)(type | 0644));
	attr.mod.base.next_entry_offset
		= const_cpu_to_le32(sizeof(attr.mod));
	attr.mod.base.flags = 0;
	attr.mod.base.name_length = sizeof(lxmod) - 1;
	attr.mod.base.value_length = const_cpu_to_le16(sizeof(mode));
	memcpy(attr.mod.name, lxmod, sizeof(lxmod));
	memcpy(attr.mod.value, &mode, sizeof(mode));
	len = sizeof(attr.mod);

	if (S_ISCHR(type) || S_ISBLK(type)) {
		device.major = cpu_to_le32(major(dev));
		device.minor = cpu_to_le32(minor(dev));
		attr.dev.base.next_entry_offset
			= const_cpu_to_le32(sizeof(attr.dev));
		attr.dev.base.flags = 0;
		attr.dev.base.name_length = sizeof(lxdev) - 1;
		attr.dev.base.value_length = const_cpu_to_le16(sizeof(device));
		memcpy(attr.dev.name, lxdev, sizeof(lxdev));
		memcpy(attr.dev.value, &device, sizeof(device));
		len += sizeof(attr.dev);
	}
	res = ntfs_set_ntfs_ea(ni, (char*)&attr, len, 0);
	return (res);
}
