/*
 * lucihttp - HTTP utility library - urlencoded data parser component
 *
 * Copyright 2018 Jo-Philipp Wich <jo@mein.io>
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#define _GNU_SOURCE

#include <lucihttp/urlencoded-parser.h>

#include <string.h>
#include <stdlib.h>
#include <stdarg.h>


static const char *lh_urldec_state_descriptions[] = {
	"start of tuple name",
	"reading tuple name",
	"start of tuple value",
	"reading tuple value",
	"end of body",
	"parser error state"
};


static void
lh_urldec_dump(FILE *fp, const char *prefix, const char *buf, size_t len)
{
	size_t i;

	fprintf(fp, "%s=(%lu)[", prefix, (unsigned long)len);

	for (i = 0; i < len; i++)
		fprintf(fp, "%c", buf[i] < 0x20 ? '.' : buf[i]);

	fprintf(fp, "]\n");
}

static void
lh_urldec_set_state(struct lh_urldec *p, enum lh_urldec_state stateval)
{
	if (stateval == p->state)
		return;

	if (p->trace)
		fprintf(p->trace, "State %d (%s) -> %d (%s)\n",
		        p->state, lh_urldec_state_descriptions[p->state],
		        stateval, lh_urldec_state_descriptions[stateval]);

	p->state = stateval;
}

static bool
_lh_urldec_invoke(struct lh_urldec *p, enum lh_urldec_callback_type type,
                const char *typename, const char *buf, size_t len)
{
	if (p->trace) {
		fprintf(p->trace, "Callback %d (%s) ", type, typename);
		lh_urldec_dump(p->trace, "data", buf, len);
	}

	if (p->cb)
		return p->cb(p, type, buf, len, p->priv);

	return true;
}

#define lh_urldec_invoke(p, type, buf, len) \
	_lh_urldec_invoke(p, LH_UD_CB_##type, #type, buf, len)

static bool
lh_urldec_set_token(struct lh_urldec *p, enum lh_urldec_token_type type,
                    bool clear, const char *buf, size_t len)
{
	struct lh_urldec_token *tok = &p->token[type];
	char *tmp;

	if (clear)
		tok->len = 0;

	if (len + tok->len + 1 > tok->size) {
		tmp = realloc(tok->value, len + tok->len + 1);

		if (!tmp)
			return false;

		tok->value = tmp;
		tok->size = len + tok->len + 1;
	}

	if (len) {
		memcpy(tok->value + tok->len, buf, len);
		tok->value[tok->len + len] = 0;
		tok->len += len;
	}

	return true;
}

static char *
lh_urldec_get_token(struct lh_urldec *p, enum lh_urldec_token_type type,
                    size_t *len)
{
	struct lh_urldec_token *tok = &p->token[type];

	if (len)
		*len = tok->len;

	if (tok->len)
		return tok->value;

	return NULL;
}

static bool
lh_urldec_error(struct lh_urldec *p, size_t off, const char *fmt, ...)
{
	va_list ap;
	char *msg;
	int rv;

	va_start(ap, fmt);
	rv = vasprintf(&msg, fmt, ap);
	va_end(ap);

	if (rv != -1) {
		rv = asprintf(&p->error, "At %s, byte offset %lu, %s",
		              lh_urldec_state_descriptions[p->state],
		              (unsigned long)(p->total + off), msg);
		free(msg);
	}

	lh_urldec_invoke(p, ERROR, (rv == -1) ? "Out of memory" : p->error,
	                           (rv == -1) ? 13 : rv);

	lh_urldec_set_state(p, LH_UD_S_ERROR);

	return false;
}

struct lh_urldec *
lh_urldec_new(FILE *trace)
{
	struct lh_urldec *p;

	p = calloc(1, sizeof(*p));

	return lh_urldec_init(p, trace);
}

struct lh_urldec *
lh_urldec_init(struct lh_urldec *p, FILE *trace)
{
	if (!p)
		return NULL;

	p->trace = trace;
	p->size_limit = LH_UD_T_DEFAULT_SIZE_LIMIT;

	lh_urldec_set_state(p, LH_UD_S_NAME_START);

	return p;
}

void
lh_urldec_set_callback(struct lh_urldec *p, lh_urldec_callback cb, void *priv)
{
	p->cb = cb;
	p->priv = priv;
}

void
lh_urldec_set_size_limit(struct lh_urldec *p, size_t limit)
{
	if (limit >= 1024)
		p->size_limit = limit;
}

#define EOB (-2)

static bool
lh_urldec_step(struct lh_urldec *p, const char *buf, size_t off, int c)
{
	size_t l, keylen, vallen;
	char *key, *val;

	switch (p->state) {
	case LH_UD_S_NAME_START:
		p->offset = off;
		p->flags &= ~LH_UD_F_GOT_NAME;
		p->flags &= ~LH_UD_F_GOT_VALUE;

		if (lh_urldec_invoke(p, TUPLE, NULL, 0))
			p->flags |= LH_UD_F_BUFFERING;
		else
			p->flags &= LH_UD_F_BUFFERING;

		lh_urldec_set_token(p, LH_UD_T_NAME, true, NULL, 0);
		lh_urldec_set_token(p, LH_UD_T_VALUE, true, NULL, 0);
		lh_urldec_set_state(p, LH_UD_S_NAME);

		/* fall through */

	case LH_UD_S_NAME:
		if (c == '=' || c == '&' || c <= EOF) {
			keylen = (off - p->offset);

			if (p->flags & LH_UD_F_BUFFERING) {
				lh_urldec_get_token(p, LH_UD_T_NAME, &l);

				if (l + keylen > p->size_limit)
					return lh_urldec_error(p, off, "the key exceeds the "
					                               "maximum allowed size");

				lh_urldec_set_token(p, LH_UD_T_NAME, false,
				                    buf + p->offset, keylen);

				if ((c == '&' || c == EOF) && (p->flags & LH_UD_F_GOT_NAME)) {
					key = lh_urldec_get_token(p, LH_UD_T_NAME, &keylen);
					val = lh_urldec_get_token(p, LH_UD_T_VALUE, &vallen);

					lh_urldec_invoke(p, NAME, key, keylen);
					lh_urldec_invoke(p, VALUE, val, vallen);
				}
			}
			else {
				lh_urldec_invoke(p, NAME, buf + p->offset, keylen);
			}

			if (c == '=')
				lh_urldec_set_state(p, LH_UD_S_VALUE_START);
			else if (c == '&')
				lh_urldec_set_state(p, LH_UD_S_NAME_START);
			else if (c == EOF)
				lh_urldec_set_state(p, LH_UD_S_END);
		}
		else {
			p->flags |= LH_UD_F_GOT_NAME;
		}

		break;

	case LH_UD_S_VALUE_START:
		p->offset = off;
		p->flags |= LH_UD_F_GOT_VALUE;
		lh_urldec_set_state(p, LH_UD_S_VALUE);

		/* fall through */

	case LH_UD_S_VALUE:
		if (c == '&' || c <= EOF) {
			vallen = (off - p->offset);

			if (p->flags & LH_UD_F_BUFFERING) {
				lh_urldec_get_token(p, LH_UD_T_VALUE, &l);

				if (l + vallen > p->size_limit)
					return lh_urldec_error(p, off, "the value exceeds the "
					                               "maximum allowed size");

				lh_urldec_set_token(p, LH_UD_T_VALUE, false,
				                    buf + p->offset, vallen);

				if ((c != EOB) && (p->flags & LH_UD_F_GOT_NAME)) {
					key = lh_urldec_get_token(p, LH_UD_T_NAME, &keylen);
					val = lh_urldec_get_token(p, LH_UD_T_VALUE, &vallen);

					lh_urldec_invoke(p, NAME, key, keylen);
					lh_urldec_invoke(p, VALUE, val, vallen);
				}
			}
			else {
				lh_urldec_invoke(p, VALUE, buf + p->offset, vallen);
			}

			if (c > EOF)
				lh_urldec_set_state(p, LH_UD_S_NAME_START);
			else if (c == EOF)
				lh_urldec_set_state(p, LH_UD_S_END);
		}

		break;

	case LH_UD_S_END:
		if (c > EOF) {
			return lh_urldec_error(p, off, "expected EOF, but got "
			                               "trailing junk");
		}

		break;

	default:
		return lh_urldec_error(p, off, "parser is in unrecoverable "
		                               "error state");
	}

	return true;
}

bool
lh_urldec_parse(struct lh_urldec *p, const char *buf, size_t len)
{
	size_t i;

	p->offset = 0;

	if (p->trace)
		lh_urldec_dump(p->trace, "Parsing buffer", buf, len);

	for (i = 0; i < len; i++)
		if (!lh_urldec_step(p, buf, i, (unsigned char)buf[i]))
			return false;

	if (!lh_urldec_step(p, buf, len, buf ? EOB : EOF))
		return false;

	p->total += i;

	return true;
}

void
lh_urldec_free(struct lh_urldec *p)
{
	int i;

	if (p->error)
		free(p->error);

	for (i = 0; i < __LH_UD_T_COUNT; i++)
		if (p->token[i].value)
			free(p->token[i].value);

	free(p);
}
