EternalWindows
Cabinet API / 圧縮の手順

今回からまず、FCIについて説明します。 FCIで用いられているコールバック関数やコンテキストといった概念はFDIでも用いられているため、 しっかりと理解しておくことが重要です。 FCIによる圧縮の手順を次に示します。

1.FCICreateでFCIコンテキストを作成する。
2.FCIAddFileでファイルを圧縮したデータを一時ファイルに書き込む。
3.FCIFlushCabinetでキャビネットをディスクに出力させる。
4.FCIDestroyでFCIコンテキストを破棄する。

FCIコンテキストというのは、後に呼び出すFCI関数のために必要なものであり、 FCIによって内部的に作成されるキャビネットや一時ファイルを管理します。 最終的な目標となるのは、このキャビネットをFCIFlushCabinetでディスクに出力することであり、 それを行うためにはFCIAddFileでキャビネットにファイルを追加しておかなければなりません。 FCIAddFileを呼び出したとき、追加元となるファイルから読み取られたデータは圧縮され、 そのデータは一時ファイルに書き込まれることになります。 そして、この一時ファイルのデータがキャビネットにコピーされたときが キャビネットの中にファイルが作成されたときということになります。 このコピーの処理は、FCIAddFileとFCIFlushCabinetのどちらでも行うことができます。

FCIコンテキストはいわばハンドル的なものですから、 それを作成するFCICrateという関数が存在することは不思議なものではありません。 ただし、この関数の引数については、最初は少し戸惑ってしまうものです。

HFCI FCICreate(
    PERF perf,
    PFNFCIFILEPLACED pfnfiledest,
    PFNFCIALLOC pfnalloc,
    PFNFCIFREE pfnfree,
    PFNFCIOPEN pfnopen,
    PFNFCIREAD pfnread,
    PFNFCIWRITE pfnwrite,
    PFNFCICLOSE pfnclose,
    PFNFCISEEK pfnseek,
    PFNFCIDELETE pfndelete,
    PFNFCIGETTEMPFILE pfnfcigtf,
    PCCAB pccab,
    void pv
);

perfは、ERF構造体のアドレスを指定します。 pfnfcifpは、ファイルがキャビネットに作成されたときに呼ばれるコールバック関数のアドレスを指定します。 pfnallocは、メモリを確保する必要が生じた場合に呼ばれるコールバック関数のアドレスを指定します。 pfnfreeは、メモリを開放する必要が生じた場合に呼ばれるコールバック関数のアドレスを指定します。 pfnopenは、ファイルをオープンする際に呼ばれるコールバック関数のアドレスを指定します。 pfnreadは、ファイルを読み取る際に呼ばれるコールバック関数のアドレスを指定します。 pfnwriteは、ファイルに書き込む際に呼ばれるコールバック関数のアドレスを指定します。 pfncloseは、ファイルを閉じる際に呼ばれるコールバック関数のアドレスを指定します。 pfnseekは、ファイルをシークする際に呼ばれるコールバック関数のアドレスを指定します。 pfndeleteは、ファイルを削除する際に呼ばれるコールバック関数のアドレスを指定します。 pfnfcigtfは、一時ファイルを作成する際に呼ばれるコールバック関数のアドレスを指定します。 pccabは、CCAB構造体のアドレスを指定します。 pvは、コールバック関数に渡す引数として独自のデータを関連付けることができます。 戻り値のHFCI型がFCIコンテキストのハンドルとなります。

一般的にいわれるコールバック関数の役割は、何らかの操作を通知するというものであり、 FCIにおいてもその意味だけであれば、圧縮の内部動作を追跡できる便利な関数でした。 しかし、このコールバック関数では、肝心の動作の部分も実装しなければならないことになっています。 つまり、メモリを確保する関数や、ファイルを読み取る関数をコールバック関数内で 記述しなければならないということです。 このような動作をFCI(そしてFDI)が既定で行はない理由については定かではありませんが、 恐らくFCI/FDIの内部にAPI依存のコードを含めたくなかったのではないかと思われます。 メモリやファイル操作などを全てアプリケーションに行わせれば、 FCI/FDIの内部は本当の意味で圧縮/展開に関するコードだけで構成され、 CRTや何らかのDLLの関数の呼び出しは一切不要になります。 これにより、コードの移植性は非常に高まり、 cabinet.dllをロードしたら他のDLLも暗黙的にロードされるといったような事も起こりえません。

FCICreateは、ERF構造体とCCAB構造体を要求することになっています。 ERF構造体には、FCI/FDIの動作が失敗したときのエラー情報が格納されます。

typedef struct {
    int     erfOper;
    int     erfType;
    BOOL    fError;
} ERF;

erfOperは、FCIERROR列挙型の定数が格納されます。 この定数はFCIERR_XXXのような形をとり、それはfci.hにて確認できます。 erfTypeは、コールバック関数のerr変数に指定したエラー値が格納されます。 fErrorは、エラーが発生した場合にTRUEとなります。

CCAB構造体は、アプリケーションがキャビネットの情報をFCIに渡すため、 もしくは受け取るために利用することができます。 FCICreateが成功すると、この構造体はFCIコンテキストにコピーされます。

typedef struct {
    ULONG  cb; 
    ULONG  cbFolderThresh;
    UINT   cbReserveCFHeader;
    UINT   cbReserveCFFolder;
    UINT   cbReserveCFData;
    int    iCab;
    int    iDisk;
#ifndef REMOVE_CHICAGO_M6_HACK
    int    fFailOnIncompressible;
#endif
    USHORT setID;
    char   szDisk[CB_MAX_DISK_NAME];
    char   szCab[CB_MAX_CABINET_NAME];
    char   szCabPath[CB_MAX_CAB_PATH];
} CCAB;

cbは、キャビネットの理想とするサイズをバイト単位で指定します。 圧縮データを維持するバッファをキャビネットに追加するとき、 キャビネットの合計サイズがcbを上回るような場合は、 新しいキャビネットが作成されることになります。 1つのキャビネットに全てのファイルを含めたいような場合は、 cbのサイズを非常に大きく設定するべきですが、0でも問題ないように思えます。 cbFolderThreshは、フォルダ(一時的なバッファ)の理想とするサイズをバイト単位で指定します。 バッファにはファイルの圧縮されたデータが書き込まれていき、 そのサイズがcbFolderThreshを上回るかFCIFlushFolderを呼び出したときにキャビネットにコピーされます。 cbReserveCFHeader、cbReserveCFFolder、cbReserveCFDataは0を指定してください。 iCabは、複数のキャビネットが作成されるときのインデックスを指定します。 iDiskは、ディスクをカウントする役割がありますが、0を指定して問題ありません。 fFailOnIncompressibleは、明示的に初期化する必要がないと思われるため0を指定します。 setIDは、キャビネットに設定したい値を自由に指定します。 複数のキャビネットが作成された場合でも、ここで設定したIDは同一になります。 szDiskはキャビネットファイルが存在するドライブ文字列を指定します。 複数のキャビネットファイルを作成するときは、 次のキャビネットファイルの位置をszDiskとszCabを合わして求めることになるため、 適切に初期化することになります。 szCabは、キャビネットファイルの名前を指定します。 szCabPathは、szCabを作成したいディレクトリを指定します。

作成したFCIコンテキストは、FCIDestroyで破棄します。 これにより、FCIによって作成された一時ファイル等も削除されます。

BOOL FCIDestroy(HFCI hfci);

hfciは、FCIコンテキストを指定します。

今回のプログラムは、FCICreateでFCIコンテキストを作成することに専念し、 キャビネットファイルの作成までは行っていません。 また、cabinet.dllを利用するため、インポートライブラリであるcabinet.libにリンクします。 exeファイルにFCIのコードを埋め込みたい場合は、fci.libにリンクするようにしてください。

#include <windows.h>
#include <fci.h>

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

HFCI CreateFciContext(LPSTR lpszOutputDirectoryPath, LPSTR lpszCreateFileName, PERF perf);

int DIAMONDAPI FilePlaceProc(PCCAB pccab, char *pszFile, long cbFile, BOOL fContinuation, void *pv);

void * DIAMONDAPI AllocProc(ULONG cb);
void DIAMONDAPI FreeProc(void *memory);
int DIAMONDAPI OpenProc(char *pszFile, int oflag, int pmode, int *err, void *pv);
UINT DIAMONDAPI ReadProc(int hf, void *memory, UINT cb, int *err, void *pv);
UINT DIAMONDAPI WriteProc(int hf, void *memory, UINT cb, int *err, void *pv);
int DIAMONDAPI CloseProc(int hf, int *err, void *pv);
long DIAMONDAPI SeekProc(int hf, long dist, int seektype, int *err, void *pv);
int DIAMONDAPI DeleteProc(char *pszFile, int *err, void *pv);
BOOL DIAMONDAPI TempFileProc(char *pszTempName, int cbTempName, void *pv);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	char  szOutputDirectoryPath[] = "c:\\";
	char  szCreateFileName[] = "sample.cab";
	HFCI  hfci;
	ERF   erf;
	TCHAR szBuf[256];

	hfci = CreateFciContext(szOutputDirectoryPath, szCreateFileName, &erf);
	if (hfci == NULL) {
		wsprintf(szBuf, TEXT("FCIコンテキストの作成に失敗しました。 ErrorCode %d"), erf.erfOper);
		MessageBox(NULL, szBuf, NULL, MB_ICONWARNING);
		return FALSE;
	}

	MessageBox(NULL, TEXT("FCIコンテキストを作成しました。"), TEXT("OK"), MB_OK);	

	FCIDestroy(hfci);

	return 0;
}

HFCI CreateFciContext(LPSTR lpszOutputDirectoryPath, LPSTR lpszCreateFileName, PERF perf)
{
	HFCI hfci;
	CCAB cab;

	ZeroMemory(&cab, sizeof(CCAB));
	lstrcpyA(cab.szCab, lpszCreateFileName);
	lstrcpyA(cab.szCabPath, lpszOutputDirectoryPath);

	hfci = FCICreate(perf, FilePlaceProc, AllocProc, FreeProc, 
		OpenProc, ReadProc, WriteProc, CloseProc, SeekProc, DeleteProc,
		TempFileProc, &cab, NULL);
	
	return hfci;
}

int DIAMONDAPI FilePlaceProc(PCCAB pccab, char *pszFile, long cbFile, BOOL fContinuation, void *pv)
{
	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, int *err, void *pv)
{
	HANDLE hFile;

	hFile = CreateFileA(pszFile, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
	if (hFile == INVALID_HANDLE_VALUE && GetLastError() == ERROR_FILE_NOT_FOUND)
		hFile = CreateFileA(pszFile, GENERIC_READ | GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

	return (int)hFile;
}

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

	ReadFile((HANDLE)hf, memory, cb, &dwReadByte, NULL);
	
	return dwReadByte;
}

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

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

	return dwWriteByte;
}

int DIAMONDAPI CloseProc(int hf, int *err, void *pv)
{
	CloseHandle((HANDLE)hf);

	return 0;
}

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

int DIAMONDAPI DeleteProc(char *pszFile, int *err, void *pv)
{
	DeleteFileA(pszFile);

	return 0;
}

BOOL DIAMONDAPI TempFileProc(char *pszTempName, int cbTempName, void *pv)
{
	char szTempPath[256];

	GetTempPathA(cbTempName, szTempPath);
	GetTempFileNameA(szTempPath, "xx", 0, pszTempName);

	return TRUE;
}

まず、今回のプログラムで注意しておかなければならないのは、 多くの関数において明示的にANSI版の関数を呼び出している点です。 これは一重にFCI/FDIがUNICODEに対応していないからなのですが、 このように明示的にAをつけるくらいであればソースコードの 冒頭に#undef UNICODEという一行を追加し、 必ずANSIとしてコンパイルさせるようにするべきだったかもしれません。 ただ、いずれにしても、char型を扱わなければならないという事実は変わりありません。

コールバック関数の定義に書かれているDIAMONDAPIは、FCI/FDIにおける呼び出し規約です。 FCICreateの呼び出しを成功させるためには、 各コールバック関数を正しく実装して、アドレスを指定しなければなりません。 実際のところ、FCICreate内部で全てのコールバック関数が呼ばれるわけではないため、 そのような引数にはNULLを指定しても関数が成功することになりますが、 後続のFCIの呼び出しでエラーとなるのは明らかです。 CCAB構造体で少なくとも明示的に初期化しておくべきなのは、 szCabメンバとszCabPathメンバです。 これらのメンバはそれぞれ、キャビネットファイルの名前、 作成先となるディレクトリ、というように分かりやすい意味を持っています。 次のコードは、WinMainの一部です。

char  szOutputDirectoryPath[] = "c:\\";
char  szCreateFileName[] = "sample.cab";
HFCI  hfci;
ERF   erf;
TCHAR szBuf[256];

hfci = CreateFciContext(szOutputDirectoryPath, szCreateFileName, &erf);
if (hfci == NULL) {
	wsprintf(szBuf, TEXT("FCIコンテキストの作成に失敗しました。 ErrorCode %d"), erf.erfOper);
	MessageBox(NULL, szBuf, NULL, MB_ICONWARNING);
	return FALSE;
}

szOutputDirectoryPathがキャビネットファイルの出力先ディレクトリ、 szCreateFileNameが作成するキャビネットファイルの名前を表し、 これらは自作関数であるCreateFciContextにて、 szCabPathメンバとszCabメンバにコピーされます。 また、この関数はERF構造体を受け取り、 関数の失敗時にはerfOperメンバを通じてエラーコードを表示しています。 CreateFciContextは、次のように実装されています。

HFCI CreateFciContext(LPSTR lpszOutputDirectoryPath, LPSTR lpszCreateFileName, PERF perf)
{
	HFCI hfci;
	CCAB cab;

	ZeroMemory(&cab, sizeof(CCAB));
	lstrcpyA(cab.szCab, lpszCreateFileName);
	lstrcpyA(cab.szCabPath, lpszOutputDirectoryPath);

	hfci = FCICreate(perf, FilePlaceProc, AllocProc, FreeProc, 
		OpenProc, ReadProc, WriteProc, CloseProc, SeekProc, DeleteProc,
		TempFileProc, &cab, NULL);
	
	return hfci;
}

CCAB構造体のメンバをZeroMemoryで全て0に初期化します。 szCabとszCabPath以外にも適切な初期化が必要なメンバもあるかもしれませんが、 試した限りでは0で正常に動作しているように思えます。 FCICreateの最後の引数はコールバック関数に独自のデータを渡したいときに使いますが、 今回は必要がないためNULLを指定しています。

コールバック関数の実装について

FCICreateに指定するコールバック関数は、正にその役割通りの実装を行うことになります。 たとえば、メモリの確保ならばHeapAllocを、ファイルの読み取りならReadFileを、 一時ファイルの作成ならばGetTempPathで一時ファイル用のディレクトリのパスを取得し、 そのパスを基にGetTempFileNameを呼び出せばよいことになります。 以下、各関数が呼ばれるタイミングについて順に説明していきます。

AllocProcとFreeProcは、FCIがメモリの確保と破棄を必要とする場合に呼ばれます。 OpenProcは、一時ファイルのオープン、もしくはキャビネットファイルを作成するときに呼ばれます。 この関数のoflagとpmodeをCreateFileの引数用にどう変換するかですが、 残念ながら判別することができませんでした。 if文のCreateFileが実行されるときがキャビネットファイルの作成のときとなります。 関数の戻り値は、ファイルハンドルとなります。 ReadProcとWriteProcは、ファイルの読み取りと書き込みを行うときに呼ばれます。 一例としては、FCIAddFileに指定したファイルからデータを読み取るときにReadProcが、 そのデータを圧縮して一時ファイルに書き込むときにWriteProcが呼ばれています。 これらの関数の戻り値は、読み取った、もしくは書き込んだサイズでなければなりません。 CloseProcは、ファイルを閉じるときに呼ばれます。 関数が成功したときに0を返すべきであるため、 TRUEを返すCloseHandleの戻り値を直に指定してはいけません。 簡単のため、プログラムでは常に0を返すようにしてあります。 SeekProcは、ファイルポインタをシークさせる必要があるときに呼ばれます。 引数のdistとseektypeは、SetFilePointerに直接指定して問題ないと思われます。 戻り値は、新しいファイルポインタの位置でなければなりません。 DeleteProcは、一時ファイルを削除するときに呼ばれます。 戻り値については、CloseProcと同じ意味を持ちます。 TempFileProcは、一時ファイルが作成されるときに呼ばれます。 pszTempNameに一時ファイルの名前を格納しますが、 その名前のサイズはcbTempNameを超えてはなりません。 この関数では成功時にTRUEを返すようにします。

OpenProcの引数となっているoflagとpmodeを直接利用したい場合には、 CRTの_open関数を呼ぶ方法が考えられます。 このようにCRTを呼び出す場合は、他の関数の呼び出しもCRTに合わせることになるでしょう。 一部のコールバック関数にはerrという変数があり、 エラー発生時にはCRTのerrnoの値を格納することになっていますが、 CRTを利用しないアプリケーションもこの変数は利用することができます。 FCI/FDIは、引数に設定された値や戻り値がCRT準拠であるようなことは調べていないため、 単純にGetLastError等の値を設定しておき、 FCI/FDIの呼び出しの失敗時にERF構造体のerfTypeの値を参照するとよいでしょう。



戻る