sltcodec 2.3.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.3.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:
@@ -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 \| str \| None` | Number of repeated field values, or an expression that evaluates to the count. |
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:
@@ -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 \| str \| None` | Number of repeated field values, or an expression that evaluates to the count. |
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.3.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"
@@ -152,6 +152,8 @@ def _resolve_byte_swap(field_def: FieldDef, env: dict[str, Any]) -> bool:
152
152
  def _resolve_repeat(value: int | str | None, env: dict[str, Any]) -> int | None:
153
153
  """Resolve a static or expression-based repeat count."""
154
154
  if isinstance(value, str):
155
+ if value == "end":
156
+ return None
155
157
  return int(SltEval(env).eval(value))
156
158
  return value
157
159
 
@@ -335,6 +337,7 @@ def encode(
335
337
 
336
338
  for field_value in struct_instance.field_instances:
337
339
  field_def = field_value.field_def
340
+ repeat_until_end = field_def.repeat == "end"
338
341
  repeat = _resolve_repeat(field_def.repeat, env)
339
342
  if _is_padding_field_def(field_def):
340
343
  has_padding = True
@@ -342,9 +345,12 @@ def encode(
342
345
  if _is_padding_field_def(field_def):
343
346
  padding_size = _resolve_info_size(field_def.size, env)
344
347
  value = bytearray(padding_size.bytes)
348
+ if repeat_until_end:
349
+ repeat_values = list(value)
350
+ repeat = len(repeat_values)
345
351
  if repeat is not None and repeat > 1:
346
352
  current_offset = _resolve_info_size(field_def.offset, env)
347
- values = list(value)
353
+ values = repeat_values if repeat_until_end else list(value)
348
354
 
349
355
  for i in range(repeat):
350
356
  field_def_repeat = _repeated_field_def(field_def, i,
@@ -508,6 +514,27 @@ def decode(
508
514
  result = StructInstance(struct_def=struct_def_obj)
509
515
  current_position = InfoSize(0, 0)
510
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))
511
538
 
512
539
  def append_padding_until(target_offset: InfoSize) -> None:
513
540
  nonlocal current_position, padding_index
@@ -537,11 +564,18 @@ def decode(
537
564
 
538
565
  current_position = target_offset
539
566
 
540
- for field_def in _as_field_defs(struct_def_obj):
541
- repeat = _resolve_repeat(field_def.repeat, env)
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))
542
571
  # Handle non-repeated fields
543
- if repeat is None or repeat <= 1:
572
+ if not repeat_until_end and (repeat is None or repeat <= 1):
544
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
545
579
  append_padding_until(resolved_offset)
546
580
  field_instance = decode_field(field_def, data, env, type_dict,
547
581
  padding_alignment_bits)
@@ -553,10 +587,79 @@ def decode(
553
587
  continue
554
588
  # Handle repeated fields
555
589
  current_offset = _resolve_info_size(field_def.offset, env)
556
- append_padding_until(current_offset)
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
+
557
637
  for i in range(repeat):
558
638
  field_def_repeat = _repeated_field_def(field_def, i, current_offset,
559
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)
560
663
  field_instance = decode_field(field_def_repeat, data, env,
561
664
  type_dict, padding_alignment_bits)
562
665
  if field_instance is not None:
@@ -569,6 +672,15 @@ def decode(
569
672
  actual_size, InfoSize):
570
673
  current_offset += actual_size
571
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
572
684
 
573
685
  actual_size = InfoSize(0, 0)
574
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)
@@ -367,6 +377,16 @@ class StructDef:
367
377
  default_factory=list,
368
378
  metadata={"desc": "The fields of the structure"},
369
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()
370
390
 
371
391
  def __lt__(self, other: object) -> bool:
372
392
  """Compare structure definitions using a stable serialized sort key."""
@@ -383,6 +403,42 @@ class StructDef:
383
403
  sort_keys=True),
384
404
  )
385
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
+
386
442
  def to_dict(self) -> dict[str, Any]:
387
443
  """Convert this structure definition to JSON-serializable data."""
388
444
  return {
@@ -432,6 +488,11 @@ class StructInstance:
432
488
  default_factory=InfoSize,
433
489
  metadata={"desc": "The total size of the structure instance"},
434
490
  )
491
+ _field_instances_by_field_def_name: dict[str, list[FieldInstance]] = field(
492
+ default_factory=dict,
493
+ init=False,
494
+ repr=False,
495
+ )
435
496
 
436
497
  def __post_init__(self) -> None:
437
498
  """Normalize stored field instances to sorted order."""
@@ -469,6 +530,19 @@ class StructInstance:
469
530
  """Return one field instance by index."""
470
531
  return self.field_instances[index]
471
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
+
472
546
  def __lt__(self, other: object) -> bool:
473
547
  """Compare structure instances using a stable serialized sort key."""
474
548
  if not isinstance(other, StructInstance):
@@ -490,6 +564,16 @@ class StructInstance:
490
564
  """Keep field instances sorted by FieldDef order."""
491
565
  self.field_instances.sort()
492
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
+
493
577
  def _rebuild_padding_field_instances(self) -> None:
494
578
  """Rebuild padding field instances for gaps between stored values."""
495
579
  non_padding_instances = [
@@ -507,6 +591,7 @@ class StructInstance:
507
591
  self.field_instances = non_padding_instances
508
592
  self._sort_field_instances()
509
593
  self._update_size()
594
+ self._rebuild_field_def_name_index()
510
595
  return
511
596
 
512
597
  rebuilt_instances: list[FieldInstance] = []
@@ -555,6 +640,7 @@ class StructInstance:
555
640
  self.field_instances = rebuilt_instances
556
641
  self._sort_field_instances()
557
642
  self._update_size()
643
+ self._rebuild_field_def_name_index()
558
644
 
559
645
  def _update_size(self) -> None:
560
646
  """Update the instance size from the current field layout."""
@@ -198,6 +198,90 @@ def test_expression_repeat_uses_previous_field_value():
198
198
  for instance in decoded.field_instances] == [2, 5, 6, 7]
199
199
 
200
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
+
201
285
  def test_encode_recurses_for_nested_field_types():
202
286
  """Test that encoding a field with a nested field type works correctly."""
203
287
  child_field_defs = [
@@ -288,6 +288,42 @@ def test_struct_def_to_json_from_json_round_trip():
288
288
  assert restored == struct_def
289
289
 
290
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
+
291
327
  def test_struct_instance_field_instance_interface():
292
328
  """Test StructInstance interface for field_instances operations."""
293
329
  field_def_0 = FieldDef(name="b",
@@ -359,6 +395,50 @@ def test_struct_instance_rebuilds_padding_when_field_instances_change():
359
395
  assert struct_instance.field_instances[1].value == b"\x00"
360
396
 
361
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
+
362
442
  def test_field_instance_sort_uses_field_def_order():
363
443
  """Test that FieldInstance sorting delegates to FieldDef ordering."""
364
444
  later = FieldInstance(
@@ -74,7 +74,7 @@ wheels = [
74
74
 
75
75
  [[package]]
76
76
  name = "sltcodec"
77
- version = "2.2.0"
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