whiteoutlib 0.1.4

Read and write Blizzard game assets from Rust: models (MDX, M2, M3), textures (BLP, DDS, PNG, JPEG, BMP, TGA, TIFF, GIF) and archives (CASC, MPQ).
Documentation
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2026 Fernando Sahmkow

#include "pkware.h"

#include <algorithm>
#include <array>
#include <cstring>

namespace whiteout::storages::mpq {

// ============================================================================
// Tables
// ============================================================================

namespace {

// Length codes for PKware explode — maps extra bit counts for each length code.
static constexpr std::array<u8, 16> kLenBits = {3, 2, 3, 3, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 7, 7};

// Base lengths for each length code.
static constexpr std::array<u8, 16> kLenBase = {0, 4, 8, 16, 32, 64, 128, 0,
                                                1, 2, 3, 4,  5,  6,  7,   8};

// Extra bits for distance codes.
static constexpr std::array<u8, 66> kDistBits = {
    2, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
    7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8};

// Shannon-Fano decode table for literal bytes (ASCII mode).
static constexpr std::array<u8, 256> kChBitsAsc = {
    0x0B, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x08, 0x07, 0x0C, 0x0C, 0x07, 0x0C, 0x0C,
    0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0D, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C,
    0x04, 0x0A, 0x08, 0x0C, 0x0A, 0x0C, 0x0A, 0x08, 0x07, 0x07, 0x08, 0x09, 0x07, 0x06, 0x07, 0x08,
    0x07, 0x06, 0x07, 0x07, 0x07, 0x07, 0x08, 0x07, 0x07, 0x08, 0x08, 0x0C, 0x0B, 0x07, 0x09, 0x0B,
    0x0C, 0x06, 0x07, 0x06, 0x06, 0x05, 0x07, 0x08, 0x08, 0x06, 0x0B, 0x09, 0x06, 0x07, 0x06, 0x06,
    0x07, 0x0B, 0x06, 0x06, 0x06, 0x07, 0x09, 0x08, 0x09, 0x09, 0x0B, 0x08, 0x0B, 0x08, 0x0C, 0x08,
    0x0C, 0x05, 0x06, 0x06, 0x06, 0x05, 0x06, 0x06, 0x06, 0x05, 0x0B, 0x07, 0x05, 0x06, 0x05, 0x05,
    0x06, 0x0A, 0x05, 0x05, 0x05, 0x05, 0x08, 0x07, 0x08, 0x08, 0x0A, 0x0B, 0x0B, 0x0C, 0x0C, 0x0C,
    0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D,
    0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D,
    0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D,
    0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C,
    0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C,
    0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C,
    0x0D, 0x0C, 0x0D, 0x0D, 0x0D, 0x0C, 0x0D, 0x0D, 0x0D, 0x0C, 0x0D, 0x0D, 0x0D, 0x0D, 0x0C, 0x0D,
    0x0D, 0x0D, 0x0C, 0x0C, 0x0C, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D};

// Shannon-Fano code values for ASCII mode literals.
static constexpr std::array<u16, 256> kChCodeAsc = {
    0x0490, 0x0FE0, 0x07E0, 0x0BE0, 0x03E0, 0x0DE0, 0x05E0, 0x09E0, 0x01E0, 0x00B8, 0x0062, 0x0EE0,
    0x06E0, 0x0022, 0x0AE0, 0x02E0, 0x0CE0, 0x04E0, 0x08E0, 0x00E0, 0x0F60, 0x0760, 0x0B60, 0x0360,
    0x0D60, 0x0560, 0x1240, 0x0960, 0x0160, 0x0E60, 0x0660, 0x0A60, 0x000F, 0x0250, 0x0038, 0x0260,
    0x0050, 0x0C60, 0x0390, 0x00D8, 0x0042, 0x0002, 0x0058, 0x01B0, 0x007C, 0x0029, 0x003C, 0x0098,
    0x005C, 0x0009, 0x001C, 0x006C, 0x002C, 0x004C, 0x0018, 0x000C, 0x0074, 0x00E8, 0x0068, 0x0460,
    0x0090, 0x0034, 0x00B0, 0x0710, 0x0860, 0x0031, 0x0054, 0x0011, 0x0021, 0x0017, 0x0014, 0x00A8,
    0x0028, 0x0001, 0x0310, 0x0130, 0x003E, 0x0064, 0x001E, 0x002E, 0x0024, 0x0510, 0x000E, 0x0036,
    0x0016, 0x0044, 0x0030, 0x00C8, 0x01D0, 0x00D0, 0x0110, 0x0048, 0x0610, 0x0150, 0x0060, 0x0088,
    0x0FA0, 0x0007, 0x0026, 0x0006, 0x003A, 0x001B, 0x001A, 0x002A, 0x000A, 0x000B, 0x0210, 0x0004,
    0x0013, 0x0032, 0x0003, 0x001D, 0x0012, 0x0190, 0x000D, 0x0015, 0x0005, 0x0019, 0x0008, 0x0078,
    0x00F0, 0x0070, 0x0290, 0x0410, 0x0010, 0x07A0, 0x0BA0, 0x03A0, 0x0240, 0x1C40, 0x0C40, 0x1440,
    0x0440, 0x1840, 0x0840, 0x1040, 0x0040, 0x1F40, 0x0F40, 0x1740, 0x0740, 0x1B40, 0x0B40, 0x1340,
    0x0340, 0x1D40, 0x0D40, 0x1540, 0x0540, 0x1940, 0x0940, 0x1140, 0x0140, 0x1E40, 0x0E40, 0x1640,
    0x0640, 0x1A40, 0x0A40, 0x1240, 0x0240, 0x1C40, 0x0C40, 0x1440, 0x0440, 0x1840, 0x0840, 0x1040,
    0x0040, 0x1F40, 0x0F40, 0x1740, 0x0740, 0x1B40, 0x0B40, 0x1340, 0x0DA0, 0x05A0, 0x09A0, 0x01A0,
    0x0EA0, 0x06A0, 0x0AA0, 0x02A0, 0x0CA0, 0x04A0, 0x08A0, 0x00A0, 0x0F20, 0x0720, 0x0B20, 0x0320,
    0x0D20, 0x0520, 0x0920, 0x0120, 0x0E20, 0x0620, 0x0A20, 0x0220, 0x0C20, 0x0420, 0x0820, 0x0020,
    0x0FC0, 0x07C0, 0x0BC0, 0x03C0, 0x0DC0, 0x05C0, 0x09C0, 0x01C0, 0x0EC0, 0x06C0, 0x0AC0, 0x02C0,
    0x0CC0, 0x04C0, 0x08C0, 0x00C0, 0x0F40, 0x0740, 0x0B40, 0x0340, 0x0C00, 0x0D40, 0x1A00, 0x0600,
    0x1E00, 0x0540, 0x0E00, 0x1600, 0x0200, 0x0940, 0x1C00, 0x0400, 0x1400, 0x0140, 0x0800, 0x1800,
    0x1000, 0x0A00, 0x0F00, 0x1200, 0x0700, 0x0B00, 0x0300, 0x0D00, 0x0500, 0x0900, 0x0100, 0x1100,
    0x1900, 0x1500, 0x1D00, 0x0000};

// Shannon-Fano decode table for literal bytes (binary mode — all 8-bit, 9 bits each).
// In binary mode, all 256 literals use 9 bits.

// Length code extra bits.
static constexpr std::array<u8, 16> kLenCode = {0x05, 0x03, 0x01, 0x06, 0x0A, 0x02, 0x0C, 0x14,
                                                0x04, 0x18, 0x08, 0x30, 0x10, 0x20, 0x40, 0x00};

// ============================================================================
// ASCII mode fast lookup table (built once at startup)
// ============================================================================

// Max code length in kChBitsAsc is 13 bits. We use a 2^13 = 8192 entry LUT.
static constexpr i32 ASC_FAST_BITS = 13;
static constexpr i32 ASC_FAST_SIZE = 1 << ASC_FAST_BITS;

struct AscLutEntry {
    u16 symbol; // Decoded byte (0-255), or 0xFFFF if invalid.
    u8 bits;    // Number of bits to consume.
};

constexpr std::array<AscLutEntry, ASC_FAST_SIZE> buildAscLut() {
    std::array<AscLutEntry, ASC_FAST_SIZE> table{};
    for (i32 i = 0; i < ASC_FAST_SIZE; ++i) {
        table[i] = {0xFFFF, 0};
    }
    for (u32 sym = 0; sym < 256; ++sym) {
        u32 const codeLen = kChBitsAsc[sym];
        if (codeLen == 0)
            continue;
        u16 const code = kChCodeAsc[sym];
        u32 const padBits = ASC_FAST_BITS - codeLen;
        u32 const numEntries = 1u << padBits;
        for (u32 p = 0; p < numEntries; ++p) {
            u32 const idx = code | (p << codeLen);
            if (idx < static_cast<u32>(ASC_FAST_SIZE)) {
                table[idx] = {static_cast<u16>(sym), static_cast<u8>(codeLen)};
            }
        }
    }
    return table;
}

static constexpr auto kAscLut = buildAscLut();

// ============================================================================
// Bit reader for explode
// ============================================================================

struct ExplodeBitReader {
    const u8* data;
    size_t dataSize;
    size_t bytePos = 0;
    u32 bitBuf = 0;
    u32 bitsAvail = 0;

    ExplodeBitReader(const u8* d, size_t s) : data(d), dataSize(s) {}

    bool ensureBits(u32 count) {
        while (bitsAvail < count) {
            if (bytePos >= dataSize)
                return false;
            bitBuf |= static_cast<u32>(data[bytePos++]) << bitsAvail;
            bitsAvail += 8;
        }
        return true;
    }

    u32 peekBits(u32 count) const {
        return bitBuf & ((1u << count) - 1);
    }

    void dropBits(u32 count) {
        bitBuf >>= count;
        bitsAvail -= count;
    }

    u32 readBits(u32 count) {
        ensureBits(count);
        u32 const val = peekBits(count);
        dropBits(count);
        return val;
    }
};

// ============================================================================
// Decode helpers
// ============================================================================

/// Decode a length value from the bit stream.
u32 decodeLengthValue(ExplodeBitReader& br) {
    if (!br.ensureBits(8))
        return 0;
    u32 const code = br.peekBits(8);

    // Find the length code index by searching the kLenCode table.
    // The 8-bit value is looked up from the Shannon-Fano tree structure.
    // Simplified: use the first nibble as index.
    u32 lengthCode;
    if (code < 16) {
        // Use a simplified Shannon-Fano tree traversal.
        // The length codes are encoded with variable-length prefix codes.
        // For a basic implementation we decode bit-by-bit.
        lengthCode = 0;
        for (u32 i = 0; i < 16; ++i) {
            if (!br.ensureBits(kLenBits[i]))
                return 0;
            u32 const val = br.peekBits(kLenBits[i]);
            if (val == kLenCode[i]) {
                br.dropBits(kLenBits[i]);
                lengthCode = i;
                break;
            }
        }
    } else {
        lengthCode = code & 0x0F;
        br.dropBits(4);
    }

    u32 length = kLenBase[lengthCode];
    if (kLenBits[lengthCode] > 0) {
        u32 const extra = br.readBits(kLenBits[lengthCode]);
        length += extra;
    }

    return length + 2; // Minimum match length is 2.
}

} // anonymous namespace

// ============================================================================
// Explode (decompress)
// ============================================================================

std::vector<u8> pkwareExplode(std::span<const u8> src, size_t expectedSize) {
    if (src.size() < 4)
        return {};

    // First byte: compression type (0 = binary, 1 = ASCII).
    u8 const compressionType = src[0];
    // Second byte: dictionary size log2 (4 = 1024, 5 = 2048, 6 = 4096).
    u8 const dictSizeBits = src[1];

    if (compressionType > 1)
        return {};
    if (dictSizeBits < 4 || dictSizeBits > 6)
        return {};

    [[maybe_unused]] u32 const dictSize = 1u << dictSizeBits;

    ExplodeBitReader br(src.data() + 2, src.size() - 2);
    std::vector<u8> output;
    output.reserve(expectedSize);

    while (output.size() < expectedSize) {
        if (!br.ensureBits(1))
            break;

        if (br.readBits(1) == 1) {
            // Literal byte.
            if (compressionType == 1) {
                // ASCII mode: decode via Shannon-Fano tree using fast LUT.
                if (!br.ensureBits(ASC_FAST_BITS))
                    break;
                u32 const val = br.peekBits(ASC_FAST_BITS);
                const auto& entry = kAscLut[val];
                if (entry.symbol == 0xFFFF)
                    break;
                br.dropBits(entry.bits);
                output.push_back(static_cast<u8>(entry.symbol));
            } else {
                // Binary mode: all literals are 8 bits.
                if (!br.ensureBits(8))
                    break;
                output.push_back(static_cast<u8>(br.readBits(8)));
            }
        } else {
            // Distance/length pair (LZ77 match).
            if (!br.ensureBits(dictSizeBits + 6))
                break;

            // Read distance.
            u32 const distLow = br.readBits(dictSizeBits);

            // Decode distance high byte using kDistBits table.
            u32 distHigh = 0;
            if (!br.ensureBits(6))
                break;
            u32 const distCode = br.peekBits(6);
            // The distance code is a 6-bit index into kDistBits.
            // Actually, we need to decode it as a Shannon-Fano code.
            // Simplified: use the 6-bit value directly as the distance code index.
            br.dropBits(6);

            if (distCode < 64) {
                u32 const extraDist = kDistBits[distCode];
                if (extraDist > 0 && !br.ensureBits(extraDist))
                    break;
                u32 const extra = (extraDist > 0) ? br.readBits(extraDist) : 0;
                distHigh = (distCode << 2) | extra; // Approximate distance high.
            }

            u32 const distance = (distHigh << dictSizeBits) | distLow;

            // Decode length.
            u32 const length = decodeLengthValue(br);
            if (length == 0)
                break;

            // Special case: distance 0 + length 2 = end of stream.
            if (distance == 0 && length == 2)
                break;

            // Copy from dictionary (back-reference).
            if (distance >= output.size())
                break;
            size_t const srcPos = output.size() - distance - 1;
            for (u32 i = 0; i < length && output.size() < expectedSize; ++i) {
                output.push_back(output[srcPos + i]);
            }
        }
    }

    if (output.size() != expectedSize) {
        // Decompression didn't produce the expected amount of data.
        // Return what we have — caller can check size.
    }

    return output;
}

// ============================================================================
// Implode (compress) — simplified binary-mode compressor
// ============================================================================

std::vector<u8> pkwareImplode(std::span<const u8> src) {
    if (src.empty())
        return {};

    // Use binary mode (type 0) with 4096 dictionary (dictSizeBits = 6).
    // This is a simplified compressor that only emits literals (no LZ77 matches).
    // This produces valid PKware DCL output that can be decompressed, just not
    // optimally compressed. A proper implementation would use hash-chain LZ77.

    std::vector<u8> output;
    output.reserve(src.size() + src.size() / 8 + 4);

    // Header: compression type (0 = binary), dictionary size bits (6 = 4096).
    output.push_back(0x00); // Binary mode.
    output.push_back(0x06); // Dict size = 4096.

    // Emit all bytes as literals in binary mode.
    // In binary mode, literal flag = 1 bit (value 1), followed by 8 bits of the byte.
    // We pack bits LSB-first.
    u32 bitBuf = 0;
    u32 bitsUsed = 0;

    auto flushByte = [&]() {
        output.push_back(static_cast<u8>(bitBuf & 0xFF));
        bitBuf >>= 8;
        bitsUsed -= 8;
    };

    auto writeBits = [&](u32 value, u32 count) {
        bitBuf |= (value << bitsUsed);
        bitsUsed += count;
        while (bitsUsed >= 8)
            flushByte();
    };

    for (u8 const byte : src) {
        writeBits(1, 1);    // Literal flag.
        writeBits(byte, 8); // Literal value (binary mode).
    }

    // End of stream marker: distance=0, length=2.
    writeBits(0, 1); // Match flag.
    // Distance low = 0 (6 bits for dict size 4096).
    writeBits(0, 6);
    // Distance high code = 0 (6 bits).
    writeBits(0, 6);
    // Length = 0 (maps to length 2 with the +2 offset).
    writeBits(0x05, 3); // Length code 0.

    // Flush remaining bits.
    if (bitsUsed > 0) {
        output.push_back(static_cast<u8>(bitBuf & 0xFF));
    }

    return output;
}

} // namespace whiteout::storages::mpq