/*
 * File system for tar archives (read-only)
 */

#include <u.h>
#include <libc.h>
#include <auth.h>
#include <fcall.h>
#include "tapefs.h"

/* fundamental constants */
enum {
	Tblock = 512,
	Namsiz = 100,
	Maxpfx = 155,		/* from POSIX */
	Maxname = Namsiz + 1 + Maxpfx,
	Maxlongname = 65535,
	Binsize = 0x80,		/* flag in size[0], from gnu: positive binary size */
	Binnegsz = 0xff,	/* flag in size[0]: negative binary size */
};

/* POSIX link flags */
enum {
	LF_PLAIN1 =	'\0',
	LF_PLAIN2 =	'0',
	LF_LINK =	'1',
	LF_SYMLINK1 =	'2',
	LF_SYMLINK2 =	's',		/* 4BSD used this */
	LF_CHR =	'3',
	LF_BLK =	'4',
	LF_DIR =	'5',
	LF_FIFO =	'6',
	LF_CONTIG =	'7',

	/* 'A' - 'Z' are reserved for custom implementations */

	LF_LONGNAME =	'L',		/* GNU extension */
	LF_LONGLINK =	'K',

};

typedef union {
	char	dummy[Tblock];
	char	tbuf[Maxbuf];
	struct Header {
		char	name[Namsiz];
		char	mode[8];
		char	uid[8];
		char	gid[8];
		char	size[12];
		char	mtime[12];
		char	chksum[8];
		char	linkflag;
		char	linkname[Namsiz];

		/* rest are defined by POSIX's ustar format; see p1003.2b */
		char	magic[6];	/* "ustar" */
		char	version[2];
		char	uname[32];
		char	gname[32];
		char	devmajor[8];
		char	devminor[8];
		char	prefix[Maxpfx]; /* if non-null, path= prefix "/" name */
	};
} Hdr;

Hdr dblock;
int tapefile;

int	checksum(void);

static int
isustar(Hdr *hp)
{
	return strcmp(hp->magic, "ustar") == 0;
}

/*
 * s is at most n bytes long, but need not be NUL-terminated.
 * if shorter than n bytes, all bytes after the first NUL must also
 * be NUL.
 */
static int
strnlen(char *s, int n)
{
	return s[n - 1] != '\0'? n: strlen(s);
}

/* set fullname from header */
static char *
tarname(Hdr *hp)
{
	int pfxlen, namlen;
	static char fullname[Maxname+1];

	namlen = strnlen(hp->name, sizeof hp->name);
	if (hp->prefix[0] == '\0' || !isustar(hp)) {	/* old-style name? */
		memmove(fullname, hp->name, namlen);
		fullname[namlen] = '\0';
		return fullname;
	}

	/* posix name: name is in two pieces */
	pfxlen = strnlen(hp->prefix, sizeof hp->prefix);
	memmove(fullname, hp->prefix, pfxlen);
	fullname[pfxlen] = '/';
	memmove(fullname + pfxlen + 1, hp->name, namlen);
	fullname[pfxlen + 1 + namlen] = '\0';
	return fullname;
}

void
populate(char *name)
{
	char longname[Maxlongname+1];
	char *nextname = nil;
	long chksum, linkflg, namelen;
	vlong blkno;
	char *fname;
	Fileinf f;
	Hdr *hp;

	tapefile = open(name, OREAD);
	if (tapefile < 0)
		error("Can't open argument file");
	replete = 1;
	hp = &dblock;
	for (blkno = 0; ; blkno++) {
		seek(tapefile, Tblock*blkno, 0);
		if (readn(tapefile, hp->dummy, sizeof hp->dummy) < sizeof hp->dummy)
			break;
		fname = nextname, nextname = nil;
		if(fname == nil || fname[0] == '\0')
			fname = tarname(hp);
		if (fname[0] == '\0')
			break;

		/* crack header */
		f.addr = blkno + 1;
		f.mode = strtoul(hp->mode, 0, 8);
		f.uid  = strtoul(hp->uid, 0, 8);
		f.gid  = strtoul(hp->gid, 0, 8);
		if((uchar)hp->size[0] == 0x80)
			f.size = b8byte(hp->size+3);
		else
			f.size = strtoull(hp->size, 0, 8);
		f.mdate = strtoul(hp->mtime, 0, 8);
		chksum  = strtoul(hp->chksum, 0, 8);
		/* the mode test is ugly but sometimes necessary */
		if (hp->linkflag == LF_DIR || (f.mode&0170000) == 040000 ||
		    strrchr(fname, '\0')[-1] == '/'){
			f.mode |= DMDIR;
			f.size = 0;
		}
		f.mode &= DMDIR | 0777;

		/* make file name safe, canonical and free of . and .. */
		while (fname[0] == '/')		/* don't allow absolute paths */
			++fname;
		cleanname(fname);
		while (strncmp(fname, "../", 3) == 0)
			fname += 3;

		/* reject links */
		linkflg = hp->linkflag == LF_SYMLINK1 ||
			hp->linkflag == LF_SYMLINK2 || hp->linkflag == LF_LINK;
		if (chksum != checksum()){
			fprint(2, "%s: bad checksum on %.28s at offset %lld\n",
				argv0, fname, Tblock*blkno);
			exits("checksum");
		}
		if (linkflg) {
			/*fprint(2, "link %s->%s skipped\n", fname, hp->linkname);*/
			f.size = 0;
		} else if (hp->linkflag == LF_LONGLINK) {
			;
		} else if (hp->linkflag == LF_LONGNAME) {
			namelen = Maxlongname;
			if(f.size < namelen)
				namelen = f.size;
			namelen = readn(tapefile, longname, namelen);
			if(namelen < 0) namelen = 0;
			longname[namelen] = '\0';
			nextname = longname;
		} else {
			/* accept this file */
			f.name = fname;
			if (f.name[0] == '\0')
				fprint(2, "%s: null name skipped\n", argv0);
			else
				poppath(f, 1);
		}
		blkno += (f.size + Tblock - 1)/Tblock;
	}
}

void
dotrunc(Ram *r)
{
	USED(r);
}

void
docreate(Ram *r)
{
	USED(r);
}

char *
doread(Ram *r, vlong off, long cnt)
{
	int n;

	seek(tapefile, Tblock*r->addr + off, 0);
	if (cnt > sizeof dblock.tbuf)
		error("read too big");
	n = readn(tapefile, dblock.tbuf, cnt);
	if (n != cnt)
		memset(dblock.tbuf + n, 0, cnt - n);
	return dblock.tbuf;
}

void
popdir(Ram *r)
{
	USED(r);
}

void
dowrite(Ram *r, char *buf, long off, long cnt)
{
	USED(r); USED(buf); USED(off); USED(cnt);
}

int
dopermw(Ram *r)
{
	USED(r);
	return 0;
}

int
checksum(void)
{
	int i, n;
	uchar *cp;

	memset(dblock.chksum, ' ', sizeof dblock.chksum);
	cp = (uchar *)dblock.dummy;
	i = 0;
	for (n = Tblock; n-- > 0; )
		i += *cp++;
	return i;
}
