IStreamを介してcabアーカイブを操作する

しばらく前、私はメモリ内のデータを直接圧縮する必要があり、これには外部のものを使用しませんでした-つまり システムに組み込まれている機能を使用します。 選択は、データ圧縮のツールとしてのCabinet.dllと、メモリ内のデータを操作するためのIStreamインターフェースにありました。 インターネットでこのようなものを見つけられなかったため、ベストプラクティスを共有することにしました。

エントリー


ライブラリを持ち歩くか、プロジェクトにソースコードを含める必要があるため、サードパーティのソリューションを使用したくありませんでした。 Windowsは、データを圧縮/解凍するためのツールの大きなセットを提供していません: Cabinet.dllZipFldr.dll (圧縮されたZipフォルダー)およびRtlCompressBuffer / RtlDecompressBuffer 。 圧縮されたZipフォルダーに関する明確なドキュメントは見つかりませんでした。Windows7のバージョンのRtlCompressBuffer / RtlDecompressBufferはLZ圧縮のみをサポートしていますが、 Cabinet.dllは Windows 95から現在までのシステムに存在します。

ファイルとメモリを操作するための関数として、ドキュメントでは、標準Cライブラリの関数またはCreateFile / CloseHandle / ReadFile / WriteFileなどのWindows API関数の使用を推奨しています。 ファイルに対するすべての操作はメモリ内で実行されたため、これらの目的でIStreamを使用することが決定されました。

Cabinet.dllについて少し


ライブラリは、機能的にFCI(ファイル圧縮インターフェイス)とFDI(ファイル解凍インターフェイス)の2つの部分に分かれています。 あなたはここでそれについて読むことができます 。 両方のインターフェイスは、ファイルとメモリを操作するために本質的に同じ機能を使用しますが、何らかの理由でMicrosoftはFCIとFDIの異なるプロトタイプを作成することにしました。 ただし、他の人を通して説明することを妨げるものは何もありません。 これを行う方法については、以下を参照してください。

ライブラリを使用するには、 FCI.hおよび/またはFDI.hファイルをそれぞれ接続し、リンカがCabinet.libを指すようにする必要があります 。 これらのファイルはすべて、Windows SDKに含まれています。

圧縮インターフェースの実装


圧縮を実装する最も単純なコードは次のようになります。

/*  : IStream* pIStreamFile —    ,      char* szFileName —    .   ,        */ ERF erf; CCAB ccab = {MAXINT, MAXINT}; *(IStream**)ccab.szCabPath = SHCreateMemStream(0, 0); //    HFCI hFCI = FCICreate(&erf, fPlaced, fAlloc, fFree, fOpen, fRead, fWrite, fClose, fSeek, fDelete, fTemp, &ccab, 0); if(hFCI){ FCIAddFile(hFCI, (PSZ)pIStreamFile, szFileName, 0, fGetNext, fStatus, fInfo, tcompTYPE_MSZIP); FCIFlushFolder(hFCI, fGetNext, fStatus); FCIFlushCabinet(hFCI, 0, fGetNext, fStatus); FCIDestroy(hFCI); } /*  : (IStream*)ccab.szCabPath — ,  cab-.     Release()   ! */ 

つまり コード自体は非常に単純です。 すべてのソルトは、FCIコンテキストが作成されたときに渡される関数に含まれています。 これらのパラメーターと戻り値についてはここで読むことができるので、基本的な情報のみを以下に示します。 以下は、各機能の分析です。

この点で、ファイル記述子が非標準になることをここに追加する必要があります。これらはIStreamへのポインタです。 この機能のため、この「記述子」の転送には注意する必要があります。 たとえば、 CCAB構造szCabPathszCabの 2つのフィールドがあり 、2番目のパラメーターにアドレスを渡すことは論理的に思えますが、いいえ。 FCIは文字列の連結を実行します(または、文字列を連結すると考えますが、何かを知っています...)。そのため、結果として、ファイルの「名前」はszCabPathになり、記述子にもなります。

置きました


新しいファイルがアーカイブに追加されるたびに呼び出されます。

 FNFCIFILEPLACED(fPlaced){ return 0; } 

Return -1はエラーを意味し、他の値はアプリケーションによって決定されます。 たとえば、ファイルの追加を示すために使用できます。

fGetNext


新しいアーカイブボリュームを作成する前に呼び出されます。

 FNFCIGETNEXTCABINET(fGetNext){ return 1; } 

成功した場合はTRUEを返し、そうでない場合はFALSEを返します 。 特筆すべきことはありません。

fStatus


ファイル処理のいくつかの段階で呼び出されます:ブロック圧縮、圧縮ブロックの追加、アーカイブの記録。

 FNFCISTATUS(fStatus){ return typeStatus == statusCabinet ? cb2 : 0; } 

エラーの場合は-1を返す必要があり、そうでない場合はすべての値( typeStatus == statusCabinetを除く-アーカイブのサイズを返す必要があり、これはcb2パラメーターを介して渡されます)。

fInfo


ファイル属性を設定します。

 FNFCIGETOPENINFO(fInfo){ *pattribs = 0; return (INT_PTR)pszName; } 

IStreamは日付属性、および実際にはファイル属性をサポートしていないため、pattribsの値を0に設定する必要があります。そうしないと、奇妙な属性を持つアーカイブ内のファイルを取得するリスクが生じます(または、アーカイブをまったく取得できない場合があります)。

-1の戻り値はエラーを意味し、そうでない場合は、開いているファイルのハンドルを返す必要があります。

fTemp


一時ファイルを作成します。

 FNFCIGETTEMPFILE(fTemp){ *(IStream**)pszTempName = SHCreateMemStream(0, 0); return 1; } 

成功した場合はTRUE 、そうでない場合はFALSEを返します 。 ファイル名(この場合はIStreamへのポインター)は、 pszTempNameパラメーターを介して渡されます。

fDelete


ファイルを削除します。

 FNFCIDELETE(fDelete){ (*(IStream**)pszFile)->Release(); return 0; } 

成功すると0、失敗すると-1を返します。 この場合のファイルの削除は、ストリームによって占有されているリソースの解放であるため、 Release()も同様です。

fAlloc、fFree


メモリの割り当て/解放。

 FNFCIALLOC(fAlloc){ return new char[cb]; } FNFCIFREE(fFree){ delete memory; } 

ここではすべてが非常に簡単なので、これらの機能を1つのセクションにまとめました。

fOpen


ファイル(ストリーム)を開きます。

 FNFCIOPEN(fOpen){ return *(INT_PTR*)pszFile; } 

なぜなら この場合のファイル名はこのファイルの記述子と同等です。そのため、名前を記述子として返します(まあ、何らかのエラーが発生した場合は-1)。

f閉じる


ファイル記述子を閉じます。

 FNFCICLOSE(fClose){ LARGE_INTEGER li = {}; ((IStream*)hf)->Seek(li, 0, 0); return 0; } 

成功すると0、失敗すると-1を返します。 なぜリリースしないのですか? 「ファイルを削除する」ため、つまり ストリームを破棄しますが、閉じる必要があるだけです。 したがって、ポインタを先頭にリセットするだけです。

fRead、fWrite


ファイルからのデータの読み取り/書き込み。

 FNFCIREAD(fRead){ ULONG ul; HRESULT hr = ((IStream*)hf)->Read(memory, cb, &ul); return (hr && hr != S_FALSE) ? -1 : ul; } FNFCIWRITE(fWrite){ ULONG ul; HRESULT hr = ((IStream*)hf)->Write(memory, cb, &ul); return (hr && hr != S_FALSE) ? -1 : ul; } 

読み書きされたバイト数を返します。エラーの場合は-1を返します(0-ファイルの終わりに達しました)。

fSeek


ファイル内のポインターの位置。

 FNFCISEEK(fSeek){ LARGE_INTEGER liDist = {dist}; HRESULT hr =((IStream*)hf)->Seek(liDist, seektype, (ULARGE_INTEGER*)&liDist); return hr ? -1 : liDist.LowPart; } 

エラーの場合-1を返します。それ以外の場合-新しいポインター位置。

展開インターフェイスの実装


解凍コードは次のとおりです。

 /*  : IStream* pIStrCab —    */ ERF erf; HFDI hFDI = FDICreate(fAlloc, fFree, fnOpen, fnRead, fnWrite, fnClose, fnSeek, cpuUNKNOWN, &erf); if(hFDI){ IStream *pIStrSrc = SHCreateMemStream(0, 0); if(FDICopy(hFDI, (PSZ)&pIStrCab, (PSZ)&pIStrCab, 0, fnNotify, 0, &pIStrSrc)){ //    pIStrSrc } pIStrSrc->Release(); FDIDestroy(hFDI); } pIStrCab->Release(); /*  : IStream* pIStrSrc —     */ 

ここではそれほど簡単ではありません。 実際、アーカイブからのすべてのファイルの抽出は、すべての魔法が発生するfnNotifyをその作業の過程で呼び出す唯一のFDICopy関数によって開始されます。 しかし、それについては後で。

一般に、プロセスは似ています:FDIコンテキスト、出力用のストリームを作成し、アーカイブからファイルをこのストリームに抽出し(私の例では、単一のファイルを抽出する必要がありました)、コンテキストを破棄します。 (PSZ)&pIStrCabは2回指定する必要があります。その過程で、関数は両方のパラメーターを連結し、一方を省略した場合はエラーが発生するためです(はい、このようなレーキにも遭遇しました)。

次に、機能について少し説明します。 一般に、2つのパラメーターがないことを除いて、FCI関数と似ています。 メモリ割り当て/割り当て解除関数は一般的に同一なので、それらを再記述しても意味がありません。 コードの量を減らすために、余分なヌルパラメーターを指定しないように、FDI関数を使用してFCI関数を書き換えることができます。

fnOpen、fnClose


ファイル(ストリーム)を開く/閉じる。

 FNOPEN(fnOpen){ return *(INT_PTR*)pszFile; } FNCLOSE(fnClose){ return fClose(hf, 0, 0); } 

fnOpenはfOpenを呼び出すよりも簡単に複製でき、 fnCloseでは、この実装では使用されないため、最後の2つのゼロパラメーターでFCI関数fCloseが呼び出されます。

fnRead、fnWrite、fnSeek


データとポインターの位置の読み取り/書き込み。

 FNREAD(fnRead){ return fRead(hf, pv, cb, 0, 0); } FNWRITE(fnWrite){ return fWrite(hf, pv, cb, 0, 0); } FNSEEK(fnSeek){ return fSeek(hf, dist, seektype, 0, 0); } 

返される値は、FCIの値に似ています。

fnNotify


最も重要な機能。

 FNFDINOTIFY(fnNotify){ if(fdint == fdintCOPY_FILE) if(!lstrcmp(pfdin->psz1, "Data")) //   ,    return (INT_PTR)*(int*)pfdin->pv; return fdint == fdintCLOSE_FILE_INFO; } 

関数に関するすべての情報はここで読むことができます 。 ここで、いくつかの説明が必要です。
ほとんどの場合、関数は成功のインジケータとして0を返します( fdintCLOSE_FILE_INFOを除き、 TRUEを返す必要があります)。 fdint == fdintCOPY_FILEの場合、動作は次のとおりです。0はファイルをスキップすることを意味し、-1はエラー( FDICopyを終了する )を意味し、別の値はデータを抽出するストリームへのハンドルです。

これで楽しい部分が始まります。この関数でスレッドを作成すると、外部でそれらにアクセスできなくなるからです。 したがって、少なくとも2つのソリューションがあり、それらの両方がこれまで使用されていなかったため、FDICopy関数の最後のパラメーターpvUserが目立たないように影響します。 それを介して、ユーザーデータを送信することができ、 pfdin-> pvに戻るのは彼です。 最初の方法は、アーカイブから抽出するファイル名の固定リストがある場合、必要なファイル名とその中に抽出するためのIStreamへのポインターを含む構造の配列として転送できます。 2番目の方法は、ファイルの数が不明で、すべてを抽出する必要がある場合です。 この場合、 pvUserを介して、抽出されたファイルの名前と記述子が格納されるコンテナアドレス(たとえば、 std :: vector )を渡すことができます。

あとがき


この方法は、結果のデータサイズがそれほど大きくない場合(約100メガバイト)に適しています。 もちろん、8 GB以上のメモリではこれはそれほど大きな費用ではありませんが、メモリの再割り当ての操作は最速の操作ではないことを忘れないでください。これはメモリの断片化にもつながります。メモリブロックはありません。

別の方法として、 構造化ストレージ (同じIStream )またはSHCreateStreamOnFile / SHCreateStreamOnFileExを使用して作成されたファイルストリームを使用できます。 したがって、メモリ内のI / O操作をファイル内の同様の操作と組み合わせることができます。 IStreamインターフェイスは、追加の操作なしで両方の場合に使用できます。

実装について質問がある場合は、コメントで回答する準備ができています。

Source: https://habr.com/ru/post/J314832/


All Articles