/*  Copyright (C) CZ.NIC, z.s.p.o. and contributors
 *  SPDX-License-Identifier: GPL-2.0-or-later
 *  For more information, see <https://www.knot-dns.cz/>
 */

#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>

#include "libknot/attribute.h"
#include "libknot/packet/pkt.h"
#include "libknot/codes.h"
#include "libknot/descriptor.h"
#include "libknot/errcode.h"
#include "libknot/rrtype/tsig.h"
#include "libknot/tsig-op.h"
#include "libknot/packet/wire.h"
#include "libknot/packet/rrset-wire.h"
#include "libknot/wire.h"
#include "contrib/mempattern.h"
#include "contrib/wire_ctx.h"

/*! \brief Packet RR array growth step. */
#define NEXT_RR_ALIGN 16
#define NEXT_RR_COUNT(count) (((count) / NEXT_RR_ALIGN + 1) * NEXT_RR_ALIGN)

/*! \brief Scan packet for RRSet existence. */
static bool pkt_contains(const knot_pkt_t *packet, const knot_rrset_t *rrset)
{
	assert(packet);
	assert(rrset);

	for (int i = 0; i < packet->rrset_count; ++i) {
		const uint16_t type = packet->rr[i].type;
		const knot_rdata_t *data = packet->rr[i].rrs.rdata;
		if (type == rrset->type && data == rrset->rrs.rdata) {
			return true;
		}
	}

	return false;
}

/*! \brief Free all RRSets and reset RRSet count. */
static void pkt_free_data(knot_pkt_t *pkt)
{
	assert(pkt);

	/* Free RRSets if applicable. */
	for (uint16_t i = 0; i < pkt->rrset_count; ++i) {
		if (pkt->rr_info[i].flags & KNOT_PF_FREE) {
			knot_rrset_clear(&pkt->rr[i], &pkt->mm);
		}
	}
	pkt->rrset_count = 0;

	/* Free EDNS option positions. */
	mm_free(&pkt->mm, pkt->edns_opts);
	pkt->edns_opts = 0;
}

/*! \brief Allocate new wireformat of given length, assuming *pkt is zeroed. */
static int pkt_wire_alloc(knot_pkt_t *pkt, uint16_t len)
{
	assert(pkt);

	if (len < KNOT_WIRE_HEADER_SIZE) {
		return KNOT_ERANGE;
	}

	pkt->wire = mm_alloc(&pkt->mm, len);
	if (pkt->wire == NULL) {
		return KNOT_ENOMEM;
	}

	pkt->flags |= KNOT_PF_FREE;
	pkt->max_size = len;

	/* Reset to header size. */
	pkt->size = KNOT_WIRE_HEADER_SIZE;
	memset(pkt->wire, 0, pkt->size);

	return KNOT_EOK;
}

/*! \brief Set packet wireformat to an existing memory. */
static void pkt_wire_set(knot_pkt_t *pkt, void *wire, uint16_t len)
{
	assert(pkt);

	pkt->wire = wire;
	pkt->size = pkt->max_size = len;
	pkt->parsed = 0;
}

/*! \brief Calculate remaining size in the packet. */
static uint16_t pkt_remaining(knot_pkt_t *pkt)
{
	assert(pkt);

	return pkt->max_size - pkt->size - pkt->reserved;
}

/*! \brief Return RR count for given section (from wire xxCOUNT in header). */
static uint16_t pkt_rr_wirecount(knot_pkt_t *pkt, knot_section_t section_id)
{
	assert(pkt);
	switch (section_id) {
	case KNOT_ANSWER:     return knot_wire_get_ancount(pkt->wire);
	case KNOT_AUTHORITY:  return knot_wire_get_nscount(pkt->wire);
	case KNOT_ADDITIONAL: return knot_wire_get_arcount(pkt->wire);
	default: assert(0);   return 0;
	}
}

/*! \brief Update RR count for given section (wire xxCOUNT in header). */
static void pkt_rr_wirecount_add(knot_pkt_t *pkt, knot_section_t section_id,
                                 int16_t val)
{
	assert(pkt);
	switch (section_id) {
	case KNOT_ANSWER:     knot_wire_add_ancount(pkt->wire, val); break;
	case KNOT_AUTHORITY:  knot_wire_add_nscount(pkt->wire, val); break;
	case KNOT_ADDITIONAL: knot_wire_add_arcount(pkt->wire, val); break;
	}
}

/*! \brief Reserve enough space in the RR arrays. */
static int pkt_rr_array_alloc(knot_pkt_t *pkt, uint16_t count)
{
	/* Enough space. */
	if (pkt->rrset_allocd >= count) {
		return KNOT_EOK;
	}

	/* Allocate rr_info and rr fields to next size. */
	size_t next_size = NEXT_RR_COUNT(count);
	knot_rrinfo_t *rr_info = mm_alloc(&pkt->mm, sizeof(knot_rrinfo_t) * next_size);
	if (rr_info == NULL) {
		return KNOT_ENOMEM;
	}

	knot_rrset_t *rr = mm_alloc(&pkt->mm, sizeof(knot_rrset_t) * next_size);
	if (rr == NULL) {
		mm_free(&pkt->mm, rr_info);
		return KNOT_ENOMEM;
	}

	/* Copy and free the old data, if any. */
	if (pkt->rrset_allocd > 0) {
		memcpy(rr_info, pkt->rr_info, pkt->rrset_allocd * sizeof(knot_rrinfo_t));
		memcpy(rr, pkt->rr, pkt->rrset_allocd * sizeof(knot_rrset_t));
		mm_free(&pkt->mm, pkt->rr);
		mm_free(&pkt->mm, pkt->rr_info);
	}
	pkt->rr = rr;
	pkt->rr_info = rr_info;
	pkt->rrset_allocd = next_size;

	return KNOT_EOK;
}

static void compr_clear(knot_compr_t *compr)
{
	compr->rrinfo = NULL;
	compr->suffix.pos = 0;
	compr->suffix.labels = 0;
}

/*! \brief Clear the packet and switch wireformat pointers (possibly allocate new). */
static int pkt_init(knot_pkt_t *pkt, void *wire, uint16_t len, knot_mm_t *mm)
{
	assert(pkt);

	memset(pkt, 0, offsetof(knot_pkt_t, lower_qname));
	pkt->lower_qname[0] = '\0';

	/* No data to free, set memory context. */
	memcpy(&pkt->mm, mm, sizeof(knot_mm_t));

	/* Initialize wire. */
	int ret = KNOT_EOK;
	if (wire == NULL) {
		ret = pkt_wire_alloc(pkt, len);
	} else {
		pkt_wire_set(pkt, wire, len);
	}

	/* Initialize compression context (zeroed above). */
	pkt->compr.wire = pkt->wire;

	return ret;
}

/*! \brief Reset packet parse state. */
static void sections_reset(knot_pkt_t *pkt)
{
	pkt->current = KNOT_ANSWER;
	memset(pkt->sections, 0, sizeof(pkt->sections));
	(void)knot_pkt_begin(pkt, KNOT_ANSWER);
}

/*! \brief Allocate new packet using memory context. */
static knot_pkt_t *pkt_new_mm(void *wire, uint16_t len, knot_mm_t *mm)
{
	assert(mm);

	knot_pkt_t *pkt = mm_alloc(mm, sizeof(knot_pkt_t));
	if (pkt == NULL) {
		return NULL;
	}

	if (pkt_init(pkt, wire, len, mm) != KNOT_EOK) {
		mm_free(mm, pkt);
		return NULL;
	}

	return pkt;
}

_public_
knot_pkt_t *knot_pkt_new(void *wire, uint16_t len, knot_mm_t *mm)
{
	/* Default memory allocator if NULL. */
	knot_mm_t _mm;
	if (mm == NULL) {
		mm_ctx_init(&_mm);
		mm = &_mm;
	}

	return pkt_new_mm(wire, len, mm);
}

static int append_tsig(knot_pkt_t *dst, const knot_pkt_t *src)
{
	/* Check if a wire TSIG is available. */
	if (src->tsig_wire.pos != NULL) {
		if (dst->max_size < src->size + src->tsig_wire.len) {
			return KNOT_ESPACE;
		}
		memcpy(dst->wire + dst->size, src->tsig_wire.pos,
		       src->tsig_wire.len);
		dst->size += src->tsig_wire.len;

		/* Increment arcount. */
		knot_wire_set_arcount(dst->wire,
		                      knot_wire_get_arcount(dst->wire) + 1);
	} else {
		return knot_tsig_append(dst->wire, &dst->size, dst->max_size,
		                        src->tsig_rr);
	}

	return KNOT_EOK;
}

_public_
int knot_pkt_copy(knot_pkt_t *dst, const knot_pkt_t *src)
{
	if (dst == NULL || src == NULL) {
		return KNOT_EINVAL;
	}

	if (dst->max_size < src->size) {
		return KNOT_ESPACE;
	}
	memcpy(dst->wire, src->wire, src->size);
	dst->size = src->size;

	/* Append TSIG record. */
	if (src->tsig_rr) {
		int ret = append_tsig(dst, src);
		if (ret != KNOT_EOK) {
			return ret;
		}
	}

	/* Invalidate arrays. */
	dst->rr = NULL;
	dst->rr_info = NULL;
	dst->rrset_count = 0;
	dst->rrset_allocd = 0;

	/* @note This could be done more effectively if needed. */
	return knot_pkt_parse(dst, 0);
}

static void payload_clear(knot_pkt_t *pkt)
{
	assert(pkt);

	/* Keep question. */
	pkt->parsed = 0;
	pkt->reserved = 0;

	/* Free RRSets if applicable. */
	pkt_free_data(pkt);

	/* Reset sections. */
	sections_reset(pkt);

	/* Reset special types. */
	pkt->opt_rr = NULL;
	pkt->tsig_rr = NULL;

	/* Reset TSIG wire reference. */
	pkt->tsig_wire.pos = NULL;
	pkt->tsig_wire.len = 0;
}

_public_
int knot_pkt_init_response(knot_pkt_t *pkt, const knot_pkt_t *query)
{
	if (pkt == NULL || query == NULL) {
		return KNOT_EINVAL;
	}

	/* Header + question size. */
	size_t base_size = KNOT_WIRE_HEADER_SIZE + knot_pkt_question_size(query);
	if (base_size > pkt->max_size) {
		return KNOT_ESPACE;
	}

	pkt->size = base_size;
	memcpy(pkt->wire, query->wire, base_size);

	/* Copy lowercased QNAME. */
	pkt->qname_size = query->qname_size;
	if (query->qname_size == 0) {
		/* Reset question count if malformed. */
		knot_wire_set_qdcount(pkt->wire, 0);
	}
	memcpy(pkt->lower_qname, query->lower_qname, pkt->qname_size);

	/* Update flags and section counters. */
	knot_wire_set_ancount(pkt->wire, 0);
	knot_wire_set_nscount(pkt->wire, 0);
	knot_wire_set_arcount(pkt->wire, 0);

	knot_wire_set_qr(pkt->wire);
	knot_wire_clear_tc(pkt->wire);
	knot_wire_clear_ad(pkt->wire);
	knot_wire_clear_ra(pkt->wire);
	knot_wire_clear_aa(pkt->wire);
	knot_wire_clear_z(pkt->wire);

	/* Clear payload. */
	payload_clear(pkt);

	/* Clear compression context. */
	compr_clear(&pkt->compr);

	return KNOT_EOK;
}

_public_
void knot_pkt_clear(knot_pkt_t *pkt)
{
	if (pkt == NULL) {
		return;
	}

	/* Reset to header size. */
	pkt->size = KNOT_WIRE_HEADER_SIZE;
	memset(pkt->wire, 0, pkt->size);

	/* Clear payload. */
	payload_clear(pkt);

	/* Clear compression context. */
	compr_clear(&pkt->compr);

	/* Initialize lowercased QNAME. */
	pkt->lower_qname[0] = '\0';
}

_public_
void knot_pkt_free(knot_pkt_t *pkt)
{
	if (pkt == NULL) {
		return;
	}

	/* Free temporary RRSets. */
	pkt_free_data(pkt);

	/* Free RR/RR info arrays. */
	mm_free(&pkt->mm, pkt->rr);
	mm_free(&pkt->mm, pkt->rr_info);

	/* Free the space for wireformat. */
	if (pkt->flags & KNOT_PF_FREE) {
		mm_free(&pkt->mm, pkt->wire);
	}

	mm_free(&pkt->mm, pkt);
}

_public_
int knot_pkt_reserve(knot_pkt_t *pkt, uint16_t size)
{
	if (pkt == NULL) {
		return KNOT_EINVAL;
	}

	/* Reserve extra space (if possible). */
	if (pkt_remaining(pkt) >= size) {
		pkt->reserved += size;
		return KNOT_EOK;
	} else {
		return KNOT_ERANGE;
	}
}

_public_
int knot_pkt_reclaim(knot_pkt_t *pkt, uint16_t size)
{
	if (pkt == NULL) {
		return KNOT_EINVAL;
	}

	if (pkt->reserved >= size) {
		pkt->reserved -= size;
		return KNOT_EOK;
	} else {
		return KNOT_ERANGE;
	}
}

_public_
int knot_pkt_begin(knot_pkt_t *pkt, knot_section_t section_id)
{
	if (pkt == NULL || section_id < pkt->current) {
		return KNOT_EINVAL;
	}

	/* Remember watermark but not on repeated calls. */
	pkt->sections[section_id].pkt = pkt;
	if (section_id > pkt->current) {
		pkt->sections[section_id].pos = pkt->rrset_count;
	}

	pkt->current = section_id;

	return KNOT_EOK;
}

_public_
int knot_pkt_put_question(knot_pkt_t *pkt, const knot_dname_t *qname, uint16_t qclass, uint16_t qtype)
{
	if (pkt == NULL || qname == NULL) {
		return KNOT_EINVAL;
	}

	assert(pkt->size == KNOT_WIRE_HEADER_SIZE);
	assert(pkt->rrset_count == 0);

	/* Copy name into wire format buffer. */
	wire_ctx_t wire = wire_ctx_init(pkt->wire, pkt->max_size);
	wire_ctx_set_offset(&wire, KNOT_WIRE_HEADER_SIZE);

	int qname_len = knot_dname_to_wire(wire.position,
	                                   qname, wire_ctx_available(&wire));
	if (qname_len < 0) {
		return qname_len;
	}
	wire_ctx_skip(&wire, qname_len);

	/* Copy QNAME and canonicalize to lowercase. */
	knot_dname_copy_lower(pkt->lower_qname, qname);

	/* Copy QTYPE & QCLASS */
	wire_ctx_write_u16(&wire, qtype);
	wire_ctx_write_u16(&wire, qclass);

	/* Check errors. */
	if (wire.error != KNOT_EOK) {
		return wire.error;
	}

	/* Update question count and sizes. */
	knot_wire_set_qdcount(pkt->wire, 1);
	pkt->size = wire_ctx_offset(&wire);
	pkt->qname_size = qname_len;

	/* Start writing ANSWER. */
	return knot_pkt_begin(pkt, KNOT_ANSWER);
}

_public_
int knot_pkt_put_rotate(knot_pkt_t *pkt, uint16_t compr_hint, const knot_rrset_t *rr,
                        uint16_t rotate, uint16_t flags)
{
	if (pkt == NULL || rr == NULL) {
		return KNOT_EINVAL;
	}

	/* Reserve memory for RR descriptors. */
	int ret = pkt_rr_array_alloc(pkt, pkt->rrset_count + 1);
	if (ret != KNOT_EOK) {
		return ret;
	}

	/* Check for double insertion. */
	if ((flags & KNOT_PF_CHECKDUP) && pkt_contains(pkt, rr)) {
		return KNOT_EOK;
	}

	knot_rrinfo_t *rrinfo = &pkt->rr_info[pkt->rrset_count];
	memset(rrinfo, 0, sizeof(knot_rrinfo_t));
	rrinfo->pos = pkt->size;
	rrinfo->flags = flags;
	rrinfo->compress_ptr[0] = compr_hint;
	memcpy(pkt->rr + pkt->rrset_count, rr, sizeof(knot_rrset_t));

	/* Disable compression if no QNAME is available. */
	knot_compr_t *compr = NULL;
	if (knot_pkt_qname(pkt) != NULL) {
		/* Initialize compression context if it did not happen yet. */
		pkt->compr.rrinfo = rrinfo;
		if (pkt->compr.suffix.pos == 0) {
			pkt->compr.suffix.pos = KNOT_WIRE_HEADER_SIZE;
			pkt->compr.suffix.labels =
				knot_dname_labels(pkt->compr.wire + pkt->compr.suffix.pos,
				                  pkt->compr.wire);
		}

		compr = &pkt->compr;
	}

	uint8_t *pos = pkt->wire + pkt->size;
	size_t maxlen = pkt_remaining(pkt);

	/* Write RRSet to wireformat. */
	ret = knot_rrset_to_wire_extra(rr, pos, maxlen, rotate, compr, flags);
	if (ret < 0) {
		/* Truncate packet if required. */
		if (ret == KNOT_ESPACE && !(flags & KNOT_PF_NOTRUNC)) {
			knot_wire_set_tc(pkt->wire);
		}
		return ret;
	}

	size_t len = ret;
	uint16_t rr_added = rr->rrs.count;

	/* Keep reference to special types. */
	if (rr->type == KNOT_RRTYPE_OPT) {
		pkt->opt_rr = &pkt->rr[pkt->rrset_count];
	}

	if (rr_added > 0) {
		pkt->rrset_count += 1;
		pkt->sections[pkt->current].count += 1;
		pkt->size += len;
		pkt_rr_wirecount_add(pkt, pkt->current, rr_added);
	}

	return KNOT_EOK;
}

_public_
int knot_pkt_parse_question(knot_pkt_t *pkt)
{
	if (pkt == NULL) {
		return KNOT_EINVAL;
	}

	/* Check at least header size. */
	if (pkt->size < KNOT_WIRE_HEADER_SIZE) {
		return KNOT_EMALF;
	}

	/* We have at least some DNS header. */
	pkt->parsed = KNOT_WIRE_HEADER_SIZE;

	/* Check QD count. */
	uint16_t qd = knot_wire_get_qdcount(pkt->wire);
	if (qd > 1) {
		return KNOT_EMALF;
	}

	/* No question. */
	if (qd == 0) {
		pkt->qname_size = 0;
		return KNOT_EOK;
	}

	/* Process question. */
	int len = knot_dname_wire_check(pkt->wire + pkt->parsed,
	                                pkt->wire + pkt->size,
	                                NULL /* No compression in QNAME. */);
	if (len <= 0) {
		return KNOT_EMALF;
	}

	/* Check QCLASS/QTYPE size. */
	uint16_t question_size = len + 2 * sizeof(uint16_t); /* QCLASS + QTYPE */
	if (pkt->parsed + question_size > pkt->size) {
		return KNOT_EMALF;
	}

	pkt->parsed += question_size;
	pkt->qname_size = len;

	/* Copy QNAME and canonicalize to lowercase. */
	knot_dname_copy_lower(pkt->lower_qname, pkt->wire + KNOT_WIRE_HEADER_SIZE);

	size_t str_len = strnlen((char *)(pkt->wire + KNOT_WIRE_HEADER_SIZE), pkt->qname_size) + 1;
	if (pkt->qname_size != str_len) {
		pkt->flags |= KNOT_PF_NULLBYTE;
	}

	return KNOT_EOK;
}

/*! \brief Check constraints (position, uniqueness, validity) for special types
 *         (TSIG, OPT).
 */
static int check_rr_constraints(knot_pkt_t *pkt, knot_rrset_t *rr, size_t rr_size,
                                unsigned flags)
{
	switch (rr->type) {
	case KNOT_RRTYPE_TSIG:
		if (pkt->current != KNOT_ADDITIONAL || pkt->tsig_rr != NULL ||
		    !knot_tsig_rdata_is_ok(rr)) {
			return KNOT_EMALF;
		}

		/* Strip TSIG RR from wireformat and decrease ARCOUNT. */
		if (!(flags & KNOT_PF_KEEPWIRE)) {
			pkt->parsed -= rr_size;
			pkt->size -= rr_size;
			pkt->tsig_wire.pos = pkt->wire + pkt->parsed;
			pkt->tsig_wire.len = rr_size;
			knot_wire_set_arcount(pkt->wire, knot_wire_get_arcount(pkt->wire) - 1);
		}

		pkt->tsig_rr = rr;
		break;
	case KNOT_RRTYPE_OPT:
		if (pkt->current != KNOT_ADDITIONAL || pkt->opt_rr != NULL ||
		    knot_edns_get_options(rr, &pkt->edns_opts, &pkt->mm) != KNOT_EOK) {
			return KNOT_EMALF;
		}

		pkt->opt_rr = rr;
		break;
	default:
		break;
	}

	return KNOT_EOK;
}

static int parse_rr(knot_pkt_t *pkt, unsigned flags)
{
	assert(pkt);

	if (pkt->parsed >= pkt->size) {
		return KNOT_EFEWDATA;
	}

	/* Reserve memory for RR descriptors. */
	int ret = pkt_rr_array_alloc(pkt, pkt->rrset_count + 1);
	if (ret != KNOT_EOK) {
		return ret;
	}

	/* Initialize RR info. */
	memset(&pkt->rr_info[pkt->rrset_count], 0, sizeof(knot_rrinfo_t));
	pkt->rr_info[pkt->rrset_count].pos = pkt->parsed;
	pkt->rr_info[pkt->rrset_count].flags = KNOT_PF_FREE;

	/* Parse wire format. */
	size_t rr_size = pkt->parsed;
	knot_rrset_t *rr = &pkt->rr[pkt->rrset_count];
	ret = knot_rrset_rr_from_wire(pkt->wire, &pkt->parsed, pkt->size,
	                              rr, &pkt->mm, !(flags & KNOT_PF_NOCANON));
	if (ret != KNOT_EOK) {
		return ret;
	}

	/* Calculate parsed RR size from before/after parsing. */
	rr_size = (pkt->parsed - rr_size);

	/* Update packet RRSet count. */
	++pkt->rrset_count;
	++pkt->sections[pkt->current].count;

	/* Check special RRs (OPT and TSIG). */
	return check_rr_constraints(pkt, rr, rr_size, flags);
}

static int parse_section(knot_pkt_t *pkt, unsigned flags)
{
	assert(pkt);

	uint16_t rr_parsed = 0;
	uint16_t rr_count = pkt_rr_wirecount(pkt, pkt->current);

	/* Parse all RRs belonging to the section. */
	for (rr_parsed = 0; rr_parsed < rr_count; ++rr_parsed) {
		int ret = parse_rr(pkt, flags);
		if (ret != KNOT_EOK) {
			return ret;
		}
	}

	return KNOT_EOK;
}

static int parse_payload(knot_pkt_t *pkt, unsigned flags)
{
	assert(pkt);
	assert(pkt->wire);
	assert(pkt->size > 0);

	/* Reserve memory in advance to avoid resizing. */
	size_t rr_count = knot_wire_get_ancount(pkt->wire) +
	                  knot_wire_get_nscount(pkt->wire) +
	                  knot_wire_get_arcount(pkt->wire);

	if (rr_count > pkt->size / KNOT_WIRE_RR_MIN_SIZE) {
		return KNOT_EMALF;
	}

	int ret = pkt_rr_array_alloc(pkt, rr_count);
	if (ret != KNOT_EOK) {
		return ret;
	}

	for (knot_section_t i = KNOT_ANSWER; i <= KNOT_ADDITIONAL; ++i) {
		ret = knot_pkt_begin(pkt, i);
		if (ret != KNOT_EOK) {
			return ret;
		}
		ret = parse_section(pkt, flags);
		if (ret != KNOT_EOK) {
			return ret;
		}
	}

	/* TSIG must be last record of AR if present. */
	const knot_pktsection_t *ar = knot_pkt_section(pkt, KNOT_ADDITIONAL);
	if (pkt->tsig_rr != NULL) {
		const knot_rrset_t *last_rr = knot_pkt_rr(ar, ar->count - 1);
		if (ar->count > 0 && pkt->tsig_rr->rrs.rdata != last_rr->rrs.rdata) {
			return KNOT_EMALF;
		}
	}

	/* Check for trailing garbage. */
	if (pkt->parsed < pkt->size) {
		return KNOT_ETRAIL;
	}

	return KNOT_EOK;
}

_public_
int knot_pkt_parse(knot_pkt_t *pkt, unsigned flags)
{
	if (pkt == NULL) {
		return KNOT_EINVAL;
	}

	/* Reset parse state. */
	sections_reset(pkt);

	int ret = knot_pkt_parse_question(pkt);
	if (ret == KNOT_EOK) {
		ret = parse_payload(pkt, flags);
	}

	return ret;
}

_public_
uint16_t knot_pkt_ext_rcode(const knot_pkt_t *pkt)
{
	if (pkt == NULL) {
		return 0;
	}

	/* Get header RCODE. */
	uint16_t rcode = knot_wire_get_rcode(pkt->wire);

	/* Update to extended RCODE if EDNS is available. */
	if (pkt->opt_rr != NULL) {
		uint8_t opt_rcode = knot_edns_get_ext_rcode(pkt->opt_rr);
		rcode = knot_edns_whole_rcode(opt_rcode, rcode);
	}

	/* Return if not NOTAUTH. */
	if (rcode != KNOT_RCODE_NOTAUTH) {
		return rcode;
	}

	/* Get TSIG RCODE. */
	uint16_t tsig_rcode = KNOT_RCODE_NOERROR;
	if (pkt->tsig_rr != NULL) {
		tsig_rcode = knot_tsig_rdata_error(pkt->tsig_rr);
	}

	/* Return proper RCODE. */
	if (tsig_rcode != KNOT_RCODE_NOERROR) {
		return tsig_rcode;
	} else {
		return rcode;
	}
}

_public_
const char *knot_pkt_ext_rcode_name(const knot_pkt_t *pkt)
{
	if (pkt == NULL) {
		return "";
	}

	uint16_t rcode = knot_pkt_ext_rcode(pkt);

	const knot_lookup_t *item = NULL;
	if (pkt->tsig_rr != NULL) {
		item = knot_lookup_by_id(knot_tsig_rcode_names, rcode);
	}
	if (item == NULL) {
		item = knot_lookup_by_id(knot_rcode_names, rcode);
	}

	return (item != NULL) ? item->name : "";
}
