v1.9.26: profiler (card file-structure & content verification)

New 'Profiler' sub-tab in card-reader view:
- Named rulesets (profiles) persisted in localStorage; each has an ordered
  list of filesystem rules (type 'file', extensible to OTA/TAR checks later)
- List page: New profile / Profile from card / Import profile + per-profile
  Edit / Check / Export / Delete
- Editor page: inline rule fields (path, file type, size, record length,
  record count, contents with exact/mask/'?'-wildcard modes)
- Results page: sequential rule checks with live progress and a pass/fail
  report (existence + FCI attributes + content match)
- 'Profile from card' scans the equipped card; creates a rule only for files
  that exist (FCI present). A scan-options dialog lets the user skip contents
  of frequently-overwritten dynamic files (LOCI/PSLOCI/EPSLOCI/5GS3GPPLOCI/
  Keys/KeysPS/SMS/Kc/KcGPRS/LOCIGPRS/CBMID/SMSS), checked by default
- ADF-rooted paths use the AID; paths resolve via a new server _select_path

Server:
- _select_path() resolves MF/ADF-AID-rooted paths (pySim can't select ADF by AID)
- /api/select and /api/read accept 'path'; /api/select returns file_size,
  record_len, num_of_rec

Also fix: silent SELECT (P2=0x0C) no longer emits a trailing Le byte
(chain builder) - matches ETSI TS 102 221 silent-select behavior.

Tests: +profiler.test.js; frontend 117 pass, Python 61 pass.
SW cache v37.
This commit is contained in:
2026-09-09 15:51:48 +03:00
parent 1db6c12d52
commit 201b0df278
5 changed files with 729 additions and 13 deletions
+42 -4
View File
@@ -18,7 +18,7 @@ from osmocom.construct import GsmOrUcs2Adapter
from osmocom.tlv import BER_TLV_IE
VERSION = '1.9.25'
VERSION = '1.9.26'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
@@ -158,6 +158,34 @@ def _select_with_parent(lchan, name, parent_sel, app):
return fcp
def _select_path(lchan, path, app):
"""Select a file described by a full path.
Path is '/' separated; the first element is either 'MF' (or the MF fid
'3F00') or an ADF AID (hex). Remaining elements are FIDs or file names.
pySim's lchan.select() cannot select an ADF by its raw AID (selectables are
keyed by name/fid only), so ADF roots are resolved through rs.mf.applications.
"""
parts = [p for p in (path or '').split('/') if p]
if not parts:
raise RuntimeError('Empty path')
rs = app.rs
first = parts[0]
if first.upper() in ('MF', '3F00'):
lchan.select('MF', app)
else:
aid = first.lower()
adf = rs.mf.applications.get(aid)
if not adf:
adf = next((v for k, v in rs.mf.applications.items() if k.lower() == aid), None)
if not adf:
raise RuntimeError('ADF not found: %s' % first)
lchan.select_file(adf, app)
for seg in parts[1:]:
lchan.select(seg, app)
return lchan.selected_file
def _parse_tree_output(output):
lines = (output or '').split('\n')
children = []
@@ -1591,6 +1619,7 @@ class PysimHandler(BaseHTTPRequestHandler):
return
body = self._read_body()
self._log_req(body)
path = body.get('path')
fid = body.get('fid')
name = fid if fid else body.get('name', '')
parent_sel = body.get('parent_sel')
@@ -1601,12 +1630,18 @@ class PysimHandler(BaseHTTPRequestHandler):
return
lchan = rs.lchan[0]
try:
_select_with_parent(lchan, name, parent_sel, app)
if path:
_select_path(lchan, path, app)
else:
_select_with_parent(lchan, name, parent_sel, app)
cur = lchan.selected_file
data = {
'name': cur.name if cur else None,
'fid': cur.fid.upper() if cur and cur.fid else None,
'file_type': _get_file_type(lchan, cur),
'file_size': lchan.selected_file_size() if lchan else None,
'record_len': lchan.selected_file_record_len() if lchan else None,
'num_of_rec': lchan.selected_file_num_of_rec() if lchan else None,
'exists': True,
}
self._send_json(data)
@@ -1623,9 +1658,9 @@ class PysimHandler(BaseHTTPRequestHandler):
return
body = self._read_body()
self._log_req(body)
path = body.get('path')
fid = body.get('fid')
name = fid if fid else body.get('name', '')
fid = body.get('fid')
parent_sel = body.get('parent_sel')
mode = body.get('mode', 'raw')
rs = app.rs
@@ -1636,7 +1671,10 @@ class PysimHandler(BaseHTTPRequestHandler):
lchan = rs.lchan[0]
try:
sel = fid if fid else name
_select_with_parent(lchan, sel, parent_sel, app)
if path:
_select_path(lchan, path, app)
else:
_select_with_parent(lchan, sel, parent_sel, app)
ft = _get_file_type(lchan, lchan.selected_file)
is_record = ft in ('linear_fixed', 'cyclic')
if mode == 'decoded':