/*
 * Create a tar archive.
 *
 * Written 25 Aug 1985 by John Gilmore, ihnp4!hoptoad!gnu.
 *
 * @(#)create.c 1.36 11/6/87 Public Domain - gnu
 */
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>

#ifndef V7
#include <fcntl.h>
#endif

#if !defined(MSDOS) && !defined(AMIGA)
#include <pwd.h>
#include <grp.h>
#endif

#ifdef BSD42
#include <sys/dir.h>
#else
#ifdef MSDOS
#include <sys/dir.h>
#else
#ifdef AMIGA
#include <dirent.h>
#else
/*
 * FIXME: On other systems there is no standard place for the header file
 * for the portable directory access routines.  Change the #include line
 * below to bring it in from wherever it is.
 */
#include "ndir.h"
#endif
#endif
#endif

#ifdef USG
#include <sys/sysmacros.h>	/* major() and minor() defined here */
#endif

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

/*
 * Most people don't have a #define for this.
 */
#ifndef	O_BINARY
#define	O_BINARY	0
#endif

#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 */

/*
 * If there are no symbolic links, there is no lstat().  Use stat().
 */
#ifndef S_IFLNK
#define lstat stat
#endif

extern char	*malloc();
extern char	*strcpy();
extern char	*strncpy();
extern void	bzero();
extern void	bcopy();
extern int	errno;

extern void print_header();

union record *start_header();
void finish_header();
void finduname();
void findgname();
char *name_next();
void to_oct(), to_hex();
void dump_file();

static nolinks;			/* Gets set if we run out of RAM */

void
create_archive()
{
	register char	*p;

	open_archive(0);		/* Open for writing */

	while (p = name_next()) {
#ifdef AMIGA
		if (!strcmp(p, "."))	/* convert . and ./ to "" */
		    p++;
		else if (!strcmp(p, "./"))
		    p += 2;
#endif
		dump_file(p, -1);
	}

	write_eot();
	close_archive();
	name_close();
}		

/*
 * Dump a single file.  If it's a directory, recurse.
 * Result is 1 for success, 0 for failure.
 * Sets global "hstat" to stat() output for this file.
 */
void
dump_file(p, curdev)
	char	*p;			/* File name to dump */
	int	curdev;			/* Device our parent dir was on */
{
	union record	*header;
	char type;

	/*
	 * Use stat if following (rather than dumping) 4.2BSD's
	 * symbolic links.  Otherwise, use lstat (which, on non-4.2
	 * systems, is #define'd to stat anyway.
	 */
	if (0 != f_follow_links? stat(p, &hstat): lstat(p, &hstat))
	{
badperror:
		perror(p);
badfile:
		errors++;
		return;
	}

	/*
	 * See if we are crossing from one file system to another,
	 * and avoid doing so if the user only wants to dump one file system.
	 */
	if (f_local_filesys && curdev >= 0 && curdev != hstat.st_dev) {
		annorec(stderr, tar);
		fprintf(stderr,
			"%s: is on a different filesystem; not dumped\n",
			p);
		return;
	}
#ifdef AMIGA
	/*
	 * If f_archive_check is set, check the archived bit of the
	 * file and don't archive it if the bit is set and it's a normal
  	 * file.
	 */
	if (f_archive_check && (hstat.st_prot & FIBF_ARCHIVE) &&
	    !(hstat.st_mode & S_IFDIR))
	{
	    if (f_verbose > 1)
	    {
		annorec(stderr, tar);
		fprintf(stderr, "%s: archive bit set, not dumping\n", p);
	    }
	    return;
	}	
#endif

	/*
	 * Check for multiple links.
	 *
	 * We maintain a list of all such files that we've written so
	 * far.  Any time we see another, we check the list and
	 * avoid dumping the data again if we've done it once already.
	 */
	if (hstat.st_nlink > 1) switch (hstat.st_mode & S_IFMT) {
		register struct link	*lp;

	case S_IFREG:			/* Regular file */
#ifdef S_IFCTG
	case S_IFCTG:			/* Contigous file */
#endif
#ifdef S_IFCHR
	case S_IFCHR:			/* Character special file */
#endif

#ifdef S_IFBLK
	case S_IFBLK:			/* Block     special file */
#endif

#ifdef S_IFIFO
	case S_IFIFO:			/* Fifo      special file */
#endif

		/* First quick and dirty.  Hashing, etc later FIXME */
		for (lp = linklist; lp; lp = lp->next) {
			if (lp->ino == hstat.st_ino &&
			    lp->dev == hstat.st_dev) {
				/* We found a link. */
				hstat.st_size = 0;
				header = start_header(p, &hstat);
				if (header == NULL) goto badfile;
				strcpy(header->header.linkname,
					lp->name);
				header->header.linkflag = LF_LINK;
				finish_header(header);
		/* FIXME: Maybe remove from list after all links found? */
				return;		/* We dumped it */
			}
		}

		/* Not found.  Add it to the list of possible links. */
		lp = (struct link *) malloc( (unsigned)
			(strlen(p) + sizeof(struct link) - NAMSIZ));
		if (!lp) {
			if (!nolinks) {
				fprintf(stderr,
	"tar: no memory for links, they will be dumped as separate files\n");
				nolinks++;
			}
		}
		lp->ino = hstat.st_ino;
		lp->dev = hstat.st_dev;
		strcpy(lp->name, p);
		lp->next = linklist;
		linklist = lp;
	}

	/*
	 * This is not a link to a previously dumped file, so dump it.
	 */
	switch (hstat.st_mode & S_IFMT) {

	case S_IFREG:			/* Regular file */
#ifdef S_IFCTG
	case S_IFCTG:			/* Contigous file */
#endif
	{
		int	f;		/* File descriptor */
		int	bufsize, count;
		register long	sizeleft;
		register union record 	*start;

		sizeleft = hstat.st_size;
		/* Don't bother opening empty, world readable files. */
		if (sizeleft > 0 || 0444 != (0444 & hstat.st_mode)) {
			f = open(p, O_RDONLY|O_BINARY);
			if (f < 0) goto badperror;
		} else {
			f = -1;
		}
		header = start_header(p, &hstat);
		if (header == NULL) goto badfile;
#ifdef S_IFCTG
		/* Mark contiguous files, if we support them */
		if (f_standard && (hstat.st_mode & S_IFMT) == S_IFCTG) {
			header->header.linkflag = LF_CONTIG;
		}
#endif
		finish_header(header);
		while (sizeleft > 0) {
			start = findrec();
			bufsize = endofrecs()->charptr - start->charptr;
			if (sizeleft < bufsize) {
				/* Last read -- zero out area beyond */
				bufsize = (int)sizeleft;
				count = bufsize % RECORDSIZE;
				if (count) 
					bzero(start->charptr + sizeleft,
						RECORDSIZE - count);
			}
			count = read(f, start->charptr, bufsize);
			if (count < 0) {
				annorec(stderr, tar);
				fprintf(stderr,
				  "read error at byte %ld, reading %d bytes, in file ",
					hstat.st_size - sizeleft,
					bufsize);
				perror(p);	/* FIXME */
				goto padit;
			}
			sizeleft -= count;
			/* This is nonportable (the type of userec's arg). */
			userec(start+(count-1)/RECORDSIZE);
			if (count == bufsize) continue;
			annorec(stderr, tar);
			fprintf(stderr,
			  "%s: file shrunk by %d bytes, padding with zeros.\n",
				p, sizeleft);
			goto padit;		/* Short read */
		}
		if (f >= 0)
			(void)close(f);
#ifdef AMIGA
		if (f_archive_set)
		{
		    if (SmartSetProtection(p, hstat.st_prot | 
			FIBF_ARCHIVE) < 0)
		    {
		        fputs("tar: Error setting archive bit on ", stderr);
			perror(p);
		    }
		}
#endif		
		break;

		/*
		 * File shrunk or gave error, pad out tape to match
		 * the size we specified in the header.
		 */
	padit:
		abort();
	}

#ifdef S_IFLNK
	case S_IFLNK:			/* Symbolic link */
	{
		int size;

		hstat.st_size = 0;		/* Force 0 size on symlink */
		header = start_header(p, &hstat);
		if (header == NULL) goto badfile;
		size = readlink(p, header->header.linkname, NAMSIZ);
		if (size < 0) goto badperror;
		if (size == NAMSIZ) {
			annorec(stderr, tar);
			fprintf(stderr,
				"%s: symbolic link too long\n", p);
			break;
		}
		header->header.linkname[size] = '\0';
		header->header.linkflag = LF_SYMLINK;
		finish_header(header);		/* Nothing more to do to it */
	}
		break;
#endif

	case S_IFDIR:			/* Directory */
	{
		register DIR *dirp;
		register struct dirent *d;
		char namebuf[NAMSIZ+2];
		register int len;
		int our_device = hstat.st_dev;

		/* Build new prototype name */
		strncpy(namebuf, p, sizeof (namebuf));
#ifdef AMIGA
		/*
		 * If user types "" (current dir), make namebuf be ./ - this
		 * will later be stripped.
		 */
		if (!*namebuf) {
			strcpy(namebuf, "./");
		}
#endif
		len = strlen(namebuf);
		while (len >= 1 && '/' == namebuf[len-1]) 
			len--;			/* Delete trailing slashes */
#ifdef AMIGA
		if (namebuf[len - 1] != ':')     /* no / after : */
		    namebuf[len++] = '/';	/* Now add exactly one back */
#else
		namebuf[len++] = '/';		/* Now add exactly one back */
#endif
		namebuf[len] = '\0';		/* Make sure null-terminated */
		/*
		 * Output directory header record with permissions
		 * FIXME, do this AFTER files, to avoid R/O dir problems?
		 * If old archive format, don't write record at all.
		 */
		if (!f_oldarch) {
			hstat.st_size = 0;	/* Force 0 size on dir */
			/*
			 * If people could really read standard archives,
			 * this should be:		(FIXME)
			header = start_header(f_standard? p: namebuf, &hstat);
			 * but since they'd interpret LF_DIR records as
			 * regular files, we'd better put the / on the name.
			 */
			header = start_header(namebuf, &hstat);
			if (header == NULL)
				goto badfile;	/* eg name too long */
			if (f_standard) {
				header->header.linkflag = LF_DIR;
			}
			finish_header(header);	/* Done with directory header */
		}

		/* Hack to remove "./" from the front of all the file names */
		if (len == 2 && namebuf[0] == '.') {
			len = 0;
		}

		/* Now output all the files in the directory */
		if (f_dironly)
			break;		/* Unless the user says no */
		errno = 0;
		dirp = opendir(p);
		if (!dirp) {
			if (errno) {
				perror (p);
			} else {
				annorec(stderr, tar);
				fprintf(stderr, "%s: error opening directory",
					p);
			}
			break;
		}
		
		/* Should speed this up by cd-ing into the dir, FIXME */
		while (NULL != (d=readdir(dirp))) {
			/* Skip . and .. */
			if (d->d_name[0] == '.') {
				if (d->d_name[1] == '\0') continue;
				if (d->d_name[1] == '.') {
					if (d->d_name[2] == '\0') continue;
				}
			}
			if (strlen(d->d_name) + len >= NAMSIZ) {
				annorec(stderr, tar);
				fprintf(stderr, "%s%s: name too long\n", 
					namebuf, d->d_name);
				continue;
			}
			strcpy(namebuf+len, d->d_name);
			dump_file(namebuf, our_device);
		}

		closedir(dirp);
#ifdef AMIGA
		if (f_archive_set)
		{
		    if (SmartSetProtection(p, hstat.st_prot | 
			FIBF_ARCHIVE) < 0)
		    {
		        fputs("tar: Error setting archive bit on ", stderr);
			perror(p);
		    }
		}
#endif
	}
		break;

#ifdef S_IFCHR
	case S_IFCHR:			/* Character special file */
		type = LF_CHR;
		goto easy;
#endif

#ifdef S_IFBLK
	case S_IFBLK:			/* Block     special file */
		type = LF_BLK;
		goto easy;
#endif

#ifdef S_IFIFO
	case S_IFIFO:			/* Fifo      special file */
		type = LF_FIFO;
#endif

	easy:
		if (!f_standard) goto unknown;

		hstat.st_size = 0;		/* Force 0 size */
		header = start_header(p, &hstat);
		if (header == NULL) goto badfile;	/* eg name too long */

		header->header.linkflag = type;
		if (type != LF_FIFO) {
			to_oct((long) major(hstat.st_rdev), 8,
				header->header.devmajor);
			to_oct((long) minor(hstat.st_rdev), 8,
				header->header.devminor);
		}

		finish_header(header);
		break;

	default:
	unknown:
		annorec(stderr, tar);
		fprintf(stderr,
			"%s: Unknown file type; file ignored.\n", p);
		break;
	}
}


/*
 * Make a header block for the file  name  whose stat info is  st .
 * Return header pointer for success, NULL if the name is too long.
 */
union record *
start_header(name, st)
	char	*name;
	register struct stat *st;
{
	register union record *header;
	char newname[NAMSIZ+2];
	header = (union record *) findrec();
	bzero(header->charptr, sizeof(*header)); /* XXX speed up */

	/*
	 * Check the file name and put it in the record.
	 */
#ifdef AMIGA
	cvtAmi2UNIX(name, newname);
	name = newname;	/* point name at newname, but leave orig string alone */
#else
	while ('/' == *name) {
		static int warned_once = 0;

		name++;				/* Force relative path */
		if (!warned_once++) {
			annorec(stderr, tar);
			fprintf(stderr,
	"Removing leading / from absolute path names in the archive.\n");
		}
	}
#endif
	strcpy(header->header.name, name);
	if (header->header.name[NAMSIZ-1]) {
		annorec(stderr, tar);
		fprintf(stderr, "%s: name too long\n", name);
		return NULL;
	}

	to_oct((long) (st->st_mode & ~S_IFMT),
					8,  header->header.mode);
	to_oct((long) st->st_uid,	8,  header->header.uid);
	to_oct((long) st->st_gid,	8,  header->header.gid);
	to_oct((long) st->st_size,	1+12, header->header.size);
	to_oct((long) st->st_mtime,	1+12, header->header.mtime);
	/* header->header.linkflag is left as null */
#ifdef AMIGA
	strcpy(header->header.magic_cookie, "AmigaTar");
	to_hex(st->st_prot, header->header.amiga_modes);
	memset(header->header.comment, '\0', sizeof(header->header.comment));
	strcpy(header->header.comment, st->st_comment);
	to_hex(st->st_date.ds_Days, header->header.ds_Days);
	to_hex(st->st_date.ds_Minute, header->header.ds_Minute);
	to_hex(st->st_date.ds_Tick, header->header.ds_Tick);
#endif
#ifndef NONAMES
	/* Fill in new Unix Standard fields if desired. */
	if (f_standard) {
		header->header.linkflag = LF_NORMAL;	/* New default */
		strcpy(header->header.magic, TMAGIC);	/* Mark as Unix Std */
		finduname(header->header.uname, st->st_uid);
		findgname(header->header.gname, st->st_gid);
	}
#endif
	return header;
}

/* 
 * Finish off a filled-in header block and write it out.
 * We also print the file name and/or full info if verbose is on.
 */
void
finish_header(header)
	register union record *header;
{
	register int	i, sum;
	register char	*p;

	bcopy(CHKBLANKS, header->header.chksum, sizeof(header->header.chksum));

	sum = 0;
	p = header->charptr;
	for (i = sizeof(*header); --i >= 0; ) {
		/*
		 * We can't use unsigned char here because of old compilers,
		 * e.g. V7.
		 */
		sum += 0xFF & *p++;
	}

	/*
	 * Fill in the checksum field.  It's formatted differently
	 * from the other fields:  it has [6] digits, a null, then a
	 * space -- rather than digits, a space, then a null.
	 * We use to_oct then write the null in over to_oct's space.
	 * The final space is already there, from checksumming, and
	 * to_oct doesn't modify it.
	 *
	 * This is a fast way to do:
	 * (void) sprintf(header->header.chksum, "%6o", sum);
	 */
	to_oct((long) sum,	8,  header->header.chksum);
	header->header.chksum[6] = '\0';	/* Zap the space */

	userec(header);

	if (f_verbose) {
		/* These globals are parameters to print_header, sigh */
		head = header;
		/* hstat is already set up */
		head_standard = f_standard;
		print_header(stderr);
	}

	return;
}


/*
 * Quick and dirty octal conversion.
 * Converts long "value" into a "digs"-digit field at "where",
 * including a trailing space and room for a null.  "digs"==3 means
 * 1 digit, a space, and room for a null.
 *
 * We assume the trailing null is already there and don't fill it in.
 * This fact is used by start_header and finish_header, so don't change it!
 *
 * This should be equivalent to:
 *	(void) sprintf(where, "%*lo ", digs-2, value);
 * except that sprintf fills in the trailing null and we don't.
 */
void
to_oct(value, digs, where)
	register long	value;
	register int	digs;
	register char	*where;
{
	
	--digs;				/* Trailing null slot is left alone */
	where[--digs] = ' ';		/* Put in the space, though */

	/* Produce the digits -- at least one */
	do {
		where[--digs] = '0' + (char)(value & 7); /* one octal digit */
		value >>= 3;
	} while (digs > 0 && value != 0);

	/* Leading spaces, if necessary */
	while (digs > 0)
		where[--digs] = ' ';

}


/*
 * Write the EOT record(s).
 * We actually zero at least one record, through the end of the block.
 * Old tar writes garbage after two zeroed records -- and PDtar used to.
 */
write_eot()
{
	union record *p;
	int bufsize;

	p = findrec();
	bufsize = endofrecs()->charptr - p->charptr;
	bzero(p->charptr, bufsize);
	userec(p);
}

#ifdef AMIGA
/*
 * 32-bit to 8 ascii chars as hex plus null term
 */
void
to_hex(u_long val, char *p)
{
    int i, nibble;

    p += 8;			/* point to last char */
    *p = '\0';			/* null term */
    for (i = 0; i < 8; i++)
    {
	nibble = val & 0xf;
	*--p = (nibble > 9) ? (nibble - 10) + 'a' : nibble + '0';
	val >>= 4;
    }
}
#endif
