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}},