/*
 *	dfpck - FilePack ユーティリティー
 *	Copyright (C) 2002-2008 Naoyuki Sawa
 *
 *	* Mon May 13 11:51:00 JST 2002 Naoyuki Sawa
 *	- 作成開始。
 *	* Wed Aug 06 20:09:34 JST 2008 Naoyuki Sawa
 *	- 15文字以上の長いファイル名も格納できる仕様を追加しました。
 *	- ファイル構造は変更していません。
 *	  長いファイル名を格納した新形式fpkも、旧形式fpkだけを認識するツールで扱うことができます。
 *	- 新形式fpkでは、size=0のエントリを、長いファイル名を格納するための分割エントリと見なします。
 *	  たとえば、「123456789abcdefghijklmnopqrstuvw.xyz」というファイルは、以下のように格納されます。
 *		FILE_PAC_INFO	head		'FPAK'
 *				famount		3
 *		FILE_INFO	fname		"123456789abcdef\0"
 *				offset		(オフセット)
 *				size		0
 *		FILE_INFO	fname		"ghijklmnopqrstu\0"
 *				offset		(オフセット)
 *				size		0
 *		FILE_INFO	fname		"vw.xyz\0"
 *				offset		(オフセット)
 *				size		(ファイルサイズ)
 *	- size=0のエントリを、長いファイル名を格納するための分割エントリと見なすようになったので、
 *	  本当にサイズが0のファイルは格納できない仕様となりました。
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>

/* [NOTE]
 * * コマンドライン引数のワイルドカードを展開するために、
 *   [プロジェクトの設定]->[リンク]->[オブジェクト/ライブラリモジュール]に
 *   「setargv.obj」を追加しています。
 */

#define VERSION		"20080806"
#define LIST_FMT	"%8d %s\n"

char arc_path[_MAX_PATH];
char arc_drive[_MAX_DRIVE];
char arc_dir[_MAX_DIR];
char arc_fname[_MAX_FNAME];
char arc_ext[_MAX_EXT];

/* fpkファイル形式に関する構造体 */
#define SIGNATURE	'FPAK'
#define FNAME_LEN	16
typedef struct _FILE_PAC_INFO {	/* パック情報 */
	unsigned long head;	/* シグネチャ */
	long famount;		/* ファイル数 */
} FILE_PAC_INFO;
typedef struct _FILE_INFO {	/* ファイル情報                         */
	char fname[FNAME_LEN];	/* ファイル名                           */
	unsigned long offset;	/* オフセット(fpkファイル先頭を0とする) */
	unsigned long size;	/* ファイルサイズ                       */
} FILE_INFO;

/* 内部データ管理用の構造体 */
typedef struct _FILE_ITEM {
//{{2008/08/06:長いファイル名対応:変更
//	char fname[FNAME_LEN];	/* ファイル名   */
//↓2008/08/06:長いファイル名対応:変更
	char fname[_MAX_FNAME];	/* ファイル名   */
//}}2008/08/06:長いファイル名対応:変更
	int size;		/* データサイズ */
	char* data;		/* データ       */
} FILE_ITEM;
int n_file_items;		/* アイテム数     */
FILE_ITEM* file_items;		/* アイテムリスト */

void cmd_list(int n_fnames, char* fnames[]);
void cmd_extract(int n_fnames, char* fnames[]);
void cmd_append(int n_fnames, char* fnames[]);
void cmd_create(int n_fnames, char* fnames[]);

FILE_ITEM* find(const char* fname);
void load();
void save();

void
err(const char* fmt, ...)
{
	va_list ap;
	va_start(ap, fmt);
	 fprintf(stderr, "### ");
	vfprintf(stderr, fmt, ap);
	 fprintf(stderr, "\n");
	va_end(ap);
	abort();
}

void
usage(int err)
{
	fprintf(stderr, "dfpck - FilePack ユーティリティー (%s)\n", VERSION);
	fprintf(stderr, "Copyright (C) 2002-2008 Naoyuki Sawa\n");
	fprintf(stderr, "\n");
	fprintf(stderr, "使い方: dfpck <command> <archive[.fpk]> [filename...]\n");
	fprintf(stderr, "\n");
	fprintf(stderr, "COMMAND:\n");
	fprintf(stderr, "	c	新しい書庫ファイルを作成します。\n");
	fprintf(stderr, "	r	既存の書庫にファイルを追加します。\n");
	fprintf(stderr, "	t	既存の書庫のファイル一覧を表示します。\n");
	fprintf(stderr, "	x	既存の書庫からファイルを抽出します。\n");

	if(err) {
		abort();
	} else {
		exit(0);
	}
}

int
main(int argc, char* argv[])
{
	/* 引数無しで起動されたら、使い方を表示します。 */
	if(argc == 1) usage(0);

	/* 少なくとも、コマンドと書庫ファイル名が必要です。 */
	if(argc < 1 + 2) usage(1);

	/* 書庫ファイル名を取得します。 */
	strcpy(arc_path, argv[2]);
	_splitpath(arc_path, arc_drive, arc_dir, arc_fname, arc_ext);
	if(strlen(arc_ext) == 0) {
		strcpy(arc_ext, ".fpk");
		_makepath(arc_path, arc_drive, arc_dir, arc_fname, arc_ext);
	}

	/* コマンドによって... */
	if(strcmp(argv[1], "c") == 0) {
		cmd_create(argc - 3, &argv[3]);
	} else if(strcmp(argv[1], "r") == 0) {
		cmd_append(argc - 3, &argv[3]);
	} else if(strcmp(argv[1], "t") == 0) {
		cmd_list(argc - 3, &argv[3]);
	} else if(strcmp(argv[1], "x") == 0) {
		cmd_extract(argc - 3, &argv[3]);
	} else {
		usage(1);
	}

	return 0;
}

void
cmd_list(int n_fnames, char* fnames[])
{
	int i;
	FILE_ITEM* item_pos;

	/* 書庫ファイルを読み込みます。 */
	load();

	if(n_fnames == 0) {
		/* 全ファイルリストを表示します。 */
		for(i = 0, item_pos = file_items;
		    i < n_file_items;
		    i++, item_pos++) {
			/* ファイル情報を表示します。 */
			printf(LIST_FMT, item_pos->size, item_pos->fname);
		}
	} else {
		/* 指定されたファイルのリストを表示します。 */
		for(i = 0; i < n_fnames; i++) {
			/* ファイルを探します。 */
			item_pos = find(fnames[i]);
			if(item_pos == NULL) err("%s ファイルが見つかりません。", fnames[i]);
			/* ファイル情報を表示します。 */
			printf(LIST_FMT, item_pos->size, item_pos->fname);
		}
	}
}

void
cmd_extract(int n_fnames, char* fnames[])
{
	int retval, i;
	FILE_ITEM* item_pos;
	FILE* fp;

	/* 書庫ファイルを読み込みます。 */
	load();

	if(n_fnames == 0) {
		/* 全ファイルを抽出します。 */
		for(i = 0, item_pos = file_items;
		    i < n_file_items;
		    i++, item_pos++) {
			/* ファイルを書き込みます。 */
			fp = fopen(item_pos->fname, "wb");
			if(fp == NULL) err("%s ファイルが作成できません。", item_pos->fname);
			retval = fwrite(item_pos->data, 1, item_pos->size, fp);
			if(retval != item_pos->size) err("%s ファイルが書き込めません。", item_pos->fname);
			fclose(fp);
		}
	} else {
		/* 指定されたファイルを抽出します。 */
		for(i = 0; i < n_fnames; i++) {
			/* ファイルを探します。 */
			item_pos = find(fnames[i]);
			if(item_pos == NULL) err("%s ファイルが見つかりません。", fnames[i]);
			/* ファイルを書き込みます。 */
			fp = fopen(item_pos->fname, "wb");
			if(fp == NULL) err("%s ファイルが作成できません。", fnames[i]);
			retval = fwrite(item_pos->data, 1, item_pos->size, fp);
			if(retval != item_pos->size) err("%s ファイルが書き込めません。", fnames[i]);
			fclose(fp);
		}
	}

}

void
cmd_append(int n_fnames, char* fnames[])
{
	/* 書庫ファイルを読み込みます。 */
	load();

	/* 後は、新規作成と同じ処理です。 */
	cmd_create(n_fnames, fnames);
}

void
cmd_create(int n_fnames, char* fnames[])
{
	int retval, i;
	FILE_ITEM* item_pos;
	char path[_MAX_PATH];
	char fname[_MAX_FNAME];
	char ext[_MAX_EXT];
	FILE* fp;

	/* ファイル管理テーブルを拡張します。 */
	file_items = (FILE_ITEM*)realloc(file_items, sizeof(FILE_ITEM) * (n_file_items + n_fnames));
	if(file_items == NULL) err("ファイル管理テーブルのメモリ不足です。");
	/* まだn_file_itemsは更新しない！ */

	/* 指定されたファイルを読み込みます。 */
	for(i = 0, item_pos = file_items + n_file_items;
	    i < n_fnames;
	    i++, item_pos++) {
		/* ファイル名を格納します。 */
		_splitpath(fnames[i], NULL, NULL, fname, ext);
		_makepath(path, NULL, NULL, fname, ext);
		strlwr(path);
//{{2008/08/06:長いファイル名対応:削除
//		if(strlen(path) > FNAME_LEN - 1) err("%s ファイル名が長すぎます。", fnames[i]);
//{{2008/08/06:長いファイル名対応:削除
		if(find(path) != NULL) err("%s ファイル名が重複しています。", fnames[i]);
		strcpy(item_pos->fname, path);
		/* ファイルを読み込みます。 */
		fp = fopen(fnames[i], "rb");
		if(fp == NULL) err("%s ファイルが開けません。", fnames[i]);
		retval = fseek(fp, 0, SEEK_END);
		if(retval != 0) err("%s ファイルのシークエラーです。", fnames[i]);
		item_pos->size = ftell(fp);
//{{2008/08/06:長いファイル名対応:追加
		if(!item_pos->size) err("%s サイズが0のファイルは格納できません。", fnames[i]);
//}}2008/08/06:長いファイル名対応:追加
		rewind(fp);
		item_pos->data = (char*)malloc(item_pos->size);
		if(item_pos->data == NULL) err("%s ファイルを読み込むためのメモリ不足です。", fnames[i]);
		retval = fread(item_pos->data, 1, item_pos->size, fp);
		if(retval != item_pos->size) err("%s ファイルが読み込めません。", fnames[i]);
		fclose(fp);
	}

	/* ここでn_file_itemsを更新！ */
	n_file_items += n_fnames;

	/* 書庫ファイルを書き込みます。 */
	save();
}

FILE_ITEM*
find(const char* fname)
{
	int i;
	FILE_ITEM* item_pos;

	for(i = 0, item_pos = file_items;
	    i < n_file_items;
	    i++, item_pos++) {
		if(strcmpi(item_pos->fname, fname) == 0) return item_pos;
	}

	return NULL;
}

void
load()
{
	int retval, i;
	FILE* fp;
	FILE_PAC_INFO pac;
	FILE_INFO *info, *info_pos;
	FILE_ITEM *item_pos;
//{{2008/08/06:長いファイル名対応:追加
	int n_info;
//}}2008/08/06:長いファイル名対応:追加

	/* 書庫ファイルを開きます。 */
	fp = fopen(arc_path, "rb");
	if(fp == NULL) err("%s 書庫ファイルが開けません。", arc_path);

//{{2008/08/06:長いファイル名対応:変更
//	/* ヘッダを読み込みます。 */
//	retval = fread(&pac, 1, sizeof pac, fp);
//	if(retval != sizeof pac) err("ヘッダが読み込めません。");
//	if(pac.head != SIGNATURE) err("シグネチャが不正です。");
//	if(pac.famount < 0) err("ファイル数が不正です。");
//	n_file_items = pac.famount;
//
//	/* ファイル情報を読み込みます。 */
//	info = (FILE_INFO*)malloc(sizeof(FILE_INFO) * n_file_items);
//	if(info == NULL) err("ファイル情報を読み込むためのメモリ不足です。");
//	retval = fread(info, 1, sizeof(FILE_INFO) * n_file_items, fp);
//	if(retval != (int)(sizeof(FILE_INFO) * n_file_items)) err("ファイル情報が読み込めません。");
//
//	/* ファイルを読み込みます。 */
//	file_items = (FILE_ITEM*)malloc(sizeof(FILE_ITEM) * n_file_items);
//	if(file_items == NULL) err("ファイル管理テーブルのメモリ不足です。");
//	for(i = 0, info_pos = info, item_pos = file_items;
//	    i < n_file_items;
//	    i++, info_pos++, item_pos++) {
//		if(memchr(info_pos->fname, '\0', FNAME_LEN) == NULL) err("ファイル名が壊れています。");
//		strcpy(item_pos->fname, info_pos->fname);
//		retval = fseek(fp, info_pos->offset, SEEK_SET);
//		if(retval != 0) err("%s ファイル位置が壊れています。", info_pos->fname);
//		item_pos->size =               info_pos->size;
//		item_pos->data = (char*)malloc(info_pos->size);
//		if(item_pos->data == NULL) err("%s ファイルを読み込むためのメモリ不足です。", info_pos->fname);
//		retval = fread(item_pos->data, 1, item_pos->size, fp);
//		if(retval != item_pos->size) err("%s ファイルサイズが壊れています。", info_pos->fname);
//	}
//↓2008/08/06:長いファイル名対応:変更
	/* ヘッダを読み込みます。 */
	retval = fread(&pac, 1, sizeof pac, fp);
	if(retval != sizeof pac) err("ヘッダが読み込めません。");
	if(pac.head != SIGNATURE) err("シグネチャが不正です。");
	if(pac.famount < 0) err("ファイル数が不正です。");
	n_info = pac.famount; /* 長いファイル名の分割エントリも含めた延べ数です (旧形式互換のため) */

	/* ファイル情報を読み込みます。 */
	info = (FILE_INFO*)malloc(sizeof(FILE_INFO) * n_info);
	if(info == NULL) err("ファイル情報を読み込むためのメモリ不足です。");
	retval = fread(info, 1, sizeof(FILE_INFO) * n_info, fp);
	if(retval != (int)(sizeof(FILE_INFO) * n_info)) err("ファイル情報が読み込めません。");

	/* ファイルを読み込みます。 */
	file_items = NULL;
	n_file_items = 0;
	for(i = 0; i < n_info; i++) {
		file_items = (FILE_ITEM*)realloc(file_items, sizeof(FILE_ITEM) * (n_file_items + 1));
		if(!file_items) err("ファイル管理テーブルのメモリ不足です。");
		item_pos = &file_items[n_file_items++];
		memset(item_pos, 0, sizeof(FILE_ITEM)); /* 必要 */
		for(;;) {
			info_pos = &info[i];
			if(!memchr(info_pos->fname, '\0', FNAME_LEN)) err("ファイル名が壊れています。");
			strcat(item_pos->fname, info_pos->fname);
			if(!info_pos->size) { /* 長いファイル名の分割エントリ (size=0) */
				i++;
				if(i >= n_info) err("長いファイル名の分割エントリが不正です。");
			} else { /* 実エントリ (size>0) */
				retval = fseek(fp, info_pos->offset, SEEK_SET);
				if(retval != 0) err("%s ファイル位置が壊れています。", item_pos->fname);
				item_pos->size =               info_pos->size;
				item_pos->data = (char*)malloc(info_pos->size);
				if(item_pos->data == NULL) err("%s ファイルを読み込むためのメモリ不足です。", item_pos->fname);
				retval = fread(item_pos->data, 1, item_pos->size, fp);
				if(retval != item_pos->size) err("%s ファイルサイズが壊れています。", item_pos->fname);
				break;
			}
		}
	}
//}}2008/08/06:長いファイル名対応:変更

	/* ファイル情報を開放します。 */
	free(info);

	/* 書庫ファイルを閉じます。 */
	fclose(fp);
}

void
save()
{
	int retval, i, offset;
	FILE* fp;
	FILE_PAC_INFO pac;
	FILE_INFO *info, *info_pos;
	FILE_ITEM *item_pos;
//{{2008/08/06:長いファイル名対応:追加
	int n_info, len;
	const char* p;
//}}2008/08/06:長いファイル名対応:追加

	/* ファイル名の順に並び替えます。
	 * * FILE_ITEMの先頭に直接ファイル名が格納されているので、
	 *   比較関数にはstrcmp()がそのまま使えます。
	 */
	qsort(file_items, n_file_items, sizeof(FILE_ITEM), strcmp);

	/* 書庫ファイルを作成します。 */
	fp = fopen(arc_path, "wb");
	if(fp == NULL) err("%s 書庫ファイルが作成できません。", arc_path);

//{{2008/08/06:長いファイル名対応:変更
//	/* ヘッダを書き込みます。 */
//	pac.head = SIGNATURE;
//	pac.famount = n_file_items;
//	retval = fwrite(&pac, 1, sizeof pac, fp);
//	if(retval != sizeof pac) err("ヘッダが書き込めません。");
//
//	/* ファイル情報を書き込みます。 */
//	info = (FILE_INFO*)malloc(sizeof(FILE_INFO) * n_file_items);
//	if(info == NULL) err("ファイル情報を書き込むためのメモリ不足です。");
//	offset = sizeof(FILE_PAC_INFO) + sizeof(FILE_INFO) * n_file_items;
//	for(i = 0, info_pos = info, item_pos = file_items;
//	    i < n_file_items;
//	    i++, info_pos++, item_pos++) {
//		strncpy(info_pos->fname, item_pos->fname, FNAME_LEN); /* 余分を'\0'で埋めるため */
//		offset = offset + 3 & ~3; /* データ開始位置を4バイト境界へ整列 */
//		info_pos->offset = offset;
//		info_pos->size = item_pos->size;
//		offset += item_pos->size;
//	}
//	retval = fwrite(info, 1, sizeof(FILE_INFO) * n_file_items, fp);
//	if(retval != (int)(sizeof(FILE_INFO) * n_file_items)) err("ファイル情報が書き込めません。");
//
//	/* ファイルを書き込みます。 */
//	for(i = 0, info_pos = info, item_pos = file_items;
//	    i < n_file_items;
//	    i++, info_pos++, item_pos++) {
//		retval = fseek(fp, info_pos->offset, SEEK_SET);
//		if(retval != 0) err("%s ファイルのシークエラーです。", item_pos->fname);
//		retval = fwrite(item_pos->data, 1, item_pos->size, fp);
//		if(retval != item_pos->size) err("%s ファイルが書き込めません。", item_pos->fname);
//	}
//↓2008/08/06:長いファイル名対応:変更
	/* ファイル情報を作成します。 */
	info = NULL;
	offset = 0;
	n_info = 0;
	for(i = 0; i < n_file_items; i++) {
		item_pos = &file_items[i];
		p = item_pos->fname;
		for(;;) {
			info = (FILE_INFO*)realloc(info, sizeof(FILE_INFO) * (n_info + 1));
			if(!info) err("ファイル情報を書き込むためのメモリ不足です。");
			info_pos = &info[n_info++];
			memset(info_pos, 0, sizeof(FILE_INFO)); /* 必要 */
			len = strlen(p);
			if(len > (FNAME_LEN - 1)) { /* 長いファイル名の分割エントリ (size=0) */
				memcpy(info_pos->fname, p, (FNAME_LEN - 1));
				p += (FNAME_LEN - 1);
			} else { /* 実エントリ (size>0) */
				memcpy(info_pos->fname, p, len);
				info_pos->size = item_pos->size;
				break;
			}
		}
	}
	offset = sizeof(FILE_PAC_INFO) + sizeof(FILE_INFO) * n_info;
	for(i = 0; i < n_info; i++) {
		info_pos = &info[i];
		info_pos->offset = offset;
		offset += (info_pos->size + 3) & ~3;
	}

	/* ヘッダを書き込みます。 */
	pac.head = SIGNATURE;
	pac.famount = n_info; /* 長いファイル名の分割エントリも含めた延べ数です (旧形式互換のため) */
	retval = fwrite(&pac, 1, sizeof pac, fp);
	if(retval != sizeof pac) err("ヘッダが書き込めません。");

	/* ファイル情報を書き込みます。 */
	retval = fwrite(info, 1, sizeof(FILE_INFO) * n_info, fp);
	if(retval != (int)(sizeof(FILE_INFO) * n_info)) err("ファイル情報が書き込めません。");

	/* ファイルを書き込みます。 */
	item_pos = file_items;
	for(i = 0; i < n_info; i++) {
		info_pos = &info[i];
		if(info_pos->size) {
			retval = fseek(fp, info_pos->offset, SEEK_SET);
			if(retval != 0) err("%s ファイルのシークエラーです。", item_pos->fname);
			retval = fwrite(item_pos->data, 1, item_pos->size, fp);
			if(retval != item_pos->size) err("%s ファイルが書き込めません。", item_pos->fname);
			item_pos++;
		}
	}
//}}2008/08/06:長いファイル名対応:変更

	/* ファイル情報を開放します。 */
	free(info);

	/* 書庫ファイルを閉じます。 */
	fclose(fp);
}
