/*
 * $Id: magnet.c 13966 2007-06-24 10:54:27Z cbiere $
 *
 * Copyright (c) 2006, Christian Biere
 *
 *----------------------------------------------------------------------
 * 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
 *
 * Handling of magnet links.
 *
 * @todo TODO: Utilize hashlists to prevent duplicate sources.
 *
 * @author Christian Biere
 * @date 2006
 */

#include "common.h"

RCSID("$Id: magnet.c 13966 2007-06-24 10:54:27Z cbiere $")

#include "lib/atoms.h"
#include "lib/glib-missing.h"
#include "lib/magnet.h"
#include "lib/tm.h"
#include "lib/url.h"
#include "lib/urn.h"
#include "lib/utf8.h"
#include "lib/walloc.h"

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

/*
 * Private prototypes;
 */

/*
 * Private data
 */

enum magnet_key {
	MAGNET_KEY_NONE,
	MAGNET_KEY_DISPLAY_NAME,	/* Display Name */
	MAGNET_KEY_KEYWORD_TOPIC,	/* Keyword Topic */
	MAGNET_KEY_EXACT_LENGTH,	/* eXact file Length */
	MAGNET_KEY_EXACT_SOURCE,	/* eXact Source */
	MAGNET_KEY_EXACT_TOPIC,		/* eXact Topic */
	MAGNET_KEY_PARQ_ID,			/* PARQ ID */
	
	NUM_MAGNET_KEYS
};

static const struct {
	const char * const key;
	const enum magnet_key id;
} magnet_keys[] = {
	{ "",			MAGNET_KEY_NONE },
	{ "dn",			MAGNET_KEY_DISPLAY_NAME },
	{ "kt",			MAGNET_KEY_KEYWORD_TOPIC },
	{ "xl",			MAGNET_KEY_EXACT_LENGTH },
	{ "xs",			MAGNET_KEY_EXACT_SOURCE },
	{ "xt",			MAGNET_KEY_EXACT_TOPIC },
	{ "x.parq-id",	MAGNET_KEY_PARQ_ID },
};

/*
 * Private functions
 */

static void
clear_error_str(const gchar ***error_str)
{
	if (NULL == *error_str) {
		static const gchar *error_dummy;
		*error_str = &error_dummy;
	}
	**error_str = NULL;
}

static enum magnet_key
magnet_key_get(const gchar *s)
{
	guint i;

	STATIC_ASSERT(G_N_ELEMENTS(magnet_keys) == NUM_MAGNET_KEYS);
	g_assert(s);
	
	for (i = 0; i < G_N_ELEMENTS(magnet_keys); i++) {
		if (0 == ascii_strcasecmp(magnet_keys[i].key, s))
			return magnet_keys[i].id;
	}

	return MAGNET_KEY_NONE;
}

static void
plus_to_space(gchar *s)
{
	while (s) {
		s = strchr(s, '+');
		if (s) {
			*s++ = ' ';
		}
	}
}

static struct magnet_source *
magnet_parse_location(const gchar *uri, const gchar **error_str)
{
	static const struct magnet_source zero_ms;
	struct magnet_source ms;
	const gchar *p, *endptr, *host, *host_end;

	clear_error_str(&error_str);
	g_return_val_if_fail(uri, NULL);

	ms = zero_ms;
	p = uri;

	if (!string_to_host_or_addr(p, &endptr, &ms.addr)) {
		*error_str = "Expected host part";
		return NULL;
	}

	if (is_host_addr(ms.addr)) {
		host = NULL;
		host_end = NULL;
	} else {
		host = p;
		host_end = endptr;
	}
	p += endptr - p;

	if (':' == *p) {
		const gchar *ep2;
		gint error;
		guint16 u;

		p++;
		u = parse_uint16(p, &ep2, 10, &error);
		if (error) {
			*error_str = "TCP port is out of range";
			/* Skip this parameter */
			return NULL;
		}

		ms.port = u;
		p += ep2 - p;
	} else {
		ms.port = 80;
	}

	if ('/' != *p) {
		*error_str = "Expected port followed by '/'";
		/* Skip this parameter */
		return NULL;
	}
	g_assert(*p == '/');

	endptr = is_strprefix(p, "/uri-res/N2R?");
	if (endptr) {
		struct sha1 sha1;
		
		p = endptr;
		if (!urn_get_sha1(p, &sha1)) {
			*error_str = "Bad SHA1 in MAGNET URI";
			return NULL;
		}
		ms.sha1 = atom_sha1_get(&sha1);
	} else {
		ms.path = atom_str_get(p);
	}

	if (host) {
		gchar *h = g_strndup(host, host_end - host);
		ms.hostname = atom_str_get(h);
		G_FREE_NULL(h);
	}

	return wcopy(&ms, sizeof ms);
}

static struct magnet_source *
magnet_parse_http_source(const gchar *uri, const gchar **error_str)
{
	const gchar *p;

	clear_error_str(&error_str);
	g_return_val_if_fail(uri, NULL);

	p = is_strcaseprefix(uri, "http://");
	g_return_val_if_fail(p, NULL);

	return magnet_parse_location(p, error_str);
}

static struct magnet_source *
magnet_parse_push_source(const gchar *uri, const gchar **error_str)
{
	struct magnet_source *ms;
	const gchar *p, *endptr;
	gchar guid[GUID_RAW_SIZE];

	clear_error_str(&error_str);
	g_return_val_if_fail(uri, NULL);

	p = is_strprefix(uri, "push://");
	g_return_val_if_fail(p, NULL);

	endptr = strchr(p, ':');
	if (
		NULL == endptr ||
		GUID_HEX_SIZE != (endptr - p) ||
		!hex_to_guid(p, guid)
	) {
		*error_str = "Bad GUID in push source";
		return NULL;
	}

	p = &endptr[1];
	ms = magnet_parse_location(p, error_str);
	if (ms) {
		ms->guid = atom_guid_get(guid);
	}
	return ms;
}

struct magnet_source *
magnet_parse_exact_source(const gchar *uri, const gchar **error_str)
{
	clear_error_str(&error_str);
	g_return_val_if_fail(uri, NULL);

	/* TODO: This should be handled elsewhere e.g., downloads.c in
	 *		a generic way. */

	if (is_strcaseprefix(uri, "http://")) {
		return magnet_parse_http_source(uri, error_str);
	} else if (is_strcaseprefix(uri, "push://")) {
		return magnet_parse_push_source(uri, error_str);
	} else {
		*error_str =
			_("MAGNET URI contained source URL for an unsupported protocol");
		/* Skip this parameter */
		return NULL;
	}
}

static void
magnet_handle_key(struct magnet_resource *res,
	const gchar *name, const gchar *value)
{
	g_return_if_fail(res);
	g_return_if_fail(name);
	g_return_if_fail(value);
	
	if (!utf8_is_valid_string(value)) {
		g_message("MAGNET URI key \"%s\" is not UTF-8 encoded", name);
		return;
	}

	switch (magnet_key_get(name)) {
	case MAGNET_KEY_DISPLAY_NAME:
		if (!res->display_name) {
			magnet_set_display_name(res, value);
		}
		break;

	case MAGNET_KEY_EXACT_SOURCE:
		{
			struct magnet_source *ms;

			ms = magnet_parse_exact_source(value, NULL);
			if (ms) {
				if (!res->sha1 && ms->sha1) {
					res->sha1 = atom_sha1_get(ms->sha1);
				}
				if (!ms->sha1 || sha1_eq(res->sha1, ms->sha1)) {
					res->sources = g_slist_prepend(res->sources, ms);
				} else {
					magnet_source_free(&ms);
				}
			}
		}
		break;

	case MAGNET_KEY_EXACT_TOPIC:
		if (!magnet_set_exact_topic(res, value)) {
			g_message("MAGNET URI contained unsupported exact topic.");
		}
		break;

	case MAGNET_KEY_KEYWORD_TOPIC:
		magnet_add_search(res, value);
		break;

	case MAGNET_KEY_EXACT_LENGTH:
		{
			gint error;
			guint64 u;

			u = parse_uint64(value, NULL, 10, &error);
			if (!error) {
				magnet_set_filesize(res, u);
			}
		}
		break;

	case MAGNET_KEY_PARQ_ID:
		magnet_set_parq_id(res, value);
		break;

	case MAGNET_KEY_NONE:
		g_message("Unhandled parameter in MAGNET URI \"%s\"", name);
		break;

	case NUM_MAGNET_KEYS:
		g_assert_not_reached();
	}
}

struct magnet_resource * 
magnet_parse(const gchar *url, const gchar **error_str)
{
	static const struct magnet_resource zero_resource;
	struct magnet_resource res;
	const gchar *p, *next;

	res = zero_resource;
	clear_error_str(&error_str);

	p = is_strcaseprefix(url, "magnet:");
	if (!p) {
		*error_str = "Not a MAGNET URI";
		return NULL;
	}

	if ('?' != p[0]) {
		*error_str = "Invalid MAGNET URI";
		return NULL;
	}
	p++;

	for (/* NOTHING */; p && '\0' != p[0]; p = next) {
		enum magnet_key key;
		const gchar *endptr;
		gchar name[16]; /* Large enough to hold longest key we know */

		name[0] = '\0';
		endptr = strchr(p, '=');
		if (endptr && p != endptr) {
			size_t name_len;

			name_len = endptr - p;
			g_assert((ssize_t) name_len > 0);

			if (name_len < sizeof name) {  /* Ignore overlong key */
				strncat(name, p, name_len);
			}
			p = &endptr[1]; /* Point behind the '=' */
		}

		endptr = strchr(p, '&');
		if (!endptr) {
			endptr = strchr(p, '\0');
		}

		key = magnet_key_get(name);
		if (MAGNET_KEY_NONE == key) {
			g_message("Skipping unknown key in MAGNET URI (%s)", name);
		} else {
			gchar *value;
			size_t value_len;

			value_len = endptr - p;
			value = g_strndup(p, value_len);

			plus_to_space(value);
			if (url_unescape(value, TRUE)) {
				magnet_handle_key(&res, name, value);
			} else {
				g_message("Invalidly encoded value in MAGNET URI");
			}
			G_FREE_NULL(value);
		}

		while ('&' == endptr[0]) {
			endptr++;
		}
		next = endptr;
	}

	res.sources = g_slist_reverse(res.sources);
	res.searches = g_slist_reverse(res.searches);

	return wcopy(&res, sizeof res);
}

void
magnet_source_free(struct magnet_source **ms_ptr)
{
	struct magnet_source *ms = *ms_ptr;
	
	if (ms) {
		atom_str_free_null(&ms->hostname);
		atom_str_free_null(&ms->path);
		atom_str_free_null(&ms->url);
		atom_sha1_free_null(&ms->sha1);
		atom_tth_free_null(&ms->tth);
		atom_guid_free_null(&ms->guid);
		wfree(ms, sizeof *ms);
		*ms_ptr = NULL;
	}
}

void
magnet_resource_free(struct magnet_resource **res_ptr)
{
	struct magnet_resource *res = *res_ptr;

	if (res) {
		GSList *sl;

		atom_str_free_null(&res->display_name);
		atom_sha1_free_null(&res->sha1);
		atom_tth_free_null(&res->tth);
		atom_str_free_null(&res->parq_id);

		for (sl = res->sources; sl != NULL; sl = g_slist_next(sl)) {
			struct magnet_source *ms = sl->data;
			magnet_source_free(&ms);
		}
		g_slist_free(res->sources);
		res->sources = NULL;

		for (sl = res->searches; sl != NULL; sl = g_slist_next(sl)) {
			const gchar *s = sl->data;
			atom_str_free_null(&s);
		}
		g_slist_free(res->searches);
		res->searches = NULL;
		
		wfree(res, sizeof *res);
		*res_ptr = NULL;
	}
}

struct magnet_resource *
magnet_resource_new(void)
{
	static const struct magnet_resource zero_resource;
	return wcopy(&zero_resource, sizeof zero_resource);
}

struct magnet_source *
magnet_source_new(void)
{
	static const struct magnet_source zero_source;
	return wcopy(&zero_source, sizeof zero_source);
}

void
magnet_add_source(struct magnet_resource *res, struct magnet_source *s)
{
	g_return_if_fail(res);
	g_return_if_fail(s);
	
	res->sources = g_slist_prepend(res->sources, s);
}

void
magnet_add_source_by_url(struct magnet_resource *res, const gchar *url)
{
	struct magnet_source *s;

	g_return_if_fail(res);
	g_return_if_fail(url);

	s = magnet_source_new();
	s->url = atom_str_get(url);
	magnet_add_source(res, s);
}

void
magnet_add_sha1_source(struct magnet_resource *res, const struct sha1 *sha1,
	const host_addr_t addr, const guint16 port, const gchar *guid)
{
	struct magnet_source *s;

	g_return_if_fail(res);
	g_return_if_fail(sha1);
	g_return_if_fail(!res->sha1 || sha1_eq(res->sha1, sha1));
	g_return_if_fail(port > 0);

	if (!res->sha1) {
		magnet_set_sha1(res, sha1);
	}

	s = magnet_source_new();
	s->addr = addr;
	s->port = port;
	s->sha1 = atom_sha1_get(sha1);
	s->guid = guid ? atom_guid_get(guid) : NULL;
	magnet_add_source(res, s);
}

void
magnet_add_search(struct magnet_resource *res, const gchar *search)
{
	g_return_if_fail(res);
	g_return_if_fail(search);

	res->searches = g_slist_prepend(res->searches,
						deconstify_gchar(atom_str_get(search)));
}


void
magnet_set_sha1(struct magnet_resource *res, const struct sha1 *sha1)
{
	const struct sha1 *atom;

	g_return_if_fail(res);
	g_return_if_fail(sha1);

	atom = atom_sha1_get(sha1);
	atom_sha1_free_null(&res->sha1);
	res->sha1 = atom;
}

void
magnet_set_tth(struct magnet_resource *res, const struct tth *tth)
{
	const struct tth *atom;

	g_return_if_fail(res);
	g_return_if_fail(tth);

	atom = atom_tth_get(tth);
	atom_tth_free_null(&res->tth);
	res->tth = atom;
}


gboolean
magnet_set_exact_topic(struct magnet_resource *res, const gchar *topic)
{
	struct sha1 sha1;
	struct tth tth;

	if (urn_get_bitprint(topic, strlen(topic), &sha1, &tth)) {
		if (!res->sha1) {
			magnet_set_sha1(res, &sha1);
		}
		if (!res->tth) {
			magnet_set_tth(res, &tth);
		}
		return TRUE;
	} else if (urn_get_sha1(topic, &sha1)) {
		if (!res->sha1) {
			magnet_set_sha1(res, &sha1);
		}
		return TRUE;
	} else if (urn_get_tth(topic, strlen(topic), &tth)) {
		if (!res->tth) {
			magnet_set_tth(res, &tth);
		}
		return TRUE;
	} else {
		return FALSE;
	}
}

void
magnet_set_display_name(struct magnet_resource *res, const gchar *name)
{
	const gchar *atom;

	g_return_if_fail(res);
	g_return_if_fail(name);

	atom = atom_str_get(name);
	atom_str_free_null(&res->display_name);
	res->display_name = atom;
}

void
magnet_set_filesize(struct magnet_resource *res, filesize_t size)
{
	res->size = size;
}

static inline void
magnet_append_item(GString **gs_ptr, gboolean escape_value,
	const gchar *key, const gchar *value)
{
	GString *gs;

	g_return_if_fail(gs_ptr);
	g_return_if_fail(*gs_ptr);
	g_return_if_fail(key);
	g_return_if_fail(value);

	gs = *gs_ptr;

	if (0 == gs->len) {
		gs = g_string_append(gs, "magnet:?");
	} else {
		gs = g_string_append_c(gs, '&');
	}
	gs = g_string_append(gs, key);
	gs = g_string_append_c(gs, '=');

	if (escape_value) {
		gchar *escaped;

		escaped = url_escape_query(value);
		gs = g_string_append(gs, escaped);
		if (escaped != value) {
			G_FREE_NULL(escaped);
		}
	} else {
		gs = g_string_append(gs, value);
	}

	*gs_ptr = gs;
}

gchar *
magnet_source_to_string(struct magnet_source *s)
{
	gchar *url;

	g_return_val_if_fail(s, NULL);

	if (s->url) {
		url = g_strdup(s->url);
	} else {
		const gchar *host, *prefix;
		gchar prefix_buf[256];
		gchar port_buf[16];

		g_return_val_if_fail(0 != s->port, NULL);
		g_return_val_if_fail(s->hostname || is_host_addr(s->addr), NULL);
		g_return_val_if_fail(s->path || s->sha1, NULL);

		if (s->guid) {
			gchar guid_buf[GUID_HEX_SIZE + 1];
			
			guid_to_string_buf(s->guid, guid_buf, sizeof guid_buf);
			concat_strings(prefix_buf, sizeof prefix_buf,
				"push://", guid_buf, ":", (void *) 0);
			prefix = prefix_buf;
		} else {
			prefix = "http://";
		}
		
		port_buf[0] = '\0';
		if (s->hostname) {
			host = s->hostname;
			if (80 != s->port) {
				gm_snprintf(port_buf, sizeof port_buf, ":%u",
					(unsigned) s->port);
			}
		} else {
			host = host_addr_port_to_string(s->addr, s->port);
		}
		if (s->path) {
			url = g_strconcat(prefix, host, port_buf, s->path, (void *) 0);
		} else {
			g_assert(s->sha1);
			url = g_strconcat(prefix, host, port_buf,
					"/uri-res/N2R?", bitprint_to_urn_string(s->sha1, s->tth),
					(void *) 0);
		}
	}

	return url;
}

gchar *
magnet_to_string(struct magnet_resource *res)
{
	GString *gs;
	GSList *sl;

	g_return_val_if_fail(res, NULL);
	
	gs = g_string_new(NULL);
	if (res->display_name) {
		magnet_append_item(&gs, TRUE, "dn", res->display_name);
	}
	if (0 != res->size) {
		gchar buf[UINT64_DEC_BUFLEN];

		uint64_to_string_buf(res->size, buf, sizeof buf);
		magnet_append_item(&gs, FALSE, "xl", buf);
	}
	if (res->sha1) {
		magnet_append_item(&gs, FALSE, "xt",
			bitprint_to_urn_string(res->sha1, res->tth));
	}
	if (res->parq_id) {
		magnet_append_item(&gs, TRUE, "x.parq-id", res->parq_id);
	}

	for (sl = res->sources; NULL != sl; sl = g_slist_next(sl)) {
		gchar *url;

		url = magnet_source_to_string(sl->data);
		magnet_append_item(&gs, TRUE, "xs", url);
		G_FREE_NULL(url);
	}

	for (sl = res->searches; NULL != sl; sl = g_slist_next(sl)) {
		magnet_append_item(&gs, TRUE, "kt", sl->data);
	}

	return gm_string_finalize(gs);
}

/**
 * This is a bit of a hack (an extension anyway) and should only be used
 * for magnets with a single logical source because the PARQ ID is only
 * valid for a certain source.
 */
void
magnet_set_parq_id(struct magnet_resource *res, const gchar *parq_id)
{
	const gchar *atom;

	g_return_if_fail(res);
	g_return_if_fail(parq_id);

	atom = atom_str_get(parq_id);
	atom_str_free_null(&res->parq_id);
	res->parq_id = atom;
}

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