sltcodec 2.0.0__tar.gz → 2.1.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.0.0 → sltcodec-2.1.1}/PKG-INFO +8 -5
- {sltcodec-2.0.0 → sltcodec-2.1.1}/README.md +19 -0
- {sltcodec-2.0.0 → sltcodec-2.1.1}/README_pypi.md +7 -4
- {sltcodec-2.0.0 → sltcodec-2.1.1}/pyproject.toml +1 -1
- {sltcodec-2.0.0 → sltcodec-2.1.1}/src/sltcodec/codec.py +61 -28
- {sltcodec-2.0.0 → sltcodec-2.1.1}/src/sltcodec/types.py +9 -41
- {sltcodec-2.0.0 → sltcodec-2.1.1}/tests/test_codec.py +49 -0
- {sltcodec-2.0.0 → sltcodec-2.1.1}/tests/test_types.py +83 -17
- {sltcodec-2.0.0 → sltcodec-2.1.1}/uv.lock +1 -1
- {sltcodec-2.0.0 → sltcodec-2.1.1}/.github/workflows/publish_to_pypi.yml +0 -0
- {sltcodec-2.0.0 → sltcodec-2.1.1}/.github/workflows/publish_to_testpypi.yml +0 -0
- {sltcodec-2.0.0 → sltcodec-2.1.1}/.gitignore +0 -0
- {sltcodec-2.0.0 → sltcodec-2.1.1}/.python-version +0 -0
- {sltcodec-2.0.0 → sltcodec-2.1.1}/.vscode/launch.json +0 -0
- {sltcodec-2.0.0 → sltcodec-2.1.1}/.vscode/settings.json +0 -0
- {sltcodec-2.0.0 → sltcodec-2.1.1}/LICENSE +0 -0
- {sltcodec-2.0.0 → sltcodec-2.1.1}/bump_major.ps1 +0 -0
- {sltcodec-2.0.0 → sltcodec-2.1.1}/bump_minor.ps1 +0 -0
- {sltcodec-2.0.0 → sltcodec-2.1.1}/bump_patch.ps1 +0 -0
- {sltcodec-2.0.0 → sltcodec-2.1.1}/src/sltcodec/__init__.py +0 -0
- {sltcodec-2.0.0 → sltcodec-2.1.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.1.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
|
|
@@ -63,6 +63,12 @@ encoded = encode(layout, instance, bytearray())
|
|
|
63
63
|
decoded = decode(layout, encoded)
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
+
`decode` returns a `StructInstance` whose `size` is the actual end of the
|
|
67
|
+
decoded layout. `decode_field` stores the actual decoded size in the returned
|
|
68
|
+
`FieldInstance.field_def.size`. For nested `StructDef` fields, that size comes
|
|
69
|
+
from the nested `StructInstance.size`; for repeated fields, the next element's
|
|
70
|
+
offset advances by the actual size of the previous element.
|
|
71
|
+
|
|
66
72
|
## Field Types
|
|
67
73
|
|
|
68
74
|
The public `PRIMITIVE_TYPES` set contains:
|
|
@@ -78,7 +84,6 @@ The public `PRIMITIVE_TYPES` set contains:
|
|
|
78
84
|
Store named structures in `TypeDict.struct_dict` and enums in `TypeDict.enum_dict`. A field refers to an enum by its `enum_def_name`; the codec resolves it through `StructLayout.type_dict`.
|
|
79
85
|
|
|
80
86
|
```python
|
|
81
|
-
| `__lt__(other)` | Compare enum definitions using their serialized ordering. |
|
|
82
87
|
from sltcore import InfoSize
|
|
83
88
|
from sltcodec import EnumDef, FieldDef, StructDef, StructLayout, TypeDict, decode
|
|
84
89
|
|
|
@@ -102,7 +107,6 @@ layout = StructLayout(
|
|
|
102
107
|
)
|
|
103
108
|
|
|
104
109
|
decoded = decode(layout, bytearray(b"\x01"))
|
|
105
|
-
| `__lt__(other)` | Compare field definitions using their serialized ordering. |
|
|
106
110
|
assert decoded.field_instances[0].enum_item == ("NG", 1)
|
|
107
111
|
```
|
|
108
112
|
|
|
@@ -117,7 +121,6 @@ from sltcodec import load_struct_layout, save_struct_layout
|
|
|
117
121
|
path = Path("struct_layout.json")
|
|
118
122
|
save_struct_layout(layout, path)
|
|
119
123
|
loaded = load_struct_layout(path)
|
|
120
|
-
| `__lt__(other)` | Compare field instances using field-definition order. |
|
|
121
124
|
assert loaded == layout
|
|
122
125
|
```
|
|
123
126
|
|
|
@@ -185,7 +188,6 @@ internal implementation details and are omitted from the public method tables.
|
|
|
185
188
|
|
|
186
189
|
| Method | Description |
|
|
187
190
|
| --- | --- |
|
|
188
|
-
| `split_repeat(index, offset=None, size=None)` | Create one field definition from a repeated field. |
|
|
189
191
|
| `to_dict()` | Convert the definition to a JSON-compatible dictionary. |
|
|
190
192
|
| `to_json()` | Convert the definition to a JSON string. |
|
|
191
193
|
| `from_dict(data)` | Create a `FieldDef` from a dictionary. |
|
|
@@ -205,6 +207,7 @@ internal implementation details and are omitted from the public method tables.
|
|
|
205
207
|
| --- | --- |
|
|
206
208
|
| `range_check(env=None)` | Evaluate the field's range expression. |
|
|
207
209
|
| `from_value(field_def, value, type_dict=None, is_padding=False)` | Create an instance and resolve matching enum metadata. |
|
|
210
|
+
| `with_value(value, type_dict=None)` | Return a new instance with the given value; keeps `field_def` and `is_padding` and recomputes `enum_item`. |
|
|
208
211
|
| `__lt__(other)` | Compare field instances using field-definition order. |
|
|
209
212
|
|
|
210
213
|
#### `StructDef`
|
|
@@ -61,6 +61,25 @@ Layout persistence uses the explicit names `save_struct_layout` and
|
|
|
61
61
|
`load_struct_layout`. The former `save_type_dict` and `load_type_dict` API is
|
|
62
62
|
not part of the current public interface.
|
|
63
63
|
|
|
64
|
+
`FieldInstance` stays frozen. To change a decoded value, use
|
|
65
|
+
`FieldInstance.with_value(value, type_dict=None)`, which returns a new
|
|
66
|
+
instance instead of mutating the original. It keeps `field_def` and
|
|
67
|
+
`is_padding`, and recomputes `enum_item` from the new value and `type_dict`
|
|
68
|
+
by delegating to `FieldInstance.from_value`.
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
updated_field = field_instance.with_value(
|
|
72
|
+
new_value,
|
|
73
|
+
struct_layout.type_dict,
|
|
74
|
+
)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Decoded sizes reflect the actual data consumed. `decode` sets
|
|
78
|
+
`StructInstance.size` to the end of the decoded layout, and `decode_field`
|
|
79
|
+
stores the actual size in `FieldInstance.field_def.size`. For nested structures,
|
|
80
|
+
the nested `StructInstance.size` is used; repeated fields advance each next
|
|
81
|
+
offset by the preceding element's actual size.
|
|
82
|
+
|
|
64
83
|
## Versioning And Release
|
|
65
84
|
|
|
66
85
|
Version bump scripts are provided for patch, minor, and major releases:
|
|
@@ -46,6 +46,12 @@ encoded = encode(layout, instance, bytearray())
|
|
|
46
46
|
decoded = decode(layout, encoded)
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
`decode` returns a `StructInstance` whose `size` is the actual end of the
|
|
50
|
+
decoded layout. `decode_field` stores the actual decoded size in the returned
|
|
51
|
+
`FieldInstance.field_def.size`. For nested `StructDef` fields, that size comes
|
|
52
|
+
from the nested `StructInstance.size`; for repeated fields, the next element's
|
|
53
|
+
offset advances by the actual size of the previous element.
|
|
54
|
+
|
|
49
55
|
## Field Types
|
|
50
56
|
|
|
51
57
|
The public `PRIMITIVE_TYPES` set contains:
|
|
@@ -61,7 +67,6 @@ The public `PRIMITIVE_TYPES` set contains:
|
|
|
61
67
|
Store named structures in `TypeDict.struct_dict` and enums in `TypeDict.enum_dict`. A field refers to an enum by its `enum_def_name`; the codec resolves it through `StructLayout.type_dict`.
|
|
62
68
|
|
|
63
69
|
```python
|
|
64
|
-
| `__lt__(other)` | Compare enum definitions using their serialized ordering. |
|
|
65
70
|
from sltcore import InfoSize
|
|
66
71
|
from sltcodec import EnumDef, FieldDef, StructDef, StructLayout, TypeDict, decode
|
|
67
72
|
|
|
@@ -85,7 +90,6 @@ layout = StructLayout(
|
|
|
85
90
|
)
|
|
86
91
|
|
|
87
92
|
decoded = decode(layout, bytearray(b"\x01"))
|
|
88
|
-
| `__lt__(other)` | Compare field definitions using their serialized ordering. |
|
|
89
93
|
assert decoded.field_instances[0].enum_item == ("NG", 1)
|
|
90
94
|
```
|
|
91
95
|
|
|
@@ -100,7 +104,6 @@ from sltcodec import load_struct_layout, save_struct_layout
|
|
|
100
104
|
path = Path("struct_layout.json")
|
|
101
105
|
save_struct_layout(layout, path)
|
|
102
106
|
loaded = load_struct_layout(path)
|
|
103
|
-
| `__lt__(other)` | Compare field instances using field-definition order. |
|
|
104
107
|
assert loaded == layout
|
|
105
108
|
```
|
|
106
109
|
|
|
@@ -168,7 +171,6 @@ internal implementation details and are omitted from the public method tables.
|
|
|
168
171
|
|
|
169
172
|
| Method | Description |
|
|
170
173
|
| --- | --- |
|
|
171
|
-
| `split_repeat(index, offset=None, size=None)` | Create one field definition from a repeated field. |
|
|
172
174
|
| `to_dict()` | Convert the definition to a JSON-compatible dictionary. |
|
|
173
175
|
| `to_json()` | Convert the definition to a JSON string. |
|
|
174
176
|
| `from_dict(data)` | Create a `FieldDef` from a dictionary. |
|
|
@@ -188,6 +190,7 @@ internal implementation details and are omitted from the public method tables.
|
|
|
188
190
|
| --- | --- |
|
|
189
191
|
| `range_check(env=None)` | Evaluate the field's range expression. |
|
|
190
192
|
| `from_value(field_def, value, type_dict=None, is_padding=False)` | Create an instance and resolve matching enum metadata. |
|
|
193
|
+
| `with_value(value, type_dict=None)` | Return a new instance with the given value; keeps `field_def` and `is_padding` and recomputes `enum_item`. |
|
|
191
194
|
| `__lt__(other)` | Compare field instances using field-definition order. |
|
|
192
195
|
|
|
193
196
|
#### `StructDef`
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import json
|
|
5
|
+
import re
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from typing import Any
|
|
7
8
|
|
|
@@ -259,17 +260,30 @@ def _prepare_field_info(
|
|
|
259
260
|
return offset, size, info
|
|
260
261
|
|
|
261
262
|
|
|
262
|
-
def
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
263
|
+
def _repeated_field_def(field_def: FieldDef, index: int, offset: InfoSize,
|
|
264
|
+
size: InfoSize | str) -> FieldDef:
|
|
265
|
+
"""Create a resolved field definition for one repeated value."""
|
|
266
|
+
repeated_name = f"{field_def.name}[{index}]"
|
|
267
|
+
|
|
268
|
+
def replace_name(value: Any) -> Any:
|
|
269
|
+
if not isinstance(value, str):
|
|
270
|
+
return value
|
|
271
|
+
pattern = (rf"(?<![0-9A-Za-z_]){re.escape(field_def.name)}"
|
|
272
|
+
rf"(?![0-9A-Za-z_])")
|
|
273
|
+
return re.sub(pattern, repeated_name, value)
|
|
274
|
+
|
|
275
|
+
return FieldDef(
|
|
276
|
+
name=repeated_name,
|
|
277
|
+
offset=offset,
|
|
278
|
+
size=size,
|
|
279
|
+
type=replace_name(field_def.type),
|
|
280
|
+
scale=field_def.scale,
|
|
281
|
+
repeat=None,
|
|
282
|
+
description=field_def.description,
|
|
283
|
+
range_expression=replace_name(field_def.range_expression),
|
|
284
|
+
enum_def_name=field_def.enum_def_name,
|
|
285
|
+
byte_swap=replace_name(field_def.byte_swap),
|
|
286
|
+
)
|
|
273
287
|
|
|
274
288
|
|
|
275
289
|
def encode_field(
|
|
@@ -345,17 +359,17 @@ def encode(
|
|
|
345
359
|
value = bytearray(padding_size.bytes)
|
|
346
360
|
if field_def.repeat is not None and field_def.repeat > 1:
|
|
347
361
|
current_offset = _resolve_offset(field_def, env)
|
|
348
|
-
resolved_size = _resolve_size(field_def, env)
|
|
349
362
|
values = list(value)
|
|
350
363
|
|
|
351
364
|
for i in range(field_def.repeat):
|
|
352
|
-
field_def_repeat =
|
|
353
|
-
|
|
365
|
+
field_def_repeat = _repeated_field_def(field_def, i,
|
|
366
|
+
current_offset,
|
|
367
|
+
field_def.size)
|
|
368
|
+
resolved_size = _resolve_size(field_def_repeat, env)
|
|
354
369
|
encode_field(field_def_repeat, values[i], buf, env, type_dict,
|
|
355
370
|
padding_alignment_bits)
|
|
356
371
|
env[field_def_repeat.name] = values[i]
|
|
357
|
-
if isinstance(current_offset, InfoSize)
|
|
358
|
-
resolved_size, InfoSize):
|
|
372
|
+
if isinstance(current_offset, InfoSize):
|
|
359
373
|
current_offset += resolved_size
|
|
360
374
|
continue
|
|
361
375
|
|
|
@@ -405,10 +419,20 @@ def decode_field(
|
|
|
405
419
|
if byte_swap:
|
|
406
420
|
info = _byte_swap_info(info, size)
|
|
407
421
|
resolved_type = _resolve_field_type(field_def.type, type_dict, env)
|
|
422
|
+
nested_value = None
|
|
423
|
+
if isinstance(resolved_type, StructDef):
|
|
424
|
+
nested_layout = _layout_for_struct_def(resolved_type,
|
|
425
|
+
type_dict,
|
|
426
|
+
name=field_def.name)
|
|
427
|
+
nested_value = decode(nested_layout, bytearray(info.to_bytes),
|
|
428
|
+
padding_alignment_bits)
|
|
429
|
+
actual_size = nested_value.size
|
|
430
|
+
else:
|
|
431
|
+
actual_size = info.info_size
|
|
408
432
|
resolved_field_def = FieldDef(
|
|
409
433
|
name=field_def.name,
|
|
410
434
|
offset=offset,
|
|
411
|
-
size=
|
|
435
|
+
size=actual_size,
|
|
412
436
|
type=resolved_type,
|
|
413
437
|
scale=field_def.scale,
|
|
414
438
|
repeat=field_def.repeat,
|
|
@@ -418,13 +442,9 @@ def decode_field(
|
|
|
418
442
|
byte_swap=byte_swap,
|
|
419
443
|
)
|
|
420
444
|
if isinstance(resolved_type, StructDef):
|
|
421
|
-
nested_layout = _layout_for_struct_def(resolved_type,
|
|
422
|
-
type_dict,
|
|
423
|
-
name=field_def.name)
|
|
424
445
|
return FieldInstance(
|
|
425
446
|
field_def=resolved_field_def,
|
|
426
|
-
value=
|
|
427
|
-
padding_alignment_bits),
|
|
447
|
+
value=nested_value,
|
|
428
448
|
)
|
|
429
449
|
if resolved_type == "bool":
|
|
430
450
|
return FieldInstance.from_value(resolved_field_def,
|
|
@@ -548,19 +568,32 @@ def decode(
|
|
|
548
568
|
continue
|
|
549
569
|
# Handle repeated fields
|
|
550
570
|
current_offset = _resolve_offset(field_def, env)
|
|
551
|
-
resolved_size = _resolve_size(field_def, env)
|
|
552
571
|
append_padding_until(current_offset)
|
|
553
572
|
for i in range(field_def.repeat):
|
|
554
|
-
field_def_repeat =
|
|
555
|
-
|
|
573
|
+
field_def_repeat = _repeated_field_def(field_def, i, current_offset,
|
|
574
|
+
field_def.size)
|
|
556
575
|
field_instance = decode_field(field_def_repeat, data, env,
|
|
557
576
|
type_dict, padding_alignment_bits)
|
|
558
577
|
if field_instance is not None:
|
|
559
578
|
env[field_instance.field_def.name] = field_instance.value
|
|
560
579
|
result.append_field_instance(field_instance)
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
580
|
+
actual_size = field_instance.field_def.size
|
|
581
|
+
else:
|
|
582
|
+
actual_size = _resolve_size(field_def_repeat, env)
|
|
583
|
+
if isinstance(current_offset, InfoSize) and isinstance(
|
|
584
|
+
actual_size, InfoSize):
|
|
585
|
+
current_offset += actual_size
|
|
586
|
+
current_position = current_offset
|
|
587
|
+
|
|
588
|
+
actual_size = InfoSize(0, 0)
|
|
589
|
+
for field_instance in result.field_instances:
|
|
590
|
+
field_def = field_instance.field_def
|
|
591
|
+
if (isinstance(field_def.offset, InfoSize)
|
|
592
|
+
and isinstance(field_def.size, InfoSize)):
|
|
593
|
+
field_end = field_def.offset + field_def.size
|
|
594
|
+
if field_end > actual_size:
|
|
595
|
+
actual_size = field_end
|
|
596
|
+
result.size = actual_size
|
|
564
597
|
append_padding_until(result.size)
|
|
565
598
|
|
|
566
599
|
return result
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import json
|
|
5
|
-
import re
|
|
6
5
|
from collections.abc import MutableMapping
|
|
7
6
|
from dataclasses import dataclass, field
|
|
8
7
|
from functools import total_ordering
|
|
@@ -112,46 +111,6 @@ class FieldDef:
|
|
|
112
111
|
default=False,
|
|
113
112
|
metadata={"desc": "Whether to reverse the field byte order"})
|
|
114
113
|
|
|
115
|
-
def split_repeat(self,
|
|
116
|
-
index: int,
|
|
117
|
-
offset: InfoSize | str | None = None,
|
|
118
|
-
size: InfoSize | str | None = None) -> "FieldDef":
|
|
119
|
-
"""Create a single repeated-field definition for the given index."""
|
|
120
|
-
split_name = f"{self.name}[{index}]"
|
|
121
|
-
split_offset_source = self.offset if offset is None else offset
|
|
122
|
-
split_size_source = self.size if size is None else size
|
|
123
|
-
split_offset = self._replace_name_in_expression(split_offset_source,
|
|
124
|
-
self.name, split_name)
|
|
125
|
-
split_size = self._replace_name_in_expression(split_size_source,
|
|
126
|
-
self.name, split_name)
|
|
127
|
-
split_type = self._replace_name_in_expression(self.type, self.name,
|
|
128
|
-
split_name)
|
|
129
|
-
split_range_expression = self._replace_name_in_expression(
|
|
130
|
-
self.range_expression, self.name, split_name)
|
|
131
|
-
split_byte_swap = self._replace_name_in_expression(
|
|
132
|
-
self.byte_swap, self.name, split_name)
|
|
133
|
-
|
|
134
|
-
return FieldDef(name=split_name,
|
|
135
|
-
offset=split_offset,
|
|
136
|
-
size=split_size,
|
|
137
|
-
type=split_type,
|
|
138
|
-
scale=self.scale,
|
|
139
|
-
repeat=None,
|
|
140
|
-
description=self.description,
|
|
141
|
-
range_expression=split_range_expression,
|
|
142
|
-
enum_def_name=self.enum_def_name,
|
|
143
|
-
byte_swap=split_byte_swap)
|
|
144
|
-
|
|
145
|
-
@staticmethod
|
|
146
|
-
def _replace_name_in_expression(value: Any, old_name: str,
|
|
147
|
-
new_name: str) -> Any:
|
|
148
|
-
"""Replace standalone old_name tokens in expression-like strings."""
|
|
149
|
-
if not isinstance(value, str):
|
|
150
|
-
return value
|
|
151
|
-
pattern = (rf"(?<![0-9A-Za-z_]){re.escape(old_name)}"
|
|
152
|
-
rf"(?![0-9A-Za-z_])")
|
|
153
|
-
return re.sub(pattern, lambda _: new_name, value)
|
|
154
|
-
|
|
155
114
|
def __lt__(self, other: object) -> bool:
|
|
156
115
|
"""Compare field definitions using a stable serialized sort key."""
|
|
157
116
|
if not isinstance(other, FieldDef):
|
|
@@ -347,6 +306,15 @@ class FieldInstance:
|
|
|
347
306
|
enum_item=enum_item,
|
|
348
307
|
is_padding=is_padding)
|
|
349
308
|
|
|
309
|
+
def with_value(self,
|
|
310
|
+
value: Any,
|
|
311
|
+
type_dict: "TypeDict | None" = None) -> "FieldInstance":
|
|
312
|
+
"""Return a new FieldInstance with the given value applied."""
|
|
313
|
+
return type(self).from_value(field_def=self.field_def,
|
|
314
|
+
value=value,
|
|
315
|
+
type_dict=type_dict,
|
|
316
|
+
is_padding=self.is_padding)
|
|
317
|
+
|
|
350
318
|
@staticmethod
|
|
351
319
|
def _resolve_enum_def(field_def: FieldDef,
|
|
352
320
|
type_dict: "TypeDict | None") -> EnumDef | None:
|
|
@@ -215,6 +215,24 @@ def test_encode_recurses_for_nested_field_types():
|
|
|
215
215
|
]
|
|
216
216
|
|
|
217
217
|
|
|
218
|
+
def test_decode_field_uses_nested_struct_instance_size():
|
|
219
|
+
"""Test that nested decoding determines the field's actual size."""
|
|
220
|
+
child_field_def = FieldDef(name="value",
|
|
221
|
+
offset=InfoSize(0, 0),
|
|
222
|
+
size=InfoSize(1, 0),
|
|
223
|
+
type="unsigned int")
|
|
224
|
+
parent_field_def = FieldDef(name="nested",
|
|
225
|
+
offset=InfoSize(0, 0),
|
|
226
|
+
size=InfoSize(4, 0),
|
|
227
|
+
type=StructDef(fields=[child_field_def]))
|
|
228
|
+
|
|
229
|
+
decoded = decode_field(parent_field_def, bytearray(b"\x7f"))
|
|
230
|
+
|
|
231
|
+
assert decoded is not None
|
|
232
|
+
assert decoded.field_def.size == InfoSize(1, 0)
|
|
233
|
+
assert decoded.value.size == InfoSize(1, 0)
|
|
234
|
+
|
|
235
|
+
|
|
218
236
|
def test_repeated_field_preserves_description():
|
|
219
237
|
"""Test that repeated fields keep their description on decoded entries."""
|
|
220
238
|
field_def = FieldDef(name="value",
|
|
@@ -572,6 +590,37 @@ def test_decode_field_returns_field_instance():
|
|
|
572
590
|
assert decoded == FieldInstance(field_def=field_def, value=127)
|
|
573
591
|
|
|
574
592
|
|
|
593
|
+
def test_decode_field_sets_actual_size_on_field_instance():
|
|
594
|
+
"""Test that decode_field stores the resolved Info size."""
|
|
595
|
+
field_def = FieldDef(name="value",
|
|
596
|
+
offset="offset",
|
|
597
|
+
size="field_size",
|
|
598
|
+
type="unsigned int")
|
|
599
|
+
|
|
600
|
+
decoded = decode_field(field_def,
|
|
601
|
+
bytearray(b"\x7f"),
|
|
602
|
+
env={
|
|
603
|
+
"offset": 0,
|
|
604
|
+
"field_size": 1
|
|
605
|
+
})
|
|
606
|
+
|
|
607
|
+
assert decoded is not None
|
|
608
|
+
assert decoded.field_def.size == InfoSize(1, 0)
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def test_decode_struct_instance_size_matches_decoded_layout():
|
|
612
|
+
"""Test that decode reports the complete decoded layout size."""
|
|
613
|
+
field_def = FieldDef(name="value",
|
|
614
|
+
offset=InfoSize(2, 0),
|
|
615
|
+
size=InfoSize(2, 0),
|
|
616
|
+
type="unsigned int")
|
|
617
|
+
data = bytearray(b"\x00\x00\x7f\x00")
|
|
618
|
+
|
|
619
|
+
decoded = decode(layout_for(StructDef(fields=[field_def])), data)
|
|
620
|
+
|
|
621
|
+
assert decoded.size == InfoSize(len(data), 0)
|
|
622
|
+
|
|
623
|
+
|
|
575
624
|
def test_encode_padding_fields_use_zero_value_and_extend_to_struct_size():
|
|
576
625
|
"""Test that padding fields are zeroed and trailing bytes are padded.
|
|
577
626
|
|
|
@@ -13,23 +13,6 @@ class ValueKind(Enum):
|
|
|
13
13
|
B = 2
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
def test_split_repeat_replaces_name_in_range_expression():
|
|
17
|
-
"""Test split_repeat replaces field-name tokens in range expressions."""
|
|
18
|
-
field_def = FieldDef(
|
|
19
|
-
name="value",
|
|
20
|
-
offset=InfoSize(0, 0),
|
|
21
|
-
size=InfoSize(1, 0),
|
|
22
|
-
type="unsigned int",
|
|
23
|
-
repeat=2,
|
|
24
|
-
range_expression=("0 <= value and value <= limit and value_count"))
|
|
25
|
-
|
|
26
|
-
split = field_def.split_repeat(1)
|
|
27
|
-
|
|
28
|
-
assert split.name == "value[1]"
|
|
29
|
-
assert split.range_expression == (
|
|
30
|
-
"0 <= value[1] and value[1] <= limit and value_count")
|
|
31
|
-
|
|
32
|
-
|
|
33
16
|
def test_field_def_json_round_trip():
|
|
34
17
|
"""Test that FieldDef can be serialized and deserialized from JSON."""
|
|
35
18
|
field_def = FieldDef(name="value",
|
|
@@ -169,6 +152,89 @@ def test_field_instance_from_value_sets_enum_item_from_field_enum_def():
|
|
|
169
152
|
assert field_instance.enum_item == ("MANUAL", 1)
|
|
170
153
|
|
|
171
154
|
|
|
155
|
+
def _mode_enum_fixture() -> tuple[EnumDef, FieldDef, TypeDict]:
|
|
156
|
+
"""Build an enum-backed field definition and its type dictionary."""
|
|
157
|
+
enum_def = EnumDef(name="Mode", values={"AUTO": 0, "MANUAL": 1})
|
|
158
|
+
field_def = FieldDef(name="mode",
|
|
159
|
+
offset=InfoSize(0, 0),
|
|
160
|
+
size=InfoSize(1, 0),
|
|
161
|
+
type="unsigned int",
|
|
162
|
+
enum_def_name=enum_def.name)
|
|
163
|
+
type_dict = TypeDict(enum_dict={enum_def.name: enum_def})
|
|
164
|
+
return enum_def, field_def, type_dict
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_field_instance_with_value_keeps_original_unchanged():
|
|
168
|
+
"""Test FieldInstance.with_value does not mutate the original."""
|
|
169
|
+
_, field_def, type_dict = _mode_enum_fixture()
|
|
170
|
+
original = FieldInstance.from_value(field_def, 0, type_dict=type_dict)
|
|
171
|
+
|
|
172
|
+
original.with_value(1, type_dict)
|
|
173
|
+
|
|
174
|
+
assert original.value == 0
|
|
175
|
+
assert original.enum_item == ("AUTO", 0)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def test_field_instance_with_value_returns_new_instance():
|
|
179
|
+
"""Test FieldInstance.with_value returns a new instance with the value."""
|
|
180
|
+
_, field_def, type_dict = _mode_enum_fixture()
|
|
181
|
+
original = FieldInstance.from_value(field_def, 0, type_dict=type_dict)
|
|
182
|
+
|
|
183
|
+
updated = original.with_value(1, type_dict)
|
|
184
|
+
|
|
185
|
+
assert updated is not original
|
|
186
|
+
assert updated.value == 1
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def test_field_instance_with_value_preserves_field_def_and_padding():
|
|
190
|
+
"""Test FieldInstance.with_value preserves field_def and is_padding."""
|
|
191
|
+
_, field_def, type_dict = _mode_enum_fixture()
|
|
192
|
+
original = FieldInstance.from_value(field_def,
|
|
193
|
+
0,
|
|
194
|
+
type_dict=type_dict,
|
|
195
|
+
is_padding=True)
|
|
196
|
+
|
|
197
|
+
updated = original.with_value(1, type_dict)
|
|
198
|
+
|
|
199
|
+
assert updated.field_def is original.field_def
|
|
200
|
+
assert updated.is_padding is True
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def test_field_instance_with_value_updates_enum_item():
|
|
204
|
+
"""Test FieldInstance.with_value recomputes a matching enum_item."""
|
|
205
|
+
_, field_def, type_dict = _mode_enum_fixture()
|
|
206
|
+
original = FieldInstance.from_value(field_def, 0, type_dict=type_dict)
|
|
207
|
+
|
|
208
|
+
updated = original.with_value(1, type_dict)
|
|
209
|
+
|
|
210
|
+
assert updated.enum_item == ("MANUAL", 1)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def test_field_instance_with_value_clears_unmatched_enum_item():
|
|
214
|
+
"""Test FieldInstance.with_value clears enum_item when unmatched."""
|
|
215
|
+
_, field_def, type_dict = _mode_enum_fixture()
|
|
216
|
+
original = FieldInstance.from_value(field_def, 0, type_dict=type_dict)
|
|
217
|
+
|
|
218
|
+
updated = original.with_value(7, type_dict)
|
|
219
|
+
|
|
220
|
+
assert updated.enum_item is None
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def test_field_instance_with_value_without_type_dict():
|
|
224
|
+
"""Test FieldInstance.with_value works for plain fields."""
|
|
225
|
+
field_def = FieldDef(name="count",
|
|
226
|
+
offset=InfoSize(0, 0),
|
|
227
|
+
size=InfoSize(1, 0),
|
|
228
|
+
type="unsigned int")
|
|
229
|
+
original = FieldInstance.from_value(field_def, 1)
|
|
230
|
+
|
|
231
|
+
updated = original.with_value(2)
|
|
232
|
+
|
|
233
|
+
assert updated.value == 2
|
|
234
|
+
assert updated.enum_item is None
|
|
235
|
+
assert original.value == 1
|
|
236
|
+
|
|
237
|
+
|
|
172
238
|
def test_struct_def_metadata_json_round_trip():
|
|
173
239
|
"""Test that StructDef name/description are preserved in JSON."""
|
|
174
240
|
child = FieldDef(name="value",
|
|
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
|