Steam FIles. Часть 1 — GCF/NCF

Steam LogoКак и обещал в предыдущей статье, начинаю публиковать статьи о той части инфраструктуры Steam, которую смогло открыть Anti-Steam сообщество путём реверс-инжиниринга и продолжительных мозговых штурмов.Файлы формата GCF до недавнего времени являлись стандартом для всех игр, выпускаемых компанией VALVE, а NCF — для всех остальных. Сами по себе эти файлы представляют образ файловой системы с несколькими уровнями защиты. Отличие NCF от GCF заключается в том, что первые содержат только заголовки, а файлы, принадлежащие им, расположены в отдельном каталоге (<каталог Steam>/SteamApps/common/<имя игры>). Поэтому описывать буду GCF, а все особенности NCF приведу после.В данной статье я подробно разберу структуру данных файлов и работу с ними на примере своей библиотеки (ссылка на неё — в конце статьи). Начало будет достаточно скучным — описание структур и назначения их полей. Самое «вкусное» будет после них…Весь код, приведенный здесь, является плодом реверс-инжиниринга библиотек Steam. Большая часть информации о формате файлов была почерпнута из открытых источников, я же немного её дополнил и значительно оптимизировал работу с файлами кеша (даже по сравнению с самой популярной на то время библиотекой HLLIB).Общая структура файловФайл логически разбит на 2 части — заголовки и непосредственно содержимое. Содержимое разбито на блоки, которые в свою очередь разбиты на сектора по 8кБ, принадлежность которых к определённым файлам и их последовательность описаны в заголовках. Все заголовки содержат поля, являющиеся четырёхбайтными целыми числами (исключение — часть, отвечающая за список имён файлов и каталогов).Заголовки состоят из следующих структур: FileHeader BlockAllocationTableHeader BlockAllocationTable[] FileAllocationTableHeader FileAllocationTable[] ManifestHeader Manifest[] FileNames HashTableKeys[] HashTableIndices[] MinimumFootprints[] UserConfig[] ManifestMapHeader ManifestMap[] ChecksumDataContainer FileIdChecksumTableHeader FileIdChecksums[] Checksums[] ChecksumSignature LatestApplicationVersion DataHeader Первое же, что бросается в глаза — это ChecksumSignature, являющийся зашифрованным хешем части заголовков, отвечающей за контрольные суммы файлов.Все данные заголовки и назначение их полей будет рассмотрено далее.Для тех, кто читал не совсем внимательно, напомню, что все поля практически всех заголовков являются четырёхбайтными целыми числами (uint32_t в C++), если это не оговорено отдельно.FileHeader Исходя из названия, является заголовком всего файла и содержит следующие поля: HeaderVersion CacheType FormatVersion ApplicationID ApplicationVersion IsMounted Dummy0 FileSize ClusterSize ClusterCount Checksum HeaderVersion — всегда равно 0×00000001, указывая на версию данного заголовка.CacheType — равно 0×00000001 для GCF и 0×00000002 для NCF.FormatVersion — указывает на версию структуры остальных заголовков. Последняя версия — 6. Она и будет описана далее.ApplicationID — идентификатор файла (AppID).ApplicationVersion — версия содержимого файла. Служит для контролем за необходимостью обновления.IsMounted — содержит 0×00000001, если файл в данный момент примонтирован другим приложением. В настоящее время не используется, поэтому всегда равно 0×00000000.Dummy0 — выравнивающее поле, содержащее 0×00000000.FileSize — общий размер файла. Если превышает 4Гб, то данное поле содержит разницу <размер файла>-ffffffff, а сам размер файла вычисляется исходя изразмера блока данных и их количества.ClusterSize — размер блока данных в содержимом. Для GCF содержит 0×00002000, а для NCF — 0×00000000.ClusterCount — количество блоков данных в содержимом.Checksum — контрольная сумма заголовка. Вычисляется следующей функцией: UINT32 HeaderChecksum (UINT8 *lpData, int Size) { UINT32 Checksum = 0; for (int i=0; iNodeCount; i++) { ManifestNode *MN = &lpManifest[Item]; if (((MN→Attributes & 0×00004000) != 0) && (MN→ParentIndex == 0xFFFFFFFF) && (MN→NextIndex == 0xFFFFFFFF) && (MN→ChildIndex == 0xFFFFFFFF) && (MN→FileId == lpManifest[Item].FileId)) { res += MN→CountOrSize << 31; break; } } } return res; } Здесь я сделал небольшой «финт ушами», допустив, что файлы размером более 4Гб всё-таки не будут входить в состав кеша…Поиск элемента по имени например, нам надо найти файл с именем «hl2/maps/background_01.bsp». Все имена у нас хранятся в древовидном виде, поэтому путь придётся разбивать на элементы, связанные разделителем (в данном случае — "/"). Затем мы ищем у потомков корневого элемента элемент с именем «hl2». У него — элемента с именем «maps», и только затем — элемент с именем «background_01.bsp». Данный путь самый очевидный, но очень медленный — происходит побайтовой сравнение строк, да ещё и обход по дереву. Сплошные затраты.Для ускорения данной процедуры в заголовках есть хеш-таблицы.Поиск элемента по имени с использование хеша C++ UINT32 CGCFFile::GetItem(char *Item) { int DelimiterPos = -1; for (UINT32 i=0 ; iHashTableKeyCount, HashFileIdx = lpHashTableKeys[HashIdx]; if (HashFileIdx == CACHE_INVALID_ITEM) if (strcmp (LowerCase (Item), Item) != 0) { Hash = jenkinsLookupHash2((UINT8*)LowerCase (Item), strlen (FileName), 1); HashIdx = Hash % pManifestHeader→HashTableKeyCount; HashFileIdx = lpHashTableKeys[HashIdx]; } if (HashFileIdx == CACHE_INVALID_ITEM) return CACHE_INVALID_ITEM;

HashFileIdx -= pManifestHeader→HashTableKeyCount; while (true) { UINT32 Value = this→lpHashTableIndices[HashFileIdx]; UINT32 FileID = Value & 0×7FFFFFFF; if (strcmp (GetItemPath (FileID), Item) == 0) return FileID; if ((Value & 0×80000000) == 0×80000000) break; HashFileIdx++; }

return CACHE_INVALID_ITEM; } Delphi function TGCFFile.GetItemByPath (Path: string): integer; var end_block: boolean; Hash, HashIdx, HashValue: ulong; FileID, HashFileIdx: integer; PathEx: AnsiString; begin result:=-1; {$IFDEF UNICODE} PathEx:=Wide2Ansi (ExtractFileName (Path)); {$ELSE} PathEx:=ExtractFileName (Path); {$ENDIF} Hash:=jenkinsLookupHash2(@PathEx[1], Length (PathEx), 1); HashIdx:=Hash mod fManifestHeader.HashTableKeyCount; HashFileIdx:=lpHashTableKeys[HashIdx]; if HashFileIdx=-1 then begin if (LowerCase (Path)<>Path) then begin {$IFDEF UNICODE} Hash:=jenkinsLookupHash2(@LowerCaseAnsi (PathEx)[1], Length (PathEx), 1); {$ELSE} Hash:=jenkinsLookupHash2(@LowerCase (PathEx)[1], Length (PathEx), 1); {$ENDIF} HashIdx:=Hash mod fManifestHeader.HashTableKeyCount; HashFileIdx:=lpHashTableKeys[HashIdx]; if HashFileIdx=-1 then Exit; end; end; dec (HashFileIdx, fManifestHeader.HashTableKeyCount); repeat HashValue:=lpHashTableIndices[HashFileIdx]; FileID:=HashValue and $7FFFFFFF; end_block:= (HashValue and $80000000 = $80000000); if CompareStr (ItemPath[FileID], Path)=0 then begin result:=FileID; Exit; end; inc (HashFileIdx); until end_block;

if (result=-1) and (LowerCase (Path)<>Path) then result:=GetItemByPath (LowerCase (Path)); end; Как видно из кода, из всего пути к файлу мы берем только его имя и рассчитываем хеш для него. Берём остаток от целочисленного деления результата на значение ManifestHeader.HashTableKeyCount — это будет номер записи в списке HashTableKeys, содержащей либо 0xffffffff (если нет такого элемента) или значение X+ManifestHeader.HashTableKeyCount. Исходя из этого вычисляем X, являющийся номером элемента в списке HashTableIndices, с которого может находиться искомый элемент. Значения из этого списка указывают на искомый элемент, имя которого сравнивается в запросом. Если не совпало — берём следующий элемент списка и повторяем до тех пор, пока старший бит номера элемента равен »0».Понимаю, что получилось запутанно, но именно так оно и работает… Вините в подобной путанице программистов VALVE.Данный метод значительно лучше прямого поиска по дереву — сравнивалась производительность при запуске игры с самописной библиотекой-эмулятором Steam.dll, о которой ещё будет разговор.Получение полного пути к элементу Данное действие несколько обратно предыдущему — по номеру элемента надо пройтись по дереву до корневого элемента и получить путь к файлу.Получение пути к файлу C++ char *CGCFFile: GetItemPath (UINT32 Item) { size_t len = strlen (&lpNames[lpManifest[Item].NameOffset]); UINT32 Idx = lpManifest[Item].ParentIndex; while (Idx!= CACHE_INVALID_ITEM) { len += strlen (&lpNames[lpManifest[Idx].NameOffset]) + 1; Idx= lpManifest[Idx].ParentIndex; } len--;

char *res = new char[len+1]; memset (res, 0, len+1); size_t l = strlen (&lpNames[lpManifest[Item].NameOffset]); memcpy (&res[len-l], &lpNames[lpManifest[Item].NameOffset], l); len -= strlen (&lpNames[lpManifest[Item].NameOffset]); res[--len] = '\\'; Item = lpManifest[Item].ParentIndex; while ((Item!= CACHE_INVALID_ITEM) && (Item!= 0)) { l = strlen (&lpNames[lpManifest[Item].NameOffset]); memcpy (&res[len-l], &lpNames[lpManifest[Item].NameOffset], l); len -= strlen (&lpNames[lpManifest[Item].NameOffset]); res[--len] = '\\'; Item = lpManifest[Item].ParentIndex; } return res; } Delphi function TGCFFile.GetItemPath (Item: integer): string; var res: AnsiString; begin res:=pAnsiChar (@fNameTable[lpManifestNodes[Item].NameOffset+1]); Item:=lpManifestNodes[Item].ParentIndex; while (Item>-1) do begin res:=pAnsiChar (@fNameTable[lpManifestNodes[Item].NameOffset+1])+'\'+res; Item:=lpManifestNodes[Item].ParentIndex; end; Delete (res, 1, 1); {$IFDEF UNICODE} result:=Ansi2Wide (res); {$ELSE} result:=res; {$ENDIF} end; Код для Delphi значительно меньше из-за того, что для C++ я не использовал класс std: string — не знал про него тогда. С ним код вышел бы значительно короче…Потоки При написании библиотек для архиво-подобных форматов файлов (которые содержат в себе другие файлы) я использую принцип «поток-в-потоке», что позволяет открывать файлы в архиве, не распаковывая его. Например, в кеше half-life.gcf старых версий был файл pak0.pak, являющийся архивом. В итоге я открывал файл half-life.gcf, в нём — pak0.pak. в котором в свою очередь читал необходимые файлы. И всё это — без распаковки даже в память, весь функционал реализуется через написанные мною же обёртки над файловыми потоками (низкоуровневыми, на уровне WindowsAPI).Открытие файла в кеше C++ CStream *CGCFFile: OpenFile (char* FileName, UINT8 Mode) { UINT32 Item = GetItem (FileName); if (Item == CACHE_INVALID_ITEM) return NULL; if ((lpManifest[Item].Attributes & CACHE_FLAG_FILE) != CACHE_FLAG_FILE) return NULL; return OpenFile (Item, Mode); }

CStream *CGCFFile: OpenFile (UINT32 Item, UINT8 Mode) { StreamData *Data = new StreamData (); memset (Data, 0, sizeof (StreamData)); Data→Handle = (handle_t)Item; Data→Package = this; Data→Size = this→GetItemSize (Item).Size;

if (IsNCF) Data→FileStream = (CStream*)new CStream (MakeStr (CommonPath, GetItemPath (Item)), Mode==CACHE_OPEN_WRITE); else BuildClustersTable (Item, &Data→Sectors);

return new CStream (pStreamMethods, Data); } Delphi function TGCFFile.OpenFile (FileName: string; Access: byte): TStream; var Item: integer; begin result:=nil; Item:=ItemByPath[FileName]; if (Item=-1) then Exit; if ((lpManifestNodes[Item].Attributes and HL_GCF_FLAG_FILE<>HL_GCF_FLAG_FILE) or (ItemSize[Item].Size=0)) then Exit;

result:=OpenFile (Item, Access); end;

function TGCFFile.OpenFile (Item: integer; Access: byte): TStream; var res: TStream; begin res:=TStream.CreateStreamOnStream (@StreamMethods); res.Data.fHandle:=ulong (Item); res.Data.Package:=self; res.Data.fSize:=(res.Data.Package as TGCFFile).ItemSize[Item].Size; res.Data.fPosition:=0;

if (IsNCF) then begin CommonPath:=IncludeTrailingPathDelimiter (CommonPath); case Access of ACCES_READ: begin res.Data.FileStream:=TStream.CreateReadFileStream (CommonPath+ItemPath[Item]); res.Methods.fSetSiz:=StreamOnStream_SetSizeNULL; res.Methods.fWrite:=StreamOnStream_WriteNULL; end; ACCES_WRITE: begin ForceDirectories (ExtractFilePath (CommonPath+ItemPath[Item])); res.Data.FileStream:=TStream.CreateWriteFileStream (CommonPath+ItemPath[Item]); end; ACCES_READWRITE: res.Data.FileStream:=TStream.CreateReadWriteFileStream (CommonPath+ItemPath[Item]); end; res.Data.FileStream.Seek (0, spBegin); end else GCF_BuildClustersTable (Item, @res.Data.SectorsTable);

result:=res; end; Таким образом значительно упрощается работа с содержимым — можно открывать файлы и читать данные из них без лишних телодвижений.Извлечение файла с проверкой контрольной суммы В данной процедуре активно используются потоки, описанные выше — я просто читаю файл фрагментами фиксированного размера (максимальный размер фрагмента для контрольных сумм — 32Кб), рассчитываю для них контрольные суммы и сверяю их со значениями из таблицы в заголовках.Извлечение файла с проверкой его КС C++ UINT64 CGCFFile: ExtractFile (UINT32 Item, char *Dest, bool IsValidation) { CStream *fileIn = this→OpenFile (Item, CACHE_OPEN_READ), *fileOut; if (fileIn == NULL) return 0; if (! IsValidation) { if (DirectoryExists (Dest)) Dest = MakeStr (IncludeTrailingPathDelimiter (Dest), GetItemName (Item)); fileOut = new CStream (Dest, true); if (fileOut→GetHandle () == INVALID_HANDLE_VALUE) return 0; fileOut→SetSize (GetItemSize (Item).Size); }

UINT8 buf[CACHE_CHECKSUM_LENGTH]; UINT32 CheckSize = CACHE_CHECKSUM_LENGTH; UINT64 res = 0; while ((fileIn→Position ()GetSize ()) && (CheckSize == CACHE_CHECKSUM_LENGTH)) { if (Stop) break; UINT32 CheckIdx = lpFileIDChecksum[lpManifest[Item].FileId].FirstChecksumIndex + ((fileIn→Position () & 0xffffffffffff8000) >> 15); CheckSize = (UINT32)fileIn→Read (buf, CheckSize);

UINT32 CheckFile = Checksum (buf, CheckSize), CheckFS = lpChecksum[CheckIdx]; if (CheckFile!= CheckFS) { break; } else if (! IsValidation) { fileOut→Write (buf, CheckSize); }

res += CheckSize; } delete fileIn; if (! IsValidation) delete fileOut; return res; } Delphi function TGCFFile.ExtractFile (Item: integer; Dest: string; IsValidation: boolean = false): int64; var StreamF, StreamP: TStream; CheckSize, CheckFile, CheckFS, CheckIdx: uint32_t; buf: array of byte; Size: int64; begin result:=0; StreamP:=OpenFile (Item, ACCES_READ); if (StreamP=nil) then Exit;

Size:=ItemSize[Item].Size; if Assigned (OnProgress) then OnProgress (ItemPath[Item], 0, Size, Data); if Assigned (OnProgressObj) then OnProgressObj (ItemPath[Item], 0, Size, Data);

StreamF:=nil; if (not IsValidation) then begin if DirectoryExists (Dest) then Dest:=IncludeTrailingPathDelimiter (Dest)+ExtractFileName (ItemName[Item]); StreamF:=TStream.CreateWriteFileStream (Dest); StreamF.Size:=ItemSize[Item].Size; if StreamF.Handle=INVALID_HANDLE_VALUE then begin StreamF.Free; Exit; end; end;

SetLength (buf, HL_GCF_CHECKSUM_LENGTH); CheckSize:=HL_GCF_CHECKSUM_LENGTH; while ((StreamP.Position

CheckFile:=Checksum (@buf[0], CheckSize); CheckFS:=lpChecksumEntries[CheckIdx]; if (CheckFile<>CheckFS) and (not IgnoreCheckError) then begin if Assigned (OnError) then OnError (GetItemPath (Item), ERROR_CHECKSUM, Data); if Assigned (OnErrorObj) then OnErrorObj (GetItemPath (Item), ERROR_CHECKSUM, Data); break; end else if (not IsValidation) then StreamF.Write (buf[0], CheckSize); inc (result, CheckSize);

if Assigned (OnProgress) then OnProgress ('', result, Size, Data); if Assigned (OnProgressObj) then OnProgressObj ('', result, Size, Data); if Stop then break; end; SetLength (buf, 0); StreamP.Free; if (not IsValidation) then StreamF.Free; end; В коде для Delphi присутствует дополнительный код для отображения прогресса работы — вызов callback-функций OnProgress, OnProgressObj.Дешифрование содержимого файлов Поскольку многие игры незадолго до выхода можно загрузить заранее, то их содержимое в таких случаях оказывается полностью или частично зашифровано. С выходом игры становится доступен ключ для дешифровки данного контента, осуществляемая следующим кодом: Дешифрование файла C++ UCHAR IV[16] = {0}; void DecryptFileChunk (char *buf, UINT32 size, char *key) { AES_KEY aes_key; AES_set_decrypt_key ((UCHAR*)key, 128, &aes_key); AES_cbc_encrypt ((UCHAR*)buf, (UCHAR*)buf, size, &aes_key, IV, false); }

UINT64 CGCFFile: DecryptFile (UINT32 Item, char *key) { UINT64 res = 0; CStream *str = OpenFile (Item, CACHE_OPEN_READWRITE); if (str == NULL) return 0; char buf[CACHE_CHECKSUM_LENGTH], dec[CACHE_CHECKSUM_LENGTH]; UINT32 CheckSize = CACHE_CHECKSUM_LENGTH; INT32 CompSize, UncompSize, sz; while ((str→Position () < str->GetSize ()) && (CheckSize == CACHE_CHECKSUM_LENGTH)) { UINT32 CheckIdx = lpFileIDChecksum[lpManifest[Item].FileId].FirstChecksumIndex + ((str→Position () & 0xffffffffffff8000) >> 15); INT32 CheckSize = (INT32)str→Read (buf, 8);

memcpy (&CompSize, &buf[0], 4); memcpy (&UncompSize, &buf[4], 4); if (((UINT32)UncompSize > pManifestHeader→CompressionBlockSize) || (CompSize > UncompSize) || (UncompSize < -1) || (CompSize < -1)) { // Chunk is not compressed CheckSize = (UINT32)str->Read (&buf[8], CACHE_CHECKSUM_LENGTH-8); DecryptFileChunk (&buf[0], CheckSize, key); } else if (((UINT32)UncompSize <= pManifestHeader->CompressionBlockSize) && (CompSize <= UncompSize) && (UncompSize > -1) || (CompSize > -1)) { // Chunk is compressed CheckSize = (UINT32)str→Read (&buf[8], UncompSize-8); INT32 CheckFile = UncompSize; if (CompSize%16 == 0) sz = CompSize; else sz = CompSize + 16 — (CompSize%16); memcpy (dec, buf, sz); DecryptFileChunk (&dec[0], sz, key); uncompress ((Bytef*)&buf[0], (uLongf*)&CheckFile, (Bytef*)&dec[0], sz); } str→Seek (-CheckSize, USE_SEEK_CURRENT); str→Write (&buf[0], CheckSize);

UINT32 Check1 = Checksum ((UINT8*)&buf[0], CheckSize), Check2 = lpChecksum[CheckIdx]; if (Check1!= Check2) break; res += CheckSize; }

lpManifest[Item].Attributes = lpManifest[Item].Attributes & (! CACHE_FLAG_ENCRYPTED); return res; } Delphi const IV: array[0…15] of byte = (0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);

procedure DecryptFileChunk (buf: pByte; ChunkSize: integer; Key: Pointer); var AES: TCipher_Rijndael; src: array[0…HL_GCF_CHECKSUM_LENGTH-1] of byte; begin Move (buf^, src[0], HL_GCF_CHECKSUM_LENGTH); AES:=TCipher_Rijndael.Create (); AES.Init (Key^, 16, IV[0], 16); AES.Mode:=cmCFBx; AES.Decode (src[0], buf^, ChunkSize); AES.Free; end;

function TGCFFile.DecryptFile (Item: integer; Key: Pointer): int64; var StreamP: TStream; CheckSize, CheckFile, CheckFS, CheckIdx, sz: uint32_t; buf: array of byte; dec: array[0…HL_GCF_CHECKSUM_LENGTH] of byte; CompSize, UncompSize: integer; Size: int64; begin result:=0; StreamP:=OpenFile (Item, ACCES_READWRITE); if (StreamP=nil) then Exit;

Size:=ItemSize[Item].Size; if Assigned (OnProgress) then OnProgress (ItemName[Item], 0, Size, Data); if Assigned (OnProgressObj) then OnProgressObj (ItemName[Item], 0, Size, Data);

SetLength (buf, HL_GCF_CHECKSUM_LENGTH); CheckSize:=HL_GCF_CHECKSUM_LENGTH; while ((StreamP.Position

Move (buf[0], CompSize, 4); Move (buf[4], UncompSize, 4); if (ulong (UncompSize)>fManifestHeader.CompressionBlockSize) or (CompSize>UncompSize) or (UncompSize<-1) or (CompSize<-1) then begin //Chunk is not compressed! CheckSize:=StreamP.Read(buf[8], HL_GCF_CHECKSUM_LENGTH-8); DecryptFileChunk(@buf[0], CheckSize, Key); end else if ((ulong(UncompSize)<=fManifestHeader.CompressionBlockSize) and (CompSize<=UncompSize)) and ((UncompSize>-1) and (CompSize>-1)) then begin CheckSize:=StreamP.Read (buf[8], UncompSize-8); CheckFile:=UncompSize; //Chunk is compressed! if (CompSize mod 16=0) then sz:=CompSize else sz:=CompSize+16-(CompSize mod 16); Move (buf[8], dec[0], sz); DecryptFileChunk (@dec[0], sz, Key); uncompress (@buf[0], CheckFile, @dec[0], sz); end; StreamP.Seek (-CheckSize, spCurrent); StreamP.Write (buf[0], CheckSize);

CheckFile:=Checksum (@buf[0], CheckSize); CheckFS:=lpChecksumEntries[CheckIdx]; if (CheckFile<>CheckFS) and (not IgnoreCheckError) then begin if Assigned (OnError) then OnError (GetItemPath (Item), ERROR_CHECKSUM, Data); if Assigned (OnErrorObj) then OnErrorObj (GetItemPath (Item), ERROR_CHECKSUM, Data); break; end; inc (result, CheckSize);

//StreamP.Position:=StreamP.Position+CheckSize;

if Assigned (OnProgress) then OnProgress ('', result, Size, Data); if Assigned (OnProgressObj) then OnProgressObj ('', result, Size, Data); if Stop then break; end; lpManifestNodes[Item].Attributes:=lpManifestNodes[Item].Attributes and (not HL_GCF_FLAG_ENCRYPTED); fIsChangeHeader[HEADER_MANIFEST_NODES]:=true; SaveChanges (); SetLength (buf, 0); end; Расчет контрольной суммы для ManifestHeader Для расчёта данного значения используются следующие структуры заголовков: ManifestHeader Manifest[] FileNames HashTableKeys[] HashTableIndices[] MinimumFootprints[] UserConfig[] Перед расчётом КС обнуляются следующие поля: ManifestHeader.Fingerprint ManifestHeader.Checksum Сам расчёт сводится к последовательному вычислению хеша функцией Adler32 для всех указанных структур: Delphi

function ManifestChecksum (Header: pCache_ManifestHeader; entries, names, hashs, table, MFP, UCF: pByte): uint32_t; var tmp1, tmp2: uint32; begin tmp1:=Header.Fingerprint; tmp2:=Header.Checksum; Header.Fingerprint:=0; Header.Checksum:=0; result:=adler32(0, pAnsiChar (Header), sizeof (TCache_ManifestHeader)); result:=adler32(result, pAnsiChar (entries), sizeof (TCache_ManifestNode)*Header^.NodeCount); result:=adler32(result, pAnsiChar (names), Header^.NameSize); result:=adler32(result, pAnsiChar (hashs), sizeof (uint32)*Header^.HashTableKeyCount); result:=adler32(result, pAnsiChar (table), sizeof (uint32)*Header^.NodeCount); if Header^.NumOfMinimumFootprintFiles>0 then result:=adler32(result, pAnsiChar (MFP), sizeof (uint32)*Header^.NumOfMinimumFootprintFiles); if Header^.NumOfUserConfigFiles>0 then result:=adler32(result, pAnsiChar (UCF), sizeof (uint32)*Header^.NumOfUserConfigFiles); Header.Fingerprint:=tmp1; Header.Checksum:=tmp2; end; Заключение Остальные функции, не рассмотренные в данной статье ввиду громоздкости их описания (использование битовых карт занятых секторов при изменении карты секторов, перестроение данной карты и многое-многое другое) можно просмотреть в репозитории (там же лежат и остальные фрагменты программ, которые будут рассмотрены в последующих статьях). Данные исходные коды можно использовать в своих проектах (если кому-то нужны такие раритеты…).Примерная дата последнего обновления всех исходных кодов — вторая половина 2011-ого года.PS: Написание данной библиотеки мне очень помогло при написании лабораторной работы по предмету Операционные системы в университете — требовалось симулировать работу файловой системы (создание, запись, чтение и удаление файлов). Моя работа была первой и, наверное, единственной за всё время, в которой использовался именно образ файловой системы с разбиением на блоки и сектора —, а это была просто-напросто урезанная версия данной бибилотеки (без контрольных сумм). Даже дефрагментатор для кеша я дописал в составе данной работы…

© Habrahabr.ru