/*-
 * Copyright 2010. Ivan Voras <ivoras@freebsd.org>
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 */
/*
 * FreeBSD install - a package for the installation and maintainance
 * of non-core utilities.
 *
 * Library of helper functions.
 */

#include <sys/cdefs.h>
__FBSDID("$FreeBSD$");

#include <sys/param.h>
#include <sys/utsname.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <assert.h>
#include <fcntl.h>
#include <paths.h>
#include <errno.h>
#include <err.h>
#include <fts.h>
#include <archive.h>
#include <archive_entry.h>

#include <pkg.h>
#include "pkg_patch.h"


/*
 * Tests if the given string ends with the other string. The return value is
 * analogous to strcmp().
 */
int
strendswith(const char *base, const char *end)
{
	int base_len, end_len;
	
	base_len = strlen(base);
	end_len = strlen(end);
	if (base_len < end_len)
		return -1;
	return strncmp(base + (base_len - end_len), end, end_len);
}


/*
 * Removes a directory hierarchy.
 */
int
rm_rf(char *dir)
{
	char cmd[PATH_MAX];
	
	/* XXX: reimplement in C? */
	sprintf(cmd, "%s -rf %s", _PATH_RM, dir);
	return system(cmd);
}


/*
 * Simple file copy.
 */
int
cp(char *from, char *to)
{
	int fd1, fd2, rval = 0;
	size_t bs = 1 * 1024 * 1024;
	char *buf;
	
	fd1 = open(from, O_RDONLY);
	if (fd1 < 0)
		return (-1);
	fd2 = open(to, O_WRONLY | O_CREAT | O_TRUNC, 0600);
	if (fd2 < 0) {
		close(fd1);
		return (-1);
	}
	buf = malloc(bs);
	if (buf == NULL) {
		rval = -1;
		goto end;
	}
	while (bs > 0) {
		bs = read(fd1, buf, bs);
		if (bs > 0) {
			ssize_t written = 0, wr;
			
			while (bs - written > 0) {
				wr = write(fd2, buf + written, bs - written);
				if (wr < 0) {
					rval = -1;
					goto end;
				}
				written += wr;
			}
		}
	}
end:
	if (buf != NULL)
		free(buf);
	close(fd2);
	close(fd1);
	return (rval);
}


/*
 * Starts a package extract (tar) job as a separate process.
 */
int
pkgxjob_start(struct pkgxjob *job, char *dir, char *filename)
{
	char cmd[PATH_MAX];
	
	/* libarchive not threadsafe for extract; call external tar */
	job->filename = filename;
	sprintf(cmd, "%s -x -C %s -f %s", _PATH_TAR, dir, filename);
	if (Verbose > 1)
		printf("pkgxjob: %s\n", cmd);
	job->fp = popen(cmd, "r+");
	if (job->fp == NULL)
		return (-1);
	return (0);
}


/*
 * Finish (cleanup) a tar job.
 */
int
pkgxjob_finish(struct pkgxjob *job)
{
	return (pclose(job->fp));
}


/*
 * Gather files in a file hierarchy into the given filelist_head.
 */
int
filelist_gather(char *dir, struct filelist_head *head)
{
	FTS *fts;
	FTSENT *fe;
	char *path_argv[] = { dir, NULL };
	size_t dir_len;
	
	fts = fts_open(path_argv, FTS_NOCHDIR | FTS_PHYSICAL | FTS_XDEV, NULL);
	if (fts == NULL)
		return (-1);
	dir_len = strlen(dir);
	
	while ((fe = fts_read(fts)) != NULL) {
		struct filelist *fl;
		
		if (fe->fts_info == FTS_D || fe->fts_info == FTS_F || 
		    fe->fts_info == FTS_SL || fe->fts_info == FTS_SLNONE) {
			if (fe->fts_pathlen == dir_len)
				continue;
			fl = malloc(sizeof(*fl));
			if (fl == NULL)
				return (-1);
			strncpy(fl->filename, fe->fts_path + dir_len + 1,
			    PATH_MAX);
			memcpy(&fl->st, fe->fts_statp, sizeof(struct stat));
			SLIST_INSERT_HEAD(head, fl, linkage);
		    }
	}
	
	fts_close(fts);
	
	return (0);
}


/*
 * Returns a list of differences between filelists.
 */
int
filelist_diff(struct filelist_head *flist1, struct filelist_head *flist2,
    struct filelist_head *fldiff)
{
	struct filelist *fl1, *fl2;
	int found;
	
	SLIST_FOREACH(fl1, flist1, linkage) {
		found = 0;
		SLIST_FOREACH(fl2, flist2, linkage) {
			if (strncmp(fl1->filename, fl2->filename, PATH_MAX) == 0) {
				found = 1;
				break;
			}
		}
		if (!found) {
			fl2 = malloc(sizeof(*fl2));
			memcpy(fl2, fl1, sizeof(*fl2));
			SLIST_INSERT_HEAD(fldiff, fl2, linkage);
		}
	}
	return (0);
}


/*
 * Return a list consisting of the intersection of two given filelists.
 */
int
filelist_intersect(struct filelist_head *flist1, struct filelist_head *flist2,
    struct filelist_head *flintersect)
{
	struct filelist *fl1, *fl2;
	int found;
	
	SLIST_FOREACH(fl1, flist1, linkage) {
		found = 0;
		SLIST_FOREACH(fl2, flist2, linkage) {
			if (strncmp(fl1->filename, fl2->filename, PATH_MAX) == 0) {
				found = 1;
				break;
			}
		}
		if (found) {
			fl2 = malloc(sizeof(*fl2));
			memcpy(fl2, fl1, sizeof(*fl2));
			SLIST_INSERT_HEAD(flintersect, fl2, linkage);
		}
	}
	return (0);
}


/*
 * Free the filelist's entries.
 */
void
filelist_free(struct filelist_head *flist)
{
	struct filelist *fl1, *fl2;
	
	SLIST_FOREACH_SAFE(fl1, flist, linkage, fl2) {
		SLIST_REMOVE(flist, fl1, filelist, linkage);
		free(fl1);
	}
}


/*
 * Free the pkgjoinlist's entries.
 */
void
pkgjoinlist_free(struct pkgjoinlist_head *plist)
{
	struct pkgjoinlist *pl1, *pl2;
	
	SLIST_FOREACH_SAFE(pl1, plist, linkage, pl2) {
		SLIST_REMOVE(plist, pl1, pkgjoinlist, linkage);
		free(pl1);
	}
}


/*
 * Returns a file list consisting of the intersection of packages from the first
 * list 
 */
int
filelist_intersect_pkg(struct filelist_head *flist1, struct filelist_head *flist2,
    struct pkgjoinlist_head *pkgisect)
{
	char basename1[PKGNAME_MAX], version1[PKGNAME_MAX], suffix1[20];
	char basename2[PKGNAME_MAX], version2[PKGNAME_MAX], suffix2[20];
	struct filelist *fl1, *fl2;
	struct pkgjoinlist *pi;
	int found;
	
	SLIST_FOREACH(fl1, flist1, linkage) {
		found = 0;
		parse_package_name(fl1->filename, basename1, version1, suffix1);
		SLIST_FOREACH(fl2, flist2, linkage) {
			parse_package_name(fl2->filename, basename2, version2, suffix2);
			if (strncmp(basename1, basename2, PKGNAME_MAX) == 0) {
				found = 1;
				break;
			}
		}
		if (found) {
			pi = calloc(1, sizeof(*pi));
			strncpy(pi->name1, fl1->filename, PKGNAME_MAX);
			strncpy(pi->name2, fl2->filename, PKGNAME_MAX);
			SLIST_INSERT_HEAD(pkgisect, pi, linkage);
		}
	}
	return (0);
}


/*
 * Returns the number of elements in the given filelist.
 */
unsigned int
filelist_count(struct filelist_head *flist)
{
	unsigned int count = 0;
	struct filelist *fl;
	
	SLIST_FOREACH(fl, flist, linkage)
		count++;
	return (count);
}


/*
 * For a given binary package archive filename, extract its base package name
 * (e.g. "apache-ant"), its version (e.g. "1.7.1") and its suffix (e.g. ".tbz").
 * Any of the component pointers / arguments can be NULL. The package filname
 * might contain path information (slashes), which will be discarded.
 */
void
parse_package_name(char *pkgfile, char *basename, char *version, char *suff)
{
	char *tmp, *p;
	
	/* Strip directory path, if any */
	p = strrchr(pkgfile, '/');
	if (p != NULL)
		tmp = strdup(p + 1);
	else
		tmp = strdup(pkgfile);
	p = strrchr(tmp, '.');
	if (suff != NULL)
		strcpy(suff, p);
	*p = '\0';
	p = strrchr(tmp, '-');
	if (version != NULL)
		strcpy(version, p + 1);
	*p = '\0';
	if (basename != NULL)
		strcpy(basename, tmp);
	free(tmp);
}


/*
 * Copy generic file attributes.
 * TODO: See if there is any need to take care of ACLs (tar apparently doesn't).
 */
int
copy_file_attrs(char *from, struct stat *st_from, char *to)
{
	struct stat *st, st2;
	struct timeval tv[2];
	
	if (st_from != NULL)
		st = st_from;
	else {
		assert(from != NULL);
		if (lstat(from, &st2) < 0) {
			warn("copy_file_attrs: lstat(%s) failed", from);
			return (-errno);
		}
		st = &st2;
	}
	if (chown(to, st->st_uid, st->st_gid) < 0) {
		warn("copy_file_attrs: chown() failed");
		return (-errno);
	}
	tv[0].tv_usec = tv[1].tv_usec = 0;
	tv[0].tv_sec = tv[1].tv_sec = st->st_mtime;
	if (lutimes(to, tv) < 0) {
		warn("copy_file_attrs: lutimes(%s,%d) failed", to, st->st_mtime);
		return (-errno);
	}
	if (lchmod(to, st->st_mode) < 0) {
		warn("copy_file_attrs: lchmod(%o) failed", st->st_mode);
		return (-errno);
	}
	return (0);
}


/*
 * File copy, preserving generic file attributes. Knows how to handle 
 * (re-create) symlinks.
 */
int
copy_file_absolute(char *from, char *to)
{
	struct stat st;
	
	if (lstat(from, &st) != 0)
		return (errno);
	
	if (S_ISDIR(st.st_mode)) {
		if (mkdir(to, 0700) != 0) {
			if (errno != EEXIST)
				return (-errno);
		}
		if (copy_file_attrs(from, &st, to) != 0)
			return (-errno);
		return (0);
	} else if (S_ISLNK(st.st_mode)) {
		char tmp[PATH_MAX];
		
		memset(tmp, 0, sizeof(tmp));
		if (readlink(from, tmp, PATH_MAX) < 0)
			return (-errno);
		if (symlink(tmp, to) < 0)
			return (-errno);
		return (0);
	}
	
	if (cp(from, to) != 0)
		return (-errno);
	
	if (copy_file_attrs(from, &st, to) != 0)
		return (-errno);
	return (0);
}


/*
 * Replicates / re-creates a directory tree in the destination to contain
 * all directories from the source, including their properties: ownership,
 * mode, mtime.
 */
int
replicate_dirtree(char *from, char __unused *to)
{
	FTS *fts;
	FTSENT *fe;
	char *path_argv[] = { from, NULL };
	size_t from_len;
	int rval;
	
	rval = 0;
	from_len = strlen(from);
	fts = fts_open(path_argv, FTS_NOCHDIR | FTS_PHYSICAL | FTS_XDEV, NULL);
	if (fts == NULL)
		return (-1);
	while ((fe = fts_read(fts)) != NULL) {
		char new_dir[PATH_MAX];
		
		if (fe->fts_info == FTS_D) {
			snprintf(new_dir, PATH_MAX, "%s%s", to, 
			    fe->fts_path + from_len);
			if (access(new_dir, F_OK) == 0)
				continue;
			if (mkdir(new_dir, 0700) < 0) {
				rval = -errno;
				goto end;
			}
			if (copy_file_attrs(fe->fts_path, fe->fts_statp,
			    new_dir) != 0) {
				rval = -errno;
				goto end;
			}
		}
	}
end:	
	if (fts_close(fts) < 0)
		return (-1);
	return (rval);
}


/*
 * Counts the elements in the given pplist.
 */
unsigned int
pplist_count(struct pplist_head *ppl)
{
	unsigned int count = 0;
	struct pplist *pl;
	
	STAILQ_FOREACH(pl, ppl, linkage)
		count++;
	return (count);
}


/*
 * Reads the given file into struct pkg_patch.
 */
void
read_pkgpatch_file(char *filename, struct pkg_patch *pp)
{
	FILE *fp;
	char line[PATH_MAX], *p, *p2, *p3, *cmd;
	int llen;
	struct pplist *pl;
	
	fp = fopen(filename, "r");
	if (fp == NULL)
		err(1, "Cannot open file: %s", filename);
	memset(pp, 0, sizeof(*pp));
	STAILQ_INIT(&pp->pp_add);
	STAILQ_INIT(&pp->pp_remove);
	STAILQ_INIT(&pp->pp_rmdir);
	STAILQ_INIT(&pp->pp_patch);
	
	while (fgets(line, PATH_MAX, fp) != NULL) {
		llen = strlen(line);
		if (line[llen-1] == '\n') {
			line[llen-1] = '\0';	/* strip newline */
			llen--;
		}
		p = strchr(line, '#'); 	/* skip comments */
		if (p != NULL)
			*p = '\0';
		if (line[0] == '\0')	/* skip empty lines */
			continue;
		cmd = line;
		p = strchr(line, ' ');
		if (p == NULL)
			errx(1, "Invalid command format in %s", PKGPATCH_FNAME);
		*p++ = '\0';
		if (strcmp(cmd, "@version") == 0) {
			p2 = strchr(p, '.');
			if (p2 == NULL)
				errx(1, "Invalid version format in %s",
				    PKGPATCH_FNAME);
			*p2++ = '\0';
			pp->version_major = atoi(p);
			pp->version_minor = atoi(p2);
		} else if (strcmp(cmd, "@source") == 0) {
			strlcpy(pp->source, p, PATH_MAX);
		} else if (strcmp(cmd, "@target") == 0) {
			strlcpy(pp->target, p, PATH_MAX);
		} else if (strcmp(cmd, "@add") == 0) {
			pl = calloc(1, sizeof(*pl));
			strlcpy(pl->filename, p, PATH_MAX);
			STAILQ_INSERT_TAIL(&pp->pp_add, pl, linkage);
		} else if (strcmp(cmd, "@remove") == 0) {
			pl = calloc(1, sizeof(*pl));
			strlcpy(pl->filename, p, PATH_MAX);
			STAILQ_INSERT_TAIL(&pp->pp_remove, pl, linkage);
		} else if (strcmp(cmd, "@rmdir") == 0) {
			pl = calloc(1, sizeof(*pl));
			strlcpy(pl->filename, p, PATH_MAX);
			STAILQ_INSERT_TAIL(&pp->pp_rmdir, pl, linkage);
		} else if (strcmp(cmd, "@patch") == 0) {
			pl = calloc(1, sizeof(*pl));
			p2 = strchr(p, '[');
			if (p2 != NULL) {
				/*
				 * Parse options block of the form
				 * \[name=value[,name=value...]\]
				 */
				char m[100], *pm, *p4, *p5;
				
				pm = m;
				p3 = strchr(p2, ']');
				assert(p3-p2 < (int)sizeof(m));
				strlcpy(m, p2 + 1, p3 - p2);
				p3++;
				while (*p3 == ' ')
					p3++;
				strlcpy(pl->filename, p3, PATH_MAX);
				while ((p4 = strsep(&pm, ",")) != NULL) {
					p5 = strchr(p4, '=');
					if (p5 != NULL)
						*p5++ = '\0';
					if (strcmp(p4, "method") == 0) {
						if (p5 == NULL)
							errx(1, "patch option "
							    "error");
						if (strcmp(p5, "bsdiff") == 0)
							pl->method = 
							    PPMETHOD_BSDIFF;
						else if (strcmp(p5, "cp") == 0)
							pl->method = PPMETHOD_CP;
					    	else if (strcmp(p5, "ln") == 0)
							pl->method = PPMETHOD_LN;
					}
				}
			} else {
				/* Default options */
				strlcpy(pl->filename, p, PATH_MAX);
				pl->method = PPMETHOD_CP;
			}
			STAILQ_INSERT_TAIL(&pp->pp_patch, pl, linkage);
		} else
			errx(1, "Unknown command: %s", cmd);
		
	}
	fclose(fp);
}


/*
 * Converts "zulu time" time_t value to iso8601 datetime. Not thread-safe.
 * Accepts -1 for time to mean "current" time.
 */
char *
time_to_iso8601(time_t t)
{
	static char stm[25];
	struct tm *tptr;
	
	if (t == -1) {
		struct timeval tp;
		
		gettimeofday(&tp, NULL);
		t = tp.tv_sec;
	}
	
	tptr = gmtime(&t);
	strftime(stm, 25, "%FT%TZ", tptr);
	return stm;
}


/* Converts given iso8601 datetime string to time_t. */
time_t
iso8601_to_time(char *t)
{
	struct tm tms;
	
	if (strptime(t, "%FT%T%Z", &tms) != NULL)
		return (0);
	return timegm(&tms);
}


/* Convert given time_t to asctime */
char *
time_ctime(time_t t)
{
	if (t == -1)
		t = time(NULL);
	return (ctime(&t));
}


/* Twirl the baton, writing backspace */
void
baton_twirl()
{
	static const char bpos[4] = { '-', '\\', '|', '/' };
	static unsigned int counter = 0;
	
	fprintf(stdout, "%c\b", bpos[counter++ % 4]);
	fflush(stdout);
}


/* Read the +CONTENTS file from the given package file */
Package *
pkg_read_plist(char *filename)
{
	struct archive *arc;
	struct archive_entry *entry;
	Package *pkg = NULL;
	int er;
	
	arc = archive_read_new();
	archive_read_support_compression_all(arc);
	archive_read_support_format_tar(arc);
	
	er = archive_read_open_filename(arc, filename, 65536);
	if (er != ARCHIVE_OK)
		return NULL;
	while (archive_read_next_header(arc, &entry) == ARCHIVE_OK) {
		FILE *fplist;
		size_t bs = 16 * 1024;
		char *buf;
		
		if (strncmp(archive_entry_pathname(entry), CONTENTS_FNAME,
		    PATH_MAX) != 0)
			continue;
		fplist = tmpfile();
		buf = malloc(bs);
		while (bs > 0) {
			bs = archive_read_data(arc, buf, bs);
			if (bs > 0)
				if (fwrite(buf, 1, bs, fplist) != bs)
					err(1, "Cannot extract plist");
		}
		free(buf);
		fseek(fplist, 0, 0);
		pkg = calloc(1, sizeof(*pkg));
		read_plist(pkg, fplist);
		fclose(fplist);
		break;
	}
	archive_read_finish(arc);
	return (pkg);
}


/*
 * Check for conflicts from metadata in the new package to recorded packages.
 * Returns (i+1) if the given package pnew conflicts on the i'th element
 * in pkglist. Returns 0 if no conflict detected.
 */
int
check_conflicts(Package *pnew, char **pkglist)
{
	int er, i;
	PackingList pl;
	
	if (pkglist == NULL) {
		pkglist = matchinstalled(MATCH_ALL, NULL, &er);
		if (pkglist == NULL || er != 0) {
			warnx("Cannot fetch a list of installed packages "
			    "(matchinstalled(MATCH_ALL...))");
			return (-1);
		}
	}
	pl = pnew->head;
	while (pl != NULL) {
		if (pl->type == PLIST_CONFLICTS) {
			for (i = 0; pkglist[i] != NULL; i++) {
				if (strncmp(pl->name, pkglist[i], PKGNAME_MAX)
				    == 0)
					return (i + 1);
			}
		}
		pl = pl->next;
	}
	/* XXX: When libpkg grows a storefree() API, use it to free pkglist
	 * if needed. */
	return (0);
}


/*
 * Compare two package names based on their individual components. Returns
 * the amount of similarity between the names.
 */
enum CMP_NAME
compare_package_names(char *pkg1, char *pkg2)
{
	char base1[PKGNAME_MAX], ver1[PKGNAME_MAX];
	char base2[PKGNAME_MAX], ver2[PKGNAME_MAX];
	
	if (pkg1 == NULL || pkg2 == NULL)
		return (CMP_NO_MATCH);
	parse_package_name(pkg1, base1, ver1, NULL);
	parse_package_name(pkg2, base2, ver2, NULL);
	if (strncmp(base1, base2, PKGNAME_MAX) != 0)
		return (CMP_NO_MATCH);
	if (strncmp(ver1, ver2, PKGNAME_MAX) != 0)
		return (CMP_BASE_MATCH);
	return (CMP_FULL_MATCH);
}


/*
 * Check package dependencies on the given pkglist or the currently installed
 * packages.
 */
enum CMP_NAME
check_dependencies(Package *pnew, char **pkglist)
{
	int er, i;
	PackingList pl;
	enum CMP_NAME best;
	
	best = CMP_FULL_MATCH;
	if (pkglist == NULL) {
		pkglist = matchinstalled(MATCH_ALL, NULL, &er);
		if (pkglist == NULL || er != 0) {
			warnx("Cannot fetch a list of installed packages "
			    "(matchinstalled(MATCH_ALL...))");
			return (-1);
		}
	}
	pl = pnew->head;
	while (pl != NULL) {
		if (pl->type == PLIST_PKGDEP) {
			enum CMP_NAME match;
			int found = 0;
			
			for (i = 0; pkglist[i] != NULL; i++) {
				match = compare_package_names(pl->name,
				    pkglist[i]);
				if (match == CMP_BASE_MATCH) {
					if (best == CMP_FULL_MATCH)
						best = CMP_BASE_MATCH;
					warnx("Expecting %s but found %s",
					    pl->name, pkglist[i]);
					found = 1;
					break;
				} else if (match == CMP_FULL_MATCH) {
					found = 1;
					break;
				}
			}
			if (!found) {
				warnx("Dependancy not found: %s",
				    pl->name);
				best = CMP_NO_MATCH;
				break;
			}
		}
		pl = pl->next;
	}
	/* XXX: When libpkg grows a storefree() API, use it to free pkglist
	 * if needed. */
	return (best);
}


/* Return a char* pointer to the filename portion of the given full filename */
char *
find_filename(char *fullname)
{
	char *p;
	
	p = strrchr(fullname, '/');
	if (p == NULL)
		return (fullname);
	return (p + 1);
}