From eee2888a2b131ad9ff823bf05f3af030a826c2c7 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: Wed, 7 Oct 2026 01:03:44 +0300 Subject: [PATCH] v3.22.8: the text/dcs load-time validation (review follow-ups) The review of the v3.22.7 fix asked when an expect can raise after its FETCH: the realistic case is the scripted TERMINAL RESPONSE encoding - _encode_text raises UnicodeEncodeError when respond.text has a non-GSM character while dcs is 00 (the default) or 04, i.e. after the command was fetched and with the card's proactive session left unanswered. The other candidates are guarded (the TPDU/PoR/file-list parsers, the PLI dictionary) or need the card to be gone (a transport failure between the FETCH and the TR). normalise_respond now runs the same coding the TR builder uses and refuses the script at load with an actionable message ("use dcs 08 (UCS2) for non-GSM characters"), so the failure happens at save/run start, before any FETCH; UCS2, ASCII and the empty answer are unaffected. Also from the review: the guide's run-log line now documents the v3.22.7 format (TEST-RUN step N failed (): ...), and the "unguarded item text decode" observation proved a non-issue - _decode_stk_text is internally tolerant (odd UCS2 / unknown prefixes fall back without raising), so the item parsers are left alone. Tests: a non-GSM text with the default dcs and with 04 is refused (the message names dcs 08), UCS2/ASCII/empty pass, and a whole script is refused with the same message. 843 frontend + 822 Python; sw cache simple-v412. --- docs/test-scripts.md | 8 ++++++-- frontend/index.html | 2 +- frontend/sw.js | 2 +- pyproject.toml | 2 +- pysim_simple_server/server.py | 2 +- pysim_simple_server/testscript.py | 10 ++++++++++ tests/test_testscript.py | 21 +++++++++++++++++++++ 7 files changed, 41 insertions(+), 6 deletions(-) diff --git a/docs/test-scripts.md b/docs/test-scripts.md index afae0b6..fc839a9 100644 --- a/docs/test-scripts.md +++ b/docs/test-scripts.md @@ -207,7 +207,8 @@ TEST-RUN step 4: MENU-SELECT ENVELOPE=80c2000009d30702020181900101 item=1 ('One' - A `proactive-drain` action logs its rounds (`STATUS i/N -> …`, `FETCH=…`, `CMD 0x…`, `TR=… -> SW=…`); the run cleanup logs `TEST-RUN drain: (0xNN) TR=… -> SW=…`. -- A failing step adds `TEST-RUN step N failed: …` with the reason. +- A failing step adds `TEST-RUN step N failed (): …` with the reason + (`` is `error` or `warning` - a tolerant step's line names its level). - The shared secured-packet sender logs each segment's answer (`OTA SEND: ENVELOPE i/N -> `). - `--apdu-trace` adds the raw transport view @@ -457,7 +458,10 @@ identities, **Result**, Duration, Text string, Item identifier, …): uses the spec's recommended codings: `"00"` GSM default alphabet 7 bits **packed**, `"04"` GSM default alphabet 8 bits (one octet per character), `"08"` **UCS2** (UTF-16-BE). An empty `text` sends the null text string - (Length `00`). + (Length `00`). A text the selected `dcs` cannot code (a non-GSM character + with `00`/`04`) is **refused at load** - the error names the coding and + points at `dcs: "08"` (the run must not fail after the command was + fetched). - `item_id` - the Item identifier for a successful SELECT ITEM answer. - `raw` - extra COMPREHENSION-TLVs appended after the standard objects. diff --git a/frontend/index.html b/frontend/index.html index 4503a7b..54c6a17 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1903,7 +1903,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.22.7'; +const SIMPLE_VERSION = '3.22.8'; 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 df2de50..e6866cc 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v411'; +const CACHE = 'simple-v412'; const URLS = [ 'index.html', 'help.html', diff --git a/pyproject.toml b/pyproject.toml index 8ec9d8d..c4eb98f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pysim-simple-server" -version = "3.22.7" +version = "3.22.8" 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 3b0d54d..b26d40e 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.22.7' +VERSION = '3.22.8' MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE diff --git a/pysim_simple_server/testscript.py b/pysim_simple_server/testscript.py index 380907e..b96f2c6 100644 --- a/pysim_simple_server/testscript.py +++ b/pysim_simple_server/testscript.py @@ -568,6 +568,16 @@ def normalise_respond(respond, cmd_type=None): out['text'] = str(respond['text']) dcs = respond.get('dcs') out['dcs'] = _int(dcs, 'respond dcs', 0, 255) if dcs is not None else 0x00 + # The same coding the TERMINAL RESPONSE builder uses: a text the + # selected dcs cannot code (a non-GSM character with 00/04) must fail + # the script at load, not mid-run after the command was fetched + # (v3.22.8 review). + try: + _encode_text(out['text'], out['dcs']) + except Exception: + raise ScriptError( + 'respond: the text cannot be coded with dcs %02X - use dcs 08 ' + '(UCS2) for non-GSM characters' % out['dcs']) extra = respond.get('raw') if extra not in (None, ''): out['raw'] = _data_hex(extra, 'respond raw') diff --git a/tests/test_testscript.py b/tests/test_testscript.py index 1774428..e9a2664 100644 --- a/tests/test_testscript.py +++ b/tests/test_testscript.py @@ -179,6 +179,27 @@ class TestValidation(unittest.TestCase): self.assertEqual(tr.hex().upper(), '8103042300' + '82028182' + '83020000' + '8D00') + def test_a_text_the_dcs_cannot_code_is_refused_at_load(self): + # v3.22.8: the load-time check uses the same coding the TERMINAL + # RESPONSE builder does - a non-GSM character with dcs 00/04 must + # fail the script at load, not mid-run after the command was fetched. + with self.assertRaises(T.ScriptError) as cm: + T.normalise_respond({'result': 'ok', 'text': 'Пароль'}) + self.assertIn('dcs 00', str(cm.exception)) + self.assertIn('dcs 08', str(cm.exception)) + with self.assertRaises(T.ScriptError): + T.normalise_respond({'result': 'ok', 'text': 'Пароль', 'dcs': '04'}) + # UCS2 accepts it; ASCII and the empty answer pass as before + self.assertEqual(T.normalise_respond({'text': 'Пароль', 'dcs': '08'})['dcs'], 0x08) + self.assertEqual(T.normalise_respond({'text': 'hello'})['text'], 'hello') + self.assertEqual(T.normalise_respond({'text': ''})['text'], '') + # a whole script is refused with the same message at run start + with self.assertRaises(T.ScriptError) as cm: + T.normalise_script({'steps': [ + {'type': 'expect', 'command': 'GET INPUT', + 'respond': {'result': 'ok', 'text': 'Пароль'}}]}, _resolver) + self.assertIn('cannot be coded', str(cm.exception)) + def test_menu_select_params_accept_id_or_text(self): script = T.normalise_script({'steps': [ {'type': 'action', 'kind': 'menu-select', 'params': {'item_id': 3}},