/********************************************************************
 *            dvarchive.c
 *
 *  Tue Aug  1 00:50:52 2006
 *  Copyright  2006-2008  Neil Williams
 *  linux@codehelp.co.uk
 * Copyright (C) 1994,1995 Ian Jackson <ian@chiark.greenend.org.uk>
 *******************************************************************/
/*
	This package 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 3 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, see <http://www.gnu.org/licenses/>.
 */

#ifdef HAVE_CONFIG_H
#  include <config.h>
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <glib.h>
#include <glib/gi18n.h>
#include <glib/gstdio.h>
#include <gio/gio.h>
#include <gtk/gtk.h>
#include <ar.h>
#include <archive.h>
#include <archive_entry.h>
#include <zlib.h>
#include "dvpreview.h"
#include "callbacks.h"
#include "support.h"
#include "interface.h"
#include "dvarchive.h"

#define BLOCKSIZE 10240
#define MAXGZ 1024*512*sizeof(gint)
#define CONTROLTAR "control.tar"
#define DATATAR "data.tar"

typedef enum {
	DV_AR_FAIL = -1,
	DV_OK,
	DV_ERR_NO_FILE,
	DV_ERR_FILE_READ,
	DV_ERR_VERSION,
	DV_ERR_CORRUPT_FILE,
	DV_ERR_NOT_DPKG,
	DV_ERR_COMPRESS,
	DV_ERR_BAD_MEMBER_NAME,
} DVError;

/* details of the files within the .deb */
typedef struct {
	gint64 size;
	gpointer content;
	gboolean compressed;
	GtkWidget *parent;
}DVFileData;

struct DV_s {
	const gchar *filename;
	gpointer readbuffer;
	gchar *copyright;
	GHashTable *control_files;
	GHashTable *data_files;
	struct archive *control_tar;
	struct archive *data_tar;
	struct ar_hdr *current;
	gulong skip;
	GFile * ghandle;
	GInputStream * stream;
	GtkWidget *parent;
	DVError error;
	gboolean store_set;
	GList * subwindows;
	const gchar * ar_message; /* error string from archive/ar_hdr */
	gchar * selected_file;
	gchar * current_path;
	/** indicates if the GFile is a URI or a path */
	gboolean needs_uri;
};

enum compression_type
{ CAT, GZ, BZ2 };

DVContents* dv_create (void) {
	DVContents *deb;

	deb = g_new0 (DVContents, 1);
	deb->control_files = g_hash_table_new (g_str_hash, g_str_equal);
	deb->data_files = g_hash_table_new (g_str_hash, g_str_equal);
	deb->subwindows = NULL;
	return deb;
}

void dv_set_needs_uri (DVContents * deb, gboolean val) {
	g_return_if_fail (deb);
	deb->needs_uri = val;
}

gboolean dv_get_needs_uri (DVContents * deb) {
	/* default value is FALSE and the toolbar callbacks expect FALSE */
	return deb->needs_uri;
}

static void dv_new_window (GFile * deb) {
	DVContents * context;
	GtkWindow *window1;
	gchar * filename;

	g_return_if_fail (deb);
	context = dv_create();
	window1 = GTK_WINDOW(create_deb_gview_window ());
	g_return_if_fail (window1);
	g_object_set_data (G_OBJECT(window1), DVCONTENTS, context);
	set_dv_parent (GTK_WIDGET(window1));
	context->ghandle = deb;
	filename = g_file_get_path (deb);
	/* set the working path for the new window from the .changes file */
	dv_set_working_path (context, filename);
	set_deb_file (deb, context);
	gtk_widget_show (GTK_WIDGET(window1));
	dv_add_child (window1);
}

void dv_parse_changes (GFile * cfile, DVContents * deb) {
	gchar **changes_file;
	gchar * debname, *debline, *contents, *chgpath, * G_GNUC_UNUSED path;
	guint c;
	GFile * parent;
	GError * result;
	gsize changes_size;
	gboolean files;

	g_return_if_fail (cfile);
	result = NULL;
	files = FALSE;
	g_file_load_contents (cfile, NULL, &contents, &changes_size, NULL, &result);
	if (result) {
		g_warning ("Unable to parse .changes file: %s", result->message);
		g_clear_error (&result);
		return;
	}
	path = g_file_get_parse_name (cfile);
	chgpath = NULL;
	parent = g_file_get_parent (cfile);
	chgpath = g_file_get_uri (parent);
	changes_file = g_strsplit (contents, "\n", -1);
	for (c = 0; c < g_strv_length(changes_file); c++) {
		// handle new format .changes files
		if (g_str_has_prefix (changes_file[c], "Checksums")) {
			files = FALSE;
		}
		if (g_str_has_prefix (changes_file[c], "Files")) {
			files = TRUE;
		}
		if (!files) {
			continue;
		}
		debline = NULL;
		debname = NULL;
		debline = g_strrstr (changes_file[c], ".deb");
		if (debline) {
			GFile * debfile;
			gchar * partial;

			/* g_str is safe because this line comes from inside the
			.changes file, not from a path or filename */
			if (!g_str_has_suffix (debline, ".deb")) {
				continue;
			}
			debfile = NULL;
			partial = g_strstrip (g_strrstr (changes_file[c], " "));
			debname = g_strconcat(chgpath, "/", partial, NULL);
			debfile = g_file_new_for_uri (debname);
			if (g_str_has_suffix (debname, ".deb")) {
				dv_new_window (debfile);
			}
		}
	}
}

void dv_archive_clear (DVContents *deb) {
	g_return_if_fail (deb);
	if (deb->control_files) {
		g_hash_table_destroy (deb->control_files);
		deb->control_files = g_hash_table_new (g_str_hash, g_str_equal);
	}
	if (deb->data_files) {
		g_hash_table_destroy (deb->data_files);
		deb->data_files = g_hash_table_new (g_str_hash, g_str_equal);
	}
	if (deb->copyright) {
		g_free (deb->copyright);
	}
}

void dv_archive_free (DVContents *deb) {
	if (!deb) {
		return;
	}
	if (deb->control_files) {
		g_hash_table_destroy (deb->control_files);
	}
	if (deb->data_files) {
		g_hash_table_destroy (deb->data_files);
	}
	if (deb->copyright) {
		g_free (deb->copyright);
	}
	if (deb->ghandle) {
		g_object_unref (deb->ghandle);
	}
	g_free (deb);
	preview_shutdown ();
	deb = NULL;
}

GtkWidget * dv_get_parent (DVContents *deb) {
	g_return_val_if_fail (deb, NULL);
	return deb->parent;
}

void set_dv_parent (GtkWidget * widget) {
	DVContents *deb;

	g_return_if_fail (widget);
	deb = (DVContents*) lookup_widget (widget, DVCONTENTS);
	g_return_if_fail (deb);
	deb->parent = widget;
}

const gchar * dv_get_selected_file (DVContents * deb) {
	return deb->selected_file;
}

gchar * dv_get_working_path (DVContents * deb) {
	g_return_val_if_fail (deb, NULL);
	return deb->current_path;
}

void dv_set_working_path (DVContents * deb, gchar * path) {
	g_return_if_fail (deb);
	if (!path) {
		return;
	}
	g_free (deb->current_path);
	deb->current_path = g_strdup (path);
}

static void dv_show_error (DVError dve, GtkWidget * widget) {
	GtkDialog *dialog;
	GtkWidget *parent;
	gboolean fatal;
	gchar *fmt;
	DVContents *deb;

	g_return_if_fail (widget);
	fatal = FALSE;
	fmt = NULL;
	deb = (DVContents*) lookup_widget (widget, DVCONTENTS);
	parent = (!widget) ? deb->parent : widget;
	switch (dve) {
		case DV_OK:
			return;
		case DV_ERR_NO_FILE: {
			fmt = _("Unable to locate package file, "
				"the file may be empty.");
			break;
		}
		case DV_AR_FAIL :
		case DV_ERR_FILE_READ: {
			fmt = _("Failed to read package file.");
			break;
		}
		case DV_ERR_VERSION: {
			fmt = _("Unsupported archive version.");
			break;
		}
		case DV_ERR_CORRUPT_FILE : {
			fmt = _("Possible corrupted file.");
			break;
		}
		case DV_ERR_NOT_DPKG : {
			fmt = _("Not a debian binary archive.");
			break;
		}
		case DV_ERR_COMPRESS : {
			fmt = _("Unable to decompress file within package.");
			break;
		}
		case DV_ERR_BAD_MEMBER_NAME : {
			fmt = _("Not a valid member name.");
			break;
		}
	}
	if (deb->ar_message) {
		fmt = g_strconcat (fmt, "\n", deb->ar_message, NULL);
	}
	dialog = GTK_DIALOG (gtk_message_dialog_new (GTK_WINDOW(parent),
			GTK_DIALOG_DESTROY_WITH_PARENT,	GTK_MESSAGE_ERROR,
			GTK_BUTTONS_CLOSE, _("Error loading file '%s'. %s"),
			deb->filename, fmt));
	deb->ar_message = NULL;
	gtk_dialog_run (dialog);
	gtk_widget_destroy (GTK_WIDGET (dialog));
	if (fatal) {
		dv_archive_free (deb);
		gtk_main_quit ();
	}
}

void set_deb_file (GFile * gfile, DVContents *deb) {
	gchar * filename;
	g_return_if_fail (deb);
	if (!gfile) {
		dv_show_error (DV_ERR_NO_FILE, deb->parent);
		return;
	}
	if (!deb->ghandle) {
		deb->ghandle = gfile;
	}
	filename = g_file_get_uri (deb->ghandle);
	if ((!g_str_has_suffix(filename, ".deb")) &&
		(!g_str_has_suffix(filename, ".udeb")) &&
		(!g_str_has_suffix(filename, ".tdeb"))) {
		return;
	}
	deb->filename = filename;
	deb->ghandle = gfile;
}

static gint dv_archive_open (struct archive *a, gpointer client_data) {
	DVContents *d = (DVContents*)client_data;
	gchar *size;

	g_return_val_if_fail (d, ARCHIVE_FATAL);
	g_return_val_if_fail (d->readbuffer == NULL, ARCHIVE_FATAL);
	/* deb->pkg already open */
	d->skip = 0;
	size = g_strndup (d->current->ar_size, 10);
	d->skip = g_ascii_strtod (size, NULL);
	g_free (size);
	if (d->skip == 0) {
		return ARCHIVE_FATAL;
	}
	d->readbuffer = g_strnfill (d->skip, ' ');
	if( d->readbuffer == NULL ) {
		archive_set_error(a, ENOMEM, _("Out of memory."));
		d->ar_message = archive_error_string (a);
		return ARCHIVE_FATAL;
	}
	return ARCHIVE_OK;
}

static ssize_t
dv_archive_read (struct archive *a, gpointer client_data, gconstpointer *buffer) {
	gsize bytesread;
	GError * result;

	DVContents *d = (DVContents*)client_data;
	g_return_val_if_fail (d, DV_AR_FAIL);
	result = NULL;
	*buffer = d->readbuffer;
	g_return_val_if_fail (d->readbuffer != NULL, DV_AR_FAIL);
	if (d->skip == 0) {
		return ARCHIVE_OK;
	}
	if (g_input_stream_is_closed (d->stream)) {
		return DV_AR_FAIL;
	}
	bytesread = g_input_stream_read (d->stream, d->readbuffer, d->skip, NULL, &result);
	if (result) {
		archive_set_error (a, errno, _("Error reading from file: %m"));
		d->ar_message = archive_error_string (a);
		dv_show_error (DV_ERR_FILE_READ, d->parent);
		g_clear_error (&result);
		return DV_AR_FAIL;
	}
	if (bytesread == 0) {
		archive_set_error (a, EIO, _("Unexpected end of file."));
		d->ar_message = archive_error_string (a);
		dv_show_error (DV_ERR_FILE_READ, d->parent);
		g_clear_error (&result);
		return DV_AR_FAIL;
	}
	d->skip -= bytesread;
	return bytesread;
}

/* A .deb is two archives within an archive so do NOT
close the FILE at this stage! */
static gint
dv_archive_close (struct archive *r __attribute__ ((unused)), gpointer client_data) {
	DVContents *d = (DVContents*)client_data;

	g_free(d->readbuffer);
	d->readbuffer = NULL;
	return ARCHIVE_OK;
}

/* from dpkg extract.c */
static gulong parseheaderlength (const gchar *inh, size_t len) {
	gchar lintbuf[15];
	gulong r;
	gchar *endp;

	if (memchr (inh, 0, len)) {
		return 0;
	}
	g_assert (sizeof (lintbuf) > len);
	memcpy (lintbuf, inh, len);
	lintbuf[len] = ' ';
	*strchr (lintbuf, ' ') = 0;
	r = strtoul (lintbuf, &endp, 10);
	if (*endp) {
		return 0;
	}
	return r;
}

static gboolean prepare_contents (GFile * gfile, DVContents * deb) {
	gsize G_GNUC_UNUSED bytesread, header, seek;
	GError *result;
	GFileInputStream * filestream;
	struct ar_hdr debian_binary;
	struct ar_hdr controlgz;
	struct ar_hdr datagz;
	gint memberlen, error_num;
	gulong bytes_left;
	gchar * versionbuf, * vers_cmp, *infobuf, * filename;

	g_return_val_if_fail (deb, FALSE);
	result = NULL;
	deb->ghandle = gfile;
	filestream = g_file_read (deb->ghandle, NULL, &result);
	if (result) {
		filename = g_file_get_parse_name (deb->ghandle);
		g_warning ("Error: %s : %s", filename, result->message);
		dv_show_error (DV_ERR_FILE_READ, deb->parent);
		g_free (filename);
		g_clear_error (&result);
		return FALSE;
	}
	deb->stream = (GInputStream*)filestream;
	if (g_input_stream_is_closed (deb->stream)) {
		return FALSE;
	}
	versionbuf = g_new0 (gchar, strlen(ARMAG) + 1);
	bytesread = g_input_stream_read (deb->stream, versionbuf, strlen(ARMAG), NULL, &result);
	if (result) {
		g_warning ("%s", result->message);
		g_clear_error (&result);
		return FALSE;
	}
	vers_cmp = g_strndup (versionbuf, strlen(ARMAG));
	g_free (versionbuf);
	if (strcmp(vers_cmp, ARMAG)) {
		dv_show_error (DV_ERR_VERSION, deb->parent);
		g_input_stream_close (deb->stream, NULL, &result);
		if (result) {
			g_warning ("%s", result->message);
			g_clear_error (&result);
		}
		return FALSE;
	}
	header = sizeof(struct ar_hdr);
	bytesread = g_input_stream_read (deb->stream, &debian_binary, header, NULL, &result);
	if (memcmp (debian_binary.ar_name, "debian-binary   ",
			sizeof (debian_binary.ar_name)) &&
		memcmp (debian_binary.ar_name, "debian-binary/   ",
			sizeof (debian_binary.ar_name)))
	{
		dv_show_error (DV_ERR_NOT_DPKG, deb->parent);
		g_input_stream_close (deb->stream, NULL, &result);
		if (result) {
			g_warning ("%s", result->message);
			g_clear_error (&result);
		}
		return FALSE;
	}
	if (result) {
		g_warning ("%s", result->message);
		g_clear_error (&result);
	}
	memberlen = parseheaderlength (debian_binary.ar_size, sizeof (debian_binary.ar_size));
	if (!memberlen) {
		dv_show_error (DV_ERR_CORRUPT_FILE, deb->parent);
	}
	header = memberlen + 1;
	infobuf = g_new0(gchar, header);
	bytesread = g_input_stream_read (deb->stream, infobuf, memberlen + (memberlen & 1), NULL, &result);
	if (result) {
		g_warning ("%s", result->message);
		g_clear_error (&result);
	}
	g_free (infobuf);

	// control.tar

	// Member after debian-binary usually will be control.tar* but
	// could be files beginning with an underscore _

	// If the next member read begins with an underscore,
	// get its file length and and skip that many bytes 
	// Continue looping until control.tar* is reached

	// Any other member name is incorrect .deb format and will cause an error

	while ((bytesread = g_input_stream_read(deb->stream, &controlgz, sizeof (struct ar_hdr), NULL, &result)) > 0) {
		// If control.tar* are the next bytes, break
		if (memcmp(controlgz.ar_name, CONTROLTAR, strlen(CONTROLTAR)) == 0) {
			break;
		}
		// If it's an underscore member, get its file size,
		// skip that many bytes (+1 if odd number file size), and continue looping
		else if (controlgz.ar_name[0] == '_') {
			memberlen = parseheaderlength (controlgz.ar_size, sizeof (controlgz.ar_size));
			if (!memberlen) {
				dv_show_error (DV_ERR_CORRUPT_FILE, deb->parent);
			}
			g_input_stream_skip(deb->stream, memberlen + (memberlen & 1), NULL, &result);
			if (result) {
				g_warning ("%s", result->message);
				g_clear_error (&result);
			}
			continue;
		}
		// If not an underscore member or control.tar*, invalid member name
		else {
			dv_show_error(DV_ERR_BAD_MEMBER_NAME, deb->parent);
			break;
		}
	}

	if (result) {
		g_warning ("%s", result->message);
		g_clear_error (&result);
	}
	deb->current = &controlgz;
	deb->control_tar = archive_read_new ();
#if ARCHIVE_VERSION_NUMBER < 3000000
	archive_read_support_compression_gzip (deb->control_tar);
	archive_read_support_compression_xz (deb->control_tar);
#else
	archive_read_support_filter_gzip (deb->control_tar);
	archive_read_support_filter_xz (deb->control_tar);
	archive_read_support_filter_zstd (deb->control_tar);
#endif
	archive_read_support_format_tar (deb->control_tar);
	error_num = ARCHIVE_OK;
	error_num = archive_read_open (deb->control_tar, deb, 
		dv_archive_open, dv_archive_read, dv_archive_close);
	if (error_num != ARCHIVE_OK) {
		dv_show_error (DV_ERR_CORRUPT_FILE, deb->parent);
		deb->ar_message = archive_error_string (deb->control_tar);
		g_input_stream_close (deb->stream, NULL, &result);
		if (result) {
			g_warning ("%s", result->message);
			g_clear_error (&result);
		}
		return FALSE;
	} else {
		struct archive_entry *entry;
		gchar *locate;
		gpointer content;
		gint64 size;
		DVFileData *dvfd;

		while (archive_read_next_header (deb->control_tar, &entry) == ARCHIVE_OK) {
			size = archive_entry_size (entry);
			locate = g_strdup (archive_entry_pathname (entry));
			content = g_new0(gpointer, size);
			archive_read_data (deb->control_tar, content, size);
			dvfd = g_new0 (DVFileData, 1);
			dvfd->size = size;
			if(S_ISREG(archive_entry_mode(entry)))
				dvfd->content = content;
			dvfd->compressed = FALSE;
			g_hash_table_insert (deb->control_files, locate, dvfd);
			if ((0 == strcmp (locate, "./control")) ||
				(0 == strcmp (locate, "control")))
			{
				GtkWidget *display_area;
				GtkTextBuffer *buffer;

				/* todo: pattern match Package: Version: for
				title of window.*/
				display_area = lookup_widget (deb->parent, "display");
				buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW(display_area));
				gtk_text_buffer_set_text (buffer, (gchar*)content, -1);
				gtk_text_view_set_buffer (GTK_TEXT_VIEW (display_area),	buffer);
			}
			archive_read_data_skip (deb->control_tar);
		}
	}
	dv_archive_close (deb->control_tar, deb);
	bytes_left = strtoul(controlgz.ar_size,NULL,10);
	/* if the size of control.tar.gz is odd,
	seek one byte to the start of data.tar.gz */
	seek = (bytes_left & 1);
	if (seek > 0) {
		g_input_stream_skip (deb->stream, seek, NULL, &result);
		if (result) {
			dv_show_error (DV_ERR_CORRUPT_FILE, deb->parent);
			deb->ar_message = archive_error_string (deb->control_tar);
			g_input_stream_close (deb->stream, NULL, &result);
			g_warning ("%s", result->message);
			g_clear_error (&result);
			return FALSE;
		}
	}
	
	// data.tar

	// Member after control.tar* usually will be data.tar* but
	// could be files beginning with an underscore _

	// If the next member read begins with an underscore,
	// get its file length and and skip that many bytes 
	// Continue looping until data.tar* is reached

	// If the next member read is data.tar*, continue processing it

	// Any other member name is incorrect .deb format and will cause an error

	while ((bytesread = g_input_stream_read(deb->stream, &datagz, sizeof (struct ar_hdr), NULL, &result)) > 0) {
		// If data.tar* are the next bytes, break
		if (memcmp(datagz.ar_name, DATATAR, strlen(DATATAR)) == 0) {
			break;
		}
		// If it's an underscore member, get its file size,
		// skip that many bytes (+1 if odd number file size), and continue looping
		else if (datagz.ar_name[0] == '_') {
			memberlen = parseheaderlength (datagz.ar_size, sizeof (datagz.ar_size));
			if (!memberlen) {
				dv_show_error (DV_ERR_CORRUPT_FILE, deb->parent);
			}
			g_input_stream_skip(deb->stream, memberlen + (memberlen & 1), NULL, &result);
			if (result) {
				g_warning ("%s", result->message);
				g_clear_error (&result);
			}
			continue;
		}
		// If not an underscore member or data.tar*, invalid member name
		else {
			dv_show_error(DV_ERR_BAD_MEMBER_NAME, deb->parent);
			break;
		}
	}

	if (result) {
		g_warning ("Data tarball read error: %s", result->message);
		g_clear_error (&result);
	}
	deb->current = &datagz;
	deb->data_tar = archive_read_new ();
#if ARCHIVE_VERSION_NUMBER < 3000000
	archive_read_support_compression_gzip (deb->data_tar);
	archive_read_support_compression_xz (deb->data_tar);
	archive_read_support_compression_bzip2 (deb->data_tar);
	archive_read_support_compression_lzma (deb->data_tar);
#else
	archive_read_support_filter_gzip (deb->data_tar);
	archive_read_support_filter_xz (deb->data_tar);
	archive_read_support_filter_zstd (deb->data_tar);
	archive_read_support_filter_bzip2 (deb->data_tar);
	archive_read_support_filter_lzma (deb->data_tar);
#endif
	archive_read_support_format_tar (deb->data_tar);
	error_num = ARCHIVE_OK;
	error_num = archive_read_open (deb->data_tar, deb,
		dv_archive_open, dv_archive_read, dv_archive_close);
	if (error_num != ARCHIVE_OK) {
		dv_show_error (DV_ERR_CORRUPT_FILE, deb->parent);
		deb->ar_message = archive_error_string (deb->data_tar);
		g_input_stream_close (deb->stream, NULL, &result);
		if (result) {
			g_warning ("%s", result->message);
			g_clear_error (&result);
		}
	} else {
		struct archive_entry *entry;
		gpointer *datacontent;
		gint64 size;
		gchar *locate;
		DVFileData *dvfd;

		while (archive_read_next_header (deb->data_tar, &entry) == ARCHIVE_OK) {
			size = archive_entry_size (entry);
			datacontent = g_new0 (gpointer, size);
			archive_read_data (deb->data_tar, datacontent, size);
			/* locate is the name of the .gz file */
			locate = g_strdup (archive_entry_pathname (entry));
			dvfd = g_new0 (DVFileData, 1);
			dvfd->size = size;
			if (g_str_has_suffix (locate, ".gz")) {
				gchar *temp_file = NULL;
				gint err;
				gzFile t;
				gpointer h;
				gint f = g_file_open_tmp ("dvXXXXXX", &temp_file, NULL);
				g_file_set_contents (temp_file, (gchar*)datacontent, size, NULL);
				close (f);
				h = g_new0 (gpointer, MAXGZ);
				t = gzopen (temp_file, "rb");
				err = gzread (t, h, MAXGZ);
				if (err <= -1) {
					deb->ar_message = locate;
					dv_show_error (DV_ERR_COMPRESS, deb->parent);
				}
				gzclose (t);
				dvfd->content = h;
				g_remove (temp_file);
			} else {
				if (g_str_has_suffix (locate, "copyright")) {
					g_hash_table_insert (deb->control_files, "./copyright", dvfd);
					deb->copyright = g_strdup ((gchar*)datacontent);
				}
				dvfd->content = datacontent;
			}
			g_hash_table_insert (deb->data_files, locate, dvfd);
			archive_read_data_skip (deb->data_tar);
		}
#if ARCHIVE_VERSION_NUMBER < 3000000
		archive_read_finish (deb->data_tar);
#else
		archive_read_free (deb->data_tar);
#endif
	}
	g_input_stream_close (deb->stream, NULL, &result);
	if (result) {
		g_warning ("%s", result->message);
		g_clear_error (&result);
	}
	return TRUE;
}

static void
add_control_files (gpointer key, gpointer value, gpointer user_data) {
	DVIter *dvi = (DVIter *) user_data;
	gchar *path = (gchar *) key;
	DVFileData *dvfd = (DVFileData*)value;
	gchar *size_str;

	if (g_str_has_suffix(path, "/")) {
		return;
	}
	size_str = g_strdup_printf ("%" G_GINT64_FORMAT, dvfd->size);
	gtk_tree_store_append (dvi->store, dvi->child_iter, dvi->parent_iter);
	gtk_tree_store_set (dvi->store, dvi->child_iter, PKG_COLUMN,
		g_filename_to_utf8 ((gchar*)key, -1, NULL, NULL, NULL),
		SIZE_COLUMN, size_str, LOCATION_COLUMN,
		g_filename_to_utf8 ((gchar*)key, -1, NULL, NULL, NULL), -1);
}

static void
add_data_files (gpointer key, gpointer value, gpointer user_data) {
	DVIter *dvi = (DVIter *) user_data;
	gchar *path = (gchar *) key;
	gchar *file = g_path_get_basename (path);
	DVFileData *dvfd = (DVFileData*)value;
	gchar *size_str;

	if (g_str_has_suffix(path, "/")) {
		return;
	}
	size_str = g_strdup_printf ("%" G_GINT64_FORMAT, dvfd->size);
	gtk_tree_store_append (dvi->store, dvi->child_iter, dvi->parent_iter);
	gtk_tree_store_set (dvi->store, dvi->child_iter, PKG_COLUMN,
		g_filename_to_utf8 (file, -1, NULL, NULL, NULL),
		SIZE_COLUMN, size_str, LOCATION_COLUMN,
		g_filename_to_utf8 (path, -1, NULL, NULL, NULL), -1);
}

void dv_archive_get_control (DVIter * dvi, DVContents *deb) {
	g_return_if_fail (deb);
	g_hash_table_foreach (deb->control_files, add_control_files, dvi);
}

void dv_archive_get_data (DVIter * dvi, DVContents *deb) {
	g_return_if_fail (deb);
	g_hash_table_foreach (deb->data_files, add_data_files, dvi);
}

static void columns_reset (gpointer value, gpointer user_data) {
	guint G_GNUC_UNUSED cols;
	GtkTreeView *treeview;
	GtkTreeViewColumn *column;

	column = (GtkTreeViewColumn*)value;
	treeview = (GtkTreeView*)user_data;
	gtk_tree_view_column_clear (column);
	cols = gtk_tree_view_remove_column (treeview, column);
}

static gint
size_compare (GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, gpointer user_data) {
	gint result;
	gchar *str_a, *str_b;
	guint64 sz_a, sz_b;

	result = 0;
	gtk_tree_model_get(model, a, SIZE_COLUMN, &str_a, -1);
	gtk_tree_model_get(model, b, SIZE_COLUMN, &str_b, -1);
	if (!str_a) {
		return -1;
	}
	if (!str_b) {
		return 1;
	}
	sz_a = g_ascii_strtoull (str_a, NULL, 10);
	sz_b = g_ascii_strtoull (str_b, NULL, 10);
	if(sz_a != sz_b) {
		result = (sz_a > sz_b) ? 1 : -1;
	}
	g_free(str_a);
	g_free(str_b);
	return result;
}

static void
tree_selection_changed_cb (GtkTreeSelection *selection, gpointer data) {
	GtkTreeIter iter;
	GtkTreeModel *model;
	DVContents *deb;

	deb = (DVContents*) data;
	g_return_if_fail (deb);

	if (gtk_tree_selection_get_selected (selection, &model, &iter)) {
		if (deb->selected_file) {
			g_free (deb->selected_file);
		}
		gtk_tree_model_get (model, &iter, LOCATION_COLUMN, &deb->selected_file, -1);
	}
}

static void
dv_update_title (const gchar * filename, DVContents * deb) {
	const gchar * current;
	gchar * newstr;

	current = _("Debian package file viewer");
	newstr = g_strconcat (filename, " - ", current, NULL);
	gtk_window_set_title (GTK_WINDOW(deb->parent), newstr);
}

/* receives new deb from toolbar selection. */
void open_deb (DVContents * deb) {
	GtkWidget *treeview;
	GtkTreeStore *store;
	GtkTreeModel *model;
	GtkTreeSortable * sort;
	GtkTreeIter parent_iter, orig_iter;
	GtkTreeIter deb_iter, tar_iter, file_iter;
	GtkCellRenderer *renderer;
	GtkTreeViewColumn *column;
	GtkTreeSelection *select;
	DVIter dvi;
	GFileInfo * ginfo;
	GError * result;
	gchar * filename;

	result = NULL;
	g_return_if_fail (deb);
	filename = g_file_get_parse_name (deb->ghandle);
	if (g_file_query_exists (deb->ghandle, NULL)) {
		ginfo = g_file_query_info (deb->ghandle, G_FILE_ATTRIBUTE_STANDARD_SIZE,
			G_FILE_QUERY_INFO_NONE, NULL, &result);
		if (result) {
			g_warning ("%s", result->message);
			g_clear_error (&result);
		}
		if (0 == g_file_info_get_attribute_uint64 (ginfo, G_FILE_ATTRIBUTE_STANDARD_SIZE)) {
			dv_show_error (DV_ERR_NO_FILE, deb->parent);
			return;
		}
	}
	if (!prepare_contents (deb->ghandle, deb)) {
		return;
	}
	dv_update_title(filename, deb);
	treeview = lookup_widget (deb->parent, "treeview");
	g_return_if_fail (treeview);
	model = gtk_tree_view_get_model (GTK_TREE_VIEW(treeview));
	if (deb->store_set) {
		GList *columns = NULL;
		store = GTK_TREE_STORE (model);
		sort = GTK_TREE_SORTABLE(store);
		gtk_tree_store_clear (store);
		columns = gtk_tree_view_get_columns (GTK_TREE_VIEW(treeview));
		g_list_foreach (columns, columns_reset, treeview);
	} else {
		store = gtk_tree_store_new (N_COLUMNS, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING);
		sort = GTK_TREE_SORTABLE(store);
		gtk_tree_view_set_model (GTK_TREE_VIEW (treeview), GTK_TREE_MODEL (sort));
		deb->store_set = TRUE;
	}
	gtk_tree_store_append (store, &parent_iter, NULL);
	gtk_tree_store_set (store, &parent_iter, PKG_COLUMN, "name",
		SIZE_COLUMN, "size", LOCATION_COLUMN, filename, -1);
	renderer = gtk_cell_renderer_text_new ();
	column = gtk_tree_view_column_new_with_attributes
		(_("Package"), renderer, "text", PKG_COLUMN, NULL);
	gtk_tree_sortable_set_sort_column_id (sort, PKG_COLUMN, GTK_SORT_ASCENDING);
	gtk_tree_view_column_set_sort_column_id (column, PKG_COLUMN);
	gtk_tree_view_append_column (GTK_TREE_VIEW (treeview), column);
	column = gtk_tree_view_column_new_with_attributes
		(_("Size"), renderer, "text", SIZE_COLUMN, NULL);
	gtk_tree_view_column_set_sort_column_id (column, SIZE_COLUMN);
	gtk_tree_sortable_set_sort_func (sort, SIZE_COLUMN, size_compare, NULL, NULL);
	gtk_tree_view_append_column (GTK_TREE_VIEW (treeview), column);
	column = gtk_tree_view_column_new_with_attributes
		(_("Location"), renderer, "text", LOCATION_COLUMN, NULL);
	gtk_tree_view_column_set_sort_column_id (column, LOCATION_COLUMN);
	gtk_tree_view_append_column (GTK_TREE_VIEW (treeview), column);
	gtk_tree_store_append (store, &deb_iter, &parent_iter);
	gtk_tree_store_set (store, &deb_iter, PKG_COLUMN,
		_("Debian data"), SIZE_COLUMN, NULL, LOCATION_COLUMN, NULL, -1);
	dvi.store = store;
	dvi.child_iter = &file_iter;
	dvi.parent_iter = &deb_iter;
	dv_archive_get_control (&dvi, deb);

	gtk_tree_store_append (store, &orig_iter, &parent_iter);
	gtk_tree_store_set (store, &orig_iter, PKG_COLUMN,
		_("Upstream data"), SIZE_COLUMN, "", LOCATION_COLUMN, "", -1);
	dvi.store = store;
	dvi.child_iter = &tar_iter;
	dvi.parent_iter = &orig_iter;
	dv_archive_get_data (&dvi, deb);
	gtk_tree_view_expand_all (GTK_TREE_VIEW(treeview));
	select = gtk_tree_view_get_selection (GTK_TREE_VIEW (treeview));
	gtk_tree_selection_set_mode (select, GTK_SELECTION_SINGLE);
	g_signal_connect (G_OBJECT (select), "changed",
		G_CALLBACK (tree_selection_changed_cb), deb);
	gtk_widget_show_all (treeview);
}

void dv_archive_preload (GtkWidget * widget) {
	DVContents *deb;

	deb = (DVContents*) lookup_widget (widget, DVCONTENTS);
	if (deb) {
		if (!deb->ghandle) {
			return;
		}
		open_deb (deb);
	}
}

gchar *
dv_get_control_content (const gchar * file, GtkWidget * widget) {
	DVFileData *dvfd;
	DVContents *deb;
	guint s_id;
	GtkWidget *sbar;

	deb = (DVContents*) lookup_widget (widget, DVCONTENTS);
	g_return_val_if_fail (deb, NULL);
	dvfd = (DVFileData*) g_hash_table_lookup (deb->control_files, file);
	if (!dvfd) {
		return NULL;
	}
	sbar = lookup_widget(GTK_WIDGET(widget), "dvstatusbar");
	s_id = gtk_statusbar_get_context_id (GTK_STATUSBAR(sbar), "errors");
	gtk_statusbar_pop (GTK_STATUSBAR(sbar), s_id);
	return (g_utf8_validate (dvfd->content, -1, NULL)) ? dvfd->content : NULL;
}

static gchar * dv_make_utf8 (gchar * string) {
	gchar *value;

	if (!string) {
		return NULL;
	}
	if (g_utf8_validate (string, -1, NULL)) {
		return string;
	}
	value = g_locale_to_utf8 (string, -1, NULL, NULL, NULL);
	if (!value) {
		value = g_convert (string, -1, "UTF-8", "ISO-8859-15", NULL, NULL, NULL);
		if (!value) {
			return NULL;
		}
		return value;
	}
	return value;
}

gint64 dv_get_content_length (DVContents * deb) {
	DVFileData *dvfd;
	const gchar * file;

	g_return_val_if_fail (deb, 0);
	file = dv_get_selected_file (deb);
	dvfd = (DVFileData*) g_hash_table_lookup (deb->data_files, file);
	if (!dvfd) {
		return 0;
	}
	return dvfd->size;
}

gpointer dv_get_raw_content (DVContents * deb) {
	guint s_id;
	GtkWidget *sbar;
	DVFileData *dvfd;
	const gchar * file;

	g_return_val_if_fail (deb, NULL);
	file = dv_get_selected_file (deb);
	dvfd = (DVFileData*) g_hash_table_lookup (deb->data_files, file);
	if (!dvfd) {
		return NULL;
	}
	sbar = lookup_widget(deb->parent, "dvstatusbar");
	s_id = gtk_statusbar_get_context_id (GTK_STATUSBAR(sbar), "errors");
	gtk_statusbar_pop (GTK_STATUSBAR(sbar), s_id);
	if (dvfd->size == 0) {
		gtk_statusbar_push (GTK_STATUSBAR(sbar), s_id, _("Empty file."));
		return NULL;
	}
	return (gpointer)dvfd->content;
}

gchar * dv_get_data_content (const gchar * file, GtkWidget * widget) {
	guint s_id;
	GtkWidget *sbar;
	gchar *check;
	DVFileData *dvfd;
	DVContents *deb;

	deb = (DVContents*) lookup_widget (widget, DVCONTENTS);
	g_return_val_if_fail (deb, NULL);
	dvfd = (DVFileData*) g_hash_table_lookup (deb->data_files, file);
	if (!dvfd) {
		return NULL;
	}
	sbar = lookup_widget(GTK_WIDGET(widget), "dvstatusbar");
	s_id = gtk_statusbar_get_context_id (GTK_STATUSBAR(sbar), "errors");
	gtk_statusbar_pop (GTK_STATUSBAR(sbar), s_id);
	if (dvfd->size == 0) {
		gtk_statusbar_push (GTK_STATUSBAR(sbar), s_id, _("Empty file."));
		return NULL;
	}
	check = dv_make_utf8((gchar*)dvfd->content);
	if (check == NULL) {
		gtk_statusbar_push (GTK_STATUSBAR(sbar), s_id,
			_("Error: Failed to convert the contents of "
			"the selected file to UTF-8."));
	}
	return check;
}

gboolean quit_last_window (gpointer data) {
	gtk_widget_destroy (gtk_widget_get_toplevel(GTK_WIDGET(data)));
	return FALSE;
}
