2025-04-04 20:42:41 -04:00
|
|
|
#include "compression.h"
|
2025-06-04 22:31:11 -04:00
|
|
|
#include "minilzo.h"
|
2025-04-23 00:09:35 -04:00
|
|
|
|
|
|
|
|
#define XBOXAPI __declspec(dllimport)
|
|
|
|
|
#include "xcompress.h"
|
|
|
|
|
|
|
|
|
|
#include <QLibrary>
|
|
|
|
|
#include <QDebug>
|
|
|
|
|
#include <QFile>
|
|
|
|
|
#include <QDataStream>
|
|
|
|
|
|
|
|
|
|
QByteArray Compression::CompressXMem(const QByteArray &data)
|
|
|
|
|
{
|
|
|
|
|
XMEMCODEC_PARAMETERS_LZX lzxParams = {};
|
|
|
|
|
lzxParams.Flags = 0;
|
2025-06-04 22:31:11 -04:00
|
|
|
lzxParams.WindowSize = 0x20000;
|
|
|
|
|
lzxParams.CompressionPartitionSize = 0x80000;
|
2025-04-23 00:09:35 -04:00
|
|
|
|
|
|
|
|
XMEMCOMPRESSION_CONTEXT ctx = nullptr;
|
|
|
|
|
if (FAILED(XMemCreateCompressionContext(XMEMCODEC_LZX, &lzxParams, 0, &ctx)) || !ctx)
|
|
|
|
|
return QByteArray();
|
|
|
|
|
|
|
|
|
|
SIZE_T estimatedSize = data.size() + XCOMPRESS_LZX_BLOCK_GROWTH_SIZE_MAX;
|
|
|
|
|
QByteArray output(static_cast<int>(estimatedSize), 0);
|
|
|
|
|
SIZE_T actualSize = estimatedSize;
|
|
|
|
|
|
|
|
|
|
HRESULT hr = XMemCompress(ctx, output.data(), &actualSize, data.constData(), data.size());
|
|
|
|
|
XMemDestroyCompressionContext(ctx);
|
|
|
|
|
|
|
|
|
|
if (FAILED(hr))
|
|
|
|
|
return QByteArray();
|
|
|
|
|
|
|
|
|
|
output.resize(static_cast<int>(actualSize));
|
|
|
|
|
return output;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-04 22:31:11 -04:00
|
|
|
QByteArray Compression::DecompressXMem(const QByteArray &data, int flags, int windowSize, int partSize)
|
2025-04-23 00:09:35 -04:00
|
|
|
{
|
2025-06-04 22:31:11 -04:00
|
|
|
if (data.isEmpty())
|
|
|
|
|
return {};
|
|
|
|
|
|
2025-04-23 00:09:35 -04:00
|
|
|
XMEMCODEC_PARAMETERS_LZX lzxParams = {};
|
2025-06-04 22:31:11 -04:00
|
|
|
lzxParams.Flags = flags;
|
|
|
|
|
lzxParams.WindowSize = windowSize;
|
|
|
|
|
lzxParams.CompressionPartitionSize = partSize;
|
2025-04-23 00:09:35 -04:00
|
|
|
|
|
|
|
|
XMEMDECOMPRESSION_CONTEXT ctx = nullptr;
|
|
|
|
|
if (FAILED(XMemCreateDecompressionContext(XMEMCODEC_LZX, &lzxParams, 0, &ctx)) || !ctx)
|
2025-06-04 22:31:11 -04:00
|
|
|
return {};
|
|
|
|
|
|
|
|
|
|
// Allocate large enough buffer for decompression (16 MB is a safe upper bound)
|
|
|
|
|
const SIZE_T kMaxOutSize = 16 * 1024 * 1024;
|
|
|
|
|
QByteArray output(static_cast<int>(kMaxOutSize), Qt::Uninitialized);
|
|
|
|
|
SIZE_T actualSize = kMaxOutSize;
|
2025-04-23 00:09:35 -04:00
|
|
|
|
2025-06-04 22:31:11 -04:00
|
|
|
HRESULT hr = XMemDecompress(ctx,
|
|
|
|
|
output.data(), &actualSize,
|
|
|
|
|
data.constData(), data.size() + 16);
|
2025-04-23 00:09:35 -04:00
|
|
|
|
|
|
|
|
XMemDestroyDecompressionContext(ctx);
|
|
|
|
|
|
2025-06-04 22:31:11 -04:00
|
|
|
if (FAILED(hr)) {
|
|
|
|
|
qWarning() << "XMemDecompress failed with HRESULT:" << hr;
|
|
|
|
|
return {};
|
|
|
|
|
}
|
2025-04-23 00:09:35 -04:00
|
|
|
|
|
|
|
|
output.resize(static_cast<int>(actualSize));
|
|
|
|
|
return output;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
quint32 Compression::CalculateAdler32Checksum(const QByteArray &data) {
|
|
|
|
|
// Start with the initial value for Adler-32
|
|
|
|
|
quint32 adler = adler32(0L, Z_NULL, 0);
|
|
|
|
|
|
|
|
|
|
// Calculate Adler-32 checksum
|
|
|
|
|
adler = adler32(adler, reinterpret_cast<const Bytef *>(data.constData()), data.size());
|
|
|
|
|
|
|
|
|
|
return adler;
|
|
|
|
|
}
|
2025-04-04 20:42:41 -04:00
|
|
|
|
2025-06-04 22:31:11 -04:00
|
|
|
qint64 Compression::FindZlibOffset(const QByteArray &bytes)
|
|
|
|
|
{
|
|
|
|
|
static const QByteArray iwffs("IWffs");
|
|
|
|
|
auto idx = bytes.indexOf(iwffs);
|
|
|
|
|
if (idx != -1)
|
|
|
|
|
return idx + 0x4000;
|
|
|
|
|
|
|
|
|
|
const char header = 0x78; // z-lib: 0x78 [FLG]
|
|
|
|
|
int pos = -1;
|
|
|
|
|
while ((pos = bytes.indexOf(header, pos + 1)) != -1)
|
|
|
|
|
{
|
|
|
|
|
QByteArray window = bytes.mid(pos, 0x20);
|
|
|
|
|
if (!window.contains(QByteArray::fromHex("000000")) &&
|
|
|
|
|
!window.contains(QByteArray::fromHex("FFFFFF")))
|
|
|
|
|
return pos;
|
|
|
|
|
}
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QByteArray Compression::StripHashBlocks(const QByteArray &raw,
|
|
|
|
|
int dataChunkSize,
|
|
|
|
|
int hashChunkSize)
|
|
|
|
|
{
|
|
|
|
|
QByteArray cleaned;
|
|
|
|
|
cleaned.reserve(raw.size()); // upper bound
|
|
|
|
|
|
|
|
|
|
int p = 0;
|
|
|
|
|
while (p < raw.size())
|
|
|
|
|
{
|
|
|
|
|
const int chunk = qMin(dataChunkSize, raw.size() - p);
|
|
|
|
|
cleaned.append(raw.constData() + p, chunk);
|
|
|
|
|
p += chunk;
|
|
|
|
|
|
|
|
|
|
// skip hash bytes if they are still inside the buffer
|
|
|
|
|
if (p < raw.size())
|
|
|
|
|
p += qMin(hashChunkSize, raw.size() - p);
|
|
|
|
|
}
|
|
|
|
|
return cleaned;
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-04 20:42:41 -04:00
|
|
|
QByteArray Compression::DecompressZLIB(const QByteArray &aCompressedData) {
|
2025-04-23 00:09:35 -04:00
|
|
|
if (aCompressedData.isEmpty()) {
|
2025-04-04 20:42:41 -04:00
|
|
|
return {};
|
2025-04-23 00:09:35 -04:00
|
|
|
}
|
2025-04-04 20:42:41 -04:00
|
|
|
|
|
|
|
|
z_stream strm{};
|
2025-04-23 00:09:35 -04:00
|
|
|
strm.zalloc = Z_NULL;
|
|
|
|
|
strm.zfree = Z_NULL;
|
|
|
|
|
strm.opaque = Z_NULL;
|
2025-04-04 20:42:41 -04:00
|
|
|
strm.avail_in = static_cast<uInt>(aCompressedData.size());
|
2025-04-23 00:09:35 -04:00
|
|
|
strm.next_in = reinterpret_cast<Bytef*>(const_cast<char*>(aCompressedData.data()));
|
2025-04-04 20:42:41 -04:00
|
|
|
|
|
|
|
|
if (inflateInit2(&strm, MAX_WBITS) != Z_OK) {
|
2025-04-23 00:09:35 -04:00
|
|
|
qWarning() << "inflateInit2 failed";
|
2025-04-04 20:42:41 -04:00
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QByteArray decompressed;
|
2025-04-23 00:09:35 -04:00
|
|
|
QByteArray buffer(fmin(strm.avail_in * 2, 4096), Qt::Uninitialized);
|
2025-04-04 20:42:41 -04:00
|
|
|
|
|
|
|
|
int ret;
|
|
|
|
|
do {
|
2025-04-23 00:09:35 -04:00
|
|
|
strm.next_out = reinterpret_cast<Bytef*>(buffer.data());
|
|
|
|
|
strm.avail_out = buffer.size();
|
2025-04-04 20:42:41 -04:00
|
|
|
|
|
|
|
|
ret = inflate(&strm, Z_NO_FLUSH);
|
|
|
|
|
|
2025-06-04 22:31:11 -04:00
|
|
|
if (strm.avail_out < buffer.size()) {
|
2025-04-23 00:09:35 -04:00
|
|
|
decompressed.append(buffer.constData(), buffer.size() - strm.avail_out);
|
2025-04-04 20:42:41 -04:00
|
|
|
}
|
|
|
|
|
|
2025-04-23 00:09:35 -04:00
|
|
|
if (ret == Z_STREAM_END) {
|
2025-06-04 22:31:11 -04:00
|
|
|
break;
|
2025-04-23 00:09:35 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ret == Z_BUF_ERROR && strm.avail_out == 0) {
|
2025-06-04 22:31:11 -04:00
|
|
|
buffer.resize(buffer.size() * 2);
|
2025-04-23 00:09:35 -04:00
|
|
|
} else if (ret != Z_OK) {
|
2025-06-04 22:31:11 -04:00
|
|
|
size_t errorOffset = strm.total_in;
|
|
|
|
|
qWarning() << "Zlib error:" << zError(ret)
|
|
|
|
|
<< "at offset" << errorOffset
|
|
|
|
|
<< "of" << aCompressedData.size() << "bytes";
|
2025-04-23 00:09:35 -04:00
|
|
|
inflateEnd(&strm);
|
2025-06-04 22:31:11 -04:00
|
|
|
return decompressed;
|
2025-04-23 00:09:35 -04:00
|
|
|
}
|
2025-04-04 20:42:41 -04:00
|
|
|
} while (ret != Z_STREAM_END);
|
|
|
|
|
|
|
|
|
|
inflateEnd(&strm);
|
|
|
|
|
return decompressed;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-04 22:31:11 -04:00
|
|
|
|
2025-04-04 20:42:41 -04:00
|
|
|
QByteArray Compression::CompressZLIB(const QByteArray &aData) {
|
|
|
|
|
return CompressZLIBWithSettings(aData);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QByteArray Compression::CompressZLIBWithSettings(const QByteArray &aData, int aCompressionLevel, int aWindowBits, int aMemLevel, int aStrategy, const QByteArray &aDictionary) {
|
|
|
|
|
if (aData.isEmpty())
|
|
|
|
|
return {};
|
|
|
|
|
|
|
|
|
|
z_stream strm{};
|
|
|
|
|
if (deflateInit2(&strm, aCompressionLevel, Z_DEFLATED, aWindowBits, aMemLevel, aStrategy) != Z_OK) {
|
|
|
|
|
qWarning() << "Failed to initialize compression with custom settings.";
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!aDictionary.isEmpty()) {
|
|
|
|
|
deflateSetDictionary(&strm, reinterpret_cast<const Bytef*>(aDictionary.constData()), aDictionary.size());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
strm.next_in = reinterpret_cast<Bytef*>(const_cast<char*>(aData.data()));
|
|
|
|
|
strm.avail_in = aData.size();
|
|
|
|
|
|
|
|
|
|
QByteArray compressed;
|
|
|
|
|
char buffer[4096];
|
|
|
|
|
|
|
|
|
|
int ret;
|
|
|
|
|
do {
|
|
|
|
|
strm.next_out = reinterpret_cast<Bytef*>(buffer);
|
|
|
|
|
strm.avail_out = sizeof(buffer);
|
|
|
|
|
|
|
|
|
|
ret = deflate(&strm, strm.avail_in ? Z_NO_FLUSH : Z_FINISH);
|
|
|
|
|
if (ret != Z_OK && ret != Z_STREAM_END) {
|
|
|
|
|
qWarning() << "Compression error:" << zError(ret);
|
|
|
|
|
deflateEnd(&strm);
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
compressed.append(buffer, sizeof(buffer) - strm.avail_out);
|
|
|
|
|
} while (ret != Z_STREAM_END);
|
|
|
|
|
|
|
|
|
|
deflateEnd(&strm);
|
|
|
|
|
return compressed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QByteArray Compression::DecompressDeflate(const QByteArray &aCompressedData) {
|
|
|
|
|
if (aCompressedData.isEmpty())
|
|
|
|
|
return {};
|
|
|
|
|
|
|
|
|
|
z_stream strm{};
|
|
|
|
|
strm.next_in = reinterpret_cast<Bytef*>(const_cast<char*>(aCompressedData.data()));
|
|
|
|
|
strm.avail_in = static_cast<uInt>(aCompressedData.size());
|
|
|
|
|
|
|
|
|
|
// Negative window bits (-MAX_WBITS) indicate raw DEFLATE data.
|
|
|
|
|
if (inflateInit2(&strm, -MAX_WBITS) != Z_OK) {
|
|
|
|
|
qWarning() << "Failed to initialize DEFLATE for decompression.";
|
|
|
|
|
return QByteArray();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QByteArray decompressed;
|
|
|
|
|
char buffer[4096];
|
|
|
|
|
|
|
|
|
|
int ret;
|
|
|
|
|
do {
|
|
|
|
|
strm.next_out = reinterpret_cast<Bytef*>(buffer);
|
|
|
|
|
strm.avail_out = sizeof(buffer);
|
|
|
|
|
|
|
|
|
|
ret = inflate(&strm, Z_NO_FLUSH);
|
|
|
|
|
|
|
|
|
|
if (ret != Z_OK && ret != Z_STREAM_END) {
|
|
|
|
|
qWarning() << "DEFLATE decompression error:" << zError(ret);
|
|
|
|
|
inflateEnd(&strm);
|
|
|
|
|
return QByteArray();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
decompressed.append(buffer, sizeof(buffer) - strm.avail_out);
|
|
|
|
|
} while (ret != Z_STREAM_END);
|
|
|
|
|
|
|
|
|
|
inflateEnd(&strm);
|
|
|
|
|
return decompressed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QByteArray Compression::CompressDeflate(const QByteArray &aData) {
|
|
|
|
|
return CompressDeflateWithSettings(aData);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QByteArray Compression::CompressDeflateWithSettings(const QByteArray &aData, int aCompressionLevel, int aWindowBits, int aMemLevel, int aStrategy, const QByteArray &aDictionary) {
|
2025-04-23 00:09:35 -04:00
|
|
|
Q_UNUSED(aDictionary);
|
|
|
|
|
|
2025-04-04 20:42:41 -04:00
|
|
|
if (aData.isEmpty())
|
|
|
|
|
return QByteArray();
|
|
|
|
|
|
|
|
|
|
z_stream strm{};
|
|
|
|
|
|
|
|
|
|
// Negative window bits (-MAX_WBITS) indicate raw DEFLATE data.
|
|
|
|
|
if (deflateInit2(&strm, aCompressionLevel, Z_DEFLATED, -aWindowBits, aMemLevel, aStrategy) != Z_OK) {
|
|
|
|
|
qWarning() << "Failed to initialize DEFLATE for compression.";
|
|
|
|
|
return QByteArray();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
strm.next_in = reinterpret_cast<Bytef*>(const_cast<char*>(aData.data()));
|
|
|
|
|
strm.avail_in = static_cast<uInt>(aData.size());
|
|
|
|
|
|
|
|
|
|
QByteArray compressed;
|
|
|
|
|
char buffer[4096];
|
|
|
|
|
|
|
|
|
|
int ret;
|
|
|
|
|
do {
|
|
|
|
|
strm.next_out = reinterpret_cast<Bytef*>(buffer);
|
|
|
|
|
strm.avail_out = sizeof(buffer);
|
|
|
|
|
|
|
|
|
|
ret = deflate(&strm, strm.avail_in ? Z_NO_FLUSH : Z_FINISH);
|
|
|
|
|
|
|
|
|
|
if (ret != Z_OK && ret != Z_STREAM_END) {
|
|
|
|
|
qWarning() << "DEFLATE compression error:" << zError(ret);
|
|
|
|
|
deflateEnd(&strm);
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
compressed.append(buffer, sizeof(buffer) - strm.avail_out);
|
|
|
|
|
} while (ret != Z_STREAM_END);
|
|
|
|
|
|
|
|
|
|
deflateEnd(&strm);
|
|
|
|
|
return compressed;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-04 22:31:11 -04:00
|
|
|
QByteArray Compression::DecompressLZO(const QByteArray &aCompressedData, quint32 aDestSize) {
|
|
|
|
|
static bool ok = (lzo_init() == LZO_E_OK);
|
|
|
|
|
if (!ok)
|
|
|
|
|
throw std::runtime_error("lzo_init failed");
|
|
|
|
|
|
|
|
|
|
QByteArray dst(aDestSize, Qt::Uninitialized);
|
|
|
|
|
lzo_uint out = aDestSize;
|
|
|
|
|
|
|
|
|
|
int rc = lzo1x_decompress_safe(
|
|
|
|
|
reinterpret_cast<const lzo_bytep>(aCompressedData.constData()),
|
|
|
|
|
static_cast<lzo_uint>(aCompressedData.size()),
|
|
|
|
|
reinterpret_cast<lzo_bytep>(dst.data()),
|
|
|
|
|
&out,
|
|
|
|
|
nullptr);
|
|
|
|
|
|
|
|
|
|
if (rc != LZO_E_OK || out != aDestSize)
|
|
|
|
|
throw std::runtime_error("LZO decompression error");
|
|
|
|
|
|
|
|
|
|
return dst;
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-04 20:42:41 -04:00
|
|
|
QByteArray Compression::DecompressOodle(const QByteArray &aCompressedData, quint32 aDecompressedSize) {
|
|
|
|
|
return pDecompressOodle(aCompressedData, aCompressedData.length(), aDecompressedSize);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QByteArray Compression::CompressOodle(const QByteArray &aData) {
|
|
|
|
|
quint32 maxSize = pGetOodleCompressedBounds(aData.length());
|
|
|
|
|
QByteArray compressedData = pCompressOodle(aData, aData.length(),
|
2025-04-23 00:09:35 -04:00
|
|
|
maxSize, OodleFormat::Kraken, OodleCompressionLevel::Optimal5);
|
2025-04-04 20:42:41 -04:00
|
|
|
|
|
|
|
|
return compressedData.mid(0, maxSize);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
quint32 Compression::pGetOodleCompressedBounds(quint32 aBufferSize) {
|
|
|
|
|
return aBufferSize + 274 * ((aBufferSize + 0x3FFFF) / 0x400000);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QByteArray Compression::pCompressOodle(QByteArray aBuffer, quint32 aBufferSize, quint32 aOutputBufferSize, OodleFormat aformat, OodleCompressionLevel alevel) {
|
|
|
|
|
QLibrary oodleLib("oo2core_8_win64");
|
|
|
|
|
|
|
|
|
|
if (!oodleLib.load()) {
|
|
|
|
|
qDebug() << "Failed to load DLL:" << oodleLib.errorString();
|
|
|
|
|
return QByteArray();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
OodleLZ_CompressFunc OodleLZ_Compress =
|
|
|
|
|
(OodleLZ_CompressFunc)oodleLib.resolve("OodleLZ_Compress");
|
|
|
|
|
|
|
|
|
|
if (!OodleLZ_Compress) {
|
|
|
|
|
qDebug() << "Failed to resolve function:" << oodleLib.errorString();
|
|
|
|
|
return QByteArray();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::byte *outputBuffer = new std::byte[aOutputBufferSize];
|
|
|
|
|
|
|
|
|
|
if (aBuffer.length() > 0 && aBufferSize > 0 && aOutputBufferSize > 0)
|
|
|
|
|
OodleLZ_Compress(aformat, reinterpret_cast<std::byte*>(aBuffer.data()), aBufferSize, outputBuffer, alevel, 0, 0, 0);
|
|
|
|
|
|
|
|
|
|
return QByteArray(reinterpret_cast<const char*>(outputBuffer), aOutputBufferSize);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QByteArray Compression::pDecompressOodle(QByteArray aBuffer, quint32 aBufferSize, quint32 aOutputBufferSize) {
|
|
|
|
|
QLibrary oodleLib("oo2core_8_win64");
|
|
|
|
|
|
|
|
|
|
if (!oodleLib.load()) {
|
|
|
|
|
qDebug() << "Failed to load DLL:" << oodleLib.errorString();
|
|
|
|
|
return QByteArray();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
OodleLZ_DecompressFunc OodleLZ_Decompress =
|
|
|
|
|
(OodleLZ_DecompressFunc)oodleLib.resolve("OodleLZ_Decompress");
|
|
|
|
|
|
|
|
|
|
if (!OodleLZ_Decompress) {
|
|
|
|
|
qDebug() << "Failed to resolve function:" << oodleLib.errorString();
|
|
|
|
|
return QByteArray();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::byte *outputBuffer = new std::byte[aOutputBufferSize];
|
|
|
|
|
|
|
|
|
|
if (aBuffer.length() > 0 && aBufferSize > 0 && aOutputBufferSize > 0)
|
|
|
|
|
OodleLZ_Decompress(reinterpret_cast<std::byte*>(aBuffer.data()), aBufferSize, outputBuffer, aOutputBufferSize, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
|
|
|
|
|
|
|
|
|
|
return QByteArray(reinterpret_cast<const char*>(outputBuffer), aOutputBufferSize);
|
|
|
|
|
}
|