CCCPaste

Vanguard Bandits LZSS

#!/usr/bin/env python

"""
A fairly straight port of Haruhiko Okumura's LZSS codec from C.
Nothing really clever, and not very pythonic.  (I'm not that good at this honestly.)
"""

# Like its C equiv., has problems when idx isn't what it would like.  Tied to node init, and instead of attempting to fix this came up with an equally problematic ringless-ring encoder.
def encode(data, ring, limit, threshold, idx, fill, byteorder, condition=True):
    """
    Compresses [data], returning a bytes object.

    [ring] may be any size between 0x4000 and 0x100.
    [limit] set the longest length for a copy; proper values depend on ring and threshold.
    [threshold] is the shortest acceptable copy, minus one.
    [idx] is typically ring - limit but may differ on application.
    [fill] is used to initialize the ring.
        *) For text applications use a space (b' ', or ASCII code 0x20)
        *) Binary applications use a zero (b'\\0' or 0)
    [byteorder] sets the byteorder copy commands are written in.
        Usually this is 'little', but 'big' is also acceptable.
    [condition] sets the flag state used for literals and dictionary hits.
    """
    output = bytearray()
    if not data: return output

    from itertools import repeat

    # Initialize the ring buffer with a common fill value.
    if isinstance(fill, (bytes, bytearray)):
        fill = fill[0]
    rng = bytearray(repeat(fill, ring + limit))
    matchlen, matchpos, nil = 0, 0, ring
    # Initialize the trees.
    lson= list(bytes(ring + 1))
    rson= list(bytes(ring + 1))
    rson.extend(repeat(nil, 256))
    dad = list(repeat(nil, ring + 1))

    # Create the masks for encoding values.
    # This assumes minimum ring of 0x100, maximum length of 0x100 + threashold + 1.
    r_mask = ring - 1
    b_shft = r_mask.bit_length() - 8
    b_mask = (0xFF00 >> b_shft) & 0xFF
    condition = 0x80 if condition else 0
    noncondition = condition ^ 0x80

    def InsertNode(v):
        """Inserts string into trees and returns
        longest match position and length as a tuple.
        If match length equals the limit, it replaces the old node.
        By embedding this function it can use the trees and ring globally."""
        cmp, ml, mp = 1, 0, matchpos
        key = rng[v:]
        pos = ring + 1 + key[0]
        rson[v] = nil
        lson[v] = nil
        while True:
            if cmp>=0:
                if rson[pos] != nil:
                    pos = rson[pos]
                else:
                    rson[pos] = v
                    dad[v] = pos
                    return (ml, mp)
            else:
                if lson[pos] != nil:
                    pos = lson[pos]
                else:
                    lson[pos] = v
                    dad[v] = pos
                    return (ml, mp)
            for i in range(1, limit+1):
                cmp = key[i] - rng[pos + i]
                if cmp:
                    break
            if i > ml:
                mp, ml = pos, i
                if ml >= limit: break
        dad[v] = dad[pos]
        lson[v]=lson[pos]
        rson[v]=rson[pos]
        dad[lson[pos]] = v
        dad[rson[pos]] = v
        if rson[dad[pos]] == pos:
            rson[dad[pos]] = v
        else:
            lson[dad[pos]] = v
        dad[pos] = nil
        return (ml, mp)

    def DeleteNode(pos):
        if dad[pos] == nil: return
        # If it's in the tree, delete it.
        q = lson[pos]
        if lson[pos] == nil:
            q = rson[pos]
        elif rson[pos] != nil:
            if rson[q] != nil:
                while True:
                    q = rson[q]
                    if rson[q] == nil: break
                rson[dad[q]] = lson[q]
                dad[lson[q]] = dad[q]
                lson[q] = lson[pos]
                dad[lson[pos]] = q
            rson[q] = rson[pos]
            dad[rson[pos]] = q
        dad[q] = dad[pos]
        if rson[dad[pos]] == pos:
            rson[dad[pos]] = q
        else:
            lson[dad[pos]] = q
        dad[pos] = nil

    # Unset flags on copies; send when less than 256.
    mask = 0xFF00
    codebuf = bytearray()
    s = 0
    # Read limit bytes into the ring at idx.
    p = min(limit, len(data)-1)
    rng[idx:idx+p] = data[0:p]
    cur = p
    for i in range(1, limit+1):
        matchlen, matchpos = InsertNode(idx - i)
    matchlen, matchpos = InsertNode(idx)
    # Now you're initialized, so do the rest of the file.
    while True:
        mask>>=1
        matchlen = min(matchlen, p)
        if matchlen <= threshold:
            matchlen = 1
            codebuf.append(rng[idx])
            mask ^= noncondition
        else:
            mask ^= condition
            a = (matchpos>>b_shft) & b_mask
            a |= (matchlen - threshold - 1)
            b = matchpos&0xFF
            if byteorder == 'little':
                codebuf.extend((b,a))
            else:
                codebuf.extend((a,b))
        # Flush when the mask is full.
        if mask < 256:
            output.append(mask)
            output.extend(codebuf)
            codebuf = bytearray()
            mask = 0xFF00
        prevmatchlen = matchlen
        j = min(prevmatchlen, len(data)-cur)
        for i in range(j):
            DeleteNode(s)
            rng[s] = data[cur]
            if s < (limit - 1):
                rng[s + ring] = data[cur]
            cur+=1
            # Correct the ring position via modulo ring
            s+=1
            s&= r_mask
            idx+=1
            idx&= r_mask
            matchlen, matchpos = InsertNode(idx)
        # Flush the rest of the buffer if necessary.
        for i in range(j, prevmatchlen):
            DeleteNode(s)
            s+=1
            s&= r_mask
            idx+=1
            idx&= r_mask
            p-=1
            if p:
                matchlen, matchpos = InsertNode(idx)
        # Loop until source empty.
        if not p:
            break
    # Flush remaining output; mask the lead bit off the mask.
    if codebuf:
        ## mask &= ~(1<>l)
        output.extend(codebuf)
    return output

def decode(data, out_sz, ring, threshold, idx, fill, byteorder, bitorder='bottom', condition=True, count=-1, relative=False):
    """
    Decompresses [data] originally compressed with encode(),
        returning a bytes object.
    if [out_sz] is None, will decompress to the end of file.
    [ring], [threshold], [idx], [fill], and [byteorder]
        should match the original compression settings.

    [ring] may be any size between 0x4000 and 0x100.
    [threshold] is the shortest acceptable copy, minus one.
    [idx] is typically ring - limit but may differ on application.
    [fill] is used to initialize the ring.
        *) For text applications use a space (b' ', or ASCII code 0x20)
        *) Binary applications use a zero (b'\\0' or 0)
    [byteorder] sets the byteorder copy commands are written in.
        Usually this is 'little', but 'big' is also acceptable.
    [bitorder] indicates if command bits are read from the 'bottom' or 'top'.
    [condition] sets if a literal is written when a flag is True or False.
    [count], when set, terminates decompression after the given number of commands.
    [relative], if True, effectively is ringless.
    """
    output, buffer = bytearray(), bytearray(ring)
    # set up ring buffer
    if fill:
        from itertools import repeat
        if isinstance(fill, str):
            fill = fill.encode()
        if isinstance(fill, (bytes, bytearray)):
            fill = fill[0]
        buffer[0:idx] = repeat(fill, idx)
    bits = 0
    c_mask = 0
    c_refill = 1 if bitorder=='bottom' else 0x80
    d = iter(data)

    r_mask = ring-1
    threshold += 1
    b_shift = r_mask.bit_length() - 8
    s_mask = (0xFF >> b_shift) & 0xFF
    b_mask = s_mask ^ 0xFF

    try:
     while count:
        #if bits<2:
        #    bits = next(d) | 0x100
        #if(bits&1):
        if not c_mask:
            bits = next(d)
            c_mask = c_refill
        count -= 1
        if bool(bits&c_mask) == condition:
            v = next(d)
            buffer[idx]=v
            idx=(idx+1)&r_mask
            output.append(v)
        else:
            if byteorder == 'little':
                val = next(d)
                size = next(d)
            else:
                size = next(d)
                val = next(d)
            back = (size & b_mask) << b_shift
            back &= 0xFF00
            back |= val
            # back &= r_mask
            size = (size & s_mask) + threshold
            if relative:
                for x in range(size):
                    buffer[idx] = output[-back]
                    output.append(output[-back])
                    idx=(idx+1)&r_mask
            else:
                for x in range(size):
                    buffer[idx] = buffer[back]
                    output.append(buffer[back])
                    idx=(idx+1)&r_mask
                    back = (back+1)&r_mask
        #bits>>=1
        if bitorder == 'bottom':
            c_mask <<= 1
        else:
            c_mask >>= 1
        c_mask &= 0xFF
        if out_sz:
            if out_sz<=len(output):
                break
    except IndexError:
        import warnings
        warnings.warn("\tError!  Insufficient data.\n  Probably not an LZSS file.\n")
        return bytes()
    except StopIteration as E:
        if out_sz:
            raise E
    return bytes(output)

def length(data, out_sz, ring, threshold, byteorder, bitorder='bottom', condition=True, count=-1):
    """
    Returns (compressed size, decompressed size).
    if [out_sz] is None, will decompress to the end of file.
    [ring], [threshold], and [byteorder]
        should match the original compression settings.

    [ring] may be any size between 0x4000 and 0x100.
    [threshold] is the shortest acceptable copy, minus one.
    [byteorder] sets the byteorder copy commands are written in.
        Usually this is 'little', but 'big' is also acceptable.
    [bitorder] indicates if command bits are read from the 'bottom' or 'top'.
    [condition] sets if a literal is written when a flag is True or False.
    [count], when set, terminates decompression after the given number of commands.
    """
    sz, out = 0, 0
    bits = 0
    c_mask = 0
    c_refill = 1 if bitorder=='bottom' else 0x80

    r_mask = ring-1
    threshold += 1
    b_shift = r_mask.bit_length() - 8
    s_mask = (0xFF >> b_shift) & 0xFF

    try:
     while count:
        if not c_mask:
            bits = data[sz]
            sz += 1
            c_mask = c_refill
        count -= 1
        if bool(bits&c_mask) == condition:
            v = data[sz]
            sz += 1
            out += 1
        else:
            if byteorder == 'little':
                val = data[sz]
                size = data[sz+1]
            else:
                size = data[sz]
                val = data[sz+1]
            sz += 2
            out += (size & s_mask) + threshold
        if bitorder == 'bottom':
            c_mask <<= 1
        else:
            c_mask >>= 1
        c_mask &= 0xFF
        if out_sz:
            if out_sz<=out:
                break
    except Exception as E:
        print(E)
        # Still return progress to this point.
    return (sz, out)

__modes = {'txt':0x20, 'bin':0}

def encode1KB(data, mode='bin', **kwargs):
    """Compresses [data] using 1KB ring and given fill mode,
        returning a bytes object.
    [mode] may be:
        'txt': fills buffer with spaces (b' ', or ASCII code 0x20)
        'bin': fills buffer with zeroes
    same as:
        encode(data, 1024, 66, 2, 958, mode, 'little', False)
    """
    f = __modes.get(mode)
    return encode(data, ring=1024, limit=66, threshold=2, idx=kwargs.pop('idx', 958), fill=f, byteorder=kwargs.pop('byteorder', 'little'), condition=kwargs.pop('condition', False), **kwargs)

def decode1KB(data, out_sz=None, mode='bin', **kwargs):
    """Decompresses [data] using 1KB ring and given fill [mode],
        returning a bytes object.
    If [out_sz] is not given, decompresses to the end of data.
    [mode] may be:
        'txt': fills buffer with spaces (b' ', or ASCII code 0x20)
        'bin': fills buffer with zeroes
    same as:
        decode(data, out_sz, 1024, 2, 958, mode, 'little', 'bottom', False)
    """
    f = __modes.get(mode)
    return decode(data, out_sz, ring=1024, threshold=2, idx=kwargs.pop('idx', 958), fill=f, byteorder=kwargs.pop('byteorder', 'little'), condition=kwargs.pop('condition', False), **kwargs)

def length1KB(data, out_sz=None, **kwargs):
    """Returns (compressed size, decompressed size) of [data] using 1KB ring,
        basically by faking decompression.
    If [out_sz] is not given, "decompresses" to the end of data.
    same as:
        length(data, out_sz, 1024, 2, 'little', 'bottom', False)
    """
    return length(data, out_sz, ring=1024, threshold=2, byteorder=kwargs.pop('byteorder', 'little'), condition=kwargs.pop('condition', False), **kwargs)

def main():
    pass

if __name__ == '__main__':
    main()