/*
 * Buffer management for public domain tar.
 *
 * Written by John Gilmore, ihnp4!hoptoad!gnu, on 25 August 1985.
 *
 * @(#) buffer.c 1.28 11/6/87 Public Domain - gnu
 */

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

#if defined(MSDOS) || defined (AMIGA)
# include <fcntl.h>
#else
# ifdef XENIX
#  include <sys/inode.h>
# endif
# include <sys/file.h>
#endif

#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();
extern char	*index(), *strcat();

/*
 * 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.  We hope compress/sh
				   never return 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 long	    saved_recno;

/*
 * PID of child program, if f_compress or remote archive access.
 */
static int	childpid = 0;

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

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

/*
 * Have we hit EOF yet?
 */
static int	eof;


/*
 * Return the location of the next available input or output record.
 * Return NULL for EOF.  Once we have returned NULL, we just keep returning
 * it, to avoid accidentally going on to the next file on the "tape".
 */
union record *
findrec()
{
	if (ar_record == ar_last) {
		if (eof)
			return (union record *)NULL;	/* EOF */
		flush_archive();
		if (ar_record == ar_last) {
			eof++;
			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;
}

#ifndef AMIGA
/* 
 * Duplicate a file descriptor into a certain slot.
 * Equivalent to BSD "dup2" with error reporting.
 */
void
dupto(from, to, msg)
	int from, to;
	char *msg;
{
	int err;

	if (from != to) {
		(void) close(to);
		err = dup(from);
		if (err != to) {
			fprintf(stderr, "tar: cannot dup ");
			perror(msg);
			exit(EX_SYSTEM);
		}
		(void) close(from);
	}
}
#endif

/*
 * Fork a child to deal with remote files or compression.
 * If rem_host is zero, we are called only for compression.
 */
void
child_open(rem_host, rem_file)
	char *rem_host, *rem_file;
{

#ifdef MSDOS
	fprintf(stderr,
	  "MSDOS %s cannot deal with compressed or remote archives\n", tar);
	exit(EX_ARGSBAD);
#else
#ifdef AMIGA
	fprintf(stderr,
	  "Amiga %s cannot deal with compressed archives\n", tar);
	exit(EX_ARGSBAD);
#else
	
	int pipes[2];
	int err;
	struct stat arstat;
	char cmdbuf[1000];		/* For big file and host names */

	/* Create a pipe to talk to the child over */
	err = pipe(pipes);
	if (err < 0) {
		perror ("tar: cannot create pipe to child");
		exit(EX_SYSTEM);
	}
	
	/* Fork child process */
	childpid = fork();
	if (childpid < 0) {
		perror("tar: cannot fork");
		exit(EX_SYSTEM);
	}

	/*
	 * Parent process.  Clean up.
	 *
	 * We always close the archive file (stdin, stdout, or opened file)
	 * since the child will end up reading or writing that for us.
	 * Note that this may leave standard input closed.
	 * We close the child's end of the pipe since they will handle
	 * that too; and we set <archive> to the other end of the pipe.
	 *
	 * If reading, we set f_reblock since reading pipes or network
	 * sockets produces odd length data.
	 */
	if (childpid > 0) {
		(void) close (archive);	
		if (ar_reading) {
			(void) close (pipes[PWRITE]);
			archive = pipes[PREAD];
			f_reblock++;
		} else {
			(void) close (pipes[PREAD]);
			archive = pipes[PWRITE];
		}
		return;
	}

	/*
	 * Child process.
	 */
	if (ar_reading) {
		/*
		 * Reading from the child...
		 *
		 * Close the read-side of the pipe, which our parent will use.
		 * Move the write-side of pipe to stdout,
		 * If local, move archive input to child's stdin,
		 * then run the child.
		 */
		(void) close (pipes[PREAD]);
		dupto(pipes[PWRITE], STDOUT, "to stdout");
		if (rem_host) {
			(void) close (STDIN);	/* rsh abuses stdin */
			if (STDIN != open("/dev/null"))
				perror("Can't open /dev/null");
			sprintf(cmdbuf,
				"rsh '%s' dd '<%s' bs=%db",
				rem_host, rem_file, blocking);
			if (f_compress)
				strcat(cmdbuf, "| compress -d");
#ifdef DEBUG
			fprintf(stderr, "Exec-ing: %s\n", cmdbuf);
#endif
			execlp("sh", "sh", "-c", cmdbuf, (char *)0);
			perror("tar: cannot exec sh");
		} else {
			/*
			 * If we are reading a disk file, compress is OK;
			 * otherwise, we have to reblock the input in case it's
			 * coming from a tape drive.  This is an optimization.
			 */
			dupto(archive, STDIN, "to stdin");
			err = fstat(STDIN, &arstat);
			if (err != 0) {
				perror("tar: can't fstat archive");
				exit(EX_SYSTEM);
			}
			if ((arstat.st_mode & S_IFMT) == S_IFREG) {
				execlp("compress", "compress", "-d", (char *)0);
				perror("tar: cannot exec compress");
			} else {
				/* Non-regular file needs dd before compress */
				sprintf(cmdbuf,
					"dd bs=%db | compress -d",
					blocking);
#ifdef DEBUG
				fprintf(stderr, "Exec-ing: %s\n", cmdbuf);
#endif
				execlp("sh", "sh", "-c", cmdbuf, (char *)0);
				perror("tar: cannot exec sh");
			}
		}
		exit(MAGIC_STAT);
	} else {
		/*
		 * Writing archive to the child.
		 * It would like to run either:
		 *	compress
		 *	compress |            dd obs=20b
		 *		   rsh 'host' dd obs=20b '>foo'
		 * or	compress | rsh 'host' dd obs=20b '>foo'
		 *
		 * We need the dd to reblock the output to the
		 * user's specs, if writing to a device or over
		 * the net.  However, it produces a stupid
		 * message about how many blocks it processed.
		 * Because the shell on the remote end could be just
		 * about any shell, we can't depend on it to do
		 * redirect stderr properly for us -- the csh
		 * doesn't use the same syntax as the Bourne shell.
		 * On the other hand, if we just ignore stderr on
		 * this end, we won't see errors from rsh, or from
		 * the inability of "dd" to write its output file.
		 * The combination of the local sh, the rsh, the
		 * remote csh, and maybe a remote sh conspires to mess
		 * up any possible quoting method, so grumble! we
		 * punt and just accept the fucking "xxx blocks"
		 * messages.  The real fix would be a "dd" that
		 * would shut up.
		 * 
		 * Close the write-side of the pipe, which our parent will use.
		 * Move the read-side of the pipe to stdin,
		 * If local, move archive output to the child's stdout.
		 * then run the child.
		 */
		(void) close (pipes[PWRITE]);
		dupto(pipes[PREAD], STDIN, "to stdin");
		if (!rem_host)
			dupto(archive, STDOUT, "to stdout");

		cmdbuf[0] = '\0';
		if (f_compress) {
			if (!rem_host) {
				err = fstat(STDOUT, &arstat);
				if (err != 0) {
					perror("tar: can't fstat archive");
					exit(EX_SYSTEM);
				}
				if ((arstat.st_mode & S_IFMT) == S_IFREG) {
					execlp("compress", "compress", (char *)0);
					perror("tar: cannot exec compress");
				}
			}
			strcat(cmdbuf, "compress | ");
		}
		if (rem_host) {
			sprintf(cmdbuf+strlen(cmdbuf),
			  "rsh '%s' dd obs=%db '>%s'",
				 rem_host, blocking, rem_file);
		} else {
			sprintf(cmdbuf+strlen(cmdbuf),
				"dd obs=%db",
				blocking);
		}
#ifdef DEBUG
		fprintf(stderr, "Exec-ing: %s\n", cmdbuf);
#endif
		execlp("sh", "sh", "-c", cmdbuf, (char *)0);
		perror("tar: cannot exec sh");
		exit(MAGIC_STAT);
	}
#endif	/* AMIGA */
#endif	/* MSDOS */
}


/*
 * Open an archive file.  The argument specifies whether we are
 * reading or writing.
 */
open_archive(read)
	int read;
{
	char *colon, *slash;
	char *rem_host = 0, *rem_file;

#ifndef AMIGA
	colon = index(ar_file, ':');
	if (colon) {
		slash = index(ar_file, '/');
		if (slash && slash > colon) {
			/*
			 * Remote file specified.  Parse out separately,
			 * and don't try to open it on the local system.
			 */
			rem_file = colon + 1;
			rem_host = ar_file;
			*colon = '\0';
			goto gotit;
		}
	}
#endif
	if (ar_file[0] == '-' && ar_file[1] == '\0') {
		f_reblock++;	/* Could be a pipe, be safe */
		if (read)	archive = STDIN;
		else		archive = STDOUT;
	} else if (read) {
		archive = open(ar_file, O_RDONLY);
	} else {
		archive = creat(ar_file, 0666);
	}

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

#ifdef	MSDOS
	setmode(archive, O_BINARY);
#endif

gotit:
	if (blocksize == 0) {
		fprintf(stderr, "tar: invalid value for blocksize\n");
		exit(EX_ARGSBAD);
	}

	/*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;
	ar_reading = read;

	if (f_compress || rem_host) 
		child_open(rem_host, rem_file);

	if (read) {
		ar_last = ar_block;		/* Set up for 1st block = # 0 */
		(void) findrec();		/* Read it in, check for EOF */
	}
}


/*
 * 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.
 *
 * We calculate "offset" then add it because some compilers end up
 * adding (baserec+ar_record), doing a 9-bit shift of baserec, then
 * subtracting ar_block from that, shifting it back, losing the top 9 bits.
 */
saverec(pointer)
	union record **pointer;
{
	long offset;

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

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

	err = write(archive, ar_block->charptr, blocksize);
	if (err == blocksize) return;
	/* FIXME, multi volume support on write goes here */
	if (err < 0)
		perror(ar_file);
	else
		fprintf(stderr, "tar: %s: write failed, short %d bytes\n",
			ar_file, blocksize - err);
	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:
	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;

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 && err > 0) {
			annorec(stderr, tar);
			fprintf(stderr, "Blocksize = %d record%s\n",
				err / RECORDSIZE, (err > RECORDSIZE)? "s": "");
		}
		ar_last = ar_block + ((unsigned)(blocksize - left))/RECORDSIZE;
		return;
	}
	if (f_reblock) {
		/*
		 * User warned us about this.  Fix up.
		 */
		if (left > 0) {
error2loop:
			err = read(archive, more, left);
			if (err < 0) {
				readerror();
				goto error2loop;	/* 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);

#if !defined(MSDOS) && !defined(AMIGA)
	if (childpid) {
		/*
		 * Loop waiting for the right child to die, or for
		 * no more kids.
		 */
		while (((child = wait(&status)) != childpid) && child != -1)
			;

		if (child != -1) {
			switch (TERM_SIGNAL(status)) {
			case 0:
				/* Child voluntarily terminated  -- but why? */
				if (TERM_VALUE(status) == MAGIC_STAT) {
					exit(EX_SYSTEM);/* Child had trouble */
				}
				if (TERM_VALUE(status) == (SIGPIPE + 128)) {
					/*
					 * /bin/sh returns this if its child
					 * dies with SIGPIPE.  'Sok.
					 */
					break;
				} else if (TERM_VALUE(status))
					fprintf(stderr,
				  "tar: child returned status %d\n",
						TERM_VALUE(status));
			case SIGPIPE:
				break;		/* This is OK. */

			default:
				fprintf(stderr,
				 "tar: child died with signal %d%s\n",
				 TERM_SIGNAL(status),
				 TERM_COREDUMP(status)? " (core dumped)": "");
			}
		}
	}
#endif	/* MSDOS */
}


/*
 * 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;
	long	offset;

	/* Make sure previous output gets out in sequence */
	if (stream == stderr)
		fflush(stdout);
	if (f_sayblock) {
		if (prefix) {
			fputs(prefix, stream);
			putc(' ', stream);
		}
		offset = ar_record - ar_block;
		sprintf(buffer, "rec %d: ",
			savedp?	saved_recno:
				baserec + offset);
		fputs(buffer, stream);
		space = ANNOWIDTH - strlen(buffer);
		if (space > 0) {
			fprintf(stream, "%*s", space, "");
		}
	} else if (prefix) {
		fputs(prefix, stream);
		fputs(": ", stream);
	}
}
