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 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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ packbin