sltcodec 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sltcodec/__init__.py +15 -0
- sltcodec/codec.py +428 -0
- sltcodec/types.py +359 -0
- sltcodec-0.2.0.dist-info/METADATA +233 -0
- sltcodec-0.2.0.dist-info/RECORD +7 -0
- sltcodec-0.2.0.dist-info/WHEEL +4 -0
- sltcodec-0.2.0.dist-info/licenses/LICENSE +21 -0
sltcodec/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from .codec import (decode, encode, encode_field, load_struct_def_dict,
|
|
2
|
+
save_struct_def_dict)
|
|
3
|
+
from .types import FieldDef, FieldInstance, StructDef, StructInstance
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"FieldDef",
|
|
7
|
+
"FieldInstance",
|
|
8
|
+
"StructDef",
|
|
9
|
+
"StructInstance",
|
|
10
|
+
"decode",
|
|
11
|
+
"encode",
|
|
12
|
+
"encode_field",
|
|
13
|
+
"load_struct_def_dict",
|
|
14
|
+
"save_struct_def_dict",
|
|
15
|
+
]
|
sltcodec/codec.py
ADDED
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
"""Encoding and decoding of structured layouts into bytearrays."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from sltcalc import SltEval
|
|
9
|
+
from sltcore import Info, InfoSize, bits_get, bits_set
|
|
10
|
+
|
|
11
|
+
from .types import FieldDef, FieldInstance, StructDef, StructInstance
|
|
12
|
+
|
|
13
|
+
_PRIMITIVE_TYPES = {
|
|
14
|
+
"bool",
|
|
15
|
+
"signed int",
|
|
16
|
+
"int",
|
|
17
|
+
"unsigned int",
|
|
18
|
+
"float",
|
|
19
|
+
"bytearray",
|
|
20
|
+
"bytes",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _as_field_defs(struct_def: StructDef | list[FieldDef]) -> list[FieldDef]:
|
|
25
|
+
"""Normalize StructDef/list inputs to a list of field definitions."""
|
|
26
|
+
if isinstance(struct_def, StructDef):
|
|
27
|
+
return struct_def.fields
|
|
28
|
+
return struct_def
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _validate_struct_instance(struct_instance: StructInstance) -> None:
|
|
32
|
+
"""Validate that encode input is a well-typed StructInstance."""
|
|
33
|
+
if not isinstance(struct_instance, StructInstance):
|
|
34
|
+
raise TypeError("encode() expects StructInstance for struct_instance")
|
|
35
|
+
for index, field_instance in enumerate(struct_instance.field_instances):
|
|
36
|
+
if not isinstance(field_instance, FieldInstance):
|
|
37
|
+
raise TypeError(
|
|
38
|
+
"encode() expects StructInstance.field_instances to be "
|
|
39
|
+
"list[FieldInstance]. "
|
|
40
|
+
f"Invalid item at index {index}: "
|
|
41
|
+
f"{type(field_instance).__name__}")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def save_struct_def_dict(path: str | Path,
|
|
45
|
+
struct_def_dict: dict[str, StructDef]) -> None:
|
|
46
|
+
"""Save a structure definition dictionary to a JSON file."""
|
|
47
|
+
payload = {
|
|
48
|
+
name: struct_def.to_dict()
|
|
49
|
+
for name, struct_def in struct_def_dict.items()
|
|
50
|
+
}
|
|
51
|
+
Path(path).write_text(json.dumps(payload, ensure_ascii=False, indent=2),
|
|
52
|
+
encoding="utf-8")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def load_struct_def_dict(path: str | Path) -> dict[str, StructDef]:
|
|
56
|
+
"""Load a structure definition dictionary from a JSON file."""
|
|
57
|
+
data = json.loads(Path(path).read_text(encoding="utf-8"))
|
|
58
|
+
return {
|
|
59
|
+
name: StructDef.from_dict(field_def_data)
|
|
60
|
+
for name, field_def_data in data.items()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _resolve_info_size(value: InfoSize | str, env: dict[str, Any]) -> InfoSize:
|
|
65
|
+
"""Resolve an InfoSize value that can be static or expression-based."""
|
|
66
|
+
if isinstance(value, str):
|
|
67
|
+
stleval = SltEval(env)
|
|
68
|
+
resolved_byte = stleval.eval(value)
|
|
69
|
+
return InfoSize(resolved_byte, 0)
|
|
70
|
+
return value
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _resolve_offset(field_def: FieldDef, env: dict[str, Any]) -> InfoSize:
|
|
74
|
+
"""Resolve a field offset that can be static or expression-based."""
|
|
75
|
+
return _resolve_info_size(field_def.offset, env)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _resolve_size(field_def: FieldDef, env: dict[str, Any]) -> InfoSize:
|
|
79
|
+
"""Resolve a field size that can be static or expression-based."""
|
|
80
|
+
return _resolve_info_size(field_def.size, env)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _is_padding_field_def(field_def: FieldDef) -> bool:
|
|
84
|
+
"""Check whether a field definition represents padding."""
|
|
85
|
+
return (field_def.name.startswith("padding[")
|
|
86
|
+
and field_def.type in ["bytes", "bytearray"])
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _info_size_to_bits(info_size: InfoSize) -> int:
|
|
90
|
+
"""Convert InfoSize to an absolute bit count."""
|
|
91
|
+
return info_size.byte * 8 + info_size.bit
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _info_size_from_bits(total_bits: int) -> InfoSize:
|
|
95
|
+
"""Create InfoSize from an absolute bit count."""
|
|
96
|
+
return InfoSize(total_bits >> 3, total_bits & 0x7)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _resolve_field_type(field_type: str | StructDef,
|
|
100
|
+
struct_def_dict: dict[str, StructDef] | None = None,
|
|
101
|
+
env: dict[str, Any] | None = None) -> str | StructDef:
|
|
102
|
+
"""Resolve a field type that can be primitive, named, or nested."""
|
|
103
|
+
if isinstance(field_type, StructDef):
|
|
104
|
+
return field_type
|
|
105
|
+
if field_type in _PRIMITIVE_TYPES:
|
|
106
|
+
return field_type
|
|
107
|
+
|
|
108
|
+
resolved_struct_def = _get_struct_def(field_type, struct_def_dict, env)
|
|
109
|
+
if isinstance(resolved_struct_def, StructDef):
|
|
110
|
+
return resolved_struct_def
|
|
111
|
+
if (isinstance(resolved_struct_def, str)
|
|
112
|
+
and resolved_struct_def in _PRIMITIVE_TYPES):
|
|
113
|
+
return resolved_struct_def
|
|
114
|
+
return field_type
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _encode_primitive(field_type: str, value: Any, size: InfoSize,
|
|
118
|
+
scale: float) -> Info:
|
|
119
|
+
"""Convert a primitive typed value into an Info object for bits_set."""
|
|
120
|
+
if field_type == "bool":
|
|
121
|
+
return Info.from_bool(bool(value), size, scale=scale)
|
|
122
|
+
if field_type == "signed int":
|
|
123
|
+
return Info.from_signed_int(int(value), size, scale=scale)
|
|
124
|
+
if field_type in ["int", "unsigned int"]:
|
|
125
|
+
return Info.from_unsigned_int(int(value), size, scale=scale)
|
|
126
|
+
if field_type == "float":
|
|
127
|
+
return Info.from_float(float(value), size, scale=scale)
|
|
128
|
+
if field_type in ["bytearray", "bytes"]:
|
|
129
|
+
return Info.from_bytes(bytes(value), size, scale=scale)
|
|
130
|
+
if isinstance(value, (bytes, bytearray)):
|
|
131
|
+
return Info.from_bytes(bytes(value), size, scale=scale)
|
|
132
|
+
if isinstance(value, bool):
|
|
133
|
+
return Info.from_bool(value, size, scale=scale)
|
|
134
|
+
if isinstance(value, int):
|
|
135
|
+
return Info.from_unsigned_int(value, size, scale=scale)
|
|
136
|
+
|
|
137
|
+
return Info(raw_value=value, info_size=size, scale=scale)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _prepare_field_info(
|
|
141
|
+
field_def: FieldDef,
|
|
142
|
+
value: Any,
|
|
143
|
+
env: dict[str, Any],
|
|
144
|
+
struct_def_dict: dict[str, StructDef] | None = None
|
|
145
|
+
) -> tuple[InfoSize, InfoSize, Info] | None:
|
|
146
|
+
"""Resolve a field definition into an offset, size, and info payload."""
|
|
147
|
+
offset = _resolve_offset(field_def, env)
|
|
148
|
+
size = _resolve_size(field_def, env)
|
|
149
|
+
if size.byte == 0 and size.bit == 0:
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
resolved_type = _resolve_field_type(field_def.type, struct_def_dict, env)
|
|
153
|
+
if isinstance(resolved_type, StructDef):
|
|
154
|
+
if not isinstance(value, StructInstance):
|
|
155
|
+
raise TypeError(
|
|
156
|
+
"Nested StructDef fields must be encoded with StructInstance "
|
|
157
|
+
"values")
|
|
158
|
+
nested_bytes = encode(value, bytearray(), struct_def_dict)
|
|
159
|
+
info = Info.from_bytes(bytes(nested_bytes), size, scale=field_def.scale)
|
|
160
|
+
else:
|
|
161
|
+
info = _encode_primitive(resolved_type, value, size, field_def.scale)
|
|
162
|
+
return offset, size, info
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _split_repeated_field(field_def: FieldDef,
|
|
166
|
+
index: int,
|
|
167
|
+
env: dict[str, Any],
|
|
168
|
+
offset: InfoSize | str | None = None) -> FieldDef:
|
|
169
|
+
"""Create a repeated-field definition with resolved offset and size."""
|
|
170
|
+
resolved_offset = (_resolve_offset(field_def, env)
|
|
171
|
+
if offset is None else offset)
|
|
172
|
+
resolved_size = _resolve_size(field_def, env)
|
|
173
|
+
return FieldDef(name=f"{field_def.name}[{index}]",
|
|
174
|
+
offset=resolved_offset,
|
|
175
|
+
size=resolved_size,
|
|
176
|
+
type=field_def.type,
|
|
177
|
+
scale=field_def.scale,
|
|
178
|
+
repeat=None,
|
|
179
|
+
description=field_def.description)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def encode_field(field_def: FieldDef,
|
|
183
|
+
value: Any,
|
|
184
|
+
buf: bytearray,
|
|
185
|
+
env: dict[str, Any] | None = None,
|
|
186
|
+
struct_def_dict: dict[str, StructDef] | None = None) -> None:
|
|
187
|
+
"""Encode a single field into a bytearray."""
|
|
188
|
+
if env is None:
|
|
189
|
+
env = {}
|
|
190
|
+
|
|
191
|
+
prepared = _prepare_field_info(field_def, value, env, struct_def_dict)
|
|
192
|
+
if prepared is None:
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
offset, size, info = prepared
|
|
196
|
+
required_bytes = (offset + size).bytes
|
|
197
|
+
if len(buf) < required_bytes:
|
|
198
|
+
buf.extend(b"\x00" * (required_bytes - len(buf)))
|
|
199
|
+
|
|
200
|
+
bits_set(buf, offset, info)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def encode(struct_instance: StructInstance,
|
|
204
|
+
buf: bytearray,
|
|
205
|
+
struct_def_dict: dict[str, StructDef] | None = None) -> bytearray:
|
|
206
|
+
"""Encode decode() result into a bytearray.
|
|
207
|
+
|
|
208
|
+
Parameters
|
|
209
|
+
----------
|
|
210
|
+
struct_instance : StructInstance
|
|
211
|
+
The structure instance to encode.
|
|
212
|
+
buf : bytearray
|
|
213
|
+
The base bytearray instance to write into.
|
|
214
|
+
struct_def_dict : dict[str, StructDef] | None, optional
|
|
215
|
+
A dictionary of structure definitions, by default None.
|
|
216
|
+
|
|
217
|
+
Returns
|
|
218
|
+
-------
|
|
219
|
+
bytearray
|
|
220
|
+
The encoded bytearray.
|
|
221
|
+
"""
|
|
222
|
+
_validate_struct_instance(struct_instance)
|
|
223
|
+
env: dict[str, Any] = {}
|
|
224
|
+
has_padding = False
|
|
225
|
+
|
|
226
|
+
for field_value in struct_instance.field_instances:
|
|
227
|
+
field_def = field_value.field_def
|
|
228
|
+
if _is_padding_field_def(field_def):
|
|
229
|
+
has_padding = True
|
|
230
|
+
value = field_value.value
|
|
231
|
+
if _is_padding_field_def(field_def):
|
|
232
|
+
padding_size = _resolve_size(field_def, env)
|
|
233
|
+
value = bytearray(padding_size.bytes)
|
|
234
|
+
if field_def.repeat is not None and field_def.repeat > 1:
|
|
235
|
+
current_offset = _resolve_offset(field_def, env)
|
|
236
|
+
resolved_size = _resolve_size(field_def, env)
|
|
237
|
+
values = list(value)
|
|
238
|
+
|
|
239
|
+
for i in range(field_def.repeat):
|
|
240
|
+
field_def_repeat = _split_repeated_field(
|
|
241
|
+
field_def, i, env, current_offset)
|
|
242
|
+
encode_field(field_def_repeat, values[i], buf, env,
|
|
243
|
+
struct_def_dict)
|
|
244
|
+
env[field_def_repeat.name] = values[i]
|
|
245
|
+
if isinstance(current_offset, InfoSize) and isinstance(
|
|
246
|
+
resolved_size, InfoSize):
|
|
247
|
+
current_offset += resolved_size
|
|
248
|
+
continue
|
|
249
|
+
|
|
250
|
+
encode_field(field_def, value, buf, env, struct_def_dict)
|
|
251
|
+
env[field_def.name] = value
|
|
252
|
+
|
|
253
|
+
if has_padding and struct_instance.size.bytes > len(buf):
|
|
254
|
+
buf.extend(b"\x00" * (struct_instance.size.bytes - len(buf)))
|
|
255
|
+
|
|
256
|
+
return buf
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def decode_field(
|
|
260
|
+
field_def: FieldDef,
|
|
261
|
+
data: bytearray | bytes,
|
|
262
|
+
env: dict[str, Any] | None = None,
|
|
263
|
+
struct_def_dict: dict[str, StructDef] | None = None
|
|
264
|
+
) -> FieldInstance | None:
|
|
265
|
+
"""Decode a single field from a bytearray according to a field definition.
|
|
266
|
+
|
|
267
|
+
Parameters
|
|
268
|
+
----------
|
|
269
|
+
field_def : FieldDef
|
|
270
|
+
The definition of the field to decode.
|
|
271
|
+
data : bytearray | bytes
|
|
272
|
+
The data to decode.
|
|
273
|
+
env : dict[str, Any] | None, optional
|
|
274
|
+
The environment for evaluating expressions, by default None.
|
|
275
|
+
struct_def_dict : dict[str, StructDef] | None, optional
|
|
276
|
+
A dictionary of structure definitions, by default None.
|
|
277
|
+
Returns
|
|
278
|
+
-------
|
|
279
|
+
FieldInstance | None
|
|
280
|
+
The decoded field instance, or None when size is zero.
|
|
281
|
+
"""
|
|
282
|
+
if env is None:
|
|
283
|
+
env = {}
|
|
284
|
+
offset = _resolve_offset(field_def, env)
|
|
285
|
+
size = _resolve_size(field_def, env)
|
|
286
|
+
if size.byte == 0 and size.bit == 0:
|
|
287
|
+
return None
|
|
288
|
+
info = bits_get(data, offset, size, scale=field_def.scale)
|
|
289
|
+
resolved_type = _resolve_field_type(field_def.type, struct_def_dict, env)
|
|
290
|
+
if isinstance(resolved_type, StructDef):
|
|
291
|
+
return FieldInstance(
|
|
292
|
+
field_def=field_def,
|
|
293
|
+
value=decode(resolved_type, bytearray(info.to_bytes),
|
|
294
|
+
struct_def_dict),
|
|
295
|
+
)
|
|
296
|
+
if resolved_type == "bool":
|
|
297
|
+
return FieldInstance(field_def=field_def, value=info.to_bool)
|
|
298
|
+
if resolved_type == "signed int":
|
|
299
|
+
return FieldInstance(field_def=field_def, value=info.to_signed_int)
|
|
300
|
+
if resolved_type in ["int", "unsigned int"]:
|
|
301
|
+
return FieldInstance(field_def=field_def, value=info.to_unsigned_int)
|
|
302
|
+
if resolved_type == "float":
|
|
303
|
+
return FieldInstance(field_def=field_def, value=info.to_float)
|
|
304
|
+
if resolved_type in ["bytearray", "bytes"]:
|
|
305
|
+
return FieldInstance(field_def=field_def, value=info.to_bytes)
|
|
306
|
+
return FieldInstance(field_def=field_def, value=info.raw_value)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def decode(
|
|
310
|
+
struct_def: StructDef | list[FieldDef],
|
|
311
|
+
data: bytearray | bytes,
|
|
312
|
+
struct_def_dict: dict[str, StructDef] | None = None) -> StructInstance:
|
|
313
|
+
"""Decode a bytearray into field values according to a layout.
|
|
314
|
+
|
|
315
|
+
Parameters
|
|
316
|
+
----------
|
|
317
|
+
struct_def : StructDef | list[FieldDef]
|
|
318
|
+
The definitions of the fields to decode.
|
|
319
|
+
data : bytearray | bytes
|
|
320
|
+
The data to decode.
|
|
321
|
+
struct_def_dict : dict[str, StructDef] | None, optional
|
|
322
|
+
A dictionary of structure definitions, by default None.
|
|
323
|
+
Returns
|
|
324
|
+
-------
|
|
325
|
+
StructInstance
|
|
326
|
+
The decoded structure instance.
|
|
327
|
+
"""
|
|
328
|
+
env = {}
|
|
329
|
+
struct_def_obj = (struct_def if isinstance(struct_def, StructDef) else
|
|
330
|
+
StructDef(fields=struct_def))
|
|
331
|
+
result = StructInstance(struct_def=struct_def_obj)
|
|
332
|
+
current_position = InfoSize(0, 0)
|
|
333
|
+
padding_index = 0
|
|
334
|
+
|
|
335
|
+
def append_padding_until(target_offset: InfoSize) -> None:
|
|
336
|
+
nonlocal current_position, padding_index
|
|
337
|
+
start_bits = _info_size_to_bits(current_position)
|
|
338
|
+
end_bits = _info_size_to_bits(target_offset)
|
|
339
|
+
if end_bits <= start_bits:
|
|
340
|
+
return
|
|
341
|
+
|
|
342
|
+
chunk_start_bits = start_bits
|
|
343
|
+
while chunk_start_bits < end_bits:
|
|
344
|
+
next_boundary_bits = ((chunk_start_bits // 32) + 1) * 32
|
|
345
|
+
chunk_end_bits = min(end_bits, next_boundary_bits)
|
|
346
|
+
padding_field_def = FieldDef(
|
|
347
|
+
name=f"padding[{padding_index}]",
|
|
348
|
+
offset=_info_size_from_bits(chunk_start_bits),
|
|
349
|
+
size=_info_size_from_bits(chunk_end_bits - chunk_start_bits),
|
|
350
|
+
type="bytes",
|
|
351
|
+
description="Auto-generated padding",
|
|
352
|
+
)
|
|
353
|
+
padding_field_instance = decode_field(padding_field_def, data)
|
|
354
|
+
if padding_field_instance is not None:
|
|
355
|
+
result.append_field_instance(padding_field_instance)
|
|
356
|
+
padding_index += 1
|
|
357
|
+
chunk_start_bits = chunk_end_bits
|
|
358
|
+
|
|
359
|
+
current_position = target_offset
|
|
360
|
+
|
|
361
|
+
for field_def in _as_field_defs(struct_def_obj):
|
|
362
|
+
# Handle non-repeated fields
|
|
363
|
+
if field_def.repeat is None or field_def.repeat <= 1:
|
|
364
|
+
resolved_offset = _resolve_offset(field_def, env)
|
|
365
|
+
append_padding_until(resolved_offset)
|
|
366
|
+
field_instance = decode_field(field_def, data, env, struct_def_dict)
|
|
367
|
+
if field_instance is not None:
|
|
368
|
+
env[field_instance.field_def.name] = field_instance.value
|
|
369
|
+
result.append_field_instance(field_instance)
|
|
370
|
+
current_position = (resolved_offset +
|
|
371
|
+
_resolve_size(field_def, env))
|
|
372
|
+
continue
|
|
373
|
+
# Handle repeated fields
|
|
374
|
+
current_offset = _resolve_offset(field_def, env)
|
|
375
|
+
resolved_size = _resolve_size(field_def, env)
|
|
376
|
+
append_padding_until(current_offset)
|
|
377
|
+
for i in range(field_def.repeat):
|
|
378
|
+
field_def_repeat = _split_repeated_field(field_def, i, env,
|
|
379
|
+
current_offset)
|
|
380
|
+
field_instance = decode_field(field_def_repeat, data, env,
|
|
381
|
+
struct_def_dict)
|
|
382
|
+
if field_instance is not None:
|
|
383
|
+
env[field_instance.field_def.name] = field_instance.value
|
|
384
|
+
result.append_field_instance(field_instance)
|
|
385
|
+
current_position = current_offset + resolved_size
|
|
386
|
+
current_offset += resolved_size
|
|
387
|
+
|
|
388
|
+
append_padding_until(result.size)
|
|
389
|
+
|
|
390
|
+
return result
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _get_struct_def(
|
|
394
|
+
struct_def_name: str,
|
|
395
|
+
struct_def_dict: dict[str, StructDef] | None = None,
|
|
396
|
+
env: dict[str, Any] | None = None) -> StructDef | str | None:
|
|
397
|
+
"""Get a structure definition from a dictionary or evaluate it.
|
|
398
|
+
|
|
399
|
+
Parameters
|
|
400
|
+
----------
|
|
401
|
+
struct_def_name : str
|
|
402
|
+
The name of the structure definition to get.
|
|
403
|
+
struct_def_dict : dict[str, StructDef] | None, optional
|
|
404
|
+
A dictionary of structure definitions, by default None.
|
|
405
|
+
env : dict[str, Any] | None, optional
|
|
406
|
+
The environment for evaluating the structure definition,
|
|
407
|
+
by default None.
|
|
408
|
+
|
|
409
|
+
Returns
|
|
410
|
+
-------
|
|
411
|
+
StructDef | str | None
|
|
412
|
+
The structure definition, primitive type, or None if not found.
|
|
413
|
+
"""
|
|
414
|
+
if struct_def_dict and struct_def_name in struct_def_dict:
|
|
415
|
+
return struct_def_dict[struct_def_name]
|
|
416
|
+
|
|
417
|
+
stleval = SltEval(env)
|
|
418
|
+
try:
|
|
419
|
+
eval_result = stleval.eval(struct_def_name)
|
|
420
|
+
except (SyntaxError, NameError, TypeError, ValueError):
|
|
421
|
+
return None
|
|
422
|
+
|
|
423
|
+
if isinstance(eval_result, str) and eval_result in _PRIMITIVE_TYPES:
|
|
424
|
+
return eval_result
|
|
425
|
+
if (isinstance(eval_result, str) and struct_def_dict
|
|
426
|
+
and eval_result in struct_def_dict):
|
|
427
|
+
return struct_def_dict[eval_result]
|
|
428
|
+
return None
|
sltcodec/types.py
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
"""Type definitions for structured layout metadata."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from sltcore import Info, InfoSize
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class FieldDef:
|
|
13
|
+
"""A field in a structured layout."""
|
|
14
|
+
name: str = field(default_factory=str,
|
|
15
|
+
metadata={"desc": "The name of the field"})
|
|
16
|
+
offset: InfoSize | str = field(default_factory=InfoSize,
|
|
17
|
+
metadata={"desc": "The offset of the field"})
|
|
18
|
+
size: InfoSize | str = field(default_factory=InfoSize,
|
|
19
|
+
metadata={"desc": "The size of the field"})
|
|
20
|
+
type: str | "StructDef" = field(default_factory=str,
|
|
21
|
+
metadata={"desc": "The type of the field"})
|
|
22
|
+
scale: float = field(default=1.0,
|
|
23
|
+
metadata={"desc": "The scale of the field"})
|
|
24
|
+
repeat: int | None = field(
|
|
25
|
+
default=None, metadata={"desc": "The repeat count of the field"})
|
|
26
|
+
description: str | None = field(
|
|
27
|
+
default=None, metadata={"desc": "The description of the field"})
|
|
28
|
+
|
|
29
|
+
def split_repeat(self,
|
|
30
|
+
index: int,
|
|
31
|
+
offset: InfoSize | str | None = None) -> "FieldDef":
|
|
32
|
+
"""Create a single repeated-field definition for the given index."""
|
|
33
|
+
return FieldDef(name=f"{self.name}[{index}]",
|
|
34
|
+
offset=self.offset if offset is None else offset,
|
|
35
|
+
size=self.size,
|
|
36
|
+
type=self.type,
|
|
37
|
+
scale=self.scale,
|
|
38
|
+
repeat=None,
|
|
39
|
+
description=self.description)
|
|
40
|
+
|
|
41
|
+
def __lt__(self, other: object) -> bool:
|
|
42
|
+
"""Compare field definitions using a stable serialized sort key."""
|
|
43
|
+
if not isinstance(other, FieldDef):
|
|
44
|
+
return NotImplemented
|
|
45
|
+
return self._sort_key() < other._sort_key()
|
|
46
|
+
|
|
47
|
+
def to_dict(self) -> dict[str, Any]:
|
|
48
|
+
"""Convert this field definition to a JSON-serializable dictionary."""
|
|
49
|
+
return {
|
|
50
|
+
"name": self.name,
|
|
51
|
+
"offset": self._serialize_value(self.offset),
|
|
52
|
+
"size": self._serialize_value(self.size),
|
|
53
|
+
"type": self._serialize_type(self.type),
|
|
54
|
+
"scale": self.scale,
|
|
55
|
+
"repeat": self.repeat,
|
|
56
|
+
"description": self.description,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
def to_json(self) -> str:
|
|
60
|
+
"""Convert this field definition to a JSON string."""
|
|
61
|
+
return json.dumps(self.to_dict(), ensure_ascii=False, indent=2)
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def from_dict(cls, data: dict[str, Any]) -> "FieldDef":
|
|
65
|
+
"""Create a field definition from a JSON-serializable dictionary."""
|
|
66
|
+
return cls(
|
|
67
|
+
name=data.get("name", ""),
|
|
68
|
+
offset=cls._deserialize_value(data.get("offset")),
|
|
69
|
+
size=cls._deserialize_value(data.get("size")),
|
|
70
|
+
type=cls._deserialize_type(data.get("type")),
|
|
71
|
+
scale=data.get("scale", 1.0),
|
|
72
|
+
repeat=data.get("repeat"),
|
|
73
|
+
description=data.get("description"),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def from_json(cls, data: str) -> "FieldDef":
|
|
78
|
+
"""Create a field definition from a JSON string."""
|
|
79
|
+
return cls.from_dict(json.loads(data))
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
def _serialize_value(value: Any) -> Any:
|
|
83
|
+
if isinstance(value, Info):
|
|
84
|
+
return {
|
|
85
|
+
"__type__": "Info",
|
|
86
|
+
"value": value.to_json(),
|
|
87
|
+
}
|
|
88
|
+
if isinstance(value, InfoSize):
|
|
89
|
+
return {
|
|
90
|
+
"__type__": "InfoSize",
|
|
91
|
+
"value": value.to_json(),
|
|
92
|
+
}
|
|
93
|
+
return value
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
def _deserialize_value(cls, value: Any) -> Any:
|
|
97
|
+
if isinstance(value, dict) and value.get("__type__") == "Info":
|
|
98
|
+
return Info.from_json(value["value"])
|
|
99
|
+
if isinstance(value, dict) and value.get("__type__") == "InfoSize":
|
|
100
|
+
return InfoSize.from_json(value["value"])
|
|
101
|
+
return value
|
|
102
|
+
|
|
103
|
+
@staticmethod
|
|
104
|
+
def _serialize_type(value: Any) -> Any:
|
|
105
|
+
if isinstance(value, StructDef):
|
|
106
|
+
return {
|
|
107
|
+
"__type__": "StructDef",
|
|
108
|
+
"fields": value.to_dict(),
|
|
109
|
+
}
|
|
110
|
+
if isinstance(value, list):
|
|
111
|
+
# Backward-compatibility for legacy list-based nested types.
|
|
112
|
+
return {
|
|
113
|
+
"__type__": "StructDef",
|
|
114
|
+
"fields": [field.to_dict() for field in value],
|
|
115
|
+
}
|
|
116
|
+
return value
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def _deserialize_type(cls, value: Any) -> Any:
|
|
120
|
+
if isinstance(value, dict) and value.get("__type__") == "StructDef":
|
|
121
|
+
return StructDef.from_dict(value["fields"])
|
|
122
|
+
if isinstance(value, list):
|
|
123
|
+
# Backward-compatibility for legacy list-based nested types.
|
|
124
|
+
return StructDef.from_dict(value)
|
|
125
|
+
return value
|
|
126
|
+
|
|
127
|
+
def _sort_key(self) -> tuple[Any, ...]:
|
|
128
|
+
"""Build a stable comparison key for ordering field definitions."""
|
|
129
|
+
return (
|
|
130
|
+
self._sortable_info_size_or_expr(self.offset),
|
|
131
|
+
self._sortable_info_size_or_expr(self.size),
|
|
132
|
+
self.name,
|
|
133
|
+
self._sortable_type(self.type),
|
|
134
|
+
self.scale,
|
|
135
|
+
-1 if self.repeat is None else self.repeat,
|
|
136
|
+
"" if self.description is None else self.description,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
@staticmethod
|
|
140
|
+
def _sortable_info_size_or_expr(value: InfoSize | str) -> tuple[Any, ...]:
|
|
141
|
+
"""Build a comparable key for InfoSize-or-expression values."""
|
|
142
|
+
if isinstance(value, InfoSize):
|
|
143
|
+
return (0, value.byte, value.bit)
|
|
144
|
+
return (1, value)
|
|
145
|
+
|
|
146
|
+
@staticmethod
|
|
147
|
+
def _sortable_type(value: str | "StructDef") -> tuple[Any, ...]:
|
|
148
|
+
"""Build a comparable key for primitive or nested field types."""
|
|
149
|
+
if isinstance(value, StructDef):
|
|
150
|
+
return (0, json.dumps(value.to_dict(), sort_keys=True))
|
|
151
|
+
return (1, value)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@dataclass(frozen=True)
|
|
155
|
+
class FieldInstance:
|
|
156
|
+
"""A decoded/encodable field value with its field definition."""
|
|
157
|
+
field_def: FieldDef = field(metadata={"desc": "The field definition"})
|
|
158
|
+
value: Any = field(metadata={"desc": "The decoded/encodable value"})
|
|
159
|
+
is_padding: bool = field(
|
|
160
|
+
default=False,
|
|
161
|
+
metadata={"desc": "Whether this field instance represents padding"},
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
def __lt__(self, other: object) -> bool:
|
|
165
|
+
"""Compare field instances using their field definition order."""
|
|
166
|
+
if not isinstance(other, FieldInstance):
|
|
167
|
+
return NotImplemented
|
|
168
|
+
return self.field_def < other.field_def
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@dataclass(frozen=True)
|
|
172
|
+
class StructDef:
|
|
173
|
+
"""A structured layout definition that groups multiple fields."""
|
|
174
|
+
name: str = field(default_factory=str,
|
|
175
|
+
metadata={"desc": "The name of the structure"})
|
|
176
|
+
description: str = field(
|
|
177
|
+
default_factory=str,
|
|
178
|
+
metadata={"desc": "The description of the structure"},
|
|
179
|
+
)
|
|
180
|
+
fields: list[FieldDef] = field(
|
|
181
|
+
default_factory=list,
|
|
182
|
+
metadata={"desc": "The fields of the structure"},
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def to_dict(self) -> dict[str, Any]:
|
|
186
|
+
"""Convert this structure definition to JSON-serializable data."""
|
|
187
|
+
return {
|
|
188
|
+
"name": self.name,
|
|
189
|
+
"description": self.description,
|
|
190
|
+
"fields": [field_def.to_dict() for field_def in self.fields],
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
def to_json(self) -> str:
|
|
194
|
+
"""Convert this structure definition to a JSON string."""
|
|
195
|
+
return json.dumps(self.to_dict(), ensure_ascii=False, indent=2)
|
|
196
|
+
|
|
197
|
+
@classmethod
|
|
198
|
+
def from_dict(
|
|
199
|
+
cls,
|
|
200
|
+
data: dict[str, Any] | list[dict[str, Any]],
|
|
201
|
+
) -> "StructDef":
|
|
202
|
+
"""Create a structure definition from JSON-serializable data."""
|
|
203
|
+
if isinstance(data, list):
|
|
204
|
+
# Backward-compatibility for legacy list-only StructDef payloads.
|
|
205
|
+
return cls(fields=[FieldDef.from_dict(item) for item in data])
|
|
206
|
+
return cls(name=data.get("name", ""),
|
|
207
|
+
description=data.get("description", ""),
|
|
208
|
+
fields=[
|
|
209
|
+
FieldDef.from_dict(item)
|
|
210
|
+
for item in data.get("fields", [])
|
|
211
|
+
])
|
|
212
|
+
|
|
213
|
+
@classmethod
|
|
214
|
+
def from_json(cls, data: str) -> "StructDef":
|
|
215
|
+
"""Create a structure definition from a JSON string."""
|
|
216
|
+
return cls.from_dict(json.loads(data))
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@dataclass
|
|
220
|
+
class StructInstance:
|
|
221
|
+
"""A decoded/encodable structure instance."""
|
|
222
|
+
struct_def: StructDef = field(
|
|
223
|
+
default_factory=StructDef,
|
|
224
|
+
metadata={"desc": "The structure definition"},
|
|
225
|
+
)
|
|
226
|
+
field_instances: list[FieldInstance] = field(
|
|
227
|
+
default_factory=list,
|
|
228
|
+
metadata={"desc": "The decoded/encodable field instances"},
|
|
229
|
+
)
|
|
230
|
+
size: InfoSize = field(
|
|
231
|
+
default_factory=InfoSize,
|
|
232
|
+
metadata={"desc": "The total size of the structure instance"},
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
def __post_init__(self) -> None:
|
|
236
|
+
"""Normalize stored field instances to sorted order."""
|
|
237
|
+
self._sort_field_instances()
|
|
238
|
+
self._update_size()
|
|
239
|
+
self._rebuild_padding_field_instances()
|
|
240
|
+
|
|
241
|
+
def append_field_instance(self, field_instance: FieldInstance) -> None:
|
|
242
|
+
"""Append one field instance to this structure instance."""
|
|
243
|
+
if not isinstance(field_instance, FieldInstance):
|
|
244
|
+
raise TypeError("field_instance must be FieldInstance")
|
|
245
|
+
self.field_instances.append(field_instance)
|
|
246
|
+
self._update_size()
|
|
247
|
+
self._rebuild_padding_field_instances()
|
|
248
|
+
|
|
249
|
+
def extend_field_instances(self,
|
|
250
|
+
field_instances: list[FieldInstance]) -> None:
|
|
251
|
+
"""Append multiple field instances to this structure instance."""
|
|
252
|
+
for field_instance in field_instances:
|
|
253
|
+
if not isinstance(field_instance, FieldInstance):
|
|
254
|
+
raise TypeError("field_instance must be FieldInstance")
|
|
255
|
+
self.field_instances.extend(field_instances)
|
|
256
|
+
self._update_size()
|
|
257
|
+
self._rebuild_padding_field_instances()
|
|
258
|
+
|
|
259
|
+
def __iter__(self):
|
|
260
|
+
"""Iterate over stored field instances."""
|
|
261
|
+
return iter(self.field_instances)
|
|
262
|
+
|
|
263
|
+
def __len__(self) -> int:
|
|
264
|
+
"""Return the number of stored field instances."""
|
|
265
|
+
return len(self.field_instances)
|
|
266
|
+
|
|
267
|
+
def __getitem__(self, index: int) -> FieldInstance:
|
|
268
|
+
"""Return one field instance by index."""
|
|
269
|
+
return self.field_instances[index]
|
|
270
|
+
|
|
271
|
+
def _sort_field_instances(self) -> None:
|
|
272
|
+
"""Keep field instances sorted by FieldDef order."""
|
|
273
|
+
self.field_instances.sort()
|
|
274
|
+
|
|
275
|
+
def _rebuild_padding_field_instances(self) -> None:
|
|
276
|
+
"""Rebuild padding field instances for gaps between stored values."""
|
|
277
|
+
non_padding_instances = [
|
|
278
|
+
field_instance for field_instance in self.field_instances
|
|
279
|
+
if not field_instance.is_padding
|
|
280
|
+
]
|
|
281
|
+
non_padding_instances.sort()
|
|
282
|
+
|
|
283
|
+
rebuilt_instances: list[FieldInstance] = []
|
|
284
|
+
current_offset = InfoSize(0, 0)
|
|
285
|
+
padding_index = 0
|
|
286
|
+
|
|
287
|
+
for field_instance in non_padding_instances:
|
|
288
|
+
field_def = field_instance.field_def
|
|
289
|
+
field_offset = self._resolve_field_offset(field_def)
|
|
290
|
+
field_size = self._resolve_field_size(field_def)
|
|
291
|
+
if field_offset > current_offset:
|
|
292
|
+
gap_size = field_offset - current_offset
|
|
293
|
+
if gap_size.byte > 0 or gap_size.bit > 0:
|
|
294
|
+
rebuilt_instances.append(
|
|
295
|
+
FieldInstance(
|
|
296
|
+
field_def=FieldDef(
|
|
297
|
+
name=f"padding[{padding_index}]",
|
|
298
|
+
offset=current_offset,
|
|
299
|
+
size=gap_size,
|
|
300
|
+
type="bytes",
|
|
301
|
+
description="Auto-generated padding",
|
|
302
|
+
),
|
|
303
|
+
value=bytearray(gap_size.byte),
|
|
304
|
+
is_padding=True,
|
|
305
|
+
))
|
|
306
|
+
padding_index += 1
|
|
307
|
+
rebuilt_instances.append(field_instance)
|
|
308
|
+
current_offset = field_offset + field_size
|
|
309
|
+
|
|
310
|
+
if self.size > current_offset:
|
|
311
|
+
gap_size = self.size - current_offset
|
|
312
|
+
if gap_size.byte > 0 or gap_size.bit > 0:
|
|
313
|
+
rebuilt_instances.append(
|
|
314
|
+
FieldInstance(
|
|
315
|
+
field_def=FieldDef(
|
|
316
|
+
name=f"padding[{padding_index}]",
|
|
317
|
+
offset=current_offset,
|
|
318
|
+
size=gap_size,
|
|
319
|
+
type="bytes",
|
|
320
|
+
description="Auto-generated padding",
|
|
321
|
+
),
|
|
322
|
+
value=bytearray(gap_size.byte),
|
|
323
|
+
is_padding=True,
|
|
324
|
+
))
|
|
325
|
+
|
|
326
|
+
self.field_instances = rebuilt_instances
|
|
327
|
+
self._sort_field_instances()
|
|
328
|
+
self._update_size()
|
|
329
|
+
|
|
330
|
+
def _update_size(self) -> None:
|
|
331
|
+
"""Update the instance size from the current field layout."""
|
|
332
|
+
if not self.field_instances:
|
|
333
|
+
return
|
|
334
|
+
|
|
335
|
+
max_end_offset = InfoSize(0, 0)
|
|
336
|
+
for field_instance in self.field_instances:
|
|
337
|
+
field_def = field_instance.field_def
|
|
338
|
+
field_offset = self._resolve_field_offset(field_def)
|
|
339
|
+
field_size = self._resolve_field_size(field_def)
|
|
340
|
+
field_end = field_offset + field_size
|
|
341
|
+
if field_end > max_end_offset:
|
|
342
|
+
max_end_offset = field_end
|
|
343
|
+
|
|
344
|
+
if max_end_offset > self.size:
|
|
345
|
+
self.size = max_end_offset
|
|
346
|
+
|
|
347
|
+
@staticmethod
|
|
348
|
+
def _resolve_field_offset(field_def: FieldDef) -> InfoSize:
|
|
349
|
+
"""Resolve field offsets to InfoSize values when possible."""
|
|
350
|
+
if isinstance(field_def.offset, InfoSize):
|
|
351
|
+
return field_def.offset
|
|
352
|
+
return InfoSize(0, 0)
|
|
353
|
+
|
|
354
|
+
@staticmethod
|
|
355
|
+
def _resolve_field_size(field_def: FieldDef) -> InfoSize:
|
|
356
|
+
"""Resolve field sizes to InfoSize values when possible."""
|
|
357
|
+
if isinstance(field_def.size, InfoSize):
|
|
358
|
+
return field_def.size
|
|
359
|
+
return InfoSize(0, 0)
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sltcodec
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Decode and encode bytearrays according to struct layout definitions using sltcore.
|
|
5
|
+
Project-URL: Homepage, https://github.com/fangface-hub/StructLayoutToolkitCodec
|
|
6
|
+
Project-URL: Documentation, https://readthedocs.org
|
|
7
|
+
Project-URL: Repository, https://github.com/fangface-hub/StructLayoutToolkitCodec
|
|
8
|
+
Project-URL: Issues, https://github.com/fangface-hub/StructLayoutToolkitCodec/issues
|
|
9
|
+
Author: fangface
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: byte,deserialization,layout,serialization,struct
|
|
13
|
+
Requires-Python: >=3.12
|
|
14
|
+
Requires-Dist: sltcalc>=0.1.0
|
|
15
|
+
Requires-Dist: sltcore>=1.3.0
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# StructLayoutToolkitCodec
|
|
19
|
+
|
|
20
|
+
sltcodec is a small package for decoding and encoding bytearrays according to structured layout definitions. It uses sltcore for bit-level access and provides a simple API for turning a layout definition into binary data and back again.
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install sltcodec
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick Example
|
|
29
|
+
|
|
30
|
+
`FieldDef.description` is an optional human-readable note that can be attached to each field definition.
|
|
31
|
+
|
|
32
|
+
`decode` returns a `StructInstance`.
|
|
33
|
+
`encode` accepts a `StructInstance` and a destination `bytearray`.
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from sltcore import InfoSize
|
|
37
|
+
from sltcodec import (FieldDef, FieldInstance, StructDef, StructInstance,
|
|
38
|
+
decode, encode)
|
|
39
|
+
|
|
40
|
+
struct_def = [
|
|
41
|
+
FieldDef(name="flag",
|
|
42
|
+
offset=InfoSize(0, 0),
|
|
43
|
+
size=InfoSize(0, 1),
|
|
44
|
+
type="bool",
|
|
45
|
+
scale=1.0,
|
|
46
|
+
description="Whether the feature is enabled"),
|
|
47
|
+
FieldDef(name="value",
|
|
48
|
+
offset=InfoSize(0, 1),
|
|
49
|
+
size=InfoSize(1, 0),
|
|
50
|
+
type="unsigned int",
|
|
51
|
+
scale=1.0,
|
|
52
|
+
description="The encoded numeric payload"),
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
encoded = encode(
|
|
56
|
+
StructInstance(
|
|
57
|
+
struct_def=StructDef(fields=struct_def),
|
|
58
|
+
field_instances=[
|
|
59
|
+
FieldInstance(struct_def[0], True),
|
|
60
|
+
FieldInstance(struct_def[1], 0xA5),
|
|
61
|
+
],
|
|
62
|
+
),
|
|
63
|
+
bytearray(),
|
|
64
|
+
)
|
|
65
|
+
decoded = decode(struct_def, encoded)
|
|
66
|
+
|
|
67
|
+
print(encoded)
|
|
68
|
+
print(decoded)
|
|
69
|
+
print(decoded.field_instances)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Output:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
bytearray(b"\xd2\x80")
|
|
76
|
+
StructInstance(struct_def=StructDef(...), field_instances=[...])
|
|
77
|
+
[FieldInstance(field_def=FieldDef(name='flag', offset=InfoSize(byte=0, bit=0), size=InfoSize(byte=0, bit=1), type='bool', scale=1.0, repeat=None), value=True),
|
|
78
|
+
FieldInstance(field_def=FieldDef(name='value', offset=InfoSize(byte=0, bit=1), size=InfoSize(byte=1, bit=0), type='unsigned int', scale=1.0, repeat=None), value=165)]
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Nested Field Types
|
|
83
|
+
|
|
84
|
+
`FieldDef.type` can be a `StructDef`, not only a primitive type name.
|
|
85
|
+
This allows you to define an inline nested structure and keep `encode_field` / `decode_field` behavior symmetrical.
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from sltcore import InfoSize
|
|
89
|
+
from sltcodec import FieldDef, FieldInstance, StructDef, StructInstance, decode, encode
|
|
90
|
+
|
|
91
|
+
child_struct_def = [
|
|
92
|
+
FieldDef(name="left", offset=InfoSize(0, 0), size=InfoSize(1, 0), type="unsigned int"),
|
|
93
|
+
FieldDef(name="right", offset=InfoSize(1, 0), size=InfoSize(1, 0), type="unsigned int"),
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
parent_field_def = FieldDef(
|
|
97
|
+
name="pair",
|
|
98
|
+
offset=InfoSize(0, 0),
|
|
99
|
+
size=InfoSize(2, 0),
|
|
100
|
+
type=StructDef(
|
|
101
|
+
name="Pair",
|
|
102
|
+
description="Two adjacent unsigned ints",
|
|
103
|
+
fields=child_struct_def,
|
|
104
|
+
),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
encoded = encode(
|
|
108
|
+
StructInstance(
|
|
109
|
+
struct_def=StructDef(fields=[parent_field_def]),
|
|
110
|
+
field_instances=[
|
|
111
|
+
FieldInstance(
|
|
112
|
+
parent_field_def,
|
|
113
|
+
StructInstance(
|
|
114
|
+
struct_def=StructDef(fields=child_struct_def),
|
|
115
|
+
field_instances=[
|
|
116
|
+
FieldInstance(child_struct_def[0], 3),
|
|
117
|
+
FieldInstance(child_struct_def[1], 4),
|
|
118
|
+
],
|
|
119
|
+
),
|
|
120
|
+
)
|
|
121
|
+
],
|
|
122
|
+
),
|
|
123
|
+
bytearray(),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
decoded = decode([parent_field_def], encoded)
|
|
127
|
+
print(encoded)
|
|
128
|
+
print(decoded)
|
|
129
|
+
print(decoded.field_instances)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Output:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
bytearray(b"\x03\x04")
|
|
136
|
+
StructInstance(struct_def=StructDef(...), field_instances=[...])
|
|
137
|
+
[FieldInstance(field_def=FieldDef(name='pair', offset=InfoSize(byte=0, bit=0), size=InfoSize(byte=2, bit=0), type=[...], scale=1.0, repeat=None),
|
|
138
|
+
value=StructInstance(struct_def=StructDef(...), field_instances=[...]))]
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Expression-Based Type Selection
|
|
142
|
+
|
|
143
|
+
`FieldDef.type` can also be an expression string.
|
|
144
|
+
The expression is evaluated with previously processed field values in scope, so you can switch decoding/encoding type dynamically.
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
from sltcore import InfoSize
|
|
148
|
+
from sltcodec import (FieldDef, FieldInstance, StructDef, StructInstance,
|
|
149
|
+
decode, encode)
|
|
150
|
+
|
|
151
|
+
struct_def = [
|
|
152
|
+
FieldDef(name="kind", offset=InfoSize(0, 0), size=InfoSize(1, 0), type="unsigned int"),
|
|
153
|
+
FieldDef(
|
|
154
|
+
name="payload",
|
|
155
|
+
offset=InfoSize(1, 0),
|
|
156
|
+
size="{1: 1, 2: 4}[kind]",
|
|
157
|
+
type="{1: 'int', 2: 'float'}[kind]",
|
|
158
|
+
),
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
# kind=1 -> payload is int (1 byte)
|
|
162
|
+
encoded_int = encode(
|
|
163
|
+
StructInstance(
|
|
164
|
+
struct_def=StructDef(fields=struct_def),
|
|
165
|
+
field_instances=[
|
|
166
|
+
FieldInstance(struct_def[0], 1),
|
|
167
|
+
FieldInstance(struct_def[1], 7),
|
|
168
|
+
],
|
|
169
|
+
),
|
|
170
|
+
bytearray(),
|
|
171
|
+
)
|
|
172
|
+
decoded_int = decode(struct_def, encoded_int)
|
|
173
|
+
|
|
174
|
+
# kind=2 -> payload is float (4 bytes)
|
|
175
|
+
encoded_float = encode(
|
|
176
|
+
StructInstance(
|
|
177
|
+
struct_def=StructDef(fields=struct_def),
|
|
178
|
+
field_instances=[
|
|
179
|
+
FieldInstance(struct_def[0], 2),
|
|
180
|
+
FieldInstance(struct_def[1], 1.5),
|
|
181
|
+
],
|
|
182
|
+
),
|
|
183
|
+
bytearray(),
|
|
184
|
+
)
|
|
185
|
+
decoded_float = decode(struct_def, encoded_float)
|
|
186
|
+
|
|
187
|
+
print(encoded_int, decoded_int)
|
|
188
|
+
print(encoded_float, decoded_float)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Saving And Loading StructDef Dictionaries
|
|
192
|
+
|
|
193
|
+
You can persist reusable structure definitions by name with
|
|
194
|
+
`save_struct_def_dict` / `load_struct_def_dict`.
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
from pathlib import Path
|
|
198
|
+
|
|
199
|
+
from sltcore import InfoSize
|
|
200
|
+
from sltcodec import FieldDef, StructDef, load_struct_def_dict, save_struct_def_dict
|
|
201
|
+
|
|
202
|
+
struct_defs = {
|
|
203
|
+
"Header": StructDef(
|
|
204
|
+
name="Header",
|
|
205
|
+
description="Simple header",
|
|
206
|
+
fields=[
|
|
207
|
+
FieldDef(name="kind", offset=InfoSize(0, 0), size=InfoSize(1, 0), type="unsigned int"),
|
|
208
|
+
FieldDef(name="flags", offset=InfoSize(1, 0), size=InfoSize(1, 0), type="unsigned int"),
|
|
209
|
+
],
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
path = Path("struct_defs.json")
|
|
214
|
+
save_struct_def_dict(path, struct_defs)
|
|
215
|
+
loaded = load_struct_def_dict(path)
|
|
216
|
+
|
|
217
|
+
print(loaded["Header"].name)
|
|
218
|
+
print(loaded["Header"].description)
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Example output:
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
bytearray(b"\x01\x07") StructInstance(struct_def=StructDef(...), field_instances=[...])
|
|
225
|
+
bytearray(b"\x02?\xc0\x00\x00") StructInstance(struct_def=StructDef(...), field_instances=[...])
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Development
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
uv sync --all-extras
|
|
232
|
+
uv run pytest
|
|
233
|
+
```
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
sltcodec/__init__.py,sha256=w7a_ZGDowo6-Gx2TjENJU23Ded6QkC9w6pBShADZj0s,394
|
|
2
|
+
sltcodec/codec.py,sha256=YPTjEBtzKT6vu63ErYDwxn4bFsbBZO5S4sUx7_fdwDg,16826
|
|
3
|
+
sltcodec/types.py,sha256=P1_g2ccJYpdygBboaBeb9H5YMY7n8WtH_MNKg2FtxG4,14416
|
|
4
|
+
sltcodec-0.2.0.dist-info/METADATA,sha256=UwgpHqOdJ_iwlcZA-0QeXLhnuwhrqPsq5Ofq5nJ5_QU,6871
|
|
5
|
+
sltcodec-0.2.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
6
|
+
sltcodec-0.2.0.dist-info/licenses/LICENSE,sha256=_WBHmfGHT8uGAnHK2W-hi7O_rO8t70lubivrqYujdIw,1065
|
|
7
|
+
sltcodec-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 fangface
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|