Skip to content

Commit 9d97507

Browse files
committed
Added new codec: whitespace
1 parent 43e37d8 commit 9d97507

7 files changed

Lines changed: 103 additions & 14 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ This library extends the native `codecs` library and provides some new encodings
2525
`nokia3310` | Nokia 3310 keystrokes <-> text | uses "`-`" as a separator for encoding, "`-`" or "`_`" or whitespace for decoding
2626
`rot-N` | ROT(N) <-> text | aka Caesar cipher (N belongs to [1,25])
2727
`xor-N` | XOR(N) <-> text | XOR with a single byte (N belongs to [1,255])
28+
`whitespace` | Whitespaces <-> text | replaces bits with whitespaces and tabs
2829

2930

3031
## Setup

codext/__common__.py

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -178,32 +178,55 @@ def _wrapper(param):
178178
smapdict = {k: v for k, v in mapdict[0].items()}
179179
else:
180180
raise ValueError("Bad mapping dictionary or list of mapping dictionaries")
181-
if param:
182-
# case 1: list or dictionary of parameter-dependent encodings
183-
if isinstance(param, int):
181+
if param is not None:
182+
p = param
183+
# case 1: param is empty string
184+
if p == "":
184185
if isinstance(mapdict, list):
185-
param -= 1
186-
if isinstance(mapdict, list) and 0 <= param < len(mapdict) or \
187-
isinstance(mapdict, dict) and param in mapdict.keys():
188-
smapdict = mapdict[param]
186+
smapdict = {k: v for k, v in mapdict[0].items()}
187+
elif isinstance(mapdict, dict):
188+
if '' in mapdict.keys() and isinstance(mapdict[''], dict):
189+
smapdict = {k: v for k, v in mapdict[''].items()}
190+
else:
191+
smapdict = {k: v for k, v in mapdict.items()}
192+
# no 'else' handling a LookupError here ; this case is covered by the first if/elif/else block
193+
# case 2: list or dictionary or dictionary of numbered encodings
194+
elif isinstance(p, int):
195+
if isinstance(mapdict, list):
196+
p -= 1
197+
if isinstance(mapdict, list) and 0 <= p < len(mapdict) or \
198+
isinstance(mapdict, dict) and p in mapdict.keys():
199+
smapdict = {k: v for k, v in mapdict[p].items()}
189200
else:
190-
raise LookupError("Bad parameter for encoding '{}': {}".format(ename, param))
191-
# case 2: encodinc characters translation
201+
raise LookupError("Bad parameter for encoding '{}': '{}'".format(ename, p))
202+
# case 3: dictionary of regex-selected encoding mappings
203+
elif isinstance(mapdict, dict) and isinstance(list(mapdict.values())[0], dict):
204+
tmp = None
205+
for r, d in mapdict.items():
206+
if r == '': # this is already handled in case 1 ; anyway, an empty regex always match, hence
207+
continue # it must be excluded
208+
if re.match(r, p):
209+
tmp = d
210+
break
211+
if tmp is None:
212+
raise LookupError("Bad parameter for encoding '{}': '{}'".format(ename, p))
213+
smapdict = tmp
214+
# case 4: encodinc characters translation
192215
else:
193216
# collect base tokens in order of appearance in the mapping dictionary
194217
base_tokens = ""
195218
for _, c in sorted(mapdict.items()):
196219
for t in c:
197220
if t not in base_tokens:
198221
base_tokens += t
199-
if param[0] in "-_" and len(param[1:]) == len(set(param[1:])) == len(base_tokens):
200-
param = param[1:]
201-
if len(param) == len(set(param)) == len(base_tokens):
202-
t = maketrans(base_tokens, param)
222+
if len(p) > 0 and p[0] in "-_" and len(p[1:]) == len(set(p[1:])) == len(base_tokens):
223+
p = p[1:]
224+
if len(p) == len(set(p)) == len(base_tokens):
225+
t = maketrans(base_tokens, p)
203226
for k, v in smapdict.items():
204227
smapdict[k] = v.translate(t)
205228
else:
206-
raise LookupError("Bad parameter for encoding '{}': {}".format(ename, param))
229+
raise LookupError("Bad parameter for encoding '{}': '{}'".format(ename, p))
207230
if ignore_case:
208231
case = ["upper", "lower"][any(c in "".join(smapdict.keys()) for c in "abcdefghijklmnopqrstuvwxyz")]
209232
# use the first mapped group from the mapping dictionary to determine token length ; this is useful e.g. for

codext/stegano/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
# -*- coding: UTF-8 -*-
22
from .leetspeak import *
33
from .nokia import *
4+
from .whitespace import *

codext/stegano/whitespace.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# -*- coding: UTF-8 -*-
2+
"""Whitespace Codec - whitespace/tabs content encoding.
3+
4+
This codec:
5+
- en/decodes strings from str to str
6+
- en/decodes strings from bytes to bytes
7+
- decodes file content to str (read)
8+
- encodes file content from str to bytes (write)
9+
"""
10+
from ..__common__ import *
11+
12+
13+
ENCMAP = {r'': {'0': "\t", '1': " "}, r'[-_]inv(erted)?': {'0': " ", '1': "\t"}}
14+
15+
16+
add_map("whitespace", ENCMAP, binary=True, pattern=r"^whitespace(?:s)?([-_]inv(?:erted)?)?$")

docs/encodings.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,3 +213,24 @@ This is a dynamic encoding, that is, it can be called with an integer to define
213213
>>> codext.encode("~bcy*cy*k*~oy~", "xor-10")
214214
'this is a test'
215215
```
216+
217+
-----
218+
219+
### Whitespaces
220+
221+
This simple encoding replaces zeros and ones of the binary version of the input text with spaces and tabs. It is supported either with its original mapping or with the inverted mapping.
222+
223+
**Codec** | **Conversions** | **Aliases** | **Comment**
224+
:---: | :---: | --- | ---
225+
`whitespace` | Whitespaces <-> text | `whitespaces?-inv(erted)?` | The default encoding uses tabs for zeros and spaces for ones
226+
227+
```python
228+
>>> codext.encode("test", "whitespace")
229+
'\t \t \t\t\t \t\t \t \t \t\t \t \t \t\t'
230+
>>> codext.encode("test", "whitespaces")
231+
'\t \t \t\t\t \t\t \t \t \t\t \t \t \t\t'
232+
>>> codext.encode("test", "whitespaces-inv")
233+
' \t\t\t \t \t\t \t \t \t\t\t \t\t \t\t\t \t '
234+
>>> codext.decode(" \t\t\t \t \t\t \t \t \t\t\t \t\t \t\t\t \t ", "whitespaces_inverted")
235+
'test'
236+
```

tests/test_common.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ def test_add_map_codec(self):
3939
self.assertEqual(codext.encode("def", "dummy2-2"), "DEF")
4040
self.assertEqual(codext.encode("ghi", "dummy2-3"), "GHI")
4141
self.assertRaises(LookupError, codext.encode, "test", "dummy2-4")
42+
ENCMAP = {'': {'a': "A", 'b': "B"}, r'bad': {'a': "B", 'b': "A"}}
43+
self.assertIsNone(codext.add_map("dummy3", ENCMAP, pattern=r"^dummy3([-_]inverted)?$"))
44+
self.assertRaises(LookupError, codext.encode, "test", "dummy3_inverted")
4245

4346
def test_remove_codec(self):
4447
self.assertIsNone(codext.add("dummy", dummy_encode, dummy_decode))

tests/test_whitespace.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#!/usr/bin/env python
2+
# -*- coding: UTF-8 -*-
3+
"""Whitespace codec tests.
4+
5+
"""
6+
from unittest import TestCase
7+
8+
from codext.__common__ import *
9+
10+
11+
if PY3:
12+
class TestCodecWhitespace(TestCase):
13+
def test_codec_whitespace(self):
14+
STR = "test"
15+
WSP1 = "\t \t \t\t\t \t\t \t \t \t\t \t \t \t\t"
16+
WSP2 = " \t\t\t \t \t\t \t \t \t\t\t \t\t \t\t\t \t "
17+
self.assertEqual(codecs.encode(STR, "whitespace"), WSP1)
18+
self.assertEqual(codecs.encode(b(STR), "whitespace"), b(WSP1))
19+
self.assertEqual(codecs.encode(STR, "whitespace-inv"), WSP2)
20+
self.assertEqual(codecs.encode(b(STR), "whitespace_inverted"), b(WSP2))
21+
self.assertEqual(codecs.decode(WSP1, "whitespace"), STR)
22+
self.assertEqual(codecs.decode(b(WSP1), "whitespace"), b(STR))
23+
self.assertEqual(codecs.decode(WSP2, "whitespace_inv"), STR)
24+
self.assertEqual(codecs.decode(b(WSP2), "whitespace-inverted"), b(STR))

0 commit comments

Comments
 (0)