/* zlib.c: routines for zlib (de)compression of data
   Copyright (c) 2002 Darren Salt, Philip Kendall

   $Id: zlib.c,v 1.9 2003/11/27 21:45:25 pak21 Exp $

   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.

   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.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

   Author contact information:

   Darren: E-mail: linux@youmustbejoking.demon.co.uk

   Philip: E-mail: pak21-fuse@srcf.ucam.org
             Post: 15 Crescent Road, Wokingham, Berks, RG40 2DB, England

*/

#include <config.h>

#ifdef HAVE_ZLIB_H

#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
//#include <unistd.h>

#include <zlib.h>

#include "internals.h"


static libspectrum_error
skip_gzip_header( const libspectrum_byte **gzptr, size_t *gzlength );
static libspectrum_error
skip_null_terminated_string( const libspectrum_byte **ptr, size_t *length,
			     const char *name );
static libspectrum_error
zlib_inflate( const libspectrum_byte *gzptr, size_t gzlength,
	      libspectrum_byte **outptr, size_t *outlength, int gzip_hack );

libspectrum_error 
libspectrum_zlib_inflate( const libspectrum_byte *gzptr, size_t gzlength,
			  libspectrum_byte **outptr, size_t *outlength )
/* Inflates a block of data.
 * Input:	gzptr		-> source (deflated) data
 *		*gzlength	== source data length
 * Output:	*outptr		-> inflated data (malloced in this fn)
 *		*outlength	== length of the inflated data
 * Returns:	error flag (libspectrum_error)
 */
{
  return zlib_inflate( gzptr, gzlength, outptr, outlength, 0 );
}

libspectrum_error
libspectrum_gzip_inflate( const libspectrum_byte *gzptr, size_t gzlength,
			  libspectrum_byte **outptr, size_t *outlength )
{
  int error;

  error = skip_gzip_header( &gzptr, &gzlength ); if( error ) return error;

  return zlib_inflate( gzptr, gzlength, outptr, outlength, 1 );
}

static libspectrum_error
zlib_inflate( const libspectrum_byte *gzptr, size_t gzlength,
	      libspectrum_byte **outptr, size_t *outlength, int gzip_hack )
{
  z_stream stream;
  int error;

  /* Use default memory management */
  stream.zalloc = Z_NULL; stream.zfree = Z_NULL; stream.opaque = Z_NULL;

  /* Cast needed to avoid warning about losing const */
  stream.next_in = (libspectrum_byte*)gzptr; stream.avail_in = gzlength;

  if( gzip_hack ) { 

    /*
     * HACK ALERT (comment from zlib 1.1.14:gzio.c:143)
     *
     * windowBits is passed < 0 to tell that there is no zlib header.
     * Note that in this case inflate *requires* an extra "dummy" byte
     * after the compressed stream in order to complete decompression
     * and return Z_STREAM_END. Here the gzip CRC32 ensures that 4 bytes
     * are present after the compressed stream.
     *
     */
    error = inflateInit2( &stream, -15 );

  } else {

    error = inflateInit( &stream );

  }

  switch( error ) {

  case Z_OK: break;

  case Z_MEM_ERROR: 
    libspectrum_print_error( LIBSPECTRUM_ERROR_MEMORY,
			     "out of memory at %s:%d", __FILE__, __LINE__ );
    inflateEnd( &stream );
    return LIBSPECTRUM_ERROR_MEMORY;

  default:
    libspectrum_print_error( LIBSPECTRUM_ERROR_LOGIC,
			     "error from inflateInit2: %s", stream.msg );
    inflateEnd( &stream );
    return LIBSPECTRUM_ERROR_MEMORY;

  }

  if( *outlength ) {

    *outptr = malloc( *outlength );
    if( !( *outptr ) ) {
      libspectrum_print_error( LIBSPECTRUM_ERROR_MEMORY,
			       "out of memory at %s:%d", __FILE__, __LINE__ );
      inflateEnd( &stream );
      return LIBSPECTRUM_ERROR_MEMORY;
    }

    stream.next_out = *outptr; stream.avail_out = *outlength;

    error = inflate( &stream, Z_FINISH );

  } else {

    *outptr = stream.next_out = NULL;
    *outlength = stream.avail_out = 0;

    do {

      libspectrum_byte *ptr;

      *outlength += 16384; stream.avail_out += 16384;
      ptr = realloc( *outptr, *outlength );
      if( !ptr ) {
	libspectrum_print_error( LIBSPECTRUM_ERROR_MEMORY,
				 "out of memory at %s:%d",
				 __FILE__, __LINE__ );
	inflateEnd( &stream );
	free( *outptr );
	return LIBSPECTRUM_ERROR_MEMORY;
      }
      
      stream.next_out = ptr + ( stream.next_out - *outptr );
      *outptr = ptr;

      error = inflate( &stream, 0 );

    } while( error == Z_OK );

  }

  *outlength = stream.next_out - *outptr;
  *outptr = realloc( *outptr, *outlength );

  switch( error ) {

  case Z_STREAM_END: break;

  case Z_NEED_DICT:
    libspectrum_print_error( LIBSPECTRUM_ERROR_UNKNOWN,
			     "gzip inflation needs dictionary" );
    free( *outptr );
    inflateEnd( &stream );
    return LIBSPECTRUM_ERROR_UNKNOWN;

  case Z_DATA_ERROR:
    libspectrum_print_error( LIBSPECTRUM_ERROR_CORRUPT, "corrupt gzip data" );
    free( *outptr );
    inflateEnd( &stream );
    return LIBSPECTRUM_ERROR_CORRUPT;

  case Z_MEM_ERROR:
    libspectrum_print_error( LIBSPECTRUM_ERROR_MEMORY,
			     "out of memory at %s:%d", __FILE__, __LINE__ );
    free( *outptr );
    inflateEnd( &stream );
    return LIBSPECTRUM_ERROR_MEMORY;

  case Z_BUF_ERROR:
    libspectrum_print_error( LIBSPECTRUM_ERROR_CORRUPT,
			     "not enough space in gzip output buffer" );
    free( *outptr );
    inflateEnd( &stream );
    return LIBSPECTRUM_ERROR_CORRUPT;

  default:
    libspectrum_print_error( LIBSPECTRUM_ERROR_LOGIC,
			     "gzip error from inflate: %s",
			     stream.msg );
    free( *outptr );
    inflateEnd( &stream );
    return LIBSPECTRUM_ERROR_LOGIC;

  }

  error = inflateEnd( &stream );
  if( error != Z_OK ) {
    libspectrum_print_error( LIBSPECTRUM_ERROR_LOGIC,
			     "gzip error from inflateEnd: %s", stream.msg );
    free( *outptr );
    inflateEnd( &stream );
    return LIBSPECTRUM_ERROR_LOGIC;
  }

  return LIBSPECTRUM_ERROR_NONE;
}

static libspectrum_error
skip_gzip_header( const libspectrum_byte **gzptr, size_t *gzlength )
{
  libspectrum_byte flags;
  libspectrum_error error;

  if( *gzlength < 10 ) {
    libspectrum_print_error( LIBSPECTRUM_ERROR_CORRUPT,
			     "not enough data for gzip header" );
    return LIBSPECTRUM_ERROR_CORRUPT;
  }

  if( (*gzptr)[0] != 0x1f || (*gzptr)[1] != 0x8b ) {
    libspectrum_print_error( LIBSPECTRUM_ERROR_CORRUPT,
			     "gzip header missing" );
    return LIBSPECTRUM_ERROR_CORRUPT;
  }

  if( (*gzptr)[2] != 8 ) {
    libspectrum_print_error( LIBSPECTRUM_ERROR_UNKNOWN,
			     "unknown gzip compression method %d",
			     (*gzptr)[2] );
    return LIBSPECTRUM_ERROR_UNKNOWN;
  }

  flags = (*gzptr)[3];

  (*gzptr) += 10; (*gzlength) -= 10;

  if( flags & 0x04 ) {		/* extra header present */

    size_t length;

    if( *gzlength < 2 ) {
      libspectrum_print_error(
        LIBSPECTRUM_ERROR_CORRUPT,
	"not enough data for gzip extra header length"
      );
      return LIBSPECTRUM_ERROR_CORRUPT;
    }

    length = (*gzptr)[0] + (*gzptr)[1] * 0x100;
    (*gzptr) += 2; (*gzlength) -= 2;

    if( *gzlength < length ) {
      libspectrum_print_error( LIBSPECTRUM_ERROR_CORRUPT,
			       "not enough data for gzip extra header" );
      return LIBSPECTRUM_ERROR_CORRUPT;
    }

  }

  if( flags & 0x08 ) {		/* original file name present */
    error = skip_null_terminated_string( gzptr, gzlength, "original name" );
    if( error ) return error;
  }

  if( flags & 0x10 ) {		/* comment present */
    error = skip_null_terminated_string( gzptr, gzlength, "comment" );
    if( error ) return error;
  }

  if( flags & 0x02 ) {		/* header CRC present */

    if( *gzlength < 2 ) {
      libspectrum_print_error( LIBSPECTRUM_ERROR_CORRUPT,
			       "not enough data for gzip header CRC" );
      return LIBSPECTRUM_ERROR_CORRUPT;
    }

    /* Could check the header CRC if we really wanted to */
    (*gzptr) += 2; (*gzptr) -= 2;
  }

  return LIBSPECTRUM_ERROR_NONE;
}

static libspectrum_error
skip_null_terminated_string( const libspectrum_byte **ptr, size_t *length,
			     const char *name )
{
  while( **ptr && *length ) { (*ptr)++; (*length)--; }

  if( !( *length ) ) {
    libspectrum_print_error( LIBSPECTRUM_ERROR_CORRUPT,
			     "not enough data for gzip %s", name );
    return LIBSPECTRUM_ERROR_CORRUPT;
  }

  /* Skip the null as well */
  (*ptr)++; (*length)--;

  return LIBSPECTRUM_ERROR_NONE;
}

libspectrum_error
libspectrum_zlib_compress( const libspectrum_byte *data, size_t length,
			   libspectrum_byte **gzptr, size_t *gzlength )
/* Deflates a block of data.
 * Input:	data		-> source data
 *		length		== source data length
 * Output:	*gzptr		-> deflated data (malloced in this fn),
 *		*gzlength	== length of the deflated data
 * Returns:	error flag (libspectrum_error)
 */
{
  uLongf gzl = ( length * 1.001 ) + 12;
  int gzret;

  *gzptr = malloc( gzl );
  if( !*gzptr ) {
    libspectrum_print_error( LIBSPECTRUM_ERROR_MEMORY,
			     "libspectrum_zlib_compress: out of memory" );
    return LIBSPECTRUM_ERROR_MEMORY;
  }

  gzret = compress2( *gzptr, &gzl, data, length, Z_BEST_COMPRESSION );

  switch (gzret) {

  case Z_OK:			/* initialised OK */
    *gzlength = gzl;
    return LIBSPECTRUM_ERROR_NONE;

  case Z_MEM_ERROR:		/* out of memory */
    free( *gzptr ); *gzptr = 0;
    libspectrum_print_error( LIBSPECTRUM_ERROR_MEMORY,
			     "libspectrum_zlib_compress: out of memory" );
    return LIBSPECTRUM_ERROR_MEMORY;

  case Z_VERSION_ERROR:		/* unrecognised version */
    free( *gzptr ); *gzptr = 0;
    libspectrum_print_error( LIBSPECTRUM_ERROR_UNKNOWN,
			     "libspectrum_zlib_compress: unknown version" );
    return LIBSPECTRUM_ERROR_UNKNOWN;

  case Z_BUF_ERROR:		/* Not enough space in output buffer.
				   Shouldn't happen */
    free( *gzptr ); *gzptr = 0;
    libspectrum_print_error( LIBSPECTRUM_ERROR_LOGIC,
			     "libspectrum_zlib_compress: out of space?" ); 
    return LIBSPECTRUM_ERROR_LOGIC;

  default:			/* some other error */
    free( *gzptr ); *gzptr = 0;
    libspectrum_print_error( LIBSPECTRUM_ERROR_LOGIC,
			     "libspectrum_zlib_compress: unexpected error?" ); 
    return LIBSPECTRUM_ERROR_LOGIC;
  }
}

#endif				/* #ifdef HAVE_ZLIB_H */
