/*
 * Extract files from a tar archive.
 *
 * Written 19 Nov 1985 by John Gilmore, ihnp4!hoptoad!gnu.
 *
 * @(#) extract.c 1.32 87/11/11 Public Domain - gnu
 */

#include <stdio.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>

#ifdef BSD42
#include <sys/file.h>
#endif

#ifdef USG
#include <fcntl.h>
#endif

#if defined(MSDOS) || defined(AMIGA)
#include <fcntl.h>
#endif	/* MSDOS */

/*
 * Some people don't have a #define for these.
 */
#ifndef	O_BINARY
#define	O_BINARY	0
#endif
#ifndef O_NDELAY
#define	O_NDELAY	0
#endif

#ifdef NO_OPEN3
/* We need the #define's even though we don't use them. */
#include "open3.h"
#endif

#ifdef EMUL_OPEN3
/* Simulated 3-argument open for systems that don't have it */
#include "open3.h"
#endif

extern int errno;			/* From libc.a */
extern time_t time();			/* From libc.a */
extern char *index();			/* From libc.a or port.c */

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

extern union record *head;		/* Points to current tape header */
extern struct stat hstat;		/* Stat struct corresponding */
extern int head_standard;		/* Tape header is in ANSI format */

extern void print_header();
extern void skip_file();
extern void pr_mkdir();

int make_dirs();			/* Makes required directories */
#ifdef AMIGA
extern int made_on_amiga;		/* decode_header() sets this if tar
					 * was made on Amiga.  Passing this
					 * as a param would change too much
					 * code so we make it a global */
#endif

static time_t now = 0;			/* Current time */
static we_are_root = 0;			/* True if our effective uid == 0 */
static int notumask = ~0;		/* Masks out bits user doesn't want */

/*
 * Set up to extract files.
 */
extr_init()
{
	int ourmask;

	now = time((time_t *)0);
	if (geteuid() == 0)
		we_are_root = 1;

	/*
	 * We need to know our umask.  But if f_use_protection is set,
	 * leave our kernel umask at 0, and our "notumask" at ~0.
	 */
	ourmask = umask(0);		/* Read it */
	if (!f_use_protection) {
		(void) umask (ourmask);	/* Set it back how it was */
		notumask = ~ourmask;	/* Make umask override permissions */
	}
}


/*
 * Extract a file from the archive.
 */
void
extract_archive()
{
	register char *data;
	int fd, check, namelen, written, openflag;
	long size;
	time_t acc_upd_times[2];
	register int skipcrud;
#ifdef AMIGA
	int protection;		/* protection bits */
#endif

	saverec(&head);			/* Make sure it sticks around */
	userec(head);			/* And go past it in the archive */
	decode_header(head, &hstat, &head_standard, 1);	/* Snarf fields */

	/* Print the record from 'head' and 'hstat' */
	if (f_verbose)
		print_header(stdout);

	/*
	 * Check for fully specified pathnames and other atrocities.
	 *
	 * Note, we can't just make a pointer to the new file name,
	 * since saverec() might move the header and adjust "head".
	 * We have to start from "head" every time we want to touch
	 * the header record.
	 */
	skipcrud = 0;
	while ('/' == head->header.name[skipcrud]) {
		static int warned_once = 0;

		skipcrud++;	/* Force relative path */
		if (!warned_once++) {
			annorec(stderr, tar);
			fprintf(stderr,
	"Removing leading / from absolute path names in the archive.\n");
		}
	}
#ifdef AMIGA
	cvtUNIX2Ami(skipcrud + head->header.name);
#endif
	switch (head->header.linkflag) {

	default:
		annofile(stderr, tar);
		fprintf(stderr,
		   "Unknown file type '%c' for %s, extracted as normal file\n",
			head->header.linkflag, skipcrud+head->header.name);
		/* FALL THRU */

	case LF_OLDNORMAL:
	case LF_NORMAL:
	case LF_CONTIG:
		/*
		 * Appears to be a file.
		 * See if it's really a directory.
		 */
		namelen = strlen(skipcrud+head->header.name)-1;
		if (head->header.name[skipcrud+namelen] == '/')
			goto really_dir;

		/* FIXME, deal with protection issues */
	again_file:
#if defined(AMIGA) && defined(LATTICE)	/* open w/O_EXCL is broken, sigh */
		openflag = O_BINARY|O_NDELAY|O_WRONLY|O_APPEND|O_CREAT|O_TRUNC;
#else
		openflag = f_keep?
			O_BINARY|O_NDELAY|O_WRONLY|O_APPEND|O_CREAT|O_EXCL:
			O_BINARY|O_NDELAY|O_WRONLY|O_APPEND|O_CREAT|O_TRUNC;
#endif

#ifdef O_CTG
		/*
		 * Contiguous files (on the Masscomp) have to specify
		 * the size in the open call that creates them.
		 */
		if (head->header.lnkflag == LF_CONTIG)
			fd = open(skipcrud+head->header.name, openflag | O_CTG,
				hstat.st_mode, hstat.st_size);
		else
#endif
		{
#ifdef NO_OPEN3
			/*
			 * On raw V7 we won't let them specify -k (f_keep), but
			 * we just bull ahead and create the files.
			 */
			fd = creat(skipcrud+head->header.name, 
				hstat.st_mode);
#else
			/*
			 * With 3-arg open(), we can do this up right.
			 */
#if defined(AMIGA) && defined(LATTICE)
			if (f_keep && !access(skipcrud+head->header.name,
					     O_RDONLY))
			{
				fd = -1;
				errno = EEXIST;
			}
			else
				fd = open(skipcrud+head->header.name, openflag,
					  hstat.st_mode);
#endif
#endif
		}

		if (fd < 0) {
			if (make_dirs(skipcrud+head->header.name))
				goto again_file;
			annofile(stderr, tar);
			fprintf(stderr, "Could not make file ");
			perror(skipcrud+head->header.name);
			skip_file((long)hstat.st_size);
			goto quit;
		}

		for (size = hstat.st_size;
		     size > 0;
		     size -= written) {
			/*
			 * Locate data, determine max length
			 * writeable, write it, record that
			 * we have used the data, then check
			 * if the write worked.
			 */
			data = findrec()->charptr;
			if (data == NULL) {	/* Check it... */
				annorec(stderr, tar);
				fprintf(stderr, "Unexpected EOF on archive file\n");
				break;
			}
			written = endofrecs()->charptr - data;
			if (written > size) written = size;
			errno = 0;
			check = write(fd, data, written);
			/*
			 * The following is in violation of strict
			 * typing, since the arg to userec
			 * should be a struct rec *.  FIXME.
			 */
			userec(data + written - 1);
			if (check == written) continue;
			/*
			 * Error in writing to file.
			 * Print it, skip to next file in archive.
			 */
			annofile(stderr, tar);
			fprintf(stderr,
	"Tried to write %d bytes to file, could only write %d:\n",
				written, check);
			perror(skipcrud+head->header.name);
			skip_file((long)(size - written));
			break;	/* Still do the close, mod time, chmod, etc */
		}

		check = close(fd);
		if (check < 0) {
			annofile(stderr, tar);
			fprintf(stderr, "Error while closing ");
			perror(skipcrud+head->header.name);
		}
		
	set_filestat:
#ifdef AMIGA
		/*
		 * Set the comment on the file
		 */
		if (made_on_amiga)
		{
		    if (SmartSetComment(skipcrud+head->header.name,
					hstat.st_comment) < 0)
		    {
			annofile(stderr, tar);
			fputs("failed to set comment on ", stderr);
			perror(skipcrud+head->header.name);
		    }
		}
#endif
		/*
		 * Set the modified time of the file.
		 * 
		 * Note that we set the accessed time to "now", which
		 * is really "the time we started extracting files".
		 */
		if (!f_modified) {
			acc_upd_times[0] = now;	         /* Accessed now */
			acc_upd_times[1] = hstat.st_mtime; /* Mod'd */
#ifdef AMIGA
			if (made_on_amiga) {
				if (utime_from_stamp(skipcrud+head->header.name,
				    &(hstat.st_date)) < 0) {
					annofile(stderr, tar);
					fputs("utime failed:", stderr);
					perror(skipcrud+head->header.name);
				}
			} else {
#endif
			if (utime(skipcrud+head->header.name,
			    acc_upd_times) < 0) {
				annofile(stderr, tar);
				fputs("utime failed:", stderr);
				perror(skipcrud+head->header.name);
			}
#ifdef AMIGA
			}
#endif
		}

		/*
		 * If we are root, set the owner and group of the extracted
		 * file.  This does what is wanted both on real Unix and on
		 * System V.  If we are running as a user, we extract as that
		 * user; if running as root, we extract as the original owner.
		 */
		if (we_are_root) {
			if (chown(skipcrud+head->header.name, hstat.st_uid,
				  hstat.st_gid) < 0) {
				annofile(stderr, tar);
				perror(skipcrud+head->header.name);
			}
		}

		/*
		 * If '-k' is not set, open() or creat() could have saved
		 * the permission bits from a previously created file,
		 * ignoring the ones we specified.
		 * Even if -k is set, if the file has abnormal
		 * mode bits, we must chmod since writing or chown() has
		 * probably reset them.
		 *
		 * If -k is set, we know *we* created this file, so the mode
		 * bits were set by our open().   If the file is "normal", we
		 * skip the chmod.  This works because we did umask(0) if -p
		 * is set, so umask will have left the specified mode alone.
		 */
#ifdef AMIGA
		/*
		 * open() ignores modes, so always do this unless
		 */
		if (made_on_amiga)
		    protection = hstat.st_prot & ~FIBF_ARCHIVE;
		else
		    protection = ~((hstat.st_mode >> 5) | 1) & 0xf;
		if (SmartSetProtection(skipcrud+head->header.name,
				       protection) < 0)
		{
		    annofile(stderr, tar);
		    fputs("failed to set protection on ", stderr);
		    perror(skipcrud+head->header.name);
		}
#else
		if ((!f_keep)
		    || (hstat.st_mode & (S_ISUID|S_ISGID|S_ISVTX))) {
			if (chmod(skipcrud+head->header.name,
				  notumask & (int)hstat.st_mode) < 0) {
				annofile(stderr, tar);
				perror(skipcrud+head->header.name);
			}
		}
#endif

	quit:
		break;

	case LF_LINK:
	again_link:
		check = link (head->header.linkname,
			      skipcrud+head->header.name);
		if (check == 0)
			break;
		if (make_dirs(skipcrud+head->header.name))
			goto again_link;
		annofile(stderr, tar);
		fprintf(stderr, "Could not link %s to ",
			skipcrud+head->header.name);
		perror(head->header.linkname);
		break;

#ifdef S_IFLNK
	case LF_SYMLINK:
	again_symlink:
		check = symlink(head->header.linkname,
			        skipcrud+head->header.name);
		/* FIXME, don't worry uid, gid, etc... */
		if (check == 0)
			break;
		if (make_dirs(skipcrud+head->header.name))
			goto again_symlink;
		annofile(stderr, tar);
		fprintf(stderr, "Could not create symlink ");
		perror(head->header.linkname);
		break;
#endif

#ifdef S_IFCHR
	case LF_CHR:
		hstat.st_mode |= S_IFCHR;
		goto make_node;
#endif

#ifdef S_IFBLK
	case LF_BLK:
		hstat.st_mode |= S_IFBLK;
		goto make_node;
#endif

#ifdef S_IFIFO
	/* If local system doesn't support FIFOs, use default case */
	case LF_FIFO:
		hstat.st_mode |= S_IFIFO;
		hstat.st_rdev = 0;		/* FIXME, do we need this? */
		goto make_node;
#endif

	make_node:
		check = mknod(skipcrud+head->header.name,
			      (int) hstat.st_mode, (int) hstat.st_rdev);
		if (check != 0) {
			if (make_dirs(skipcrud+head->header.name))
				goto make_node;
			annofile(stderr, tar);
			fprintf(stderr, "Could not make ");
			perror(skipcrud+head->header.name);
			break;
		};
		goto set_filestat;

	case LF_DIR:
		namelen = strlen(skipcrud+head->header.name)-1;
	really_dir:
		/* Check for trailing /, and zap as many as we find. */
		while (namelen && head->header.name[skipcrud+namelen] == '/')
			head->header.name[skipcrud+namelen--] = '\0';
		
	again_dir:
#ifdef AMIGA
		/* don't mkdir("", mode) */
		check = 0;
		if (*(skipcrud+head->header.name))
#endif
		check = mkdir(skipcrud+head->header.name,
			      0300 | (int)hstat.st_mode);
		if (check != 0) {
			if (make_dirs(skipcrud+head->header.name))
				goto again_dir;
			/* If we're trying to create '.', let it be. */
			if (head->header.name[skipcrud+namelen] == '.' && 
			    (namelen==0 ||
			     head->header.name[skipcrud+namelen-1]=='/'))
				goto check_perms;
			annofile(stderr, tar);
			fprintf(stderr, "Could not make directory ");
			perror(skipcrud+head->header.name);
			break;
		}
		
	check_perms:
		if (0300 != (0300 & (int) hstat.st_mode)) {
			hstat.st_mode |= 0300;
			annofile(stderr, tar);
			fprintf(stderr,
			  "Added write & execute permission to directory %s\n",
			  skipcrud+head->header.name);
		}

		goto set_filestat;
		/* FIXME, Remember timestamps for after files created? */
		/* FIXME, change mode after files created (if was R/O dir) */

	}

	/* We don't need to save it any longer. */
	saverec((union record **) 0);	/* Unsave it */
}

/*
 * After a file/link/symlink/dir creation has failed, see if
 * it's because some required directory was not present, and if
 * so, create all required dirs.
 */
int
make_dirs(pathname)
	char *pathname;
{
	char *p;			/* Points into path */
	int madeone = 0;		/* Did we do anything yet? */
	int save_errno = errno;		/* Remember caller's errno */
	int check;

	if (errno != ENOENT)
		return 0;		/* Not our problem */
#if defined(AMIGA) && defined(LATTICE)
	/*
	 * ARRRRGGGGHHHH!  If delete bit is not set, open fails with ENOENT.
	 * I think it should be EPERM.  So we check for the file's existance
	 * here and return if it does exist.
	 */
	if (!access(pathname, O_RDONLY))
	{
	    errno = EPERM;		/* perror should say "Permission Denied"
					 * but, says something stupid instead */
	    return(0);			/* Not our problem */
	}
#endif
	for (p = index(pathname, '/'); p != NULL; p = index(p+1, '/')) {
		/* Avoid mkdir of empty string, if leading or double '/' */
		if (p == pathname || p[-1] == '/')
			continue;
		/* Avoid mkdir where last part of path is '.' */
		if (p[-1] == '.' && (p == pathname+1 || p[-2] == '/'))
			continue;
		*p = 0;				/* Truncate the path there */
		check = mkdir (pathname, 0777);	/* Try to create it as a dir */
		if (check == 0) {
			/* Fix ownership */
			if (we_are_root) {
				if (chown(pathname, hstat.st_uid,
					  hstat.st_gid) < 0) {
					annofile(stderr, tar);
					perror(pathname);
				}
			}
			pr_mkdir(pathname, p-pathname, notumask&0777, stdout);
			madeone++;		/* Remember if we made one */
			*p = '/';
			continue;
		}
		*p = '/';
		if (errno == EEXIST)		/* Directory already exists */
			continue;
		/*
		 * Some other error in the mkdir.  We return to the caller.
		 */
		break;
	}

	errno = save_errno;		/* Restore caller's errno */
	return madeone;			/* Tell them to retry if we made one */
}
