EternalWindows
MSI データベース編 / キャビネットファイルの抽出

MSIファイルを編集するアプリケーションを開発していると、単にレコードやフィールドを 編集するだけでなく、実際にインストールされることになるファイルの中身も 必要に応じて編集したいという要望が出てくると思われます。 理論上では、MSIファイルにファイルが格納されているからこそ、 インストールの際にそれらのファイルをディスクに抽出できるわけですが、 これらのファイルがcab形式で圧縮されているということを忘れてはいけません。 cab形式で圧縮されたファイルは、.cabという拡張子を持つキャビネットファイルであり、 これを展開することで実際にファイルを取得することができるため、 まずはキャビネットファイルをMSIファイルから抽出する方法から考えなければなりません。

テーブルの1つであるMediaテーブルは、ソース媒体のインストール情報を記述しています。 ここで言うソースとは、インストールされることになる一連のファイルのことで、 Mediaテーブルには、それらのファイルが格納されているボリュームのラベルや、 ファイルを圧縮したキャビネットファイルの名前を表すカラムが存在しています。 Cabinetというカラムのフィールド値は、キャビネットファイルの名前が格納されるため、 まずはこれを取得するよう設定します。

MsiDatabaseOpenView(hDatabase, TEXT("SELECT Cabinet FROM Media"), &hView);

これにより、以後のMsiViewFetchの呼び出しはMediaテーブルのレコードが参照され、 レコードに格納されるフィールドもCabinetカラムのものだけとなります。 キャビネットファイルの名前は先頭に#がつく場合とそうでない場合がありますが、 この違いを知ることは非常に重要です。 #がつく場合、#を取り除いた正規のキャビネットファイル名を後述する_Streamsテーブルの プライマリキーと照合し、実際に圧縮データを取得することになります。 一方、#がつかない場合は、キャビネットファイルがMSIファイルの中ではなく、 外部のファイル、つまりディスク上に存在しているため、以降に示す手順を行う必要はありません。

_Streamsテーブルは、MsiDatabaseOpenViewによって一時的に作成されるテーブルであり、 取得すべきレコード、対象となるフィールドをSQL文に明確に指定しなければ、 目的のデータを得ることはできません。 たとえ、SQL文のカラムの部分に*を指定して_Streamsテーブルのレコードを走査しようとしても、 そのレコードのフィールドにはキャビネットファイルの名前は格納されないでしょう。 _Streamsテーブルには、NameとDataという2つのカラムが存在しますが、 明確にSQL文を指定した場合のみ、Nameには#を除いたキャビネットファイル名が格納され、 Dataには圧縮データが格納されることになっています。 _Streamsテーブルに格納されたデータは、MsiRecordReadStreamで取得することができます。

UINT MsiRecordReadStream(
  MSIHANDLE hRecord,
  UINT iField,
  char *szDataBuf,
  DWORD *pcbDataBuf
);

hRecordは、_Streamsテーブルから取得したレコードのハンドルを指定します。 iFieldは、何番目のフィールド値を指定するのかを指定します。 szDataBufは、ストリームから取得したデータを受け取るバッファのアドレスを指定します。 pcbDataBufは、バッファのサイズを格納した変数のアドレスを指定します。

今回のプログラムは、Mediaテーブルに格納されているレコードを走査し、 #がついているキャビネットファイル名があれば、#を除いた文字列をファイル名として カレントディレクトに出力します。 場合によっては拡張子として.cabを持たないファイルも存在するため、 そのようなときは後から明示的に拡張子を追加するようにしてください。

#include <windows.h>
#include <msiquery.h>

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

void CreateCabinetFile(MSIHANDLE hDatabase, LPTSTR lpszFileName);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR     szBuf[256];
	DWORD     dwSize;
	UINT      uResult;
	MSIHANDLE hDatabase;
	MSIHANDLE hView;
	MSIHANDLE hRecord;

	uResult = MsiOpenDatabase(TEXT("sample.msi"), MSIDBOPEN_READONLY, &hDatabase);
	if (uResult != ERROR_SUCCESS) {
		MessageBox(NULL, TEXT("データベースのオープンに失敗しました。"), NULL, MB_ICONWARNING);
		return 0;
	}

	uResult = MsiDatabaseOpenView(hDatabase, TEXT("SELECT Cabinet FROM Media"), &hView);
	if (uResult != ERROR_SUCCESS) {
		MessageBox(NULL, TEXT("ビューのオープンに失敗しました。"), NULL, MB_ICONWARNING);
		return 0;
	}

	MsiViewExecute(hView, 0);
	
	for (;;) {
		uResult = MsiViewFetch(hView, &hRecord);
		if (uResult != ERROR_SUCCESS)
			break;
		dwSize = sizeof(szBuf);
		MsiRecordGetString(hRecord, 1, szBuf, &dwSize);
		MsiCloseHandle(hRecord);

		if (szBuf[0] == '#')
			CreateCabinetFile(hDatabase, szBuf + 1);
	}

	MsiCloseHandle(hView);
	MsiCloseHandle(hDatabase);

	MessageBox(NULL, TEXT("終了します。"), TEXT("OK"), MB_OK);
	
	return 0;
}

void CreateCabinetFile(MSIHANDLE hDatabase, LPTSTR lpszFileName)
{
	char      *lpData;
	TCHAR     szSyntax[256];
	DWORD     dwSize;
	DWORD     dwWriteByte;
	UINT      uResult;
	HANDLE    hFile;
	MSIHANDLE hView;
	MSIHANDLE hRecord;
	
	wsprintf(szSyntax, TEXT("SELECT Data FROM _Streams WHERE Name = '%s'"), lpszFileName);
	uResult = MsiDatabaseOpenView(hDatabase, szSyntax, &hView);
	if (uResult != ERROR_SUCCESS) {
		MessageBox(NULL, TEXT("ビューのオープンに失敗しました。"), NULL, MB_ICONWARNING);
		return;
	}
	
	MsiViewExecute(hView, 0);
	MsiViewFetch(hView, &hRecord);

	MsiRecordReadStream(hRecord, 1, NULL, &dwSize);
	lpData = (char *)HeapAlloc(GetProcessHeap(), 0, dwSize);
	MsiRecordReadStream(hRecord, 1, lpData, &dwSize);
	
	MsiCloseHandle(hRecord);
	MsiCloseHandle(hView);
	
	hFile = CreateFile(lpszFileName, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
	WriteFile(hFile, lpData, dwSize, &dwWriteByte, NULL);
	CloseHandle(hFile);

	HeapFree(GetProcessHeap(), 0, lpData);
}

CreateCabinetFileという自作関数は、#を除いたキャビネットファイル名を受け取り、 それを基にキャビネットファイルを作成するコードで構成されています。 この関数を呼び出すための処理は、次のようになっています。

for (;;) {
	uResult = MsiViewFetch(hView, &hRecord);
	if (uResult != ERROR_SUCCESS)
		break;
	dwSize = sizeof(szBuf);
	MsiRecordGetString(hRecord, 1, szBuf, &dwSize);
	MsiCloseHandle(hRecord);

	if (szBuf[0] == '#')
		CreateCabinetFile(hDatabase, szBuf + 1);
}

Mediaテーブルのレコードは複数存在することがあるため、 MsiViewFetchの呼び出しはループ文に書かなければなりません。 Cabinetカラムは本来であれば4番目の列ですが、ビューのオープン時にカラムをCabinetに指定したため、 MsiRecordGetStringの第2引数を1とすることで、Cabinetカラムのフィールド値を取得できます。 取得した文字列の先頭の文字が#である場合は、キャビネットファイルはMSIファイルに存在するため、 バッファのアドレスを1つ進めることで#をカットして、文字列を関数に渡しています。 続いて、CreateCabinetFileの内部を順に見ていきます。

wsprintf(szSyntax, TEXT("SELECT Data FROM _Streams WHERE Name = '%s'"), lpszFileName);
uResult = MsiDatabaseOpenView(hDatabase, szSyntax, &hView);

参照すべきは_StreamsテーブルのDataカラムであるため、WHERE句までは問題ないでしょう。 取得するレコードは、Nameカラムのフィールドがキャビネットファイルの名前と 一致しなければならないため、WHERE句でNameとlpszFileNameを指定しています。

MsiRecordReadStream(hRecord, 1, NULL, &dwSize);
lpData = (char *)HeapAlloc(GetProcessHeap(), 0, dwSize);
MsiRecordReadStream(hRecord, 1, lpData, &dwSize);

MsiRecordReadStreamでストリームからデータを取得する部分です。 まず、最初はサイズが分からないためバッファのアドレスにNULLを指定して サイズを初期化することに専念し、サイズ分のメモリを確保すれば、 そのアドレスをバッファに指定してデータを取得することになります。 後は、このデータを作成したファイルに書き込めば、キャビネットファイルが作成されます。

hFile = CreateFile(lpszFileName, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
WriteFile(hFile, lpData, dwSize, &dwWriteByte, NULL);
CloseHandle(hFile);

lpszFileNameは、#を除いたキャビネットファイル名であるため、_Streamsテーブルへの参照だけでなく、 作成するファイル名にも適応できます。 WriteFileの第2引数は書き込むべきデータを維持しているバッファのアドレス、 第3引数はバッファのサイズであるため、それぞれMsiRecordReadStreamで取得した lpDataとdwSizeを指定すればよいことになります。

キャビネットファイルの展開

MSIファイルから抽出したキャビネットファイルから、その中に格納されているファイルを 抽出する作業は、MSIとはまた別の話題となります。 Cabinet APIと呼ばれる関数群は、キャビネットファイルの作成や展開するコードを実装していますが、 実はもう少し簡単な方法で、キャビネットファイルを展開することができます。 system32ディレクトリに存在するextrac32.exeは、コマンドライン引数で指定された キャビネットファイルを展開する機能を持っているので、これを利用する方法を検討します。

void ExpandCabinetFile(LPTSTR lpszFileName)
{
	TCHAR szDirectory[256];
	TCHAR szParameters[256];

	GetCurrentDirectory(sizeof(szDirectory), szDirectory);
	wsprintf(szParameters, TEXT("/E %s"), lpszFileName);

	ShellExecute(NULL, NULL, TEXT("extrac32"), szParameters, szDirectory, SW_HIDE);
}

この自作関数は、ShellExecuteを呼び出してextrac32.exeを起動します。 第4引数に指定するコマンドライン引数は、/Eオプションとキャビネットファイル名を 連結した文字列を指定することになります。 第5引数は、キャビネットファイルを展開するディレクトリを指定します。 上記の例では、カレントディレクトリを指定しているため、 カレントディレクトリにキャビネットファイルの中身が抽出されます。 できれば、既存ファイルと混じらないように専用のディレクトリを指定したいところですが、 カレントディレクトリ以外の場合は、展開に失敗しているように思えます。 第6引数のSW_HIDEは、展開のためにextrac32.exeを利用していることをユーザーに 見せないようにするという狙いですが、展開の進行度合いを確認したい場合はSW_SHOWでよいでしょう。 extrac32.exeは、キャビネットファイルの中身が既に展開先ディレクトリに存在する場合は 無限ループに陥るため、この点は注意しなければなりません。

展開先ディレクトリに作成されたファイル群を確認してみると、 恐らく想像していたイメージとは違う結果があると思われます。 たとえば、ファイルのファイル名が変更されていたりすることがあるでしょう。 これらを解決するには、今一度MSIファイルのテーブルを参照することになります。 ファイルの正規の名前は、FileテーブルのFileNameカラムから参照できます。 キャビネットファイルから展開されたファイル名は、FileテーブルのFileカラムの いずれかのフィールド値と一致するため、その名前をキーとしてレコードを取得します。



戻る