2025-04-04 20:42:41 -04:00
|
|
|
#include "compression.h"
|
2025-09-10 21:54:57 -04:00
|
|
|
#include "minilzo.h"
|
2026-01-13 14:27:34 -05:00
|
|
|
|
|
|
|
|
#ifdef Q_OS_WIN
|
2025-09-10 21:54:57 -04:00
|
|
|
#include "xcompress.h"
|
2026-01-13 14:27:34 -05:00
|
|
|
#endif
|
2025-04-23 00:09:35 -04:00
|
|
|
|
2026-01-12 20:52:18 -05:00
|
|
|
// libmspack for LZX decompression
|
|
|
|
|
extern "C" {
|
|
|
|
|
#include "mspack/mspack.h"
|
|
|
|
|
#include "mspack/lzx.h"
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-23 00:09:35 -04:00
|
|
|
#include <QLibrary>
|
|
|
|
|
#include <QDebug>
|
|
|
|
|
#include <QFile>
|
2026-01-08 00:38:25 -05:00
|
|
|
#include <QDir>
|
|
|
|
|
#include <QProcess>
|
|
|
|
|
#include <QtEndian>
|
2026-01-12 20:52:18 -05:00
|
|
|
#include <cstdio>
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// Memory-based mspack_system for LZX decompression
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
struct MemoryFile {
|
|
|
|
|
const unsigned char* data;
|
|
|
|
|
size_t size;
|
|
|
|
|
size_t pos;
|
|
|
|
|
size_t rest; // Remaining bytes in current block (Xbox 360 LZX)
|
|
|
|
|
QByteArray* output; // For write operations
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static struct mspack_file* mem_open(struct mspack_system* /*self*/, const char* filename, int mode) {
|
|
|
|
|
// filename is actually a pointer to our MemoryFile struct
|
|
|
|
|
(void)mode;
|
|
|
|
|
return reinterpret_cast<struct mspack_file*>(const_cast<char*>(filename));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void mem_close(struct mspack_file* /*file*/) {
|
|
|
|
|
// Nothing to do - we don't own the memory
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Simple mem_read without block headers - for raw LZX streams
|
|
|
|
|
static int mem_read_raw(struct mspack_file* file, void* buffer, int bytes) {
|
|
|
|
|
MemoryFile* mf = reinterpret_cast<MemoryFile*>(file);
|
|
|
|
|
if (!mf || !mf->data) return -1;
|
|
|
|
|
|
|
|
|
|
size_t remaining = mf->size - mf->pos;
|
|
|
|
|
size_t toRead = static_cast<size_t>(bytes);
|
|
|
|
|
if (toRead > remaining) toRead = remaining;
|
|
|
|
|
if (toRead == 0) return 0;
|
|
|
|
|
|
|
|
|
|
memcpy(buffer, mf->data + mf->pos, toRead);
|
|
|
|
|
mf->pos += toRead;
|
|
|
|
|
|
|
|
|
|
return static_cast<int>(toRead);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// UE Viewer / Xbox 360 LZX block format with per-block headers
|
|
|
|
|
static int mem_read_xbox(struct mspack_file* file, void* buffer, int bytes) {
|
|
|
|
|
MemoryFile* mf = reinterpret_cast<MemoryFile*>(file);
|
|
|
|
|
if (!mf || !mf->data) return -1;
|
|
|
|
|
|
|
|
|
|
// UE Viewer / Xbox 360 LZX block format:
|
|
|
|
|
// Each block has a header:
|
|
|
|
|
// - If first byte is 0xFF: 5-byte header (0xFF + 2-byte uncompressed BE + 2-byte compressed BE)
|
|
|
|
|
// - Otherwise: 2-byte compressed size header (BE)
|
|
|
|
|
if (mf->rest == 0) {
|
|
|
|
|
// Need to read next block header
|
|
|
|
|
if (mf->pos >= mf->size) return 0; // EOF
|
|
|
|
|
|
|
|
|
|
if (mf->data[mf->pos] == 0xFF) {
|
|
|
|
|
// 5-byte header: [0]=0xFF, [1,2]=uncompressed size (BE), [3,4]=compressed size (BE)
|
|
|
|
|
if (mf->pos + 5 > mf->size) return 0;
|
|
|
|
|
mf->rest = (static_cast<size_t>(mf->data[mf->pos + 3]) << 8) |
|
|
|
|
|
(static_cast<size_t>(mf->data[mf->pos + 4]));
|
|
|
|
|
mf->pos += 5;
|
|
|
|
|
} else {
|
|
|
|
|
// 2-byte header: [0,1]=compressed size (BE)
|
|
|
|
|
if (mf->pos + 2 > mf->size) return 0;
|
|
|
|
|
mf->rest = (static_cast<size_t>(mf->data[mf->pos + 0]) << 8) |
|
|
|
|
|
(static_cast<size_t>(mf->data[mf->pos + 1]));
|
|
|
|
|
mf->pos += 2;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clamp to remaining data
|
|
|
|
|
if (mf->rest > mf->size - mf->pos) {
|
|
|
|
|
mf->rest = mf->size - mf->pos;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Read from current block
|
|
|
|
|
size_t toRead = static_cast<size_t>(bytes);
|
|
|
|
|
if (toRead > mf->rest) toRead = mf->rest;
|
|
|
|
|
if (toRead == 0) return 0;
|
|
|
|
|
|
|
|
|
|
memcpy(buffer, mf->data + mf->pos, toRead);
|
|
|
|
|
mf->pos += toRead;
|
|
|
|
|
mf->rest -= toRead;
|
|
|
|
|
|
|
|
|
|
return static_cast<int>(toRead);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Dead Rising 2 LZX format:
|
|
|
|
|
// Per-block: 4-byte BE block_size + 0xFF + 2-byte uncompressed BE + 2-byte compressed BE + LZX data
|
|
|
|
|
static int mem_read_deadrising(struct mspack_file* file, void* buffer, int bytes) {
|
|
|
|
|
MemoryFile* mf = reinterpret_cast<MemoryFile*>(file);
|
|
|
|
|
if (!mf || !mf->data) return -1;
|
|
|
|
|
|
|
|
|
|
if (mf->rest == 0) {
|
|
|
|
|
// Need to read next block header
|
|
|
|
|
if (mf->pos >= mf->size) return 0; // EOF
|
|
|
|
|
|
|
|
|
|
// Dead Rising has 4-byte BE block size header before each block
|
|
|
|
|
// Format: [0-3] = block_size (BE), [4] = 0xFF, [5-6] = uncompressed (BE), [7-8] = compressed (BE)
|
|
|
|
|
if (mf->pos + 9 > mf->size) return 0; // Need at least 9 bytes for headers
|
|
|
|
|
|
|
|
|
|
// Skip the 4-byte block size header (we don't need it, we use the 2-byte compressed size)
|
|
|
|
|
mf->pos += 4;
|
|
|
|
|
|
|
|
|
|
// Now parse UE Viewer format
|
|
|
|
|
if (mf->data[mf->pos] == 0xFF) {
|
|
|
|
|
// 5-byte header: [0]=0xFF, [1,2]=uncompressed (BE), [3,4]=compressed (BE)
|
|
|
|
|
if (mf->pos + 5 > mf->size) return 0;
|
|
|
|
|
mf->rest = (static_cast<size_t>(mf->data[mf->pos + 3]) << 8) |
|
|
|
|
|
(static_cast<size_t>(mf->data[mf->pos + 4]));
|
|
|
|
|
mf->pos += 5;
|
|
|
|
|
} else {
|
|
|
|
|
// 2-byte header: [0,1]=compressed size (BE)
|
|
|
|
|
if (mf->pos + 2 > mf->size) return 0;
|
|
|
|
|
mf->rest = (static_cast<size_t>(mf->data[mf->pos + 0]) << 8) |
|
|
|
|
|
(static_cast<size_t>(mf->data[mf->pos + 1]));
|
|
|
|
|
mf->pos += 2;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clamp to remaining data
|
|
|
|
|
if (mf->rest > mf->size - mf->pos) {
|
|
|
|
|
mf->rest = mf->size - mf->pos;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Read from current block
|
|
|
|
|
size_t toRead = static_cast<size_t>(bytes);
|
|
|
|
|
if (toRead > mf->rest) toRead = mf->rest;
|
|
|
|
|
if (toRead == 0) return 0;
|
|
|
|
|
|
|
|
|
|
memcpy(buffer, mf->data + mf->pos, toRead);
|
|
|
|
|
mf->pos += toRead;
|
|
|
|
|
mf->rest -= toRead;
|
|
|
|
|
|
|
|
|
|
return static_cast<int>(toRead);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Default mem_read - use Dead Rising format
|
|
|
|
|
static int mem_read(struct mspack_file* file, void* buffer, int bytes) {
|
|
|
|
|
return mem_read_deadrising(file, buffer, bytes);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int mem_write(struct mspack_file* file, void* buffer, int bytes) {
|
|
|
|
|
MemoryFile* mf = reinterpret_cast<MemoryFile*>(file);
|
|
|
|
|
if (!mf || !mf->output) return -1;
|
|
|
|
|
|
|
|
|
|
mf->output->append(reinterpret_cast<const char*>(buffer), bytes);
|
|
|
|
|
return bytes;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int mem_seek(struct mspack_file* file, off_t offset, int mode) {
|
|
|
|
|
MemoryFile* mf = reinterpret_cast<MemoryFile*>(file);
|
|
|
|
|
if (!mf) return -1;
|
|
|
|
|
|
|
|
|
|
switch (mode) {
|
|
|
|
|
case MSPACK_SYS_SEEK_START:
|
|
|
|
|
mf->pos = static_cast<size_t>(offset);
|
|
|
|
|
break;
|
|
|
|
|
case MSPACK_SYS_SEEK_CUR:
|
|
|
|
|
mf->pos += static_cast<size_t>(offset);
|
|
|
|
|
break;
|
|
|
|
|
case MSPACK_SYS_SEEK_END:
|
|
|
|
|
mf->pos = mf->size + static_cast<size_t>(offset);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static off_t mem_tell(struct mspack_file* file) {
|
|
|
|
|
MemoryFile* mf = reinterpret_cast<MemoryFile*>(file);
|
|
|
|
|
return mf ? static_cast<off_t>(mf->pos) : -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void mem_message(struct mspack_file* /*file*/, const char* format, ...) {
|
|
|
|
|
(void)format;
|
|
|
|
|
// Suppress messages
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void* mem_alloc(struct mspack_system* /*self*/, size_t bytes) {
|
|
|
|
|
return malloc(bytes);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void mem_free(void* ptr) {
|
|
|
|
|
free(ptr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void mem_copy(void* src, void* dest, size_t bytes) {
|
|
|
|
|
memcpy(dest, src, bytes);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static struct mspack_system g_memSystem = {
|
|
|
|
|
mem_open,
|
|
|
|
|
mem_close,
|
|
|
|
|
mem_read,
|
|
|
|
|
mem_write,
|
|
|
|
|
mem_seek,
|
|
|
|
|
mem_tell,
|
|
|
|
|
mem_message,
|
|
|
|
|
mem_alloc,
|
|
|
|
|
mem_free,
|
|
|
|
|
mem_copy,
|
|
|
|
|
nullptr // null_ptr
|
|
|
|
|
};
|
2025-04-23 00:09:35 -04:00
|
|
|
|
2026-01-08 00:54:57 -05:00
|
|
|
// Static member initialization
|
|
|
|
|
QString Compression::s_quickBmsPath;
|
|
|
|
|
|
|
|
|
|
void Compression::setQuickBmsPath(const QString &path)
|
|
|
|
|
{
|
|
|
|
|
s_quickBmsPath = path;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QString Compression::quickBmsPath()
|
|
|
|
|
{
|
|
|
|
|
return s_quickBmsPath;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-13 14:27:34 -05:00
|
|
|
#ifdef Q_OS_WIN
|
2025-04-23 00:09:35 -04:00
|
|
|
QByteArray Compression::CompressXMem(const QByteArray &data)
|
|
|
|
|
{
|
2025-09-10 21:54:57 -04:00
|
|
|
XMEMCODEC_PARAMETERS_LZX lzxParams = {};
|
|
|
|
|
lzxParams.Flags = 0;
|
|
|
|
|
lzxParams.WindowSize = 0x20000;
|
|
|
|
|
lzxParams.CompressionPartitionSize = 0x80000;
|
2025-04-23 00:09:35 -04:00
|
|
|
|
2025-09-10 21:54:57 -04:00
|
|
|
XMEMCOMPRESSION_CONTEXT ctx = nullptr;
|
|
|
|
|
if (FAILED(XMemCreateCompressionContext(XMEMCODEC_LZX, &lzxParams, 0, &ctx)) || !ctx)
|
|
|
|
|
return QByteArray();
|
2025-04-23 00:09:35 -04:00
|
|
|
|
2025-09-10 21:54:57 -04:00
|
|
|
SIZE_T estimatedSize = data.size() + XCOMPRESS_LZX_BLOCK_GROWTH_SIZE_MAX;
|
|
|
|
|
QByteArray output(static_cast<int>(estimatedSize), 0);
|
|
|
|
|
SIZE_T actualSize = estimatedSize;
|
2025-04-23 00:09:35 -04:00
|
|
|
|
2025-09-10 21:54:57 -04:00
|
|
|
HRESULT hr = XMemCompress(ctx, output.data(), &actualSize, data.constData(), data.size());
|
|
|
|
|
XMemDestroyCompressionContext(ctx);
|
2025-04-23 00:09:35 -04:00
|
|
|
|
2025-09-10 21:54:57 -04:00
|
|
|
if (FAILED(hr))
|
|
|
|
|
return QByteArray();
|
2025-04-23 00:09:35 -04:00
|
|
|
|
2025-09-10 21:54:57 -04:00
|
|
|
output.resize(static_cast<int>(actualSize));
|
|
|
|
|
return output;
|
2025-04-23 00:09:35 -04:00
|
|
|
}
|
2026-01-13 14:27:34 -05:00
|
|
|
#else
|
|
|
|
|
QByteArray Compression::CompressXMem(const QByteArray &)
|
|
|
|
|
{
|
|
|
|
|
qWarning() << "XMem compression not available on this platform";
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
#endif
|
2025-04-23 00:09:35 -04:00
|
|
|
|
2026-01-13 14:27:34 -05:00
|
|
|
#ifdef Q_OS_WIN
|
2025-09-15 18:52:05 -04:00
|
|
|
QByteArray Compression::DecompressXMem(const QByteArray &data,
|
|
|
|
|
int flags, int windowSize, int partSize)
|
2025-04-23 00:09:35 -04:00
|
|
|
{
|
2026-01-08 00:38:25 -05:00
|
|
|
if (data.size() < 8)
|
2025-09-10 21:54:57 -04:00
|
|
|
return {};
|
2025-06-04 22:31:11 -04:00
|
|
|
|
2026-01-08 00:38:25 -05:00
|
|
|
// Read header identifier (big-endian in file)
|
|
|
|
|
quint32 identifier = qFromBigEndian<quint32>(reinterpret_cast<const uchar*>(data.constData()));
|
|
|
|
|
|
|
|
|
|
if (identifier == 0x0FF512EE) {
|
|
|
|
|
// LZXNATIVE format - full header with codec params and block structure
|
|
|
|
|
return DecompressXMemNative(data);
|
|
|
|
|
} else if (identifier == 0x0FF512ED) {
|
|
|
|
|
// LZXTDECODE format - simpler format
|
|
|
|
|
return DecompressXMemTDecode(data, flags, windowSize, partSize);
|
|
|
|
|
} else {
|
|
|
|
|
// No header - treat as raw LZX data with provided params
|
|
|
|
|
return DecompressXMemRaw(data, flags, windowSize, partSize);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QByteArray Compression::DecompressXMemNative(const QByteArray &data)
|
|
|
|
|
{
|
|
|
|
|
if (data.size() < 40) {
|
|
|
|
|
qWarning() << "LZXNATIVE header too short";
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const uchar* ptr = reinterpret_cast<const uchar*>(data.constData());
|
|
|
|
|
|
|
|
|
|
// Parse XCOMPRESS_FILE_HEADER_LZXNATIVE (all fields big-endian in file)
|
|
|
|
|
// Offset 0-3: Identifier (already validated)
|
|
|
|
|
// Offset 4-5: Version
|
|
|
|
|
// Offset 6-7: Reserved
|
|
|
|
|
// Offset 8-11: ContextFlags
|
|
|
|
|
// Offset 12-15: CodecParams.Flags
|
|
|
|
|
// Offset 16-19: CodecParams.WindowSize
|
|
|
|
|
// Offset 20-23: CodecParams.CompressionPartitionSize
|
|
|
|
|
// Offset 24-27: UncompressedSizeHigh
|
|
|
|
|
// Offset 28-31: UncompressedSizeLow
|
|
|
|
|
// Offset 32-35: CompressedSizeHigh
|
|
|
|
|
// Offset 36-39: CompressedSizeLow
|
|
|
|
|
// Offset 40-43: UncompressedBlockSize
|
|
|
|
|
// Offset 44-47: CompressedBlockSizeMax
|
|
|
|
|
|
2025-09-10 21:54:57 -04:00
|
|
|
XMEMCODEC_PARAMETERS_LZX lzxParams = {};
|
2026-01-08 00:38:25 -05:00
|
|
|
lzxParams.Flags = qFromBigEndian<quint32>(ptr + 12);
|
|
|
|
|
lzxParams.WindowSize = qFromBigEndian<quint32>(ptr + 16);
|
|
|
|
|
lzxParams.CompressionPartitionSize = qFromBigEndian<quint32>(ptr + 20);
|
2025-04-23 00:09:35 -04:00
|
|
|
|
2026-01-08 00:38:25 -05:00
|
|
|
quint64 uncompressedSize = (static_cast<quint64>(qFromBigEndian<quint32>(ptr + 24)) << 32) |
|
|
|
|
|
qFromBigEndian<quint32>(ptr + 28);
|
|
|
|
|
quint32 uncompressedBlockSize = qFromBigEndian<quint32>(ptr + 40);
|
2025-09-15 18:52:05 -04:00
|
|
|
|
2026-01-08 00:38:25 -05:00
|
|
|
qDebug() << "LZXNATIVE: windowSize=" << lzxParams.WindowSize
|
|
|
|
|
<< "partSize=" << lzxParams.CompressionPartitionSize
|
|
|
|
|
<< "uncompressedSize=" << uncompressedSize
|
|
|
|
|
<< "blockSize=" << uncompressedBlockSize;
|
2025-09-15 18:52:05 -04:00
|
|
|
|
2026-01-08 00:38:25 -05:00
|
|
|
// Create decompression context
|
|
|
|
|
XMEMDECOMPRESSION_CONTEXT ctx = nullptr;
|
|
|
|
|
HRESULT hr = XMemCreateDecompressionContext(XMEMCODEC_LZX, &lzxParams, 0, &ctx);
|
|
|
|
|
if (FAILED(hr) || !ctx) {
|
|
|
|
|
qWarning() << "Failed to create LZX decompression context, hr=" << Qt::hex << hr;
|
2025-09-10 21:54:57 -04:00
|
|
|
return {};
|
2025-09-15 18:52:05 -04:00
|
|
|
}
|
2025-06-04 22:31:11 -04:00
|
|
|
|
2025-09-15 18:52:05 -04:00
|
|
|
QByteArray output;
|
2026-01-08 00:38:25 -05:00
|
|
|
output.reserve(static_cast<int>(uncompressedSize));
|
2025-04-23 00:09:35 -04:00
|
|
|
|
2026-01-08 00:38:25 -05:00
|
|
|
// Data starts after 48-byte header, in blocks prefixed by 4-byte size
|
|
|
|
|
int offset = 48;
|
|
|
|
|
while (offset + 4 <= data.size()) {
|
|
|
|
|
quint32 compressedBlockSize = qFromBigEndian<quint32>(ptr + offset);
|
|
|
|
|
offset += 4;
|
|
|
|
|
|
|
|
|
|
if (compressedBlockSize == 0 || offset + static_cast<int>(compressedBlockSize) > data.size())
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
// Decompress this block
|
|
|
|
|
QByteArray blockOut(uncompressedBlockSize, Qt::Uninitialized);
|
|
|
|
|
SIZE_T destSize = blockOut.size();
|
|
|
|
|
SIZE_T srcSize = compressedBlockSize;
|
|
|
|
|
|
|
|
|
|
hr = XMemDecompress(ctx, blockOut.data(), &destSize, ptr + offset, srcSize);
|
|
|
|
|
if (FAILED(hr)) {
|
|
|
|
|
qWarning() << "XMemDecompress block failed, hr=" << Qt::hex << hr;
|
|
|
|
|
XMemDestroyDecompressionContext(ctx);
|
|
|
|
|
return output; // Return what we have
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
output.append(blockOut.constData(), static_cast<int>(destSize));
|
|
|
|
|
offset += compressedBlockSize;
|
|
|
|
|
|
|
|
|
|
// Reset context for next block
|
|
|
|
|
XMemResetDecompressionContext(ctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
XMemDestroyDecompressionContext(ctx);
|
|
|
|
|
return output;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QByteArray Compression::DecompressXMemTDecode(const QByteArray &data, int flags, int windowSize, int partSize)
|
|
|
|
|
{
|
|
|
|
|
Q_UNUSED(flags);
|
|
|
|
|
Q_UNUSED(windowSize);
|
|
|
|
|
Q_UNUSED(partSize);
|
|
|
|
|
|
|
|
|
|
if (data.size() < 8)
|
|
|
|
|
return {};
|
|
|
|
|
|
|
|
|
|
// LZXTDECODE (0x0FF512ED) - Use QuickBMS as external tool since xcompress64.dll is unreliable
|
|
|
|
|
// Write temp file, run QuickBMS, read result
|
|
|
|
|
|
|
|
|
|
QString tempDir = QDir::tempPath();
|
|
|
|
|
QString inputFile = tempDir + "/xmem_input.bin";
|
|
|
|
|
QString outputFile = tempDir + "/xmem_output.bin";
|
|
|
|
|
QString bmsScript = tempDir + "/xmem_decomp.bms";
|
|
|
|
|
|
|
|
|
|
// Write input data
|
|
|
|
|
QFile inFile(inputFile);
|
|
|
|
|
if (!inFile.open(QIODevice::WriteOnly)) {
|
|
|
|
|
qWarning() << "Failed to create temp input file";
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
inFile.write(data);
|
|
|
|
|
inFile.close();
|
|
|
|
|
|
|
|
|
|
// Write BMS script
|
|
|
|
|
QFile scriptFile(bmsScript);
|
|
|
|
|
if (!scriptFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
|
|
|
|
qWarning() << "Failed to create BMS script";
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
scriptFile.write("comtype xmemdecompress\n"
|
|
|
|
|
"get SIZE asize\n"
|
|
|
|
|
"clog \"\" 0 SIZE SIZE\n");
|
|
|
|
|
scriptFile.close();
|
|
|
|
|
|
|
|
|
|
// Run QuickBMS
|
|
|
|
|
QProcess proc;
|
|
|
|
|
proc.setWorkingDirectory(tempDir);
|
2026-01-08 00:54:57 -05:00
|
|
|
QString quickbmsExe = quickBmsPath();
|
|
|
|
|
if (quickbmsExe.isEmpty()) {
|
|
|
|
|
qWarning() << "QuickBMS path not configured";
|
|
|
|
|
return {};
|
|
|
|
|
}
|
2026-01-08 00:38:25 -05:00
|
|
|
QStringList args = {"-o", "-O", outputFile, bmsScript, inputFile};
|
|
|
|
|
|
2026-01-08 00:54:57 -05:00
|
|
|
proc.start(quickbmsExe, args);
|
2026-01-08 00:38:25 -05:00
|
|
|
if (!proc.waitForFinished(30000)) {
|
2026-01-08 00:54:57 -05:00
|
|
|
qWarning() << "QuickBMS timeout or failed to start:" << quickbmsExe;
|
2026-01-08 00:38:25 -05:00
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (proc.exitCode() != 0) {
|
|
|
|
|
qWarning() << "QuickBMS failed:" << proc.readAllStandardError();
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Read output
|
|
|
|
|
QFile outFile(outputFile);
|
|
|
|
|
if (!outFile.open(QIODevice::ReadOnly)) {
|
|
|
|
|
qWarning() << "Failed to read QuickBMS output";
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
QByteArray result = outFile.readAll();
|
|
|
|
|
outFile.close();
|
|
|
|
|
|
|
|
|
|
// Cleanup
|
|
|
|
|
QFile::remove(inputFile);
|
|
|
|
|
QFile::remove(outputFile);
|
|
|
|
|
QFile::remove(bmsScript);
|
|
|
|
|
|
|
|
|
|
qDebug() << "TDECODE via QuickBMS:" << data.size() << "->" << result.size() << "bytes";
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QByteArray Compression::DecompressXMemRaw(const QByteArray &data, int flags, int windowSize, int partSize)
|
|
|
|
|
{
|
|
|
|
|
if (data.isEmpty())
|
|
|
|
|
return {};
|
|
|
|
|
|
|
|
|
|
XMEMCODEC_PARAMETERS_LZX lzxParams = {};
|
|
|
|
|
lzxParams.Flags = flags;
|
|
|
|
|
lzxParams.WindowSize = windowSize;
|
|
|
|
|
lzxParams.CompressionPartitionSize = partSize;
|
|
|
|
|
|
|
|
|
|
XMEMDECOMPRESSION_CONTEXT ctx = nullptr;
|
|
|
|
|
HRESULT hr = XMemCreateDecompressionContext(XMEMCODEC_LZX, &lzxParams, 0, &ctx);
|
|
|
|
|
if (FAILED(hr) || !ctx) {
|
|
|
|
|
qWarning() << "Failed to create raw LZX context";
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const uchar* nextIn = reinterpret_cast<const uchar*>(data.constData());
|
2025-09-15 18:52:05 -04:00
|
|
|
SIZE_T availIn = data.size();
|
2025-04-23 00:09:35 -04:00
|
|
|
|
2026-01-08 00:38:25 -05:00
|
|
|
QByteArray output;
|
|
|
|
|
output.reserve(16 * 1024 * 1024);
|
|
|
|
|
|
|
|
|
|
QByteArray scratch(0x10000, Qt::Uninitialized);
|
2025-04-23 00:09:35 -04:00
|
|
|
|
2025-09-15 18:52:05 -04:00
|
|
|
while (availIn > 0) {
|
2026-01-08 00:38:25 -05:00
|
|
|
SIZE_T inSize = availIn;
|
|
|
|
|
SIZE_T outSize = scratch.size();
|
2025-09-15 18:52:05 -04:00
|
|
|
|
2026-01-08 00:38:25 -05:00
|
|
|
hr = XMemDecompressStream(ctx, scratch.data(), &outSize, nextIn, &inSize);
|
2025-09-15 18:52:05 -04:00
|
|
|
|
|
|
|
|
if (FAILED(hr)) {
|
2026-01-08 00:38:25 -05:00
|
|
|
qWarning() << "XMemDecompressStream raw failed, hr=" << Qt::hex << hr;
|
|
|
|
|
break;
|
2025-09-15 18:52:05 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (inSize == 0 && outSize == 0)
|
2026-01-08 00:38:25 -05:00
|
|
|
break;
|
2025-09-15 18:52:05 -04:00
|
|
|
|
|
|
|
|
output.append(scratch.constData(), static_cast<int>(outSize));
|
|
|
|
|
nextIn += inSize;
|
|
|
|
|
availIn -= inSize;
|
2025-09-10 21:54:57 -04:00
|
|
|
}
|
2025-04-23 00:09:35 -04:00
|
|
|
|
2025-09-15 18:52:05 -04:00
|
|
|
XMemDestroyDecompressionContext(ctx);
|
2025-09-10 21:54:57 -04:00
|
|
|
return output;
|
2025-04-23 00:09:35 -04:00
|
|
|
}
|
2026-01-13 14:27:34 -05:00
|
|
|
#else
|
|
|
|
|
// Non-Windows stub implementations
|
|
|
|
|
QByteArray Compression::DecompressXMem(const QByteArray &, int, int, int)
|
|
|
|
|
{
|
|
|
|
|
qWarning() << "XMem decompression not available on this platform";
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QByteArray Compression::DecompressXMemNative(const QByteArray &)
|
|
|
|
|
{
|
|
|
|
|
qWarning() << "XMem native decompression not available on this platform";
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QByteArray Compression::DecompressXMemTDecode(const QByteArray &, int, int, int)
|
|
|
|
|
{
|
|
|
|
|
qWarning() << "XMem TDecode decompression not available on this platform";
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QByteArray Compression::DecompressXMemRaw(const QByteArray &, int, int, int)
|
|
|
|
|
{
|
|
|
|
|
qWarning() << "XMem raw decompression not available on this platform";
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
#endif
|
2025-04-23 00:09:35 -04:00
|
|
|
|
2026-01-12 20:52:18 -05:00
|
|
|
// ============================================================================
|
|
|
|
|
// lzxdhelper-style LZX decompression (UE Viewer/Gildor approach)
|
|
|
|
|
// Uses their exact mspack_file structure and read function
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
struct LzxHelperFile {
|
|
|
|
|
unsigned char* buf;
|
|
|
|
|
int bufSize;
|
|
|
|
|
int pos;
|
|
|
|
|
int rest;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Simple raw read without block parsing (for testing)
|
|
|
|
|
static int lzx_helper_read_raw(struct mspack_file* file, void* buffer, int bytes) {
|
|
|
|
|
LzxHelperFile* f = reinterpret_cast<LzxHelperFile*>(file);
|
|
|
|
|
int remaining = f->bufSize - f->pos;
|
|
|
|
|
if (bytes > remaining) bytes = remaining;
|
|
|
|
|
if (bytes <= 0) return 0;
|
|
|
|
|
memcpy(buffer, f->buf + f->pos, bytes);
|
|
|
|
|
f->pos += bytes;
|
|
|
|
|
return bytes;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// UE Viewer-style block read
|
|
|
|
|
static int lzx_helper_read_blocks(struct mspack_file* file, void* buffer, int bytes) {
|
|
|
|
|
LzxHelperFile* f = reinterpret_cast<LzxHelperFile*>(file);
|
|
|
|
|
|
|
|
|
|
if (!f->rest) {
|
|
|
|
|
// Read block header
|
|
|
|
|
if (f->pos >= f->bufSize) return 0;
|
|
|
|
|
|
|
|
|
|
if (f->buf[f->pos] == 0xFF) {
|
|
|
|
|
// [0] = FF
|
|
|
|
|
// [1,2] = uncompressed block size
|
|
|
|
|
// [3,4] = compressed block size
|
|
|
|
|
if (f->pos + 5 > f->bufSize) return 0;
|
|
|
|
|
f->rest = (f->buf[f->pos + 3] << 8) | f->buf[f->pos + 4];
|
|
|
|
|
f->pos += 5;
|
|
|
|
|
} else {
|
|
|
|
|
// [0,1] = compressed size
|
|
|
|
|
if (f->pos + 2 > f->bufSize) return 0;
|
|
|
|
|
f->rest = (f->buf[f->pos + 0] << 8) | f->buf[f->pos + 1];
|
|
|
|
|
f->pos += 2;
|
|
|
|
|
}
|
|
|
|
|
if (f->rest > f->bufSize - f->pos)
|
|
|
|
|
f->rest = f->bufSize - f->pos;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (bytes > f->rest) bytes = f->rest;
|
|
|
|
|
if (bytes <= 0) return 0;
|
|
|
|
|
|
|
|
|
|
memcpy(buffer, f->buf + f->pos, bytes);
|
|
|
|
|
f->pos += bytes;
|
|
|
|
|
f->rest -= bytes;
|
|
|
|
|
return bytes;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int lzx_helper_read(struct mspack_file* file, void* buffer, int bytes) {
|
|
|
|
|
// Use raw read (Dead Rising 2 data has no UE block headers inside)
|
|
|
|
|
return lzx_helper_read_raw(file, buffer, bytes);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int lzx_helper_write(struct mspack_file* file, void* buffer, int bytes) {
|
|
|
|
|
LzxHelperFile* f = reinterpret_cast<LzxHelperFile*>(file);
|
|
|
|
|
memcpy(f->buf + f->pos, buffer, bytes);
|
|
|
|
|
f->pos += bytes;
|
|
|
|
|
return bytes;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void* lzx_helper_alloc(struct mspack_system*, size_t bytes) {
|
|
|
|
|
return malloc(bytes);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void lzx_helper_free(void* ptr) {
|
|
|
|
|
free(ptr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void lzx_helper_copy(void* src, void* dst, size_t bytes) {
|
|
|
|
|
memcpy(dst, src, bytes);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static struct mspack_system g_lzxHelperSystem = {
|
|
|
|
|
nullptr, // open
|
|
|
|
|
nullptr, // close
|
|
|
|
|
lzx_helper_read,
|
|
|
|
|
lzx_helper_write,
|
|
|
|
|
nullptr, // seek
|
|
|
|
|
nullptr, // tell
|
|
|
|
|
nullptr, // message
|
|
|
|
|
lzx_helper_alloc,
|
|
|
|
|
lzx_helper_free,
|
|
|
|
|
lzx_helper_copy,
|
|
|
|
|
nullptr
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
QByteArray Compression::DecompressDeadRisingLZX(const QByteArray &data)
|
|
|
|
|
{
|
|
|
|
|
// Dead Rising 2 LZX format:
|
|
|
|
|
// File Header (8 bytes):
|
|
|
|
|
// [0-3] u32_be total_uncompressed_size
|
|
|
|
|
// [4-7] u32_be window_size (0x8000 = 32KB)
|
|
|
|
|
//
|
|
|
|
|
// Per-Block (repeating):
|
|
|
|
|
// [0-3] u32_be block_size (includes UE header + LZX data)
|
|
|
|
|
// [4] 0xFF marker
|
|
|
|
|
// [5-6] u16_be block_uncompressed_size
|
|
|
|
|
// [7-8] u16_be block_compressed_size
|
|
|
|
|
// [9+] LZX compressed data
|
|
|
|
|
//
|
|
|
|
|
// Strip the 4-byte block headers and decompress as continuous stream.
|
|
|
|
|
|
|
|
|
|
if (data.size() < 8) {
|
|
|
|
|
qWarning() << "Dead Rising LZX data too short:" << data.size();
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const uchar* ptr = reinterpret_cast<const uchar*>(data.constData());
|
|
|
|
|
|
|
|
|
|
// Parse file header (big-endian)
|
|
|
|
|
quint32 uncompressedSize = qFromBigEndian<quint32>(ptr);
|
|
|
|
|
|
|
|
|
|
// Sanity check
|
|
|
|
|
if (uncompressedSize == 0 || uncompressedSize > 100 * 1024 * 1024) {
|
|
|
|
|
qWarning() << "Dead Rising LZX: unreasonable uncompressed size:" << uncompressedSize;
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Decompress each block independently and concatenate results
|
|
|
|
|
QByteArray output;
|
|
|
|
|
output.reserve(static_cast<int>(uncompressedSize));
|
|
|
|
|
|
|
|
|
|
int offset = 8;
|
|
|
|
|
while (output.size() < static_cast<int>(uncompressedSize) && offset + 4 < data.size()) {
|
|
|
|
|
quint32 blockSize = qFromBigEndian<quint32>(ptr + offset);
|
|
|
|
|
offset += 4;
|
|
|
|
|
if (blockSize == 0 || offset + static_cast<int>(blockSize) > data.size()) break;
|
|
|
|
|
|
|
|
|
|
// Parse UE header to get sizes
|
|
|
|
|
const uchar* blockPtr = ptr + offset;
|
|
|
|
|
int blockUncompressed = 0;
|
|
|
|
|
int compressedSize = 0;
|
|
|
|
|
int headerSize = 0;
|
|
|
|
|
|
|
|
|
|
if (blockPtr[0] == 0xFF) {
|
|
|
|
|
// 5-byte header: 0xFF + uncompressed(2) + compressed(2)
|
|
|
|
|
blockUncompressed = (blockPtr[1] << 8) | blockPtr[2];
|
|
|
|
|
compressedSize = (blockPtr[3] << 8) | blockPtr[4];
|
|
|
|
|
headerSize = 5;
|
|
|
|
|
} else {
|
|
|
|
|
// 2-byte header: compressed(2) - uncompressed defaults to 32KB
|
|
|
|
|
compressedSize = (blockPtr[0] << 8) | blockPtr[1];
|
|
|
|
|
blockUncompressed = 0x8000;
|
|
|
|
|
headerSize = 2;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Don't exceed remaining expected output
|
|
|
|
|
blockUncompressed = qMin(blockUncompressed,
|
|
|
|
|
static_cast<int>(uncompressedSize) - output.size());
|
|
|
|
|
|
|
|
|
|
if (compressedSize <= 0 || headerSize + compressedSize > static_cast<int>(blockSize)) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Extract raw LZX data for this block
|
|
|
|
|
QByteArray blockData(reinterpret_cast<const char*>(blockPtr + headerSize), compressedSize);
|
|
|
|
|
|
|
|
|
|
// Decompress this block with its own LZX context
|
|
|
|
|
QByteArray blockOutput(blockUncompressed, Qt::Uninitialized);
|
|
|
|
|
|
|
|
|
|
LzxHelperFile src, dst;
|
|
|
|
|
src.buf = reinterpret_cast<unsigned char*>(blockData.data());
|
|
|
|
|
src.bufSize = blockData.size();
|
|
|
|
|
src.pos = 0;
|
|
|
|
|
src.rest = 0;
|
|
|
|
|
|
|
|
|
|
dst.buf = reinterpret_cast<unsigned char*>(blockOutput.data());
|
|
|
|
|
dst.bufSize = blockUncompressed;
|
|
|
|
|
dst.pos = 0;
|
|
|
|
|
|
|
|
|
|
struct lzxd_stream* lzx = lzxd_init(
|
|
|
|
|
&g_lzxHelperSystem,
|
|
|
|
|
reinterpret_cast<struct mspack_file*>(&src),
|
|
|
|
|
reinterpret_cast<struct mspack_file*>(&dst),
|
|
|
|
|
15, // window_bits=15 (32KB window)
|
|
|
|
|
0, // reset_interval
|
|
|
|
|
256 * 1024, // input_buffer_size
|
|
|
|
|
blockUncompressed,
|
|
|
|
|
0 // is_delta
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!lzx) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int err = lzxd_decompress(lzx, blockUncompressed);
|
|
|
|
|
lzxd_free(lzx);
|
|
|
|
|
|
|
|
|
|
if (err != MSPACK_ERR_OK) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
output.append(blockOutput.constData(), dst.pos);
|
|
|
|
|
offset += static_cast<int>(blockSize);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return output.isEmpty() ? QByteArray{} : output;
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-23 00:09:35 -04:00
|
|
|
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)
|
|
|
|
|
{
|
2025-09-15 18:52:05 -04:00
|
|
|
QDataStream stream(bytes);
|
|
|
|
|
|
|
|
|
|
while (!stream.atEnd())
|
2025-06-04 22:31:11 -04:00
|
|
|
{
|
2025-09-15 18:52:05 -04:00
|
|
|
QByteArray testSegment = stream.device()->peek(2).toHex().toUpper();
|
|
|
|
|
if (testSegment == "7801" ||
|
|
|
|
|
testSegment == "785E" ||
|
|
|
|
|
testSegment == "789C" ||
|
|
|
|
|
testSegment == "78DA") {
|
|
|
|
|
return stream.device()->pos();
|
|
|
|
|
}
|
|
|
|
|
stream.skipRawData(1);
|
2025-06-04 22:31:11 -04:00
|
|
|
}
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-12 20:52:18 -05:00
|
|
|
QByteArray Compression::DecompressZlibAuto(const QByteArray &aCompressedData) {
|
|
|
|
|
if (aCompressedData.isEmpty())
|
|
|
|
|
return {};
|
|
|
|
|
|
|
|
|
|
z_stream strm{};
|
|
|
|
|
strm.zalloc = Z_NULL;
|
|
|
|
|
strm.zfree = Z_NULL;
|
|
|
|
|
strm.opaque = Z_NULL;
|
|
|
|
|
strm.avail_in = static_cast<uInt>(aCompressedData.size());
|
|
|
|
|
strm.next_in = reinterpret_cast<Bytef*>(const_cast<char*>(aCompressedData.data()));
|
|
|
|
|
|
|
|
|
|
// MAX_WBITS + 32 enables automatic header detection (zlib, gzip, or raw deflate)
|
|
|
|
|
if (inflateInit2(&strm, MAX_WBITS + 32) != Z_OK) {
|
|
|
|
|
qWarning() << "inflateInit2 (auto) failed";
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QByteArray decompressed;
|
|
|
|
|
QByteArray buffer(fmin(strm.avail_in * 2, 4096), Qt::Uninitialized);
|
|
|
|
|
|
|
|
|
|
int ret;
|
|
|
|
|
do {
|
|
|
|
|
strm.next_out = reinterpret_cast<Bytef*>(buffer.data());
|
|
|
|
|
strm.avail_out = buffer.size();
|
|
|
|
|
|
|
|
|
|
ret = inflate(&strm, Z_NO_FLUSH);
|
|
|
|
|
|
|
|
|
|
if (strm.avail_out < buffer.size()) {
|
|
|
|
|
decompressed.append(buffer.constData(), buffer.size() - strm.avail_out);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ret == Z_STREAM_END) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ret == Z_BUF_ERROR && strm.avail_out == 0) {
|
|
|
|
|
buffer.resize(buffer.size() * 2);
|
|
|
|
|
} else if (ret != Z_OK) {
|
|
|
|
|
qDebug() << "Zlib auto-detect error:" << zError(ret)
|
|
|
|
|
<< "at offset" << strm.total_in
|
|
|
|
|
<< "of" << aCompressedData.size() << "bytes";
|
|
|
|
|
inflateEnd(&strm);
|
|
|
|
|
return decompressed; // Return partial data on error
|
|
|
|
|
}
|
|
|
|
|
} 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) {
|
2025-09-05 18:35:17 -04:00
|
|
|
QByteArray dst;
|
2025-09-10 21:54:57 -04:00
|
|
|
static bool ok = (lzo_init() == LZO_E_OK);
|
|
|
|
|
if (!ok)
|
|
|
|
|
throw std::runtime_error("lzo_init failed");
|
|
|
|
|
|
|
|
|
|
dst = QByteArray(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);
|
|
|
|
|
|
2026-01-12 20:52:18 -05:00
|
|
|
if (rc != LZO_E_OK)
|
2025-09-10 21:54:57 -04:00
|
|
|
throw std::runtime_error("LZO decompression error");
|
2025-06-04 22:31:11 -04:00
|
|
|
|
2026-01-12 20:52:18 -05:00
|
|
|
// Accept partial success - resize to actual output if size differs
|
|
|
|
|
if (out != aDestSize) {
|
|
|
|
|
dst.resize(out);
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-04 22:31:11 -04:00
|
|
|
return dst;
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-04 20:42:41 -04:00
|
|
|
QByteArray Compression::DecompressOodle(const QByteArray &aCompressedData, quint32 aDecompressedSize) {
|
2025-12-19 23:06:03 -05:00
|
|
|
return pDecompressOodle(aCompressedData, aCompressedData.size(), aDecompressedSize);
|
2025-04-04 20:42:41 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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) {
|
2025-12-19 23:06:03 -05:00
|
|
|
QLibrary lib("../../../third_party/oodle_lib/dll/oo2core_8_win64.dll"); // adjust path if needed
|
|
|
|
|
if (!lib.load()) {
|
|
|
|
|
qDebug() << "Failed to load:" << lib.errorString();
|
2025-04-04 20:42:41 -04:00
|
|
|
return QByteArray();
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-19 23:06:03 -05:00
|
|
|
OodleLZ_CompressFunc OodleLZ_Compress = (OodleLZ_CompressFunc)lib.resolve("OodleLZ_Compress");
|
2025-04-04 20:42:41 -04:00
|
|
|
|
|
|
|
|
if (!OodleLZ_Compress) {
|
2025-12-19 23:06:03 -05:00
|
|
|
qDebug() << "Failed to resolve OodleLZ_Compress:" << lib.errorString();
|
2025-04-04 20:42:41 -04:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-19 23:06:03 -05:00
|
|
|
QByteArray Compression::pDecompressOodle(const QByteArray &aBuffer,
|
|
|
|
|
quint32 aBufferSize,
|
|
|
|
|
quint32 aOutputBufferSize)
|
|
|
|
|
{
|
|
|
|
|
QLibrary lib("../../../third_party/oodle_lib/dll/oo2core_8_win64.dll");
|
|
|
|
|
if (!lib.load()) {
|
|
|
|
|
qWarning() << "Failed to load Oodle DLL:" << lib.errorString();
|
|
|
|
|
return {};
|
2025-04-04 20:42:41 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
OodleLZ_DecompressFunc OodleLZ_Decompress =
|
2025-12-19 23:06:03 -05:00
|
|
|
reinterpret_cast<OodleLZ_DecompressFunc>(lib.resolve("OodleLZ_Decompress"));
|
2025-04-04 20:42:41 -04:00
|
|
|
if (!OodleLZ_Decompress) {
|
2025-12-19 23:06:03 -05:00
|
|
|
qWarning() << "Failed to resolve OodleLZ_Decompress:" << lib.errorString();
|
|
|
|
|
return {};
|
2025-04-04 20:42:41 -04:00
|
|
|
}
|
|
|
|
|
|
2025-12-19 23:06:03 -05:00
|
|
|
QByteArray out(aOutputBufferSize + 1, Qt::Uninitialized);
|
2025-04-04 20:42:41 -04:00
|
|
|
|
2025-12-19 23:06:03 -05:00
|
|
|
if (aBuffer.isEmpty() || aBufferSize == 0 || aOutputBufferSize == 0) {
|
|
|
|
|
qWarning() << "Invalid Oodle parameters (empty input or size 0)";
|
|
|
|
|
return {};
|
|
|
|
|
}
|
2025-04-04 20:42:41 -04:00
|
|
|
|
2025-12-19 23:06:03 -05:00
|
|
|
int result = OodleLZ_Decompress(
|
|
|
|
|
aBuffer.constData(),
|
|
|
|
|
static_cast<int64_t>(aBufferSize),
|
|
|
|
|
out.data(),
|
|
|
|
|
static_cast<int64_t>(aOutputBufferSize),
|
|
|
|
|
1,
|
|
|
|
|
0,
|
|
|
|
|
0,
|
|
|
|
|
0,
|
|
|
|
|
0,
|
|
|
|
|
0,
|
|
|
|
|
0,
|
|
|
|
|
0,
|
|
|
|
|
0,
|
|
|
|
|
3);
|
|
|
|
|
|
|
|
|
|
if (result < 0) {
|
|
|
|
|
qWarning() << "OodleLZ_Decompress failed with code" << result;
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (result > out.size()) {
|
|
|
|
|
qWarning() << "Oodle returned more than expected:" << result
|
|
|
|
|
<< "expected" << aOutputBufferSize;
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
out.resize(result);
|
|
|
|
|
|
|
|
|
|
return out;
|
2025-04-04 20:42:41 -04:00
|
|
|
}
|