sltcodec 2.1.1__tar.gz → 2.2.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.1.1 → sltcodec-2.2.0}/PKG-INFO +2 -2
- {sltcodec-2.1.1 → sltcodec-2.2.0}/pyproject.toml +2 -2
- {sltcodec-2.1.1 → sltcodec-2.2.0}/src/sltcodec/codec.py +21 -45
- {sltcodec-2.1.1 → sltcodec-2.2.0}/tests/test_codec.py +10 -1
- {sltcodec-2.1.1 → sltcodec-2.2.0}/uv.lock +5 -5
- {sltcodec-2.1.1 → sltcodec-2.2.0}/.github/workflows/publish_to_pypi.yml +0 -0
- {sltcodec-2.1.1 → sltcodec-2.2.0}/.github/workflows/publish_to_testpypi.yml +0 -0
- {sltcodec-2.1.1 → sltcodec-2.2.0}/.gitignore +0 -0
- {sltcodec-2.1.1 → sltcodec-2.2.0}/.python-version +0 -0
- {sltcodec-2.1.1 → sltcodec-2.2.0}/.vscode/launch.json +0 -0
- {sltcodec-2.1.1 → sltcodec-2.2.0}/.vscode/settings.json +0 -0
- {sltcodec-2.1.1 → sltcodec-2.2.0}/LICENSE +0 -0
- {sltcodec-2.1.1 → sltcodec-2.2.0}/README.md +0 -0
- {sltcodec-2.1.1 → sltcodec-2.2.0}/README_pypi.md +0 -0
- {sltcodec-2.1.1 → sltcodec-2.2.0}/bump_major.ps1 +0 -0
- {sltcodec-2.1.1 → sltcodec-2.2.0}/bump_minor.ps1 +0 -0
- {sltcodec-2.1.1 → sltcodec-2.2.0}/bump_patch.ps1 +0 -0
- {sltcodec-2.1.1 → sltcodec-2.2.0}/src/sltcodec/__init__.py +0 -0
- {sltcodec-2.1.1 → sltcodec-2.2.0}/src/sltcodec/types.py +0 -0
- {sltcodec-2.1.1 → sltcodec-2.2.0}/tests/test_persistence.py +0 -0
- {sltcodec-2.1.1 → sltcodec-2.2.0}/tests/test_types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sltcodec
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.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
|
|
@@ -11,7 +11,7 @@ License-Expression: MIT
|
|
|
11
11
|
License-File: LICENSE
|
|
12
12
|
Keywords: byte,deserialization,layout,serialization,struct
|
|
13
13
|
Requires-Python: >=3.12
|
|
14
|
-
Requires-Dist: sltcalc>=
|
|
14
|
+
Requires-Dist: sltcalc>=1.0.0
|
|
15
15
|
Requires-Dist: sltcore>=1.5.0
|
|
16
16
|
Description-Content-Type: text/markdown
|
|
17
17
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sltcodec"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.2.0"
|
|
4
4
|
description = "Decode and encode bytearrays according to struct layout definitions using sltcore."
|
|
5
5
|
readme = "README_pypi.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
7
7
|
dependencies = [
|
|
8
|
-
"sltcalc>=
|
|
8
|
+
"sltcalc>=1.0.0",
|
|
9
9
|
"sltcore>=1.5.0",
|
|
10
10
|
]
|
|
11
11
|
license = "MIT"
|
|
@@ -136,20 +136,12 @@ def _resolve_info_size(value: InfoSize | str, env: dict[str, Any]) -> InfoSize:
|
|
|
136
136
|
if isinstance(value, str):
|
|
137
137
|
stleval = SltEval(env)
|
|
138
138
|
resolved_byte = stleval.eval(value)
|
|
139
|
+
if isinstance(resolved_byte, InfoSize):
|
|
140
|
+
return resolved_byte
|
|
139
141
|
return InfoSize(resolved_byte, 0)
|
|
140
142
|
return value
|
|
141
143
|
|
|
142
144
|
|
|
143
|
-
def _resolve_offset(field_def: FieldDef, env: dict[str, Any]) -> InfoSize:
|
|
144
|
-
"""Resolve a field offset that can be static or expression-based."""
|
|
145
|
-
return _resolve_info_size(field_def.offset, env)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
def _resolve_size(field_def: FieldDef, env: dict[str, Any]) -> InfoSize:
|
|
149
|
-
"""Resolve a field size that can be static or expression-based."""
|
|
150
|
-
return _resolve_info_size(field_def.size, env)
|
|
151
|
-
|
|
152
|
-
|
|
153
145
|
def _resolve_byte_swap(field_def: FieldDef, env: dict[str, Any]) -> bool:
|
|
154
146
|
"""Resolve a byte-swap flag that can be static or expression-based."""
|
|
155
147
|
if isinstance(field_def.byte_swap, str):
|
|
@@ -157,27 +149,12 @@ def _resolve_byte_swap(field_def: FieldDef, env: dict[str, Any]) -> bool:
|
|
|
157
149
|
return field_def.byte_swap
|
|
158
150
|
|
|
159
151
|
|
|
160
|
-
def _byte_swap_info(info: Info, size: InfoSize) -> Info:
|
|
161
|
-
"""Reverse the byte representation of an Info payload."""
|
|
162
|
-
return Info.from_bytes(info.to_bytes[::-1], size, scale=info.scale)
|
|
163
|
-
|
|
164
|
-
|
|
165
152
|
def _is_padding_field_def(field_def: FieldDef) -> bool:
|
|
166
153
|
"""Check whether a field definition represents padding."""
|
|
167
154
|
return (field_def.name.startswith("padding[")
|
|
168
155
|
and field_def.type in ["bytes", "bytearray"])
|
|
169
156
|
|
|
170
157
|
|
|
171
|
-
def _info_size_to_bits(info_size: InfoSize) -> int:
|
|
172
|
-
"""Convert InfoSize to an absolute bit count."""
|
|
173
|
-
return info_size.byte * 8 + info_size.bit
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
def _info_size_from_bits(total_bits: int) -> InfoSize:
|
|
177
|
-
"""Create InfoSize from an absolute bit count."""
|
|
178
|
-
return InfoSize(total_bits >> 3, total_bits & 0x7)
|
|
179
|
-
|
|
180
|
-
|
|
181
158
|
def _validate_padding_alignment_bits(padding_alignment_bits: int) -> None:
|
|
182
159
|
"""Validate padding alignment as a positive power-of-two bit size."""
|
|
183
160
|
if (isinstance(padding_alignment_bits, bool)
|
|
@@ -237,8 +214,8 @@ def _prepare_field_info(
|
|
|
237
214
|
padding_alignment_bits: int = _DEFAULT_PADDING_ALIGNMENT_BITS,
|
|
238
215
|
) -> tuple[InfoSize, InfoSize, Info] | None:
|
|
239
216
|
"""Resolve a field definition into an offset, size, and info payload."""
|
|
240
|
-
offset =
|
|
241
|
-
size =
|
|
217
|
+
offset = _resolve_info_size(field_def.offset, env)
|
|
218
|
+
size = _resolve_info_size(field_def.size, env)
|
|
242
219
|
if size.byte == 0 and size.bit == 0:
|
|
243
220
|
return None
|
|
244
221
|
|
|
@@ -305,7 +282,7 @@ def encode_field(
|
|
|
305
282
|
|
|
306
283
|
offset, size, info = prepared
|
|
307
284
|
if _resolve_byte_swap(field_def, env):
|
|
308
|
-
info =
|
|
285
|
+
info = info.byte_swap
|
|
309
286
|
required_bytes = (offset + size).bytes
|
|
310
287
|
if len(buf) < required_bytes:
|
|
311
288
|
buf.extend(b"\x00" * (required_bytes - len(buf)))
|
|
@@ -355,17 +332,17 @@ def encode(
|
|
|
355
332
|
has_padding = True
|
|
356
333
|
value = field_value.value
|
|
357
334
|
if _is_padding_field_def(field_def):
|
|
358
|
-
padding_size =
|
|
335
|
+
padding_size = _resolve_info_size(field_def.size, env)
|
|
359
336
|
value = bytearray(padding_size.bytes)
|
|
360
337
|
if field_def.repeat is not None and field_def.repeat > 1:
|
|
361
|
-
current_offset =
|
|
338
|
+
current_offset = _resolve_info_size(field_def.offset, env)
|
|
362
339
|
values = list(value)
|
|
363
340
|
|
|
364
341
|
for i in range(field_def.repeat):
|
|
365
342
|
field_def_repeat = _repeated_field_def(field_def, i,
|
|
366
343
|
current_offset,
|
|
367
344
|
field_def.size)
|
|
368
|
-
resolved_size =
|
|
345
|
+
resolved_size = _resolve_info_size(field_def_repeat.size, env)
|
|
369
346
|
encode_field(field_def_repeat, values[i], buf, env, type_dict,
|
|
370
347
|
padding_alignment_bits)
|
|
371
348
|
env[field_def_repeat.name] = values[i]
|
|
@@ -410,14 +387,14 @@ def decode_field(
|
|
|
410
387
|
"""
|
|
411
388
|
if env is None:
|
|
412
389
|
env = {}
|
|
413
|
-
offset =
|
|
414
|
-
size =
|
|
390
|
+
offset = _resolve_info_size(field_def.offset, env)
|
|
391
|
+
size = _resolve_info_size(field_def.size, env)
|
|
415
392
|
if size.byte == 0 and size.bit == 0:
|
|
416
393
|
return None
|
|
417
394
|
info = bits_get(data, offset, size, scale=field_def.scale)
|
|
418
395
|
byte_swap = _resolve_byte_swap(field_def, env)
|
|
419
396
|
if byte_swap:
|
|
420
|
-
info =
|
|
397
|
+
info = info.byte_swap
|
|
421
398
|
resolved_type = _resolve_field_type(field_def.type, type_dict, env)
|
|
422
399
|
nested_value = None
|
|
423
400
|
if isinstance(resolved_type, StructDef):
|
|
@@ -526,22 +503,21 @@ def decode(
|
|
|
526
503
|
|
|
527
504
|
def append_padding_until(target_offset: InfoSize) -> None:
|
|
528
505
|
nonlocal current_position, padding_index
|
|
529
|
-
start_bits =
|
|
530
|
-
end_bits =
|
|
506
|
+
start_bits = current_position.bits
|
|
507
|
+
end_bits = target_offset.bits
|
|
531
508
|
if end_bits <= start_bits:
|
|
532
509
|
return
|
|
533
510
|
|
|
534
511
|
chunk_start_bits = start_bits
|
|
535
512
|
while chunk_start_bits < end_bits:
|
|
536
|
-
chunk_start =
|
|
537
|
-
bits_to_boundary =
|
|
538
|
-
chunk_start.align_to(padding_alignment_bits))
|
|
513
|
+
chunk_start = InfoSize(0, chunk_start_bits)
|
|
514
|
+
bits_to_boundary = chunk_start.align_to(padding_alignment_bits).bits
|
|
539
515
|
chunk_size_bits = bits_to_boundary or padding_alignment_bits
|
|
540
516
|
chunk_end_bits = min(end_bits, chunk_start_bits + chunk_size_bits)
|
|
541
517
|
padding_field_def = FieldDef(
|
|
542
518
|
name=f"padding[{padding_index}]",
|
|
543
|
-
offset=
|
|
544
|
-
size=
|
|
519
|
+
offset=InfoSize(0, chunk_start_bits),
|
|
520
|
+
size=InfoSize(0, chunk_end_bits - chunk_start_bits),
|
|
545
521
|
type="bytes",
|
|
546
522
|
description="Auto-generated padding",
|
|
547
523
|
)
|
|
@@ -556,7 +532,7 @@ def decode(
|
|
|
556
532
|
for field_def in _as_field_defs(struct_def_obj):
|
|
557
533
|
# Handle non-repeated fields
|
|
558
534
|
if field_def.repeat is None or field_def.repeat <= 1:
|
|
559
|
-
resolved_offset =
|
|
535
|
+
resolved_offset = _resolve_info_size(field_def.offset, env)
|
|
560
536
|
append_padding_until(resolved_offset)
|
|
561
537
|
field_instance = decode_field(field_def, data, env, type_dict,
|
|
562
538
|
padding_alignment_bits)
|
|
@@ -564,10 +540,10 @@ def decode(
|
|
|
564
540
|
env[field_instance.field_def.name] = field_instance.value
|
|
565
541
|
result.append_field_instance(field_instance)
|
|
566
542
|
current_position = (resolved_offset +
|
|
567
|
-
|
|
543
|
+
_resolve_info_size(field_def.size, env))
|
|
568
544
|
continue
|
|
569
545
|
# Handle repeated fields
|
|
570
|
-
current_offset =
|
|
546
|
+
current_offset = _resolve_info_size(field_def.offset, env)
|
|
571
547
|
append_padding_until(current_offset)
|
|
572
548
|
for i in range(field_def.repeat):
|
|
573
549
|
field_def_repeat = _repeated_field_def(field_def, i, current_offset,
|
|
@@ -579,7 +555,7 @@ def decode(
|
|
|
579
555
|
result.append_field_instance(field_instance)
|
|
580
556
|
actual_size = field_instance.field_def.size
|
|
581
557
|
else:
|
|
582
|
-
actual_size =
|
|
558
|
+
actual_size = _resolve_info_size(field_def_repeat.size, env)
|
|
583
559
|
if isinstance(current_offset, InfoSize) and isinstance(
|
|
584
560
|
actual_size, InfoSize):
|
|
585
561
|
current_offset += actual_size
|
|
@@ -7,7 +7,7 @@ from sltcore import InfoSize
|
|
|
7
7
|
from sltcodec import (PRIMITIVE_TYPES, EnumDef, FieldDef, FieldInstance,
|
|
8
8
|
StructDef, StructInstance, StructLayout, TypeDict, decode,
|
|
9
9
|
encode)
|
|
10
|
-
from sltcodec.codec import decode_field
|
|
10
|
+
from sltcodec.codec import _resolve_info_size, decode_field
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def layout_for(struct_def):
|
|
@@ -608,6 +608,15 @@ def test_decode_field_sets_actual_size_on_field_instance():
|
|
|
608
608
|
assert decoded.field_def.size == InfoSize(1, 0)
|
|
609
609
|
|
|
610
610
|
|
|
611
|
+
def test_resolve_info_size_returns_expression_info_size_unchanged():
|
|
612
|
+
"""Test that expressions resolving to InfoSize preserve the result."""
|
|
613
|
+
info_size = InfoSize(2, 3)
|
|
614
|
+
|
|
615
|
+
resolved = _resolve_info_size("field_size", {"field_size": info_size})
|
|
616
|
+
|
|
617
|
+
assert resolved is info_size
|
|
618
|
+
|
|
619
|
+
|
|
611
620
|
def test_decode_struct_instance_size_matches_decoded_layout():
|
|
612
621
|
"""Test that decode reports the complete decoded layout size."""
|
|
613
622
|
field_def = FieldDef(name="value",
|
|
@@ -65,16 +65,16 @@ wheels = [
|
|
|
65
65
|
|
|
66
66
|
[[package]]
|
|
67
67
|
name = "sltcalc"
|
|
68
|
-
version = "
|
|
68
|
+
version = "1.0.0"
|
|
69
69
|
source = { registry = "https://pypi.org/simple" }
|
|
70
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
70
|
+
sdist = { url = "https://files.pythonhosted.org/packages/b5/01/19db81d0e1f1330ab884abf282569b3f05ffe1f2c7b8aff910125295d4be/sltcalc-1.0.0.tar.gz", hash = "sha256:968053bbecc85c30caf5ddfd48962e409c7844ccfc441418d029a4f743030350", size = 10499, upload-time = "2026-08-18T14:38:14.052Z" }
|
|
71
71
|
wheels = [
|
|
72
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
72
|
+
{ url = "https://files.pythonhosted.org/packages/da/77/6d03b5c160ffe42bee2fdabdd46521033794fb02e8975b4da7a1f1c641de/sltcalc-1.0.0-py3-none-any.whl", hash = "sha256:12531fef9633b25e6743c4a41873971c83dffb763590f42e2f1f78accea08088", size = 4824, upload-time = "2026-08-18T14:38:13.247Z" },
|
|
73
73
|
]
|
|
74
74
|
|
|
75
75
|
[[package]]
|
|
76
76
|
name = "sltcodec"
|
|
77
|
-
version = "2.1.
|
|
77
|
+
version = "2.1.1"
|
|
78
78
|
source = { editable = "." }
|
|
79
79
|
dependencies = [
|
|
80
80
|
{ name = "sltcalc" },
|
|
@@ -88,7 +88,7 @@ dev = [
|
|
|
88
88
|
|
|
89
89
|
[package.metadata]
|
|
90
90
|
requires-dist = [
|
|
91
|
-
{ name = "sltcalc", specifier = ">=
|
|
91
|
+
{ name = "sltcalc", specifier = ">=1.0.0" },
|
|
92
92
|
{ name = "sltcore", specifier = ">=1.5.0" },
|
|
93
93
|
]
|
|
94
94
|
|
|
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
|
|
File without changes
|
|
File without changes
|