#include <sys/mount.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <linux/nfs.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

#include "nfsmount.h"
#include "sunrpc.h"

static uint32_t mount_port;

struct mount_call {
	struct rpc_call rpc;
	uint32_t path_len;
	char path[0];
};

/*
 * The following structure is the NFS v3 on-the-wire file handle,
 * as defined in rfc1813.
 * This differs from the structure used by the kernel,
 * defined in <linux/nfh3.h>: rfc has a long in network order,
 * kernel has a short in native order.
 * Both kernel and rfc use the name nfs_fh; kernel name is
 * visible to user apps in some versions of libc.
 * Use different name to avoid clashes.
 */
#define NFS_MAXFHSIZE_WIRE 64
struct nfs_fh_wire {
	uint32_t size;
	char data[NFS_MAXFHSIZE_WIRE];
} __attribute__ ((packed));

struct mount_reply {
	struct rpc_reply reply;
	uint32_t status;
	struct nfs_fh_wire fh;
} __attribute__ ((packed));

#define MNT_REPLY_MINSIZE (sizeof(struct rpc_reply) + sizeof(uint32_t))

static int get_ports(uint32_t server, const struct nfs_mount_data *data)
{
	uint32_t nfs_ver, mount_ver;
	uint32_t proto;

	if (data->flags & NFS_MOUNT_VER3) {
		nfs_ver = NFS3_VERSION;
		mount_ver = NFS_MNT3_VERSION;
	} else {
		nfs_ver = NFS2_VERSION;
		mount_ver = NFS_MNT_VERSION;
	}

	proto = (data->flags & NFS_MOUNT_TCP) ? IPPROTO_TCP : IPPROTO_UDP;

	if (nfs_port == 0) {
		nfs_port = portmap(server, NFS_PROGRAM, nfs_ver, proto);
		if (nfs_port == 0) {
			if (proto == IPPROTO_TCP) {
				struct in_addr addr = { server };
				fprintf(stderr, "NFS over TCP not "
					"available from %s\n", inet_ntoa(addr));
				return -1;
			}
			nfs_port = NFS_PORT;
		}
	}

	if (mount_port == 0) {
		mount_port = portmap(server, NFS_MNT_PROGRAM, mount_ver, proto);
		if (mount_port == 0)
			mount_port = MOUNT_PORT;
	}
	return 0;
}

static inline int pad_len(int len)
{
	return (len + 3) & ~3;
}

static inline void dump_params(uint32_t server,
			       const char *path,
			       const struct nfs_mount_data *data)
{
	(void)server;
	(void)path;
	(void)data;

#ifdef NFS_DEBUG
	struct in_addr addr = { server };

	printf("NFS params:\n");
	printf("  server = %s, path = \"%s\", ", inet_ntoa(addr), path);
	printf("version = %d, proto = %s\n",
	       data->flags & NFS_MOUNT_VER3 ? 3 : 2,
	       (data->flags & NFS_MOUNT_TCP) ? "tcp" : "udp");
	printf("  mount_port = %d, nfs_port = %d, flags = %08x\n",
	       mount_port, nfs_port, data->flags);
	printf("  rsize = %d, wsize = %d, timeo = %d, retrans = %d\n",
	       data->rsize, data->wsize, data->timeo, data->retrans);
	printf("  acreg (min, max) = (%d, %d), acdir (min, max) = (%d, %d)\n",
	       data->acregmin, data->acregmax, data->acdirmin, data->acdirmax);
	printf("  soft = %d, intr = %d, posix = %d, nocto = %d, noac = %d\n",
	       (data->flags & NFS_MOUNT_SOFT) != 0,
	       (data->flags & NFS_MOUNT_INTR) != 0,
	       (data->flags & NFS_MOUNT_POSIX) != 0,
	       (data->flags & NFS_MOUNT_NOCTO) != 0,
	       (data->flags & NFS_MOUNT_NOAC) != 0);
#endif
}

static inline void dump_fh(const char *data, int len)
{
	(void)data;
	(void)len;

#ifdef NFS_DEBUG
	int i = 0;
	int max = len - (len % 8);

	printf("Root file handle: %d bytes\n", NFS2_FHSIZE);

	while (i < max) {
		int j;

		printf("  %4d:  ", i);
		for (j = 0; j < 4; j++) {
			printf("%02x %02x %02x %02x  ",
			       data[i] & 0xff, data[i + 1] & 0xff,
			       data[i + 2] & 0xff, data[i + 3] & 0xff);
		}
		i += j;
		printf("\n");
	}
#endif
}

static struct mount_reply mnt_reply;

static int mount_call(uint32_t proc, uint32_t version,
		      const char *path, struct client *clnt)
{
	struct mount_call *mnt_call = NULL;
	size_t path_len, call_len;
	struct rpc rpc;
	int ret = 0;

	path_len = strlen(path);
	call_len = sizeof(*mnt_call) + pad_len(path_len);

	if ((mnt_call = malloc(call_len)) == NULL) {
		perror("malloc");
		goto bail;
	}

	memset(mnt_call, 0, sizeof(*mnt_call));

	mnt_call->rpc.program = htonl(NFS_MNT_PROGRAM);
	mnt_call->rpc.prog_vers = htonl(version);
	mnt_call->rpc.proc = htonl(proc);
	mnt_call->path_len = htonl(path_len);
	memcpy(mnt_call->path, path, path_len);

	rpc.call = (struct rpc_call *)mnt_call;
	rpc.call_len = call_len;
	rpc.reply = (struct rpc_reply *)&mnt_reply;
	rpc.reply_len = sizeof(mnt_reply);

	if (rpc_call(clnt, &rpc) < 0)
		goto bail;

	if (proc != MNTPROC_MNT) {
		goto done;
	}

	if (rpc.reply_len < MNT_REPLY_MINSIZE) {
		fprintf(stderr, "incomplete reply: %zu < %zu\n",
			rpc.reply_len, MNT_REPLY_MINSIZE);
		goto bail;
	}

	if (mnt_reply.status != 0) {
		fprintf(stderr, "mount call failed: %d\n",
			ntohl(mnt_reply.status));
		goto bail;
	}

	goto done;

bail:
	ret = -1;

done:
	if (mnt_call) {
		free(mnt_call);
	}

	return ret;
}

static int mount_v2(const char *path,
		    struct nfs_mount_data *data, struct client *clnt)
{
	int ret = mount_call(MNTPROC_MNT, NFS_MNT_VERSION, path, clnt);

	if (ret == 0) {
		dump_fh((const char *)&mnt_reply.fh, NFS2_FHSIZE);

		data->root.size = NFS_FHSIZE;
		memcpy(data->root.data, &mnt_reply.fh, NFS_FHSIZE);
		memcpy(data->old_root.data, &mnt_reply.fh, NFS_FHSIZE);
	}

	return ret;
}

static inline int umount_v2(const char *path, struct client *clnt)
{
	return mount_call(MNTPROC_UMNT, NFS_MNT_VERSION, path, clnt);
}

static int mount_v3(const char *path,
		    struct nfs_mount_data *data, struct client *clnt)
{
	int ret = mount_call(MNTPROC_MNT, NFS_MNT3_VERSION, path, clnt);

	if (ret == 0) {
		size_t fhsize = ntohl(mnt_reply.fh.size);

		dump_fh((const char *)&mnt_reply.fh.data, fhsize);

		memset(data->old_root.data, 0, NFS_FHSIZE);
		memset(&data->root, 0, sizeof(data->root));
		data->root.size = fhsize;
		memcpy(&data->root.data, mnt_reply.fh.data, fhsize);
		data->flags |= NFS_MOUNT_VER3;
	}

	return ret;
}

static inline int umount_v3(const char *path, struct client *clnt)
{
	return mount_call(MNTPROC_UMNT, NFS_MNT3_VERSION, path, clnt);
}

int nfs_mount(const char *pathname, const char *hostname,
	      uint32_t server, const char *rem_path, const char *path,
	      struct nfs_mount_data *data)
{
	struct client *clnt = NULL;
	struct sockaddr_in addr;
	char mounted = 0;
	int sock = -1;
	int ret = 0;
	int mountflags;

	if (get_ports(server, data) != 0) {
		goto bail;
	}

	dump_params(server, rem_path, data);

	if (data->flags & NFS_MOUNT_TCP) {
		clnt = tcp_client(server, mount_port, CLI_RESVPORT);
	} else {
		clnt = udp_client(server, mount_port, CLI_RESVPORT);
	}

	if (clnt == NULL) {
		goto bail;
	}

	if (data->flags & NFS_MOUNT_VER3) {
		ret = mount_v3(rem_path, data, clnt);
	} else {
		ret = mount_v2(rem_path, data, clnt);
	}

	if (ret == -1) {
		goto bail;
	}
	mounted = 1;

	if (data->flags & NFS_MOUNT_TCP) {
		sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
	} else {
		sock = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
	}

	if (sock == -1) {
		perror("socket");
		goto bail;
	}

	if (bindresvport(sock, 0) == -1) {
		perror("bindresvport");
		goto bail;
	}

	addr.sin_family = AF_INET;
	addr.sin_addr.s_addr = server;
	addr.sin_port = htons(nfs_port);
	memcpy(&data->addr, &addr, sizeof(data->addr));

	strncpy(data->hostname, hostname, sizeof(data->hostname));

	data->fd = sock;

	mountflags = (data->flags & NFS_MOUNT_KLIBC_RONLY) ? MS_RDONLY : 0;
	data->flags = data->flags & NFS_MOUNT_FLAGMASK;
	ret = mount(pathname, path, "nfs", mountflags, data);

	if (ret == -1) {
		if (errno == ENODEV) {
			fprintf(stderr, "mount: the kernel lacks NFS v%d "
				"support\n",
				(data->flags & NFS_MOUNT_VER3) ? 3 : 2);
		} else {
			perror("mount");
		}
		goto bail;
	}

	DEBUG(("Mounted %s on %s\n", pathname, path));

	goto done;

      bail:
	if (mounted) {
		if (data->flags & NFS_MOUNT_VER3) {
			umount_v3(path, clnt);
		} else {
			umount_v2(path, clnt);
		}
	}

	ret = -1;

      done:
	if (clnt) {
		client_free(clnt);
	}

	if (sock != -1) {
		close(sock);
	}

	return ret;
}
