Алгоритм получения данных из ZIP файла:
- находим запись
EOCD
- загружаем записи
CentralDirectoryFileHeader
- для каждой
CentralDirectoryFileHeader
, находим и загружаемLocalFileHeader
- данные располагаются сразу после
LocalFileHeader
, иногда размер данных записывается после самих данных в структуруDataDescriptor
, об этом сигнализирует флагgeneralPurposeBitFlag
Теперь каждый шаг подробнее.
Находим запись EOCD
Эта запись располагается в конце файла. Ее поиск производится от конца файла к началу по сигнатуре. Стартовое место поиска равно размеру файла минус размер структуры EOCD. Если сигнатура не найдена, то смещаемся к началу файла по одному байту за раз.
struct EOCD { uint16_t diskNumber; uint16_t startDiskNumber; uint16_t numberCentralDirectoryRecord; uint16_t totalCentralDirectoryRecord; uint32_t sizeOfCentralDirectory; uint32_t centralDirectoryOffset; uint16_t commentLength; } __attribute__((packed)); // Вычисляем размер файла is.seekg(0, is.end); const size_t fileSize = is.tellg(); // Ищем сигнатуру EOCD for (size_t offset = fileSize - sizeof(EOCD); offset != 0; --offset) { uint32_t signature = 0; // Считываем четыре байта сигнатуры is.seekg(offset, is.beg); is.read((char *) &signature, sizeof(signature)); if (0x06054b50 == signature) { // Есть контакт! break; } } EOCD eocd; is.read((char *) &eocd, sizeof(eocd)); // Считываем комментарий, если есть if (eocd.commentLength) { // Длина комментария uint8_t *comment = new uint8_t[eocd.commentLength + 1]; is.read((char *) comment, eocd.commentLength); // Выведем комментарий в консоль comment[eocd.commentLength] = 0; std::cout << comment; delete [] comment; } |
Загружаем записи Central directory file header
Смещение для поиска первой записи Central directory file header находится в EOCD
, который мы загрузили на предыдущем шаге. Записи располагаются последовательно одна за другой. Загрузка происходит следующим образом: считываем поля, размер которых известен, затем загружаем остальные поля (filename
, extraField
, fileComment
).
struct CentralDirectoryFileHeader { uint32_t signature; uint16_t versionMadeBy; uint16_t versionToExtract; uint16_t generalPurposeBitFlag; uint16_t compressionMethod; uint16_t modificationTime; uint16_t modificationDate; uint32_t crc32; uint32_t compressedSize; uint32_t uncompressedSize; uint16_t filenameLength; uint16_t extraFieldLength; uint16_t fileCommentLength; uint16_t diskNumber; uint16_t internalFileAttributes; uint32_t externalFileAttributes; uint32_t localFileHeaderOffset; } __attribute__((packed)); // Смещение первой записи is.seekg(eocd.centralDirectoryOffset, is.beg); for (uint16_t i = 0; i < eocd.numberCentralDirectoryRecord; ++i) { CentralDirectoryFileHeader cdfh; is.read((char *) &cdfh, sizeof(cdfh)); if (0x02014b50 != cdfh.signature) { // Ошибка break; } // Считываем имя файла/папки if (cdfh.filenameLength) { uint8_t *filename = new uint8_t[cdfh.filenameLength + 1]; is.read((char *) filename, cdfh.filenameLength); filename[cdfh.filenameLength] = 0; std::cout << filename << "\n"; delete [] filename; } // Пропускаем дополнительные поля if (cdfh.extraFieldLength) { is.seekg(cdfh.extraFieldLength, is.cur); } // Пропускаем комментарий if (cdfh.fileCommentLength) { is.seekg(cdfh.fileCommentLength, is.cur); } } |
Загружаем записи Local File Header
Для каждой записи типа CentralDirectoryFileHeader
существует запись типа Local File Header. Ее расположение в файле описывается полем CentralDirectoryFileHeader.localFileHeaderOffset
. Переходим по смещению, считываем поля известной длины, затем остальные поля (filename
, extraField
).
struct LocalFileHeader { uint32_t signature; uint16_t versionToExtract; uint16_t generalPurposeBitFlag; uint16_t compressionMethod; uint16_t modificationTime; uint16_t modificationDate; uint32_t crc32; uint32_t compressedSize; uint32_t uncompressedSize; uint16_t filenameLength; uint16_t extraFieldLength; } __attribute__((packed)); // Переходим к началу записи is.seekg(cdfh.localFileHeaderOffset, is.beg); // Считываем ее целиком LocalFileHeader lfh; is.read((char *) &lfh, sizeof(lfh)); if (0x04034b50 != lfh.signature) { // Ошибка break; } // Пропускаем название файла is.seekg(lfh.filenameLength, is.cur); // Пропускаем дополнительную информацию is.seekg(lfh.extraFieldLength, is.cur); |
Загружаем данные
Для этого шага нам понадобится библиотека zlib, потому что данные сжимаются методом deflate. Как правило, сразу после LocalFileHeader
идут данные размером compressedSize
. Если данные сжаты, то флаг compressionMethod
равен 8 (или Z_DEFLATED
), иначе он нулевой (или Z_STORED
). Сжатые данные необходимо считать во временный буфер размером compressedSize
и распаковать с помощью библиотеки zlib в буфер размером uncompressedSize
.
// Выделяем буфер для чтения данных uint8_t *readBuffer = new uint8_t[lfh.compressedSize]; // Считываем данные в буфер is.read((char *) readBuffer, lfh.compressedSize); // Если сжатие не применяется if (0 == lfh.compressionMethod) { // То необходимые данные уже лежат в буфере для чтения } // Данные сжаты методом deflate else if (Z_DEFLATED == lfh.compressionMethod) { // Выделяем буфер для распакованных данных uint8_t *result = new uint8_t[lfh.uncompressedSize]; // Инициализация структуры для распаковки z_stream zs; memset(&zs, 0, sizeof(zs)); inflateInit2(&zs, -MAX_WBITS); // Размер входного буфера zs.avail_in = lfh.compressedSize; // Входной буфер zs.next_in = readBuffer; // Размер выходного буфера zs.avail_out = lfh.uncompressedSize; // Выходной буфер zs.next_out = result; // Распаковка inflate(&zs, Z_FINISH); // Очищаем структуру для распаковки inflateEnd(&zs); // Удаляем буфер для распакованных данных delete [] result; } // Удаляем буфер для чтения delete [] readBuffer; |
Общие комментарии по коду
- странная конструкция
__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: (/Не знаю как код оформить на Вашем форуме)
Если вывести элементы buff c 31 по 35, уже не как значения, а как символы то выведется: 1.txt — то есть это название моего зазиповнного файла идущего сразу после uint8_t *filename, НО тогда вопрос, что за значение 97 — указанное в 30 элементе буффера, то есть в параметре filename ? Явно же не длинна ? Так как длинна файла 1.txt — всего 5 байт.
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 заголовков ?
Могу лишь предположить, что такая последовательная структура была предназначена для чтения/записи с ленточных накопителей. А запись в конце файла для жёстких дисков.