sltcodec 2.3.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.3.0 → sltcodec-2.4.0}/PKG-INFO +11 -2
- {sltcodec-2.3.0 → sltcodec-2.4.0}/README.md +14 -0
- {sltcodec-2.3.0 → sltcodec-2.4.0}/README_pypi.md +10 -1
- {sltcodec-2.3.0 → sltcodec-2.4.0}/pyproject.toml +1 -1
- {sltcodec-2.3.0 → sltcodec-2.4.0}/src/sltcodec/codec.py +117 -5
- {sltcodec-2.3.0 → sltcodec-2.4.0}/src/sltcodec/types.py +86 -0
- {sltcodec-2.3.0 → sltcodec-2.4.0}/tests/test_codec.py +84 -0
- {sltcodec-2.3.0 → sltcodec-2.4.0}/tests/test_types.py +80 -0
- {sltcodec-2.3.0 → sltcodec-2.4.0}/uv.lock +1 -1
- {sltcodec-2.3.0 → sltcodec-2.4.0}/.github/workflows/publish_to_pypi.yml +0 -0
- {sltcodec-2.3.0 → sltcodec-2.4.0}/.github/workflows/publish_to_testpypi.yml +0 -0
- {sltcodec-2.3.0 → sltcodec-2.4.0}/.gitignore +0 -0
- {sltcodec-2.3.0 → sltcodec-2.4.0}/.python-version +0 -0
- {sltcodec-2.3.0 → sltcodec-2.4.0}/.vscode/launch.json +0 -0
- {sltcodec-2.3.0 → sltcodec-2.4.0}/.vscode/settings.json +0 -0
- {sltcodec-2.3.0 → sltcodec-2.4.0}/LICENSE +0 -0
- {sltcodec-2.3.0 → sltcodec-2.4.0}/bump_major.ps1 +0 -0
- {sltcodec-2.3.0 → sltcodec-2.4.0}/bump_minor.ps1 +0 -0
- {sltcodec-2.3.0 → sltcodec-2.4.0}/bump_patch.ps1 +0 -0
- {sltcodec-2.3.0 → sltcodec-2.4.0}/src/sltcodec/__init__.py +0 -0
- {sltcodec-2.3.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:
|
|
@@ -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 \| str \| 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:
|
|
@@ -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 \| str \| 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. |
|
|
@@ -152,6 +152,8 @@ def _resolve_byte_swap(field_def: FieldDef, env: dict[str, Any]) -> bool:
|
|
|
152
152
|
def _resolve_repeat(value: int | str | None, env: dict[str, Any]) -> int | None:
|
|
153
153
|
"""Resolve a static or expression-based repeat count."""
|
|
154
154
|
if isinstance(value, str):
|
|
155
|
+
if value == "end":
|
|
156
|
+
return None
|
|
155
157
|
return int(SltEval(env).eval(value))
|
|
156
158
|
return value
|
|
157
159
|
|
|
@@ -335,6 +337,7 @@ def encode(
|
|
|
335
337
|
|
|
336
338
|
for field_value in struct_instance.field_instances:
|
|
337
339
|
field_def = field_value.field_def
|
|
340
|
+
repeat_until_end = field_def.repeat == "end"
|
|
338
341
|
repeat = _resolve_repeat(field_def.repeat, env)
|
|
339
342
|
if _is_padding_field_def(field_def):
|
|
340
343
|
has_padding = True
|
|
@@ -342,9 +345,12 @@ def encode(
|
|
|
342
345
|
if _is_padding_field_def(field_def):
|
|
343
346
|
padding_size = _resolve_info_size(field_def.size, env)
|
|
344
347
|
value = bytearray(padding_size.bytes)
|
|
348
|
+
if repeat_until_end:
|
|
349
|
+
repeat_values = list(value)
|
|
350
|
+
repeat = len(repeat_values)
|
|
345
351
|
if repeat is not None and repeat > 1:
|
|
346
352
|
current_offset = _resolve_info_size(field_def.offset, env)
|
|
347
|
-
values = list(value)
|
|
353
|
+
values = repeat_values if repeat_until_end else list(value)
|
|
348
354
|
|
|
349
355
|
for i in range(repeat):
|
|
350
356
|
field_def_repeat = _repeated_field_def(field_def, i,
|
|
@@ -508,6 +514,27 @@ def decode(
|
|
|
508
514
|
result = StructInstance(struct_def=struct_def_obj)
|
|
509
515
|
current_position = InfoSize(0, 0)
|
|
510
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))
|
|
511
538
|
|
|
512
539
|
def append_padding_until(target_offset: InfoSize) -> None:
|
|
513
540
|
nonlocal current_position, padding_index
|
|
@@ -537,11 +564,18 @@ def decode(
|
|
|
537
564
|
|
|
538
565
|
current_position = target_offset
|
|
539
566
|
|
|
540
|
-
for field_def in
|
|
541
|
-
|
|
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))
|
|
542
571
|
# Handle non-repeated fields
|
|
543
|
-
if repeat is None or repeat <= 1:
|
|
572
|
+
if not repeat_until_end and (repeat is None or repeat <= 1):
|
|
544
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
|
|
545
579
|
append_padding_until(resolved_offset)
|
|
546
580
|
field_instance = decode_field(field_def, data, env, type_dict,
|
|
547
581
|
padding_alignment_bits)
|
|
@@ -553,10 +587,79 @@ def decode(
|
|
|
553
587
|
continue
|
|
554
588
|
# Handle repeated fields
|
|
555
589
|
current_offset = _resolve_info_size(field_def.offset, env)
|
|
556
|
-
|
|
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
|
+
|
|
557
637
|
for i in range(repeat):
|
|
558
638
|
field_def_repeat = _repeated_field_def(field_def, i, current_offset,
|
|
559
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)
|
|
560
663
|
field_instance = decode_field(field_def_repeat, data, env,
|
|
561
664
|
type_dict, padding_alignment_bits)
|
|
562
665
|
if field_instance is not None:
|
|
@@ -569,6 +672,15 @@ def decode(
|
|
|
569
672
|
actual_size, InfoSize):
|
|
570
673
|
current_offset += actual_size
|
|
571
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
|
|
572
684
|
|
|
573
685
|
actual_size = InfoSize(0, 0)
|
|
574
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)
|
|
@@ -367,6 +377,16 @@ class StructDef:
|
|
|
367
377
|
default_factory=list,
|
|
368
378
|
metadata={"desc": "The fields of the structure"},
|
|
369
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()
|
|
370
390
|
|
|
371
391
|
def __lt__(self, other: object) -> bool:
|
|
372
392
|
"""Compare structure definitions using a stable serialized sort key."""
|
|
@@ -383,6 +403,42 @@ class StructDef:
|
|
|
383
403
|
sort_keys=True),
|
|
384
404
|
)
|
|
385
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
|
+
|
|
386
442
|
def to_dict(self) -> dict[str, Any]:
|
|
387
443
|
"""Convert this structure definition to JSON-serializable data."""
|
|
388
444
|
return {
|
|
@@ -432,6 +488,11 @@ class StructInstance:
|
|
|
432
488
|
default_factory=InfoSize,
|
|
433
489
|
metadata={"desc": "The total size of the structure instance"},
|
|
434
490
|
)
|
|
491
|
+
_field_instances_by_field_def_name: dict[str, list[FieldInstance]] = field(
|
|
492
|
+
default_factory=dict,
|
|
493
|
+
init=False,
|
|
494
|
+
repr=False,
|
|
495
|
+
)
|
|
435
496
|
|
|
436
497
|
def __post_init__(self) -> None:
|
|
437
498
|
"""Normalize stored field instances to sorted order."""
|
|
@@ -469,6 +530,19 @@ class StructInstance:
|
|
|
469
530
|
"""Return one field instance by index."""
|
|
470
531
|
return self.field_instances[index]
|
|
471
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
|
+
|
|
472
546
|
def __lt__(self, other: object) -> bool:
|
|
473
547
|
"""Compare structure instances using a stable serialized sort key."""
|
|
474
548
|
if not isinstance(other, StructInstance):
|
|
@@ -490,6 +564,16 @@ class StructInstance:
|
|
|
490
564
|
"""Keep field instances sorted by FieldDef order."""
|
|
491
565
|
self.field_instances.sort()
|
|
492
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
|
+
|
|
493
577
|
def _rebuild_padding_field_instances(self) -> None:
|
|
494
578
|
"""Rebuild padding field instances for gaps between stored values."""
|
|
495
579
|
non_padding_instances = [
|
|
@@ -507,6 +591,7 @@ class StructInstance:
|
|
|
507
591
|
self.field_instances = non_padding_instances
|
|
508
592
|
self._sort_field_instances()
|
|
509
593
|
self._update_size()
|
|
594
|
+
self._rebuild_field_def_name_index()
|
|
510
595
|
return
|
|
511
596
|
|
|
512
597
|
rebuilt_instances: list[FieldInstance] = []
|
|
@@ -555,6 +640,7 @@ class StructInstance:
|
|
|
555
640
|
self.field_instances = rebuilt_instances
|
|
556
641
|
self._sort_field_instances()
|
|
557
642
|
self._update_size()
|
|
643
|
+
self._rebuild_field_def_name_index()
|
|
558
644
|
|
|
559
645
|
def _update_size(self) -> None:
|
|
560
646
|
"""Update the instance size from the current field layout."""
|
|
@@ -198,6 +198,90 @@ def test_expression_repeat_uses_previous_field_value():
|
|
|
198
198
|
for instance in decoded.field_instances] == [2, 5, 6, 7]
|
|
199
199
|
|
|
200
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
|
+
|
|
201
285
|
def test_encode_recurses_for_nested_field_types():
|
|
202
286
|
"""Test that encoding a field with a nested field type works correctly."""
|
|
203
287
|
child_field_defs = [
|
|
@@ -288,6 +288,42 @@ def test_struct_def_to_json_from_json_round_trip():
|
|
|
288
288
|
assert restored == struct_def
|
|
289
289
|
|
|
290
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
|
+
|
|
291
327
|
def test_struct_instance_field_instance_interface():
|
|
292
328
|
"""Test StructInstance interface for field_instances operations."""
|
|
293
329
|
field_def_0 = FieldDef(name="b",
|
|
@@ -359,6 +395,50 @@ def test_struct_instance_rebuilds_padding_when_field_instances_change():
|
|
|
359
395
|
assert struct_instance.field_instances[1].value == b"\x00"
|
|
360
396
|
|
|
361
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
|
+
|
|
362
442
|
def test_field_instance_sort_uses_field_def_order():
|
|
363
443
|
"""Test that FieldInstance sorting delegates to FieldDef ordering."""
|
|
364
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
|