From e5a6000bfe2e4442b13f9073f324f650ec962fc3 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: Thu, 13 Aug 2026 10:24:59 +0300 Subject: [PATCH] parse_tlv: support BER-TLV long-form lengths Proactive commands with a body over 127 bytes (e.g. SET UP MENU with a long item list) use a long-form length (0x81 NN / 0x82 NN NN) in their D0 wrapper. Previously parse_tlv bailed on those, so the FETCH decoded as a bare 'FETCH' without its command name or items. Tests: 105 (long-form 1/2-byte, real SET UP MENU sample). --- simtrace2_pysniff/server/decode.py | 14 ++++++++++---- tests/test_decode.py | 27 +++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/simtrace2_pysniff/server/decode.py b/simtrace2_pysniff/server/decode.py index 0585826..7aafbac 100644 --- a/simtrace2_pysniff/server/decode.py +++ b/simtrace2_pysniff/server/decode.py @@ -454,8 +454,9 @@ ENVELOPE_TYPES = { def parse_tlv(data): """Parse a BER-TLV structure into a list of (tag, length, value) tuples. - Handles single-byte tags and single-byte lengths (< 128), which cover - the CAT proactive command and ENVELOPE structures we decode. + Handles single-byte tags and both short-form (< 128) and long-form + BER-TLV lengths (0x81 NN, 0x82 NN NN). Indefinite lengths (0x80) + and truncated data cause parsing to stop at that point. """ tlvs = [] i = 0 @@ -468,8 +469,13 @@ def parse_tlv(data): length = data[i] i += 1 if length & 0x80: - # long form length — not needed for top-level CAT decode - break + num_len = length & 0x7F + if num_len == 0: # indefinite length — not supported + break + if i + num_len > n: + break + length = int.from_bytes(data[i:i + num_len], 'big') + i += num_len if i + length > n: break value = data[i:i + length] diff --git a/tests/test_decode.py b/tests/test_decode.py index 27f85ab..c5d1ad6 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -26,6 +26,19 @@ class TestParseTlv(unittest.TestCase): def test_empty(self): self.assertEqual(parse_tlv(b''), []) + def test_long_form_length(self): + # D0 81 BC … — a proactive command whose length is 0xBC (188), + # encoded in BER-TLV long form. + inner = bytes.fromhex('8103012500') + b'\x82\x02\x81\x82' + data = b'\xd0' + b'\x81' + bytes([len(inner)]) + inner + self.assertEqual(parse_tlv(data), [(0xD0, len(inner), inner)]) + + def test_long_form_two_byte(self): + # 0x82 followed by two length bytes. + value = b'\x00' * 300 + data = b'\x70' + b'\x82\x01\x2c' + value + self.assertEqual(parse_tlv(data), [(0x70, 300, value)]) + class TestAuthenticateNaming(unittest.TestCase): def test_ins_88(self): @@ -421,6 +434,20 @@ class TestProactiveDecode(unittest.TestCase): self.assertEqual(r['cmd']['tpdu']['mti'], 'SMS-SUBMIT') self.assertEqual(r['cmd']['tpdu']['da'], '999') + def test_setup_menu_long_form_length(self): + # Real Mi A1 startup trace: D0 length encoded as 0x81 0xBC (188 bytes). + r = decode_message(bytes.fromhex( + '80120000BFD081BC810301250082028182051580004D0065006700610046' + '006F006E00500052004F8F100180041C0435043304300424043E043D8F18' + '02800420043004370432043B043504470435043D0438044F8F0C03800421' + '043F043E044004428F100480041D043E0432043E0441044204388F100580' + '04240438043D0430043D0441044B8F1006800421043F044004300432043A' + '04308F100780041E043104490435043D043804358F188080041A0430043B' + '043504390434043E0441043A043E043F9000')) + self.assertEqual(r['cat_command'], 'SET UP MENU') + self.assertEqual(r['cmd']['title'], 'MegaFonPRO') + self.assertEqual(r['cmd']['items'][0], {'id': 1, 'text': 'МегаФон'}) + class TestAnnexA(unittest.TestCase): def test_gsm_default(self):