fea2d9f6bf1e272b0e16ed8f9838613124186eba
[vpp.git] / src / vpp-api / python / vpp_papi / vpp_serializer.py
1 #
2 # Copyright (c) 2018 Cisco and/or its affiliates.
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at:
6 #
7 #     http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14 #
15 import collections
16 import logging
17 import socket
18 import struct
19 import sys
20
21 if sys.version_info <= (3, 4):
22     from aenum import IntEnum  # noqa: F401
23 else:
24     from enum import IntEnum  # noqa: F401
25
26 if sys.version_info <= (3, 6):
27     from aenum import IntFlag  # noqa: F401
28 else:
29
30     from enum import IntFlag  # noqa: F401
31
32 from . import vpp_format  # noqa: E402
33
34 #
35 # Set log-level in application by doing e.g.:
36 # logger = logging.getLogger('vpp_serializer')
37 # logger.setLevel(logging.DEBUG)
38 #
39 logger = logging.getLogger(__name__)
40
41 if sys.version[0] == '2':
42     def check(d): type(d) is dict
43 else:
44     def check(d): type(d) is dict or type(d) is bytes
45
46
47 def conversion_required(data, field_type):
48     if check(data):
49         return False
50     try:
51         if type(data).__name__ in vpp_format.conversion_table[field_type]:
52             return True
53     except KeyError:
54         return False
55
56
57 def conversion_packer(data, field_type):
58     t = type(data).__name__
59     return types[field_type].pack(vpp_format.
60                                   conversion_table[field_type][t](data))
61
62
63 def conversion_unpacker(data, field_type):
64     if field_type not in vpp_format.conversion_unpacker_table:
65         return data
66     return vpp_format.conversion_unpacker_table[field_type](data)
67
68
69 # TODO: post 20.01, remove inherit from object.
70 class Packer(object):
71     options = {}
72
73     def pack(self, data, kwargs):
74         raise NotImplementedError
75
76     def unpack(self, data, offset, result=None, ntc=False):
77         raise NotImplementedError
78
79     # override as appropriate in subclasses
80     def _get_packer_with_options(self, f_type, options):
81         return types[f_type]
82
83     def get_packer_with_options(self, f_type, options):
84         if options is not None:
85             try:
86                 return self._get_packer_with_options(f_type, options)
87             except IndexError:
88                 raise VPPSerializerValueError(
89                     "Options not supported for {}{} ({})".
90                         format(f_type, types[f_type].__class__,
91                                options))
92
93
94 class BaseTypes(Packer):
95     def __init__(self, type, elements=0, options=None):
96         self._type = type
97         self._elements = elements
98         base_types = {'u8': '>B',
99                       'i8': '>b',
100                       'string': '>s',
101                       'u16': '>H',
102                       'i16': '>h',
103                       'u32': '>I',
104                       'i32': '>i',
105                       'u64': '>Q',
106                       'i64': '>q',
107                       'f64': '=d',
108                       'bool': '>?',
109                       'header': '>HI'}
110
111         if elements > 0 and (type == 'u8' or type == 'string'):
112             self.packer = struct.Struct('>%ss' % elements)
113         else:
114             self.packer = struct.Struct(base_types[type])
115         self.size = self.packer.size
116         self.options = options
117
118     def pack(self, data, kwargs=None):
119         if data is None:  # Default to zero if not specified
120             if self.options and 'default' in self.options:
121                 data = self.options['default']
122             else:
123                 data = 0
124         return self.packer.pack(data)
125
126     def unpack(self, data, offset, result=None, ntc=False):
127         return self.packer.unpack_from(data, offset)[0], self.packer.size
128
129     def _get_packer_with_options(self, f_type, options):
130         c = types[f_type].__class__
131         return c(f_type, options=options)
132
133     def __repr__(self):
134         return "BaseTypes(type=%s, elements=%s, options=%s)" % (self._type,
135                                                                 self._elements,
136                                                                 self.options)
137
138
139 class String(Packer):
140     def __init__(self, name, num, options):
141         self.name = name
142         self.num = num
143         self.size = 1
144         self.length_field_packer = BaseTypes('u32')
145         self.limit = options['limit'] if 'limit' in options else num
146         self.fixed = True if num else False
147         if self.fixed and not self.limit:
148             raise VPPSerializerValueError(
149                 "Invalid argument length for: {}, {} maximum {}".
150                 format(list, len(list), self.limit))
151
152     def pack(self, list, kwargs=None):
153         if not list:
154             if self.fixed:
155                 return b"\x00" * self.limit
156             return self.length_field_packer.pack(0) + b""
157         if self.limit and len(list) > self.limit - 1:
158             raise VPPSerializerValueError(
159                 "Invalid argument length for: {}, {} maximum {}".
160                 format(list, len(list), self.limit - 1))
161         if self.fixed:
162             return list.encode('ascii').ljust(self.limit, b'\x00')
163         return self.length_field_packer.pack(len(list)) + list.encode('ascii')
164
165     def unpack(self, data, offset=0, result=None, ntc=False):
166         if self.fixed:
167             p = BaseTypes('u8', self.num)
168             s = p.unpack(data, offset)
169             s2 = s[0].split(b'\0', 1)[0]
170             return (s2.decode('ascii'), self.num)
171
172         length, length_field_size = self.length_field_packer.unpack(data,
173                                                                     offset)
174         if length == 0:
175             return '', 0
176         p = BaseTypes('u8', length)
177         x, size = p.unpack(data, offset + length_field_size)
178         #x2 = x.split(b'\0', 1)[0]
179         return (x.decode('ascii', errors='replace'), size + length_field_size)
180
181
182 types = {'u8': BaseTypes('u8'), 'u16': BaseTypes('u16'),
183          'u32': BaseTypes('u32'), 'i32': BaseTypes('i32'),
184          'u64': BaseTypes('u64'), 'f64': BaseTypes('f64'),
185          'bool': BaseTypes('bool'), 'string': String}
186
187 class_types = {}
188
189
190 def vpp_get_type(name):
191     try:
192         return types[name]
193     except KeyError:
194         return None
195
196
197 class VPPSerializerValueError(ValueError):
198     pass
199
200
201 class FixedList_u8(Packer):
202     def __init__(self, name, field_type, num):
203         self.name = name
204         self.num = num
205         self.packer = BaseTypes(field_type, num)
206         self.size = self.packer.size
207         self.field_type = field_type
208
209     def pack(self, data, kwargs=None):
210         """Packs a fixed length bytestring. Left-pads with zeros
211         if input data is too short."""
212         if not data:
213             return b'\x00' * self.size
214
215         if len(data) > self.num:
216             raise VPPSerializerValueError(
217                 'Fixed list length error for "{}", got: {}'
218                 ' expected: {}'
219                 .format(self.name, len(data), self.num))
220
221         try:
222             return self.packer.pack(data)
223         except struct.error:
224             raise VPPSerializerValueError(
225                 'Packing failed for "{}" {}'
226                 .format(self.name, kwargs))
227
228     def unpack(self, data, offset=0, result=None, ntc=False):
229         if len(data[offset:]) < self.num:
230             raise VPPSerializerValueError(
231                 'Invalid array length for "{}" got {}'
232                 ' expected {}'
233                 .format(self.name, len(data[offset:]), self.num))
234         return self.packer.unpack(data, offset)
235
236     def __repr__(self):
237         return "FixedList_u8(name=%s, field_type=%s, num=%s)" % (
238             self.name, self.field_type, self.num
239         )
240
241
242 class FixedList(Packer):
243     def __init__(self, name, field_type, num):
244         self.num = num
245         self.packer = types[field_type]
246         self.size = self.packer.size * num
247         self.name = name
248         self.field_type = field_type
249
250     def pack(self, list, kwargs):
251         if len(list) != self.num:
252             raise VPPSerializerValueError(
253                 'Fixed list length error, got: {} expected: {}'
254                 .format(len(list), self.num))
255         b = bytes()
256         for e in list:
257             b += self.packer.pack(e)
258         return b
259
260     def unpack(self, data, offset=0, result=None, ntc=False):
261         # Return a list of arguments
262         result = []
263         total = 0
264         for e in range(self.num):
265             x, size = self.packer.unpack(data, offset, ntc=ntc)
266             result.append(x)
267             offset += size
268             total += size
269         return result, total
270
271     def __repr__(self):
272         return "FixedList_(name=%s, field_type=%s, num=%s)" % (
273             self.name, self.field_type, self.num )
274
275
276 class VLAList(Packer):
277     def __init__(self, name, field_type, len_field_name, index):
278         self.name = name
279         self.field_type = field_type
280         self.index = index
281         self.packer = types[field_type]
282         self.size = self.packer.size
283         self.length_field = len_field_name
284
285     def pack(self, lst, kwargs=None):
286         if not lst:
287             return b""
288         if len(lst) != kwargs[self.length_field]:
289             raise VPPSerializerValueError(
290                 'Variable length error, got: {} expected: {}'
291                 .format(len(lst), kwargs[self.length_field]))
292
293         # u8 array
294         if self.packer.size == 1:
295             if isinstance(lst, list):
296                 return b''.join(lst)
297             return bytes(lst)
298
299         b = bytes()
300         for e in lst:
301             b += self.packer.pack(e)
302         return b
303
304     def unpack(self, data, offset=0, result=None, ntc=False):
305         # Return a list of arguments
306         total = 0
307
308         # u8 array
309         if self.packer.size == 1:
310             if result[self.index] == 0:
311                 return b'', 0
312             p = BaseTypes('u8', result[self.index])
313             return p.unpack(data, offset, ntc=ntc)
314
315         r = []
316         for e in range(result[self.index]):
317             x, size = self.packer.unpack(data, offset, ntc=ntc)
318             r.append(x)
319             offset += size
320             total += size
321         return r, total
322
323     def __repr__(self):
324         return "VLAList(name=%s, field_type=%s, " \
325                "len_field_name=%s, index=%s)" % (
326             self.name, self.field_type, self.length_field, self.index
327         )
328
329
330 class VLAList_legacy(Packer):
331     def __init__(self, name, field_type):
332         self.name = name
333         self.field_type = field_type
334         self.packer = types[field_type]
335         self.size = self.packer.size
336
337     def pack(self, list, kwargs=None):
338         if self.packer.size == 1:
339             return bytes(list)
340
341         b = bytes()
342         for e in list:
343             b += self.packer.pack(e)
344         return b
345
346     def unpack(self, data, offset=0, result=None, ntc=False):
347         total = 0
348         # Return a list of arguments
349         if (len(data) - offset) % self.packer.size:
350             raise VPPSerializerValueError(
351                 'Legacy Variable Length Array length mismatch.')
352         elements = int((len(data) - offset) / self.packer.size)
353         r = []
354         for e in range(elements):
355             x, size = self.packer.unpack(data, offset, ntc=ntc)
356             r.append(x)
357             offset += self.packer.size
358             total += size
359         return r, total
360
361     def __repr__(self):
362         return "VLAList_legacy(name=%s, field_type=%s)" % (
363             self.name, self.field_type
364         )
365
366
367 class VPPEnumType(Packer):
368     def __init__(self, name, msgdef, options=None):
369         self.size = types['u32'].size
370         self.name = name
371         self.enumtype = 'u32'
372         self.msgdef = msgdef
373         e_hash = {}
374         for f in msgdef:
375             if type(f) is dict and 'enumtype' in f:
376                 if f['enumtype'] != 'u32':
377                     self.size = types[f['enumtype']].size
378                     self.enumtype = f['enumtype']
379                 continue
380             ename, evalue = f
381             e_hash[ename] = evalue
382         self.enum = IntFlag(name, e_hash)
383         types[name] = self
384         class_types[name] = VPPEnumType
385         self.options = options
386
387     def __getattr__(self, name):
388         return self.enum[name]
389
390     def __bool__(self):
391         return True
392
393     # TODO: Remove post 20.01.
394     if sys.version[0] == '2':
395         __nonzero__ = __bool__
396
397     def pack(self, data, kwargs=None):
398         if data is None:  # Default to zero if not specified
399             if self.options and 'default' in self.options:
400                 data = self.options['default']
401             else:
402                 data = 0
403
404         return types[self.enumtype].pack(data)
405
406     def unpack(self, data, offset=0, result=None, ntc=False):
407         x, size = types[self.enumtype].unpack(data, offset)
408         return self.enum(x), size
409
410     def _get_packer_with_options(self, f_type, options):
411         c = types[f_type].__class__
412         return c(f_type, types[f_type].msgdef, options=options)
413
414     def __repr__(self):
415         return "VPPEnumType(name=%s, msgdef=%s, options=%s)" % (
416             self.name, self.msgdef, self.options
417         )
418
419
420 class VPPUnionType(Packer):
421     def __init__(self, name, msgdef):
422         self.name = name
423         self.msgdef = msgdef
424         self.size = 0
425         self.maxindex = 0
426         fields = []
427         self.packers = collections.OrderedDict()
428         for i, f in enumerate(msgdef):
429             if type(f) is dict and 'crc' in f:
430                 self.crc = f['crc']
431                 continue
432             f_type, f_name = f
433             if f_type not in types:
434                 logger.debug('Unknown union type {}'.format(f_type))
435                 raise VPPSerializerValueError(
436                     'Unknown message type {}'.format(f_type))
437             fields.append(f_name)
438             size = types[f_type].size
439             self.packers[f_name] = types[f_type]
440             if size > self.size:
441                 self.size = size
442                 self.maxindex = i
443
444         types[name] = self
445         self.tuple = collections.namedtuple(name, fields, rename=True)
446
447     # Union of variable length?
448     def pack(self, data, kwargs=None):
449         if not data:
450             return b'\x00' * self.size
451
452         for k, v in data.items():
453             logger.debug("Key: {} Value: {}".format(k, v))
454             b = self.packers[k].pack(v, kwargs)
455             break
456         r = bytearray(self.size)
457         r[:len(b)] = b
458         return r
459
460     def unpack(self, data, offset=0, result=None, ntc=False):
461         r = []
462         maxsize = 0
463         for k, p in self.packers.items():
464             x, size = p.unpack(data, offset, ntc=ntc)
465             if size > maxsize:
466                 maxsize = size
467             r.append(x)
468         return self.tuple._make(r), maxsize
469
470     def __repr__(self):
471         return"VPPUnionType(name=%s, msgdef=%r)" % (self.name, self.msgdef)
472
473
474 class VPPTypeAlias(Packer):
475     def __init__(self, name, msgdef, options=None):
476         self.name = name
477         self.msgdef = msgdef
478         t = vpp_get_type(msgdef['type'])
479         if not t:
480             raise ValueError('No such type: {}'.format(msgdef['type']))
481         if 'length' in msgdef:
482             if msgdef['length'] == 0:
483                 raise ValueError()
484             if msgdef['type'] == 'u8':
485                 self.packer = FixedList_u8(name, msgdef['type'],
486                                            msgdef['length'])
487                 self.size = self.packer.size
488             else:
489                 self.packer = FixedList(name, msgdef['type'], msgdef['length'])
490         else:
491             self.packer = t
492             self.size = t.size
493
494         types[name] = self
495         self.toplevelconversion = False
496         self.options = options
497
498     def pack(self, data, kwargs=None):
499         if data and conversion_required(data, self.name):
500             try:
501                 return conversion_packer(data, self.name)
502             # Python 2 and 3 raises different exceptions from inet_pton
503             except(OSError, socket.error, TypeError):
504                 pass
505         if data is None:  # Default to zero if not specified
506             if self.options and 'default' in self.options:
507                 data = self.options['default']
508             else:
509                 data = 0
510
511         return self.packer.pack(data, kwargs)
512
513     def _get_packer_with_options(self, f_type, options):
514         c = types[f_type].__class__
515         return c(f_type, types[f_type].msgdef, options=options)
516
517     def unpack(self, data, offset=0, result=None, ntc=False):
518         if ntc is False and self.name in vpp_format.conversion_unpacker_table:
519             # Disable type conversion for dependent types
520             ntc = True
521             self.toplevelconversion = True
522         t, size = self.packer.unpack(data, offset, result, ntc=ntc)
523         if self.toplevelconversion:
524             self.toplevelconversion = False
525             return conversion_unpacker(t, self.name), size
526         return t, size
527
528     def __repr__(self):
529         return "VPPTypeAlias(name=%s, msgdef=%s, options=%s)" % (
530             self.name, self.msgdef, self.options)
531
532
533 class VPPType(Packer):
534     # Set everything up to be able to pack / unpack
535     def __init__(self, name, msgdef):
536         self.name = name
537         self.msgdef = msgdef
538         self.packers = []
539         self.fields = []
540         self.fieldtypes = []
541         self.field_by_name = {}
542         size = 0
543         for i, f in enumerate(msgdef):
544             if type(f) is dict and 'crc' in f:
545                 self.crc = f['crc']
546                 continue
547             f_type, f_name = f[:2]
548             self.fields.append(f_name)
549             self.field_by_name[f_name] = None
550             self.fieldtypes.append(f_type)
551             if f_type not in types:
552                 logger.debug('Unknown type {}'.format(f_type))
553                 raise VPPSerializerValueError(
554                     'Unknown message type {}'.format(f_type))
555
556             fieldlen = len(f)
557             options = [x for x in f if type(x) is dict]
558             if len(options):
559                 self.options = options[0]
560                 fieldlen -= 1
561             else:
562                 self.options = {}
563             if fieldlen == 3:  # list
564                 list_elements = f[2]
565                 if list_elements == 0:
566                     if f_type == 'string':
567                         p = String(f_name, 0, self.options)
568                     else:
569                         p = VLAList_legacy(f_name, f_type)
570                     self.packers.append(p)
571                 elif f_type == 'u8':
572                     p = FixedList_u8(f_name, f_type, list_elements)
573                     self.packers.append(p)
574                     size += p.size
575                 elif f_type == 'string':
576                     p = String(f_name, list_elements, self.options)
577                     self.packers.append(p)
578                     size += p.size
579                 else:
580                     p = FixedList(f_name, f_type, list_elements)
581                     self.packers.append(p)
582                     size += p.size
583             elif fieldlen == 4:  # Variable length list
584                 length_index = self.fields.index(f[3])
585                 p = VLAList(f_name, f_type, f[3], length_index)
586                 self.packers.append(p)
587             else:
588                 # default support for types that decay to basetype
589                 if 'default' in self.options:
590                     p = self.get_packer_with_options(f_type, self.options)
591                 else:
592                     p = types[f_type]
593
594                 self.packers.append(p)
595                 size += p.size
596
597         self.size = size
598         self.tuple = collections.namedtuple(name, self.fields, rename=True)
599         types[name] = self
600         self.toplevelconversion = False
601
602     def pack(self, data, kwargs=None):
603         if not kwargs:
604             kwargs = data
605         b = bytes()
606
607         # Try one of the format functions
608         if data and conversion_required(data, self.name):
609             return conversion_packer(data, self.name)
610
611         for i, a in enumerate(self.fields):
612             if data and type(data) is not dict and a not in data:
613                 raise VPPSerializerValueError(
614                     "Invalid argument: {} expected {}.{}".
615                     format(data, self.name, a))
616
617             # Defaulting to zero.
618             if not data or a not in data:  # Default to 0
619                 arg = None
620                 kwarg = None  # No default for VLA
621             else:
622                 arg = data[a]
623                 kwarg = kwargs[a] if a in kwargs else None
624             if isinstance(self.packers[i], VPPType):
625                 b += self.packers[i].pack(arg, kwarg)
626             else:
627                 b += self.packers[i].pack(arg, kwargs)
628
629         return b
630
631     def unpack(self, data, offset=0, result=None, ntc=False):
632         # Return a list of arguments
633         result = []
634         total = 0
635         if ntc is False and self.name in vpp_format.conversion_unpacker_table:
636             # Disable type conversion for dependent types
637             ntc = True
638             self.toplevelconversion = True
639
640         for p in self.packers:
641             x, size = p.unpack(data, offset, result, ntc)
642             if type(x) is tuple and len(x) == 1:
643                 x = x[0]
644             result.append(x)
645             offset += size
646             total += size
647         t = self.tuple._make(result)
648
649         if self.toplevelconversion:
650             self.toplevelconversion = False
651             t = conversion_unpacker(t, self.name)
652         return t, total
653
654     def __repr__(self):
655         return "%s(name=%s, msgdef=%s)" % (
656             self.__class__.__name__, self.name, self.msgdef
657         )
658
659
660 class VPPMessage(VPPType):
661     pass