/*
** Copyright (c) 2011 Roy S. Keene
**
** This program is free software; you can redistribute it and/or
** modify it under the terms of the Simplified BSD License (also
** known as the "2-Clause License" or "FreeBSD License".)

** 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.
**
** Author contact information:
**   fossil@rkeene.org
**   http://www.rkeene.org/
**
*******************************************************************************
**
** This file contains code used to generate USTAR gzip'd archives.
*/
#include <assert.h>
#include <zlib.h>
#include "config.h"
#include "tar.h"

/* Convert a Fossil date to a UNIX date */
static unsigned long get_unix_time_from_fossil(double rDate) {
	unsigned long unixTime;

	unixTime = (rDate - 2440587.5) * 86400.0;

	return(unixTime);
}


/*
 ** Initialize a new tar.gz archive.
 */
void tgz_open(Blob *pTar) {
	blob_zero(pTar);

	return;
}

void tgz_close(Blob *pTar) {
	unsigned char gzip_header[10], gzip_trailer[8];
	unsigned long tar_length, tar_checksum;
	char tar_header[512];
	Blob temp_blob;

	/* Terminate with 2 blank blocks */
	memset(tar_header, '\0', sizeof(tar_header));
	blob_append(pTar, tar_header, sizeof(tar_header));
	blob_append(pTar, tar_header, sizeof(tar_header));

	/* Store CRC32 and length of uncompressed data for later use */
	tar_length = blob_size(pTar);
	tar_checksum = crc32(0, NULL, 0);
	tar_checksum = crc32(tar_checksum, (unsigned char *) blob_buffer(pTar), blob_size(pTar));

	/* Compress data */
	blob_compress(pTar, pTar);

	/* GZIP Header from From RFC 1952 */
	/** ID1 and ID2 **/
	gzip_header[0] = 0x1F;
	gzip_header[1] = 0x8B;

	/** Compression Method (CM) **/
	gzip_header[2] = 8; /* Deflate */

	/** Flags (FLG) **/
	gzip_header[3] = 0;

	/** Modification Time (MTIME) **/
	gzip_header[4] = 0;
	gzip_header[5] = 0;
	gzip_header[6] = 0;
	gzip_header[7] = 0;

	/** Extra Flags, type of compression used (XFL) **/
	gzip_header[8] = 0; /* Default */

	/** Operating System (OS) **/
	gzip_header[9] = 255;

	/** Uncompressed data CRC32 (CRC32) **/
	gzip_trailer[3] = (tar_checksum >> 24) & 0xff;
	gzip_trailer[2] = (tar_checksum >> 16) & 0xff;
	gzip_trailer[1] = (tar_checksum >>  8) & 0xff;
	gzip_trailer[0] = (tar_checksum >>  0) & 0xff;

	/** Uncompressed size (ISIZE) **/
	gzip_trailer[7] = (tar_length >> 24) & 0xff;
	gzip_trailer[6] = (tar_length >> 16) & 0xff;
	gzip_trailer[5] = (tar_length >>  8) & 0xff;
	gzip_trailer[4] = (tar_length >>  0) & 0xff;

	/* Combine header, data, and trailer into temporary blob */
	blob_zero(&temp_blob);

	/** Append gzip header to buffer **/
	blob_append(&temp_blob, (char *) gzip_header, sizeof(gzip_header));

	/** Append data to buffer **/
	if (blob_size(pTar) >= 10) {
		blob_append(&temp_blob, blob_buffer(pTar) + 6, blob_size(pTar) - 10);
	}

	/** Append trailer to buffer **/
	blob_append(&temp_blob, (char *) gzip_trailer, sizeof(gzip_trailer));

	/* Copy temporary blob to output blob */
	blob_copy(pTar, &temp_blob);

	/* Release temporary blob */
	blob_zero(&temp_blob);

	return;
}

/*
** Append a single file to a growing tar.gz archive.
**
** pFile is the file to be appended.  zName is the name
** that the file should be saved as.
*/
void tgz_add_file(Blob *pTar, const char *zName, const Blob *pFile, unsigned long file_mtime, int executable) {
	char tar_header[512], tar_padding[512];
	char *local_zName = NULL, *dirname, *filename;
	unsigned long tar_checksum;
	unsigned int tar_mode, tar_type;
	int tar_padding_bytes;
	int i;

	if (strlen(zName) < 100) {
		filename = (char *) zName;
		dirname = "";
	} else {
		local_zName = strdup(zName);

		/*
		 * Split the filename into 2 parts, the directory name and
		 * the file name.  USTAR allows us to fit longer file names
		 * if we do this.
		 */
		filename = strrchr(local_zName, '/');
		if (filename) {
			*filename = '\0';
			filename++;

			dirname = local_zName;
		} else {
			filename = local_zName;
			dirname = "";
		}
	}

	/* Verify that the file attributes can fit into a USTAR header */
	if (strlen(filename) >= 100) {
		if (local_zName) {
			free(local_zName);
		}

		return;
	}

	if (strlen(dirname) >= 155) {
		if (local_zName) {
			free(local_zName);
		}

		return;
	}

	if (blob_size(pFile) >= 0x200000000LLU) {
		if (local_zName) {
			free(local_zName);
		}

		return;
	}

	/* Determine file attributes */
	/** Type **/
	tar_type = 0; /* Regular file */

	/** Permissions **/
	if (executable) {
		tar_mode = 0755;
	} else {
		tar_mode = 0644;
	}

	/* Create USTAR header */
	/** Clear header **/
	memset(tar_header, '\0', sizeof(tar_header));
	/** File name **/
	memcpy(tar_header, filename, strlen(filename));

	/** Attributes **/
	sprintf(tar_header + 100, "%07o", tar_mode);

	/** User ID (Numeric) **/
	sprintf(tar_header + 108, "%07o", 0);

	/** Group ID (Numeric) **/
	sprintf(tar_header + 116, "%07o", 0);

	/** File Size **/
	sprintf(tar_header + 124, "%011o", (unsigned int) blob_size(pFile));

	/** Modification Time **/
	sprintf(tar_header + 136, "%011lo", file_mtime);

	/** Checksum dummy value (will be replaced later) **/
	sprintf(tar_header + 148, "        ");

	/** File Type **/
	sprintf(tar_header + 156, "%1o", (unsigned int) tar_type);

	/** US-TAR Header Marker **/
	sprintf(tar_header + 257, "ustar");

	/** ??? **/
	sprintf(tar_header + 263, "  ");

	/** User ID (Text) **/
	sprintf(tar_header + 265, "%s", "root");

	/** Group ID (Text) **/
	sprintf(tar_header + 297, "%s", "root");

	/** Directory Name **/
	sprintf(tar_header + 345, "%s", dirname);

	/* Compute header checksum */
	tar_checksum = 0;
	for (i = 0; i < sizeof(tar_header); i++) {
		tar_checksum += (unsigned char) tar_header[i];
	}

	/** Checksum **/
	sprintf(tar_header + 148, "%06lo", tar_checksum);

	/* Clean up */
	if (local_zName) {
		free(local_zName);
	}

	/** Append header to buffer **/
	blob_append(pTar, tar_header, sizeof(tar_header));

	/* Append data */
	/** Append contents **/
	blob_append(pTar, blob_buffer(pFile), blob_size(pFile));

	/** Append padding to align to 512 byte blocks **/
	if ((blob_size(pFile) % sizeof(tar_padding)) != 0) {
		memset(tar_padding, '\0', sizeof(tar_padding));

		tar_padding_bytes = sizeof(tar_padding) - (blob_size(pFile) % sizeof(tar_padding));

		blob_append(pTar, tar_padding, tar_padding_bytes);
	}

	return;
}


/*
** COMMAND: test-filetgz
**
** Generate a tar.gz archive specified by the first argument that
** contains files given in the second and subsequent arguments.
*/
void filetgz_cmd(void) {
	int i;
	Blob targz;
	Blob file;

	if (g.argc < 3) {
		usage("ARCHIVE FILE....");
	}

	tgz_open(&targz);

	for (i = 3; i < g.argc; i++) {
		blob_zero(&file);

		blob_read_from_file(&file, g.argv[i]);

		tgz_add_file(&targz, g.argv[i], &file, 0, 0);

		blob_reset(&file);
	}

	tgz_close(&targz);

	blob_write_to_file(&targz, g.argv[2]);
}

/*
** Given the RID for a manifest, construct a tar.gz archive containing
** all files in the corresponding baseline.
**
** If RID is for an object that is not a real manifest, then the
** resulting tar.gz archive contains a single file which is the RID
** object.
**
** If the RID object does not exist in the repository, then
** pTar is zeroed.
**
** zDir is a "synthetic" subdirectory which all targzped files get
** added to as part of the targz file. It may be 0 or an empty string,
** in which case it is ignored. The intention is to create a targz which
** politely expands into a subdir instead of filling your current dir
** with source files. For example, pass a UUID or "ProjectName".
**
*/
void tgz_of_baseline(int rid, Blob *pTar, const char *zDir) {
	Blob mfile, hash, file;
	Manifest *pManifest;
	ManifestFile *pFile;
	Blob filename;
	unsigned long nFileTimeStamp;
	int bExecutable;
	int nPrefix;
  
	content_get(rid, &mfile);
	if (blob_size(&mfile) == 0) {
		blob_zero(pTar);
		return;
	}

	blob_zero(&hash);
	blob_zero(&filename);

	tgz_open(pTar);

	if (zDir && zDir[0]) {
		blob_appendf(&filename, "%s/", zDir);
	}
	nPrefix = blob_size(&filename);

	pManifest = manifest_get(rid, CFTYPE_MANIFEST);
	if (pManifest) {
		char *zName;

		nFileTimeStamp = get_unix_time_from_fossil(pManifest->rDate);

		if (db_get_boolean("manifest", 0)) {
			blob_append(&filename, "manifest", -1);
			zName = blob_str(&filename);
			tgz_add_file(pTar, zName, &mfile, nFileTimeStamp, 0);

			sha1sum_blob(&mfile, &hash);
			blob_reset(&mfile);
			blob_append(&hash, "\n", 1);
			blob_resize(&filename, nPrefix);
			blob_append(&filename, "manifest.uuid", -1);
			zName = blob_str(&filename);
			tgz_add_file(pTar, zName, &hash, nFileTimeStamp, 0);
			blob_reset(&hash);
		}

		manifest_file_rewind(pManifest);

		while ((pFile = manifest_file_next(pManifest,0)) !=0) {
			int fid;

			fid = uuid_to_rid(pFile->zUuid, 0);

			if (fid) {
				if (pFile->zPerm && strchr(pFile->zPerm, 'x')) {
					bExecutable = 1;
				} else {
					bExecutable = 0;
				}

				content_get(fid, &file);
				blob_resize(&filename, nPrefix);
				blob_append(&filename, pFile->zName, -1);
				zName = blob_str(&filename);
				tgz_add_file(pTar, zName, &file, nFileTimeStamp, bExecutable); 
				blob_reset(&file);
			}
		}
	} else {
		blob_reset(&mfile);
	}

	manifest_destroy(pManifest);

	blob_reset(&filename);

	tgz_close(pTar);
}

/*
** COMMAND: tgz
**
** Usage: %fossil tgz VERSION OUTPUTFILE [--name DIRECTORYNAME]
**
** Generate a tar.gz archive for a specified version.  If the --name option is
** used, it argument becomes the name of the top-level directory in the
** resulting tar.gz archive.  If --name is omitted, the top-level directory
** named is derived from the project name, the check-in date and time, and
** the artifact ID of the check-in.
*/
void baseline_tgz_cmd(void) {
	int rid;
	Blob targz;
	const char *zName;

	zName = find_option("name", 0, 1);

	db_find_and_open_repository(0, 0);
	if (g.argc != 4) {
		usage("VERSION OUTPUTFILE");
	}

	rid = name_to_rid(g.argv[2]);
	if (zName == 0) {
		zName = db_text("default-name",
		                "SELECT replace(%Q,' ','_') "
		                " || strftime('_%%Y-%%m-%%d_%%H%%M%%S_', event.mtime) "
		                " || substr(blob.uuid, 1, 10)"
		                "  FROM event, blob"
		                " WHERE event.objid=%d"
		                "   AND blob.rid=%d",
		                db_get("project-name", "unnamed"), rid, rid
		               );
	}

	tgz_of_baseline(rid, &targz, zName);

	blob_write_to_file(&targz, g.argv[3]);
}

/*
** WEBPAGE: tgz
** URL: /tgz/RID.tgz
**
** Generate a tar.gz archive for the baseline.
** Return that tar.gz archive as the HTTP reply content.
*/
void baseline_tgz_page(void) {
	int rid;
	char *zName, *zRid;
	int nName, nRid;
	Blob targz;

	login_check_credentials();
	if (!g.okZip) {
		login_needed();

		return;
	}

	zName = mprintf("%s", PD("name", ""));
	nName = strlen(zName);

	zRid = mprintf("%s", PD("uuid", ""));
	nRid = strlen(zRid);

	for (nName = strlen(zName) - 1; nName > 5; nName--) {
		if (zName[nName] == '.') {
			zName[nName] = 0;
			break;
		}
	}

	rid = name_to_rid(nRid ? zRid : zName);
	if (rid == 0) {
		@ Not found
		return;
	}

	if (nRid == 0 && nName > 10) {
		zName[10] = 0;
	}

	tgz_of_baseline(rid, &targz, zName);

	free(zName);
	free(zRid);

	cgi_set_content(&targz);
	cgi_set_content_type("application/x-compressed");
}
