Skip to content

Commit 8ae3c6a

Browse files
committed
Added codec: base92
1 parent a0f9b7a commit 8ae3c6a

6 files changed

Lines changed: 125 additions & 0 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@ CACTCGGTCGGCCATATGTTCGGCCATATGTTCGTCTGTTCACTCGCCCATACACT
290290
- [X] `base69`: [Base69](https://github.com/pshihn/base69) encoding (chunks of 7 bytes, 2 characters per 7 bits)
291291
- [X] `base85`: all variants of Base85 ([Ascii85](https://fr.wikipedia.org/wiki/Ascii85), [z85](https://rfc.zeromq.org/spec/32), [Adobe](https://dencode.com/string/ascii85), [(x)btoa](https://dencode.com/string/ascii85), [RFC1924](https://datatracker.ietf.org/doc/html/rfc1924), [XML](https://datatracker.ietf.org/doc/html/draft-kwiatkowski-base85-for-xml-00))
292292
- [X] `base91`: [Base91](http://base91.sourceforge.net) custom conversion
293+
- [X] `base92`: [Base92](https://github.com/thenoviceoof/base92) custom conversion
293294
- [X] `base100` (or *emoji*): [Base100](https://github.com/AdamNiederer/base100) custom conversion
294295
- [X] `base122`: [Base122](http://blog.kevinalbs.com/base122) custom conversion
295296
- [X] `base128`: conversion to the ASCII charset (with control characters)

‎docs/pages/enc/base.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ Note that for `base64`, it overwrites the native `base64_codec` to also support
7272
`base69` | data <-> Base69 encoded text | `base[-_]?69` | [Base69](https://github.com/pshihn/base69) encoding (chunks of 7 bytes, 2 characters per 7 bits, `N=` for *N* padding bytes)
7373
`base91` | text <-> Base91 encoded text | `base[-_]?91(|[-_]inv(erted)?)` |
7474
`base91-alt` | text <-> Alternate Base91 encoded text | `base[-_]?91[-_]alt(?:ernate)?(|[-_]inv(erted)?)` | Another version of Base91
75+
`base92` | data <-> Base92 encoded text | `base[-_]?92` | [Base92](https://github.com/thenoviceoof/base92) encoding (13 bits per pair of characters, `~` for an empty string) ; same alphabet and bit packing as `base91-alt`, which however neither encodes the empty string as `~` nor checks the pairs when decoding
7576

7677
```python
7778
>>> codext.encode("test", "base3")
@@ -129,6 +130,10 @@ Base38 is the encoding of the payloads of the [Matter](https://github.com/projec
129130
'nX,<:WRT%yxth90oZB^C'
130131
>>> codext.encode("This is a test !", "base91-alt")
131132
'?a&[jv4S3Wg>,71@Jo#K'
133+
>>> codext.encode("This is a test !", "base92")
134+
'?a&[jv4S3Wg>,71@Jo#K'
135+
>>> codext.encode("", "base92")
136+
'~'
132137
```
133138

134139
!!! note "Generic encodings"

‎pyproject.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ base85-ipv6 = "codext.base.base85:main85rfc1924"
110110
base85-xml = "codext.base.base85:main85xml"
111111
base85-zeromq = "codext.base.base85:main85zeromq"
112112
base91 = "codext.base.base91:main91"
113+
base92 = "codext.base.base92:main92"
113114
base100 = "codext.base.base100:main100"
114115
base122 = "codext.base.base122:main122"
115116
base128-latin1 = "codext.base.base128:main128latin1"

‎src/codext/base/__init__.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from .base69 import *
88
from .base85 import *
99
from .base91 import *
10+
from .base92 import *
1011
from .base100 import *
1112
from .base122 import *
1213
from .base128 import *

‎src/codext/base/base92.py‎

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# -*- coding: UTF-8 -*-
2+
"""Base92 Codec - base92 content encoding.
3+
4+
Base92, designed by Nathan Hwang (thenoviceoof), uses 91 printable ASCII characters ("!", then "#" to "_" and "a" to
5+
"}", hence all but space, '"', "`" and "~"), the empty string being encoded as "~". The input bits are read by groups
6+
of 13 bits, each group being written with 2 characters (91 x 91 = 8281 >= 8192), hence an expansion of about 23% ; the
7+
last bits are padded with zeros and written with a single character if they fit in 6 bits, or with 2 characters
8+
otherwise. When decoding, a pair of characters whose value exceeds 13 bits is invalid, as well as a single character.
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://github.com/thenoviceoof/base92
17+
"""
18+
from ._base import main
19+
from ..__common__ import *
20+
21+
22+
__examples__ = {
23+
'enc(base92|base-92|base_92)': {'hello world': "Fc_$aOTdKnsM*k", '': "~", 'a': "D,",
24+
'^\xb6;\xbb\xe0\x1e\xee\xd0\x93\xcb"\xbb\x8fZ\xcd\xc3': "C=i.w6'IvB/viUpRAwco"},
25+
'dec(base92)': {'Fc_$aOTdKnsM*k': "hello world", '~': "", 'Fc_$aOTd\nKnsM*k': "hello world", 'F': None,
26+
'Fc"$': None, '}}': None, 'Fc~': None},
27+
'enc-dec(base92)': ["test", "This is a test", "aoeuaoeuaoeu", "@random{512,1024,2048}"],
28+
}
29+
__guess__ = ["base92"]
30+
31+
32+
_range = lambda c1, c2: "".join(chr(c) for c in range(ord(c1), ord(c2) + 1))
33+
B92 = "!" + _range("#", "_") + _range("a", "}")
34+
B92_VALUES = {c: i for i, c in enumerate(B92)}
35+
36+
37+
def base92_encode(text, errors="strict"):
38+
t = b(text)
39+
if len(t) == 0:
40+
return "~", 0
41+
r, bits, n = [], 0, 0
42+
for byte in t:
43+
bits, n = (bits << 8) | byte, n + 8
44+
while n >= 13:
45+
n -= 13
46+
v, bits = bits >> n, bits & ((1 << n) - 1)
47+
r.extend((B92[v // 91], B92[v % 91]))
48+
# the last bits are padded with zeros, to 6 bits (1 character) or to 13 bits (2 characters)
49+
if n > 0:
50+
if n < 7:
51+
r.append(B92[bits << (6 - n)])
52+
else:
53+
v = bits << (13 - n)
54+
r.extend((B92[v // 91], B92[v % 91]))
55+
return "".join(r), len(t)
56+
57+
58+
def base92_decode(text, errors="strict"):
59+
t = ensure_str(_stripl(text, True, True))
60+
if t == "~":
61+
return "", len(text)
62+
_h = handle_error("base92", errors, decode=True)
63+
# characters out of the alphabet are errors (only skipped if errors are not handled strictly)
64+
values, r = [], bytearray()
65+
for i, c in enumerate(t):
66+
if c in B92_VALUES:
67+
values.append(B92_VALUES[c])
68+
else:
69+
_h(c, i, bytes(r))
70+
if len(values) == 1:
71+
_h(t, 0, bytes(r))
72+
return "", len(text)
73+
bits, n = 0, 0
74+
for i in range(0, len(values) - 1, 2):
75+
v = values[i] * 91 + values[i+1]
76+
if v >= 8192:
77+
_h(t[i:i+2], i, bytes(r))
78+
continue
79+
bits, n = (bits << 13) | v, n + 13
80+
while n >= 8:
81+
n -= 8
82+
r.append(bits >> n)
83+
bits &= (1 << n) - 1
84+
# a single last character holds 6 bits ; the padding bits that remain are dropped
85+
if len(values) % 2 == 1:
86+
bits, n = (bits << 6) | values[-1], n + 6
87+
while n >= 8:
88+
n -= 8
89+
r.append(bits >> n)
90+
bits &= (1 << n) - 1
91+
return bytes(r), len(text)
92+
93+
94+
add("base92", base92_encode, base92_decode, r"^base[-_]?92$", entropy=6.5, expansion_factor=1.231, len_charset=91,
95+
printables_rate=1.)
96+
main92 = main(92, "<https://github.com/thenoviceoof/base92>", inv=False)

‎tests/test_base.py‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,27 @@ def test_codec_base91(self):
233233
self.assertRaises(ValueError, codecs.decode, "a\xff", "base91")
234234
self.assertIsNotNone(codecs.encode("\x00\x00", "base91-alt"))
235235

236+
def test_codec_base92(self):
237+
# the actual Base92 takes the name, while the generic base remains available
238+
self.assertEqual(codecs.lookup("base92").parameters['name'], "base92")
239+
self.assertEqual(codecs.lookup("base92-generic").parameters['name'], "base")
240+
self.assertNotEqual(codecs.encode(STR, "base92"), codecs.encode(STR, "base92-generic"))
241+
# invalid characters, pairs and single characters are skipped when errors are not handled strictly
242+
self.assertEqual(codecs.decode('Fc_$aOTd"KnsM*k', "base92", "ignore"), "hello world")
243+
self.assertEqual(codecs.decode("F", "base92", "ignore"), "")
244+
self.assertEqual(codecs.decode("}}Fc_$aOTdKnsM*k", "base92", "ignore"), "hello world")
245+
# the CLI tool
246+
import os, subprocess
247+
src = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "src")
248+
env = {**os.environ, 'PYTHONPATH': src + os.pathsep + os.environ.get('PYTHONPATH', "")}
249+
args = lambda *a: "".join(", %r" % x for x in a)
250+
cmd = lambda *a: [sys.executable, "-c", "import sys ; from codext.base.base92 import main92 ; "
251+
"sys.argv = ['base92'%s] ; sys.exit(main92())" % args(*a)]
252+
r = subprocess.run(cmd(), input=b"hello world", capture_output=True, env=env)
253+
self.assertEqual(r.stdout.strip(), b"Fc_$aOTdKnsM*k")
254+
r = subprocess.run(cmd("-d"), input=b"Fc_$aOTdKnsM*k", capture_output=True, env=env)
255+
self.assertEqual(r.stdout, b"hello world")
256+
236257
def test_codec_base100(self):
237258
B100 = "\U0001f46b\U0001f45f\U0001f460\U0001f46a\U0001f417\U0001f460\U0001f46a\U0001f417\U0001f458\U0001f417" \
238259
"\U0001f46b\U0001f45c\U0001f46a\U0001f46b"

0 commit comments

Comments
 (0)