From 14445ae38f40960ce41b2a4cab9db6ed47d3c80b 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: Mon, 24 Aug 2026 13:03:31 +0300 Subject: [PATCH] Fix EF_SPDI decode crash: parse inner '80' TLVs, emit dict plmns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit stk_test1 msg 14036 regression: READ BINARY of EF_SPDI returned "No decoded data available" — _decode_spdi (added v1.16.0) parsed the A3 container value as bare 3-byte records (missing the inner '80' value TLVs) and returned a list of strings, while _build_summary/_file_summary expects the EF_PLMNwAcT shape (list of {mcc, mnc} dicts). The resulting TypeError aborted the whole message decode — this was the source of the 28 "Decode error ... string indices" messages seen across all sessions. _decode_spdi now unwraps inner '80' TLVs (bare-records fallback kept) and always returns {'plmns': [{mcc, mnc}...]}. Replay of all 33 sessions: decode exceptions 28 → 0 (2 remaining undecoded tpdu are 1-byte truncated capture records, not decodable by nature). Tests: +3 (standard inner-80 form, bare-records fallback, exact stk_test1 regression via decode_message). 344 total. Version 1.17.1, SW cache simtrace-analyser-v25. --- frontend/index.html | 2 +- frontend/sw.js | 2 +- pyproject.toml | 2 +- simtrace2_pysniff/__init__.py | 2 +- simtrace2_pysniff/server/decode.py | 31 +++++++++++++++++++++--------- tests/test_decode.py | 25 ++++++++++++++++++++++-- tests/test_pcap_import.py | 2 +- 7 files changed, 50 insertions(+), 16 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index d2a1c6b..60c9398 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1901,7 +1901,7 @@ function onFilterChange() { } // ──────────────────── Versioning ──────────────────── -const PWA_VERSION = '1.17.0'; +const PWA_VERSION = '1.17.1'; const MIN_SERVER_VERSION = '1.0.0'; function parseVersion(v) { diff --git a/frontend/sw.js b/frontend/sw.js index d4faf00..9540a82 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simtrace-analyser-v24'; +const CACHE = 'simtrace-analyser-v25'; const PRECACHE = [ 'index.html', diff --git a/pyproject.toml b/pyproject.toml index 3c7f5a6..7eef658 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.backends._legacy:_Backend" [project] name = "simtrace2-pysniff" -version = "1.17.0" +version = "1.17.1" 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 9790553..5a2c95a 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.17.0' +__version__ = '1.17.1' __all__ = [ 'SniffSession', 'DeviceDisconnected', diff --git a/simtrace2_pysniff/server/decode.py b/simtrace2_pysniff/server/decode.py index 98d9125..4a89460 100644 --- a/simtrace2_pysniff/server/decode.py +++ b/simtrace2_pysniff/server/decode.py @@ -3243,19 +3243,32 @@ def _decode_smsp(raw, p1=None): def _decode_spdi(raw, p1=None): - """EF_SPDI (§4.2.66): 'A3' container with PLMN records (§0.3).""" + """EF_SPDI (§4.2.66): 'A3' container (or bare '80' value tags) with + 3-byte PLMN records — same shape as EF_PLMNwAcT for _file_summary.""" if all(b == 0xff for b in raw): return {'empty': True} + plmns = [] + + def collect(value): + for i in range(0, len(value) - 2, 3): + plmn = _decode_plmn(value[i:i + 3]) + if plmn and plmn not in plmns: + plmns.append(plmn) + for tag, _length, value in parse_tlv(raw): if tag == 0xA3: - plmns = [] - for i in range(0, len(value) - 2, 3): - plmn = _decode_plmn(value[i:i + 3]) - if plmn: - plmns.append(f"{plmn['mcc']}-{plmn['mnc']}") - if plmns: - return {'plmns': plmns if len(plmns) > 1 else plmns[0]} - return {'raw': raw.hex().upper()} + got = False + for t2, _l2, v2 in parse_tlv(value): + if t2 == 0x80: + collect(v2) + got = True + if not got: + collect(value) # bare records inside A3 + elif tag == 0x80: + collect(value) + if not plmns: + return {'raw': raw.hex().upper()} + return {'plmns': plmns} def _decode_spn(raw, p1=None): diff --git a/tests/test_decode.py b/tests/test_decode.py index 7cad17b..cd00048 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -1976,8 +1976,29 @@ class TestNewFileDecoders(unittest.TestCase): def test_spdi(self): from simtrace2_pysniff.server.decode import _decode_spdi - r = _decode_spdi(bytes.fromhex('a30652f01000f110'), p1=None) - self.assertEqual(r['plmns'], ['250-01', '001-01']) + # Standard form: A3 wrapping inner '80' TLVs, one PLMN each + r = _decode_spdi(bytes.fromhex('a30a8003' '52f010' '8003' '72f010'), p1=None) + self.assertEqual(r['plmns'], [{'mcc': '250', 'mnc': '01'}, + {'mcc': '270', 'mnc': '01'}]) + + def test_spdi_bare_records_fallback(self): + from simtrace2_pysniff.server.decode import _decode_spdi + r = _decode_spdi(bytes.fromhex('a306' '52f010' '00f110'), p1=None) + self.assertEqual(r['plmns'], [{'mcc': '250', 'mnc': '01'}, + {'mcc': '001', 'mnc': '01'}]) + + def test_spdi_read_binary_regression(self): + # stk_test1 session msg 14036: READ BINARY of EF_SPDI crashed + # _build_summary (plmns were strings, not dicts) → whole message + # decoded as None ("No decoded data available"). + prev = {'sel': {'fid': '6fcd', 'name': 'EF_SPDI', + 'structure': 'transparent'}, + 'ins': 0xA4, 'ins_name': 'SELECT', 'sw1': '61'} + r = decode_message(bytes.fromhex( + '00b000000da305800352f020ffffffffffff9000'), prev=prev) + self.assertEqual(r['file']['plmns'], + [{'mcc': '250', 'mnc': '02'}]) + self.assertIn('250/02', r['summary']) def test_smsp(self): from simtrace2_pysniff.server.decode import _decode_smsp diff --git a/tests/test_pcap_import.py b/tests/test_pcap_import.py index 7e5ab53..2a32b32 100644 --- a/tests/test_pcap_import.py +++ b/tests/test_pcap_import.py @@ -197,7 +197,7 @@ class TestImportEndpoint(unittest.TestCase): class TestVersion(unittest.TestCase): def test_package_version(self): from simtrace2_pysniff import __version__ - self.assertEqual(__version__, '1.17.0') + self.assertEqual(__version__, '1.17.1') def test_status_includes_version(self): import os