From 6bdbbea209c37d870ef2167102c09cbda992fc86 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: Sat, 5 Sep 2026 23:20:02 +0300 Subject: [PATCH] Show T=15 as PPS capability, not a transport protocol T=15 in the ATR is a Protocol-and-Parameter-Selection capability, not a data protocol like T=0/T=1. Previously 'Protocols: T=0, T=15' was misleading. - _decode_atr: exclude T=15 from protocols, set pps=True instead - TCK presence: only-t0 rule now also requires no PPS (T=15 implies TCK) - PWA ATR detail: 'Protocol: T=0' + 'PPS: supported' rows - +1 test with the real UICC ATR (3B 9F 95 80 1F C7 ...) - Bump 1.22.2, SW cache v33->v34 --- frontend/index.html | 7 +++++-- frontend/sw.js | 2 +- pyproject.toml | 2 +- simtrace2_pysniff/__init__.py | 2 +- simtrace2_pysniff/server/decode.py | 10 ++++++++-- tests/test_decode.py | 13 +++++++++++++ tests/test_pcap_import.py | 2 +- 7 files changed, 30 insertions(+), 8 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index a638635..c6d493f 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1450,7 +1450,10 @@ function atrPpsRows(decoded) { rows.push(['Convention', decoded.convention || 'unknown']); if (decoded.ts) rows.push(['TS', '0x' + decoded.ts]); if (decoded.t0) rows.push(['T0', '0x' + decoded.t0 + ' — ' + decoded.historical_len + ' historical bytes']); - if (decoded.protocols) rows.push(['Protocols', decoded.protocols.join(', ')]); + if (decoded.protocols && decoded.protocols.length) { + rows.push([decoded.protocols.length > 1 ? 'Protocols' : 'Protocol', decoded.protocols.join(', ')]); + } + if (decoded.pps) rows.push(['PPS', 'supported']); if (decoded.interface) { for (const ifc of decoded.interface) { rows.push([ifc.name, ifaceValue(ifc)]); @@ -2511,7 +2514,7 @@ function onFilterChange() { } // ──────────────────── Versioning ──────────────────── -const PWA_VERSION = '1.22.1'; +const PWA_VERSION = '1.22.2'; const MIN_SERVER_VERSION = '1.0.0'; function parseVersion(v) { diff --git a/frontend/sw.js b/frontend/sw.js index c68e36b..719d5c5 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simtrace-analyser-v33'; +const CACHE = 'simtrace-analyser-v34'; const PRECACHE = [ 'index.html', diff --git a/pyproject.toml b/pyproject.toml index 5d5bd23..83f6118 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.backends._legacy:_Backend" [project] name = "simtrace2-pysniff" -version = "1.22.1" +version = "1.22.2" 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 be6f06d..580d50c 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.1' +__version__ = '1.22.2' __all__ = [ 'SniffSession', 'DeviceDisconnected', diff --git a/simtrace2_pysniff/server/decode.py b/simtrace2_pysniff/server/decode.py index f8bbb31..b0f6f74 100644 --- a/simtrace2_pysniff/server/decode.py +++ b/simtrace2_pysniff/server/decode.py @@ -4177,6 +4177,7 @@ def _decode_atr(data): interface = [] protocols = [] + pps = False flags = t0 & 0xF0 i = 1 level = 1 @@ -4229,7 +4230,10 @@ def _decode_atr(data): b = body[i] i += 1 cur_t = b & 0x0F - protocols.append(_T_PROTOCOLS.get(cur_t, f'T={cur_t}')) + if cur_t == 15: + pps = True + else: + protocols.append(_T_PROTOCOLS.get(cur_t, f'T={cur_t}')) flags = b & 0xF0 level += 1 else: @@ -4239,6 +4243,8 @@ def _decode_atr(data): result['interface'] = interface if protocols: result['protocols'] = protocols + if pps: + result['pps'] = True k = result['historical_len'] if k and i + k <= len(body): @@ -4246,7 +4252,7 @@ def _decode_atr(data): i += k # TCK present unless only T=0 is proposed (ISO 7816-3 §8.2.5). - only_t0 = not protocols or protocols == ['T=0'] + only_t0 = (not protocols or protocols == ['T=0']) and not pps if not only_t0 and i < len(body): tck = body[i] check = 0 diff --git a/tests/test_decode.py b/tests/test_decode.py index 42d373d..f19a9f4 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -898,6 +898,19 @@ class TestAtrPps(unittest.TestCase): self.assertEqual(r['historical']['category'], 'status information (life cycle)') self.assertEqual(r['historical']['life_cycle'], 'operational state (activated)') + def test_atr_t15_is_pps_capability(self): + from simtrace2_pysniff.server.decode import _decode_atr + # Real UICC ATR: T0=9F (TA1+TD1, 15 hist bytes), TA1=95, TD1=80 (T=0), + # TD2=1F (T=15 → PPS capability), TA3=C7 (clock stop + classes A/B/C). + r = _decode_atr(bytes.fromhex('3B9F95801FC78031E073FE21136364035B83079000F7')) + self.assertEqual(r['protocols'], ['T=0']) + self.assertTrue(r['pps']) + ta3 = r['interface'][1] + self.assertEqual(ta3['name'], 'TA3') + self.assertEqual(ta3['clock_stop'], 'no preference') + self.assertEqual(ta3['classes'], ['A', 'B', 'C']) + self.assertTrue(r['tck_valid']) + def test_pps(self): from simtrace2_pysniff.server.decode import _decode_pps # FF 10 11 xx: PPS0=10 (T=0, PPS1 present), PPS1=11 (Fi=1,Di=1), PCK=XOR(FF,10,11)=FE diff --git a/tests/test_pcap_import.py b/tests/test_pcap_import.py index 84fd6e4..f676dbf 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.1') + self.assertEqual(__version__, '1.22.2') def test_status_includes_version(self): import os