Module refinery.lib.decompression

A module with common functions used by decompression units.

Expand source code Browse git
"""
A module with common functions used by decompression units.
"""
from __future__ import annotations

import bz2
import lzma
import zlib

from collections import Counter
from itertools import repeat
from typing import TYPE_CHECKING

from refinery.lib.structures import StructReader

if TYPE_CHECKING:
    from typing import Iterator

    from refinery.lib.types import buf

DECODE_TABLE_SYMBOL_SHIFT = 4
DECODE_TABLE_MAX_SYMBOL = ((1 << (16 - DECODE_TABLE_SYMBOL_SHIFT)) - 1)
DECODE_TABLE_MAX_LENGTH = ((1 << DECODE_TABLE_SYMBOL_SHIFT) - 1)
DECODE_TABLE_LENGTH_MASK = DECODE_TABLE_MAX_LENGTH


def MAKE_DECODE_TABLE_ENTRY(symbol, length):
    v = ((symbol << DECODE_TABLE_SYMBOL_SHIFT) | length)
    assert v & 0xFFFF == v
    return v


def s32shift(k: int, shift: int):
    """
    This helper function implements a signed left shift for 32bit integers.
    """
    M = 1 << 32
    shift %= 32
    k = k * (1 << shift) % (1 << 32)
    return k - M if k >> 31 else k


def make_huffman_decode_table(
    table_data: bytearray,
    table_bits: int,
    max_codeword_len: int,
) -> list[int]:
    remainder = 1
    codeword_length = 1
    entry_pos = 0
    decode_table: list[int] = [0] * (1 << table_bits)
    sym_count = len(table_data)
    len_counts = Counter(table_data)

    for sym in range(1, max_codeword_len + 1):
        remainder = (remainder << 1) - len_counts[sym]
        if remainder < 0:
            raise OverflowError('Lengths have overflowed the code space.')
    if remainder:
        if remainder != 1 << max_codeword_len:
            raise RuntimeError('Incomplete & nonempty code encountered.')
        return decode_table

    offsets = [0]
    for sym in range(max_codeword_len):
        offsets.append(offsets[sym] + len_counts[sym])

    sorted_syms = {}
    for i, sym in enumerate(table_data):
        offset = offsets[sym]
        offsets[sym] += 1
        sorted_syms[offset] = i

    sym_index = offsets[0]
    stores_per_loop = 1 << (table_bits - codeword_length)
    while stores_per_loop:
        end_sym_idx = sym_index + len_counts[codeword_length]
        for k in range(sym_index, end_sym_idx):
            entry_end = entry_pos + stores_per_loop
            decode_table[entry_pos:entry_end] = repeat(
                MAKE_DECODE_TABLE_ENTRY(sorted_syms[k], codeword_length),
                stores_per_loop)
            entry_pos = entry_end
        codeword_length += 1
        sym_index = end_sym_idx
        stores_per_loop >>= 1

    assert sym_index <= sym_count

    if sym_index == sym_count:
        return decode_table

    codeword = entry_pos * 2
    subtable_pos = 1 << table_bits
    subtable_bits = table_bits
    subtable_prefix = -1

    while sym_index < sym_count:
        while len_counts[codeword_length] == 0:
            if codeword_length > sym_count:
                raise IndexError('Error computing codeword')
            codeword_length += 1
            codeword <<= 1

        prefix = codeword >> (codeword_length - table_bits)

        if prefix != subtable_prefix:
            subtable_prefix = prefix
            subtable_bits = codeword_length - table_bits
            remainder = s32shift(1, subtable_bits)
            while True:
                remainder -= len_counts[table_bits + subtable_bits]
                if remainder <= 0:
                    break
                subtable_bits += 1
                remainder <<= 1
            decode_table[subtable_prefix] = MAKE_DECODE_TABLE_ENTRY(subtable_pos, subtable_bits)

        entry = MAKE_DECODE_TABLE_ENTRY(sorted_syms[sym_index], codeword_length - table_bits)
        count = 1 << (table_bits + subtable_bits - codeword_length)
        end = subtable_pos + count
        decode_table[subtable_pos:end] = repeat(entry, count)
        subtable_pos = end
        len_counts[codeword_length] -= 1
        codeword += 1
        sym_index += 1

    return decode_table


class BitBufferedReader:
    """
    A helper class to read bitwise from the compressed input stream.
    """

    def __init__(self, buffer: bytearray | StructReader, bits_per_read: int = 32):
        if not isinstance(buffer, StructReader):
            buffer = StructReader(memoryview(buffer), bigendian=False)
        self._reader: StructReader[memoryview] = buffer
        self._bit_buffer_data: int = 0
        self._bit_buffer_size: int = 0
        self._bits_per_read = bits_per_read

    def variable_length_integer(self) -> int:
        value = 1
        while True:
            chunk = self.read(2)
            value = (value << 1) + (chunk >> 1)
            if not chunk & 1:
                return value

    @property
    def overshoot(self) -> int:
        return self._bit_buffer_size // 8

    def __getattr__(self, k):
        return getattr(self._reader, k)

    def __next__(self) -> int:
        return self.read(1)

    def next(self):
        return self.read(1)

    def peek(self, count: int):
        return self._bit_buffer_data >> self.collect(count)

    def __len__(self):
        return self._bit_buffer_size

    def __getitem__(self, k: int):
        if k not in range(self._bit_buffer_size):
            raise IndexError(k)
        offset = self._bit_buffer_size - k
        return (self._bit_buffer_data >> offset) & 1

    def __enter__(self):
        return self

    def __exit__(self, *_):
        return False

    def read(self, count: int) -> int:
        offset = self.collect(count)
        bits = self._bit_buffer_data >> offset
        self._bit_buffer_data ^= bits << offset
        self._bit_buffer_size -= count
        assert self._bit_buffer_data.bit_length() <= self._bit_buffer_size
        assert bits.bit_length() <= count
        return bits

    def collect(self, count: int | None = None) -> int:
        if count is None:
            count = self._bits_per_read
        offset = self._bit_buffer_size - count
        if offset < 0:
            more = count - self._bit_buffer_size
            reads, _r = divmod(more, self._bits_per_read)
            reads += int(bool(_r))
            reads *= self._bits_per_read
            self._bit_buffer_data <<= reads
            self._bit_buffer_data |= self._reader.read_integer(reads)
            self._bit_buffer_size += reads
            offset += reads
            assert offset >= 0
        return offset

    def align(self):
        self._bit_buffer_size = 0
        self._bit_buffer_data = 0


def read_huffman_symbol(reader: BitBufferedReader, decode_table: list[int], table_bits: int, max_codeword_len: int):
    reader.collect(max_codeword_len)
    entry = decode_table[reader.peek(table_bits)]
    symbol = entry >> DECODE_TABLE_SYMBOL_SHIFT
    length = entry & DECODE_TABLE_LENGTH_MASK
    if max_codeword_len > table_bits and entry >= (1 << (table_bits + DECODE_TABLE_SYMBOL_SHIFT)):
        reader.read(table_bits)
        entry = decode_table[symbol + reader.peek(length)]
        symbol = entry >> DECODE_TABLE_SYMBOL_SHIFT
        length = entry & DECODE_TABLE_LENGTH_MASK
    reader.read(length)
    return symbol


def parse_lzma_properties(data: memoryview | bytes | bytearray, version: int, min_dict: int = 0, max_dict: int = 0xFFFFFFFF):
    pv = data[0]
    if version == 1:
        ds = int.from_bytes(data[1:5], 'little')
        pv, lc = divmod(pv, 9)
        pb, lp = divmod(pv, 5)
        _valid = lc <= 8 and lp <= 4 and pb <= 4
        if not _valid:
            raise ValueError(F'Invalid LZMA1 properties {lc=} {lp=} {pb=}.')
        df = dict(id=lzma.FILTER_LZMA1, lc=lc, pb=pb, lp=lp, dict_size=ds)
    elif version == 2:
        if pv > 40:
            raise ValueError(F'Invalid LZMA2 encoded dictionary size {pv=}')
        ph, pl = divmod(pv, 2)
        ds = 0xFFFFFFFF if pv == 40 else (pl + 2) << (ph + 11)
        df = dict(id=lzma.FILTER_LZMA2, dict_size=ds)
    else:
        raise ValueError(F'Invalid LZMA version {version}.')
    if ds > max_dict:
        raise ValueError(F'Dictionary size in excess of 0x{max_dict:08X}.')
    if ds < min_dict:
        raise ValueError(F'Dictionary size is less than 0x{min_dict:08X}.')
    return df


def decompress_pieces(
    decompressor: lzma.LZMADecompressor | bz2.BZ2Decompressor,
    data: buf,
    size: int = 0x100000,
) -> Iterator[buf]:
    """
    Feed the given data to the given decompressor and generate its output in pieces of at most the
    given size. The output ends where the decompressor reaches the end of its stream or where the
    data ends.
    """
    view = memoryview(data)
    position = 0
    while not decompressor.eof:
        if decompressor.needs_input:
            if position >= len(view):
                return
            chunk = view[position:position + size]
            position += len(chunk)
        else:
            chunk = B''
        if piece := decompressor.decompress(chunk, size):
            yield piece


def inflate_pieces(data: buf, size: int = 0x100000) -> Iterator[buf]:
    """
    Inflate the given zlib stream and generate the output in pieces of at most the given size. The
    output ends where the stream or the data ends.
    """
    inflate = zlib.decompressobj()
    view = memoryview(data)
    position = 0
    tail: buf = B''
    while not inflate.eof:
        if not tail:
            if position >= len(view):
                break
            tail = view[position:position + size]
            position += len(tail)
        if piece := inflate.decompress(tail, size):
            yield piece
        tail = inflate.unconsumed_tail
    if rest := inflate.flush():
        yield rest

Functions

def MAKE_DECODE_TABLE_ENTRY(symbol, length)
Expand source code Browse git
def MAKE_DECODE_TABLE_ENTRY(symbol, length):
    v = ((symbol << DECODE_TABLE_SYMBOL_SHIFT) | length)
    assert v & 0xFFFF == v
    return v
def s32shift(k, shift)

This helper function implements a signed left shift for 32bit integers.

Expand source code Browse git
def s32shift(k: int, shift: int):
    """
    This helper function implements a signed left shift for 32bit integers.
    """
    M = 1 << 32
    shift %= 32
    k = k * (1 << shift) % (1 << 32)
    return k - M if k >> 31 else k
def make_huffman_decode_table(table_data, table_bits, max_codeword_len)
Expand source code Browse git
def make_huffman_decode_table(
    table_data: bytearray,
    table_bits: int,
    max_codeword_len: int,
) -> list[int]:
    remainder = 1
    codeword_length = 1
    entry_pos = 0
    decode_table: list[int] = [0] * (1 << table_bits)
    sym_count = len(table_data)
    len_counts = Counter(table_data)

    for sym in range(1, max_codeword_len + 1):
        remainder = (remainder << 1) - len_counts[sym]
        if remainder < 0:
            raise OverflowError('Lengths have overflowed the code space.')
    if remainder:
        if remainder != 1 << max_codeword_len:
            raise RuntimeError('Incomplete & nonempty code encountered.')
        return decode_table

    offsets = [0]
    for sym in range(max_codeword_len):
        offsets.append(offsets[sym] + len_counts[sym])

    sorted_syms = {}
    for i, sym in enumerate(table_data):
        offset = offsets[sym]
        offsets[sym] += 1
        sorted_syms[offset] = i

    sym_index = offsets[0]
    stores_per_loop = 1 << (table_bits - codeword_length)
    while stores_per_loop:
        end_sym_idx = sym_index + len_counts[codeword_length]
        for k in range(sym_index, end_sym_idx):
            entry_end = entry_pos + stores_per_loop
            decode_table[entry_pos:entry_end] = repeat(
                MAKE_DECODE_TABLE_ENTRY(sorted_syms[k], codeword_length),
                stores_per_loop)
            entry_pos = entry_end
        codeword_length += 1
        sym_index = end_sym_idx
        stores_per_loop >>= 1

    assert sym_index <= sym_count

    if sym_index == sym_count:
        return decode_table

    codeword = entry_pos * 2
    subtable_pos = 1 << table_bits
    subtable_bits = table_bits
    subtable_prefix = -1

    while sym_index < sym_count:
        while len_counts[codeword_length] == 0:
            if codeword_length > sym_count:
                raise IndexError('Error computing codeword')
            codeword_length += 1
            codeword <<= 1

        prefix = codeword >> (codeword_length - table_bits)

        if prefix != subtable_prefix:
            subtable_prefix = prefix
            subtable_bits = codeword_length - table_bits
            remainder = s32shift(1, subtable_bits)
            while True:
                remainder -= len_counts[table_bits + subtable_bits]
                if remainder <= 0:
                    break
                subtable_bits += 1
                remainder <<= 1
            decode_table[subtable_prefix] = MAKE_DECODE_TABLE_ENTRY(subtable_pos, subtable_bits)

        entry = MAKE_DECODE_TABLE_ENTRY(sorted_syms[sym_index], codeword_length - table_bits)
        count = 1 << (table_bits + subtable_bits - codeword_length)
        end = subtable_pos + count
        decode_table[subtable_pos:end] = repeat(entry, count)
        subtable_pos = end
        len_counts[codeword_length] -= 1
        codeword += 1
        sym_index += 1

    return decode_table
def read_huffman_symbol(reader, decode_table, table_bits, max_codeword_len)
Expand source code Browse git
def read_huffman_symbol(reader: BitBufferedReader, decode_table: list[int], table_bits: int, max_codeword_len: int):
    reader.collect(max_codeword_len)
    entry = decode_table[reader.peek(table_bits)]
    symbol = entry >> DECODE_TABLE_SYMBOL_SHIFT
    length = entry & DECODE_TABLE_LENGTH_MASK
    if max_codeword_len > table_bits and entry >= (1 << (table_bits + DECODE_TABLE_SYMBOL_SHIFT)):
        reader.read(table_bits)
        entry = decode_table[symbol + reader.peek(length)]
        symbol = entry >> DECODE_TABLE_SYMBOL_SHIFT
        length = entry & DECODE_TABLE_LENGTH_MASK
    reader.read(length)
    return symbol
def parse_lzma_properties(data, version, min_dict=0, max_dict=4294967295)
Expand source code Browse git
def parse_lzma_properties(data: memoryview | bytes | bytearray, version: int, min_dict: int = 0, max_dict: int = 0xFFFFFFFF):
    pv = data[0]
    if version == 1:
        ds = int.from_bytes(data[1:5], 'little')
        pv, lc = divmod(pv, 9)
        pb, lp = divmod(pv, 5)
        _valid = lc <= 8 and lp <= 4 and pb <= 4
        if not _valid:
            raise ValueError(F'Invalid LZMA1 properties {lc=} {lp=} {pb=}.')
        df = dict(id=lzma.FILTER_LZMA1, lc=lc, pb=pb, lp=lp, dict_size=ds)
    elif version == 2:
        if pv > 40:
            raise ValueError(F'Invalid LZMA2 encoded dictionary size {pv=}')
        ph, pl = divmod(pv, 2)
        ds = 0xFFFFFFFF if pv == 40 else (pl + 2) << (ph + 11)
        df = dict(id=lzma.FILTER_LZMA2, dict_size=ds)
    else:
        raise ValueError(F'Invalid LZMA version {version}.')
    if ds > max_dict:
        raise ValueError(F'Dictionary size in excess of 0x{max_dict:08X}.')
    if ds < min_dict:
        raise ValueError(F'Dictionary size is less than 0x{min_dict:08X}.')
    return df
def decompress_pieces(decompressor, data, size=1048576)

Feed the given data to the given decompressor and generate its output in pieces of at most the given size. The output ends where the decompressor reaches the end of its stream or where the data ends.

Expand source code Browse git
def decompress_pieces(
    decompressor: lzma.LZMADecompressor | bz2.BZ2Decompressor,
    data: buf,
    size: int = 0x100000,
) -> Iterator[buf]:
    """
    Feed the given data to the given decompressor and generate its output in pieces of at most the
    given size. The output ends where the decompressor reaches the end of its stream or where the
    data ends.
    """
    view = memoryview(data)
    position = 0
    while not decompressor.eof:
        if decompressor.needs_input:
            if position >= len(view):
                return
            chunk = view[position:position + size]
            position += len(chunk)
        else:
            chunk = B''
        if piece := decompressor.decompress(chunk, size):
            yield piece
def inflate_pieces(data, size=1048576)

Inflate the given zlib stream and generate the output in pieces of at most the given size. The output ends where the stream or the data ends.

Expand source code Browse git
def inflate_pieces(data: buf, size: int = 0x100000) -> Iterator[buf]:
    """
    Inflate the given zlib stream and generate the output in pieces of at most the given size. The
    output ends where the stream or the data ends.
    """
    inflate = zlib.decompressobj()
    view = memoryview(data)
    position = 0
    tail: buf = B''
    while not inflate.eof:
        if not tail:
            if position >= len(view):
                break
            tail = view[position:position + size]
            position += len(tail)
        if piece := inflate.decompress(tail, size):
            yield piece
        tail = inflate.unconsumed_tail
    if rest := inflate.flush():
        yield rest

Classes

class BitBufferedReader (buffer, bits_per_read=32)

A helper class to read bitwise from the compressed input stream.

Expand source code Browse git
class BitBufferedReader:
    """
    A helper class to read bitwise from the compressed input stream.
    """

    def __init__(self, buffer: bytearray | StructReader, bits_per_read: int = 32):
        if not isinstance(buffer, StructReader):
            buffer = StructReader(memoryview(buffer), bigendian=False)
        self._reader: StructReader[memoryview] = buffer
        self._bit_buffer_data: int = 0
        self._bit_buffer_size: int = 0
        self._bits_per_read = bits_per_read

    def variable_length_integer(self) -> int:
        value = 1
        while True:
            chunk = self.read(2)
            value = (value << 1) + (chunk >> 1)
            if not chunk & 1:
                return value

    @property
    def overshoot(self) -> int:
        return self._bit_buffer_size // 8

    def __getattr__(self, k):
        return getattr(self._reader, k)

    def __next__(self) -> int:
        return self.read(1)

    def next(self):
        return self.read(1)

    def peek(self, count: int):
        return self._bit_buffer_data >> self.collect(count)

    def __len__(self):
        return self._bit_buffer_size

    def __getitem__(self, k: int):
        if k not in range(self._bit_buffer_size):
            raise IndexError(k)
        offset = self._bit_buffer_size - k
        return (self._bit_buffer_data >> offset) & 1

    def __enter__(self):
        return self

    def __exit__(self, *_):
        return False

    def read(self, count: int) -> int:
        offset = self.collect(count)
        bits = self._bit_buffer_data >> offset
        self._bit_buffer_data ^= bits << offset
        self._bit_buffer_size -= count
        assert self._bit_buffer_data.bit_length() <= self._bit_buffer_size
        assert bits.bit_length() <= count
        return bits

    def collect(self, count: int | None = None) -> int:
        if count is None:
            count = self._bits_per_read
        offset = self._bit_buffer_size - count
        if offset < 0:
            more = count - self._bit_buffer_size
            reads, _r = divmod(more, self._bits_per_read)
            reads += int(bool(_r))
            reads *= self._bits_per_read
            self._bit_buffer_data <<= reads
            self._bit_buffer_data |= self._reader.read_integer(reads)
            self._bit_buffer_size += reads
            offset += reads
            assert offset >= 0
        return offset

    def align(self):
        self._bit_buffer_size = 0
        self._bit_buffer_data = 0

Instance variables

var overshoot
Expand source code Browse git
@property
def overshoot(self) -> int:
    return self._bit_buffer_size // 8

Methods

def variable_length_integer(self)
Expand source code Browse git
def variable_length_integer(self) -> int:
    value = 1
    while True:
        chunk = self.read(2)
        value = (value << 1) + (chunk >> 1)
        if not chunk & 1:
            return value
def next(self)
Expand source code Browse git
def next(self):
    return self.read(1)
def peek(self, count)
Expand source code Browse git
def peek(self, count: int):
    return self._bit_buffer_data >> self.collect(count)
def read(self, count)
Expand source code Browse git
def read(self, count: int) -> int:
    offset = self.collect(count)
    bits = self._bit_buffer_data >> offset
    self._bit_buffer_data ^= bits << offset
    self._bit_buffer_size -= count
    assert self._bit_buffer_data.bit_length() <= self._bit_buffer_size
    assert bits.bit_length() <= count
    return bits
def collect(self, count=None)
Expand source code Browse git
def collect(self, count: int | None = None) -> int:
    if count is None:
        count = self._bits_per_read
    offset = self._bit_buffer_size - count
    if offset < 0:
        more = count - self._bit_buffer_size
        reads, _r = divmod(more, self._bits_per_read)
        reads += int(bool(_r))
        reads *= self._bits_per_read
        self._bit_buffer_data <<= reads
        self._bit_buffer_data |= self._reader.read_integer(reads)
        self._bit_buffer_size += reads
        offset += reads
        assert offset >= 0
    return offset
def align(self)
Expand source code Browse git
def align(self):
    self._bit_buffer_size = 0
    self._bit_buffer_data = 0