Формат пакета ресурсов
Логический пакет состоит из полного Pack.fores и, при необходимости, изменяемого Pack.patch.fores. Патч содержит новые данные и полный актуальный каталог: каждая запись ссылается на данные в базе или патче, а удалённые пути просто отсутствуют. Цепочек патчей, файлов наборов ресурсов и частей пакетов нет. Resources.foindex — удаляемый кеш объединённого поиска по выбранным парам.
Синхронизация описана в updater, порядок подключения — в конфигурации и источниках данных, файловые примитивы — в Essentials.
Кодирование и идентичность
Все целые поля имеют явно заданную ширину и порядок little-endian. Поддерживается только версия 2.0; чтения старых версий нет. Смещения и длины имеют 64 бита. Смещения в пуле строк, длины путей и число записей имеют 32 бита; декодированный каталог не может превышать UINT32_MAX байт.
Пути уникальны, отсортированы по байтам UTF-8 и относительны. Пустые компоненты, ., .., обратная косая черта, двоеточие и NUL запрещены. Перед проверкой writer заменяет обратную косую черту на /. Разные записи могут ссылаться на один диапазон данных, в том числе при переименовании.
Все хеши — FNV-1a 64 с начальным значением 0xcbf29ce484222325 и множителем 0x100000001b3:
PackHashохватывает физические байты после заголовка полного пакета, включая кодированный каталог.FileContentHashвычисляется по декодированным байтам одного ресурса.ContentHashвычисляется поuint32 entry_countи для каждой отсортированной записи поuint32 path_byte_length,uint64 decoded_size,uint64 FileContentHashи байтам пути UTF-8.- Контрольные суммы заголовков, каталогов и footer охватывают диапазоны, указанные ниже.
ContentHash не зависит от кодека, источника и смещений: повторное сжатие меняет физическую, но не логическую идентичность. Патч привязан к физическому BasePackHash, потому что смещения базы должны соответствовать конкретному артефакту. Эти хеши выявляют повреждения и изменения, но не обеспечивают аутентификацию.
Полная база
[80-byte header][payload blobs][complete encoded catalog]
Несжатый заголовок:
| Смещение | Байты | Поле |
|---|---|---|
| 0 | 4 | Magic 0x53524F46 (FORS) |
| 4 | 2 | Major = 2 |
| 6 | 2 | Minor = 0 |
| 8 | 8 | PackHash, FNV по [80, EOF) |
| 16 | 8 | IndexOffset |
| 24 | 8 | IndexStoredSize |
| 32 | 8 | IndexDecodedSize |
| 40 | 4 | IndexCodec |
| 44 | 4 | EntryCount |
| 48 | 8 | DataOffset = 80 |
| 56 | 8 | DataSize |
| 64 | 8 | ContentHash |
| 72 | 8 | Контрольная сумма заголовка, FNV по [0, 72) |
IndexOffset = DataOffset + DataSize; каталог заканчивается точно на EOF. Отступов и резервной области нет. При упаковке последовательно записываются кодированные блоки с вычислением физического хеша, затем каталог, затем заголовок. Каноническая упаковка добавляет блоки в порядке сортировки путей; метки времени не сериализуются.
Записи каталога
Декодированный каталог содержит Entry[EntryCount] по 48 байт, за ними — пул строк UTF-8 без терминаторов.
| Смещение записи | Байты | Поле |
|---|---|---|
| 0 | 4 | Смещение пути в декодированном каталоге |
| 4 | 4 | Длина пути в байтах |
| 8 | 8 | Смещение кодированных данных в выбранном файле |
| 16 | 8 | Размер в хранилище |
| 24 | 8 | Размер после декодирования |
| 32 | 4 | Кодек: 0 = Stored, 1 = Deflate |
| 36 | 4 | Источник: 0 = Base, 1 = Patch; остальные значения запрещены |
| 40 | 8 | FileContentHash |
В полной базе допустим только Source 0; каталог патча может ссылаться на оба источника. Ссылки на базу ограничены её областью данных; ссылки на патч начинаются после его 32-байтного заголовка и заканчиваются перед выбранным каталогом. Обращаться к текущему каталогу, footer или незавершённому хвосту нельзя. Для Stored размеры до и после декодирования равны. При чтении проверяются точный размер и FileContentHash.
ResourcePackSource владеет записями и строками путей; ключи карты поиска ссылаются на эти строки. Буфер декодированного каталога после разбора освобождается. Перечисление файлов отсортировано по путям. Если выбран подтверждённый патч, все записи показывают время его изменения, даже если данные лежат в базе; иначе показывается время базы.
Дописываемый патч
[32-byte patch header]
[payloads 1][complete catalog 1][80-byte footer 1]
[payloads 2][complete catalog 2][80-byte footer 2]
...
Постоянный заголовок не переписывается при публикации обновления:
| Смещение | Байты | Поле |
|---|---|---|
| 0 | 4 | Magic 0x50524F46 (FORP) |
| 4 | 2 | Major = 2 |
| 6 | 2 | Minor = 0 |
| 8 | 8 | BasePackHash |
| 16 | 8 | Reserved = 0 |
| 24 | 8 | Контрольная сумма заголовка, FNV по [0, 24) |
Каждый подтверждённый append завершается несжатым footer:
| Смещение | Байты | Поле |
|---|---|---|
| 0 | 4 | Magic 0x54524F46 (FORT) |
| 4 | 2 | Major = 2 |
| 6 | 2 | Minor = 0 |
| 8 | 8 | BasePackHash |
| 16 | 8 | Текущий ContentHash |
| 24 | 8 | Смещение каталога |
| 32 | 8 | Размер каталога в хранилище |
| 40 | 8 | Декодированный размер каталога |
| 48 | 8 | Подтверждённый размер файла с этим footer |
| 56 | 4 | Кодек каталога |
| 60 | 4 | Число записей |
| 64 | 8 | FNV кодированного каталога |
| 72 | 8 | Контрольная сумма footer, FNV по [0, 72) |
Каталог непосредственно предшествует своему footer. Старые данные, каталоги и footer остаются неиспользуемыми, если на старые данные не ссылается текущий каталог. Применяется только последний полный каталог; прежние каталоги не накладываются друг на друга.
ResourcePatchWriter планирует смещения, повторно использует совпадающие (FileContentHash, DecodedSize) из базы или текущего патча и кодирует полный новый каталог до записи. Он проверяет каждый полученный ресурс, дописывает каталог, сбрасывает его, затем дописывает footer и снова сбрасывает. На POSIX сохраняется запись каталога. Изменение файла защищено блокировкой каталога ОС: Begin принимает уже удерживаемую вызывающим кодом блокировку, поэтому она может охватывать всю сессию. File writer отдельно исключает второго writer перед усечением или дописыванием.
Reader сохраняет свой каталог и границы файла во время append. Незавершённый хвост не удаляется без проверки: план размещает загрузки в фиксированном порядке от последнего подтверждённого конца, а GetResumedDownloads последовательно сохраняет блоки, которые декодируются в ожидаемую запись по ожидаемому смещению. Первый несовпадающий, оборванный или посторонний блок завершает этот префикс. Begin усекает файл после последнего сохранённого блока, AddEncodedFile продолжает следующую загрузку. Так же возобновляется патч без подтверждённого commit, если его заголовок привязан к выбранной базе; иначе файл пересоздаётся под блокировкой. Патч с другой базой updater удаляет как устаревший; writer также пересоздаёт его при следующем append. Патч нельзя усекать под reader другой версии базы: на POSIX старый reader удерживает удалённый inode, Windows отклоняет удаление/замену пока он открыт.
Восстановление и проверка
При обычном чтении проверяются заголовок, footer на EOF и его каталог. Если EOF недействителен, выполняется обратный поиск окнами по 64 KiB с перекрытием на размер footer. Одного magic недостаточно: проверяются версия, привязка к базе, абсолютная подтверждённая длина, контрольная сумма footer, диапазон и сумма каталога и вычисленный логический хеш. Побеждает последний корректный кандидат. Без commit доступна только полная база. Некорректный заголовок патча тоже не подтверждает ничего: после сбоя питания созданный файл может иметь длину, но ещё не иметь записанного заголовка. Клиент тогда должен загрузить базу, а следующее обновление пересоздаст патч. Begin сбрасывает новый заголовок до записи данных. Корректный заголовок другой базы исключается.
Все проверки диапазонов выполняют вычитание после проверки уменьшаемого; умножение размеров таблицы предварительно расширяется. Неизвестные кодеки и источники, неканонические/повторяющиеся пути и неправильные ссылки в пуле строк запрещены. Mount не хеширует все данные: повреждение обнаруживается при чтении, а полученные блоки патча проверяются до публикации. Полная загрузка проверяет физический хеш и каталог до установки. ResourcePairVerifier по запросу проверяет всю локальную пару ограниченными Step: базу по PackHash над [80, EOF), затем каждый подтверждённый диапазон патча с Source = 1 однократным декодированием и сравнением FileContentHash. Нечитаемый или неподключаемый заголовок базы означает повреждение; её патч тогда не проверяется, поскольку замена базы сбросит его. Updater выполняет проверку один раз на идентичность файла; см. описание updater.
Python-packager проверяет каждый декодированный блок входного архива: точные размеры, завершённость Deflate без лишних байтов и FileContentHash. Хеширование и декодирование потоковые и не выделяют память по объявленному декодированному размеру.
После прерванного append загрузка возобновляется с уже записанных блоков; отдельного журнала нет, а подтверждённые данные остаются пригодными для повторного использования. Формат сам по себе не ограничивает размер патча: мёртвые блоки и старые каталоги накапливаются, пока ограничение updater не заменит пару полным пакетом. Порядок ремонта/установки полной базы и лимит патча описаны в updater.
Кодеки
Кодек 0 хранит байты без изменений; кодек 1 — поток zlib Deflate. Упаковка берёт уровень из Baking.ResourcePackCompressLevel, а минимальный выигрыш из Baking.ResourcePackMinCompressGain (по умолчанию 5). Блок сжимается только если экономия превышает size * gain / 100 при целочисленном делении. Блоки меньше 64 байт не сжимаются. То же правило применяется к каталогам. Для каталогов и кешей, которые пишет сам клиент (patch, .foindex), действуют значения ResourcePackWriteSettings: уровень 6, выигрыш 5. Совпадающее декодированное содержимое может использовать старый диапазон даже при другом кодировании на сервере.
Объединённый кеш .foindex
Кеш содержит только ссылки. Он объединяет эффективные пары из настроенного суффикса после последней записи Embedded; ранние пакеты и Embedded остаются на своих местах. Патч не подключается как отдельный перекрывающий пакет.
При mount проверяются заголовки базы и идентичности подтверждённых патчей через удерживаемые handles. Замена файла между разрешением пути и открытием инвалидирует кеш вместо смешения старых смещений с новыми данными. Reader удерживает зафиксированный вид даже после append и POSIX replacement.
72-байтный заголовок содержит magic 0x58494F46 (FOIX), версию 2.0, PackListHash в позиции 8, смещение и размеры каталога в 16/24/32, кодек в 40, число записей в 44, число пакетов в 48 и контрольную сумму по [0,64) в 64. Декодированная часть состоит из 32-байтных записей пакетов, 56-байтных записей ресурсов и пула строк.
Запись пакета хранит смещение/длину имени (по 4 байта), PackHash базы (8), сумму каталога подтверждённого патча (8) и его подтверждённый размер (8). Два последних поля равны нулю без патча. PackListHash включает байты каждого имени и эти три 64-битных значения в порядке конфигурации.
Запись ресурса хранит смещение/длину пути, индекс пакета и кодек (четыре 4-байтных поля), смещение данных, размер в хранилище, декодированный размер и хеш содержимого (четыре 8-байтных поля), источник (4) и нулевой резерв (4).
При совпадении путей выигрывает последний настроенный пакет. Таблица на диске отсортирована по путям; runtime перечисляет победителей по убыванию приоритета пакета, затем по пути, как при прямом mount. Время записи берётся из эффективной пары. Актуальность кеша включает идентичности базы и commit патча: полное обновление не может оставить старые смещения только потому, что логическое содержимое не изменилось. Повреждённый кеш отбрасывается, подключаются авторитетные пары. Перестройка пишет временный кеш и заменяет прежний.
Платформы и API
Android помещает .fores в APK без внешнего ZIP-сжатия. Activity передаёт <installed APK>!/assets/<configured client resource directory> как Baking.ClientResources, а каталог private files как Common.UserWritablePath. OpenResourcePackFile находит Stored-запись APK ZIP и открывает ограниченный 64-битный позиционный диапазон fs::disk_read_file поверх APK. Записи с внешним сжатием или шифрованием запрещены. Разделитель !/ распознаёт архив только если перед ним существует файл; обычный каталог вроде Bob! не превращается в архив. Полные пакеты и дерево ресурсов не копируются в память и не распаковываются в app storage для mount. Заменённые базы и патчи хранятся в приватном каталоге Resources.
Web использует предварительно загруженную in-memory filesystem и может обновлять пары в рамках сеанса. Обновления не переживают перезагрузку страницы, объединённый кеш не создаётся.
Основной API находится в Source/Common/ResourcePack.h: ResourcePackWriter, ResourcePatchWriter, ResourcePackSource, ResourcePairVerifier, ReadResourcePackHeader, DecodeResourcePackIndex, ReadResourcePatchInfo и VerifyResourcePackFile. GetClientPackDirs, GetClientResourcePackPath и AddClientPackSource в FileSystem.h дают bootstrap, updater и runtime общий выбор базы/патча.
Test_ResourcePack.cpp закрепляет общие эталонные байты Python/C++, проверку данных, повторные append, восстановление после прерванного footer, возобновление append и проверку пары. Test_ResourceIndex.cpp проверяет чтение через кеш и границы APK-диапазона; Test_ClientServerIntegration.cpp — реальный цикл updater/backend.