// 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.  

#define _GNU_SOURCE
#include <getopt.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include "config.h"
#include "bdiff.h"
#include "debpatch.h"
#include "md5gf.h"

// Various global state variables.
int verbose = 0;
int iver = OUTPUT_VERSION;

FILE *in; // The debdiff being processed.

Pkg *srcpkg; // The source package.
Pkg *chkpkg; // The package used for checksums.
GFILE *srcfile = NULL; // The file selected by moi from the source package.
GFILE *chkfile = NULL; // The file selected by moi for use by checksum.

// The filename of the new package, and the prefix used to construct it.
char *newfn = NULL;
char *newprefix = ".";

// The gunzip command 
char *gzipcmd = GZIP_COMPRESS;

// Used in arhdr and fixar.
FILE *arf = NULL;
long arhdrloc = -1;

// The global mode.
int mode = 1;

int keep_incomplete = 0;
pid_t decompress_pid = 0;
pid_t decompress_feed = 0;

// Output Stack /////////////////////////////////////////////////////////////

struct OFile {	
	struct OFile *next;
	FILE *f;
	int pid;
	int pida;
	int br;
	char *type;
};

struct OFile *outst = NULL;

void add_file(FILE *f) {
	OFile *of;

	of = new OFile();
	of->f = f;
	of->next = outst;
	of->br = 0;
	of->type = "file";
	of->pid = 0;
	of->pida = 0;
	
	outst = of;
}

void add_pipe(FILE *f, int pid) {
	OFile *of = new OFile();

	of->f = f;
	of->next = outst;
	of->br = 0;
	of->pid = pid;
	of->pida = 0;
	of->type = "pipe";

	outst = of;
}

void add_2pipe(FILE *f, int pid, int pida) {
	OFile *of = new OFile();

	of->f = f;
	of->next = outst;
	of->br = 0;
	of->pid = pid;
	of->pida = pida;
	of->type = "pipe";

	outst = of;
}


// There must be at least one thing on the stack for this to work.
void pop_file() {
	OFile *of;

	of = outst;
	outst = of->next;

	fclose(of->f);
	if (of->pid) waitpid(of->pid, NULL, 0);
	if (of->pida) waitpid(of->pida, NULL, 0);
	
	delete of;
}

// Aliases to make coding look nicer.
#define out outst->f
#define obytes outst->br

// Bdiff ////////////////////////////////////////////////////////////////////

int readint() {
	int i;
	int t = 0;
	int c;

	for (i = 0; i < 4; i++) {
		t *= 256;

		c = fgetc(in);
		if (c == EOF) {
			err("End of file while reading in bdiff.");
			die();
		}

		t += c;
	}

	return t;
}

void bdiffcopy(GFILE *src, int l) {
	int lr;
	char buf[1024];

	while (l) {
		lr = src->read(buf, 1, (l > 1024) ? 1024 : l);
		if (!lr) {
			err("Problem reading from source in bdiff.");
			die();
		}

		fwrite(buf, 1, lr, out);
		
		l-= lr;
		obytes += lr;
	}
}

int bdiffcmd(GFILE *src) {
	int c;
	int first = 1;

	while (1) {
		c = fgetc(in);
		switch(c) {
		case EOF:
			err("End of file while reading in bdiff.");
			die();
		case BDIFF_ENTRY:
			if (first) {
				fputc(BDIFF_ENTRY, out);
				obytes += 1;
			}
			return 1;
		case BDIFF_CMD_DONE:
			return 0;
		case BDIFF_CMD_SEEK4:
			src->seek(readint());
			break;
		case BDIFF_CMD_COPY4:
			bdiffcopy(src, readint());
			break;
		default:
			err("Parsed unknown bdiff command %d.", c);
			die();
		}

		first = 0;
	}
}

void unbdiff(GFILE *src) {
	int c;

	while (1) {
		c = fgetc(in);
		switch(c) {
		case EOF:
			err("End of file while reading in bdiff.");
			die();
		case BDIFF_ENTRY:
			if (!bdiffcmd(src)) return;
			break;
		default:
			fputc(c, out);
			obytes += 1;
		}
	}
}

// Misc /////////////////////////////////////////////////////////////////////

void realgzipcmd(char *o, int l, int lv) {
	snprintf(o, l, gzipcmd, lv);
}

// Commands /////////////////////////////////////////////////////////////////

// The following text is included at the start of commands.txt.

/// Dpatch Command List
/// ===================
///
/// This file contains a list of commands that can be understood by
/// dpatch. Each entry begins with the name of the command and a list
/// of the arguments it takes. That's followed by a paragraph
/// explaining what the command does. The last thing in each
/// entry is the mode that the command is valid in. (Dpatch starts
/// off in header mode.)

/// arhdr <name> <date> <uid> <gid> <mode>
///
/// This outputs an ar header to the current output stream. The current
/// output stream must be seekable, and a fixar command must be output
/// between arhdr commands.
/// (data)
void arhdr(int argc, char **argv) {
	if (arhdrloc > 0) {
		err("fixar must be called between calls to arhdr.");
		die();
	}

	arhdrloc = ftell(out);
	arf = out;
	if (arhdrloc == -1) {
		err("Couldn't tell the current output stream in arhdr.");
		err("(Either arhdr is misplaced, or the output isn't a file.");
		die();
	}

	verbose("Outputting ar header for '%s'", argv[1]);
	
	obytes += fprintf(out, "%-16s%-12s%-6s%-6s%-8s          `\n",
			  argv[1], argv[2], argv[3], argv[4], argv[5]);
}

/// bdiff
///
/// The data following this command is interpreted as a bdiff. The source
/// file is specified by the previous moi command.
/// (moi)
void bdiff(int argc, char **argv) {
	if (!srcfile) {
		err("A moi must be specified before bdiff.");
		die();
	}

	verbose("Applying bdiff.");
	unbdiff(srcfile);
}

/// check <md5>
///
/// This command computes the md5 for the current moi, and compares that
/// to the md5 given with this command. It signals an error and terminates
/// if the two don't match.
/// (moi)
void check(int argc, char **argv) {
	char csum[33];

	if (!chkfile) {
		err("A moi must be specified before checksum.");
		die();
	}

	md5gf(chkfile, csum);

	if (strcmp(argv[1], csum)) {
		err("Source member's checksum doesn't match.");
		die();
	}

	verbose("Checksum matches.");
}
	

/// data <length>
///
/// Copies length bytes of data from the debdiff into the current
/// output stream.
/// (data)
void data(int argc, char **argv) {
	int l;
	int lr;
	char buf[1024];
	
	l = atoi(argv[1]);
	obytes += l;
	
	verbose("Copying %d bytes of data in directly.", l);

	while (l > 0) {
		lr = fread(buf, 1, (l > 1024) ? 1024 : l , in);
		if (!lr) {
			err("Couldn't read input while copying data.");
			die();
		}
		fwrite(buf, 1, lr, out);
		l -= lr;
	}
}	

/// debdiff <version>
///
/// This command should be the first one in a debdiff file. It tells
/// dpatch the version of ddiff that produced this debdiff, so that
/// dpatch could possibly adjust accordingly.
/// (header)
void debdiff(int argc, char **argv) {
	iver = atoi(argv[1]);

	if (iver > OUTPUT_VERSION || iver < EARLIEST_UNDERSTOOD) {
		err("This version of dpatch doesn't understand input version %d.", iver);
		err("(only versions %d to %d are understood.)",
		    EARLIEST_UNDERSTOOD, OUTPUT_VERSION);
		die();
	}

	verbose("Input is of version %d.", iver);
}

/// decompress
///
/// This command tells dpatch that the rest of the input stream is
/// compressed. Dpatch begins decompressing the input stream and
/// continues on.
/// (data)
void decompress(int argc, char **argv) {
	int inp[2];
	int outp[2];

	if (pipe(inp) || pipe(outp)) {
		err("Couldn't open decompression pipes.");
		die();
	}
	    
	decompress_pid = fork();

	if (decompress_pid == -1) {
		err("Couldn't fork decompression process.");
		die();
	}

	if (!decompress_pid) {
		dup2(inp[0], 0);
		dup2(outp[1], 1);

		close(inp[1]);
		close(outp[0]);

		execlp("/bin/sh", "/bin/sh", "-c", GZIP_DECOMPRESS, NULL);

		exit(-1);
	}

	close(inp[0]);
	close(outp[1]);

	decompress_feed = fork();
	
	if (decompress_feed == -1) {
		err("Couldn't fork decompression feed process.");
		die();
	}

	if (!decompress_feed) {
		int l;
		FILE *o;
		char buf[1024];

		close(outp[0]);
		
		o = fdopen(inp[1], "w");
		
		while (l = fread(buf, 1, 1024, in)) {
			fwrite(buf, 1, l, o);
		}

		exit(0);
	}

	fclose(in);
	close(inp[1]);

	verbose("Began input stream decompression.");
	
	in = fdopen(outp[0], "r");	
}


/// end header
///
/// This ends header mode, and tells dpatch to prepare to receive the
/// diff data. After this, dpatch enters data mode.
/// (header)
void end_header() {
	FILE *nof;

	if (!newfn) {
		err("No output file name specified or derived.");
		die();
	}

	nof = fopen(newfn, "w");
	if (!nof) {
		err("Couldn't open output file.");
		die();
	}

	add_file(nof);

	mode = 2;
	obytes = 0;
	verbose("Output file opened, entering body mode.");
}

// This routine dispatches commands beginning with end.
void end(int argc, char **argv) {
	if (!strcmp(argv[1], "header")) {
		end_header();
		return;
	}

	err("Unknown command 'end %s'.", argv[1]);
	die();
}

/// fixar
///
/// This updates the header output the arhdr command to include
/// the length of the data written. It also outputs a padding
/// byte if the length of the data is odd. (As per the ar spec.)
/// This must be called on the same output stream as the previous
/// arhdr command.
/// (data)
void fixar(int argc, char **argv) {
	long newpos;
	long size;

	if (arhdrloc < 0) {
		err("fixar called without a previous arhdr.");
		die();
	}
		
	if (out != arf) {
		err("fixar called on a different output stream.");
		die();
	}

	newpos = ftell(out);

	size = (newpos - arhdrloc) - 60;
	fseek(out, arhdrloc + 48, SEEK_SET);

	fprintf(out, "%d", size);

	fseek(out, newpos, SEEK_SET);

	if (size & 1) {
		fprintf(out, "\n");
	}

	verbose("Added size to arheader and padding to output stream.");
	
	arf = NULL;
	arhdrloc = -1;
}

/// frompkg <name> <version> <arch>
///
/// The frompkg command causes name, version, and architecture checking
/// of the source package to take place. In future versions, this command
/// may invoke code that searches out and opens a matching source file.
/// (header)
void frompkg(int argc, char **argv) {
	if (strcmp(srcpkg->name(), argv[1])) {
		err("Source package name doesn't match diff.");
		err("(source is '%s', diff is '%s')", argv[1],
		    srcpkg->name());
		die();
	}

	if (strcmp(srcpkg->version(), argv[2])) {
		err("Source package version doesn't match diff.");
		err("(source is '%s', diff is '%s')", argv[2],
		    srcpkg->version());
		die();
	}

	if (strcmp(srcpkg->arch(), argv[3])) {
		err("Source package architecture doesn't match diff.");
		err("(source is '%s', diff is '%s')", argv[3],
		    srcpkg->arch());
		die();
	}
}

/// gzip
///
/// This command changes the current output stream to a process
/// that compresses via a gzip process into a new gzip output
/// stream.
/// (data)
void gzip(int argc, char **argv) {
	int newpid;
	int inpipe[2];
	char gzcmd[128];
	
	fflush(out);

	realgzipcmd(gzcmd, 128, 9);
	
	if (pipe(inpipe)) {
		err("Couldn't open pipe in gzip.");
		die();
	}

	newpid = fork();

	if (newpid == -1) {
		err("Couldn't fork in gzip.");
		die();
	}

	if (!newpid) {
		dup2(fileno(out), 1);
		dup2(inpipe[0], 0);
		close(inpipe[1]);

		execlp("/bin/sh", "/bin/sh", "-c", gzcmd, NULL);
		err("Couldn't exec gzip. Big problems!");
		die();
	}

	close(inpipe[0]);

	add_pipe(fdopen(inpipe[1], "w"), newpid);
	verbose("Now writing to gzip pipe.");
}

/// gznh <method> <extra>
///
/// This command changes the current output stream to a header-stripped
/// gzip output stream. When combined with a gzip header sent using the
/// data command, this allows for the exact reconstruction of a gzip
/// stream. (Provided that the same data compresses to the same output
/// stream.) Method and extra are used to check that the compression
/// level matches.
/// (data)
void gznh(int argc, char **argv) {
	int level;
	int method;
	int extra;
	char gzcmd[128];

	fflush(out);
	
	method = atoi(argv[1]);
	extra = atoi(argv[2]);

	if (method != 8) {
		err("Unknown gzip compression method %d\n.");
		die();
	}

	switch(extra) {
	case 0:
		level = 6;
		break;
	case 2:
		level = 9;
		break;
	case 4:
		level = 1;
		break;
	default:
		err("Unknown gzip compression level (extra = %d).\n", extra);
		die();
	}

	realgzipcmd(gzcmd, 128, level);

	do_gznh(out, gzcmd, method, extra);
}	

/// moi <arpart> [tarpart]
///
/// This selects the member of interest from the source package. This
/// changes modes into moi|data mode. It's an error if the package
/// can't be found in the source package. A second command replaces
/// the member of interest.
/// (data)
void moi(int argc, char **argv) {
	char *tarpart = NULL;
	char *arpart = NULL;

	arpart = argv[1];
	if (argc > 2) tarpart = argv[2];

	srcfile = srcpkg->element(arpart, tarpart);

	if (!srcfile) {
		err("Couldn't find source archive element:");
		err("  %s %s", arpart, (tarpart) ? tarpart : "");
		die();
	}

	chkfile = chkpkg->element(arpart, tarpart);

	if (!chkfile) {
		err("Couldn't find check archive element:");
		err("  %s %s", arpart, (tarpart) ? tarpart : "");
		die();
	}

	verbose("moi is %s %s", arpart, (tarpart) ? tarpart : "");

	mode = 6; /* data | moi */
}

/// pop
///
/// This pops the top element off of the output stream stack. It's an
/// error to allow the output stream stack to become empty.
/// (data)
void pop(int argc, char **argv) {
	pop_file();
	if (!outst) {
		err("Popped the last element off the outstack before eof.");
		die();
	}

	verbose("Ended writing to current stream. Now writing to previous %s.",
		outst->type);
}

/// tarh
///
/// This command tells debdiff to copy 512 bytes from the debdiff into
/// the current output stream. (It's used to copy tar header blocks from
/// the input to the output.)
/// (data)
void tarh(int argc, char **argv) {
	char buf[512];
	int l;

	verbose("Tar header block.");
	
	l = fread(buf, 1, 512, in);
	if (l != 512) {
		err("End of file in tar header block.");
		die();
	}

	fwrite(buf, 1, l, out);
	obytes += l;
}

/// tarpad
///
/// This command zero-pads the current output stream to a 512 byte
/// boundary.
/// (data)
void tarpad(int argc, char **argv) {
	char buf[512];
	int l;

	l = 512 - obytes % 512;
	if (l == 512) return;

	bzero((void *) buf, l);
	fwrite(buf, 1, l, out);

	verbose("Added %d bytes of tar padding.", l);
	obytes += l;
}
	

/// topkg <name> <version> <arch>
///
/// If a filename is supplied on the command line to debdiff, this
/// is a no-op command. Otherwise, this supplies information used
/// to select the output file name.
/// (header)
void topkg(int argc, char **argv) {
	char name[1024];

	if (newfn) return;

	snprintf(name, 1024, "%s/%s_%s_%s.deb", newprefix, argv[1],
		 argv[2], argv[3]);
	newfn = strdup(name);
	
	verbose("Using '%s' as output file.", newfn);
}
	
/// debug <arg>
///
/// This causes dpatch to print out arg as a debugging message.
/// (any)
void dprint(int argc, char **argv) {
	verbose("debug in input: %s", argv[1]);
}

void error(int argc, char **argv) {
	err("Couldn't match command '%s' with %d args.", argv[0], argc - 1);
	err("(This takes into account internal state, too.)");
	die();
}

// Debdiff file parsing and command table. //////////////////////////////////

// Mode Numbers
// 1 = Header Mode
// 2 = Non-Header Mode
// 4 = Moi has been seen.

struct _cmdtbl {
	char *name;
	int nargs;
	int mode;
	void (*func)(int, char **);
};

struct _cmdtbl cmdtbl[] = {
	{"arhdr", 5, 2, arhdr},
	{"bdiff", 0, 4, bdiff},
	{"check", 1, 4, check},
	{"debdiff", 1, 1, debdiff},
	{"data", 1, 2, data},
	{"decompress", 0, 2, decompress},
	{"debug", 1, 0, dprint},
	{"end", 1, 1, end},
	{"fixar", 0, 2, fixar},	
	{"frompkg", 3, 1, frompkg},
	{"gzip", 0, 2, gzip},	
	{"gznh", 2, 2, gznh},	
	{"moi", 1, 2, moi},	
	{"moi", 2, 2, moi},	
	{"pop", 0, 2, pop},	
	{"tarh", 0, 2, tarh},	
	{"tarpad", 0, 2, tarpad},	
	{"topkg", 3, 1, topkg},	
	{NULL, 0, 0, error}
};		

// Unquote the string, and turn unquoted spaces into '\1's.
void unquote(char *src, char *dst) {
	int qmode = 0;
	
	while (*src) {
		if (!qmode) {
			switch (*src) {
			case '\n':
				break;
			case '"':
				qmode = 1;
				break;
			case ' ':
				*dst++ = 1;
				break;
			default:
				*dst++ = *src;
				break;
			}
		} else {
			switch(*src) {
			case '"':
				qmode = 0;
				break;
			case '\\':
				src++;
				*dst++ = *src;
				if (!*src) return;
				break;
			default:
				*dst++ = *src;
				break;
			}
		}

		src++;
	}
	*dst = 0;
}

void input() {
	char ibuf[1024];
	char buf[1024];
	char *argv[10];
	char *s;
	int i;
	struct _cmdtbl *ct;
	
	while(fgets(ibuf, 1024, in)) {
		if (ibuf[0] == '#') continue;
 
		unquote(ibuf, buf);

		s = buf;
		argv[0] = buf;
		i = 1;
		
		while (*s) {
			if (*s == 1) {
				if (i == 10) {
					err("Too many arguments in patch file.");
					die();
				}
				
				*s = 0;
				s++;
				argv[i++] = s;
			}

			s++;
		}
		
		ct = cmdtbl;
		while (ct->name) {
			if (!strcmp(ct->name, argv[0]) &&
				ct->nargs == i - 1 &&
				(mode & ct->mode) == ct->mode) break;
			ct++;
		}

		ct->func(i, argv);
	}
}
				
				

// Basic functions, main, etc. //////////////////////////////////////////////

void die() {
	if ((outst) && !keep_incomplete) unlink(newfn);

	if (decompress_pid > 0) {
		kill(SIGTERM, decompress_pid);
		waitpid(decompress_pid, NULL, 0);
	}

	if (decompress_feed > 0) {
		kill(SIGTERM, decompress_feed);
		waitpid(decompress_feed, NULL, 0);
	}

	exit(-1);
}

void usage(char *argv0) {
	fprintf(stderr, "usage: %s [options] <patch file> <original deb> <output file>\n",
		argv0);
	exit(-1);
}

// This is run to close off the output when gfpipe forks.
void onfork() {
	while(outst) pop_file();
}

void version() {
	fprintf(stderr, "debpatch version %s (understands input version %d to %d)\n",
		VERSION, EARLIEST_UNDERSTOOD, OUTPUT_VERSION);
	fprintf(stderr, "\tCopyright 2000 Tom Rothamel\n");
	fprintf(stderr, "\tThis program has ABSOLUTELY NO WARRANTY.\n");
	exit(0);
}

void formats() {
	printf("%d\n%d\n", EARLIEST_UNDERSTOOD, OUTPUT_VERSION);
	exit(0);
}

struct option optab[] = {
	{"formats", 0, NULL, 'F'},
	{"gzip-command", 1, NULL, 'G'},
	{"help", 0, NULL, 'h'},		
	{"keep-incomplete", 0, NULL, 'k'},		
	{"output-prefix", 1, NULL, 'O'},
	{"verbose", 0, NULL, 'v'},
	{"version", 0, NULL, 'V'},
	{NULL, 0, 0, 0}
};

int main(int argc, char **argv) {
	int o;

	while (1) {
		o = getopt_long(argc, argv, "FG:hkO:vV", optab, NULL);
		if (o == -1) break;

		switch(o) {
		case 'F':
			formats();
		case 'G':
			// This command doesn't do anything anymore.
			// gzipcmd = optarg;
			break;
		case 'h':
			usage(argv[0]);
		case 'k':
			keep_incomplete = 1;
			break;
		case 'O':
			newprefix = optarg;
			break;
		case 'v':
			verbose = 1;
			break;
		case 'V':
			version();
		case '?':
			usage(argv[0]);
		}
	}

	if (argc - optind < 2) {
		err("Not enough arguments.");
		usage(argv[0]);
	}

	if (!strcmp("-", argv[optind])) {
		in = stdin;
	} else {
		in = fopen(argv[optind], "r");
		if (!in) {
			err("Couldn't open '%s'.", argv[optind]);
			die();
		}
	}

	srcpkg = open_deb(argv[optind + 1]);
	if (!srcpkg) {
		err("Couldn't open source package '%s'.", argv[optind + 1]);
		die();
	}

	chkpkg = open_deb(argv[optind + 1]);
	if (!chkpkg) {
		err("Couldn't open source package '%s'.", argv[optind + 1]);
		die();
	}
	
	if (argc - optind > 2) {
		newfn = argv[optind + 2];
	}

	on_gfpipe = onfork;
	
	input();

	pop_file();

	if (outst) {
		err("Hit end of file with multiple output streams open.");
		err("(This could be an oddly truncated file.)");
		die();
	}

	if (decompress_pid > 0) {
		waitpid(decompress_pid, NULL, 0);
	}

	if (decompress_feed > 0) {
		waitpid(decompress_feed, NULL, 0);
	}
	
	verbose("done.");
	exit(0);			   
}
