From f2046d7f0af58f8bfd75f1b45969bd88432d8ba1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD=20=D0=A2=D1=80=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Sun, 11 Oct 2026 16:34:08 +0300 Subject: [PATCH] v3.25.5: the PLI dictionary override guard fix The live report: POST /api/pli-dict did not change the dictionary. _PLI_DATA held a '' placeholder for every qualifier, and the clear branch's pop removed the placeholder even when there was no override. The PWA's Save posts every field, so one Save wiped the placeholders; the guard `if code in _PLI_DATA` then silently skipped every later override for those qualifiers (HTTP 200 + the unchanged/default dict) until a restart - and the next Save wiped them again. _PLI_DATA is now overrides-only ({}; the GET and _pli_data_hex already fall back to PLI_DEFAULTS) and the guard tests PLI_QUALIFIER_NAMES, so a new override always sticks and a clear never poisons a later set. Tests: the endpoint test now saves/restores the dict (its pop(0x01) cleanup was the same trap) plus a regression - the PWA-style full-dict POST, then new overrides with and without defaults must stick, a clear restores the default and a later set still works, an unknown qualifier is ignored. 886 frontend + 874 Python; sw cache simple-v429. --- frontend/index.html | 2 +- frontend/sw.js | 2 +- pyproject.toml | 2 +- pysim_simple_server/server.py | 15 ++++++++++--- tests/test_fetch_routes.py | 41 ++++++++++++++++++++++++++++++++++- 5 files changed, 55 insertions(+), 7 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index 070cf92..f35d232 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1905,7 +1905,7 @@ // ===== Version ===== // Single source of truth for the PWA version: shown in the header and used // by the server version check in pysimConnect(). -const SIMPLE_VERSION = '3.25.4'; +const SIMPLE_VERSION = '3.25.5'; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; // ===== view history (the browser Back/Forward buttons) ===== diff --git a/frontend/sw.js b/frontend/sw.js index 6c68cc2..4918ca2 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v428'; +const CACHE = 'simple-v429'; const URLS = [ 'index.html', 'help.html', diff --git a/pyproject.toml b/pyproject.toml index e7ff9c8..f6b5d7e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pysim-simple-server" -version = "3.25.4" +version = "3.25.5" description = "HTTP REST server wrapping pysim for the SIMple PWA" requires-python = ">=3.8" # pysim is a git-only dependency installed explicitly by setup.bat/setup.sh. diff --git a/pysim_simple_server/server.py b/pysim_simple_server/server.py index 2f72860..941857f 100644 --- a/pysim_simple_server/server.py +++ b/pysim_simple_server/server.py @@ -36,7 +36,7 @@ from osmocom.tlv import BER_TLV_IE from osmocom.utils import rpad -VERSION = '3.25.4' +VERSION = '3.25.5' MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE @@ -2669,7 +2669,13 @@ PLI_DEFAULTS = { 0x0E: '3F02' + '0308', } -_PLI_DATA = {q: '' for q in PLI_QUALIFIER_NAMES} # the operator overrides +# The operator's overrides only, keyed by qualifier code - the built-in +# PLI_DEFAULTS above are the base layer and stay the fallback; a field cleared +# through /api/pli-dict removes its entry (v3.25.5). Keeping this dict free +# of placeholder keys is deliberate: the POST's guard tests the known +# qualifiers, never "an override already exists" (a full-dict Save posts every +# field - an empty one must not make later overrides no-ops). +_PLI_DATA = {} _BIP = httpota.BipTerminal() # Active BIP session, shared by the SCP81 listener and the Simulator's @@ -9102,7 +9108,10 @@ class PysimHandler(BaseHTTPRequestHandler): if isinstance(v, str): try: code = int(k, 16) - if code in _PLI_DATA: + # a known qualifier - never "an override already + # exists": a new override must stick, and a clear + # must not poison a later set (v3.25.5, live report) + if code in PLI_QUALIFIER_NAMES: if v: bytes.fromhex(v) # valid hex only _PLI_DATA[code] = v diff --git a/tests/test_fetch_routes.py b/tests/test_fetch_routes.py index 3625877..ec587ce 100644 --- a/tests/test_fetch_routes.py +++ b/tests/test_fetch_routes.py @@ -142,6 +142,7 @@ class PliDictHttpTests(unittest.TestCase): def test_defaults_overrides_and_clearing(self): import pysim_simple_server.server as srv + saved = dict(srv._PLI_DATA) try: status, resp = self._req('GET', '/api/pli-dict') self.assertEqual(status, 200, resp) @@ -162,8 +163,46 @@ class PliDictHttpTests(unittest.TestCase): # a bad value is ignored status, resp = self._req('POST', '/api/pli-dict', {'01': 'ZZ'}) self.assertEqual(resp['01'], srv.PLI_DEFAULTS[0x01]) + self.assertNotIn(0x01, srv._PLI_DATA) finally: - srv._PLI_DATA.pop(0x01, None) + srv._PLI_DATA.clear() + srv._PLI_DATA.update(saved) + + def test_a_new_override_after_a_full_save_sticks(self): + # The PWA's Save posts every field; the empty ones used to pop the + # dictionary's placeholder keys, after which the old `code in + # _PLI_DATA` guard silently skipped every later override for those + # qualifiers - POST answered 200 with the unchanged/default dict + # (live report 2026-10-11). The guard now tests the known qualifiers. + import pysim_simple_server.server as srv + saved = dict(srv._PLI_DATA) + try: + full = {('%02X' % q): '' for q in srv.PLI_QUALIFIER_NAMES} + status, resp = self._req('POST', '/api/pli-dict', full) + self.assertEqual(status, 200, resp) + self.assertEqual(srv._PLI_DATA, {}) + # a new override for a qualifier with a default sticks + status, resp = self._req('POST', '/api/pli-dict', {'0A': '630102'}) + self.assertEqual(resp['0A'], '630102') + # and one for a qualifier without a default + status, resp = self._req('POST', '/api/pli-dict', {'02': 'AABBCC'}) + self.assertEqual(resp['02'], 'AABBCC') + status, resp = self._req('GET', '/api/pli-dict') + self.assertEqual(resp['0A'], '630102') + self.assertEqual(resp['02'], 'AABBCC') + # clearing restores the default and a later set still works + status, resp = self._req('POST', '/api/pli-dict', {'0A': ''}) + self.assertEqual(resp['0A'], srv.PLI_DEFAULTS[0x0A]) + status, resp = self._req('POST', '/api/pli-dict', {'0A': '630103'}) + self.assertEqual(resp['0A'], '630103') + # an unknown qualifier is ignored + status, resp = self._req('POST', '/api/pli-dict', {'18': 'AABB'}) + self.assertEqual(status, 200, resp) + self.assertNotIn(0x18, srv._PLI_DATA) + self.assertEqual(resp['0A'], '630103') + finally: + srv._PLI_DATA.clear() + srv._PLI_DATA.update(saved) class CardFreeGetTests(unittest.TestCase):