Skip to content

Commit 4be073e

Browse files
committed
Improved codec: tap
1 parent 4947ff1 commit 4be073e

4 files changed

Lines changed: 118 additions & 51 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,7 @@ This category also contains `ascii85`, `adobe`, `[x]btoa`, `zeromq` with the `ba
425425
- [X] `radio`: aka NATO or radio phonetic alphabet
426426
- [X] `southpark`: converts letters to Kenny's language from Southpark (whitespace is also handled)
427427
- [X] `southpark-icase`: case insensitive variant of `southpark`
428-
- [X] `tap`: converts text to tap/knock code, commonly used by prisoners
428+
- [X] `tap`: converts text to tap/knock code, commonly used by prisoners (as written by cryptii)
429429
- [X] `tomtom`: similar to `morse`, using slashes and backslashes
430430

431431
#### [Others](https://python-codext.readthedocs.io/en/latest/enc/others.html)

‎docs/pages/enc/languages.md‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -168,17 +168,19 @@ This encodes text according to Kenny's language in Southpark.
168168

169169
### Tap
170170

171-
This codec implements the [tap/knock code](https://en.wikipedia.org/wiki/Tap_code) commonly used by prisoners. It uses 25 letters, "*k*" is encoded to the same token than "*c*".
171+
This codec implements the [tap/knock code](https://en.wikipedia.org/wiki/Tap_code) commonly used by prisoners. Each letter is written as its row and column in a 5x5 Polybius square of 25 letters, "*k*" being encoded as "*c*". As with [cryptii](https://cryptii.com/pipes/tap-code), the row and the column are separated by a space and the letters by two spaces ; words are separated by " / ".
172+
173+
When decoding, the dots are read in pairs whatever separates them (e.g. spaces, commas or the blank braille pattern written by former versions of this codec), a "/" giving a space.
172174

173175
**Codec** | **Conversions** | **Aliases** | **Comment**
174176
:---: | :---: | --- | ---
175-
`tap` | text <-> tap/knock encoded text | `knock`, `tap-code` | uses a large Unicode whitespace as a token separator ; Python 3 only
177+
`tap` | text <-> tap/knock encoded text | `knock`, `tap-code`, `tap-inv`, ... | `-inv` uses the reversed alphabet
176178

177179
```python
178180
>>> codext.encode("this is a test", "tap")
179-
'.... ....⠀.. ...⠀.. ....⠀.... ...⠀ ⠀.. ....⠀.... ...⠀ ⠀. .⠀ ⠀.... ....⠀. .....⠀.... ...⠀.... ....'
180-
>>> codext.decode(".... ....⠀.. ...⠀.. ....⠀.... ...⠀ ⠀.. ....⠀.... ...⠀ ⠀. .⠀ ⠀.... ....⠀. .....⠀.... ...⠀.... ....", "knock")
181-
'this is a test'
181+
'.... .... .. ... .. .... .... ... / .. .... .... ... / . . / .... .... . ..... .... ... .... ....'
182+
>>> codext.decode("..... .., . ., .... ...., . ....., .... ..", "knock")
183+
'water'
182184
```
183185

184186
-----

‎src/codext/languages/tap.py‎

Lines changed: 103 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,103 @@
1-
# -*- coding: UTF-8 -*-
2-
"""Tap code - Tap/knock code encoding.
3-
4-
The tap code (or knock code), used by prisoners of war (e.g. in Vietnam) to communicate by tapping on walls, writes each
5-
letter as its row and column in a 5x5 Polybius square, K being replaced by C. Each coordinate is written as dots (taps)
6-
separated by a space, the letters being separated by the blank braille pattern ; the "-inv" variant uses the reversed
7-
alphabet.
8-
9-
This codec:
10-
- en/decodes strings from str to str
11-
- en/decodes strings from bytes to bytes
12-
- decodes file content to str (read)
13-
- encodes file content from str to bytes (write)
14-
15-
Reference: https://en.wikipedia.org/wiki/Tap_code
16-
"""
17-
from ..__common__ import *
18-
19-
20-
__examples__ = {
21-
'enc(tap|knock-code|tap_code)': {'this is a test' : ".... ....⠀.. ...⠀.. ....⠀.... ...⠀ ⠀.. ....⠀.... ...⠀ ⠀. ."
22-
"⠀ ⠀.... ....⠀. .....⠀.... ...⠀.... ...."},
23-
}
24-
__guess__ = ["tap", "tap-inv"]
25-
26-
27-
def __build_encmap(a):
28-
d, i = {}, 0
29-
for x in range(1,6):
30-
for y in range(1,6):
31-
d[a[i]] = x * "." + " " + y * "."
32-
i += 1
33-
d['k'], d[' '] = d['c'], " "
34-
return d
35-
36-
37-
38-
ENCMAP = {
39-
'': __build_encmap("abcdefghijlmnopqrstuvwxyz"),
40-
'inv': __build_encmap("abcdefghijlmnopqrstuvwxyz"[::-1]),
41-
}
42-
43-
44-
add_map("tap", ENCMAP, ignore_case="both", sep="⠀", pattern=r"^(?:tap|knock)(?:[-_]code)?(|inv)$")
45-
1+
# -*- coding: UTF-8 -*-
2+
"""Tap code - Tap/knock code encoding.
3+
4+
The tap code (or knock code), used by prisoners of war (e.g. in Vietnam) to communicate by tapping on walls, writes each
5+
letter as its row and column in a 5x5 Polybius square, K being replaced by C. Each coordinate is written as dots
6+
(taps), as cryptii does: the row and the column are separated by a space and the letters by two spaces. The words are
7+
separated by " / " ; the "-inv" variant uses the reversed alphabet.
8+
9+
When decoding, the dots are read in pairs whatever separates them (e.g. spaces, commas or the blank braille pattern, as
10+
written by former versions of this codec), a "/" (or a space between blank braille patterns) giving a space.
11+
12+
This codec:
13+
- en/decodes strings from str to str
14+
- en/decodes strings from bytes to bytes
15+
- decodes file content to str (read)
16+
- encodes file content from str to bytes (write)
17+
18+
Reference: https://en.wikipedia.org/wiki/Tap_code
19+
https://cryptii.com/pipes/tap-code
20+
"""
21+
from ..__common__ import *
22+
23+
24+
__examples__ = {
25+
'enc(tap|knock-code|tap_code)': {
26+
'this is a test': ".... .... .. ... .. .... .... ... / .. .... .... ... / . . / .... .... . ..... "
27+
".... ... .... ....",
28+
'water': "..... .. . . .... .... . ..... .... ..",
29+
'Kick': ". ... .. .... . ... . ...",
30+
'test!': None,
31+
},
32+
'dec(tap)': {
33+
# Wikipedia example, with commas
34+
'..... .., . ., .... ...., . ....., .... ..': "water",
35+
# as written by former versions of this codec
36+
".... ....⠀.. ...⠀.. ....⠀.... ...⠀ ⠀.. ....⠀.... ...": "this is",
37+
'/ . . // . .. /': " a b ",
38+
'. . .': None,
39+
'. ......': None,
40+
'. / .': None,
41+
'. . x': None,
42+
},
43+
'enc-dec(tap|tap-inv)': ["this is a test", "the quicc brown fox jumps over the lazy dog", " ", ""],
44+
}
45+
__guess__ = ["tap", "tap-inv"]
46+
47+
48+
ALPHABETS = {'': "abcdefghijlmnopqrstuvwxyz", 'inv': "abcdefghijlmnopqrstuvwxyz"[::-1]}
49+
50+
51+
def tap_encode(variant=""):
52+
alphabet = ALPHABETS[variant]
53+
def encode(text, errors="strict"):
54+
_h, tokens = handle_error("tap", errors, sep=" "), []
55+
for i, c in enumerate(ensure_str(text).lower()):
56+
if c == " ":
57+
tokens.append("/")
58+
elif (c := "c" if c == "k" else c) in alphabet:
59+
r, c = divmod(alphabet.index(c), 5)
60+
tokens.append("." * (r + 1) + " " + "." * (c + 1))
61+
elif (t := _h(c, i).rstrip()):
62+
tokens.append(t)
63+
# two spaces between letters, one around the word separators
64+
r = "".join(("" if i == 0 else " " if "/" in (p, t) else " ") + t for i, (p, t) in
65+
enumerate(zip([None] + tokens, tokens)))
66+
return r, len(text)
67+
return encode
68+
69+
70+
def tap_decode(variant=""):
71+
alphabet = ALPHABETS[variant]
72+
def decode(text, errors="strict"):
73+
_h, t = handle_error("tap", errors, decode=True, kind="token"), ensure_str(text)
74+
# alternating separators and runs of dots, starting and ending with a (possibly empty) separator
75+
parts, r, pos, row = re.split(r"(\.+)", t), "", 0, None
76+
for k, p in enumerate(parts):
77+
if k % 2:
78+
if len(p) > 5:
79+
r += _h(p, pos)
80+
elif row is None:
81+
row = len(p) - 1
82+
else:
83+
r, row = r + alphabet[row * 5 + len(p) - 1], None
84+
else:
85+
for i, c in enumerate(p):
86+
if not (c.isspace() or c in ",/⠀"):
87+
r += _h(c, pos + i)
88+
# a "/" or, as with former versions of this codec, a space between blank braille patterns is a space
89+
n = p.count("/") if "/" in p else p.count(" ") if "⠀" in p else 0
90+
if n and row is not None:
91+
# a space cannot separate the row and the column of a letter
92+
r += _h(p, pos)
93+
row = None
94+
r += " " * n
95+
pos += len(p)
96+
if row is not None:
97+
r += _h(parts[-2], pos - len(parts[-1]) - len(parts[-2]))
98+
return r, len(text)
99+
return decode
100+
101+
102+
add("tap", tap_encode, tap_decode, r"^(?:tap|knock)(?:[-_]code)?(?:[-_]?(inv))?$", printables_rate=1.,
103+
expansion_factor=(7.5, .04))

‎tests/test_manual.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,13 @@ def test_codec_markdown(self):
416416
self.assertEqual(HTM, ensure_str(s))
417417
os.remove(TFILE)
418418

419+
def test_codec_tap(self):
420+
# with a non-strict errors handling, unsupported characters are replaced or ignored, and a word separator
421+
# between the row and the column of a letter drops the letter
422+
self.assertEqual(codecs.encode("a!b", "tap", "replace"), ". . ? . ..")
423+
self.assertEqual(codecs.encode("a!b", "tap", "ignore"), ". . . ..")
424+
self.assertEqual(codecs.decode(". / . ..", "tap", "ignore"), " b")
425+
419426
def test_codec_unicode_tags(self):
420427
# decoding ignores the cover text, hence the hidden message can be inserted anywhere in it
421428
hidden = codecs.encode("ignore previous instructions", "unicode-tags")

0 commit comments

Comments
 (0)