// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2003 Andrew Tridgell
// This file is part of Gromox.
#include <cassert>
#include <climits>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <libHX/endian_float.h>
#include <gromox/common_types.hpp>
#include <gromox/defs.h>
#include <gromox/ndr.hpp>
#include <gromox/util.hpp>
#define TRY(expr) do { pack_result klfdv{expr}; if (klfdv != pack_result::ok) return klfdv; } while (false)
#define NDR_BE(pndr) ((pndr->flags & NDR_FLAG_BIGENDIAN) != 0)
#define CLAMP16(v) ((v) = std::min((v), static_cast<uint16_t>(UINT16_MAX)))
#define CLAMP32(v) ((v) = std::min((v), static_cast<uint32_t>(UINT32_MAX)))
#define CLAMP64(v) ((v) = std::min((v), static_cast<uint64_t>(UINT64_MAX)))

pack_result NDR_PULL::advance(uint32_t size)
{
	auto pndr = this;
	pndr->offset += size;
	return pndr->offset > pndr->data_size ? pack_result::bufsize : pack_result::ok;
}

void ndr_set_flags(uint32_t *pflags, uint32_t new_flags)
{
	if (new_flags & NDR_ALIGN_FLAGS)
		/* Ensure we only have the passed-in
		   align flag set in the new_flags,
		   remove any old align flag. */
		(*pflags) &= ~NDR_ALIGN_FLAGS;
	if (new_flags & NDR_FLAG_NO_RELATIVE_REVERSE)
		(*pflags) &= ~NDR_FLAG_RELATIVE_REVERSE;
	(*pflags) |= new_flags;
}

static size_t ndr_align_size(uint32_t offset, size_t n)
{
	if (!(offset & (n - 1)))
		return 0;
	return n - (offset & (n - 1));
}

void NDR_PULL::init(const void *d, uint32_t ds, uint32_t fl)
{
	auto pndr = this;
	data = static_cast<const uint8_t *>(d);
	data_size = ds;
	pndr->offset = 0;
	flags = fl;
	pndr->ptr_count = 0;
}

static bool ndr_pull_check_padding(NDR_PULL *pndr, size_t n)
{
	size_t ofs2;

	ofs2 = (pndr->offset + (n - 1)) & ~(n - 1);
	for (size_t i = pndr->offset; i < ofs2; ++i)
		if (pndr->data[i] != 0)
			return false;
	return true;
}

pack_result NDR_PULL::align(size_t size)
{
	auto pndr = this;
	if (size == 5)
		size = (pndr->flags & NDR_FLAG_NDR64) ? 8 : 4;
	else if (size == 3)
		size = (pndr->flags & NDR_FLAG_NDR64) ? 4 : 2;
	
	if (!(pndr->flags & NDR_FLAG_NOALIGN)) {
		if (pndr->flags & NDR_FLAG_PAD_CHECK &&
		    !ndr_pull_check_padding(pndr, size))
			return pack_result::padding;
		pndr->offset = (pndr->offset + (size - 1)) & ~(size - 1);
	}
	if (pndr->offset > pndr->data_size)
		return pack_result::bufsize;
	return pack_result::ok;
}

pack_result NDR_PULL::union_align(size_t size)
{
	/* MS-RPCE section 2.2.5.3.4.4 */
	return (flags & NDR_FLAG_NDR64) ? align(size) : pack_result::ok;
}

pack_result NDR_PULL::trailer_align(size_t size)
{
	/* MS-RPCE section 2.2.5.3.4.1 */
	return (flags & NDR_FLAG_NDR64) ? align(size) : pack_result::ok;
}

pack_result NDR_PULL::g_str(char *buff, uint32_t inbytes)
{
	auto pndr = this;
	if (0 == inbytes) {
		buff[0] = '\0';
		return pack_result::ok;
	}
	if (pndr->data_size < inbytes || pndr->offset + inbytes > pndr->data_size)
		return pack_result::bufsize;
	memcpy(buff, pndr->data + pndr->offset, inbytes);
	buff[inbytes] = '\0';
	return pndr->advance(inbytes);
}

pack_result NDR_PULL::g_str(std::string *buf, size_t z) try
{
	if (z == 0) {
		buf->clear();
		return pack_result::ok;
	}
	if (data_size < z || offset + z > data_size)
		return pack_result::bufsize;
	buf->assign(reinterpret_cast<const char *>(&data[offset]), z);
	return advance(z);
} catch (const std::bad_alloc &) {
	return pack_result::alloc;
}

pack_result NDR_PULL::g_uint8(uint8_t *v)
{
	auto pndr = this;
	if (pndr->data_size < 1 || pndr->offset + 1 > pndr->data_size)
		return pack_result::bufsize;
	*v = pndr->data[pndr->offset];
	pndr->offset += 1;
	return pack_result::ok;
}

pack_result NDR_PULL::g_uint16(uint16_t *v)
{
	auto pndr = this;
	TRY(pndr->align(2));
	if (pndr->data_size < 2 || pndr->offset + 2 > pndr->data_size)
		return pack_result::bufsize;
	auto r = &pndr->data[pndr->offset];
	*v = NDR_BE(pndr) ? be16p_to_cpu(r) : le16p_to_cpu(r);
	CLAMP16(*v);
	pndr->offset += 2;
	return pack_result::ok;
}

pack_result NDR_PULL::g_int32(int32_t *v)
{
	auto pndr = this;
	return pndr->g_uint32(reinterpret_cast<uint32_t *>(v));
}

pack_result NDR_PULL::g_uint32(uint32_t *v)
{
	auto pndr = this;
	TRY(pndr->align(4));
	if (pndr->data_size < 4 || pndr->offset + 4 > pndr->data_size)
		return pack_result::bufsize;
	auto r = &pndr->data[pndr->offset];
	*v = NDR_BE(pndr) ? be32p_to_cpu(r) : le32p_to_cpu(r);
	CLAMP32(*v);
	pndr->offset += 4;
	return pack_result::ok;
}

pack_result NDR_PULL::g_uint32_x2(uint64_t *v)
{
	TRY(align(4));
	uint32_t lo, hi;
	TRY(g_uint32(&lo));
	TRY(g_uint32(&hi));
	CLAMP32(lo);
	CLAMP32(hi);
	*v = (static_cast<uint64_t>(hi) << 32) | lo;
	return trailer_align(4);
}

pack_result NDR_PULL::g_uint64(uint64_t *v)
{
	auto pndr = this;
	TRY(pndr->align(8));
	if (pndr->data_size < 8 || pndr->offset + 8 > pndr->data_size)
		return pack_result::bufsize;
	auto r = &pndr->data[pndr->offset];
	*v = std::min(NDR_BE(pndr) ? be64p_to_cpu(r) : le64p_to_cpu(r), UINT64_MAX);
	pndr->offset += 8;
	return pack_result::ok;
}

pack_result NDR_PULL::g_float(float *v)
{
	auto pndr = this;
	TRY(pndr->align(4));
	if (pndr->data_size < 4 || pndr->offset + 4 > pndr->data_size)
		return pack_result::bufsize;
	auto r = &pndr->data[pndr->offset];
	*v = NDR_BE(pndr) ? float_be32p_to_cpu(r) : float_le32p_to_cpu(r);
	pndr->offset += 4;
	return pack_result::ok;
}

pack_result NDR_PULL::g_double(double *v)
{
	auto pndr = this;
	TRY(pndr->align(8));
	if (pndr->data_size < 8 || pndr->offset + 8 > pndr->data_size)
		return pack_result::bufsize;
	auto r = &pndr->data[pndr->offset];
	*v = NDR_BE(pndr) ? float_be64p_to_cpu(r) : float_le64p_to_cpu(r);
	static_assert(sizeof(double) == 8);
	pndr->offset += 8;
	return pack_result::ok;
}

pack_result NDR_PULL::g_ulong(uint32_t *v)
{
	auto pndr = this;
	uint64_t v64;
	
	if (pndr->flags & NDR_FLAG_NDR64) {
		TRY(pndr->g_uint64(&v64));
		*v = v64;
		if (v64 != *v)
			return pack_result::ndr64;
		return pack_result::ok;
	}
	return pndr->g_uint32(v);
}

pack_result NDR_PULL::g_bytes(void *d, uint32_t n)
{
	auto pndr = this;
	if (pndr->data_size < n || pndr->offset + n > pndr->data_size)
		return pack_result::bufsize;
	memcpy(d, &pndr->data[pndr->offset], n);
	pndr->offset += n;
	return pack_result::ok;
}

pack_result NDR_PULL::g_guid(GUID *r)
{
	auto pndr = this;
	TRY(pndr->align(4));
	TRY(pndr->g_uint32(&r->time_low));
	TRY(pndr->g_uint16(&r->time_mid));
	TRY(pndr->g_uint16(&r->time_hi_and_version));
	TRY(pndr->g_bytes(r->clock_seq, 2));
	TRY(pndr->g_bytes(r->node, 6));
	TRY(pndr->trailer_align(4));
	return pack_result::ok;
}

pack_result NDR_PULL::g_syntax(SYNTAX_ID *r)
{
	auto pndr = this;
	TRY(pndr->align(4));
	TRY(pndr->g_guid(&r->uuid));
	TRY(pndr->g_uint32(&r->version));
	TRY(pndr->trailer_align(4));
	return pack_result::ok;
}

pack_result NDR_PULL::g_blob(DATA_BLOB *pblob)
{
	auto pndr = this;
	uint32_t length;

	length = 0;
	if (pndr->flags & NDR_FLAG_REMAINING) {
		length = pndr->data_size - pndr->offset;
	} else if (pndr->flags & (NDR_ALIGN_FLAGS & ~NDR_FLAG_NOALIGN)) {
		if (pndr->flags & NDR_FLAG_ALIGN2)
			length = ndr_align_size(pndr->offset, 2);
		else if (pndr->flags & NDR_FLAG_ALIGN4)
			length = ndr_align_size(pndr->offset, 4);
		else if (pndr->flags & NDR_FLAG_ALIGN8)
			length = ndr_align_size(pndr->offset, 8);
		if (pndr->data_size - pndr->offset < length)
			length = pndr->data_size - pndr->offset;
	} else {
		TRY(pndr->g_uint32(&length));
	}
	if (pndr->data_size < length || pndr->offset + length > pndr->data_size)
		return pack_result::bufsize;
	pblob->pb = gromox::me_alloc<uint8_t>(length);
	if (pblob->pb == nullptr)
		return pack_result::alloc;
	memcpy(pblob->pb, &pndr->data[pndr->offset], length);
	pblob->cb = length;
	pndr->offset += length;
	return pack_result::ok;
}

/* free memory internal of blob except of blob itself */
void ndr_free_data_blob(DATA_BLOB *pblob)
{
	if (pblob->pb != nullptr) {
		free(pblob->pb);
		pblob->pb = nullptr;
	}
	pblob->cb = 0;
}

pack_result NDR_PULL::check_str(uint32_t count, uint32_t element_size)
{
	auto pndr = this;
	uint32_t i;
	uint32_t saved_offset;

	saved_offset = pndr->offset;
	TRY(pndr->advance((count - 1) * element_size));
	if (pndr->data_size < element_size ||
	    pndr->offset + element_size > pndr->data_size)
		return pack_result::bufsize;
	for (i=0; i<element_size; i++) {
		if (0 != pndr->data[pndr->offset + i]) {
			pndr->offset = saved_offset;
			return pack_result::array_size;
		}
	}
	
	pndr->offset = saved_offset;
	return pack_result::ok;
}

pack_result NDR_PULL::g_genptr(uint32_t *v)
{
	auto pndr = this;
	auto status = pndr->g_ulong(v);
	if (status == pack_result::ok && *v != 0)
		pndr->ptr_count ++;
	return pack_result::ok;
}

pack_result NDR_PULL::g_ctx_handle(CONTEXT_HANDLE *r)
{
	auto pndr = this;
	TRY(pndr->align(4));
	TRY(pndr->g_uint32(&r->handle_type));
	TRY(pndr->g_guid(&r->guid));
	TRY(pndr->trailer_align(4));
	return pack_result::ok;
}

bool NDR_PUSH::init(void *d, uint32_t as, uint32_t fl, const EXT_BUFFER_MGT *mgt)
{
	const EXT_BUFFER_MGT default_mgt = {gromox::zalloc, realloc, free};
	auto pndr = this;
	m_mgt = mgt != nullptr ? *mgt : default_mgt;
	flags = fl;
	if (d == nullptr) {
		flags |= EXT_FLAG_DYNAMIC;
		alloc_size = 8192;
		data = static_cast<uint8_t *>(m_mgt.alloc(alloc_size));
		if (data == nullptr) {
			alloc_size = 0;
			return false;
		}
	} else {
		flags &= ~EXT_FLAG_DYNAMIC;
		data = static_cast<uint8_t *>(d);
		alloc_size = as;
	}
	pndr->offset = 0;
	pndr->ptr_count = 0;
	double_list_init(&pndr->full_ptr_list);
	return true;
}

NDR_PUSH::~NDR_PUSH()
{
	auto pndr = this;
	DOUBLE_LIST_NODE *pnode;
	
	while ((pnode = double_list_pop_front(&pndr->full_ptr_list)) != nullptr)
		free(pnode->pdata);
	double_list_free(&pndr->full_ptr_list);
	if (flags & EXT_FLAG_DYNAMIC)
		m_mgt.free(data);
}

static bool ndr_push_make_room(NDR_PUSH *pndr, uint32_t extra_size)
{
	auto reqtot = extra_size + pndr->offset;
	if (pndr->alloc_size >= reqtot)
		return TRUE;
	if (!(pndr->flags & EXT_FLAG_DYNAMIC))
		return FALSE;
	if (reqtot < pndr->alloc_size * 2)
		/* Exponential growth policy, needed to reach amoreqtotized linear time (like std::string) */
		reqtot = pndr->alloc_size * 2;
	auto pdata = static_cast<uint8_t *>(pndr->m_mgt.realloc(pndr->data, reqtot));
	if (pdata == nullptr)
		return FALSE;
	pndr->data = pdata;
	pndr->alloc_size = reqtot;
	return TRUE;
}

pack_result NDR_PUSH::p_bytes(const void *pdata, uint32_t n)
{
	auto pndr = this;
	if (n == 0)
		return pack_result::ok;
	if (!ndr_push_make_room(pndr, n))
		return pack_result::bufsize;
	if (pdata == nullptr)
		memset(pndr->data + pndr->offset, 0, n);
	else
		memcpy(pndr->data + pndr->offset, pdata, n);
	pndr->offset += n;
	return pack_result::ok;
}

pack_result NDR_PUSH::p_bytes(std::string_view sv)
{
	if (sv.size() > UINT32_MAX)
		return pack_result::format;
	return p_bytes(sv.data(), sv.size());
}

pack_result NDR_PUSH::p_uint8(uint8_t v)
{
	auto pndr = this;
	if (!ndr_push_make_room(pndr, 1))
		return pack_result::bufsize;
	pndr->data[pndr->offset] = v;
	pndr->offset += 1;
	return pack_result::ok;
}

pack_result NDR_PUSH::align(size_t size)
{
	auto pndr = this;
	uint32_t pad;
	
	if (size == 5)
		size = (pndr->flags & NDR_FLAG_NDR64) ? 8 : 4;
	else if (size == 3)
		size = (pndr->flags & NDR_FLAG_NDR64) ? 4 : 2;
	if (!(pndr->flags & NDR_FLAG_NOALIGN)) {
		pad = ((pndr->offset + (size - 1)) & ~(size - 1)) - pndr->offset;
		while (pad--)
			TRY(pndr->p_uint8(0));
	}
	return pack_result::ok;
}

pack_result NDR_PUSH::union_align(size_t size)
{
	return (flags & NDR_FLAG_NDR64) ? align(size) : pack_result::ok;
}

pack_result NDR_PUSH::trailer_align(size_t size)
{
	return (flags & NDR_FLAG_NDR64) ? align(size) : pack_result::ok;
}

pack_result NDR_PUSH::p_uint16(uint16_t v)
{
	auto pndr = this;
	TRY(pndr->align(2));
	if (!ndr_push_make_room(pndr, 2))
		return pack_result::bufsize;
	auto r = &pndr->data[pndr->offset];
	NDR_BE(pndr) ? cpu_to_be16p(r, v) : cpu_to_le16p(r, v);
	pndr->offset += 2;
	return pack_result::ok;
}

pack_result NDR_PUSH::p_uint32(uint32_t v)
{
	auto pndr = this;
	TRY(pndr->align(4));
	if (!ndr_push_make_room(pndr, 4))
		return pack_result::bufsize;
	auto r = &pndr->data[pndr->offset];
	NDR_BE(pndr) ? cpu_to_be32p(r, v) : cpu_to_le32p(r, v);
	pndr->offset += 4;
	return pack_result::ok;
}

pack_result NDR_PUSH::p_uint32_x2(uint64_t v)
{
	TRY(align(4));
	TRY(p_uint32(v));
	TRY(p_uint32(v >> 32));
	return trailer_align(4);
}

pack_result NDR_PUSH::p_uint64(uint64_t v)
{
	static_assert(CHAR_BIT == 8, "");
	auto pndr = this;
	TRY(pndr->align(8));
	if (!ndr_push_make_room(pndr, 8))
		return pack_result::bufsize;
	auto r = &pndr->data[pndr->offset];
	NDR_BE(pndr) ? cpu_to_be64p(r, v) : cpu_to_le64p(r, v);
	pndr->offset += 8;
	return pack_result::ok;
}

pack_result NDR_PUSH::p_float(float v)
{
	auto pndr = this;
	TRY(pndr->align(4));
	if (!ndr_push_make_room(pndr, 4))
		return pack_result::bufsize;
	auto r = &pndr->data[pndr->offset];
	NDR_BE(pndr) ? float_cpu_to_be32p(r, v) : float_cpu_to_le32p(r, v);
	pndr->offset += 4;
	return pack_result::ok;
}

pack_result NDR_PUSH::p_double(double v)
{
	auto pndr = this;
	TRY(pndr->align(8));
	if (!ndr_push_make_room(pndr, 8))
		return pack_result::bufsize;
	auto r = &pndr->data[pndr->offset];
	NDR_BE(pndr) ? float_cpu_to_be64p(r, v) : float_cpu_to_le64p(r, v);
	pndr->offset += 8;
	return pack_result::ok;
}

pack_result NDR_PUSH::p_ulong(uint32_t v)
{
	return (flags & NDR_FLAG_NDR64) ? p_uint64(v) : p_uint32(v);
}

/*
 * Push a DATA_BLOB onto the wire.
 * 1) When called with NDR_FLAG_ALIGN* alignment flags set, push padding
 *    bytes _only_. The length is determined by the alignment required and the
 *    current ndr offset.
 * 2) When called with the NDR_FLAG_REMAINING flag, push the byte array to
 *    the ndr buffer.
 * 3) Otherwise, push a uint32 length _and_ a corresponding byte array to the
 *    ndr buffer.
 */
pack_result NDR_PUSH::p_blob(DATA_BLOB blob)
{
	auto pndr = this;
	int length = 0;
	uint8_t buff[8];
	
	if (pndr->flags & NDR_FLAG_REMAINING) {
		/* nothing to do */
	} else if (pndr->flags & (NDR_ALIGN_FLAGS & ~NDR_FLAG_NOALIGN)) {
		if (pndr->flags & NDR_FLAG_ALIGN2)
			length = ndr_align_size(pndr->offset, 2);
		else if (pndr->flags & NDR_FLAG_ALIGN4)
			length = ndr_align_size(pndr->offset, 4);
		else if (pndr->flags & NDR_FLAG_ALIGN8)
			length = ndr_align_size(pndr->offset, 8);
		memset(buff, 0, length);
		return pndr->p_bytes(buff, length);
	} else {
		TRY(pndr->p_uint32(blob.cb));
	}
	assert(blob.pb != nullptr || blob.cb == 0);
	TRY(pndr->p_bytes(blob));
	return pack_result::ok;
}

pack_result NDR_PUSH::p_str(const char *var, uint32_t required)
{	
	auto pndr = this;
	if (!ndr_push_make_room(pndr, required))
		return pack_result::bufsize;
	memcpy(pndr->data + pndr->offset, var, required);
	pndr->offset += required;
	return pack_result::ok;
}

pack_result NDR_PUSH::p_guid(const GUID &v)
{
	auto pndr = this;
	auto r = &v;
	TRY(pndr->align(4));
	TRY(pndr->p_uint32(r->time_low));
	TRY(pndr->p_uint16(r->time_mid));
	TRY(pndr->p_uint16(r->time_hi_and_version));
	TRY(pndr->p_bytes(r->clock_seq, 2));
	TRY(pndr->p_bytes(r->node, 6));
	return pndr->trailer_align(4);
}

pack_result NDR_PUSH::p_syntax(const SYNTAX_ID &v)
{
	auto pndr = this;
	auto r = &v;
	TRY(pndr->align(4));
	TRY(pndr->p_guid(r->uuid));
	TRY(pndr->p_uint32(r->version));
	TRY(pndr->trailer_align(4));
	return pack_result::ok;
}

pack_result NDR_PUSH::p_zero(uint32_t n)
{
	auto pndr = this;
	if (!ndr_push_make_room(pndr, n))
		return pack_result::bufsize;
	memset(pndr->data + pndr->offset, 0, n);
	pndr->offset += n;
	return pack_result::ok;
}

pack_result NDR_PUSH::p_unique_ptr(const void *p)
{
	auto pndr = this;
	uint32_t ptr;
	
	ptr = 0;
	if (NULL != p) {
		ptr = pndr->ptr_count * 4;
		ptr |= 0x00020000;
		pndr->ptr_count++;
	}
	return pndr->p_ulong(ptr);
}

pack_result NDR_PUSH::p_ctx_handle(const CONTEXT_HANDLE &v)
{
	auto pndr = this;
	auto r = &v;
	TRY(pndr->align(4));
	TRY(pndr->p_uint32(r->handle_type));
	TRY(pndr->p_guid(r->guid));
	TRY(pndr->trailer_align(4));
	return pack_result::ok;
}
