EternalWindows
Cabinet API / フォーマット解析

キャビネットファイルを展開するアプリケーションからすれば、 展開を行う前にキャビネットファイルに含まれるファイル名を確認し、 それをGUIで階層的に表示したいという要望は多いと思われます。 しかし、残念ながらFDIはこうした関数を提供していません。 たとえば、FDIIsCabinetはキャビネットファイルの情報を取得するものの、 そこにはファイルの名前は含まれておらず、 FDICopyではファイル名こそ取得できるものの、 あくまでそれは展開処理の最中でした。 このようなことから、ファイル名を取得する場合は、 キャビネットファイルのフォーマットを明示的に解析することになります。

キャビネットファイルのフォーマットについては、 Cabinet SDKのDOCSフォルダに含まれているCABFMT.docに詳しく記述されています。 要点としては、キャビネットファイルの先頭にCFHEADER構造体が格納されている点と、 ファイルの情報(属性や名前)と圧縮されたデータがそれぞれ、 CFFILE構造体とCFDATA構造体というように別々に格納されている点、 そして、CFDATA構造体の参照のためにCFFOLDER構造体が存在している点が挙げられます。 まず、CFHEADER構造体について見てみます。

struct CFHEADER {
	BYTE  signature[4];
	DWORD reserved1;
	DWORD cbCabinet;
	DWORD reserved2;
	DWORD coffFiles;
	DWORD reserved3;
	BYTE  versionMinor;
	BYTE  versionMajor;
	WORD  cFolders;
	WORD  cFiles;
	WORD  flags;
	WORD  setID;
	WORD  iCabinet;
};

signatureは、ファイルがキャビネットファイルであることを示す M,S,C,F,(0x4d, 0x53, 0x43, 0x46)の4つが格納されています。 reserved1は予約されており、0が格納されています。 cbCabinetは、キャビネットファイルのサイズがバイト単位で格納されています。 reserved2は予約されており、0が格納されています。 coffFilesは、ファイルの先頭から最初のCFFILE構造体までのオフセットです。 reserved3は予約されており、0が格納されています。 versionMinorは、フォーマートのマイナーバージョンで、3が格納されます。 versionMajorは、フォーマートのメジャーバージョンで、1が格納されます。 cFoldersは、このファイルに存在するCFFOLDER構造体の数です。 cFilesは、このファイルに存在するCFFILE構造体の数です。 flagsについては、後述します。 setIDは、キャビネットファイルに設定したIDを指定します。 iCabinetは、複数キャビネット時のゼロベースのインデックスです。

CFHEADER構造体の後には、CFFOLDER構造体が格納されることになりますが、 単純にファイルの先頭からsizeof(CFHEADER)進めるだけでは、 場合によっては思うようにいかないことがあります。 これは、上記のCFHEADER構造体の後にオプションとして下記のデータが含まれることがあり、 このような場合、CFHEADER構造体のサイズは容易に知ることはできません。

// 以下のデータは、CFHEADER.flagsにcfhdrRESERVE_PRESENT(0x0004)が含まれている場合に存在する。
WORD cbCFHeader; // CFHEADER.abReserveのサイズ
BYTE cbCFFolder; // CFFOLDER.abReserveのサイズ
BYTE cbCFData;  // CFDATA.abReserveのサイズ
BYTE abReserve[CFHEADER.cbCFHeader]; // アプリケーションデータ

// 以下のデータは、CFHEADER.flagsにcfhdrPREV_CABINET(0x0001)が含まれている場合に存在する。
BYTE szCabinetPrev[]; // 前のキャビネットファイルの名前。
BYTE szDiskPrev[]; // 前のキャビネットが存在するディスク名。


// 以下のデータは、CFHEADER.flagsにcfhdrNEXT_CABINET(0x0002)が含まれている場合に存在する。
BYTE szCabinetNext[]; // 次のキャビネットファイルの名前。
BYTE szDiskNext[]; // 次のキャビネットが存在するディスク名。

CFHEADER.flagsにcfhdrRESERVE_PRESENTが含まれている場合、 後述するCFFOLDER構造体の最後にabReserve[CFHEADER.cbCFFolder]というBYTE型の配列が含まれます。 また、後述するCFDATA構造体のcbUncompメンバの後にabReserve[CFHEADER.cbCFData]というBYTE型の配列が含まれます。

続いて、CFFOLDER構造体の定義を見てみます。 CFFOLDER構造体はCFHEADER構造体の後に存在し、 複数のCFFOLDER構造体がある場合は、互いに隣接しています。 つまり、2つ目のCFFOLDER構造体があるならば、それは1つ目のCFFOLDER構造体の後に存在します。

struct CFFOLDER {
	DWORD coffCabStart;
	WORD  cCFData;
	WORD  typeCompress;
};

coffCabStartは、ファイルの先頭からこのCFFOLDER構造体に関連付けられているCFDATA構造体へのオフセットです。 cCFDataは、このCFFOLDER構造体に関連付けられているCFDATA構造体の数です。 typeCompressは、圧縮形式を示す値です。 キャビネットファイルはファイルのフォーマットを定義していますが、 圧縮形式に関しては多くの種類がサポートされており、どれを利用するかは自由です。 CFFOLDER構造体はファイルのデータを含むという言い方をされることがありますが、 実際にはこの構造体にファイルのデータが含まれるわけではなく、 単にデータを格納しているCFDATA構造体へのオフセットを維持しているだけです。

続いて、CFFILE構造体の定義を見てみます。 この構造体は、CFHEADER.coffFilesから参照可能で、複数のCFFILE構造体は互いに隣接しています。 CFFILE構造体は、キャビネットファイルに含まれる1個のファイルを表現し、 圧縮データ以外の情報を含んでいます。

struct CFFILE {
	DWORD cbFile;
	DWORD uoffFolderStart;
	WORD  iFolder;
	WORD  date;
	WORD  time;
	WORD  attribs;
	BYTE  szName[1];
};

cbFileは、このファイルが展開されたときのサイズがバイト単位で格納されます。 uoffFolderStartは、このファイルが展開されたときのデータの先頭アドレスです。 iFolderは、このファイルが関連付けられているCFFOLDER構造体のインデックスです。 0ならば1つ目のCFFOLDER構造体、1ならば2つ目のCFFOLDER構造体という要領になります。 date、time、attribsは、それぞれファイルの日付、時刻、属性となり、 扱いに関してはFDICopyのときと同様になります。 szNameは、ファイルの名前が格納されており、サイズが事前に分からないことから、 配列の要素数は1としています。 通常、ファイル名はASCII文字列ですが、attribsに_A_NAME_IS_UTFが含まれている場合は、 UTF形式となります。

最後に、CFDATA構造体の定義を見てみます。 この構造体はファイルの圧縮されたデータを含んでいますが、 1つのCFDATA構造体が1つのファイルの圧縮データを含んでいるようなことはなく、 複数のCFDATA構造体で1つ以上のファイルの圧縮データを表現することになっています。 複数のCFDATA構造体は互いに隣接し、最初のCFDATA構造体はCFFOLDER.coffCabStartから導けます。

struct CFDATA {
	DWORD csum;
	WORD  cbData;
	WORD  cbUncomp;
};

csumは、cbDataからab[cbData - 1]までのチェックサムです。 チェックサムが提供されない場合は、0の場合もあります。 cbDataは、圧縮データのサイズです。 cbUncompは、この構造体に含まれる圧縮データを展開したサイズのようですが、 後続のCFDATA構造体にも同じ値が格納されているように思えます。 cbUncompのメンバの後に、cbData分の圧縮データが格納されています。 後続のCFDATA構造体のアドレスを取得するには、次のようなコードを記述します。

// 圧縮データのサイズと構造体のサイズ分だけポインタを進める
lpData = (LPDATA)((LPBYTE)lpData + lpData->cbData + sizeof(CFDATA));

それでは、これまでの内容をまとめてみましょう。 まず、キャビネットファイルに含まれているファイルの情報を取得するには、 CFHEADER.coffFilesを利用してCFFILE構造体の先頭にアクセスします。 さらに、そのファイルの圧縮されたデータを取得したい場合、 次のようにiFolderを利用して適切なCFFOLDER構造体にアクセスし、 その後、CFFOLDER.coffCabStartを介してCFDATA構造体にアクセスします。

// lpはファイルの先頭アドレスと仮定
lpFolder = (LPCFFOLDER)((LPBYTE)lp + (sizeof(CFFOLDER) * lpFile->iFolder));
lpData = (LPCFDATA)lpFolder->coffCabStart;

CFFOLDER.cCFDataを参照すれば、CFDATA構造体の数は特定できます。 後は、一連のCFDATA構造体の圧縮データを1つに連結し、そのデータに対して展開処理を行います。 そこで得られた展開データに対して、特定のファイルのデータがどこから含まれているかは、 CFFILE.uoffFolderStartで特定できると思われます。 その位置から、CFFILE.cbFile分がそのファイルのデータになると思われます。

今回のプログラムは、CFFILE構造体にアクセスしてファイル名を表示します。

#include <windows.h>

struct CFHEADER {
	BYTE  signature[4];
	DWORD reserved1;
	DWORD cbCabinet;
	DWORD reserved2;
	DWORD coffFiles;
	DWORD reserved3;
	BYTE  versionMinor;
	BYTE  versionMajor;
	WORD  cFolders;
	WORD  cFiles;
	WORD  flags;
	WORD  setID;
	WORD  iCabinet;
};
typedef struct CFHEADER CFHEADER;
typedef struct CFHEADER *LPCFHEADER;

struct CFFILE {
	DWORD cbFile;
	DWORD uoffFolderStart;
	WORD  iFolder;
	WORD  date;
	WORD  time;
	WORD  attribs;
	BYTE  szName[1];
};
typedef struct CFFILE CFFILE;
typedef struct CFFILE *LPCFFILE;

LPVOID ReadCabinetFile(LPTSTR lpszFileName);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	LPCFHEADER lpHeader;
	LPCFFILE   lpFile;
	WORD       i;
	DWORD      dwOffset;
	
	lpHeader = (LPCFHEADER)ReadCabinetFile(TEXT("sample.cab"));
	if (lpHeader == NULL)
		return 0;

	lpFile = (LPCFFILE)((LPBYTE)lpHeader + lpHeader->coffFiles);

	for (i = 0; i < lpHeader->cFiles; i++) {
		MessageBoxA(NULL, (LPSTR)lpFile->szName, "OK", MB_OK);
		dwOffset = lstrlenA((LPSTR)lpFile->szName) + 1;
		lpFile = (LPCFFILE)&lpFile->szName[dwOffset];
	}

	HeapFree(GetProcessHeap(), 0, lpHeader);

	return 0;
}

LPVOID ReadCabinetFile(LPTSTR lpszFileName)
{
	DWORD      dwReadByte;
	DWORD      dwFileSize;
	HANDLE     hFile;
	LPCFHEADER lpHeader;
	
	hFile = CreateFile(lpszFileName, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
	if (hFile == INVALID_HANDLE_VALUE) {
		MessageBox(NULL, TEXT("ファイルのオープンに失敗しました。"), NULL, MB_ICONWARNING);
		return NULL;
	}

	dwFileSize = GetFileSize(hFile, NULL);
	lpHeader = (LPCFHEADER)HeapAlloc(GetProcessHeap(), 0, dwFileSize);
	
	ReadFile(hFile, lpHeader, dwFileSize, &dwReadByte, NULL);

	CloseHandle(hFile);

	if (*((LPDWORD)lpHeader->signature) != *((LPDWORD)"MSCF")) {
		MessageBox(NULL, TEXT("キャビネットファイルではありません。"), NULL, MB_ICONWARNING);
		HeapFree(GetProcessHeap(), 0, lpHeader);
		return NULL;
	}

	return lpHeader;
}

ReadCabinetFileという自作関数は、キャビネットファイルから全てのデータを取得し、 その先頭アドレスを呼び出し元に返します。 キャビネットファイルでは特定位置への移動が頻繁に発生するため、 SetFilePointerによるシークよりも、ポインタによるアドレスの増減の方が分かりやすいでしょう。 CFFILE構造体のアドレスは、次のように取得しています。

lpFile = (LPCFFILE)((LPBYTE)lpHeader + lpHeader->coffFiles);

lpHeaderはファイルの先頭アドレスを指すと同時に、CFHEADER構造体としても機能します。 CFFILE構造体のアドレスは、CFHEADER.coffFilesに格納されているため、 これを先頭アドレスに足せばよいことになります。

for (i = 0; i < lpHeader->cFiles; i++) {
	MessageBoxA(NULL, (LPSTR)lpFile->szName, "OK", MB_OK);
	dwOffset = lstrlenA((LPSTR)lpFile->szName) + 1;
	lpFile = (LPCFFILE)&lpFile->szName[dwOffset];
}

キャビネットファイルに含まれるファイルの数は、CFHEADER.cFilesに格納されています。 複数のCFDATA構造体は互いに隣接していますが、ファイル名が一定でないことから、 szNameの長さを考慮する必要があります。 1つ目のCFDATA構造体の文字列が表す終端のNULL文字の次のバイトからが2つ目のCFDATA構造体になりますから、 NULL文字を含めて長さを保存し、それをszNameの要素に指定してアドレスを返します。


戻る