/*
  IMPORTANT NOTE: IF THIS FILE IS CHANGED, WININST-6.EXE MUST BE RECOMPILED
  WITH THE MSVC6 WININST.DSW WORKSPACE FILE MANUALLY, AND WININST-7.1.EXE MUST
  BE RECOMPILED WITH THE MSVC 2003.NET WININST-7.1.VCPROJ FILE MANUALLY.

  IF CHANGES TO THIS FILE ARE CHECKED INTO PYTHON CVS, THE RECOMPILED BINARIES
  MUST BE CHECKED IN AS WELL!
*/

#include <windows.h>

#include "zlib.h"

#include <stdio.h>
#include <stdarg.h>

#include "archive.h"

/* Convert unix-path to dos-path */
static void normpath(char *path)
{
	while (path && *path) {
		if (*path == '/')
			*path = '\\';
		++path;
	}
}

BOOL ensure_directory(char *pathname, char *new_part, NOTIFYPROC notify)
{
	while (new_part && *new_part && (new_part = strchr(new_part, '\\'))) {
		DWORD attr;
		*new_part = '\0';
		attr = GetFileAttributes(pathname);
		if (attr == -1) {
			/* nothing found */
			if (!CreateDirectory(pathname, NULL) && notify)
				notify(SYSTEM_ERROR,
				       "CreateDirectory (%s)", pathname);
			else
				notify(DIR_CREATED, pathname);
		}
		if (attr & FILE_ATTRIBUTE_DIRECTORY) {
			;
		} else {
			SetLastError(183);
			if (notify)
				notify(SYSTEM_ERROR,
				       "CreateDirectory (%s)", pathname);
		}
		*new_part = '\\';
		++new_part;
	}
	return TRUE;
}

/* XXX Should better explicitely specify
 * uncomp_size and file_times instead of pfhdr!
 */
char *map_new_file(DWORD flags, char *filename,
		   char *pathname_part, int size,
		   WORD wFatDate, WORD wFatTime,
		   NOTIFYPROC notify)
{
	HANDLE hFile, hFileMapping;
	char *dst;
	FILETIME ft;

  try_again:
	if (!flags)
		flags = CREATE_NEW;
	hFile = CreateFile(filename,
			   GENERIC_WRITE | GENERIC_READ,
			   0, NULL,
			   flags,
			   FILE_ATTRIBUTE_NORMAL, NULL);
	if (hFile == INVALID_HANDLE_VALUE) {
		DWORD x = GetLastError();
		switch (x) {
		case ERROR_FILE_EXISTS:
			if (notify && notify(CAN_OVERWRITE, filename))
				hFile = CreateFile(filename,
						   GENERIC_WRITE|GENERIC_READ,
						   0, NULL,
						   CREATE_ALWAYS,
						   FILE_ATTRIBUTE_NORMAL,
						   NULL);
			else {
				if (notify)
					notify(FILE_OVERWRITTEN, filename);
				return NULL;
			}
			break;
		case ERROR_PATH_NOT_FOUND:
			if (ensure_directory(filename, pathname_part, notify))
				goto try_again;
			else
				return FALSE;
			break;
		default:
			SetLastError(x);
			break;
		}
	}
	if (hFile == INVALID_HANDLE_VALUE) {
		if (notify)
			notify (SYSTEM_ERROR, "CreateFile (%s)", filename);
		return NULL;
	}

	if (notify)
		notify(FILE_CREATED, filename);

	DosDateTimeToFileTime(wFatDate, wFatTime, &ft);
	SetFileTime(hFile, &ft, &ft, &ft);


	if (size == 0) {
		/* We cannot map a zero-length file (Also it makes
		   no sense */
		CloseHandle(hFile);
		return NULL;
	}

	hFileMapping = CreateFileMapping(hFile,
					 NULL, PAGE_READWRITE, 0, size, NULL);

	CloseHandle(hFile);

	if (hFileMapping == INVALID_HANDLE_VALUE) {
		if (notify)
			notify(SYSTEM_ERROR,
			       "CreateFileMapping (%s)", filename);
		return NULL;
	}

	dst = MapViewOfFile(hFileMapping,
			    FILE_MAP_WRITE, 0, 0, 0);

	CloseHandle(hFileMapping);

	if (!dst) {
		if (notify)
			notify(SYSTEM_ERROR, "MapViewOfFile (%s)", filename);
		return NULL;
	}
	return dst;
}


BOOL
extract_file(char *dst, char *src, int method, int comp_size,
	     int uncomp_size, NOTIFYPROC notify)
{
	z_stream zstream;
	int result;

	if (method == Z_DEFLATED) {
		int x;
		memset(&zstream, 0, sizeof(zstream));
		zstream.next_in = src;
		zstream.avail_in = comp_size+1;
		zstream.next_out = dst;
		zstream.avail_out = uncomp_size;

/* Apparently an undocumented feature of zlib: Set windowsize
   to negative values to supress the gzip header and be compatible with
   zip! */
		result = TRUE;
		if (Z_OK != (x = inflateInit2(&zstream, -15))) {
			if (notify)
				notify(ZLIB_ERROR,
				       "inflateInit2 returns %d", x);
			result = FALSE;
			goto cleanup;
		}
		if (Z_STREAM_END != (x = inflate(&zstream, Z_FINISH))) {
			if (notify)
				notify(ZLIB_ERROR,
				       "inflate returns %d", x);
			result = FALSE;
		}
	  cleanup:
		if (Z_OK != (x = inflateEnd(&zstream))) {
			if (notify)
				notify (ZLIB_ERROR,
					"inflateEnd returns %d", x);
			result = FALSE;
		}
	} else if (method == 0) {
		memcpy(dst, src, uncomp_size);
		result = TRUE;
	} else
		result = FALSE;
	UnmapViewOfFile(dst);
	return result;
}

/* Open a zip-compatible archive and extract all files
 * into the specified directory (which is assumed to exist)
 */
BOOL
unzip_archive(SCHEME *scheme, char *dirname, char *data, DWORD size,
	      NOTIFYPROC notify)
{
	int n;
	char pathname[MAX_PATH];
	char *new_part;

	/* read the end of central directory record */
	struct eof_cdir *pe = (struct eof_cdir *)&data[size - sizeof
						       (struct eof_cdir)];

	int arc_start = size - sizeof (struct eof_cdir) - pe->nBytesCDir -
		pe->ofsCDir;

	/* set position to start of central directory */
	int pos = arc_start + pe->ofsCDir;

	/* make sure this is a zip file */
	if (pe->tag != 0x06054b50)
		return FALSE;
    
	/* Loop through the central directory, reading all entries */
	for (n = 0; n < pe->nTotalCDir; ++n) {
		int i;
		char *fname;
		char *pcomp;
		char *dst;
		struct cdir *pcdir;
		struct fhdr *pfhdr;

		pcdir = (struct cdir *)&data[pos];
		pfhdr = (struct fhdr *)&data[pcdir->ofs_local_header +
					     arc_start];

		if (pcdir->tag != 0x02014b50)
			return FALSE;
		if (pfhdr->tag != 0x04034b50)
			return FALSE;
		pos += sizeof(struct cdir);
		fname = (char *)&data[pos]; /* This is not null terminated! */
		pos += pcdir->fname_length + pcdir->extra_length +
			pcdir->comment_length;

		pcomp = &data[pcdir->ofs_local_header
			      + sizeof(struct fhdr)
			      + arc_start
			      + pfhdr->fname_length
			      + pfhdr->extra_length];

		/* dirname is the Python home directory (prefix) */
		strcpy(pathname, dirname);
		if (pathname[strlen(pathname)-1] != '\\')
			strcat(pathname, "\\");
		new_part = &pathname[lstrlen(pathname)];
		/* we must now match the first part of the pathname
		 * in the archive to a component in the installation
		 * scheme (PURELIB, PLATLIB, HEADERS, SCRIPTS, or DATA)
		 * and replace this part by the one in the scheme to use
		 */
		for (i = 0; scheme[i].name; ++i) {
			if (0 == strnicmp(scheme[i].name, fname,
					  strlen(scheme[i].name))) {
				char *rest;
				int len;
				
				/* length of the replaced part */
				int namelen = strlen(scheme[i].name);
				
				strcat(pathname, scheme[i].prefix);
				
				rest = fname + namelen;
				len = pfhdr->fname_length - namelen;
				
				if ((pathname[strlen(pathname)-1] != '\\')
				    && (pathname[strlen(pathname)-1] != '/'))
					strcat(pathname, "\\");
				/* Now that pathname ends with a separator,
				 * we must make sure rest does not start with
				 * an additional one.
				 */
				if ((rest[0] == '\\') || (rest[0] == '/')) {
					++rest;
					--len;
				}

				strncat(pathname, rest, len);
				goto Done;
			}
		}
		/* no prefix to replace found, go unchanged */
		strncat(pathname, fname, pfhdr->fname_length);
	  Done:
		normpath(pathname);
		if (pathname[strlen(pathname)-1] != '\\') {
			/*
			 * The local file header (pfhdr) does not always
			 * contain the compressed and uncompressed sizes of
			 * the data depending on bit 3 of the flags field.  So
			 * it seems better to use the data from the central
			 * directory (pcdir).
			 */
			dst = map_new_file(0, pathname, new_part,
					   pcdir->uncomp_size,
					   pcdir->last_mod_file_date,
					   pcdir->last_mod_file_time, notify);
			if (dst) {
				if (!extract_file(dst, pcomp, pfhdr->method,
						  pcdir->comp_size,
						  pcdir->uncomp_size,
						  notify))
					return FALSE;
			} /* else ??? */
		}
		if (notify)
			notify(NUM_FILES, new_part, (int)pe->nTotalCDir,
			       (int)n+1);
	}
	return TRUE;
}
