shepherd-core 2025.6.3__py3-none-any.whl → 2025.8.1__py3-none-any.whl

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.
Files changed (50) hide show
  1. shepherd_core/data_models/base/cal_measurement.py +4 -5
  2. shepherd_core/data_models/base/calibration.py +8 -10
  3. shepherd_core/data_models/base/content.py +2 -3
  4. shepherd_core/data_models/base/shepherd.py +6 -8
  5. shepherd_core/data_models/base/wrapper.py +3 -4
  6. shepherd_core/data_models/content/energy_environment.py +4 -5
  7. shepherd_core/data_models/content/firmware.py +3 -5
  8. shepherd_core/data_models/content/virtual_harvester.py +5 -6
  9. shepherd_core/data_models/experiment/experiment.py +9 -17
  10. shepherd_core/data_models/experiment/observer_features.py +22 -38
  11. shepherd_core/data_models/experiment/target_config.py +10 -11
  12. shepherd_core/data_models/task/__init__.py +1 -3
  13. shepherd_core/data_models/task/emulation.py +18 -19
  14. shepherd_core/data_models/task/firmware_mod.py +3 -4
  15. shepherd_core/data_models/task/harvest.py +7 -10
  16. shepherd_core/data_models/task/observer_tasks.py +12 -10
  17. shepherd_core/data_models/task/programming.py +2 -2
  18. shepherd_core/data_models/task/testbed_tasks.py +8 -10
  19. shepherd_core/data_models/testbed/cape.py +3 -5
  20. shepherd_core/data_models/testbed/gpio.py +7 -8
  21. shepherd_core/data_models/testbed/mcu.py +1 -2
  22. shepherd_core/data_models/testbed/observer.py +5 -6
  23. shepherd_core/data_models/testbed/target.py +4 -6
  24. shepherd_core/data_models/testbed/testbed.py +2 -3
  25. shepherd_core/decoder_waveform/uart.py +12 -13
  26. shepherd_core/fw_tools/converter.py +1 -2
  27. shepherd_core/fw_tools/converter_elf.py +1 -2
  28. shepherd_core/fw_tools/patcher.py +65 -40
  29. shepherd_core/fw_tools/validation.py +7 -1
  30. shepherd_core/inventory/python.py +8 -9
  31. shepherd_core/inventory/system.py +1 -2
  32. shepherd_core/inventory/target.py +1 -2
  33. shepherd_core/logger.py +1 -2
  34. shepherd_core/reader.py +18 -23
  35. shepherd_core/testbed_client/client_abc_fix.py +2 -7
  36. shepherd_core/testbed_client/client_web.py +5 -9
  37. shepherd_core/testbed_client/fixtures.py +3 -5
  38. shepherd_core/testbed_client/user_model.py +4 -5
  39. shepherd_core/version.py +1 -1
  40. shepherd_core/vsource/virtual_converter_model.py +1 -2
  41. shepherd_core/vsource/virtual_harvester_simulation.py +1 -2
  42. shepherd_core/vsource/virtual_source_model.py +3 -5
  43. shepherd_core/vsource/virtual_source_simulation.py +2 -3
  44. shepherd_core/writer.py +12 -14
  45. {shepherd_core-2025.6.3.dist-info → shepherd_core-2025.8.1.dist-info}/METADATA +4 -8
  46. shepherd_core-2025.8.1.dist-info/RECORD +83 -0
  47. shepherd_core-2025.6.3.dist-info/RECORD +0 -83
  48. {shepherd_core-2025.6.3.dist-info → shepherd_core-2025.8.1.dist-info}/WHEEL +0 -0
  49. {shepherd_core-2025.6.3.dist-info → shepherd_core-2025.8.1.dist-info}/top_level.txt +0 -0
  50. {shepherd_core-2025.6.3.dist-info → shepherd_core-2025.8.1.dist-info}/zip-safe +0 -0
@@ -4,9 +4,7 @@ from collections.abc import Set as AbstractSet
4
4
  from pathlib import Path
5
5
  from pathlib import PurePosixPath
6
6
  from typing import Annotated
7
- from typing import Optional
8
7
  from typing import TypedDict
9
- from typing import Union
10
8
 
11
9
  from pydantic import Field
12
10
  from pydantic import model_validator
@@ -31,9 +29,9 @@ from .helper_paths import path_posix
31
29
  class FirmwareModTask(ShpModel):
32
30
  """Config for Task that adds the custom ID to the firmware & stores it into a file."""
33
31
 
34
- data: Union[FirmwareStr, Path]
32
+ data: FirmwareStr | Path
35
33
  data_type: FirmwareDType
36
- custom_id: Optional[IdInt16] = None
34
+ custom_id: IdInt16 | None = None
37
35
  firmware_file: Path
38
36
 
39
37
  verbose: Annotated[int, Field(ge=0, le=4)] = 2
@@ -95,6 +93,7 @@ class FirmwareModTask(ShpModel):
95
93
  return cls(**kwargs)
96
94
 
97
95
  def is_contained(self, paths: AbstractSet[PurePosixPath]) -> bool:
96
+ """Limit paths to allowed directories."""
98
97
  all_ok = any(self.firmware_file.is_relative_to(path) for path in paths)
99
98
  if isinstance(self.data, Path):
100
99
  all_ok = any(self.data.is_relative_to(path) for path in paths)
@@ -6,12 +6,10 @@ from datetime import timedelta
6
6
  from pathlib import Path
7
7
  from pathlib import PurePosixPath
8
8
  from typing import Annotated
9
- from typing import Optional
10
9
 
11
10
  from pydantic import Field
12
11
  from pydantic import model_validator
13
12
  from typing_extensions import Self
14
- from typing_extensions import deprecated
15
13
 
16
14
  from shepherd_core.data_models.base.shepherd import ShpModel
17
15
  from shepherd_core.data_models.base.timezone import local_tz
@@ -35,14 +33,13 @@ class HarvestTask(ShpModel):
35
33
  """
36
34
  force_overwrite: bool = False
37
35
  """ ⤷ Overwrite existing file"""
38
- output_compression: Optional[Compression] = Compression.default
36
+ output_compression: Compression | None = Compression.default
39
37
  """ ⤷ should be 1 (level 1 gzip), lzf, or None (order of recommendation)"""
40
38
 
41
- time_start: Optional[datetime] = None
39
+ time_start: datetime | None = None
42
40
  """ timestamp or unix epoch time, None = ASAP"""
43
- duration: Optional[timedelta] = None
41
+ duration: timedelta | None = None
44
42
  """ ⤷ Duration of recording in seconds, None = till EOFSys"""
45
- abort_on_error: Annotated[bool, deprecated("has no effect")] = False
46
43
 
47
44
  # emulation-specific
48
45
  use_cal_default: bool = False
@@ -52,7 +49,7 @@ class HarvestTask(ShpModel):
52
49
  """ ⤷ Choose one of the predefined virtual harvesters or configure a new one
53
50
  """
54
51
  power_tracing: PowerTracing = PowerTracing()
55
- sys_logging: Optional[SystemLogging] = SystemLogging()
52
+ sys_logging: SystemLogging | None = SystemLogging()
56
53
 
57
54
  verbose: Annotated[int, Field(ge=0, le=4)] = 2
58
55
  """ ⤷ 0=Errors, 1=Warnings, 2=Info, 3=Debug"""
@@ -75,12 +72,11 @@ class HarvestTask(ShpModel):
75
72
  @model_validator(mode="after")
76
73
  def post_validation(self) -> Self:
77
74
  # TODO: limit paths
78
- has_time = self.time_start is not None
75
+ has_time = False # TODO: deactivated, self.time_start is not None
79
76
  time_now = datetime.now().astimezone()
80
77
  if has_time and self.time_start < time_now:
81
78
  msg = (
82
- "Start-Time for Emulation can't be in the past "
83
- f"('{self.time_start}' vs '{time_now}'."
79
+ f"Start-Time for Harvest can't be in the past ('{self.time_start}' vs '{time_now}'."
84
80
  )
85
81
  raise ValueError(msg)
86
82
  if self.duration and self.duration.total_seconds() < 0:
@@ -88,4 +84,5 @@ class HarvestTask(ShpModel):
88
84
  return self
89
85
 
90
86
  def is_contained(self, paths: AbstractSet[PurePosixPath]) -> bool:
87
+ """Limit paths to allowed directories."""
91
88
  return any(self.output_path.is_relative_to(path) for path in paths)
@@ -5,7 +5,6 @@ from datetime import datetime
5
5
  from pathlib import Path
6
6
  from pathlib import PurePosixPath
7
7
  from typing import Annotated
8
- from typing import Optional
9
8
 
10
9
  from pydantic import validate_call
11
10
  from typing_extensions import Self
@@ -29,20 +28,20 @@ class ObserverTasks(ShpModel):
29
28
  observer: NameStr
30
29
 
31
30
  # PRE PROCESS
32
- time_prep: Optional[datetime] = None # TODO: currently not used
31
+ time_prep: datetime | None = None # TODO: currently not used
33
32
  root_path: Path
34
33
 
35
34
  # fw mod, store as hex-file and program
36
- fw1_mod: Optional[FirmwareModTask] = None
37
- fw2_mod: Optional[FirmwareModTask] = None
38
- fw1_prog: Optional[ProgrammingTask] = None
39
- fw2_prog: Optional[ProgrammingTask] = None
35
+ fw1_mod: FirmwareModTask | None = None
36
+ fw2_mod: FirmwareModTask | None = None
37
+ fw1_prog: ProgrammingTask | None = None
38
+ fw2_prog: ProgrammingTask | None = None
40
39
 
41
40
  # MAIN PROCESS
42
- emulation: Optional[EmulationTask] = None
41
+ emulation: EmulationTask | None = None
43
42
 
44
43
  # deprecations, TODO: remove before public release
45
- owner_id: Annotated[Optional[IdInt], deprecated("not needed anymore")] = None
44
+ owner_id: Annotated[IdInt | None, deprecated("not needed anymore")] = None
46
45
  abort_on_error: Annotated[bool, deprecated("has no effect")] = False
47
46
 
48
47
  # post_copy / cleanup, Todo: could also just intake emuTask
@@ -53,12 +52,14 @@ class ObserverTasks(ShpModel):
53
52
 
54
53
  @classmethod
55
54
  @validate_call
56
- def from_xp(cls, xp: Experiment, tb: Testbed, tgt_id: IdInt) -> Self:
55
+ def from_xp(cls, xp: Experiment, xp_folder: str | None, tb: Testbed, tgt_id: IdInt) -> Self:
57
56
  if not tb.shared_storage:
58
57
  raise ValueError("Implementation currently relies on shared storage!")
59
58
 
60
59
  obs = tb.get_observer(tgt_id)
61
- root_path = tb.data_on_observer / "experiments" / xp.folder_name()
60
+ if xp_folder is None:
61
+ xp_folder = xp.folder_name() # moved a layer up for consistent naming
62
+ root_path = tb.data_on_observer / "experiments" / xp_folder
62
63
  fw_paths = [root_path / f"fw{_i}_{obs.name}.hex" for _i in [1, 2]]
63
64
 
64
65
  return cls(
@@ -94,6 +95,7 @@ class ObserverTasks(ShpModel):
94
95
  return values
95
96
 
96
97
  def is_contained(self, paths: AbstractSet[PurePosixPath]) -> bool:
98
+ """Limit paths to allowed directories."""
97
99
  all_ok = any(self.root_path.is_relative_to(path) for path in paths)
98
100
  all_ok &= self.fw1_mod.is_contained(paths)
99
101
  all_ok &= self.fw2_mod.is_contained(paths)
@@ -4,7 +4,6 @@ from collections.abc import Set as AbstractSet
4
4
  from pathlib import Path
5
5
  from pathlib import PurePosixPath
6
6
  from typing import Annotated
7
- from typing import Optional
8
7
 
9
8
  from pydantic import Field
10
9
  from pydantic import model_validator
@@ -60,7 +59,7 @@ class ProgrammingTask(ShpModel):
60
59
  tgt_id: IdInt,
61
60
  mcu_port: MCUPort,
62
61
  fw_path: Path,
63
- ) -> Optional[Self]:
62
+ ) -> Self | None:
64
63
  obs = tb.get_observer(tgt_id)
65
64
  tgt_cfg = xp.get_target_config(tgt_id)
66
65
 
@@ -79,4 +78,5 @@ class ProgrammingTask(ShpModel):
79
78
  )
80
79
 
81
80
  def is_contained(self, paths: AbstractSet[PurePosixPath]) -> bool:
81
+ """Limit paths to allowed directories."""
82
82
  return any(self.firmware_file.is_relative_to(path) for path in paths)
@@ -4,14 +4,11 @@ from pathlib import Path
4
4
  from pathlib import PurePosixPath
5
5
  from typing import TYPE_CHECKING
6
6
  from typing import Annotated
7
- from typing import Optional
8
7
 
9
8
  from pydantic import Field
10
9
  from pydantic import validate_call
11
10
  from typing_extensions import Self
12
- from typing_extensions import deprecated
13
11
 
14
- from shepherd_core.data_models.base.content import IdInt
15
12
  from shepherd_core.data_models.base.content import NameStr
16
13
  from shepherd_core.data_models.base.shepherd import ShpModel
17
14
  from shepherd_core.data_models.experiment.experiment import Experiment
@@ -29,30 +26,30 @@ class TestbedTasks(ShpModel):
29
26
  name: NameStr
30
27
  observer_tasks: Annotated[list[ObserverTasks], Field(min_length=1, max_length=128)]
31
28
 
32
- # deprecated, TODO: remove before public release
33
- email_results: Annotated[Optional[bool], deprecated("not needed anymore")] = False
34
- owner_id: Annotated[Optional[IdInt], deprecated("not needed anymore")] = None
35
-
36
29
  @classmethod
37
30
  @validate_call
38
- def from_xp(cls, xp: Experiment, tb: Optional[Testbed] = None) -> Self:
31
+ def from_xp(cls, xp: Experiment, tb: Testbed | None = None) -> Self:
39
32
  if tb is None:
40
33
  # TODO: is tb-argument really needed? prob. not
41
34
  tb = Testbed() # this will query the first (and only) entry of client
42
35
 
43
36
  tgt_ids = xp.get_target_ids()
44
- obs_tasks = [ObserverTasks.from_xp(xp, tb, _id) for _id in tgt_ids]
37
+ xp_folder = xp.folder_name()
38
+ obs_tasks = [ObserverTasks.from_xp(xp, xp_folder, tb, _id) for _id in tgt_ids]
45
39
  return cls(
46
40
  name=xp.name,
47
41
  observer_tasks=obs_tasks,
48
42
  )
49
43
 
50
- def get_observer_tasks(self, observer: str) -> Optional[ObserverTasks]:
44
+ def get_observer_tasks(self, observer: str) -> ObserverTasks | None:
51
45
  for tasks in self.observer_tasks:
52
46
  if observer == tasks.observer:
53
47
  return tasks
54
48
  return None
55
49
 
50
+ def get_observers(self) -> set[str]:
51
+ return {tasks.observer for tasks in self.observer_tasks}
52
+
56
53
  def get_output_paths(self) -> dict[str, Path]:
57
54
  # TODO: computed field preferred, but they don't work here, as
58
55
  # - they are always stored in yaml despite "repr=False"
@@ -63,6 +60,7 @@ class TestbedTasks(ShpModel):
63
60
  return values
64
61
 
65
62
  def is_contained(self) -> bool:
63
+ """Limit paths to allowed directories."""
66
64
  paths_allowed: AbstractSet[PurePosixPath] = {
67
65
  PurePosixPath("/var/shepherd/"),
68
66
  PurePosixPath("/tmp/"), # noqa: S108
@@ -4,8 +4,6 @@ from datetime import date
4
4
  from datetime import datetime
5
5
  from enum import Enum
6
6
  from typing import Any
7
- from typing import Optional
8
- from typing import Union
9
7
 
10
8
  from pydantic import Field
11
9
  from pydantic import model_validator
@@ -31,12 +29,12 @@ class Cape(ShpModel, title="Shepherd-Cape"):
31
29
  name: NameStr
32
30
  version: NameStr
33
31
  description: SafeStr
34
- comment: Optional[SafeStr] = None
32
+ comment: SafeStr | None = None
35
33
  # TODO: wake_interval, calibration
36
34
 
37
35
  active: bool = True
38
- created: Union[date, datetime] = Field(default_factory=datetime.now)
39
- calibrated: Union[date, datetime, None] = None
36
+ created: date | datetime = Field(default_factory=datetime.now)
37
+ calibrated: date | datetime | None = None
40
38
 
41
39
  def __str__(self) -> str:
42
40
  return self.name
@@ -3,7 +3,6 @@
3
3
  from enum import Enum
4
4
  from typing import Annotated
5
5
  from typing import Any
6
- from typing import Optional
7
6
 
8
7
  from pydantic import Field
9
8
  from pydantic import StringConstraints
@@ -30,16 +29,16 @@ class GPIO(ShpModel, title="GPIO of Observer Node"):
30
29
 
31
30
  id: IdInt
32
31
  name: NameStr
33
- description: Optional[SafeStr] = None
34
- comment: Optional[SafeStr] = None
32
+ description: SafeStr | None = None
33
+ comment: SafeStr | None = None
35
34
 
36
35
  direction: Direction = Direction.Input
37
- dir_switch: Optional[Annotated[str, StringConstraints(max_length=32)]] = None
36
+ dir_switch: Annotated[str, StringConstraints(max_length=32)] | None = None
38
37
 
39
- reg_pru: Optional[Annotated[str, StringConstraints(max_length=10)]] = None
40
- pin_pru: Optional[Annotated[str, StringConstraints(max_length=10)]] = None
41
- reg_sys: Optional[Annotated[int, Field(ge=0)]] = None
42
- pin_sys: Optional[Annotated[str, StringConstraints(max_length=10)]] = None
38
+ reg_pru: Annotated[str, StringConstraints(max_length=10)] | None = None
39
+ pin_pru: Annotated[str, StringConstraints(max_length=10)] | None = None
40
+ reg_sys: Annotated[int, Field(ge=0)] | None = None
41
+ pin_sys: Annotated[str, StringConstraints(max_length=10)] | None = None
43
42
 
44
43
  def __str__(self) -> str:
45
44
  return self.name
@@ -3,7 +3,6 @@
3
3
  from enum import Enum
4
4
  from typing import Annotated
5
5
  from typing import Any
6
- from typing import Optional
7
6
 
8
7
  from pydantic import Field
9
8
  from pydantic import model_validator
@@ -30,7 +29,7 @@ class MCU(ShpModel, title="Microcontroller of the Target Node"):
30
29
  id: IdInt
31
30
  name: NameStr
32
31
  description: SafeStr
33
- comment: Optional[SafeStr] = None
32
+ comment: SafeStr | None = None
34
33
 
35
34
  platform: NameStr
36
35
  core: NameStr
@@ -3,7 +3,6 @@
3
3
  from datetime import datetime
4
4
  from typing import Annotated
5
5
  from typing import Any
6
- from typing import Optional
7
6
 
8
7
  from pydantic import Field
9
8
  from pydantic import IPvAnyAddress
@@ -33,7 +32,7 @@ class Observer(ShpModel, title="Shepherd-Sheep"):
33
32
  id: IdInt
34
33
  name: NameStr
35
34
  description: SafeStr
36
- comment: Optional[SafeStr] = None
35
+ comment: SafeStr | None = None
37
36
 
38
37
  ip: IPvAnyAddress
39
38
  mac: MACStr
@@ -46,12 +45,12 @@ class Observer(ShpModel, title="Shepherd-Sheep"):
46
45
  """ ⤷ cfaed-floor"""
47
46
 
48
47
  active: bool = True
49
- cape: Optional[Cape] = None
50
- target_a: Optional[Target] = None
51
- target_b: Optional[Target] = None
48
+ cape: Cape | None = None
49
+ target_a: Target | None = None
50
+ target_b: Target | None = None
52
51
 
53
52
  created: datetime = Field(default_factory=datetime.now)
54
- alive_last: Optional[datetime] = None
53
+ alive_last: datetime | None = None
55
54
 
56
55
  def __str__(self) -> str:
57
56
  return self.name
@@ -3,8 +3,6 @@
3
3
  from datetime import datetime
4
4
  from typing import Annotated
5
5
  from typing import Any
6
- from typing import Optional
7
- from typing import Union
8
6
 
9
7
  from pydantic import Field
10
8
  from pydantic import model_validator
@@ -30,15 +28,15 @@ class Target(ShpModel, title="Target Node (DuT)"):
30
28
  version: NameStr
31
29
  description: SafeStr
32
30
 
33
- comment: Optional[SafeStr] = None
31
+ comment: SafeStr | None = None
34
32
 
35
33
  active: bool = True
36
34
  created: datetime = Field(default_factory=datetime.now)
37
35
 
38
- testbed_id: Optional[IdInt16] = None
36
+ testbed_id: IdInt16 | None = None
39
37
  """ ⤷ is derived from ID (targets are still selected by id!)"""
40
- mcu1: Union[MCU, NameStr]
41
- mcu2: Union[MCU, NameStr, None] = None
38
+ mcu1: MCU | NameStr
39
+ mcu2: MCU | NameStr | None = None
42
40
 
43
41
  # TODO: programming pins per mcu should be here (or better in Cape)
44
42
 
@@ -4,7 +4,6 @@ from datetime import timedelta
4
4
  from pathlib import Path
5
5
  from typing import Annotated
6
6
  from typing import Any
7
- from typing import Optional
8
7
 
9
8
  from pydantic import Field
10
9
  from pydantic import HttpUrl
@@ -29,9 +28,9 @@ class Testbed(ShpModel):
29
28
  id: IdInt
30
29
  name: NameStr
31
30
  description: SafeStr
32
- comment: Optional[SafeStr] = None
31
+ comment: SafeStr | None = None
33
32
 
34
- url: Optional[HttpUrl] = None
33
+ url: HttpUrl | None = None
35
34
 
36
35
  observers: Annotated[list[Observer], Field(min_length=1, max_length=128)]
37
36
 
@@ -23,8 +23,6 @@ https://sigrok.org/wiki/Protocol_decoder:Uart
23
23
 
24
24
  from enum import Enum
25
25
  from pathlib import Path
26
- from typing import Optional
27
- from typing import Union
28
26
 
29
27
  import numpy as np
30
28
 
@@ -51,12 +49,13 @@ class Uart:
51
49
 
52
50
  def __init__(
53
51
  self,
54
- content: Union[Path, np.ndarray],
55
- baud_rate: Optional[int] = None,
56
- frame_length: Optional[int] = 8,
57
- inversion: Optional[bool] = None,
58
- parity: Optional[Parity] = Parity.no,
59
- bit_order: Optional[BitOrder] = BitOrder.lsb_first,
52
+ content: Path | np.ndarray,
53
+ *,
54
+ baud_rate: int | None = None,
55
+ frame_length: int | None = 8,
56
+ inversion: bool | None = None,
57
+ parity: Parity | None = Parity.no,
58
+ bit_order: BitOrder | None = BitOrder.lsb_first,
60
59
  ) -> None:
61
60
  """Provide a file with two columns: TS & Signal.
62
61
 
@@ -115,9 +114,9 @@ class Uart:
115
114
  log.error("Signal still inverted?!? Check parameters and input")
116
115
 
117
116
  # results
118
- self.events_symbols: Optional[np.ndarray] = None
119
- self.events_lines: Optional[np.ndarray] = None
120
- self.text: Optional[str] = None
117
+ self.events_symbols: np.ndarray | None = None
118
+ self.events_lines: np.ndarray | None = None
119
+ self.text: str | None = None
121
120
 
122
121
  def _convert_analog2digital(self, *, invert: bool = False) -> None:
123
122
  """Divide dimension in two, divided by mean-value."""
@@ -208,9 +207,9 @@ class Uart:
208
207
  if self.events_symbols is not None:
209
208
  return self.events_symbols
210
209
 
211
- pos_df: Optional[int] = None
210
+ pos_df: int | None = None
212
211
  symbol: int = 0
213
- t_start: Optional[float] = None
212
+ t_start: float | None = None
214
213
  content: list = []
215
214
 
216
215
  for time, value, steps in self.events_sig:
@@ -4,7 +4,6 @@ import base64
4
4
  import hashlib
5
5
  import shutil
6
6
  from pathlib import Path
7
- from typing import Union
8
7
 
9
8
  import zstandard as zstd
10
9
  from pydantic import validate_call
@@ -81,7 +80,7 @@ def base64_to_hash(content: str) -> str:
81
80
 
82
81
 
83
82
  @validate_call
84
- def extract_firmware(data: Union[str, Path], data_type: FirmwareDType, file_path: Path) -> Path:
83
+ def extract_firmware(data: str | Path, data_type: FirmwareDType, file_path: Path) -> Path:
85
84
  """Make embedded firmware-data usable in filesystem.
86
85
 
87
86
  - base64-string will be transformed to file
@@ -2,7 +2,6 @@
2
2
 
3
3
  import subprocess
4
4
  from pathlib import Path
5
- from typing import Optional
6
5
 
7
6
  from pydantic import validate_call
8
7
 
@@ -10,7 +9,7 @@ from pydantic import validate_call
10
9
 
11
10
 
12
11
  @validate_call
13
- def elf_to_hex(file_elf: Path, file_hex: Optional[Path] = None) -> Path:
12
+ def elf_to_hex(file_elf: Path, file_hex: Path | None = None) -> Path:
14
13
  """Convert ELF to hex file using objcopy."""
15
14
  if not file_elf.is_file():
16
15
  raise ValueError("Fn needs an existing file as input")
@@ -1,8 +1,9 @@
1
1
  """Read and modify symbols in ELF-files."""
2
2
 
3
+ import shutil
3
4
  from pathlib import Path
5
+ from tempfile import TemporaryDirectory
4
6
  from typing import Annotated
5
- from typing import Optional
6
7
 
7
8
  from pydantic import Field
8
9
  from pydantic import validate_call
@@ -29,27 +30,32 @@ def find_symbol(file_elf: Path, symbol: str) -> bool:
29
30
  return False
30
31
  if ELF is None:
31
32
  raise RuntimeError(elf_error_text)
32
- elf = ELF(path=file_elf)
33
- try:
34
- addr = elf.symbols[symbol]
35
- except KeyError:
36
- addr = None
37
- if addr is None:
38
- log.debug("Symbol '%s' not found in ELF-File %s", symbol, file_elf.name)
39
- return False
40
- log.debug(
41
- "Symbol '%s' found in ELF-File %s, arch=%s, order=%s",
42
- symbol,
43
- file_elf.name,
44
- elf.arch,
45
- elf.endian,
46
- )
47
- elf.close()
33
+ with TemporaryDirectory(ignore_cleanup_errors=True) as tmp:
34
+ # switcheroo that might prevent windows bug - overwrite fails in modify_symbol_value()
35
+ file_tmp = Path(tmp) / file_elf.name
36
+ shutil.copy(file_elf, file_tmp)
37
+ elf = ELF(path=file_tmp)
38
+ try:
39
+ addr = elf.symbols[symbol]
40
+ except KeyError:
41
+ addr = None
42
+ if addr is None:
43
+ elf.close() # better be safe
44
+ log.debug("Symbol '%s' not found in ELF-File %s", symbol, file_elf.name)
45
+ return False
46
+ log.debug(
47
+ "Symbol '%s' found in ELF-File %s, arch=%s, order=%s",
48
+ symbol,
49
+ file_elf.name,
50
+ elf.arch,
51
+ elf.endian,
52
+ )
53
+ elf.close()
48
54
  return True
49
55
 
50
56
 
51
57
  @validate_call
52
- def read_symbol(file_elf: Path, symbol: str, length: int) -> Optional[int]:
58
+ def read_symbol(file_elf: Path, symbol: str, length: int) -> int | None:
53
59
  """Read value of symbol in ELF-File.
54
60
 
55
61
  Will be interpreted as int.
@@ -61,24 +67,28 @@ def read_symbol(file_elf: Path, symbol: str, length: int) -> Optional[int]:
61
67
  elf = ELF(path=file_elf)
62
68
  addr = elf.symbols[symbol]
63
69
  value_raw = elf.read(address=addr, count=length)[-length:]
70
+ endian = elf.endian
64
71
  elf.close()
65
- return int.from_bytes(bytes=value_raw, byteorder=elf.endian, signed=False)
72
+ return int.from_bytes(bytes=value_raw, byteorder=endian, signed=False)
66
73
 
67
74
 
68
- def read_uid(file_elf: Path) -> Optional[int]:
75
+ def read_uid(file_elf: Path) -> int | None:
69
76
  """Read value of UID-symbol for shepherd testbed."""
70
77
  return read_symbol(file_elf, symbol=config.UID_NAME, length=config.UID_SIZE)
71
78
 
72
79
 
73
- def read_arch(file_elf: Path) -> Optional[str]:
80
+ def read_arch(file_elf: Path) -> str | None:
74
81
  """Determine chip-architecture from elf-metadata."""
75
82
  if not is_elf(file_elf):
76
83
  return None
77
84
  if ELF is None:
78
85
  raise RuntimeError(elf_error_text)
79
86
  elf = ELF(path=file_elf)
80
- if "exec" in elf.elftype.lower():
81
- return elf.arch.lower()
87
+ elf_type = elf.elftype.lower()
88
+ elf_arch = elf.arch.lower()
89
+ elf.close()
90
+ if "exec" in elf_type:
91
+ return elf_arch
82
92
  log.error("ELF is not Executable")
83
93
  return None
84
94
 
@@ -90,7 +100,7 @@ def modify_symbol_value(
90
100
  value: Annotated[int, Field(ge=0, lt=2 ** (8 * config.UID_SIZE))],
91
101
  *,
92
102
  overwrite: bool = False,
93
- ) -> Optional[Path]:
103
+ ) -> Path | None:
94
104
  """Replace value of uint16-symbol in ELF-File.
95
105
 
96
106
  Hardcoded for uint16_t (2 byte).
@@ -103,22 +113,37 @@ def modify_symbol_value(
103
113
  return None
104
114
  if ELF is None:
105
115
  raise RuntimeError(elf_error_text)
106
- elf = ELF(path=file_elf)
107
- addr = elf.symbols[symbol]
108
- value_raw = elf.read(address=addr, count=config.UID_SIZE)[-config.UID_SIZE :]
109
- # ⤷ cutting needed -> msp produces 4b instead of 2
110
- value_old = int.from_bytes(bytes=value_raw, byteorder=elf.endian, signed=False)
111
- value_raw = value.to_bytes(length=config.UID_SIZE, byteorder=elf.endian, signed=False)
112
-
113
- try:
114
- elf.write(address=addr, data=value_raw)
115
- except AttributeError:
116
- log.warning("ELF-Modifier failed @%s for symbol '%s'", f"0x{addr:X}", symbol)
117
- return None
116
+ with TemporaryDirectory(ignore_cleanup_errors=True) as tmp:
117
+ # switcheroo that also prevents windows bug (overwrite fails)
118
+ file_tmp = Path(tmp) / file_elf.name
119
+ shutil.copy(file_elf, file_tmp)
120
+
121
+ elf = ELF(path=file_elf)
122
+ addr = elf.symbols[symbol]
123
+ value_raw = elf.read(address=addr, count=config.UID_SIZE)[-config.UID_SIZE :]
124
+ # ⤷ cutting needed -> msp produces 4b instead of 2
125
+ value_old = int.from_bytes(bytes=value_raw, byteorder=elf.endian, signed=False)
126
+ value_raw = value.to_bytes(length=config.UID_SIZE, byteorder=elf.endian, signed=False)
127
+
128
+ try:
129
+ elf.write(address=addr, data=value_raw)
130
+ except AttributeError:
131
+ log.warning("ELF-Modifier failed @%s for symbol '%s'", f"0x{addr:X}", symbol)
132
+ elf.close()
133
+ return None
134
+
135
+ file_new = file_elf if overwrite else file_elf.with_stem(file_elf.stem + "_" + str(value))
136
+ try:
137
+ file_new.unlink(missing_ok=True)
138
+ except PermissionError:
139
+ elf.close()
140
+ log.error(
141
+ "Failed to overwrite file, because it's somehow still in use (typical for WinOS)."
142
+ )
143
+ return None
144
+ elf.save(path=file_new)
145
+ elf.close()
118
146
 
119
- file_new = file_elf if overwrite else file_elf.with_stem(file_elf.stem + "_" + str(value))
120
- elf.save(path=file_new)
121
- elf.close()
122
147
  log.debug(
123
148
  "Value of Symbol '%s' modified: %s -> %s @%s",
124
149
  symbol,
@@ -129,6 +154,6 @@ def modify_symbol_value(
129
154
  return file_new
130
155
 
131
156
 
132
- def modify_uid(file_elf: Path, value: int) -> Optional[Path]:
157
+ def modify_uid(file_elf: Path, value: int) -> Path | None:
133
158
  """Replace value of UID-symbol for shepherd testbed."""
134
159
  return modify_symbol_value(file_elf, symbol=config.UID_NAME, value=value, overwrite=True)
@@ -5,6 +5,7 @@ TODO: Work in Progress.
5
5
  - detection-functions that register in main validator.
6
6
  """
7
7
 
8
+ import shutil
8
9
  import tempfile
9
10
  from pathlib import Path
10
11
 
@@ -92,7 +93,12 @@ def is_elf(file: Path) -> bool:
92
93
  if not file.is_file():
93
94
  return False
94
95
  try:
95
- _ = ELF(path=file)
96
+ with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmp:
97
+ # switcheroo that might prevent windows bug - overwrite fails in modify_symbol_value()
98
+ file_tmp = Path(tmp) / file.name
99
+ shutil.copy(file, file_tmp)
100
+ elf = ELF(path=file_tmp)
101
+ elf.close()
96
102
  except ELFError:
97
103
  log.debug("File %s is not ELF - Magic number does not match", file.name)
98
104
  return False