Алгоритм получения данных из 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
, проверяя сигнатуру, если сигнатуры неправильная, значит все файлы считаны.