Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions repeater/companion/bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,24 @@
logger = logging.getLogger("RepeaterCompanionBridge")


def _prefs_bytes_from_json(value: Any) -> bytes:
"""Restore a ``bytes`` NodePrefs field from JSON (hex string from :func:`_to_json_safe`)."""
if value is None:
return b""
if isinstance(value, (bytes, bytearray)):
return bytes(value)
if isinstance(value, str):
s = value.strip()
if not s:
return b""
try:
return bytes.fromhex(s)
except ValueError:
logger.debug("Invalid hex for prefs bytes field (prefix %r)", s[:32])
return b""
return b""


def _to_json_safe(value: Any) -> Any:
"""Convert a value to a JSON-serializable form (avoids TypeError from enums, bytes, etc.)."""
if value is None or isinstance(value, (bool, int, float, str)):
Expand Down Expand Up @@ -70,8 +88,6 @@ def __init__(
authenticate_callback=authenticate_callback,
initial_contacts=initial_contacts,
)
# Load persisted prefs (e.g. node_name) from SQLite so matching uses last-saved name
self._load_prefs()

def _save_prefs(self) -> None:
"""Persist full NodePrefs as JSON to SQLite."""
Expand Down Expand Up @@ -104,6 +120,9 @@ def _load_prefs(self) -> None:
try:
if value is None:
continue
if isinstance(current, bytes):
setattr(self.prefs, key, _prefs_bytes_from_json(value))
continue
if isinstance(current, bool):
setattr(self.prefs, key, bool(value))
elif isinstance(current, int):
Expand Down
54 changes: 54 additions & 0 deletions tests/test_companion_bridge_prefs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""Tests for RepeaterCompanionBridge prefs JSON round-trip (bytes fields)."""

import pytest

from pymc_core import LocalIdentity

from repeater.companion.bridge import RepeaterCompanionBridge, _prefs_bytes_from_json


@pytest.fixture
def identity():
return LocalIdentity()


def test_prefs_bytes_from_json_round_trip():
assert _prefs_bytes_from_json("") == b""
assert _prefs_bytes_from_json("00") == b"\x00"
key = bytes(range(16))
assert _prefs_bytes_from_json(key.hex()) == key
assert _prefs_bytes_from_json(bytearray(key)) == key
assert _prefs_bytes_from_json(key) == key
assert _prefs_bytes_from_json("not-hex") == b""


def test_load_prefs_restores_default_scope_key_as_bytes(identity):
"""Hex strings from SQLite JSON must become bytes (not str) on NodePrefs."""

class FakeSqlite:
def companion_load_prefs(self, companion_hash: str):
return {
"default_scope_name": "region1",
"default_scope_key": bytes(range(16)).hex(),
}

def companion_save_prefs(self, companion_hash: str, prefs: dict) -> bool:
return True

async def inject(pkt, wait_for_ack=False):
return True

bridge = RepeaterCompanionBridge(
identity,
inject,
sqlite_handler=FakeSqlite(),
companion_hash="testhash",
node_name="bootname",
)
assert bridge.prefs.default_scope_name == "region1"
assert isinstance(bridge.prefs.default_scope_key, bytes)
assert bridge.prefs.default_scope_key == bytes(range(16))
scope = bridge.get_default_flood_scope()
assert scope is not None
assert scope[0] == "region1"
assert scope[1] == bytes(range(16))
Loading