From d4a378695c9817adef1c5b45cfb910f80b2fff58 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: Sun, 20 Sep 2026 14:06:40 +0300 Subject: [PATCH] Decode cross-spec "reserved" values via the sibling specs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - REFRESH '07'/'08': use the TS 31.111 §8.6 names ("Steering of Roaming" / "... for I-WLAN", per TS 23.122 / TS 24.234) instead of "Reserved (...)" - envelope tags 'E1'-'E3': TS 31.111 §9.1 leaves them reserved for future usage — drop the placeholder label and decode them as unknown - TR result '28' = Bearer Independent Protocol temporary error (TS 102 223 §8.12); '29' stays reserved - PLI qualifier '0B' = MEID of the terminal added; '05' documented as TS 31.111's Timing Advance (ETSI text says "Reserved for GSM") - ISO 7816-4:2005 Table 6: add '62 86' (sensor) and '63 81' (file filled) - drop the two fabricated '66xx' names (no such rows in the pinned specs) - tests: add the new value tests plus a guard that no decoder table emits a "Reserved ..." label - v2.1.1; SW cache simtrace-analyser-v56 --- frontend/index.html | 2 +- frontend/sw.js | 2 +- pyproject.toml | 2 +- simtrace_analyser/decode.py | 18 ++++++----- simtrace_analyser/version.py | 2 +- tests/test_decode.py | 63 ++++++++++++++++++++++++++++++++++++ tests/test_pcap_import.py | 2 +- 7 files changed, 78 insertions(+), 13 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index 6fbc192..92666a9 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2706,7 +2706,7 @@ function onFilterChange() { } // ──────────────────── Versioning ──────────────────── -const PWA_VERSION = '2.1.0'; +const PWA_VERSION = '2.1.1'; const MIN_SERVER_VERSION = '1.0.0'; function parseVersion(v) { diff --git a/frontend/sw.js b/frontend/sw.js index 547250b..49585dd 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simtrace-analyser-v55'; +const CACHE = 'simtrace-analyser-v56'; const PRECACHE = [ 'index.html', diff --git a/pyproject.toml b/pyproject.toml index 967b6ea..1939d7d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.backends._legacy:_Backend" [project] name = "simtrace-analyser" -version = "2.1.0" +version = "2.1.1" description = "SIMtrace Analyser — APDU trace analyser for GSMTAP/PCAP captures (bundles the simtrace2-pysniff utility)" readme = {file = "README.md", content-type = "text/markdown"} license = {text = "GPL-2.0-or-later"} diff --git a/simtrace_analyser/decode.py b/simtrace_analyser/decode.py index bedc37c..c1ef11e 100644 --- a/simtrace_analyser/decode.py +++ b/simtrace_analyser/decode.py @@ -17,18 +17,18 @@ SW_NAMES = { (0x62, 0x83): 'Warning — selected file deactivated', (0x62, 0x84): 'Warning — FCI not formatted per ISO', (0x62, 0x85): 'Warning — selected file in termination state', + (0x62, 0x86): 'No input data available from a sensor on the card', (0x62, 0xf1): 'More data available', (0x62, 0xf2): 'More data available and proactive command pending', (0x62, 0xf3): 'Response data available', (0x63, 0x00): 'Warning — no information', + (0x63, 0x81): 'File filled up by the last write', (0x63, 0xf1): 'More data expected', (0x63, 0xf2): 'More data expected and proactive command pending', (0x64, 0x00): 'Execution error — no information', (0x64, 0x01): 'Execution error — immediate response required', (0x65, 0x00): 'Execution error — no information, NV memory changed', (0x65, 0x81): 'Execution error — memory failure', - (0x66, 0x00): 'Security error — no information', - (0x66, 0x81): 'Reserved for security-related issues', (0x67, 0x00): 'Checking error — wrong length', (0x68, 0x00): 'Checking error — no information', (0x68, 0x81): 'Logical channel not supported', @@ -1030,8 +1030,8 @@ REFRESH_MODES = { 0x04: 'UICC Reset', 0x05: 'NAA Application Reset', 0x06: 'NAA Session Reset', - 0x07: 'Reserved (Steering of Roaming)', - 0x08: 'Reserved (Steering of Roaming for I-WLAN)', + 0x07: 'Steering of Roaming', + 0x08: 'Steering of Roaming for I-WLAN', 0x09: 'eUICC Profile State Change', 0x0A: 'Application Update', } @@ -1053,9 +1053,8 @@ ENVELOPE_TYPES = { 0xDE: 'ENVELOPE CONTAINER', 0xDF: 'PROSE REPORT', 0xE0: '5G PROSE REPORT', - 0xE1: 'Reserved for 3GPP', - 0xE2: 'Reserved for 3GPP', - 0xE3: 'Reserved for 3GPP', + # 'E1'-'E3': reserved for 3GPP (for future usage) per TS 31.111 §9.1 — + # no name assigned yet, so they decode as unknown. 0xE4: 'GSMA', } @@ -1100,12 +1099,14 @@ PLI_QUALIFIERS = { 0x02: 'Network Measurement results', 0x03: 'Date, time and time zone', 0x04: 'Language setting', + # ETSI TS 102 223: "Reserved for GSM" — defined by TS 31.111 §8.6. 0x05: 'Timing Advance', 0x06: 'Access Technology (single)', 0x07: 'ESN of the terminal', 0x08: 'IMEISV', 0x09: 'Search Mode', 0x0A: 'Battery charge state', + 0x0B: 'MEID of the terminal', 0x0C: 'Current WSID', 0x0D: 'Broadcast Network info', 0x0E: 'Multiple Access Technologies', @@ -1410,7 +1411,8 @@ TR_RESULTS = { 0x25: 'Interaction with call control by NAA, temporary problem', 0x26: 'Launch browser generic error', 0x27: 'MMS temporary problem', - # '28'-'29' reserved for 3GPP + 0x28: 'Bearer Independent Protocol temporary error', + # '29' reserved for 3GPP (for future usage) 0x30: 'Command beyond ME capabilities', 0x31: 'Command type not understood by ME', 0x32: 'Command data not understood by ME', diff --git a/simtrace_analyser/version.py b/simtrace_analyser/version.py index 9a35ae8..6d05c04 100644 --- a/simtrace_analyser/version.py +++ b/simtrace_analyser/version.py @@ -1,3 +1,3 @@ """Single source of truth for the SIMtrace Analyser version.""" -__version__ = '2.1.0' +__version__ = '2.1.1' diff --git a/tests/test_decode.py b/tests/test_decode.py index 85d4a07..2853dd0 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -136,6 +136,12 @@ class TestCommandTypeTables(unittest.TestCase): self.assertEqual(ENVELOPE_TYPES[0xD3], 'MENU SELECTION') self.assertEqual(ENVELOPE_TYPES[0xD6], 'EVENT DOWNLOAD') + def test_unassigned_envelope_tags_unnamed(self): + # TS 31.111 §9.1: 'E1'-'E3' reserved for 3GPP (for future usage); + # no name exists, so no placeholder label may be shown. + for tag in (0xE1, 0xE2, 0xE3): + self.assertNotIn(tag, ENVELOPE_TYPES) + class TestChangeAndFidiDecoding(unittest.TestCase): def test_change_reset_assert(self): @@ -215,6 +221,13 @@ class TestPliQualifier(unittest.TestCase): self.assertEqual(r['cat_command'], 'PROVIDE LOCAL INFORMATION') self.assertEqual(r['cmd']['qualifier'], '0x03 Date, time and time zone') + def test_fetch_pli_meid(self): + # '0B' = MEID of the terminal (TS 102 223 §8.6). + r = decode_message(bytes.fromhex( + '801200000BD009810301260B820281829000')) + self.assertEqual(r['cat_command'], 'PROVIDE LOCAL INFORMATION') + self.assertEqual(r['cmd']['qualifier'], '0x0B MEID of the terminal') + def test_non_pli_no_qualifier(self): r = decode_message(bytes.fromhex( '801400000C810301250002028281830100910F')) @@ -1757,6 +1770,23 @@ class TestSwWrongLength(unittest.TestCase): from simtrace_analyser.decode import decode_sw self.assertIsNone(decode_sw(bytes.fromhex('6205'))['name']) + def test_62_86_sensor_warning(self): + from simtrace_analyser.decode import decode_sw + self.assertEqual(decode_sw(bytes.fromhex('6286'))['name'], + 'No input data available from a sensor on the card') + + def test_63_81_file_filled_warning(self): + from simtrace_analyser.decode import decode_sw + self.assertEqual(decode_sw(bytes.fromhex('6381'))['name'], + 'File filled up by the last write') + + def test_66_class_not_fabricated(self): + # No '66xx' row exists in ISO 7816-4:2005 Table 6 or TS 102 221 + # tables 10.9/10.10 — the decoder must not invent a name. + from simtrace_analyser.decode import decode_sw + self.assertIsNone(decode_sw(bytes.fromhex('6600'))['name']) + self.assertIsNone(decode_sw(bytes.fromhex('6681'))['name']) + class TestStatusFcp(unittest.TestCase): def test_status_body_is_fcp(self): @@ -1849,6 +1879,32 @@ class TestRefresh(unittest.TestCase): self.assertEqual(r['cmd']['qualifier'], '0x01 File Change Notification') self.assertEqual(r['cmd']['file_list'], ['6F07', '6F20']) + def test_steering_of_roaming(self): + # ETSI "Reserved for GSM/3G" — TS 31.111 §8.6 defines the mode. + r = decode_message(bytes.fromhex('801200000bd0098103010107820281829000')) + self.assertEqual(r['cmd']['qualifier'], '0x07 Steering of Roaming') + + def test_steering_of_roaming_iwlan(self): + r = decode_message(bytes.fromhex('801200000bd0098103010108820281829000')) + self.assertEqual(r['cmd']['qualifier'], + '0x08 Steering of Roaming for I-WLAN') + + +class TestNoReservedLabels(unittest.TestCase): + # ETSI 'Reserved for GSM/3GPP' values must be resolved via the sibling + # spec or left unnamed — never surfaced as a 'Reserved ...' label. + + def test_no_reserved_labels(self): + from simtrace_analyser import decode as d + for name in ('SW_NAMES', 'REFRESH_MODES', 'ENVELOPE_TYPES', + 'TR_RESULTS', 'PLI_QUALIFIERS', 'CAT_COMMAND_TYPES', + 'EVENT_TYPES'): + table = getattr(d, name) + for value in table.values(): + if isinstance(value, str): + self.assertFalse(value.startswith('Reserved'), + f'{name}: {value}') + class TestIdleModeText(unittest.TestCase): """SET UP IDLE MODE TEXT (TS 102 223 §6.6.13) proactive command.""" @@ -2005,6 +2061,13 @@ class TestSpecRegistry(unittest.TestCase): self.assertEqual(TR_RESULTS[0x3C], 'Frames error') self.assertEqual(TR_RESULTS[0x3D], 'MMS error') + def test_tr_result_bip_temporary(self): + # '28' per TS 102 223 §8.12 (timer in Additional information). + from simtrace_analyser.decode import TR_RESULTS + self.assertEqual(TR_RESULTS[0x28], + 'Bearer Independent Protocol temporary error') + self.assertNotIn(0x29, TR_RESULTS) + def test_envelope_types_extended(self): from simtrace_analyser.decode import ENVELOPE_TYPES self.assertEqual(ENVELOPE_TYPES[0xD9], 'USSD DOWNLOAD') diff --git a/tests/test_pcap_import.py b/tests/test_pcap_import.py index 8c4dedb..890504b 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__, '2.1.0') + self.assertEqual(__version__, '2.1.1') def test_status_includes_version(self): import os