biocape has uploaded this change for review. ( https://gerrit.osmocom.org/c/python/pyosmocom/+/43371?usp=email )
Change subject: Add GSM 7-bit packing and Network Name IE support ......................................................................
Add GSM 7-bit packing and Network Name IE support
Add gsm7_pack()/gsm7_unpack() (TS 23.038 Section 6.1.2.1) and a NetworkNameAdapter for the Network Name IE (TS 24.008 Section 10.5.3.5a), as used by EF.PNN and MM INFORMATION.
This is a separate adapter rather than a parameterization of GsmOrUcs2Adapter: the Network Name header octet clashes with the TS 102 221 Annex A magic bytes, e.g. a 10-septet GSM 7-bit name starts with 0x82, which Annex A reads as UCS-2 variant 3.
Change-Id: I4661352e39031dc0041fc7851f675d6083a1035f --- M src/osmocom/construct.py M src/osmocom/utils.py M tests/test_construct.py M tests/test_utils.py 4 files changed, 117 insertions(+), 3 deletions(-)
git pull ssh://gerrit.osmocom.org:29418/python/pyosmocom refs/changes/71/43371/1
diff --git a/src/osmocom/construct.py b/src/osmocom/construct.py index 548f22d..ef97943 100644 --- a/src/osmocom/construct.py +++ b/src/osmocom/construct.py @@ -18,7 +18,7 @@ from construct.core import evaluate from construct.lib import integertypes
-from osmocom.utils import b2h, h2b, swap_nibbles, int_bytes_required +from osmocom.utils import b2h, h2b, swap_nibbles, int_bytes_required, gsm7_pack, gsm7_unpack
# (C) 2021-2022 by Harald Welte laforge@osmocom.org # @@ -252,6 +252,39 @@ else: return _encode_variant1(obj)
+class NetworkNameAdapter(Adapter): + """convert the value part of a Network Name IE (TS 24.008 Section 10.5.3.5a) + to a string (and back). + + Wire format: + octet 1: header + bits 7-5: coding scheme + bit 4: add country initials + bits 3-1: spare bits in last octet + octet 2+: GSM 7-bit packed (TS 23.038) or UCS-2 encoded text + + The encoder prefers GSM 7-bit packed and falls back to UCS-2. + It sets the add country initials bit to 0.""" + def _decode(self, obj, context, path): + if len(obj) < 1: + return '' + coding_scheme = (obj[0] >> 4) & 0x07 + num_spare_bits = obj[0] & 0x07 + if coding_scheme == 0: + # GSM 7 bit default alphabet, packed + return codecs.decode(gsm7_unpack(obj[1:], num_spare_bits), 'gsm03.38') + if coding_scheme == 1: + # UCS-2 (16 bit) + return codecs.decode(obj[1:], 'utf_16_be') + raise ValueError('reserved Network Name coding scheme %u' % coding_scheme) + + def _encode(self, obj, context, path): + try: + packed, num_spare_bits = gsm7_pack(codecs.encode(obj, 'gsm03.38')) + return bytes([0x80 | num_spare_bits]) + packed + except ValueError: + return b'\x90' + codecs.encode(obj, 'utf_16_be') + class BcdAdapter(Adapter): """convert a bytes() type to a string of BCD nibbles."""
diff --git a/src/osmocom/utils.py b/src/osmocom/utils.py index 7963c05..def7cd7 100644 --- a/src/osmocom/utils.py +++ b/src/osmocom/utils.py @@ -8,7 +8,7 @@ import datetime import argparse from io import BytesIO -from typing import Optional, List, NewType +from typing import Optional, List, NewType, Tuple
# Copyright (C) 2009-2010 Sylvain Munaut tnt@246tNt.com # Copyright (C) 2021 Harald Welte laforge@osmocom.org @@ -109,6 +109,38 @@ return hexstr(''.join([x+y for x, y in zip(s[1::2], s[0::2])]))
+def gsm7_unpack(packed: bytes, num_spare_bits: int = 0) -> bytes: + """Unpack GSM 7-bit packed septets (3GPP TS 23.038 Section 6.1.2.1). + + Args: + packed : packed 7-bit data + num_spare_bits : number of spare bits in the last octet (0..7) + Returns: + unpacked bytes, one septet per byte + """ + packed_int = int.from_bytes(packed, byteorder='little') + num_septets = (len(packed) * 8 - num_spare_bits) // 7 + return bytes((packed_int >> (i * 7)) & 0x7f for i in range(num_septets)) + + +def gsm7_pack(septets: bytes) -> Tuple[bytes, int]: + """Pack septets into GSM 7-bit packed format (3GPP TS 23.038 Section 6.1.2.1). + + Args: + septets : one septet per byte; each byte must be within 0..127 + Returns: + tuple of (packed bytes, number of spare bits in the last octet) + """ + packed_int = 0 + for i, septet in enumerate(septets): + if septet > 0x7f: + raise ValueError('septet value 0x%02x exceeds 7 bits' % septet) + packed_int |= septet << (i * 7) + num_bits = len(septets) * 7 + num_bytes = (num_bits + 7) // 8 + return packed_int.to_bytes(num_bytes, byteorder='little'), num_bytes * 8 - num_bits + + def rpad(s: str, l: int, c='f') -> str: """pad string on the right side. Args: diff --git a/tests/test_construct.py b/tests/test_construct.py index 195732d..3ccd169 100755 --- a/tests/test_construct.py +++ b/tests/test_construct.py @@ -4,6 +4,7 @@ from osmocom.utils import b2h, h2b from osmocom.construct import Asn1DerInteger, Bytes, DnsAdapter, GreedyInteger, GreedyBytes, PlmnAdapter from osmocom.construct import StripHeaderAdapter, StripTrailerAdapter, Ucs2Adapter, filter_dict +from osmocom.construct import NetworkNameAdapter # pylint: disable=no-name-in-module from construct import FlagsEnum
@@ -79,6 +80,32 @@ re_enc = self.ad._encode(string, None, None) self.assertEqual(encoded, re_enc)
+class TestNetworkNameAdapter(unittest.TestCase): + ad = NetworkNameAdapter(GreedyBytes) + + testdata = [ + # GSM 7-bit packed, 2 spare bits + ( "Telekom.de", '82d432bbbc7eb75de432' ), + # GSM 7-bit packed, 4 spare bits + ( "Cape", '84c330bc0c' ), + # UCS-2, as the string is not representable in the GSM default alphabet + ( "МТС", '90041c04220421' ), + ] + + def test_data_decode(self): + for string, encoded_hex in self.testdata: + dec = self.ad._decode(h2b(encoded_hex), None, None) + self.assertEqual(dec, string) + + def test_data_encode(self): + for string, encoded_hex in self.testdata: + re_enc = self.ad._encode(string, None, None) + self.assertEqual(h2b(encoded_hex), re_enc) + + def test_reserved_coding_scheme(self): + with self.assertRaises(ValueError): + self.ad._decode(h2b('a0c330bc0c'), None, None) + class TestTrailerAdapter(unittest.TestCase): Privileges = FlagsEnum(StripTrailerAdapter(GreedyBytes, 3), security_domain=0x800000, dap_verification=0x400000, diff --git a/tests/test_utils.py b/tests/test_utils.py index 1b80011..ca9a42b 100755 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -18,7 +18,7 @@
import unittest
-from osmocom.utils import hexstr, int_bytes_required +from osmocom.utils import hexstr, int_bytes_required, gsm7_pack, gsm7_unpack
class TestHexstr(unittest.TestCase): def test_cmp(self): @@ -54,6 +54,28 @@ self.assertEqual(str(hexstr('ABCD')), 'abcd')
+class TestGsm7PackUnpack(unittest.TestCase): + # (packed, num_spare_bits, septets) + tests = [ + ( b'', 0, b'' ), + ( b'\x61', 1, b'a' ), + ( b'\xd4\x32\xbb\xbc\x7e\xb7\x5d\xe4\x32', 2, b'Telekom.de' ), + ( b'\xc8\x32\x9b\xfd\x06\x5d\xdf\x72\x36\x19', 3, b'Hello World' ), + ] + + def test_gsm7_unpack(self): + for packed, num_spare_bits, septets in self.tests: + self.assertEqual(gsm7_unpack(packed, num_spare_bits), septets) + + def test_gsm7_pack(self): + for packed, num_spare_bits, septets in self.tests: + self.assertEqual(gsm7_pack(septets), (packed, num_spare_bits)) + + def test_gsm7_pack_septet_out_of_range(self): + with self.assertRaises(ValueError): + gsm7_pack(b'\x80') + + class Test_int_bytes_required(unittest.TestCase):
def test_int_bytes_required(self):