Запаковка файлов , присвоение метаданных и чтение метаданных из файла при помощи zlib

Я не понимаю как мне корректно запаковать файлы , присваивая метаданные запакованным файлам. Код который запаковывает файлы и присваивает метаданные:

#include <zlib.h>
#include <fstream>
#include <vector>
#include <iostream>
#include <unordered_map>

// Структура для хранения метаданных ресурса
struct ResourceMetaData {
    std::string id;
    size_t originalSize;
    size_t compressedSize;
    size_t offset;
    std::string type; 
};

// Функция для сжатия данных
std::vector<uint8_t> compressData(const std::vector<uint8_t>& data) {
    uLongf compressedSize = compressBound(data.size());
    std::vector<uint8_t> compressedData(compressedSize);

    if (compress(compressedData.data(), &compressedSize, data.data(), data.size()) != Z_OK) {
        throw std::runtime_error("Compression failed");
    }

    compressedData.resize(compressedSize);
    return compressedData;
}

// Функция для упаковки ресурсов в один файл
void packResources(const std::unordered_map<std::string, std::pair<std::string, std::string>>& resources, const std::string& outputPath) {
    std::ofstream outFile(outputPath, std::ios::binary);
    if (!outFile) {
        throw std::runtime_error("Failed to open output file");
    }

    // Записываем количество ресурсов
    uint32_t resourceCount = resources.size();
    outFile.write(reinterpret_cast<const char*>(&resourceCount), sizeof(resourceCount));

    // Сохраняем место для метаданных
    std::streampos metaDataOffset = outFile.tellp();
    outFile.seekp(resourceCount * sizeof(ResourceMetaData), std::ios::cur);

    std::vector<ResourceMetaData> metaDataList;
    size_t currentOffset = outFile.tellp();

    for (const auto& resource : resources) {
        const std::string& id = resource.first;
        const std::string& type = resource.second.first;
        const std::string& path = resource.second.second;

        std::ifstream inFile(path, std::ios::binary | std::ios::ate);
        if (!inFile) {
            throw std::runtime_error("Failed to open resource file: " + path);
        }

        std::streamsize size = inFile.tellg();
        inFile.seekg(0, std::ios::beg);

        std::vector<uint8_t> buffer(size);
        if (!inFile.read(reinterpret_cast<char*>(buffer.data()), size)) {
            throw std::runtime_error("Failed to read resource file: " + path);
        }

        std::vector<uint8_t> compressedData = compressData(buffer);

        ResourceMetaData metaData;
        metaData.id = id;
        metaData.originalSize = buffer.size();
        metaData.compressedSize = compressedData.size();
        metaData.offset = currentOffset;
        metaData.type = type;

        metaDataList.push_back(metaData);

        outFile.write(reinterpret_cast<const char*>(compressedData.data()), compressedData.size());
        currentOffset += compressedData.size();
    }

    // Записываем метаданные
    outFile.seekp(metaDataOffset);
    for (const auto& metaData : metaDataList) {
        outFile.write(reinterpret_cast<const char*>(&metaData), sizeof(metaData));
    }
}

int main() {
    try {
        // Указываем ресурсы для упаковки
        std::unordered_map<std::string, std::pair<std::string, std::string>> resources = {
            {"fontSansation", {"font", "Fonts/Sansation.ttf"}},
            {"textureButton", {"texture", "Textures/Buttons.png"}},
            {"textureEntities", {"texture", "Textures/Entities.png"}},
            {"textureExplosion", {"texture", "Textures/Explosion.png"}},
            {"textureFinishLine", {"texture", "Textures/FinishLine.png"}},
            {"textureJungle", {"texture", "Textures/Jungle.png"}},
            {"textureParticle", {"texture", "Textures/Particle.png"}},
            {"textureTitleScreen", {"texture", "Textures/TitleScreen.png"}},
            {"menuTheme", {"music", "Music/MenuTheme.ogg"}},
            {"missionTheme", {"music", "Music/MissionTheme.ogg"}},
            {"shaderAdd", {"shader", "Shaders/Add.frag"}},
            {"shaderBrightness", {"shader", "Shaders/Brightness.frag"}},
            {"shaderDownSample", {"shader", "Shaders/DownSample.frag"}},
            {"shaderFullpass", {"shader", "Shaders/Fullpass.vert"}},
            {"shaderGuassianBlur", {"shader", "Shaders/GuassianBlur.frag"}},
            {"soundAlliedGunfire", {"sound", "Sound/AlliedGunfire.wav"}},
            {"soundButton", {"sound", "Sound/Button.wav"}},
            {"soundCollectPickup", {"sound", "Sound/CollectPickup.wav"}},
            {"soundEnemyGunfire", {"sound", "Sound/EnemyGunfire.wav"}},
            {"soundExplosion1", {"sound", "Sound/Explosion1.wav"}},
            {"soundExplosion2", {"sound", "Sound/Explosion2.wav"}},
            {"soundLaunchMissile", {"sound", "Sound/LaunchMissile.wav"}}
        };

        packResources(resources, "data.bin");
        std::cout << "Resources packed successfully." << std::endl;
    }
    catch (const std::exception& e) {
        std::cerr << e.what() << std::endl;
    }
    system("pause");
    return 0;
}

Код который читает файл и добавляет считанные метаданные в массив:

bool ResourceHolder<Resource, Identifier>::loadPackedResources(const std::string& filePath) {
    std::ifstream inFile(filePath, std::ios::binary);
    if (!inFile) {
        throw std::runtime_error("ResourceHolder::load - Failed to load " + filePath);
    }

    
    uint32_t resourceCount;
    inFile.read(reinterpret_cast<char*>(&resourceCount), sizeof(resourceCount));

    
    uint32_t compressedMetaDataSize;
    inFile.read(reinterpret_cast<char*>(&compressedMetaDataSize), sizeof(compressedMetaDataSize));

    
    std::vector<char> compressedMetaData(compressedMetaDataSize);
    inFile.read(compressedMetaData.data(), compressedMetaDataSize);

    
    uLongf uncompressedMetaDataSize = resourceCount * sizeof(ResourceMetaData);
    std::vector<char> uncompressedMetaData(uncompressedMetaDataSize);

    int result = uncompress(reinterpret_cast<Bytef*>(uncompressedMetaData.data()), &uncompressedMetaDataSize,
        reinterpret_cast<const Bytef*>(compressedMetaData.data()), compressedMetaDataSize);

    if (result != Z_OK) {
        throw std::runtime_error("ResourceHolder::load - Failed to decompress metadata");
    }

    for (uint32_t i = 0; i < resourceCount; ++i) {
        ResourceMetaData metaData;
        std::memcpy(&metaData, uncompressedMetaData.data() + i * sizeof(ResourceMetaData), sizeof(ResourceMetaData));
        resourcesMeta[metaData.id] = metaData;
        std::cout << "Loaded resource: ID = " << metaData.id << ", Type = " << metaData.type << std::endl;
    }

    this->inFile.swap(inFile);

    return true;
}

Вот что выводит консоль:

EXCEPTION: ResourceHolder::load - Failed to decompress metadata

Уже 3 часа долблюсь об эту проблему, не могу понять как ее решить


Ответы (2 шт):

Автор решения: GxdTxnz

У вас проблема возникает из за неверной записи и чтения метаданных (несоответствик размеров данных, записанных в файл, и тех, которые пытаетесь прочитать и разархивировать). Вот ResourceMetaData вы напрямую в файл запихиваете, а так делать нельзя (выравнивание данных в оператосе и прочее (вот здесь вопрос обсуждался). А в loadPackedResources при чтении данных вы их обрабатываете, считая, что это один сжатый блок, который потом разархивируете. Короче говоря, стандартизируемся и используем сериализацию.

На примере бинарной сериализации (upd):

#include <iostream>
#include <fstream>
#include <cstring>

struct ResourceMetaData {
    std::string id;
    size_t originalSize;
    size_t compressedSize;
    size_t offset;
    std::string type;
};

// Бинарная сериализация
void serializeBinary(const ResourceMetaData& metaData, const std::string& filename) {
    std::ofstream outFile(filename, std::ios::binary);
    if (!outFile) {
        std::cerr << "Failed to open output file: " << filename << std::endl;
        return;
    }

    // Записываем данные в бинарный формат
    size_t idLength = metaData.id.size() + 1; // +1 для нуль-терминатора
    outFile.write(reinterpret_cast<const char*>(&idLength), sizeof(idLength));
    outFile.write(metaData.id.c_str(), idLength);
    outFile.write(reinterpret_cast<const char*>(&metaData.originalSize), sizeof(metaData.originalSize));
    outFile.write(reinterpret_cast<const char*>(&metaData.compressedSize), sizeof(metaData.compressedSize));
    outFile.write(reinterpret_cast<const char*>(&metaData.offset), sizeof(metaData.offset));
    
    size_t typeLength = metaData.type.size() + 1; // +1 для нуль-терминатора
    outFile.write(reinterpret_cast<const char*>(&typeLength), sizeof(typeLength));
    outFile.write(metaData.type.c_str(), typeLength);

    outFile.close();
}

// Десериализация из бинарного формата
ResourceMetaData deserializeBinary(const std::string& filename) {
    ResourceMetaData metaData;

    std::ifstream inFile(filename, std::ios::binary);
    if (!inFile) {
        std::cerr << "Failed to open input file: " << filename << std::endl;
        return metaData;
    }

    // Читаем данные из бинарного файла
    size_t idLength;
    inFile.read(reinterpret_cast<char*>(&idLength), sizeof(idLength));
    char* idBuffer = new char[idLength];
    inFile.read(idBuffer, idLength);
    metaData.id = std::string(idBuffer);
    delete[] idBuffer;

    inFile.read(reinterpret_cast<char*>(&metaData.originalSize), sizeof(metaData.originalSize));
    inFile.read(reinterpret_cast<char*>(&metaData.compressedSize), sizeof(metaData.compressedSize));
    inFile.read(reinterpret_cast<char*>(&metaData.offset), sizeof(metaData.offset));

    size_t typeLength;
    inFile.read(reinterpret_cast<char*>(&typeLength), sizeof(typeLength));
    char* typeBuffer = new char[typeLength];
    inFile.read(typeBuffer, typeLength);
    metaData.type = std::string(typeBuffer);
    delete[] typeBuffer;

    inFile.close();

    return metaData;
}

int main() {
    ResourceMetaData metaData;
    metaData.id = "textureButton";
    metaData.originalSize = 1024;
    metaData.compressedSize = 512;
    metaData.offset = 0;
    metaData.type = "texture";

    // Сохраняем в бинарный файл
    serializeBinary(metaData, "metaData.bin");

    // Восстанавливаем из бинарного файла
    ResourceMetaData restoredMetaData = deserializeBinary("metaData.bin");

    // Выводим восстановленные данные
    std::cout << "Restored Metadata:\n"
              << "ID: " << restoredMetaData.id << "\n"
              << "Original Size: " << restoredMetaData.originalSize << "\n"
              << "Compressed Size: " << restoredMetaData.compressedSize << "\n"
              << "Offset: " << restoredMetaData.offset << "\n"
              << "Type: " << restoredMetaData.type << std::endl;

    return 0;
}
→ Ссылка
Автор решения: GxdTxnz

Можете код вот так отредачить:

Упаковка

#include <zlib.h>
#include <fstream>
#include <vector>
#include <iostream>
#include <unordered_map>
#include <cstring>

struct ResourceMetaData {
    char id[64];
    size_t originalSize;
    size_t compressedSize;
    size_t offset;
    char type[32];
};

// Функция для сжатия данных
std::vector<uint8_t> compressData(const std::vector<uint8_t>& data) {
    uLongf compressedSize = compressBound(data.size());
    std::vector<uint8_t> compressedData(compressedSize);

    if (compress(compressedData.data(), &compressedSize, data.data(), data.size()) != Z_OK) {
        throw std::runtime_error("Compression failed");
    }

    compressedData.resize(compressedSize);
    return compressedData;
}

// Функция для упаковки ресурсов в один файл
void packResources(const std::unordered_map<std::string, std::pair<std::string, std::string>>& resources, const std::string& outputPath) {
    std::ofstream outFile(outputPath, std::ios::binary);
    if (!outFile) {
        throw std::runtime_error("Failed to open output file");
    }

    // Записываем количество ресурсов
    uint32_t resourceCount = resources.size();
    outFile.write(reinterpret_cast<const char*>(&resourceCount), sizeof(resourceCount));

    std::vector<ResourceMetaData> metaDataList;
    size_t currentOffset = sizeof(resourceCount) + sizeof(uint32_t); // Смещение для количества ресурсов и размера сжатых метаданных

    for (const auto& resource : resources) {
        const std::string& id = resource.first;
        const std::string& type = resource.second.first;
        const std::string& path = resource.second.second;

        std::ifstream inFile(path, std::ios::binary | std::ios::ate);
        if (!inFile) {
            throw std::runtime_error("Failed to open resource file: " + path);
        }

        std::streamsize size = inFile.tellg();
        inFile.seekg(0, std::ios::beg);

        std::vector<uint8_t> buffer(size);
        if (!inFile.read(reinterpret_cast<char*>(buffer.data()), size)) {
            throw std::runtime_error("Failed to read resource file: " + path);
        }

        std::vector<uint8_t> compressedData = compressData(buffer);

        ResourceMetaData metaData;
        std::memset(metaData.id, 0, sizeof(metaData.id));
        std::strncpy(metaData.id, id.c_str(), sizeof(metaData.id) - 1);
        metaData.originalSize = buffer.size();
        metaData.compressedSize = compressedData.size();
        metaData.offset = currentOffset;
        std::memset(metaData.type, 0, sizeof(metaData.type));
        std::strncpy(metaData.type, type.c_str(), sizeof(metaData.type) - 1);

        metaDataList.push_back(metaData);

        outFile.write(reinterpret_cast<const char*>(compressedData.data()), compressedData.size());
        currentOffset += compressedData.size();
    }

    // Сжимаем метаданные
    std::vector<uint8_t> metaDataBuffer(metaDataList.size() * sizeof(ResourceMetaData));
    std::memcpy(metaDataBuffer.data(), metaDataList.data(), metaDataBuffer.size());
    std::vector<uint8_t> compressedMetaData = compressData(metaDataBuffer);

    // Записываем размер сжатых метаданных
    uint32_t compressedMetaDataSize = compressedMetaData.size();
    outFile.write(reinterpret_cast<const char*>(&compressedMetaDataSize), sizeof(compressedMetaDataSize));

    // Записываем сжатые метаданные
    outFile.write(reinterpret_cast<const char*>(compressedMetaData.data()), compressedMetaData.size());

    outFile.close();
}

int main() {
    try {
        // Указываем ресурсы для упаковки
        std::unordered_map<std::string, std::pair<std::string, std::string>> resources = {
            {"fontSansation", {"font", "Fonts/Sansation.ttf"}},
            {"textureButton", {"texture", "Textures/Buttons.png"}},
            {"textureEntities", {"texture", "Textures/Entities.png"}},
            {"textureExplosion", {"texture", "Textures/Explosion.png"}},
            {"textureFinishLine", {"texture", "Textures/FinishLine.png"}},
            {"textureJungle", {"texture", "Textures/Jungle.png"}},
            {"textureParticle", {"texture", "Textures/Particle.png"}},
            {"textureTitleScreen", {"texture", "Textures/TitleScreen.png"}},
            {"menuTheme", {"music", "Music/MenuTheme.ogg"}},
            {"missionTheme", {"music", "Music/MissionTheme.ogg"}},
            {"shaderAdd", {"shader", "Shaders/Add.frag"}},
            {"shaderBrightness", {"shader", "Shaders/Brightness.frag"}},
            {"shaderDownSample", {"shader", "Shaders/DownSample.frag"}},
            {"shaderFullpass", {"shader", "Shaders/Fullpass.vert"}},
            {"shaderGuassianBlur", {"shader", "Shaders/GuassianBlur.frag"}},
            {"soundAlliedGunfire", {"sound", "Sound/AlliedGunfire.wav"}},
            {"soundButton", {"sound", "Sound/Button.wav"}},
            {"soundCollectPickup", {"sound", "Sound/CollectPickup.wav"}},
            {"soundEnemyGunfire", {"sound", "Sound/EnemyGunfire.wav"}},
            {"soundExplosion1", {"sound", "Sound/Explosion1.wav"}},
            {"soundExplosion2", {"sound", "Sound/Explosion2.wav"}},
            {"soundLaunchMissile", {"sound", "Sound/LaunchMissile.wav"}}
        };

        packResources(resources, "data.bin");
        std::cout << "Resources packed successfully." << std::endl;
    }
    catch (const std::exception& e) {
        std::cerr << e.what() << std::endl;
    }
    return 0;
}

Чтение упакованных данных:

bool loadPackedResources(const std::string& filePath, std::unordered_map<std::string, ResourceMetaData>& resourcesMeta) {
    std::ifstream inFile(filePath, std::ios::binary);
    if (!inFile) {
        throw std::runtime_error("ResourceHolder::load - Failed to load " + filePath);
    }

    uint32_t resourceCount;
    inFile.read(reinterpret_cast<char*>(&resourceCount), sizeof(resourceCount));

    uint32_t compressedMetaDataSize;
    inFile.read(reinterpret_cast<char*>(&compressedMetaDataSize), sizeof(compressedMetaDataSize));

    std::vector<char> compressedMetaData(compressedMetaDataSize);
    inFile.read(compressedMetaData.data(), compressedMetaDataSize);

    uLongf uncompressedMetaDataSize = resourceCount * sizeof(ResourceMetaData);
    std::vector<char> uncompressedMetaData(uncompressedMetaDataSize);

    int result = uncompress(reinterpret_cast<Bytef*>(uncompressedMetaData.data()), &uncompressedMetaDataSize,
                            reinterpret_cast<const Bytef*>(compressedMetaData.data()), compressedMetaDataSize);

    if (result != Z_OK) {
        throw std::runtime_error("ResourceHolder::load - Failed to decompress metadata");
    }

    for (uint32_t i = 0; i < resourceCount; ++i) {
        ResourceMetaData metaData;
        std::memcpy(&metaData, uncompressedMetaData.data() + i * sizeof(ResourceMetaData), sizeof(ResourceMetaData));
        resourcesMeta[metaData.id] = metaData;
        std::cout << "Loaded resource: ID = " << metaData.id << ", Type = " << metaData.type << std::endl;
    }

    return true;
}

По изменениям:

  • Поменял структуру для хранения метаданных для хранения фиксированного размера строк (для простоты се/десериализации)
  • Метаданные сжимаются перед записью
  • Сжатые метаданные и их размер читаютяс и распаковываются перед использованием

Проблема была с тем, что вы предпологаете, что метаданные уже сжаты, и пытаетесь их записать. Чтобы пофиксить, надо их сжать во время записи, а потом распаковать при чтении.

→ Ссылка