/* $Id: tarfile.c,v 1.10 2015/09/29 13:17:42 onoe Exp $ */

/*-
 * Copyright (c) 1999-2000 Atsushi Onoe
 * All rights reserved.
 *
 * 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.
 * 3. The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include <sys/types.h>
#include <sys/mman.h>
#include <sys/stat.h>

#include <dirent.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <tar.h>
#include <unistd.h>
#include <zlib.h>

#include "tarfile.h"

static struct tar_file *tfilehead;
static struct tar_dir *tdirhead;
static struct tar_fd *tfdarray;
static int tfdmax;

#define	TFDBUFSZ	100

static struct tar_ent *
tar_getent(const char *dname, const char *fname)
{
	int i;
	int dnamelen;
	char *p;
	struct tar_dir *tdir, *tdir2;
	struct tar_ent *tp;

	if (dname) {
		dnamelen = strlen(dname);
	} else {
		if ((p = strrchr(fname, '/')) == NULL) {
			dname = ".";
			dnamelen = 1;
		} else {
			dname = fname;
			dnamelen = p - fname;
			fname = p + 1;
		}
	}

	for (tdir = tdirhead; tdir; tdir = tdir->next) {
		if (tdir->namelen != dnamelen
		||  memcmp(tdir->name, dname, dnamelen) != 0)
			continue;
		for (tdir2 = tdir; tdir2; tdir2 = tdir2->nextent) {
			for (i = 0, tp = tdir2->ent; i < tdir2->nent; i++, tp++) {
				if (strcmp(tp->name, fname) == 0)
					return tp;
			}
		}
		break;
	}
	return NULL;
}

static struct tar_dir *
tar_adddir(const char *dname, int dnamelen)
{
	struct tar_dir *tdir;

	for (tdir = tdirhead; tdir; tdir = tdir->next) {
		if (tdir->namelen == dnamelen
		&&  memcmp(tdir->name, dname, dnamelen) == 0) {
			/* XXX: duplicate file check ? */
			break;
		}
	}
	if (tdir == NULL) {
		tdir = malloc(sizeof(*tdir));
		tdir->name = tdir->nmbuf.buf;
		memcpy(tdir->name, dname, dnamelen);
		tdir->name[dnamelen] = '\0';
		tdir->nmbuf.next = NULL;
		tdir->nmbuf.use = dnamelen + 1;
		tdir->namelen = dnamelen;
		tdir->nent = 0;
		tdir->nextent = NULL;
		tdir->next = tdirhead;
		tdirhead = tdir;
	}
	return tdir;
}

static int
tar_addent(struct tar_file *tfile, const char *dname, int dnamelen, const char *fname, int fnamelen, int off, int size)
{
	struct tar_dir *tdir;
	struct tar_ent *tp;
	struct tar_name *tn;

	tdir = tar_adddir(dname, dnamelen);
	while (tdir->nextent)
		tdir = tdir->nextent;
	if (tdir->nent == TARENTBLOCK) {
		tdir->nextent = malloc(sizeof(*tdir));
		tdir->nextent->name = tdir->name;	/* debug */
		tdir->nextent->namelen = tdir->namelen; /* debug */
		tdir = tdir->nextent;
		tdir->nextent = NULL;
		tdir->next = NULL;			/* debug */
		tdir->nent = 0;
		tdir->nmbuf.next = NULL;
		tdir->nmbuf.use = 0;
	}
	tp = &tdir->ent[tdir->nent++];
	tp->file = tfile;
	tp->off = off;
	tp->size = size;
	for (tn = &tdir->nmbuf; tn->use + fnamelen >= TARNMBLOCK; tn = tn->next) {
		if (tn->next == NULL) {
			tn->next = malloc(sizeof(*tn));
			tn->next->next = NULL;
			tn->next->use = 0;
		}
	}
	tp->name = tn->buf + tn->use;
	strlcpy(tp->name, fname, sizeof(tp->name) - tn->use);
	tn->use += fnamelen + 1;
	return 0;
}

static int
tar_addindex(struct tar_file *tfile, char *dname, int dnamelen, int off, int size)
{
	int fd, fnamelen, fsize, cc;
	char *buf, *p, *fname;

	buf = malloc(size);
	fd = tar_open(dname);	/*XXX*/
	if (fd == -1)
		return -1;
	cc = tar_read(fd, buf, size);
	tar_close(fd);
	if (cc != size)
		return -1;
	p = buf;
	off += (size + TARBSIZE - 1) / TARBSIZE * TARBSIZE;
	while (p < buf + size) {
		fname = p;
		while (*p != ' ' && *p != '\t') {
			p++;
			if (p > buf + size)
				return -1;
		}
		fnamelen = p - fname;
		p++;
		fsize = strtoul(p, &p, 10);
		if (p >= buf + size || *p != '\n')
			return -1;
		p++;
		memcpy(dname + tfile->dirlen, fname, fnamelen);
		dname[tfile->dirlen + fnamelen] = '\0';
		if ((fname = strrchr(dname, '/')) == NULL) {
			fname = dname;
			dnamelen = 0;
		} else {
			dnamelen = fname - dname;
			fname++;
		}
		fnamelen = strlen(fname);
		off += TARBSIZE;
		tar_addent(tfile, dname, dnamelen, fname, fnamelen, off, fsize);
		off += (fsize + TARBSIZE - 1) / TARBSIZE * TARBSIZE;
	}
	free(buf);
	return 0;
}

static int
tar_addblock(struct tar_file *tfile, char *buf, int off)
{
	struct tar_hdr *hdr;
	struct tar_ent *tp;
	char *dname, *fname;
	char path[1024];
	int size, dnamelen, fnamelen;

	hdr = (struct tar_hdr *)buf;
	if (hdr->name[0] == '\0')
		return off;
	dname = hdr->name;
	while (dname[0] == '.' && dname[1] == '/')
		dname += 2;
	if (tfile->dirlen)
		memcpy(path, tfile->name, tfile->dirlen);
	strlcpy(path + tfile->dirlen, dname, sizeof(path) - tfile->dirlen);
	dname = path;
	if ((fname = strrchr(dname, '/')) == NULL) {
		fname = dname;
		dnamelen = 0;
	} else {
		dnamelen = fname - dname;
		fname++;
	}
	fnamelen = strlen(fname);
	size = strtol(hdr->size, NULL, 8);

	switch (hdr->linkflag) {
	case REGTYPE:
	case AREGTYPE:
		tar_addent(tfile, dname, dnamelen, fname, fnamelen, off, size);
		if (strcmp(fname, ".index") == 0 &&
		    tar_addindex(tfile, dname, dnamelen, off, size) == 0) {
			tfile->have_index = 1;
			return -1;
		}
		break;
	case LNKTYPE:
		tp = tar_getent(NULL, hdr->linkname);
		if (tp == NULL) {
			printf("%s: not found\n", hdr->linkname);
			break;
		}
		tar_addent(tfile, dname, dnamelen, fname, fnamelen, tp->off, tp->size);
		break;
	case SYMTYPE:
		break;
	case DIRTYPE:
		break;
	default:
		/* ignore */
		break;
	}
	off += (size + TARBSIZE - 1) / TARBSIZE * TARBSIZE;
	return off;
}

static int
tar_addfile(struct tar_file *tf)
{
	int off, noff;
	void *gz;
	char buf[TARZBUFSIZ];

	gz = gzopen(tf->name, "rb");
	if (gz == NULL)
		return -1;
	tf->gz = gz;
	off = 0;
	while (gzread(gz, buf, TARBSIZE) == TARBSIZE) {
		off += TARBSIZE;
		noff = tar_addblock(tf, buf, off);
		if (noff < 0)
			break;
		gzseek(gz, noff - off, SEEK_CUR);
		off = noff;
	}
	gzclose(gz);
	tf->gz = NULL;
	return 0;
}

int
tar_addtgz(const char *name)
{
	struct tar_file *tf;
	struct stat stbuf;
	char *p;

	for (tf = tfilehead; tf; tf = tf->next) {
		if (strcmp(name, tf->name) == 0) {
			/* XXX: check update */
			return 0;
		}
	}
	if (stat(name, &stbuf) != 0)
		return -1;
	if ((tf = calloc(1, sizeof(*tf))) == NULL)
		return -1;
	tf->next = tfilehead;
	tfilehead = tf;
	tf->name = strdup(name);	/*XXX*/
	if ((p = strrchr(tf->name, '/')) != NULL)
		tf->dirlen = p - tf->name + 1;
	else
		tf->dirlen = 0;
	tf->size = stbuf.st_size;
	tf->mtime = stbuf.st_mtime;
	tf->ino = stbuf.st_ino;
	tar_addfile(tf);
	return tf->have_index ? 2 : 1;
}

DIR *
tar_opendir(const char *filename)
{
	struct tar_dirp *dirp;
	struct tar_dir *td;

	if ((dirp = calloc(1, sizeof(*dirp))) == NULL)
		return NULL;
	dirp->sysdir = opendir(filename);
	for (td = tdirhead; td; td = td->next) {
		if (strcmp(filename, td->name) == 0) {
			dirp->tardir = td;
			break;
		}
	}
	if (dirp->sysdir == NULL && dirp->tardir == NULL) {
		free(dirp);
		return NULL;
	}
	return (DIR *)dirp;
}

struct dirent *
tar_readdir(DIR *arg)
{
	struct tar_dirp *dirp;
	struct tar_dir *td;
	struct tar_ent *tp;
	struct dirent *dp;
	static struct dirent dbuf;
	int n;

	dirp = (struct tar_dirp *)arg;
	if (dirp->sysdir != NULL) {
		if ((dp = readdir(dirp->sysdir)) != NULL)
			return dp;
	}
	dp = &dbuf;
	td = dirp->tardir;
	n = dirp->tloc;
	while (n >= td->nent) {
		n -= td->nent;
		td = td->nextent;
		if (td == NULL)
			return NULL;
	}
	tp = &td->ent[n];
	dirp->tloc++;
	strlcpy(dp->d_name, tp->name, sizeof(dp->d_name));
#ifdef notdef	/* no member in SVR4 */
	dp->d_namlen = strlen(dp->d_name);
#endif
#ifdef DT_REG
	dp->d_type = DT_REG;
#endif /* DT_REG */
	return dp;
}

int
tar_closedir(DIR *arg)
{
	struct tar_dirp *dirp;

	dirp = (struct tar_dirp *)arg;
	if (dirp->sysdir)
		closedir(dirp->sysdir);
	free(dirp);
	return 0;
}

int
tar_open(const char *path)
{
	struct tar_fd *fp;
	struct tar_ent *tp;
	int i;

	if ((tp = tar_getent(NULL, path)) == NULL)
		return -1;
	for (fp = tfdarray, i = 0; ; i++) {
		if (i == tfdmax) {
			fp = realloc(tfdarray, tfdmax + TFDBUFSZ);
			if (fp == NULL)
				return -1;
			tfdmax += TFDBUFSZ;
			tfdarray = fp;
			fp += TFDBUFSZ;
			memset(fp, 0, sizeof(*fp) * TFDBUFSZ);
			break;
		}
		if (fp->ent == NULL)
			break;
	}
	fp->ent = tp;
	fp->pos = 0;
	return fp - tfdarray;
}

int
tar_stat(const char *path, struct stat *stbuf)
{
	struct tar_ent *tp;

	if ((tp = tar_getent(NULL, path)) == NULL)
		return -1;
	stbuf->st_size = tp->size;
	stbuf->st_mtime = tp->file->mtime;
	stbuf->st_ino = tp->file->ino;
	return 0;
}

int
tar_fstat(int fd, struct stat *stbuf)
{
	struct tar_fd *fp = tfdarray + fd;

	if (fd < -1 || fd >= tfdmax || fp->ent == NULL)
		return -1;
	stbuf->st_size = fp->ent->size;
	stbuf->st_mtime = fp->ent->file->mtime;
	stbuf->st_ino = fp->ent->file->ino;
	return 0;
}

int
tar_read(int fd, char *buf, int size)
{
	struct tar_fd *fp = tfdarray + fd;
	void *gz;
	int off, cc;

	if (fd < -1 || fd >= tfdmax || fp->ent == NULL)
		return -1;
	if ((gz = fp->ent->file->gz) == NULL) {
		gz = gzopen(fp->ent->file->name, "rb");
		if (gz == NULL)
			return -1;
		/* XXX: too many open files? */
		fp->ent->file->gz = gz;
	}
	off = fp->ent->off + fp->pos;
	if (gzseek(gz, off, SEEK_SET) != off)
		return -1;
	for (off = 0; off < size; off += cc) {
		cc = gzread(gz, buf + off, size - off);
		if (cc < 0)
			break;
	}
	fp->pos += off;
	return off;
}

int
tar_close(int fd)
{
	struct tar_fd *fp = tfdarray + fd;

	if (fd < -1 || fd >= tfdmax || fp->ent == NULL)
		return -1;
	fp->ent = NULL;
	return 0;
}
