/* High level mDNS encoding and decoding functions for whole messages:
 * Request message (header, question)
 * Answer message (header, resource record 1, ... resource record N)*/

/* Copyright 2019 by sysmocom s.f.m.c. GmbH <info@sysmocom.de>
 *
 * All Rights Reserved
 *
 * This program 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.
 *
 * 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, see <http://www.gnu.org/licenses/>.
 */

#include <errno.h>
#include <string.h>
#include <osmocom/hlr/logging.h>
#include <osmocom/mslookup/mdns_msg.h>

/*! Encode request message into one mDNS packet, consisting of the header section and one question section.
 * \returns 0 on success, -EINVAL on error.
 */
int osmo_mdns_msg_request_encode(void *ctx, struct msgb *msg, const struct osmo_mdns_msg_request *req)
{
	struct osmo_mdns_rfc_header hdr = {0};
	struct osmo_mdns_rfc_question qst = {0};

	hdr.id = req->id;
	hdr.qdcount = 1;
	osmo_mdns_rfc_header_encode(msg, &hdr);

	qst.domain = req->domain;
	qst.qtype = req->type;
	qst.qclass = OSMO_MDNS_RFC_CLASS_IN;
	if (osmo_mdns_rfc_question_encode(msg, &qst) != 0)
		return -EINVAL;

	return 0;
}

/*! Decode request message from a mDNS packet, consisting of the header section and one question section.
 * \returns allocated request message on success, NULL on error.
 */
struct osmo_mdns_msg_request *osmo_mdns_msg_request_decode(void *ctx, const uint8_t *data, size_t data_len)
{
	struct osmo_mdns_rfc_header hdr = {0};
	size_t hdr_len = sizeof(struct osmo_mdns_rfc_header);
	struct osmo_mdns_rfc_question* qst = NULL;
	struct osmo_mdns_msg_request *ret = NULL;

	if (data_len < hdr_len || osmo_mdns_rfc_header_decode(data, hdr_len, &hdr) != 0 || hdr.qr != 0)
		return NULL;

	qst = osmo_mdns_rfc_question_decode(ctx, data + hdr_len, data_len - hdr_len);
	if (!qst)
		return NULL;

	ret = talloc_zero(ctx, struct osmo_mdns_msg_request);
	ret->id = hdr.id;
	ret->domain = talloc_strdup(ret, qst->domain);
	ret->type = qst->qtype;

	talloc_free(qst);
	return ret;
}

/*! Initialize the linked list for resource records in a answer message. */
void osmo_mdns_msg_answer_init(struct osmo_mdns_msg_answer *ans)
{
	*ans = (struct osmo_mdns_msg_answer){};
	INIT_LLIST_HEAD(&ans->records);
}

/*! Encode answer message into one mDNS packet, consisting of the header section and N resource records.
 *
 * To keep things simple, this sends the domain with each resource record. Other DNS implementations make use of
 * "message compression", which would send a question section with the domain before the resource records, and then
 * point inside each resource record with an offset back to the domain in the question section (RFC 1035 4.1.4).
 * \returns 0 on success, -EINVAL on error.
 */
int osmo_mdns_msg_answer_encode(void *ctx, struct msgb *msg, const struct osmo_mdns_msg_answer *ans)
{
	struct osmo_mdns_rfc_header hdr = {0};
	struct osmo_mdns_record *ans_record;

	hdr.id = ans->id;
	hdr.qr = 1;
	hdr.ancount = llist_count(&ans->records);
	osmo_mdns_rfc_header_encode(msg, &hdr);

	llist_for_each_entry(ans_record, &ans->records, list) {
		struct osmo_mdns_rfc_record rec = {0};

		rec.domain = ans->domain;
		rec.type = ans_record->type;
		rec.class = OSMO_MDNS_RFC_CLASS_IN;
		rec.ttl = 0;
		rec.rdlength = ans_record->length;
		rec.rdata = ans_record->data;

		if (osmo_mdns_rfc_record_encode(msg, &rec) != 0)
			return -EINVAL;
	}

	return 0;
}

/*! Decode answer message from a mDNS packet.
 *
 * Answer messages must consist of one header and one or more resource records. An additional question section or
 * message compression (RFC 1035 4.1.4) are not supported.
* \returns allocated answer message on success, NULL on error.
 */
struct osmo_mdns_msg_answer *osmo_mdns_msg_answer_decode(void *ctx, const uint8_t *data, size_t data_len)
{
	struct osmo_mdns_rfc_header hdr = {};
	size_t hdr_len = sizeof(struct osmo_mdns_rfc_header);
	struct osmo_mdns_msg_answer *ret = talloc_zero(ctx, struct osmo_mdns_msg_answer);

	/* Parse header section */
	if (data_len < hdr_len || osmo_mdns_rfc_header_decode(data, hdr_len, &hdr) != 0 || hdr.qr != 1)
		goto error;
	ret->id = hdr.id;
	data_len -= hdr_len;
	data += hdr_len;

	/* Parse resource records */
	INIT_LLIST_HEAD(&ret->records);
	while (data_len) {
		size_t record_len;
		struct osmo_mdns_rfc_record *rec;
		struct osmo_mdns_record* ret_record;

		rec = osmo_mdns_rfc_record_decode(ret, data, data_len, &record_len);
		if (!rec)
			goto error;

		/* Copy domain to ret */
		if (ret->domain) {
			if (strcmp(ret->domain, rec->domain) != 0) {
				LOGP(DMSLOOKUP, LOGL_ERROR, "domain mismatch in resource records ('%s' vs '%s')\n",
				     ret->domain, rec->domain);
				goto error;
			}
		}
		else
			ret->domain = talloc_strdup(ret, rec->domain);

		/* Add simplified record to ret */
		ret_record = talloc_zero(ret, struct osmo_mdns_record);
		ret_record->type = rec->type;
		ret_record->length = rec->rdlength;
		ret_record->data = talloc_memdup(ret_record, rec->rdata, rec->rdlength);
		llist_add_tail(&ret_record->list, &ret->records);

		data += record_len;
		data_len -= record_len;
		talloc_free(rec);
	}

	/* Verify record count */
	if (llist_count(&ret->records) != hdr.ancount) {
		LOGP(DMSLOOKUP, LOGL_ERROR, "amount of parsed records (%i) doesn't match count in header (%i)\n",
		     llist_count(&ret->records), hdr.ancount);
		goto error;
	}

	return ret;
error:
	talloc_free(ret);
	return NULL;
}

/*! Get a TXT resource record, which stores a key=value string.
 * \returns allocated resource record on success, NULL on error.
 */
static struct osmo_mdns_record *_osmo_mdns_record_txt_encode(void *ctx, const char *key, const char *value)
{
	struct osmo_mdns_record *ret = talloc_zero(ctx, struct osmo_mdns_record);
	size_t len = strlen(key) + 1 + strlen(value);

	if (len > OSMO_MDNS_RFC_MAX_CHARACTER_STRING_LEN - 1)
		return NULL;

	/* redundant len is required, see RFC 1035 3.3.14 and 3.3. */
	ret->data = (uint8_t *)talloc_asprintf(ctx, "%c%s=%s", (char)len, key, value);
	if (!ret->data)
		return NULL;
	ret->type = OSMO_MDNS_RFC_RECORD_TYPE_TXT;
	ret->length = len + 1;
	return ret;
}

/*! Get a TXT resource record, which stores a key=value string, but build value from a format string.
 * \returns allocated resource record on success, NULL on error.
 */
struct osmo_mdns_record *osmo_mdns_record_txt_keyval_encode(void *ctx, const char *key, const char *value_fmt, ...)
{
	va_list ap;
	char *value = NULL;
	struct osmo_mdns_record *r;

	if (!value_fmt)
		return _osmo_mdns_record_txt_encode(ctx, key, "");

	va_start(ap, value_fmt);
	value = talloc_vasprintf(ctx, value_fmt, ap);
	va_end(ap);
	if (!value)
		return NULL;
	r = _osmo_mdns_record_txt_encode(ctx, key, value);
	talloc_free(value);
	return r;
}

/*! Decode a TXT resource record, which stores a key=value string.
 * \returns 0 on success, -EINVAL on error.
 */
int osmo_mdns_record_txt_keyval_decode(const struct osmo_mdns_record *rec,
				       char *key_buf, size_t key_size, char *value_buf, size_t value_size)
{
	const char *key_value;
	const char *key_value_end;
	const char *sep;
	const char *value;

	if (rec->type != OSMO_MDNS_RFC_RECORD_TYPE_TXT)
		return -EINVAL;

	key_value = (const char *)rec->data;
	key_value_end = key_value + rec->length;

	/* Verify and then skip the redundant string length byte */
	if (*key_value != rec->length - 1)
		return -EINVAL;
	key_value++;

	if (key_value >= key_value_end)
		return -EINVAL;

	/* Find equals sign */
	sep = osmo_strnchr(key_value, key_value_end - key_value, '=');
	if (!sep)
		return -EINVAL;

	/* Parse key */
	osmo_print_n(key_buf, key_size, key_value, sep - key_value);

	/* Parse value */
	value = sep + 1;
	osmo_print_n(value_buf, value_size, value, key_value_end - value);
	return 0;
}
