/*
 * msgbuff.c - netlink message buffer
 *
 * Data structures and code for flexible message buffer abstraction.
 */

#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <stdint.h>

#include "../internal.h"
#include "netlink.h"
#include "msgbuff.h"

#define MAX_MSG_SIZE (4 << 20)		/* 4 MB */

/**
 * msgbuff_realloc() - reallocate buffer if needed
 * @msgbuff:  message buffer
 * @new_size: requested minimum size (add MNL_SOCKET_BUFFER_SIZE if zero)
 *
 * Make sure allocated buffer has size at least @new_size. If @new_size is
 * shorter than current size, do nothing. If @new_size is 0, grow buffer by
 * MNL_SOCKET_BUFFER_SIZE. Fail if new size would exceed MAX_MSG_SIZE.
 *
 * Return: 0 on success or negative error code
 */
int msgbuff_realloc(struct nl_msg_buff *msgbuff, unsigned int new_size)
{
	unsigned int nlhdr_off, genlhdr_off, payload_off;
	unsigned int old_size = msgbuff->size;
	char *nbuff;

	nlhdr_off = (char *)msgbuff->nlhdr - msgbuff->buff;
	genlhdr_off = (char *)msgbuff->genlhdr - msgbuff->buff;
	payload_off = (char *)msgbuff->payload - msgbuff->buff;

	if (!new_size)
		new_size = old_size + MNL_SOCKET_BUFFER_SIZE;
	if (new_size <= old_size)
		return 0;
	if (new_size > MAX_MSG_SIZE)
		return -EMSGSIZE;
	nbuff = realloc(msgbuff->buff, new_size);
	if (!nbuff) {
		msgbuff->buff = NULL;
		msgbuff->size = 0;
		msgbuff->left = 0;
		return -ENOMEM;
	}
	if (nbuff != msgbuff->buff) {
		if (new_size > old_size)
			memset(nbuff + old_size, '\0', new_size - old_size);
		msgbuff->nlhdr = (struct nlmsghdr *)(nbuff + nlhdr_off);
		msgbuff->genlhdr = (struct genlmsghdr *)(nbuff + genlhdr_off);
		msgbuff->payload = nbuff + payload_off;
		msgbuff->buff = nbuff;
	}
	msgbuff->size = new_size;
	msgbuff->left += (new_size - old_size);

	return 0;
}

/**
 * msgbuff_append() - add contents of another message buffer
 * @dest: target message buffer
 * @src:  source message buffer
 *
 * Append contents of @src at the end of @dest. Fail if target buffer cannot
 * be reallocated to sufficient size.
 *
 * Return: 0 on success or negative error code.
 */
int msgbuff_append(struct nl_msg_buff *dest, struct nl_msg_buff *src)
{
	unsigned int src_len = mnl_nlmsg_get_payload_len(src->nlhdr);
	unsigned int dest_len = MNL_ALIGN(msgbuff_len(dest));
	int ret;

	src_len -= GENL_HDRLEN;
	ret = msgbuff_realloc(dest, dest_len + src_len);
	if (ret < 0)
		return ret;
	memcpy(mnl_nlmsg_get_payload_tail(dest->nlhdr), src->payload, src_len);
	msgbuff_reset(dest, dest_len + src_len);

	return 0;
}

/**
 * ethnla_put - write a netlink attribute to message buffer
 * @msgbuff: message buffer
 * @type:    attribute type
 * @len:     attribute payload length
 * @data:    attribute payload
 *
 * Appends a netlink attribute with header to message buffer, reallocates
 * if needed. This is mostly used via specific ethnla_put_* wrappers for
 * basic data types.
 *
 * Return: false on success, true on error (reallocation failed)
 */
bool ethnla_put(struct nl_msg_buff *msgbuff, uint16_t type, size_t len,
		const void *data)
{
	struct nlmsghdr *nlhdr = msgbuff->nlhdr;

	while (!mnl_attr_put_check(nlhdr, msgbuff->left, type, len, data)) {
		int ret = msgbuff_realloc(msgbuff, 0);

		if (ret < 0)
			return true;
	}

	return false;
}

/**
 * ethnla_nest_start - start a nested attribute
 * @msgbuff: message buffer
 * @type:    nested attribute type (NLA_F_NESTED is added automatically)
 *
 * Return: pointer to the nest attribute or null of error
 */
struct nlattr *ethnla_nest_start(struct nl_msg_buff *msgbuff, uint16_t type)
{
	struct nlmsghdr *nlhdr = msgbuff->nlhdr;
	struct nlattr *attr;

	do {
		attr = mnl_attr_nest_start_check(nlhdr, msgbuff->left, type);
		if (attr)
			return attr;
	} while (msgbuff_realloc(msgbuff, 0) == 0);

	return NULL;
}

static bool __ethnla_fill_header_phy(struct nl_msg_buff *msgbuff, uint16_t type,
				     const char *devname, uint32_t phy_index,
				     uint32_t flags)
{
	struct nlattr *nest;

	nest = ethnla_nest_start(msgbuff, type);
	if (!nest)
		return true;

	if ((devname &&
	     ethnla_put_strz(msgbuff, ETHTOOL_A_HEADER_DEV_NAME, devname)) ||
	    (flags &&
	     ethnla_put_u32(msgbuff, ETHTOOL_A_HEADER_FLAGS, flags)) ||
	    (phy_index &&
	     ethnla_put_u32(msgbuff, ETHTOOL_A_HEADER_PHY_INDEX, phy_index)))
		goto err;

	ethnla_nest_end(msgbuff, nest);
	return false;

err:
	ethnla_nest_cancel(msgbuff, nest);
	return true;
}

/**
 * ethnla_fill_header() - write standard ethtool request header to message
 * @msgbuff: message buffer
 * @type:    attribute type for header nest
 * @devname: device name (NULL to omit)
 * @flags:   request flags (omitted if 0)
 *
 * Return: pointer to the nest attribute or null of error
 */
bool ethnla_fill_header(struct nl_msg_buff *msgbuff, uint16_t type,
			const char *devname, uint32_t flags)
{
	return __ethnla_fill_header_phy(msgbuff, type, devname, 0, flags);
}

/**
 * ethnla_fill_header_phy() - write standard ethtool request header to message,
 *			      targetting a device's phy
 * @msgbuff: message buffer
 * @type:    attribute type for header nest
 * @devname: device name (NULL to omit)
 * @phy_index: phy index to target (0 to omit)
 * @flags:   request flags (omitted if 0)
 *
 * Return: pointer to the nest attribute or null of error
 */
bool ethnla_fill_header_phy(struct nl_msg_buff *msgbuff, uint16_t type,
			    const char *devname, uint32_t phy_index,
			    uint32_t flags)
{
	return __ethnla_fill_header_phy(msgbuff, type, devname, phy_index,
					flags);
}

/**
 * __msg_init() - init a genetlink message, fill netlink and genetlink header
 * @msgbuff: message buffer
 * @family:  genetlink family
 * @cmd:     genetlink command (genlmsghdr::cmd)
 * @flags:   netlink flags (nlmsghdr::nlmsg_flags)
 * @version: genetlink family version (genlmsghdr::version)
 *
 * Initialize a new genetlink message, fill netlink and genetlink header and
 * set pointers in struct nl_msg_buff.
 *
 * Return: 0 on success or negative error code.
 */
int __msg_init(struct nl_msg_buff *msgbuff, int family, int cmd,
	       unsigned int flags, int version)
{
	struct nlmsghdr *nlhdr;
	struct genlmsghdr *genlhdr;
	int ret;

	ret = msgbuff_realloc(msgbuff, MNL_SOCKET_BUFFER_SIZE);
	if (ret < 0)
		return ret;
	memset(msgbuff->buff, '\0', NLMSG_HDRLEN + GENL_HDRLEN);

	nlhdr = mnl_nlmsg_put_header(msgbuff->buff);
	nlhdr->nlmsg_type = family;
	nlhdr->nlmsg_flags = flags;
	msgbuff->nlhdr = nlhdr;

	genlhdr = mnl_nlmsg_put_extra_header(nlhdr, sizeof(*genlhdr));
	genlhdr->cmd = cmd;
	genlhdr->version = version;
	msgbuff->genlhdr = genlhdr;

	msgbuff->payload = mnl_nlmsg_get_payload_offset(nlhdr, GENL_HDRLEN);

	return 0;
}

/**
 * msg_init() - init an ethtool netlink message
 * @msgbuff: message buffer
 * @cmd:     genetlink command (genlmsghdr::cmd)
 * @flags:   netlink flags (nlmsghdr::nlmsg_flags)
 *
 * Initialize a new ethtool netlink message, fill netlink and genetlink header
 * and set pointers in struct nl_msg_buff.
 *
 * Return: 0 on success or negative error code.
 */
int msg_init(struct nl_context *nlctx, struct nl_msg_buff *msgbuff, int cmd,
	     unsigned int flags)
{
	return __msg_init(msgbuff, nlctx->ethnl_fam, cmd, flags,
			  ETHTOOL_GENL_VERSION);
}

/**
 * msgbuff_init() - initialize a message buffer
 * @msgbuff: message buffer
 *
 * Initialize a message buffer structure before first use. Buffer length is
 * set to zero and the buffer is not allocated until the first call to
 * msgbuff_reallocate().
 */
void msgbuff_init(struct nl_msg_buff *msgbuff)
{
	memset(msgbuff, '\0', sizeof(*msgbuff));
}

/**
 * msg_done() - destroy a message buffer
 * @msgbuff: message buffer
 *
 * Free the buffer and reset size and remaining size.
 */
void msgbuff_done(struct nl_msg_buff *msgbuff)
{
	free(msgbuff->buff);
	msgbuff->buff = NULL;
	msgbuff->size = 0;
	msgbuff->left = 0;
}
