// 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 <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include "config.h"
#include "debdiff.h"
#include "pkg.h"
#include "md5gf.h"

FILE *outf;
char *outfn = NULL;
int verbose = 0;
int ocomments = 0;
int compress = 0;
pid_t compress_pid = 0;

// The from package. A global variable so we don't have to keep
// passing it around all the time, and also so we can have a
// global chkpkg, to make the checksums run faster when we go
// for that.
Pkg *pkg;
Pkg *chkpkg;

void do_diff(char *, char *, GFILE *);

// This begins compression of the output stream.
void begin_compress() {
	int p[2];
	
	oprintf("decompress\n");
	fflush(outf);

	if (pipe(p)) {
		err("Couldn't open compression pipe.");
		die();
	}

	verbose("Beginning compression of the output stream.");

	compress_pid = fork();

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

	if (!compress_pid) {
		dup2(p[0], 0);	   
		dup2(fileno(outf), 1);
		close(p[1]);
		
		execlp("/bin/sh", "/bin/sh", "-c", GZIP_COMPRESS_9, NULL);
		exit(-1);
	}

	close(p[0]);
	outf = fdopen(p[1], "w");
}

	      
// Output the commands needed to place the tar header information
// from gf into the reconstructed output.
void tarheader(GFILE *gf) {
	TarBlock *tb;

	tb = gf->finfo->tar;

	while(tb) {
		oprintf("tarh\n");
		fwrite(tb->data, 1, 512, outf);
		tb = tb->next;
	}
}

// Output the checksum command and the md5sum for the current moi.
void check(char *tarpart, char *arpart) {
	GFILE *cf;
	char sum[33];

	cf = chkpkg->element(tarpart, arpart);
	if (!cf) {
		err("Couldn't find element while computing checksum.");
		die();
	}

	md5gf(cf, sum);

	oprintf("check %s\n", sum);
}
	
// Process one of the tar.gzs that make up a deb.
void diff_targz(char *arpart, GFILE *gf) {
	GFILE *gz;
	GARCHIVE *tar;

	gz = open_gfgzip(gf);
	if (!gz) {
		err("Couldn't open gzip decompression pipe.");
		die();
	}

	// Output gzip header commands.
	oprintf("data %d\n", gz->finfo->hl);
	fwrite(gz->finfo->hdr, 1, gz->finfo->hl, outf);
	oprintf("gznh %d %d\n", gz->finfo->method, gz->finfo->extra);
	
	tar = open_gftar(gz);
	if (!tar) {
		err("Couldn't open tar member.");
		die();
	}

	verbose("Entered tar archive.");

	while (tar->member()) {
		tarheader(tar);
		
		if (!tar->finfo->size) {
			if (tar->finfo->name) {
				verbose("- tar non-file data ('%s').", tar->finfo->name);
			} else {
				verbose("- tar non-file data (anonymous).");
			}
			continue;
		}

		verbose("- tar member '%s'", tar->finfo->name);

		do_diff(arpart, tar->finfo->name, tar);
		oprintf("tarpad\n");
	}

	verbose("Leaving tar archive.");

	delete tar;
	delete gz;

	oprintf("pop\n");
}

// Handle the ar that makes up the outer structure of a deb.
void diff_ar(GFILE *gf) {
	GARCHIVE *ar;
	GFILE *oldf;
	
	ar = open_gfarchive(gf);
	if (!ar) {
		err("Couldn't open to package as arfile.");
		die();
	}

	verbose("Opened package as an arfile. About to traverse members.");

	oprintf("data 8\n!<arch>\n");
	
	while (ar->member()) {
		verbose("- ar member '%s'.", ar->finfo->name);

		oprintf("arhdr \"%s\" %d %d %d %o\n",
			ar->finfo->name, ar->finfo->mtime,
			ar->finfo->uid, ar->finfo->gid, ar->finfo->mode);
		
		if (ar->finfo->size) {
			do_diff(ar->finfo->name, NULL, ar);
		}

		oprintf("fixar\n");
	}

	verbose("Done with the arfile.");
	
	delete ar;
}

// If a file member is found, this is called to figure out what to
// do with it. 
void do_diff(char *arpart, char *tarpart, GFILE *to) {
	GFILE *from;

	comment("#\n## %d %s %s\n", to->finfo->size, arpart,
		 (tarpart) ? tarpart : "");
	
	if (!tarpart) {
		if (!strcmp(arpart, "control.tar.gz") ||
		    !strcmp(arpart, "data.tar.gz")) {
			verbose("  Choosing to enter it as a tarfile.");
			diff_targz(arpart, to);
			return;
		}
	}

	if (to->finfo->size == 0) {
		verbose("  Empty file needing no action.");
		return;
	}
		
	from = pkg->element(arpart, tarpart);

	if (!from) {
		verbose("  File is new in target archive. Writing straight.");
		straightdata(outf, to);
		return;
	}

	// Perhaps 256 should be tunable, or something.
	if (to->finfo->size < 256) {
		verbose("  File is short. Writing straight.");
		straightdata(outf, to);
		return;
	}
		
	// Outputting a bdiff takes care of a whole bunch of cases, from
	// identical files to completely different files.
	verbose("  Outputting a bdiff between versions.");
	moi(outf, arpart, tarpart);
	check(arpart, tarpart);
	writebdiff(outf, from, to);
}

// This opens up a package, and prints out a header line with the
// name, version, and arch information.
Pkg *openpkg_header(char *header, char *fn) {
	Pkg *pkg;

	pkg = open_deb(fn);
	if (!pkg) {
		err("Couldn't open package file '%s'.", fn);
		die();
	}

	oprintf("%s %s %s %s\n", header, pkg->name(), pkg->version(),
		pkg->arch());
	verbose("Opened package file '%s' as %s.", fn, header);

	return pkg;
}

// Shuffle off this mortal coil. Delete bad output files, too.
void die() {
	fclose(outf);

	if (compress_pid > 0) waitpid(compress_pid, NULL, 0);
	
	if (outfn) {		
		unlink(outfn);
		verbose("Deleted output file '%s'.", outfn);
	}
	
	exit(-1);
}

void usage(char *argv0) {
	fprintf(stderr, "usage: %s [options] <from> <to>\n",
		argv0);
}

void version() {
	printf("gendebpatch version %s (output version %d)\n",
	       VERSION, OUTPUT_VERSION);
	printf("\tCopyright 2000 Tom Rothamel <tom-debdiff@onegeek.org>\n");
	printf("\tThis program has ABSOLUTELY NO WARRANTY.\n");
}

void onfork() {
	fclose(outf);
}

struct option opts[] = {
	{"compress", 0, NULL, 'z'},
	{"help", 0, NULL, 'h'},
	{"output", 1, NULL, 'o'},
	{"output-comments", 0, NULL, 'c'},	
	{"verbose", 0, NULL, 'v'}, 
	{"version", 0, NULL, 'V'},
	{"blocks", 1, NULL, 'b'},	
	{0, 0, 0, 0}
};

int main(int argc, char **argv) {
	int o;
	char *argv0;
	char *from;
	char *to;
	Pkg *top;
	GFILE *togf;
	int blocks = 256;
	
	argv0 = argv[0];
	outf = stdout;

	while (1) {
		o = getopt_long(argc, argv, "bvchVo:z", opts, NULL);
		if (o == -1) break;

		switch(o) {
		case 'b':
			blocks = atoi(optarg);
			break;
		case 'v':
			verbose = 1;
			break;
		case 'c':
			ocomments = 1;
			break;
		case 'h':
			usage(argv0);
			exit(0);
		case 'V':
			version();
			exit(0);
		case 'o':
			outf = fopen(optarg, "w");
			if (!outf) {
				err("Couldn't open '%s' for writing.", optarg);
				exit(-1);
			}
			outfn = optarg;
			
			break;
		case 'z':
			compress = 1;
			break;
		case '?':
			usage(argv0);
			die();
		}
	}

	on_gfpipe = onfork;
	
	if (!blocks) {
		err("You must specify a valid number of blocks for bdiff.");
		die();
	}

	bdiff_init(blocks);
	
	if (argc - optind != 2) {
		err("Improper number of arguments.");
		usage(argv0);
		die();
	}

	from = argv[optind];
	to = argv[optind + 1];

	oprintf("debdiff %d\n", OUTPUT_VERSION);

	pkg = openpkg_header("frompkg", from);
	top = openpkg_header("topkg", to);
	delete top;

	chkpkg = open_deb(from);
	if (!chkpkg) {
		err("Couldn't reopen from package.");
		die();
	}
	
	oprintf("end header\n");
	verbose("Done writing header. About to start on body.");
	begin_compress();
	
	togf = open_gfstdio(to);
	if (!togf) {
		err("Couldn't reopen to deb file '%s'.", to);
		die();
	}

	diff_ar(togf);

	verbose("Done with the diff. Cleaning up and finishing.");
	
	delete togf;
	delete pkg;
	
	fclose(outf);
	if (compress_pid > 0) waitpid(compress_pid, NULL, 0);
	
	bdiff_free();

	exit(0);
}
