sltcodec 2.3.0__tar.gz → 2.4.1__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.1}/PKG-INFO +11 -2
- {sltcodec-2.3.0 → sltcodec-2.4.1}/README.md +14 -0
- {sltcodec-2.3.0 → sltcodec-2.4.1}/README_pypi.md +10 -1
- {sltcodec-2.3.0 → sltcodec-2.4.1}/pyproject.toml +1 -1
- {sltcodec-2.3.0 → sltcodec-2.4.1}/src/sltcodec/codec.py +142 -8
- {sltcodec-2.3.0 → sltcodec-2.4.1}/src/sltcodec/types.py +86 -0
- {sltcodec-2.3.0 → sltcodec-2.4.1}/tests/test_codec.py +116 -0
- {sltcodec-2.3.0 → sltcodec-2.4.1}/tests/test_types.py +80 -0
- {sltcodec-2.3.0 → sltcodec-2.4.1}/uv.lock +1 -1
- {sltcodec-2.3.0 → sltcodec-2.4.1}/.github/workflows/publish_to_pypi.yml +0 -0
- {sltcodec-2.3.0 → sltcodec-2.4.1}/.github/workflows/publish_to_testpypi.yml +0 -0
- {sltcodec-2.3.0 → sltcodec-2.4.1}/.gitignore +0 -0
- {sltcodec-2.3.0 → sltcodec-2.4.1}/.python-version +0 -0
- {sltcodec-2.3.0 → sltcodec-2.4.1}/.vscode/launch.json +0 -0
- {sltcodec-2.3.0 → sltcodec-2.4.1}/.vscode/settings.json +0 -0
- {sltcodec-2.3.0 → sltcodec-2.4.1}/LICENSE +0 -0
- {sltcodec-2.3.0 → sltcodec-2.4.1}/bump_major.ps1 +0 -0
- {sltcodec-2.3.0 → sltcodec-2.4.1}/bump_minor.ps1 +0 -0
- {sltcodec-2.3.0 → sltcodec-2.4.1}/bump_patch.ps1 +0 -0
- {sltcodec-2.3.0 → sltcodec-2.4.1}/src/sltcodec/__init__.py +0 -0
- {sltcodec-2.3.0 → sltcodec-2.4.1}/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.1
|
|
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. |
|
|
@@ -134,8 +134,20 @@ def load_enum_def_dict(path: str | Path) -> dict[str, EnumDef]:
|
|
|
134
134
|
def _resolve_info_size(value: InfoSize | str, env: dict[str, Any]) -> InfoSize:
|
|
135
135
|
"""Resolve an InfoSize value that can be static or expression-based."""
|
|
136
136
|
if isinstance(value, str):
|
|
137
|
-
|
|
138
|
-
|
|
137
|
+
eval_env = dict(env)
|
|
138
|
+
|
|
139
|
+
def replace_info_size(match: re.Match[str]) -> str:
|
|
140
|
+
name = f"_info_size_{len(eval_env)}"
|
|
141
|
+
eval_env[name] = InfoSize(int(match.group(1)), int(match.group(2)))
|
|
142
|
+
return name
|
|
143
|
+
|
|
144
|
+
expression = re.sub(
|
|
145
|
+
r"\bInfoSize\s*\(\s*(-?\d+)\s*,\s*(-?\d+)\s*\)",
|
|
146
|
+
replace_info_size,
|
|
147
|
+
value,
|
|
148
|
+
)
|
|
149
|
+
stleval = SltEval(eval_env)
|
|
150
|
+
resolved_byte = stleval.eval(expression)
|
|
139
151
|
if isinstance(resolved_byte, InfoSize):
|
|
140
152
|
return resolved_byte
|
|
141
153
|
return InfoSize(resolved_byte, 0)
|
|
@@ -152,6 +164,8 @@ def _resolve_byte_swap(field_def: FieldDef, env: dict[str, Any]) -> bool:
|
|
|
152
164
|
def _resolve_repeat(value: int | str | None, env: dict[str, Any]) -> int | None:
|
|
153
165
|
"""Resolve a static or expression-based repeat count."""
|
|
154
166
|
if isinstance(value, str):
|
|
167
|
+
if value == "end":
|
|
168
|
+
return None
|
|
155
169
|
return int(SltEval(env).eval(value))
|
|
156
170
|
return value
|
|
157
171
|
|
|
@@ -162,6 +176,13 @@ def _is_padding_field_def(field_def: FieldDef) -> bool:
|
|
|
162
176
|
and field_def.type in ["bytes", "bytearray"])
|
|
163
177
|
|
|
164
178
|
|
|
179
|
+
def _struct_def_has_dynamic_extent(struct_def: StructDef) -> bool:
|
|
180
|
+
"""Check whether a nested structure can consume data beyond its fields."""
|
|
181
|
+
return any(field_def.repeat == "end" or isinstance(field_def.offset, str)
|
|
182
|
+
or isinstance(field_def.size, str)
|
|
183
|
+
for field_def in struct_def.fields)
|
|
184
|
+
|
|
185
|
+
|
|
165
186
|
def _validate_padding_alignment_bits(padding_alignment_bits: int) -> None:
|
|
166
187
|
"""Validate padding alignment as a positive power-of-two bit size."""
|
|
167
188
|
if (isinstance(padding_alignment_bits, bool)
|
|
@@ -335,6 +356,7 @@ def encode(
|
|
|
335
356
|
|
|
336
357
|
for field_value in struct_instance.field_instances:
|
|
337
358
|
field_def = field_value.field_def
|
|
359
|
+
repeat_until_end = field_def.repeat == "end"
|
|
338
360
|
repeat = _resolve_repeat(field_def.repeat, env)
|
|
339
361
|
if _is_padding_field_def(field_def):
|
|
340
362
|
has_padding = True
|
|
@@ -342,9 +364,12 @@ def encode(
|
|
|
342
364
|
if _is_padding_field_def(field_def):
|
|
343
365
|
padding_size = _resolve_info_size(field_def.size, env)
|
|
344
366
|
value = bytearray(padding_size.bytes)
|
|
367
|
+
if repeat_until_end:
|
|
368
|
+
repeat_values = list(value)
|
|
369
|
+
repeat = len(repeat_values)
|
|
345
370
|
if repeat is not None and repeat > 1:
|
|
346
371
|
current_offset = _resolve_info_size(field_def.offset, env)
|
|
347
|
-
values = list(value)
|
|
372
|
+
values = repeat_values if repeat_until_end else list(value)
|
|
348
373
|
|
|
349
374
|
for i in range(repeat):
|
|
350
375
|
field_def_repeat = _repeated_field_def(field_def, i,
|
|
@@ -409,7 +434,10 @@ def decode_field(
|
|
|
409
434
|
nested_layout = _layout_for_struct_def(resolved_type,
|
|
410
435
|
type_dict,
|
|
411
436
|
name=field_def.name)
|
|
412
|
-
|
|
437
|
+
nested_data = bytearray(info.to_bytes)
|
|
438
|
+
if _struct_def_has_dynamic_extent(resolved_type) and offset.bit == 0:
|
|
439
|
+
nested_data = bytearray(data[offset.byte:])
|
|
440
|
+
nested_value = decode(nested_layout, nested_data,
|
|
413
441
|
padding_alignment_bits)
|
|
414
442
|
actual_size = nested_value.size
|
|
415
443
|
else:
|
|
@@ -508,6 +536,27 @@ def decode(
|
|
|
508
536
|
result = StructInstance(struct_def=struct_def_obj)
|
|
509
537
|
current_position = InfoSize(0, 0)
|
|
510
538
|
padding_index = 0
|
|
539
|
+
stop_due_to_out_of_range = False
|
|
540
|
+
data_bits = len(data) * 8
|
|
541
|
+
field_defs = _as_field_defs(struct_def_obj)
|
|
542
|
+
|
|
543
|
+
def is_out_of_range(offset: InfoSize, size: InfoSize) -> bool:
|
|
544
|
+
"""Check whether the field range exceeds available input bits."""
|
|
545
|
+
if size.bits <= 0:
|
|
546
|
+
return False
|
|
547
|
+
if offset.bits < 0:
|
|
548
|
+
return True
|
|
549
|
+
if offset.bits >= data_bits:
|
|
550
|
+
return True
|
|
551
|
+
return offset.bits + size.bits > data_bits
|
|
552
|
+
|
|
553
|
+
def append_none_tail(start_index: int, first_field_def: FieldDef) -> None:
|
|
554
|
+
"""Append current and remaining fields with None values."""
|
|
555
|
+
result.field_instances.append(
|
|
556
|
+
FieldInstance(field_def=first_field_def, value=None))
|
|
557
|
+
for tail_field_def in field_defs[start_index + 1:]:
|
|
558
|
+
result.field_instances.append(
|
|
559
|
+
FieldInstance(field_def=tail_field_def, value=None))
|
|
511
560
|
|
|
512
561
|
def append_padding_until(target_offset: InfoSize) -> None:
|
|
513
562
|
nonlocal current_position, padding_index
|
|
@@ -537,11 +586,18 @@ def decode(
|
|
|
537
586
|
|
|
538
587
|
current_position = target_offset
|
|
539
588
|
|
|
540
|
-
for field_def in
|
|
541
|
-
|
|
589
|
+
for field_index, field_def in enumerate(field_defs):
|
|
590
|
+
repeat_until_end = field_def.repeat == "end"
|
|
591
|
+
repeat = (None if repeat_until_end else _resolve_repeat(
|
|
592
|
+
field_def.repeat, env))
|
|
542
593
|
# Handle non-repeated fields
|
|
543
|
-
if repeat is None or repeat <= 1:
|
|
594
|
+
if not repeat_until_end and (repeat is None or repeat <= 1):
|
|
544
595
|
resolved_offset = _resolve_info_size(field_def.offset, env)
|
|
596
|
+
resolved_size = _resolve_info_size(field_def.size, env)
|
|
597
|
+
if is_out_of_range(resolved_offset, resolved_size):
|
|
598
|
+
append_none_tail(field_index, field_def)
|
|
599
|
+
stop_due_to_out_of_range = True
|
|
600
|
+
break
|
|
545
601
|
append_padding_until(resolved_offset)
|
|
546
602
|
field_instance = decode_field(field_def, data, env, type_dict,
|
|
547
603
|
padding_alignment_bits)
|
|
@@ -553,10 +609,79 @@ def decode(
|
|
|
553
609
|
continue
|
|
554
610
|
# Handle repeated fields
|
|
555
611
|
current_offset = _resolve_info_size(field_def.offset, env)
|
|
556
|
-
|
|
612
|
+
if repeat_until_end:
|
|
613
|
+
stop_decoding = False
|
|
614
|
+
i = 0
|
|
615
|
+
while True:
|
|
616
|
+
if current_offset.bits >= data_bits:
|
|
617
|
+
if i == 0:
|
|
618
|
+
append_none_tail(
|
|
619
|
+
field_index,
|
|
620
|
+
_repeated_field_def(field_def, i, current_offset,
|
|
621
|
+
field_def.size),
|
|
622
|
+
)
|
|
623
|
+
stop_decoding = True
|
|
624
|
+
break
|
|
625
|
+
|
|
626
|
+
append_padding_until(current_offset)
|
|
627
|
+
|
|
628
|
+
field_def_repeat = _repeated_field_def(field_def, i,
|
|
629
|
+
current_offset,
|
|
630
|
+
field_def.size)
|
|
631
|
+
resolved_size = _resolve_info_size(field_def_repeat.size, env)
|
|
632
|
+
if resolved_size.bits <= 0:
|
|
633
|
+
break
|
|
634
|
+
if is_out_of_range(current_offset, resolved_size):
|
|
635
|
+
if i == 0:
|
|
636
|
+
append_none_tail(field_index, field_def_repeat)
|
|
637
|
+
stop_decoding = True
|
|
638
|
+
break
|
|
639
|
+
|
|
640
|
+
field_instance = decode_field(field_def_repeat, data, env,
|
|
641
|
+
type_dict, padding_alignment_bits)
|
|
642
|
+
if field_instance is not None:
|
|
643
|
+
env[field_instance.field_def.name] = field_instance.value
|
|
644
|
+
result.append_field_instance(field_instance)
|
|
645
|
+
actual_size = field_instance.field_def.size
|
|
646
|
+
else:
|
|
647
|
+
actual_size = resolved_size
|
|
648
|
+
|
|
649
|
+
if isinstance(current_offset, InfoSize) and isinstance(
|
|
650
|
+
actual_size, InfoSize):
|
|
651
|
+
current_offset += actual_size
|
|
652
|
+
current_position = current_offset
|
|
653
|
+
i += 1
|
|
654
|
+
if stop_decoding:
|
|
655
|
+
stop_due_to_out_of_range = True
|
|
656
|
+
break
|
|
657
|
+
continue
|
|
658
|
+
|
|
557
659
|
for i in range(repeat):
|
|
558
660
|
field_def_repeat = _repeated_field_def(field_def, i, current_offset,
|
|
559
661
|
field_def.size)
|
|
662
|
+
resolved_size = _resolve_info_size(field_def_repeat.size, env)
|
|
663
|
+
if is_out_of_range(current_offset, resolved_size):
|
|
664
|
+
result.field_instances.append(
|
|
665
|
+
FieldInstance(field_def=field_def_repeat, value=None))
|
|
666
|
+
next_offset = current_offset
|
|
667
|
+
for remaining_index in range(i + 1, repeat):
|
|
668
|
+
if isinstance(next_offset, InfoSize):
|
|
669
|
+
next_offset += resolved_size
|
|
670
|
+
remaining_field_def = _repeated_field_def(
|
|
671
|
+
field_def,
|
|
672
|
+
remaining_index,
|
|
673
|
+
next_offset,
|
|
674
|
+
field_def.size,
|
|
675
|
+
)
|
|
676
|
+
result.field_instances.append(
|
|
677
|
+
FieldInstance(field_def=remaining_field_def,
|
|
678
|
+
value=None))
|
|
679
|
+
for tail_field_def in field_defs[field_index + 1:]:
|
|
680
|
+
result.field_instances.append(
|
|
681
|
+
FieldInstance(field_def=tail_field_def, value=None))
|
|
682
|
+
stop_due_to_out_of_range = True
|
|
683
|
+
break
|
|
684
|
+
append_padding_until(current_offset)
|
|
560
685
|
field_instance = decode_field(field_def_repeat, data, env,
|
|
561
686
|
type_dict, padding_alignment_bits)
|
|
562
687
|
if field_instance is not None:
|
|
@@ -569,6 +694,15 @@ def decode(
|
|
|
569
694
|
actual_size, InfoSize):
|
|
570
695
|
current_offset += actual_size
|
|
571
696
|
current_position = current_offset
|
|
697
|
+
else:
|
|
698
|
+
continue
|
|
699
|
+
break
|
|
700
|
+
|
|
701
|
+
if stop_due_to_out_of_range:
|
|
702
|
+
result._sort_field_instances()
|
|
703
|
+
result._update_size()
|
|
704
|
+
result._rebuild_field_def_name_index()
|
|
705
|
+
return result
|
|
572
706
|
|
|
573
707
|
actual_size = InfoSize(0, 0)
|
|
574
708
|
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,122 @@ 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_info_size_expression_resolves_constructor():
|
|
236
|
+
"""Test that serialized InfoSize constructors work in expressions."""
|
|
237
|
+
resolved = _resolve_info_size("InfoSize(1, 0) * count", {"count": 3})
|
|
238
|
+
|
|
239
|
+
assert resolved == InfoSize(3, 0)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def test_repeat_end_uses_dynamic_nested_struct_size():
|
|
243
|
+
"""Test repeat='end' advances by a variable-size nested structure."""
|
|
244
|
+
length_field = FieldDef(name="length",
|
|
245
|
+
offset=InfoSize(0, 0),
|
|
246
|
+
size=InfoSize(1, 0),
|
|
247
|
+
type="unsigned int")
|
|
248
|
+
payload_field = FieldDef(name="payload",
|
|
249
|
+
offset=InfoSize(1, 0),
|
|
250
|
+
size="InfoSize(1, 0) * length",
|
|
251
|
+
type="bytearray")
|
|
252
|
+
record_field = FieldDef(
|
|
253
|
+
name="record",
|
|
254
|
+
offset=InfoSize(0, 0),
|
|
255
|
+
size=InfoSize(1, 0),
|
|
256
|
+
type=StructDef(fields=[length_field, payload_field]),
|
|
257
|
+
repeat="end")
|
|
258
|
+
|
|
259
|
+
decoded = decode(layout_for([record_field]), bytearray(b"\x02ab\x01c"))
|
|
260
|
+
|
|
261
|
+
assert [instance.field_def.name for instance in decoded.field_instances
|
|
262
|
+
] == ["record[0]", "record[1]"]
|
|
263
|
+
assert [instance.value.size for instance in decoded.field_instances
|
|
264
|
+
] == [InfoSize(3, 0), InfoSize(2, 0)]
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def test_decode_out_of_range_sets_current_and_tail_values_to_none():
|
|
268
|
+
"""Test out-of-range decode fills current/tail field values with None."""
|
|
269
|
+
fields = [
|
|
270
|
+
FieldDef(name="head",
|
|
271
|
+
offset=InfoSize(0, 0),
|
|
272
|
+
size=InfoSize(1, 0),
|
|
273
|
+
type="unsigned int"),
|
|
274
|
+
FieldDef(name="far",
|
|
275
|
+
offset=InfoSize(10, 0),
|
|
276
|
+
size=InfoSize(1, 0),
|
|
277
|
+
type="unsigned int"),
|
|
278
|
+
FieldDef(name="tail",
|
|
279
|
+
offset=InfoSize(11, 0),
|
|
280
|
+
size=InfoSize(1, 0),
|
|
281
|
+
type="unsigned int"),
|
|
282
|
+
]
|
|
283
|
+
|
|
284
|
+
decoded = decode(layout_for(fields), bytearray(b"\x11"))
|
|
285
|
+
|
|
286
|
+
assert [instance.field_def.name
|
|
287
|
+
for instance in decoded.field_instances] == ["head", "far", "tail"]
|
|
288
|
+
assert [instance.value
|
|
289
|
+
for instance in decoded.field_instances] == [17, None, None]
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def test_decode_out_of_range_skips_following_field_expression_evaluation():
|
|
293
|
+
"""Test out-of-range decode skips evaluating following expressions."""
|
|
294
|
+
fields = [
|
|
295
|
+
FieldDef(name="head",
|
|
296
|
+
offset=InfoSize(0, 0),
|
|
297
|
+
size=InfoSize(1, 0),
|
|
298
|
+
type="unsigned int"),
|
|
299
|
+
FieldDef(name="far",
|
|
300
|
+
offset=InfoSize(10, 0),
|
|
301
|
+
size=InfoSize(1, 0),
|
|
302
|
+
type="unsigned int"),
|
|
303
|
+
FieldDef(name="tail",
|
|
304
|
+
offset="1 / 0",
|
|
305
|
+
size=InfoSize(1, 0),
|
|
306
|
+
type="unsigned int"),
|
|
307
|
+
]
|
|
308
|
+
|
|
309
|
+
decoded = decode(layout_for(fields), bytearray(b"\x11"))
|
|
310
|
+
|
|
311
|
+
assert [instance.field_def.name
|
|
312
|
+
for instance in decoded.field_instances] == ["head", "far", "tail"]
|
|
313
|
+
assert [instance.value
|
|
314
|
+
for instance in decoded.field_instances] == [17, None, None]
|
|
315
|
+
|
|
316
|
+
|
|
201
317
|
def test_encode_recurses_for_nested_field_types():
|
|
202
318
|
"""Test that encoding a field with a nested field type works correctly."""
|
|
203
319
|
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
|