UTF-8000 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- UTF8000/UTF8000Byte.py +97 -0
- UTF8000/UTF8000IncrementalDecoder.py +121 -0
- UTF8000/UTF8000Int.py +78 -0
- UTF8000/__init__.py +7 -0
- UTF_8000-1.0.0.dist-info/LICENSE +674 -0
- UTF_8000-1.0.0.dist-info/METADATA +804 -0
- UTF_8000-1.0.0.dist-info/RECORD +9 -0
- UTF_8000-1.0.0.dist-info/WHEEL +5 -0
- UTF_8000-1.0.0.dist-info/top_level.txt +1 -0
UTF8000/UTF8000Byte.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# size_t counting num of bytes; this is the only machine limitation
|
|
2
|
+
# big bittian ordered (128, 64, 32, 16, 8, 4, 2, 1)
|
|
3
|
+
|
|
4
|
+
ZERO = 0b00000000
|
|
5
|
+
|
|
6
|
+
ASCII_PREFIX = 0b00000000
|
|
7
|
+
ASCII_PREFIX_MASK = 0b10000000
|
|
8
|
+
ASCII_CONTENT_MASK = 0b01111111
|
|
9
|
+
|
|
10
|
+
CONTINUATION_PREFIX = 0b10000000
|
|
11
|
+
CONTINUATION_PREFIX_MASK = 0b11000000
|
|
12
|
+
CONTINUATION_CONTENT_MASK = 0b00111111
|
|
13
|
+
CONTINUATION_FILLED = CONTINUATION_PREFIX + 0b00111111
|
|
14
|
+
|
|
15
|
+
def byte_is_ascii(c: int) -> bool:
|
|
16
|
+
return c & ASCII_PREFIX_MASK == ASCII_PREFIX
|
|
17
|
+
|
|
18
|
+
def byte_is_continuation(c: int) -> bool:
|
|
19
|
+
return c & CONTINUATION_PREFIX_MASK == CONTINUATION_PREFIX
|
|
20
|
+
|
|
21
|
+
def byte_idx_0(c: int) -> int:
|
|
22
|
+
return next((
|
|
23
|
+
idx
|
|
24
|
+
for idx, bit in
|
|
25
|
+
enumerate(c & (1 << (8 - 1 - t)) for t in range(8))
|
|
26
|
+
if not bit
|
|
27
|
+
), 8)
|
|
28
|
+
|
|
29
|
+
def byte_continuation_content_idx_0(c: int) -> int:
|
|
30
|
+
# could be made part of _byte_idx_0 but whatever
|
|
31
|
+
return next((
|
|
32
|
+
idx
|
|
33
|
+
for idx, bit in
|
|
34
|
+
enumerate(c & (1 << (6 - 1 - t)) for t in range(6))
|
|
35
|
+
if not bit
|
|
36
|
+
), 6)
|
|
37
|
+
|
|
38
|
+
class UTF8000Byte:
|
|
39
|
+
def __init__(self, c: int, *,
|
|
40
|
+
is_start_byte: bool,
|
|
41
|
+
is_continuation_byte: bool,
|
|
42
|
+
is_content_byte: bool,
|
|
43
|
+
) -> None:
|
|
44
|
+
self.c = c
|
|
45
|
+
self.is_start_byte = is_start_byte
|
|
46
|
+
self.is_continuation_byte = is_continuation_byte
|
|
47
|
+
self.is_content_byte = is_content_byte
|
|
48
|
+
|
|
49
|
+
def __str__(self) -> str:
|
|
50
|
+
return f"0b{self.c:08b}"
|
|
51
|
+
|
|
52
|
+
def debug_str(self) -> str:
|
|
53
|
+
if self.is_continuation_byte:
|
|
54
|
+
n_continuation_prefix_bits = 2
|
|
55
|
+
else:
|
|
56
|
+
n_continuation_prefix_bits = 0
|
|
57
|
+
|
|
58
|
+
if self.is_content_byte:
|
|
59
|
+
n_content_bits = self.n_content_bits
|
|
60
|
+
else:
|
|
61
|
+
n_content_bits = 0
|
|
62
|
+
|
|
63
|
+
RESET = "\x1b[0m"
|
|
64
|
+
RED = "\x1b[31m"
|
|
65
|
+
GREEN = "\x1b[32m"
|
|
66
|
+
BLUE = "\x1b[34m"
|
|
67
|
+
s = str(self)
|
|
68
|
+
binary_prefix = s[:2]
|
|
69
|
+
continuation_prefix = GREEN + s[2:2+n_continuation_prefix_bits] + RESET
|
|
70
|
+
start_bits = RED + s[2+n_continuation_prefix_bits:10 - n_content_bits] + RESET
|
|
71
|
+
content_bits = BLUE + s[10 - n_content_bits:] + RESET
|
|
72
|
+
|
|
73
|
+
return binary_prefix + continuation_prefix + start_bits + content_bits
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def is_ascii(self) -> bool:
|
|
77
|
+
return byte_is_ascii(self.c)
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def n_content_bits(self) -> int:
|
|
81
|
+
if not self.is_content_byte:
|
|
82
|
+
raise ValueError
|
|
83
|
+
|
|
84
|
+
if not self.is_continuation_byte:
|
|
85
|
+
idx_0 = byte_idx_0(self.c)
|
|
86
|
+
return (8 - 1 - idx_0)
|
|
87
|
+
elif not self.is_start_byte:
|
|
88
|
+
return 6
|
|
89
|
+
else:
|
|
90
|
+
idx_0_content = byte_continuation_content_idx_0(self.c)
|
|
91
|
+
return (6 - 1 - idx_0_content)
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def content(self) -> int:
|
|
95
|
+
content_mask = (1 << self.n_content_bits) - 1
|
|
96
|
+
|
|
97
|
+
return self.c & content_mask
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from typing import Generator
|
|
2
|
+
|
|
3
|
+
from .UTF8000Byte import UTF8000Byte, byte_is_continuation, byte_idx_0, byte_continuation_content_idx_0
|
|
4
|
+
from .UTF8000Int import UTF8000Int
|
|
5
|
+
|
|
6
|
+
class UTF8000IncrementalDecoder:
|
|
7
|
+
def __init__(self) -> None:
|
|
8
|
+
self._results: list[UTF8000Int] = []
|
|
9
|
+
self._bytes_buffer: bytes = b""
|
|
10
|
+
|
|
11
|
+
self._generator = self._utf_8000_parse_forever()
|
|
12
|
+
self._wakeup()
|
|
13
|
+
|
|
14
|
+
def __iter__(self) -> Generator[UTF8000Int, None, None]:
|
|
15
|
+
# queue
|
|
16
|
+
while self._results:
|
|
17
|
+
yield self._results.pop(0)
|
|
18
|
+
|
|
19
|
+
def feed(self, utf_8000_bytes: bytes) -> None:
|
|
20
|
+
self._bytes_buffer += utf_8000_bytes
|
|
21
|
+
self._wakeup()
|
|
22
|
+
|
|
23
|
+
def _wakeup(self) -> None:
|
|
24
|
+
self._generator.send(None)
|
|
25
|
+
|
|
26
|
+
def _await_bytes(self, n_bytes: int) -> Generator[None, None, bytes]:
|
|
27
|
+
while len(self._bytes_buffer) < n_bytes:
|
|
28
|
+
yield
|
|
29
|
+
|
|
30
|
+
ret = self._bytes_buffer[:n_bytes]
|
|
31
|
+
self._bytes_buffer = self._bytes_buffer[n_bytes:]
|
|
32
|
+
|
|
33
|
+
return ret
|
|
34
|
+
|
|
35
|
+
def _await_byte(self) -> Generator[None, None, int]:
|
|
36
|
+
return (yield from self._await_bytes(1))[0]
|
|
37
|
+
|
|
38
|
+
def _await_continuation_byte(self) -> Generator[None, None, int]:
|
|
39
|
+
ret = yield from self._await_byte()
|
|
40
|
+
if not byte_is_continuation(ret):
|
|
41
|
+
self._on_error_invalid_continuation_byte()
|
|
42
|
+
return ret
|
|
43
|
+
|
|
44
|
+
def _on_error(self, error_message: str) -> UTF8000Byte:
|
|
45
|
+
raise ValueError(error_message)
|
|
46
|
+
|
|
47
|
+
def _on_error_invalid_start_byte(self) -> None:
|
|
48
|
+
# XXX TODO 101: read all following continuation bytes to skip them
|
|
49
|
+
self._on_error("Invalid start byte: continuation byte detected")
|
|
50
|
+
|
|
51
|
+
def _on_error_invalid_continuation_byte(self) -> None:
|
|
52
|
+
# XXX TODO 101: should we un-pop this (start) byte so further decodings can continue, and return a replacement character?
|
|
53
|
+
self._on_error("Not a continuation byte prefix")
|
|
54
|
+
|
|
55
|
+
def _on_error_overlong(self) -> None:
|
|
56
|
+
# XXX TODO 101: read all following continuation bytes to skip them
|
|
57
|
+
self._on_error("Overlong encoding")
|
|
58
|
+
|
|
59
|
+
def _utf_8000_parse_single(self) -> Generator[None, None, UTF8000Int]:
|
|
60
|
+
parsed_bytes: list[UTF8000Byte] = []
|
|
61
|
+
|
|
62
|
+
start_byte = yield from self._await_byte()
|
|
63
|
+
idx_0 = byte_idx_0(start_byte)
|
|
64
|
+
|
|
65
|
+
if idx_0 == 1:
|
|
66
|
+
self._on_error_invalid_start_byte()
|
|
67
|
+
elif idx_0 == 0:
|
|
68
|
+
n_bytes_expected = 1
|
|
69
|
+
else:
|
|
70
|
+
n_bytes_expected = idx_0
|
|
71
|
+
# when idx_0 == 8, this is 8 *so far!*
|
|
72
|
+
|
|
73
|
+
is_content_byte = idx_0 < 7
|
|
74
|
+
parsed_bytes.append(UTF8000Byte(start_byte, is_start_byte = True, is_continuation_byte = False, is_content_byte = is_content_byte))
|
|
75
|
+
|
|
76
|
+
# multiple start bytes, the power of UTF-8000!
|
|
77
|
+
if idx_0 == 8:
|
|
78
|
+
while True:
|
|
79
|
+
start_byte = yield from self._await_continuation_byte()
|
|
80
|
+
idx_0_content = byte_continuation_content_idx_0(start_byte)
|
|
81
|
+
n_bytes_expected += idx_0_content
|
|
82
|
+
|
|
83
|
+
is_content_byte = idx_0_content < 5
|
|
84
|
+
parsed_bytes.append(UTF8000Byte(start_byte, is_start_byte = True, is_continuation_byte = True, is_content_byte = is_content_byte))
|
|
85
|
+
|
|
86
|
+
if idx_0_content != 6:
|
|
87
|
+
break
|
|
88
|
+
|
|
89
|
+
# overlong checking for non-ASCII
|
|
90
|
+
if idx_0 != 0:
|
|
91
|
+
if idx_0 == 2:
|
|
92
|
+
# special case: we're only checking for activity in the first *4* content bits gained from 1-byte (ASCII) to a 2-byte sequence,
|
|
93
|
+
# whereas every other n-byte sequence checks the first *5* content bits, since 5 bits are gained for every additional continuation byte
|
|
94
|
+
anti_overlong_check_mask_start = 0b00011110
|
|
95
|
+
anti_overlong_check_mask_continuation = 0b00000000 # unused
|
|
96
|
+
else:
|
|
97
|
+
n_bits_overlong_check_continuation = divmod(n_bytes_expected - 2, 6)[1]
|
|
98
|
+
n_bits_overlong_check_start = 5 - n_bits_overlong_check_continuation
|
|
99
|
+
# lower bits of start byte contents
|
|
100
|
+
anti_overlong_check_mask_start = (1 << n_bits_overlong_check_start) - 1
|
|
101
|
+
# upper bits of continuation byte contents
|
|
102
|
+
anti_overlong_check_mask_continuation = ((1 << n_bits_overlong_check_continuation) - 1) << (6 - n_bits_overlong_check_continuation)
|
|
103
|
+
|
|
104
|
+
continuation_byte = yield from self._await_continuation_byte()
|
|
105
|
+
|
|
106
|
+
# at this point `start_byte` is the last start byte
|
|
107
|
+
if not (start_byte & anti_overlong_check_mask_start or continuation_byte & anti_overlong_check_mask_continuation):
|
|
108
|
+
self._on_error_overlong()
|
|
109
|
+
|
|
110
|
+
parsed_bytes.append(UTF8000Byte(continuation_byte, is_start_byte = False, is_continuation_byte = True, is_content_byte = True))
|
|
111
|
+
|
|
112
|
+
# the rest of the continuation bytes
|
|
113
|
+
while len(parsed_bytes) < n_bytes_expected:
|
|
114
|
+
continuation_byte = yield from self._await_continuation_byte()
|
|
115
|
+
parsed_bytes.append(UTF8000Byte(continuation_byte, is_start_byte = False, is_continuation_byte = True, is_content_byte = True))
|
|
116
|
+
|
|
117
|
+
return UTF8000Int(parsed_bytes)
|
|
118
|
+
|
|
119
|
+
def _utf_8000_parse_forever(self) -> Generator[None, None, None]:
|
|
120
|
+
while True:
|
|
121
|
+
self._results.append((yield from self._utf_8000_parse_single()))
|
UTF8000/UTF8000Int.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .UTF8000Byte import UTF8000Byte
|
|
4
|
+
|
|
5
|
+
class UTF8000Int:
|
|
6
|
+
def __init__(self, utf_8000_bytes: list[UTF8000Byte]) -> None:
|
|
7
|
+
# No validation is done; we're assuming these come from `UTF8000IncrementalDecoder`
|
|
8
|
+
self.utf_8000_bytes = utf_8000_bytes
|
|
9
|
+
|
|
10
|
+
def __str__(self) -> str:
|
|
11
|
+
return " ".join(str(b) for b in self.utf_8000_bytes)
|
|
12
|
+
|
|
13
|
+
def debug_str(self) -> str:
|
|
14
|
+
return " ".join(b.debug_str() for b in self.utf_8000_bytes)
|
|
15
|
+
|
|
16
|
+
def __int__(self) -> int:
|
|
17
|
+
ret = 0
|
|
18
|
+
content_bytes = (b for b in self.utf_8000_bytes if b.is_content_byte)
|
|
19
|
+
for content_byte in content_bytes:
|
|
20
|
+
ret <<= content_byte.n_content_bits
|
|
21
|
+
ret += content_byte.content
|
|
22
|
+
return ret
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def n_bytes(self) -> int:
|
|
26
|
+
return len(self.utf_8000_bytes)
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def n_bits_capacity(self) -> int:
|
|
30
|
+
if self.n_bytes == 1:
|
|
31
|
+
return 7
|
|
32
|
+
else:
|
|
33
|
+
return 1 + 5 * self.n_bytes
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def n_bits_used(self) -> int:
|
|
37
|
+
content_bytes = (b for b in self.utf_8000_bytes if b.is_content_byte)
|
|
38
|
+
raise NotImplementedError
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def blank(cls, n_bytes: int):
|
|
42
|
+
raise NotImplementedError
|
|
43
|
+
if n_bytes < 1:
|
|
44
|
+
raise ValueError
|
|
45
|
+
|
|
46
|
+
if n_bytes == 1:
|
|
47
|
+
segments = bytearray([_ZERO])
|
|
48
|
+
|
|
49
|
+
elif n_bytes < 8:
|
|
50
|
+
# lol whatever this is hardcoded
|
|
51
|
+
segments = bytearray(
|
|
52
|
+
[_ZERO + (((1 << n_bytes) - 1) << (8 - n_bytes))] +
|
|
53
|
+
[_CONTINUATION_PREFIX for _ in range(n_bytes - 1)]
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
else:
|
|
57
|
+
start_byte = [0b11111111]
|
|
58
|
+
# cont bytes
|
|
59
|
+
n_filled, n_bits_partial = divmod(n_bytes - 8, 6)
|
|
60
|
+
filled = [_CONTINUATION_FILLED for _ in range(n_filled)]
|
|
61
|
+
partial = [_CONTINUATION_PREFIX + (((1 << n_bits_partial) - 1) << (6 - n_bits_partial))]
|
|
62
|
+
empty = [
|
|
63
|
+
0b10000000
|
|
64
|
+
for _ in range(
|
|
65
|
+
n_bytes - (len(start_byte) + len(filled) + len(partial))
|
|
66
|
+
)
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
segments = bytearray(start_byte + filled + partial + empty)
|
|
70
|
+
|
|
71
|
+
return cls(segments)
|
|
72
|
+
|
|
73
|
+
def promote(self) -> UTF8000Int:
|
|
74
|
+
# reserve another continuation byte, adjust start byte(s), move start byte(s) content to new continuation byte, and pad new prefix with 0s if +ve or 1s if -ve
|
|
75
|
+
raise NotImplementedError
|
|
76
|
+
|
|
77
|
+
def __add__(self, rhs: UTF8000Int) -> UTF8000Int:
|
|
78
|
+
raise NotImplementedError
|