sltcodec 0.4.0__tar.gz → 0.6.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-0.4.0 → sltcodec-0.6.0}/PKG-INFO +12 -1
- {sltcodec-0.4.0 → sltcodec-0.6.0}/README.md +11 -0
- {sltcodec-0.4.0 → sltcodec-0.6.0}/pyproject.toml +1 -1
- {sltcodec-0.4.0 → sltcodec-0.6.0}/src/sltcodec/__init__.py +3 -2
- {sltcodec-0.4.0 → sltcodec-0.6.0}/src/sltcodec/codec.py +22 -8
- {sltcodec-0.4.0 → sltcodec-0.6.0}/src/sltcodec/types.py +71 -18
- {sltcodec-0.4.0 → sltcodec-0.6.0}/tests/test_codec.py +24 -8
- {sltcodec-0.4.0 → sltcodec-0.6.0}/uv.lock +1 -1
- {sltcodec-0.4.0 → sltcodec-0.6.0}/.github/workflows/publish_to_pypi.yml +0 -0
- {sltcodec-0.4.0 → sltcodec-0.6.0}/.github/workflows/publish_to_testpypi.yml +0 -0
- {sltcodec-0.4.0 → sltcodec-0.6.0}/.gitignore +0 -0
- {sltcodec-0.4.0 → sltcodec-0.6.0}/.python-version +0 -0
- {sltcodec-0.4.0 → sltcodec-0.6.0}/.vscode/launch.json +0 -0
- {sltcodec-0.4.0 → sltcodec-0.6.0}/.vscode/settings.json +0 -0
- {sltcodec-0.4.0 → sltcodec-0.6.0}/LICENSE +0 -0
- {sltcodec-0.4.0 → sltcodec-0.6.0}/bump_major.ps1 +0 -0
- {sltcodec-0.4.0 → sltcodec-0.6.0}/bump_minor.ps1 +0 -0
- {sltcodec-0.4.0 → sltcodec-0.6.0}/bump_patch.ps1 +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sltcodec
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.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
|
|
@@ -25,6 +25,17 @@ sltcodec is a small package for decoding and encoding bytearrays according to st
|
|
|
25
25
|
pip install sltcodec
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
+
## Primitive Type Set
|
|
29
|
+
|
|
30
|
+
`PRIMITIVE_TYPES` is the public set of built-in field types understood by the codec.
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from sltcodec import PRIMITIVE_TYPES
|
|
34
|
+
|
|
35
|
+
print(PRIMITIVE_TYPES)
|
|
36
|
+
# {'bool', 'signed int', 'int', 'unsigned int', 'float', 'bytearray', 'bytes'}
|
|
37
|
+
```
|
|
38
|
+
|
|
28
39
|
## Quick Example
|
|
29
40
|
|
|
30
41
|
`FieldDef.description` is an optional human-readable note that can be attached to each field definition.
|
|
@@ -8,6 +8,17 @@ sltcodec is a small package for decoding and encoding bytearrays according to st
|
|
|
8
8
|
pip install sltcodec
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
## Primitive Type Set
|
|
12
|
+
|
|
13
|
+
`PRIMITIVE_TYPES` is the public set of built-in field types understood by the codec.
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from sltcodec import PRIMITIVE_TYPES
|
|
17
|
+
|
|
18
|
+
print(PRIMITIVE_TYPES)
|
|
19
|
+
# {'bool', 'signed int', 'int', 'unsigned int', 'float', 'bytearray', 'bytes'}
|
|
20
|
+
```
|
|
21
|
+
|
|
11
22
|
## Quick Example
|
|
12
23
|
|
|
13
24
|
`FieldDef.description` is an optional human-readable note that can be attached to each field definition.
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
from .codec import (decode, encode, encode_field,
|
|
2
|
-
save_struct_def_dict)
|
|
1
|
+
from .codec import (PRIMITIVE_TYPES, decode, encode, encode_field,
|
|
2
|
+
load_struct_def_dict, save_struct_def_dict)
|
|
3
3
|
from .types import EnumDef, FieldDef, FieldInstance, StructDef, StructInstance
|
|
4
4
|
|
|
5
5
|
__all__ = [
|
|
6
|
+
"PRIMITIVE_TYPES",
|
|
6
7
|
"EnumDef",
|
|
7
8
|
"FieldDef",
|
|
8
9
|
"FieldInstance",
|
|
@@ -10,7 +10,7 @@ from sltcore import Info, InfoSize, bits_get, bits_set
|
|
|
10
10
|
|
|
11
11
|
from .types import EnumDef, FieldDef, FieldInstance, StructDef, StructInstance
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
PRIMITIVE_TYPES = {
|
|
14
14
|
"bool",
|
|
15
15
|
"signed int",
|
|
16
16
|
"int",
|
|
@@ -20,6 +20,9 @@ _PRIMITIVE_TYPES = {
|
|
|
20
20
|
"bytes",
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
# Backward-compatible alias for internal usage.
|
|
24
|
+
_PRIMITIVE_TYPES = PRIMITIVE_TYPES
|
|
25
|
+
|
|
23
26
|
_DEFAULT_PADDING_ALIGNMENT_BITS = 32
|
|
24
27
|
|
|
25
28
|
|
|
@@ -319,34 +322,45 @@ def decode_field(
|
|
|
319
322
|
return None
|
|
320
323
|
info = bits_get(data, offset, size, scale=field_def.scale)
|
|
321
324
|
resolved_type = _resolve_field_type(field_def.type, struct_def_dict, env)
|
|
325
|
+
resolved_field_def = FieldDef(
|
|
326
|
+
name=field_def.name,
|
|
327
|
+
offset=offset,
|
|
328
|
+
size=size,
|
|
329
|
+
type=resolved_type,
|
|
330
|
+
scale=field_def.scale,
|
|
331
|
+
repeat=field_def.repeat,
|
|
332
|
+
description=field_def.description,
|
|
333
|
+
range_expression=field_def.range_expression,
|
|
334
|
+
enum_def=field_def.enum_def,
|
|
335
|
+
)
|
|
322
336
|
if isinstance(resolved_type, StructDef):
|
|
323
337
|
return FieldInstance(
|
|
324
|
-
field_def=
|
|
338
|
+
field_def=resolved_field_def,
|
|
325
339
|
value=decode(resolved_type, bytearray(info.to_bytes),
|
|
326
340
|
struct_def_dict, enum_def_dict,
|
|
327
341
|
padding_alignment_bits),
|
|
328
342
|
)
|
|
329
343
|
if resolved_type == "bool":
|
|
330
|
-
return FieldInstance.from_value(
|
|
344
|
+
return FieldInstance.from_value(resolved_field_def,
|
|
331
345
|
info.to_bool,
|
|
332
346
|
enum_def_dict=enum_def_dict)
|
|
333
347
|
if resolved_type == "signed int":
|
|
334
|
-
return FieldInstance.from_value(
|
|
348
|
+
return FieldInstance.from_value(resolved_field_def,
|
|
335
349
|
info.to_signed_int,
|
|
336
350
|
enum_def_dict=enum_def_dict)
|
|
337
351
|
if resolved_type in ["int", "unsigned int"]:
|
|
338
|
-
return FieldInstance.from_value(
|
|
352
|
+
return FieldInstance.from_value(resolved_field_def,
|
|
339
353
|
info.to_unsigned_int,
|
|
340
354
|
enum_def_dict=enum_def_dict)
|
|
341
355
|
if resolved_type == "float":
|
|
342
|
-
return FieldInstance.from_value(
|
|
356
|
+
return FieldInstance.from_value(resolved_field_def,
|
|
343
357
|
info.to_float,
|
|
344
358
|
enum_def_dict=enum_def_dict)
|
|
345
359
|
if resolved_type in ["bytearray", "bytes"]:
|
|
346
|
-
return FieldInstance.from_value(
|
|
360
|
+
return FieldInstance.from_value(resolved_field_def,
|
|
347
361
|
info.to_bytes,
|
|
348
362
|
enum_def_dict=enum_def_dict)
|
|
349
|
-
return FieldInstance.from_value(
|
|
363
|
+
return FieldInstance.from_value(resolved_field_def,
|
|
350
364
|
info.raw_value,
|
|
351
365
|
enum_def_dict=enum_def_dict)
|
|
352
366
|
|
|
@@ -4,12 +4,14 @@ from __future__ import annotations
|
|
|
4
4
|
import json
|
|
5
5
|
import re
|
|
6
6
|
from dataclasses import dataclass, field
|
|
7
|
+
from functools import total_ordering
|
|
7
8
|
from typing import Any
|
|
8
9
|
|
|
9
10
|
from sltcalc import SltEval
|
|
10
11
|
from sltcore import Info, InfoSize
|
|
11
12
|
|
|
12
13
|
|
|
14
|
+
@total_ordering
|
|
13
15
|
@dataclass(frozen=True)
|
|
14
16
|
class EnumDef:
|
|
15
17
|
"""An enumeration definition."""
|
|
@@ -67,7 +69,22 @@ class EnumDef:
|
|
|
67
69
|
"""Create an enum definition from a JSON string."""
|
|
68
70
|
return cls.from_dict(json.loads(data))
|
|
69
71
|
|
|
72
|
+
def __lt__(self, other: object) -> bool:
|
|
73
|
+
"""Compare enum definitions using a stable serialized sort key."""
|
|
74
|
+
if not isinstance(other, EnumDef):
|
|
75
|
+
return NotImplemented
|
|
76
|
+
return self._sort_key() < other._sort_key()
|
|
77
|
+
|
|
78
|
+
def _sort_key(self) -> tuple[Any, ...]:
|
|
79
|
+
"""Build a stable comparison key for ordering enum definitions."""
|
|
80
|
+
return (
|
|
81
|
+
self.name,
|
|
82
|
+
"" if self.description is None else self.description,
|
|
83
|
+
json.dumps(self.values, sort_keys=True),
|
|
84
|
+
)
|
|
70
85
|
|
|
86
|
+
|
|
87
|
+
@total_ordering
|
|
71
88
|
@dataclass(frozen=True)
|
|
72
89
|
class FieldDef:
|
|
73
90
|
"""A field in a structured layout."""
|
|
@@ -283,6 +300,7 @@ class FieldDef:
|
|
|
283
300
|
sort_keys=True)
|
|
284
301
|
|
|
285
302
|
|
|
303
|
+
@total_ordering
|
|
286
304
|
@dataclass(frozen=True)
|
|
287
305
|
class FieldInstance:
|
|
288
306
|
"""A decoded/encodable field value with its field definition."""
|
|
@@ -355,6 +373,7 @@ class FieldInstance:
|
|
|
355
373
|
return None
|
|
356
374
|
|
|
357
375
|
|
|
376
|
+
@total_ordering
|
|
358
377
|
@dataclass(frozen=True)
|
|
359
378
|
class StructDef:
|
|
360
379
|
"""A structured layout definition that groups multiple fields."""
|
|
@@ -369,6 +388,21 @@ class StructDef:
|
|
|
369
388
|
metadata={"desc": "The fields of the structure"},
|
|
370
389
|
)
|
|
371
390
|
|
|
391
|
+
def __lt__(self, other: object) -> bool:
|
|
392
|
+
"""Compare structure definitions using a stable serialized sort key."""
|
|
393
|
+
if not isinstance(other, StructDef):
|
|
394
|
+
return NotImplemented
|
|
395
|
+
return self._sort_key() < other._sort_key()
|
|
396
|
+
|
|
397
|
+
def _sort_key(self) -> tuple[Any, ...]:
|
|
398
|
+
"""Build a stable comparison key for ordering structure definitions."""
|
|
399
|
+
return (
|
|
400
|
+
self.name,
|
|
401
|
+
"" if self.description is None else self.description,
|
|
402
|
+
json.dumps([field.to_dict() for field in self.fields],
|
|
403
|
+
sort_keys=True),
|
|
404
|
+
)
|
|
405
|
+
|
|
372
406
|
def to_dict(self) -> dict[str, Any]:
|
|
373
407
|
"""Convert this structure definition to JSON-serializable data."""
|
|
374
408
|
return {
|
|
@@ -402,6 +436,7 @@ class StructDef:
|
|
|
402
436
|
return cls.from_dict(json.loads(data))
|
|
403
437
|
|
|
404
438
|
|
|
439
|
+
@total_ordering
|
|
405
440
|
@dataclass
|
|
406
441
|
class StructInstance:
|
|
407
442
|
"""A decoded/encodable structure instance."""
|
|
@@ -454,6 +489,23 @@ class StructInstance:
|
|
|
454
489
|
"""Return one field instance by index."""
|
|
455
490
|
return self.field_instances[index]
|
|
456
491
|
|
|
492
|
+
def __lt__(self, other: object) -> bool:
|
|
493
|
+
"""Compare structure instances using a stable serialized sort key."""
|
|
494
|
+
if not isinstance(other, StructInstance):
|
|
495
|
+
return NotImplemented
|
|
496
|
+
return self._sort_key() < other._sort_key()
|
|
497
|
+
|
|
498
|
+
def _sort_key(self) -> tuple[Any, ...]:
|
|
499
|
+
"""Build a stable comparison key for ordering structure instances."""
|
|
500
|
+
return (
|
|
501
|
+
self.struct_def._sort_key(),
|
|
502
|
+
json.dumps([
|
|
503
|
+
field_instance.to_dict()
|
|
504
|
+
for field_instance in self.field_instances
|
|
505
|
+
],
|
|
506
|
+
sort_keys=True),
|
|
507
|
+
)
|
|
508
|
+
|
|
457
509
|
def _sort_field_instances(self) -> None:
|
|
458
510
|
"""Keep field instances sorted by FieldDef order."""
|
|
459
511
|
self.field_instances.sort()
|
|
@@ -466,14 +518,25 @@ class StructInstance:
|
|
|
466
518
|
]
|
|
467
519
|
non_padding_instances.sort()
|
|
468
520
|
|
|
521
|
+
has_unresolved_layout = any(
|
|
522
|
+
not isinstance(field_instance.field_def.offset, InfoSize)
|
|
523
|
+
or not isinstance(field_instance.field_def.size, InfoSize)
|
|
524
|
+
for field_instance in non_padding_instances)
|
|
525
|
+
if has_unresolved_layout:
|
|
526
|
+
# Layout expressions are resolved later in codec paths.
|
|
527
|
+
self.field_instances = non_padding_instances
|
|
528
|
+
self._sort_field_instances()
|
|
529
|
+
self._update_size()
|
|
530
|
+
return
|
|
531
|
+
|
|
469
532
|
rebuilt_instances: list[FieldInstance] = []
|
|
470
533
|
current_offset = InfoSize(0, 0)
|
|
471
534
|
padding_index = 0
|
|
472
535
|
|
|
473
536
|
for field_instance in non_padding_instances:
|
|
474
537
|
field_def = field_instance.field_def
|
|
475
|
-
field_offset =
|
|
476
|
-
field_size =
|
|
538
|
+
field_offset = field_def.offset
|
|
539
|
+
field_size = field_def.size
|
|
477
540
|
if field_offset > current_offset:
|
|
478
541
|
gap_size = field_offset - current_offset
|
|
479
542
|
if gap_size.byte > 0 or gap_size.bit > 0:
|
|
@@ -521,25 +584,15 @@ class StructInstance:
|
|
|
521
584
|
max_end_offset = InfoSize(0, 0)
|
|
522
585
|
for field_instance in self.field_instances:
|
|
523
586
|
field_def = field_instance.field_def
|
|
524
|
-
|
|
525
|
-
|
|
587
|
+
if not isinstance(field_def.offset, InfoSize):
|
|
588
|
+
continue
|
|
589
|
+
if not isinstance(field_def.size, InfoSize):
|
|
590
|
+
continue
|
|
591
|
+
field_offset = field_def.offset
|
|
592
|
+
field_size = field_def.size
|
|
526
593
|
field_end = field_offset + field_size
|
|
527
594
|
if field_end > max_end_offset:
|
|
528
595
|
max_end_offset = field_end
|
|
529
596
|
|
|
530
597
|
if max_end_offset > self.size:
|
|
531
598
|
self.size = max_end_offset
|
|
532
|
-
|
|
533
|
-
@staticmethod
|
|
534
|
-
def _resolve_field_offset(field_def: FieldDef) -> InfoSize:
|
|
535
|
-
"""Resolve field offsets to InfoSize values when possible."""
|
|
536
|
-
if isinstance(field_def.offset, InfoSize):
|
|
537
|
-
return field_def.offset
|
|
538
|
-
return InfoSize(0, 0)
|
|
539
|
-
|
|
540
|
-
@staticmethod
|
|
541
|
-
def _resolve_field_size(field_def: FieldDef) -> InfoSize:
|
|
542
|
-
"""Resolve field sizes to InfoSize values when possible."""
|
|
543
|
-
if isinstance(field_def.size, InfoSize):
|
|
544
|
-
return field_def.size
|
|
545
|
-
return InfoSize(0, 0)
|
|
@@ -5,9 +5,9 @@ from pathlib import Path
|
|
|
5
5
|
import pytest
|
|
6
6
|
from sltcore import InfoSize
|
|
7
7
|
|
|
8
|
-
from sltcodec import (EnumDef, FieldDef, FieldInstance,
|
|
9
|
-
StructInstance, decode, encode,
|
|
10
|
-
save_struct_def_dict)
|
|
8
|
+
from sltcodec import (PRIMITIVE_TYPES, EnumDef, FieldDef, FieldInstance,
|
|
9
|
+
StructDef, StructInstance, decode, encode,
|
|
10
|
+
load_struct_def_dict, save_struct_def_dict)
|
|
11
11
|
from sltcodec.codec import decode_field
|
|
12
12
|
|
|
13
13
|
|
|
@@ -17,6 +17,19 @@ class ValueKind(Enum):
|
|
|
17
17
|
B = 2
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
def test_primitive_types_are_exposed_at_package_root():
|
|
21
|
+
"""Test that primitive types are exposed for external consumers."""
|
|
22
|
+
assert PRIMITIVE_TYPES == {
|
|
23
|
+
"bool",
|
|
24
|
+
"signed int",
|
|
25
|
+
"int",
|
|
26
|
+
"unsigned int",
|
|
27
|
+
"float",
|
|
28
|
+
"bytearray",
|
|
29
|
+
"bytes",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
20
33
|
def test_encode_and_decode_round_trip():
|
|
21
34
|
"""Test that encoding and then decoding returns the original data."""
|
|
22
35
|
struct_def = [
|
|
@@ -475,10 +488,11 @@ def test_type_expression_uses_previous_field_value():
|
|
|
475
488
|
bytearray(),
|
|
476
489
|
)
|
|
477
490
|
decoded_int = decode(struct_def, encoded_int)
|
|
478
|
-
assert decoded_int.field_instances == [
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
]
|
|
491
|
+
assert decoded_int.field_instances[0] == FieldInstance(struct_def[0], 1)
|
|
492
|
+
assert decoded_int.field_instances[1].field_def.offset == InfoSize(1, 0)
|
|
493
|
+
assert decoded_int.field_instances[1].field_def.size == InfoSize(1, 0)
|
|
494
|
+
assert decoded_int.field_instances[1].field_def.type == "int"
|
|
495
|
+
assert decoded_int.field_instances[1].value == 7
|
|
482
496
|
|
|
483
497
|
encoded_float = encode(
|
|
484
498
|
StructInstance(struct_def=StructDef(fields=struct_def),
|
|
@@ -490,7 +504,9 @@ def test_type_expression_uses_previous_field_value():
|
|
|
490
504
|
)
|
|
491
505
|
decoded_float = decode(struct_def, encoded_float)
|
|
492
506
|
assert decoded_float.field_instances[0] == FieldInstance(struct_def[0], 2)
|
|
493
|
-
assert decoded_float.field_instances[1].field_def ==
|
|
507
|
+
assert decoded_float.field_instances[1].field_def.offset == InfoSize(1, 0)
|
|
508
|
+
assert decoded_float.field_instances[1].field_def.size == InfoSize(4, 0)
|
|
509
|
+
assert decoded_float.field_instances[1].field_def.type == "float"
|
|
494
510
|
assert decoded_float.field_instances[1].value == 1.5
|
|
495
511
|
|
|
496
512
|
|
|
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
|