Чтение ZIP файла

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

Дополнительные ссылки по теме

Отвратительно!Ужасно.НормальноХорошо.Отлично! (2 оценок, среднее: 5,00 из 5)
Загрузка...

Ваши комментарии к статье:

Оставить комментарий: