sltcodec 1.0.0__tar.gz → 1.2.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-1.0.0 → sltcodec-1.2.0}/PKG-INFO +39 -1
- {sltcodec-1.0.0 → sltcodec-1.2.0}/README.md +38 -0
- {sltcodec-1.0.0 → sltcodec-1.2.0}/pyproject.toml +1 -1
- {sltcodec-1.0.0 → sltcodec-1.2.0}/src/sltcodec/codec.py +19 -1
- {sltcodec-1.0.0 → sltcodec-1.2.0}/src/sltcodec/types.py +27 -29
- {sltcodec-1.0.0 → sltcodec-1.2.0}/tests/test_codec.py +80 -16
- {sltcodec-1.0.0 → sltcodec-1.2.0}/uv.lock +1 -1
- {sltcodec-1.0.0 → sltcodec-1.2.0}/.github/workflows/publish_to_pypi.yml +0 -0
- {sltcodec-1.0.0 → sltcodec-1.2.0}/.github/workflows/publish_to_testpypi.yml +0 -0
- {sltcodec-1.0.0 → sltcodec-1.2.0}/.gitignore +0 -0
- {sltcodec-1.0.0 → sltcodec-1.2.0}/.python-version +0 -0
- {sltcodec-1.0.0 → sltcodec-1.2.0}/.vscode/launch.json +0 -0
- {sltcodec-1.0.0 → sltcodec-1.2.0}/.vscode/settings.json +0 -0
- {sltcodec-1.0.0 → sltcodec-1.2.0}/LICENSE +0 -0
- {sltcodec-1.0.0 → sltcodec-1.2.0}/bump_major.ps1 +0 -0
- {sltcodec-1.0.0 → sltcodec-1.2.0}/bump_minor.ps1 +0 -0
- {sltcodec-1.0.0 → sltcodec-1.2.0}/bump_patch.ps1 +0 -0
- {sltcodec-1.0.0 → sltcodec-1.2.0}/src/sltcodec/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sltcodec
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.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
|
|
@@ -236,6 +236,44 @@ bytearray(b"\x01\x07") StructInstance(struct_def=StructDef(...), field_instances
|
|
|
236
236
|
bytearray(b"\x02?\xc0\x00\x00") StructInstance(struct_def=StructDef(...), field_instances=[...])
|
|
237
237
|
```
|
|
238
238
|
|
|
239
|
+
## Enum Definitions
|
|
240
|
+
|
|
241
|
+
`FieldDef.enum_def_name` holds only the name of an `EnumDef`, not the definition
|
|
242
|
+
itself. Pass the actual definitions via an `enum_def_dict` (mapping name to
|
|
243
|
+
`EnumDef`) to `decode` / `encode` so the matching `EnumDef` can be resolved and
|
|
244
|
+
attached as `FieldInstance.enum_item`.
|
|
245
|
+
|
|
246
|
+
```python
|
|
247
|
+
from sltcore import InfoSize
|
|
248
|
+
from sltcodec import EnumDef, FieldDef, decode
|
|
249
|
+
|
|
250
|
+
status_enum = EnumDef(name="Status", values={"OK": 0, "NG": 1})
|
|
251
|
+
|
|
252
|
+
struct_def = [
|
|
253
|
+
FieldDef(name="status",
|
|
254
|
+
offset=InfoSize(0, 0),
|
|
255
|
+
size=InfoSize(1, 0),
|
|
256
|
+
type="unsigned int",
|
|
257
|
+
enum_def_name="Status"),
|
|
258
|
+
]
|
|
259
|
+
|
|
260
|
+
decoded = decode(struct_def, bytearray(b"\x01"),
|
|
261
|
+
enum_def_dict={"Status": status_enum})
|
|
262
|
+
|
|
263
|
+
print(decoded.field_instances[0].value)
|
|
264
|
+
print(decoded.field_instances[0].enum_item)
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Output:
|
|
268
|
+
|
|
269
|
+
```python
|
|
270
|
+
1
|
|
271
|
+
('NG', 1)
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
You can also persist enum definitions by name with
|
|
275
|
+
`save_enum_def_dict` / `load_enum_def_dict`, mirroring `StructDef` dictionaries.
|
|
276
|
+
|
|
239
277
|
## Development
|
|
240
278
|
|
|
241
279
|
```bash
|
|
@@ -219,6 +219,44 @@ bytearray(b"\x01\x07") StructInstance(struct_def=StructDef(...), field_instances
|
|
|
219
219
|
bytearray(b"\x02?\xc0\x00\x00") StructInstance(struct_def=StructDef(...), field_instances=[...])
|
|
220
220
|
```
|
|
221
221
|
|
|
222
|
+
## Enum Definitions
|
|
223
|
+
|
|
224
|
+
`FieldDef.enum_def_name` holds only the name of an `EnumDef`, not the definition
|
|
225
|
+
itself. Pass the actual definitions via an `enum_def_dict` (mapping name to
|
|
226
|
+
`EnumDef`) to `decode` / `encode` so the matching `EnumDef` can be resolved and
|
|
227
|
+
attached as `FieldInstance.enum_item`.
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
from sltcore import InfoSize
|
|
231
|
+
from sltcodec import EnumDef, FieldDef, decode
|
|
232
|
+
|
|
233
|
+
status_enum = EnumDef(name="Status", values={"OK": 0, "NG": 1})
|
|
234
|
+
|
|
235
|
+
struct_def = [
|
|
236
|
+
FieldDef(name="status",
|
|
237
|
+
offset=InfoSize(0, 0),
|
|
238
|
+
size=InfoSize(1, 0),
|
|
239
|
+
type="unsigned int",
|
|
240
|
+
enum_def_name="Status"),
|
|
241
|
+
]
|
|
242
|
+
|
|
243
|
+
decoded = decode(struct_def, bytearray(b"\x01"),
|
|
244
|
+
enum_def_dict={"Status": status_enum})
|
|
245
|
+
|
|
246
|
+
print(decoded.field_instances[0].value)
|
|
247
|
+
print(decoded.field_instances[0].enum_item)
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Output:
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
1
|
|
254
|
+
('NG', 1)
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
You can also persist enum definitions by name with
|
|
258
|
+
`save_enum_def_dict` / `load_enum_def_dict`, mirroring `StructDef` dictionaries.
|
|
259
|
+
|
|
222
260
|
## Development
|
|
223
261
|
|
|
224
262
|
```bash
|
|
@@ -105,6 +105,18 @@ def _resolve_size(field_def: FieldDef, env: dict[str, Any]) -> InfoSize:
|
|
|
105
105
|
return _resolve_info_size(field_def.size, env)
|
|
106
106
|
|
|
107
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
|
+
|
|
108
120
|
def _is_padding_field_def(field_def: FieldDef) -> bool:
|
|
109
121
|
"""Check whether a field definition represents padding."""
|
|
110
122
|
return (field_def.name.startswith("padding[")
|
|
@@ -235,6 +247,8 @@ def encode_field(
|
|
|
235
247
|
return
|
|
236
248
|
|
|
237
249
|
offset, size, info = prepared
|
|
250
|
+
if _resolve_byte_swap(field_def, env):
|
|
251
|
+
info = _byte_swap_info(info, size)
|
|
238
252
|
required_bytes = (offset + size).bytes
|
|
239
253
|
if len(buf) < required_bytes:
|
|
240
254
|
buf.extend(b"\x00" * (required_bytes - len(buf)))
|
|
@@ -341,6 +355,9 @@ def decode_field(
|
|
|
341
355
|
if size.byte == 0 and size.bit == 0:
|
|
342
356
|
return None
|
|
343
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)
|
|
344
361
|
resolved_type = _resolve_field_type(field_def.type, struct_def_dict, env)
|
|
345
362
|
resolved_field_def = FieldDef(
|
|
346
363
|
name=field_def.name,
|
|
@@ -351,7 +368,8 @@ def decode_field(
|
|
|
351
368
|
repeat=field_def.repeat,
|
|
352
369
|
description=field_def.description,
|
|
353
370
|
range_expression=field_def.range_expression,
|
|
354
|
-
|
|
371
|
+
enum_def_name=field_def.enum_def_name,
|
|
372
|
+
byte_swap=byte_swap,
|
|
355
373
|
)
|
|
356
374
|
if isinstance(resolved_type, StructDef):
|
|
357
375
|
return FieldInstance(
|
|
@@ -104,8 +104,12 @@ class FieldDef:
|
|
|
104
104
|
default=None, metadata={"desc": "The description of the field"})
|
|
105
105
|
range_expression: str | None = field(
|
|
106
106
|
default=None, metadata={"desc": "The value range expression"})
|
|
107
|
-
|
|
108
|
-
default=None,
|
|
107
|
+
enum_def_name: str | None = field(
|
|
108
|
+
default=None,
|
|
109
|
+
metadata={"desc": "The name of the enum definition for the field"})
|
|
110
|
+
byte_swap: bool | str = field(
|
|
111
|
+
default=False,
|
|
112
|
+
metadata={"desc": "Whether to reverse the field byte order"})
|
|
109
113
|
|
|
110
114
|
def split_repeat(self,
|
|
111
115
|
index: int,
|
|
@@ -123,6 +127,8 @@ class FieldDef:
|
|
|
123
127
|
split_name)
|
|
124
128
|
split_range_expression = self._replace_name_in_expression(
|
|
125
129
|
self.range_expression, self.name, split_name)
|
|
130
|
+
split_byte_swap = self._replace_name_in_expression(
|
|
131
|
+
self.byte_swap, self.name, split_name)
|
|
126
132
|
|
|
127
133
|
return FieldDef(name=split_name,
|
|
128
134
|
offset=split_offset,
|
|
@@ -132,7 +138,8 @@ class FieldDef:
|
|
|
132
138
|
repeat=None,
|
|
133
139
|
description=self.description,
|
|
134
140
|
range_expression=split_range_expression,
|
|
135
|
-
|
|
141
|
+
enum_def_name=self.enum_def_name,
|
|
142
|
+
byte_swap=split_byte_swap)
|
|
136
143
|
|
|
137
144
|
@staticmethod
|
|
138
145
|
def _replace_name_in_expression(value: Any, old_name: str,
|
|
@@ -178,8 +185,10 @@ class FieldDef:
|
|
|
178
185
|
self.description,
|
|
179
186
|
"range_expression":
|
|
180
187
|
self.range_expression,
|
|
181
|
-
"
|
|
182
|
-
|
|
188
|
+
"enum_def_name":
|
|
189
|
+
self.enum_def_name,
|
|
190
|
+
"byte_swap":
|
|
191
|
+
self.byte_swap,
|
|
183
192
|
}
|
|
184
193
|
|
|
185
194
|
def to_json(self) -> str:
|
|
@@ -197,7 +206,8 @@ class FieldDef:
|
|
|
197
206
|
repeat = data.get("repeat")
|
|
198
207
|
description = data.get("description")
|
|
199
208
|
range_expression = data.get("range_expression")
|
|
200
|
-
|
|
209
|
+
enum_def_name = data.get("enum_def_name")
|
|
210
|
+
byte_swap = data.get("byte_swap", False)
|
|
201
211
|
|
|
202
212
|
def _deserialize_info_like(value: Any) -> Any:
|
|
203
213
|
if isinstance(value, dict):
|
|
@@ -233,14 +243,6 @@ class FieldDef:
|
|
|
233
243
|
else:
|
|
234
244
|
type_value = type_data
|
|
235
245
|
|
|
236
|
-
if enum_def_data is None:
|
|
237
|
-
enum_def = None
|
|
238
|
-
elif (isinstance(enum_def_data, dict)
|
|
239
|
-
and enum_def_data.get("__type__") == "EnumDef"):
|
|
240
|
-
enum_def = EnumDef.deserialize(enum_def_data)
|
|
241
|
-
else:
|
|
242
|
-
enum_def = None
|
|
243
|
-
|
|
244
246
|
return cls(
|
|
245
247
|
name=name,
|
|
246
248
|
offset=offset,
|
|
@@ -250,7 +252,8 @@ class FieldDef:
|
|
|
250
252
|
repeat=repeat,
|
|
251
253
|
description=description,
|
|
252
254
|
range_expression=range_expression,
|
|
253
|
-
|
|
255
|
+
enum_def_name=enum_def_name,
|
|
256
|
+
byte_swap=byte_swap,
|
|
254
257
|
)
|
|
255
258
|
|
|
256
259
|
@classmethod
|
|
@@ -269,7 +272,8 @@ class FieldDef:
|
|
|
269
272
|
-1 if self.repeat is None else self.repeat,
|
|
270
273
|
"" if self.description is None else self.description,
|
|
271
274
|
"" if self.range_expression is None else self.range_expression,
|
|
272
|
-
self.
|
|
275
|
+
"" if self.enum_def_name is None else self.enum_def_name,
|
|
276
|
+
self._sortable_byte_swap(self.byte_swap),
|
|
273
277
|
)
|
|
274
278
|
|
|
275
279
|
@staticmethod
|
|
@@ -287,17 +291,11 @@ class FieldDef:
|
|
|
287
291
|
return (1, value)
|
|
288
292
|
|
|
289
293
|
@staticmethod
|
|
290
|
-
def
|
|
291
|
-
"""Build a comparable key for
|
|
292
|
-
if value
|
|
293
|
-
return
|
|
294
|
-
return
|
|
295
|
-
{
|
|
296
|
-
"name": value.name,
|
|
297
|
-
"description": value.description,
|
|
298
|
-
"values": value.values,
|
|
299
|
-
},
|
|
300
|
-
sort_keys=True)
|
|
294
|
+
def _sortable_byte_swap(value: bool | str) -> tuple[int, str]:
|
|
295
|
+
"""Build a comparable key for a byte-swap flag or expression."""
|
|
296
|
+
if isinstance(value, bool):
|
|
297
|
+
return 0, str(value)
|
|
298
|
+
return 1, value
|
|
301
299
|
|
|
302
300
|
|
|
303
301
|
@total_ordering
|
|
@@ -351,10 +349,10 @@ class FieldInstance:
|
|
|
351
349
|
field_def: FieldDef,
|
|
352
350
|
enum_def_dict: dict[str, EnumDef] | None) -> EnumDef | None:
|
|
353
351
|
"""Resolve enum definition from field metadata or lookup dictionary."""
|
|
354
|
-
if field_def.enum_def is not None:
|
|
355
|
-
return field_def.enum_def
|
|
356
352
|
if not enum_def_dict:
|
|
357
353
|
return None
|
|
354
|
+
if field_def.enum_def_name is not None:
|
|
355
|
+
return enum_def_dict.get(field_def.enum_def_name)
|
|
358
356
|
if isinstance(field_def.type, str) and field_def.type in enum_def_dict:
|
|
359
357
|
return enum_def_dict[field_def.type]
|
|
360
358
|
return enum_def_dict.get(field_def.name)
|
|
@@ -179,7 +179,7 @@ def test_repeated_field_preserves_range_and_enum_metadata():
|
|
|
179
179
|
type="unsigned int",
|
|
180
180
|
repeat=2,
|
|
181
181
|
range_expression="0 <= value <= 255",
|
|
182
|
-
|
|
182
|
+
enum_def_name=enum_def.name)
|
|
183
183
|
|
|
184
184
|
encoded = encode(
|
|
185
185
|
StructInstance(struct_def=StructDef(fields=[field_def]),
|
|
@@ -192,8 +192,8 @@ def test_repeated_field_preserves_range_and_enum_metadata():
|
|
|
192
192
|
"0 <= value[0] <= 255")
|
|
193
193
|
assert decoded.field_instances[1].field_def.range_expression == (
|
|
194
194
|
"0 <= value[1] <= 255")
|
|
195
|
-
assert decoded.field_instances[0].field_def.
|
|
196
|
-
assert decoded.field_instances[1].field_def.
|
|
195
|
+
assert decoded.field_instances[0].field_def.enum_def_name == enum_def.name
|
|
196
|
+
assert decoded.field_instances[1].field_def.enum_def_name == enum_def.name
|
|
197
197
|
|
|
198
198
|
|
|
199
199
|
def test_split_repeat_replaces_name_in_range_expression():
|
|
@@ -242,23 +242,86 @@ def test_field_def_to_json_from_json_round_trip():
|
|
|
242
242
|
repeat=3,
|
|
243
243
|
description="A repeated value",
|
|
244
244
|
range_expression="0 <= value <= 255",
|
|
245
|
-
|
|
245
|
+
enum_def_name=enum_def.name)
|
|
246
246
|
|
|
247
247
|
payload = field_def.to_json()
|
|
248
248
|
restored = FieldDef.from_json(payload)
|
|
249
249
|
|
|
250
250
|
assert restored == field_def
|
|
251
251
|
assert restored.range_expression == "0 <= value <= 255"
|
|
252
|
-
assert restored.
|
|
253
|
-
assert restored.to_dict()["
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
252
|
+
assert restored.enum_def_name == enum_def.name
|
|
253
|
+
assert restored.to_dict()["enum_def_name"] == "ValueKind"
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def test_field_def_byte_swap_json_round_trip():
|
|
257
|
+
"""Test that expression-based byte_swap metadata is preserved."""
|
|
258
|
+
field_def = FieldDef(name="value",
|
|
259
|
+
offset=InfoSize(0, 0),
|
|
260
|
+
size=InfoSize(2, 0),
|
|
261
|
+
type="unsigned int",
|
|
262
|
+
byte_swap="kind == 1")
|
|
263
|
+
|
|
264
|
+
restored = FieldDef.from_json(field_def.to_json())
|
|
265
|
+
|
|
266
|
+
assert restored == field_def
|
|
267
|
+
assert restored.byte_swap == "kind == 1"
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def test_byte_swap_reverses_bytearray_input_and_output():
|
|
271
|
+
"""Test byte_swap reverses bytearray values symmetrically."""
|
|
272
|
+
field_def = FieldDef(name="payload",
|
|
273
|
+
offset=InfoSize(0, 0),
|
|
274
|
+
size=InfoSize(3, 0),
|
|
275
|
+
type="bytearray",
|
|
276
|
+
byte_swap=True)
|
|
277
|
+
|
|
278
|
+
encoded = encode(
|
|
279
|
+
StructInstance(struct_def=StructDef(fields=[field_def]),
|
|
280
|
+
field_instances=[
|
|
281
|
+
FieldInstance(field_def, bytearray(b"\x01\x02\x03"))
|
|
282
|
+
]),
|
|
283
|
+
bytearray(),
|
|
284
|
+
)
|
|
285
|
+
decoded = decode([field_def], encoded)
|
|
286
|
+
|
|
287
|
+
assert encoded == bytearray(b"\x03\x02\x01")
|
|
288
|
+
assert decoded.field_instances == [
|
|
289
|
+
FieldInstance(field_def, bytearray(b"\x01\x02\x03"))
|
|
290
|
+
]
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def test_byte_swap_expression_uses_previous_field_value():
|
|
294
|
+
"""Test byte_swap expressions use values decoded or encoded before it."""
|
|
295
|
+
kind_field = FieldDef(name="kind",
|
|
296
|
+
offset=InfoSize(0, 0),
|
|
297
|
+
size=InfoSize(1, 0),
|
|
298
|
+
type="unsigned int")
|
|
299
|
+
value_field = FieldDef(name="value",
|
|
300
|
+
offset=InfoSize(1, 0),
|
|
301
|
+
size=InfoSize(2, 0),
|
|
302
|
+
type="unsigned int",
|
|
303
|
+
byte_swap="kind == 1")
|
|
304
|
+
|
|
305
|
+
encoded = encode(
|
|
306
|
+
StructInstance(struct_def=StructDef(fields=[kind_field, value_field]),
|
|
307
|
+
field_instances=[
|
|
308
|
+
FieldInstance(kind_field, 1),
|
|
309
|
+
FieldInstance(value_field, 0x1234),
|
|
310
|
+
]),
|
|
311
|
+
bytearray(),
|
|
312
|
+
)
|
|
313
|
+
decoded = decode([kind_field, value_field], encoded)
|
|
314
|
+
|
|
315
|
+
assert encoded == bytearray(b"\x01\x34\x12")
|
|
316
|
+
assert decoded.field_instances == [
|
|
317
|
+
FieldInstance(kind_field, 1),
|
|
318
|
+
FieldInstance(
|
|
319
|
+
FieldDef(name="value",
|
|
320
|
+
offset=InfoSize(1, 0),
|
|
321
|
+
size=InfoSize(2, 0),
|
|
322
|
+
type="unsigned int",
|
|
323
|
+
byte_swap=True), 0x1234),
|
|
324
|
+
]
|
|
262
325
|
|
|
263
326
|
|
|
264
327
|
def test_enum_def_to_json_from_json_round_trip():
|
|
@@ -354,9 +417,10 @@ def test_field_instance_from_value_sets_enum_item_from_field_enum_def():
|
|
|
354
417
|
offset=InfoSize(0, 0),
|
|
355
418
|
size=InfoSize(1, 0),
|
|
356
419
|
type="unsigned int",
|
|
357
|
-
|
|
420
|
+
enum_def_name=enum_def.name)
|
|
358
421
|
|
|
359
|
-
field_instance = FieldInstance.from_value(
|
|
422
|
+
field_instance = FieldInstance.from_value(
|
|
423
|
+
field_def, 1, enum_def_dict={enum_def.name: enum_def})
|
|
360
424
|
|
|
361
425
|
assert field_instance.enum_item == ("MANUAL", 1)
|
|
362
426
|
|
|
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
|