packbin 0.1.9__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.
- packbin/__init__.py +65 -0
- packbin/_errors.py +49 -0
- packbin/_nodes.py +323 -0
- packbin/_pack.py +264 -0
- packbin/_scheme.py +120 -0
- packbin/_unpack.py +327 -0
- packbin-0.1.9.dist-info/METADATA +195 -0
- packbin-0.1.9.dist-info/RECORD +10 -0
- packbin-0.1.9.dist-info/WHEEL +5 -0
- packbin-0.1.9.dist-info/top_level.txt +1 -0
packbin/__init__.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from packbin._errors import ShortPacket, TrailingBytes, TypeMismatch, UnpackResult
|
|
4
|
+
from packbin._nodes import (
|
|
5
|
+
be,
|
|
6
|
+
bits,
|
|
7
|
+
bool,
|
|
8
|
+
bytes,
|
|
9
|
+
dict,
|
|
10
|
+
eq,
|
|
11
|
+
flags,
|
|
12
|
+
flag_byte,
|
|
13
|
+
group,
|
|
14
|
+
i8,
|
|
15
|
+
i16,
|
|
16
|
+
i32,
|
|
17
|
+
i64,
|
|
18
|
+
f32,
|
|
19
|
+
f64,
|
|
20
|
+
list,
|
|
21
|
+
repeat,
|
|
22
|
+
sized,
|
|
23
|
+
u2,
|
|
24
|
+
u8,
|
|
25
|
+
u16,
|
|
26
|
+
u32,
|
|
27
|
+
u64,
|
|
28
|
+
utf8,
|
|
29
|
+
when,
|
|
30
|
+
)
|
|
31
|
+
from packbin._scheme import BinaryPacker, Scheme
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"BinaryPacker",
|
|
35
|
+
"Scheme",
|
|
36
|
+
"ShortPacket",
|
|
37
|
+
"TrailingBytes",
|
|
38
|
+
"TypeMismatch",
|
|
39
|
+
"UnpackResult",
|
|
40
|
+
"u8",
|
|
41
|
+
"u16",
|
|
42
|
+
"u32",
|
|
43
|
+
"u64",
|
|
44
|
+
"i8",
|
|
45
|
+
"i16",
|
|
46
|
+
"i32",
|
|
47
|
+
"i64",
|
|
48
|
+
"f32",
|
|
49
|
+
"f64",
|
|
50
|
+
"bool",
|
|
51
|
+
"bytes",
|
|
52
|
+
"be",
|
|
53
|
+
"flags",
|
|
54
|
+
"flag_byte",
|
|
55
|
+
"eq",
|
|
56
|
+
"when",
|
|
57
|
+
"repeat",
|
|
58
|
+
"group",
|
|
59
|
+
"sized",
|
|
60
|
+
"u2",
|
|
61
|
+
"bits",
|
|
62
|
+
"utf8",
|
|
63
|
+
"list",
|
|
64
|
+
"dict",
|
|
65
|
+
]
|
packbin/_errors.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Generic, TypeVar
|
|
5
|
+
|
|
6
|
+
T = TypeVar("T")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True, slots=True)
|
|
10
|
+
class ShortPacket:
|
|
11
|
+
field: str
|
|
12
|
+
needed: int
|
|
13
|
+
left: int
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True, slots=True)
|
|
17
|
+
class TrailingBytes:
|
|
18
|
+
left: int
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True, slots=True)
|
|
22
|
+
class TypeMismatch:
|
|
23
|
+
expected: int
|
|
24
|
+
actual: int
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True, slots=True)
|
|
28
|
+
class UnpackResult(Generic[T]):
|
|
29
|
+
ok: bool
|
|
30
|
+
value: T | None = None
|
|
31
|
+
error: ShortPacket | TrailingBytes | TypeMismatch | None = None
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def field(self) -> str | None:
|
|
35
|
+
if isinstance(self.error, ShortPacket):
|
|
36
|
+
return self.error.field
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def needed(self) -> int | None:
|
|
41
|
+
if isinstance(self.error, ShortPacket):
|
|
42
|
+
return self.error.needed
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def left(self) -> int | None:
|
|
47
|
+
if isinstance(self.error, (ShortPacket, TrailingBytes)):
|
|
48
|
+
return self.error.left
|
|
49
|
+
return None
|
packbin/_nodes.py
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Sequence
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
Get = Callable[[Any], Any]
|
|
9
|
+
Set = Callable[[Any, Any], None]
|
|
10
|
+
_builtin_list = list
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class _Node:
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(slots=True)
|
|
18
|
+
class _Scalar(_Node):
|
|
19
|
+
field_id: int
|
|
20
|
+
get: Get
|
|
21
|
+
set: Set
|
|
22
|
+
kind: str
|
|
23
|
+
size: int
|
|
24
|
+
fmt_le: str
|
|
25
|
+
fmt_be: str
|
|
26
|
+
signed: bool
|
|
27
|
+
min_v: int | float
|
|
28
|
+
max_v: int | float
|
|
29
|
+
big_endian: bool = False
|
|
30
|
+
|
|
31
|
+
def endian_fmt(self) -> str:
|
|
32
|
+
return self.fmt_be if self.big_endian else self.fmt_le
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(slots=True)
|
|
36
|
+
class _Bytes(_Node):
|
|
37
|
+
field_id: int
|
|
38
|
+
get: Get
|
|
39
|
+
set: Set
|
|
40
|
+
size: int
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(slots=True)
|
|
44
|
+
class _Bool(_Node):
|
|
45
|
+
field_id: int
|
|
46
|
+
get: Get
|
|
47
|
+
set: Set
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(slots=True)
|
|
51
|
+
class _FlagByte(_Node):
|
|
52
|
+
bits: list[_Node]
|
|
53
|
+
|
|
54
|
+
def bit(self, field: _Node) -> _FlagBit:
|
|
55
|
+
index = len(self.bits)
|
|
56
|
+
if index >= 8:
|
|
57
|
+
raise ValueError("flags already has 8 bits")
|
|
58
|
+
self.bits.append(field)
|
|
59
|
+
return _FlagBit(self, field, index)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(slots=True)
|
|
63
|
+
class _FlagBit(_Node):
|
|
64
|
+
owner: _FlagByte
|
|
65
|
+
field: _Node
|
|
66
|
+
index: int
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(slots=True)
|
|
70
|
+
class _Flags(_Node):
|
|
71
|
+
fields: list[_Node]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(slots=True)
|
|
75
|
+
class _Eq:
|
|
76
|
+
field_id: int
|
|
77
|
+
value: Any
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(slots=True)
|
|
81
|
+
class _When(_Node):
|
|
82
|
+
condition: _Eq
|
|
83
|
+
fields: list[_Node]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(slots=True)
|
|
87
|
+
class _Repeat(_Node):
|
|
88
|
+
fields: list[_Node]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(slots=True)
|
|
92
|
+
class _Group(_Node):
|
|
93
|
+
fields: list[_Node]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass(slots=True)
|
|
97
|
+
class _Sized(_Node):
|
|
98
|
+
field_id: int
|
|
99
|
+
get: Get
|
|
100
|
+
set: Set
|
|
101
|
+
count: int
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass(slots=True)
|
|
105
|
+
class _U2Slot(_Node):
|
|
106
|
+
field_id: int
|
|
107
|
+
get: Get
|
|
108
|
+
set: Set
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass(slots=True)
|
|
112
|
+
class _U2(_Node):
|
|
113
|
+
slots: list[_U2Slot]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass(slots=True)
|
|
117
|
+
class _Bits(_Node):
|
|
118
|
+
field_id: int
|
|
119
|
+
get: Get
|
|
120
|
+
set: Set
|
|
121
|
+
count: int
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass(slots=True)
|
|
125
|
+
class _Utf8(_Node):
|
|
126
|
+
field_id: int
|
|
127
|
+
get: Get
|
|
128
|
+
set: Set
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@dataclass(slots=True)
|
|
132
|
+
class _List(_Node):
|
|
133
|
+
get: Get
|
|
134
|
+
set: Set
|
|
135
|
+
element: _Node
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@dataclass(slots=True)
|
|
139
|
+
class _Dict(_Node):
|
|
140
|
+
get: Get
|
|
141
|
+
set: Set
|
|
142
|
+
element: _Node
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _scalar(
|
|
146
|
+
field_id: int,
|
|
147
|
+
get: Get,
|
|
148
|
+
set: Set,
|
|
149
|
+
kind: str,
|
|
150
|
+
size: int,
|
|
151
|
+
fmt: str,
|
|
152
|
+
signed: bool,
|
|
153
|
+
min_v: int | float,
|
|
154
|
+
max_v: int | float,
|
|
155
|
+
) -> _Scalar:
|
|
156
|
+
return _Scalar(
|
|
157
|
+
field_id=field_id,
|
|
158
|
+
get=get,
|
|
159
|
+
set=set,
|
|
160
|
+
kind=kind,
|
|
161
|
+
size=size,
|
|
162
|
+
fmt_le="<" + fmt,
|
|
163
|
+
fmt_be=">" + fmt,
|
|
164
|
+
signed=signed,
|
|
165
|
+
min_v=min_v,
|
|
166
|
+
max_v=max_v,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def u8(field_id: int, get: Get, set: Set) -> _Scalar:
|
|
171
|
+
return _scalar(field_id, get, set, "u8", 1, "B", False, 0, 0xFF)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def u16(field_id: int, get: Get, set: Set) -> _Scalar:
|
|
175
|
+
return _scalar(field_id, get, set, "u16", 2, "H", False, 0, 0xFFFF)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def u32(field_id: int, get: Get, set: Set) -> _Scalar:
|
|
179
|
+
return _scalar(field_id, get, set, "u32", 4, "I", False, 0, 0xFFFFFFFF)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def u64(field_id: int, get: Get, set: Set) -> _Scalar:
|
|
183
|
+
return _scalar(field_id, get, set, "u64", 8, "Q", False, 0, 0xFFFFFFFFFFFFFFFF)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def i8(field_id: int, get: Get, set: Set) -> _Scalar:
|
|
187
|
+
return _scalar(field_id, get, set, "i8", 1, "b", True, -0x80, 0x7F)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def i16(field_id: int, get: Get, set: Set) -> _Scalar:
|
|
191
|
+
return _scalar(field_id, get, set, "i16", 2, "h", True, -0x8000, 0x7FFF)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def i32(field_id: int, get: Get, set: Set) -> _Scalar:
|
|
195
|
+
return _scalar(field_id, get, set, "i32", 4, "i", True, -0x80000000, 0x7FFFFFFF)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def i64(field_id: int, get: Get, set: Set) -> _Scalar:
|
|
199
|
+
return _scalar(field_id, get, set, "i64", 8, "q", True, -0x8000000000000000, 0x7FFFFFFFFFFFFFFF)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def f32(field_id: int, get: Get, set: Set) -> _Scalar:
|
|
203
|
+
return _scalar(field_id, get, set, "f32", 4, "f", True, float("-inf"), float("inf"))
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def f64(field_id: int, get: Get, set: Set) -> _Scalar:
|
|
207
|
+
return _scalar(field_id, get, set, "f64", 8, "d", True, float("-inf"), float("inf"))
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def bool(field_id: int, get: Get, set: Set) -> _Bool: # noqa: A001
|
|
211
|
+
return _Bool(field_id=field_id, get=get, set=set)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def bytes(field_id: int, get: Get, set: Set, n: int) -> _Bytes: # noqa: A001
|
|
215
|
+
if n < 0:
|
|
216
|
+
raise ValueError("bytes length must be >= 0")
|
|
217
|
+
return _Bytes(field_id=field_id, get=get, set=set, size=n)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def be(field: _Scalar) -> _Scalar:
|
|
221
|
+
if not isinstance(field, _Scalar):
|
|
222
|
+
raise TypeError("be() expects a numeric field")
|
|
223
|
+
return _Scalar(
|
|
224
|
+
field_id=field.field_id,
|
|
225
|
+
get=field.get,
|
|
226
|
+
set=field.set,
|
|
227
|
+
kind=field.kind,
|
|
228
|
+
size=field.size,
|
|
229
|
+
fmt_le=field.fmt_le,
|
|
230
|
+
fmt_be=field.fmt_be,
|
|
231
|
+
signed=field.signed,
|
|
232
|
+
min_v=field.min_v,
|
|
233
|
+
max_v=field.max_v,
|
|
234
|
+
big_endian=True,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def flags(*fields: _Node) -> _Flags:
|
|
239
|
+
return _Flags(fields=_builtin_list(fields))
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def flag_byte() -> _FlagByte:
|
|
243
|
+
return _FlagByte(bits=[])
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def eq(field_id: int, value: Any) -> _Eq:
|
|
247
|
+
return _Eq(field_id=field_id, value=value)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def when(condition: _Eq, *fields: _Node) -> _When:
|
|
251
|
+
return _When(condition=condition, fields=_builtin_list(fields))
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def repeat(*fields: _Node) -> _Repeat:
|
|
255
|
+
return _Repeat(fields=_builtin_list(fields))
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def group(*fields: _Node) -> _Group:
|
|
259
|
+
return _Group(fields=_builtin_list(fields))
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def sized(field_id: int, get: Get, set: Set, count: int) -> _Sized:
|
|
263
|
+
return _Sized(field_id=field_id, get=get, set=set, count=count)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def u2(*slots: _U2Slot | tuple[int, Get, Set]) -> _U2:
|
|
267
|
+
if not slots:
|
|
268
|
+
raise ValueError("u2 needs at least one slot")
|
|
269
|
+
out: list[_U2Slot] = []
|
|
270
|
+
for slot in slots:
|
|
271
|
+
if isinstance(slot, _U2Slot):
|
|
272
|
+
out.append(slot)
|
|
273
|
+
else:
|
|
274
|
+
field_id, get, set = slot
|
|
275
|
+
out.append(_U2Slot(field_id=field_id, get=get, set=set))
|
|
276
|
+
return _U2(slots=out)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def bits(field_id: int, get: Get, set: Set, count: int) -> _Bits:
|
|
280
|
+
return _Bits(field_id=field_id, get=get, set=set, count=count)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def utf8(field_id: int, get: Get, set: Set) -> _Utf8:
|
|
284
|
+
return _Utf8(field_id=field_id, get=get, set=set)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def list(get: Get, set: Set, element: _Node) -> _List: # noqa: A001
|
|
288
|
+
if isinstance(element, _Repeat):
|
|
289
|
+
raise ValueError("repeat is not a list element")
|
|
290
|
+
return _List(get=get, set=set, element=element)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def dict(get: Get, set: Set, element: _Node) -> _Dict: # noqa: A001
|
|
294
|
+
if isinstance(element, _Repeat):
|
|
295
|
+
raise ValueError("repeat is not a dictionary element")
|
|
296
|
+
return _Dict(get=get, set=set, element=element)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _validate_order(nodes: Sequence[_Node], next_id: int = 0) -> int:
|
|
300
|
+
for node in nodes:
|
|
301
|
+
if isinstance(node, (_Scalar, _Bytes, _Bool, _Utf8, _Sized, _Bits)):
|
|
302
|
+
if node.field_id != next_id:
|
|
303
|
+
raise ValueError(f"field id {node.field_id} is not the next order {next_id}")
|
|
304
|
+
next_id += 1
|
|
305
|
+
elif isinstance(node, _U2Slot):
|
|
306
|
+
if node.field_id != next_id:
|
|
307
|
+
raise ValueError(f"field id {node.field_id} is not the next order {next_id}")
|
|
308
|
+
next_id += 1
|
|
309
|
+
elif isinstance(node, _U2):
|
|
310
|
+
next_id = _validate_order(node.slots, next_id)
|
|
311
|
+
elif isinstance(node, _Flags):
|
|
312
|
+
next_id = _validate_order(node.fields, next_id)
|
|
313
|
+
elif isinstance(node, (_When, _Repeat, _Group)):
|
|
314
|
+
next_id = _validate_order(node.fields, next_id)
|
|
315
|
+
elif isinstance(node, _FlagByte):
|
|
316
|
+
next_id = _validate_order(node.bits, next_id)
|
|
317
|
+
elif isinstance(node, _FlagBit):
|
|
318
|
+
next_id = _validate_order([node.field], next_id)
|
|
319
|
+
elif isinstance(node, (_List, _Dict)):
|
|
320
|
+
_validate_order([node.element], 0)
|
|
321
|
+
else:
|
|
322
|
+
raise TypeError(f"unknown field node: {type(node)!r}")
|
|
323
|
+
return next_id
|
packbin/_pack.py
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import struct
|
|
4
|
+
from collections.abc import Callable, Sequence
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from packbin._nodes import (
|
|
8
|
+
_Bits,
|
|
9
|
+
_Bool,
|
|
10
|
+
_Bytes,
|
|
11
|
+
_Dict,
|
|
12
|
+
_FlagBit,
|
|
13
|
+
_FlagByte,
|
|
14
|
+
_Flags,
|
|
15
|
+
_Group,
|
|
16
|
+
_List,
|
|
17
|
+
_Node,
|
|
18
|
+
_Repeat,
|
|
19
|
+
_Scalar,
|
|
20
|
+
_Sized,
|
|
21
|
+
_U2,
|
|
22
|
+
_Utf8,
|
|
23
|
+
_When,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
_builtin_bytes = bytes
|
|
27
|
+
_builtin_list = list
|
|
28
|
+
_builtin_dict = dict
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _utf8_payload(label: str, value: Any) -> bytes:
|
|
32
|
+
if not isinstance(value, str):
|
|
33
|
+
raise TypeError(f"{label}: expected str")
|
|
34
|
+
raw = value.encode("utf-8")
|
|
35
|
+
if len(raw) > 65535:
|
|
36
|
+
raise ValueError(f"{label}: utf-8 length {len(raw)}")
|
|
37
|
+
return raw
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _present_value(value: Any) -> bool:
|
|
41
|
+
return value is not None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _bool_on(value: Any) -> bool:
|
|
45
|
+
return value is True
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _group_on(row: Any, node: _Group) -> bool:
|
|
49
|
+
for child in node.fields:
|
|
50
|
+
if _child_on(row, child):
|
|
51
|
+
return True
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _child_on(row: Any, child: _Node) -> bool:
|
|
56
|
+
if isinstance(child, _Group):
|
|
57
|
+
return _group_on(row, child)
|
|
58
|
+
if isinstance(child, _Bool):
|
|
59
|
+
return _bool_on(child.get(row))
|
|
60
|
+
if isinstance(child, (_Scalar, _Bytes, _Utf8, _Sized, _Bits, _List, _Dict)):
|
|
61
|
+
return _present_value(child.get(row))
|
|
62
|
+
if isinstance(child, _FlagBit):
|
|
63
|
+
return _child_on(row, child.field)
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _write_u2(buf: bytearray, slots: Sequence[Any], row: Any, seen: dict[int, Any]) -> None:
|
|
68
|
+
nbytes = (len(slots) + 3) // 4
|
|
69
|
+
raw = bytearray(nbytes)
|
|
70
|
+
for i, slot in enumerate(slots):
|
|
71
|
+
value = slot.get(row)
|
|
72
|
+
seen[slot.field_id] = value
|
|
73
|
+
if isinstance(value, bool) or not isinstance(value, int) or value < 0 or value > 3:
|
|
74
|
+
raise ValueError(f"{slot.field_id}: expected 2-bit int")
|
|
75
|
+
raw[i // 4] |= value << ((i % 4) * 2)
|
|
76
|
+
buf.extend(raw)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _write_bits(buf: bytearray, label: str, count: int, raw: Any) -> None:
|
|
80
|
+
if not isinstance(raw, _builtin_list) or len(raw) != count:
|
|
81
|
+
raise ValueError(f"{label}: expected {count} bits")
|
|
82
|
+
nbytes = (count + 7) // 8
|
|
83
|
+
packed = bytearray(nbytes)
|
|
84
|
+
for i, bit in enumerate(raw):
|
|
85
|
+
if bit not in (0, 1):
|
|
86
|
+
raise ValueError(f"{label}: expected 0 or 1")
|
|
87
|
+
packed[i // 8] |= int(bit) << (i % 8)
|
|
88
|
+
buf.extend(packed)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _require_int(field: _Scalar, value: Any) -> int | float:
|
|
92
|
+
if field.kind in ("f32", "f64"):
|
|
93
|
+
return float(value)
|
|
94
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
95
|
+
raise TypeError(f"{field.field_id}: expected int, got {type(value).__name__}")
|
|
96
|
+
if value < field.min_v or value > field.max_v:
|
|
97
|
+
raise OverflowError(f"{field.field_id}: {value} does not fit in {field.kind}")
|
|
98
|
+
return value
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _write_scalar(buf: bytearray, field: _Scalar, value: Any) -> None:
|
|
102
|
+
packed = struct.pack(field.endian_fmt(), _require_int(field, value))
|
|
103
|
+
buf.extend(packed)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _is_leaf(node: _Node) -> bool:
|
|
107
|
+
return isinstance(node, (_Scalar, _Bytes, _Utf8, _Bool, _Sized, _Bits))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _pack_element(buf: bytearray, element: _Node, item: Any) -> None:
|
|
111
|
+
if isinstance(element, _List):
|
|
112
|
+
_pack_list_items(buf, element.element, item)
|
|
113
|
+
elif isinstance(element, _Dict):
|
|
114
|
+
_pack_dict_items(buf, element.element, item)
|
|
115
|
+
elif _is_leaf(element):
|
|
116
|
+
pack_nodes(buf, [element], item, {}, lambda _n: item)
|
|
117
|
+
else:
|
|
118
|
+
pack_nodes(buf, [element], item, {})
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _pack_list_items(buf: bytearray, element: _Node, items: Any) -> None:
|
|
122
|
+
if not isinstance(items, _builtin_list):
|
|
123
|
+
raise TypeError("list: expected list")
|
|
124
|
+
if len(items) > 65535:
|
|
125
|
+
raise ValueError(f"list: length {len(items)}")
|
|
126
|
+
buf.append(len(items) & 0xFF)
|
|
127
|
+
buf.append((len(items) >> 8) & 0xFF)
|
|
128
|
+
for item in items:
|
|
129
|
+
_pack_element(buf, element, item)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _pack_dict_items(buf: bytearray, element: _Node, mapping: Any) -> None:
|
|
133
|
+
if not isinstance(mapping, _builtin_dict):
|
|
134
|
+
raise TypeError("dict: expected dict")
|
|
135
|
+
if len(mapping) > 65535:
|
|
136
|
+
raise ValueError(f"dict: length {len(mapping)}")
|
|
137
|
+
pairs = sorted(mapping.items(), key=lambda kv: kv[0].encode("utf-8"))
|
|
138
|
+
buf.append(len(pairs) & 0xFF)
|
|
139
|
+
buf.append((len(pairs) >> 8) & 0xFF)
|
|
140
|
+
for key, value in pairs:
|
|
141
|
+
raw = _utf8_payload("dict", key)
|
|
142
|
+
buf.append(len(raw) & 0xFF)
|
|
143
|
+
buf.append((len(raw) >> 8) & 0xFF)
|
|
144
|
+
buf.extend(raw)
|
|
145
|
+
_pack_element(buf, element, value)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def pack_nodes(
|
|
149
|
+
buf: bytearray,
|
|
150
|
+
nodes: Sequence[_Node],
|
|
151
|
+
row: Any,
|
|
152
|
+
seen: dict[int, Any] | None = None,
|
|
153
|
+
get_value: Callable[[_Node], Any] | None = None,
|
|
154
|
+
) -> None:
|
|
155
|
+
if seen is None:
|
|
156
|
+
seen = {}
|
|
157
|
+
|
|
158
|
+
def take(node: _Node) -> Any:
|
|
159
|
+
if get_value is not None:
|
|
160
|
+
return get_value(node)
|
|
161
|
+
return node.get(row) # type: ignore[attr-defined]
|
|
162
|
+
|
|
163
|
+
for node in nodes:
|
|
164
|
+
if isinstance(node, _Scalar):
|
|
165
|
+
value = take(node)
|
|
166
|
+
if not _present_value(value) and get_value is None:
|
|
167
|
+
raise KeyError(f"missing field {node.field_id}")
|
|
168
|
+
seen[node.field_id] = value
|
|
169
|
+
_write_scalar(buf, node, value)
|
|
170
|
+
elif isinstance(node, _Bytes):
|
|
171
|
+
raw = take(node)
|
|
172
|
+
if not _present_value(raw) and get_value is None:
|
|
173
|
+
raise KeyError(f"missing field {node.field_id}")
|
|
174
|
+
seen[node.field_id] = raw
|
|
175
|
+
if not isinstance(raw, (_builtin_bytes, bytearray, memoryview)):
|
|
176
|
+
raise TypeError(f"{node.field_id}: expected bytes")
|
|
177
|
+
if len(raw) != node.size:
|
|
178
|
+
raise ValueError(f"{node.field_id}: expected {node.size} bytes, got {len(raw)}")
|
|
179
|
+
buf.extend(raw)
|
|
180
|
+
elif isinstance(node, _Bool):
|
|
181
|
+
seen[node.field_id] = take(node)
|
|
182
|
+
elif isinstance(node, _Flags):
|
|
183
|
+
flag = 0
|
|
184
|
+
for i, child in enumerate(node.fields):
|
|
185
|
+
if _child_on(row, child):
|
|
186
|
+
flag |= 1 << i
|
|
187
|
+
buf.append(flag)
|
|
188
|
+
for i, child in enumerate(node.fields):
|
|
189
|
+
if flag & (1 << i):
|
|
190
|
+
if isinstance(child, _Bool):
|
|
191
|
+
seen[child.field_id] = child.get(row)
|
|
192
|
+
elif isinstance(child, _Group):
|
|
193
|
+
pack_nodes(buf, child.fields, row, seen, get_value)
|
|
194
|
+
else:
|
|
195
|
+
pack_nodes(buf, [child], row, seen, get_value)
|
|
196
|
+
elif isinstance(node, _Sized):
|
|
197
|
+
count = seen.get(node.count)
|
|
198
|
+
if count is None:
|
|
199
|
+
raise RuntimeError(f"{node.field_id}: count {node.count} is missing")
|
|
200
|
+
raw = take(node)
|
|
201
|
+
seen[node.field_id] = raw
|
|
202
|
+
if not isinstance(raw, (_builtin_bytes, bytearray, memoryview)):
|
|
203
|
+
raise TypeError(f"{node.field_id}: expected bytes")
|
|
204
|
+
if len(raw) != count:
|
|
205
|
+
raise ValueError(f"{node.field_id}: expected {count} bytes, got {len(raw)}")
|
|
206
|
+
buf.extend(raw)
|
|
207
|
+
elif isinstance(node, _U2):
|
|
208
|
+
_write_u2(buf, node.slots, row, seen)
|
|
209
|
+
elif isinstance(node, _Bits):
|
|
210
|
+
count = seen.get(node.count)
|
|
211
|
+
if count is None:
|
|
212
|
+
raise RuntimeError(f"{node.field_id}: count {node.count} is missing")
|
|
213
|
+
value = take(node)
|
|
214
|
+
seen[node.field_id] = value
|
|
215
|
+
_write_bits(buf, str(node.field_id), int(count), value)
|
|
216
|
+
elif isinstance(node, _FlagByte):
|
|
217
|
+
flag = 0
|
|
218
|
+
for i, child in enumerate(node.bits):
|
|
219
|
+
if _child_on(row, child):
|
|
220
|
+
flag |= 1 << i
|
|
221
|
+
buf.append(flag)
|
|
222
|
+
elif isinstance(node, _FlagBit):
|
|
223
|
+
if _child_on(row, node.field):
|
|
224
|
+
pack_nodes(buf, [node.field], row, seen, get_value)
|
|
225
|
+
elif isinstance(node, _When):
|
|
226
|
+
if seen.get(node.condition.field_id) == node.condition.value:
|
|
227
|
+
pack_nodes(buf, node.fields, row, seen, get_value)
|
|
228
|
+
elif isinstance(node, _Repeat):
|
|
229
|
+
lengths = []
|
|
230
|
+
for child in node.fields:
|
|
231
|
+
val = child.get(row) # type: ignore[attr-defined]
|
|
232
|
+
if val is None:
|
|
233
|
+
lengths.append(0)
|
|
234
|
+
elif isinstance(val, _builtin_list):
|
|
235
|
+
lengths.append(len(val))
|
|
236
|
+
else:
|
|
237
|
+
lengths.append(1)
|
|
238
|
+
count = max(lengths) if lengths else 0
|
|
239
|
+
if any(n not in (0, count) for n in lengths):
|
|
240
|
+
raise ValueError("repeat fields must have equal lengths")
|
|
241
|
+
for i in range(count):
|
|
242
|
+
|
|
243
|
+
def at(child: _Node, index: int = i) -> Any:
|
|
244
|
+
val = child.get(row) # type: ignore[attr-defined]
|
|
245
|
+
if isinstance(val, _builtin_list):
|
|
246
|
+
return val[index]
|
|
247
|
+
return val
|
|
248
|
+
|
|
249
|
+
pack_nodes(buf, node.fields, row, seen, at)
|
|
250
|
+
elif isinstance(node, _Utf8):
|
|
251
|
+
value = take(node)
|
|
252
|
+
seen[node.field_id] = value
|
|
253
|
+
raw = _utf8_payload(str(node.field_id), value)
|
|
254
|
+
buf.append(len(raw) & 0xFF)
|
|
255
|
+
buf.append((len(raw) >> 8) & 0xFF)
|
|
256
|
+
buf.extend(raw)
|
|
257
|
+
elif isinstance(node, _List):
|
|
258
|
+
_pack_list_items(buf, node.element, take(node))
|
|
259
|
+
elif isinstance(node, _Dict):
|
|
260
|
+
_pack_dict_items(buf, node.element, take(node))
|
|
261
|
+
elif isinstance(node, _Group):
|
|
262
|
+
pack_nodes(buf, node.fields, row, seen, get_value)
|
|
263
|
+
else:
|
|
264
|
+
raise TypeError(f"unknown field node: {type(node)!r}")
|
packbin/_scheme.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Mapping
|
|
4
|
+
from typing import Any, Generic, TypeVar
|
|
5
|
+
|
|
6
|
+
from packbin._errors import ShortPacket, TrailingBytes, TypeMismatch, UnpackResult
|
|
7
|
+
from packbin._nodes import _Node, _validate_order
|
|
8
|
+
from packbin._pack import pack_nodes
|
|
9
|
+
from packbin._unpack import unpack_nodes
|
|
10
|
+
|
|
11
|
+
T = TypeVar("T")
|
|
12
|
+
_builtin_bytes = bytes
|
|
13
|
+
_builtin_dict = dict
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Scheme(Generic[T]):
|
|
17
|
+
__slots__ = ("_type_number", "_row_type", "_fields")
|
|
18
|
+
|
|
19
|
+
def __init__(self, type_number: int, row_type: type[T], *fields: _Node) -> None:
|
|
20
|
+
if isinstance(type_number, bool) or not isinstance(type_number, int) or type_number < 0 or type_number > 255:
|
|
21
|
+
raise ValueError(f"type number must be 0..255, got {type_number!r}")
|
|
22
|
+
_validate_order(fields)
|
|
23
|
+
self._type_number = type_number
|
|
24
|
+
self._row_type = row_type
|
|
25
|
+
self._fields = list(fields)
|
|
26
|
+
|
|
27
|
+
def on(self, handler: Callable[[T], None]) -> _Handler[T]:
|
|
28
|
+
return _Handler(self, handler)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class _Handler(Generic[T]):
|
|
32
|
+
__slots__ = ("scheme", "handler")
|
|
33
|
+
|
|
34
|
+
def __init__(self, scheme: Scheme[T], handler: Callable[[T], None]) -> None:
|
|
35
|
+
self.scheme = scheme
|
|
36
|
+
self.handler = handler
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _new_row(row_type: type[T]) -> T:
|
|
40
|
+
if row_type is _builtin_dict:
|
|
41
|
+
return {} # type: ignore[return-value]
|
|
42
|
+
return row_type()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class BinaryPacker:
|
|
46
|
+
@staticmethod
|
|
47
|
+
def pack(scheme: Scheme[T], row: T | Mapping[str, Any]) -> bytes:
|
|
48
|
+
buf = bytearray()
|
|
49
|
+
buf.append(scheme._type_number)
|
|
50
|
+
pack_nodes(buf, scheme._fields, row)
|
|
51
|
+
return _builtin_bytes(buf)
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def unpack(
|
|
55
|
+
first: Scheme[T] | bytes | bytearray | memoryview,
|
|
56
|
+
second: bytes | bytearray | memoryview | _Handler[Any] | None = None,
|
|
57
|
+
*rest: _Handler[Any],
|
|
58
|
+
) -> UnpackResult[Any]:
|
|
59
|
+
if isinstance(first, Scheme):
|
|
60
|
+
if second is None:
|
|
61
|
+
raise TypeError("unpack() missing data")
|
|
62
|
+
if isinstance(second, _Handler):
|
|
63
|
+
raise TypeError("known unpack expects bytes")
|
|
64
|
+
return BinaryPacker._unpack_known(first, second)
|
|
65
|
+
if second is None and not rest:
|
|
66
|
+
raise TypeError("unpack() missing handlers")
|
|
67
|
+
handlers: list[_Handler[Any]] = []
|
|
68
|
+
if second is not None:
|
|
69
|
+
if not isinstance(second, _Handler):
|
|
70
|
+
raise TypeError("unknown unpack expects handlers")
|
|
71
|
+
handlers.append(second)
|
|
72
|
+
handlers.extend(rest)
|
|
73
|
+
return BinaryPacker._unpack_dispatch(first, tuple(handlers))
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
def _unpack_fields(scheme: Scheme[T], view: memoryview, offset: int) -> UnpackResult[T]:
|
|
77
|
+
row = _new_row(scheme._row_type)
|
|
78
|
+
offset, err = unpack_nodes(view, offset, scheme._fields, row)
|
|
79
|
+
if err is not None:
|
|
80
|
+
return UnpackResult(ok=False, value=None, error=err)
|
|
81
|
+
left = len(view) - offset
|
|
82
|
+
if left > 0:
|
|
83
|
+
return UnpackResult(ok=False, value=None, error=TrailingBytes(left=left))
|
|
84
|
+
return UnpackResult(ok=True, value=row, error=None)
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
def _unpack_known(scheme: Scheme[T], data: bytes | bytearray | memoryview) -> UnpackResult[T]:
|
|
88
|
+
view = memoryview(data)
|
|
89
|
+
left = len(view)
|
|
90
|
+
if left < 1:
|
|
91
|
+
return UnpackResult(ok=False, value=None, error=ShortPacket(field="", needed=1, left=left))
|
|
92
|
+
actual = int(view[0])
|
|
93
|
+
if actual != scheme._type_number:
|
|
94
|
+
return UnpackResult(ok=False, value=None, error=TypeMismatch(expected=scheme._type_number, actual=actual))
|
|
95
|
+
return BinaryPacker._unpack_fields(scheme, view, 1)
|
|
96
|
+
|
|
97
|
+
@staticmethod
|
|
98
|
+
def _unpack_dispatch(
|
|
99
|
+
data: bytes | bytearray | memoryview,
|
|
100
|
+
handlers: tuple[_Handler[Any], ...],
|
|
101
|
+
) -> UnpackResult[Any]:
|
|
102
|
+
by_type: dict[int, _Handler[Any]] = {}
|
|
103
|
+
for handler in handlers:
|
|
104
|
+
number = handler.scheme._type_number
|
|
105
|
+
if number in by_type:
|
|
106
|
+
raise ValueError(f"duplicate type number {number}")
|
|
107
|
+
by_type[number] = handler
|
|
108
|
+
view = memoryview(data)
|
|
109
|
+
left = len(view)
|
|
110
|
+
if left < 1:
|
|
111
|
+
return UnpackResult(ok=False, value=None, error=ShortPacket(field="", needed=1, left=left))
|
|
112
|
+
actual = int(view[0])
|
|
113
|
+
matched = by_type.get(actual)
|
|
114
|
+
if matched is None:
|
|
115
|
+
return UnpackResult(ok=False, value=None, error=TypeMismatch(expected=-1, actual=actual))
|
|
116
|
+
result = BinaryPacker._unpack_fields(matched.scheme, view, 1)
|
|
117
|
+
if not result.ok or result.value is None:
|
|
118
|
+
return result
|
|
119
|
+
matched.handler(result.value)
|
|
120
|
+
return result
|
packbin/_unpack.py
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import struct
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from packbin._errors import ShortPacket, TypeMismatch
|
|
8
|
+
from packbin._nodes import (
|
|
9
|
+
_Bits,
|
|
10
|
+
_Bool,
|
|
11
|
+
_Bytes,
|
|
12
|
+
_Dict,
|
|
13
|
+
_FlagBit,
|
|
14
|
+
_FlagByte,
|
|
15
|
+
_Flags,
|
|
16
|
+
_Group,
|
|
17
|
+
_List,
|
|
18
|
+
_Node,
|
|
19
|
+
_Repeat,
|
|
20
|
+
_Scalar,
|
|
21
|
+
_Sized,
|
|
22
|
+
_U2,
|
|
23
|
+
_Utf8,
|
|
24
|
+
_When,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
_builtin_bytes = bytes
|
|
28
|
+
_builtin_list = list
|
|
29
|
+
_builtin_dict = dict
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _read_u2(data: memoryview, offset: int, count: int, label: str) -> tuple[list[int], int] | ShortPacket:
|
|
33
|
+
nbytes = (count + 3) // 4
|
|
34
|
+
left = len(data) - offset
|
|
35
|
+
if left < nbytes:
|
|
36
|
+
return ShortPacket(field=label, needed=nbytes, left=left)
|
|
37
|
+
out: list[int] = []
|
|
38
|
+
for i in range(count):
|
|
39
|
+
out.append((data[offset + i // 4] >> ((i % 4) * 2)) & 3)
|
|
40
|
+
return out, offset + nbytes
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _read_bits(data: memoryview, offset: int, label: str, count: int) -> tuple[list[int], int] | ShortPacket:
|
|
44
|
+
nbytes = (count + 7) // 8
|
|
45
|
+
left = len(data) - offset
|
|
46
|
+
if left < nbytes:
|
|
47
|
+
return ShortPacket(field=label, needed=nbytes, left=left)
|
|
48
|
+
out = [((data[offset + i // 8] >> (i % 8)) & 1) for i in range(count)]
|
|
49
|
+
return out, offset + nbytes
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _read_scalar(
|
|
53
|
+
data: memoryview, offset: int, field: _Scalar
|
|
54
|
+
) -> tuple[Any, int] | ShortPacket:
|
|
55
|
+
left = len(data) - offset
|
|
56
|
+
if left < field.size:
|
|
57
|
+
return ShortPacket(field=str(field.field_id), needed=field.size, left=left)
|
|
58
|
+
value = struct.unpack_from(field.endian_fmt(), data, offset)[0]
|
|
59
|
+
return value, offset + field.size
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _is_leaf(node: _Node) -> bool:
|
|
63
|
+
return isinstance(node, (_Scalar, _Bytes, _Utf8, _Bool, _Sized, _Bits))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _append(row: Any, node: Any, value: Any, as_list: bool) -> None:
|
|
67
|
+
if as_list:
|
|
68
|
+
cur = node.get(row)
|
|
69
|
+
if cur is None:
|
|
70
|
+
node.set(row, [value])
|
|
71
|
+
elif isinstance(cur, _builtin_list):
|
|
72
|
+
cur.append(value)
|
|
73
|
+
else:
|
|
74
|
+
node.set(row, [cur, value])
|
|
75
|
+
else:
|
|
76
|
+
node.set(row, value)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _unpack_leaf(
|
|
80
|
+
data: memoryview, offset: int, node: _Node, seen: dict[int, Any]
|
|
81
|
+
) -> tuple[Any, int, ShortPacket | TypeMismatch | None]:
|
|
82
|
+
if isinstance(node, _Scalar):
|
|
83
|
+
got = _read_scalar(data, offset, node)
|
|
84
|
+
if isinstance(got, ShortPacket):
|
|
85
|
+
return None, offset, got
|
|
86
|
+
value, offset = got
|
|
87
|
+
seen[node.field_id] = value
|
|
88
|
+
return value, offset, None
|
|
89
|
+
if isinstance(node, _Bytes):
|
|
90
|
+
left = len(data) - offset
|
|
91
|
+
if left < node.size:
|
|
92
|
+
return None, offset, ShortPacket(field=str(node.field_id), needed=node.size, left=left)
|
|
93
|
+
raw = _builtin_bytes(data[offset : offset + node.size])
|
|
94
|
+
offset += node.size
|
|
95
|
+
seen[node.field_id] = raw
|
|
96
|
+
return raw, offset, None
|
|
97
|
+
if isinstance(node, _Utf8):
|
|
98
|
+
left = len(data) - offset
|
|
99
|
+
if left < 2:
|
|
100
|
+
return None, offset, ShortPacket(field=str(node.field_id), needed=2, left=left)
|
|
101
|
+
count = struct.unpack_from("<H", data, offset)[0]
|
|
102
|
+
offset += 2
|
|
103
|
+
left = len(data) - offset
|
|
104
|
+
if left < count:
|
|
105
|
+
return None, offset, ShortPacket(field=str(node.field_id), needed=count, left=left)
|
|
106
|
+
raw = _builtin_bytes(data[offset : offset + count])
|
|
107
|
+
offset += count
|
|
108
|
+
value = raw.decode("utf-8")
|
|
109
|
+
seen[node.field_id] = value
|
|
110
|
+
return value, offset, None
|
|
111
|
+
if isinstance(node, _Bool):
|
|
112
|
+
return True, offset, None
|
|
113
|
+
raise TypeError(f"not a leaf: {type(node)!r}")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _unpack_element(
|
|
117
|
+
data: memoryview, offset: int, element: _Node
|
|
118
|
+
) -> tuple[Any, int, ShortPacket | TypeMismatch | None]:
|
|
119
|
+
if isinstance(element, _List):
|
|
120
|
+
return _unpack_list_items(data, offset, element.element)
|
|
121
|
+
if isinstance(element, _Dict):
|
|
122
|
+
return _unpack_dict_items(data, offset, element.element)
|
|
123
|
+
if _is_leaf(element):
|
|
124
|
+
return _unpack_leaf(data, offset, element, {})
|
|
125
|
+
child_row: dict[str, Any] = {}
|
|
126
|
+
offset, err = unpack_nodes(data, offset, [element], child_row)
|
|
127
|
+
if err is not None:
|
|
128
|
+
return None, offset, err
|
|
129
|
+
return child_row, offset, None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _unpack_list_items(
|
|
133
|
+
data: memoryview, offset: int, element: _Node
|
|
134
|
+
) -> tuple[list[Any], int, ShortPacket | TypeMismatch | None]:
|
|
135
|
+
left = len(data) - offset
|
|
136
|
+
if left < 2:
|
|
137
|
+
return [], offset, ShortPacket(field="", needed=2, left=left)
|
|
138
|
+
count = struct.unpack_from("<H", data, offset)[0]
|
|
139
|
+
offset += 2
|
|
140
|
+
items: list[Any] = []
|
|
141
|
+
for _ in range(count):
|
|
142
|
+
value, offset, err = _unpack_element(data, offset, element)
|
|
143
|
+
if err is not None:
|
|
144
|
+
return items, offset, err
|
|
145
|
+
items.append(value)
|
|
146
|
+
return items, offset, None
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _unpack_dict_items(
|
|
150
|
+
data: memoryview, offset: int, element: _Node
|
|
151
|
+
) -> tuple[dict[str, Any], int, ShortPacket | TypeMismatch | None]:
|
|
152
|
+
left = len(data) - offset
|
|
153
|
+
if left < 2:
|
|
154
|
+
return {}, offset, ShortPacket(field="", needed=2, left=left)
|
|
155
|
+
count = struct.unpack_from("<H", data, offset)[0]
|
|
156
|
+
offset += 2
|
|
157
|
+
mapping: dict[str, Any] = {}
|
|
158
|
+
for _ in range(count):
|
|
159
|
+
left = len(data) - offset
|
|
160
|
+
if left < 2:
|
|
161
|
+
return mapping, offset, ShortPacket(field="", needed=2, left=left)
|
|
162
|
+
key_len = struct.unpack_from("<H", data, offset)[0]
|
|
163
|
+
offset += 2
|
|
164
|
+
left = len(data) - offset
|
|
165
|
+
if left < key_len:
|
|
166
|
+
return mapping, offset, ShortPacket(field="", needed=key_len, left=left)
|
|
167
|
+
key = _builtin_bytes(data[offset : offset + key_len]).decode("utf-8")
|
|
168
|
+
offset += key_len
|
|
169
|
+
value, offset, err = _unpack_element(data, offset, element)
|
|
170
|
+
if err is not None:
|
|
171
|
+
return mapping, offset, err
|
|
172
|
+
if key in mapping:
|
|
173
|
+
return mapping, offset, ShortPacket(field="", needed=0, left=0)
|
|
174
|
+
mapping[key] = value
|
|
175
|
+
return mapping, offset, None
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def unpack_nodes(
|
|
179
|
+
data: memoryview,
|
|
180
|
+
offset: int,
|
|
181
|
+
nodes: Sequence[_Node],
|
|
182
|
+
row: Any,
|
|
183
|
+
seen: dict[int, Any] | None = None,
|
|
184
|
+
*,
|
|
185
|
+
as_list: bool = False,
|
|
186
|
+
flag_state: dict[int, int] | None = None,
|
|
187
|
+
) -> tuple[int, ShortPacket | TypeMismatch | None]:
|
|
188
|
+
if seen is None:
|
|
189
|
+
seen = {}
|
|
190
|
+
if flag_state is None:
|
|
191
|
+
flag_state = {}
|
|
192
|
+
|
|
193
|
+
for node in nodes:
|
|
194
|
+
if isinstance(node, _Scalar):
|
|
195
|
+
got = _read_scalar(data, offset, node)
|
|
196
|
+
if isinstance(got, ShortPacket):
|
|
197
|
+
return offset, got
|
|
198
|
+
value, offset = got
|
|
199
|
+
seen[node.field_id] = value
|
|
200
|
+
_append(row, node, value, as_list)
|
|
201
|
+
elif isinstance(node, _Bytes):
|
|
202
|
+
left = len(data) - offset
|
|
203
|
+
if left < node.size:
|
|
204
|
+
return offset, ShortPacket(field=str(node.field_id), needed=node.size, left=left)
|
|
205
|
+
raw = _builtin_bytes(data[offset : offset + node.size])
|
|
206
|
+
offset += node.size
|
|
207
|
+
seen[node.field_id] = raw
|
|
208
|
+
_append(row, node, raw, as_list)
|
|
209
|
+
elif isinstance(node, _Bool):
|
|
210
|
+
pass
|
|
211
|
+
elif isinstance(node, _Flags):
|
|
212
|
+
left = len(data) - offset
|
|
213
|
+
if left < 1:
|
|
214
|
+
return offset, ShortPacket(field="", needed=1, left=left)
|
|
215
|
+
flag = data[offset]
|
|
216
|
+
offset += 1
|
|
217
|
+
for i, child in enumerate(node.fields):
|
|
218
|
+
if flag & (1 << i):
|
|
219
|
+
if isinstance(child, _Bool):
|
|
220
|
+
seen[child.field_id] = True
|
|
221
|
+
_append(row, child, True, as_list)
|
|
222
|
+
elif isinstance(child, _Group):
|
|
223
|
+
offset, err = unpack_nodes(
|
|
224
|
+
data, offset, child.fields, row, seen, as_list=as_list, flag_state=flag_state
|
|
225
|
+
)
|
|
226
|
+
if err is not None:
|
|
227
|
+
return offset, err
|
|
228
|
+
else:
|
|
229
|
+
offset, err = unpack_nodes(
|
|
230
|
+
data, offset, [child], row, seen, as_list=as_list, flag_state=flag_state
|
|
231
|
+
)
|
|
232
|
+
if err is not None:
|
|
233
|
+
return offset, err
|
|
234
|
+
elif isinstance(node, _FlagByte):
|
|
235
|
+
left = len(data) - offset
|
|
236
|
+
if left < 1:
|
|
237
|
+
return offset, ShortPacket(field="", needed=1, left=left)
|
|
238
|
+
flag = data[offset]
|
|
239
|
+
offset += 1
|
|
240
|
+
flag_state[id(node)] = flag
|
|
241
|
+
elif isinstance(node, _FlagBit):
|
|
242
|
+
flag = flag_state.get(id(node.owner))
|
|
243
|
+
if flag is None:
|
|
244
|
+
raise RuntimeError("flag bit before flag byte")
|
|
245
|
+
if flag & (1 << node.index):
|
|
246
|
+
offset, err = unpack_nodes(
|
|
247
|
+
data, offset, [node.field], row, seen, as_list=as_list, flag_state=flag_state
|
|
248
|
+
)
|
|
249
|
+
if err is not None:
|
|
250
|
+
return offset, err
|
|
251
|
+
elif isinstance(node, _When):
|
|
252
|
+
if seen.get(node.condition.field_id) == node.condition.value:
|
|
253
|
+
offset, err = unpack_nodes(
|
|
254
|
+
data, offset, node.fields, row, seen, as_list=as_list, flag_state=flag_state
|
|
255
|
+
)
|
|
256
|
+
if err is not None:
|
|
257
|
+
return offset, err
|
|
258
|
+
elif isinstance(node, _Repeat):
|
|
259
|
+
while offset < len(data):
|
|
260
|
+
offset, err = unpack_nodes(
|
|
261
|
+
data, offset, node.fields, row, seen, as_list=True, flag_state=flag_state
|
|
262
|
+
)
|
|
263
|
+
if err is not None:
|
|
264
|
+
return offset, err
|
|
265
|
+
elif isinstance(node, _Sized):
|
|
266
|
+
count = seen.get(node.count)
|
|
267
|
+
if not isinstance(count, int):
|
|
268
|
+
raise RuntimeError(f"{node.field_id}: count {node.count} is missing")
|
|
269
|
+
left = len(data) - offset
|
|
270
|
+
if left < count:
|
|
271
|
+
return offset, ShortPacket(field=str(node.field_id), needed=count, left=left)
|
|
272
|
+
raw = _builtin_bytes(data[offset : offset + count])
|
|
273
|
+
offset += count
|
|
274
|
+
seen[node.field_id] = raw
|
|
275
|
+
_append(row, node, raw, as_list)
|
|
276
|
+
elif isinstance(node, _U2):
|
|
277
|
+
label = str(node.slots[0].field_id) if node.slots else "0"
|
|
278
|
+
got = _read_u2(data, offset, len(node.slots), label)
|
|
279
|
+
if isinstance(got, ShortPacket):
|
|
280
|
+
return offset, got
|
|
281
|
+
values_u2, offset = got
|
|
282
|
+
for slot, value in zip(node.slots, values_u2, strict=True):
|
|
283
|
+
seen[slot.field_id] = value
|
|
284
|
+
_append(row, slot, value, as_list)
|
|
285
|
+
elif isinstance(node, _Bits):
|
|
286
|
+
count = seen.get(node.count)
|
|
287
|
+
if not isinstance(count, int):
|
|
288
|
+
raise RuntimeError(f"{node.field_id}: count {node.count} is missing")
|
|
289
|
+
got_bits = _read_bits(data, offset, str(node.field_id), count)
|
|
290
|
+
if isinstance(got_bits, ShortPacket):
|
|
291
|
+
return offset, got_bits
|
|
292
|
+
bits_value, offset = got_bits
|
|
293
|
+
seen[node.field_id] = bits_value
|
|
294
|
+
_append(row, node, bits_value, as_list)
|
|
295
|
+
elif isinstance(node, _Utf8):
|
|
296
|
+
left = len(data) - offset
|
|
297
|
+
if left < 2:
|
|
298
|
+
return offset, ShortPacket(field=str(node.field_id), needed=2, left=left)
|
|
299
|
+
count = struct.unpack_from("<H", data, offset)[0]
|
|
300
|
+
offset += 2
|
|
301
|
+
left = len(data) - offset
|
|
302
|
+
if left < count:
|
|
303
|
+
return offset, ShortPacket(field=str(node.field_id), needed=count, left=left)
|
|
304
|
+
raw = _builtin_bytes(data[offset : offset + count])
|
|
305
|
+
offset += count
|
|
306
|
+
value = raw.decode("utf-8")
|
|
307
|
+
seen[node.field_id] = value
|
|
308
|
+
_append(row, node, value, as_list)
|
|
309
|
+
elif isinstance(node, _List):
|
|
310
|
+
items, offset, err = _unpack_list_items(data, offset, node.element)
|
|
311
|
+
if err is not None:
|
|
312
|
+
return offset, err
|
|
313
|
+
_append(row, node, items, as_list)
|
|
314
|
+
elif isinstance(node, _Dict):
|
|
315
|
+
mapping, offset, err = _unpack_dict_items(data, offset, node.element)
|
|
316
|
+
if err is not None:
|
|
317
|
+
return offset, err
|
|
318
|
+
_append(row, node, mapping, as_list)
|
|
319
|
+
elif isinstance(node, _Group):
|
|
320
|
+
offset, err = unpack_nodes(
|
|
321
|
+
data, offset, node.fields, row, seen, as_list=as_list, flag_state=flag_state
|
|
322
|
+
)
|
|
323
|
+
if err is not None:
|
|
324
|
+
return offset, err
|
|
325
|
+
else:
|
|
326
|
+
raise TypeError(f"unknown field node: {type(node)!r}")
|
|
327
|
+
return offset, None
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: packbin
|
|
3
|
+
Version: 0.1.9
|
|
4
|
+
Summary: Pack and unpack a caller-owned field list
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
|
|
9
|
+
# packbin
|
|
10
|
+
Binary packing and unpacking across languages, declarative mapping, and zero overhead in the binary data.
|
|
11
|
+
|
|
12
|
+
Both sides keep the same field list. The bytes are only the values.
|
|
13
|
+
|
|
14
|
+
Can be used for WebSocket, TCP, UDP, and other means of efficient communication
|
|
15
|
+
|
|
16
|
+
## Example
|
|
17
|
+
|
|
18
|
+
Python → binary → TypeScript
|
|
19
|
+
|
|
20
|
+
### Python
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from packbin import BinaryPacker, Scheme, flags, i16, i32, u8, u16
|
|
24
|
+
|
|
25
|
+
class Position:
|
|
26
|
+
def __init__(self):
|
|
27
|
+
self.sid = 1
|
|
28
|
+
self.lat = 500_000_000
|
|
29
|
+
self.lon = 300_000_000
|
|
30
|
+
self.profile = 1
|
|
31
|
+
self.heading = None
|
|
32
|
+
self.speed = None
|
|
33
|
+
self.altitude = None
|
|
34
|
+
|
|
35
|
+
def bind(name):
|
|
36
|
+
return (lambda row: getattr(row, name), lambda row, value: setattr(row, name, value))
|
|
37
|
+
|
|
38
|
+
target = Scheme(
|
|
39
|
+
0x40,
|
|
40
|
+
Position,
|
|
41
|
+
u16(0, *bind("sid")),
|
|
42
|
+
i32(1, *bind("lat")),
|
|
43
|
+
i32(2, *bind("lon")),
|
|
44
|
+
u8(3, *bind("profile")),
|
|
45
|
+
flags(
|
|
46
|
+
u16(4, *bind("heading")),
|
|
47
|
+
u8(5, *bind("speed")),
|
|
48
|
+
i16(6, *bind("altitude")),
|
|
49
|
+
),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
raw = BinaryPacker.pack(target, Position())
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
40 01 00 00 65 cd 1d 00 a3 e1 11 01 00
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### TypeScript
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { BinaryPacker, flags, i16, i32, scheme, u8, u16 } from "packbin"
|
|
63
|
+
|
|
64
|
+
class Target {
|
|
65
|
+
sid = 1
|
|
66
|
+
lat = 500_000_000
|
|
67
|
+
lon = 300_000_000
|
|
68
|
+
profile = 1
|
|
69
|
+
heading: number | null = null
|
|
70
|
+
speed: number | null = null
|
|
71
|
+
altitude: number | null = null
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const target = scheme<Target>(
|
|
75
|
+
0x40,
|
|
76
|
+
u16(0, (x) => x.sid),
|
|
77
|
+
i32(1, (x) => x.lat),
|
|
78
|
+
i32(2, (x) => x.lon),
|
|
79
|
+
u8(3, (x) => x.profile),
|
|
80
|
+
flags([
|
|
81
|
+
u16(4, (x) => x.heading),
|
|
82
|
+
u8(5, (x) => x.speed),
|
|
83
|
+
i16(6, (x) => x.altitude),
|
|
84
|
+
]),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
const got = BinaryPacker.unpack(target, raw)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`flags` is how an optional field takes no space when you have no value for it. `heading`, `speed`, and `altitude` are measurements, so `0` is still a value and has to be written. `None` means the field is not in the packet.
|
|
91
|
+
|
|
92
|
+
`motion` is always one byte in front of those fields. Bit 0 is `heading`, bit 1 is `speed`, bit 2 is `altitude`. A set bit writes that field next. A clear bit skips it.
|
|
93
|
+
|
|
94
|
+
| Field | Type | Bytes when present | Values |
|
|
95
|
+
|---|---|---|---|
|
|
96
|
+
| `heading` | `u16` | 2 | 0 … 65535 |
|
|
97
|
+
| `speed` | `u8` | 1 | 0 … 255 |
|
|
98
|
+
| `altitude` | `i16` | 2 | −32768 … 32767 |
|
|
99
|
+
|
|
100
|
+
In this example all three are `None`, so `motion` is `00` and those 5 bytes are absent. The packet is 13 bytes: type number 1, `sid` 2, `lat` 4, `lon` 4, `profile` 1, `motion` 1.
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
40 type
|
|
104
|
+
01 00 sid
|
|
105
|
+
00 65 cd 1d lat
|
|
106
|
+
00 a3 e1 11 lon
|
|
107
|
+
01 profile
|
|
108
|
+
00 motion
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`heading = 90` sets bit 0, so `motion` is `01` and `5a 00` follows it. `heading = 0` sets the same bit and writes `00 00`. `speed = 10` sets bit 1 and writes one byte. The present fields are written in the order listed, and only those.
|
|
112
|
+
|
|
113
|
+
## Example
|
|
114
|
+
|
|
115
|
+
C# → binary → Rust
|
|
116
|
+
|
|
117
|
+
### C#
|
|
118
|
+
|
|
119
|
+
```csharp
|
|
120
|
+
using Packbin;
|
|
121
|
+
|
|
122
|
+
sealed class User
|
|
123
|
+
{
|
|
124
|
+
public string Username { get; set; } = "";
|
|
125
|
+
public List<Role> Roles { get; set; } = [];
|
|
126
|
+
public Dictionary<string, ActionList> Access { get; set; } = [];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
sealed class Role { public string RoleName { get; set; } = ""; }
|
|
130
|
+
sealed class ActionName { public string Action { get; set; } = ""; }
|
|
131
|
+
sealed class ActionList { public List<ActionName> Actions { get; set; } = []; }
|
|
132
|
+
|
|
133
|
+
var userScheme = new Scheme<User>(1, f => [
|
|
134
|
+
f.Utf8(0, x => x.Username),
|
|
135
|
+
f.List(x => x.Roles, r => r.Utf8(0, role => role.RoleName)),
|
|
136
|
+
f.Dict(x => x.Access, e => e.List(a => a.Actions, n => n.Utf8(0, action => action.Action)))]);
|
|
137
|
+
|
|
138
|
+
var raw = BinaryPacker.Pack(userScheme, new User
|
|
139
|
+
{
|
|
140
|
+
Username = "ada",
|
|
141
|
+
Roles = [new Role { RoleName = "user" }, new Role { RoleName = "admin" }],
|
|
142
|
+
Access = new()
|
|
143
|
+
{
|
|
144
|
+
["map"] = new ActionList { Actions = [new ActionName { Action = "read" }, new ActionName { Action = "edit" }] },
|
|
145
|
+
["store"] = new ActionList { Actions = [new ActionName { Action = "write" }] },
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
01 03 00 61 64 61 02 00 04 00 75 73 65 72 05 00 61 64 6d 69 6e
|
|
152
|
+
02 00 03 00 6d 61 70 02 00 04 00 72 65 61 64 04 00 65 64 69 74
|
|
153
|
+
05 00 73 74 6f 72 65 01 00 05 00 77 72 69 74 65
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The first byte is the scheme type number.
|
|
157
|
+
|
|
158
|
+
### Rust
|
|
159
|
+
|
|
160
|
+
```rust
|
|
161
|
+
use packbin::{dict, list, unpack_map, utf8, MapScheme};
|
|
162
|
+
|
|
163
|
+
let user = MapScheme::new(
|
|
164
|
+
1,
|
|
165
|
+
vec![
|
|
166
|
+
utf8("username"),
|
|
167
|
+
list("roles", utf8("role")),
|
|
168
|
+
dict("access", list("actions", utf8("action"))),
|
|
169
|
+
],
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
let got = unpack_map(&user, &raw).unwrap();
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Data types
|
|
176
|
+
|
|
177
|
+
| Helper | What it writes |
|
|
178
|
+
|--------|----------------|
|
|
179
|
+
| `u8` `u16` `u32` `u64` | unsigned integer, little-endian |
|
|
180
|
+
| `i8` `i16` `i32` `i64` | signed integer, little-endian |
|
|
181
|
+
| `f32` `f64` | IEEE 754 float |
|
|
182
|
+
| `bytes(n)` | exactly `n` raw bytes |
|
|
183
|
+
| `be(field)` | that number, big-endian |
|
|
184
|
+
| `utf8` | UTF-8 string, `u16` length |
|
|
185
|
+
| `list` | `u16` count, then that many elements |
|
|
186
|
+
| `dict` | `u16` pair count; keys in unsigned byte order |
|
|
187
|
+
| `flags(name, fields)` | one `u8`; bit 0 is the first field; a clear bit omits that field |
|
|
188
|
+
| `when(eq(field, value), fields)` | the group only when an earlier field equals `value` |
|
|
189
|
+
| `repeat(fields)` | the group until the buffer ends |
|
|
190
|
+
|
|
191
|
+
[`_docs/01_solution/schema.md`](_docs/01_solution/schema.md)
|
|
192
|
+
|
|
193
|
+
## License
|
|
194
|
+
|
|
195
|
+
MIT
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
packbin/__init__.py,sha256=sVzv7vjrU0CgXTv_BKy9nygjRnV3t2USSh1fJF1tMJE,864
|
|
2
|
+
packbin/_errors.py,sha256=Gslv_uomJcsKsE_iCQneSf_-biZYgDZwbnbL-VxNKr8,1048
|
|
3
|
+
packbin/_nodes.py,sha256=sje7mm0mpkd93ZKw1R3iAvnuUdvMvaHZQ0cU_7-ygSM,7783
|
|
4
|
+
packbin/_pack.py,sha256=XeqiacohhxV6Ca3EZuffK0sQ-ZNgWoSXEZexE_l67wM,9536
|
|
5
|
+
packbin/_scheme.py,sha256=-TuzNgCeFr4nOvpOELENbrAMmeUv18mHnDPpThlXOyg,4655
|
|
6
|
+
packbin/_unpack.py,sha256=3kwQQhiBRLWmcoO6ji7RV9hbQ_gP5xOTXRq6eVS6w5A,12452
|
|
7
|
+
packbin-0.1.9.dist-info/METADATA,sha256=hFKlTkx9_p4nl8P02ILQzKRWHCCs8UntgxUcT_yRjlA,5382
|
|
8
|
+
packbin-0.1.9.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
9
|
+
packbin-0.1.9.dist-info/top_level.txt,sha256=dwrq4kGidAMiDQvoGgkmTpLcJe7ZvQdlxYHsLOPBiek,8
|
|
10
|
+
packbin-0.1.9.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
packbin
|