// ini.c
// Copyright : 2022-02-16 Yutaka Sawada
// License : The MIT license

#ifndef _UNICODE
#define _UNICODE
#endif
#ifndef UNICODE
#define UNICODE
#endif
#ifndef _WIN32_WINNT
#define _WIN32_WINNT 0x0600	// Windows Vista or later
#endif

#include <malloc.h>
#include <stdio.h>

#include <windows.h>

#include "common.h"
#include "crc.h"
#include "ini.h"

/*
ファイル名は 0_#.bin (2+32+4 = 38文字)
「#」部分はチェックサム・ファイルを UTF-16 にエンコードした内容の
MD5ハッシュ値を 16進数表記にする。

ファイル・フォーマット
<!-- 検査結果ファイル識別用 -->
2: 検査結果の書式バージョン
4: 6バイト目以降からの CRC-32 チェックサム
<!-- チェックサム・セットの識別用 -->
4: チェックサム・ファイルの UTF-16 エンコード後の文字数
4: チェックサム・ファイルに記録されてるソース・ファイルの数

<!-- 検査状態をファイルごとに記録する -->
前半 12バイトでファイル項目を識別して、後半 18バイトに状態を保存する。
4: ボリュームのシリアル番号
8: ファイルのオブジェクトID
8: ソース・ファイルのサイズ
4: ソース・ファイルの作成日時
4: ソース・ファイルの更新日時
2: 下位 1-bit はファイルの状態 0=完全, 1=破損あるいはエラー
   上位 15-bit はソース・ファイル番号 0～32767

*/

#define INI_VERSION	0x1270	// 検査結果の書式が決まった時のバージョン
#define REUSE_MIN	16384	// ファイル・サイズがこれより大きければ検査結果を利用する (16KB 以上 2GB 未満)
#define HEADER_SIZE	14
#define STATE_SIZE	30
#define STATE_READ	136
//#define VERBOSE		1		// 状態を冗長に出力する

static HANDLE hIniBin = NULL;	// バイナリ・データ用の検査結果ファイル
static int ini_off;

// 検査結果をどのくらいの期間保存するか
int recent_data = 0;
/*
  0 = 検査結果の再利用機能を無効にする(読み込まないし、記録もしない)
1～7= 前回の検査結果を読み込んで、今回のを記録する。
      指定された期間よりも経過した古い記録は削除される。
      1= 1日, 2= 3日, 3= 1週間, 4= 半月, 5= 1ヶ月, 6= 1年, 7= 無制限
 +8 = 同じセットの記録を削除する、今回の結果は記録する。
      他のセットは指定された期間よりも古いものだけ削除する。
*/

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

// File Time (UTC) を UNIX Time (UTC) に変換する
unsigned int time_f_u(FILETIME *file_time)
{
	unsigned __int64 int8;

	// 1970/01/01 の File Time との差を求める
	memcpy(&int8, file_time, 8);
	int8 = (int8 - 116444736000000000) / 10000000;

	return (unsigned int)int8;
}

#ifdef VERBOSE
// File Time (UTC) を日付の文字列に変換する
static void time_f_date(FILETIME *ft, char *buf)
{
	int len;
	SYSTEMTIME st_utc, st;

	FileTimeToSystemTime(ft, &st_utc);
	if (SystemTimeToTzSpecificLocalTime(NULL, &st_utc, &st) == 0)
		memcpy(&st, &st_utc, sizeof(SYSTEMTIME));
	len = GetDateFormatA(LOCALE_USER_DEFAULT, DATE_SHORTDATE | LOCALE_USE_CP_ACP, &st, NULL, buf, 32);
	if (len > 0){
		buf[len - 1] = ' ';
		len = GetTimeFormatA(LOCALE_USER_DEFAULT, LOCALE_USE_CP_ACP, &st, NULL, buf + len, 32 - len);
	}
	if (len == 0){
		wsprintfA(buf, "%4d/%02d/%02d %2d:%02d:%02d",
		st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
	}
}
#endif

// 2GB 未満のファイルの開始位置以降のハッシュ値を計算する
static unsigned int file_crc_part(HANDLE hFile)
{
	unsigned char buf[4096];
	unsigned int len, crc = 0xFFFFFFFF;

	// 末尾まで読み込む
	do {
		if (!ReadFile(hFile, buf, 4096, &len, NULL) || (len == 0))
			break;
		// CRC-32 計算
		crc = crc_update(crc, buf, len);
	} while (len > 0);

	return crc ^ 0xFFFFFFFF;
}

// .BIN ファイルの offset バイト目から size バイトのデータを buf に読み込む
static int file_read_bin(
	int offset,
	unsigned char *buf,
	unsigned int size)
{
	unsigned int rv;

	// ファイルの位置を offsetバイト目にする
	rv = SetFilePointer(hIniBin, offset, NULL, FILE_BEGIN);
	if (rv == INVALID_SET_FILE_POINTER)
		return 1;

	// size バイトを読み込む
	if ((!ReadFile(hIniBin, buf, size, &rv, NULL)) || (size != rv))
		return 1;	// 指定サイズを読み込めなかったらエラーになる

	return 0;
}

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
// 検査結果ファイルを利用する為の関数

// ini_path で指定されてる検査結果ファイルを削除する
static void delete_ini_file(void)
{
	// 開いてるなら閉じる
	if (hIniBin != NULL){
		CloseHandle(hIniBin);
		hIniBin = NULL;
	}

	DeleteFile(ini_path);	// .bin ファイルを削除する

	recent_data = 0;	// これ以降は検査結果は利用できない
}

// 検査するチェックサム・ファイルが同じであれば、再検査する必要は無い
int check_ini_file(unsigned char *set_hash, unsigned int set_len)
{
	unsigned char set_data[HEADER_SIZE * 2];
	wchar_t path[MAX_LEN], ini_name[INI_NAME_LEN + 1];
	int i, match;
	unsigned int file_time, time_now, time_limit;
	HANDLE hFind;
	WIN32_FIND_DATA FindData;
	FILETIME ft;

	if (recent_data == 0)
		return 0;

	// 設定ファイルのパス
	wsprintf(ini_name, L"0_%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X.bin",
		set_hash[0], set_hash[1], set_hash[2], set_hash[3], set_hash[4], set_hash[5], set_hash[6], set_hash[7],
		set_hash[8], set_hash[9], set_hash[10], set_hash[11],set_hash[12], set_hash[13], set_hash[14], set_hash[15]);

	// 期限を設定する
	switch (recent_data & 7){
	case 1:
		time_limit = 3600 * 24;	// 1 day
		break;
	case 2:
		time_limit = 3600 * 24 * 3;	// 3 days
		break;
	case 3:
		time_limit = 3600 * 24 * 7;	// 1 week
		break;
	case 4:
		time_limit = 3600 * 24 * 15;	// half month
		break;
	case 5:
		time_limit = 3600 * 24 * 30;	// 1 month
		break;
	case 6:
		time_limit = 3600 * 24 * 365;	// 1 year
		break;
	case 7:
		time_limit = 0xFFFFFFFF;	// unlimited
		break;
	default:	// 期間が指定されてない
		recent_data = 0;
		return 0;
	}
	GetSystemTimeAsFileTime(&ft);	// 現在のシステム時刻 (UTC) を取得する
	time_now = time_f_u(&ft);	// UNIX Time に変換する

	// 記録の日時を調べて、古いのは削除する
	match = 0;
	wcscpy(path, ini_path);
	wcscat(path, L"0_*.bin");	// 文字数は後でチェックする
	hFind = FindFirstFile(path, &FindData);
	if (hFind != INVALID_HANDLE_VALUE){
		do {
			if (((FindData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0) &&
					(wcslen(FindData.cFileName) == INI_NAME_LEN)){
				// 指定時間より古い記録は削除する
				file_time = time_f_u(&(FindData.ftLastWriteTime));	// 更新日時
				if (time_now - file_time > time_limit){
					wcscpy(path, ini_path);
					wcscat(path, FindData.cFileName);
					DeleteFile(path);
#if VERBOSE > 1
					time_f_date(&(FindData.ftLastWriteTime), (char *)path);
					printf("Delete result: %S, %s\n", FindData.cFileName, (char *)path);
#endif
				} else if (wcscmp(FindData.cFileName, ini_name) == 0){
					if (recent_data & 8){	// 検査結果を読み込まないが、今回のは書き込む場合
						// 以前の検査結果が存在すれば削除する
						wcscpy(path, ini_path);
						wcscat(path, ini_name);
						DeleteFile(path);
#if VERBOSE > 1
						printf("Delete result: %S\n", ini_name);
#endif
					} else {
						match = 1;	// 設定ファイルが存在する
					}
#ifdef VERBOSE
					ft = FindData.ftLastWriteTime;	// 検査結果の更新日時
#endif
				}
			}
		} while (FindNextFile(hFind, &FindData));	// 次のファイルを検索する
		FindClose(hFind);
	}

	// バイナリ・データ用の設定ファイルを開く
	wcscat(ini_path, ini_name);
	hIniBin = CreateFile(ini_path, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_FLAG_RANDOM_ACCESS, NULL);
	if (hIniBin == INVALID_HANDLE_VALUE){
		hIniBin = NULL;
		recent_data = 0;	// 開けない場合は再利用しない
		return 0;
	}
	//printf("ini file path = %S\n", ini_path);

	// 既に同名の (Set ID が同じ) ファイルが存在する場合
	set_data[0] = INI_VERSION >> 8;	// 検査結果の書式バージョン
	set_data[1] = INI_VERSION & 0xFF;
	memset(set_data +  2, 0, 4);	// CRC-32 は後で書き込む
	memcpy(set_data +  6, &set_len, 4);
	memcpy(set_data + 10, &file_num, 4);
	if (match != 0){	// ID が同じでも Set 内容が異なる場合は初期化する
		i = file_read_data(hIniBin, 0, set_data + HEADER_SIZE, HEADER_SIZE);
		if ((set_data[HEADER_SIZE    ] != (INI_VERSION >> 8)) ||
			(set_data[HEADER_SIZE + 1] != (INI_VERSION & 0xFF))){
			i = 1;	// 古いバージョンの検査結果は参照しない
		}
		if (i == 0){	// 検査結果が破損してないか確かめる
			if (SetFilePointer(hIniBin, 6, 0, FILE_BEGIN) != INVALID_SET_FILE_POINTER)
				time_now = file_crc_part(hIniBin);
			i = memcmp(&time_now, set_data + (HEADER_SIZE + 2), 4);
		}
		if (i == 0)	// チェックサム・ファイルのデータを比較する
			i = memcmp(set_data + 6, set_data + (HEADER_SIZE + 6), HEADER_SIZE - 6);
		if (i != 0)	// ID が同じでも Set 内容が異なる場合は初期化する
			match = 0;
	}

	// 一致しなかった場合は今回のデータを記録する
	if (match == 0){
		// 今回のデータを書き込む
		if (file_write_data(hIniBin, 0, set_data, HEADER_SIZE) != 0){
			delete_ini_file();
			return 0;
		}
		if (SetEndOfFile(hIniBin) == 0){
			delete_ini_file();
			return 0;
		}
		return 0;
#ifdef VERBOSE
	} else {
		time_f_date(&ft, (char *)path);
		printf("Date of result: %s\n", (char *)path);
#endif
	}

	return 1;
}

// 検査結果ファイルを閉じる
void close_ini_file(void)
{
	if ((recent_data != 0) && (hIniBin != NULL)){
		unsigned int new_crc, old_crc;

		// 閉じる前に検査結果の CRC-32 を計算して記録しておく
		FlushFileBuffers(hIniBin);
		file_read_data(hIniBin, 2, (unsigned char *)&old_crc, 4);
		new_crc = file_crc_part(hIniBin);
		if (new_crc != old_crc)	// 検査結果が同じなら更新しない
			file_write_data(hIniBin, 2, (unsigned char *)&new_crc, 4);
		CloseHandle(hIniBin);
		hIniBin = NULL;
	}
}

// 検査結果が記録されてるかどうか
// -1=エラー, -2=記録なし, 0=完全, 2=破損
int check_ini_state(
	int num,				// ファイル番号
	unsigned int meta[7],	// サイズ、作成日時、更新日時、ボリューム番号、オブジェクト番号
	HANDLE hFile)			// そのファイルのハンドル
{
	unsigned char buf[STATE_SIZE * STATE_READ];
	unsigned int rv, off, state;
	BY_HANDLE_FILE_INFORMATION fi;

	// 現在のファイル属性を取得する
	memset(&fi, 0, sizeof(BY_HANDLE_FILE_INFORMATION));
	if (GetFileInformationByHandle(hFile, &fi) != 0){
		meta[0] = fi.nFileSizeLow;
		meta[1] = fi.nFileSizeHigh;
		if ((recent_data == 0) || ((fi.nFileSizeLow <= REUSE_MIN) && (fi.nFileSizeHigh == 0)))
			return -2;	// 小さなファイルは記録を参照しない
		meta[2] = time_f_u(&(fi.ftCreationTime));
		meta[3] = time_f_u(&(fi.ftLastWriteTime));
		meta[4] = fi.dwVolumeSerialNumber;
		meta[5] = fi.nFileIndexLow;
		meta[6] = fi.nFileIndexHigh;
	} else {
		meta[0] = 0;
		meta[1] = 0;
		return -1;	// 属性の読み取りエラー
	}

	// ヘッダーの直後から開始する
	if (SetFilePointer(hIniBin, HEADER_SIZE, NULL, FILE_BEGIN) == INVALID_SET_FILE_POINTER){
		delete_ini_file();
		return -2;
	}
	ini_off = HEADER_SIZE;	// ファイル項目の位置
	// 一度に STATE_READ 個ずつ読み込む
	while (ReadFile(hIniBin, buf, STATE_SIZE * STATE_READ, &rv, NULL) != 0){
		if (rv < STATE_SIZE)
			break;
		// ファイル識別番号から同じファイルの記録を探す
		for (off = 0; off < rv; off += STATE_SIZE){
			if (memcmp(buf + off, meta + 4, 12) == 0){
				ini_off += off;	// 同じファイルの記録があった
				state = 0;
				memcpy(&state, buf + (off + 28), 2);
#ifdef VERBOSE
				printf("check state, num = %d, offset = %d, state = %d\n", num, HEADER_SIZE + off, (state & 1) << 1);
#endif
				// 番号が一致したら、ファイルのサイズと日時を比較する
				if (((state >> 1) == (num & 0x7FFF)) && (memcmp(buf + (off + 12), meta, 16) == 0))
					return (state & 1) << 1;
				return -2;	// 状態が変化してる
			}
		}
		ini_off += rv;
	}

	ini_off = 0;
	return -2;	// これ以上の検査記録は無い
}

// ソース・ファイル状態は完全か破損だけ (消失だと検査しない)
void write_ini_state(
	int num,				// ファイル番号
	unsigned int meta[7],	// サイズ、作成日時、更新日時、ボリューム番号、オブジェクト番号
	int state)				// 状態、0=完全, 2=破損
{
	unsigned char buf[STATE_SIZE];

	if ((recent_data == 0) || ((meta[0] <= REUSE_MIN) && (meta[1] == 0)))
		return;	// 小さなファイルは検査結果を記録しない

	if (ini_off == 0){	// 記録が無ければ末尾に追加する
		ini_off = GetFileSize(hIniBin, NULL);
		if (ini_off == INVALID_FILE_SIZE){
			delete_ini_file();
			return;
		}
	}

	// 今回の状態を書き込む
	memcpy(buf, meta + 4, 12);
	memcpy(buf + 12, meta, 16);
	buf[28] = (unsigned char)((state >> 1) | (num << 1));
	buf[29] = (unsigned char)(num >> 7);
#ifdef VERBOSE
	printf("write state, num = %d, offset = %d, state = %d\n", num, ini_off, state);
#endif
	if (file_write_data(hIniBin, ini_off, buf, STATE_SIZE) != 0){
		delete_ini_file();
		return;
	}
}

