sltcodec 2.4.0__tar.gz → 2.5.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.4.0 → sltcodec-2.5.0}/PKG-INFO +25 -3
- {sltcodec-2.4.0 → sltcodec-2.5.0}/README.md +16 -0
- {sltcodec-2.4.0 → sltcodec-2.5.0}/README_pypi.md +24 -2
- {sltcodec-2.4.0 → sltcodec-2.5.0}/pyproject.toml +1 -1
- {sltcodec-2.4.0 → sltcodec-2.5.0}/src/sltcodec/__init__.py +3 -2
- {sltcodec-2.4.0 → sltcodec-2.5.0}/src/sltcodec/codec.py +109 -11
- {sltcodec-2.4.0 → sltcodec-2.5.0}/tests/test_codec.py +149 -2
- {sltcodec-2.4.0 → sltcodec-2.5.0}/.github/workflows/publish_to_pypi.yml +0 -0
- {sltcodec-2.4.0 → sltcodec-2.5.0}/.github/workflows/publish_to_testpypi.yml +0 -0
- {sltcodec-2.4.0 → sltcodec-2.5.0}/.gitignore +0 -0
- {sltcodec-2.4.0 → sltcodec-2.5.0}/.python-version +0 -0
- {sltcodec-2.4.0 → sltcodec-2.5.0}/.vscode/launch.json +0 -0
- {sltcodec-2.4.0 → sltcodec-2.5.0}/.vscode/settings.json +0 -0
- {sltcodec-2.4.0 → sltcodec-2.5.0}/LICENSE +0 -0
- {sltcodec-2.4.0 → sltcodec-2.5.0}/bump_major.ps1 +0 -0
- {sltcodec-2.4.0 → sltcodec-2.5.0}/bump_minor.ps1 +0 -0
- {sltcodec-2.4.0 → sltcodec-2.5.0}/bump_patch.ps1 +0 -0
- {sltcodec-2.4.0 → sltcodec-2.5.0}/src/sltcodec/types.py +0 -0
- {sltcodec-2.4.0 → sltcodec-2.5.0}/tests/test_persistence.py +0 -0
- {sltcodec-2.4.0 → sltcodec-2.5.0}/tests/test_types.py +0 -0
- {sltcodec-2.4.0 → sltcodec-2.5.0}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sltcodec
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.5.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
|
|
@@ -63,6 +63,26 @@ encoded = encode(layout, instance, bytearray())
|
|
|
63
63
|
decoded = decode(layout, encoded)
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
+
## Progress Callbacks
|
|
67
|
+
|
|
68
|
+
`encode` and `decode` accept an optional `progress_callback` argument. The
|
|
69
|
+
callback is typed as `ProgressCallback` and receives a `float` progress value
|
|
70
|
+
from `0.0` to `1.0`.
|
|
71
|
+
|
|
72
|
+
Progress is calculated for top-level fields as `(offset + size) / total_size`.
|
|
73
|
+
Nested structure encode/decode calls pass `None` internally, so recursive
|
|
74
|
+
fields do not emit additional callback events.
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from sltcodec import ProgressCallback, decode
|
|
78
|
+
|
|
79
|
+
def update_progress(progress: float) -> None:
|
|
80
|
+
progress_bar.value = progress
|
|
81
|
+
|
|
82
|
+
progress_callback: ProgressCallback = update_progress
|
|
83
|
+
decoded = decode(layout, encoded, progress_callback=progress_callback)
|
|
84
|
+
```
|
|
85
|
+
|
|
66
86
|
`decode` returns a `StructInstance` whose `size` is the actual end of the
|
|
67
87
|
decoded layout. `decode_field` stores the actual decoded size in the returned
|
|
68
88
|
`FieldInstance.field_def.size`. For nested `StructDef` fields, that size comes
|
|
@@ -138,6 +158,7 @@ The following symbols are exported by `sltcodec.__all__` in this order:
|
|
|
138
158
|
| Symbol | Description |
|
|
139
159
|
| --- | --- |
|
|
140
160
|
| `PRIMITIVE_TYPES` | Set of built-in field type names supported by the codec. |
|
|
161
|
+
| `ProgressCallback` | Callable type alias for encode/decode progress updates. |
|
|
141
162
|
| `EnumDef` | Immutable definition of an enumeration. |
|
|
142
163
|
| `EnumDict` | Dictionary-like container for `EnumDef` objects. |
|
|
143
164
|
| `FieldDef` | Immutable definition of one structured field. |
|
|
@@ -310,15 +331,16 @@ beginning with `_` are internal helpers and are not part of the public API.
|
|
|
310
331
|
| Definition | Signature | Description |
|
|
311
332
|
| --- | --- | --- |
|
|
312
333
|
| `PRIMITIVE_TYPES` | `set[str]` | Built-in field type names. |
|
|
334
|
+
| `ProgressCallback` | `Callable[[float], None]` | Progress callback type for top-level encode/decode updates. |
|
|
313
335
|
| `save_struct_layout` | `(struct_layout, path) -> None` | Save a layout to a JSON file. |
|
|
314
336
|
| `load_struct_layout` | `(path) -> StructLayout` | Load a layout from a JSON file. |
|
|
315
337
|
| `save_struct_def_dict` | `(path, struct_def_dict) -> None` | Save a structure-definition dictionary through the layout format. |
|
|
316
338
|
| `load_struct_def_dict` | `(path) -> dict[str, StructDef]` | Load a structure-definition dictionary. |
|
|
317
339
|
| `save_enum_def_dict` | `(path, enum_def_dict) -> None` | Save an enum-definition dictionary through the layout format. |
|
|
318
340
|
| `load_enum_def_dict` | `(path) -> dict[str, EnumDef]` | Load an enum-definition dictionary. |
|
|
319
|
-
| `encode` | `(struct_layout, struct_instance, buf, padding_alignment_bits=32) -> bytearray` | Encode a complete structure. |
|
|
341
|
+
| `encode` | `(struct_layout, struct_instance, buf, padding_alignment_bits=32, progress_callback=None) -> bytearray` | Encode a complete structure. |
|
|
320
342
|
| `decode_field` | `(field_def, data, env=None, type_dict=None, padding_alignment_bits=32) -> FieldInstance \| None` | Decode one field. |
|
|
321
|
-
| `decode` | `(struct_layout, data, padding_alignment_bits=32) -> StructInstance` | Decode a complete structure. |
|
|
343
|
+
| `decode` | `(struct_layout, data, padding_alignment_bits=32, progress_callback=None) -> StructInstance` | Decode a complete structure. |
|
|
322
344
|
|
|
323
345
|
`save_struct_def_dict`, `load_struct_def_dict`, `save_enum_def_dict`, and
|
|
324
346
|
`load_enum_def_dict` are available from `sltcodec.codec` for dictionary-level
|
|
@@ -57,6 +57,22 @@ definition is resolved by `struct_layout.struct_def_name` from
|
|
|
57
57
|
`struct_layout.type_dict.struct_dict`; enum definitions are resolved from the
|
|
58
58
|
same `TypeDict`.
|
|
59
59
|
|
|
60
|
+
`encode` and `decode` accept an optional `progress_callback` argument typed as
|
|
61
|
+
`ProgressCallback`. The callback receives a `float` progress value calculated
|
|
62
|
+
from the top-level field extent: `(offset + size) / total_size`. Recursive
|
|
63
|
+
nested structure calls pass `None`, so callers receive progress updates only
|
|
64
|
+
for the top-level encode or decode operation.
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from sltcodec import ProgressCallback, decode
|
|
68
|
+
|
|
69
|
+
def update_progress(progress: float) -> None:
|
|
70
|
+
progress_bar.value = progress
|
|
71
|
+
|
|
72
|
+
progress_callback: ProgressCallback = update_progress
|
|
73
|
+
decoded = decode(layout, data, progress_callback=progress_callback)
|
|
74
|
+
```
|
|
75
|
+
|
|
60
76
|
Layout persistence uses the explicit names `save_struct_layout` and
|
|
61
77
|
`load_struct_layout`. The former `save_type_dict` and `load_type_dict` API is
|
|
62
78
|
not part of the current public interface.
|
|
@@ -46,6 +46,26 @@ encoded = encode(layout, instance, bytearray())
|
|
|
46
46
|
decoded = decode(layout, encoded)
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
## Progress Callbacks
|
|
50
|
+
|
|
51
|
+
`encode` and `decode` accept an optional `progress_callback` argument. The
|
|
52
|
+
callback is typed as `ProgressCallback` and receives a `float` progress value
|
|
53
|
+
from `0.0` to `1.0`.
|
|
54
|
+
|
|
55
|
+
Progress is calculated for top-level fields as `(offset + size) / total_size`.
|
|
56
|
+
Nested structure encode/decode calls pass `None` internally, so recursive
|
|
57
|
+
fields do not emit additional callback events.
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from sltcodec import ProgressCallback, decode
|
|
61
|
+
|
|
62
|
+
def update_progress(progress: float) -> None:
|
|
63
|
+
progress_bar.value = progress
|
|
64
|
+
|
|
65
|
+
progress_callback: ProgressCallback = update_progress
|
|
66
|
+
decoded = decode(layout, encoded, progress_callback=progress_callback)
|
|
67
|
+
```
|
|
68
|
+
|
|
49
69
|
`decode` returns a `StructInstance` whose `size` is the actual end of the
|
|
50
70
|
decoded layout. `decode_field` stores the actual decoded size in the returned
|
|
51
71
|
`FieldInstance.field_def.size`. For nested `StructDef` fields, that size comes
|
|
@@ -121,6 +141,7 @@ The following symbols are exported by `sltcodec.__all__` in this order:
|
|
|
121
141
|
| Symbol | Description |
|
|
122
142
|
| --- | --- |
|
|
123
143
|
| `PRIMITIVE_TYPES` | Set of built-in field type names supported by the codec. |
|
|
144
|
+
| `ProgressCallback` | Callable type alias for encode/decode progress updates. |
|
|
124
145
|
| `EnumDef` | Immutable definition of an enumeration. |
|
|
125
146
|
| `EnumDict` | Dictionary-like container for `EnumDef` objects. |
|
|
126
147
|
| `FieldDef` | Immutable definition of one structured field. |
|
|
@@ -293,15 +314,16 @@ beginning with `_` are internal helpers and are not part of the public API.
|
|
|
293
314
|
| Definition | Signature | Description |
|
|
294
315
|
| --- | --- | --- |
|
|
295
316
|
| `PRIMITIVE_TYPES` | `set[str]` | Built-in field type names. |
|
|
317
|
+
| `ProgressCallback` | `Callable[[float], None]` | Progress callback type for top-level encode/decode updates. |
|
|
296
318
|
| `save_struct_layout` | `(struct_layout, path) -> None` | Save a layout to a JSON file. |
|
|
297
319
|
| `load_struct_layout` | `(path) -> StructLayout` | Load a layout from a JSON file. |
|
|
298
320
|
| `save_struct_def_dict` | `(path, struct_def_dict) -> None` | Save a structure-definition dictionary through the layout format. |
|
|
299
321
|
| `load_struct_def_dict` | `(path) -> dict[str, StructDef]` | Load a structure-definition dictionary. |
|
|
300
322
|
| `save_enum_def_dict` | `(path, enum_def_dict) -> None` | Save an enum-definition dictionary through the layout format. |
|
|
301
323
|
| `load_enum_def_dict` | `(path) -> dict[str, EnumDef]` | Load an enum-definition dictionary. |
|
|
302
|
-
| `encode` | `(struct_layout, struct_instance, buf, padding_alignment_bits=32) -> bytearray` | Encode a complete structure. |
|
|
324
|
+
| `encode` | `(struct_layout, struct_instance, buf, padding_alignment_bits=32, progress_callback=None) -> bytearray` | Encode a complete structure. |
|
|
303
325
|
| `decode_field` | `(field_def, data, env=None, type_dict=None, padding_alignment_bits=32) -> FieldInstance \| None` | Decode one field. |
|
|
304
|
-
| `decode` | `(struct_layout, data, padding_alignment_bits=32) -> StructInstance` | Decode a complete structure. |
|
|
326
|
+
| `decode` | `(struct_layout, data, padding_alignment_bits=32, progress_callback=None) -> StructInstance` | Decode a complete structure. |
|
|
305
327
|
|
|
306
328
|
`save_struct_def_dict`, `load_struct_def_dict`, `save_enum_def_dict`, and
|
|
307
329
|
`load_enum_def_dict` are available from `sltcodec.codec` for dictionary-level
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
from .codec import (PRIMITIVE_TYPES, decode, encode,
|
|
2
|
-
save_struct_layout)
|
|
1
|
+
from .codec import (PRIMITIVE_TYPES, ProgressCallback, decode, encode,
|
|
2
|
+
load_struct_layout, save_struct_layout)
|
|
3
3
|
from .types import (EnumDef, EnumDict, FieldDef, FieldInstance, StructDef,
|
|
4
4
|
StructDict, StructInstance, StructLayout, TypeDict)
|
|
5
5
|
|
|
6
6
|
__all__ = [
|
|
7
7
|
"PRIMITIVE_TYPES",
|
|
8
|
+
"ProgressCallback",
|
|
8
9
|
"EnumDef",
|
|
9
10
|
"EnumDict",
|
|
10
11
|
"FieldDef",
|
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
import json
|
|
5
5
|
import re
|
|
6
6
|
from pathlib import Path
|
|
7
|
-
from typing import Any
|
|
7
|
+
from typing import Any, Callable
|
|
8
8
|
|
|
9
9
|
from sltcalc import SltEval
|
|
10
10
|
from sltcore import Info, InfoSize, bits_get, bits_set
|
|
@@ -27,6 +27,58 @@ _PRIMITIVE_TYPES = PRIMITIVE_TYPES
|
|
|
27
27
|
|
|
28
28
|
_DEFAULT_PADDING_ALIGNMENT_BITS = 32
|
|
29
29
|
|
|
30
|
+
ProgressCallback = Callable[[float], None]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _notify_progress(progress_callback: ProgressCallback | None,
|
|
34
|
+
offset: InfoSize, size: InfoSize, total_bits: int) -> None:
|
|
35
|
+
"""Notify caller of top-level encode/decode progress."""
|
|
36
|
+
if progress_callback is None or total_bits <= 0:
|
|
37
|
+
return
|
|
38
|
+
progress = min((offset + size).bits / total_bits, 1.0)
|
|
39
|
+
progress_callback(progress)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _estimate_encode_total_bits(struct_instance: StructInstance,
|
|
43
|
+
initial_size_bits: int) -> int:
|
|
44
|
+
"""Estimate encoded top-level extent before mutating the output buffer."""
|
|
45
|
+
env: dict[str, Any] = {}
|
|
46
|
+
total_bits = max(struct_instance.size.bits, initial_size_bits)
|
|
47
|
+
|
|
48
|
+
for field_value in struct_instance.field_instances:
|
|
49
|
+
field_def = field_value.field_def
|
|
50
|
+
repeat_until_end = field_def.repeat == "end"
|
|
51
|
+
repeat = _resolve_repeat(field_def.repeat, env)
|
|
52
|
+
value = field_value.value
|
|
53
|
+
if _is_padding_field_def(field_def):
|
|
54
|
+
padding_size = _resolve_info_size(field_def.size, env)
|
|
55
|
+
value = bytearray(padding_size.bytes)
|
|
56
|
+
if repeat_until_end:
|
|
57
|
+
repeat_values = list(value)
|
|
58
|
+
repeat = len(repeat_values)
|
|
59
|
+
if repeat is not None and repeat > 1:
|
|
60
|
+
current_offset = _resolve_info_size(field_def.offset, env)
|
|
61
|
+
values = repeat_values if repeat_until_end else list(value)
|
|
62
|
+
|
|
63
|
+
for i in range(repeat):
|
|
64
|
+
field_def_repeat = _repeated_field_def(field_def, i,
|
|
65
|
+
current_offset,
|
|
66
|
+
field_def.size)
|
|
67
|
+
resolved_size = _resolve_info_size(field_def_repeat.size, env)
|
|
68
|
+
total_bits = max(total_bits,
|
|
69
|
+
(current_offset + resolved_size).bits)
|
|
70
|
+
env[field_def_repeat.name] = values[i]
|
|
71
|
+
if isinstance(current_offset, InfoSize):
|
|
72
|
+
current_offset += resolved_size
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
resolved_offset = _resolve_info_size(field_def.offset, env)
|
|
76
|
+
resolved_size = _resolve_info_size(field_def.size, env)
|
|
77
|
+
total_bits = max(total_bits, (resolved_offset + resolved_size).bits)
|
|
78
|
+
env[field_def.name] = value
|
|
79
|
+
|
|
80
|
+
return total_bits
|
|
81
|
+
|
|
30
82
|
|
|
31
83
|
def _as_field_defs(struct_def: StructDef | list[FieldDef]) -> list[FieldDef]:
|
|
32
84
|
"""Normalize StructDef/list inputs to a list of field definitions."""
|
|
@@ -134,8 +186,20 @@ def load_enum_def_dict(path: str | Path) -> dict[str, EnumDef]:
|
|
|
134
186
|
def _resolve_info_size(value: InfoSize | str, env: dict[str, Any]) -> InfoSize:
|
|
135
187
|
"""Resolve an InfoSize value that can be static or expression-based."""
|
|
136
188
|
if isinstance(value, str):
|
|
137
|
-
|
|
138
|
-
|
|
189
|
+
eval_env = dict(env)
|
|
190
|
+
|
|
191
|
+
def replace_info_size(match: re.Match[str]) -> str:
|
|
192
|
+
name = f"_info_size_{len(eval_env)}"
|
|
193
|
+
eval_env[name] = InfoSize(int(match.group(1)), int(match.group(2)))
|
|
194
|
+
return name
|
|
195
|
+
|
|
196
|
+
expression = re.sub(
|
|
197
|
+
r"\bInfoSize\s*\(\s*(-?\d+)\s*,\s*(-?\d+)\s*\)",
|
|
198
|
+
replace_info_size,
|
|
199
|
+
value,
|
|
200
|
+
)
|
|
201
|
+
stleval = SltEval(eval_env)
|
|
202
|
+
resolved_byte = stleval.eval(expression)
|
|
139
203
|
if isinstance(resolved_byte, InfoSize):
|
|
140
204
|
return resolved_byte
|
|
141
205
|
return InfoSize(resolved_byte, 0)
|
|
@@ -164,6 +228,13 @@ def _is_padding_field_def(field_def: FieldDef) -> bool:
|
|
|
164
228
|
and field_def.type in ["bytes", "bytearray"])
|
|
165
229
|
|
|
166
230
|
|
|
231
|
+
def _struct_def_has_dynamic_extent(struct_def: StructDef) -> bool:
|
|
232
|
+
"""Check whether a nested structure can consume data beyond its fields."""
|
|
233
|
+
return any(field_def.repeat == "end" or isinstance(field_def.offset, str)
|
|
234
|
+
or isinstance(field_def.size, str)
|
|
235
|
+
for field_def in struct_def.fields)
|
|
236
|
+
|
|
237
|
+
|
|
167
238
|
def _validate_padding_alignment_bits(padding_alignment_bits: int) -> None:
|
|
168
239
|
"""Validate padding alignment as a positive power-of-two bit size."""
|
|
169
240
|
if (isinstance(padding_alignment_bits, bool)
|
|
@@ -234,12 +305,11 @@ def _prepare_field_info(
|
|
|
234
305
|
raise TypeError(
|
|
235
306
|
"Nested StructDef fields must be encoded with StructInstance "
|
|
236
307
|
"values")
|
|
237
|
-
nested_bytes = encode(
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
padding_alignment_bits=padding_alignment_bits)
|
|
308
|
+
nested_bytes = encode(
|
|
309
|
+
_layout_for_struct_def(value.struct_def,
|
|
310
|
+
type_dict,
|
|
311
|
+
name=field_def.name), value, bytearray(),
|
|
312
|
+
padding_alignment_bits, None)
|
|
243
313
|
info = Info.from_bytes(bytes(nested_bytes), size, scale=field_def.scale)
|
|
244
314
|
else:
|
|
245
315
|
info = _encode_primitive(resolved_type, value, size, field_def.scale)
|
|
@@ -304,6 +374,7 @@ def encode(
|
|
|
304
374
|
struct_instance: StructInstance,
|
|
305
375
|
buf: bytearray,
|
|
306
376
|
padding_alignment_bits: int = _DEFAULT_PADDING_ALIGNMENT_BITS,
|
|
377
|
+
progress_callback: ProgressCallback | None = None,
|
|
307
378
|
) -> bytearray:
|
|
308
379
|
"""Encode decode() result into a bytearray.
|
|
309
380
|
|
|
@@ -318,6 +389,9 @@ def encode(
|
|
|
318
389
|
The base bytearray instance to write into.
|
|
319
390
|
padding_alignment_bits : int, optional
|
|
320
391
|
The padding alignment boundary in bits, by default 32.
|
|
392
|
+
progress_callback : Callable[[float], None] | None, optional
|
|
393
|
+
Called after each top-level field is encoded with progress in the
|
|
394
|
+
range 0.0 to 1.0. Nested recursive encodes are skipped.
|
|
321
395
|
|
|
322
396
|
Returns
|
|
323
397
|
-------
|
|
@@ -334,6 +408,7 @@ def encode(
|
|
|
334
408
|
type_dict = struct_layout.type_dict
|
|
335
409
|
env: dict[str, Any] = {}
|
|
336
410
|
has_padding = False
|
|
411
|
+
total_bits = _estimate_encode_total_bits(struct_instance, len(buf) * 8)
|
|
337
412
|
|
|
338
413
|
for field_value in struct_instance.field_instances:
|
|
339
414
|
field_def = field_value.field_def
|
|
@@ -359,13 +434,19 @@ def encode(
|
|
|
359
434
|
resolved_size = _resolve_info_size(field_def_repeat.size, env)
|
|
360
435
|
encode_field(field_def_repeat, values[i], buf, env, type_dict,
|
|
361
436
|
padding_alignment_bits)
|
|
437
|
+
_notify_progress(progress_callback, current_offset,
|
|
438
|
+
resolved_size, total_bits)
|
|
362
439
|
env[field_def_repeat.name] = values[i]
|
|
363
440
|
if isinstance(current_offset, InfoSize):
|
|
364
441
|
current_offset += resolved_size
|
|
365
442
|
continue
|
|
366
443
|
|
|
444
|
+
resolved_offset = _resolve_info_size(field_def.offset, env)
|
|
445
|
+
resolved_size = _resolve_info_size(field_def.size, env)
|
|
367
446
|
encode_field(field_def, value, buf, env, type_dict,
|
|
368
447
|
padding_alignment_bits)
|
|
448
|
+
_notify_progress(progress_callback, resolved_offset, resolved_size,
|
|
449
|
+
total_bits)
|
|
369
450
|
env[field_def.name] = value
|
|
370
451
|
|
|
371
452
|
if has_padding and struct_instance.size.bytes > len(buf):
|
|
@@ -415,8 +496,15 @@ def decode_field(
|
|
|
415
496
|
nested_layout = _layout_for_struct_def(resolved_type,
|
|
416
497
|
type_dict,
|
|
417
498
|
name=field_def.name)
|
|
418
|
-
|
|
419
|
-
|
|
499
|
+
nested_data = bytearray(info.to_bytes)
|
|
500
|
+
if _struct_def_has_dynamic_extent(resolved_type) and offset.bit == 0:
|
|
501
|
+
nested_data = bytearray(data[offset.byte:])
|
|
502
|
+
nested_value = decode(
|
|
503
|
+
nested_layout,
|
|
504
|
+
nested_data,
|
|
505
|
+
padding_alignment_bits,
|
|
506
|
+
None,
|
|
507
|
+
)
|
|
420
508
|
actual_size = nested_value.size
|
|
421
509
|
else:
|
|
422
510
|
actual_size = info.info_size
|
|
@@ -485,6 +573,7 @@ def decode(
|
|
|
485
573
|
struct_layout: StructLayout,
|
|
486
574
|
data: bytearray | bytes,
|
|
487
575
|
padding_alignment_bits: int = _DEFAULT_PADDING_ALIGNMENT_BITS,
|
|
576
|
+
progress_callback: ProgressCallback | None = None,
|
|
488
577
|
) -> StructInstance:
|
|
489
578
|
"""Decode a bytearray into field values according to a layout.
|
|
490
579
|
|
|
@@ -497,6 +586,9 @@ def decode(
|
|
|
497
586
|
The data to decode.
|
|
498
587
|
padding_alignment_bits : int, optional
|
|
499
588
|
The padding alignment boundary in bits, by default 32.
|
|
589
|
+
progress_callback : Callable[[float], None] | None, optional
|
|
590
|
+
Called after each top-level field is decoded with progress in the
|
|
591
|
+
range 0.0 to 1.0. Nested recursive decodes are skipped.
|
|
500
592
|
Returns
|
|
501
593
|
-------
|
|
502
594
|
StructInstance
|
|
@@ -582,6 +674,8 @@ def decode(
|
|
|
582
674
|
if field_instance is not None:
|
|
583
675
|
env[field_instance.field_def.name] = field_instance.value
|
|
584
676
|
result.append_field_instance(field_instance)
|
|
677
|
+
_notify_progress(progress_callback, resolved_offset,
|
|
678
|
+
field_instance.field_def.size, data_bits)
|
|
585
679
|
current_position = (resolved_offset +
|
|
586
680
|
_resolve_info_size(field_def.size, env))
|
|
587
681
|
continue
|
|
@@ -621,6 +715,8 @@ def decode(
|
|
|
621
715
|
env[field_instance.field_def.name] = field_instance.value
|
|
622
716
|
result.append_field_instance(field_instance)
|
|
623
717
|
actual_size = field_instance.field_def.size
|
|
718
|
+
_notify_progress(progress_callback, current_offset,
|
|
719
|
+
actual_size, data_bits)
|
|
624
720
|
else:
|
|
625
721
|
actual_size = resolved_size
|
|
626
722
|
|
|
@@ -666,6 +762,8 @@ def decode(
|
|
|
666
762
|
env[field_instance.field_def.name] = field_instance.value
|
|
667
763
|
result.append_field_instance(field_instance)
|
|
668
764
|
actual_size = field_instance.field_def.size
|
|
765
|
+
_notify_progress(progress_callback, current_offset, actual_size,
|
|
766
|
+
data_bits)
|
|
669
767
|
else:
|
|
670
768
|
actual_size = _resolve_info_size(field_def_repeat.size, env)
|
|
671
769
|
if isinstance(current_offset, InfoSize) and isinstance(
|
|
@@ -5,8 +5,8 @@ import pytest
|
|
|
5
5
|
from sltcore import InfoSize
|
|
6
6
|
|
|
7
7
|
from sltcodec import (PRIMITIVE_TYPES, EnumDef, FieldDef, FieldInstance,
|
|
8
|
-
StructDef, StructInstance, StructLayout,
|
|
9
|
-
encode)
|
|
8
|
+
ProgressCallback, StructDef, StructInstance, StructLayout,
|
|
9
|
+
TypeDict, decode, encode)
|
|
10
10
|
from sltcodec.codec import _resolve_info_size, decode_field
|
|
11
11
|
|
|
12
12
|
|
|
@@ -42,6 +42,13 @@ def test_primitive_types_are_exposed_at_package_root():
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
|
|
45
|
+
def test_progress_callback_type_is_exposed_at_package_root():
|
|
46
|
+
"""Test that ProgressCallback is exposed for external consumers."""
|
|
47
|
+
progress_callback: ProgressCallback = lambda progress: None
|
|
48
|
+
|
|
49
|
+
assert callable(progress_callback)
|
|
50
|
+
|
|
51
|
+
|
|
45
52
|
def test_encode_and_decode_round_trip():
|
|
46
53
|
"""Test that encoding and then decoding returns the original data."""
|
|
47
54
|
struct_def = [
|
|
@@ -75,6 +82,44 @@ def test_encode_and_decode_round_trip():
|
|
|
75
82
|
bytearray()) == encoded
|
|
76
83
|
|
|
77
84
|
|
|
85
|
+
def test_encode_and_decode_progress_callbacks_use_top_level_progress():
|
|
86
|
+
"""Test progress callbacks report top-level encoded/decoded progress."""
|
|
87
|
+
fields = [
|
|
88
|
+
FieldDef(name="head",
|
|
89
|
+
offset=InfoSize(0, 0),
|
|
90
|
+
size=InfoSize(1, 0),
|
|
91
|
+
type="unsigned int"),
|
|
92
|
+
FieldDef(name="tail",
|
|
93
|
+
offset=InfoSize(1, 0),
|
|
94
|
+
size=InfoSize(1, 0),
|
|
95
|
+
type="unsigned int"),
|
|
96
|
+
]
|
|
97
|
+
struct_def = StructDef(fields=fields)
|
|
98
|
+
struct_instance = StructInstance(
|
|
99
|
+
struct_def=struct_def,
|
|
100
|
+
field_instances=[
|
|
101
|
+
FieldInstance(fields[0], 0x12),
|
|
102
|
+
FieldInstance(fields[1], 0x34),
|
|
103
|
+
],
|
|
104
|
+
)
|
|
105
|
+
encode_progress = []
|
|
106
|
+
decode_progress = []
|
|
107
|
+
|
|
108
|
+
encoded = encode(layout_for(struct_def),
|
|
109
|
+
struct_instance,
|
|
110
|
+
bytearray(),
|
|
111
|
+
progress_callback=encode_progress.append)
|
|
112
|
+
decoded = decode(layout_for(struct_def),
|
|
113
|
+
encoded,
|
|
114
|
+
progress_callback=decode_progress.append)
|
|
115
|
+
|
|
116
|
+
assert encoded == bytearray(b"\x12\x34")
|
|
117
|
+
assert [field_instance.value
|
|
118
|
+
for field_instance in decoded.field_instances] == [0x12, 0x34]
|
|
119
|
+
assert encode_progress == [0.5, 1.0]
|
|
120
|
+
assert decode_progress == [0.5, 1.0]
|
|
121
|
+
|
|
122
|
+
|
|
78
123
|
def test_decode_accepts_struct_layout():
|
|
79
124
|
"""Decode a legacy field list via a StructLayout bundle."""
|
|
80
125
|
struct_def = StructDef(
|
|
@@ -166,6 +211,33 @@ def test_encode_layout_handles_repeat():
|
|
|
166
211
|
]
|
|
167
212
|
|
|
168
213
|
|
|
214
|
+
def test_progress_callbacks_use_repeated_top_level_field_extent():
|
|
215
|
+
"""Test repeated fields report progress against the encoded extent."""
|
|
216
|
+
field_def = FieldDef(name="value",
|
|
217
|
+
offset=InfoSize(0, 0),
|
|
218
|
+
size=InfoSize(1, 0),
|
|
219
|
+
type="unsigned int",
|
|
220
|
+
repeat=2)
|
|
221
|
+
struct_def = StructDef(fields=[field_def])
|
|
222
|
+
struct_instance = StructInstance(
|
|
223
|
+
struct_def=struct_def,
|
|
224
|
+
field_instances=[FieldInstance(field_def, [1, 2])],
|
|
225
|
+
)
|
|
226
|
+
encode_progress = []
|
|
227
|
+
decode_progress = []
|
|
228
|
+
|
|
229
|
+
encoded = encode(layout_for(struct_def),
|
|
230
|
+
struct_instance,
|
|
231
|
+
bytearray(),
|
|
232
|
+
progress_callback=encode_progress.append)
|
|
233
|
+
decode(layout_for([field_def]),
|
|
234
|
+
encoded,
|
|
235
|
+
progress_callback=decode_progress.append)
|
|
236
|
+
|
|
237
|
+
assert encode_progress == [0.5, 1.0]
|
|
238
|
+
assert decode_progress == [0.5, 1.0]
|
|
239
|
+
|
|
240
|
+
|
|
169
241
|
def test_expression_repeat_uses_previous_field_value():
|
|
170
242
|
"""Test that repeat expressions are evaluated against the field env."""
|
|
171
243
|
seed_field = FieldDef(name="count",
|
|
@@ -232,6 +304,38 @@ def test_repeat_end_stops_when_offset_is_out_of_range():
|
|
|
232
304
|
assert [instance.value for instance in decoded.field_instances] == [None]
|
|
233
305
|
|
|
234
306
|
|
|
307
|
+
def test_info_size_expression_resolves_constructor():
|
|
308
|
+
"""Test that serialized InfoSize constructors work in expressions."""
|
|
309
|
+
resolved = _resolve_info_size("InfoSize(1, 0) * count", {"count": 3})
|
|
310
|
+
|
|
311
|
+
assert resolved == InfoSize(3, 0)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def test_repeat_end_uses_dynamic_nested_struct_size():
|
|
315
|
+
"""Test repeat='end' advances by a variable-size nested structure."""
|
|
316
|
+
length_field = FieldDef(name="length",
|
|
317
|
+
offset=InfoSize(0, 0),
|
|
318
|
+
size=InfoSize(1, 0),
|
|
319
|
+
type="unsigned int")
|
|
320
|
+
payload_field = FieldDef(name="payload",
|
|
321
|
+
offset=InfoSize(1, 0),
|
|
322
|
+
size="InfoSize(1, 0) * length",
|
|
323
|
+
type="bytearray")
|
|
324
|
+
record_field = FieldDef(
|
|
325
|
+
name="record",
|
|
326
|
+
offset=InfoSize(0, 0),
|
|
327
|
+
size=InfoSize(1, 0),
|
|
328
|
+
type=StructDef(fields=[length_field, payload_field]),
|
|
329
|
+
repeat="end")
|
|
330
|
+
|
|
331
|
+
decoded = decode(layout_for([record_field]), bytearray(b"\x02ab\x01c"))
|
|
332
|
+
|
|
333
|
+
assert [instance.field_def.name for instance in decoded.field_instances
|
|
334
|
+
] == ["record[0]", "record[1]"]
|
|
335
|
+
assert [instance.value.size for instance in decoded.field_instances
|
|
336
|
+
] == [InfoSize(3, 0), InfoSize(2, 0)]
|
|
337
|
+
|
|
338
|
+
|
|
235
339
|
def test_decode_out_of_range_sets_current_and_tail_values_to_none():
|
|
236
340
|
"""Test out-of-range decode fills current/tail field values with None."""
|
|
237
341
|
fields = [
|
|
@@ -331,6 +435,49 @@ def test_encode_recurses_for_nested_field_types():
|
|
|
331
435
|
]
|
|
332
436
|
|
|
333
437
|
|
|
438
|
+
def test_progress_callbacks_skip_nested_recursive_fields():
|
|
439
|
+
"""Test nested recursive encode/decode calls do not report progress."""
|
|
440
|
+
child_field_defs = [
|
|
441
|
+
FieldDef(name="left",
|
|
442
|
+
offset=InfoSize(0, 0),
|
|
443
|
+
size=InfoSize(1, 0),
|
|
444
|
+
type="unsigned int"),
|
|
445
|
+
FieldDef(name="right",
|
|
446
|
+
offset=InfoSize(1, 0),
|
|
447
|
+
size=InfoSize(1, 0),
|
|
448
|
+
type="unsigned int"),
|
|
449
|
+
]
|
|
450
|
+
parent_field_def = FieldDef(name="pair",
|
|
451
|
+
offset=InfoSize(0, 0),
|
|
452
|
+
size=InfoSize(2, 0),
|
|
453
|
+
type=StructDef(fields=child_field_defs))
|
|
454
|
+
child_instance = StructInstance(
|
|
455
|
+
struct_def=StructDef(fields=child_field_defs),
|
|
456
|
+
field_instances=[
|
|
457
|
+
FieldInstance(child_field_defs[0], 3),
|
|
458
|
+
FieldInstance(child_field_defs[1], 4),
|
|
459
|
+
],
|
|
460
|
+
)
|
|
461
|
+
parent_struct_def = StructDef(fields=[parent_field_def])
|
|
462
|
+
parent_instance = StructInstance(
|
|
463
|
+
struct_def=parent_struct_def,
|
|
464
|
+
field_instances=[FieldInstance(parent_field_def, child_instance)],
|
|
465
|
+
)
|
|
466
|
+
encode_progress = []
|
|
467
|
+
decode_progress = []
|
|
468
|
+
|
|
469
|
+
encoded = encode(layout_for(parent_struct_def),
|
|
470
|
+
parent_instance,
|
|
471
|
+
bytearray(),
|
|
472
|
+
progress_callback=encode_progress.append)
|
|
473
|
+
decode(layout_for(parent_struct_def),
|
|
474
|
+
encoded,
|
|
475
|
+
progress_callback=decode_progress.append)
|
|
476
|
+
|
|
477
|
+
assert encode_progress == [1.0]
|
|
478
|
+
assert decode_progress == [1.0]
|
|
479
|
+
|
|
480
|
+
|
|
334
481
|
def test_decode_field_uses_nested_struct_instance_size():
|
|
335
482
|
"""Test that nested decoding determines the field's actual size."""
|
|
336
483
|
child_field_def = 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
|
|
File without changes
|
|
File without changes
|