#!/usr/bin/env python3 """A reference encoder and decoder for Detour files, written from the format's description, independently of the Rust `detour` tool. It is slow (about a million letters a second) but short, so it can be read and checked. python3 reference.py decode [--max-length L] FILE.detour [OUTPUT] write the word (to standard output if no OUTPUT) python3 reference.py encode --n N [--alphabet SYMBOLS] [--sync E] INPUT OUTPUT.detour Standard library only (Python 3.10 or later). """ import argparse import hashlib import lzma import struct import sys MAGIC, VERSION = b"DTR1", 1 BASE = {1: 0, 2: 1, 3: 3, 4: 9, 5: 33} # the code byte of a step of weight k <= 5 and rank r is BASE[k] + r SYNC, BIG, LIT, START, END = 251, 252, 253, 254, 255 LITERAL_PIECE = 1 << 20 XZ_FILTERS = [{"id": lzma.FILTER_LZMA2, "preset": 9 | lzma.PRESET_EXTREME, "lc": 0, "lp": 0, "pb": 0}] XZ_MEMORY_LIMIT = 128 << 20 # a decoder refuses an xz stream that needs more memory than this to decompress class DetourError(ValueError): pass def factorial(k): result = 1 for i in range(2, k + 1): result *= i return result def rank(removed, new): """The rank of `new` as an ordering of `removed`; the reference order is `removed` reversed.""" reference = list(reversed(removed)) r = 0 for j, letter in enumerate(new): c = reference.index(letter) r = r * (len(removed) - j) + c del reference[c] return r def unrank(removed, r): k = len(removed) if not 0 <= r < factorial(k): raise DetourError(f"rank {r} is out of range for weight {k}") reference = list(reversed(removed)) digits = [] for base in range(1, k + 1): # the last digit has base 1, the first base k digits.append(r % base) r //= base digits.reverse() out = bytearray() for c in digits: out.append(reference.pop(c)) return bytes(out) def default_weight(levels, n): """The least weight k whose level is below n - k, or None when every level is finished.""" for k in range(1, n): if levels[k] < n - k: return k return None def record(levels, k): levels[k] += 1 for j in range(1, k): levels[j] = 0 def put_varint(out, v): while v >= 0x80: out.append((v & 0x7F) | 0x80) v >>= 7 out.append(v) def check_header(n, alphabet, sync): if not 2 <= n <= 16 or len(alphabet) != n or len(set(alphabet)) != n or not 0 <= sync <= 40: raise DetourError("bad header") def encode_tokens(word, n, alphabet, sync=0): """The token stream for `word` (bytes): what a .detour file holds inside its xz container.""" alphabet = bytes(alphabet) check_header(n, alphabet, sync) out = bytearray(MAGIC + bytes([VERSION, n]) + alphabet + bytes([sync])) symbols = set(alphabet) gap = since_sync = 0 levels = [0] * (n + 1) window = None # the current window, or None after a literal pending = bytearray() def token(code, payload=b""): nonlocal gap, since_sync put_varint(out, gap) gap = 0 out.append(code) out.extend(payload) since_sync += 1 def literal(data): payload = bytearray() put_varint(payload, len(data)) token(LIT, bytes(payload) + bytes(data)) def maybe_sync(offset): nonlocal since_sync if sync and since_sync >= 1 << sync: put_varint(out, 0) out.append(SYNC) out.extend(struct.pack("= n else b"" if len(last) == n and set(last) <= symbols and len(set(last)) == n: if window is not None and len(pending) <= n: k = len(pending) r = rank(window[:k], bytes(pending)) deviates = not (default_weight(levels, n) == k and r == 0) if deviates: if k <= 5: token(BASE[k] + r) else: payload = bytearray([k]) put_varint(payload, r) token(BIG, bytes(payload)) else: gap += 1 record(levels, k) window = window[k:] + bytes(pending) if deviates: maybe_sync(i + 1) else: if len(pending) > n: literal(pending[:-n]) window = bytes(last) levels = [0] * (n + 1) token(START, window) maybe_sync(i + 1) pending = bytearray() elif len(pending) >= LITERAL_PIECE: literal(pending[:-(n - 1)]) del pending[:-(n - 1)] window = None if pending: literal(pending) put_varint(out, gap) out.append(END) out.extend(struct.pack(" len(self.data): raise DetourError("the stream ends early") chunk = self.data[self.pos:self.pos + k] self.pos += k return chunk def byte(self): return self.take(1)[0] def varint(self): """An unsigned LEB128 varint of at most 10 bytes, below 2^64, in its shortest form: a varint of two or more bytes may not end in the byte 0x00.""" value = 0 for i in range(10): b = self.byte() value |= (b & 0x7F) << (7 * i) if b < 0x80: if value >= 1 << 64: break if i > 0 and b == 0: raise DetourError("a varint is not in its shortest form") return value raise DetourError("a varint is too long") def decode_tokens(data, limit=None): """The word a token stream holds, checking the stream as the Rust decoder does.""" rd = Reader(data) if rd.take(4) != MAGIC: raise DetourError("not a Detour stream") if rd.byte() != VERSION: raise DetourError("unsupported version") n = rd.byte() if not 2 <= n <= 16: raise DetourError("bad header") alphabet = rd.take(n) sync = rd.byte() check_header(n, alphabet, sync) out = bytearray() window = None levels = [0] * (n + 1) since_sync = 0 after_window_token = False def emit(letters): if limit is not None and len(out) + len(letters) > limit: raise DetourError(f"the word is longer than {limit} letters") out.extend(letters) def step(k, r): nonlocal window new = unrank(window[:k], r) emit(new) window = window[k:] + new record(levels, k) while True: gap = rd.varint() code = rd.byte() due = sync and after_window_token and since_sync >= 1 << sync if due and (code != SYNC or gap): raise DetourError("a SYNC token is missing here") if code == SYNC and not due: raise DetourError("a SYNC token where none is due") after_window_token = False if gap and window is None: raise DetourError("default steps with no current window") for _ in range(gap): k = default_weight(levels, n) if k is None: raise DetourError("a default step where every level is finished") step(k, 0) if code == SYNC: offset = struct.unpack(" n: raise DetourError(f"a step of weight {k} with n = {n}") if default_weight(levels, n) == k and r == 0: raise DetourError("a step token equal to the default step") step(k, r) after_window_token = True length = struct.unpack("= 14 else 0) with open(args.input, "rb") as f: tokens = encode_tokens(f.read(), args.n, alphabet, sync) with open(args.output, "wb") as f: f.write(lzma.compress(tokens, format=lzma.FORMAT_XZ, check=lzma.CHECK_CRC64, filters=XZ_FILTERS)) except (DetourError, OSError) as error: sys.exit(f"reference.py: {error}") if __name__ == "__main__": main()