diff --git a/frontend/index.html b/frontend/index.html index b20cb7a..404d6d8 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1047,7 +1047,11 @@ function data_text(m) { function cmd_description(m) { if (m.type === 'gap') return t('device disconnected — messages may be missing'); - if (m.decoded && m.decoded.summary) return m.decoded.summary; + const d = m.decoded; + // STATUS has no command body: without response data there is nothing + // to describe beyond the type and SW (no raw-hex fallback). + if (d && d.ins_hex === 'f2' && !d.body) return d.summary || ''; + if (d && d.summary) return d.summary; return data_text(m); } @@ -2629,7 +2633,7 @@ function onFilterChange() { } // ──────────────────── Versioning ──────────────────── -const PWA_VERSION = '1.22.18'; +const PWA_VERSION = '1.22.19'; const MIN_SERVER_VERSION = '1.0.0'; function parseVersion(v) { diff --git a/frontend/sw.js b/frontend/sw.js index f9ff5d5..ec52685 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simtrace-analyser-v50'; +const CACHE = 'simtrace-analyser-v51'; const PRECACHE = [ 'index.html', diff --git a/pyproject.toml b/pyproject.toml index 6e109fb..52c8f3c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.backends._legacy:_Backend" [project] name = "simtrace2-pysniff" -version = "1.22.18" +version = "1.22.19" description = "Python-based sniffing tool for SIMtrace2 hardware (SIM card communication sniffer)" readme = {file = "README.md", content-type = "text/markdown"} license = {text = "GPL-2.0-or-later"} diff --git a/simtrace2_pysniff/__init__.py b/simtrace2_pysniff/__init__.py index b497707..190e4ca 100644 --- a/simtrace2_pysniff/__init__.py +++ b/simtrace2_pysniff/__init__.py @@ -6,7 +6,7 @@ from .gsmtap import GsmtapSender, GsmtapReceiver from .pcap import PcapWriter from .dump import FileDumper, format_message -__version__ = '1.22.18' +__version__ = '1.22.19' __all__ = [ 'SniffSession', 'DeviceDisconnected', diff --git a/simtrace2_pysniff/server/decode.py b/simtrace2_pysniff/server/decode.py index 54d8e9f..f17ff82 100644 --- a/simtrace2_pysniff/server/decode.py +++ b/simtrace2_pysniff/server/decode.py @@ -3629,6 +3629,39 @@ def _fcp_summary(response_for, fd, response): return label +def _fcp_brief(response): + """Build a short "what this FCP describes" fragment for STATUS entries. + + E.g. ``FCP of MF (DF or ADF)`` or ``FCP of EF_ICCID (Working EF, + transparent, 10 B)``. + """ + fd = response.get('file_descriptor') or {} + fid = response.get('file_id') + name = response.get('file_id_name') + what = name or (fid.upper() if fid else None) + if not what and not fd: + return None + label = f'FCP of {what}' if what else 'FCP' + details = [] + ft = fd.get('file_type') + if ft: + details.append(ft) + if ft and ft != 'DF or ADF': + struct = fd.get('structure') + if struct: + details.append(struct) + nrec = fd.get('num_records') + rlen = fd.get('record_length') + fsize = response.get('file_size') or response.get('total_file_size') + if nrec and rlen: + details.append(f'{nrec} rec \u00d7 {rlen} B') + elif fsize: + details.append(f'{fsize} B') + if details: + label += f" ({', '.join(details)})" + return label + + def _build_summary(result): """Build a concise human-readable description of a decoded command. @@ -3685,6 +3718,16 @@ def _build_summary(result): if bodytxt: parts.append(bodytxt) + if ins == 'f2' and body: + # STATUS response data is the FCP of the current DF/EF (seen in a + # TPDU capture as the bytes after the 5-byte command header). + size = body.get('size') or 0 + txt = f"Data: {size} byte{'s' if size != 1 else ''}" + brief = _fcp_brief(result.get('response') or {}) + if brief: + txt += f' \u2014 {brief}' + parts.append(txt) + cmd = result.get('cmd') if cmd: if cmd.get('context'): diff --git a/tests/test_decode.py b/tests/test_decode.py index fd430c4..1e2b463 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -1586,6 +1586,40 @@ class TestStatusFcp(unittest.TestCase): self.assertEqual(resp['df_name'], 'A0000000871002FF45FF018902011100') self.assertEqual(resp['file_descriptor']['file_type'], 'DF or ADF') self.assertEqual(resp['life_cycle'], 'operational state (activated)') + self.assertEqual(r['summary'], 'Data: 56 bytes \u2014 FCP of 7FF0 (DF or ADF)') + + def test_status_no_data_has_no_summary(self): + # 80 F2 00 00 00 6C 25 — P3=0, card answers "wrong length"; no data. + r = decode_message(bytes.fromhex('80f20000006c25')) + self.assertEqual(r['ins_name'], 'STATUS') + self.assertNotIn('body', r) + self.assertNotIn('response', r) + self.assertIsNone(r['summary']) + + def test_status_mf_summary(self): + raw = bytes.fromhex( + '80f200000d' + '620b8202782183023f008a0105' + '9000') + r = decode_message(raw) + self.assertEqual(r['response']['file_id'], '3F00') + self.assertEqual(r['response']['file_id_name'], 'MF') + self.assertEqual(r['summary'], 'Data: 13 bytes \u2014 FCP of MF (DF or ADF)') + + def test_status_ef_summary(self): + raw = bytes.fromhex( + '80f2000024' + '62228202412183026f73a509c0018091047f206f538a01058b036f06098002000e880160' + '9000') + r = decode_message(raw) + self.assertEqual(r['response']['file_id_name'], 'EF_PSLOCI') + self.assertEqual( + r['summary'], + 'Data: 36 bytes \u2014 FCP of EF_PSLOCI (Working EF, transparent, 14 B)') + + def test_status_single_byte_pluralization(self): + r = decode_message(bytes.fromhex('80f2000001009000')) + self.assertEqual(r['summary'], 'Data: 1 byte') class TestLengthMismatch(unittest.TestCase): diff --git a/tests/test_pcap_import.py b/tests/test_pcap_import.py index bfb6bff..b94ad12 100644 --- a/tests/test_pcap_import.py +++ b/tests/test_pcap_import.py @@ -224,7 +224,7 @@ class TestImportEndpoint(unittest.TestCase): class TestVersion(unittest.TestCase): def test_package_version(self): from simtrace2_pysniff import __version__ - self.assertEqual(__version__, '1.22.18') + self.assertEqual(__version__, '1.22.19') def test_status_includes_version(self): import os