// unzip.cpp : Defines the entry point for the application.
//

#include <windows.h>
#include "unzip.h"
#include "zconf.h"
#include "zlib.h"

/**
 * ZIPファイルエントリ
 */
struct zip_entry {
	WORD  compression_method;				// 圧縮方法（０は非圧縮）
	DWORD compressed_size;					// 圧縮後サイズ
	DWORD uncompressed_size;				// 非圧縮サイズ
	DWORD relative_offset_of_local_header;	// ローカルヘッダ開始アドレスのオフセット
};

/**
 * ZIPファイルハンドル
 */
struct ZIPFILE {
	HANDLE hFile;							// ファイルハンドル
	HANDLE hMapping;						// マッピングハンドル
	LPVOID data;							// マップされたアドレス	

	DWORD file_size;						// ZIPファイルサイズ 

	DWORD central_directory_size;			// central directoryのサイズ
	DWORD central_directory_offset;			// central directoryの開始オフセット

	zip_entry current_entry;				// カレントエントリ
};

/**
 * 32ビット値を読み込む
 */
#define READ_INT32(p) \
	(((__int32) (p)[0] & 0x000000ff) \
			| ((__int32) (p)[1] << 8 & 0x0000ff00) \
			| ((__int32) (p)[2] << 16 & 0x00ff0000) \
			| ((__int32) (p)[3] << 24 & 0xff000000))

/**
 * 16ビット値を読み込む
 */
#define READ_INT16(p) \
	((__int16) (p)[0] & 0x00ff) \
			| ((__int16) (p)[1] << 8 & 0xff00)

/**
 * 指定されたZIPファイルをオープンする
 */
ZIPFILE* unzip_open(LPTSTR filename) {
	ZIPFILE* zfile = NULL;

	// マッピング用にファイルをオープンする
	HANDLE hFile = CreateFileForMapping(filename,
		                                GENERIC_READ,
										FILE_SHARE_READ,
										NULL,
										OPEN_EXISTING,
										FILE_FLAG_RANDOM_ACCESS,
										NULL);
	if (hFile != NULL) {
		// ファイルサイズを取得する
		DWORD size = GetFileSize(hFile, NULL);
		// ファイルマッピングハンドルを作成する
		HANDLE hMapping = CreateFileMapping(hFile,
											NULL,
											PAGE_READONLY,
											0,				// 最大サイズ制限なし
											0,				// （ファイル全体）
											NULL);			// 名前なし
		if (hMapping != NULL) {
			// ファイルをメモリにマップする
			LPVOID buff = MapViewOfFile(hMapping,
										FILE_MAP_READ,
										0,					// ファイルの先頭からマッピング
										0,					// 
										0);					// ファイル全体をマッピング
			if (buff != NULL) {
				// ファイルの最後尾 - 20バイト目に移動し、"End of central directory record"
				// の内容を読み込む
				for (char* p = (char*) buff + (size - 1 - 20); p > buff; --p) {
					// 最後尾からシグニチャに一致する数値を探す
					if (p[0] == 0x50 && p[1] == 0x4b && p[2] == 0x05 && p[3] == 0x06) {
						// ZIPFILE構造体の内容を初期化する
						zfile = (ZIPFILE*) malloc(sizeof(ZIPFILE));
						zfile->hFile = hFile;
						zfile->hMapping = hMapping;
						zfile->data = buff;
						zfile->file_size = size;
						zfile->central_directory_size	= READ_INT32(p+12);
						zfile->central_directory_offset = READ_INT32(p+16);

						zfile->current_entry.relative_offset_of_local_header = (DWORD) -1;

						break;
					}
				}
				if (zfile == NULL) {
					// 正しいZIPファイルではなかったため、アンマップする
					UnmapViewOfFile(buff);
				}
			}
			if (zfile == NULL) {
				// 正しいZIPファイルではなかったため、マッピングハンドルをクローズする
				CloseHandle(hMapping);
			}
		}
		
		if (zfile == NULL) {
			// 正しいZIPファイルではなかったため、ファイルハンドルをクローズする
			CloseHandle(hFile);
		}
	}

	return zfile;
}

/**
 * 指定されたZIPファイルをクローズする
 */
BOOL unzip_close(ZIPFILE* zfile) {
	if (zfile == NULL) {
		return FALSE;
	}
	
	// マップを解除し、ハンドルをクローズする
	BOOL result1 = UnmapViewOfFile(zfile->data);
	BOOL result2 = CloseHandle(zfile->hMapping);
	BOOL result3 = CloseHandle(zfile->hFile);
	
	// メモリを解放する
	free(zfile);

	return result1 && result2 && result3;
}

/**
 * 指定されたファイル名の位置まで移動する
 */
BOOL unzip_seek(ZIPFILE* zfile, const char* filename) {
	BOOL result = FALSE;
	int len = strlen(filename);

	// central directoryのアドレスを計算する
	const char* p = (const char*) ((char*) zfile->data + zfile->central_directory_offset);

	// 検索終了アドレスを計算する
	const char* end = p + zfile->central_directory_size;

	while (p < end) {
		if (! (p[0] == 0x50 && p[1] == 0x4b && p[2] == 0x01 && p[3] == 0x02)) {
			// シグニチャがcentral directoryのものではない場合、処理を中止する
			break;
		}
		
		// ファイル名、エクストラフィールド、ファイルコメントの長さを得る
		WORD filename_length		= READ_INT16(p + 28);
		WORD extra_field_length		= READ_INT16(p + 30);
		WORD file_comment_length	= READ_INT16(p + 32);

		if (filename_length == len) {
			// ファイル名の比較を行う
			const char* name = (const char*) (p + 46);
			if (memcmp(name, filename, len) == 0) {
				// ファイル名が一致した場合、エントリを読み込む
				zip_entry* entry = &zfile->current_entry;
				entry->compression_method			   = READ_INT16(p + 10);
				entry->compressed_size				   = READ_INT32(p + 20);
				entry->uncompressed_size			   = READ_INT32(p + 24);
				entry->relative_offset_of_local_header = READ_INT32(p + 42);
				result = TRUE;
				break;
			}
		}
		
		// 次の位置に移動する
		p += 46							// central directoryの固定部分サイズ
			 + filename_length
			 + extra_field_length
			 + file_comment_length;
	}
	if (! result) {
		// ファイルが見つからなかった
		zfile->current_entry.relative_offset_of_local_header = (DWORD) -1;
	}

	return result;
}

/**
 * 現在のエントリに対応するファイルの展開後のサイズを返す。
 */
DWORD unzip_get_uncompressed_size(ZIPFILE* zfile) {
	DWORD size = (DWORD) -1;
	if (zfile->current_entry.relative_offset_of_local_header != (DWORD) -1) {
		size = zfile->current_entry.uncompressed_size;
	}
	return size;
}

/**
 * エントリを解凍し、指定されたバッファに入れる
 * （現在の実装では、バッファサイズ == 展開後のサイズになっていなければならない）
 */
BOOL unzip_read(ZIPFILE* zfile, char* buff, DWORD bufflen) {
	zip_entry* entry = &zfile->current_entry;
	if (entry->relative_offset_of_local_header == (DWORD) -1) {
		return FALSE;
	}
	
	// local header開始アドレスを得る
	const char* header = (const char*) zfile->data
										+ entry->relative_offset_of_local_header;
	if (! (header[0] == 0x50 && header[1] == 0x4b && header[2] == 0x03 && header[3] == 0x04)) {
		// シグニチャがlocal headerのものと一致しない場合
		return FALSE;
	}
	
	// データを読み込む
	BOOL result = FALSE;

	// local headerのサイズを計算するために
	// ファイル名とエクストラフィールドの長さを取得する
	WORD filename_length    = READ_INT16(header + 26);
	WORD extra_field_length = READ_INT16(header + 28);
	unsigned char* body = (unsigned char*) header
			                              + 30					// local headerの固定部分サイズ
										  + filename_length
										  + extra_field_length; 
	if (entry->compression_method == 0) {
		// 非圧縮の場合、コピーする
		memcpy(buff, body, bufflen);
		result = TRUE;
	} else {
		// 圧縮
		z_stream stream;
		stream.zalloc = Z_NULL;
		stream.zfree  = Z_NULL;
		stream.opaque = Z_NULL;
		if (inflateInit2(&stream, -MAX_WBITS) == Z_OK) {
			// local header直後に圧縮データがある
			stream.next_in = body;
			stream.avail_in = zfile->file_size - entry->relative_offset_of_local_header;
			stream.next_out = (unsigned char*) buff;
			stream.avail_out = bufflen;

			int err = inflate(&stream, Z_FINISH);
			if (err == Z_STREAM_END) {
				result = TRUE;
			}
			inflateEnd(&stream);
		}
	}
	
	return result;
}
