blob: 6daa66a8c389516b5f539c59f22ebb5926395926 [file] [log] [blame]
Harald Weltec91085e2022-02-10 18:05:45 +01001from construct.lib.containers import Container, ListContainer
2from construct.core import EnumIntegerString
Harald Welte07c7b1f2021-05-28 22:01:29 +02003import typing
Harald Weltee0f9ef12021-04-10 17:22:35 +02004from construct import *
Harald Welted0519e02022-02-11 18:05:48 +01005from construct.core import evaluate, bytes2integer, integer2bytes
6from construct.lib import integertypes
Harald Welte2db5cfb2021-04-10 19:05:37 +02007from pySim.utils import b2h, h2b, swap_nibbles
Robert Falkenbergb07a3e92021-05-07 15:23:20 +02008import gsm0338
Harald Weltee0f9ef12021-04-10 17:22:35 +02009
10"""Utility code related to the integration of the 'construct' declarative parser."""
11
Harald Welted0519e02022-02-11 18:05:48 +010012# (C) 2021-2022 by Harald Welte <laforge@osmocom.org>
Harald Weltee0f9ef12021-04-10 17:22:35 +020013#
14# This program is free software: you can redistribute it and/or modify
15# it under the terms of the GNU General Public License as published by
16# the Free Software Foundation, either version 2 of the License, or
17# (at your option) any later version.
18#
19# This program is distributed in the hope that it will be useful,
20# but WITHOUT ANY WARRANTY; without even the implied warranty of
21# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22# GNU General Public License for more details.
23#
24# You should have received a copy of the GNU General Public License
25# along with this program. If not, see <http://www.gnu.org/licenses/>.
26
27
28class HexAdapter(Adapter):
29 """convert a bytes() type to a string of hex nibbles."""
Harald Weltec91085e2022-02-10 18:05:45 +010030
Harald Weltee0f9ef12021-04-10 17:22:35 +020031 def _decode(self, obj, context, path):
32 return b2h(obj)
Harald Weltec91085e2022-02-10 18:05:45 +010033
Harald Weltee0f9ef12021-04-10 17:22:35 +020034 def _encode(self, obj, context, path):
35 return h2b(obj)
36
Harald Weltec91085e2022-02-10 18:05:45 +010037
Harald Welte2db5cfb2021-04-10 19:05:37 +020038class BcdAdapter(Adapter):
39 """convert a bytes() type to a string of BCD nibbles."""
Harald Weltec91085e2022-02-10 18:05:45 +010040
Harald Welte2db5cfb2021-04-10 19:05:37 +020041 def _decode(self, obj, context, path):
42 return swap_nibbles(b2h(obj))
Harald Weltec91085e2022-02-10 18:05:45 +010043
Harald Welte2db5cfb2021-04-10 19:05:37 +020044 def _encode(self, obj, context, path):
45 return h2b(swap_nibbles(obj))
46
Harald Weltec91085e2022-02-10 18:05:45 +010047
Robert Falkenbergb07a3e92021-05-07 15:23:20 +020048class Rpad(Adapter):
49 """
50 Encoder appends padding bytes (b'\\xff') up to target size.
51 Decoder removes trailing padding bytes.
52
53 Parameters:
54 subcon: Subconstruct as defined by construct library
55 pattern: set padding pattern (default: b'\\xff')
56 """
57
58 def __init__(self, subcon, pattern=b'\xff'):
59 super().__init__(subcon)
60 self.pattern = pattern
61
62 def _decode(self, obj, context, path):
63 return obj.rstrip(self.pattern)
64
65 def _encode(self, obj, context, path):
66 if len(obj) > self.sizeof():
Harald Weltec91085e2022-02-10 18:05:45 +010067 raise SizeofError("Input ({}) exceeds target size ({})".format(
68 len(obj), self.sizeof()))
Robert Falkenbergb07a3e92021-05-07 15:23:20 +020069 return obj + self.pattern * (self.sizeof() - len(obj))
70
Harald Weltec91085e2022-02-10 18:05:45 +010071
Robert Falkenbergb07a3e92021-05-07 15:23:20 +020072class GsmStringAdapter(Adapter):
73 """Convert GSM 03.38 encoded bytes to a string."""
74
75 def __init__(self, subcon, codec='gsm03.38', err='strict'):
76 super().__init__(subcon)
77 self.codec = codec
78 self.err = err
79
80 def _decode(self, obj, context, path):
81 return obj.decode(self.codec)
82
83 def _encode(self, obj, context, path):
84 return obj.encode(self.codec, self.err)
85
Harald Weltec91085e2022-02-10 18:05:45 +010086
Harald Weltee0f9ef12021-04-10 17:22:35 +020087def filter_dict(d, exclude_prefix='_'):
88 """filter the input dict to ensure no keys starting with 'exclude_prefix' remain."""
Harald Welte7fca85b2021-05-29 21:27:46 +020089 if not isinstance(d, dict):
90 return d
Harald Weltee0f9ef12021-04-10 17:22:35 +020091 res = {}
92 for (key, value) in d.items():
93 if key.startswith(exclude_prefix):
94 continue
95 if type(value) is dict:
96 res[key] = filter_dict(value)
97 else:
98 res[key] = value
99 return res
100
Harald Welte07c7b1f2021-05-28 22:01:29 +0200101
102def normalize_construct(c):
103 """Convert a construct specific type to a related base type, mostly useful
104 so we can serialize it."""
105 # we need to include the filter_dict as we otherwise get elements like this
106 # in the dict: '_io': <_io.BytesIO object at 0x7fdb64e05860> which we cannot json-serialize
107 c = filter_dict(c)
108 if isinstance(c, Container) or isinstance(c, dict):
Harald Weltec91085e2022-02-10 18:05:45 +0100109 r = {k: normalize_construct(v) for (k, v) in c.items()}
Harald Welte07c7b1f2021-05-28 22:01:29 +0200110 elif isinstance(c, ListContainer):
111 r = [normalize_construct(x) for x in c]
112 elif isinstance(c, list):
113 r = [normalize_construct(x) for x in c]
114 elif isinstance(c, EnumIntegerString):
115 r = str(c)
116 else:
117 r = c
118 return r
119
Harald Weltec91085e2022-02-10 18:05:45 +0100120
121def parse_construct(c, raw_bin_data: bytes, length: typing.Optional[int] = None, exclude_prefix: str = '_'):
Harald Welte07c7b1f2021-05-28 22:01:29 +0200122 """Helper function to wrap around normalize_construct() and filter_dict()."""
123 if not length:
124 length = len(raw_bin_data)
125 parsed = c.parse(raw_bin_data, total_len=length)
126 return normalize_construct(parsed)
127
Harald Weltec91085e2022-02-10 18:05:45 +0100128
Harald Weltee0f9ef12021-04-10 17:22:35 +0200129# here we collect some shared / common definitions of data types
130LV = Prefixed(Int8ub, HexAdapter(GreedyBytes))
Robert Falkenberg9d16fbc2021-04-12 11:43:22 +0200131
132# Default value for Reserved for Future Use (RFU) bits/bytes
133# See TS 31.101 Sec. "3.4 Coding Conventions"
134__RFU_VALUE = 0
135
136# Field that packs Reserved for Future Use (RFU) bit
137FlagRFU = Default(Flag, __RFU_VALUE)
138
139# Field that packs Reserved for Future Use (RFU) byte
140ByteRFU = Default(Byte, __RFU_VALUE)
141
142# Field that packs all remaining Reserved for Future Use (RFU) bytes
143GreedyBytesRFU = Default(GreedyBytes, b'')
144
Harald Weltec91085e2022-02-10 18:05:45 +0100145
Robert Falkenberg9d16fbc2021-04-12 11:43:22 +0200146def BitsRFU(n=1):
147 '''
148 Field that packs Reserved for Future Use (RFU) bit(s)
149 as defined in TS 31.101 Sec. "3.4 Coding Conventions"
150
151 Use this for (currently) unused/reserved bits whose contents
152 should be initialized automatically but should not be cleared
153 in the future or when restoring read data (unlike padding).
154
155 Parameters:
156 n (Integer): Number of bits (default: 1)
157 '''
158 return Default(BitsInteger(n), __RFU_VALUE)
159
Harald Weltec91085e2022-02-10 18:05:45 +0100160
Robert Falkenberg9d16fbc2021-04-12 11:43:22 +0200161def BytesRFU(n=1):
162 '''
163 Field that packs Reserved for Future Use (RFU) byte(s)
164 as defined in TS 31.101 Sec. "3.4 Coding Conventions"
165
166 Use this for (currently) unused/reserved bytes whose contents
167 should be initialized automatically but should not be cleared
168 in the future or when restoring read data (unlike padding).
169
170 Parameters:
171 n (Integer): Number of bytes (default: 1)
172 '''
173 return Default(Bytes(n), __RFU_VALUE)
Robert Falkenbergb07a3e92021-05-07 15:23:20 +0200174
Harald Weltec91085e2022-02-10 18:05:45 +0100175
Robert Falkenbergb07a3e92021-05-07 15:23:20 +0200176def GsmString(n):
177 '''
178 GSM 03.38 encoded byte string of fixed length n.
179 Encoder appends padding bytes (b'\\xff') to maintain
180 length. Decoder removes those trailing bytes.
181
182 Exceptions are raised for invalid characters
183 and length excess.
184
185 Parameters:
186 n (Integer): Fixed length of the encoded byte string
187 '''
188 return GsmStringAdapter(Rpad(Bytes(n), pattern=b'\xff'), codec='gsm03.38')
Harald Welted0519e02022-02-11 18:05:48 +0100189
190class GreedyInteger(Construct):
191 """A variable-length integer implementation, think of combining GrredyBytes with BytesInteger."""
192 def __init__(self, signed=False, swapped=False):
193 super().__init__()
194 self.signed = signed
195 self.swapped = swapped
196
197 def _parse(self, stream, context, path):
198 data = stream_read_entire(stream, path)
199 if evaluate(self.swapped, context):
200 data = swapbytes(data)
201 try:
202 return bytes2integer(data, self.signed)
203 except ValueError as e:
204 raise IntegerError(str(e), path=path)
205
206 def __bytes_required(self, i):
207 if self.signed:
208 raise NotImplementedError("FIXME: Implement support for encoding signed integer")
209 nbytes = 1
210 while True:
211 i = i >> 8
212 if i == 0:
213 return nbytes
214 else:
215 nbytes = nbytes + 1
216 # this should never happen, above loop must return eventually...
217 raise IntegerError(f"value {i} is out of range")
218
219 def _build(self, obj, stream, context, path):
220 if not isinstance(obj, integertypes):
221 raise IntegerError(f"value {obj} is not an integer", path=path)
222 length = self.__bytes_required(obj)
223 try:
224 data = integer2bytes(obj, length, self.signed)
225 except ValueError as e:
226 raise IntegerError(str(e), path=path)
227 if evaluate(self.swapped, context):
228 data = swapbytes(data)
229 stream_write(stream, data, length, path)
230 return obj