/* Lunzip - Decompressor for the lzip format
Copyright (C) 2010-2016 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 .
*/
/*
Exit status: 0 for a normal exit, 1 for environmental problems
(file not found, invalid flags, I/O errors, etc), 2 to indicate a
corrupt or invalid input file, 3 for an internal consistency error
(eg, bug) which caused lunzip to panic.
*/
#define _FILE_OFFSET_BITS 64
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#if defined(__MSVCRT__)
#include
#define fchmod(x,y) 0
#define fchown(x,y,z) 0
#define SIGHUP SIGTERM
#define S_ISSOCK(x) 0
#define S_IRGRP 0
#define S_IWGRP 0
#define S_IROTH 0
#define S_IWOTH 0
#endif
#if defined(__OS2__)
#include
#endif
#include "carg_parser.h"
#include "lzip.h"
#include "decoder.h"
#ifndef O_BINARY
#define O_BINARY 0
#endif
#if CHAR_BIT != 8
#error "Environments where CHAR_BIT != 8 are not supported."
#endif
int verbosity = 0;
const char * const Program_name = "Lunzip";
const char * const program_name = "lunzip";
const char * const program_year = "2016";
const char * invocation_name = 0;
struct { const char * from; const char * to; } const known_extensions[] = {
{ ".lz", "" },
{ ".tlz", ".tar" },
{ 0, 0 } };
char * output_filename = 0;
int outfd = -1;
bool delete_output_on_interrupt = false;
static void show_help( void )
{
printf( "Lunzip is a decompressor for the lzip format. It is written in C and its\n"
"small size makes it well suited for embedded devices or software\n"
"installers that need to decompress files but don't need compression\n"
"capabilities. Lunzip is fully compatible with lzip-1.4 or newer.\n"
"\nLunzip provides a 'low memory' mode able to decompress any file using as\n"
"little memory as 50 kB, irrespective of the dictionary size used to\n"
"compress the file. To activate it, specify the size of the output buffer\n"
"with the '--buffer-size' option and lunzip will use the decompressed\n"
"file as dictionary for distances beyond the buffer size. Of course, the\n"
"smaller the buffer size used in relation to the dictionary size, the\n"
"more accesses to disk are needed and the slower the decompression is.\n"
"This 'low memory' mode only works when decompressing to a regular file\n"
"and is intended for systems without enough memory (RAM + swap) to keep\n"
"the whole dictionary at once.\n"
"\nUsage: %s [options] [files]\n", invocation_name );
printf( "\nOptions:\n"
" -h, --help display this help and exit\n"
" -V, --version output version information and exit\n"
" -a, --trailing-error exit with error status if trailing data\n"
" -c, --stdout write to standard output, keep input files\n"
" -d, --decompress decompress (this is the default)\n"
" -f, --force overwrite existing output files\n"
" -k, --keep keep (don't delete) input files\n"
" -o, --output= if reading standard input, write to \n"
" -q, --quiet suppress all messages\n"
" -t, --test test compressed file integrity\n"
" -u, --buffer-size= set output buffer size in bytes\n"
" -v, --verbose be verbose (a 2nd -v gives more)\n"
"If no file names are given, or if a file is '-', lunzip decompresses\n"
"from standard input to standard output.\n"
"Numbers may be followed by a multiplier: k = kB = 10^3 = 1000,\n"
"Ki = KiB = 2^10 = 1024, M = 10^6, Mi = 2^20, G = 10^9, Gi = 2^30, etc...\n"
"\nExit status: 0 for a normal exit, 1 for environmental problems (file\n"
"not found, invalid flags, I/O errors, etc), 2 to indicate a corrupt or\n"
"invalid input file, 3 for an internal consistency error (eg, bug) which\n"
"caused lunzip to panic.\n"
"\nReport bugs to lzip-bug@nongnu.org\n"
"Lunzip home page: http://www.nongnu.org/lzip/lunzip.html\n" );
}
static void show_version( void )
{
printf( "%s %s\n", program_name, PROGVERSION );
printf( "Copyright (C) %s Antonio Diaz Diaz.\n", program_year );
printf( "License GPLv2+: GNU GPL version 2 or later \n"
"This is free software: you are free to change and redistribute it.\n"
"There is NO WARRANTY, to the extent permitted by law.\n" );
}
static void show_header( const unsigned dictionary_size )
{
if( verbosity >= 3 )
{
const char * const prefix[8] =
{ "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi", "Yi" };
enum { factor = 1024 };
const char * p = "";
const char * np = " ";
unsigned num = dictionary_size, i;
bool exact = ( num % factor == 0 );
for( i = 0; i < 8 && ( num > 9999 || ( exact && num >= factor ) ); ++i )
{ num /= factor; if( num % factor != 0 ) exact = false;
p = prefix[i]; np = ""; }
fprintf( stderr, "dictionary size %s%4u %sB. ", np, num, p );
}
}
static unsigned long getnum( const char * const ptr,
const unsigned long llimit,
const unsigned long ulimit )
{
unsigned long result;
char * tail;
errno = 0;
result = strtoul( ptr, &tail, 0 );
if( tail == ptr )
{
show_error( "Bad or missing numerical argument.", 0, true );
exit( 1 );
}
if( !errno && tail[0] )
{
const int factor = ( tail[1] == 'i' ) ? 1024 : 1000;
int exponent = 0; /* 0 = bad multiplier */
int i;
switch( tail[0] )
{
case 'Y': exponent = 8; break;
case 'Z': exponent = 7; break;
case 'E': exponent = 6; break;
case 'P': exponent = 5; break;
case 'T': exponent = 4; break;
case 'G': exponent = 3; break;
case 'M': exponent = 2; break;
case 'K': if( factor == 1024 ) exponent = 1; break;
case 'k': if( factor == 1000 ) exponent = 1; break;
}
if( exponent <= 0 )
{
show_error( "Bad multiplier in numerical argument.", 0, true );
exit( 1 );
}
for( i = 0; i < exponent; ++i )
{
if( ulimit / factor >= result ) result *= factor;
else { errno = ERANGE; break; }
}
}
if( !errno && ( result < llimit || result > ulimit ) ) errno = ERANGE;
if( errno )
{
show_error( "Numerical argument out of limits.", 0, false );
exit( 1 );
}
return result;
}
static int get_dict_size( const char * const arg )
{
char * tail;
const int bits = strtol( arg, &tail, 0 );
if( bits >= min_dictionary_bits &&
bits <= max_dictionary_bits && *tail == 0 )
return ( 1 << bits );
return getnum( arg, min_dictionary_size, max_dictionary_size );
}
static int extension_index( const char * const name )
{
int i;
for( i = 0; known_extensions[i].from; ++i )
{
const char * const ext = known_extensions[i].from;
const unsigned name_len = strlen( name );
const unsigned ext_len = strlen( ext );
if( name_len > ext_len &&
strncmp( name + name_len - ext_len, ext, ext_len ) == 0 )
return i;
}
return -1;
}
static int open_instream( const char * const name, struct stat * const in_statsp,
const bool no_ofile )
{
int infd = open( name, O_RDONLY | O_BINARY );
if( infd < 0 )
{
if( verbosity >= 0 )
fprintf( stderr, "%s: Can't open input file '%s': %s\n",
program_name, name, strerror( errno ) );
}
else
{
const int i = fstat( infd, in_statsp );
const mode_t mode = in_statsp->st_mode;
const bool can_read = ( i == 0 &&
( S_ISBLK( mode ) || S_ISCHR( mode ) ||
S_ISFIFO( mode ) || S_ISSOCK( mode ) ) );
if( i != 0 || ( !S_ISREG( mode ) && ( !can_read || !no_ofile ) ) )
{
if( verbosity >= 0 )
fprintf( stderr, "%s: Input file '%s' is not a regular file%s.\n",
program_name, name,
( can_read && !no_ofile ) ?
",\n and '--stdout' was not specified" : "" );
close( infd );
infd = -1;
}
}
return infd;
}
/* assure at least a minimum size for buffer 'buf' */
static void * resize_buffer( void * buf, const int min_size )
{
if( buf ) buf = realloc( buf, min_size );
else buf = malloc( min_size );
if( !buf )
{
show_error( "Not enough memory.", 0, false );
cleanup_and_fail( 1 );
}
return buf;
}
static void set_d_outname( const char * const name, const int i )
{
const unsigned name_len = strlen( name );
if( i >= 0 )
{
const char * const from = known_extensions[i].from;
const unsigned from_len = strlen( from );
if( name_len > from_len )
{
output_filename = resize_buffer( output_filename, name_len +
strlen( known_extensions[0].to ) + 1 );
strcpy( output_filename, name );
strcpy( output_filename + name_len - from_len, known_extensions[i].to );
return;
}
}
output_filename = resize_buffer( output_filename, name_len + 4 + 1 );
strcpy( output_filename, name );
strcat( output_filename, ".out" );
if( verbosity >= 1 )
fprintf( stderr, "%s: Can't guess original name for '%s' -- using '%s'\n",
program_name, name, output_filename );
}
static bool open_outstream( const bool force, const bool from_stdin )
{
const mode_t usr_rw = S_IRUSR | S_IWUSR;
const mode_t all_rw = usr_rw | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH;
const mode_t outfd_mode = from_stdin ? all_rw : usr_rw;
int flags = O_APPEND | O_CREAT | O_RDWR | O_BINARY;
if( force ) flags |= O_TRUNC; else flags |= O_EXCL;
outfd = open( output_filename, flags, outfd_mode );
if( outfd >= 0 ) delete_output_on_interrupt = true;
else if( verbosity >= 0 )
{
if( errno == EEXIST )
fprintf( stderr, "%s: Output file '%s' already exists, skipping.\n",
program_name, output_filename );
else
fprintf( stderr, "%s: Can't create output file '%s': %s\n",
program_name, output_filename, strerror( errno ) );
}
return ( outfd >= 0 );
}
void cleanup_and_fail( const int retval )
{
if( delete_output_on_interrupt )
{
delete_output_on_interrupt = false;
if( verbosity >= 0 )
fprintf( stderr, "%s: Deleting output file '%s', if it exists.\n",
program_name, output_filename );
if( outfd >= 0 ) { close( outfd ); outfd = -1; }
if( remove( output_filename ) != 0 && errno != ENOENT )
show_error( "WARNING: deletion of output file (apparently) failed.", 0, false );
}
exit( retval );
}
/* Set permissions, owner and times. */
static void close_and_set_permissions( const struct stat * const in_statsp )
{
bool warning = false;
if( in_statsp )
{
const mode_t mode = in_statsp->st_mode;
/* fchown will in many cases return with EPERM, which can be safely ignored. */
if( fchown( outfd, in_statsp->st_uid, in_statsp->st_gid ) == 0 )
{ if( fchmod( outfd, mode ) != 0 ) warning = true; }
else
if( errno != EPERM ||
fchmod( outfd, mode & ~( S_ISUID | S_ISGID | S_ISVTX ) ) != 0 )
warning = true;
}
if( close( outfd ) != 0 )
{
show_error( "Error closing output file", errno, false );
cleanup_and_fail( 1 );
}
outfd = -1;
delete_output_on_interrupt = false;
if( in_statsp )
{
struct utimbuf t;
t.actime = in_statsp->st_atime;
t.modtime = in_statsp->st_mtime;
if( utime( output_filename, &t ) != 0 ) warning = true;
}
if( warning && verbosity >= 1 )
show_error( "Can't change output file attributes.", 0, false );
}
static unsigned char xdigit( const int value )
{
if( value >= 0 && value <= 9 ) return '0' + value;
if( value >= 10 && value <= 15 ) return 'A' + value - 10;
return 0;
}
static bool show_trailing_data( const uint8_t * const data, const int size,
struct Pretty_print * const pp, const bool all,
const bool ignore_trailing )
{
if( verbosity >= 4 || !ignore_trailing )
{
int i;
char buf[80];
int len = snprintf( buf, sizeof buf, "%strailing data = ",
all ? "" : "first bytes of " );
bool text = true;
for( i = 0; i < size; ++i )
if( !isprint( data[i] ) ) { text = false; break; }
if( text )
{
if( len > 0 && len < (int)sizeof buf )
snprintf( buf + len, sizeof buf - len, "'%.*s'", size, (const char *)data );
}
else
{
for( i = 0; i < size && len > 0 && len + 3 < (int)sizeof buf; ++i )
{
if( i > 0 ) buf[len++] = ' ';
buf[len++] = xdigit( data[i] >> 4 );
buf[len++] = xdigit( data[i] & 0x0F );
buf[len] = 0;
}
}
Pp_show_msg( pp, buf );
if( !ignore_trailing ) show_error( "Trailing data not allowed.", 0, false );
}
return ignore_trailing;
}
static int decompress( const int infd, struct Pretty_print * const pp,
const int buffer_size,
const bool ignore_trailing, const bool testing )
{
unsigned long long partial_file_pos = 0;
struct Range_decoder rdec;
int retval = 0;
bool first_member;
if( !Rd_init( &rdec, infd ) )
{
show_error( "Not enough memory.", 0, false );
cleanup_and_fail( 1 );
}
for( first_member = true; ; first_member = false )
{
int result, size;
unsigned dictionary_size;
File_header header;
struct LZ_decoder decoder;
Rd_reset_member_position( &rdec );
size = Rd_read_data( &rdec, header, Fh_size );
if( Rd_finished( &rdec ) ) /* End Of File */
{
if( first_member || Fh_verify_prefix( header, size ) )
{ Pp_show_msg( pp, "File ends unexpectedly at member header." );
retval = 2; }
else if( size > 0 && !show_trailing_data( header, size, pp,
true, ignore_trailing ) )
retval = 2;
break;
}
if( !Fh_verify_magic( header ) )
{
if( first_member )
{ Pp_show_msg( pp, "Bad magic number (file not in lzip format)." );
retval = 2; }
else if( !show_trailing_data( header, size, pp, false, ignore_trailing ) )
retval = 2;
break;
}
if( !Fh_verify_version( header ) )
{
if( verbosity >= 0 )
{ Pp_show_msg( pp, 0 );
fprintf( stderr, "Version %d member format not supported.\n",
Fh_version( header ) ); }
retval = 2; break;
}
dictionary_size = Fh_get_dictionary_size( header );
if( dictionary_size < min_dictionary_size ||
dictionary_size > max_dictionary_size )
{ Pp_show_msg( pp, "Invalid dictionary size in member header." );
retval = 2; break; }
if( verbosity >= 2 || ( verbosity == 1 && first_member ) )
{ Pp_show_msg( pp, 0 ); show_header( dictionary_size ); }
if( !LZd_init( &decoder, &rdec, buffer_size, dictionary_size, outfd ) )
{
Pp_show_msg( pp, "Not enough memory. Try a smaller output buffer size." );
retval = 1; break;
}
result = LZd_decode_member( &decoder, pp );
partial_file_pos += Rd_member_position( &rdec );
LZd_free( &decoder );
if( result != 0 )
{
if( verbosity >= 0 && result <= 2 )
{
Pp_show_msg( pp, 0 );
fprintf( stderr, "%s at pos %llu\n", ( result == 2 ) ?
"File ends unexpectedly" : "Decoder error", partial_file_pos );
}
retval = 2; break;
}
if( verbosity >= 2 )
{ fputs( testing ? "ok\n" : "done\n", stderr ); Pp_reset( pp ); }
}
Rd_free( &rdec );
if( verbosity == 1 && retval == 0 )
fputs( testing ? "ok\n" : "done\n", stderr );
return retval;
}
void signal_handler( int sig )
{
if( sig ) {} /* keep compiler happy */
show_error( "Control-C or similar caught, quitting.", 0, false );
cleanup_and_fail( 1 );
}
static void set_signals( void )
{
signal( SIGHUP, signal_handler );
signal( SIGINT, signal_handler );
signal( SIGTERM, signal_handler );
}
void show_error( const char * const msg, const int errcode, const bool help )
{
if( verbosity < 0 ) return;
if( msg && msg[0] )
{
fprintf( stderr, "%s: %s", program_name, msg );
if( errcode > 0 ) fprintf( stderr, ": %s", strerror( errcode ) );
fputc( '\n', stderr );
}
if( help )
fprintf( stderr, "Try '%s --help' for more information.\n",
invocation_name );
}
void internal_error( const char * const msg )
{
if( verbosity >= 0 )
fprintf( stderr, "%s: internal error: %s\n", program_name, msg );
exit( 3 );
}
int main( const int argc, const char * const argv[] )
{
const char * input_filename = "";
const char * default_output_filename = "";
const char ** filenames = 0;
int num_filenames = 0;
int buffer_size = max_dictionary_size;
int infd = -1;
int argind = 0;
int retval = 0;
int i;
bool filenames_given = false;
bool force = false;
bool ignore_trailing = true;
bool keep_input_files = false;
bool stdin_used = false;
bool testing = false;
bool to_stdout = false;
struct Pretty_print pp;
const struct ap_Option options[] =
{
{ 'a', "trailing-error", ap_no },
{ 'c', "stdout", ap_no },
{ 'd', "decompress", ap_no },
{ 'f', "force", ap_no },
{ 'h', "help", ap_no },
{ 'k', "keep", ap_no },
{ 'n', "threads", ap_yes },
{ 'o', "output", ap_yes },
{ 'q', "quiet", ap_no },
{ 't', "test", ap_no },
{ 'u', "buffer-size", ap_yes },
{ 'v', "verbose", ap_no },
{ 'V', "version", ap_no },
{ 0 , 0, ap_no } };
struct Arg_parser parser;
invocation_name = argv[0];
CRC32_init();
if( !ap_init( &parser, argc, argv, options, 0 ) )
{ show_error( "Not enough memory.", 0, false ); return 1; }
if( ap_error( &parser ) ) /* bad option */
{ show_error( ap_error( &parser ), 0, true ); return 1; }
for( ; argind < ap_arguments( &parser ); ++argind )
{
const int code = ap_code( &parser, argind );
const char * const arg = ap_argument( &parser, argind );
if( !code ) break; /* no more options */
switch( code )
{
case 'a': ignore_trailing = false; break;
case 'c': to_stdout = true; break;
case 'd': testing = false; break;
case 'f': force = true; break;
case 'h': show_help(); return 0;
case 'k': keep_input_files = true; break;
case 'n': break;
case 'o': default_output_filename = arg; break;
case 'q': verbosity = -1; break;
case 't': testing = true; break;
case 'u': buffer_size = get_dict_size( arg ); break;
case 'v': if( verbosity < 4 ) ++verbosity; break;
case 'V': show_version(); return 0;
default : internal_error( "uncaught option." );
}
} /* end process options */
#if defined(__MSVCRT__) || defined(__OS2__)
setmode( STDIN_FILENO, O_BINARY );
setmode( STDOUT_FILENO, O_BINARY );
#endif
if( testing )
outfd = -1;
num_filenames = max( 1, ap_arguments( &parser ) - argind );
filenames = resize_buffer( filenames, num_filenames * sizeof filenames[0] );
filenames[0] = "-";
for( i = 0; argind + i < ap_arguments( &parser ); ++i )
{
filenames[i] = ap_argument( &parser, argind + i );
if( strcmp( filenames[i], "-" ) != 0 ) filenames_given = true;
}
if( buffer_size < max_dictionary_size )
{
bool from_stdin = false;
if( to_stdout || testing )
{ show_error( "'--buffer-size' is incompatible with '--stdout' and '--test'.", 0, false );
return 1; }
for( i = 0; i < num_filenames; ++i )
if( !filenames[i][0] || strcmp( filenames[i], "-" ) == 0 )
{ from_stdin = true; break; }
if( from_stdin && !default_output_filename[0] )
{ show_error( "Output file must be specified when decompressing from standard input\n"
" with a reduced buffer size.", 0, false ); return 1; }
}
if( !to_stdout && !testing &&
( filenames_given || default_output_filename[0] ) )
set_signals();
Pp_init( &pp, filenames, num_filenames, verbosity );
output_filename = resize_buffer( output_filename, 1 );
for( i = 0; i < num_filenames; ++i )
{
int tmp;
struct stat in_stats;
const struct stat * in_statsp;
output_filename[0] = 0;
if( !filenames[i][0] || strcmp( filenames[i], "-" ) == 0 )
{
if( stdin_used ) continue; else stdin_used = true;
input_filename = "";
infd = STDIN_FILENO;
if( !testing )
{
if( to_stdout || !default_output_filename[0] )
outfd = STDOUT_FILENO;
else
{
output_filename = resize_buffer( output_filename,
strlen( default_output_filename ) + 1 );
strcpy( output_filename, default_output_filename );
if( !open_outstream( force, true ) )
{
if( retval < 1 ) retval = 1;
close( infd ); infd = -1;
continue;
}
}
}
}
else
{
input_filename = filenames[i];
infd = open_instream( input_filename, &in_stats, to_stdout || testing );
if( infd < 0 ) { if( retval < 1 ) retval = 1; continue; }
if( !testing )
{
if( to_stdout ) outfd = STDOUT_FILENO;
else
{
set_d_outname( input_filename, extension_index( input_filename ) );
if( !open_outstream( force, false ) )
{
if( retval < 1 ) retval = 1;
close( infd ); infd = -1;
continue;
}
}
}
}
if( isatty( infd ) )
{
show_error( "I won't read compressed data from a terminal.", 0, true );
if( retval < 1 ) retval = 1;
cleanup_and_fail( retval );
}
if( delete_output_on_interrupt && buffer_size < max_dictionary_size )
{
struct stat st;
if( fstat( outfd, &st ) != 0 || !S_ISREG( st.st_mode ) )
{
if( verbosity >= 0 )
fprintf( stderr, "%s: Output file '%s' is not a regular file,\n"
" and 'low memory' mode has been requested.\n",
program_name, output_filename );
if( retval < 1 ) retval = 1;
cleanup_and_fail( retval );
}
}
in_statsp = input_filename[0] ? &in_stats : 0;
Pp_set_name( &pp, input_filename );
tmp = decompress( infd, &pp, buffer_size, ignore_trailing, testing );
if( tmp > retval ) retval = tmp;
if( tmp && !testing ) cleanup_and_fail( retval );
if( delete_output_on_interrupt )
close_and_set_permissions( in_statsp );
if( input_filename[0] )
{
close( infd ); infd = -1;
if( !keep_input_files && !to_stdout && !testing )
remove( input_filename );
}
}
if( outfd >= 0 && close( outfd ) != 0 )
{
show_error( "Can't close stdout", errno, false );
if( retval < 1 ) retval = 1;
}
free( output_filename );
free( filenames );
ap_free( &parser );
return retval;
}