#!/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()