sltcodec 2.2.0__tar.gz → 2.4.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.2.0 → sltcodec-2.4.0}/PKG-INFO +12 -3
- {sltcodec-2.2.0 → sltcodec-2.4.0}/README.md +14 -0
- {sltcodec-2.2.0 → sltcodec-2.4.0}/README_pypi.md +11 -2
- {sltcodec-2.2.0 → sltcodec-2.4.0}/pyproject.toml +1 -1
- {sltcodec-2.2.0 → sltcodec-2.4.0}/src/sltcodec/codec.py +128 -7
- {sltcodec-2.2.0 → sltcodec-2.4.0}/src/sltcodec/types.py +97 -2
- {sltcodec-2.2.0 → sltcodec-2.4.0}/tests/test_codec.py +116 -0
- {sltcodec-2.2.0 → sltcodec-2.4.0}/tests/test_types.py +82 -1
- {sltcodec-2.2.0 → sltcodec-2.4.0}/uv.lock +1 -1
- {sltcodec-2.2.0 → sltcodec-2.4.0}/.github/workflows/publish_to_pypi.yml +0 -0
- {sltcodec-2.2.0 → sltcodec-2.4.0}/.github/workflows/publish_to_testpypi.yml +0 -0
- {sltcodec-2.2.0 → sltcodec-2.4.0}/.gitignore +0 -0
- {sltcodec-2.2.0 → sltcodec-2.4.0}/.python-version +0 -0
- {sltcodec-2.2.0 → sltcodec-2.4.0}/.vscode/launch.json +0 -0
- {sltcodec-2.2.0 → sltcodec-2.4.0}/.vscode/settings.json +0 -0
- {sltcodec-2.2.0 → sltcodec-2.4.0}/LICENSE +0 -0
- {sltcodec-2.2.0 → sltcodec-2.4.0}/bump_major.ps1 +0 -0
- {sltcodec-2.2.0 → sltcodec-2.4.0}/bump_minor.ps1 +0 -0
- {sltcodec-2.2.0 → sltcodec-2.4.0}/bump_patch.ps1 +0 -0
- {sltcodec-2.2.0 → sltcodec-2.4.0}/src/sltcodec/__init__.py +0 -0
- {sltcodec-2.2.0 → sltcodec-2.4.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.4.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
|
|
@@ -69,6 +69,11 @@ decoded layout. `decode_field` stores the actual decoded size in the returned
|
|
|
69
69
|
from the nested `StructInstance.size`; for repeated fields, the next element's
|
|
70
70
|
offset advances by the actual size of the previous element.
|
|
71
71
|
|
|
72
|
+
`FieldDef.repeat` also accepts the special string value `"end"`. When
|
|
73
|
+
`repeat="end"`, decoding repeats until the input bytearray ends. Decoding
|
|
74
|
+
stops when the next element offset is out of range or when the next element
|
|
75
|
+
size would exceed the remaining input.
|
|
76
|
+
|
|
72
77
|
## Field Types
|
|
73
78
|
|
|
74
79
|
The public `PRIMITIVE_TYPES` set contains:
|
|
@@ -77,7 +82,7 @@ The public `PRIMITIVE_TYPES` set contains:
|
|
|
77
82
|
{"bool", "signed int", "int", "unsigned int", "float", "bytearray", "bytes"}
|
|
78
83
|
```
|
|
79
84
|
|
|
80
|
-
`FieldDef.type` may also be a nested `StructDef` or an expression string. Expressions can use values from previously processed fields, allowing dynamic types and
|
|
85
|
+
`FieldDef.type` may also be a nested `StructDef` or an expression string. Expressions can use values from previously processed fields, allowing dynamic types, sizes, and repeat counts. Repeated fields, byte swapping, padding, ranges, and enum metadata are supported.
|
|
81
86
|
|
|
82
87
|
## Enums And Named Structures
|
|
83
88
|
|
|
@@ -180,7 +185,7 @@ internal implementation details and are omitted from the public method tables.
|
|
|
180
185
|
| `size` | `InfoSize \| str` | Static size or expression. |
|
|
181
186
|
| `type` | `str \| StructDef` | Primitive, named, or nested field type. |
|
|
182
187
|
| `scale` | `float` | Numeric scale applied to the field. |
|
|
183
|
-
| `repeat` | `int \| None` | Number of repeated field values. |
|
|
188
|
+
| `repeat` | `int \| str \| None` | Number of repeated field values, an expression that evaluates to the count, or `"end"` to decode repeatedly until input end. |
|
|
184
189
|
| `description` | `str \| None` | Optional field description. |
|
|
185
190
|
| `range_expression` | `str \| None` | Optional value-range expression. |
|
|
186
191
|
| `enum_def_name` | `str \| None` | Name of the associated enum definition. |
|
|
@@ -220,6 +225,8 @@ internal implementation details and are omitted from the public method tables.
|
|
|
220
225
|
|
|
221
226
|
| Method | Description |
|
|
222
227
|
| --- | --- |
|
|
228
|
+
| `get_field(struct_def_name)` | Return the first field whose `type` references the given structure name. |
|
|
229
|
+
| `get_fields(struct_def_name)` | Return all fields whose `type` references the given structure name. |
|
|
223
230
|
| `to_dict()` | Convert the definition to a JSON-compatible dictionary. |
|
|
224
231
|
| `to_json()` | Convert the definition to a JSON string. |
|
|
225
232
|
| `from_dict(data)` | Create a `StructDef` from a dictionary or legacy field list. |
|
|
@@ -238,6 +245,8 @@ internal implementation details and are omitted from the public method tables.
|
|
|
238
245
|
| --- | --- |
|
|
239
246
|
| `append_field_instance(field_instance)` | Append one field instance. |
|
|
240
247
|
| `extend_field_instances(field_instances)` | Append multiple field instances. |
|
|
248
|
+
| `get_field(field_def_name)` | Return the first field instance whose `field_def.name` matches. |
|
|
249
|
+
| `get_fields(field_def_name)` | Return all field instances whose `field_def.name` matches. |
|
|
241
250
|
| `__iter__()` | Iterate over field instances. |
|
|
242
251
|
| `__len__()` | Return the number of field instances. |
|
|
243
252
|
| `__getitem__(index)` | Get a field instance by index. |
|
|
@@ -80,6 +80,20 @@ stores the actual size in `FieldInstance.field_def.size`. For nested structures,
|
|
|
80
80
|
the nested `StructInstance.size` is used; repeated fields advance each next
|
|
81
81
|
offset by the preceding element's actual size.
|
|
82
82
|
|
|
83
|
+
`FieldDef.repeat` also supports the special string value `"end"`.
|
|
84
|
+
When `repeat="end"`, decoding repeats until the input bytearray ends.
|
|
85
|
+
The decoder stops when the next element offset is out of range or when the
|
|
86
|
+
next element size would exceed the remaining input.
|
|
87
|
+
|
|
88
|
+
`StructDef` and `StructInstance` provide indexed accessors:
|
|
89
|
+
|
|
90
|
+
- `get_field(name)`: return the first matching item, or `None`.
|
|
91
|
+
- `get_fields(name)`: return all matching items as a list.
|
|
92
|
+
|
|
93
|
+
For `StructDef`, these accessors resolve by referenced structure name in
|
|
94
|
+
`FieldDef.type` (non-primitive types only). For `StructInstance`, they resolve
|
|
95
|
+
by `FieldInstance.field_def.name`.
|
|
96
|
+
|
|
83
97
|
## Versioning And Release
|
|
84
98
|
|
|
85
99
|
Version bump scripts are provided for patch, minor, and major releases:
|
|
@@ -52,6 +52,11 @@ decoded layout. `decode_field` stores the actual decoded size in the returned
|
|
|
52
52
|
from the nested `StructInstance.size`; for repeated fields, the next element's
|
|
53
53
|
offset advances by the actual size of the previous element.
|
|
54
54
|
|
|
55
|
+
`FieldDef.repeat` also accepts the special string value `"end"`. When
|
|
56
|
+
`repeat="end"`, decoding repeats until the input bytearray ends. Decoding
|
|
57
|
+
stops when the next element offset is out of range or when the next element
|
|
58
|
+
size would exceed the remaining input.
|
|
59
|
+
|
|
55
60
|
## Field Types
|
|
56
61
|
|
|
57
62
|
The public `PRIMITIVE_TYPES` set contains:
|
|
@@ -60,7 +65,7 @@ The public `PRIMITIVE_TYPES` set contains:
|
|
|
60
65
|
{"bool", "signed int", "int", "unsigned int", "float", "bytearray", "bytes"}
|
|
61
66
|
```
|
|
62
67
|
|
|
63
|
-
`FieldDef.type` may also be a nested `StructDef` or an expression string. Expressions can use values from previously processed fields, allowing dynamic types and
|
|
68
|
+
`FieldDef.type` may also be a nested `StructDef` or an expression string. Expressions can use values from previously processed fields, allowing dynamic types, sizes, and repeat counts. Repeated fields, byte swapping, padding, ranges, and enum metadata are supported.
|
|
64
69
|
|
|
65
70
|
## Enums And Named Structures
|
|
66
71
|
|
|
@@ -163,7 +168,7 @@ internal implementation details and are omitted from the public method tables.
|
|
|
163
168
|
| `size` | `InfoSize \| str` | Static size or expression. |
|
|
164
169
|
| `type` | `str \| StructDef` | Primitive, named, or nested field type. |
|
|
165
170
|
| `scale` | `float` | Numeric scale applied to the field. |
|
|
166
|
-
| `repeat` | `int \| None` | Number of repeated field values. |
|
|
171
|
+
| `repeat` | `int \| str \| None` | Number of repeated field values, an expression that evaluates to the count, or `"end"` to decode repeatedly until input end. |
|
|
167
172
|
| `description` | `str \| None` | Optional field description. |
|
|
168
173
|
| `range_expression` | `str \| None` | Optional value-range expression. |
|
|
169
174
|
| `enum_def_name` | `str \| None` | Name of the associated enum definition. |
|
|
@@ -203,6 +208,8 @@ internal implementation details and are omitted from the public method tables.
|
|
|
203
208
|
|
|
204
209
|
| Method | Description |
|
|
205
210
|
| --- | --- |
|
|
211
|
+
| `get_field(struct_def_name)` | Return the first field whose `type` references the given structure name. |
|
|
212
|
+
| `get_fields(struct_def_name)` | Return all fields whose `type` references the given structure name. |
|
|
206
213
|
| `to_dict()` | Convert the definition to a JSON-compatible dictionary. |
|
|
207
214
|
| `to_json()` | Convert the definition to a JSON string. |
|
|
208
215
|
| `from_dict(data)` | Create a `StructDef` from a dictionary or legacy field list. |
|
|
@@ -221,6 +228,8 @@ internal implementation details and are omitted from the public method tables.
|
|
|
221
228
|
| --- | --- |
|
|
222
229
|
| `append_field_instance(field_instance)` | Append one field instance. |
|
|
223
230
|
| `extend_field_instances(field_instances)` | Append multiple field instances. |
|
|
231
|
+
| `get_field(field_def_name)` | Return the first field instance whose `field_def.name` matches. |
|
|
232
|
+
| `get_fields(field_def_name)` | Return all field instances whose `field_def.name` matches. |
|
|
224
233
|
| `__iter__()` | Iterate over field instances. |
|
|
225
234
|
| `__len__()` | Return the number of field instances. |
|
|
226
235
|
| `__getitem__(index)` | Get a field instance by index. |
|
|
@@ -149,6 +149,15 @@ def _resolve_byte_swap(field_def: FieldDef, env: dict[str, Any]) -> bool:
|
|
|
149
149
|
return field_def.byte_swap
|
|
150
150
|
|
|
151
151
|
|
|
152
|
+
def _resolve_repeat(value: int | str | None, env: dict[str, Any]) -> int | None:
|
|
153
|
+
"""Resolve a static or expression-based repeat count."""
|
|
154
|
+
if isinstance(value, str):
|
|
155
|
+
if value == "end":
|
|
156
|
+
return None
|
|
157
|
+
return int(SltEval(env).eval(value))
|
|
158
|
+
return value
|
|
159
|
+
|
|
160
|
+
|
|
152
161
|
def _is_padding_field_def(field_def: FieldDef) -> bool:
|
|
153
162
|
"""Check whether a field definition represents padding."""
|
|
154
163
|
return (field_def.name.startswith("padding[")
|
|
@@ -328,17 +337,22 @@ def encode(
|
|
|
328
337
|
|
|
329
338
|
for field_value in struct_instance.field_instances:
|
|
330
339
|
field_def = field_value.field_def
|
|
340
|
+
repeat_until_end = field_def.repeat == "end"
|
|
341
|
+
repeat = _resolve_repeat(field_def.repeat, env)
|
|
331
342
|
if _is_padding_field_def(field_def):
|
|
332
343
|
has_padding = True
|
|
333
344
|
value = field_value.value
|
|
334
345
|
if _is_padding_field_def(field_def):
|
|
335
346
|
padding_size = _resolve_info_size(field_def.size, env)
|
|
336
347
|
value = bytearray(padding_size.bytes)
|
|
337
|
-
if
|
|
348
|
+
if repeat_until_end:
|
|
349
|
+
repeat_values = list(value)
|
|
350
|
+
repeat = len(repeat_values)
|
|
351
|
+
if repeat is not None and repeat > 1:
|
|
338
352
|
current_offset = _resolve_info_size(field_def.offset, env)
|
|
339
|
-
values = list(value)
|
|
353
|
+
values = repeat_values if repeat_until_end else list(value)
|
|
340
354
|
|
|
341
|
-
for i in range(
|
|
355
|
+
for i in range(repeat):
|
|
342
356
|
field_def_repeat = _repeated_field_def(field_def, i,
|
|
343
357
|
current_offset,
|
|
344
358
|
field_def.size)
|
|
@@ -500,6 +514,27 @@ def decode(
|
|
|
500
514
|
result = StructInstance(struct_def=struct_def_obj)
|
|
501
515
|
current_position = InfoSize(0, 0)
|
|
502
516
|
padding_index = 0
|
|
517
|
+
stop_due_to_out_of_range = False
|
|
518
|
+
data_bits = len(data) * 8
|
|
519
|
+
field_defs = _as_field_defs(struct_def_obj)
|
|
520
|
+
|
|
521
|
+
def is_out_of_range(offset: InfoSize, size: InfoSize) -> bool:
|
|
522
|
+
"""Check whether the field range exceeds available input bits."""
|
|
523
|
+
if size.bits <= 0:
|
|
524
|
+
return False
|
|
525
|
+
if offset.bits < 0:
|
|
526
|
+
return True
|
|
527
|
+
if offset.bits >= data_bits:
|
|
528
|
+
return True
|
|
529
|
+
return offset.bits + size.bits > data_bits
|
|
530
|
+
|
|
531
|
+
def append_none_tail(start_index: int, first_field_def: FieldDef) -> None:
|
|
532
|
+
"""Append current and remaining fields with None values."""
|
|
533
|
+
result.field_instances.append(
|
|
534
|
+
FieldInstance(field_def=first_field_def, value=None))
|
|
535
|
+
for tail_field_def in field_defs[start_index + 1:]:
|
|
536
|
+
result.field_instances.append(
|
|
537
|
+
FieldInstance(field_def=tail_field_def, value=None))
|
|
503
538
|
|
|
504
539
|
def append_padding_until(target_offset: InfoSize) -> None:
|
|
505
540
|
nonlocal current_position, padding_index
|
|
@@ -529,10 +564,18 @@ def decode(
|
|
|
529
564
|
|
|
530
565
|
current_position = target_offset
|
|
531
566
|
|
|
532
|
-
for field_def in
|
|
567
|
+
for field_index, field_def in enumerate(field_defs):
|
|
568
|
+
repeat_until_end = field_def.repeat == "end"
|
|
569
|
+
repeat = (None if repeat_until_end else _resolve_repeat(
|
|
570
|
+
field_def.repeat, env))
|
|
533
571
|
# Handle non-repeated fields
|
|
534
|
-
if
|
|
572
|
+
if not repeat_until_end and (repeat is None or repeat <= 1):
|
|
535
573
|
resolved_offset = _resolve_info_size(field_def.offset, env)
|
|
574
|
+
resolved_size = _resolve_info_size(field_def.size, env)
|
|
575
|
+
if is_out_of_range(resolved_offset, resolved_size):
|
|
576
|
+
append_none_tail(field_index, field_def)
|
|
577
|
+
stop_due_to_out_of_range = True
|
|
578
|
+
break
|
|
536
579
|
append_padding_until(resolved_offset)
|
|
537
580
|
field_instance = decode_field(field_def, data, env, type_dict,
|
|
538
581
|
padding_alignment_bits)
|
|
@@ -544,10 +587,79 @@ def decode(
|
|
|
544
587
|
continue
|
|
545
588
|
# Handle repeated fields
|
|
546
589
|
current_offset = _resolve_info_size(field_def.offset, env)
|
|
547
|
-
|
|
548
|
-
|
|
590
|
+
if repeat_until_end:
|
|
591
|
+
stop_decoding = False
|
|
592
|
+
i = 0
|
|
593
|
+
while True:
|
|
594
|
+
if current_offset.bits >= data_bits:
|
|
595
|
+
if i == 0:
|
|
596
|
+
append_none_tail(
|
|
597
|
+
field_index,
|
|
598
|
+
_repeated_field_def(field_def, i, current_offset,
|
|
599
|
+
field_def.size),
|
|
600
|
+
)
|
|
601
|
+
stop_decoding = True
|
|
602
|
+
break
|
|
603
|
+
|
|
604
|
+
append_padding_until(current_offset)
|
|
605
|
+
|
|
606
|
+
field_def_repeat = _repeated_field_def(field_def, i,
|
|
607
|
+
current_offset,
|
|
608
|
+
field_def.size)
|
|
609
|
+
resolved_size = _resolve_info_size(field_def_repeat.size, env)
|
|
610
|
+
if resolved_size.bits <= 0:
|
|
611
|
+
break
|
|
612
|
+
if is_out_of_range(current_offset, resolved_size):
|
|
613
|
+
if i == 0:
|
|
614
|
+
append_none_tail(field_index, field_def_repeat)
|
|
615
|
+
stop_decoding = True
|
|
616
|
+
break
|
|
617
|
+
|
|
618
|
+
field_instance = decode_field(field_def_repeat, data, env,
|
|
619
|
+
type_dict, padding_alignment_bits)
|
|
620
|
+
if field_instance is not None:
|
|
621
|
+
env[field_instance.field_def.name] = field_instance.value
|
|
622
|
+
result.append_field_instance(field_instance)
|
|
623
|
+
actual_size = field_instance.field_def.size
|
|
624
|
+
else:
|
|
625
|
+
actual_size = resolved_size
|
|
626
|
+
|
|
627
|
+
if isinstance(current_offset, InfoSize) and isinstance(
|
|
628
|
+
actual_size, InfoSize):
|
|
629
|
+
current_offset += actual_size
|
|
630
|
+
current_position = current_offset
|
|
631
|
+
i += 1
|
|
632
|
+
if stop_decoding:
|
|
633
|
+
stop_due_to_out_of_range = True
|
|
634
|
+
break
|
|
635
|
+
continue
|
|
636
|
+
|
|
637
|
+
for i in range(repeat):
|
|
549
638
|
field_def_repeat = _repeated_field_def(field_def, i, current_offset,
|
|
550
639
|
field_def.size)
|
|
640
|
+
resolved_size = _resolve_info_size(field_def_repeat.size, env)
|
|
641
|
+
if is_out_of_range(current_offset, resolved_size):
|
|
642
|
+
result.field_instances.append(
|
|
643
|
+
FieldInstance(field_def=field_def_repeat, value=None))
|
|
644
|
+
next_offset = current_offset
|
|
645
|
+
for remaining_index in range(i + 1, repeat):
|
|
646
|
+
if isinstance(next_offset, InfoSize):
|
|
647
|
+
next_offset += resolved_size
|
|
648
|
+
remaining_field_def = _repeated_field_def(
|
|
649
|
+
field_def,
|
|
650
|
+
remaining_index,
|
|
651
|
+
next_offset,
|
|
652
|
+
field_def.size,
|
|
653
|
+
)
|
|
654
|
+
result.field_instances.append(
|
|
655
|
+
FieldInstance(field_def=remaining_field_def,
|
|
656
|
+
value=None))
|
|
657
|
+
for tail_field_def in field_defs[field_index + 1:]:
|
|
658
|
+
result.field_instances.append(
|
|
659
|
+
FieldInstance(field_def=tail_field_def, value=None))
|
|
660
|
+
stop_due_to_out_of_range = True
|
|
661
|
+
break
|
|
662
|
+
append_padding_until(current_offset)
|
|
551
663
|
field_instance = decode_field(field_def_repeat, data, env,
|
|
552
664
|
type_dict, padding_alignment_bits)
|
|
553
665
|
if field_instance is not None:
|
|
@@ -560,6 +672,15 @@ def decode(
|
|
|
560
672
|
actual_size, InfoSize):
|
|
561
673
|
current_offset += actual_size
|
|
562
674
|
current_position = current_offset
|
|
675
|
+
else:
|
|
676
|
+
continue
|
|
677
|
+
break
|
|
678
|
+
|
|
679
|
+
if stop_due_to_out_of_range:
|
|
680
|
+
result._sort_field_instances()
|
|
681
|
+
result._update_size()
|
|
682
|
+
result._rebuild_field_def_name_index()
|
|
683
|
+
return result
|
|
563
684
|
|
|
564
685
|
actual_size = InfoSize(0, 0)
|
|
565
686
|
for field_instance in result.field_instances:
|
|
@@ -10,6 +10,16 @@ from typing import Any, Dict, Iterator, Optional
|
|
|
10
10
|
from sltcalc import SltEval
|
|
11
11
|
from sltcore import Info, InfoSize
|
|
12
12
|
|
|
13
|
+
_PRIMITIVE_FIELD_TYPES = {
|
|
14
|
+
"bool",
|
|
15
|
+
"signed int",
|
|
16
|
+
"int",
|
|
17
|
+
"unsigned int",
|
|
18
|
+
"float",
|
|
19
|
+
"bytearray",
|
|
20
|
+
"bytes",
|
|
21
|
+
}
|
|
22
|
+
|
|
13
23
|
|
|
14
24
|
@total_ordering
|
|
15
25
|
@dataclass(frozen=True)
|
|
@@ -98,7 +108,7 @@ class FieldDef:
|
|
|
98
108
|
metadata={"desc": "The type of the field"})
|
|
99
109
|
scale: float = field(default=1.0,
|
|
100
110
|
metadata={"desc": "The scale of the field"})
|
|
101
|
-
repeat: int | None = field(
|
|
111
|
+
repeat: int | str | None = field(
|
|
102
112
|
default=None, metadata={"desc": "The repeat count of the field"})
|
|
103
113
|
description: str | None = field(
|
|
104
114
|
default=None, metadata={"desc": "The description of the field"})
|
|
@@ -231,7 +241,7 @@ class FieldDef:
|
|
|
231
241
|
self.name,
|
|
232
242
|
self._sortable_type(self.type),
|
|
233
243
|
self.scale,
|
|
234
|
-
|
|
244
|
+
self._sortable_repeat(self.repeat),
|
|
235
245
|
"" if self.description is None else self.description,
|
|
236
246
|
"" if self.range_expression is None else self.range_expression,
|
|
237
247
|
"" if self.enum_def_name is None else self.enum_def_name,
|
|
@@ -245,6 +255,15 @@ class FieldDef:
|
|
|
245
255
|
return (0, value.byte, value.bit)
|
|
246
256
|
return (1, value)
|
|
247
257
|
|
|
258
|
+
@staticmethod
|
|
259
|
+
def _sortable_repeat(value: int | str | None) -> tuple[int, int | str]:
|
|
260
|
+
"""Build a comparable key for static or expression repeat counts."""
|
|
261
|
+
if value is None:
|
|
262
|
+
return 0, -1
|
|
263
|
+
if isinstance(value, int):
|
|
264
|
+
return 1, value
|
|
265
|
+
return 2, value
|
|
266
|
+
|
|
248
267
|
@staticmethod
|
|
249
268
|
def _sortable_type(value: str | "StructDef") -> tuple[Any, ...]:
|
|
250
269
|
"""Build a comparable key for primitive or nested field types."""
|
|
@@ -358,6 +377,16 @@ class StructDef:
|
|
|
358
377
|
default_factory=list,
|
|
359
378
|
metadata={"desc": "The fields of the structure"},
|
|
360
379
|
)
|
|
380
|
+
_fields_by_struct_def_name: dict[str, list[FieldDef]] = field(
|
|
381
|
+
default_factory=dict,
|
|
382
|
+
init=False,
|
|
383
|
+
repr=False,
|
|
384
|
+
compare=False,
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
def __post_init__(self) -> None:
|
|
388
|
+
"""Build field index grouped by referenced struct definition name."""
|
|
389
|
+
self._rebuild_struct_def_name_index()
|
|
361
390
|
|
|
362
391
|
def __lt__(self, other: object) -> bool:
|
|
363
392
|
"""Compare structure definitions using a stable serialized sort key."""
|
|
@@ -374,6 +403,42 @@ class StructDef:
|
|
|
374
403
|
sort_keys=True),
|
|
375
404
|
)
|
|
376
405
|
|
|
406
|
+
def get_fields(self, struct_def_name: str) -> list[FieldDef]:
|
|
407
|
+
"""Return fields that reference the given struct definition name."""
|
|
408
|
+
self._rebuild_struct_def_name_index()
|
|
409
|
+
return list(self._fields_by_struct_def_name.get(struct_def_name, []))
|
|
410
|
+
|
|
411
|
+
def get_field(self, struct_def_name: str) -> FieldDef | None:
|
|
412
|
+
"""Return the first field that references the given name."""
|
|
413
|
+
self._rebuild_struct_def_name_index()
|
|
414
|
+
fields = self._fields_by_struct_def_name.get(struct_def_name)
|
|
415
|
+
if not fields:
|
|
416
|
+
return None
|
|
417
|
+
return fields[0]
|
|
418
|
+
|
|
419
|
+
@staticmethod
|
|
420
|
+
def _field_struct_def_name(field_def: FieldDef) -> str | None:
|
|
421
|
+
"""Resolve struct definition name referenced by a field type."""
|
|
422
|
+
field_type = field_def.type
|
|
423
|
+
if isinstance(field_type, StructDef):
|
|
424
|
+
return field_type.name or None
|
|
425
|
+
if (isinstance(field_type, str) and field_type
|
|
426
|
+
and field_type not in _PRIMITIVE_FIELD_TYPES):
|
|
427
|
+
return field_type
|
|
428
|
+
return None
|
|
429
|
+
|
|
430
|
+
def _rebuild_struct_def_name_index(self) -> None:
|
|
431
|
+
"""Rebuild struct_def_name index from current fields."""
|
|
432
|
+
fields_by_name: dict[str, list[FieldDef]] = {}
|
|
433
|
+
for field_def in self.fields:
|
|
434
|
+
struct_def_name = self._field_struct_def_name(field_def)
|
|
435
|
+
if struct_def_name is None:
|
|
436
|
+
continue
|
|
437
|
+
if struct_def_name not in fields_by_name:
|
|
438
|
+
fields_by_name[struct_def_name] = []
|
|
439
|
+
fields_by_name[struct_def_name].append(field_def)
|
|
440
|
+
object.__setattr__(self, "_fields_by_struct_def_name", fields_by_name)
|
|
441
|
+
|
|
377
442
|
def to_dict(self) -> dict[str, Any]:
|
|
378
443
|
"""Convert this structure definition to JSON-serializable data."""
|
|
379
444
|
return {
|
|
@@ -423,6 +488,11 @@ class StructInstance:
|
|
|
423
488
|
default_factory=InfoSize,
|
|
424
489
|
metadata={"desc": "The total size of the structure instance"},
|
|
425
490
|
)
|
|
491
|
+
_field_instances_by_field_def_name: dict[str, list[FieldInstance]] = field(
|
|
492
|
+
default_factory=dict,
|
|
493
|
+
init=False,
|
|
494
|
+
repr=False,
|
|
495
|
+
)
|
|
426
496
|
|
|
427
497
|
def __post_init__(self) -> None:
|
|
428
498
|
"""Normalize stored field instances to sorted order."""
|
|
@@ -460,6 +530,19 @@ class StructInstance:
|
|
|
460
530
|
"""Return one field instance by index."""
|
|
461
531
|
return self.field_instances[index]
|
|
462
532
|
|
|
533
|
+
def get_fields(self, field_def_name: str) -> list[FieldInstance]:
|
|
534
|
+
"""Return field instances that match the given field definition name."""
|
|
535
|
+
return list(
|
|
536
|
+
self._field_instances_by_field_def_name.get(field_def_name, []))
|
|
537
|
+
|
|
538
|
+
def get_field(self, field_def_name: str) -> FieldInstance | None:
|
|
539
|
+
"""Return the first field instance that matches the given name."""
|
|
540
|
+
field_instances = self._field_instances_by_field_def_name.get(
|
|
541
|
+
field_def_name)
|
|
542
|
+
if not field_instances:
|
|
543
|
+
return None
|
|
544
|
+
return field_instances[0]
|
|
545
|
+
|
|
463
546
|
def __lt__(self, other: object) -> bool:
|
|
464
547
|
"""Compare structure instances using a stable serialized sort key."""
|
|
465
548
|
if not isinstance(other, StructInstance):
|
|
@@ -481,6 +564,16 @@ class StructInstance:
|
|
|
481
564
|
"""Keep field instances sorted by FieldDef order."""
|
|
482
565
|
self.field_instances.sort()
|
|
483
566
|
|
|
567
|
+
def _rebuild_field_def_name_index(self) -> None:
|
|
568
|
+
"""Rebuild field_def.name index from current field instances."""
|
|
569
|
+
field_instances_by_name: dict[str, list[FieldInstance]] = {}
|
|
570
|
+
for field_instance in self.field_instances:
|
|
571
|
+
field_def_name = field_instance.field_def.name
|
|
572
|
+
if field_def_name not in field_instances_by_name:
|
|
573
|
+
field_instances_by_name[field_def_name] = []
|
|
574
|
+
field_instances_by_name[field_def_name].append(field_instance)
|
|
575
|
+
self._field_instances_by_field_def_name = field_instances_by_name
|
|
576
|
+
|
|
484
577
|
def _rebuild_padding_field_instances(self) -> None:
|
|
485
578
|
"""Rebuild padding field instances for gaps between stored values."""
|
|
486
579
|
non_padding_instances = [
|
|
@@ -498,6 +591,7 @@ class StructInstance:
|
|
|
498
591
|
self.field_instances = non_padding_instances
|
|
499
592
|
self._sort_field_instances()
|
|
500
593
|
self._update_size()
|
|
594
|
+
self._rebuild_field_def_name_index()
|
|
501
595
|
return
|
|
502
596
|
|
|
503
597
|
rebuilt_instances: list[FieldInstance] = []
|
|
@@ -546,6 +640,7 @@ class StructInstance:
|
|
|
546
640
|
self.field_instances = rebuilt_instances
|
|
547
641
|
self._sort_field_instances()
|
|
548
642
|
self._update_size()
|
|
643
|
+
self._rebuild_field_def_name_index()
|
|
549
644
|
|
|
550
645
|
def _update_size(self) -> None:
|
|
551
646
|
"""Update the instance size from the current field layout."""
|
|
@@ -166,6 +166,122 @@ def test_encode_layout_handles_repeat():
|
|
|
166
166
|
]
|
|
167
167
|
|
|
168
168
|
|
|
169
|
+
def test_expression_repeat_uses_previous_field_value():
|
|
170
|
+
"""Test that repeat expressions are evaluated against the field env."""
|
|
171
|
+
seed_field = FieldDef(name="count",
|
|
172
|
+
offset=InfoSize(0, 0),
|
|
173
|
+
size=InfoSize(1, 0),
|
|
174
|
+
type="unsigned int")
|
|
175
|
+
repeated_field = FieldDef(name="value",
|
|
176
|
+
offset=InfoSize(1, 0),
|
|
177
|
+
size=InfoSize(1, 0),
|
|
178
|
+
type="unsigned int",
|
|
179
|
+
repeat="count + 1")
|
|
180
|
+
struct_def = StructDef(fields=[seed_field, repeated_field])
|
|
181
|
+
|
|
182
|
+
encoded = encode(
|
|
183
|
+
layout_for(struct_def),
|
|
184
|
+
StructInstance(
|
|
185
|
+
struct_def=struct_def,
|
|
186
|
+
field_instances=[
|
|
187
|
+
FieldInstance(seed_field, 2),
|
|
188
|
+
FieldInstance(repeated_field, [5, 6, 7]),
|
|
189
|
+
],
|
|
190
|
+
),
|
|
191
|
+
bytearray(),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
assert encoded == bytearray(b"\x02\x05\x06\x07")
|
|
195
|
+
decoded = decode(layout_for(struct_def), encoded)
|
|
196
|
+
|
|
197
|
+
assert [instance.value
|
|
198
|
+
for instance in decoded.field_instances] == [2, 5, 6, 7]
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def test_repeat_end_decodes_until_bytearray_end():
|
|
202
|
+
"""Test repeat='end' decodes values until the input bytearray ends."""
|
|
203
|
+
field_def = FieldDef(name="value",
|
|
204
|
+
offset=InfoSize(0, 0),
|
|
205
|
+
size=InfoSize(1, 0),
|
|
206
|
+
type="unsigned int",
|
|
207
|
+
repeat="end")
|
|
208
|
+
|
|
209
|
+
decoded = decode(layout_for([field_def]), bytearray(b"\x01\x02\x03"))
|
|
210
|
+
|
|
211
|
+
assert [instance.field_def.name
|
|
212
|
+
for instance in decoded.field_instances] == [
|
|
213
|
+
"value[0]",
|
|
214
|
+
"value[1]",
|
|
215
|
+
"value[2]",
|
|
216
|
+
]
|
|
217
|
+
assert [instance.value for instance in decoded.field_instances] == [1, 2, 3]
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def test_repeat_end_stops_when_offset_is_out_of_range():
|
|
221
|
+
"""Test repeat='end' stops when next element offset is out of range."""
|
|
222
|
+
field_def = FieldDef(name="value",
|
|
223
|
+
offset=InfoSize(4, 0),
|
|
224
|
+
size=InfoSize(1, 0),
|
|
225
|
+
type="unsigned int",
|
|
226
|
+
repeat="end")
|
|
227
|
+
|
|
228
|
+
decoded = decode(layout_for([field_def]), bytearray(b"\x01\x02\x03"))
|
|
229
|
+
|
|
230
|
+
assert [instance.field_def.name
|
|
231
|
+
for instance in decoded.field_instances] == ["value[0]"]
|
|
232
|
+
assert [instance.value for instance in decoded.field_instances] == [None]
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def test_decode_out_of_range_sets_current_and_tail_values_to_none():
|
|
236
|
+
"""Test out-of-range decode fills current/tail field values with None."""
|
|
237
|
+
fields = [
|
|
238
|
+
FieldDef(name="head",
|
|
239
|
+
offset=InfoSize(0, 0),
|
|
240
|
+
size=InfoSize(1, 0),
|
|
241
|
+
type="unsigned int"),
|
|
242
|
+
FieldDef(name="far",
|
|
243
|
+
offset=InfoSize(10, 0),
|
|
244
|
+
size=InfoSize(1, 0),
|
|
245
|
+
type="unsigned int"),
|
|
246
|
+
FieldDef(name="tail",
|
|
247
|
+
offset=InfoSize(11, 0),
|
|
248
|
+
size=InfoSize(1, 0),
|
|
249
|
+
type="unsigned int"),
|
|
250
|
+
]
|
|
251
|
+
|
|
252
|
+
decoded = decode(layout_for(fields), bytearray(b"\x11"))
|
|
253
|
+
|
|
254
|
+
assert [instance.field_def.name
|
|
255
|
+
for instance in decoded.field_instances] == ["head", "far", "tail"]
|
|
256
|
+
assert [instance.value
|
|
257
|
+
for instance in decoded.field_instances] == [17, None, None]
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def test_decode_out_of_range_skips_following_field_expression_evaluation():
|
|
261
|
+
"""Test out-of-range decode skips evaluating following expressions."""
|
|
262
|
+
fields = [
|
|
263
|
+
FieldDef(name="head",
|
|
264
|
+
offset=InfoSize(0, 0),
|
|
265
|
+
size=InfoSize(1, 0),
|
|
266
|
+
type="unsigned int"),
|
|
267
|
+
FieldDef(name="far",
|
|
268
|
+
offset=InfoSize(10, 0),
|
|
269
|
+
size=InfoSize(1, 0),
|
|
270
|
+
type="unsigned int"),
|
|
271
|
+
FieldDef(name="tail",
|
|
272
|
+
offset="1 / 0",
|
|
273
|
+
size=InfoSize(1, 0),
|
|
274
|
+
type="unsigned int"),
|
|
275
|
+
]
|
|
276
|
+
|
|
277
|
+
decoded = decode(layout_for(fields), bytearray(b"\x11"))
|
|
278
|
+
|
|
279
|
+
assert [instance.field_def.name
|
|
280
|
+
for instance in decoded.field_instances] == ["head", "far", "tail"]
|
|
281
|
+
assert [instance.value
|
|
282
|
+
for instance in decoded.field_instances] == [17, None, None]
|
|
283
|
+
|
|
284
|
+
|
|
169
285
|
def test_encode_recurses_for_nested_field_types():
|
|
170
286
|
"""Test that encoding a field with a nested field type works correctly."""
|
|
171
287
|
child_field_defs = [
|
|
@@ -39,7 +39,7 @@ def test_field_def_to_json_from_json_round_trip():
|
|
|
39
39
|
size=InfoSize(1, 0),
|
|
40
40
|
type="unsigned int",
|
|
41
41
|
scale=2.0,
|
|
42
|
-
repeat=
|
|
42
|
+
repeat="count + 1",
|
|
43
43
|
description="A repeated value",
|
|
44
44
|
range_expression="0 <= value <= 255",
|
|
45
45
|
enum_def_name=enum_def.name)
|
|
@@ -48,6 +48,7 @@ def test_field_def_to_json_from_json_round_trip():
|
|
|
48
48
|
restored = FieldDef.from_json(payload)
|
|
49
49
|
|
|
50
50
|
assert restored == field_def
|
|
51
|
+
assert restored.repeat == "count + 1"
|
|
51
52
|
assert restored.range_expression == "0 <= value <= 255"
|
|
52
53
|
assert restored.enum_def_name == enum_def.name
|
|
53
54
|
assert restored.to_dict()["enum_def_name"] == "ValueKind"
|
|
@@ -287,6 +288,42 @@ def test_struct_def_to_json_from_json_round_trip():
|
|
|
287
288
|
assert restored == struct_def
|
|
288
289
|
|
|
289
290
|
|
|
291
|
+
def test_struct_def_struct_def_name_accessors_use_fields_index():
|
|
292
|
+
"""Test StructDef field accessors grouped by struct_def_name."""
|
|
293
|
+
nested_struct = StructDef(name="Nested", fields=[])
|
|
294
|
+
primitive_field = FieldDef(name="value",
|
|
295
|
+
offset=InfoSize(0, 0),
|
|
296
|
+
size=InfoSize(1, 0),
|
|
297
|
+
type="unsigned int")
|
|
298
|
+
nested_field = FieldDef(name="nested",
|
|
299
|
+
offset=InfoSize(1, 0),
|
|
300
|
+
size=InfoSize(1, 0),
|
|
301
|
+
type=nested_struct)
|
|
302
|
+
named_nested_a = FieldDef(name="payload_a",
|
|
303
|
+
offset=InfoSize(2, 0),
|
|
304
|
+
size=InfoSize(1, 0),
|
|
305
|
+
type="Payload")
|
|
306
|
+
named_nested_b = FieldDef(name="payload_b",
|
|
307
|
+
offset=InfoSize(3, 0),
|
|
308
|
+
size=InfoSize(1, 0),
|
|
309
|
+
type="Payload")
|
|
310
|
+
struct_def = StructDef(fields=[
|
|
311
|
+
primitive_field,
|
|
312
|
+
nested_field,
|
|
313
|
+
named_nested_a,
|
|
314
|
+
named_nested_b,
|
|
315
|
+
])
|
|
316
|
+
|
|
317
|
+
assert struct_def.get_field("Nested") == nested_field
|
|
318
|
+
assert struct_def.get_fields("Payload") == [
|
|
319
|
+
named_nested_a,
|
|
320
|
+
named_nested_b,
|
|
321
|
+
]
|
|
322
|
+
assert struct_def.get_field("missing") is None
|
|
323
|
+
assert struct_def.get_fields("missing") == []
|
|
324
|
+
assert struct_def.get_fields("unsigned int") == []
|
|
325
|
+
|
|
326
|
+
|
|
290
327
|
def test_struct_instance_field_instance_interface():
|
|
291
328
|
"""Test StructInstance interface for field_instances operations."""
|
|
292
329
|
field_def_0 = FieldDef(name="b",
|
|
@@ -358,6 +395,50 @@ def test_struct_instance_rebuilds_padding_when_field_instances_change():
|
|
|
358
395
|
assert struct_instance.field_instances[1].value == b"\x00"
|
|
359
396
|
|
|
360
397
|
|
|
398
|
+
def test_struct_instance_field_def_name_accessors_use_index():
|
|
399
|
+
"""Test accessors return field instances indexed by field_def.name."""
|
|
400
|
+
field_def_a = FieldDef(name="a",
|
|
401
|
+
offset=InfoSize(0, 0),
|
|
402
|
+
size=InfoSize(1, 0),
|
|
403
|
+
type="unsigned int")
|
|
404
|
+
field_def_b = FieldDef(name="b",
|
|
405
|
+
offset=InfoSize(1, 0),
|
|
406
|
+
size=InfoSize(1, 0),
|
|
407
|
+
type="unsigned int")
|
|
408
|
+
struct_instance = StructInstance(
|
|
409
|
+
struct_def=StructDef(fields=[field_def_a, field_def_b]),
|
|
410
|
+
field_instances=[
|
|
411
|
+
FieldInstance(field_def_b, 2),
|
|
412
|
+
FieldInstance(field_def_a, 1),
|
|
413
|
+
],
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
assert struct_instance.get_field("a") == (FieldInstance(field_def_a, 1))
|
|
417
|
+
assert struct_instance.get_fields("b") == [FieldInstance(field_def_b, 2)]
|
|
418
|
+
assert struct_instance.get_field("missing") is None
|
|
419
|
+
assert struct_instance.get_fields("missing") == []
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def test_struct_instance_field_def_name_index_updates_after_append_and_extend():
|
|
423
|
+
"""Test name index is refreshed after append/extend operations."""
|
|
424
|
+
field_def_a = FieldDef(name="a",
|
|
425
|
+
offset=InfoSize(0, 0),
|
|
426
|
+
size=InfoSize(1, 0),
|
|
427
|
+
type="unsigned int")
|
|
428
|
+
field_def_b = FieldDef(name="b",
|
|
429
|
+
offset=InfoSize(1, 0),
|
|
430
|
+
size=InfoSize(1, 0),
|
|
431
|
+
type="unsigned int")
|
|
432
|
+
struct_instance = StructInstance(struct_def=StructDef(
|
|
433
|
+
fields=[field_def_a, field_def_b]))
|
|
434
|
+
|
|
435
|
+
struct_instance.append_field_instance(FieldInstance(field_def_a, 1))
|
|
436
|
+
struct_instance.extend_field_instances([FieldInstance(field_def_b, 2)])
|
|
437
|
+
|
|
438
|
+
assert struct_instance.get_field("a") == (FieldInstance(field_def_a, 1))
|
|
439
|
+
assert struct_instance.get_fields("b") == [FieldInstance(field_def_b, 2)]
|
|
440
|
+
|
|
441
|
+
|
|
361
442
|
def test_field_instance_sort_uses_field_def_order():
|
|
362
443
|
"""Test that FieldInstance sorting delegates to FieldDef ordering."""
|
|
363
444
|
later = FieldInstance(
|
|
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
|