v3.17.0: test scripts for the applet handoff - menu by text, spec-order TRs, codings
The applet-testing batch agreed after v3.15/3.16:
- menu-select by text: the step names the item by `text` (+ `mode`
exact/contains, `case_sensitive`) instead of the install-dependent id
(both or neither is refused); the runner resolves it against the card's
cached menu and raises a step error naming the available items when nothing
(or several items) matches. The cache is refreshed on every SET UP MENU
fetch - the proactive chain and the expect step - so an applet install (new
items/ids after its REFRESH) no longer leaves a stale menu behind. The
step editor offers the id or the text + mode and the client-side pre-check
bounds them.
- TERMINAL RESPONSE object order: both builders emitted the Item identifier
before the Result (and the scripted builder its Text string before it too);
TS 102 223 6.8.0 mandates Command details, Device identities, Result,
Duration, Text string, Item identifier. `testscript.build_tr` and
`_menu_send_response` now follow it; `respond.raw` is documented as
appended after the standard objects, and the exact-byte tests pin the order.
- Text codings: the scripted response codes the GSM default alphabet via the
GSM 03.38 codec ('00' packed, '04' 8-bit) instead of latin-1, keeps '08'
UCS2 and sends the spec's null text string for an empty answer; the packing
helper is shared with the interactive path (`testscript.pack_gsm7`). A
UCS2 GET INPUT answer is documented and tested.
- docs/test-scripts.md gained the handoff brief for the applet agent: the
value-collection table, fill-in worksheets for the two applet scenarios, a
menu-by-text example, the validation checklist and a format stamp; help
EN/RU and docs/api.md updated.
Tests: 809 frontend + 705 Python; sw cache simple-v380.
This commit is contained in:
@@ -33,7 +33,7 @@ from osmocom.tlv import BER_TLV_IE
|
||||
from osmocom.utils import rpad
|
||||
|
||||
|
||||
VERSION = '3.16.0'
|
||||
VERSION = '3.17.0'
|
||||
|
||||
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||
|
||||
@@ -5210,6 +5210,17 @@ def _run_proactive_chain(scc, sw91, on_fetch=None, status_poll=True):
|
||||
entry = _log_proactive(cmd_type, raw, cmd_qual, cmd_num)
|
||||
except Exception as e:
|
||||
sys.stderr.write('FETCH log error: %s (raw=%s)\n' % (e, raw.hex()))
|
||||
try:
|
||||
if cmd_type == 0x25:
|
||||
# Keep the cached menu current: text-based menu selection
|
||||
# and the Phone tab must see the menu the card sends now
|
||||
# (an applet install changes the items and their ids).
|
||||
menu = _parse_setup_menu_command(raw)
|
||||
if menu and _server_ref:
|
||||
_server_ref.sim_menu = menu
|
||||
except Exception as e:
|
||||
sys.stderr.write('SET UP MENU cache error: %s (raw=%s)\n'
|
||||
% (e, raw.hex()))
|
||||
try:
|
||||
if raw and cmd_type in (0x03, 0x04):
|
||||
_handle_card_poll_command(cmd_type, raw)
|
||||
@@ -5446,20 +5457,10 @@ def _validate_input_response(pd, text):
|
||||
|
||||
|
||||
def _pack_gsm7(septets):
|
||||
"""Pack GSM 03.38 septets into 7-bit octets (TS 23.038 4, SMS packing)."""
|
||||
out = bytearray()
|
||||
acc = 0
|
||||
bits = 0
|
||||
for s in septets:
|
||||
acc |= (s & 0x7F) << bits
|
||||
bits += 7
|
||||
while bits >= 8:
|
||||
out.append(acc & 0xFF)
|
||||
acc >>= 8
|
||||
bits -= 8
|
||||
if bits:
|
||||
out.append(acc & 0xFF)
|
||||
return bytes(out)
|
||||
"""Pack GSM 03.38 septets into 7-bit octets (TS 23.038 4, SMS packing).
|
||||
One implementation, shared with the scripted TR builder
|
||||
(`testscript.pack_gsm7`)."""
|
||||
return testscript.pack_gsm7(septets)
|
||||
|
||||
|
||||
def _input_text_tlv(pd, text):
|
||||
@@ -5513,12 +5514,13 @@ def _menu_send_response(server, result, item_id=None, text=None):
|
||||
duration_tlv = bytes([0x04, 0x02, pd['duration_unit'], value])
|
||||
cd = bytes([0x81, 0x03, pd['cmd_num'], pd['cmd_type'], 0x00])
|
||||
di = bytes([0x82, 0x02, pd['dev_dst'], pd['dev_src']])
|
||||
# TS 102 223 6.8.0 object order: Result (C), Duration (D), Text string (E),
|
||||
# Item identifier (F).
|
||||
tr_data = cd + di
|
||||
tr_data += bytes([0x83, 0x02, gr, 0x00])
|
||||
tr_data += duration_tlv + text_tlv
|
||||
if isinstance(item_id, int) and result == 'ok' and pd['type'] == 'select_item':
|
||||
tr_data += bytes([0x90, 0x01, item_id])
|
||||
tr_data += bytes([0x83, 0x02, gr, 0x00])
|
||||
# Table 6.8.0 order after Result: Duration (D) then Text string (E)
|
||||
tr_data += duration_tlv + text_tlv
|
||||
tr_hex = '%s140000%02x%s' % (scc.cat_cla, len(tr_data), tr_data.hex())
|
||||
tr_rv = scc._tp.send_apdu(tr_hex)
|
||||
sys.stderr.write('TR(menu): cmd=%02x type=%02x result=%02x -> %s\n' % (pd['cmd_num'], pd['cmd_type'], gr, tr_rv[1]))
|
||||
@@ -5912,6 +5914,34 @@ def _test_run_scp80(server, step, ctx):
|
||||
return data, sw, sent, por, counter
|
||||
|
||||
|
||||
def _menu_item_by_text(server, p):
|
||||
"""Resolve a menu-select step's text against the cached SET UP MENU items
|
||||
(the item ids vary with the applet's install parameters, so a script
|
||||
matches the text the card actually shows). Raises a ScriptError naming
|
||||
the available items when nothing - or more than one item - matches."""
|
||||
menu = getattr(server, 'sim_menu', None) or {}
|
||||
items = menu.get('items') or []
|
||||
spec = {'mode': p.get('mode', 'exact'), 'value': p['text'],
|
||||
'case_sensitive': p.get('case_sensitive', True)}
|
||||
matches = [it for it in items
|
||||
if testscript.match_text(spec, it.get('text') or '')]
|
||||
if not matches:
|
||||
listing = ', '.join('%s=%r' % (it.get('id'), it.get('text'))
|
||||
for it in items)
|
||||
raise testscript.ScriptError(
|
||||
'menu-select: no menu item matches %r (%s)'
|
||||
% (p['text'], 'menu: ' + listing if listing
|
||||
else 'no menu cached - fetch the menu (a SET UP MENU '
|
||||
'expectation) first'))
|
||||
if len(matches) > 1:
|
||||
raise testscript.ScriptError(
|
||||
'menu-select: %d items match %r (%s) - use the item id'
|
||||
% (len(matches), p['text'],
|
||||
', '.join('%s=%r' % (it.get('id'), it.get('text'))
|
||||
for it in matches)))
|
||||
return matches[0]['id']
|
||||
|
||||
|
||||
def _test_run_action(server, step, ctx):
|
||||
scc = server.scc
|
||||
kind = step['kind']
|
||||
@@ -5924,7 +5954,10 @@ def _test_run_action(server, step, ctx):
|
||||
drain=False)
|
||||
sent = 'ENVELOPE(Event Download) type=0x%02X' % p['event']
|
||||
elif kind == 'menu-select':
|
||||
tlv = bytes([0xD3, 0x07, 0x02, 0x02, 0x01, 0x81, 0x90, 0x01, p['item_id']])
|
||||
item_id = p.get('item_id')
|
||||
if item_id is None:
|
||||
item_id = _menu_item_by_text(server, p)
|
||||
tlv = bytes([0xD3, 0x07, 0x02, 0x02, 0x01, 0x81, 0x90, 0x01, item_id])
|
||||
sent = '%sc20000%02x%s' % (scc.cat_cla, len(tlv), tlv.hex())
|
||||
server.menu_active = True
|
||||
data, sw = scc._tp.send_apdu(sent)
|
||||
@@ -6061,6 +6094,12 @@ def _test_run_expect(server, step, pending, ctx=None):
|
||||
# submit mode (SPI2 bit 0x20): decode it with the triggering scp80 step's
|
||||
# context so the checks can assert the PoR contents.
|
||||
por = _test_expect_por(ctx, raw) if cmd_type == 0x13 else None
|
||||
if cmd_type == 0x25:
|
||||
# The card may re-send its menu (e.g. after an applet install); keep
|
||||
# the cache that text-based menu selection resolves against current.
|
||||
menu = _parse_setup_menu_command(raw)
|
||||
if menu:
|
||||
server.sim_menu = menu
|
||||
checks = []
|
||||
want_type = step['command'].get('type')
|
||||
type_ok = want_type is None or cmd_type == want_type
|
||||
|
||||
@@ -21,10 +21,13 @@ profiler), e.g. ``{"mode": "mask", "value": "91??"}``.
|
||||
|
||||
import re
|
||||
|
||||
import gsm0338 # registers the 'gsm03.38' codec (the default alphabet coding)
|
||||
|
||||
__all__ = [
|
||||
'ScriptError', 'ACTION_KINDS', 'FAIL_LEVELS', 'POR_CHECKS', 'RESULT_NAMES',
|
||||
'normalise_script', 'normalise_step', 'normalise_respond',
|
||||
'match_value', 'match_text', 'match_item', 'combine_levels', 'build_tr',
|
||||
'pack_gsm7',
|
||||
]
|
||||
|
||||
ACTION_KINDS = ('envelope', 'menu-select', 'file-write', 'file-read', 'apdu',
|
||||
@@ -216,7 +219,25 @@ def _normalise_params(kind, p):
|
||||
return {'event': _int(p['event'], 'envelope event', 0, 255),
|
||||
'data': _data_hex(p.get('data'), 'envelope data', allow_empty=True)}
|
||||
if kind == 'menu-select':
|
||||
return {'item_id': _int(p.get('item_id'), 'menu item_id', 1, 255)}
|
||||
# The item is named by its id, or by its text - the id varies with the
|
||||
# applet's install parameters, so a script can match the text the card
|
||||
# actually shows (the runner resolves it against the cached menu).
|
||||
out = {}
|
||||
if p.get('item_id') not in (None, ''):
|
||||
out['item_id'] = _int(p.get('item_id'), 'menu item_id', 1, 255)
|
||||
text = p.get('text')
|
||||
if text is not None and str(text).strip():
|
||||
out['text'] = str(text)
|
||||
mode = str(p.get('mode') or 'exact').lower()
|
||||
if mode not in ('exact', 'contains'):
|
||||
raise ScriptError('menu-select: mode must be exact or contains')
|
||||
out['mode'] = mode
|
||||
out['case_sensitive'] = bool(p.get('case_sensitive', True))
|
||||
if not out:
|
||||
raise ScriptError('menu-select: item_id or text is required')
|
||||
if 'item_id' in out and 'text' in out:
|
||||
raise ScriptError('menu-select: give item_id or text, not both')
|
||||
return out
|
||||
if kind in ('file-write', 'file-read'):
|
||||
path = str(p.get('path') or '').strip()
|
||||
if not path:
|
||||
@@ -417,26 +438,57 @@ def normalise_respond(respond, cmd_type=None):
|
||||
|
||||
# ─── scripted TERMINAL RESPONSE ─────────────────────────────────────────
|
||||
|
||||
def pack_gsm7(septets):
|
||||
"""Pack GSM 03.38 septets into 7-bit octets (TS 23.038 4, SMS packing)."""
|
||||
out = bytearray()
|
||||
acc = 0
|
||||
bits = 0
|
||||
for s in septets:
|
||||
acc |= (s & 0x7F) << bits
|
||||
bits += 7
|
||||
while bits >= 8:
|
||||
out.append(acc & 0xFF)
|
||||
acc >>= 8
|
||||
bits -= 8
|
||||
if bits:
|
||||
out.append(acc & 0xFF)
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def _encode_text(text, dcs):
|
||||
"""Text string coding for a scripted TERMINAL RESPONSE (TS 102 223 8.15):
|
||||
'00' = GSM default alphabet 7 bits packed, '04' (or any other 8-bit
|
||||
scheme) = GSM default alphabet 8 bits (TS 23.038 via the gsm03.38 codec,
|
||||
bit 8 clear), '08' = UCS2. Matches the interactive GET INKEY/GET INPUT
|
||||
response codings."""
|
||||
if (dcs & 0x0C) == 0x08:
|
||||
return text.encode('utf-16-be')
|
||||
return text.encode('latin-1', 'replace')
|
||||
if dcs == 0x00:
|
||||
return pack_gsm7(text.encode('gsm03.38'))
|
||||
return text.encode('gsm03.38')
|
||||
|
||||
|
||||
def build_tr(cmd_num, cmd_type, dev_dst, dev_src, respond):
|
||||
"""Flat COMPREHENSION-TLV TERMINAL RESPONSE payload (TS 102 223 6.8):
|
||||
command details + device identities + optional item identifier / text
|
||||
string / raw TLVs + result. Matches the interactive menu TR layout."""
|
||||
"""Flat COMPREHENSION-TLV TERMINAL RESPONSE payload (TS 102 223 6.8) in the
|
||||
6.8.0 object order: command details, device identities, Result, then the
|
||||
command-specific objects (Text string for GET INKEY/GET INPUT, Item
|
||||
identifier for SELECT ITEM) and any extra raw TLVs. Matches the
|
||||
interactive menu TR layout."""
|
||||
out = bytearray([0x81, 0x03, cmd_num & 0xFF, cmd_type & 0xFF, 0x00])
|
||||
out += bytes([0x82, 0x02, dev_dst & 0xFF, dev_src & 0xFF])
|
||||
result = int(respond.get('result', 0))
|
||||
out += bytes([0x83, 0x02, result & 0xFF, 0x00])
|
||||
if respond.get('text') is not None:
|
||||
text = str(respond['text'])
|
||||
if text == '':
|
||||
# TS 102 223 8.15: a null text string is Length 00, no value part
|
||||
out += bytes([0x8D, 0x00])
|
||||
else:
|
||||
dcs = int(respond.get('dcs', 0x00))
|
||||
body = _encode_text(text, dcs)
|
||||
out += bytes([0x8D, len(body) + 1, dcs]) + body
|
||||
if respond.get('item_id') is not None and result == 0x00:
|
||||
out += bytes([0x90, 0x01, respond['item_id'] & 0xFF])
|
||||
if respond.get('raw'):
|
||||
out += bytes.fromhex(respond['raw'])
|
||||
if respond.get('text') is not None:
|
||||
dcs = int(respond.get('dcs', 0x00))
|
||||
body = _encode_text(respond['text'], dcs)
|
||||
out += bytes([0x8D, len(body) + 1, dcs]) + body
|
||||
out += bytes([0x83, 0x02, result & 0xFF, 0x00])
|
||||
return bytes(out)
|
||||
|
||||
Reference in New Issue
Block a user