sltcodec 0.3.0__tar.gz → 0.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sltcodec
3
- Version: 0.3.0
3
+ Version: 0.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
@@ -12,7 +12,7 @@ License-File: LICENSE
12
12
  Keywords: byte,deserialization,layout,serialization,struct
13
13
  Requires-Python: >=3.12
14
14
  Requires-Dist: sltcalc>=0.1.0
15
- Requires-Dist: sltcore>=1.3.0
15
+ Requires-Dist: sltcore>=1.5.0
16
16
  Description-Content-Type: text/markdown
17
17
 
18
18
  # StructLayoutToolkitCodec
@@ -1,12 +1,12 @@
1
1
  [project]
2
2
  name = "sltcodec"
3
- version = "0.3.0"
3
+ version = "0.5.0"
4
4
  description = "Decode and encode bytearrays according to struct layout definitions using sltcore."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
7
7
  dependencies = [
8
8
  "sltcalc>=0.1.0",
9
- "sltcore>=1.3.0",
9
+ "sltcore>=1.5.0",
10
10
  ]
11
11
  license = "MIT"
12
12
  license-files = ["LICENSE"]
@@ -1,8 +1,9 @@
1
1
  from .codec import (decode, encode, encode_field, load_struct_def_dict,
2
2
  save_struct_def_dict)
3
- from .types import FieldDef, FieldInstance, StructDef, StructInstance
3
+ from .types import EnumDef, FieldDef, FieldInstance, StructDef, StructInstance
4
4
 
5
5
  __all__ = [
6
+ "EnumDef",
6
7
  "FieldDef",
7
8
  "FieldInstance",
8
9
  "StructDef",
@@ -8,7 +8,7 @@ from typing import Any
8
8
  from sltcalc import SltEval
9
9
  from sltcore import Info, InfoSize, bits_get, bits_set
10
10
 
11
- from .types import FieldDef, FieldInstance, StructDef, StructInstance
11
+ from .types import EnumDef, FieldDef, FieldInstance, StructDef, StructInstance
12
12
 
13
13
  _PRIMITIVE_TYPES = {
14
14
  "bool",
@@ -20,6 +20,8 @@ _PRIMITIVE_TYPES = {
20
20
  "bytes",
21
21
  }
22
22
 
23
+ _DEFAULT_PADDING_ALIGNMENT_BITS = 32
24
+
23
25
 
24
26
  def _as_field_defs(struct_def: StructDef | list[FieldDef]) -> list[FieldDef]:
25
27
  """Normalize StructDef/list inputs to a list of field definitions."""
@@ -96,6 +98,16 @@ def _info_size_from_bits(total_bits: int) -> InfoSize:
96
98
  return InfoSize(total_bits >> 3, total_bits & 0x7)
97
99
 
98
100
 
101
+ def _validate_padding_alignment_bits(padding_alignment_bits: int) -> None:
102
+ """Validate padding alignment as a positive power-of-two bit size."""
103
+ if (isinstance(padding_alignment_bits, bool)
104
+ or not isinstance(padding_alignment_bits, int)
105
+ or padding_alignment_bits <= 0
106
+ or (padding_alignment_bits & (padding_alignment_bits - 1)) != 0):
107
+ raise ValueError(
108
+ "padding_alignment_bits must be a positive power of two")
109
+
110
+
99
111
  def _resolve_field_type(field_type: str | StructDef,
100
112
  struct_def_dict: dict[str, StructDef] | None = None,
101
113
  env: dict[str, Any] | None = None) -> str | StructDef:
@@ -141,7 +153,9 @@ def _prepare_field_info(
141
153
  field_def: FieldDef,
142
154
  value: Any,
143
155
  env: dict[str, Any],
144
- struct_def_dict: dict[str, StructDef] | None = None
156
+ struct_def_dict: dict[str, StructDef] | None = None,
157
+ enum_def_dict: dict[str, EnumDef] | None = None,
158
+ padding_alignment_bits: int = _DEFAULT_PADDING_ALIGNMENT_BITS,
145
159
  ) -> tuple[InfoSize, InfoSize, Info] | None:
146
160
  """Resolve a field definition into an offset, size, and info payload."""
147
161
  offset = _resolve_offset(field_def, env)
@@ -155,7 +169,11 @@ def _prepare_field_info(
155
169
  raise TypeError(
156
170
  "Nested StructDef fields must be encoded with StructInstance "
157
171
  "values")
158
- nested_bytes = encode(value, bytearray(), struct_def_dict)
172
+ nested_bytes = encode(value,
173
+ bytearray(),
174
+ struct_def_dict,
175
+ enum_def_dict,
176
+ padding_alignment_bits=padding_alignment_bits)
159
177
  info = Info.from_bytes(bytes(nested_bytes), size, scale=field_def.scale)
160
178
  else:
161
179
  info = _encode_primitive(resolved_type, value, size, field_def.scale)
@@ -175,16 +193,21 @@ def _split_repeated_field(field_def: FieldDef,
175
193
  size=resolved_size)
176
194
 
177
195
 
178
- def encode_field(field_def: FieldDef,
179
- value: Any,
180
- buf: bytearray,
181
- env: dict[str, Any] | None = None,
182
- struct_def_dict: dict[str, StructDef] | None = None) -> None:
196
+ def encode_field(
197
+ field_def: FieldDef,
198
+ value: Any,
199
+ buf: bytearray,
200
+ env: dict[str, Any] | None = None,
201
+ struct_def_dict: dict[str, StructDef] | None = None,
202
+ enum_def_dict: dict[str, EnumDef] | None = None,
203
+ padding_alignment_bits: int = _DEFAULT_PADDING_ALIGNMENT_BITS,
204
+ ) -> None:
183
205
  """Encode a single field into a bytearray."""
184
206
  if env is None:
185
207
  env = {}
186
208
 
187
- prepared = _prepare_field_info(field_def, value, env, struct_def_dict)
209
+ prepared = _prepare_field_info(field_def, value, env, struct_def_dict,
210
+ enum_def_dict, padding_alignment_bits)
188
211
  if prepared is None:
189
212
  return
190
213
 
@@ -196,9 +219,13 @@ def encode_field(field_def: FieldDef,
196
219
  bits_set(buf, offset, info)
197
220
 
198
221
 
199
- def encode(struct_instance: StructInstance,
200
- buf: bytearray,
201
- struct_def_dict: dict[str, StructDef] | None = None) -> bytearray:
222
+ def encode(
223
+ struct_instance: StructInstance,
224
+ buf: bytearray,
225
+ struct_def_dict: dict[str, StructDef] | None = None,
226
+ enum_def_dict: dict[str, EnumDef] | None = None,
227
+ padding_alignment_bits: int = _DEFAULT_PADDING_ALIGNMENT_BITS,
228
+ ) -> bytearray:
202
229
  """Encode decode() result into a bytearray.
203
230
 
204
231
  Parameters
@@ -209,6 +236,10 @@ def encode(struct_instance: StructInstance,
209
236
  The base bytearray instance to write into.
210
237
  struct_def_dict : dict[str, StructDef] | None, optional
211
238
  A dictionary of structure definitions, by default None.
239
+ enum_def_dict : dict[str, EnumDef] | None, optional
240
+ A dictionary of enum definitions, by default None.
241
+ padding_alignment_bits : int, optional
242
+ The padding alignment boundary in bits, by default 32.
212
243
 
213
244
  Returns
214
245
  -------
@@ -216,6 +247,7 @@ def encode(struct_instance: StructInstance,
216
247
  The encoded bytearray.
217
248
  """
218
249
  _validate_struct_instance(struct_instance)
250
+ _validate_padding_alignment_bits(padding_alignment_bits)
219
251
  env: dict[str, Any] = {}
220
252
  has_padding = False
221
253
 
@@ -236,14 +268,16 @@ def encode(struct_instance: StructInstance,
236
268
  field_def_repeat = _split_repeated_field(
237
269
  field_def, i, env, current_offset)
238
270
  encode_field(field_def_repeat, values[i], buf, env,
239
- struct_def_dict)
271
+ struct_def_dict, enum_def_dict,
272
+ padding_alignment_bits)
240
273
  env[field_def_repeat.name] = values[i]
241
274
  if isinstance(current_offset, InfoSize) and isinstance(
242
275
  resolved_size, InfoSize):
243
276
  current_offset += resolved_size
244
277
  continue
245
278
 
246
- encode_field(field_def, value, buf, env, struct_def_dict)
279
+ encode_field(field_def, value, buf, env, struct_def_dict, enum_def_dict,
280
+ padding_alignment_bits)
247
281
  env[field_def.name] = value
248
282
 
249
283
  if has_padding and struct_instance.size.bytes > len(buf):
@@ -256,7 +290,9 @@ def decode_field(
256
290
  field_def: FieldDef,
257
291
  data: bytearray | bytes,
258
292
  env: dict[str, Any] | None = None,
259
- struct_def_dict: dict[str, StructDef] | None = None
293
+ struct_def_dict: dict[str, StructDef] | None = None,
294
+ enum_def_dict: dict[str, EnumDef] | None = None,
295
+ padding_alignment_bits: int = _DEFAULT_PADDING_ALIGNMENT_BITS,
260
296
  ) -> FieldInstance | None:
261
297
  """Decode a single field from a bytearray according to a field definition.
262
298
 
@@ -283,29 +319,56 @@ def decode_field(
283
319
  return None
284
320
  info = bits_get(data, offset, size, scale=field_def.scale)
285
321
  resolved_type = _resolve_field_type(field_def.type, struct_def_dict, env)
322
+ resolved_field_def = FieldDef(
323
+ name=field_def.name,
324
+ offset=offset,
325
+ size=size,
326
+ type=resolved_type,
327
+ scale=field_def.scale,
328
+ repeat=field_def.repeat,
329
+ description=field_def.description,
330
+ range_expression=field_def.range_expression,
331
+ enum_def=field_def.enum_def,
332
+ )
286
333
  if isinstance(resolved_type, StructDef):
287
334
  return FieldInstance(
288
- field_def=field_def,
335
+ field_def=resolved_field_def,
289
336
  value=decode(resolved_type, bytearray(info.to_bytes),
290
- struct_def_dict),
337
+ struct_def_dict, enum_def_dict,
338
+ padding_alignment_bits),
291
339
  )
292
340
  if resolved_type == "bool":
293
- return FieldInstance(field_def=field_def, value=info.to_bool)
341
+ return FieldInstance.from_value(resolved_field_def,
342
+ info.to_bool,
343
+ enum_def_dict=enum_def_dict)
294
344
  if resolved_type == "signed int":
295
- return FieldInstance(field_def=field_def, value=info.to_signed_int)
345
+ return FieldInstance.from_value(resolved_field_def,
346
+ info.to_signed_int,
347
+ enum_def_dict=enum_def_dict)
296
348
  if resolved_type in ["int", "unsigned int"]:
297
- return FieldInstance(field_def=field_def, value=info.to_unsigned_int)
349
+ return FieldInstance.from_value(resolved_field_def,
350
+ info.to_unsigned_int,
351
+ enum_def_dict=enum_def_dict)
298
352
  if resolved_type == "float":
299
- return FieldInstance(field_def=field_def, value=info.to_float)
353
+ return FieldInstance.from_value(resolved_field_def,
354
+ info.to_float,
355
+ enum_def_dict=enum_def_dict)
300
356
  if resolved_type in ["bytearray", "bytes"]:
301
- return FieldInstance(field_def=field_def, value=info.to_bytes)
302
- return FieldInstance(field_def=field_def, value=info.raw_value)
357
+ return FieldInstance.from_value(resolved_field_def,
358
+ info.to_bytes,
359
+ enum_def_dict=enum_def_dict)
360
+ return FieldInstance.from_value(resolved_field_def,
361
+ info.raw_value,
362
+ enum_def_dict=enum_def_dict)
303
363
 
304
364
 
305
365
  def decode(
306
- struct_def: StructDef | list[FieldDef],
307
- data: bytearray | bytes,
308
- struct_def_dict: dict[str, StructDef] | None = None) -> StructInstance:
366
+ struct_def: StructDef | list[FieldDef],
367
+ data: bytearray | bytes,
368
+ struct_def_dict: dict[str, StructDef] | None = None,
369
+ enum_def_dict: dict[str, EnumDef] | None = None,
370
+ padding_alignment_bits: int = _DEFAULT_PADDING_ALIGNMENT_BITS,
371
+ ) -> StructInstance:
309
372
  """Decode a bytearray into field values according to a layout.
310
373
 
311
374
  Parameters
@@ -316,11 +379,16 @@ def decode(
316
379
  The data to decode.
317
380
  struct_def_dict : dict[str, StructDef] | None, optional
318
381
  A dictionary of structure definitions, by default None.
382
+ enum_def_dict : dict[str, EnumDef] | None, optional
383
+ A dictionary of enum definitions, by default None.
384
+ padding_alignment_bits : int, optional
385
+ The padding alignment boundary in bits, by default 32.
319
386
  Returns
320
387
  -------
321
388
  StructInstance
322
389
  The decoded structure instance.
323
390
  """
391
+ _validate_padding_alignment_bits(padding_alignment_bits)
324
392
  env = {}
325
393
  struct_def_obj = (struct_def if isinstance(struct_def, StructDef) else
326
394
  StructDef(fields=struct_def))
@@ -337,8 +405,11 @@ def decode(
337
405
 
338
406
  chunk_start_bits = start_bits
339
407
  while chunk_start_bits < end_bits:
340
- next_boundary_bits = ((chunk_start_bits // 32) + 1) * 32
341
- chunk_end_bits = min(end_bits, next_boundary_bits)
408
+ chunk_start = _info_size_from_bits(chunk_start_bits)
409
+ bits_to_boundary = _info_size_to_bits(
410
+ chunk_start.align_to(padding_alignment_bits))
411
+ chunk_size_bits = bits_to_boundary or padding_alignment_bits
412
+ chunk_end_bits = min(end_bits, chunk_start_bits + chunk_size_bits)
342
413
  padding_field_def = FieldDef(
343
414
  name=f"padding[{padding_index}]",
344
415
  offset=_info_size_from_bits(chunk_start_bits),
@@ -359,7 +430,8 @@ def decode(
359
430
  if field_def.repeat is None or field_def.repeat <= 1:
360
431
  resolved_offset = _resolve_offset(field_def, env)
361
432
  append_padding_until(resolved_offset)
362
- field_instance = decode_field(field_def, data, env, struct_def_dict)
433
+ field_instance = decode_field(field_def, data, env, struct_def_dict,
434
+ enum_def_dict, padding_alignment_bits)
363
435
  if field_instance is not None:
364
436
  env[field_instance.field_def.name] = field_instance.value
365
437
  result.append_field_instance(field_instance)
@@ -374,7 +446,8 @@ def decode(
374
446
  field_def_repeat = _split_repeated_field(field_def, i, env,
375
447
  current_offset)
376
448
  field_instance = decode_field(field_def_repeat, data, env,
377
- struct_def_dict)
449
+ struct_def_dict, enum_def_dict,
450
+ padding_alignment_bits)
378
451
  if field_instance is not None:
379
452
  env[field_instance.field_def.name] = field_instance.value
380
453
  result.append_field_instance(field_instance)
@@ -1,16 +1,90 @@
1
1
  """Type definitions for structured layout metadata."""
2
2
  from __future__ import annotations
3
3
 
4
- import importlib
5
4
  import json
6
5
  import re
7
6
  from dataclasses import dataclass, field
8
- from enum import Enum
7
+ from functools import total_ordering
9
8
  from typing import Any
10
9
 
10
+ from sltcalc import SltEval
11
11
  from sltcore import Info, InfoSize
12
12
 
13
13
 
14
+ @total_ordering
15
+ @dataclass(frozen=True)
16
+ class EnumDef:
17
+ """An enumeration definition."""
18
+ name: str = field(default_factory=str,
19
+ metadata={"desc": "The name of the enum"})
20
+ description: str | None = field(
21
+ default=None, metadata={"desc": "The description of the enum"})
22
+ values: dict[str, int] = field(
23
+ default_factory=dict,
24
+ metadata={"desc": "The mapping of enum names to integer values"},
25
+ )
26
+
27
+ def to_dict(self) -> dict[str, Any]:
28
+ """Convert this enum definition to a JSON-serializable dictionary."""
29
+ return {
30
+ "name": self.name,
31
+ "description": self.description,
32
+ "values": self.values,
33
+ }
34
+
35
+ def to_json(self) -> str:
36
+ """Convert this enum definition to a JSON string."""
37
+ return json.dumps(self.to_dict(), ensure_ascii=False, indent=2)
38
+
39
+ @classmethod
40
+ def from_dict(cls, data: dict[str, Any]) -> "EnumDef":
41
+ """Create an enum definition from a JSON-serializable dictionary."""
42
+ name = data.get("name", "")
43
+ description = data.get("description")
44
+ values = data.get("values", {})
45
+ return cls(name=name, description=description, values=values)
46
+
47
+ def serialize(self) -> dict[str, Any]:
48
+ """Serialize this enum definition with an explicit type tag."""
49
+ return {
50
+ "__type__": "EnumDef",
51
+ "name": self.name,
52
+ "description": self.description,
53
+ "values": self.values,
54
+ }
55
+
56
+ @classmethod
57
+ def deserialize(cls, data: dict[str, Any]) -> "EnumDef":
58
+ """Deserialize an enum definition from a typed dictionary."""
59
+ if data.get("__type__") != "EnumDef":
60
+ raise ValueError("Invalid EnumDef payload")
61
+ return cls(
62
+ name=data.get("name", ""),
63
+ description=data.get("description"),
64
+ values=data.get("values", {}),
65
+ )
66
+
67
+ @classmethod
68
+ def from_json(cls, data: str) -> "EnumDef":
69
+ """Create an enum definition from a JSON string."""
70
+ return cls.from_dict(json.loads(data))
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
+ )
85
+
86
+
87
+ @total_ordering
14
88
  @dataclass(frozen=True)
15
89
  class FieldDef:
16
90
  """A field in a structured layout."""
@@ -30,8 +104,8 @@ class FieldDef:
30
104
  default=None, metadata={"desc": "The description of the field"})
31
105
  range_expression: str | None = field(
32
106
  default=None, metadata={"desc": "The value range expression"})
33
- enum_type: type[Enum] | None = field(default=None,
34
- metadata={"desc": "The enum type"})
107
+ enum_def: EnumDef | None = field(
108
+ default=None, metadata={"desc": "The enum definition for the field"})
35
109
 
36
110
  def split_repeat(self,
37
111
  index: int,
@@ -58,7 +132,7 @@ class FieldDef:
58
132
  repeat=None,
59
133
  description=self.description,
60
134
  range_expression=split_range_expression,
61
- enum_type=self.enum_type)
135
+ enum_def=self.enum_def)
62
136
 
63
137
  @staticmethod
64
138
  def _replace_name_in_expression(value: Any, old_name: str,
@@ -79,15 +153,33 @@ class FieldDef:
79
153
  def to_dict(self) -> dict[str, Any]:
80
154
  """Convert this field definition to a JSON-serializable dictionary."""
81
155
  return {
82
- "name": self.name,
83
- "offset": self._serialize_value(self.offset),
84
- "size": self._serialize_value(self.size),
85
- "type": self._serialize_type(self.type),
86
- "scale": self.scale,
87
- "repeat": self.repeat,
88
- "description": self.description,
89
- "range_expression": self.range_expression,
90
- "enum_type": self._serialize_enum_type(self.enum_type),
156
+ "name":
157
+ self.name,
158
+ "offset":
159
+ self.offset.serialize() if isinstance(self.offset,
160
+ (Info,
161
+ InfoSize)) else self.offset,
162
+ "size":
163
+ self.size.serialize() if isinstance(self.size,
164
+ (Info,
165
+ InfoSize)) else self.size,
166
+ "type": {
167
+ "__type__": "StructDef",
168
+ "fields": self.type.to_dict(),
169
+ } if isinstance(self.type, StructDef) else {
170
+ "__type__": "StructDef",
171
+ "fields": [field.to_dict() for field in self.type],
172
+ } if isinstance(self.type, list) else self.type,
173
+ "scale":
174
+ self.scale,
175
+ "repeat":
176
+ self.repeat,
177
+ "description":
178
+ self.description,
179
+ "range_expression":
180
+ self.range_expression,
181
+ "enum_def":
182
+ None if self.enum_def is None else self.enum_def.serialize(),
91
183
  }
92
184
 
93
185
  def to_json(self) -> str:
@@ -97,16 +189,68 @@ class FieldDef:
97
189
  @classmethod
98
190
  def from_dict(cls, data: dict[str, Any]) -> "FieldDef":
99
191
  """Create a field definition from a JSON-serializable dictionary."""
192
+ name = data.get("name", "")
193
+ offset_data = data.get("offset")
194
+ size_data = data.get("size")
195
+ type_data = data.get("type")
196
+ scale = data.get("scale", 1.0)
197
+ repeat = data.get("repeat")
198
+ description = data.get("description")
199
+ range_expression = data.get("range_expression")
200
+ enum_def_data = data.get("enum_def")
201
+
202
+ def _deserialize_info_like(value: Any) -> Any:
203
+ if isinstance(value, dict):
204
+ value_type = value.get("__type__")
205
+ if value_type == "Info":
206
+ return Info.deserialize(json.dumps(value))
207
+ if value_type == "InfoSize":
208
+ return InfoSize.deserialize(json.dumps(value))
209
+ return value
210
+ if not isinstance(value, str):
211
+ return value
212
+ try:
213
+ parsed_value = json.loads(value)
214
+ except json.JSONDecodeError:
215
+ return value
216
+ if (isinstance(parsed_value, dict)
217
+ and parsed_value.get("__type__") == "Info"):
218
+ return Info.deserialize(value)
219
+ if (isinstance(parsed_value, dict)
220
+ and parsed_value.get("__type__") == "InfoSize"):
221
+ return InfoSize.deserialize(value)
222
+ return value
223
+
224
+ offset = _deserialize_info_like(offset_data)
225
+ size = _deserialize_info_like(size_data)
226
+
227
+ if (isinstance(type_data, dict)
228
+ and type_data.get("__type__") == "StructDef"):
229
+ type_value = StructDef.from_dict(type_data["fields"])
230
+ elif isinstance(type_data, list):
231
+ # Backward-compatibility for legacy list-based nested types.
232
+ type_value = StructDef.from_dict(type_data)
233
+ else:
234
+ type_value = type_data
235
+
236
+ if enum_def_data is None:
237
+ enum_def = None
238
+ elif (isinstance(enum_def_data, dict)
239
+ and enum_def_data.get("__type__") == "EnumDef"):
240
+ enum_def = EnumDef.deserialize(enum_def_data)
241
+ else:
242
+ enum_def = None
243
+
100
244
  return cls(
101
- name=data.get("name", ""),
102
- offset=cls._deserialize_value(data.get("offset")),
103
- size=cls._deserialize_value(data.get("size")),
104
- type=cls._deserialize_type(data.get("type")),
105
- scale=data.get("scale", 1.0),
106
- repeat=data.get("repeat"),
107
- description=data.get("description"),
108
- range_expression=data.get("range_expression"),
109
- enum_type=cls._deserialize_enum_type(data.get("enum_type")),
245
+ name=name,
246
+ offset=offset,
247
+ size=size,
248
+ type=type_value,
249
+ scale=scale,
250
+ repeat=repeat,
251
+ description=description,
252
+ range_expression=range_expression,
253
+ enum_def=enum_def,
110
254
  )
111
255
 
112
256
  @classmethod
@@ -114,83 +258,6 @@ class FieldDef:
114
258
  """Create a field definition from a JSON string."""
115
259
  return cls.from_dict(json.loads(data))
116
260
 
117
- @staticmethod
118
- def _serialize_value(value: Any) -> Any:
119
- if isinstance(value, Info):
120
- return {
121
- "__type__": "Info",
122
- "value": value.to_json(),
123
- }
124
- if isinstance(value, InfoSize):
125
- return {
126
- "__type__": "InfoSize",
127
- "value": value.to_json(),
128
- }
129
- return value
130
-
131
- @classmethod
132
- def _deserialize_value(cls, value: Any) -> Any:
133
- if isinstance(value, dict) and value.get("__type__") == "Info":
134
- return Info.from_json(value["value"])
135
- if isinstance(value, dict) and value.get("__type__") == "InfoSize":
136
- return InfoSize.from_json(value["value"])
137
- return value
138
-
139
- @staticmethod
140
- def _serialize_type(value: Any) -> Any:
141
- if isinstance(value, StructDef):
142
- return {
143
- "__type__": "StructDef",
144
- "fields": value.to_dict(),
145
- }
146
- if isinstance(value, list):
147
- # Backward-compatibility for legacy list-based nested types.
148
- return {
149
- "__type__": "StructDef",
150
- "fields": [field.to_dict() for field in value],
151
- }
152
- return value
153
-
154
- @classmethod
155
- def _deserialize_type(cls, value: Any) -> Any:
156
- if isinstance(value, dict) and value.get("__type__") == "StructDef":
157
- return StructDef.from_dict(value["fields"])
158
- if isinstance(value, list):
159
- # Backward-compatibility for legacy list-based nested types.
160
- return StructDef.from_dict(value)
161
- return value
162
-
163
- @staticmethod
164
- def _serialize_enum_type(value: type[Enum] | None) -> Any:
165
- if value is None:
166
- return None
167
- return {
168
- "__type__": "EnumType",
169
- "module": value.__module__,
170
- "qualname": value.__qualname__,
171
- }
172
-
173
- @classmethod
174
- def _deserialize_enum_type(cls, value: Any) -> type[Enum] | None:
175
- if value is None:
176
- return None
177
- if not isinstance(value, dict) or value.get("__type__") != "EnumType":
178
- return None
179
- module_name = value.get("module")
180
- qualname = value.get("qualname")
181
- if not isinstance(module_name, str) or not isinstance(qualname, str):
182
- return None
183
- try:
184
- module = importlib.import_module(module_name)
185
- enum_type: Any = module
186
- for attr in qualname.split("."):
187
- enum_type = getattr(enum_type, attr)
188
- if isinstance(enum_type, type) and issubclass(enum_type, Enum):
189
- return enum_type
190
- except (ImportError, AttributeError, TypeError):
191
- return None
192
- return None
193
-
194
261
  def _sort_key(self) -> tuple[Any, ...]:
195
262
  """Build a stable comparison key for ordering field definitions."""
196
263
  return (
@@ -202,7 +269,7 @@ class FieldDef:
202
269
  -1 if self.repeat is None else self.repeat,
203
270
  "" if self.description is None else self.description,
204
271
  "" if self.range_expression is None else self.range_expression,
205
- self._sortable_enum_type(self.enum_type),
272
+ self._sortable_enum_type(self.enum_def),
206
273
  )
207
274
 
208
275
  @staticmethod
@@ -220,18 +287,29 @@ class FieldDef:
220
287
  return (1, value)
221
288
 
222
289
  @staticmethod
223
- def _sortable_enum_type(value: type[Enum] | None) -> str:
224
- """Build a comparable key for enum type metadata."""
290
+ def _sortable_enum_type(value: EnumDef | None) -> str:
291
+ """Build a comparable key for enum definition metadata."""
225
292
  if value is None:
226
293
  return ""
227
- return f"{value.__module__}.{value.__qualname__}"
294
+ return json.dumps(
295
+ {
296
+ "name": value.name,
297
+ "description": value.description,
298
+ "values": value.values,
299
+ },
300
+ sort_keys=True)
228
301
 
229
302
 
303
+ @total_ordering
230
304
  @dataclass(frozen=True)
231
305
  class FieldInstance:
232
306
  """A decoded/encodable field value with its field definition."""
233
307
  field_def: FieldDef = field(metadata={"desc": "The field definition"})
234
308
  value: Any = field(metadata={"desc": "The decoded/encodable value"})
309
+ enum_item: tuple[str, int] | None = field(
310
+ default=None,
311
+ metadata={"desc": "Matched enum item for this value"},
312
+ )
235
313
  is_padding: bool = field(
236
314
  default=False,
237
315
  metadata={"desc": "Whether this field instance represents padding"},
@@ -243,7 +321,59 @@ class FieldInstance:
243
321
  return NotImplemented
244
322
  return self.field_def < other.field_def
245
323
 
324
+ def range_check(self, env: dict[str, Any] | None = None) -> Any | None:
325
+ """Evaluate range_expression and return its evaluated result."""
326
+ range_expression = self.field_def.range_expression
327
+ if range_expression is None:
328
+ return None
246
329
 
330
+ eval_env = {} if env is None else dict(env)
331
+ eval_env[self.field_def.name] = self.value
332
+ stleval = SltEval(eval_env)
333
+ return stleval.eval(range_expression)
334
+
335
+ @classmethod
336
+ def from_value(cls,
337
+ field_def: FieldDef,
338
+ value: Any,
339
+ enum_def_dict: dict[str, EnumDef] | None = None,
340
+ is_padding: bool = False) -> "FieldInstance":
341
+ """Create a FieldInstance and attach a matched enum item if any."""
342
+ enum_def = cls._resolve_enum_def(field_def, enum_def_dict)
343
+ enum_item = cls._resolve_enum_item(enum_def, value)
344
+ return cls(field_def=field_def,
345
+ value=value,
346
+ enum_item=enum_item,
347
+ is_padding=is_padding)
348
+
349
+ @staticmethod
350
+ def _resolve_enum_def(
351
+ field_def: FieldDef,
352
+ enum_def_dict: dict[str, EnumDef] | None) -> EnumDef | None:
353
+ """Resolve enum definition from field metadata or lookup dictionary."""
354
+ if field_def.enum_def is not None:
355
+ return field_def.enum_def
356
+ if not enum_def_dict:
357
+ return None
358
+ if isinstance(field_def.type, str) and field_def.type in enum_def_dict:
359
+ return enum_def_dict[field_def.type]
360
+ return enum_def_dict.get(field_def.name)
361
+
362
+ @staticmethod
363
+ def _resolve_enum_item(enum_def: EnumDef | None,
364
+ value: Any) -> tuple[str, int] | None:
365
+ """Resolve one enum values item that matches the given value."""
366
+ if enum_def is None:
367
+ return None
368
+ if isinstance(value, bool) or not isinstance(value, int):
369
+ return None
370
+ for enum_name, enum_value in enum_def.values.items():
371
+ if enum_value == value:
372
+ return enum_name, enum_value
373
+ return None
374
+
375
+
376
+ @total_ordering
247
377
  @dataclass(frozen=True)
248
378
  class StructDef:
249
379
  """A structured layout definition that groups multiple fields."""
@@ -258,6 +388,21 @@ class StructDef:
258
388
  metadata={"desc": "The fields of the structure"},
259
389
  )
260
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
+
261
406
  def to_dict(self) -> dict[str, Any]:
262
407
  """Convert this structure definition to JSON-serializable data."""
263
408
  return {
@@ -279,12 +424,11 @@ class StructDef:
279
424
  if isinstance(data, list):
280
425
  # Backward-compatibility for legacy list-only StructDef payloads.
281
426
  return cls(fields=[FieldDef.from_dict(item) for item in data])
282
- return cls(name=data.get("name", ""),
283
- description=data.get("description", ""),
284
- fields=[
285
- FieldDef.from_dict(item)
286
- for item in data.get("fields", [])
287
- ])
427
+ name = data.get("name", "")
428
+ description = data.get("description", "")
429
+ fields_data = data.get("fields", [])
430
+ fields = [FieldDef.from_dict(item) for item in fields_data]
431
+ return cls(name=name, description=description, fields=fields)
288
432
 
289
433
  @classmethod
290
434
  def from_json(cls, data: str) -> "StructDef":
@@ -292,6 +436,7 @@ class StructDef:
292
436
  return cls.from_dict(json.loads(data))
293
437
 
294
438
 
439
+ @total_ordering
295
440
  @dataclass
296
441
  class StructInstance:
297
442
  """A decoded/encodable structure instance."""
@@ -344,6 +489,23 @@ class StructInstance:
344
489
  """Return one field instance by index."""
345
490
  return self.field_instances[index]
346
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
+
347
509
  def _sort_field_instances(self) -> None:
348
510
  """Keep field instances sorted by FieldDef order."""
349
511
  self.field_instances.sort()
@@ -356,14 +518,25 @@ class StructInstance:
356
518
  ]
357
519
  non_padding_instances.sort()
358
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
+
359
532
  rebuilt_instances: list[FieldInstance] = []
360
533
  current_offset = InfoSize(0, 0)
361
534
  padding_index = 0
362
535
 
363
536
  for field_instance in non_padding_instances:
364
537
  field_def = field_instance.field_def
365
- field_offset = self._resolve_field_offset(field_def)
366
- field_size = self._resolve_field_size(field_def)
538
+ field_offset = field_def.offset
539
+ field_size = field_def.size
367
540
  if field_offset > current_offset:
368
541
  gap_size = field_offset - current_offset
369
542
  if gap_size.byte > 0 or gap_size.bit > 0:
@@ -411,25 +584,15 @@ class StructInstance:
411
584
  max_end_offset = InfoSize(0, 0)
412
585
  for field_instance in self.field_instances:
413
586
  field_def = field_instance.field_def
414
- field_offset = self._resolve_field_offset(field_def)
415
- field_size = self._resolve_field_size(field_def)
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
416
593
  field_end = field_offset + field_size
417
594
  if field_end > max_end_offset:
418
595
  max_end_offset = field_end
419
596
 
420
597
  if max_end_offset > self.size:
421
598
  self.size = max_end_offset
422
-
423
- @staticmethod
424
- def _resolve_field_offset(field_def: FieldDef) -> InfoSize:
425
- """Resolve field offsets to InfoSize values when possible."""
426
- if isinstance(field_def.offset, InfoSize):
427
- return field_def.offset
428
- return InfoSize(0, 0)
429
-
430
- @staticmethod
431
- def _resolve_field_size(field_def: FieldDef) -> InfoSize:
432
- """Resolve field sizes to InfoSize values when possible."""
433
- if isinstance(field_def.size, InfoSize):
434
- return field_def.size
435
- return InfoSize(0, 0)
@@ -5,14 +5,14 @@ from pathlib import Path
5
5
  import pytest
6
6
  from sltcore import InfoSize
7
7
 
8
- from sltcodec import (FieldDef, FieldInstance, StructDef, StructInstance,
9
- decode, encode, load_struct_def_dict,
8
+ from sltcodec import (EnumDef, FieldDef, FieldInstance, StructDef,
9
+ StructInstance, decode, encode, load_struct_def_dict,
10
10
  save_struct_def_dict)
11
11
  from sltcodec.codec import decode_field
12
12
 
13
13
 
14
14
  class ValueKind(Enum):
15
- """Enum used for FieldDef enum_type metadata tests."""
15
+ """Enum used for EnumDef metadata tests."""
16
16
  A = 1
17
17
  B = 2
18
18
 
@@ -155,13 +155,17 @@ def test_repeated_field_preserves_description():
155
155
 
156
156
  def test_repeated_field_preserves_range_and_enum_metadata():
157
157
  """Test repeated fields preserve range/enum metadata on decoded entries."""
158
+ enum_def = EnumDef(
159
+ name="ValueKind",
160
+ values={member.name: member.value
161
+ for member in ValueKind})
158
162
  field_def = FieldDef(name="value",
159
163
  offset=InfoSize(0, 0),
160
164
  size=InfoSize(1, 0),
161
165
  type="unsigned int",
162
166
  repeat=2,
163
167
  range_expression="0 <= value <= 255",
164
- enum_type=ValueKind)
168
+ enum_def=enum_def)
165
169
 
166
170
  encoded = encode(
167
171
  StructInstance(struct_def=StructDef(fields=[field_def]),
@@ -174,8 +178,8 @@ def test_repeated_field_preserves_range_and_enum_metadata():
174
178
  "0 <= value[0] <= 255")
175
179
  assert decoded.field_instances[1].field_def.range_expression == (
176
180
  "0 <= value[1] <= 255")
177
- assert decoded.field_instances[0].field_def.enum_type is ValueKind
178
- assert decoded.field_instances[1].field_def.enum_type is ValueKind
181
+ assert decoded.field_instances[0].field_def.enum_def == enum_def
182
+ assert decoded.field_instances[1].field_def.enum_def == enum_def
179
183
 
180
184
 
181
185
  def test_split_repeat_replaces_name_in_range_expression():
@@ -212,6 +216,10 @@ def test_field_def_json_round_trip():
212
216
 
213
217
  def test_field_def_to_json_from_json_round_trip():
214
218
  """Test FieldDef.to_json/from_json round-trip conversion."""
219
+ enum_def = EnumDef(
220
+ name="ValueKind",
221
+ values={member.name: member.value
222
+ for member in ValueKind})
215
223
  field_def = FieldDef(name="value",
216
224
  offset=InfoSize(0, 0),
217
225
  size=InfoSize(1, 0),
@@ -220,14 +228,123 @@ def test_field_def_to_json_from_json_round_trip():
220
228
  repeat=3,
221
229
  description="A repeated value",
222
230
  range_expression="0 <= value <= 255",
223
- enum_type=ValueKind)
231
+ enum_def=enum_def)
224
232
 
225
233
  payload = field_def.to_json()
226
234
  restored = FieldDef.from_json(payload)
227
235
 
228
236
  assert restored == field_def
229
237
  assert restored.range_expression == "0 <= value <= 255"
230
- assert restored.enum_type is ValueKind
238
+ assert restored.enum_def == enum_def
239
+ assert restored.to_dict()["enum_def"] == {
240
+ "__type__": "EnumDef",
241
+ "name": "ValueKind",
242
+ "description": None,
243
+ "values": {
244
+ member.name: member.value
245
+ for member in ValueKind
246
+ },
247
+ }
248
+
249
+
250
+ def test_enum_def_to_json_from_json_round_trip():
251
+ """Test EnumDef.to_json/from_json round-trip conversion."""
252
+ enum_def = EnumDef(name="Status",
253
+ description="Status enum",
254
+ values={
255
+ "OK": 1,
256
+ "NG": 2
257
+ })
258
+
259
+ payload = enum_def.to_json()
260
+ restored = EnumDef.from_json(payload)
261
+
262
+ assert restored == enum_def
263
+ assert restored.to_dict() == {
264
+ "name": "Status",
265
+ "description": "Status enum",
266
+ "values": {
267
+ "OK": 1,
268
+ "NG": 2,
269
+ },
270
+ }
271
+
272
+
273
+ def test_enum_def_to_dict_from_dict_round_trip():
274
+ """Test EnumDef.to_dict/from_dict round-trip conversion."""
275
+ enum_def = EnumDef(name="Mode",
276
+ description="Mode enum",
277
+ values={
278
+ "AUTO": 0,
279
+ "MANUAL": 1,
280
+ })
281
+
282
+ payload = enum_def.to_dict()
283
+ restored = EnumDef.from_dict(payload)
284
+
285
+ assert restored == enum_def
286
+ assert payload == {
287
+ "name": "Mode",
288
+ "description": "Mode enum",
289
+ "values": {
290
+ "AUTO": 0,
291
+ "MANUAL": 1,
292
+ },
293
+ }
294
+
295
+
296
+ def test_enum_def_serialize_deserialize_round_trip():
297
+ """Test EnumDef.serialize/deserialize round-trip conversion."""
298
+ enum_def = EnumDef(name="State",
299
+ description="State enum",
300
+ values={
301
+ "ON": 1,
302
+ "OFF": 0,
303
+ })
304
+
305
+ payload = enum_def.serialize()
306
+ restored = EnumDef.deserialize(payload)
307
+
308
+ assert restored == enum_def
309
+ assert payload == {
310
+ "__type__": "EnumDef",
311
+ "name": "State",
312
+ "description": "State enum",
313
+ "values": {
314
+ "ON": 1,
315
+ "OFF": 0,
316
+ },
317
+ }
318
+
319
+
320
+ def test_decode_uses_enum_def_dict_to_set_enum_item():
321
+ """Test decode uses enum_def_dict to resolve FieldInstance enum_item."""
322
+ enum_def = EnumDef(name="Status", values={"OK": 1, "NG": 2})
323
+ field_def = FieldDef(name="status",
324
+ offset=InfoSize(0, 0),
325
+ size=InfoSize(1, 0),
326
+ type="unsigned int")
327
+
328
+ decoded = decode([field_def],
329
+ bytearray(b"\x02"),
330
+ enum_def_dict={"status": enum_def})
331
+
332
+ assert decoded.field_instances[0].value == 2
333
+ assert decoded.field_instances[0].enum_item == ("NG", 2)
334
+
335
+
336
+ def test_field_instance_from_value_sets_enum_item_from_field_enum_def():
337
+ """Test FieldInstance.from_value sets enum_item from field enum_def."""
338
+ enum_def = EnumDef(name="Mode", values={"AUTO": 0, "MANUAL": 1})
339
+ field_def = FieldDef(name="mode",
340
+ offset=InfoSize(0, 0),
341
+ size=InfoSize(1, 0),
342
+ type="unsigned int",
343
+ enum_def=enum_def)
344
+
345
+ field_instance = FieldInstance.from_value(field_def, 1)
346
+
347
+ assert field_instance.enum_item == ("MANUAL", 1)
231
348
 
232
349
 
233
350
  def test_struct_def_metadata_json_round_trip():
@@ -358,10 +475,11 @@ def test_type_expression_uses_previous_field_value():
358
475
  bytearray(),
359
476
  )
360
477
  decoded_int = decode(struct_def, encoded_int)
361
- assert decoded_int.field_instances == [
362
- FieldInstance(struct_def[0], 1),
363
- FieldInstance(struct_def[1], 7),
364
- ]
478
+ assert decoded_int.field_instances[0] == FieldInstance(struct_def[0], 1)
479
+ assert decoded_int.field_instances[1].field_def.offset == InfoSize(1, 0)
480
+ assert decoded_int.field_instances[1].field_def.size == InfoSize(1, 0)
481
+ assert decoded_int.field_instances[1].field_def.type == "int"
482
+ assert decoded_int.field_instances[1].value == 7
365
483
 
366
484
  encoded_float = encode(
367
485
  StructInstance(struct_def=StructDef(fields=struct_def),
@@ -373,7 +491,9 @@ def test_type_expression_uses_previous_field_value():
373
491
  )
374
492
  decoded_float = decode(struct_def, encoded_float)
375
493
  assert decoded_float.field_instances[0] == FieldInstance(struct_def[0], 2)
376
- assert decoded_float.field_instances[1].field_def == struct_def[1]
494
+ assert decoded_float.field_instances[1].field_def.offset == InfoSize(1, 0)
495
+ assert decoded_float.field_instances[1].field_def.size == InfoSize(4, 0)
496
+ assert decoded_float.field_instances[1].field_def.type == "float"
377
497
  assert decoded_float.field_instances[1].value == 1.5
378
498
 
379
499
 
@@ -410,6 +530,84 @@ def test_decode_inserts_padding_fields_for_gaps_and_trailing_space():
410
530
  assert decoded.field_instances[2].value == b"\xdd\xee"
411
531
 
412
532
 
533
+ def test_decode_splits_padding_at_32_bit_boundaries():
534
+ """Test split into 32-bit chunks when a gap crosses a boundary."""
535
+ struct_def = StructDef(fields=[
536
+ FieldDef(name="head",
537
+ offset=InfoSize(0, 0),
538
+ size=InfoSize(1, 0),
539
+ type="unsigned int"),
540
+ FieldDef(name="tail",
541
+ offset=InfoSize(5, 0),
542
+ size=InfoSize(1, 0),
543
+ type="unsigned int"),
544
+ ])
545
+ decoded = decode(struct_def, bytearray(8))
546
+
547
+ assert [
548
+ field_instance.field_def.name
549
+ for field_instance in decoded.field_instances
550
+ ] == [
551
+ "head",
552
+ "padding[0]",
553
+ "padding[1]",
554
+ "tail",
555
+ ]
556
+
557
+
558
+ def test_decode_splits_padding_with_custom_alignment_bits():
559
+ """Test that decode can split padding by a custom bit alignment."""
560
+ struct_def = StructDef(fields=[
561
+ FieldDef(name="head",
562
+ offset=InfoSize(0, 0),
563
+ size=InfoSize(1, 0),
564
+ type="unsigned int"),
565
+ FieldDef(name="tail",
566
+ offset=InfoSize(4, 0),
567
+ size=InfoSize(1, 0),
568
+ type="unsigned int"),
569
+ ])
570
+ data = bytearray(b"\x11\xaa\xbb\xcc\x22")
571
+
572
+ decoded = decode(struct_def, data, padding_alignment_bits=16)
573
+
574
+ assert [
575
+ field_instance.field_def.name
576
+ for field_instance in decoded.field_instances
577
+ ] == [
578
+ "head",
579
+ "padding[0]",
580
+ "padding[1]",
581
+ "tail",
582
+ ]
583
+ assert decoded.field_instances[1].field_def.offset == InfoSize(1, 0)
584
+ assert decoded.field_instances[1].field_def.size == InfoSize(1, 0)
585
+ assert decoded.field_instances[1].value == b"\xaa"
586
+ assert decoded.field_instances[2].field_def.offset == InfoSize(2, 0)
587
+ assert decoded.field_instances[2].field_def.size == InfoSize(2, 0)
588
+ assert decoded.field_instances[2].value == b"\xbb\xcc"
589
+
590
+
591
+ def test_decode_rejects_invalid_padding_alignment_bits():
592
+ """Test that decode validates padding alignment bit size."""
593
+ struct_def = StructDef(fields=[])
594
+
595
+ with pytest.raises(
596
+ ValueError,
597
+ match=r"padding_alignment_bits must be a positive power of two"):
598
+ decode(struct_def, bytearray(), padding_alignment_bits=24)
599
+
600
+
601
+ def test_encode_rejects_invalid_padding_alignment_bits():
602
+ """Test that encode validates padding alignment bit size."""
603
+ struct_instance = StructInstance(struct_def=StructDef(fields=[]))
604
+
605
+ with pytest.raises(
606
+ ValueError,
607
+ match=r"padding_alignment_bits must be a positive power of two"):
608
+ encode(struct_instance, bytearray(), padding_alignment_bits=0)
609
+
610
+
413
611
  def test_encode_rejects_tuple_input():
414
612
  """Test that encode only accepts StructInstance input."""
415
613
  field_def = FieldDef(name="value",
@@ -535,6 +733,33 @@ def test_field_instance_sort_uses_field_def_order():
535
733
  assert sorted_instances == [earlier, later]
536
734
 
537
735
 
736
+ def test_field_instance_range_check_evaluates_expression_with_field_env():
737
+ """Test that range_check evaluates expression with field name bound."""
738
+ field_def = FieldDef(name="value",
739
+ offset=InfoSize(0, 0),
740
+ size=InfoSize(1, 0),
741
+ type="unsigned int",
742
+ range_expression="0 <= value <= limit")
743
+ field_instance = FieldInstance(field_def=field_def, value=7)
744
+
745
+ assert field_instance.range_check({"limit": 10}) is True
746
+ assert field_instance.range_check({"limit": 5}) is False
747
+
748
+
749
+ def test_field_instance_range_check_returns_none_when_expression_is_none():
750
+ """Test that range_check returns None when range_expression is None."""
751
+ field_def = FieldDef(name="value",
752
+ offset=InfoSize(0, 0),
753
+ size=InfoSize(1, 0),
754
+ type="unsigned int",
755
+ range_expression=None)
756
+ field_instance = FieldInstance(field_def=field_def, value=7)
757
+
758
+ result = field_instance.range_check()
759
+
760
+ assert result is None
761
+
762
+
538
763
  def test_struct_instance_size_property_returns_initial_size():
539
764
  """Test that StructInstance size is initialized from the provided value."""
540
765
  struct_instance = StructInstance(size=InfoSize(5, 0),
@@ -74,7 +74,7 @@ wheels = [
74
74
 
75
75
  [[package]]
76
76
  name = "sltcodec"
77
- version = "0.2.0"
77
+ version = "0.4.0"
78
78
  source = { editable = "." }
79
79
  dependencies = [
80
80
  { name = "sltcalc" },
@@ -89,7 +89,7 @@ dev = [
89
89
  [package.metadata]
90
90
  requires-dist = [
91
91
  { name = "sltcalc", specifier = ">=0.1.0" },
92
- { name = "sltcore", specifier = ">=1.3.0" },
92
+ { name = "sltcore", specifier = ">=1.5.0" },
93
93
  ]
94
94
 
95
95
  [package.metadata.requires-dev]
@@ -97,9 +97,9 @@ dev = [{ name = "pytest", specifier = ">=9.1.1" }]
97
97
 
98
98
  [[package]]
99
99
  name = "sltcore"
100
- version = "1.3.0"
100
+ version = "1.5.0"
101
101
  source = { registry = "https://pypi.org/simple" }
102
- sdist = { url = "https://files.pythonhosted.org/packages/9a/fa/856dea8a444493506f1c73aa434d30c895fb793ef73f47ff777e938fc639/sltcore-1.3.0.tar.gz", hash = "sha256:01b77f2932a028a1a1d7239ac15aa8c7fa88973f4db71a851a4bcc399649fd15", size = 16916, upload-time = "2026-08-08T13:10:11.545Z" }
102
+ sdist = { url = "https://files.pythonhosted.org/packages/f0/8d/692c45c8d007b71b627fb4c53d90ecb5ea1168d892dc14e5ad3ecae0b8d3/sltcore-1.5.0.tar.gz", hash = "sha256:26280f743947f22e31e334fd793679f3aad19367bbf9bfddb6215123d7bd2d18", size = 18050, upload-time = "2026-08-11T13:14:51.241Z" }
103
103
  wheels = [
104
- { url = "https://files.pythonhosted.org/packages/7f/5f/15c2d6d78df481a62c60762c3824a9574f9bf610bc9d0463d3c8e5197e04/sltcore-1.3.0-py3-none-any.whl", hash = "sha256:85368bd7014879f4e67f98cdd6d489c29b3dc12f04e8964fcb701d75efa0c953", size = 9729, upload-time = "2026-08-08T13:10:10.5Z" },
104
+ { url = "https://files.pythonhosted.org/packages/0f/ca/38d1ab93a871ba9e60e500b93f8c48fdb2be3308a8ab08e6ea0a73e516c2/sltcore-1.5.0-py3-none-any.whl", hash = "sha256:148611c58329fcadc793b38a973412144c073a036516baf9430b3170e71180c4", size = 10458, upload-time = "2026-08-11T13:14:50.295Z" },
105
105
  ]
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