Skip to content

Commit d5bd90b

Browse files
committed
Added codec: route
1 parent c8e3dcd commit d5bd90b

4 files changed

Lines changed: 137 additions & 0 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,7 @@ This category also contains `ascii85`, `adobe`, `[x]btoa`, `zeromq` with the `ba
363363
- [X] `porta`: aka Porta Cipher (reciprocal variant of Vigenere Cipher with 13 alphabets)
364364
- [X] `railfence`: aka Rail Fence Cipher
365365
- [X] `rotN`: aka Caesar cipher (*N* belongs to [1,25])
366+
- [X] `routeN`: aka Route Cipher ; transposition along a route (spiral, rows, columns or snakes) in a grid of *N* columns
366367
- [X] `scytaleN`: encrypts using the number of letters on the rod (*N* belongs to [1,[)
367368
- [X] `shiftN`: shift ordinals (*N* belongs to [1,255])
368369
- [X] `skip`: aka Skip Cipher (transposition reading every N-th character, looping on the text)

‎docs/pages/enc/ciphers.md‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,27 @@ This implements the Rail Fence encoding algorithm, using 3 rails and offset 0 as
546546
```
547547

548548
-----
549+
### Route Cipher
550+
551+
This writes the text in a grid of N columns, by rows (or down the columns with `-down`), then reads it along a route starting from a corner (`tl` by default, `tr`, `bl` or `br`): an inward spiral, clockwise (`spiral`, the default) or counter-clockwise (`spiral-ccw`), row by row or column by column in the same direction (`rows`, `columns`, the "lines" of [dCode](https://www.dcode.fr/route-cipher)) or alternating directions (`snake-rows`, `snake-columns`, the "routes" of dCode). The last cells of the grid remain empty when the text does not fill it and are skipped by the route ; padding the text beforehand (e.g. with "`.`" as dCode) gives a full grid.
552+
553+
**Codec** | **Conversions** | **Aliases** | **Comment**
554+
:---: | :---: | --- | ---
555+
`route` | text <-> route ciphertext | `route-N`, `route-N-down`, `route-N-spiral-ccw-br`, `route_cipher_N_snake_rows`, ... | `N` (number of columns) is required
556+
557+
```python
558+
>>> codext.encode("this is a test", "route-4")
559+
'this etsa ist '
560+
>>> codext.decode("this etsa ist ", "route-4")
561+
'this is a test'
562+
>>> codext.encode("WEAREDISCOVEREDFLEEATONCEJX", "route-9-down-spiral-tr") # example of Wikipedia
563+
'EJXCTEDECDAEWRIORFEONALEVSE'
564+
>>> codext.encode("DCODEROUTE..", "route-4-snake-columns") # example of dCode
565+
'DETERCOO..UD'
566+
```
567+
568+
-----
569+
549570
### ROT N
550571

551572
This is a dynamic encoding, that is, it can be called with an integer to define the ROT offset. Encoding will apply a positive offset, decoding will apply a negative one.

‎src/codext/ciphers/__init__.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from .polybius import *
2323
from .railfence import *
2424
from .rot import *
25+
from .route import *
2526
from .scytale import *
2627
from .shift import *
2728
from .skip import *

‎src/codext/ciphers/route.py‎

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
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

Comments
 (0)