/* Xlunzip - Test tool for the lzip_decompress linux module
   Copyright (C) 2016-2024 Antonio Diaz Diaz.

   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, see <http://www.gnu.org/licenses/>.
*/

#define _FILE_OFFSET_BITS 64

#include <errno.h>
#include <limits.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "linux_lunzip.h"
#include "lzip.h"


/* Return the number of bytes really read.
   If (value returned < size) and (errno == 0), means EOF was reached.
*/
static long readblock( const int fd, uint8_t * const buf, const long size )
  {
  long sz = 0;
  errno = 0;
  while( sz < size )
    {
    const int n = read( fd, buf + sz, min( 1L << 20, size - sz ) );
    if( n > 0 ) sz += n;
    else if( n == 0 ) break;				/* EOF */
    else if( errno != EINTR ) break;
    errno = 0;
    }
  return sz;
  }


/* Return the address of a malloc'd buffer containing the file data and
   the buffer and file sizes in '*buffer_sizep' and '*file_sizep'.
   In case of error, return 0 and do not modify '*sizep'.
*/
static uint8_t * read_file( const int infd, long * const buffer_sizep,
                      long * const file_sizep, const char * const filename )
  {
  long buffer_size = 1 << 20;
  uint8_t * buffer = (uint8_t *)malloc( buffer_size );
  if( !buffer ) { show_file_error( filename, mem_msg, 0 ); return 0; }

  long file_size = readblock( infd, buffer, buffer_size );
  while( file_size >= buffer_size && !errno )
    {
    if( buffer_size >= LONG_MAX )
      { show_file_error( filename, "Input file is too large.", 0 );
        free( buffer ); return 0; }
    buffer_size = ( buffer_size <= LONG_MAX / 2 ) ? 2 * buffer_size : LONG_MAX;
    uint8_t * const tmp = (uint8_t *)realloc( buffer, buffer_size );
    if( !tmp )
      { show_file_error( filename, mem_msg, 0 ); free( buffer ); return 0; }
    buffer = tmp;
    file_size += readblock( infd, buffer + file_size, buffer_size - file_size );
    }
  if( errno )
    { show_file_error( filename, "Error reading file", errno ); free( buffer );
      return 0; }
  *buffer_sizep = buffer_size;
  *file_sizep = file_size;
  return buffer;
  }


struct File_sizes
  {
  unsigned long long csize;		/* compressed size */
  unsigned long long dsize;		/* decompressed size */
  unsigned long tsize;			/* trailing data size */
  long members;
  };

static const char * set_file_sizes( struct File_sizes * const file_sizes,
                                    const uint8_t * const buffer,
                                    const unsigned long file_size )
  {
  if( file_size <= Lh_size ) return "File ends unexpectedly at member header.";
  if( file_size < min_member_size ) return "Input file is too short.";
  const Lzip_header * header = (const Lzip_header *)buffer;
  if( !Lh_check_magic( *header ) )
    return "Bad magic number (file not in lzip format).";
  if( !Lh_check_version( *header ) )
    return "Version of lzip member format not supported.";

  file_sizes->csize = file_sizes->dsize = file_sizes->tsize = 0;
  file_sizes->members = 0;
  unsigned long pos = file_size;	/* always points to a header or to EOF */
  while( pos >= min_member_size )
    {
    const Lzip_trailer * const trailer =
      (const Lzip_trailer *)( buffer + pos - Lt_size );
    const unsigned long long member_size = Lt_get_member_size( *trailer );
    if( member_size < min_member_size || member_size > pos )
      {
      if( file_sizes->csize == 0 )		/* maybe trailing data */
        {
        if( member_size == 0 )			/* skip trailing zeros */
          while( pos > Lt_size && buffer[pos-8] == 0 ) --pos;
        else --pos;
        continue;
        }
      return "Member size in trailer is corrupt.";
      }
    header = (const Lzip_header *)( buffer + pos - member_size );
    if( !Lh_check_magic( *header ) || !Lh_check_version( *header ) )
      {
      if( file_sizes->csize == 0 ) { --pos; continue; }	/* maybe trailing data */
      return "Bad member header inside file.";
      }
    if( file_sizes->csize == 0 && file_size - pos > 0 )
      {
      file_sizes->tsize = file_size - pos;
      header = (const Lzip_header *)( buffer + pos );
      if( file_size - pos > Lh_size &&
          Lh_check_magic( *header ) && Lh_check_version( *header ) )
        return "Last member in input file is truncated or corrupt.";
      }
    pos -= member_size;
    file_sizes->csize += member_size;
    file_sizes->dsize += Lt_get_data_size( *trailer );
    ++file_sizes->members;
    }
  if( pos != 0 || file_sizes->csize == 0 ) return "Can't get file sizes.";
  if( file_sizes->csize + file_sizes->tsize != file_size )
    return "Error getting file sizes.";
  if( file_sizes->csize > LONG_MAX ) return "File is larger than LONG_MAX.";
  if( file_sizes->dsize > LONG_MAX ) return "Data is larger than LONG_MAX.";
  return 0;
  }


static const char * global_name;	/* copy of filename for 'error' */
static void error(char *x) { show_file_error( global_name, x, 0 ); }


/*
 * Load the compressed file at the end of the buffer used to hold the
 * decompressed data. Check that the in-place decompression does not
 * overwrite the compressed data. The buffer must be large enough to contain
 * after the decompressed data extra space for a marker, a trailer, the
 * maximum possible data expansion, and (if multimember) N-1 empty members.
 *
 *                  |------ compressed data ------|
 *                  V                             V
 * |----------------|-------------------|---------|
 * ^                                    ^  extra
 * |-------- decompressed data ---------|
 */

int decompress_in_place( const int infd, struct Pretty_print * const pp,
                         const bool testing )
  {
  long buffer_size = 0, file_size = 0;
  uint8_t * buffer = read_file( infd, &buffer_size, &file_size, pp->name );
  if( !buffer ) return 1;
  struct File_sizes file_sizes;
  const char * emsg = set_file_sizes( &file_sizes, buffer, file_size );
  if( emsg ) { show_file_error( pp->name, emsg, 0 ); return 2; }

  const long csize = file_sizes.csize;
  const long dsize = file_sizes.dsize;
/*  const long extra_bytes = ( dsize >> 8 ) + 65536; wrong linux formula */
  const long extra_bytes = ( csize >> 6 ) + file_sizes.members * min_member_size;
  const long long target_buffer_size = max( dsize, csize ) + extra_bytes;
  if( target_buffer_size > LONG_MAX )
    { show_file_error( pp->name, "Buffer is larger than LONG_MAX.", 0 );
      return 1; }
  if( buffer_size < target_buffer_size )  /* avoid realloc if big enough */
    {
    buffer = (uint8_t *)realloc( buffer, target_buffer_size );
    if( !buffer ) { show_file_error( pp->name, mem_msg, 0 ); return 1; }
    }
  buffer_size = target_buffer_size;
  const long cbegin = buffer_size - csize;	/* overwrite trailing data */
  if( cbegin > 0 ) memmove( buffer + cbegin, buffer, csize );

  long in_pos, out_pos;
  int retval;
  global_name = pp->name;
  retval = convert_retval( __lunzip( buffer + cbegin, csize, 0, 0, buffer,
                                     buffer_size, &in_pos, &out_pos, error ) );
  if( retval == 0 && !testing )
    {
    const long len = flush( buffer, out_pos );
    if( len < out_pos )
      { show_file_error( pp->name, "Write error", errno ); return 1; }
    }
  free( buffer );
  if( retval ) return retval;
  show_results( pp, in_pos, out_pos, testing );
  return 0;
  }