Алгоритм получения данных из ZIP файла:
- находим запись
EOCD
- загружаем записи
CentralDirectoryFileHeader
- для каждой
CentralDirectoryFileHeader
, находим и загружаемLocalFileHeader
- данные располагаются сразу после
LocalFileHeader
, иногда размер данных записывается после самих данных в структуруDataDescriptor
, об этом сигнализирует флагgeneralPurposeBitFlag
Теперь каждый шаг подробнее.
Находим запись EOCD
Эта запись располагается в конце файла. Ее поиск производится от конца файла к началу по сигнатуре. Стартовое место поиска равно размеру файла минус размер структуры EOCD. Если сигнатура не найдена, то смещаемся к началу файла по одному байту за раз.
Загружаем записи Central directory file header
Смещение для поиска первой записи Central directory file header находится в EOCD
, который мы загрузили на предыдущем шаге. Записи располагаются последовательно одна за другой. Загрузка происходит следующим образом: считываем поля, размер которых известен, затем загружаем остальные поля (filename
, extraField
, fileComment
).
Загружаем записи Local File Header
Для каждой записи типа CentralDirectoryFileHeader
существует запись типа Local File Header. Ее расположение в файле описывается полем CentralDirectoryFileHeader.localFileHeaderOffset
. Переходим по смещению, считываем поля известной длины, затем остальные поля (filename
, extraField
).
Загружаем данные
Для этого шага нам понадобится библиотека zlib, потому что данные сжимаются методом deflate. Как правило, сразу после LocalFileHeader
идут данные размером compressedSize
. Если данные сжаты, то флаг compressionMethod
равен 8 (или Z_DEFLATED
), иначе он нулевой (или Z_STORED
). Сжатые данные необходимо считать во временный буфер размером compressedSize
и распаковать с помощью библиотеки zlib в буфер размером uncompressedSize
.
Общие комментарии по коду
- странная конструкция
__attribute__((packed))
всего лишь означает команду для GCC не использовать выравнивание внутри структуры, для Visual Studio читайте про #pragma push(pack/pop) - обратите внимание, что поток типа
z_stream
инициализируется с помощью функцииinflateInit2()
. Это сделано потому, что стандартная функцияinflateInit()
ожидает в начале потока данных специальный заголовок, который отсутствует в ZIP файле - is — это объект типа
std::istream
(напримерstd::ifstream
) - не стоит выделять буфер для чтения каждый раз, лучше вычислить его заранее и выделить один раз в начале программы
- проверьте метод сжатия перед чтением данных, тогда при отсутствии сжатия, можно читать данные сразу в нужный буфер
- если размер файла внутри ZIP архива неизвестен и может быть очень большим, то распаковывайте его частями
- структуру типа
z_stream
можно инициализировать и удалять только один раз, а перед работой с ней вызвать функциюinflateReset2()
- в коде совсем не учитывается порядок байтов, так что будьте внимательны при переносе на другую платформу
P.S. Есть способ чтения быстрее и короче: с начала файла считываем все структуры типа LocalFileHeader
, проверяя сигнатуру, если сигнатуры неправильная, значит все файлы считаны.
Отличная статья
Спасибо!
«Эта запись располагается в конце файла. Ее поиск производится от конца файла к началу по сигнатуре. Стартовое место поиска равно размеру файла минус размер структуры EOCD. Если сигнатура не найдена, то смещаемся к началу файла по одному байту за раз.»(с)
Опять ниче не понятно. Ищем начало EOCD по сигнатуре 0x06054b50 (uint32_t signature). Далее вы пишите:
«Если сигнатура не найдена, то смещаемся к началу файла по одному байту за раз.»(с)
Это блин как ?? Если обязательная сигнатура не найдена, то файл или битый или вообще не zip. Какой смысл смещаться к началу файла ???
Да, ZIP файлы бывают битыми без структуры EOCD. Битые файлы могут содержать только наборы LocalFileHeader с данными. Такое бывает и надо быть к этому готовым, на всякий случай. :)
«странная конструкция __attribute__((packed)) всего лишь означает команду для GCC не использовать выравнивание внутри структуры»(с)
А зачем ? Почему бы компиляторы не выровнять структуру ? На что это в данном случае влияет ?
Если убрать выравнивание, тогда метод is.read() не сможет прочитать структуру за один вызов (в зависимости от настроек компилятора) и придётся читать её побайтно.
Про выравнивание понял, чтобы не мучится с определением размера структуры и начальной точкой поиска.
Чтоб не писать кучу функций типа readUINT32 для простого примера.
А может быть можете пожалуйста еще подсказать: я создал обычный zip архив — поместил туда несколько текстовых файлов и папку.
Просто считал первые 38 байт(то есть это данные структуры Local File Header) этого ZIP файла методом read в простой буффер char`ов.
Далее смотрю, какие байты считались c 29 по 32 байты (это диапазон параметра uint8_t *filename). И там в этих 4ех байтах такие значения(десятичные): 0 0 97 50 — эта комбинация байт, если ее привезти в int покажет десятичное целое — 845 217 792.
Я не могу понять, о чем должна говорить эта цифра ?? Куда она должна указывать ?
LocalFileHeader нельзя считать за один раз. Сначала надо считать те данные, длина которых известна, а это ровно 30 байт (по моим подсчётам — без filename и extraField). У меня в примере приведена эта структура для чтения.
Сначала читаете 30 байт, потом выделяете память под название файла размером fileNameLength, затем считываете fileNameLength в эту переменную.
Псевдокод будет примерно таким (язык Си):
И ещё я обратите внимание, что для папок тоже создаётся LocalFileHeader.
Спасибо!
Но, я как раз в этом и запутался, где увидеть информацию о кол-ве байт под filename ?
Или я запутался вот в этом —> uint8_t *filename; В данном случае это переменная типа указатель или звездочка здесь для красоты? Если эта указатель, то он будет заниматься минимум 4 байта(на 32 битной системе) и 8 байт(на 64битной) или же это просто переменная uint8_t — занимающая 1 байт ?
И если это просто переменная занимающая 1 байт, то тогда, как я понял — там и находится размер имени файла в байтах (максимум 255 байт) и самое имя следует, как раз за этим uint8_t байтом.
Есть структура, которая физически лежит в файле на диске и есть структура, где вы будете хранить считанные данные в памяти. Разумеется «uint8_t *» не может лежать на диске, потому что это указатель. Количество байт под filename хранится в поле fileNameLength. Фактически, структура LocalFileHeader имеет переменную длину.
То есть я считал бинарно данные моего zip-файла в массив buff.
Если вывести 30 элемент массива buff и привести его к int: (/Не знаю как код оформить на Вашем форуме)
29 и 30 байт — это поле extraFieldLength структуры LocalFileHeader. Оно определяет длину дополнительных данных (extraField), которые идут сразу после filename. Про это поле я немного писал в статье про Расширение ZIP64. Не имею ни малейшего понятия — какая информация там может быть в вашем случае.
P.S. Поправил форматирование в вашем комментарии.
Спасибо! Разобрался.
Но тогда у Вас небольшая неточность в описании: «Описание формата ZIP файла» — https://blog2k.ru/archives/3391
Что и вызвало путаницу :)
У Вас в описании Local File Header, два последних параметра этой структуры некорректно записаны:
struct LocalFileHeader
{
…
uint8_t *filename; // Название файла (размером filenameLength)
uint8_t *extraField; // Дополнительные данные (размером extraFieldLength)
};
-То есть звездочка реально путает, как будто бы это переменная типа указатель на uint8_t, что естественно не так.
-И в соответствии с описанием на сайте: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT —-> filename и extraField — занимают два байта, а у Вас как бы однобайтовый int получается.
Скорее всего вы путаете эти указатели с полями fileNameLength и extraFieldLength, которые действительно занимают каждый по два байта, что означает, что максимальная длина имени файла не может превышать 65536 байт.
Приветствую,
Честно говоря так и не смог вкурить, зачем нужен заголовок центрального каталога:
-По сути поля Центрального каталога на 90% дублируются Заголовком Локального каталога. Зачем тогда лишнее место занимать?
-Почему бы просто не указать в Центрального каталога — те поля, которые не совпадают с Локальным ? Или даже просто сделать один объеденный каталог, где будут все поля и Локального и Центрального ?
Случаем не знаете в чем сакральный смысл таких ZIP заголовков ?
Могу лишь предположить, что такая последовательная структура была предназначена для чтения/записи с ленточных накопителей. А запись в конце файла для жёстких дисков.