EternalWindows
Cabinet API / キャビネットファイルの展開

FDIコンテキストを取得すれば、FDICopyで展開処理を行うことができます。

BOOL FDICopy(
    HFDI hfdi,
    char *pszCabinet,
    char *pszCabPath,
    int flags,
    PFNFDINOTIFY pfnfdin,
    PFNFDIDECRYPT pfnfdid,
    void *pvUser
);

hfdiは、FDIコンテキストを指定します。 pszCabinetは、展開するキャビネットファイルの名前を指定します。 pszCabPathは、pszCabinetで指定したファイルが存在するパスを指定します。 flagsは、展開処理を調整するためのフラグですが、 現在は定義されているフラグが存在しないため0を指定します。 pfnfdinは、展開の処理の動作を追跡するコールバック関数のアドレスを指定します。 pfnfdidは、復号化に関するコールバック関数のアドレスとなっていますが、 現在のFDIでは復号がサポートされていないため、NULLを指定します。 pvUserは、pfnfdinのコールバック関数に渡したいデータのアドレスを指定します。

FDICopyに指定するコールバック関数は、単に展開処理の流れを通知するものではありません。 FDICopyは展開処理を行うものの、展開したファイルをディスクに作成するようなことは行わないため、 それらの処理に関してはアプリケーション自身がコールバック関数内で行わなければなりません。 次に、PFNFDINOTIF型のコールバック関数を示すと共に、 最低限、補足すべき通知タイプを記述します。

int DIAMONDAPI NotifyProc(FDINOTIFICATIONTYPE type, PFDINOTIFICATION pNotify)
{
	switch (type) {

	case fdintCOPY_FILE: {
		// ファイルを作成してファイルハンドルを返す。
	}

	case fdintCLOSE_FILE_INFO: {
		// ファイルに時刻と属性を設定してTRUEを返す。
	}
	
	default:
		break;

	}

	return 0;
}

FDINOTIFICATIONTYPEは、通知タイプを示す列挙型です。 この型に応じてコールバック関数では適切な処理を行うことになります。 PFDINOTIFICATIONはFDINOTIFICATION構造体へのポインタであり、 そのメンバの意味については、FDINOTIFICATIONTYPEの意味に応じて異なります。 次に、FDINOTIFICATIONTYPEで得られる値と、FDINOTIFICATION構造体のメンバの関係について示します(複数キャビネット時のfdintPARTIAL_FILEとfdintNEXT_CABINETは除いています)。

タイプ 説明
fdintCABINET_INFO FDICopyがキャビネットファイルをオープンしたときに送られる。 psz1は次のキャビネットファイルの名前、pszは次のキャビネットが存在するはずのディスク名、 psz3はキャビネットファイルのパス。 戻り値は、-1以外のときに成功とされる。
fdintCOPY_FILE キャビネットファイルに含まれるファイルをディスク上に作成すべきときに送られる。 psz1は作成すべきファイル名、cbはファイルが展開されたサイズ。 戻り値は-1のとき失敗となり、0のときはこのファイルの展開をスキップ、ファイルハンドルを返した場合は、 キャビネットファイルに含まれる次のファイル名がfdintCLOSE_FILE_INFOの後に送られる。 スキップ機能を応用すれば、特定のファイルの展開時にMessageBoxを表示してファイル作成の可否を問うようなことができる。
fdintCLOSE_FILE_INFO fdintCOPY_FILEで作成したファイルに、展開データの書き込みが終了したときに呼ばれる。 hfはfdintCOPY_FILEで作成したファイルのハンドル、dateはファイルの日付、timeはファイルの時刻、 attribsはファイルの属性となる。戻り値は常にTRUEとすべきである。
fdintENUMERATE キャビネットファイルのデータをバイト単位で見立てる場合、CFFILE構造体を走査することになるが、 この走査が行われるときに呼ばれる。また、走査を終えたときにも呼ばれる。 戻り値は、-1以外のときに成功とされる。

送られる順番は、 fdintCABINET_INFO -> fdintENUMERATE -> (fdintCOPY_FILE -> fdintCLOSE_FILE_INFO) -> fdintENUMERATEのようになります。 括弧の部分については、キャビネットファイルの数だけ繰り返されることになります。

今回のプログラムは、FDICopyを呼び出して展開処理を行います。 ディレクトリの作成にMakeSureDirectoryPathExistsという関数を利用しているため、 dbghelp.hのインクルードとdbghelp.libを行っています。

#include <windows.h>
#include <dbghelp.h>
#include <fdi.h>

#pragma comment (lib, "dbghelp.lib")
#pragma comment (lib, "cabinet.lib")

int DIAMONDAPI NotifyProc(FDINOTIFICATIONTYPE type, PFDINOTIFICATION pNotify);
void * DIAMONDAPI AllocProc(ULONG cb);
void DIAMONDAPI FreeProc(void *memory);
int DIAMONDAPI OpenProc(char *pszFile, int oflag, int pmode);
UINT DIAMONDAPI ReadProc(int hf, void *pv, UINT cb);
UINT DIAMONDAPI WriteProc(int hf, void *pv, UINT cb);
int DIAMONDAPI CloseProc(int hf);
long DIAMONDAPI SeekProc(int hf, long dist, int seektype);
BOOL StartFdi(LPSTR lpszOutputDirectory, LPSTR lpszFileName, LPSTR lpszDirectoryName);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	BOOL bResult;
	
	bResult = StartFdi("c:\\expand\\", "sample.cab", "c:\\");
	if (bResult)
		MessageBox(NULL, TEXT("キャビネットファイルを展開しました。"), TEXT("OK"), MB_OK);

	return 0;
}

BOOL StartFdi(LPSTR lpszOutputDirectory, LPSTR lpszFileName, LPSTR lpszDirectoryName)
{
	HFDI  hfdi;
	ERF   erf;
	BOOL  bResult;
	TCHAR szBuf[256];

	hfdi = FDICreate(AllocProc, FreeProc, OpenProc, ReadProc, WriteProc, CloseProc, SeekProc, cpu80386, &erf);
	if (hfdi == NULL) {
		wsprintf(szBuf, TEXT("FDIコンテキストの作成に失敗しました。 ErrorCode %d"), erf.erfOper);
		MessageBox(NULL, szBuf, NULL, MB_ICONWARNING);
		return FALSE;
	}
	
	bResult = FDICopy(hfdi, lpszFileName, lpszDirectoryName, 0, NotifyProc, NULL, lpszOutputDirectory);
	if (!bResult) {
		wsprintf(szBuf, TEXT("ファイルのコピーに失敗しました。 ErrorCode %d"), erf.erfOper);
		MessageBox(NULL, szBuf, NULL, MB_ICONWARNING);
		FDIDestroy(hfdi);
		return FALSE;
	}

	FDIDestroy(hfdi);

	return TRUE;
}

int DIAMONDAPI NotifyProc(FDINOTIFICATIONTYPE type, PFDINOTIFICATION pNotify)
{
	switch (type) {

	case fdintCOPY_FILE: {
		char   szFileName[256];
		HANDLE hFile;

		lstrcpyA(szFileName, (LPSTR)pNotify->pv);
		lstrcatA(szFileName, (LPSTR)pNotify->psz1);
		MakeSureDirectoryPathExists(szFileName);

		hFile = CreateFileA(szFileName, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
		if (hFile == INVALID_HANDLE_VALUE)
			return -1;

		return (int)hFile;
	}

	case fdintCLOSE_FILE_INFO: {
		char     szFileName[256];
		FILETIME fileTime;
		FILETIME localFileTime;
		
		DosDateTimeToFileTime(pNotify->date, pNotify->time, &fileTime);
		LocalFileTimeToFileTime(&fileTime, &localFileTime);
		SetFileTime((HANDLE)pNotify->hf, &localFileTime, NULL, &localFileTime);

		CloseHandle((HANDLE)pNotify->hf);
		
		lstrcpyA(szFileName, (LPSTR)pNotify->pv);
		lstrcatA(szFileName, (LPSTR)pNotify->psz1);
		SetFileAttributesA(szFileName, pNotify->attribs);

		return TRUE;
	}
	
	default:
		break;

	}

	return 0;
}

void * DIAMONDAPI AllocProc(ULONG cb)
{
	return HeapAlloc(GetProcessHeap(), 0, cb);
}

void DIAMONDAPI FreeProc(void *memory)
{
	HeapFree(GetProcessHeap(), 0, memory);
}

int DIAMONDAPI OpenProc(char *pszFile, int oflag, int pmode)
{
	HANDLE hFile;

	hFile = CreateFileA(pszFile, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

	return (int)hFile;
}

UINT DIAMONDAPI ReadProc(int hf, void *pv, UINT cb)
{
	DWORD dwReadByte;

	ReadFile((HANDLE)hf, pv, cb, &dwReadByte, NULL);

	return dwReadByte;
}

UINT DIAMONDAPI WriteProc(int hf, void *pv, UINT cb)
{
	DWORD dwWriteByte;

	WriteFile((HANDLE)hf, pv, cb, &dwWriteByte, NULL);

	return dwWriteByte;
}

int DIAMONDAPI CloseProc(int hf)
{
	CloseHandle((HANDLE)hf);

	return 0;
}

long DIAMONDAPI SeekProc(int hf, long dist, int seektype)
{
	return SetFilePointer((HANDLE)hf, dist, 0, seektype);
}

WinMainで呼ばれている自作関数のStartFdiは、内部でFDICopyの呼び出しを行っています。 この関数の第1引数は展開したファイルの出力先ディレクトリ、 第2引数は展開したいキャビネットファイルの名前、 第3引数はそのキャビネットファイルが存在するパスとなっています。 ファイル名とパスを分割しているのは、FDICopyの引数がそのようになっているためです。 次に、FDICopyの呼び出しを確認します。

bResult = FDICopy(hfdi, lpszFileName, lpszDirectoryName, 0, NotifyProc, NULL, lpszOutputDirectory);

最後の引数に、展開先ディレクトリを示した文字列を指定している点が重要です。 NotifyProcで示されるコールバック関数では、 FDICopyによって展開されたファイルをディスク上に作成しなければならないため、 どこに作成するかを示すパス情報が必要になるのです。 次に、NotifyProcのfdintCOPY_FILEの処理を確認します。

case fdintCOPY_FILE: {
	char   szFileName[256];
	HANDLE hFile;

	lstrcpyA(szFileName, (LPSTR)pNotify->pv);
	lstrcatA(szFileName, (LPSTR)pNotify->psz1);
	MakeSureDirectoryPathExists(szFileName);

	hFile = CreateFileA(szFileName, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
	if (hFile == INVALID_HANDLE_VALUE)
		return -1;

	return (int)hFile;
}

作成すべきファイル名は、PFDINOTIFICATION構造体のpsz1メンバから参照できるため、 この文字列をパス情報を格納したバッファに連結することになります。 キャビネットファイルに追加されるファイル名は、パス情報を含むことができることから、 単純に連結した文字列をCreateFileAに指定しては、ディレクトリが存在していないためにエラーが発生することがあります。 たとえば、pvメンバの値が"c:\\expand\\"で、psz1が"data\\sample.txt"である場合、 連結した文字列は"c:\\expand\\data\\sample.txt"となりますが、 このdataフォルダに関しては前もって予測して作成しておくことはできません。 しかし、dbghelpとして定義されるMakeSureDirectoryPathExistsには、 パスに含まれるディレクトリをまとめて作成する機能があるため、 これを呼び出せば上の例のdataフォルダは"c:\\expand"に作成されることになります。 そうすれば、後はファイルを作成すればよいことになります。 この処理でファイルを作成することによって、 FDICopyはそのファイルに対して展開したデータを書き込み、 その書き込みが終了したらfdintCLOSE_FILE_INFOを通知することになります。

case fdintCLOSE_FILE_INFO: {
	char     szFileName[256];
	FILETIME fileTime;
	FILETIME localFileTime;
	
	DosDateTimeToFileTime(pNotify->date, pNotify->time, &fileTime);
	LocalFileTimeToFileTime(&fileTime, &localFileTime);
	SetFileTime((HANDLE)pNotify->hf, &localFileTime, NULL, &localFileTime);

	CloseHandle((HANDLE)pNotify->hf);
	
	lstrcpyA(szFileName, (LPSTR)pNotify->pv);
	lstrcatA(szFileName, (LPSTR)pNotify->psz1);
	SetFileAttributesA(szFileName, pNotify->attribs);

	return TRUE;
}

fdintCOPY_FILEで作成したファイルがキャビネットファイルに含まれているファイルの情報と矛盾しないように、 ファイルの時間と属性と設定する必要があります。 キャビネットファイルでは、時刻をMS_DOS形式で格納しているため、 まずこれをDosDateTimeToFileTimeで64ビット形式に変換し、 さらにLocalFileTimeToFileTimeを呼び出すことでNTFSファイルシステム上で有効なUTC形式に変換します。 SetFileTimeでは、ファイル作成の日時と最終更新日時をキャビネットファイル上のファイルと同一にしたいため、 第2引数と第4引数にファイル時刻を指定することになります。 一方、ファイル属性に関しては単純にキャビネットファイル上のファイルの属性を設定するだけで構いません。

複数キャビネットの展開

キャビネットファイルを分割して作成した場合、 FDINOTIFICATIONTYPEとしてfdintPARTIAL_FILEやfdintNEXT_CABINETが送られる場合があります。 まず、fdintPARTIAL_FILEですが、これは複数キャビネットのうち、 最初のインデックスに相当するキャビネット以外を指定した場合に送られます。 キャビネットは順番に展開していく必要があるため、 このような場合は展開が正常に行われる保証はありません。 送られる順番としては、fdintCABINET_INFO -> fdintENUMERATE -> (fdintPARTIAL_FILE) -> fdintENUMERATEのようになります。 fdintPARTIAL_FILEが送られる回数は、 現在のインデックスとキャビネットファイルに含まれるファイル数で決定されます。

次に、fdintNEXT_CABINETですが、これは次のキャビネットファイルの存在を保障するために呼ばれます。 キャビネットファイルには、次のキャビネットファイルのパス情報が含まれていますが、 このパスに実際にファイルが存在しないような場合、fdintNEXT_CABINETが無限に送られることになります。 このため、psz1で指定されたファイルが実際に存在するかを調べ、 存在しないよう場合は止む無く-1を返すことになるでしょう。 ただし、分割したキャビネットをCD-ROM等に格納していた場合は、 そのディスクのセットをユーザーに促すことで、ファイルの存在を確認できる可能性はあります。 psz2を調べれば、キャビネットファイルが存在すべきディスク名を確認できます。 fdintNEXT_CABINETが送られる順番は、 fdintCABINET_INFO -> fdintENUMERATE -> (fdintCOPY_FILE -> fdintCLOSE_FILE_INFO -> fdintNEXT_CABINET) -> fdintCABINET_INFO のようになります。



戻る