/*
 * $Id: header.c 13369 2007-04-19 23:23:34Z cbiere $
 *
 * Copyright (c) 2001-2003, Raphael Manfredi
 *
 *----------------------------------------------------------------------
 * This file is part of gtk-gnutella.
 *
 *  gtk-gnutella 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.
 *
 *  gtk-gnutella 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 gtk-gnutella; if not, write to the Free Software
 *  Foundation, Inc.:
 *      59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *----------------------------------------------------------------------
 */

/**
 * @ingroup lib
 * @file
 *
 * Header parsing routines.
 *
 * @author Raphael Manfredi
 * @date 2001-2003
 */

#include "common.h"

RCSID("$Id: header.c 13369 2007-04-19 23:23:34Z cbiere $")

#include "header.h"
#include "glib-missing.h"
#include "misc.h"
#include "walloc.h"
#include "getline.h"		/* For MAX_LINE_SIZE */
#include "slist.h"

#include "override.h"		/* Must be the last header included */

/*
 * The `headers' field is a hash table indexed by field name (case-insensitive).
 * Each value (GString) holds a private copy of the string making that header,
 * with all continuations removed (leading spaces collapsed into one), and
 * indentical fields concatenated using ", " separators, per RFC2616.
 *
 * The `fields' field holds a list of all the fields, in the order they
 * appeared.  The value is a header_field_t structure.  It allows one to
 * dump the header exactly as it was read.
 */

struct header {
	GHashTable *headers;		/**< Indexed by name */
	slist_t *fields;			/**< Ordered list of header_field_t */
	gint flags;					/**< Various operating flags */
	gint size;					/**< Total header size, in bytes */
	gint num_lines;				/**< Total header lines seen */
};

/**
 * A header field.
 *
 * It holds the field name, and all the lines that make up that field.
 * The first line has the field name and the ":" stripped, as well as
 * all the leading spaces.  Continuations also have their leading spaces
 * stripped out.
 *
 * For instance, assume the following header field:
 *
 *    - X-Comment: first line
 *         and continuation of first line
 *
 * Then the structure would contain, with () denoting a list:
 *
 *    - name = "X-Comment"
 *    - lines = ("first line", "and continuation of first line")
 */

typedef struct {
	gchar *name;				/**< Field name */
	slist_t *lines;				/**< List of lines making this header */
} header_field_t;

/***
 *** Operating flags
 ***/

enum {
	HEAD_F_EOH	= 0x00000001,	/**< EOH reached */
	HEAD_F_SKIP	= 0x00000002	/**< Skip continuations */
};

/***
 *** Error code management
 ***/

static const char *error_str[] = {
	"OK",									/**< HEAD_OK */
	"Unexpected continuation line",			/**< HEAD_CONTINUATION */
	"Malformed header line",				/**< HEAD_MALFORMED */
	"Invalid characters in field name",		/**< HEAD_BAD_CHARS */
	"End of header already reached",		/**< HEAD_EOH_REACHED */
	"Skipped continuation line",			/**< HEAD_SKIPPED */
	"Header too large",						/**< HEAD_TOO_LARGE */
	"Header has too many lines",			/**< HEAD_MANY_LINES */
	"End of header",						/**< HEAD_EOH */
};

/**
 * @return human-readable error string corresponding to error code `errnum'.
 */
const gchar *
header_strerror(guint errnum)
{
	if (errnum >= G_N_ELEMENTS(error_str))
		return "Invalid error code";

	return error_str[errnum];
}

gint
header_num_lines(const header_t *h)
{
	return h->num_lines;
}

/***
 *** header_field object
 ***/

/**
 * Create a new empty header field, whose name is `name'.
 * A private copy of `name' is done.
 */
static header_field_t *
hfield_make(const gchar *name)
{
	header_field_t *h;

	h = walloc0(sizeof *h);
	h->name = g_strdup(name);

	return h;
}

static void
hfield_free_item(gpointer p, gpointer unused_data)
{
	(void) unused_data;
	G_FREE_NULL(p);
}

/**
 * Dispose of the header field.
 */
static void
hfield_free(header_field_t *h)
{
	if (h->lines) {
		slist_foreach(h->lines, hfield_free_item, NULL);
		slist_free(&h->lines);
	}
	G_FREE_NULL(h->name);
	wfree(h, sizeof *h);
}

/**
 * Append line of text to given header field.
 * A private copy of the data is made.
 */
static void
hfield_append(header_field_t *h, const gchar *text)
{
	if (!h->lines) {
		h->lines = slist_new();
	}
	slist_append(h->lines, g_strdup(text));
}

/**
 * Dump field on specified file descriptor.
 */
static void
hfield_dump(const header_field_t *h, FILE *out)
{
	slist_iter_t *iter;
	gboolean first;

	g_assert(h->lines);

	fprintf(out, "%s: ", h->name);

	first = TRUE;
	iter = slist_iter_on_head(h->lines);
	for (/* NOTHING */; slist_iter_has_item(iter); slist_iter_next(iter)) {
		if (!first) {
			first = FALSE;
			fputs("    ", out);			/* Continuation line */
		}
		fputs(slist_iter_current(iter), out);
		fputc('\n', out);
	}
	slist_iter_free(&iter);	
}

/***
 *** header object
 ***/

static GHashTable *
header_get_table(header_t *o)
{
	if (!o->headers) {
		o->headers = g_hash_table_new(str_case_hash_func, str_case_eq_func);
	}
	return o->headers;
}

/**
 * Create a new header object.
 */
header_t *
header_make(void)
{
	header_t *o;

	o = walloc0(sizeof *o);
	return o;
}

/**
 * Frees the key/values from the headers hash.
 */
static gboolean
free_header_data(gpointer key, gpointer value, gpointer unused_udata)
{
	(void) unused_udata;

	G_FREE_NULL(key);		/* XXX if shared, don't do that */
	g_string_free(value, TRUE);
	return TRUE;
}

/**
 * Destroy header object.
 */
void
header_free(header_t *o)
{
	g_assert(o);

	header_reset(o);
	wfree(o, sizeof *o);
}

static void
header_reset_item(gpointer p, gpointer unused_data)
{
	(void) unused_data;
	hfield_free(p);
}

/**
 * Reset header object, for new header parsing.
 */
void
header_reset(header_t *o)
{
	static const header_t zero_header;

	g_assert(o);

	if (o->headers) {
		g_hash_table_foreach_remove(o->headers, free_header_data, NULL);
		g_hash_table_destroy(o->headers);
		o->headers = NULL;
	}
	if (o->fields) {
		slist_foreach(o->fields, header_reset_item, NULL);
		slist_free(&o->fields);
	}
	*o = zero_header;
}

/**
 * Get field value, or NULL if not present.  The value returned is a
 * pointer to the internals of the header structure, so it must not be
 * kept around.
 */
gchar *
header_get(const header_t *o, const gchar *field)
{
	GString *v;

	if (o->headers) {
		v = g_hash_table_lookup(o->headers, deconstify_gchar(field));
	} else {
		v = NULL;
	}
	return v ? v->str : NULL;
}

/**
 * Get field value, or NULL if not present.  The value returned is a
 * copy of the internal value, so it may be kept around, but must be
 * freed by the caller.
 */
gchar *
header_getdup(const header_t *o, const gchar *field)
{
	return g_strdup(header_get(o, field));
}

/**
 * Add header line to the `headers' hash for specified field name.
 * A private copy of the `field' name and of the `text' data is made.
 */
static void
add_header(header_t *o, const gchar *field, const gchar *text)
{
	GHashTable *ht;
	GString *v;

	ht = header_get_table(o);
	v = g_hash_table_lookup(ht, field);
	if (v) {
		/*
		 * Header already exists, according to RFC2616 we need to append
		 * the value, comma-separated.
		 */

		g_string_append(v, ", ");
		g_string_append(v, text);

	} else {
		gchar *key;

		/*
		 * Create a new header entry in the hash table.
		 */

		key = g_strdup(field);
		v = g_string_new(text);
		g_hash_table_insert(ht, key, v);
	}
}

/**
 * Add continuation line to the `headers' hash for specified field name.
 * A private copy of the data is made.
 */
static void
add_continuation(header_t *o, const gchar *field, const gchar *text)
{
	GString *v;

	g_assert(o->headers);
	v = g_hash_table_lookup(o->headers, field);
	g_assert(v);
	g_string_append_c(v, ' ');
	g_string_append(v, text);
}

/**
 * Append a new line of text at the end of the header.
 * A private copy of the text is made.
 *
 * @return an error code, or HEAD_OK if appending was successful.
 */
gint
header_append(header_t *o, const gchar *text, gint len)
{
	gchar buf[MAX_LINE_SIZE];
	const gchar *p = text;
	guchar c;
	header_field_t *hf;

	if (o->flags & HEAD_F_EOH)
		return HEAD_EOH_REACHED;

	/*
	 * If empty line, we reached EOH.
	 */

	if (len == 0) {
		o->flags |= HEAD_F_EOH;				/* Mark we reached EOH */
		return HEAD_EOH;
	}

	/*
	 * Sanity checks.
	 */

	if (o->size >= HEAD_MAX_SIZE)
		return HEAD_TOO_LARGE;

	if (++(o->num_lines) >= HEAD_MAX_LINES)
		return HEAD_MANY_LINES;

	/*
	 * Detect whether line is a new header or a continuation.
	 */

	c = *p;
	if (is_ascii_space(c)) {

		/*
		 * It's a continuation.
		 *
		 * Make sure we already have recorded something, or we have
		 * an unexpected continuation line.
		 */

		if (NULL == o->fields)
			return HEAD_CONTINUATION;		/* Unexpected continuation */

		/*
		 * When a previous header line was malformed, we cannot accept
		 * further continuation lines.
		 */

		if (o->flags & HEAD_F_SKIP)
			return HEAD_SKIPPED;

		/*
		 * We strip leading spaces of all continuations before storing
		 * them.  If we have to dump the header, we will have to put
		 * some spaces, but we don't guarantee we'll put the same amount.
		 */

		p++;								/* First char is known space */
		while ((c = *p)) {
			if (!is_ascii_space(c))
				break;
			p++;
		}

		/*
		 * If we've reached the end of the line, then the continuation
		 * line was made of spaces only.  Weird, but we can ignore it.
		 * Note that it's not an EOH mark.
		 */

		if (*p == '\0')
			return HEAD_OK;

		/*
		 * Save the continuation line by appending into the last header
		 * field we handled.
		 */

		hf = slist_tail(o->fields);
		hfield_append(hf, p);
		add_continuation(o, hf->name, p);
		o->size += len - (p - text);	/* Count only effective text */

		/*
		 * Also append the data in the hash table.
		 */

	} else {
		gchar *b;
		gboolean seen_space = FALSE;

		/*
		 * It's a new header line.
		 */

		o->flags &= ~HEAD_F_SKIP;		/* Assume this line will be OK */

		/*
		 * Parse header field.  Must be composed of ascii chars only.
		 * (no control characters, no space, no ISO Latin or other extension).
		 * The field name ends with ':', after possible white spaces.
		 */

		for (b = buf, c = *p; c; c = *(++p)) {
			if (c == ':') {
				*b++ = '\0';			/* Reached end of field */
				break;					/* Done, buf[] holds field name */
			}
			if (is_ascii_space(c)) {
				seen_space = TRUE;		/* Only trailing spaces allowed */
				continue;
			}
			if (
				seen_space || (c != '-' &&
					(!isascii(c) || is_ascii_cntrl(c) || is_ascii_punct(c)))
			) {
				o->flags |= HEAD_F_SKIP;
				return HEAD_BAD_CHARS;
			}
			*b++ = c;
		}

		/*
		 * If buf[] does not end with a NUL, we did not fully recognize
		 * the header: we reached the end of the line without encountering
		 * the ':' marker.
		 *
		 * If the buffer starts with a NUL char, it's also clearly malformed.
		 */

		g_assert(b > buf || (b == buf && *text == '\0'));

		if (b == buf || *(b-1) != '\0') {
			o->flags |= HEAD_F_SKIP;
			return HEAD_MALFORMED;
		}

		/*
		 * We have a valid header field in buf[].
		 */

		hf = hfield_make(buf);

		/*
		 * Strip leading spaces in the value.
		 */

		g_assert(*p == ':');

		p++;							/* First char is field separator */
		p = skip_ascii_spaces(p);

		/*
		 * Record field value.
		 */

		hfield_append(hf, p);
		add_header(o, buf, p);
		if (!o->fields) {
			o->fields = slist_new();
		}
		slist_append(o->fields, hf);
		o->size += len - (p - text);	/* Count only effective text */
	}

	return HEAD_OK;
}

static void
header_dump_item(gpointer p, gpointer user_data)
{
	hfield_dump(p, user_data);
}

/**
 * Dump whole header on specified file.
 */
void
header_dump(const header_t *o, FILE *out)
{
	if (o->fields) {
		slist_foreach(o->fields, header_dump_item, out);
	}
}

/***
 *** Header formatting with continuations.
 ***/

#define HEADER_FMT_MAGIC		0xf7a91c
#define HEADER_FMT_DFLT_LEN		256		/**< Default field length if no hint */
#define HEADER_FMT_LINE_LEN		78		/**< Try to never emit longer lines */
#define HEADER_FMT_MAX_SIZE		1024	/**< Max line size for header */

/**
 * Header formatting context.
 */
struct header_fmt {
	guint32 magic;
	gint maxlen;			/**< Maximum line length before continuation */
	GString *header;		/**< Header being built */
	gchar sep[257];			/**< Optional separator */
	gint seplen;			/**< Length of separator string */
	gint stripped_seplen;	/**< Length of separator without trailing space */
	gint current_len;		/**< Length of currently built line */
	gboolean data_emitted;	/**< Whether data was ever emitted */
	gboolean frozen;		/**< Header terminated */
};

/**
 * Compute the length of the string `s' whose length is `len' with trailing
 * whitespace ignored.
 */
static gint
stripped_strlen(const gchar *s, gint len)
{
	const gchar *end = s + len;
	gint i;

	/*
	 * Locate last non-space char in separator.
	 */

	/* XXX: Shouldn't this allow at least \t or maybe use isspace()? */
	for (i = len - 1; i >= 0; i--, end--) {
		if (s[i] != ' ')
			return end - s;
	}

	return 0;
}

/**
 * Create a new formatting context for a header line.
 *
 * @param `field' is the header field name, without trailing ':'.
 *
 * @param `separator' is the optional default separator to emit between
 * the values added via header_fmd_append_value().  To supersede the
 * default separator, use header_fmd_append() and specify another separator
 * explicitly.  If set to NULL, there will be no default separator and
 * values will be simply concatenated together.  The value given must
 * NOT be freed before the header_fmt_end() call (usually it will just
 * be a static string).  Trailing spaces in the separator will be stripped
 * if it is emitted at the end of a line before a continuation.
 *
 * @param `len_hint' is the expected line size, for pre-sizing purposes.
 * (0 to guess).
 *
 * @return opaque pointer.
 */
gpointer
header_fmt_make(const gchar *field, const gchar *separator, gint len_hint)
{
	struct header_fmt *hf;
	size_t len;

	g_assert(!separator || strlen(separator) < sizeof(hf->sep));
	g_assert(len_hint >= 0);

	hf = walloc(sizeof(*hf));
	hf->magic = HEADER_FMT_MAGIC;
	hf->header = g_string_sized_new(len_hint ? len_hint : HEADER_FMT_DFLT_LEN);
	hf->maxlen = HEADER_FMT_LINE_LEN;
	hf->data_emitted = FALSE;
	hf->frozen = FALSE;
	len = g_strlcpy(hf->sep, separator ? separator : "", sizeof(hf->sep));
	hf->seplen = MIN(len, (sizeof hf->sep - 1));
	hf->stripped_seplen = stripped_strlen(hf->sep, hf->seplen);
	g_string_append(hf->header, field);
	g_string_append(hf->header, ": ");

	hf->current_len = hf->header->len;

	return hf;
}

/**
 * Set max line length.
 */
void
header_fmt_set_line_length(gpointer o, gint maxlen)
{
	struct header_fmt *hf = o;

	g_assert(hf->magic == HEADER_FMT_MAGIC);
	g_assert(maxlen > 0);

	hf->maxlen = maxlen;
}

/**
 * Dispose of header formatting context.
 */
void
header_fmt_free(gpointer o)
{
	struct header_fmt *hf = o;

	g_assert(hf->magic == HEADER_FMT_MAGIC);

	g_string_free(hf->header, TRUE);
	hf->magic = 0;
	wfree(hf, sizeof *hf);
}

/**
 * Checks whether appending `len' bytes of data to the header would fit
 * within the `maxlen' total header size requirement in case a continuation
 * is emitted, and using the configured separator.
 *
 * @attention
 * NB: The `maxlen' parameter is the amount of data that can be generated for
 * the header string, not counting the final "\r\n" + the trailing NUL byte.
 */
gboolean
header_fmt_value_fits(gpointer o, gint len, gint maxlen)
{
	struct header_fmt *hf = o;
	gint final_len;

	g_assert(hf->magic == HEADER_FMT_MAGIC);

	/*
	 * If it fits on the line, no continuation will have to be emitted.
	 * Otherwise, we'll need the stripped version of the separator,
	 * followed by "\r\n\t" (3 chars).
	 */

	if (hf->current_len + len + hf->seplen <= hf->maxlen)
		final_len = hf->header->len + len + hf->seplen;
	else
		final_len = hf->header->len + len + hf->stripped_seplen + 3;

	return final_len < maxlen;	/* Could say "<=" perhaps, but let's be safe */
}

/**
 * Append data `str' to the header line, atomically.
 *
 * @param `hf' no brief description.
 * @param `str' no brief description.
 * @param `separator' is an optional separator string that will be emitted
 *         BEFORE outputting the data, and only when nothing has been emitted
 *         already.
 * @param `slen' is the separator length, 0 if empty.
 * @param `sslen' is the stripped separator length, -1 if unknown yet.
 */
static void
header_fmt_append_full(struct header_fmt *hf, const gchar *str,
	const gchar *separator, gint slen, gint sslen)
{
	size_t len;
	gint curlen;

	len = strlen(str);
	g_assert(len <= INT_MAX);	/* Legacy bug */
	curlen = hf->current_len;

	if (curlen + len + slen > (size_t) hf->maxlen) {
		/*
		 * Emit sperator, if any and data was already emitted.
		 */

		if (separator != NULL && hf->data_emitted) {
			gint s = sslen >= 0 ? sslen : stripped_strlen(separator, slen);
			const gchar *p;

			for (p = separator; s > 0; p++, s--)
				g_string_append_c(hf->header, *p);
		}

		g_string_append(hf->header, "\r\n\t");	/* Includes continuation */
		curlen = 1;								/* One tab */
	} else if (hf->data_emitted) {
		g_string_append(hf->header, separator);
		curlen += slen;
	}

	hf->data_emitted = TRUE;
	g_string_append(hf->header, str);
	hf->current_len = curlen + len;
}

/**
 * Append data `str' to the header line, atomically.
 *
 * `separator' is an optional separator string that will be emitted BEFORE
 * outputting the data, and only when nothing has been emitted already.
 * Any trailing space will be stripped out of `separator' if emitting at the
 * end of a line.  It supersedes any separator configured at make time.
 *
 * To use the standard separator, use header_fmt_append_value().
 */
void
header_fmt_append(gpointer o, const gchar *str, const gchar *separator)
{
	struct header_fmt *hf = o;
	gint seplen;

	g_assert(hf->magic == HEADER_FMT_MAGIC);
	g_assert(!hf->frozen);

	seplen = (separator == NULL) ? 0 : strlen(separator);

	header_fmt_append_full(hf, str, separator, seplen, -1);
}

/**
 * Append data `str' to the header line, atomically.
 *
 * Values are separated using the string specified at make time, if any.
 * If emitted before a continuation, the version with stripped trailing
 * whitespaces is used.
 *
 * To supersede the default separator, use header_fmt_append().
 */
void
header_fmt_append_value(gpointer o, const gchar *str)
{
	struct header_fmt *hf = o;

	g_assert(hf->magic == HEADER_FMT_MAGIC);
	g_assert(!hf->frozen);

	header_fmt_append_full(hf, str, hf->sep, hf->seplen, hf->stripped_seplen);
}

/**
 * @return length of currently formatted header.
 */
gint
header_fmt_length(gpointer o)
{
	struct header_fmt *hf = o;

	g_assert(hf->magic == HEADER_FMT_MAGIC);

	return hf->header->len;
}

/**
 * Terminate header, emitting the trailing "\r\n".
 * Further appending is forbidden.
 */
void
header_fmt_end(gpointer o)
{
	struct header_fmt *hf = o;

	g_assert(hf->magic == HEADER_FMT_MAGIC);
	g_assert(!hf->frozen);

	g_string_append(hf->header, "\r\n");
	hf->frozen = TRUE;
}

/**
 * @return current header string.
 */
gchar *
header_fmt_string(gpointer o)
{
	struct header_fmt *hf = o;

	g_assert(hf->magic == HEADER_FMT_MAGIC);

	return hf->header->str;		/* Guaranteed to be always NUL-terminated */
}

/**
 * Convert current header to a string.
 *
 * @attention
 * NB: returns pointer to static data!
 */
gchar *
header_fmt_to_string(gpointer o)
{
	static gchar line[HEADER_FMT_MAX_SIZE + 1];
	struct header_fmt *hf = o;

	g_assert(hf->magic == HEADER_FMT_MAGIC);

	if (hf->header->len > HEADER_FMT_MAX_SIZE)
		g_warning("trying to format too long an HTTP line (%d bytes)",
			(int) hf->header->len);

	strncpy(line, hf->header->str, HEADER_FMT_MAX_SIZE);
	line[HEADER_FMT_MAX_SIZE] = '\0';

	return line;
}

/* vi: set ts=4 sw=4 cindent: */
