#include "compression.h" #include "minilzo.h" #ifdef Q_OS_WIN #include "xcompress.h" #endif // libmspack for LZX decompression extern "C" { #include "mspack/mspack.h" #include "mspack/lzx.h" } #include #include #include #include #include #include #include // ============================================================================ // 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(const_cast(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(file); if (!mf || !mf->data) return -1; size_t remaining = mf->size - mf->pos; size_t toRead = static_cast(bytes); if (toRead > remaining) toRead = remaining; if (toRead == 0) return 0; memcpy(buffer, mf->data + mf->pos, toRead); mf->pos += toRead; return static_cast(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(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(mf->data[mf->pos + 3]) << 8) | (static_cast(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(mf->data[mf->pos + 0]) << 8) | (static_cast(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(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(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(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(mf->data[mf->pos + 3]) << 8) | (static_cast(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(mf->data[mf->pos + 0]) << 8) | (static_cast(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(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(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(file); if (!mf || !mf->output) return -1; mf->output->append(reinterpret_cast(buffer), bytes); return bytes; } static int mem_seek(struct mspack_file* file, off_t offset, int mode) { MemoryFile* mf = reinterpret_cast(file); if (!mf) return -1; switch (mode) { case MSPACK_SYS_SEEK_START: mf->pos = static_cast(offset); break; case MSPACK_SYS_SEEK_CUR: mf->pos += static_cast(offset); break; case MSPACK_SYS_SEEK_END: mf->pos = mf->size + static_cast(offset); break; } return 0; } static off_t mem_tell(struct mspack_file* file) { MemoryFile* mf = reinterpret_cast(file); return mf ? static_cast(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 }; // Static member initialization QString Compression::s_quickBmsPath; void Compression::setQuickBmsPath(const QString &path) { s_quickBmsPath = path; } QString Compression::quickBmsPath() { return s_quickBmsPath; } #ifdef Q_OS_WIN QByteArray Compression::CompressXMem(const QByteArray &data) { XMEMCODEC_PARAMETERS_LZX lzxParams = {}; lzxParams.Flags = 0; lzxParams.WindowSize = 0x20000; lzxParams.CompressionPartitionSize = 0x80000; 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(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(actualSize)); return output; } #else QByteArray Compression::CompressXMem(const QByteArray &) { qWarning() << "XMem compression not available on this platform"; return {}; } #endif #ifdef Q_OS_WIN QByteArray Compression::DecompressXMem(const QByteArray &data, int flags, int windowSize, int partSize) { if (data.size() < 8) return {}; // Read header identifier (big-endian in file) quint32 identifier = qFromBigEndian(reinterpret_cast(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(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 XMEMCODEC_PARAMETERS_LZX lzxParams = {}; lzxParams.Flags = qFromBigEndian(ptr + 12); lzxParams.WindowSize = qFromBigEndian(ptr + 16); lzxParams.CompressionPartitionSize = qFromBigEndian(ptr + 20); quint64 uncompressedSize = (static_cast(qFromBigEndian(ptr + 24)) << 32) | qFromBigEndian(ptr + 28); quint32 uncompressedBlockSize = qFromBigEndian(ptr + 40); qDebug() << "LZXNATIVE: windowSize=" << lzxParams.WindowSize << "partSize=" << lzxParams.CompressionPartitionSize << "uncompressedSize=" << uncompressedSize << "blockSize=" << uncompressedBlockSize; // 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; return {}; } QByteArray output; output.reserve(static_cast(uncompressedSize)); // Data starts after 48-byte header, in blocks prefixed by 4-byte size int offset = 48; while (offset + 4 <= data.size()) { quint32 compressedBlockSize = qFromBigEndian(ptr + offset); offset += 4; if (compressedBlockSize == 0 || offset + static_cast(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(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); QString quickbmsExe = quickBmsPath(); if (quickbmsExe.isEmpty()) { qWarning() << "QuickBMS path not configured"; return {}; } QStringList args = {"-o", "-O", outputFile, bmsScript, inputFile}; proc.start(quickbmsExe, args); if (!proc.waitForFinished(30000)) { qWarning() << "QuickBMS timeout or failed to start:" << quickbmsExe; 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(data.constData()); SIZE_T availIn = data.size(); QByteArray output; output.reserve(16 * 1024 * 1024); QByteArray scratch(0x10000, Qt::Uninitialized); while (availIn > 0) { SIZE_T inSize = availIn; SIZE_T outSize = scratch.size(); hr = XMemDecompressStream(ctx, scratch.data(), &outSize, nextIn, &inSize); if (FAILED(hr)) { qWarning() << "XMemDecompressStream raw failed, hr=" << Qt::hex << hr; break; } if (inSize == 0 && outSize == 0) break; output.append(scratch.constData(), static_cast(outSize)); nextIn += inSize; availIn -= inSize; } XMemDestroyDecompressionContext(ctx); return output; } #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 // ============================================================================ // 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(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(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(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(data.constData()); // Parse file header (big-endian) quint32 uncompressedSize = qFromBigEndian(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(uncompressedSize)); int offset = 8; while (output.size() < static_cast(uncompressedSize) && offset + 4 < data.size()) { quint32 blockSize = qFromBigEndian(ptr + offset); offset += 4; if (blockSize == 0 || offset + static_cast(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(uncompressedSize) - output.size()); if (compressedSize <= 0 || headerSize + compressedSize > static_cast(blockSize)) { break; } // Extract raw LZX data for this block QByteArray blockData(reinterpret_cast(blockPtr + headerSize), compressedSize); // Decompress this block with its own LZX context QByteArray blockOutput(blockUncompressed, Qt::Uninitialized); LzxHelperFile src, dst; src.buf = reinterpret_cast(blockData.data()); src.bufSize = blockData.size(); src.pos = 0; src.rest = 0; dst.buf = reinterpret_cast(blockOutput.data()); dst.bufSize = blockUncompressed; dst.pos = 0; struct lzxd_stream* lzx = lzxd_init( &g_lzxHelperSystem, reinterpret_cast(&src), reinterpret_cast(&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(blockSize); } return output.isEmpty() ? QByteArray{} : 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(data.constData()), data.size()); return adler; } qint64 Compression::FindZlibOffset(const QByteArray &bytes) { QDataStream stream(bytes); while (!stream.atEnd()) { QByteArray testSegment = stream.device()->peek(2).toHex().toUpper(); if (testSegment == "7801" || testSegment == "785E" || testSegment == "789C" || testSegment == "78DA") { return stream.device()->pos(); } stream.skipRawData(1); } 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; } QByteArray Compression::DecompressZLIB(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(aCompressedData.size()); strm.next_in = reinterpret_cast(const_cast(aCompressedData.data())); if (inflateInit2(&strm, MAX_WBITS) != Z_OK) { qWarning() << "inflateInit2 failed"; return {}; } QByteArray decompressed; QByteArray buffer(fmin(strm.avail_in * 2, 4096), Qt::Uninitialized); int ret; do { strm.next_out = reinterpret_cast(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) { size_t errorOffset = strm.total_in; qWarning() << "Zlib error:" << zError(ret) << "at offset" << errorOffset << "of" << aCompressedData.size() << "bytes"; inflateEnd(&strm); return decompressed; } } while (ret != Z_STREAM_END); inflateEnd(&strm); return decompressed; } 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(aCompressedData.size()); strm.next_in = reinterpret_cast(const_cast(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(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; } 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(aDictionary.constData()), aDictionary.size()); } strm.next_in = reinterpret_cast(const_cast(aData.data())); strm.avail_in = aData.size(); QByteArray compressed; char buffer[4096]; int ret; do { strm.next_out = reinterpret_cast(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(const_cast(aCompressedData.data())); strm.avail_in = static_cast(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(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) { Q_UNUSED(aDictionary); 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(const_cast(aData.data())); strm.avail_in = static_cast(aData.size()); QByteArray compressed; char buffer[4096]; int ret; do { strm.next_out = reinterpret_cast(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; } QByteArray Compression::DecompressLZO(const QByteArray &aCompressedData, quint32 aDestSize) { QByteArray dst; 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(aCompressedData.constData()), static_cast(aCompressedData.size()), reinterpret_cast(dst.data()), &out, nullptr); if (rc != LZO_E_OK) throw std::runtime_error("LZO decompression error"); // Accept partial success - resize to actual output if size differs if (out != aDestSize) { dst.resize(out); } return dst; } QByteArray Compression::DecompressOodle(const QByteArray &aCompressedData, quint32 aDecompressedSize) { return pDecompressOodle(aCompressedData, aCompressedData.size(), aDecompressedSize); } QByteArray Compression::CompressOodle(const QByteArray &aData) { quint32 maxSize = pGetOodleCompressedBounds(aData.length()); QByteArray compressedData = pCompressOodle(aData, aData.length(), maxSize, OodleFormat::Kraken, OodleCompressionLevel::Optimal5); 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 lib("../../../third_party/oodle_lib/dll/oo2core_8_win64.dll"); // adjust path if needed if (!lib.load()) { qDebug() << "Failed to load:" << lib.errorString(); return QByteArray(); } OodleLZ_CompressFunc OodleLZ_Compress = (OodleLZ_CompressFunc)lib.resolve("OodleLZ_Compress"); if (!OodleLZ_Compress) { qDebug() << "Failed to resolve OodleLZ_Compress:" << lib.errorString(); return QByteArray(); } std::byte *outputBuffer = new std::byte[aOutputBufferSize]; if (aBuffer.length() > 0 && aBufferSize > 0 && aOutputBufferSize > 0) OodleLZ_Compress(aformat, reinterpret_cast(aBuffer.data()), aBufferSize, outputBuffer, alevel, 0, 0, 0); return QByteArray(reinterpret_cast(outputBuffer), aOutputBufferSize); } 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 {}; } OodleLZ_DecompressFunc OodleLZ_Decompress = reinterpret_cast(lib.resolve("OodleLZ_Decompress")); if (!OodleLZ_Decompress) { qWarning() << "Failed to resolve OodleLZ_Decompress:" << lib.errorString(); return {}; } QByteArray out(aOutputBufferSize + 1, Qt::Uninitialized); if (aBuffer.isEmpty() || aBufferSize == 0 || aOutputBufferSize == 0) { qWarning() << "Invalid Oodle parameters (empty input or size 0)"; return {}; } int result = OodleLZ_Decompress( aBuffer.constData(), static_cast(aBufferSize), out.data(), static_cast(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; }