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 <[email protected]>
 #
@@ -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 <[email protected]>
 # Copyright (C) 2021 Harald Welte <[email protected]>
@@ -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):

--
To view, visit https://gerrit.osmocom.org/c/python/pyosmocom/+/43371?usp=email
To unsubscribe, or for help writing mail filters, visit 
https://gerrit.osmocom.org/settings?usp=email

Gerrit-MessageType: newchange
Gerrit-Project: python/pyosmocom
Gerrit-Branch: master
Gerrit-Change-Id: I4661352e39031dc0041fc7851f675d6083a1035f
Gerrit-Change-Number: 43371
Gerrit-PatchSet: 1
Gerrit-Owner: biocape <[email protected]>

Reply via email to