/*
 * Copyright (C) 2000-2012 Free Software Foundation, Inc.
 *
 * Author: Nikos Mavrogiannopoulos
 *
 * 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 <http://www.gnu.org/licenses/>
 *
 */

/* This file contains the functions which convert the TLS plaintext
 * packet to TLS compressed packet.
 */

#include "gnutls_int.h"
#include "gnutls_compress.h"
#include "gnutls_errors.h"
#include "gnutls_constate.h"
#include <algorithms.h>
#include <gnutls/gnutls.h>

/* Compression Section */
#define GNUTLS_COMPRESSION_ENTRY(name, id, wb, ml, cl)	\
  { #name, name, id, wb, ml, cl}


#define MAX_COMP_METHODS 5
const int _gnutls_comp_algorithms_size = MAX_COMP_METHODS;

gnutls_compression_entry _gnutls_compression_algorithms[MAX_COMP_METHODS] = {
	GNUTLS_COMPRESSION_ENTRY(GNUTLS_COMP_NULL, 0x00, 0, 0, 0),
#ifdef HAVE_LIBZ
	/* draft-ietf-tls-compression-02 */
	GNUTLS_COMPRESSION_ENTRY(GNUTLS_COMP_DEFLATE, 0x01, 15, 8, 3),
#endif
	{0, 0, 0, 0, 0, 0}
};

static const gnutls_compression_method_t supported_compressions[] = {
#ifdef HAVE_LIBZ
	GNUTLS_COMP_DEFLATE,
#endif
	GNUTLS_COMP_NULL,
	0
};

#define GNUTLS_COMPRESSION_LOOP(b)	   \
  const gnutls_compression_entry *p;					\
  for(p = _gnutls_compression_algorithms; p->name != NULL; p++) { b ; }
#define GNUTLS_COMPRESSION_ALG_LOOP(a)					\
  GNUTLS_COMPRESSION_LOOP( if(p->id == algorithm) { a; break; } )
#define GNUTLS_COMPRESSION_ALG_LOOP_NUM(a)				\
  GNUTLS_COMPRESSION_LOOP( if(p->num == num) { a; break; } )

/* Compression Functions */

/**
 * gnutls_compression_get_name:
 * @algorithm: is a Compression algorithm
 *
 * Convert a #gnutls_compression_method_t value to a string.
 *
 * Returns: a pointer to a string that contains the name of the
 *   specified compression algorithm, or %NULL.
 **/
const char *gnutls_compression_get_name(gnutls_compression_method_t
					algorithm)
{
	const char *ret = NULL;

	/* avoid prefix */
	GNUTLS_COMPRESSION_ALG_LOOP(ret =
				    p->name + sizeof("GNUTLS_COMP_") - 1);

	return ret;
}

/**
 * gnutls_compression_get_id:
 * @name: is a compression method name
 *
 * The names are compared in a case insensitive way.
 *
 * Returns: an id of the specified in a string compression method, or
 *   %GNUTLS_COMP_UNKNOWN on error.
 **/
gnutls_compression_method_t gnutls_compression_get_id(const char *name)
{
	gnutls_compression_method_t ret = GNUTLS_COMP_UNKNOWN;

	GNUTLS_COMPRESSION_LOOP(if
				(strcasecmp
				 (p->name + sizeof("GNUTLS_COMP_") - 1,
				  name) == 0) ret = p->id);

	return ret;
}

/**
 * gnutls_compression_list:
 *
 * Get a list of compression methods.  
 *
 * Returns: a zero-terminated list of #gnutls_compression_method_t
 *   integers indicating the available compression methods.
 **/
const gnutls_compression_method_t *gnutls_compression_list(void)
{
	return supported_compressions;
}

/* return the tls number of the specified algorithm */
int _gnutls_compression_get_num(gnutls_compression_method_t algorithm)
{
	int ret = -1;

	/* avoid prefix */
	GNUTLS_COMPRESSION_ALG_LOOP(ret = p->num);

	return ret;
}

#ifdef HAVE_LIBZ

static int get_wbits(gnutls_compression_method_t algorithm)
{
	int ret = -1;
	/* avoid prefix */
	GNUTLS_COMPRESSION_ALG_LOOP(ret = p->window_bits);
	return ret;
}

static int get_mem_level(gnutls_compression_method_t algorithm)
{
	int ret = -1;
	/* avoid prefix */
	GNUTLS_COMPRESSION_ALG_LOOP(ret = p->mem_level);
	return ret;
}

static int get_comp_level(gnutls_compression_method_t algorithm)
{
	int ret = -1;
	/* avoid prefix */
	GNUTLS_COMPRESSION_ALG_LOOP(ret = p->comp_level);
	return ret;
}

#endif

/* returns the gnutls internal ID of the TLS compression
 * method num
 */
gnutls_compression_method_t _gnutls_compression_get_id(int num)
{
	gnutls_compression_method_t ret = -1;

	/* avoid prefix */
	GNUTLS_COMPRESSION_ALG_LOOP_NUM(ret = p->id);

	return ret;
}

int _gnutls_compression_is_ok(gnutls_compression_method_t algorithm)
{
	ssize_t ret = -1;
	GNUTLS_COMPRESSION_ALG_LOOP(ret = p->id);
	if (ret >= 0)
		ret = 0;
	else
		ret = 1;
	return ret;
}



/* For compression  */

#define MIN_PRIVATE_COMP_ALGO 0xEF

/* returns the TLS numbers of the compression methods we support
 */
#define SUPPORTED_COMPRESSION_METHODS session->internals.priorities.compression.algorithms
int
_gnutls_supported_compression_methods(gnutls_session_t session,
				      uint8_t * comp, size_t comp_size)
{
	unsigned int i, j;
	int tmp;

	if (comp_size < SUPPORTED_COMPRESSION_METHODS)
		return gnutls_assert_val(GNUTLS_E_INTERNAL_ERROR);

	for (i = j = 0; i < SUPPORTED_COMPRESSION_METHODS; i++) {
		if (IS_DTLS(session) && session->internals.priorities.compression.priority[i] != GNUTLS_COMP_NULL) {
		        gnutls_assert();
		        continue;
                }

		tmp =
		    _gnutls_compression_get_num(session->
						internals.priorities.
						compression.priority[i]);

		/* remove private compression algorithms, if requested.
		 */
		if (tmp == -1 || (tmp >= MIN_PRIVATE_COMP_ALGO &&
				  session->internals.enable_private == 0))
		{
			gnutls_assert();
			continue;
		}

		comp[j] = (uint8_t) tmp;
		j++;
	}

	if (j == 0) {
		gnutls_assert();
		return GNUTLS_E_NO_COMPRESSION_ALGORITHMS;
	}
	return j;
}


/* The flag d is the direction (compress, decompress). Non zero is
 * decompress.
 */
int _gnutls_comp_init(comp_hd_st * handle,
		      gnutls_compression_method_t method, int d)
{
	handle->algo = method;
	handle->handle = NULL;

	switch (method) {
	case GNUTLS_COMP_DEFLATE:
#ifdef HAVE_LIBZ
		{
			int window_bits, mem_level;
			int comp_level;
			z_stream *zhandle;
			int err;

			window_bits = get_wbits(method);
			mem_level = get_mem_level(method);
			comp_level = get_comp_level(method);

			handle->handle = gnutls_malloc(sizeof(z_stream));
			if (handle->handle == NULL)
				return
				    gnutls_assert_val
				    (GNUTLS_E_MEMORY_ERROR);

			zhandle = handle->handle;

			zhandle->zalloc = (alloc_func) 0;
			zhandle->zfree = (free_func) 0;
			zhandle->opaque = (voidpf) 0;

			if (d)
				err = inflateInit2(zhandle, window_bits);
			else {
				err = deflateInit2(zhandle,
						   comp_level, Z_DEFLATED,
						   window_bits, mem_level,
						   Z_DEFAULT_STRATEGY);
			}
			if (err != Z_OK) {
				gnutls_assert();
				gnutls_free(handle->handle);
				return GNUTLS_E_COMPRESSION_FAILED;
			}
		}
		break;
#endif
	case GNUTLS_COMP_NULL:
	case GNUTLS_COMP_UNKNOWN:
		break;
	default:
		return GNUTLS_E_UNKNOWN_COMPRESSION_ALGORITHM;
	}

	return 0;
}

/* The flag d is the direction (compress, decompress). Non zero is
 * decompress.
 */
void _gnutls_comp_deinit(comp_hd_st * handle, int d)
{
	if (handle != NULL) {
		switch (handle->algo) {
#ifdef HAVE_LIBZ
		case GNUTLS_COMP_DEFLATE:
			{
				if (d)
					inflateEnd(handle->handle);
				else
					deflateEnd(handle->handle);
				break;
			}
#endif
		default:
			break;
		}
		gnutls_free(handle->handle);
		handle->handle = NULL;
	}
}

/* These functions are memory consuming 
 */

int
_gnutls_compress(comp_hd_st * handle, const uint8_t * plain,
		 size_t plain_size, uint8_t * compressed,
		 size_t max_comp_size, unsigned int stateless)
{
	int compressed_size = GNUTLS_E_COMPRESSION_FAILED;

	/* NULL compression is not handled here
	 */
	if (handle == NULL) {
		gnutls_assert();
		return GNUTLS_E_INTERNAL_ERROR;
	}

	switch (handle->algo) {
#ifdef HAVE_LIBZ
	case GNUTLS_COMP_DEFLATE:
		{
			z_stream *zhandle;
			int err;
			int type;

			if (stateless) {
				type = Z_FULL_FLUSH;
			} else
				type = Z_SYNC_FLUSH;

			zhandle = handle->handle;

			zhandle->next_in = (Bytef *) plain;
			zhandle->avail_in = plain_size;
			zhandle->next_out = (Bytef *) compressed;
			zhandle->avail_out = max_comp_size;

			err = deflate(zhandle, type);
			if (err != Z_OK || zhandle->avail_in != 0)
				return
				    gnutls_assert_val
				    (GNUTLS_E_COMPRESSION_FAILED);


			compressed_size =
			    max_comp_size - zhandle->avail_out;
			break;
		}
#endif
	default:
		gnutls_assert();
		return GNUTLS_E_INTERNAL_ERROR;
	}			/* switch */

#ifdef COMPRESSION_DEBUG
	_gnutls_debug_log("Compression ratio: %f\n",
			  (float) ((float) compressed_size /
				   (float) plain_size));
#endif

	return compressed_size;
}



int
_gnutls_decompress(comp_hd_st * handle, uint8_t * compressed,
		   size_t compressed_size, uint8_t * plain,
		   size_t max_plain_size)
{
	int plain_size = GNUTLS_E_DECOMPRESSION_FAILED;

	if (compressed_size > max_plain_size + EXTRA_COMP_SIZE) {
		gnutls_assert();
		return GNUTLS_E_DECOMPRESSION_FAILED;
	}

	/* NULL compression is not handled here
	 */

	if (handle == NULL) {
		gnutls_assert();
		return GNUTLS_E_INTERNAL_ERROR;
	}

	switch (handle->algo) {
#ifdef HAVE_LIBZ
	case GNUTLS_COMP_DEFLATE:
		{
			z_stream *zhandle;
			int err;

			zhandle = handle->handle;

			zhandle->next_in = (Bytef *) compressed;
			zhandle->avail_in = compressed_size;

			zhandle->next_out = (Bytef *) plain;
			zhandle->avail_out = max_plain_size;
			err = inflate(zhandle, Z_SYNC_FLUSH);

			if (err != Z_OK)
				return
				    gnutls_assert_val
				    (GNUTLS_E_DECOMPRESSION_FAILED);

			plain_size = max_plain_size - zhandle->avail_out;
			break;
		}
#endif
	default:
		gnutls_assert();
		return GNUTLS_E_INTERNAL_ERROR;
	}			/* switch */

	return plain_size;
}
