blob: e1e28525b6d3c228a9ecc9d320e1a183fe022c06 [file] [log] [blame]
Harald Weltebb3b5df2021-05-24 23:15:54 +02001"""object-oriented TLV parser/encoder library."""
2
3# (C) 2021 by Harald Welte <laforge@osmocom.org>
4# All Rights Reserved
5#
6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation, either version 2 of the License, or
9# (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program. If not, see <http://www.gnu.org/licenses/>.
18
19
20from typing import Optional, List, Dict, Any, Tuple
21from bidict import bidict
22from construct import *
23
24from pySim.utils import bertlv_encode_len, bertlv_parse_len, bertlv_encode_tag, bertlv_parse_tag
25from pySim.utils import comprehensiontlv_encode_tag, comprehensiontlv_parse_tag
26from pySim.utils import bertlv_parse_one, comprehensiontlv_parse_one
27from pySim.utils import bertlv_parse_tag_raw, comprehensiontlv_parse_tag_raw
28
29from pySim.construct import parse_construct, LV, HexAdapter, BcdAdapter, BitsRFU, GsmStringAdapter
30from pySim.exceptions import *
31
32import inspect
33import abc
34
35class TlvMeta(abc.ABCMeta):
36 """Metaclass which we use to set some class variables at the time of defining a subclass.
37 This allows us to create subclasses for each TLV/IE type, where the class represents fixed
38 parameters like the tag/type and instances of it represent the actual TLV data."""
39 def __new__(metacls, name, bases, namespace, **kwargs):
40 #print("TlvMeta_new_(metacls=%s, name=%s, bases=%s, namespace=%s, kwargs=%s)" % (metacls, name, bases, namespace, kwargs))
41 x = super().__new__(metacls, name, bases, namespace)
42 # this becomes a _class_ variable, not an instance variable
43 x.tag = namespace.get('tag', kwargs.get('tag', None))
44 x.desc = namespace.get('desc', kwargs.get('desc', None))
45 nested = namespace.get('nested', kwargs.get('nested', None))
46 if nested is None or inspect.isclass(nested) and issubclass(nested, TLV_IE_Collection):
47 # caller has specified TLV_IE_Collection sub-class, we can directly reference it
48 x.nested_collection_cls = nested
49 else:
50 # caller passed list of other TLV classes that might possibly appear within us,
51 # build a dynamically-created TLV_IE_Collection sub-class and reference it
52 name = 'auto_collection_%s' % (name)
53 cls = type(name, (TLV_IE_Collection,), {'nested': nested})
54 x.nested_collection_cls = cls
55 return x
56
57class TlvCollectionMeta(abc.ABCMeta):
58 """Metaclass which we use to set some class variables at the time of defining a subclass.
59 This allows us to create subclasses for each Collection type, where the class represents fixed
60 parameters like the nested IE classes and instances of it represent the actual TLV data."""
61 def __new__(metacls, name, bases, namespace, **kwargs):
62 #print("TlvCollectionMeta_new_(metacls=%s, name=%s, bases=%s, namespace=%s, kwargs=%s)" % (metacls, name, bases, namespace, kwargs))
63 x = super().__new__(metacls, name, bases, namespace)
64 # this becomes a _class_ variable, not an instance variable
65 x.possible_nested = namespace.get('nested', kwargs.get('nested', None))
66 return x
67
68
69class Transcodable(abc.ABC):
70 _construct = None
71 """Base class for something that can be encoded + encoded. Decoding and Encoding happens either
72 * via a 'construct' object stored in a derived class' _construct variable, or
73 * via a 'construct' object stored in an instance _construct variable, or
74 * via a derived class' _{to,from}_bytes() methods."""
75 def __init__(self):
76 self.encoded = None
77 self.decoded = None
78 self._construct = None
79
80 def to_bytes(self) -> bytes:
81 """Convert from internal representation to binary bytes. Store the binary result
82 in the internal state and return it."""
Harald Welte04c13022021-10-21 10:02:10 +020083 if not self.decoded:
84 do = b''
85 elif self._construct:
Harald Weltebb3b5df2021-05-24 23:15:54 +020086 do = self._construct.build(self.decoded, total_len=None)
87 elif self.__class__._construct:
88 do = self.__class__._construct.build(self.decoded, total_len=None)
89 else:
90 do = self._to_bytes()
91 self.encoded = do
92 return do
93
94 # not an abstractmethod, as it is only required if no _construct exists
95 def _to_bytes(self):
96 raise NotImplementedError
97
98 def from_bytes(self, do:bytes):
99 """Convert from binary bytes to internal representation. Store the decoded result
100 in the internal state and return it."""
101 self.encoded = do
Harald Welte04c13022021-10-21 10:02:10 +0200102 if self.encoded == b'':
103 self.decoded = None
104 elif self._construct:
Harald Weltebb3b5df2021-05-24 23:15:54 +0200105 self.decoded = parse_construct(self._construct, do)
106 elif self.__class__._construct:
107 self.decoded = parse_construct(self.__class__._construct, do)
108 else:
109 self.decoded = self._from_bytes(do)
110 return self.decoded
111
112 # not an abstractmethod, as it is only required if no _construct exists
113 def _from_bytes(self, do:bytes):
114 raise NotImplementedError
115
116class IE(Transcodable, metaclass=TlvMeta):
117 # we specify the metaclass so any downstream subclasses will automatically use it
118 """Base class for various Information Elements. We understand the notion of a hierarchy
119 of IEs on top of the Transcodable class."""
120 # this is overridden by the TlvMeta metaclass, if it is used to create subclasses
121 nested_collection_cls = None
122 tag = None
123
124 def __init__(self, **kwargs):
125 super().__init__()
126 self.nested_collection = None
127 if self.nested_collection_cls:
128 self.nested_collection = self.nested_collection_cls()
129 # if we are a constructed IE, [ordered] list of actual child-IE instances
130 self.children = kwargs.get('children', [])
131 self.decoded = kwargs.get('decoded', None)
132
133 def __repr__(self):
134 """Return a string representing the [nested] IE data (for print)."""
135 if len(self.children):
136 member_strs = [repr(x) for x in self.children]
137 return '%s(%s)' % (type(self).__name__, ','.join(member_strs))
138 else:
139 return '%s(%s)' % (type(self).__name__, self.decoded)
140
141 def to_dict(self):
142 """Return a JSON-serializable dict representing the [nested] IE data."""
143 if len(self.children):
144 v = [x.to_dict() for x in self.children]
145 else:
146 v = self.decoded
147 return {type(self).__name__: v}
148
149 def from_dict(self, decoded:dict):
150 """Set the IE internal decoded representation to data from the argument.
151 If this is a nested IE, the child IE instance list is re-created."""
152 if self.nested_collection:
153 self.children = self.nested_collection.from_dict(decoded)
154 else:
155 self.children = []
156 self.decoded = decoded
157
158 def is_constructed(self):
159 """Is this IE constructed by further nested IEs?"""
160 if len(self.children):
161 return True
162 else:
163 return False
164
165 @abc.abstractmethod
166 def to_ie(self) -> bytes:
167 """Convert the internal representation to entire IE including IE header."""
168
169 def to_bytes(self) -> bytes:
170 """Convert the internal representation _of the value part_ to binary bytes."""
171 if self.is_constructed():
172 # concatenate the encoded IE of all children to form the value part
173 out = b''
174 for c in self.children:
175 out += c.to_ie()
176 return out
177 else:
178 return super().to_bytes()
179
180 def from_bytes(self, do:bytes):
181 """Parse _the value part_ from binary bytes to internal representation."""
182 if self.nested_collection:
183 self.children = self.nested_collection.from_bytes(do)
184 else:
185 self.children = []
186 return super().from_bytes(do)
187
188
189class TLV_IE(IE):
190 """Abstract base class for various TLV type Information Elements."""
191 def __init__(self, **kwargs):
192 super().__init__(**kwargs)
193
194 def _compute_tag(self) -> int:
195 """Compute the tag (sometimes the tag encodes part of the value)."""
196 return self.tag
197
198 @classmethod
199 @abc.abstractmethod
200 def _parse_tag_raw(cls, do:bytes) -> Tuple[int, bytes]:
201 """Obtain the raw TAG at the start of the bytes provided by the user."""
202
203 @classmethod
204 @abc.abstractmethod
205 def _parse_len(cls, do:bytes) -> Tuple[int, bytes]:
206 """Obtain the length encoded at the start of the bytes provided by the user."""
207
208 @abc.abstractmethod
209 def _encode_tag(self) -> bytes:
210 """Encode the tag part. Must be provided by derived (TLV format specific) class."""
211
212 @abc.abstractmethod
213 def _encode_len(self, val:bytes) -> bytes:
214 """Encode the length part assuming a certain binary value. Must be provided by
215 derived (TLV format specific) class."""
216
217 def to_ie(self):
218 return self.to_tlv()
219
220 def to_tlv(self):
221 """Convert the internal representation to binary TLV bytes."""
222 val = self.to_bytes()
223 return self._encode_tag() + self._encode_len(val) + val
224
225 def from_tlv(self, do:bytes):
226 (rawtag, remainder) = self.__class__._parse_tag_raw(do)
227 if rawtag:
228 if rawtag != self.tag:
229 raise ValueError("%s: Encountered tag %s doesn't match our supported tag %s" %
230 (self, rawtag, self.tag))
231 (length, remainder) = self.__class__._parse_len(remainder)
232 value = remainder[:length]
233 remainder = remainder[length:]
234 else:
235 value = do
236 remainder = b''
237 dec = self.from_bytes(value)
238 return dec, remainder
239
240
241class BER_TLV_IE(TLV_IE):
242 """TLV_IE formatted as ASN.1 BER described in ITU-T X.690 8.1.2."""
243 def __init__(self, **kwargs):
244 super().__init__(**kwargs)
245
246 @classmethod
247 def _decode_tag(cls, do:bytes) -> Tuple[dict, bytes]:
248 return bertlv_parse_tag(do)
249
250 @classmethod
251 def _parse_tag_raw(cls, do:bytes) -> Tuple[int, bytes]:
252 return bertlv_parse_tag_raw(do)
253
254 @classmethod
255 def _parse_len(cls, do:bytes) -> Tuple[int, bytes]:
256 return bertlv_parse_len(do)
257
258 def _encode_tag(self) -> bytes:
259 return bertlv_encode_tag(self._compute_tag())
260
261 def _encode_len(self, val:bytes) -> bytes:
262 return bertlv_encode_len(len(val))
263
264
265class COMPR_TLV_IE(TLV_IE):
266 """TLV_IE formated as COMPREHENSION-TLV as described in ETSI TS 101 220."""
267 def __init__(self, **kwargs):
268 super().__init__(**kwargs)
269 self.comprehension = False
270
271 @classmethod
272 def _decode_tag(cls, do:bytes) -> Tuple[dict, bytes]:
273 return comprehensiontlv_parse_tag(do)
274
275 @classmethod
276 def _parse_tag_raw(cls, do:bytes) -> Tuple[int, bytes]:
277 return comprehensiontlv_parse_tag_raw(do)
278
279 @classmethod
280 def _parse_len(cls, do:bytes) -> Tuple[int, bytes]:
281 return bertlv_parse_len(do)
282
283 def _encode_tag(self) -> bytes:
284 return comprehensiontlv_encode_tag(self._compute_tag())
285
286 def _encode_len(self, val:bytes) -> bytes:
287 return bertlv_encode_len(len(val))
288
289
290class TLV_IE_Collection(metaclass=TlvCollectionMeta):
291 # we specify the metaclass so any downstream subclasses will automatically use it
292 """A TLV_IE_Collection consists of multiple TLV_IE classes identified by their tags.
293 A given encoded DO may contain any of them in any order, and may contain multiple instances
294 of each DO."""
295 # this is overridden by the TlvCollectionMeta metaclass, if it is used to create subclasses
296 possible_nested = []
297 def __init__(self, desc=None, **kwargs):
298 self.desc = desc
299 #print("possible_nested: ", self.possible_nested)
300 self.members = kwargs.get('nested', self.possible_nested)
301 self.members_by_tag = {}
302 self.members_by_name = {}
303 self.members_by_tag = { m.tag:m for m in self.members }
304 self.members_by_name = { m.__name__:m for m in self.members }
305 # if we are a constructed IE, [ordered] list of actual child-IE instances
306 self.children = kwargs.get('children', [])
307 self.encoded = None
308
309 def __str__(self):
310 member_strs = [str(x) for x in self.members]
311 return '%s(%s)' % (type(self).__name__, ','.join(member_strs))
312
313 def __repr__(self):
314 member_strs = [repr(x) for x in self.members]
315 return '%s(%s)' % (self.__class__, ','.join(member_strs))
316
317 def __add__(self, other):
318 """Extending TLV_IE_Collections with other TLV_IE_Collections or TLV_IEs."""
319 if isinstance(other, TLV_IE_Collection):
320 # adding one collection to another
321 members = self.members + other.members
322 return TLV_IE_Collection(self.desc, nested=members)
323 elif inspect.isclass(other) and issubclass(other, TLV_IE):
324 # adding a member to a collection
325 return TLV_IE_Collection(self.desc, nested = self.members + [other])
326 else:
327 raise TypeError
328
329 def from_bytes(self, binary:bytes) -> List[TLV_IE]:
330 """Create a list of TLV_IEs from the collection based on binary input data.
331 Args:
332 binary : binary bytes of encoded data
333 Returns:
334 list of instances of TLV_IE sub-classes containing parsed data
335 """
336 self.encoded = binary
337 # list of instances of TLV_IE collection member classes appearing in the data
338 res = []
339 remainder = binary
340 first = next(iter(self.members_by_tag.values()))
341 # iterate until no binary trailer is left
342 while len(remainder):
343 # obtain the tag at the start of the remainder
344 tag, r = first._parse_tag_raw(remainder)
Harald Weltefb506212021-05-29 21:28:24 +0200345 if tag == None:
346 return res
Harald Weltebb3b5df2021-05-24 23:15:54 +0200347 if tag in self.members_by_tag:
348 cls = self.members_by_tag[tag]
349 # create an instance and parse accordingly
350 inst = cls()
351 dec, remainder = inst.from_tlv(remainder)
352 res.append(inst)
353 else:
354 # unknown tag; create the related class on-the-fly using the same base class
355 name = 'unknown_%s_%X' % (first.__base__.__name__, tag)
356 cls = type(name, (first.__base__,), {'tag':tag, 'possible_nested':[],
357 'nested_collection_cls':None})
358 cls._from_bytes = lambda s, a : {'raw': a.hex()}
359 cls._to_bytes = lambda s: bytes.fromhex(s.decoded['raw'])
360 # create an instance and parse accordingly
361 inst = cls()
362 dec, remainder = inst.from_tlv(remainder)
363 res.append(inst)
364 self.children = res
365 return res
366
367 def from_dict(self, decoded:List[dict]) -> List[TLV_IE]:
368 """Create a list of TLV_IE instances from the collection based on an array
369 of dicts, where they key indicates the name of the TLV_IE subclass to use."""
370 # list of instances of TLV_IE collection member classes appearing in the data
371 res = []
372 for i in decoded:
373 for k in i.keys():
374 if k in self.members_by_name:
375 cls = self.members_by_name[k]
Harald Welte58953802021-10-21 11:33:44 +0200376 inst = cls()
377 inst.from_dict(i[k])
Harald Weltebb3b5df2021-05-24 23:15:54 +0200378 res.append(inst)
379 else:
380 raise ValueError('%s: Unknown TLV Class %s in %s; expected %s' %
381 (self, i[0], decoded, self.members_by_name.keys()))
382 self.children = res
383 return res
384
385 def to_dict(self):
386 return [x.to_dict() for x in self.children]
387
388 def to_bytes(self):
389 out = b''
390 for c in self.children:
391 out += c.to_tlv()
392 return out
393
394 def from_tlv(self, do):
395 return self.from_bytes(do)
396
397 def to_tlv(self):
398 return self.to_bytes()