/*
   Unix SMB/CIFS implementation.

   Copyright (c) 2019 Guenther Deschner <gd@samba.org>

   This program 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 3 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/>.
*/

#include "includes.h"
#include "smbd/smbd.h"
#include "system/filesys.h"

#define GLUSTER_NAME_MAX 255

static NTSTATUS vfs_gluster_fuse_get_real_filename_at(
	struct vfs_handle_struct *handle,
	struct files_struct *dirfsp,
	const char *name,
	TALLOC_CTX *mem_ctx,
	char **_found_name)
{
	int ret, dirfd;
	char key_buf[GLUSTER_NAME_MAX + 64];
	char val_buf[GLUSTER_NAME_MAX + 1];
	char *found_name = NULL;

	if (strlen(name) >= GLUSTER_NAME_MAX) {
		return NT_STATUS_OBJECT_NAME_INVALID;
	}

	snprintf(key_buf, GLUSTER_NAME_MAX + 64,
		 "glusterfs.get_real_filename:%s", name);

	dirfd = openat(fsp_get_pathref_fd(dirfsp), ".", O_RDONLY);
	if (dirfd == -1) {
		NTSTATUS status = map_nt_error_from_unix(errno);
		DBG_DEBUG("Could not open '.' in %s: %s\n",
			  fsp_str_dbg(dirfsp),
			  strerror(errno));
		return status;
	}

	ret = fgetxattr(dirfd, key_buf, val_buf, GLUSTER_NAME_MAX + 1);
	close(dirfd);
	if (ret == -1) {
		if (errno == ENOATTR) {
			errno = ENOENT;
		}
		return map_nt_error_from_unix(errno);
	}

	found_name = talloc_strdup(mem_ctx, val_buf);
	if (found_name == NULL) {
		return NT_STATUS_NO_MEMORY;
	}
	*_found_name = found_name;
	return NT_STATUS_OK;
}

struct device_mapping_entry {
	SMB_DEV_T device;       /* the local device, for reference */
	uint64_t mapped_device; /* the mapped device */
};

struct vfs_glusterfs_fuse_handle_data {
	unsigned num_mapped_devices;
	struct device_mapping_entry *mapped_devices;
};

/* a 64 bit hash, based on the one in tdb, copied from vfs_fileied */
static uint64_t vfs_glusterfs_fuse_uint64_hash(const uint8_t *s, size_t len)
{
	uint64_t value; /* Used to compute the hash value.  */
	uint32_t i;     /* Used to cycle through random values. */

	/* Set the initial value from the key size. */
	for (value = 0x238F13AFLL * len, i=0; i < len; i++)
		value = (value + (((uint64_t)s[i]) << (i*5 % 24)));

	return (1103515243LL * value + 12345LL);
}

static void vfs_glusterfs_fuse_load_devices(
		struct vfs_glusterfs_fuse_handle_data *data)
{
	FILE *f;
	struct mntent *m;

	data->num_mapped_devices = 0;
	TALLOC_FREE(data->mapped_devices);

	f = setmntent("/etc/mtab", "r");
	if (!f) {
		return;
	}

	while ((m = getmntent(f))) {
		struct stat st;
		char *p;
		uint64_t mapped_device;

		if (stat(m->mnt_dir, &st) != 0) {
			/* TODO: log? */
			continue;
		}

		/* strip the host part off of the fsname */
		p = strrchr(m->mnt_fsname, ':');
		if (p == NULL) {
			p = m->mnt_fsname;
		} else {
			/* TODO: consider the case of '' ? */
			p++;
		}

		mapped_device = vfs_glusterfs_fuse_uint64_hash(
						(const uint8_t *)p,
						strlen(p));

		data->mapped_devices = talloc_realloc(data,
						data->mapped_devices,
						struct device_mapping_entry,
						data->num_mapped_devices + 1);
		if (data->mapped_devices == NULL) {
			goto nomem;
		}

		data->mapped_devices[data->num_mapped_devices].device =
								st.st_dev;
		data->mapped_devices[data->num_mapped_devices].mapped_device =
								mapped_device;

		data->num_mapped_devices++;
	}

	endmntent(f);
	return;

nomem:
	data->num_mapped_devices = 0;
	TALLOC_FREE(data->mapped_devices);

	endmntent(f);
	return;
}

static int vfs_glusterfs_fuse_map_device_cached(
				struct vfs_glusterfs_fuse_handle_data *data,
				SMB_DEV_T device,
				uint64_t *mapped_device)
{
	unsigned i;

	for (i = 0; i < data->num_mapped_devices; i++) {
		if (data->mapped_devices[i].device == device) {
			*mapped_device = data->mapped_devices[i].mapped_device;
			return 0;
		}
	}

	return -1;
}

static int vfs_glusterfs_fuse_map_device(
				struct vfs_glusterfs_fuse_handle_data *data,
				SMB_DEV_T device,
				uint64_t *mapped_device)
{
	int ret;

	ret = vfs_glusterfs_fuse_map_device_cached(data, device, mapped_device);
	if (ret == 0) {
		return 0;
	}

	vfs_glusterfs_fuse_load_devices(data);

	ret = vfs_glusterfs_fuse_map_device_cached(data, device, mapped_device);

	return ret;
}

static struct file_id vfs_glusterfs_fuse_file_id_create(
			struct vfs_handle_struct *handle,
			const SMB_STRUCT_STAT *sbuf)
{
	struct vfs_glusterfs_fuse_handle_data *data;
	struct file_id id;
	uint64_t mapped_device;
	int ret;

	ZERO_STRUCT(id);

	id = SMB_VFS_NEXT_FILE_ID_CREATE(handle, sbuf);

	SMB_VFS_HANDLE_GET_DATA(handle, data,
				struct vfs_glusterfs_fuse_handle_data,
				return id);

	ret = vfs_glusterfs_fuse_map_device(data, sbuf->st_ex_dev,
					    &mapped_device);
	if (ret == 0) {
		id.devid = mapped_device;
	} else {
		DBG_WARNING("Failed to map device [%jx], falling back to "
			    "standard file_id [%jx]\n",
			    (uintmax_t)sbuf->st_ex_dev,
			    (uintmax_t)id.devid);
	}

	DBG_DEBUG("Returning dev [%jx] inode [%jx]\n",
		  (uintmax_t)id.devid, (uintmax_t)id.inode);

	return id;
}

static int vfs_glusterfs_fuse_connect(struct vfs_handle_struct *handle,
				      const char *service, const char *user)
{
	struct vfs_glusterfs_fuse_handle_data *data;
	int ret = SMB_VFS_NEXT_CONNECT(handle, service, user);

	if (ret < 0) {
		return ret;
	}

	data = talloc_zero(handle->conn, struct vfs_glusterfs_fuse_handle_data);
	if (data == NULL) {
		DBG_ERR("talloc_zero() failed.\n");
		SMB_VFS_NEXT_DISCONNECT(handle);
		return -1;
	}

	/*
	 * Fill the cache in the tree connect, so that the first file/dir access
	 * has chances of being fast...
	 */
	vfs_glusterfs_fuse_load_devices(data);

	SMB_VFS_HANDLE_SET_DATA(handle, data, NULL,
				struct vfs_glusterfs_fuse_handle_data,
				return -1);

	DBG_DEBUG("vfs_glusterfs_fuse_connect(): connected to service[%s]\n",
		  service);

	return 0;
}

struct vfs_fn_pointers glusterfs_fuse_fns = {

	.connect_fn = vfs_glusterfs_fuse_connect,
	.get_real_filename_at_fn = vfs_gluster_fuse_get_real_filename_at,
	.file_id_create_fn = vfs_glusterfs_fuse_file_id_create,
};

static_decl_vfs;
NTSTATUS vfs_glusterfs_fuse_init(TALLOC_CTX *ctx)
{
	return smb_register_vfs(SMB_VFS_INTERFACE_VERSION,
				"glusterfs_fuse", &glusterfs_fuse_fns);
}
