sltcodec 2.1.0__tar.gz → 2.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-2.1.0 → sltcodec-2.2.0}/PKG-INFO +8 -6
- {sltcodec-2.1.0 → sltcodec-2.2.0}/README.md +6 -0
- {sltcodec-2.1.0 → sltcodec-2.2.0}/README_pypi.md +6 -4
- {sltcodec-2.1.0 → sltcodec-2.2.0}/pyproject.toml +2 -2
- {sltcodec-2.1.0 → sltcodec-2.2.0}/src/sltcodec/codec.py +80 -71
- {sltcodec-2.1.0 → sltcodec-2.2.0}/src/sltcodec/types.py +0 -41
- {sltcodec-2.1.0 → sltcodec-2.2.0}/tests/test_codec.py +59 -1
- {sltcodec-2.1.0 → sltcodec-2.2.0}/tests/test_types.py +0 -17
- {sltcodec-2.1.0 → sltcodec-2.2.0}/uv.lock +5 -5
- {sltcodec-2.1.0 → sltcodec-2.2.0}/.github/workflows/publish_to_pypi.yml +0 -0
- {sltcodec-2.1.0 → sltcodec-2.2.0}/.github/workflows/publish_to_testpypi.yml +0 -0
- {sltcodec-2.1.0 → sltcodec-2.2.0}/.gitignore +0 -0
- {sltcodec-2.1.0 → sltcodec-2.2.0}/.python-version +0 -0
- {sltcodec-2.1.0 → sltcodec-2.2.0}/.vscode/launch.json +0 -0
- {sltcodec-2.1.0 → sltcodec-2.2.0}/.vscode/settings.json +0 -0
- {sltcodec-2.1.0 → sltcodec-2.2.0}/LICENSE +0 -0
- {sltcodec-2.1.0 → sltcodec-2.2.0}/bump_major.ps1 +0 -0
- {sltcodec-2.1.0 → sltcodec-2.2.0}/bump_minor.ps1 +0 -0
- {sltcodec-2.1.0 → sltcodec-2.2.0}/bump_patch.ps1 +0 -0
- {sltcodec-2.1.0 → sltcodec-2.2.0}/src/sltcodec/__init__.py +0 -0
- {sltcodec-2.1.0 → sltcodec-2.2.0}/tests/test_persistence.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sltcodec
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.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
|
|
@@ -11,7 +11,7 @@ License-Expression: MIT
|
|
|
11
11
|
License-File: LICENSE
|
|
12
12
|
Keywords: byte,deserialization,layout,serialization,struct
|
|
13
13
|
Requires-Python: >=3.12
|
|
14
|
-
Requires-Dist: sltcalc>=
|
|
14
|
+
Requires-Dist: sltcalc>=1.0.0
|
|
15
15
|
Requires-Dist: sltcore>=1.5.0
|
|
16
16
|
Description-Content-Type: text/markdown
|
|
17
17
|
|
|
@@ -63,6 +63,12 @@ encoded = encode(layout, instance, bytearray())
|
|
|
63
63
|
decoded = decode(layout, encoded)
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
+
`decode` returns a `StructInstance` whose `size` is the actual end of the
|
|
67
|
+
decoded layout. `decode_field` stores the actual decoded size in the returned
|
|
68
|
+
`FieldInstance.field_def.size`. For nested `StructDef` fields, that size comes
|
|
69
|
+
from the nested `StructInstance.size`; for repeated fields, the next element's
|
|
70
|
+
offset advances by the actual size of the previous element.
|
|
71
|
+
|
|
66
72
|
## Field Types
|
|
67
73
|
|
|
68
74
|
The public `PRIMITIVE_TYPES` set contains:
|
|
@@ -78,7 +84,6 @@ The public `PRIMITIVE_TYPES` set contains:
|
|
|
78
84
|
Store named structures in `TypeDict.struct_dict` and enums in `TypeDict.enum_dict`. A field refers to an enum by its `enum_def_name`; the codec resolves it through `StructLayout.type_dict`.
|
|
79
85
|
|
|
80
86
|
```python
|
|
81
|
-
| `__lt__(other)` | Compare enum definitions using their serialized ordering. |
|
|
82
87
|
from sltcore import InfoSize
|
|
83
88
|
from sltcodec import EnumDef, FieldDef, StructDef, StructLayout, TypeDict, decode
|
|
84
89
|
|
|
@@ -102,7 +107,6 @@ layout = StructLayout(
|
|
|
102
107
|
)
|
|
103
108
|
|
|
104
109
|
decoded = decode(layout, bytearray(b"\x01"))
|
|
105
|
-
| `__lt__(other)` | Compare field definitions using their serialized ordering. |
|
|
106
110
|
assert decoded.field_instances[0].enum_item == ("NG", 1)
|
|
107
111
|
```
|
|
108
112
|
|
|
@@ -117,7 +121,6 @@ from sltcodec import load_struct_layout, save_struct_layout
|
|
|
117
121
|
path = Path("struct_layout.json")
|
|
118
122
|
save_struct_layout(layout, path)
|
|
119
123
|
loaded = load_struct_layout(path)
|
|
120
|
-
| `__lt__(other)` | Compare field instances using field-definition order. |
|
|
121
124
|
assert loaded == layout
|
|
122
125
|
```
|
|
123
126
|
|
|
@@ -185,7 +188,6 @@ internal implementation details and are omitted from the public method tables.
|
|
|
185
188
|
|
|
186
189
|
| Method | Description |
|
|
187
190
|
| --- | --- |
|
|
188
|
-
| `split_repeat(index, offset=None, size=None)` | Create one field definition from a repeated field. |
|
|
189
191
|
| `to_dict()` | Convert the definition to a JSON-compatible dictionary. |
|
|
190
192
|
| `to_json()` | Convert the definition to a JSON string. |
|
|
191
193
|
| `from_dict(data)` | Create a `FieldDef` from a dictionary. |
|
|
@@ -74,6 +74,12 @@ updated_field = field_instance.with_value(
|
|
|
74
74
|
)
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
+
Decoded sizes reflect the actual data consumed. `decode` sets
|
|
78
|
+
`StructInstance.size` to the end of the decoded layout, and `decode_field`
|
|
79
|
+
stores the actual size in `FieldInstance.field_def.size`. For nested structures,
|
|
80
|
+
the nested `StructInstance.size` is used; repeated fields advance each next
|
|
81
|
+
offset by the preceding element's actual size.
|
|
82
|
+
|
|
77
83
|
## Versioning And Release
|
|
78
84
|
|
|
79
85
|
Version bump scripts are provided for patch, minor, and major releases:
|
|
@@ -46,6 +46,12 @@ encoded = encode(layout, instance, bytearray())
|
|
|
46
46
|
decoded = decode(layout, encoded)
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
`decode` returns a `StructInstance` whose `size` is the actual end of the
|
|
50
|
+
decoded layout. `decode_field` stores the actual decoded size in the returned
|
|
51
|
+
`FieldInstance.field_def.size`. For nested `StructDef` fields, that size comes
|
|
52
|
+
from the nested `StructInstance.size`; for repeated fields, the next element's
|
|
53
|
+
offset advances by the actual size of the previous element.
|
|
54
|
+
|
|
49
55
|
## Field Types
|
|
50
56
|
|
|
51
57
|
The public `PRIMITIVE_TYPES` set contains:
|
|
@@ -61,7 +67,6 @@ The public `PRIMITIVE_TYPES` set contains:
|
|
|
61
67
|
Store named structures in `TypeDict.struct_dict` and enums in `TypeDict.enum_dict`. A field refers to an enum by its `enum_def_name`; the codec resolves it through `StructLayout.type_dict`.
|
|
62
68
|
|
|
63
69
|
```python
|
|
64
|
-
| `__lt__(other)` | Compare enum definitions using their serialized ordering. |
|
|
65
70
|
from sltcore import InfoSize
|
|
66
71
|
from sltcodec import EnumDef, FieldDef, StructDef, StructLayout, TypeDict, decode
|
|
67
72
|
|
|
@@ -85,7 +90,6 @@ layout = StructLayout(
|
|
|
85
90
|
)
|
|
86
91
|
|
|
87
92
|
decoded = decode(layout, bytearray(b"\x01"))
|
|
88
|
-
| `__lt__(other)` | Compare field definitions using their serialized ordering. |
|
|
89
93
|
assert decoded.field_instances[0].enum_item == ("NG", 1)
|
|
90
94
|
```
|
|
91
95
|
|
|
@@ -100,7 +104,6 @@ from sltcodec import load_struct_layout, save_struct_layout
|
|
|
100
104
|
path = Path("struct_layout.json")
|
|
101
105
|
save_struct_layout(layout, path)
|
|
102
106
|
loaded = load_struct_layout(path)
|
|
103
|
-
| `__lt__(other)` | Compare field instances using field-definition order. |
|
|
104
107
|
assert loaded == layout
|
|
105
108
|
```
|
|
106
109
|
|
|
@@ -168,7 +171,6 @@ internal implementation details and are omitted from the public method tables.
|
|
|
168
171
|
|
|
169
172
|
| Method | Description |
|
|
170
173
|
| --- | --- |
|
|
171
|
-
| `split_repeat(index, offset=None, size=None)` | Create one field definition from a repeated field. |
|
|
172
174
|
| `to_dict()` | Convert the definition to a JSON-compatible dictionary. |
|
|
173
175
|
| `to_json()` | Convert the definition to a JSON string. |
|
|
174
176
|
| `from_dict(data)` | Create a `FieldDef` from a dictionary. |
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sltcodec"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.2.0"
|
|
4
4
|
description = "Decode and encode bytearrays according to struct layout definitions using sltcore."
|
|
5
5
|
readme = "README_pypi.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
7
7
|
dependencies = [
|
|
8
|
-
"sltcalc>=
|
|
8
|
+
"sltcalc>=1.0.0",
|
|
9
9
|
"sltcore>=1.5.0",
|
|
10
10
|
]
|
|
11
11
|
license = "MIT"
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import json
|
|
5
|
+
import re
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from typing import Any
|
|
7
8
|
|
|
@@ -135,20 +136,12 @@ def _resolve_info_size(value: InfoSize | str, env: dict[str, Any]) -> InfoSize:
|
|
|
135
136
|
if isinstance(value, str):
|
|
136
137
|
stleval = SltEval(env)
|
|
137
138
|
resolved_byte = stleval.eval(value)
|
|
139
|
+
if isinstance(resolved_byte, InfoSize):
|
|
140
|
+
return resolved_byte
|
|
138
141
|
return InfoSize(resolved_byte, 0)
|
|
139
142
|
return value
|
|
140
143
|
|
|
141
144
|
|
|
142
|
-
def _resolve_offset(field_def: FieldDef, env: dict[str, Any]) -> InfoSize:
|
|
143
|
-
"""Resolve a field offset that can be static or expression-based."""
|
|
144
|
-
return _resolve_info_size(field_def.offset, env)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
def _resolve_size(field_def: FieldDef, env: dict[str, Any]) -> InfoSize:
|
|
148
|
-
"""Resolve a field size that can be static or expression-based."""
|
|
149
|
-
return _resolve_info_size(field_def.size, env)
|
|
150
|
-
|
|
151
|
-
|
|
152
145
|
def _resolve_byte_swap(field_def: FieldDef, env: dict[str, Any]) -> bool:
|
|
153
146
|
"""Resolve a byte-swap flag that can be static or expression-based."""
|
|
154
147
|
if isinstance(field_def.byte_swap, str):
|
|
@@ -156,27 +149,12 @@ def _resolve_byte_swap(field_def: FieldDef, env: dict[str, Any]) -> bool:
|
|
|
156
149
|
return field_def.byte_swap
|
|
157
150
|
|
|
158
151
|
|
|
159
|
-
def _byte_swap_info(info: Info, size: InfoSize) -> Info:
|
|
160
|
-
"""Reverse the byte representation of an Info payload."""
|
|
161
|
-
return Info.from_bytes(info.to_bytes[::-1], size, scale=info.scale)
|
|
162
|
-
|
|
163
|
-
|
|
164
152
|
def _is_padding_field_def(field_def: FieldDef) -> bool:
|
|
165
153
|
"""Check whether a field definition represents padding."""
|
|
166
154
|
return (field_def.name.startswith("padding[")
|
|
167
155
|
and field_def.type in ["bytes", "bytearray"])
|
|
168
156
|
|
|
169
157
|
|
|
170
|
-
def _info_size_to_bits(info_size: InfoSize) -> int:
|
|
171
|
-
"""Convert InfoSize to an absolute bit count."""
|
|
172
|
-
return info_size.byte * 8 + info_size.bit
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
def _info_size_from_bits(total_bits: int) -> InfoSize:
|
|
176
|
-
"""Create InfoSize from an absolute bit count."""
|
|
177
|
-
return InfoSize(total_bits >> 3, total_bits & 0x7)
|
|
178
|
-
|
|
179
|
-
|
|
180
158
|
def _validate_padding_alignment_bits(padding_alignment_bits: int) -> None:
|
|
181
159
|
"""Validate padding alignment as a positive power-of-two bit size."""
|
|
182
160
|
if (isinstance(padding_alignment_bits, bool)
|
|
@@ -236,8 +214,8 @@ def _prepare_field_info(
|
|
|
236
214
|
padding_alignment_bits: int = _DEFAULT_PADDING_ALIGNMENT_BITS,
|
|
237
215
|
) -> tuple[InfoSize, InfoSize, Info] | None:
|
|
238
216
|
"""Resolve a field definition into an offset, size, and info payload."""
|
|
239
|
-
offset =
|
|
240
|
-
size =
|
|
217
|
+
offset = _resolve_info_size(field_def.offset, env)
|
|
218
|
+
size = _resolve_info_size(field_def.size, env)
|
|
241
219
|
if size.byte == 0 and size.bit == 0:
|
|
242
220
|
return None
|
|
243
221
|
|
|
@@ -259,17 +237,30 @@ def _prepare_field_info(
|
|
|
259
237
|
return offset, size, info
|
|
260
238
|
|
|
261
239
|
|
|
262
|
-
def
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
240
|
+
def _repeated_field_def(field_def: FieldDef, index: int, offset: InfoSize,
|
|
241
|
+
size: InfoSize | str) -> FieldDef:
|
|
242
|
+
"""Create a resolved field definition for one repeated value."""
|
|
243
|
+
repeated_name = f"{field_def.name}[{index}]"
|
|
244
|
+
|
|
245
|
+
def replace_name(value: Any) -> Any:
|
|
246
|
+
if not isinstance(value, str):
|
|
247
|
+
return value
|
|
248
|
+
pattern = (rf"(?<![0-9A-Za-z_]){re.escape(field_def.name)}"
|
|
249
|
+
rf"(?![0-9A-Za-z_])")
|
|
250
|
+
return re.sub(pattern, repeated_name, value)
|
|
251
|
+
|
|
252
|
+
return FieldDef(
|
|
253
|
+
name=repeated_name,
|
|
254
|
+
offset=offset,
|
|
255
|
+
size=size,
|
|
256
|
+
type=replace_name(field_def.type),
|
|
257
|
+
scale=field_def.scale,
|
|
258
|
+
repeat=None,
|
|
259
|
+
description=field_def.description,
|
|
260
|
+
range_expression=replace_name(field_def.range_expression),
|
|
261
|
+
enum_def_name=field_def.enum_def_name,
|
|
262
|
+
byte_swap=replace_name(field_def.byte_swap),
|
|
263
|
+
)
|
|
273
264
|
|
|
274
265
|
|
|
275
266
|
def encode_field(
|
|
@@ -291,7 +282,7 @@ def encode_field(
|
|
|
291
282
|
|
|
292
283
|
offset, size, info = prepared
|
|
293
284
|
if _resolve_byte_swap(field_def, env):
|
|
294
|
-
info =
|
|
285
|
+
info = info.byte_swap
|
|
295
286
|
required_bytes = (offset + size).bytes
|
|
296
287
|
if len(buf) < required_bytes:
|
|
297
288
|
buf.extend(b"\x00" * (required_bytes - len(buf)))
|
|
@@ -341,21 +332,21 @@ def encode(
|
|
|
341
332
|
has_padding = True
|
|
342
333
|
value = field_value.value
|
|
343
334
|
if _is_padding_field_def(field_def):
|
|
344
|
-
padding_size =
|
|
335
|
+
padding_size = _resolve_info_size(field_def.size, env)
|
|
345
336
|
value = bytearray(padding_size.bytes)
|
|
346
337
|
if field_def.repeat is not None and field_def.repeat > 1:
|
|
347
|
-
current_offset =
|
|
348
|
-
resolved_size = _resolve_size(field_def, env)
|
|
338
|
+
current_offset = _resolve_info_size(field_def.offset, env)
|
|
349
339
|
values = list(value)
|
|
350
340
|
|
|
351
341
|
for i in range(field_def.repeat):
|
|
352
|
-
field_def_repeat =
|
|
353
|
-
|
|
342
|
+
field_def_repeat = _repeated_field_def(field_def, i,
|
|
343
|
+
current_offset,
|
|
344
|
+
field_def.size)
|
|
345
|
+
resolved_size = _resolve_info_size(field_def_repeat.size, env)
|
|
354
346
|
encode_field(field_def_repeat, values[i], buf, env, type_dict,
|
|
355
347
|
padding_alignment_bits)
|
|
356
348
|
env[field_def_repeat.name] = values[i]
|
|
357
|
-
if isinstance(current_offset, InfoSize)
|
|
358
|
-
resolved_size, InfoSize):
|
|
349
|
+
if isinstance(current_offset, InfoSize):
|
|
359
350
|
current_offset += resolved_size
|
|
360
351
|
continue
|
|
361
352
|
|
|
@@ -396,19 +387,29 @@ def decode_field(
|
|
|
396
387
|
"""
|
|
397
388
|
if env is None:
|
|
398
389
|
env = {}
|
|
399
|
-
offset =
|
|
400
|
-
size =
|
|
390
|
+
offset = _resolve_info_size(field_def.offset, env)
|
|
391
|
+
size = _resolve_info_size(field_def.size, env)
|
|
401
392
|
if size.byte == 0 and size.bit == 0:
|
|
402
393
|
return None
|
|
403
394
|
info = bits_get(data, offset, size, scale=field_def.scale)
|
|
404
395
|
byte_swap = _resolve_byte_swap(field_def, env)
|
|
405
396
|
if byte_swap:
|
|
406
|
-
info =
|
|
397
|
+
info = info.byte_swap
|
|
407
398
|
resolved_type = _resolve_field_type(field_def.type, type_dict, env)
|
|
399
|
+
nested_value = None
|
|
400
|
+
if isinstance(resolved_type, StructDef):
|
|
401
|
+
nested_layout = _layout_for_struct_def(resolved_type,
|
|
402
|
+
type_dict,
|
|
403
|
+
name=field_def.name)
|
|
404
|
+
nested_value = decode(nested_layout, bytearray(info.to_bytes),
|
|
405
|
+
padding_alignment_bits)
|
|
406
|
+
actual_size = nested_value.size
|
|
407
|
+
else:
|
|
408
|
+
actual_size = info.info_size
|
|
408
409
|
resolved_field_def = FieldDef(
|
|
409
410
|
name=field_def.name,
|
|
410
411
|
offset=offset,
|
|
411
|
-
size=
|
|
412
|
+
size=actual_size,
|
|
412
413
|
type=resolved_type,
|
|
413
414
|
scale=field_def.scale,
|
|
414
415
|
repeat=field_def.repeat,
|
|
@@ -418,13 +419,9 @@ def decode_field(
|
|
|
418
419
|
byte_swap=byte_swap,
|
|
419
420
|
)
|
|
420
421
|
if isinstance(resolved_type, StructDef):
|
|
421
|
-
nested_layout = _layout_for_struct_def(resolved_type,
|
|
422
|
-
type_dict,
|
|
423
|
-
name=field_def.name)
|
|
424
422
|
return FieldInstance(
|
|
425
423
|
field_def=resolved_field_def,
|
|
426
|
-
value=
|
|
427
|
-
padding_alignment_bits),
|
|
424
|
+
value=nested_value,
|
|
428
425
|
)
|
|
429
426
|
if resolved_type == "bool":
|
|
430
427
|
return FieldInstance.from_value(resolved_field_def,
|
|
@@ -506,22 +503,21 @@ def decode(
|
|
|
506
503
|
|
|
507
504
|
def append_padding_until(target_offset: InfoSize) -> None:
|
|
508
505
|
nonlocal current_position, padding_index
|
|
509
|
-
start_bits =
|
|
510
|
-
end_bits =
|
|
506
|
+
start_bits = current_position.bits
|
|
507
|
+
end_bits = target_offset.bits
|
|
511
508
|
if end_bits <= start_bits:
|
|
512
509
|
return
|
|
513
510
|
|
|
514
511
|
chunk_start_bits = start_bits
|
|
515
512
|
while chunk_start_bits < end_bits:
|
|
516
|
-
chunk_start =
|
|
517
|
-
bits_to_boundary =
|
|
518
|
-
chunk_start.align_to(padding_alignment_bits))
|
|
513
|
+
chunk_start = InfoSize(0, chunk_start_bits)
|
|
514
|
+
bits_to_boundary = chunk_start.align_to(padding_alignment_bits).bits
|
|
519
515
|
chunk_size_bits = bits_to_boundary or padding_alignment_bits
|
|
520
516
|
chunk_end_bits = min(end_bits, chunk_start_bits + chunk_size_bits)
|
|
521
517
|
padding_field_def = FieldDef(
|
|
522
518
|
name=f"padding[{padding_index}]",
|
|
523
|
-
offset=
|
|
524
|
-
size=
|
|
519
|
+
offset=InfoSize(0, chunk_start_bits),
|
|
520
|
+
size=InfoSize(0, chunk_end_bits - chunk_start_bits),
|
|
525
521
|
type="bytes",
|
|
526
522
|
description="Auto-generated padding",
|
|
527
523
|
)
|
|
@@ -536,7 +532,7 @@ def decode(
|
|
|
536
532
|
for field_def in _as_field_defs(struct_def_obj):
|
|
537
533
|
# Handle non-repeated fields
|
|
538
534
|
if field_def.repeat is None or field_def.repeat <= 1:
|
|
539
|
-
resolved_offset =
|
|
535
|
+
resolved_offset = _resolve_info_size(field_def.offset, env)
|
|
540
536
|
append_padding_until(resolved_offset)
|
|
541
537
|
field_instance = decode_field(field_def, data, env, type_dict,
|
|
542
538
|
padding_alignment_bits)
|
|
@@ -544,23 +540,36 @@ def decode(
|
|
|
544
540
|
env[field_instance.field_def.name] = field_instance.value
|
|
545
541
|
result.append_field_instance(field_instance)
|
|
546
542
|
current_position = (resolved_offset +
|
|
547
|
-
|
|
543
|
+
_resolve_info_size(field_def.size, env))
|
|
548
544
|
continue
|
|
549
545
|
# Handle repeated fields
|
|
550
|
-
current_offset =
|
|
551
|
-
resolved_size = _resolve_size(field_def, env)
|
|
546
|
+
current_offset = _resolve_info_size(field_def.offset, env)
|
|
552
547
|
append_padding_until(current_offset)
|
|
553
548
|
for i in range(field_def.repeat):
|
|
554
|
-
field_def_repeat =
|
|
555
|
-
|
|
549
|
+
field_def_repeat = _repeated_field_def(field_def, i, current_offset,
|
|
550
|
+
field_def.size)
|
|
556
551
|
field_instance = decode_field(field_def_repeat, data, env,
|
|
557
552
|
type_dict, padding_alignment_bits)
|
|
558
553
|
if field_instance is not None:
|
|
559
554
|
env[field_instance.field_def.name] = field_instance.value
|
|
560
555
|
result.append_field_instance(field_instance)
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
556
|
+
actual_size = field_instance.field_def.size
|
|
557
|
+
else:
|
|
558
|
+
actual_size = _resolve_info_size(field_def_repeat.size, env)
|
|
559
|
+
if isinstance(current_offset, InfoSize) and isinstance(
|
|
560
|
+
actual_size, InfoSize):
|
|
561
|
+
current_offset += actual_size
|
|
562
|
+
current_position = current_offset
|
|
563
|
+
|
|
564
|
+
actual_size = InfoSize(0, 0)
|
|
565
|
+
for field_instance in result.field_instances:
|
|
566
|
+
field_def = field_instance.field_def
|
|
567
|
+
if (isinstance(field_def.offset, InfoSize)
|
|
568
|
+
and isinstance(field_def.size, InfoSize)):
|
|
569
|
+
field_end = field_def.offset + field_def.size
|
|
570
|
+
if field_end > actual_size:
|
|
571
|
+
actual_size = field_end
|
|
572
|
+
result.size = actual_size
|
|
564
573
|
append_padding_until(result.size)
|
|
565
574
|
|
|
566
575
|
return result
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import json
|
|
5
|
-
import re
|
|
6
5
|
from collections.abc import MutableMapping
|
|
7
6
|
from dataclasses import dataclass, field
|
|
8
7
|
from functools import total_ordering
|
|
@@ -112,46 +111,6 @@ class FieldDef:
|
|
|
112
111
|
default=False,
|
|
113
112
|
metadata={"desc": "Whether to reverse the field byte order"})
|
|
114
113
|
|
|
115
|
-
def split_repeat(self,
|
|
116
|
-
index: int,
|
|
117
|
-
offset: InfoSize | str | None = None,
|
|
118
|
-
size: InfoSize | str | None = None) -> "FieldDef":
|
|
119
|
-
"""Create a single repeated-field definition for the given index."""
|
|
120
|
-
split_name = f"{self.name}[{index}]"
|
|
121
|
-
split_offset_source = self.offset if offset is None else offset
|
|
122
|
-
split_size_source = self.size if size is None else size
|
|
123
|
-
split_offset = self._replace_name_in_expression(split_offset_source,
|
|
124
|
-
self.name, split_name)
|
|
125
|
-
split_size = self._replace_name_in_expression(split_size_source,
|
|
126
|
-
self.name, split_name)
|
|
127
|
-
split_type = self._replace_name_in_expression(self.type, self.name,
|
|
128
|
-
split_name)
|
|
129
|
-
split_range_expression = self._replace_name_in_expression(
|
|
130
|
-
self.range_expression, self.name, split_name)
|
|
131
|
-
split_byte_swap = self._replace_name_in_expression(
|
|
132
|
-
self.byte_swap, self.name, split_name)
|
|
133
|
-
|
|
134
|
-
return FieldDef(name=split_name,
|
|
135
|
-
offset=split_offset,
|
|
136
|
-
size=split_size,
|
|
137
|
-
type=split_type,
|
|
138
|
-
scale=self.scale,
|
|
139
|
-
repeat=None,
|
|
140
|
-
description=self.description,
|
|
141
|
-
range_expression=split_range_expression,
|
|
142
|
-
enum_def_name=self.enum_def_name,
|
|
143
|
-
byte_swap=split_byte_swap)
|
|
144
|
-
|
|
145
|
-
@staticmethod
|
|
146
|
-
def _replace_name_in_expression(value: Any, old_name: str,
|
|
147
|
-
new_name: str) -> Any:
|
|
148
|
-
"""Replace standalone old_name tokens in expression-like strings."""
|
|
149
|
-
if not isinstance(value, str):
|
|
150
|
-
return value
|
|
151
|
-
pattern = (rf"(?<![0-9A-Za-z_]){re.escape(old_name)}"
|
|
152
|
-
rf"(?![0-9A-Za-z_])")
|
|
153
|
-
return re.sub(pattern, lambda _: new_name, value)
|
|
154
|
-
|
|
155
114
|
def __lt__(self, other: object) -> bool:
|
|
156
115
|
"""Compare field definitions using a stable serialized sort key."""
|
|
157
116
|
if not isinstance(other, FieldDef):
|
|
@@ -7,7 +7,7 @@ from sltcore import InfoSize
|
|
|
7
7
|
from sltcodec import (PRIMITIVE_TYPES, EnumDef, FieldDef, FieldInstance,
|
|
8
8
|
StructDef, StructInstance, StructLayout, TypeDict, decode,
|
|
9
9
|
encode)
|
|
10
|
-
from sltcodec.codec import decode_field
|
|
10
|
+
from sltcodec.codec import _resolve_info_size, decode_field
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def layout_for(struct_def):
|
|
@@ -215,6 +215,24 @@ def test_encode_recurses_for_nested_field_types():
|
|
|
215
215
|
]
|
|
216
216
|
|
|
217
217
|
|
|
218
|
+
def test_decode_field_uses_nested_struct_instance_size():
|
|
219
|
+
"""Test that nested decoding determines the field's actual size."""
|
|
220
|
+
child_field_def = FieldDef(name="value",
|
|
221
|
+
offset=InfoSize(0, 0),
|
|
222
|
+
size=InfoSize(1, 0),
|
|
223
|
+
type="unsigned int")
|
|
224
|
+
parent_field_def = FieldDef(name="nested",
|
|
225
|
+
offset=InfoSize(0, 0),
|
|
226
|
+
size=InfoSize(4, 0),
|
|
227
|
+
type=StructDef(fields=[child_field_def]))
|
|
228
|
+
|
|
229
|
+
decoded = decode_field(parent_field_def, bytearray(b"\x7f"))
|
|
230
|
+
|
|
231
|
+
assert decoded is not None
|
|
232
|
+
assert decoded.field_def.size == InfoSize(1, 0)
|
|
233
|
+
assert decoded.value.size == InfoSize(1, 0)
|
|
234
|
+
|
|
235
|
+
|
|
218
236
|
def test_repeated_field_preserves_description():
|
|
219
237
|
"""Test that repeated fields keep their description on decoded entries."""
|
|
220
238
|
field_def = FieldDef(name="value",
|
|
@@ -572,6 +590,46 @@ def test_decode_field_returns_field_instance():
|
|
|
572
590
|
assert decoded == FieldInstance(field_def=field_def, value=127)
|
|
573
591
|
|
|
574
592
|
|
|
593
|
+
def test_decode_field_sets_actual_size_on_field_instance():
|
|
594
|
+
"""Test that decode_field stores the resolved Info size."""
|
|
595
|
+
field_def = FieldDef(name="value",
|
|
596
|
+
offset="offset",
|
|
597
|
+
size="field_size",
|
|
598
|
+
type="unsigned int")
|
|
599
|
+
|
|
600
|
+
decoded = decode_field(field_def,
|
|
601
|
+
bytearray(b"\x7f"),
|
|
602
|
+
env={
|
|
603
|
+
"offset": 0,
|
|
604
|
+
"field_size": 1
|
|
605
|
+
})
|
|
606
|
+
|
|
607
|
+
assert decoded is not None
|
|
608
|
+
assert decoded.field_def.size == InfoSize(1, 0)
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def test_resolve_info_size_returns_expression_info_size_unchanged():
|
|
612
|
+
"""Test that expressions resolving to InfoSize preserve the result."""
|
|
613
|
+
info_size = InfoSize(2, 3)
|
|
614
|
+
|
|
615
|
+
resolved = _resolve_info_size("field_size", {"field_size": info_size})
|
|
616
|
+
|
|
617
|
+
assert resolved is info_size
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def test_decode_struct_instance_size_matches_decoded_layout():
|
|
621
|
+
"""Test that decode reports the complete decoded layout size."""
|
|
622
|
+
field_def = FieldDef(name="value",
|
|
623
|
+
offset=InfoSize(2, 0),
|
|
624
|
+
size=InfoSize(2, 0),
|
|
625
|
+
type="unsigned int")
|
|
626
|
+
data = bytearray(b"\x00\x00\x7f\x00")
|
|
627
|
+
|
|
628
|
+
decoded = decode(layout_for(StructDef(fields=[field_def])), data)
|
|
629
|
+
|
|
630
|
+
assert decoded.size == InfoSize(len(data), 0)
|
|
631
|
+
|
|
632
|
+
|
|
575
633
|
def test_encode_padding_fields_use_zero_value_and_extend_to_struct_size():
|
|
576
634
|
"""Test that padding fields are zeroed and trailing bytes are padded.
|
|
577
635
|
|
|
@@ -13,23 +13,6 @@ class ValueKind(Enum):
|
|
|
13
13
|
B = 2
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
def test_split_repeat_replaces_name_in_range_expression():
|
|
17
|
-
"""Test split_repeat replaces field-name tokens in range expressions."""
|
|
18
|
-
field_def = FieldDef(
|
|
19
|
-
name="value",
|
|
20
|
-
offset=InfoSize(0, 0),
|
|
21
|
-
size=InfoSize(1, 0),
|
|
22
|
-
type="unsigned int",
|
|
23
|
-
repeat=2,
|
|
24
|
-
range_expression=("0 <= value and value <= limit and value_count"))
|
|
25
|
-
|
|
26
|
-
split = field_def.split_repeat(1)
|
|
27
|
-
|
|
28
|
-
assert split.name == "value[1]"
|
|
29
|
-
assert split.range_expression == (
|
|
30
|
-
"0 <= value[1] and value[1] <= limit and value_count")
|
|
31
|
-
|
|
32
|
-
|
|
33
16
|
def test_field_def_json_round_trip():
|
|
34
17
|
"""Test that FieldDef can be serialized and deserialized from JSON."""
|
|
35
18
|
field_def = FieldDef(name="value",
|
|
@@ -65,16 +65,16 @@ wheels = [
|
|
|
65
65
|
|
|
66
66
|
[[package]]
|
|
67
67
|
name = "sltcalc"
|
|
68
|
-
version = "
|
|
68
|
+
version = "1.0.0"
|
|
69
69
|
source = { registry = "https://pypi.org/simple" }
|
|
70
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
70
|
+
sdist = { url = "https://files.pythonhosted.org/packages/b5/01/19db81d0e1f1330ab884abf282569b3f05ffe1f2c7b8aff910125295d4be/sltcalc-1.0.0.tar.gz", hash = "sha256:968053bbecc85c30caf5ddfd48962e409c7844ccfc441418d029a4f743030350", size = 10499, upload-time = "2026-08-18T14:38:14.052Z" }
|
|
71
71
|
wheels = [
|
|
72
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
72
|
+
{ url = "https://files.pythonhosted.org/packages/da/77/6d03b5c160ffe42bee2fdabdd46521033794fb02e8975b4da7a1f1c641de/sltcalc-1.0.0-py3-none-any.whl", hash = "sha256:12531fef9633b25e6743c4a41873971c83dffb763590f42e2f1f78accea08088", size = 4824, upload-time = "2026-08-18T14:38:13.247Z" },
|
|
73
73
|
]
|
|
74
74
|
|
|
75
75
|
[[package]]
|
|
76
76
|
name = "sltcodec"
|
|
77
|
-
version = "1.
|
|
77
|
+
version = "2.1.1"
|
|
78
78
|
source = { editable = "." }
|
|
79
79
|
dependencies = [
|
|
80
80
|
{ name = "sltcalc" },
|
|
@@ -88,7 +88,7 @@ dev = [
|
|
|
88
88
|
|
|
89
89
|
[package.metadata]
|
|
90
90
|
requires-dist = [
|
|
91
|
-
{ name = "sltcalc", specifier = ">=
|
|
91
|
+
{ name = "sltcalc", specifier = ">=1.0.0" },
|
|
92
92
|
{ name = "sltcore", specifier = ">=1.5.0" },
|
|
93
93
|
]
|
|
94
94
|
|
|
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
|
|
File without changes
|