EternalWindows
ZIP / IStorageによる展開

前節では、IShellDispatchを使用してZIPファイルを展開しましたが、 今回はシェル拡張の機能を使用してZIPファイルを展開します。 シェル拡張とはシェルの機能を強化するためのDLLであり、 たとえばZIPファイル上でのコンテキストメニューに「すべて展開」という項目が追加されるのは、 シェル拡張の機能によるものです。 ZIPファイルに関連付けられているシェル拡張は、HKEY_CLASSES_ROOT\.zipから参照することができます。

既定のエントリに格納されているのは、シェル拡張のProgIDです。 これを基に関連するCLSIDを見つけるために、HKEY_CLASSES_ROOT\CompressedFolder以下を参照します。

ShellEx以下に存在するキーがZIPファイルに関連付けられているシェル拡張であり、 データとして格納されているCLSIDをCoCreateInstanceに指定することができます。 これにより、取得したインターフェースでZIPファイルを操作できることになります。 ちなみに、シェル拡張のCLSIDはそれぞれ異なっていますが、 いずれもその実体はzipfldr.dllになっています。 つまり、zipfldr.dllはCOMサーバーであり、 ZIPファイルを操作するためのインターフェースはこのDLL内で実装されています。 この事実からいえることは、DLL内の関数の呼び出し方が分からない場合でも、 そのDLLがCOMサーバーであればDLLの機能を利用できる可能性があるといういう点です。 既存のCOMインターフェースの使い方はMSDNに記述されていますから、 そのようなインターフェースをQueryInterfaceで取得することができれば、 そのインターフェースでオブジェクトを操作できることになります。

上記した3つのシェル拡張の中で、今回使用することになるのはStorageHandlerです。 StorageHandlerはIStorageを実装し、ファイルを構造化ストレージに見立てるための機能を提供します。 構造化ストレージというのは階層モデルであり、 ストレージの下にはストリームが存在するという概念で成り立っています。 これはファイルシステムにおける、フォルダの下にはファイルが存在するという仕組みと全く同じです。 IStorageは1つのストレージ(フォルダ)を表しており、OpenStreamを呼び出せば1つのストリーム(ファイル)を表すIStreamを取得することができます。 このIStreamのReadを呼び出せば、ストリームのデータを展開された状態で取得することができるため、 これをファイルに保存すればよいことになります。

今回のプログラムは、構造化ストレージを使用してZIPファイルを展開します。

#include <windows.h>
#include <shlwapi.h>

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

BOOL Unzip(IStorage *pStorage, LPWSTR lpszFolderPath);
BOOL CreateExpandFile(IStream *pStream, LPWSTR lpszFilePath);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	HRESULT      hr;
	IStorage     *pStorage;
	IPersistFile *pPersistFile;
	CLSID        CLSID_StorageHandler = {0xe88dcce0, 0xb7b3, 0x11d1, {0xa9, 0xf0, 0x00, 0xaa, 0x00, 0x60, 0xfa, 0x31}};

	CoInitialize(NULL);

	hr = CoCreateInstance(CLSID_StorageHandler, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pStorage));
	if (FAILED(hr)) {
		CoUninitialize();
		return 0;
	}

	hr = pStorage->QueryInterface(IID_PPV_ARGS(&pPersistFile));
	if (FAILED(hr)) {
		pStorage->Release();
		CoUninitialize();
		return 0;
	}
	
	hr = pPersistFile->Load(TEXT("C:\\sample.zip"), STGM_READ);
	if (FAILED(hr)) {
		pPersistFile->Release();
		pStorage->Release();
		CoUninitialize();
		return 0;
	}

	if (Unzip(pStorage, L"C:\\sample"))
		MessageBox(NULL, TEXT("ZIPファイルを展開しました。"), TEXT("OK"), MB_OK);
	else
		MessageBox(NULL, TEXT("ZIPファイルの展開に失敗しました。"), NULL, MB_ICONWARNING);
	
	pPersistFile->Release();
	pStorage->Release();
	CoUninitialize();

	return 0;
}

BOOL Unzip(IStorage *pStorage, LPWSTR lpszFolderPath)
{
	HRESULT      hr;
	WCHAR        szNewPath[256];
	STATSTG      statstg;
	IEnumSTATSTG *pEnumSTATSTG;
	
	CreateDirectory(lpszFolderPath, NULL);

	hr = pStorage->EnumElements(NULL, NULL, NULL, &pEnumSTATSTG);
	if (FAILED(hr))
		return FALSE;

	while (pEnumSTATSTG->Next(1, &statstg, NULL) == S_OK) {
		PathCombine(szNewPath, lpszFolderPath, statstg.pwcsName);

		if (statstg.type == STGTY_STORAGE) {
			IStorage *pStorageNew;

			pStorage->OpenStorage(statstg.pwcsName, NULL, STGM_READ, 0, 0, &pStorageNew);
			Unzip(pStorageNew, szNewPath);
			pStorageNew->Release();
		}
		else if (statstg.type == STGTY_STREAM) {
			IStream *pStream;

			pStorage->OpenStream(statstg.pwcsName, NULL, STGM_READ, NULL, &pStream);
			CreateExpandFile(pStream, szNewPath);
			pStream->Release();
		}
		else
			;
	}

	pEnumSTATSTG->Release();

	return TRUE;
}

BOOL CreateExpandFile(IStream *pStream, LPWSTR lpszFilePath)
{
	ULARGE_INTEGER uiSize;
	ULONG          uRead;
	HANDLE         hFile;
	LPBYTE         lpData;
	DWORD          dwWriteByte;

	IStream_Size(pStream, &uiSize);

	lpData = (LPBYTE)CoTaskMemAlloc(uiSize.LowPart);	
	pStream->Read(lpData, uiSize.LowPart, &uRead);

	hFile = CreateFile(lpszFilePath, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
	WriteFile(hFile, lpData, uRead, &dwWriteByte, NULL);
	CloseHandle(hFile);
	
	CoTaskMemFree(lpData);

	return TRUE;
}

CoCreateInstanceに指定するCLSIDは、CompressedFolderのStorageHandlerのものでなければなりません。 これはレジストリから動的に取得することもできますが、 今回は簡単のためプログラム内にハードコードしています。 IStorageを取得した後にIPersistFileを取得しているのは、 StorageHandlerにZIPファイルをロードさせるためです。 このようなロードを行うメンバはIStorageに含まれていないため、 別途IPersistFileが必要になるわけです。 ロードが成功すれば自作関数のUnzipを呼び出して、ZIPファイルを展開することになります。 展開先のフォルダは第2引数に指定します。

Unzipの実装は次のようになっています。

BOOL Unzip(IStorage *pStorage, LPWSTR lpszFolderPath)
{
	HRESULT      hr;
	WCHAR        szNewPath[256];
	STATSTG      statstg;
	IEnumSTATSTG *pEnumSTATSTG;
	
	CreateDirectory(lpszFolderPath, NULL);

	hr = pStorage->EnumElements(NULL, NULL, NULL, &pEnumSTATSTG);
	if (FAILED(hr))
		return FALSE;

	while (pEnumSTATSTG->Next(1, &statstg, NULL) == S_OK) {
		PathCombine(szNewPath, lpszFolderPath, statstg.pwcsName);

		if (statstg.type == STGTY_STORAGE) {
			IStorage *pStorageNew;

			pStorage->OpenStorage(statstg.pwcsName, NULL, STGM_READ, 0, 0, &pStorageNew);
			Unzip(pStorageNew, szNewPath);
			pStorageNew->Release();
		}
		else if (statstg.type == STGTY_STREAM) {
			IStream *pStream;

			pStorage->OpenStream(statstg.pwcsName, NULL, STGM_READ, NULL, &pStream);
			CreateExpandFile(pStream, szNewPath);
			pStream->Release();
		}
		else
			;
	}

	pEnumSTATSTG->Release();

	return TRUE;
}

まず、CreateDirectoryで展開先となるフォルダを作成します。 次に、このフォルダに対して展開すべき要素(ストレージまたはストリーム)を列挙するために、 IStorage::EnumElementsを呼び出してIEnumSTATSTGを取得します。 IEnumSTATSTGを取得すればNextを呼び出すことによって、 要素についての情報を格納したSTATSTG構造体を取得することができます。 PathCombineは、1つの要素の展開先となるパスを作成するために呼び出しており、 これはlpszFolderPathとstatstg.pwcsName(要素の名前)を連結したものになります。 パスが完成したらそのパスにファイルかフォルダを作成することになりますが、 これは要素のタイプを見て判断しなければなりません。 STGTY_STORAGEの場合は要素がストレージということで、OpenStorageでそのストレージのIStorageを取得し、 これと新しいパスをUnzipに指定します。 この再帰呼び出しによって、全てのストレージとストリームを走査できるようになります。 タイプがSTGTY_STREAMの場合はストリームということで、OpenStreamでそのストリームを取得し、 CreateExpandFileで新しいファイルを作成することになります。

CreateExpandFileの実装は次のようになっています。

BOOL CreateExpandFile(IStream *pStream, LPWSTR lpszFilePath)
{
	ULARGE_INTEGER uiSize;
	ULONG          uRead;
	HANDLE         hFile;
	LPBYTE         lpData;
	DWORD          dwWriteByte;

	IStream_Size(pStream, &uiSize);

	lpData = (LPBYTE)CoTaskMemAlloc(uiSize.LowPart);	
	pStream->Read(lpData, uiSize.LowPart, &uRead);

	hFile = CreateFile(lpszFilePath, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
	WriteFile(hFile, lpData, uRead, &dwWriteByte, NULL);
	CloseHandle(hFile);
	
	CoTaskMemFree(lpData);

	return TRUE;
}

ZIPファイルの中に存在するファイルを展開するというのは、 そのファイルの中身を持った新しいファイルを作成するということです。 このファイルの中身というのはストリームに格納されているため、 まずはIStream_Sizeでストリームのサイズを取得し、 続いてIStream::Readでストリームの全データを取得します。 後はこれを新しく作成したファイルに書き込めば、 ZIPファイルの中のファイルが展開されたように見えることになります。

今回はIStorageを使用してZIPファイルを展開しましたが、 それではIStorageを使用してZIPファイルを作成することはできるのでしょうか。 結論からいうと、これは無理ではないかと思われます。 ZIPファイルを作成するには、構造化ストレージの中にストリームを作成しなければなりませんが、 そのためのメソッドであるIStorage::CreateStreamが失敗してしまうからです。 仮にこれが成功してもIPersistFile::SaveがE_NOTIMPLを返すため、 作成した構造化ストレージをZIPファイルとして保存する手段がありません。 IPersistFile::LoadでロードしたZIPファイルにファイルを追加する場合は、 IStorage::CreateStreamが成功することを確認していますが、 後続のIStream::Writeが失敗してストリームにファイルデータを書き込むことがでません。


戻る