/*
 * net/9p/protocol.c
 *
 * 9P Protocol Support Code
 *
 *  Copyright (C) 2008 by Eric Van Hensbergen <ericvh@gmail.com>
 *
 *  Base on code from Anthony Liguori <aliguori@us.ibm.com>
 *  Copyright (C) 2008 by IBM, Corp.
 *
 *  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.
 *
 *  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, write to:
 *  Free Software Foundation
 *  51 Franklin Street, Fifth Floor
 *  Boston, MA  02111-1301  USA
 *
 */

#include <linux/module.h>
#include <linux/errno.h>
#include <net/9p/9p.h>
#include <net/9p/client.h>
#include "protocol.h"

#ifndef MIN
#define MIN(a, b) (((a) < (b)) ? (a) : (b))
#endif

#ifndef MAX
#define MAX(a, b) (((a) > (b)) ? (a) : (b))
#endif

#ifndef offset_of
#define offset_of(type, memb) \
	((unsigned long)(&((type *)0)->memb))
#endif
#ifndef container_of
#define container_of(obj, type, memb) \
	((type *)(((char *)obj) - offset_of(type, memb)))
#endif

static int
p9pdu_writef(struct p9_fcall *pdu, int optional, const char *fmt, ...);

void p9stat_free(struct p9_wstat *stbuf)
{
	kfree(stbuf->name);
	kfree(stbuf->uid);
	kfree(stbuf->gid);
	kfree(stbuf->muid);
	kfree(stbuf->extension);
}
EXPORT_SYMBOL(p9stat_free);

static size_t pdu_read(struct p9_fcall *pdu, void *data, size_t size)
{
	size_t len = MIN(pdu->size - pdu->offset, size);
	memcpy(data, &pdu->sdata[pdu->offset], len);
	pdu->offset += len;
	return size - len;
}

static size_t pdu_write(struct p9_fcall *pdu, const void *data, size_t size)
{
	size_t len = MIN(pdu->capacity - pdu->size, size);
	memcpy(&pdu->sdata[pdu->size], data, len);
	pdu->size += len;
	return size - len;
}

/*
	b - int8_t
	w - int16_t
	d - int32_t
	q - int64_t
	s - string
	S - stat
	Q - qid
	D - data blob (int32_t size followed by void *, results are not freed)
	T - array of strings (int16_t count, followed by strings)
	R - array of qids (int16_t count, followed by qids)
	? - if optional = 1, continue parsing
*/

static int
p9pdu_vreadf(struct p9_fcall *pdu, int optional, const char *fmt, va_list ap)
{
	const char *ptr;
	int errcode = 0;

	for (ptr = fmt; *ptr; ptr++) {
		switch (*ptr) {
		case 'b':{
				int8_t *val = va_arg(ap, int8_t *);
				if (pdu_read(pdu, val, sizeof(*val))) {
					errcode = -EFAULT;
					break;
				}
			}
			break;
		case 'w':{
				int16_t *val = va_arg(ap, int16_t *);
				if (pdu_read(pdu, val, sizeof(*val))) {
					errcode = -EFAULT;
					break;
				}
				*val = cpu_to_le16(*val);
			}
			break;
		case 'd':{
				int32_t *val = va_arg(ap, int32_t *);
				if (pdu_read(pdu, val, sizeof(*val))) {
					errcode = -EFAULT;
					break;
				}
				*val = cpu_to_le32(*val);
			}
			break;
		case 'q':{
				int64_t *val = va_arg(ap, int64_t *);
				if (pdu_read(pdu, val, sizeof(*val))) {
					errcode = -EFAULT;
					break;
				}
				*val = cpu_to_le64(*val);
			}
			break;
		case 's':{
				char **ptr = va_arg(ap, char **);
				int16_t len;
				int size;

				errcode = p9pdu_readf(pdu, optional, "w", &len);
				if (errcode)
					break;

				size = MAX(len, 0);

				*ptr = kmalloc(size + 1, GFP_KERNEL);
				if (*ptr == NULL) {
					errcode = -EFAULT;
					break;
				}
				if (pdu_read(pdu, *ptr, size)) {
					errcode = -EFAULT;
					kfree(*ptr);
					*ptr = NULL;
				} else
					(*ptr)[size] = 0;
			}
			break;
		case 'Q':{
				struct p9_qid *qid =
				    va_arg(ap, struct p9_qid *);

				errcode = p9pdu_readf(pdu, optional, "bdq",
						      &qid->type, &qid->version,
						      &qid->path);
			}
			break;
		case 'S':{
				struct p9_wstat *stbuf =
				    va_arg(ap, struct p9_wstat *);

				stbuf->extension = NULL;
				stbuf->n_uid = stbuf->n_gid = stbuf->n_muid =
				    -1;

				errcode =
				    p9pdu_readf(pdu, optional,
						"wwdQdddqssss?sddd",
						&stbuf->size, &stbuf->type,
						&stbuf->dev, &stbuf->qid,
						&stbuf->mode, &stbuf->atime,
						&stbuf->mtime, &stbuf->length,
						&stbuf->name, &stbuf->uid,
						&stbuf->gid, &stbuf->muid,
						&stbuf->extension,
						&stbuf->n_uid, &stbuf->n_gid,
						&stbuf->n_muid);
				if (errcode)
					p9stat_free(stbuf);
			}
			break;
		case 'D':{
				int32_t *count = va_arg(ap, int32_t *);
				void **data = va_arg(ap, void **);

				errcode =
				    p9pdu_readf(pdu, optional, "d", count);
				if (!errcode) {
					*count =
					    MIN(*count,
						pdu->size - pdu->offset);
					*data = &pdu->sdata[pdu->offset];
				}
			}
			break;
		case 'T':{
				int16_t *nwname = va_arg(ap, int16_t *);
				char ***wnames = va_arg(ap, char ***);

				errcode =
				    p9pdu_readf(pdu, optional, "w", nwname);
				if (!errcode) {
					*wnames =
					    kmalloc(sizeof(char *) * *nwname,
						    GFP_KERNEL);
					if (!*wnames)
						errcode = -ENOMEM;
				}

				if (!errcode) {
					int i;

					for (i = 0; i < *nwname; i++) {
						errcode =
						    p9pdu_readf(pdu, optional,
								"s",
								&(*wnames)[i]);
						if (errcode)
							break;
					}
				}

				if (errcode) {
					if (*wnames) {
						int i;

						for (i = 0; i < *nwname; i++)
							kfree((*wnames)[i]);
					}
					kfree(*wnames);
					*wnames = NULL;
				}
			}
			break;
		case 'R':{
				int16_t *nwqid = va_arg(ap, int16_t *);
				struct p9_qid **wqids =
				    va_arg(ap, struct p9_qid **);

				*wqids = NULL;

				errcode =
				    p9pdu_readf(pdu, optional, "w", nwqid);
				if (!errcode) {
					*wqids =
					    kmalloc(*nwqid *
						    sizeof(struct p9_qid),
						    GFP_KERNEL);
					if (*wqids == NULL)
						errcode = -ENOMEM;
				}

				if (!errcode) {
					int i;

					for (i = 0; i < *nwqid; i++) {
						errcode =
						    p9pdu_readf(pdu, optional,
								"Q",
								&(*wqids)[i]);
						if (errcode)
							break;
					}
				}

				if (errcode) {
					kfree(*wqids);
					*wqids = NULL;
				}
			}
			break;
		case '?':
			if (!optional)
				return 0;
			break;
		default:
			BUG();
			break;
		}

		if (errcode)
			break;
	}

	return errcode;
}

int
p9pdu_vwritef(struct p9_fcall *pdu, int optional, const char *fmt, va_list ap)
{
	const char *ptr;
	int errcode = 0;

	for (ptr = fmt; *ptr; ptr++) {
		switch (*ptr) {
		case 'b':{
				int8_t val = va_arg(ap, int);
				if (pdu_write(pdu, &val, sizeof(val)))
					errcode = -EFAULT;
			}
			break;
		case 'w':{
				int16_t val = va_arg(ap, int);
				if (pdu_write(pdu, &val, sizeof(val)))
					errcode = -EFAULT;
			}
			break;
		case 'd':{
				int32_t val = va_arg(ap, int32_t);
				if (pdu_write(pdu, &val, sizeof(val)))
					errcode = -EFAULT;
			}
			break;
		case 'q':{
				int64_t val = va_arg(ap, int64_t);
				if (pdu_write(pdu, &val, sizeof(val)))
					errcode = -EFAULT;
			}
			break;
		case 's':{
				const char *ptr = va_arg(ap, const char *);
				int16_t len = 0;

				if (ptr)
					len = MIN(strlen(ptr), USHORT_MAX);

				errcode = p9pdu_writef(pdu, optional, "w", len);
				if (!errcode && pdu_write(pdu, ptr, len))
					errcode = -EFAULT;
			}
			break;
		case 'Q':{
				const struct p9_qid *qid =
				    va_arg(ap, const struct p9_qid *);
				errcode =
				    p9pdu_writef(pdu, optional, "bdq",
						 qid->type, qid->version,
						 qid->path);
			} break;
		case 'S':{
				const struct p9_wstat *stbuf =
				    va_arg(ap, const struct p9_wstat *);
				errcode =
				    p9pdu_writef(pdu, optional,
						 "wwdQdddqssss?sddd",
						 stbuf->size, stbuf->type,
						 stbuf->dev, stbuf->qid,
						 stbuf->mode, stbuf->atime,
						 stbuf->mtime, stbuf->length,
						 stbuf->name, stbuf->uid,
						 stbuf->gid, stbuf->muid,
						 stbuf->extension, stbuf->n_uid,
						 stbuf->n_gid, stbuf->n_muid);
			} break;
		case 'D':{
				int32_t count = va_arg(ap, int32_t);
				const void *data = va_arg(ap, const void *);

				errcode =
				    p9pdu_writef(pdu, optional, "d", count);
				if (!errcode && pdu_write(pdu, data, count))
					errcode = -EFAULT;
			}
			break;
		case 'T':{
				int16_t nwname = va_arg(ap, int);
				const char **wnames = va_arg(ap, const char **);

				errcode =
				    p9pdu_writef(pdu, optional, "w", nwname);
				if (!errcode) {
					int i;

					for (i = 0; i < nwname; i++) {
						errcode =
						    p9pdu_writef(pdu, optional,
								 "s",
								 wnames[i]);
						if (errcode)
							break;
					}
				}
			}
			break;
		case 'R':{
				int16_t nwqid = va_arg(ap, int);
				struct p9_qid *wqids =
				    va_arg(ap, struct p9_qid *);

				errcode =
				    p9pdu_writef(pdu, optional, "w", nwqid);
				if (!errcode) {
					int i;

					for (i = 0; i < nwqid; i++) {
						errcode =
						    p9pdu_writef(pdu, optional,
								 "Q",
								 &wqids[i]);
						if (errcode)
							break;
					}
				}
			}
			break;
		case '?':
			if (!optional)
				return 0;
			break;
		default:
			BUG();
			break;
		}

		if (errcode)
			break;
	}

	return errcode;
}

int p9pdu_readf(struct p9_fcall *pdu, int optional, const char *fmt, ...)
{
	va_list ap;
	int ret;

	va_start(ap, fmt);
	ret = p9pdu_vreadf(pdu, optional, fmt, ap);
	va_end(ap);

	return ret;
}

static int
p9pdu_writef(struct p9_fcall *pdu, int optional, const char *fmt, ...)
{
	va_list ap;
	int ret;

	va_start(ap, fmt);
	ret = p9pdu_vwritef(pdu, optional, fmt, ap);
	va_end(ap);

	return ret;
}