/* UnTAR.c -- Display contents and extract files from a gzip'd TAR file
 *
 * Based on untgz.c, found in the zlib 1.1.2 archive.
 *
 * Originally written by Pedro A. Aranda Gutīrrez <paag@tid.es>.
 * Adaptated to Unix by Jean-loup Gailly <jloup@gzip.org>.
 * Amiga version by Magnus Holmgren <lear@algonet.se>.
 */

#include <clib/alib_stdio_protos.h>
#include <clib/asyncio_protos.h>
#include <dos/rdargs.h>
#include <exec/execbase.h>
#include <exec/memory.h>
#include <proto/dos.h>
#include <proto/exec.h>
#include <proto/locale.h>
#include <proto/utility.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "untar.h"


enum HeaderStatus
{
	HEADER_OK, HEADER_ZERO, HEADER_FAIL
};


enum Action
{
	ACT_EXTRACT, ACT_LIST
};


/* -- Globals ----------------------------------------------------------- */


struct DosLibrary	*DOSBase;
struct Library		*UtilityBase;

LOCAL const TEXT Version[] = "$VER: UnTAR 37.1 (29.8.98)";


/* -- Misc help functions ----------------------------------------------- */


/* Clear a (small) block of memory. This used rather than memset, since
 * when compiling for a 68020+, SAS/C insists on using a rather large,
 * optimized version of memset and we only need to clear about a dozen
 * bytes...
 */
LOCAL VOID
ClrMem( APTR mem, LONG size )
{
	UBYTE	*ptr;

	for( ptr = mem; size; --size )
	{
		*ptr++ = 0;
	}
}


/* Convert an octal string to a long. Up to width chars are converted. */
LOCAL LONG
GetOct( STRPTR p, LONG width )
{
	LONG	rc = 0;
	TEXT	c;

	/* Skip leading spaces */
	while( width && ( *p == ' ' ) )
	{
		++p;
		--width;
	}

	while( width-- )
	{
		c = *p++;

		if( ( c < '0' ) || ( c > '7' ) )
		{
			/* Break if EOS or non-octal char */
			break;
		}

		rc = rc * 8 + ( c - '0' );
	}

	return( rc );
}


/* -- Tar handling ------------------------------------------------------ */


/* Some information about a file in a tar - and the tar itself - for
 * passing around to different funcitons.
 */
struct TarFile
{
	STRPTR		Name;		/* File name, as found in the header */
	STRPTR		Filename;	/* Name to use when creating the file. Adjusted by ExtractHead */
	LONG		Type;		/* Typeflag from the header */
	LONG		Size;		/* Size of the file */
	LONG		Time;		/* Creation date, in "Unix seconds */
	LONG		Mode;		/* Protection bits, in Unix format */
	LONG		DirEnding;	/* TRUE if Name ends with "/" */

	/* Some other information... */

	LONG		TotalSize;	/* Total size of listed files */
	LONG		NumFiles;	/* Total number of listed files */

	LONG		Overwrite;	/* TRUE if OK to overwrite files on extract */
	LONG		Quiet;		/* Don't print "Extracting"/"Skipping" messages */
	AsyncFile	*OutFile;	/* Current output file */
	struct List	*Patterns;	/* Patterns for files to process */
};


/* Print information about a file in a tar archive */
LOCAL LONG
ListHead( struct TarFile *file )
{
	struct ClockData	clock;
	TEXT	mode[ 16 ], size[ 16 ], time[ 32 ];
	STRPTR	s;
	LONG	i, tarmode;

	/* Format mode string */
	tarmode = file->Mode;
	s = mode;
	*s++ = '-';	/* Replaced with 'l', 'd' or '?', if appropriate */

	for( i = 3; i; --i )
	{
		*s++ = tarmode & TM_UREAD  ? 'r' : '-';
		*s++ = tarmode & TM_UWRITE ? 'w' : '-';
		*s++ = tarmode & TM_UEXEC  ? 'x' : '-';
		tarmode <<= 3;
	}

	*s = '\0';

	/* Format time string */
	Amiga2Date( file->Time - UNIX2AMIGA_TIME - GMTOffset, &clock );
	sprintf( time, GetString( MSG_LIST_DATE_TIME ),
		clock.mday, GetMonthString( ( LONG ) clock.month ), clock.year,
		clock.hour, clock.min, clock.sec );

	/* Do the rest */
	switch( file->Type )
	{
		case TF_LINK:
		case TF_SYM:
			strcpy( size, GetString( MSG_LIST_LINK ) );
			*mode = 'l';

		case TF_FILE:
		case TF_AFILE:
			if( !file->DirEnding )
			{
				sprintf( size, LocaleBase ? "%12lD" : "%12ld", file->Size );
				file->TotalSize += file->Size;
				++file->NumFiles;
				break;
			}
			/* else fall through */

		case TF_DIR:
			*mode = 'd';
			strcpy( size, GetString( MSG_LIST_DIR ) );
			break;

		default:
			*mode = '?';
			strcpy( size, GetString( MSG_LIST_UNKNOWN ) );
			break;
	}

	Printf( "%s %s %s %s\n", size, mode, time, file->Name );
	return( TRUE );
}


/* Try to open the TarFile for output. AsyncFile handle is stored in the
 * file structure. Prints any error messages.
 */
LOCAL VOID
OpenTarFile( struct TarFile *file )
{
	if( !file->Size )
	{
		/* No contents */
		return;
	}

	/* Does the name match the pattern? */
	if( MatchName( file->Name, file->Patterns ) )
	{
		/* OK to overwrite file if it exists? */
		if( file->Overwrite || OverwriteOk( file->Filename ) )
		{
			/* Writing is done in 512 byte blocks, so having
			 * larger buffers here doesn't help much.
			 */
			if( !( file->OutFile = OpenAsync( file->Filename, MODE_WRITE, BUFSIZE ) ) )
			{
				/* Couldn't open file, so try creating drawer(s)
				 * as well.
				 */
				STRPTR	p;

				/* Using FilePart()/PathPart() doesn't improve things. */
				if( p = strrchr( file->Filename, '/' ) )
				{
					*p = '\0';
					Makedir( file->Filename );
					*p = '/';
					file->OutFile = OpenAsync( file->Filename, MODE_WRITE, BUFSIZE );
				}
			}

			if( file->OutFile )
			{
				if( !file->Quiet )
				{
					Printf( GetString( MSG_EXTRACT ), file->Name );
				}
			}
			else
			{
				PrintfFault( IoErr(), GetString( MSG_ERR_EXTRACT ), file->Name );
			}
		}
		else if( !file->Quiet )
		{
			Printf( GetString( MSG_WARN_SKIP_EXISTS ), file->Name );
		}
	}
	else if( !file->Quiet )
	{
		Printf( GetString( MSG_WARN_SKIP_PATTERN ), file->Name );
	}
}


/* Begin extract of a file */
LOCAL LONG
ExtractHead( struct TarFile *file )
{
	LONG	rc = TRUE;

	/* Strip any leading "/" char */
	if( file->Name[ 0 ] == '/' )
	{
		++file->Filename;
	}

	{
		STRPTR	s;

		/* Convert any illegal chars to something better */
		for( s = file->Filename; *s; ++s )
		{
			if( *s == ':' )
			{
				*s = '_';
			}
		}
	}

	switch( file->Type )
	{
		case TF_LINK:
		case TF_SYM:
			if( !file->Quiet )
			{
				Printf( GetString( MSG_WARN_SKIP_LINK ), file->Name );
			}

			break;

		case TF_FILE:
		case TF_AFILE:
			/* Is it really a file? */
			if( !file->DirEnding )
			{
				OpenTarFile( file );
				break;
			}
			/* else fall through */

		case TF_DIR:
			if( Makedir( file->Filename ) )
			{
				SetTarTime( file->Filename, file->Time );
				SetTarMode( file->Filename, file->Mode );
			}
			else
			{
				rc = FALSE;
			}

			break;

		default:
			if( !file->Quiet )
			{
				Printf( GetString( MSG_WARN_SKIP_UNKNOWN ), file->Name, file->Type );
			}

			break;
	}

	return( rc );
}


LOCAL VOID
WriteError( LONG err, struct TarFile *file, LONG keep )
{
	PrintfFault( err, GetString( MSG_ERR_WRITE ), file->Name );

	if( !keep )
	{
		DeleteFile( file->Filename );
	}
}


/* Check header checksum */
LOCAL enum HeaderStatus
ValidHeader( struct TarHeader *head )
{
	LONG	i, unsignedsum, signedsum, recordedsum;
	LONG	rc = HEADER_OK;
	BYTE	*p;

	/* Header checksum can be calculate with or without unsigned math,
	 * so check for both.
	 */
	recordedsum = GetOct( head->Checksum, sizeof( head->Checksum ) );
	unsignedsum = 0;
	signedsum = 0;

	for( p = ( BYTE * ) head, i = sizeof( *head ); i > 0; ++p, --i )
	{
		unsignedsum += 0xFF & *p;
		signedsum   += *p;
	}

	/* Adjust checksum to count the "Checksum" field as blanks.  */

	for( p = ( BYTE * ) head->Checksum, i = sizeof( head->Checksum ); i > 0; ++p, --i )
	{
		unsignedsum -= 0xFF & *p;
		signedsum   -= *p;
	}

	unsignedsum += ' ' * sizeof( head->Checksum );
	signedsum   += ' ' * sizeof( head->Checksum );

	if( unsignedsum == sizeof( head->Checksum ) * ' ' )
	{
		rc = HEADER_ZERO;
	}
	else if( ( unsignedsum != recordedsum ) && ( signedsum != recordedsum ) )
	{
		rc = HEADER_FAIL;
	}

	return( rc );
}


/* Process the TAR archive and do all the stuff. */
LOCAL LONG
ProcessTAR( gzStream *in, enum Action action, const STRPTR to,
	LONG keep, LONG overwrite, LONG quiet, struct List *patterns )
{
	static union TarBuffer	Buffer;
	static struct TarFile	File;
	static TEXT		Filename[ TAR_NAME_LEN + 1 ];
	static TEXT		Filename2[ TAR_NAME_LEN + 1 ];
	BPTR	olddir = NULL;
	LONG	rc = RETURN_OK, remaining = 0, getheader = TRUE;
	LONG	status = HEADER_OK, oldstatus, breaked = FALSE;
	LONG	len = 0;

	File.Name	= Filename;
	File.Filename	= Filename2;
	File.OutFile	= NULL;
	File.TotalSize	= 0;
	File.NumFiles	= 0;
	File.Overwrite	= overwrite;
	File.Quiet	= quiet;
	File.Patterns	= patterns;

	/* CD to dest if we are about to extract */
	if( ( action == ACT_EXTRACT ) && to && *to )
	{
		if( !( olddir = ChangeDir( to ) ) )
		{
			return( RETURN_ERROR );
		}
	}

	/* Print listing header */
	if( action == ACT_LIST )
	{
		/* The format is similar to Amiga LHA 'l' output */
		Printf( GetString( MSG_LIST_HEAD ) );
	}

	for( ;; )
	{
		if( CheckSignal( SIGBREAKF_CTRL_C ) )
		{
			Printf( GetString( MSG_BREAK ) );
			breaked = TRUE;
			rc = RETURN_WARN;
			break;
		}

		if( ( len = ReadGZ( in, &Buffer, BLOCKSIZE ) ) < 0 )
		{
			break;
		}

		/* If we met the end of the tar, we are done */
		if( !len )
		{
			break;
		}

		/* Always expect complete blocks to process the tar
		 * information.
		 */
		if( len != BLOCKSIZE )
		{
			if( IoErr() && ( len >= 0 ) )
			{
				PrintfFault( IoErr(), GetString( MSG_ERR_EARLY_EOF ), in->Path );
			}
			else
			{
				Printf( GetString( MSG_ERR_EARLY_EOF ), in->Path );
				Printf( "\n" );
			}

			rc = RETURN_WARN;
		}

		/* If we have to get a tar header */
		if( getheader )
		{
			/* If we met the end-of-tar block, we are done */
			if( !Buffer.Header.Name[ 0 ] )
			{
				break;
			}

			oldstatus = status;

			/* Is header checksum correct? */
			if( HEADER_OK != ( status = ValidHeader( &Buffer.Header ) ) )
			{
				if( status == HEADER_ZERO )
				{
					status = oldstatus;
				}
				else if( ( status == HEADER_FAIL ) && ( oldstatus != HEADER_FAIL ) )
				{
					Printf( GetString( MSG_ERR_BAD_FILE_HEADER ) );
				}

				getheader = TRUE;
				continue;
			}

			/* We have a valid header block, so continue processing. */

			strncpy( File.Name,     Buffer.Header.Name, TAR_NAME_LEN );
			strncpy( File.Filename, Buffer.Header.Name, TAR_NAME_LEN );
			File.Type	= Buffer.Header.Typeflag;
			File.Time	= GetOct( Buffer.Header.MTime, 12 );
			File.Mode	= GetOct( Buffer.Header.Mode, 8 );
			File.Size	= GetOct( Buffer.Header.Size, 12 );
			remaining	= File.Size;

			/* Special case for BSD tar "files" */
			File.DirEnding = File.Name[ strlen( File.Name ) - 1 ] == '/';

			/* Could have no contents */
			getheader = ( remaining == 0 );

			switch( action )
			{
				case ACT_EXTRACT:
					if( !ExtractHead( &File ) )
					{
						rc = RETURN_WARN;
					}

					break;

				case ACT_LIST:
					ListHead( &File );
					break;
			}
		}
		else
		{
			/* Generic code for all actions */
			ULONG	bytes;

			bytes = ( remaining > BLOCKSIZE ) ? BLOCKSIZE : remaining;

			if( File.OutFile )
			{
				if( WriteAsync( File.OutFile, &Buffer, bytes ) != bytes )
				{
					LONG	err = IoErr();

					CloseAsync( File.OutFile );
					File.OutFile = NULL;
					WriteError( err, &File, keep );
					rc = RETURN_WARN;
				}
			}

			remaining -= bytes;

			if( !remaining )
			{
				getheader = TRUE;

				if( File.OutFile )
				{
					if( CloseAsync( File.OutFile ) )
					{
						SetTarTime( File.Filename, File.Time );
						SetTarMode( File.Filename, File.Mode );
					}
					else
					{
						WriteError( IoErr(), &File, keep );
						rc = RETURN_WARN;
					}

					File.OutFile = NULL;
				}
			}
		}
	}

	/* Should only be true if we were breaked. */
	if( File.OutFile )
	{
		if( !CloseAsync( File.OutFile ) )
		{
			WriteError( IoErr(), &File, keep );
			rc = RETURN_WARN;
		}
		else if( remaining && keep )
		{
			Printf( GetString( MSG_WARN_FILE_INCOMPLETE ), File.Name );
		}

		File.OutFile = NULL;
	}

	if( !breaked && ( len >= 0 ) )
	{
		/* Process any data left, so we can see if there were any
		 * CRC errors and the like.
		 */
		while( ( len = ReadGZ( in, &Buffer, BLOCKSIZE ) ) > 0 )
		{
		}
	}

	/* Any data errors? */
	if( len < 0 )
	{
		Printf( GetString( MSG_ERR_READ_Z ), in->Path, ErrorGZ( in ) );

		if( action == ACT_EXTRACT )
		{
			Printf( GetString( MSG_WARN_CORRUPT ) );
		}

		rc = RETURN_ERROR;
	}

	/* Print listing footer. */
	if( action == ACT_LIST )
	{
		Printf( "-------------\n" );
		Printf( LocaleBase ? GetString( MSG_LIST_FOOT ) : ( const STRPTR ) "%12ld  %3ld%%  %ld files\n",
			File.TotalSize,
			/* Compression factor as in LhA. Make sure we don't
			 * get division by zero errors.
			 */
			in->Stream.total_out
				? ( 100 - ( 100 * in->Stream.total_in / in->Stream.total_out ) )
				: 0,
			File.NumFiles );
	}

	if( olddir )
	{
		/* Change back to old current dir */
		UnLock( CurrentDir( olddir ) );
	}

	return( rc );
}


/* -- Main -------------------------------------------------------------- */


struct Args
{
	STRPTR	File;
	STRPTR	To;
	LONG	List;
	LONG	Keep;
	LONG	NoPrompt;
	LONG	Quiet;
	LONG	Case;
	STRPTR	*Patterns;
};

#define TEMPLATE_MSG	"FILE/A,TO/K,LIST/S,KEEP/S,NOPROMPT/S,QUIET/S,CASE/S,PATTERNS/K/M"


LONG
_STI_200_OpenLibs( VOID )
{
	if( DOSBase = ( struct DosLibrary * ) OpenLibrary( "dos.library", 37 ) )
	{
		if( UtilityBase = OpenLibrary( "utility.library", 37 ) )
		{
			return( 0 );
		}
	}

	return( 20 );
}


VOID
_STD_200_CloseLibs( VOID )
{
	if( DOSBase )
	{
		CloseLibrary( UtilityBase );
		CloseLibrary( ( struct Library * ) DOSBase );
	}
}


__stdargs LONG
Main( VOID )
{
	struct Args	args;
	struct RDArgs	*rda;
	LONG	rc = RETURN_FAIL;

	ClrMem( &args, sizeof( args ) );

	if( rda = ReadArgs( TEMPLATE_MSG, ( LONG * ) &args, NULL ) )
	{
		gzStream	*file;

		if( file = OpenGZ( args.File, BUFSIZE ) )
		{
			struct List	patterns;

			if( ParsePatterns( &patterns, args.Patterns, args.Case ) )
			{
				rc = ProcessTAR( file,
					args.List ? ACT_LIST : ACT_EXTRACT,
					args.To, args.Keep, args.NoPrompt,
					args.Quiet, &patterns );
				FreePatterns( &patterns );
			}

			CloseGZ( file );
		}
		else
		{
			PrintfFault( IoErr(), GetString( MSG_ERR_OPEN_INPUT ), args.File );
		}
	}
	else
	{
		PrintfFault( IoErr(), GetString( MSG_ERR_ARGUMENT ) );
	}

	return( rc );
}
