sltcodec 0.6.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: 0.6.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 = "0.6.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"
@@ -1,5 +1,6 @@
1
1
  from .codec import (PRIMITIVE_TYPES, decode, encode, encode_field,
2
- load_struct_def_dict, save_struct_def_dict)
2
+ load_enum_def_dict, load_struct_def_dict,
3
+ save_enum_def_dict, save_struct_def_dict)
3
4
  from .types import EnumDef, FieldDef, FieldInstance, StructDef, StructInstance
4
5
 
5
6
  __all__ = [
@@ -12,6 +13,8 @@ __all__ = [
12
13
  "decode",
13
14
  "encode",
14
15
  "encode_field",
16
+ "load_enum_def_dict",
15
17
  "load_struct_def_dict",
18
+ "save_enum_def_dict",
16
19
  "save_struct_def_dict",
17
20
  ]
@@ -66,6 +66,26 @@ def load_struct_def_dict(path: str | Path) -> dict[str, StructDef]:
66
66
  }
67
67
 
68
68
 
69
+ def save_enum_def_dict(path: str | Path, enum_def_dict: dict[str,
70
+ EnumDef]) -> None:
71
+ """Save an enum definition dictionary to a JSON file."""
72
+ payload = {
73
+ name: enum_def.to_dict()
74
+ for name, enum_def in enum_def_dict.items()
75
+ }
76
+ Path(path).write_text(json.dumps(payload, ensure_ascii=False, indent=2),
77
+ encoding="utf-8")
78
+
79
+
80
+ def load_enum_def_dict(path: str | Path) -> dict[str, EnumDef]:
81
+ """Load an enum definition dictionary from a JSON file."""
82
+ data = json.loads(Path(path).read_text(encoding="utf-8"))
83
+ return {
84
+ name: EnumDef.from_dict(enum_def_data)
85
+ for name, enum_def_data in data.items()
86
+ }
87
+
88
+
69
89
  def _resolve_info_size(value: InfoSize | str, env: dict[str, Any]) -> InfoSize:
70
90
  """Resolve an InfoSize value that can be static or expression-based."""
71
91
  if isinstance(value, str):
@@ -85,6 +105,18 @@ def _resolve_size(field_def: FieldDef, env: dict[str, Any]) -> InfoSize:
85
105
  return _resolve_info_size(field_def.size, env)
86
106
 
87
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
+
88
120
  def _is_padding_field_def(field_def: FieldDef) -> bool:
89
121
  """Check whether a field definition represents padding."""
90
122
  return (field_def.name.startswith("padding[")
@@ -215,6 +247,8 @@ def encode_field(
215
247
  return
216
248
 
217
249
  offset, size, info = prepared
250
+ if _resolve_byte_swap(field_def, env):
251
+ info = _byte_swap_info(info, size)
218
252
  required_bytes = (offset + size).bytes
219
253
  if len(buf) < required_bytes:
220
254
  buf.extend(b"\x00" * (required_bytes - len(buf)))
@@ -321,6 +355,9 @@ def decode_field(
321
355
  if size.byte == 0 and size.bit == 0:
322
356
  return None
323
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)
324
361
  resolved_type = _resolve_field_type(field_def.type, struct_def_dict, env)
325
362
  resolved_field_def = FieldDef(
326
363
  name=field_def.name,
@@ -332,6 +369,7 @@ def decode_field(
332
369
  description=field_def.description,
333
370
  range_expression=field_def.range_expression,
334
371
  enum_def=field_def.enum_def,
372
+ byte_swap=byte_swap,
335
373
  )
336
374
  if isinstance(resolved_type, StructDef):
337
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)
@@ -7,7 +7,8 @@ from sltcore import InfoSize
7
7
 
8
8
  from sltcodec import (PRIMITIVE_TYPES, EnumDef, FieldDef, FieldInstance,
9
9
  StructDef, StructInstance, decode, encode,
10
- load_struct_def_dict, save_struct_def_dict)
10
+ load_enum_def_dict, load_struct_def_dict,
11
+ save_enum_def_dict, save_struct_def_dict)
11
12
  from sltcodec.codec import decode_field
12
13
 
13
14
 
@@ -260,6 +261,77 @@ def test_field_def_to_json_from_json_round_trip():
260
261
  }
261
262
 
262
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
+
263
335
  def test_enum_def_to_json_from_json_round_trip():
264
336
  """Test EnumDef.to_json/from_json round-trip conversion."""
265
337
  enum_def = EnumDef(name="Status",
@@ -413,6 +485,22 @@ def test_struct_def_dict_load_and_save(tmp_path: Path):
413
485
  assert loaded["value"] == struct_def
414
486
 
415
487
 
488
+ def test_enum_def_dict_load_and_save(tmp_path: Path):
489
+ """Test that an EnumDef dictionary can be saved and reloaded."""
490
+ enum_def = EnumDef(name="Status",
491
+ description="Status enum",
492
+ values={
493
+ "OK": 1,
494
+ "NG": 2
495
+ })
496
+ path = tmp_path / "enum_defs.json"
497
+
498
+ save_enum_def_dict(path, {"status": enum_def})
499
+ loaded = load_enum_def_dict(path)
500
+
501
+ assert loaded["status"] == enum_def
502
+
503
+
416
504
  def test_struct_def_to_json_from_json_round_trip():
417
505
  """Test StructDef.to_json/from_json round-trip conversion."""
418
506
  struct_def = StructDef(name="ValueStruct",
@@ -74,7 +74,7 @@ wheels = [
74
74
 
75
75
  [[package]]
76
76
  name = "sltcodec"
77
- version = "0.4.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