sltcodec 2.2.0__tar.gz → 2.4.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sltcodec
3
- Version: 2.2.0
3
+ Version: 2.4.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
@@ -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:
@@ -77,7 +82,7 @@ The public `PRIMITIVE_TYPES` set contains:
77
82
  {"bool", "signed int", "int", "unsigned int", "float", "bytearray", "bytes"}
78
83
  ```
79
84
 
80
- `FieldDef.type` may also be a nested `StructDef` or an expression string. Expressions can use values from previously processed fields, allowing dynamic types and sizes. Repeated fields, byte swapping, padding, ranges, and enum metadata are supported.
85
+ `FieldDef.type` may also be a nested `StructDef` or an expression string. Expressions can use values from previously processed fields, allowing dynamic types, sizes, and repeat counts. Repeated fields, byte swapping, padding, ranges, and enum metadata are supported.
81
86
 
82
87
  ## Enums And Named Structures
83
88
 
@@ -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 \| 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:
@@ -60,7 +65,7 @@ The public `PRIMITIVE_TYPES` set contains:
60
65
  {"bool", "signed int", "int", "unsigned int", "float", "bytearray", "bytes"}
61
66
  ```
62
67
 
63
- `FieldDef.type` may also be a nested `StructDef` or an expression string. Expressions can use values from previously processed fields, allowing dynamic types and sizes. Repeated fields, byte swapping, padding, ranges, and enum metadata are supported.
68
+ `FieldDef.type` may also be a nested `StructDef` or an expression string. Expressions can use values from previously processed fields, allowing dynamic types, sizes, and repeat counts. Repeated fields, byte swapping, padding, ranges, and enum metadata are supported.
64
69
 
65
70
  ## Enums And Named Structures
66
71
 
@@ -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 \| 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. |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sltcodec"
3
- version = "2.2.0"
3
+ version = "2.4.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"
@@ -149,6 +149,15 @@ def _resolve_byte_swap(field_def: FieldDef, env: dict[str, Any]) -> bool:
149
149
  return field_def.byte_swap
150
150
 
151
151
 
152
+ def _resolve_repeat(value: int | str | None, env: dict[str, Any]) -> int | None:
153
+ """Resolve a static or expression-based repeat count."""
154
+ if isinstance(value, str):
155
+ if value == "end":
156
+ return None
157
+ return int(SltEval(env).eval(value))
158
+ return value
159
+
160
+
152
161
  def _is_padding_field_def(field_def: FieldDef) -> bool:
153
162
  """Check whether a field definition represents padding."""
154
163
  return (field_def.name.startswith("padding[")
@@ -328,17 +337,22 @@ def encode(
328
337
 
329
338
  for field_value in struct_instance.field_instances:
330
339
  field_def = field_value.field_def
340
+ repeat_until_end = field_def.repeat == "end"
341
+ repeat = _resolve_repeat(field_def.repeat, env)
331
342
  if _is_padding_field_def(field_def):
332
343
  has_padding = True
333
344
  value = field_value.value
334
345
  if _is_padding_field_def(field_def):
335
346
  padding_size = _resolve_info_size(field_def.size, env)
336
347
  value = bytearray(padding_size.bytes)
337
- if field_def.repeat is not None and field_def.repeat > 1:
348
+ if repeat_until_end:
349
+ repeat_values = list(value)
350
+ repeat = len(repeat_values)
351
+ if repeat is not None and repeat > 1:
338
352
  current_offset = _resolve_info_size(field_def.offset, env)
339
- values = list(value)
353
+ values = repeat_values if repeat_until_end else list(value)
340
354
 
341
- for i in range(field_def.repeat):
355
+ for i in range(repeat):
342
356
  field_def_repeat = _repeated_field_def(field_def, i,
343
357
  current_offset,
344
358
  field_def.size)
@@ -500,6 +514,27 @@ def decode(
500
514
  result = StructInstance(struct_def=struct_def_obj)
501
515
  current_position = InfoSize(0, 0)
502
516
  padding_index = 0
517
+ stop_due_to_out_of_range = False
518
+ data_bits = len(data) * 8
519
+ field_defs = _as_field_defs(struct_def_obj)
520
+
521
+ def is_out_of_range(offset: InfoSize, size: InfoSize) -> bool:
522
+ """Check whether the field range exceeds available input bits."""
523
+ if size.bits <= 0:
524
+ return False
525
+ if offset.bits < 0:
526
+ return True
527
+ if offset.bits >= data_bits:
528
+ return True
529
+ return offset.bits + size.bits > data_bits
530
+
531
+ def append_none_tail(start_index: int, first_field_def: FieldDef) -> None:
532
+ """Append current and remaining fields with None values."""
533
+ result.field_instances.append(
534
+ FieldInstance(field_def=first_field_def, value=None))
535
+ for tail_field_def in field_defs[start_index + 1:]:
536
+ result.field_instances.append(
537
+ FieldInstance(field_def=tail_field_def, value=None))
503
538
 
504
539
  def append_padding_until(target_offset: InfoSize) -> None:
505
540
  nonlocal current_position, padding_index
@@ -529,10 +564,18 @@ def decode(
529
564
 
530
565
  current_position = target_offset
531
566
 
532
- for field_def in _as_field_defs(struct_def_obj):
567
+ for field_index, field_def in enumerate(field_defs):
568
+ repeat_until_end = field_def.repeat == "end"
569
+ repeat = (None if repeat_until_end else _resolve_repeat(
570
+ field_def.repeat, env))
533
571
  # Handle non-repeated fields
534
- if field_def.repeat is None or field_def.repeat <= 1:
572
+ if not repeat_until_end and (repeat is None or repeat <= 1):
535
573
  resolved_offset = _resolve_info_size(field_def.offset, env)
574
+ resolved_size = _resolve_info_size(field_def.size, env)
575
+ if is_out_of_range(resolved_offset, resolved_size):
576
+ append_none_tail(field_index, field_def)
577
+ stop_due_to_out_of_range = True
578
+ break
536
579
  append_padding_until(resolved_offset)
537
580
  field_instance = decode_field(field_def, data, env, type_dict,
538
581
  padding_alignment_bits)
@@ -544,10 +587,79 @@ def decode(
544
587
  continue
545
588
  # Handle repeated fields
546
589
  current_offset = _resolve_info_size(field_def.offset, env)
547
- append_padding_until(current_offset)
548
- for i in range(field_def.repeat):
590
+ if repeat_until_end:
591
+ stop_decoding = False
592
+ i = 0
593
+ while True:
594
+ if current_offset.bits >= data_bits:
595
+ if i == 0:
596
+ append_none_tail(
597
+ field_index,
598
+ _repeated_field_def(field_def, i, current_offset,
599
+ field_def.size),
600
+ )
601
+ stop_decoding = True
602
+ break
603
+
604
+ append_padding_until(current_offset)
605
+
606
+ field_def_repeat = _repeated_field_def(field_def, i,
607
+ current_offset,
608
+ field_def.size)
609
+ resolved_size = _resolve_info_size(field_def_repeat.size, env)
610
+ if resolved_size.bits <= 0:
611
+ break
612
+ if is_out_of_range(current_offset, resolved_size):
613
+ if i == 0:
614
+ append_none_tail(field_index, field_def_repeat)
615
+ stop_decoding = True
616
+ break
617
+
618
+ field_instance = decode_field(field_def_repeat, data, env,
619
+ type_dict, padding_alignment_bits)
620
+ if field_instance is not None:
621
+ env[field_instance.field_def.name] = field_instance.value
622
+ result.append_field_instance(field_instance)
623
+ actual_size = field_instance.field_def.size
624
+ else:
625
+ actual_size = resolved_size
626
+
627
+ if isinstance(current_offset, InfoSize) and isinstance(
628
+ actual_size, InfoSize):
629
+ current_offset += actual_size
630
+ current_position = current_offset
631
+ i += 1
632
+ if stop_decoding:
633
+ stop_due_to_out_of_range = True
634
+ break
635
+ continue
636
+
637
+ for i in range(repeat):
549
638
  field_def_repeat = _repeated_field_def(field_def, i, current_offset,
550
639
  field_def.size)
640
+ resolved_size = _resolve_info_size(field_def_repeat.size, env)
641
+ if is_out_of_range(current_offset, resolved_size):
642
+ result.field_instances.append(
643
+ FieldInstance(field_def=field_def_repeat, value=None))
644
+ next_offset = current_offset
645
+ for remaining_index in range(i + 1, repeat):
646
+ if isinstance(next_offset, InfoSize):
647
+ next_offset += resolved_size
648
+ remaining_field_def = _repeated_field_def(
649
+ field_def,
650
+ remaining_index,
651
+ next_offset,
652
+ field_def.size,
653
+ )
654
+ result.field_instances.append(
655
+ FieldInstance(field_def=remaining_field_def,
656
+ value=None))
657
+ for tail_field_def in field_defs[field_index + 1:]:
658
+ result.field_instances.append(
659
+ FieldInstance(field_def=tail_field_def, value=None))
660
+ stop_due_to_out_of_range = True
661
+ break
662
+ append_padding_until(current_offset)
551
663
  field_instance = decode_field(field_def_repeat, data, env,
552
664
  type_dict, padding_alignment_bits)
553
665
  if field_instance is not None:
@@ -560,6 +672,15 @@ def decode(
560
672
  actual_size, InfoSize):
561
673
  current_offset += actual_size
562
674
  current_position = current_offset
675
+ else:
676
+ continue
677
+ break
678
+
679
+ if stop_due_to_out_of_range:
680
+ result._sort_field_instances()
681
+ result._update_size()
682
+ result._rebuild_field_def_name_index()
683
+ return result
563
684
 
564
685
  actual_size = InfoSize(0, 0)
565
686
  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)
@@ -98,7 +108,7 @@ class FieldDef:
98
108
  metadata={"desc": "The type of the field"})
99
109
  scale: float = field(default=1.0,
100
110
  metadata={"desc": "The scale of the field"})
101
- repeat: int | None = field(
111
+ repeat: int | str | None = field(
102
112
  default=None, metadata={"desc": "The repeat count of the field"})
103
113
  description: str | None = field(
104
114
  default=None, metadata={"desc": "The description of the field"})
@@ -231,7 +241,7 @@ class FieldDef:
231
241
  self.name,
232
242
  self._sortable_type(self.type),
233
243
  self.scale,
234
- -1 if self.repeat is None else self.repeat,
244
+ self._sortable_repeat(self.repeat),
235
245
  "" if self.description is None else self.description,
236
246
  "" if self.range_expression is None else self.range_expression,
237
247
  "" if self.enum_def_name is None else self.enum_def_name,
@@ -245,6 +255,15 @@ class FieldDef:
245
255
  return (0, value.byte, value.bit)
246
256
  return (1, value)
247
257
 
258
+ @staticmethod
259
+ def _sortable_repeat(value: int | str | None) -> tuple[int, int | str]:
260
+ """Build a comparable key for static or expression repeat counts."""
261
+ if value is None:
262
+ return 0, -1
263
+ if isinstance(value, int):
264
+ return 1, value
265
+ return 2, value
266
+
248
267
  @staticmethod
249
268
  def _sortable_type(value: str | "StructDef") -> tuple[Any, ...]:
250
269
  """Build a comparable key for primitive or nested field types."""
@@ -358,6 +377,16 @@ class StructDef:
358
377
  default_factory=list,
359
378
  metadata={"desc": "The fields of the structure"},
360
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()
361
390
 
362
391
  def __lt__(self, other: object) -> bool:
363
392
  """Compare structure definitions using a stable serialized sort key."""
@@ -374,6 +403,42 @@ class StructDef:
374
403
  sort_keys=True),
375
404
  )
376
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
+
377
442
  def to_dict(self) -> dict[str, Any]:
378
443
  """Convert this structure definition to JSON-serializable data."""
379
444
  return {
@@ -423,6 +488,11 @@ class StructInstance:
423
488
  default_factory=InfoSize,
424
489
  metadata={"desc": "The total size of the structure instance"},
425
490
  )
491
+ _field_instances_by_field_def_name: dict[str, list[FieldInstance]] = field(
492
+ default_factory=dict,
493
+ init=False,
494
+ repr=False,
495
+ )
426
496
 
427
497
  def __post_init__(self) -> None:
428
498
  """Normalize stored field instances to sorted order."""
@@ -460,6 +530,19 @@ class StructInstance:
460
530
  """Return one field instance by index."""
461
531
  return self.field_instances[index]
462
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
+
463
546
  def __lt__(self, other: object) -> bool:
464
547
  """Compare structure instances using a stable serialized sort key."""
465
548
  if not isinstance(other, StructInstance):
@@ -481,6 +564,16 @@ class StructInstance:
481
564
  """Keep field instances sorted by FieldDef order."""
482
565
  self.field_instances.sort()
483
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
+
484
577
  def _rebuild_padding_field_instances(self) -> None:
485
578
  """Rebuild padding field instances for gaps between stored values."""
486
579
  non_padding_instances = [
@@ -498,6 +591,7 @@ class StructInstance:
498
591
  self.field_instances = non_padding_instances
499
592
  self._sort_field_instances()
500
593
  self._update_size()
594
+ self._rebuild_field_def_name_index()
501
595
  return
502
596
 
503
597
  rebuilt_instances: list[FieldInstance] = []
@@ -546,6 +640,7 @@ class StructInstance:
546
640
  self.field_instances = rebuilt_instances
547
641
  self._sort_field_instances()
548
642
  self._update_size()
643
+ self._rebuild_field_def_name_index()
549
644
 
550
645
  def _update_size(self) -> None:
551
646
  """Update the instance size from the current field layout."""
@@ -166,6 +166,122 @@ def test_encode_layout_handles_repeat():
166
166
  ]
167
167
 
168
168
 
169
+ def test_expression_repeat_uses_previous_field_value():
170
+ """Test that repeat expressions are evaluated against the field env."""
171
+ seed_field = FieldDef(name="count",
172
+ offset=InfoSize(0, 0),
173
+ size=InfoSize(1, 0),
174
+ type="unsigned int")
175
+ repeated_field = FieldDef(name="value",
176
+ offset=InfoSize(1, 0),
177
+ size=InfoSize(1, 0),
178
+ type="unsigned int",
179
+ repeat="count + 1")
180
+ struct_def = StructDef(fields=[seed_field, repeated_field])
181
+
182
+ encoded = encode(
183
+ layout_for(struct_def),
184
+ StructInstance(
185
+ struct_def=struct_def,
186
+ field_instances=[
187
+ FieldInstance(seed_field, 2),
188
+ FieldInstance(repeated_field, [5, 6, 7]),
189
+ ],
190
+ ),
191
+ bytearray(),
192
+ )
193
+
194
+ assert encoded == bytearray(b"\x02\x05\x06\x07")
195
+ decoded = decode(layout_for(struct_def), encoded)
196
+
197
+ assert [instance.value
198
+ for instance in decoded.field_instances] == [2, 5, 6, 7]
199
+
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_decode_out_of_range_sets_current_and_tail_values_to_none():
236
+ """Test out-of-range decode fills current/tail field values with None."""
237
+ fields = [
238
+ FieldDef(name="head",
239
+ offset=InfoSize(0, 0),
240
+ size=InfoSize(1, 0),
241
+ type="unsigned int"),
242
+ FieldDef(name="far",
243
+ offset=InfoSize(10, 0),
244
+ size=InfoSize(1, 0),
245
+ type="unsigned int"),
246
+ FieldDef(name="tail",
247
+ offset=InfoSize(11, 0),
248
+ size=InfoSize(1, 0),
249
+ type="unsigned int"),
250
+ ]
251
+
252
+ decoded = decode(layout_for(fields), bytearray(b"\x11"))
253
+
254
+ assert [instance.field_def.name
255
+ for instance in decoded.field_instances] == ["head", "far", "tail"]
256
+ assert [instance.value
257
+ for instance in decoded.field_instances] == [17, None, None]
258
+
259
+
260
+ def test_decode_out_of_range_skips_following_field_expression_evaluation():
261
+ """Test out-of-range decode skips evaluating following expressions."""
262
+ fields = [
263
+ FieldDef(name="head",
264
+ offset=InfoSize(0, 0),
265
+ size=InfoSize(1, 0),
266
+ type="unsigned int"),
267
+ FieldDef(name="far",
268
+ offset=InfoSize(10, 0),
269
+ size=InfoSize(1, 0),
270
+ type="unsigned int"),
271
+ FieldDef(name="tail",
272
+ offset="1 / 0",
273
+ size=InfoSize(1, 0),
274
+ type="unsigned int"),
275
+ ]
276
+
277
+ decoded = decode(layout_for(fields), bytearray(b"\x11"))
278
+
279
+ assert [instance.field_def.name
280
+ for instance in decoded.field_instances] == ["head", "far", "tail"]
281
+ assert [instance.value
282
+ for instance in decoded.field_instances] == [17, None, None]
283
+
284
+
169
285
  def test_encode_recurses_for_nested_field_types():
170
286
  """Test that encoding a field with a nested field type works correctly."""
171
287
  child_field_defs = [
@@ -39,7 +39,7 @@ def test_field_def_to_json_from_json_round_trip():
39
39
  size=InfoSize(1, 0),
40
40
  type="unsigned int",
41
41
  scale=2.0,
42
- repeat=3,
42
+ repeat="count + 1",
43
43
  description="A repeated value",
44
44
  range_expression="0 <= value <= 255",
45
45
  enum_def_name=enum_def.name)
@@ -48,6 +48,7 @@ def test_field_def_to_json_from_json_round_trip():
48
48
  restored = FieldDef.from_json(payload)
49
49
 
50
50
  assert restored == field_def
51
+ assert restored.repeat == "count + 1"
51
52
  assert restored.range_expression == "0 <= value <= 255"
52
53
  assert restored.enum_def_name == enum_def.name
53
54
  assert restored.to_dict()["enum_def_name"] == "ValueKind"
@@ -287,6 +288,42 @@ def test_struct_def_to_json_from_json_round_trip():
287
288
  assert restored == struct_def
288
289
 
289
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
+
290
327
  def test_struct_instance_field_instance_interface():
291
328
  """Test StructInstance interface for field_instances operations."""
292
329
  field_def_0 = FieldDef(name="b",
@@ -358,6 +395,50 @@ def test_struct_instance_rebuilds_padding_when_field_instances_change():
358
395
  assert struct_instance.field_instances[1].value == b"\x00"
359
396
 
360
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
+
361
442
  def test_field_instance_sort_uses_field_def_order():
362
443
  """Test that FieldInstance sorting delegates to FieldDef ordering."""
363
444
  later = FieldInstance(
@@ -74,7 +74,7 @@ wheels = [
74
74
 
75
75
  [[package]]
76
76
  name = "sltcodec"
77
- version = "2.1.1"
77
+ version = "2.3.0"
78
78
  source = { editable = "." }
79
79
  dependencies = [
80
80
  { name = "sltcalc" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes