Skip to content

Commit f9eac7b

Browse files
committed
Added codec: base128
1 parent 72d3533 commit f9eac7b

7 files changed

Lines changed: 106 additions & 2 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,7 @@ CACTCGGTCGGCCATATGTTCGGCCATATGTTCGTCTGTTCACTCGCCCATACACT
283283
- [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))
284284
- [X] `base91`: [Base91](http://base91.sourceforge.net) custom conversion
285285
- [X] `base100` (or *emoji*): [Base100](https://github.com/AdamNiederer/base100) custom conversion
286+
- [X] `base128-latin1`: [Base128](https://github.com/rpuntaie/base128) with printable characters (digits, letters and Latin-1)
286287
- [X] `base122`: [Base100](http://blog.kevinalbs.com/base122) custom conversion
287288
- [X] `base1024` (or *ecoji*): [Ecoji](https://github.com/keith-turner/ecoji) custom conversion (10 bits per emoji, versions 1 and 2)
288289
- [X] `base2048`: [Base2048](https://github.com/qntm/base2048) custom conversion (11 bits per character, optimised for Twitter)

‎docs/pages/enc/base.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ This encoding implements various different versions of Base85.
173173
`base100` | text <-> Base100 encoded text | `base[-_]?100|emoji` | Python 3 only
174174
`base122` | text <-> Base122 encoded text | `base[-_]?122` | Python 3 only
175175
`base128` | text <-> Base128 encoded text | `base[-_]?128` | Relies on the ASCII charset
176+
`base128-latin1` | data <-> Base128 encoded text | `base128[-_]latin[-_]?1` | [Base128](https://github.com/rpuntaie/base128) with printable characters (digits, letters and Latin-1) ; chunks of 7 bytes, 7 bits per character, a last character giving the length of the last chunk
176177
`leb128` | integers <-> LEB128 (varint) bytes | `uleb128`, `varint` | unsigned integers, separated by whitespaces or commas
177178
`sleb128` | integers <-> signed LEB128 bytes | `leb128-signed` | signed integers (two's complement)
178179
`base1024` | data <-> Ecoji encoded text | `base[-_]?1024`, `ecoji`, `ecoji-v1`, `ecoji-v2` | 10 bits per emoji ; encodes with Ecoji version 2 by default
@@ -196,6 +197,13 @@ This encoding implements various different versions of Base85.
196197
'this is a test'
197198
```
198199

200+
```python
201+
>>> codecs.encode("Hello World!", "base128-latin1")
202+
'aPjÃây·ÕtÚÊÃX15'
203+
>>> codecs.decode("aPjÃây·ÕtÚÊÃX15", "base128-latin1")
204+
'Hello World!'
205+
```
206+
199207
Base2048, Base32768 and Base65536 were designed by [qntm](https://github.com/qntm/base65536). The data is written with 11, 15 or 16 bits per character, using repertoires of Unicode characters that are safe to transmit (no whitespace, no punctuation, no control or combining characters). The final bits are padded with 1s and may be written with a character of a smaller final repertoire, which can only end the encoded text. When decoding, whitespaces are ignored.
200208

201209
```python

‎pyproject.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ base85-zeromq = "codext.base.base85:main85zeromq"
107107
base91 = "codext.base.base91:main91"
108108
base100 = "codext.base.base100:main100"
109109
base122 = "codext.base.base122:main122"
110+
base128-latin1 = "codext.base.base128:main128latin1"
110111
base1024 = "codext.base.base1024:main1024"
111112
base2048 = "codext.base.base65536:main2048"
112113
base32768 = "codext.base.base65536:main32768"

‎src/codext/base/__init__.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from .base91 import *
1010
from .base100 import *
1111
from .base122 import *
12+
from .base128 import *
1213
from .base1024 import *
1314
from .bech32 import *
1415
from .base65536 import *

‎src/codext/base/base128.py‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# -*- coding: UTF-8 -*-
2+
"""Base128-Latin1 Codec - base128-latin1 content encoding.
3+
4+
This Base128 encoding, unlike the "base128" codec (relying on the ASCII charset, hence holding control characters), uses
5+
a set of 128 printable characters (digits, letters and Latin-1 characters). The data is taken by
6+
chunks of 7 bytes, each chunk being split into 8 groups of 7 bits written with 1 character each ; the bits of a last
7+
incomplete chunk are split the same way, those of its last group being right-aligned. A last character gives the length
8+
of the last chunk (0 when it is complete). Whitespace is ignored when decoding.
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/rpuntaie/base128
17+
"""
18+
from ._base import main
19+
from ..__common__ import *
20+
21+
22+
__examples__ = {
23+
'enc(base128-latin1|base128_latin1|base-128-latin-1)': {
24+
'': "0", '\x00' * 7: "000000000", '\xff' * 7: "ÿ" * 8 + "0", '\x01': "011", '\xff': "ÿ11",
25+
'Hello World!': "aPjÃây·ÕtÚÊÃX15",
26+
},
27+
'dec(base128-latin1)': {
28+
'aPjÃây·ÕtÚÊÃX15': "Hello World!", '011': "\x01", '0': "", '': None, '7': None, '1': None, '01': None,
29+
'0111': None, '021': None, '01!1': None, '0000000000': None, '0000000001': None,
30+
},
31+
'enc-dec(base128-latin1)': ["\xff", "\xff\xfe\xfd\xfc\xfb\xfa", "\xff\xfe\xfd\xfc\xfb\xfa\xf9",
32+
"@random{512,1024}"],
33+
}
34+
__guess__ = ["base128-latin1"]
35+
36+
37+
B128 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzµ¶" \
38+
"·¼½¾ÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÑÒÓÔÕÖרÙÚÛÜÝÞßáâãäåæçèéêëìíîïñòóôõö÷øùúûüýþÿ"
39+
40+
41+
def base128_latin1_encode(text, errors="strict"):
42+
t, r = b(text), []
43+
for i in range(0, len(t), 7):
44+
chunk = t[i:i+7]
45+
n, v = 8 * len(chunk), int.from_bytes(chunk, "big")
46+
# groups of 7 bits from the most significant ones, the last group of an incomplete chunk being right-aligned
47+
for k in range(n - 7, -7, -7):
48+
r.append(B128[v >> k & 0x7f if k >= 0 else v & (1 << (k + 7)) - 1])
49+
r.append(B128[len(t) % 7])
50+
return (s := "".join(r)), len(s)
51+
52+
53+
def base128_latin1_decode(text, errors="strict"):
54+
_h, text, r = handle_error("base128-latin1", errors, decode=True), "".join(ensure_str(text).split()), b""
55+
# a wrong character cannot be replaced in a chunk, hence it is dropped
56+
t = "".join(c if c in B128 else _h(c, i) and "" for i, c in enumerate(text))
57+
if len(t) == 0 or (m := B128.index(t[-1])) > 6:
58+
return b(_h(t[-1:], len(t) - 1)), len(text)
59+
# the last character gives the length of the last chunk of data (m bytes, encoded with ceil(8m/7) characters)
60+
t = t[:-1]
61+
if m and len(t) == 0:
62+
return b(_h(text, 0)), len(text)
63+
for i in range(0, len(t), 8):
64+
chunk, n = t[i:i+8], m if m and i + 8 >= len(t) else 7
65+
groups = [B128.index(c) for c in chunk]
66+
# the last group of an incomplete chunk only holds the remaining bits, right-aligned
67+
rbits = 8 * n - 7 * (len(chunk) - 1)
68+
if len(chunk) != (8 if n == 7 else -(-8 * m // 7)) or groups[-1] >> rbits:
69+
r += b(_h(chunk, i, r))
70+
continue
71+
v = 0
72+
for g in groups[:-1]:
73+
v = v << 7 | g
74+
r += (v << rbits | groups[-1]).to_bytes(n, "big")
75+
return r, len(r)
76+
77+
78+
add("base128-latin1", base128_latin1_encode, base128_latin1_decode, r"^base[-_]?128[-_]latin[-_]?1$",
79+
expansion_factor=8 / 7)
80+
main128latin1 = main(128, "<https://github.com/rpuntaie/base128>", "latin1", inv=False, swap=False)

‎src/codext/stegano/sms.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@
2424
ENCMAP = {
2525
' ': "0", 'a': "2", 'b': "22", 'c': "222", 'd': "3", 'e': "33", 'f': "333", 'g': "4", 'h': "44", 'i': "444",
2626
'j': "5", 'k': "55", 'l': "555", 'm': "6", 'n': "66", 'o': "666", 'p': "7", 'q': "77", 'r': "777", 's': "7777",
27-
't': "8", 'u': "88", 'v': "888", 'w': "9", 'x': "99", 'y': "999", 'z': "9999", '*': "*", '#': "#", ' ': " ",
28-
'_': "1", ':': "11", '@': "111",
27+
't': "8", 'u': "88", 'v': "888", 'w': "9", 'x': "99", 'y': "999", 'z': "9999", '*': "*", '#': "#", '_': "1",
28+
':': "11", '@': "111",
2929
}
3030

3131

‎tests/test_base.py‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,19 @@ def test_codec_base69(self):
252252
# non-Base69 characters are dropped
253253
self.assertEqual(codecs.decode(enc[:5] + "!" + enc[5:], "base69", "ignore"), "ABCDEFGHIJKLMN")
254254

255+
def test_codec_base128_latin1(self):
256+
# a bad chunk is handled as a whole, other chunks being kept ; non-Base128 characters are dropped
257+
enc = codecs.encode("ABCDEFGHIJ", "base128-latin1")
258+
self.assertEqual(codecs.decode(enc[:11] + "ÿ" + enc[12:], "base128-latin1", "ignore"), "ABCDEFG")
259+
self.assertEqual(codecs.decode(enc[:6] + "\n" + enc[6:], "base128-latin1"), "ABCDEFGHIJ")
260+
self.assertEqual(codecs.decode(enc[:3] + "!" + enc[3:], "base128-latin1", "ignore"), "ABCDEFGHIJ")
261+
# the last character must be a length from 0 to 6, and give the length of the last chunk
262+
self.assertEqual(codecs.decode(enc[:-1] + "7", "base128-latin1", "ignore"), "")
263+
self.assertEqual(codecs.decode("1", "base128-latin1", "ignore"), "")
264+
self.assertRaises(ValueError, codecs.decode, enc[:-1] + "2", "base128-latin1")
265+
# the existing base128 codec (ASCII charset) is not affected
266+
self.assertEqual(codecs.encode("Hello World!", "base128"), "\t\x06+1Xo\x10\x15mw\x131H!")
267+
255268
def test_codec_base45(self):
256269
# RFC 9285 test vectors
257270
for s, b45 in [("AB", "BB8"), ("Hello!!", "%69 VD92EX0"), ("base-45", "UJCLQE7W581")]:

0 commit comments

Comments
 (0)