/*
 * Copyright (C) 2014-2022 Andreas Steffen
 *
 * Copyright (C) secunet Security Networks AG
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation; either version 2 of the License, or (at your
 * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
 *
 * This program 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 General Public License
 * for more details.
 */

#include "seg_env.h"

#include "imcv.h"
#include "pa_tnc/pa_tnc_msg.h"
#include "ietf/ietf_attr_pa_tnc_error.h"
#include "tcg/seg/tcg_seg_attr_seg_env.h"

#include <utils/debug.h>
#include <bio/bio_reader.h>
#include <bio/bio_writer.h>

#define BASE_ATTR_ID_PREFIX	0xFF

typedef struct private_seg_env_t private_seg_env_t;

/**
 * Private data of a seg_env_t object.
 */
struct private_seg_env_t {

	/**
	 * Public seg_env_t interface.
	 */
	seg_env_t public;

	/**
	 * Base Message ID
	 */
	uint32_t base_msg_id;

	/**
	 * Base Attribute
	 */
	pa_tnc_attr_t *base_attr;

	/**
	 * Base Attribute Info to be used for PA-TNC error messages
	 */
	u_char base_attr_info[8];

	/**
	 * Base Attribute needs more segment data
	 */
	bool need_more;

	/**
	 * Pointer to remaining attribute data to be sent
	 */
	chunk_t data;

	/**
	 * Maximum PA-TNC attribute segment size
	 */
	uint32_t max_seg_size;

};

METHOD(seg_env_t, get_base_msg_id, uint32_t,
	private_seg_env_t *this)
{
	return this->base_msg_id;
}

METHOD(seg_env_t, get_base_attr, pa_tnc_attr_t*,
	private_seg_env_t *this)
{
	return this->need_more ? NULL : this->base_attr->get_ref(this->base_attr);
}

METHOD(seg_env_t, get_base_attr_info, chunk_t,
	private_seg_env_t *this)
{
	return chunk_create(this->base_attr_info, 8);
}

METHOD(seg_env_t, first_segment, pa_tnc_attr_t*,
	private_seg_env_t *this, size_t max_attr_len)
{
	pa_tnc_attr_t *seg_env_attr;
	bio_writer_t *writer;
	pen_type_t type;
	chunk_t segment_data, value;
	size_t seg_size;
	uint8_t flags, seg_env_flags;

	/* compute size of first segment */
	seg_size = max_attr_len ? min(this->max_seg_size,
								  max_attr_len - PA_TNC_ATTR_HEADER_SIZE
											   - TCG_SEG_ATTR_SEG_ENV_HEADER)
							: this->max_seg_size;

	/* get components of base attribute header and data */
	flags = this->base_attr->get_noskip_flag(this->base_attr) ?
				PA_TNC_ATTR_FLAG_NOSKIP : PA_TNC_ATTR_FLAG_NONE;
	type = this->base_attr->get_type(this->base_attr);

	/* attribute data going into the first segment */
	segment_data = this->data;
	segment_data.len = seg_size - PA_TNC_ATTR_HEADER_SIZE;

	/* build encoding of the base attribute header and first segment data */
	writer = bio_writer_create(this->max_seg_size);
	writer->write_uint8 (writer, flags);
	writer->write_uint24(writer, type.vendor_id);
	writer->write_uint32(writer, type.type);
	writer->write_uint32(writer, PA_TNC_ATTR_HEADER_SIZE + this->data.len);
	writer->write_data  (writer, segment_data);
	value = writer->extract_buf(writer);
	writer->destroy(writer);
	this->data = chunk_skip(this->data, segment_data.len);

	DBG2(DBG_TNC, "creating first segment for base message ID %d (%d bytes)",
		 this->base_msg_id, seg_size);

	seg_env_flags = SEG_ENV_FLAG_START | SEG_ENV_FLAG_MORE;
	seg_env_attr = tcg_seg_attr_seg_env_create(value, seg_env_flags,
											   this->base_msg_id);
	chunk_free(&value);

	return seg_env_attr;
}

METHOD(seg_env_t, next_segment, pa_tnc_attr_t*,
	private_seg_env_t *this, bool *last)
{
	pa_tnc_attr_t *seg_env_attr;
	chunk_t segment_data;
	uint8_t seg_env_flags;
	bool is_last_segment;

	if (this->data.len == 0)
	{
		/* no more attribute data to segment available */
		return NULL;
	}

	/* attribute data going into the next segment */
	segment_data = this->data;
	segment_data.len = min(this->max_seg_size, this->data.len);
	this->data = chunk_skip(this->data, segment_data.len);

	is_last_segment = (this->data.len == 0);
	if (last)
	{
		*last = is_last_segment;
	}
	DBG2(DBG_TNC, "creating %s segment for base message ID %d (%d bytes)",
				   is_last_segment ? "last" : "next", this->base_msg_id,
				   segment_data.len);

	seg_env_flags = is_last_segment ? SEG_ENV_FLAG_NONE : SEG_ENV_FLAG_MORE;
	seg_env_attr = tcg_seg_attr_seg_env_create(segment_data, seg_env_flags,
											   this->base_msg_id);

	return seg_env_attr;
}

METHOD(seg_env_t, add_segment, bool,
	private_seg_env_t *this, chunk_t segment, pa_tnc_attr_t **error)
{
	pen_type_t type, error_code;
	uint32_t attr_offset;
	chunk_t msg_info;
	status_t status;

	this->base_attr->add_segment(this->base_attr, segment);
	status = this->base_attr->process(this->base_attr, &attr_offset);

	if (status != SUCCESS && status != NEED_MORE)
	{
		type = this->base_attr->get_type(this->base_attr);
		if (type.vendor_id == PEN_IETF && type.type == IETF_ATTR_PA_TNC_ERROR)
		{
			/* error while processing a PA-TNC error attribute - abort */
			return FALSE;
		}
		error_code = pen_type_create(PEN_IETF, PA_ERROR_INVALID_PARAMETER);
		msg_info = get_base_attr_info(this);
		*error = ietf_attr_pa_tnc_error_create_with_offset(error_code,
					msg_info, PA_TNC_ATTR_HEADER_SIZE + attr_offset);
		return FALSE;
	}
	this->need_more = (status == NEED_MORE);

	return TRUE;
}

METHOD(seg_env_t, destroy, void,
	private_seg_env_t *this)
{
	DESTROY_IF(this->base_attr);
	free(this);
}

/**
 * See header
 */
seg_env_t *seg_env_create(uint32_t base_msg_id, pa_tnc_attr_t *base_attr,
						  uint32_t max_seg_size)
{
	private_seg_env_t *this;
	chunk_t value;

	base_attr->build(base_attr);
	value = base_attr->get_value(base_attr);

	/**
	 * The PA-TNC attribute header must not be segmented and
	 * there must be at least a first and one next segment
	 */
	if (max_seg_size <  PA_TNC_ATTR_HEADER_SIZE ||
		max_seg_size >= PA_TNC_ATTR_HEADER_SIZE + value.len)
	{
		base_attr->destroy(base_attr);
		return NULL;
	}

	INIT(this,
		.public = {
			.get_base_msg_id = _get_base_msg_id,
			.get_base_attr = _get_base_attr,
			.get_base_attr_info = _get_base_attr_info,
			.first_segment = _first_segment,
			.next_segment = _next_segment,
			.add_segment = _add_segment,
			.destroy = _destroy,
		},
		.base_msg_id = base_msg_id,
		.base_attr = base_attr,
		.max_seg_size = max_seg_size,
		.data = base_attr->get_value(base_attr),
	);

	return &this->public;
}

/**
 * See header
 */
seg_env_t *seg_env_create_from_data(uint32_t base_msg_id, chunk_t data,
									uint32_t max_seg_size, pa_tnc_attr_t** error)
{
	private_seg_env_t *this;
	pen_type_t type, error_code;
	bio_reader_t *reader;
	chunk_t msg_info;
	uint32_t offset = 0, attr_offset;
	status_t status;

	INIT(this,
		.public = {
			.get_base_msg_id = _get_base_msg_id,
			.get_base_attr = _get_base_attr,
			.get_base_attr_info = _get_base_attr_info,
			.first_segment = _first_segment,
			.next_segment = _next_segment,
			.add_segment = _add_segment,
			.destroy = _destroy,
		},
		.base_msg_id = base_msg_id,
		.max_seg_size = max_seg_size,
	);

	/* create info field to be used by PA-TNC error messages */
	memset(this->base_attr_info, 0xff, 4);
	htoun32(this->base_attr_info + 4, base_msg_id);
	msg_info = get_base_attr_info(this);

	/* extract from base attribute segment from data */
	reader = bio_reader_create(data);
	this->base_attr = imcv_pa_tnc_attributes->create(imcv_pa_tnc_attributes,
									 reader, TRUE, &offset, msg_info, error);
	reader->destroy(reader);

	if (!this->base_attr)
	{
		destroy(this);
		return NULL;
	}
	status = this->base_attr->process(this->base_attr, &attr_offset);

	if (status != SUCCESS && status != NEED_MORE)
	{
		type = this->base_attr->get_type(this->base_attr);
		if (!(type.vendor_id == PEN_IETF &&
			  type.type == IETF_ATTR_PA_TNC_ERROR))
		{
			error_code = pen_type_create(PEN_IETF, PA_ERROR_INVALID_PARAMETER);
			*error = ietf_attr_pa_tnc_error_create_with_offset(error_code,
						msg_info, PA_TNC_ATTR_HEADER_SIZE + attr_offset);
		}
		destroy(this);
		return NULL;
	}
	this->need_more = (status == NEED_MORE);

	return &this->public;
}

