sltcodec 1.0.0__tar.gz → 1.1.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: 1.0.0
3
+ Version: 1.1.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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sltcodec"
3
- version = "1.0.0"
3
+ version = "1.1.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"
@@ -105,6 +105,18 @@ def _resolve_size(field_def: FieldDef, env: dict[str, Any]) -> InfoSize:
105
105
  return _resolve_info_size(field_def.size, env)
106
106
 
107
107
 
108
+ def _resolve_byte_swap(field_def: FieldDef, env: dict[str, Any]) -> bool:
109
+ """Resolve a byte-swap flag that can be static or expression-based."""
110
+ if isinstance(field_def.byte_swap, str):
111
+ return bool(SltEval(env).eval(field_def.byte_swap))
112
+ return field_def.byte_swap
113
+
114
+
115
+ def _byte_swap_info(info: Info, size: InfoSize) -> Info:
116
+ """Reverse the byte representation of an Info payload."""
117
+ return Info.from_bytes(info.to_bytes[::-1], size, scale=info.scale)
118
+
119
+
108
120
  def _is_padding_field_def(field_def: FieldDef) -> bool:
109
121
  """Check whether a field definition represents padding."""
110
122
  return (field_def.name.startswith("padding[")
@@ -235,6 +247,8 @@ def encode_field(
235
247
  return
236
248
 
237
249
  offset, size, info = prepared
250
+ if _resolve_byte_swap(field_def, env):
251
+ info = _byte_swap_info(info, size)
238
252
  required_bytes = (offset + size).bytes
239
253
  if len(buf) < required_bytes:
240
254
  buf.extend(b"\x00" * (required_bytes - len(buf)))
@@ -341,6 +355,9 @@ def decode_field(
341
355
  if size.byte == 0 and size.bit == 0:
342
356
  return None
343
357
  info = bits_get(data, offset, size, scale=field_def.scale)
358
+ byte_swap = _resolve_byte_swap(field_def, env)
359
+ if byte_swap:
360
+ info = _byte_swap_info(info, size)
344
361
  resolved_type = _resolve_field_type(field_def.type, struct_def_dict, env)
345
362
  resolved_field_def = FieldDef(
346
363
  name=field_def.name,
@@ -352,6 +369,7 @@ def decode_field(
352
369
  description=field_def.description,
353
370
  range_expression=field_def.range_expression,
354
371
  enum_def=field_def.enum_def,
372
+ byte_swap=byte_swap,
355
373
  )
356
374
  if isinstance(resolved_type, StructDef):
357
375
  return FieldInstance(
@@ -106,6 +106,9 @@ class FieldDef:
106
106
  default=None, metadata={"desc": "The value range expression"})
107
107
  enum_def: EnumDef | None = field(
108
108
  default=None, metadata={"desc": "The enum definition for the field"})
109
+ byte_swap: bool | str = field(
110
+ default=False,
111
+ metadata={"desc": "Whether to reverse the field byte order"})
109
112
 
110
113
  def split_repeat(self,
111
114
  index: int,
@@ -123,6 +126,8 @@ class FieldDef:
123
126
  split_name)
124
127
  split_range_expression = self._replace_name_in_expression(
125
128
  self.range_expression, self.name, split_name)
129
+ split_byte_swap = self._replace_name_in_expression(
130
+ self.byte_swap, self.name, split_name)
126
131
 
127
132
  return FieldDef(name=split_name,
128
133
  offset=split_offset,
@@ -132,7 +137,8 @@ class FieldDef:
132
137
  repeat=None,
133
138
  description=self.description,
134
139
  range_expression=split_range_expression,
135
- enum_def=self.enum_def)
140
+ enum_def=self.enum_def,
141
+ byte_swap=split_byte_swap)
136
142
 
137
143
  @staticmethod
138
144
  def _replace_name_in_expression(value: Any, old_name: str,
@@ -180,6 +186,8 @@ class FieldDef:
180
186
  self.range_expression,
181
187
  "enum_def":
182
188
  None if self.enum_def is None else self.enum_def.serialize(),
189
+ "byte_swap":
190
+ self.byte_swap,
183
191
  }
184
192
 
185
193
  def to_json(self) -> str:
@@ -198,6 +206,7 @@ class FieldDef:
198
206
  description = data.get("description")
199
207
  range_expression = data.get("range_expression")
200
208
  enum_def_data = data.get("enum_def")
209
+ byte_swap = data.get("byte_swap", False)
201
210
 
202
211
  def _deserialize_info_like(value: Any) -> Any:
203
212
  if isinstance(value, dict):
@@ -251,6 +260,7 @@ class FieldDef:
251
260
  description=description,
252
261
  range_expression=range_expression,
253
262
  enum_def=enum_def,
263
+ byte_swap=byte_swap,
254
264
  )
255
265
 
256
266
  @classmethod
@@ -270,6 +280,7 @@ class FieldDef:
270
280
  "" if self.description is None else self.description,
271
281
  "" if self.range_expression is None else self.range_expression,
272
282
  self._sortable_enum_type(self.enum_def),
283
+ self._sortable_byte_swap(self.byte_swap),
273
284
  )
274
285
 
275
286
  @staticmethod
@@ -299,6 +310,13 @@ class FieldDef:
299
310
  },
300
311
  sort_keys=True)
301
312
 
313
+ @staticmethod
314
+ def _sortable_byte_swap(value: bool | str) -> tuple[int, str]:
315
+ """Build a comparable key for a byte-swap flag or expression."""
316
+ if isinstance(value, bool):
317
+ return 0, str(value)
318
+ return 1, value
319
+
302
320
 
303
321
  @total_ordering
304
322
  @dataclass(frozen=True)
@@ -261,6 +261,77 @@ def test_field_def_to_json_from_json_round_trip():
261
261
  }
262
262
 
263
263
 
264
+ def test_field_def_byte_swap_json_round_trip():
265
+ """Test that expression-based byte_swap metadata is preserved."""
266
+ field_def = FieldDef(name="value",
267
+ offset=InfoSize(0, 0),
268
+ size=InfoSize(2, 0),
269
+ type="unsigned int",
270
+ byte_swap="kind == 1")
271
+
272
+ restored = FieldDef.from_json(field_def.to_json())
273
+
274
+ assert restored == field_def
275
+ assert restored.byte_swap == "kind == 1"
276
+
277
+
278
+ def test_byte_swap_reverses_bytearray_input_and_output():
279
+ """Test byte_swap reverses bytearray values symmetrically."""
280
+ field_def = FieldDef(name="payload",
281
+ offset=InfoSize(0, 0),
282
+ size=InfoSize(3, 0),
283
+ type="bytearray",
284
+ byte_swap=True)
285
+
286
+ encoded = encode(
287
+ StructInstance(struct_def=StructDef(fields=[field_def]),
288
+ field_instances=[
289
+ FieldInstance(field_def, bytearray(b"\x01\x02\x03"))
290
+ ]),
291
+ bytearray(),
292
+ )
293
+ decoded = decode([field_def], encoded)
294
+
295
+ assert encoded == bytearray(b"\x03\x02\x01")
296
+ assert decoded.field_instances == [
297
+ FieldInstance(field_def, bytearray(b"\x01\x02\x03"))
298
+ ]
299
+
300
+
301
+ def test_byte_swap_expression_uses_previous_field_value():
302
+ """Test byte_swap expressions use values decoded or encoded before it."""
303
+ kind_field = FieldDef(name="kind",
304
+ offset=InfoSize(0, 0),
305
+ size=InfoSize(1, 0),
306
+ type="unsigned int")
307
+ value_field = FieldDef(name="value",
308
+ offset=InfoSize(1, 0),
309
+ size=InfoSize(2, 0),
310
+ type="unsigned int",
311
+ byte_swap="kind == 1")
312
+
313
+ encoded = encode(
314
+ StructInstance(struct_def=StructDef(fields=[kind_field, value_field]),
315
+ field_instances=[
316
+ FieldInstance(kind_field, 1),
317
+ FieldInstance(value_field, 0x1234),
318
+ ]),
319
+ bytearray(),
320
+ )
321
+ decoded = decode([kind_field, value_field], encoded)
322
+
323
+ assert encoded == bytearray(b"\x01\x34\x12")
324
+ assert decoded.field_instances == [
325
+ FieldInstance(kind_field, 1),
326
+ FieldInstance(
327
+ FieldDef(name="value",
328
+ offset=InfoSize(1, 0),
329
+ size=InfoSize(2, 0),
330
+ type="unsigned int",
331
+ byte_swap=True), 0x1234),
332
+ ]
333
+
334
+
264
335
  def test_enum_def_to_json_from_json_round_trip():
265
336
  """Test EnumDef.to_json/from_json round-trip conversion."""
266
337
  enum_def = EnumDef(name="Status",
@@ -74,7 +74,7 @@ wheels = [
74
74
 
75
75
  [[package]]
76
76
  name = "sltcodec"
77
- version = "0.6.0"
77
+ version = "1.0.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
File without changes