Steamファイル パート2-BLOB、CDR、VDF、PAK、VPK

Steamロゴ



かなり遅れて、記事のサイクルの継続を公開します。

知り合いの場合:



この記事では、残りのファイル形式について説明します。



関連する情報が比較的少なく、アルゴリズムの例がほとんどないため、この記事は情報提供のみを目的としており、前述のリポジトリですべてを表示できます



BLOB(バイナリラージオブジェクト)



クライアントの以前のバージョンでは、SteamはClienRegistry.blobという単一のコピーで使用されていました。

明確なツリー構造を持ち、子が使い果たされるまで再帰的に読み取られます。 個別のヘッダーはありません-少なくとも1つの子孫を持つルートノードがすぐに移動します。 以下で指摘するように、 形式はやや非線形です。



ノードヘッダー


各ノードには、ノード自体のヘッダーとノードのデータヘッダーの2つのヘッダーがあります。

ノードヘッダーの形式:

struct TBLOBNodeHeader { UINT16 Magic; UINT32 Size; UINT32 SlackSize; };
      
      





マジック -ノードのタイプを記述するフィールド。 可能な値:



サイズ -ノードに保存されているデータの実際のサイズ(ヘッダーは含まれません)。

SlackSize-ファイル内のアライメント用に書き込まれたデータブロックのサイズ。



圧縮データヘッダー


ノードが圧縮されている場合、圧縮データのヘッダーはノードのヘッダーの後に続きます。

 struct TBLOBCompressedDataHeader { UINT32 UncompressedSize; UINT32 unknown1; UINT16 unknown2; };
      
      





UncompressedSize-メモリを割り当てる必要がある「生の」データのサイズ。

unknown1、unknown2-宛先不明、常に0x00000001、解析には影響しません。

前述のように、ZLibからuncompressを呼び出した後に受信したデータについては、ホストヘッダーを再読み取りする必要があります。



データ解析


ノードヘッダーを読み取り、必要に応じてその内容を展開すると、最も「楽しい」部分が入ります-ノードの内容を読み取ります。 アルゴリズムは可能な限り最適化されました。そのため、そのような期間の後にそれを把握することはそれほど容易ではありませんでした。

データの解析はTBLOBNodeHeader.Magicフィールドに依存します-0x5001の場合、すぐに子孫ノードを読み取ります。

それ以外の場合は、ヘッダーTBLOBDataHeaderを読み取ります

 struct TBLOBDataHeader { UINT16 NameLen; UINT32 DataLen; };
      
      





このヘッダーの後はノードの名前であり、その後にデータが続きます。

データでは、子孫ノードのヘッダーがすぐに読み取られ、ノードのタイプに応じて分岐が発生します。



データ解析
C ++

 void CBLOBNode::DeserializeFromMem(char *mem) { TBLOBNodeHeader *NodeHeader = (TBLOBNodeHeader*)mem; TBLOBDataHeader *DataHeader = (TBLOBDataHeader*)mem; char *data = NULL; if (NodeHeader->Magic == NODE_COMPRESSED_MAGIC) { mem += sizeof(TBLOBNodeHeader); TBLOBCompressedDataHeader *CompressedHeader = (TBLOBCompressedDataHeader*)mem; mem += sizeof(TBLOBCompressedDataHeader); UINT32 compSize = NodeHeader->Size, uncompSize = CompressedHeader->UncompressedSize; data = new char[uncompSize]; if (uncompress((Bytef*)data, (uLongf*)&uncompSize, (Bytef*)mem, compSize) != Z_OK) return; mem = data; NodeHeader = (TBLOBNodeHeader*)mem; DataHeader = (TBLOBDataHeader*)mem; } if (NodeHeader->Magic == NODE_MAGIC) { fIsData = false; fDataSize = NodeHeader->Size; fSlackSize = NodeHeader->SlackSize; fChildrensCount = GetChildrensCount(mem); fChildrens = new CBLOBNode*[fChildrensCount]; mem += sizeof(TBLOBNodeHeader); for (UINT i=0 ; i<fChildrensCount ; i++) { fChildrens[i] = new CBLOBNode(); fChildrens[i]->DeserializeFromMem(mem); NodeHeader = (TBLOBNodeHeader*)mem; DataHeader = (TBLOBDataHeader*)mem; if ((NodeHeader->Magic == NODE_MAGIC) || (NodeHeader->Magic == NODE_COMPRESSED_MAGIC)) mem += NodeHeader->Size + NodeHeader->SlackSize; else mem += sizeof(TBLOBDataHeader) + DataHeader->DataLen + DataHeader->NameLen; } } else { fIsData = true; fNameLen = DataHeader->NameLen; fDataSize = DataHeader->DataLen; mem += sizeof(TBLOBDataHeader); fName = new char[fNameLen+1]; memcpy(fName, mem, fNameLen); fName[fNameLen] = '\x00'; mem += fNameLen; UINT16 node; memcpy(&node, mem, 2); if ((node == NODE_MAGIC) || (node == NODE_COMPRESSED_MAGIC)) { DeserializeFromMem(mem); fData = NULL; } else { fData = new char[fDataSize]; memcpy(fData, mem, fDataSize); } } if (data != NULL) delete data; }
      
      





デルファイ

 procedure TBLOBNode.DeserializeFromMem(Mem: pByte); var NodeHeader: pBLOBNodeHeader; DataHeader: pBLOBDataHeader; CompressedHeader: TBLOBCompressedDataHeader; compSize, uncompSize: uint32; Data: Pointer; ChildrensCount, i: integer; //str: TStream; begin NodeHeader:=pBLOBNodeHeader(Mem); DataHeader:=pBLOBDataHeader(Mem); Data:=nil; if (NodeHeader^.Magic=NODE_COMPRESSED_MAGIC) then begin inc(Mem, sizeof(TBLOBNodeHeader)); Move(Mem^, CompressedHeader, sizeof(TBLOBCompressedDataHeader)); inc(Mem, sizeof(TBLOBCompressedDataHeader)); compSize:=NodeHeader^.Size-sizeof(TBLOBNodeHeader)-sizeof(TBLOBCompressedDataHeader); uncompSize:=CompressedHeader.UncompressedSize; GetMem(Data, uncompSize); uncompress(Data, uncompSize, Mem, compSize); Mem:=Data; NodeHeader:=pBLOBNodeHeader(Mem); DataHeader:=pBLOBDataHeader(Mem); { Str:=TStream.CreateWriteFileStream('.\dr.unc'); str.Write(Mem^, uncompSize); str.Free; } end; if (NodeHeader^.Magic=NODE_MAGIC) then begin fIsData:=false; fDataLen:=NodeHeader^.Size; fSlackLen:=NodeHeader^.StackSize; {if fSlackLen<>0 then Writeln(fSlackLen);} ChildrensCount:=GetChildrensCount(Mem); SetLength(fChildrens, ChildrensCount); inc(Mem, sizeof(TBLOBNodeHeader)); for i:=0 to ChildrensCount-1 do begin fChildrens[i]:=TBLOBNode.Create(); fChildrens[i].DeserializeFromMem(Mem); NodeHeader:=pBLOBNodeHeader(Mem); DataHeader:=pBLOBDataHeader(Mem); if (NodeHeader^.Magic=NODE_MAGIC) or (NodeHeader^.Magic=NODE_COMPRESSED_MAGIC) then inc(Mem, NodeHeader^.Size+NodeHeader^.StackSize) else inc(Mem, sizeof(TBLOBDataHeader)+DataHeader^.NameLen+DataHeader^.DataLen); end; end else begin fIsData:=true; fNameLen:=DataHeader^.NameLen; fDataLen:=DataHeader^.DataLen; inc(Mem, sizeof(TBLOBDataHeader)); SetLength(fName, fNameLen); Move(Mem^, fName[1], fNameLen); inc(Mem, fNameLen); {if (fDataLen=160) and (fName=AnsiString(#0#0#0#0)) and (puint16(Mem)^<>NODE_MAGIC) then writeln(''); } if (puint16(Mem)^=NODE_MAGIC) or (puint16(Mem)^=NODE_COMPRESSED_MAGIC) then begin DeserializeFromMem(Mem); fData:=nil; end else begin GetMem(fData, fDataLen); Move(Mem^, fData^, fDataLen); end; end; if Data<>nil then FreeMem(Data, uncompSize); end;
      
      









CDR(コンテンツ記述レコード)


これはblobコンテナに含まれ、ルートノードにいくつかの主要な子孫があり、その場所はハードコードされています(子孫についても同じです)。



多くの、非常に退屈で長いリスト、あなたは読むことさえできません。 一部のフィールドの目的はまだ不明です。
申請記録


フィールド(インデックスによるBLOBノードも):

  • 1-アプリケーションID。
  • 2-アプリケーションの名前。
  • 3-アプリケーションディレクトリ。
  • 4-キャッシュファイルの最小サイズ。
  • 5-キャッシュファイルの最大サイズ。
  • 6-起動パラメーターのリストが含まれています。
  • 7-アプリケーションアイコンのリストが含まれています。
  • 8-アプリケーションID。 最初の起動時に起動する必要があります。
  • 9-帯域幅貪欲のフラグ;
  • 10-アプリケーションのバージョンのリスト。
  • 11-アプリケーションの現在のバージョンのID。
  • 12-アプリケーションキャッシュファイルのリスト。
  • 13-テストバージョンの番号。
  • 14-名前と値のペアのリスト形式の追加フィールド。
  • 15-テストバージョンのパスワード。
  • 16-テストバージョンのID。
  • 17-ゲームの元のフォルダー。
  • 18-SkipMFPOverwriteフラグ。
  • 19-UseFilesystemDvrフラグ。


起動パラメーター:

  • 1-説明。
  • 2-コマンドラインオプション。
  • 3-アイコン番号。
  • 4-デスクトップにショートカットが存在しないことを示すフラグ。
  • 5-[スタート]メニューにショートカットがないことを示すフラグ。
  • 6-長時間実行無人のフラグ。


アプリケーションのバージョン:

  • 1-バージョンの説明。
  • 2-バージョン番号。
  • 3-このバージョンのアプリケーションが使用できないことを示すフラグ。
  • 4-このバージョンの起動パラメーターIDのリスト。
  • 5-コンテンツの復号化キー。
  • 6-復号化キーの存在を示すフラグ。
  • 7-IsRebasedフラグ。
  • 8-IsLongVersionRollのフラグ。


アプリケーションキャッシュファイル:

  • 1-キャッシュファイルID。
  • 2-マウントされたキャッシュファイルの名前。
  • 3-このキャッシュファイルのオプション性を担当するフラグ。


アプリケーションパッケージの説明


1-パケットID。

2-パッケージ名。

3-パッケージタイプ。

4-セント単位の価格。

5-数分の期間があります。

6-このパッケージのアプリケーションIDのリスト。

7-起動されたアプリケーションのID(WTF?);

8-フラグOnSubscribeRunLaunchOptionIndex;

9-リストRateLimitRecord;

10-割引のリスト。

11-予約注文フラグ。

12-バイヤーの住所の要件を示すフラグ。

13-国内価格(セント)。

14-国際価格(セント)。

15-必要なキーのタイプ。

16-このパッケージがサイバーカフェ専用であることを示すフラグ。

17-特定のゲームコード。

18-このコードの説明。

19-パッケージが利用できないことを示すフラグ。

20-ゲームでディスクの要件にフラグを立てます。

21-市外局番。 このゲームが利用可能です;

22-パッケージがバージョン3で使用可能であることを示すフラグ。

23-名前と値のペアのリストとしての追加フィールド。



VDF


クライアント設定はこの形式のファイルに保存され、現在のバージョンではアプリケーションに関する情報もあります。 バイナリファイルまたはテキストファイルのいずれかです。

BLOBと同様に、ツリー構造になっています。

バイナリファイルを検討します。 構造とヘッダーが異なるファイルにはいくつかの種類がありますが、ノードの形式は同じです。



各ノードは、ノードのタイプを説明するバイトで始まり、その後にノードの名前を含むNULLで終わる文字列が続きます。

ノードの種類:



ノードの子孫のリストを読み取る場合、タイプが8になるまでノードが読み取られます。



VDF形式のバイナリバージョンを使用するメインバイナリファイルを検討します。



appcache / appinfo.vdf


最初に、次の内容の見出しがあります。

 struct TVDFHeader { uint8_t version1; uint16_t type; uint8_t version2; uint32_t version3; };
      
      





version1およびversion2フィールドは、以前は署名の一部と見なされていましたが、時間が経つにつれて変更されました。以前は0x24と0x06でしたが、現在はそれぞれ0x26と0x07です。

タイプフィールドは署名であり、0x4456( 'DV')が含まれます。

version3フィールドには常に0x00000001が含まれます。



タイトルがアプリケーションに関する情報のリストになった後、各要素には独自のタイトルがあります。

 struct TVDFAppHeader { uint32_t AppID; uint32_t DataSize; };
      
      





ヘッダーの後には、リストの末尾のラベル(末尾の場合は0x00)の1バイトとVDFツリーの要素を含むノードパラメーターのリストがあります。



appcache / packageinfo.vdf


タイトルは前のものと似ていますが、最初の3つのフィールドのみが異なります。



ヘッダーの後には、アプリケーションパッケージを記述するノードのリストがあります。 リストの各要素の前には4バイトの数字があり、リストの最後に到達すると0xFFFFFFFFになります。



サンプルVDFテキストファイル



PAK



Half-Life 1の最初のバージョンで使用されていた古いアーカイブ形式。圧縮は行われず、単なるファイルのコンテナーです。

ファイルヘッダー:

 struct TPAKHeader { char Sign[4]; uint32_t DirectoryOffset; uint32_t DirectoryLength; };
      
      





署名-署名、「PACK」が含まれます。

DirectoryOffset-アイテムのリストの先頭のオフセット。

DirectoryLength-アイテムのリストのサイズ。



指定されたオフセットには、アーカイブに含まれる要素のヘッダーの配列があります。

 struct TPAKDirectoryItem { char ItemName[56]; uint32_t ItemOffset; uint32_t ItemLength; };
      
      





ここで説明することは何もないと思います。すべてが明確です。



VPK



ゲームファイルのアーカイブの形式。一連のファイルの形式で表示され、そのうちの1つにはファイルの場所の説明が含まれ、残りにはファイル自体が含まれます。 ルートファイルの名前は「<アーカイブ名> _dir.vpk」という形式で、残りは「<アーカイブ名> _ <アーカイブ番号> .vpk」です。

次のヘッダーから始まるルートファイルの構造を検討します。

 struct TVPKHeader { uint32_t Signature; uint32_t PaksCount; uint32_t DirSize; }
      
      





署名 -常に0x55aa1234が含まれます。

PaksCount-ファイルの内容を含むアーカイブの数。

DirSize-ファイルに関するメタ情報を含むデータサイズ。



ヘッダーの後は、要素を含む階層リストです。 さらに、リスト構造はファイル拡張子とそれらへのパスでソートされます。

つまり、最初にファイル拡張子を持つNULLで終わる行、次にそのようなファイルが存在するパスを持つNULLで終わる行、次にファイルに関する情報を持つファイル名(拡張子なし)を持つNULLで終わる行が続きます。 リストの各レベルの終わりは空の文字列です。

擬似構造の例、文字列部分のみ
大さじ

hl2 /マップ

map1

map2

map3



wav

音/ amb

amb1

amb2



音/声

声1

voice2



ファイル情報形式:

 struct TVPKDirectoryEntry { uint32_t CRC; uint16_t PreloadBytes; uint16_t ArchiveIndex; uint32_t EntryOffset uint32_t EntryLength uint16_t Dummy1; };
      
      





CRC-ファイルのチェックサム。

PreloadBytes-この構造の後のルートファイルに含まれるファイルの先頭のデータのサイズ。

ArchiveIndex-これらのファイルを含むアーカイブ番号。

EntryOffset-アーカイブ内のデータオフセット。

EntryLength-データサイズ。

おわりに



これは、私が自分で開いた、またはcs.rin.ruフォーラムの資料の助けを借りて開いたすべてのSteamファイル形式の説明です。 この記事を追加して初めて、私はそれを前の記事に安全に含めることができることに気付きました-ボリュームはそれほど増えず、小さなスタブがハングします...

次の記事では、すべてのサーバー(ルート、認証、コンテンツなど)でのSteamの動作について説明します。 すでに時代遅れのSteamNetwork2プロトコルが検討されます(HTTPSに基づく3番目のバージョンは現在動作しています)。



All Articles