Add codecs for EF_SPN and GSM strings via construct

This will replace the hand-crafted codec for EF_SPN
by a struct definition using the construct library.
Old encoders are updated and kept for API compatibility
but are not used internally anymore.

New data structures:
* Rpad(Adapter): Right-padded bytestring (0xff, adjustable)
* GsmStringAdapter(Adapter): Codec for "SMS default 7-bit
	coded alphabet as defined int TS 23.038" using
	the gsm0338 library.
* GsmString(n): Convenient wrapper of both above

Adjustments:
* utils: update+deprecate old dec_spn(), enc_spn()
* remove refs to deprecated functions

Change-Id: Ia1d3a3835933bac0002b7c52511481dd8094b994
diff --git a/pySim/construct.py b/pySim/construct.py
index b0f03b7..a903305 100644
--- a/pySim/construct.py
+++ b/pySim/construct.py
@@ -1,5 +1,6 @@
 from construct import *
 from pySim.utils import b2h, h2b, swap_nibbles
+import gsm0338
 
 """Utility code related to the integration of the 'construct' declarative parser."""
 
@@ -33,6 +34,42 @@
     def _encode(self, obj, context, path):
         return h2b(swap_nibbles(obj))
 
+class Rpad(Adapter):
+    """
+    Encoder appends padding bytes (b'\\xff') up to target size.
+    Decoder removes trailing padding bytes.
+
+    Parameters:
+        subcon: Subconstruct as defined by construct library
+        pattern: set padding pattern (default: b'\\xff')
+    """
+
+    def __init__(self, subcon, pattern=b'\xff'):
+        super().__init__(subcon)
+        self.pattern = pattern
+
+    def _decode(self, obj, context, path):
+        return obj.rstrip(self.pattern)
+
+    def _encode(self, obj, context, path):
+        if len(obj) > self.sizeof():
+            raise SizeofError("Input ({}) exceeds target size ({})".format(len(obj), self.sizeof()))
+        return obj + self.pattern * (self.sizeof() - len(obj))
+
+class GsmStringAdapter(Adapter):
+    """Convert GSM 03.38 encoded bytes to a string."""
+
+    def __init__(self, subcon, codec='gsm03.38', err='strict'):
+        super().__init__(subcon)
+        self.codec = codec
+        self.err = err
+
+    def _decode(self, obj, context, path):
+        return obj.decode(self.codec)
+
+    def _encode(self, obj, context, path):
+        return obj.encode(self.codec, self.err)
+
 def filter_dict(d, exclude_prefix='_'):
     """filter the input dict to ensure no keys starting with 'exclude_prefix' remain."""
     res = {}
@@ -88,3 +125,17 @@
         n (Integer): Number of bytes (default: 1)
     '''
     return Default(Bytes(n), __RFU_VALUE)
+
+def GsmString(n):
+    '''
+    GSM 03.38 encoded byte string of fixed length n.
+    Encoder appends padding bytes (b'\\xff') to maintain
+    length. Decoder removes those trailing bytes.
+
+    Exceptions are raised for invalid characters
+    and length excess.
+
+    Parameters:
+        n (Integer): Fixed length of the encoded byte string
+    '''
+    return GsmStringAdapter(Rpad(Bytes(n), pattern=b'\xff'), codec='gsm03.38')