Skip to content

025 Загрузка из архива

badcoiq edited this page Aug 8, 2023 · 2 revisions

Загрузка из архива

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

Первая - сжатие и разжатие данных. Вторая - распаковка данных из архива.

Для первого будет использован fastlz. Для второго minizip.

zlib будет использован для PNG, ещё чего-то. Тратить время на его использование в сжатии данных не буду. Может быть кто-то реализует этот функционал за меня.

Добавлю - библиотека и алгоритм которым можно сжимать данные.

// Имя библиотеки_алгоритм, или просто имя если в ней один алгоритм
//     megalib_deflate,
//     megalib_lz77,
//     megalib_lzma,
// Имеется:
//     -   fastlz (LZ77)
enum class bqCompressorType : uint32_t
{
    // m_level:
    //  1 скорость
    //  2 сжатие
    fastlz
};

Информация о сжатии

// Информация для сжатия и расжатия
struct bqCompressionInfo
{
    bqCompressorType m_compressorType = bqCompressorType::fastlz;

    // Когда надо сжать данные
    // Установи размер данных и указатель на данные.
    // после вызова Compress m_sizeCompressed и m_dataCompressed
    // будут иметь значения
    uint32_t m_sizeUncompressed = 0;
    uint8_t* m_dataUncompressed = 0;

    // Когда надо расжать
    // Устанавливаем размер сжатых данных, указатель на сжатые данные.
    // Так-же должен знать m_sizeUncompressed и надо выделить память для распакованных
    // данных в m_dataUncompressed
    // После вызова Decompress m_dataUncompressed будет иметь расжатые данные
    uint32_t m_sizeCompressed = 0;
    uint8_t* m_dataCompressed = 0;

    // Смотри CompressorType. Если значение не верно
    // будет использовано значение по умолчанию.
    uint32_t m_level = 2;
};

Эта вещь для распаковки файла. Зависит от типа библиотеки которая используется. minizip использует свой тип для файла. оно находится в m_implementation Возможно использование других библиотек для работы с другими архивами. Наверно. Пока ничего другого не имею, и не нужно. Можно поразмышлять как сделать распаковку используя minizip и zlib (gz файл).

// Структура с описанием файла архива.
// Путь к файлу и специфичные данные
struct bqArchiveZipFile
{
    bqStringA m_fileName;
    void* m_implementation = 0;
};

Интерфейс для работы

class bqArchiveSystem
{
public:
    // Добавить файл из которого будет происходить распаковка
    static bqArchiveZipFile* ZipAdd(const char* zipFile);

    // Распаковать файл. Если `a` NULL первый найденный файл будет распакован.
    // Установи `a` если используешь много zip файлов которые содержат файлы с одинаковым именем.
    // Функция выделит память используя bqMemory::malloc, и запишет размер в size
    // Если всё OK то вернётся указатель.
    // Используй bqMemory::free для освобождения памяти
    static uint8_t* ZipUnzip(const char* fileInZip, uint32_t* size, bqArchiveZipFile* a = NULL);

    // Установи все данные в `info`. Если сжатие было OK, вернётся true
    // и m_dataCompressed будет иметь адрес
    // Надо вызвать bqMemory::free(m_dataCompressed) для освобождения памяти
    static bool Compress(bqCompressionInfo* info);

    // Подобно методу `Compress`
    // Выделяй память для m_dataUncompressed самостоятельно.
    // Ты должен знать m_sizeUncompressed.
    static bool Decompress(bqCompressionInfo* info);

    // Расжать данные и сунуть всё в массив
    // Если info.m_dataCompressed NULL (или !info.m_sizeCompressed)
    // тогда функция попробует копировать данные из info.m_dataUncompressed
    // Функция выделит память, вставит все данные в массив и освободит память.
    // info.m_dataUncompressed и info.m_sizeUncompressed будут проигнорированы
    static bool Decompress(bqCompressionInfo* info, bqArray<uint8_t>& toArray);
};

Всё же по уму надо делать своё чтение ZIP + добавить свой формат, схожий с zip.

Compress/Decompress

Для того чтобы сжать буфер нужно указать библиотеку с алгоритмом. Реализовано fastlz.

bool bqArchiveSystem::Compress(bqCompressionInfo* info)
{
	BQ_ASSERT_ST(info);
	BQ_ASSERT_ST(info->m_dataUncompressed);
	BQ_ASSERT_ST(info->m_sizeUncompressed);

	info->m_dataCompressed = 0;
	info->m_sizeCompressed = 0;

	switch (info->m_compressorType)
	{
	case bqCompressorType::fastlz:
		return g_framework->_compress_fastlz(info);
	}
	return false;
}

bool bqArchiveSystem::Decompress(bqCompressionInfo* info)
{
	BQ_ASSERT_ST(info);
	BQ_ASSERT_ST(info->m_dataCompressed);
	BQ_ASSERT_ST(info->m_sizeCompressed);
	BQ_ASSERT_ST(info->m_dataUncompressed);
	BQ_ASSERT_ST(info->m_sizeUncompressed);

	switch (info->m_compressorType)
	{
	case bqCompressorType::fastlz:
		return g_framework->_decompress_fastlz(info);
	}
	return false;
}

bool bqArchiveSystem::Decompress(bqCompressionInfo* info, bqArray<uint8_t>& toVector)
{
	BQ_ASSERT_ST(info);

	toVector.clear();
	toVector.reserve(info->m_sizeUncompressed);
	if (!info->m_dataCompressed || !info->m_sizeCompressed)
	{
		if (info->m_dataUncompressed && info->m_sizeUncompressed)
		{
			// НАДО РЕАЛИЗОВАТЬ ЭТО ДЛЯ bqArray !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
			std::copy(info->m_dataUncompressed,
				info->m_dataUncompressed + info->m_sizeUncompressed,
				std::back_inserter(toVector));

			return true;
		}
		return false;
	}

	switch (info->m_compressorType)
	{
	case bqCompressorType::fastlz:
	{
		bqCompressionInfo cmpInf = *info;
		cmpInf.m_dataUncompressed = (uint8_t*)bqMemory::malloc(cmpInf.m_sizeUncompressed);
		bool result = g_framework->_decompress_fastlz(&cmpInf);
		if (result)
		{
			// НАДО РЕАЛИЗОВАТЬ ЭТО ДЛЯ bqArray !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
			std::copy(
				cmpInf.m_dataUncompressed,
				cmpInf.m_dataUncompressed + cmpInf.m_sizeUncompressed,
				std::back_inserter(toVector));
		}
		bqMemory::free(cmpInf.m_dataUncompressed);
		return result;
	}break;
	}
	return false;
}

bool bqFrameworkImpl::_compress_fastlz(bqCompressionInfo* info)
{
	if (info->m_level != 1 && info->m_level != 2)
		info->m_level = 2;

	if (info->m_sizeUncompressed < 32)
		return false;

	uint8_t* output = (uint8_t*)bqMemory::malloc(info->m_sizeUncompressed + (info->m_sizeUncompressed / 3));

	int compressed_size = fastlz_compress_level(
		(int)info->m_level,
		info->m_dataUncompressed,
		info->m_sizeUncompressed,
		output);

	if ((uint32_t)compressed_size >= info->m_sizeUncompressed)
	{
		bqMemory::free(output);
		return false;
	}

	output = (uint8_t*)bqMemory::realloc(output, compressed_size);

	info->m_dataCompressed = output;
	info->m_sizeCompressed = compressed_size;
	return true;
}

bool bqFrameworkImpl::_decompress_fastlz(bqCompressionInfo* info)
{
	int decompressed_size = fastlz_decompress(
		info->m_dataCompressed,
		info->m_sizeCompressed,
		info->m_dataUncompressed,
		info->m_sizeUncompressed);
	if (!decompressed_size)
	{
		bqLog::PrintWarning("%s: %s\n", __FUNCTION__, "can't decompress");
		return false;
	}

	if (info->m_sizeUncompressed != decompressed_size)
	{
		info->m_sizeUncompressed = decompressed_size;
		bqLog::PrintWarning("%s: %s\n", __FUNCTION__, "`decompressed size` is not the sme with m_sizeUncompressed");
	}
	return true;
}

ZipAdd ZipUnzip

ZipAdd добавит файл архива в список из которого нужно будет делать поиск при загрузке ресурса. Вернётся указатель на структуру. Можно не делать поиск, а сразу послать этот указатель, хотя зачем замарачиваться, обычно архивов так-то используется не много.

bqArchiveZipFile* bqArchiveSystem::ZipAdd(const char* fn)
{
	BQ_ASSERT_ST(fn);
	bqLog::Print("AddZip: [%s]\n", fn);

	if (!std::filesystem::exists(fn))
	{
		bqLog::PrintWarning("AddZip: Unable to find zip file [%s]\n", fn);
		return 0;
	}

	unzFile uf = unzOpen64(fn);
	if (uf == NULL)
	{
		bqLog::PrintWarning("AddZip: unzOpen64 failed [%s]\n", fn);
		return 0;
	}

	bqArchiveZipFile* zp = bqCreate<bqArchiveZipFile>();
	zp->m_fileName.assign(fn);
	zp->m_implementation = uf;

	g_framework->m_zipFiles.push_back(zp);

	return zp;
}

uint8_t* bqArchiveSystem::ZipUnzip(const char* fileInZip, uint32_t* size, bqArchiveZipFile* a)
{
	BQ_ASSERT_ST(fileInZip);
	BQ_ASSERT_ST(size);
	if (a)
		goto lockate_file;

	for (size_t i = 0, sz = g_framework->m_zipFiles.size(); i < sz; ++i)
	{
		if (unzLocateFile(g_framework->m_zipFiles[i]->m_implementation, fileInZip, 0) == UNZ_OK)
		{
			a = g_framework->m_zipFiles[i];
			goto unzip_file;
		}
	}

	bqLog::PrintWarning("file %s not found in the zipfile\n", fileInZip);
	return 0;

lockate_file:;
	if (unzLocateFile(a->m_implementation, fileInZip, 0) != UNZ_OK)
	{
		bqLog::PrintWarning("file %s not found in the zipfile\n", fileInZip);
		return 0;
	}

unzip_file:;
	char filename_inzip[256];

	unz_file_info64 file_info;
	int err = unzGetCurrentFileInfo64(a->m_implementation, &file_info,
		filename_inzip,
		sizeof(filename_inzip),
		NULL, 0, NULL, 0);

	if (err != UNZ_OK)
	{
		bqLog::PrintWarning("error %d with zipfile in unzGetCurrentFileInfo [%s]\n", err, fileInZip);
		return 0;
	}

	err = unzOpenCurrentFilePassword(a->m_implementation, 0);
	if (err != UNZ_OK)
	{
		bqLog::PrintWarning("error %d with zipfile in unzOpenCurrentFilePassword [%s]\n", err, fileInZip);
		return 0;
	}

	uint8_t* data = (uint8_t*)bqMemory::malloc((size_t)file_info.uncompressed_size);
	*size = (uint32_t)file_info.uncompressed_size;

	err = unzReadCurrentFile(a->m_implementation, data, (unsigned int)file_info.uncompressed_size);
	if (err < 0)
	{
		bqLog::PrintWarning("error %d with zipfile in unzReadCurrentFile [%s]\n", err, fileInZip);
		free(data);
		data = 0;
	}

	unzCloseCurrentFile(a->m_implementation);

	return data;
}

Загрузка

Так как при загрузке BMP и OBJ я уже использую буфер (я не читаю файл напрямую), то ничего менять не надо, надо просто зайти в метод bqFramework::SummonFileBuffer и в конце добавить

uint8_t* bqFramework::SummonFileBuffer(const char* path, uint32_t* szOut, bool isText)
{
	BQ_ASSERT_ST(path);
	BQ_ASSERT_ST(szOut);

	*szOut = 0;

	std::filesystem::path p = path;

	if (std::filesystem::exists(p))
	{
		...
	}
	return bqArchiveSystem::ZipUnzip(path, szOut, 0);
}

Если файла нет, то будет попытка его распаковать из добавленных ранее архивов.

bqFramework

Массив с архивами лежит там bqFrameworkImpl. И методы.

// Архивы, сжатие
	bool _compress_fastlz(bqCompressionInfo* info);
	bool _decompress_fastlz(bqCompressionInfo* info);
	void _onDestroy_archive();
	std::vector<bqArchiveZipFile*> m_zipFiles;

При завершении работы надо закрыть все архивы

void bqFrameworkImpl::_onDestroy_archive()
{
	for (size_t i = 0; i < m_zipFiles.size(); ++i)
	{
		if (m_zipFiles[i]->m_implementation)
		{
			unzClose(m_zipFiles[i]->m_implementation);
		}
		bqDestroy(m_zipFiles[i]);
	}
}

Все сторонние библиотеки скомпилированы в .lib файлы. Их подключение.

BQ_LINK_LIBRARY("minizip");
BQ_LINK_LIBRARY("fastlz");
BQ_LINK_LIBRARY("zlib");
BQ_LINK_LIBRARY("bzip2");

Пути

Путь к файлу надо указывать так как он лежит в архиве.

Например, в архиве лежит папка 'media' и в ней файлы, то при загрузке надо указать

media/file.ext

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

Так как для загрузки из файла путь указывается относительно рабочей папки

../media/1.obj

то надо преобразовать эту строку в понятную для распаковки из ZIP

media/1.obj

Сначала надо знать где лежит программа.

wchar_t pth[1000];
GetModuleFileName(0, pth, 1000);
g_framework->m_appPath = pth;
g_framework->m_appPath.pop_back_before(U'\\');
g_framework->m_appPath.replace(U'\\', U'/');

Добавляется метод для получения этого пути

bqString bqFramework::GetAppPath()
{
	return g_framework->m_appPath;
}

Далее преобразование пути

bqStringA bqFramework::GetPath(const bqString& v)
{
	bqString p = g_framework->m_appPath;
	p.append(v);
	bqStringA stra;
	p.to_utf8(stra);

	if (!std::filesystem::exists(stra.c_str()))
	{
		p.assign(v);

		while (p.size())
		{
			if (p[0] == U'.'
				|| p[0] == U'\\'
				|| p[0] == U'/')
				p.pop_front();
			else
				break;
		}

		p.to_utf8(stra);
	}

	return stra;
}

Если файл найден, то идёт возврат пути типа

C:\code\app\bin32\..\media\1.obj

если не найден, то возвращается обрезанная версия.

media\1.obj

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

bqImage* image = bqFramework::SummonImage(bqFramework::GetPath("../media/image.bmp").c_str());

Если же файла в папке не будет, будет распакован файл из архива. Главное не забыть добавить архив

например bqArchiveSystem::ZipAdd("../media/data.zip");

и чтобы пути совпадали (названия папок в архиве и на диске)

https://github.com/badcoiq/badcoiq/tree/faf1a44ae7d997bd90b7f0f0309a5566655f8060