|
| 1 | +# -*- coding: UTF-8 -*- |
| 2 | +"""Route Cipher Codec - route transposition content encoding. |
| 3 | +
|
| 4 | +The route cipher is a transposition cipher: the text is written in a grid of N columns (by rows, or down the columns |
| 5 | + with "-down"), then read along a route starting from a corner (top-left by default, or "-tr", "-bl", "-br"): |
| 6 | +- "spiral" (default) and "spiral-ccw": inwards, clockwise or counter-clockwise ; |
| 7 | +- "rows" and "columns": row by row or column by column, in the same direction (the "lines" of dCode) ; |
| 8 | +- "snake-rows" and "snake-columns": row by row or column by column, alternating directions (the "routes" of dCode). |
| 9 | +The grid has as many rows as needed ; when the text does not fill it, its last cells remain empty and are skipped by the |
| 10 | + route (a full grid is obtained by padding the text beforehand, e.g. with "." as dCode or "X"). Every character is |
| 11 | + transposed, including whitespaces and punctuation. |
| 12 | +
|
| 13 | +This codec: |
| 14 | +- en/decodes strings from str to str |
| 15 | +- en/decodes strings from bytes to bytes |
| 16 | +- decodes file content to str (read) |
| 17 | +- encodes file content from str to bytes (write) |
| 18 | +
|
| 19 | +Reference: https://en.wikipedia.org/wiki/Transposition_cipher#Route_cipher |
| 20 | + https://www.dcode.fr/route-cipher |
| 21 | +""" |
| 22 | +from math import ceil |
| 23 | + |
| 24 | +from ..__common__ import * |
| 25 | + |
| 26 | + |
| 27 | +__examples__ = { |
| 28 | + 'enc(route-0|route-3-diagonal|route-3-spiral-xx|route)': None, |
| 29 | + # Wikipedia's example: 3 rows of 9 columns written downwards, read in a clockwise spiral from the top-right corner |
| 30 | + 'enc(route-9-down-spiral-tr|route_cipher_9_down_spiral_tr)': { |
| 31 | + 'WEAREDISCOVEREDFLEEATONCEJX': "EJXCTEDECDAEWRIORFEONALEVSE"}, |
| 32 | + # dCode's example: 4 columns, padded with ".", read along the vertical route from the top-left corner |
| 33 | + 'enc(route-4-snake-columns|route4_snake_columns-tl)': {'DCODEROUTE..': "DETERCOO..UD", 'DCODEROUTE': "DETERCOOUD"}, |
| 34 | + 'enc(route-3|route-3-spiral|route-3-spiral-tl)': {'ABCDEFGHI': "ABCFIHGDE", 'ABCDEFGH': "ABCFHGDE"}, |
| 35 | + 'enc(route-3-spiral-ccw)': {'ABCDEFGHI': "ADGHIFCBE"}, |
| 36 | + 'enc(route-3-spiral-br)': {'ABCDEFGHI': "IHGDABCFE"}, |
| 37 | + 'enc(route-3-rows-br)': {'ABCDEFGHI': "IHGFEDCBA"}, |
| 38 | + 'enc(route-3-columns)': {'ABCDEFGHI': "ADGBEHCFI"}, |
| 39 | + 'enc(route-3-snake-rows-tr)': {'ABCDEFGHI': "CBADEFIHG"}, |
| 40 | + 'enc(route-3-down-rows)': {'ABCDEFGHI': "ADGBEHCFI"}, |
| 41 | + 'enc-dec(route-2|route-5-down|route-4-snake-rows-bl|route-7-spiral-ccw-br|route-3-down-snake-columns-tr)': [ |
| 42 | + "", "a", "Hello, World!", "We are discovered, flee at once!", "@random{512,1024,2048}"], |
| 43 | +} |
| 44 | +__guess__ = ["route-%d" % n for n in range(2, 9)] + ["route-%d-spiral-ccw" % n for n in range(2, 9)] |
| 45 | + |
| 46 | + |
| 47 | +# directions (row, column) in clockwise order, and the first direction of a clockwise spiral by starting corner |
| 48 | +CW, FIRST = [(0, 1), (1, 0), (0, -1), (-1, 0)], {'tl': 0, 'tr': 1, 'br': 2, 'bl': 3} |
| 49 | + |
| 50 | + |
| 51 | +def _route(rows, cols, path, corner): |
| 52 | + """ Get the positions (row, column) of a grid in the order of the given route from the given corner. """ |
| 53 | + bottom, right = corner[0] == "b", corner[1] == "r" |
| 54 | + rs, cs = range(rows)[::-1 if bottom else 1], range(cols)[::-1 if right else 1] |
| 55 | + if path.startswith("spiral"): |
| 56 | + ccw = path.endswith("ccw") |
| 57 | + # a counter-clockwise spiral goes along the other side of its corner first, turning the other way |
| 58 | + d, step = (FIRST[corner] + ccw) % 4, -1 if ccw else 1 |
| 59 | + r, c, seen, route = rs[0], cs[0], set(), [] |
| 60 | + for _ in range(rows * cols): |
| 61 | + route.append((r, c)) |
| 62 | + seen.add((r, c)) |
| 63 | + for _ in range(4): |
| 64 | + nr, nc = r + CW[d][0], c + CW[d][1] |
| 65 | + if 0 <= nr < rows and 0 <= nc < cols and (nr, nc) not in seen: |
| 66 | + break |
| 67 | + d = (d + step) % 4 |
| 68 | + r, c = nr, nc |
| 69 | + return route |
| 70 | + snake, by_rows = path.startswith("snake"), path.endswith("rows") |
| 71 | + outer, inner = (rs, cs) if by_rows else (cs, rs) |
| 72 | + route = [] |
| 73 | + for i, x in enumerate(outer): |
| 74 | + for y in (inner[::-1] if snake and i % 2 else inner): |
| 75 | + route.append((x, y) if by_rows else (y, x)) |
| 76 | + return route |
| 77 | + |
| 78 | + |
| 79 | +def _order(n, length, down, path, corner): |
| 80 | + """ Get the indices of the characters of the text in the order of the route. """ |
| 81 | + if n <= 0: |
| 82 | + raise LookupError("Bad parameter for encoding 'route': the number of columns must be a positive integer") |
| 83 | + if length == 0: |
| 84 | + return [] |
| 85 | + rows = ceil(length / n) |
| 86 | + # positions of the characters in the grid, written by rows or down the columns |
| 87 | + fill = [(i % rows, i // rows) if down else (i // n, i % n) for i in range(length)] |
| 88 | + index = {p: i for i, p in enumerate(fill)} |
| 89 | + return [index[p] for p in _route(rows, n, path, corner) if p in index] |
| 90 | + |
| 91 | + |
| 92 | +def _params(n, down, path, corner): |
| 93 | + return int(n or 0), down == "down", (path or "spiral").replace("_", "-"), corner or "tl" |
| 94 | + |
| 95 | + |
| 96 | +def route_encode(n, down="", path="", corner=""): |
| 97 | + n, down, path, corner = _params(n, down, path, corner) |
| 98 | + def encode(text, errors="strict"): |
| 99 | + return "".join(text[i] for i in _order(n, len(text), down, path, corner)), len(text) |
| 100 | + return encode |
| 101 | + |
| 102 | + |
| 103 | +def route_decode(n, down="", path="", corner=""): |
| 104 | + n, down, path, corner = _params(n, down, path, corner) |
| 105 | + def decode(text, errors="strict"): |
| 106 | + r = [""] * len(text) |
| 107 | + for c, i in zip(text, _order(n, len(text), down, path, corner)): |
| 108 | + r[i] = c |
| 109 | + return "".join(r), len(text) |
| 110 | + return decode |
| 111 | + |
| 112 | + |
| 113 | +add("route", route_encode, route_decode, r"^route(?:[-_]cipher)?[-_]?(\d+)(?:[-_](down))?" |
| 114 | + r"(?:[-_](spiral[-_]ccw|spiral|snake[-_]rows|snake[-_]columns|rows|columns))?(?:[-_](tl|tr|bl|br))?$") |
0 commit comments