/*	install 1.9 - install files.			Author: Kees J. Bot
 *								21 Feb 1993
 */
#define nil 0
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <a.out.h>
#include <limits.h>
#include <pwd.h>
#include <grp.h>
#include <utime.h>
#include <signal.h>

/* First line used on a self-decompressing executable. */
char ZCAT[]=	"#!/usr/bin/zexec /usr/bin/zcat\n";
char GZCAT[]=	"#!/usr/bin/zexec /usr/bin/gzcat\n";

/* Compression filters. */
char *COMPRESS[]=	{ "compress", nil };
char *GZIP[]=		{ "gzip", "-#", nil };

int excode= 0;		/* Exit code. */

void report(char *label)
{
	if (label == nil || label[0] == 0)
		fprintf(stderr, "install: %s\n", strerror(errno));
	else
		fprintf(stderr, "install: %s: %s\n", label, strerror(errno));
	excode= 1;
}

void fatal(char *label)
{
	report(label);
	exit(1);
}

void *allocate(void *mem, size_t size)
/* Safe malloc/realloc. */
{
	mem= mem == nil ? malloc(size) : realloc(mem, size);

	if (mem == nil) fatal(nil);
	return mem;
}

void deallocate(void *mem)
{
	if (mem != nil) free(mem);
}

int lflag= 0;		/* Make a hard link if possible. */
int cflag= 0;		/* Copy if you can't link, otherwise symlink. */
int dflag= 0;		/* Create a directory. */
int strip= 0;		/* Strip the copy. */
char **compress= nil;	/* Compress utility to make a compressed executable. */
char *zcat= nil;	/* Line one to decompress. */

long stack= -1;		/* Amount of heap + stack. */
int wordpow= 1;		/* Must be multiplied with wordsize ** wordpow */
			/* So 8kb for an 8086 and 16kb for the rest. */

pid_t filter(int fd, char **command)
/* Let a command filter the output to fd. */
{
	pid_t pid;
	int pfd[2];

	if (pipe(pfd) < 0) {
		report("pipe()");
		return -1;
	}

	switch ((pid= fork())) {
	case -1:
		report("fork()");
		return -1;
	case 0:
		/* Run the filter. */
		dup2(pfd[0], 0);
		dup2(fd, 1);
		close(pfd[0]);
		close(pfd[1]);
		close(fd);
		signal(SIGPIPE, SIG_DFL);
		execvp(command[0], command);
		fatal(command[0]);
	}
	/* Connect fd to the pipe. */
	dup2(pfd[1], fd);
	close(pfd[0]);
	close(pfd[1]);
	return pid;
}

int mkdirp(char *dir, int mode, int owner, int group)
/* mkdir -p dir */
{
	int keep;
	char *sep, *pref;

	sep= dir;
	while (*sep == '/') sep++;
	
	if (*sep == 0) {
		errno= EINVAL;
		return -1;
	}

	do {
		while (*sep != '/' && *sep != 0) sep++;
		pref= sep;
		while (*sep == '/') sep++;

		keep= *pref; *pref= 0;

		if (strcmp(dir, ".") == 0 || strcmp(dir, "..") == 0) continue;

		if (mkdir(dir, mode) < 0) {
			if (errno != EEXIST || *sep == 0) {
				/* On purpose not doing: *pref= keep; */
				return -1;
			}
		} else {
			if (chown(dir, owner, group) < 0 && errno != EPERM)
				return -1;
		}
	} while (*pref= keep, *sep != 0);
	return 0;
}

void makedir(char *dir, int mode, int owner, int group)
/* Install a directory, and set it's modes. */
{
	struct stat st;

	if (stat(dir, &st) < 0) {
		if (errno != ENOENT) { report(dir); return; }

		/* The target doesn't exist, make it. */
		if (mode == -1) mode= 0755;
		if (owner == -1) owner= getuid();
		if (group == -1) group= getgid();

		if (mkdirp(dir, mode, owner, group) < 0) {
			report(dir); return;
		}
	} else {
		/* The target does exist, change mode and ownership. */
		if (mode == -1) mode= (st.st_mode & 07777) | 0555;

		if ((st.st_mode & 07777) != mode) {
			if (chmod(dir, mode) < 0) { report(dir); return; }
		}
		if (owner == -1) owner= st.st_uid;
		if (group == -1) group= st.st_gid;
		if (st.st_uid != owner || st.st_gid != group) {
			if (chown(dir, owner, group) < 0 && errno != EPERM) {
				report(dir); return;
			}
			/* Set the mode again, chown may have wrecked it. */
			(void) chmod(dir, mode);
		}
	}
}

int setstack(struct exec *hdr)
/* Set the stack size in a header.  Return true if something changed. */
{
	long total;

	total= stack;
	while (wordpow > 0) {
		total *= hdr->a_cpu == A_I8086 ? 2 : 4;
		wordpow--;
	}
	total+= hdr->a_data + hdr->a_bss;

	if (!(hdr->a_flags & A_SEP)) {
		total+= hdr->a_text;
#ifdef A_PAL
		if (hdr->a_flags & A_PAL) total+= hdr->a_hdrlen;
#endif
	}
	if (hdr->a_cpu == A_I8086 && total > 0x10000L)
		total= 0x10000L;

	if (hdr->a_total != total) {
		/* Need to change stack allocation. */
		hdr->a_total= total;

		return 1;
	}
	return 0;
}

void copylink(char *source, char *dest, int mode, int owner, int group)
{
	struct stat sst, dst;
	int sfd, dfd, n;
	int r, same= 0, change= 0, docopy= 1;
	char buf[4096];
#	define hdr ((struct exec *) buf)
	pid_t pid;
	int status;

	/* Source must exist as a plain file, dest may exist as a plain file. */

	if (stat(source, &sst) < 0) { report(source); return; }

	if (mode == -1) {
		mode= sst.st_mode & 07777;
		if (!lflag || cflag) {
			mode|= 0444;
			if (mode & 0111) mode|= 0111;
		}
	}
	if (owner == -1) owner= sst.st_uid;
	if (group == -1) group= sst.st_gid;

	if (!S_ISREG(sst.st_mode)) {
		fprintf(stderr, "install: %s is not a regular file\n", source);
		return;
	}
	r= stat(dest, &dst);
	if (r < 0) {
		if (errno != ENOENT) { report(dest); return; }
	} else {
		if (!S_ISREG(dst.st_mode)) {
			fprintf(stderr, "install: %s is not a regular file\n",
									dest);
			return;
		}

		/* Are the files the same? */
		if (sst.st_dev == dst.st_dev && sst.st_ino == dst.st_ino) {
			if (!lflag && cflag) {
				fprintf(stderr,
				"install: %s and %s are the same, can't copy\n",
					source, dest);
				return;
			}
			same= 1;
		}
	}

	if (lflag && !same) {
		/* Try to link the files. */

		if (r >= 0 && unlink(dest) < 0) {
			report(dest); return;
		}

		if (link(source, dest) >= 0) {
			docopy= 0;
		} else {
			if (!cflag || errno != EXDEV) {
				fprintf(stderr,
					"install: can't link %s to %s: %s\n",
					source, dest, strerror(errno));
				return;
			}
		}
	}

	if (docopy && !same) {
		/* Copy the files, stripping if necessary. */
		long count= LONG_MAX;
		int first= 1;

		if ((sfd= open(source, O_RDONLY)) < 0) {
			report(source); return;
		}

		/* Open for write is less simple, its mode may be 444. */
		dfd= open(dest, O_WRONLY|O_CREAT|O_TRUNC, mode | 0600);
		if (dfd < 0 && errno == EACCES) {
			(void) chmod(dest, mode | 0600);
			dfd= open(dest, O_WRONLY|O_TRUNC);
		}
		if (dfd < 0) {
			report(dest);
			close(sfd);
			return;
		}

		pid= 0;
		while (count > 0 && (n= read(sfd, buf, sizeof(buf))) > 0) {
			if (first && n >= A_MINHDR && !BADMAG(*hdr)) {
				if (strip) {
					count= hdr->a_hdrlen
						+ hdr->a_text + hdr->a_data;
#ifdef A_NSYM
					hdr->a_flags &= ~A_NSYM;
#endif
					hdr->a_syms= 0;
				}
				if (stack != -1 && setstack(hdr)) change= 1;

				if (compress != nil) {
					/* Write first #! line. */
					(void) write(dfd, zcat, strlen(zcat));

					/* Put a compressor in between. */
					if ((pid= filter(dfd, compress)) < 0) {
						close(sfd);
						close(dfd);
						return;
					}
					change= 1;
				}
			}
			if (count < n) n= count;

			if (write(dfd, buf, n) < 0) {
				report(dest);
				close(sfd);
				close(dfd);
				if (pid != 0) (void) waitpid(pid, nil, 0);
				return;
			}
			count-= n;
			first= 0;
		}
		if (n < 0) report(source);
		close(sfd);
		close(dfd);
		if (pid != 0 && waitpid(pid, &status, 0) < 0 || status != 0) {
			excode= 1;
			return;
		}
		if (n < 0) return;
	} else {
		if (stack != -1) {
			/* The file has been linked into place.  Set the
			 * stack size.
			 */
			if ((dfd= open(dest, O_RDWR)) < 0) {
				report(dest);
				return;
			}

			if ((n= read(dfd, buf, sizeof(*hdr))) < 0) {
				report(dest); return;
			}

			if (n >= A_MINHDR && !BADMAG(*hdr) && setstack(hdr)) {
				if (lseek(dfd, (off_t) 0, SEEK_SET) == -1
					|| write(dfd, buf, n) < 0
				) {
					report(dest);
					close(dfd);
					return;
				}
				change= 1;
			}
			close(dfd);
		}
	}

	if (stat(dest, &dst) < 0) { report(dest); return; }

	if ((dst.st_mode & 07777) != mode) {
		if (chmod(dest, mode) < 0) { report(dest); return; }
	}
	if (dst.st_uid != owner || dst.st_gid != group) {
		if (chown(dest, owner, group) < 0 && errno != EPERM) {
			report(dest); return;
		}
		/* Set the mode again, chown may have wrecked it. */
		(void) chmod(dest, mode);
	}
	if (!change) {
		struct utimbuf ubuf;

		ubuf.actime= dst.st_atime;
		ubuf.modtime= sst.st_mtime;

		if (utime(dest, &ubuf) < 0) { report(dest); return; }
	}
}

void usage(void)
{
	fprintf(stderr, "\
Usage:\n\
  install [-lcsz#] [-o owner] [-g group] [-m mode] [-S stack] [file1] file2\n\
  install [-lcsz#] [-o owner] [-g group] [-m mode] [-S stack] file ... dir\n\
  install -d [-o owner] [-g group] [-m mode] directory\n");
	exit(1);
}

void main(int argc, char **argv)
{
	int i= 1;
	int mode= -1;		/* Mode of target. */
	int owner= -1;		/* Owner. */
	int group= -1;		/* Group. */
	int super = 0;
#if NGROUPS_MAX > 0
	gid_t groups[NGROUPS_MAX];
	int ngroups;
	int g;
#endif

	/* Only those in group 0 are allowed to set owner and group. */
	if (getgid() == 0) super = 1;
#if NGROUPS_MAX > 0
	ngroups= getgroups(NGROUPS_MAX, groups);
	for (g= 0; g < ngroups; g++) if (groups[g] == 0) super= 1;
#endif
	if (!super) {
		setgid(getgid());
		setuid(getuid());
	}

	/* May use a filter. */
	signal(SIGPIPE, SIG_IGN);

	while (i < argc && argv[i][0] == '-') {
		char *p= argv[i++]+1;
		char *end;
		unsigned long num;
		int wp;
		struct passwd *pw;
		struct group *gr;

		if (strcmp(p, "-") == 0) break;

		while (*p != 0) {
			switch (*p++) {
			case 'l':	lflag= 1;	break;
			case 'c':	cflag= 1;	break;
			case 's':	strip= 1;	break;
			case 'd':	dflag= 1;	break;
			case 'z':
				if (compress == nil) {
					compress= COMPRESS;
					zcat= ZCAT;
				}
				break;
			case 'o':
				if (*p == 0) {
					if (i == argc) usage();
					p= argv[i++];
					if (*p == 0) usage();
				}
				num= strtoul(p, &end, 10);
				if (*end == 0) {
					if ((uid_t) num != num) usage();
					owner= num;
				} else {
					if ((pw= getpwnam(p)) == nil) {
						fprintf(stderr,
						"install: %s: unknown user\n",
							p);
						exit(1);
					}
					owner= pw->pw_uid;
				}
				p= "";
				break;
			case 'g':
				if (*p == 0) {
					if (i == argc) usage();
					p= argv[i++];
					if (*p == 0) usage();
				}
				num= strtoul(p, &end, 10);
				if (*end == 0) {
					if ((gid_t) num != num) usage();
					group= num;
				} else {
					if ((gr= getgrnam(p)) == nil) {
						fprintf(stderr,
						"install: %s: unknown user\n",
							p);
						exit(1);
					}
					group= gr->gr_gid;
				}
				p= "";
				break;
			case 'm':
				if (*p == 0) {
					if (i == argc) usage();
					p= argv[i++];
					if (*p == 0) usage();
				}
				num= strtoul(p, &end, 010);
				if (*end != 0 || (num & 07777) != num) usage();
				mode= num;
				if ((mode & S_ISUID) && super && owner == -1) {
					/* Setuid what?  Root most likely. */
					owner= 0;
				}
				if ((mode & S_ISGID) && super && group == -1) {
					group= 0;
				}
				p= "";
				break;
			case 'S':
				if (*p == 0) {
					if (i == argc) usage();
					p= argv[i++];
					if (*p == 0) usage();
				}
				stack= strtol(p, &end, 0);
				wp= 0;
				if (end == p || stack < 0) usage();
				p= end;
				while (*p != 0) {
					switch (*p++) {
					case 'm':
					case 'M': num= 1024 * 1024L; break;
					case 'k':
					case 'K': num= 1024; break;
					case 'w':
					case 'W': num= 4; wp++; break;
					case 'b':
					case 'B': num= 1; break;
					default: usage();
					}
					if (stack > LONG_MAX / num) usage();
					stack*= num;
				}
				wordpow= 0;
				while (wp > 0) { stack /= 4; wordpow++; wp--; }
				break;
			default:
				if ((unsigned) (p[-1] - '1') <= ('9' - '1')) {
					compress= GZIP;
					GZIP[1][1]= p[-1];
					zcat= GZCAT;
					break;
				}
				usage();
			}
		}
	}
	/* Some options don't mix. */
	if (dflag && (cflag || lflag || strip)) usage();

	/* Don't let the user umask interfere. */
	umask(000);

	if (dflag) {
		/* install directory */
		if ((argc - i) != 1) usage();

		makedir(argv[i], mode, owner, group);
	} else {
		struct stat st;

		if ((argc - i) < 1) usage();
		if ((lflag || cflag) && (argc - i) == 1) usage();

		if (stat(argv[argc-1], &st) >= 0 && S_ISDIR(st.st_mode)) {
			/* install file ... dir */
			char *target= nil;
			char *base;

			if ((argc - i) == 1) usage();

			while (i < argc-1) {
				if ((base= strrchr(argv[i], '/')) == nil)
					base= argv[i];
				else
					base++;
				target= allocate(target, strlen(argv[argc-1])
						+ 1 + strlen(base) + 1);
				strcpy(target, argv[argc-1]);
				strcat(target, "/");
				strcat(target, base);

				copylink(argv[i++], target, mode, owner, group);
			}
		} else {
			/* install [file1] file2 */

			copylink(argv[i], argv[argc-1], mode, owner, group);
		}
	}
	exit(excode);
}
