diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index 383e65c..6e21c90 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -7,9 +7,9 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.9", "3.10"] + python-version: ["3.11"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v3 with: diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..bcad6b1 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,3 @@ +[MESSAGES CONTROL] + +disable=too-few-public-methods, diff --git a/huffman.py b/huffman.py index 31034fe..332b0ef 100755 --- a/huffman.py +++ b/huffman.py @@ -1,23 +1,39 @@ #!/usr/bin/env python3 +""" +Teeworlds huffman compression + +Example usage:: + +from huffman import Huffman + +huffman = Huffman() +decompressed = huffman.decompress(bytes([ + 174, 149, 19, 92, 9, 87, 194, + 22, 177, 86, 220, 218, 34, 56, + 185, 18, 156, 168, 184, 1 +]))) +print(decompressed) # => b'hello world' +""" import functools from typing import Optional FREQUENCY_TABLE = [ - 1 << 30, 4545, 2657, 431, 1950, 919, 444, 482, 2244, 617, 838, 542, 715, 1814, 304, 240, 754, 212, 647, 186, - 283, 131, 146, 166, 543, 164, 167, 136, 179, 859, 363, 113, 157, 154, 204, 108, 137, 180, 202, 176, - 872, 404, 168, 134, 151, 111, 113, 109, 120, 126, 129, 100, 41, 20, 16, 22, 18, 18, 17, 19, - 16, 37, 13, 21, 362, 166, 99, 78, 95, 88, 81, 70, 83, 284, 91, 187, 77, 68, 52, 68, - 59, 66, 61, 638, 71, 157, 50, 46, 69, 43, 11, 24, 13, 19, 10, 12, 12, 20, 14, 9, - 20, 20, 10, 10, 15, 15, 12, 12, 7, 19, 15, 14, 13, 18, 35, 19, 17, 14, 8, 5, - 15, 17, 9, 15, 14, 18, 8, 10, 2173, 134, 157, 68, 188, 60, 170, 60, 194, 62, 175, 71, - 148, 67, 167, 78, 211, 67, 156, 69, 1674, 90, 174, 53, 147, 89, 181, 51, 174, 63, 163, 80, - 167, 94, 128, 122, 223, 153, 218, 77, 200, 110, 190, 73, 174, 69, 145, 66, 277, 143, 141, 60, - 136, 53, 180, 57, 142, 57, 158, 61, 166, 112, 152, 92, 26, 22, 21, 28, 20, 26, 30, 21, - 32, 27, 20, 17, 23, 21, 30, 22, 22, 21, 27, 25, 17, 27, 23, 18, 39, 26, 15, 21, - 12, 18, 18, 27, 20, 18, 15, 19, 11, 17, 33, 12, 18, 15, 19, 18, 16, 26, 17, 18, - 9, 10, 25, 22, 22, 17, 20, 16, 6, 16, 15, 20, 14, 18, 24, 335, 1517 ] + 1 << 30, 4545, 2657, 431, 1950, 919, 444, 482, 2244, + 617, 838, 542, 715, 1814, 304, 240, 754, 212, 647, 186, + 283, 131, 146, 166, 543, 164, 167, 136, 179, 859, 363, 113, 157, 154, 204, 108, 137, 180, 202, 176, + 872, 404, 168, 134, 151, 111, 113, 109, 120, 126, 129, 100, 41, 20, 16, 22, 18, 18, 17, 19, + 16, 37, 13, 21, 362, 166, 99, 78, 95, 88, 81, 70, 83, 284, 91, 187, 77, 68, 52, 68, + 59, 66, 61, 638, 71, 157, 50, 46, 69, 43, 11, 24, 13, 19, 10, 12, 12, 20, 14, 9, + 20, 20, 10, 10, 15, 15, 12, 12, 7, 19, 15, 14, 13, 18, 35, 19, 17, 14, 8, 5, + 15, 17, 9, 15, 14, 18, 8, 10, 2173, 134, 157, 68, 188, 60, 170, 60, 194, 62, 175, 71, + 148, 67, 167, 78, 211, 67, 156, 69, 1674, 90, 174, 53, 147, 89, 181, 51, 174, 63, 163, 80, + 167, 94, 128, 122, 223, 153, 218, 77, 200, 110, 190, 73, 174, 69, 145, 66, 277, 143, 141, 60, + 136, 53, 180, 57, 142, 57, 158, 61, 166, 112, 152, 92, 26, 22, 21, 28, 20, 26, 30, 21, + 32, 27, 20, 17, 23, 21, 30, 22, 22, 21, 27, 25, 17, 27, 23, 18, 39, 26, 15, 21, + 12, 18, 18, 27, 20, 18, 15, 19, 11, 17, 33, 12, 18, 15, 19, 18, 16, 26, 17, 18, + 9, 10, 25, 22, 22, 17, 20, 16, 6, 16, 15, 20, 14, 18, 24, 335, 1517] HUFFMAN_EOF_SYMBOL = 256 @@ -25,22 +41,34 @@ HUFFMAN_MAX_NODES = HUFFMAN_MAX_SYMBOLS * 2 - 1 HUFFMAN_LUTBITS = 10 -HUFFMAN_LUTSIZE = (1 << HUFFMAN_LUTBITS) -HUFFMAN_LUTMASK = (HUFFMAN_LUTSIZE - 1) +HUFFMAN_LUTSIZE = 1 << HUFFMAN_LUTBITS +HUFFMAN_LUTMASK = HUFFMAN_LUTSIZE - 1 class HuffmanConstructNode: + """ + Internal class to build the huffman tree + """ def __init__(self, node_id: int, frequency: int) -> None: self.node_id = node_id self.frequency = frequency -def compare_nodes_by_frequency_desc(node1: HuffmanConstructNode, node2: HuffmanConstructNode) -> int: +def compare_nodes_by_frequency_desc( + node1: HuffmanConstructNode, + node2: HuffmanConstructNode + ) -> int: + """ + Comparison callback for stable sort + """ if node2.frequency < node1.frequency: return -1 if node2.frequency > node1.frequency: return 1 return 0 -class Node: +class Node(): + """ + Internal class to represent the huffman tree + """ def __init__(self, bits, num_bits, symbol): self.bits = bits self.num_bits = num_bits @@ -48,7 +76,25 @@ def __init__(self, bits, num_bits, symbol): self.symbol = symbol class Huffman: + """ + Teeworlds huffman compression + + Example usage:: + + from huffman import Huffman + + huffman = Huffman() + decompressed = huffman.decompress(bytes([ + 174, 149, 19, 92, 9, 87, 194, + 22, 177, 86, 220, 218, 34, 56, + 185, 18, 156, 168, 184, 1 + ]))) + print(decompressed) # => b'hello world' + """ def setbits_r(self, node: Node, bits: int, depth: int) -> None: + """ + Internal method to initialize the huffman tree. + """ if node.leafs[1] != 0xFFFF: self.setbits_r(self.nodes[node.leafs[1]], bits|(1< None: node.num_bits = depth def construct_tree(self, frequencies: list[int]) -> None: - nodes_left: list[HuffmanConstructNode] = [HuffmanConstructNode(0, 0) for _ in range(0, HUFFMAN_MAX_SYMBOLS)] + """ + Internal method to initialize the huffman tree. + """ + nodes_left: list[HuffmanConstructNode] = [ + HuffmanConstructNode(0, 0) for _ in range(0, HUFFMAN_MAX_SYMBOLS) + ] num_nodes_left = HUFFMAN_MAX_SYMBOLS @@ -69,7 +120,7 @@ def construct_tree(self, frequencies: list[int]) -> None: self.nodes[i].leafs[0] = 0xFFFF self.nodes[i].leafs[1] = 0xFFFF - if(i == HUFFMAN_EOF_SYMBOL): + if i == HUFFMAN_EOF_SYMBOL: nodes_left[i].frequency = 1 else: nodes_left[i].frequency = frequencies[i] @@ -79,7 +130,10 @@ def construct_tree(self, frequencies: list[int]) -> None: self.num_nodes = HUFFMAN_MAX_SYMBOLS while num_nodes_left > 1: - nodes_left = sorted(nodes_left, key=functools.cmp_to_key(compare_nodes_by_frequency_desc)) + nodes_left = sorted( + nodes_left, + key=functools.cmp_to_key(compare_nodes_by_frequency_desc) + ) self.nodes[self.num_nodes].num_bits = 0 self.nodes[self.num_nodes].leafs[0] = nodes_left[num_nodes_left-1].node_id @@ -118,6 +172,9 @@ def __init__(self) -> None: self.decoded_lut[i] = node def decompress(self, data: bytes) -> bytes: + """ + Decompresses data using the teeworlds frequency table + """ src_index = 0 size = len(data) dst = bytearray() @@ -161,4 +218,3 @@ def decompress(self, data: bytes) -> bytes: break dst.append(node.symbol) return bytes(dst) -