/*
 * edns.c
 *
 * edns implementation
 *
 * a Net::DNS like library for C
 *
 * (c) NLnet Labs, 2004-2022
 *
 * See the file LICENSE for the license
 */

#include <ldns/ldns.h>

#define LDNS_OPTIONLIST_INIT 8

/*
 * Access functions
 * functions to get and set type checking
 */

/* read */
size_t
ldns_edns_get_size(const ldns_edns_option *edns)
{
	assert(edns != NULL);
	return edns->_size;
}

ldns_edns_option_code
ldns_edns_get_code(const ldns_edns_option *edns)
{
	assert(edns != NULL);
	return edns->_code;
}

uint8_t *
ldns_edns_get_data(const ldns_edns_option *edns)
{
	assert(edns != NULL);
	return edns->_data;
}

ldns_buffer *
ldns_edns_get_wireformat_buffer(const ldns_edns_option *edns)
{
	uint16_t option;
	size_t size;
	uint8_t* data;
	ldns_buffer* buffer;

	if (edns == NULL) {
		return NULL;
	}

	option = ldns_edns_get_code(edns);
	size = ldns_edns_get_size(edns);
	data = ldns_edns_get_data(edns);

	buffer = ldns_buffer_new(size + 4);

	if (buffer == NULL) {
		return NULL;
	}

	ldns_buffer_write_u16(buffer, option);
	ldns_buffer_write_u16(buffer, size);
	ldns_buffer_write(buffer, data, size);

	ldns_buffer_flip(buffer);

	return buffer;
}

/* write */
static void
ldns_edns_set_size(ldns_edns_option *edns, size_t size)
{
	assert(edns != NULL);
	edns->_size = size;
}

static void
ldns_edns_set_code(ldns_edns_option *edns, ldns_edns_option_code code)
{
	assert(edns != NULL);
	edns->_code = code;
}

static void
ldns_edns_set_data(ldns_edns_option *edns, void *data)
{
	/* only copy the pointer */
	assert(edns != NULL);
	edns->_data = data;
}

/* note: data must be allocated memory */
ldns_edns_option *
ldns_edns_new(ldns_edns_option_code code, size_t size, void *data)
{
	ldns_edns_option *edns;
	edns = LDNS_MALLOC(ldns_edns_option);
	if (!edns) {
		return NULL;
	}
	ldns_edns_set_code(edns, code);
	ldns_edns_set_size(edns, size);
	ldns_edns_set_data(edns, data);

	return edns;
}

ldns_edns_option *
ldns_edns_new_from_data(ldns_edns_option_code code, size_t size, const void *data)
{
	ldns_edns_option *edns;
	edns = LDNS_MALLOC(ldns_edns_option);
	if (!edns) {
		return NULL;
	}
	edns->_data = LDNS_XMALLOC(uint8_t, size);
	if (!edns->_data) {
		LDNS_FREE(edns);
		return NULL;
	}

	/* set the values */
	ldns_edns_set_code(edns, code);
	ldns_edns_set_size(edns, size);
	memcpy(edns->_data, data, size);

	return edns;
}

ldns_edns_option *
ldns_edns_clone(ldns_edns_option *edns)
{
	ldns_edns_option *new_option;

	assert(edns != NULL);

	new_option = ldns_edns_new_from_data(ldns_edns_get_code(edns),
		ldns_edns_get_size(edns),
		ldns_edns_get_data(edns));

	return new_option;
}

void
ldns_edns_deep_free(ldns_edns_option *edns)
{
	if (edns) {
		if (edns->_data) {
			LDNS_FREE(edns->_data);
		}
		LDNS_FREE(edns);
	}
}

void 
ldns_edns_free(ldns_edns_option *edns)
{
	if (edns) {
		LDNS_FREE(edns);
	}
}

ldns_edns_option_list*
ldns_edns_option_list_new()
{
	ldns_edns_option_list *option_list = LDNS_MALLOC(ldns_edns_option_list);
	if(!option_list) {
		return NULL;
	}

	option_list->_option_count = 0;
	option_list->_option_capacity = 0;
	option_list->_options_size = 0;
	option_list->_options = NULL;
	return option_list;
}

ldns_edns_option_list *
ldns_edns_option_list_clone(ldns_edns_option_list *old_list)
{
	size_t i;
	ldns_edns_option_list *new_list;

	if (!old_list) {
		return NULL;
	}

	new_list = ldns_edns_option_list_new();
	if (!new_list) {
		return NULL;
	}

	if (old_list->_option_count == 0) {
		return new_list;
	}

	/* adding options also updates the total options size */
	for (i = 0; i < old_list->_option_count; i++) {
		ldns_edns_option *option = ldns_edns_clone(ldns_edns_option_list_get_option(old_list, i));
		if (!ldns_edns_option_list_push(new_list, option)) {
			ldns_edns_deep_free(option);
			ldns_edns_option_list_deep_free(new_list);
			return NULL;
		}
	}
	return new_list;
}

void
ldns_edns_option_list_free(ldns_edns_option_list *option_list)
{
	if (option_list) {
		LDNS_FREE(option_list->_options);
		LDNS_FREE(option_list);
	}
}

void
ldns_edns_option_list_deep_free(ldns_edns_option_list *option_list)
{
	size_t i;

	if (option_list) {
		for (i=0; i < ldns_edns_option_list_get_count(option_list); i++) {
			ldns_edns_deep_free(ldns_edns_option_list_get_option(option_list, i));
		}
		ldns_edns_option_list_free(option_list);
	}
}

size_t
ldns_edns_option_list_get_count(const ldns_edns_option_list *option_list)
{
	if (option_list) {
		return option_list->_option_count;
	} else {
		return 0;
	}
}

ldns_edns_option *
ldns_edns_option_list_get_option(const ldns_edns_option_list *option_list, size_t index)
{
	if (option_list && index < ldns_edns_option_list_get_count(option_list)) {
		assert(option_list->_options[index]);
		return option_list->_options[index];
	} else {
		return NULL;
	}
}

size_t
ldns_edns_option_list_get_options_size(const ldns_edns_option_list *option_list)
{
	if (option_list) {
		return option_list->_options_size;
	} else {
		return 0;
	}
}


ldns_edns_option *
ldns_edns_option_list_set_option(ldns_edns_option_list *option_list,
	ldns_edns_option *option, size_t index)
{
	ldns_edns_option* old;

	assert(option_list != NULL);

	if (index > ldns_edns_option_list_get_count(option_list)) {
		return NULL;
	}

	if (option == NULL) {
		return NULL;
	}

	old = ldns_edns_option_list_get_option(option_list, index);

	/* shrink the total EDNS size if the old EDNS option exists */
	if (old != NULL) {
		option_list->_options_size -= (ldns_edns_get_size(old) + 4);
	}

	option_list->_options_size += (ldns_edns_get_size(option) + 4);

	option_list->_options[index] = option;
	return old;
}

bool
ldns_edns_option_list_push(ldns_edns_option_list *option_list,
	ldns_edns_option *option)
{
	size_t cap;
	size_t option_count;

	assert(option_list != NULL);

	if (option == NULL) {
		return false;
	}

	cap = option_list->_option_capacity;
	option_count = ldns_edns_option_list_get_count(option_list);

	/* verify we need to grow the array to fit the new option */
	if (option_count+1 > cap) {
		ldns_edns_option **new_list;

		/* initialize the capacity if needed, otherwise grow by doubling */
		if (cap == 0) {
			cap = LDNS_OPTIONLIST_INIT; /* initial list size */
		} else {
			cap *= 2;
		}

		new_list = LDNS_XREALLOC(option_list->_options,
			ldns_edns_option *, cap);

		if (!new_list) {
			return false;
		}

		option_list->_options = new_list;
		option_list->_option_capacity = cap;
	}

	/* add the new option */
	ldns_edns_option_list_set_option(option_list, option,
		option_list->_option_count);
	option_list->_option_count += 1;

	return true;
}

ldns_edns_option *
ldns_edns_option_list_pop(ldns_edns_option_list *option_list)
{
	ldns_edns_option* pop;
	size_t count;
	size_t cap;

	assert(option_list != NULL);

	cap = option_list->_option_capacity;
	count = ldns_edns_option_list_get_count(option_list);

	if (count == 0) {
		return NULL;
	}
	/* get the last option from the list */
	pop = ldns_edns_option_list_get_option(option_list, count-1);

	/* shrink the array */
	if (cap > LDNS_OPTIONLIST_INIT && count-1 <= cap/2) {
		ldns_edns_option **new_list;

		cap /= 2;

		new_list = LDNS_XREALLOC(option_list->_options,
			ldns_edns_option *, cap);
		if (new_list) {
			option_list->_options = new_list;
		}
		/* if the realloc fails, the capacity for the list remains unchanged */
	}

	/* shrink the total EDNS size of the options if the popped EDNS option exists */
	if (pop != NULL) {
		option_list->_options_size -= (ldns_edns_get_size(pop) + 4);
	}

	option_list->_option_count = count - 1;

	return pop;
}

ldns_buffer *
ldns_edns_option_list2wireformat_buffer(const ldns_edns_option_list *option_list)
{
	size_t i, list_size, options_size, option, size;
	ldns_buffer* buffer;
	ldns_edns_option *edns;
	uint8_t* data = NULL;

	if (!option_list) {
		return NULL;
	}

	/* get the number of EDNS options in the list*/
	list_size = ldns_edns_option_list_get_count(option_list);

	/* create buffer the size of the total EDNS wireformat options */
	options_size = ldns_edns_option_list_get_options_size(option_list);
	buffer = ldns_buffer_new(options_size);
	
	if (!buffer) {
		return NULL;
	}

	/* write individual serialized EDNS options to final buffer*/
	for (i = 0; i < list_size; i++) {
		edns = ldns_edns_option_list_get_option(option_list, i);

		if (edns == NULL) {
			/* this shouldn't be possible */
			return NULL;
		}

		option = ldns_edns_get_code(edns);
		size = ldns_edns_get_size(edns);
		data = ldns_edns_get_data(edns);

		/* make sure the option fits */
		if (!(ldns_buffer_available(buffer, size + 4))) {
			ldns_buffer_free(buffer);
			return NULL;
		}

		ldns_buffer_write_u16(buffer, option);
		ldns_buffer_write_u16(buffer, size);
		ldns_buffer_write(buffer, data, size);
	}

	ldns_buffer_flip(buffer);

	return buffer;
}
