sltcodec 0.6.0__tar.gz → 1.1.0__tar.gz
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-0.6.0 → sltcodec-1.1.0}/PKG-INFO +1 -1
- {sltcodec-0.6.0 → sltcodec-1.1.0}/pyproject.toml +1 -1
- {sltcodec-0.6.0 → sltcodec-1.1.0}/src/sltcodec/__init__.py +4 -1
- {sltcodec-0.6.0 → sltcodec-1.1.0}/src/sltcodec/codec.py +38 -0
- {sltcodec-0.6.0 → sltcodec-1.1.0}/src/sltcodec/types.py +19 -1
- {sltcodec-0.6.0 → sltcodec-1.1.0}/tests/test_codec.py +89 -1
- {sltcodec-0.6.0 → sltcodec-1.1.0}/uv.lock +1 -1
- {sltcodec-0.6.0 → sltcodec-1.1.0}/.github/workflows/publish_to_pypi.yml +0 -0
- {sltcodec-0.6.0 → sltcodec-1.1.0}/.github/workflows/publish_to_testpypi.yml +0 -0
- {sltcodec-0.6.0 → sltcodec-1.1.0}/.gitignore +0 -0
- {sltcodec-0.6.0 → sltcodec-1.1.0}/.python-version +0 -0
- {sltcodec-0.6.0 → sltcodec-1.1.0}/.vscode/launch.json +0 -0
- {sltcodec-0.6.0 → sltcodec-1.1.0}/.vscode/settings.json +0 -0
- {sltcodec-0.6.0 → sltcodec-1.1.0}/LICENSE +0 -0
- {sltcodec-0.6.0 → sltcodec-1.1.0}/README.md +0 -0
- {sltcodec-0.6.0 → sltcodec-1.1.0}/bump_major.ps1 +0 -0
- {sltcodec-0.6.0 → sltcodec-1.1.0}/bump_minor.ps1 +0 -0
- {sltcodec-0.6.0 → sltcodec-1.1.0}/bump_patch.ps1 +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sltcodec
|
|
3
|
-
Version:
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: Decode and encode bytearrays according to struct layout definitions using sltcore.
|
|
5
5
|
Project-URL: Homepage, https://github.com/fangface-hub/StructLayoutToolkitCodec
|
|
6
6
|
Project-URL: Documentation, https://readthedocs.org
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from .codec import (PRIMITIVE_TYPES, decode, encode, encode_field,
|
|
2
|
-
load_struct_def_dict,
|
|
2
|
+
load_enum_def_dict, load_struct_def_dict,
|
|
3
|
+
save_enum_def_dict, save_struct_def_dict)
|
|
3
4
|
from .types import EnumDef, FieldDef, FieldInstance, StructDef, StructInstance
|
|
4
5
|
|
|
5
6
|
__all__ = [
|
|
@@ -12,6 +13,8 @@ __all__ = [
|
|
|
12
13
|
"decode",
|
|
13
14
|
"encode",
|
|
14
15
|
"encode_field",
|
|
16
|
+
"load_enum_def_dict",
|
|
15
17
|
"load_struct_def_dict",
|
|
18
|
+
"save_enum_def_dict",
|
|
16
19
|
"save_struct_def_dict",
|
|
17
20
|
]
|
|
@@ -66,6 +66,26 @@ def load_struct_def_dict(path: str | Path) -> dict[str, StructDef]:
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
|
|
69
|
+
def save_enum_def_dict(path: str | Path, enum_def_dict: dict[str,
|
|
70
|
+
EnumDef]) -> None:
|
|
71
|
+
"""Save an enum definition dictionary to a JSON file."""
|
|
72
|
+
payload = {
|
|
73
|
+
name: enum_def.to_dict()
|
|
74
|
+
for name, enum_def in enum_def_dict.items()
|
|
75
|
+
}
|
|
76
|
+
Path(path).write_text(json.dumps(payload, ensure_ascii=False, indent=2),
|
|
77
|
+
encoding="utf-8")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def load_enum_def_dict(path: str | Path) -> dict[str, EnumDef]:
|
|
81
|
+
"""Load an enum definition dictionary from a JSON file."""
|
|
82
|
+
data = json.loads(Path(path).read_text(encoding="utf-8"))
|
|
83
|
+
return {
|
|
84
|
+
name: EnumDef.from_dict(enum_def_data)
|
|
85
|
+
for name, enum_def_data in data.items()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
69
89
|
def _resolve_info_size(value: InfoSize | str, env: dict[str, Any]) -> InfoSize:
|
|
70
90
|
"""Resolve an InfoSize value that can be static or expression-based."""
|
|
71
91
|
if isinstance(value, str):
|
|
@@ -85,6 +105,18 @@ def _resolve_size(field_def: FieldDef, env: dict[str, Any]) -> InfoSize:
|
|
|
85
105
|
return _resolve_info_size(field_def.size, env)
|
|
86
106
|
|
|
87
107
|
|
|
108
|
+
def _resolve_byte_swap(field_def: FieldDef, env: dict[str, Any]) -> bool:
|
|
109
|
+
"""Resolve a byte-swap flag that can be static or expression-based."""
|
|
110
|
+
if isinstance(field_def.byte_swap, str):
|
|
111
|
+
return bool(SltEval(env).eval(field_def.byte_swap))
|
|
112
|
+
return field_def.byte_swap
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _byte_swap_info(info: Info, size: InfoSize) -> Info:
|
|
116
|
+
"""Reverse the byte representation of an Info payload."""
|
|
117
|
+
return Info.from_bytes(info.to_bytes[::-1], size, scale=info.scale)
|
|
118
|
+
|
|
119
|
+
|
|
88
120
|
def _is_padding_field_def(field_def: FieldDef) -> bool:
|
|
89
121
|
"""Check whether a field definition represents padding."""
|
|
90
122
|
return (field_def.name.startswith("padding[")
|
|
@@ -215,6 +247,8 @@ def encode_field(
|
|
|
215
247
|
return
|
|
216
248
|
|
|
217
249
|
offset, size, info = prepared
|
|
250
|
+
if _resolve_byte_swap(field_def, env):
|
|
251
|
+
info = _byte_swap_info(info, size)
|
|
218
252
|
required_bytes = (offset + size).bytes
|
|
219
253
|
if len(buf) < required_bytes:
|
|
220
254
|
buf.extend(b"\x00" * (required_bytes - len(buf)))
|
|
@@ -321,6 +355,9 @@ def decode_field(
|
|
|
321
355
|
if size.byte == 0 and size.bit == 0:
|
|
322
356
|
return None
|
|
323
357
|
info = bits_get(data, offset, size, scale=field_def.scale)
|
|
358
|
+
byte_swap = _resolve_byte_swap(field_def, env)
|
|
359
|
+
if byte_swap:
|
|
360
|
+
info = _byte_swap_info(info, size)
|
|
324
361
|
resolved_type = _resolve_field_type(field_def.type, struct_def_dict, env)
|
|
325
362
|
resolved_field_def = FieldDef(
|
|
326
363
|
name=field_def.name,
|
|
@@ -332,6 +369,7 @@ def decode_field(
|
|
|
332
369
|
description=field_def.description,
|
|
333
370
|
range_expression=field_def.range_expression,
|
|
334
371
|
enum_def=field_def.enum_def,
|
|
372
|
+
byte_swap=byte_swap,
|
|
335
373
|
)
|
|
336
374
|
if isinstance(resolved_type, StructDef):
|
|
337
375
|
return FieldInstance(
|
|
@@ -106,6 +106,9 @@ class FieldDef:
|
|
|
106
106
|
default=None, metadata={"desc": "The value range expression"})
|
|
107
107
|
enum_def: EnumDef | None = field(
|
|
108
108
|
default=None, metadata={"desc": "The enum definition for the field"})
|
|
109
|
+
byte_swap: bool | str = field(
|
|
110
|
+
default=False,
|
|
111
|
+
metadata={"desc": "Whether to reverse the field byte order"})
|
|
109
112
|
|
|
110
113
|
def split_repeat(self,
|
|
111
114
|
index: int,
|
|
@@ -123,6 +126,8 @@ class FieldDef:
|
|
|
123
126
|
split_name)
|
|
124
127
|
split_range_expression = self._replace_name_in_expression(
|
|
125
128
|
self.range_expression, self.name, split_name)
|
|
129
|
+
split_byte_swap = self._replace_name_in_expression(
|
|
130
|
+
self.byte_swap, self.name, split_name)
|
|
126
131
|
|
|
127
132
|
return FieldDef(name=split_name,
|
|
128
133
|
offset=split_offset,
|
|
@@ -132,7 +137,8 @@ class FieldDef:
|
|
|
132
137
|
repeat=None,
|
|
133
138
|
description=self.description,
|
|
134
139
|
range_expression=split_range_expression,
|
|
135
|
-
enum_def=self.enum_def
|
|
140
|
+
enum_def=self.enum_def,
|
|
141
|
+
byte_swap=split_byte_swap)
|
|
136
142
|
|
|
137
143
|
@staticmethod
|
|
138
144
|
def _replace_name_in_expression(value: Any, old_name: str,
|
|
@@ -180,6 +186,8 @@ class FieldDef:
|
|
|
180
186
|
self.range_expression,
|
|
181
187
|
"enum_def":
|
|
182
188
|
None if self.enum_def is None else self.enum_def.serialize(),
|
|
189
|
+
"byte_swap":
|
|
190
|
+
self.byte_swap,
|
|
183
191
|
}
|
|
184
192
|
|
|
185
193
|
def to_json(self) -> str:
|
|
@@ -198,6 +206,7 @@ class FieldDef:
|
|
|
198
206
|
description = data.get("description")
|
|
199
207
|
range_expression = data.get("range_expression")
|
|
200
208
|
enum_def_data = data.get("enum_def")
|
|
209
|
+
byte_swap = data.get("byte_swap", False)
|
|
201
210
|
|
|
202
211
|
def _deserialize_info_like(value: Any) -> Any:
|
|
203
212
|
if isinstance(value, dict):
|
|
@@ -251,6 +260,7 @@ class FieldDef:
|
|
|
251
260
|
description=description,
|
|
252
261
|
range_expression=range_expression,
|
|
253
262
|
enum_def=enum_def,
|
|
263
|
+
byte_swap=byte_swap,
|
|
254
264
|
)
|
|
255
265
|
|
|
256
266
|
@classmethod
|
|
@@ -270,6 +280,7 @@ class FieldDef:
|
|
|
270
280
|
"" if self.description is None else self.description,
|
|
271
281
|
"" if self.range_expression is None else self.range_expression,
|
|
272
282
|
self._sortable_enum_type(self.enum_def),
|
|
283
|
+
self._sortable_byte_swap(self.byte_swap),
|
|
273
284
|
)
|
|
274
285
|
|
|
275
286
|
@staticmethod
|
|
@@ -299,6 +310,13 @@ class FieldDef:
|
|
|
299
310
|
},
|
|
300
311
|
sort_keys=True)
|
|
301
312
|
|
|
313
|
+
@staticmethod
|
|
314
|
+
def _sortable_byte_swap(value: bool | str) -> tuple[int, str]:
|
|
315
|
+
"""Build a comparable key for a byte-swap flag or expression."""
|
|
316
|
+
if isinstance(value, bool):
|
|
317
|
+
return 0, str(value)
|
|
318
|
+
return 1, value
|
|
319
|
+
|
|
302
320
|
|
|
303
321
|
@total_ordering
|
|
304
322
|
@dataclass(frozen=True)
|
|
@@ -7,7 +7,8 @@ from sltcore import InfoSize
|
|
|
7
7
|
|
|
8
8
|
from sltcodec import (PRIMITIVE_TYPES, EnumDef, FieldDef, FieldInstance,
|
|
9
9
|
StructDef, StructInstance, decode, encode,
|
|
10
|
-
load_struct_def_dict,
|
|
10
|
+
load_enum_def_dict, load_struct_def_dict,
|
|
11
|
+
save_enum_def_dict, save_struct_def_dict)
|
|
11
12
|
from sltcodec.codec import decode_field
|
|
12
13
|
|
|
13
14
|
|
|
@@ -260,6 +261,77 @@ def test_field_def_to_json_from_json_round_trip():
|
|
|
260
261
|
}
|
|
261
262
|
|
|
262
263
|
|
|
264
|
+
def test_field_def_byte_swap_json_round_trip():
|
|
265
|
+
"""Test that expression-based byte_swap metadata is preserved."""
|
|
266
|
+
field_def = FieldDef(name="value",
|
|
267
|
+
offset=InfoSize(0, 0),
|
|
268
|
+
size=InfoSize(2, 0),
|
|
269
|
+
type="unsigned int",
|
|
270
|
+
byte_swap="kind == 1")
|
|
271
|
+
|
|
272
|
+
restored = FieldDef.from_json(field_def.to_json())
|
|
273
|
+
|
|
274
|
+
assert restored == field_def
|
|
275
|
+
assert restored.byte_swap == "kind == 1"
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def test_byte_swap_reverses_bytearray_input_and_output():
|
|
279
|
+
"""Test byte_swap reverses bytearray values symmetrically."""
|
|
280
|
+
field_def = FieldDef(name="payload",
|
|
281
|
+
offset=InfoSize(0, 0),
|
|
282
|
+
size=InfoSize(3, 0),
|
|
283
|
+
type="bytearray",
|
|
284
|
+
byte_swap=True)
|
|
285
|
+
|
|
286
|
+
encoded = encode(
|
|
287
|
+
StructInstance(struct_def=StructDef(fields=[field_def]),
|
|
288
|
+
field_instances=[
|
|
289
|
+
FieldInstance(field_def, bytearray(b"\x01\x02\x03"))
|
|
290
|
+
]),
|
|
291
|
+
bytearray(),
|
|
292
|
+
)
|
|
293
|
+
decoded = decode([field_def], encoded)
|
|
294
|
+
|
|
295
|
+
assert encoded == bytearray(b"\x03\x02\x01")
|
|
296
|
+
assert decoded.field_instances == [
|
|
297
|
+
FieldInstance(field_def, bytearray(b"\x01\x02\x03"))
|
|
298
|
+
]
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def test_byte_swap_expression_uses_previous_field_value():
|
|
302
|
+
"""Test byte_swap expressions use values decoded or encoded before it."""
|
|
303
|
+
kind_field = FieldDef(name="kind",
|
|
304
|
+
offset=InfoSize(0, 0),
|
|
305
|
+
size=InfoSize(1, 0),
|
|
306
|
+
type="unsigned int")
|
|
307
|
+
value_field = FieldDef(name="value",
|
|
308
|
+
offset=InfoSize(1, 0),
|
|
309
|
+
size=InfoSize(2, 0),
|
|
310
|
+
type="unsigned int",
|
|
311
|
+
byte_swap="kind == 1")
|
|
312
|
+
|
|
313
|
+
encoded = encode(
|
|
314
|
+
StructInstance(struct_def=StructDef(fields=[kind_field, value_field]),
|
|
315
|
+
field_instances=[
|
|
316
|
+
FieldInstance(kind_field, 1),
|
|
317
|
+
FieldInstance(value_field, 0x1234),
|
|
318
|
+
]),
|
|
319
|
+
bytearray(),
|
|
320
|
+
)
|
|
321
|
+
decoded = decode([kind_field, value_field], encoded)
|
|
322
|
+
|
|
323
|
+
assert encoded == bytearray(b"\x01\x34\x12")
|
|
324
|
+
assert decoded.field_instances == [
|
|
325
|
+
FieldInstance(kind_field, 1),
|
|
326
|
+
FieldInstance(
|
|
327
|
+
FieldDef(name="value",
|
|
328
|
+
offset=InfoSize(1, 0),
|
|
329
|
+
size=InfoSize(2, 0),
|
|
330
|
+
type="unsigned int",
|
|
331
|
+
byte_swap=True), 0x1234),
|
|
332
|
+
]
|
|
333
|
+
|
|
334
|
+
|
|
263
335
|
def test_enum_def_to_json_from_json_round_trip():
|
|
264
336
|
"""Test EnumDef.to_json/from_json round-trip conversion."""
|
|
265
337
|
enum_def = EnumDef(name="Status",
|
|
@@ -413,6 +485,22 @@ def test_struct_def_dict_load_and_save(tmp_path: Path):
|
|
|
413
485
|
assert loaded["value"] == struct_def
|
|
414
486
|
|
|
415
487
|
|
|
488
|
+
def test_enum_def_dict_load_and_save(tmp_path: Path):
|
|
489
|
+
"""Test that an EnumDef dictionary can be saved and reloaded."""
|
|
490
|
+
enum_def = EnumDef(name="Status",
|
|
491
|
+
description="Status enum",
|
|
492
|
+
values={
|
|
493
|
+
"OK": 1,
|
|
494
|
+
"NG": 2
|
|
495
|
+
})
|
|
496
|
+
path = tmp_path / "enum_defs.json"
|
|
497
|
+
|
|
498
|
+
save_enum_def_dict(path, {"status": enum_def})
|
|
499
|
+
loaded = load_enum_def_dict(path)
|
|
500
|
+
|
|
501
|
+
assert loaded["status"] == enum_def
|
|
502
|
+
|
|
503
|
+
|
|
416
504
|
def test_struct_def_to_json_from_json_round_trip():
|
|
417
505
|
"""Test StructDef.to_json/from_json round-trip conversion."""
|
|
418
506
|
struct_def = StructDef(name="ValueStruct",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|