|
| 1 | +# -*- coding: UTF-8 -*- |
| 2 | +"""Bifid/Trifid Cipher Codec - bifid/trifid content encoding. |
| 3 | +
|
| 4 | +The Bifid and Trifid ciphers were invented by Felix Delastelle. Each letter is replaced by its coordinates in a 5x5 |
| 5 | + Polybius square (Bifid ; I and J sharing a cell) or in a 3x3x3 cube (Trifid ; 26 letters and "+"). Within each block of |
| 6 | + letters (whose size is the period, the whole text by default), the coordinates are written in rows (all the rows, then |
| 7 | + all the columns, ...), then read again as groups of coordinates that give the new letters. The square or cube is |
| 8 | + filled with an optional keyword (without duplicates), then with the remaining characters of the alphabet. |
| 9 | +
|
| 10 | +This codec: |
| 11 | +- en/decodes strings from str to str |
| 12 | +- en/decodes strings from bytes to bytes |
| 13 | +- decodes file content to str (read) |
| 14 | +- encodes file content from str to bytes (write) |
| 15 | +
|
| 16 | +Reference: https://en.wikipedia.org/wiki/Bifid_cipher |
| 17 | + https://en.wikipedia.org/wiki/Trifid_cipher |
| 18 | +""" |
| 19 | +from string import ascii_uppercase as UC |
| 20 | + |
| 21 | +from ..__common__ import * |
| 22 | + |
| 23 | + |
| 24 | +__examples1__ = { |
| 25 | + 'enc(bifid-bgwkzqpndsioaxefclumthyvr)': {'FLEEATONCE': "UAEOLWRINS", 'Flee at once!': "Uaeo lw rins!"}, |
| 26 | + 'dec(bifid_cipher-bgwkzqpndsioaxefclumthyvr)': {'UAEOLWRINS': "FLEEATONCE"}, |
| 27 | + 'enc(bifid)': {'TEST': "QTUO", 'Jump': "Inuk"}, |
| 28 | + 'enc-dec(bifid|bifid-keyword|bifid-keyword-5|bifid-7|bifid-0)': ["", "Hello, World!", "Test 1234!", |
| 29 | + "Delastelle's ciphers"], |
| 30 | +} |
| 31 | +__examples2__ = { |
| 32 | + 'enc(trifid-felixmariedelastelle-5)': {'AIDETOILECIELTAIDERA': "FMJFVOISSUFTFPUFEQQC", |
| 33 | + "aide-toi, le ciel t'aidera": "FMJF-VOI, SS UFTF P'UFEQQC"}, |
| 34 | + 'dec(trifid_cipher-felixmariedelastelle-5)': {'FMJFVOISSUFTFPUFEQQC': "AIDETOILECIELTAIDERA"}, |
| 35 | + 'enc(trifid)': {'TEST': "UTBK"}, |
| 36 | + 'enc-dec(trifid|trifid-keyword|trifid-keyword-5|trifid-7|trifid-0)': ["", "HELLO, WORLD!", "JUMP + 42"], |
| 37 | +} |
| 38 | +__guess1__, __guess2__ = ["bifid"], ["trifid"] |
| 39 | + |
| 40 | + |
| 41 | +# cipher: (alphabet, base of the coordinates, number of coordinates per letter) |
| 42 | +_PARAMS = {'bifid': (UC.replace("J", ""), 5, 2), 'trifid': (UC + "+", 3, 3)} |
| 43 | + |
| 44 | + |
| 45 | +def __fractionation(name, decode=False): |
| 46 | + alphabet, base, n = _PARAMS[name] |
| 47 | + def _wrapper(key=None, period=None): |
| 48 | + key, square = str(key or "").upper(), [] |
| 49 | + for c in (key.replace("J", "I") if name == "bifid" else key) + alphabet: |
| 50 | + if c in alphabet and c not in square: |
| 51 | + square.append(c) |
| 52 | + coords = {c: tuple(i // base ** (n - 1 - k) % base for k in range(n)) for i, c in enumerate(square)} |
| 53 | + letters = {v: c for c, v in coords.items()} |
| 54 | + period = int(str(period or 0)) |
| 55 | + def _code(text, errors="strict"): |
| 56 | + text = ensure_str(text) |
| 57 | + # only letters of the alphabet are fractionated ; other characters are kept in place |
| 58 | + slots, chars = [], [] |
| 59 | + for i, c in enumerate(text): |
| 60 | + if (u := c.upper()) == "J" and name == "bifid": |
| 61 | + u = "I" |
| 62 | + if u in coords: |
| 63 | + slots.append(i) |
| 64 | + chars.append(u) |
| 65 | + result, size = [], period or len(chars) or 1 |
| 66 | + for b in range(0, len(chars), size): |
| 67 | + block = chars[b:b+size] |
| 68 | + if decode: |
| 69 | + # the coordinates of the block, written in a row, hold the rows of the encoding matrix one by one |
| 70 | + digits, l = [d for c in block for d in coords[c]], len(block) |
| 71 | + result += [letters[tuple(digits[k*l+j] for k in range(n))] for j in range(l)] |
| 72 | + else: |
| 73 | + digits = [coords[c][k] for k in range(n) for c in block] |
| 74 | + result += [letters[tuple(digits[j:j+n])] for j in range(0, len(digits), n)] |
| 75 | + r = list(text) |
| 76 | + for pos, c in zip(slots, result): |
| 77 | + # with Trifid, a letter may be substituted by "+", which cannot hold its case ; hence, uppercase |
| 78 | + r[pos] = c.lower() if name == "bifid" and text[pos].islower() else c |
| 79 | + return (s := "".join(r)), len(s) |
| 80 | + return _code |
| 81 | + return _wrapper |
| 82 | + |
| 83 | + |
| 84 | +for _name, _ex, _gs in [("bifid", __examples1__, __guess1__), ("trifid", __examples2__, __guess2__)]: |
| 85 | + add(_name, __fractionation(_name), __fractionation(_name, True), |
| 86 | + rf"^{_name}(?:[-_]cipher)?(?:[-_]([a-zA-Z]+))?(?:[-_](\d+))?$", |
| 87 | + examples=_ex, guess=_gs, printables_rate=1., penalty=.1) |
0 commit comments