// ddiff/dpatch - manipulate differences between debian packages.
// Copyright 2000 Tom Rothamel <tom-ddiff@onegeek.org>
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.  

#include <stdio.h>
#include <string.h>
#include "genfile.h"
#include <malloc.h>

/* #define debug(x, y...) fprintf(stderr, "gftar: " x , ## y) /**/
#define debug(x, y...) /**/

// Notes about the tar garchive implementation:
//
// Tar contains headers for things that aren't files. (Like symlinks, devices
// et al.) Should this code encounter one of these unknown blocks, member
// will return true but finfo->size will be 0. (This will also happen
// with 0-byte files, but we don't really care too much about them, as we
// can treat them as something that isn't a file.)
//
// In all cases, finfo->tar may point to a list of header blocks. These
// blocks should be copied into the reconstructed output before any
// data that follows.

struct TarGA : public GARCHIVE {
	GFILE *parent;
	long memstart;
	long left;
	long offset;
	int pad;
	int valid;
	
	void reset();
	int member();

	char *gets(char *, int);
	size_t read(void *, size_t, size_t);
	void seek(long);
	void skip(long);
	long tell();
	GFILE *dup();
		
	TarBlock *getblock();
	void addtb(TarBlock *);
	void freetbs();
	void fixname();
	
	char *lfn(int);
	void advance(int);
	
	TarGA(GFILE *);
	~TarGA();
};

TarBlock *TarGA::getblock() {
	TarBlock *tb;

	tb = new TarBlock;
	if (parent->read(tb->data, 1, 512) != 512) {
		delete tb;
		return NULL;
	}

	tb->next = NULL;
	return tb;
}

void TarGA::addtb(TarBlock *tb) {
	TarBlock *cur;

	if (!finfo->tar) {
		finfo->tar = tb;
		return;
	}

	cur = finfo->tar;
	while(cur->next) cur = cur->next;

	cur->next = tb;
}
	
void TarGA::freetbs() {
	TarBlock *cur;
	TarBlock *next;

	cur = finfo->tar;

	while (cur) {
		next = cur->next;
		delete cur;
		cur = next;
	}

	finfo->tar = NULL;
}

void TarGA::reset() {
	parent->seek(0);

	left = 0;
	offset = 0;
	valid = 0;
	pad = 0;
}

void TarGA::fixname() {
	char *s;

	s = finfo->name;
	if (!s) return;

	if (s[0] == '.' && s[1] == '/') {
		finfo->name = strdup(&s[2]);
		debug("Fixed '%s' into '%s'.\n", s, finfo->name);
		free(s);
	}
}

char *TarGA::lfn(int len) {
	TarBlock *tb;
	char *s;
	int o = 0;
	int bl;

	s = (char *) malloc(len + 1);
	s[len] = 0;
	
	while (len) {
		bl = (len > 512) ? 512 : len;
		tb = getblock();
		if (!tb) {
			free(s);
			return NULL;
		}
		addtb(tb);

		memcpy(&s[o], tb->data, bl);
		len -= bl;
		o += bl;
	}

	return s;
}
	

void TarGA::advance(int l) {
	int blocks;
	TarBlock *tb;
	
	blocks = l/512;
	if (l % 512) blocks += 1;

	while (blocks--) {
		tb = getblock();
		if (!tb) return;
		addtb(tb);
	}
}

int TarGA::member() {
	TarBlock *tb;
	char type;
	int size;
	char name[101];
	
	parent->skip(left);
	parent->skip(pad);
	
	valid = 0;
	left = 0;
	offset = 0;
	pad = 0;

	if (finfo->name) free(finfo->name);
	finfo->name = NULL;
	finfo->size = 0;
	freetbs();
		
	while (tb = getblock()) {
		addtb(tb);

		if (strncmp(&(tb->data[257]), "ustar", 5)) {
			// We have an invalid block. Ignore it and move
			// along. (This is usually padding at the end.)
			debug("Normal bad magic '%s'.\n", &(tb->data[257]));
			continue;
		}

		type = tb->data[156];
		size = extroct(&(tb->data[124]), 12);
		
		debug("Found a block of type '%c' size %d.\n", type, size);

		switch(type) {
		case '0':
		case '\0':				
			// An honest-to-bob file. We'll set up so that
			// people can access it.

			finfo->type = type;
			
			if (!finfo->name) {
				extrstr(name, tb->data, 100);
				finfo->name = strdup(name);
			}
			
			valid = 1;
			memstart = parent->tell();

			finfo->size = size;
			left = size;
			if (size % 512) {
				pad = 512 - size % 512;
			}

			debug("Regular file '%s' of length %d (%d pad).\n",
			      finfo->name, finfo->size, pad);

			fixname();
			
			return 1;
			
		case '1':
		case '2':
		case '3':
		case '4':
		case '5':
		case '6':
		case '7':
			// Other tar members. We'll take the name, but the
			// size should stay firmly at 0. (We also don't skip
			// around.)
			
			finfo->type = type;
			finfo->size = 0;

			if (!finfo->name) {
				extrstr(name, tb->data, 100);
				finfo->name = strdup(name);
			}	

			debug("Other entry '%s'.\n", finfo->name);

			fixname();
			
			return 1;

		case 'L':
			// The long-filename tar extension.

			if (finfo->name) free(finfo->name);
			finfo->name = lfn(size);
			debug("Found long filename '%s'.\n", finfo->name);
			break;
			
		default:
			debug("Unknown block type '%c'. Advancing.\n", type);
			advance(size);
		}						
	}

	fixname();
	
	if (finfo->tar) return 1;
	return 0;
}

char *TarGA::gets(char *buf, int len) {
	char *rv;
	int l;
	
	if (!valid) return NULL;
	if (!left) return NULL;

	if (len > left + 1) len = left + 1;
	rv = parent->gets(buf, len);
	if (!rv) return rv;

	l = strlen(buf);
	left -= l;
	offset += l;
	
	return rv;
}

size_t TarGA::read(void *buf, size_t size, size_t nmemb) {
	int l;
	
	if (!valid) return 0;
	if (!left) return 0;
	
	while (size * nmemb > left) nmemb--;
	l = parent->read(buf, size, nmemb);
	
	offset += l * size;
	left -= l * size;

	return l;
}

void TarGA::seek(long off) {
	if (!valid) return;

	if (off < offset) {
		parent->seek(memstart);
		offset = 0;
		left = finfo->size;
		skip(off);
		return;
	}

	skip(off - offset);
}

void TarGA::skip(long off) {
	if (!valid) return;

	off = (left < off) ? left : off;
	parent->skip(off);
	left -= off;
	offset += off;
}

long TarGA::tell() {
	return offset;
}

GFILE *TarGA::dup() {
	TarGA *t;

	if (!valid) return NULL;
	t = new TarGA(parent->dup());
	t->parent->seek(memstart - 512);
	t->member();
	
	return t;
}

TarGA::TarGA(GFILE *p) {
	parent = p;
	finfo = new FINFO;
	reset();
}

TarGA::~TarGA() {
	freetbs();
	delete finfo;
	finfo = NULL;
}

GARCHIVE *open_gftar(GFILE *p) {
	return new TarGA(p);
}
