v3.19.0: the applet-agent script batch - por objects, alpha/sms checks, scp80 format

The test scripts get the pieces an applet test needed:

- scp80 check.por accepts an object {status?, sw?, data?} asserting the
  decoded PoR of this very exchange (inline or via SEND SHORT MESSAGE);
  the evaluation is shared with the expect-step por check
  (_por_check_results).  The editor's PoR select gains 'custom' with the
  three fields (an empty object refuses on save).
- a new alpha content check reads the command's Alpha identifier (05/85,
  Annex A coded) - the text check stays bound to the 8D Text string, so a
  command carrying both keeps them distinct.
- a new sms content check asserts a SEND SHORT MESSAGE's submit TPDU:
  DA / PID / DCS / UDL / UD (the parsed fields are logged and reported;
  an applet's own SMS, not the OTA PoR the por check decodes).
- scp80.params.format wraps the C-APDU in the TS 102 226 5.2.1 Command
  Scripting template (expanded = AA..., expanded-ae = AE 80...00 00); the
  log shows WRAPPED=, the report C-APDU=<effective> format=<fmt>.
- docs: Worksheet 1 addresses an RFM TAR (B00000/B00001/B00010 compact,
  B00120/B00140/B00130 expanded) with the own-TAR and MSL/spi1 notes, and
  file-write is documented as a direct APDU that does not fire
  EVENT_REMOTE_FILE_UPDATE.

Tests: 819 frontend + 724 Python; sw cache simple-v387.
This commit is contained in:
2026-10-04 20:57:38 +03:00
parent d1ea4fe450
commit 92506e72b9
11 changed files with 650 additions and 80 deletions
+62 -11
View File
@@ -54,6 +54,21 @@ class ScriptError(ValueError):
# ─── normalisation helpers ──────────────────────────────────────────────
def _por_fields(c, what='por check'):
"""The decoded-PoR assertion fields (status/sw/data); at least one."""
out = {}
status = c.get('status')
if status is not None and str(status).strip():
out['status'] = str(status).strip()
if c.get('sw') not in (None, ''):
out['sw'] = _check_spec(c.get('sw'), what + ' sw')
if c.get('data') not in (None, ''):
out['data'] = _check_spec(c.get('data'), what + ' data')
if not (out.get('status') or out.get('sw') or out.get('data')):
raise ScriptError(what + ': status, sw or data is required')
return out
def _fail_level(value, default='error'):
if value in (None, ''):
return default
@@ -312,6 +327,15 @@ def _normalise_params(kind, p):
# the counter stay preset-owned, the step only picks which keyset.
if p.get('kvn') not in (None, ''):
out['kvn'] = _int(p.get('kvn'), 'scp80 keyset number', 1, 15)
# The command format: the C-APDU verbatim (compact, the default) or
# wrapped in the expanded Command Scripting template (TS 102 226
# 5.2.1) - `expanded` = AA/<len>/22/..., `expanded-ae` = AE 80/22/00 00.
fmt = str(p.get('format') or '').strip().lower()
if fmt:
if fmt not in ('compact', 'expanded', 'expanded-ae'):
raise ScriptError('scp80: format must be compact, expanded or '
'expanded-ae')
out['format'] = fmt
return out
if kind == 'status':
attempts = p.get('attempts')
@@ -337,14 +361,20 @@ def _normalise_check(check, kind, params):
sw = {'mode': 'exact', 'value': '9000'}
data = _check_spec(check.get('data'), 'check.data')
por = check.get('por')
if por is None:
if isinstance(por, dict):
# The decoded-PoR assertion (available on scp80 actions whether the
# PoR arrived inline or via SEND SHORT MESSAGE).
if kind != 'scp80':
raise ScriptError('check.por is only valid for scp80 actions')
por = _por_fields(por, 'check.por')
elif por is None:
por = 'any'
else:
por = str(por).lower()
if kind != 'scp80':
raise ScriptError('check.por is only valid for scp80 actions')
if por not in POR_CHECKS:
raise ScriptError('check.por must be none, ok or any')
raise ScriptError('check.por must be none, ok, any or an object')
return {'sw': sw, 'data': data, 'por': por}
@@ -416,15 +446,36 @@ def _normalise_content_check(c, default_level):
'on_fail': level}
if kind == 'por':
out = {'kind': 'por', 'on_fail': level}
status = c.get('status')
if status is not None and str(status).strip():
out['status'] = str(status).strip()
if c.get('sw') not in (None, ''):
out['sw'] = _check_spec(c.get('sw'), 'por check sw')
if c.get('data') not in (None, ''):
out['data'] = _check_spec(c.get('data'), 'por check data')
if not (out.get('status') or out.get('sw') or out.get('data')):
raise ScriptError('por check: status, sw or data is required')
out.update(_por_fields(c, 'por check'))
return out
if kind == 'alpha':
mode = str(c.get('mode') or 'contains').lower()
if mode not in ('contains', 'exact'):
raise ScriptError('alpha check: mode must be contains or exact')
if c.get('value') is None:
raise ScriptError('alpha check: value is required')
return {'kind': 'alpha', 'mode': mode, 'value': str(c['value']),
'case_sensitive': bool(c.get('case_sensitive', True)),
'on_fail': level}
if kind == 'sms':
# The SEND SHORT MESSAGE TPDU fields (an applet's own SMS, as opposed
# to the PoR of an OTA exchange).
out = {'kind': 'sms', 'on_fail': 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):
raise ScriptError('sms check: da must be up to 20 digits')
out['da'] = d
for key in ('pid', 'dcs'):
if c.get(key) not in (None, ''):
out[key] = _check_spec(c.get(key), 'sms check %s' % key)
if c.get('udl') not in (None, ''):
out['udl'] = _int(c.get('udl'), 'sms check udl', 0, 255)
if c.get('ud') not in (None, ''):
out['ud'] = _check_spec(c.get('ud'), 'sms check ud')
if not any(k in out for k in ('da', 'pid', 'dcs', 'udl', 'ud')):
raise ScriptError('sms check: da, pid, dcs, udl or ud is required')
return out
if kind == 'files':
val = c.get('files', c.get('value'))