From d5aed563a12fe856175d544f349789715fcf62d1 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, 4 Oct 2026 21:26:44 +0300 Subject: [PATCH] v3.19.0 (docs/engine): test-script guide audit; sms da accepts a leading + An audit of docs/test-scripts.md against the engine (testscript.py, events.py, the runner) found a handful of doc-level gaps, now fixed: - the scp80 param defaults (tar = the preset's ISD role TAR, spi1 = that TAR's MSL, spi2 = 01, kvn = the first keyset); - the counter rule as implemented: the run advances/persists the counter after every answered scp80 step when SPI1 requests a counter check (refused packets move the local value forward too - it only moves up); - respond is optional (an omitted object answers ok/0x00); - the run-log section: the exact READ BINARY/READ RECORD/UPDATE BINARY, SCP80 WRAPPED=/SP= and 'TEST-RUN step N failed:' lines; - Known limitations: an expanded-format 61xx has no in-packet GET RESPONSE (TS 102 226 5.2.1.1) - send the GET RESPONSE as a follow-up scp80 step; - sms check da: a leading + and whitespace are ignored. The engine now drops the + (the TPDU carries the international flag in the dialling type, so '+79...' could otherwise never match the parsed digits); docs/api.md and the local AGENTS note it. Tests: 821 frontend + 726 Python. --- docs/api.md | 3 ++- docs/test-scripts.md | 42 +++++++++++++++++++++++-------- pysim_simple_server/testscript.py | 6 ++++- tests/test_testscript.py | 9 ++++++- 4 files changed, 46 insertions(+), 14 deletions(-) diff --git a/docs/api.md b/docs/api.md index ff82b3c..f52f8a3 100644 --- a/docs/api.md +++ b/docs/api.md @@ -702,7 +702,8 @@ SHORT MESSAGE carrying the PoR of the preceding `scp80` step - PoR-in-submit, SPI2 bit `20`: `status` (the response status name, `ok` = `por_ok`), `sw` (the R-APDU status word, exact/mask) and `data` (the R-APDU response data, exact/mask)), `sms` (the SEND SHORT MESSAGE -TPDU: `da` digits, `pid`/`dcs`/`ud` hex exact/mask, `udl` decimal; SMS-SUBMIT +TPDU: `da` digits with whitespace skipped and a leading `+` ignored, +`pid`/`dcs`/`ud` hex exact/mask, `udl` decimal; SMS-SUBMIT only) or `files` (the File List of a REFRESH, TS 102 223 8.18; `files` is a list of path hex strings, matched order-insensitively); `respond` is the TERMINAL RESPONSE in the TS 102 223 diff --git a/docs/test-scripts.md b/docs/test-scripts.md index 53be390..e18520b 100644 --- a/docs/test-scripts.md +++ b/docs/test-scripts.md @@ -103,8 +103,9 @@ PoR-in-submit: 4. **Iterate**: a mismatch on `data` usually means the mask length differs from the actual value, or the value changed between runs - mask the varying bytes (counter, date-time) with `?`. -5. **Counters**: the run starts from the preset's stored counter and persists - each accepted packet's next value; consecutive runs continue correctly. +5. **Counters**: the run starts from the preset's stored counter and advances + it after every scp80 step the card answers (when SPI1 requests a counter + check); a refused packet moves it forward too - see the presets section. ### Reading the run log @@ -129,8 +130,13 @@ TEST-RUN step 4: MENU-SELECT ENVELOPE=80c2000009d30702020181900101 item=1 ('One' - A SEND SHORT MESSAGE expectation also logs its parsed TPDU (`SMS DA=… PID=… DCS=… UDL=… UD=…`) after the `CMD` line, so an applet's own SMS can be read even without an `sms` check. +- A wrapped scp80 step logs both forms: + `SCP80 C-APDU= WRAPPED= ()`; a pre-built packet logs + `SCP80 SP=…`. - Other actions log their send/response pair too (`APDU TX=… -> SW=…`, - `ENVELOPE(Event Download) … -> SW=…`, `READ MF/… -> SW=…`). + `ENVELOPE(Event Download) … -> SW=…`, `READ BINARY `, + `READ RECORD `, `UPDATE BINARY `, each with `-> SW=…`). +- A failing step adds `TEST-RUN step N failed: …` with the reason. - The shared secured-packet sender logs each segment's answer (`OTA SEND: ENVELOPE i/N -> `). - `--apdu-trace` adds the raw transport view @@ -164,9 +170,16 @@ preset's id** with the run; the server resolves it and takes: So before a run the matched preset (ICCID) must contain the keyset and, for every TAR the script addresses, either a TAR entry with its MSL or an explicit -`spi1` in the step. Counter handling: the run starts from the keyset's stored -counter and persists each accepted packet's next value, so consecutive runs -continue correctly. +`spi1` in the step. A step that omits them gets the preset's ISD role TAR as +`tar`, that TAR's MSL as `spi1` and `01` as `spi2`; `kvn` defaults to the +first keyset. + +Counter handling: the run starts from the keyset's stored counter and advances +(and persists) its next value after **every** scp80 step the card answers, when +the packet requests a counter check (SPI1 b5b4 ≠ 00, TS 102 225 §5.1.1) - a +refused packet moves the local value forward too. The local counter therefore +only ever moves up, so a later packet is never below what the card has seen; +consecutive runs continue correctly. ## Actions @@ -232,7 +245,8 @@ the default is the terminal, matching the Phone tab's forms. (`response_status == por_ok`), `"none"` (no PoR expected), or an **object** `{"status"?, "sw"?, "data"?}` asserting the decoded PoR of this very exchange (at least one field; the same fields as the expectation's `por` - check) - it works for both the inline and the SEND SHORT MESSAGE transport. + check, `status` accepting `ok` for `por_ok`) - it works for both the inline + and the SEND SHORT MESSAGE transport. Example: `"por": {"status": "por_ok", "sw": "9000"}`. - A plain hex string is exact; a value containing `?` is a mask. **A mask must have exactly the same length as the actual value** (`?` is a per-nibble @@ -276,7 +290,9 @@ the `por` check on the step that completes the sequence (intermediate steps should use `raw` or no PoR check). `sms` parses the fetched SEND SHORT MESSAGE's TPDU (SMS-SUBMIT only - any other -MTI fails the check): `da` the destination digits (exact, whitespace ignored), +MTI fails the check): `da` the destination digits as the TPDU carries them +(exact; whitespace and a leading `+` in the script are ignored - the +international flag lives in the TPDU's type-of-number), `pid`/`dcs` single bytes and `ud` the raw user data (a UDH included) as hex exact/mask, `udl` the user-data length in decimal (octets, or septets for the 7-bit alphabet - whatever the TPDU carries). Use it for an applet's own SMS; @@ -285,9 +301,9 @@ field is required. ### `respond` -The TERMINAL RESPONSE for the fetched command, in the TS 102 223 §6.8.0 -object order (Command details, Device identities, **Result**, Duration, Text -string, Item identifier, …): +Optional - an omitted (or empty) object answers `ok` (`0x00`). The TERMINAL +RESPONSE follows the TS 102 223 §6.8.0 object order (Command details, Device +identities, **Result**, Duration, Text string, Item identifier, …): - `result` - name or hex (`ok`, `partial`, `missing`, `refused`, `not_understood`, `modified`, `cancel`, `back`, `timeout`, `no_response`). @@ -443,6 +459,10 @@ PoR-in-submit (with the transport asserted by the expectation): `status` action. - A `por` check only decodes SEND SHORT MESSAGE commands; the inline transport is checked on the action (`por`/`data`). +- An expanded-format command (`"format": "expanded"`/`"expanded-ae"`) has no + in-packet GET RESPONSE (TS 102 226 §5.2.1.1): if the card answers `61xx`, + send the GET RESPONSE as a follow-up `scp80` step (its own secured packet, + same format). - `files` matches the File List as a set (order-insensitive); duplicate paths are kept. - Semantic `event` actions cover four events (0x03/0x0B/0x12/0x1D); every diff --git a/pysim_simple_server/testscript.py b/pysim_simple_server/testscript.py index b609a1a..4aaeb0d 100644 --- a/pysim_simple_server/testscript.py +++ b/pysim_simple_server/testscript.py @@ -467,7 +467,11 @@ def _normalise_content_check(c, default_level): da = c.get('da') if da is not None and str(da).strip(): d = re.sub(r'\s', '', str(da)) - if not re.fullmatch(r'[0-9*#+]{1,20}', d): + # The international flag lives in the TPDU's type-of-number, not + # in the digits: a leading '+' is accepted and dropped. + if d.startswith('+'): + d = d[1:] + if not re.fullmatch(r'[0-9*#]{1,20}', d): raise ScriptError('sms check: da must be up to 20 digits') out['da'] = d for key in ('pid', 'dcs'): diff --git a/tests/test_testscript.py b/tests/test_testscript.py index 885f89a..8876ecb 100644 --- a/tests/test_testscript.py +++ b/tests/test_testscript.py @@ -276,6 +276,13 @@ class TestValidation(unittest.TestCase): self.assertEqual(sms['dcs'], {'mode': 'exact', 'value': '00'}) self.assertEqual(sms['udl'], 5) self.assertEqual(sms['ud'], {'mode': 'mask', 'value': '0271??AABB'}) + # a leading '+' (the TPDU keeps the international flag in its + # type-of-number) and whitespace are dropped from `da` + script = T.normalise_script({'steps': [ + {'type': 'expect', 'command': 'SEND SHORT MESSAGE', + 'checks': [{'kind': 'sms', 'da': '+79 332 505 884'}]}, + ]}, _resolver) + self.assertEqual(script['steps'][0]['checks'][0]['da'], '79332505884') for check in ({'kind': 'alpha'}, # no value {'kind': 'alpha', 'mode': 'nope', 'value': 'x'}, {'kind': 'sms'}, # no fields @@ -1075,7 +1082,7 @@ class TestRunnerActions(RunnerTestCase): 'check': {'sw': {'mode': 'mask', 'value': '91??'}}}, {'type': 'expect', 'command': 'SEND SHORT MESSAGE', 'checks': [{'kind': 'alpha', 'mode': 'exact', 'value': 'Отправка...'}, - {'kind': 'sms', 'da': '79332505884', 'pid': '00', + {'kind': 'sms', 'da': '+79 332 505 884', 'pid': '00', 'dcs': '00', 'udl': 8, 'ud': '4B1C12579C9D83'}], 'respond': {'result': 'ok'}}, ])