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