/*
 * A public domain tar(1) program.
 * 
 * Written by John Gilmore, ihnp4!hoptoad!gnu, starting 25 Aug 85.
 *
 * @(#)tar.c 1.34 11/6/87 Public Domain - gnu
 */

#include <stdio.h>
#include <sys/types.h>		/* Needed for typedefs in tar.h */

extern char 	*malloc();
extern char 	*getenv();
extern char	*strncpy();
extern char	*optarg;	/* Pointer to argument */
extern int	optind;		/* Global argv index from getopt */

/*
 * The following causes "tar.h" to produce definitions of all the
 * global variables, rather than just "extern" declarations of them.
 */
#define TAR_EXTERN /**/
#include "tar.h"

/*
 * We should use a conversion routine that does reasonable error
 * checking -- atoi doesn't.  For now, punt.  FIXME.
 */
#define intconv	atoi
extern int	getoldopt();
extern void	read_and();
extern void	list_archive();
extern void	extract_archive();
extern void	diff_archive();
extern void	create_archive();

static FILE	*namef;		/* File to read names from */
static char	**n_argv;	/* Argv used by name routines */
static int	n_argc;	/* Argc used by name routines */
				/* They also use "optind" from getopt(). */

void	describe();

#ifdef AMIGA
extern char *_TZ;		/* timezone stuff */
#endif

/*
 * Main routine for tar.
 */
main(argc, argv)
	int	argc;
	char	**argv;
{

	/* Uncomment this message in particularly buggy versions...
	fprintf(stderr,
	 "tar: You are running an experimental PD tar, maybe use /bin/tar.\n");
	 */

	tar = "tar";		/* Set program name */

	options(argc, argv);

	name_init(argc, argv);

	if (f_create) {
		if (f_extract || f_list || f_diff) goto dupflags;
		create_archive();
	} else if (f_extract) {
		if (f_list || f_diff) goto dupflags;
		extr_init();
		read_and(extract_archive);
	} else if (f_list) {
		if (f_diff) goto dupflags;
		read_and(list_archive);
	} else if (f_diff) {
		diff_init();
		read_and(diff_archive);
	} else {
dupflags:
		fprintf (stderr,
"tar: you must specify exactly one of the c, t, x, or d options\n");
		describe();
		exit(EX_ARGSBAD);
	}
	exit(0);
	/* NOTREACHED */
}


/*
 * Parse the options for tar.
 */
int
options(argc, argv)
	int	argc;
	char	**argv;
{
	register int	c;		/* Option letter */
#ifdef AMIGA
	char *timezone;
#endif

	/* Set default option values */
	blocking = DEFBLOCKING;		/* From Makefile */
	ar_file = getenv("TAPE");	/* From environment, or */
#ifdef AMIGA
	timezone = getenv("TZ");
	if (timezone)
	{
	    _TZ = malloc(strlen(timezone) + 1);	/* man page unclear about */
	    strcpy(_TZ, timezone);		/* whether this is needed */
	    free(timezone);
	}
	else
	    _TZ = "CST6CDT";
	tzset();
#endif
	if (ar_file == 0)
#ifdef AMIGA
		ar_file = "ram:tarfile";	/* From Makefile */
#else
		ar_file = DEF_AR_FILE;	/* From Makefile */
#endif
	/* Parse options */
#ifdef AMIGA
	while ((c = getoldopt(argc, argv, "aAb:BcdDf:hiklmopRstT:vxzZ")
#else
	while ((c = getoldopt(argc, argv, "b:BcdDf:hiklmopRstT:vxzZ")
#endif
		) != EOF) {
		switch (c) {

#ifdef AMIGA
		case 'a':
			f_archive_set++;
			break;

		case 'A':
			f_archive_check++;
			break;
#endif

		case 'b':
			blocking = intconv(optarg);
			break;

		case 'B':
			f_reblock++;		/* For reading 4.2BSD pipes */
			break;

		case 'c':
			f_create++;
			break;

		case 'd':
			f_diff++;		/* Find difference tape/disk */
			break;

		case 'D':
			f_dironly++;		/* Dump dir, not contents */
			break;

		case 'f':
			ar_file = optarg;
			break;

		case 'h':
			f_follow_links++;	/* follow symbolic links */
			break;

		case 'i':
			f_ignorez++;		/* Ignore zero records (eofs) */
			/*
			 * This can't be the default, because Unix tar
			 * writes two records of zeros, then pads out the
			 * block with garbage.
			 */
			break;

		case 'k':			/* Don't overwrite files */
#ifdef NO_OPEN3
			fprintf(stderr,
				"tar: can't do -k option on this system\n");
			exit(EX_ARGSBAD);
#else
			f_keep++;
#endif
			break;

		case 'l':
			f_local_filesys++;
			break;

		case 'm':
			f_modified++;
			break;

		case 'o':			/* Generate old archive */
			f_oldarch++;
			break;

		case 'p':
			f_use_protection++;
			break;

		case 'R':
			f_sayblock++;		/* Print block #s for debug */
			break;			/* of bad tar archives */

		case 's':
			f_sorted_names++;	/* Names to extr are sorted */
			break;

		case 't':
			f_list++;
			f_verbose++;		/* "t" output == "cv" or "xv" */
			break;

		case 'T':
			name_file = optarg;
			f_namefile++;
			break;

		case 'v':
			f_verbose++;
			break;

		case 'x':
			f_extract++;
			break;

		case 'z':		/* Easy to type */
		case 'Z':		/* Like the filename extension .Z */
			f_compress++;
			break;

		case '?':
			describe();
			exit(EX_ARGSBAD);

		}
	}

	blocksize = blocking * RECORDSIZE;
}


/* 
 * Print as much help as the user's gonna get.
 *
 * We have to sprinkle in the KLUDGE lines because too many compilers
 * cannot handle character strings longer than about 512 bytes.  Yuk!
 * In particular, MSDOS MSC 4.0 (and 5.0) and PDP-11 V7 Unix have this
 * problem.
 */
void
describe()
{
	/* sorry, can't #ifdef AMIGA inside string */
	fputs("\
tar: valid options:\n\
-a	set the archived bit of each file as it's added to the archive\n\
-b N	blocking factor N (block size = Nx512 bytes)\n\
-B	reblock as we read (for reading 4.2BSD pipes)\n\
-c	create an archive\n\
-d	find differences between archive and file system\n\
-D	don't dump the contents of directories, just the directory\n\
", stderr); /* KLUDGE */ fputs("\
-f F	read/write archive from file or device F (or hostname:/ForD)\n\
-h	don't dump symbolic links; dump the files they point to\n\
-i	ignore blocks of zeros in the archive, which normally mean EOF\n\
-k	keep existing files, don't overwrite them from the archive\n\
-l	stay in the local file system (like dump(8)) when creating an archive\n\
", stderr); /* KLUDGE */ fputs("\
-m	don't extract file modified time\n\
-o	write an old V7 format archive, rather than ANSI [draft 6] format\n\
-p	do extract all protection information\n\
-R	dump record number within archive with each message\n\
-s	list of names to extract is sorted to match the archive\n\
-t	list a table of contents of an archive\n\
", stderr); /* KLUDGE */ fputs("\
-T F	get names to extract or create from file F\n\
-v	verbosely list what files we process\n\
-x	extract files from an archive\n\
-z or Z	run the archive through compress(1)\n\
", stderr);
}


/*
 * Set up to gather file names for tar.
 *
 * They can either come from stdin or from argv.
 */
name_init(argc, argv)
	int	argc;
	char	**argv;
{

	if (f_namefile) {
		if (optind < argc) {
			fprintf(stderr, "tar: too many args with -T option\n");
			exit(EX_ARGSBAD);
		}
		if (!strcmp(name_file, "-")) {
			namef = stdin;
		} else {
			namef = fopen(name_file, "r");
			if (namef == NULL) {
				fprintf(stderr, "tar: ");
				perror(name_file);
				exit(EX_BADFILE);
			}
		}
	} else {
		/* Get file names from argv, after options. */
		n_argc = argc;
		n_argv = argv;
	}
}

/*
 * Get the next name from argv or the name file.
 *
 * Result is in static storage and can't be relied upon across two calls.
 */
char *
name_next()
{
	static char	buffer[NAMSIZ+2];	/* Holding pattern */
	register char	*p;
	register char	*q;

	if (namef == NULL) {
		/* Names come from argv, after options */
		if (optind < n_argc)
			return n_argv[optind++];
		return (char *)NULL;
	}
	for (;;) {
		p = fgets(buffer, NAMSIZ+1 /*nl*/, namef);
		if (p == NULL) return p;	/* End of file */
		q = p+strlen(p)-1;		/* Find the newline */
		if (q <= p) continue;		/* Ignore empty lines */
		*q-- = '\0';			/* Zap the newline */
		while (q > p && *q == '/')  *q-- = '\0'; /* Zap trailing /s */
		return p;
	}
	/* NOTREACHED */
}


/*
 * Close the name file, if any.
 */
name_close()
{

	if (namef != NULL && namef != stdin) fclose(namef);
}


/*
 * Gather names in a list for scanning.
 * Could hash them later if we really care.
 *
 * If the names are already sorted to match the archive, we just
 * read them one by one.  name_gather reads the first one, and it
 * is called by name_match as appropriate to read the next ones.
 * At EOF, the last name read is just left in the buffer.
 * This option lets users of small machines extract an arbitrary
 * number of files by doing "tar t" and editing down the list of files.
 */
name_gather()
{
	register char *p;
	static struct name namebuf[1];	/* One-name buffer */

	if (f_sorted_names) {
		p = name_next();
		if (p) {
			namebuf[0].length = strlen(p);
			if (namebuf[0].length >= sizeof namebuf[0].name) {
				fprintf(stderr, "Argument name too long: %s\n",
					p);
				namebuf[0].length = (sizeof namebuf[0].name) - 1;
			}
			strncpy(namebuf[0].name, p, namebuf[0].length);
			namebuf[0].name[ namebuf[0].length ] = 0;
			namebuf[0].next = (struct name *)NULL;
			namebuf[0].found = 0;
			namelist = namebuf;
			namelast = namelist;
		}
		return;
	}

	/* Non sorted names -- read them all in */
	while (NULL != (p = name_next())) {
		addname(p);
	}
}

/*
 * Add a name to the namelist.
 */
addname(name)
	char	*name;			/* pointer to name */
{
	register int	i;		/* Length of string */
	register struct name	*p;	/* Current struct pointer */

	i = strlen(name);
	/*NOSTRICT*/
	p = (struct name *)
		malloc((unsigned)(i + sizeof(struct name) - NAMSIZ));
	if (!p) {
		fprintf(stderr,"tar: cannot allocate mem for namelist entry\n");
		exit(EX_SYSTEM);
	}
	p->next = (struct name *)NULL;
	p->length = i;
	strncpy(p->name, name, i);
	p->name[i] = '\0';	/* Null term */
	p->found = 0;
	p->regexp = 0;		/* Assume not a regular expression */
	p->firstch = 1;		/* Assume first char is literal */
	if (index(name, '*') || index(name, '[') || index(name, '?')) {
		p->regexp = 1;	/* No, it's a regexp */
		if (name[0] == '*' || name[0] == '[' || name[0] == '?')
			p->firstch = 0;		/* Not even 1st char literal */
	}

	if (namelast) namelast->next = p;
	namelast = p;
	if (!namelist) namelist = p;
}


/*
 * Match a name from an archive, p, with a name from the namelist.
 */
name_match(p)
	register char *p;
{
	register struct name	*nlp;
	register int		len;

again:
	if (0 == (nlp = namelist))	/* Empty namelist is easy */
		return 1;
	len = strlen(p);
	for (; nlp != 0; nlp = nlp->next) {
		/* If first chars don't match, quick skip */
		if (nlp->firstch && nlp->name[0] != p[0])
			continue;

		/* Regular expressions */
		if (nlp->regexp) {
			if (wildmat(p, nlp->name)) {
				nlp->found = 1;	/* Remember it matched */
				return 1;	/* We got a match */
			}
			continue;
		}

		/* Plain Old Strings */
		if (nlp->length <= len		/* Archive len >= specified */
		 && (p[nlp->length] == '\0' || p[nlp->length] == '/')
						/* Full match on file/dirname */
		 && strncmp(p, nlp->name, nlp->length) == 0) /* Name compare */
		{
			nlp->found = 1;		/* Remember it matched */
			return 1;		/* We got a match */
		}
	}

	/*
	 * Filename from archive not found in namelist.
	 * If we have the whole namelist here, just return 0.
	 * Otherwise, read the next name in and compare it.
	 * If this was the last name, namelist->found will remain on.
	 * If not, we loop to compare the newly read name.
	 */
	if (f_sorted_names && namelist->found) {
		name_gather();		/* Read one more */
		if (!namelist->found) goto again;
	}
	return 0;
}


/*
 * Print the names of things in the namelist that were not matched.
 */
names_notfound()
{
	register struct name	*nlp;
	register char		*p;

	for (nlp = namelist; nlp != 0; nlp = nlp->next) {
		if (!nlp->found) {
			fprintf(stderr, "tar: %s not found in archive\n",
				nlp->name);
		}
		/*
		 * We could free() the list, but the process is about
		 * to die anyway, so save some CPU time.  Amigas and
		 * other similarly broken software will need to waste
		 * the time, though.
		 */
#ifndef unix
		if (!f_sorted_names)
			free(nlp);
#endif
	}
	namelist = (struct name *)NULL;
	namelast = (struct name *)NULL;

	if (f_sorted_names) {
		while (0 != (p = name_next()))
			fprintf(stderr, "tar: %s not found in archive\n", p);
	}
}
