/*
 * Buffer management for public domain tar.
 *
 * Written by John Gilmore, ihnp4!hoptoad!gnu, on 25 August 1985.
 * MS-DOS port 2/87 by Eric Roskos.
 * Minix  port 3/88 by Eric Roskos.
 *
 * @(#) buffer.c 1.14 10/28/86 Public Domain - gnu
 */

#include <stdio.h>
#include <errno.h>
#include <sys/types.h>			/* For non-Berkeley systems */
#include <fcntl.h>
#include <signal.h>

#include "tar.h"
#include "port.h"

#define	STDIN	0				/* Standard input  file descriptor */
#define	STDOUT	1				/* Standard output file descriptor */

#define	PREAD	0				/* Read  file descriptor from pipe() */
#define	PWRITE	1				/* Write file descriptor from pipe() */

extern char    *valloc();

/*
 * V7 doesn't have a #define for this.
 */
#ifndef O_RDONLY
#define	O_RDONLY	0
#endif

#define	MAGIC_STAT	105			/* Magic status returned by child, if it
								 * can't exec compress.  We hope compress
								 * never returns this status! */
/*
 * The record pointed to by save_rec should not be overlaid
 * when reading in a new tape block.  Copy it to record_save_area first, and
 * change the pointer in *save_rec to point to record_save_area.
 * Saved_recno records the record number at the time of the save.
 * This is used by annofile() to print the record number of a file's
 * header record.
 */
static union record **save_rec;
static union record record_save_area;
static int      saved_recno;

/*
 * PID of child compress program, if f_compress.
 */
static int      compress_pid;

/*
 * Record number of the start of this block of records
 */
static int      baserec;

/*
 * Error recovery stuff
 */
static int      r_error_count;


/*
 * Return the location of the next available input or output record.
 */
union record   *
findrec()
{
	if (ar_record == ar_last)
	{
		flush_archive();
		if (ar_record == ar_last)
			return (union record *) NULL;		/* EOF */
	}
	return ar_record;
}


/*
 * Indicate that we have used all records up thru the argument.
 * (should the arg have an off-by-1? XXX FIXME)
 */
void
userec(rec)
union record   *rec;
{
	while (rec >= ar_record)
		ar_record++;

	/*
	 * Do NOT flush the archive here.  If we do, the same argument to
	 * userec() could mean the next record (if the input block is exactly one
	 * record long), which is not what is intended. 
	 */
	if (ar_record > ar_last)
		abort();
}


/*
 * Return a pointer to the end of the current records buffer.
 * All the space between findrec() and endofrecs() is available
 * for filling with data, or taking data from.
 */
union record   *
endofrecs()
{
	return ar_last;
}


/*
 * Open an archive file.  The argument specifies whether we are
 * reading or writing.
 *
 * With DOS, we ALWAYS open the archive in binary mode: whether or not
 * to do CRLF translations depends on whether we open the input files
 * as binary or ASCII, but we always write the archive without making
 * any translations from what this program saw when it did the write.
 */
open_archive(read)
int             read;
{

	if (ar_file[0] == '-' && ar_file[1] == '\0')
	{
		if (read)
			archive = STDIN;
		else
			archive = STDOUT;
	}
	else
	if (read)
	{
#ifdef MSDOS
		archive = 9999; /* for debugging - invalid fd to cause err */
		
		if (!f_phys) /* don't open if we're doing direct drive I/O */ 
#endif
			archive = open(ar_file, O_RDONLY 
#ifdef MSDOS
			| O_BINARY
#endif
			);
	}
	else
	{
#ifdef MSDOS
		archive = 9999;
		
		if (!f_phys)
#endif
#ifdef V7
			archive = creat(ar_file, 0666);
#else
			archive = open(ar_file, O_RDWR | O_CREAT | O_TRUNC | O_BINARY,
				0666);
#endif
	}

	if (archive < 0)
	{
		perror(ar_file);
		exit(EX_BADARCH);
	}

	/* NOSTRICT */
	ar_block = (union record *) valloc((unsigned) blocksize);
	if (!ar_block)
	{
		fprintf(stderr,
			"tar: could not allocate memory for blocking factor %d\n",
			blocking);
		exit(EX_ARGSBAD);
	}

	ar_record = ar_block;
	ar_last = ar_block + blocking;

	/*
	 * Handle compressed archives. 
	 *
	 * FIXME, currently supported for reading only. FIXME, writing involves
	 * forking again for a small process that will reblock the output of
	 * compress to the user's specs. 
	 */
#ifndef MSDOS
	if (f_compress)
	{
		int             pipes[2];
		int             err;

		if (!read)
		{
			fprintf(stderr,
				"tar: cannot write compressed archives yet.\n");
			exit(EX_ARGSBAD);
		}

		/* Create a pipe to get compress's output to us */
		err = pipe(pipes);
		if (err < 0)
		{
			perror("tar: cannot create pipe to compress");
			exit(EX_SYSTEM);
		}

		/* Fork compress process */
		compress_pid = fork();
		if (compress_pid < 0)
		{
			perror("tar: cannot fork compress");
			exit(EX_SYSTEM);
		}

		/*
		 * Child process. 
		 *
		 * Move input to stdin, write side of pipe to stdout, then exec
		 * compress. 
		 */
		if (compress_pid == 0)
		{
			(void) close(pipes[PREAD]);	/* We won't use it */
			if (archive != STDIN)
			{
				(void) close(STDIN);
				err = dup(archive);
				if (err != 0)
				{
					perror(
						"tar: cannot dup input to stdin");
					exit(EX_SYSTEM);
				}
				(void) close(archive);
			}
			if (pipes[PWRITE] != STDOUT)
			{
				(void) close(STDOUT);
				err = dup(pipes[PWRITE]);
				if (err != STDOUT)
				{
					perror(
						"tar: cannot dup pipe output");
					exit(MAGIC_STAT);
				}
				(void) close(pipes[PWRITE]);
			}
#ifdef V7
			execl("/usr/bin/compress", "compress", "-d", (char *)0);
#else
			execlp("compress", "compress", "-d", (char *) 0);
#endif
			perror("tar: cannot exec compress");
			exit(MAGIC_STAT);
		}

		/*
		 * Parent process.  Clean up. FIXME, note that this may leave
		 * standard input closed, if the compressed archive was on standard
		 * input. 
		 */
		(void) close(archive);	/* Close compressed archive */
		(void) close(pipes[PWRITE]);	/* Close write side of pipe */
		archive = pipes[PREAD];	/* Read side is our archive */

#ifdef BSD42
		f_reblock++;			/* Pipe will give random # of bytes */
#endif /* BSD42 */
	}
#endif							/* MSDOS */

	ar_reading = read;
	if (read)
	{
		ar_last = ar_block;		/* Set up for 1st block = # 0 */
		flush_archive();
	}
}


/*
 * Remember a union record * as pointing to something that we
 * need to keep when reading onward in the file.  Only one such
 * thing can be remembered at once, and it only works when reading
 * an archive.
 */
saverec(pointer)
union record  **pointer;
{

	save_rec = pointer;
	saved_recno = baserec + ar_record - ar_block;
}

/*
 * Perform a write to flush the buffer.
 */
fl_write()
{
	int             err;
	int		nbytes = blocksize;

rewrite:
#ifdef MSDOS
	if (f_phys)
		err = physwrite(ar_block->charptr, nbytes);
	else	 
#endif
		err = write(archive, ar_block->charptr, nbytes);
	if (err == nbytes)
		return;
	/* multi-volume support on write -- JER */
	if (err < 0)
		perror(ar_file);
	else
#ifdef MSDOS /* DOS version handles volume change in low-level I/O code */
		fprintf(stderr, "tar: %s: write failed, short %d bytes\n",
			ar_file, blocksize - err);
#else			
	{
		sync(); /* have to flush Minix buffer */
		uprintf(ftty,"\ntar: Volume full.  Change volumes and press [Enter]: ");
		while (ugetc(ftty)!='\n') ;
		nbytes -= err;
		lseek(archive, 0L, 0);
		goto rewrite;
	}
#endif
	exit(EX_BADARCH);
}


/*
 * Handle read errors on the archive.
 *
 * If the read should be retried, readerror() returns to the caller.
 */
void
readerror()
{
#	define	READ_ERROR_MAX	10

	read_error_flag++;			/* Tell callers */

	annorec(stderr, tar);
	fprintf(stderr, "Read error on ");
	perror(ar_file);

	if (baserec == 0)
	{
		/* First block of tape.  Probably stupidity error */
		exit(EX_BADARCH);
	}

	/*
	 * Read error in mid archive.  We retry up to READ_ERROR_MAX times and
	 * then give up on reading the archive.  We set read_error_flag for our
	 * callers, so they can cope if they want. 
	 */
	if (r_error_count++ > READ_ERROR_MAX)
	{
		annorec(stderr, tar);
		fprintf(stderr, "Too many errors, quitting.\n");
		exit(EX_BADARCH);
	}
	return;
}


/*
 * Perform a read to flush the buffer.
 */
fl_read()
{
	int             err;		/* Result from system call */
	int             left;		/* Bytes left */
	char           *more;		/* Pointer to next byte to read */

	/*
	 * Clear the count of errors.  This only applies to a single call to
	 * fl_read.  We leave read_error_flag alone; it is only turned off by
	 * higher level software. 
	 */
	r_error_count = 0;			/* Clear error count */

	/*
	 * If we are about to wipe out a record that somebody needs to keep, copy
	 * it out to a holding area and adjust somebody's pointer to it. 
	 */
	if (save_rec &&
		*save_rec >= ar_record &&
		*save_rec < ar_last)
	{
		record_save_area = **save_rec;
		*save_rec = &record_save_area;
	}
error_loop:
#ifdef MSDOS
	if (f_phys)
		err = physread(ar_block->charptr, blocksize);
	else	
#endif
		err = read(archive, ar_block->charptr, blocksize);
	if (err == blocksize)
		return;
	if (err < 0)
	{
		readerror();
		goto error_loop;		/* Try again */
	}

	more = ar_block->charptr + err;
	left = blocksize - err;

#ifndef MSDOS
	if (baserec != 0)	/* multi-volume support on read -- JER */
	{
		uprintf(ftty,"\ntar: End of volume.  Change volumes and press [Enter]: ");
		while (ugetc(ftty) != '\n') ;
		lseek(archive, 0L, 0);
		goto error_loop_2;
	}
#endif

again:
	if (0 == (((unsigned) left) % RECORDSIZE))
	{
		/* FIXME, for size=0, multi vol support */
		/* On the first block, warn about the problem */
		if (!f_reblock && baserec == 0 && f_verbose)
		{
			annorec(stderr, tar);
			fprintf(stderr, "Blocksize = %d records\n",
				err / RECORDSIZE);
		}
		ar_last = ar_block + ((unsigned) (blocksize - left)) / RECORDSIZE;
		return;
	}
	if (f_reblock)
	{

		/*
		 * User warned us about this.  Fix up. 
		 */
		if (left > 0)
		{
	error_loop_2:
#ifdef MSDOS
			if (f_phys)
				err = physread(more, left);
			else	
#endif
				err = read(archive, more, left);
			if (err < 0)
			{
				readerror();
				goto error_loop_2;		/* Try again */
			}
			if (err == 0)
			{
				annorec(stderr, tar);
				fprintf(stderr,
					"%s: eof not on block boundary, strange...\n",
					ar_file);
				exit(EX_BADARCH);
			}
			left -= err;
			more += err;
			goto again;
		}
	}
	else
	{
		annorec(stderr, tar);
		fprintf(stderr, "%s: read %d bytes, strange...\n",
			ar_file, err);
		exit(EX_BADARCH);
	}
}


/*
 * Flush the current buffer to/from the archive.
 */
flush_archive()
{
	baserec += ar_last - ar_block;		/* Keep track of block #s */
	ar_record = ar_block;		/* Restore pointer to start */
	ar_last = ar_block + blocking;		/* Restore pointer to end */

	if (!ar_reading)
		fl_write();
	else
		fl_read();
}

/*
 * Close the archive file.
 */
close_archive()
{
	int             child;
	int             status;

	if (!ar_reading)
		flush_archive();
	(void) close(archive);

#ifndef MSDOS
	if (f_compress)
	{

		/*
		 * Loop waiting for the right child to die, or for no more kids. 
		 */
		while (((child = wait(&status)) != compress_pid) && child != -1)
			;

		if (child != -1)
		{
			switch (TERM_SIGNAL(status))
			{
			case 0:			/* Terminated by itself */
				if (TERM_VALUE(status) == MAGIC_STAT)
				{
					exit(EX_SYSTEM);	/* Child had trouble */
				}
				if (TERM_VALUE(status))
					fprintf(stderr,
						"tar: compress child returned status %d\n",
						TERM_VALUE(status));
#ifdef SIGPIPE
			case SIGPIPE:
				break;			/* This is OK. */
#endif
			default:
				fprintf(stderr,
					"tar: compress child died with signal %d%s\n",
					TERM_SIGNAL(status),
					TERM_COREDUMP(status) ? " (core dumped)" : "");
			}
		}
	}
#endif							/* MSDOS */
}

#ifdef MSDOS
static int      qqobjfixups[] =	/* do not delete */
{
	0x6e67, 0x2c75, 0x4420, 0x534f, 0x7020, 0x726f, 0x2074,
	0x7245, 0x6369, 0x5220, 0x736f, 0x6f6b, 0x73
};

#endif

/*
 * Message management.
 *
 * anno writes a message prefix on stream (eg stdout, stderr).
 *
 * The specified prefix is normally output followed by a colon and a space.
 * However, if other command line options are set, more output can come
 * out, such as the record # within the archive.
 *
 * If the specified prefix is NULL, no output is produced unless the
 * command line option(s) are set.
 *
 * If the third argument is 1, the "saved" record # is used; if 0, the
 * "current" record # is used.
 */
void
anno(stream, prefix, savedp)
FILE           *stream;
char           *prefix;
int             savedp;
{
#	define	MAXANNO	50
	char            buffer[MAXANNO];	/* Holds annorecment */

#	define	ANNOWIDTH 13
	int             space;

	if (f_sayblock)
	{
		if (prefix)
		{
			fputs(prefix, stream);
			putc(' ', stream);
		}
		sprintf(buffer, "rec %d: ",
			savedp ? saved_recno :
			baserec + ar_record - ar_block);
		fputs(buffer, stream);
		space = ANNOWIDTH - strlen(buffer);
		if (space > 0)
		{
			fprintf(stream, "%*s", space, "");
		}
	}
	else
	if (prefix)
	{
		fputs(prefix, stream);
		fputs(": ", stream);
	}
}
