Алгоритм получения данных из 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: (/Не знаю как код оформить на Вашем форуме)
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 заголовков ?
Могу лишь предположить, что такая последовательная структура была предназначена для чтения/записи с ленточных накопителей. А запись в конце файла для жёстких дисков.