/*
 * Copyright (c) 2005-2006 Tom N Harris.
 *
 *     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.
 *
 *     A copy of the GNU General Public License can be found at
 *     http://www.gnu.org/
 *
 * Written by Tom N Harris <telliamed@whoopdedo.org>.
 *
 */

#include "ZipArchive.h"

#include <cstring>
#include <cstdlib>
#include <algorithm>

#include "zlib/ioapi.h"
#include "zlib/wioapi.h"

using namespace zLib;
using namespace std;

UnzipArchive::~UnzipArchive()
{
	Close();
}

UnzipArchive::UnzipArchive()
	: m_zf(NULL)
{
	memset(&m_info, 0, sizeof(m_info));
	memset(&m_currfile, 0, sizeof(m_currfile));
}

int UnzipArchive::Close(void)
{
	int err = UNZ_OK;

	switch (m_status)
	{
	  case unzip_archive_error:
	  case unzip_archive_closed:
		break;
	  case unzip_archive_fileread:
	  {
		int e = unzCloseCurrentFile(m_zf);
		if (e != UNZ_OK && errno == UNZ_OK)
			err = e;
	  }
	  case unzip_archive_fileopen:
	  case unzip_archive_open:
	  {
		int e = unzClose(m_zf);
		if (e != UNZ_OK && errno == UNZ_OK)
			err = e;
		m_zf = NULL;
		memset(&m_info, 0, sizeof(m_info));
		memset(&m_currfile, 0, sizeof(m_currfile));
		m_status = unzip_archive_closed;
		break;
	  }
	}
	return err;
}

int UnzipArchive::Open(const char* fname)
{
	int err;
	err = Close();
	if (err != UNZ_OK)
		return err;

	zlib_filefunc_def filefuncs;
	fill_fopen_filefunc(&filefuncs);
	m_zf = unzOpen2(fname, &filefuncs);
	if (m_zf == NULL)
	{
		return UNZ_ERRNO;
	}

	m_status = unzip_archive_open;
	err = unzGetGlobalInfo(m_zf, &m_info);
	if (err != UNZ_OK)
	{
		Close();
		m_status = unzip_archive_error;
		return err;
	}

	return UNZ_OK;
}

int UnzipArchive::Open(const wchar_t* fname)
{
	int err;
	err = Close();
	if (err != UNZ_OK)
		return err;

	zlib_filefunc_def filefuncs;
	fill_wfopen_filefunc(&filefuncs);
	m_zf = unzOpen2(reinterpret_cast<const char*>(fname), &filefuncs);
	if (m_zf == NULL)
	{
		return UNZ_ERRNO;
	}

	m_status = unzip_archive_open;
	err = unzGetGlobalInfo(m_zf, &m_info);
	if (err != UNZ_OK)
	{
		Close();
		m_status = unzip_archive_error;
		return err;
	}

	return UNZ_OK;
}

unsigned long UnzipArchive::GetCommentLength(void)
{
	return m_info.size_comment;
}

int UnzipArchive::GetComment(char* comment, unsigned long bufsize)
{
	if (m_status == unzip_archive_error 
	 || m_status == unzip_archive_closed)
		return 0;

	return unzGetGlobalComment(m_zf, comment, bufsize);
}

int UnzipArchive::FirstFile(void)
{
	int err = UNZ_OK;
	switch (m_status)
	{
	case unzip_archive_error:
	case unzip_archive_closed:
		return UNZ_ERRNO;
	case unzip_archive_fileread:
		err = CloseFile();
		if (err != UNZ_OK)
		{
			return err;
		}
	case unzip_archive_fileopen:
	case unzip_archive_open:
		break;
	}

	err = unzGoToFirstFile(m_zf);
	if (err != UNZ_OK)
	{
		Close();
		m_status = unzip_archive_error;
		return err;
	}

	return loadfileinfo();
}

int UnzipArchive::NextFile(void)
{
	int err = UNZ_OK;
	switch (m_status)
	{
	case unzip_archive_error:
	case unzip_archive_closed:
		return UNZ_ERRNO;
	case unzip_archive_fileread:
		err = CloseFile();
		if (err != UNZ_OK)
		{
			return err;
		}
	case unzip_archive_fileopen:
	case unzip_archive_open:
		break;
	}

	err = unzGoToNextFile(m_zf);
	if (err != UNZ_OK || err != UNZ_END_OF_LIST_OF_FILE)
	{
		Close();
		m_status = unzip_archive_error;
		return err;
	}
	if (err == UNZ_OK)
	{
		err = loadfileinfo();
	}

	return err;
}

int UnzipArchive::LocateFile(const char* fname, int casecmp)
{
	int err = UNZ_OK;
	switch (m_status)
	{
	case unzip_archive_error:
	case unzip_archive_closed:
		return UNZ_ERRNO;
	case unzip_archive_fileread:
		err = CloseFile();
		if (err != UNZ_OK)
		{
			return err;
		}
	case unzip_archive_fileopen:
	case unzip_archive_open:
		break;
	}

	err = unzLocateFile(m_zf, fname, casecmp);
	if (err != UNZ_OK || err != UNZ_END_OF_LIST_OF_FILE)
	{
		Close();
		m_status = unzip_archive_error;
		return err;
	}
	if (err == UNZ_OK)
	{
		err = loadfileinfo();
	}

	return err;
}

unsigned long UnzipArchive::GetFileNameLength(void)
{
	return m_currfile.size_filename;
}

int UnzipArchive::GetFileName(char* fname, unsigned long bufsize)
{
	switch (m_status)
	{
	case unzip_archive_error:
	case unzip_archive_closed:
	case unzip_archive_open:
		return 0;
	case unzip_archive_fileopen:
	case unzip_archive_fileread:
		break;
	}

	return unzGetCurrentFileInfo(m_zf, NULL, fname, bufsize, NULL, 0, NULL, 0);
}

unsigned long UnzipArchive::GetFileTime(void)
{
	return m_currfile.dosDate;
}

unsigned long UnzipArchive::GetFileAttributes(void)
{
	return m_currfile.external_fa;
}

unsigned long UnzipArchive::GetCompressedSize(void)
{
	return m_currfile.compressed_size;
}

unsigned long UnzipArchive::GetUncompressedSize(void)
{
	return m_currfile.uncompressed_size;
}

unsigned long UnzipArchive::GetFileCommentLength(void)
{
	return m_currfile.size_file_comment;
}

int UnzipArchive::GetFileComment(char* comment, unsigned long bufsize)
{
	switch (m_status)
	{
	case unzip_archive_error:
	case unzip_archive_closed:
	case unzip_archive_open:
		return 0;
	case unzip_archive_fileopen:
	case unzip_archive_fileread:
		break;
	}

	return unzGetCurrentFileInfo(m_zf, NULL, NULL, 0, NULL, 0, comment, bufsize);
}

int UnzipArchive::GetExtraField(unsigned short tag, char* ldata, unsigned int lsize, char* gdata, unsigned int gsize)
{
	int err = UNZ_OK;
	switch (m_status)
	{
	case unzip_archive_error:
	case unzip_archive_closed:
	case unzip_archive_open:
		return UNZ_ERRNO;
	case unzip_archive_fileopen:
		err = OpenFile();
		if (err != UNZ_OK)
		{
			return err;
		}
	case unzip_archive_fileread:
		break;
	}
	
	int locsz, cidsz;
	cidsz = m_currfile.size_file_extra;
	locsz = unzGetLocalExtrafield(m_zf, NULL, 0);
	if (locsz < 0)
	{
		return locsz;
	}

	char* locbuf = NULL;
	char* cidbuf = NULL;
	if (ldata && locsz > 0)
	{
		locbuf = reinterpret_cast<char*>(malloc(locsz+sizeof(zip_extrafield_tag)));
		if (!locbuf)
		{
			return Z_MEM_ERROR;
		}
		memset(locbuf, 0, locsz+sizeof(zip_extrafield_tag));
		locsz = unzGetLocalExtrafield(m_zf, locbuf, locsz);
		if (locsz < 0)
		{
			free(locbuf);
			return locsz;
		}
	}
	if (gdata && cidsz > 0)
	{
		cidbuf = reinterpret_cast<char*>(malloc(cidsz+sizeof(zip_extrafield_tag)));
		if (!cidbuf)
		{
			return Z_MEM_ERROR;
		}
		memset(cidbuf, 0, cidsz+sizeof(zip_extrafield_tag));
		cidsz = unzGetCurrentFileInfo(m_zf, NULL, NULL, 0, cidbuf, cidsz, NULL, 0);
		if (cidsz < 0)
		{
			free(cidbuf);
			if (locbuf)
				free(locbuf);
			return cidsz;
		}
	}

	zip_extrafield_tag *exfield;
	char* p;
	err = UNZ_OK;

	if (gdata != NULL)
	{
		exfield = reinterpret_cast<zip_extrafield_tag*>(cidbuf);
		p = cidbuf + sizeof(zip_extrafield_tag);
		while (exfield->tag != 0)
		{
			if (exfield->tag == tag)
			{
				memcpy(gdata, p, min(gsize, unsigned(exfield->blksz)));
				break;
			}
			p += exfield->blksz;
			exfield = reinterpret_cast<zip_extrafield_tag*>(p);
			p += sizeof(zip_extrafield_tag);
		}
	}

	if (ldata != NULL)
	{
		exfield = reinterpret_cast<zip_extrafield_tag*>(locbuf);
		p = locbuf + sizeof(zip_extrafield_tag);
		while (exfield->tag != 0)
		{
			if (exfield->tag == tag)
			{
				memcpy(ldata, p, min(lsize, unsigned(exfield->blksz)));
				break;
			}
			p += exfield->blksz;
			exfield = reinterpret_cast<zip_extrafield_tag*>(p);
			p += sizeof(zip_extrafield_tag);
		}
	}

	if (cidbuf)
		free(cidbuf);
	if (locbuf)
		free(locbuf);
	return err;
}

int UnzipArchive::GetExtraFieldLength(unsigned short tag, unsigned int *lsize, unsigned int *gsize)
{
	int err = UNZ_OK;
	switch (m_status)
	{
	case unzip_archive_error:
	case unzip_archive_closed:
	case unzip_archive_open:
		return UNZ_ERRNO;
	case unzip_archive_fileopen:
		err = OpenFile();
		if (err != UNZ_OK)
		{
			return err;
		}
	case unzip_archive_fileread:
		break;
	}
	
	int locsz, cidsz;
	cidsz = m_currfile.size_file_extra;
	locsz = unzGetLocalExtrafield(m_zf, NULL, 0);
	if (locsz < 0)
	{
		return locsz;
	}

	char* locbuf = NULL;
	char* cidbuf = NULL;
	if (lsize && locsz > 0)
	{
		locbuf = reinterpret_cast<char*>(malloc(locsz+sizeof(zip_extrafield_tag)));
		if (!locbuf)
		{
			return Z_MEM_ERROR;
		}
		memset(locbuf, 0, locsz+sizeof(zip_extrafield_tag));
		locsz = unzGetLocalExtrafield(m_zf, locbuf, locsz);
		if (locsz < 0)
		{
			free(locbuf);
			return locsz;
		}
	}
	if (gsize && cidsz > 0)
	{
		cidbuf = reinterpret_cast<char*>(malloc(cidsz+sizeof(zip_extrafield_tag)));
		if (!cidbuf)
		{
			return Z_MEM_ERROR;
		}
		memset(cidbuf, 0, cidsz+sizeof(zip_extrafield_tag));
		cidsz = unzGetCurrentFileInfo(m_zf, NULL, NULL, 0, cidbuf, cidsz, NULL, 0);
		if (cidsz < 0)
		{
			free(cidbuf);
			if (locbuf)
				free(locbuf);
			return cidsz;
		}
	}

	zip_extrafield_tag *exfield;
	char* p;
	err = UNZ_OK;

	if (gsize != NULL)
	{
		exfield = reinterpret_cast<zip_extrafield_tag*>(cidbuf);
		p = cidbuf + sizeof(zip_extrafield_tag);
		while (exfield->tag != 0)
		{
			if (exfield->tag == tag)
			{
				*gsize = exfield->blksz;
				break;
			}
			p += exfield->blksz;
			exfield = reinterpret_cast<zip_extrafield_tag*>(p);
			p += sizeof(zip_extrafield_tag);
		}
	}

	if (lsize != NULL)
	{
		exfield = reinterpret_cast<zip_extrafield_tag*>(locbuf);
		p = locbuf + sizeof(zip_extrafield_tag);
		while (exfield->tag != 0)
		{
			if (exfield->tag == tag)
			{
				*lsize = exfield->blksz;
				break;
			}
			p += exfield->blksz;
			exfield = reinterpret_cast<zip_extrafield_tag*>(p);
			p += sizeof(zip_extrafield_tag);
		}
	}

	if (cidbuf)
		free(cidbuf);
	if (locbuf)
		free(locbuf);
	return err;
}

int UnzipArchive::OpenFile(void)
{
	int err = UNZ_OK;
	switch (m_status)
	{
	case unzip_archive_error:
	case unzip_archive_closed:
		return UNZ_ERRNO;
	case unzip_archive_fileread:
		err = CloseFile();
		if (err != UNZ_OK)
		{
			return err;
		}
	case unzip_archive_fileopen:
	case unzip_archive_open:
		break;
	}

	err = unzOpenCurrentFile(m_zf);
	if (err != UNZ_OK)
	{
		Close();
		m_status = unzip_archive_error;
	}

	m_status = unzip_archive_fileread;
	return UNZ_OK;
}

int UnzipArchive::CloseFile(void)
{
	switch (m_status)
	{
	case unzip_archive_error:
	case unzip_archive_closed:
		return UNZ_ERRNO;
	case unzip_archive_open:
	case unzip_archive_fileopen:
		return UNZ_OK;
	case unzip_archive_fileread:
		break;
	}

	int err = unzCloseCurrentFile(m_zf);
	if (err != UNZ_OK && err != UNZ_CRCERROR)
	{
		Close();
		m_status = unzip_archive_error;
	}

	return err;
}

int UnzipArchive::Read(char* data, unsigned long nbytes)
{
	int err = UNZ_OK;
	switch (m_status)
	{
	case unzip_archive_error:
	case unzip_archive_closed:
		return UNZ_ERRNO;
	case unzip_archive_open:
		err = FirstFile();
		if (err != UNZ_OK)
		{
			return err;
		}
	case unzip_archive_fileopen:
		err = OpenFile();
		if (err != UNZ_OK)
		{
			return err;
		}
	case unzip_archive_fileread:
		break;
	}

	err = unzReadCurrentFile(m_zf, data, nbytes);
	return err;
}

int UnzipArchive::loadfileinfo(void)
{
	if (m_status == unzip_archive_error 
	 || m_status == unzip_archive_closed)
		return UNZ_INTERNALERROR;

	int err = unzGetCurrentFileInfo(m_zf, &m_currfile, NULL, 0, NULL, 0, NULL, 0);
	if (err != UNZ_OK)
	{
		Close();
		m_status = unzip_archive_error;
	}

	m_status = unzip_archive_fileopen;
	return UNZ_OK;
}

