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