/*
 * Copyright (C) 2019 Red Hat, Inc.
 *
 * Author: Daiki Ueno
 *
 * This file is part of GnuTLS.
 *
 * The GnuTLS is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>
 *
 */

#include "gnutls_int.h"
#include "iov.h"

/**
 * _gnutls_iov_iter_init:
 * @iter: the iterator
 * @iov: the data buffers
 * @iov_count: the number of data buffers
 * @block_size: block size to iterate
 *
 * Initialize the iterator.
 *
 * Returns: On success, %GNUTLS_E_SUCCESS (0) is returned, otherwise
 *   an error code is returned
 */
int _gnutls_iov_iter_init(struct iov_iter_st *iter, const giovec_t *iov,
			  size_t iov_count, size_t block_size)
{
	if (unlikely(block_size > MAX_CIPHER_BLOCK_SIZE))
		return gnutls_assert_val(GNUTLS_E_INVALID_REQUEST);

	iter->iov = iov;
	iter->iov_count = iov_count;
	iter->iov_index = 0;
	iter->iov_offset = 0;
	iter->block_size = block_size;
	iter->block_offset = 0;
	return 0;
}

/**
 * _gnutls_iov_iter_next:
 * @iter: the iterator
 * @data: the return location of extracted data
 *
 * Retrieve block(s) pointed by @iter and advance it to the next
 * position.  It returns the number of bytes in @data.  At the end of
 * iteration, 0 is returned.
 *
 * If the data stored in @iter is not multiple of the block size, the
 * remaining data is stored in the "block" field of @iter with the
 * size stored in the "block_offset" field.
 *
 * Returns: On success, a value greater than or equal to zero is
 *   returned, otherwise a negative error code is returned
 */
ssize_t _gnutls_iov_iter_next(struct iov_iter_st *iter, uint8_t **data)
{
	while (iter->iov_index < iter->iov_count) {
		const giovec_t *iov = &iter->iov[iter->iov_index];
		uint8_t *p = iov->iov_base;
		size_t len = iov->iov_len;
		size_t block_left;

		if (!p) {
			// skip NULL iov entries, else we run into issues below
			iter->iov_index++;
			continue;
		}

		if (unlikely(len < iter->iov_offset))
			return gnutls_assert_val(
				GNUTLS_E_UNEXPECTED_PACKET_LENGTH);
		len -= iter->iov_offset;
		p += iter->iov_offset;

		/* We have at least one full block, return a whole set
		 * of full blocks immediately. */
		if (iter->block_offset == 0 && len >= iter->block_size) {
			if ((len % iter->block_size) == 0) {
				iter->iov_index++;
				iter->iov_offset = 0;
			} else {
				len -= (len % iter->block_size);
				iter->iov_offset += len;
			}

			/* Return the blocks. */
			*data = p;
			return len;
		}

		/* We can complete one full block to return. */
		block_left = iter->block_size - iter->block_offset;
		if (len >= block_left) {
			memcpy(iter->block + iter->block_offset, p, block_left);
			if (len == block_left) {
				iter->iov_index++;
				iter->iov_offset = 0;
			} else
				iter->iov_offset += block_left;
			iter->block_offset = 0;

			/* Return the filled block. */
			*data = iter->block;
			return iter->block_size;
		}

		/* Not enough data for a full block, store in temp
		 * memory and continue. */
		memcpy(iter->block + iter->block_offset, p, len);
		iter->block_offset += len;
		iter->iov_index++;
		iter->iov_offset = 0;
	}

	if (iter->block_offset > 0) {
		size_t len = iter->block_offset;

		/* Return the incomplete block. */
		*data = iter->block;
		iter->block_offset = 0;
		return len;
	}

	return 0;
}

/**
 * _gnutls_iov_iter_sync:
 * @iter: the iterator
 * @data: data returned by _gnutls_iov_iter_next
 * @data_size: size of @data
 *
 * Flush the content of temp buffer (if any) to the data buffer.
 */
int _gnutls_iov_iter_sync(struct iov_iter_st *iter, const uint8_t *data,
			  size_t data_size)
{
	size_t iov_index;
	size_t iov_offset;

	/* We didn't return the cached block. */
	if (data != iter->block)
		return 0;

	iov_index = iter->iov_index;
	iov_offset = iter->iov_offset;

	/* When syncing a cache block we walk backwards because we only have a
	 * pointer to were the block ends in the iovec, walking backwards is
	 * fine as we are always writing a full block, so the whole content
	 * is written in the right places:
	 * iovec:     |--0--|---1---|--2--|-3-|
	 * block:     |-----------------------|
	 * 1st write                      |---|
	 * 2nd write                |-----
	 * 3rd write        |-------
	 * last write |-----
	 */
	while (data_size > 0) {
		const giovec_t *iov;
		uint8_t *p;
		size_t to_write;

		while (iov_offset == 0) {
			if (unlikely(iov_index == 0))
				return gnutls_assert_val(
					GNUTLS_E_INTERNAL_ERROR);

			iov_index--;
			iov_offset = iter->iov[iov_index].iov_len;
		}

		iov = &iter->iov[iov_index];
		p = iov->iov_base;
		to_write = MIN(data_size, iov_offset);

		iov_offset -= to_write;
		data_size -= to_write;

		memcpy(p + iov_offset, &iter->block[data_size], to_write);
	}

	return 0;
}
