API: Use string type instead of u8.
[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
16 import struct
17 import collections
18 from enum import IntEnum
19 import logging
20 from . import vpp_format
21 import ipaddress
22 import sys
23 import socket
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     check = lambda d: type(d) is dict
34 else:
35     check = lambda d: type(d) is dict or type(d) is bytes
36
37 def conversion_required(data, field_type):
38     if check(data):
39         return False
40     try:
41         if type(data).__name__ in vpp_format.conversion_table[field_type]:
42             return True
43     except KeyError:
44         return False
45
46
47 def conversion_packer(data, field_type):
48     t = type(data).__name__
49     return types[field_type].pack(vpp_format.
50                                   conversion_table[field_type][t](data))
51
52
53 def conversion_unpacker(data, field_type):
54     if field_type not in vpp_format.conversion_unpacker_table:
55         return data
56     return vpp_format.conversion_unpacker_table[field_type](data)
57
58
59 class BaseTypes(object):
60     def __init__(self, type, elements=0):
61         base_types = {'u8': '>B',
62                       'string': '>s',
63                       'u16': '>H',
64                       'u32': '>I',
65                       'i32': '>i',
66                       'u64': '>Q',
67                       'f64': '>d',
68                       'bool': '>?',
69                       'header': '>HI'}
70
71         if elements > 0 and (type == 'u8' or type == 'string'):
72             self.packer = struct.Struct('>%ss' % elements)
73         else:
74             self.packer = struct.Struct(base_types[type])
75         self.size = self.packer.size
76
77     def pack(self, data, kwargs=None):
78         if not data:  # Default to zero if not specified
79             data = 0
80         return self.packer.pack(data)
81
82     def unpack(self, data, offset, result=None, ntc=False):
83         return self.packer.unpack_from(data, offset)[0], self.packer.size
84
85
86 class String(object):
87     def __init__(self):
88         self.name = 'string'
89         self.size = 1
90         self.length_field_packer = BaseTypes('u32')
91
92     def pack(self, list, kwargs=None):
93         if not list:
94             return self.length_field_packer.pack(0) + b""
95         return self.length_field_packer.pack(len(list)) + list.encode('utf8')
96
97     def unpack(self, data, offset=0, result=None, ntc=False):
98         length, length_field_size = self.length_field_packer.unpack(data,
99                                                                     offset)
100         if length == 0:
101             return b'', 0
102         p = BaseTypes('u8', length)
103         x, size = p.unpack(data, offset + length_field_size)
104         x2 = x.split(b'\0',1)[0]
105         return (x2.decode('utf8'), size + length_field_size)
106
107
108 types = {'u8': BaseTypes('u8'), 'u16': BaseTypes('u16'),
109          'u32': BaseTypes('u32'), 'i32': BaseTypes('i32'),
110          'u64': BaseTypes('u64'), 'f64': BaseTypes('f64'),
111          'bool': BaseTypes('bool'), 'string': String()}
112
113
114 def vpp_get_type(name):
115     try:
116         return types[name]
117     except KeyError:
118         return None
119
120
121 class VPPSerializerValueError(ValueError):
122     pass
123
124
125 class FixedList_u8(object):
126     def __init__(self, name, field_type, num):
127         self.name = name
128         self.num = num
129         self.packer = BaseTypes(field_type, num)
130         self.size = self.packer.size
131         self.field_type = field_type
132
133     def pack(self, data, kwargs=None):
134         """Packs a fixed length bytestring. Left-pads with zeros
135         if input data is too short."""
136         if not data:
137             return b'\x00' * self.size
138
139         if len(data) > self.num:
140             raise VPPSerializerValueError(
141                 'Fixed list length error for "{}", got: {}'
142                 ' expected: {}'
143                 .format(self.name, len(data), self.num))
144
145         return self.packer.pack(data)
146
147     def unpack(self, data, offset=0, result=None, ntc=False):
148         if len(data[offset:]) < self.num:
149             raise VPPSerializerValueError(
150                 'Invalid array length for "{}" got {}'
151                 ' expected {}'
152                 .format(self.name, len(data[offset:]), self.num))
153         if self.field_type == 'string':
154             s = self.packer.unpack(data, offset)
155             s2 = s[0].split(b'\0', 1)[0]
156             return (s2.decode('utf-8'), self.num)
157         return self.packer.unpack(data, offset)
158
159
160 class FixedList(object):
161     def __init__(self, name, field_type, num):
162         self.num = num
163         self.packer = types[field_type]
164         self.size = self.packer.size * num
165         self.name = name
166         self.field_type = field_type
167
168     def pack(self, list, kwargs):
169         if len(list) != self.num:
170             raise VPPSerializerValueError(
171                 'Fixed list length error, got: {} expected: {}'
172                 .format(len(list), self.num))
173         b = bytes()
174         for e in list:
175             b += self.packer.pack(e)
176         return b
177
178     def unpack(self, data, offset=0, result=None, ntc=False):
179         # Return a list of arguments
180         result = []
181         total = 0
182         for e in range(self.num):
183             x, size = self.packer.unpack(data, offset, ntc=ntc)
184             result.append(x)
185             offset += size
186             total += size
187         return result, total
188
189
190 class VLAList(object):
191     def __init__(self, name, field_type, len_field_name, index):
192         self.name = name
193         self.field_type = field_type
194         self.index = index
195         self.packer = types[field_type]
196         self.size = self.packer.size
197         self.length_field = len_field_name
198
199     def pack(self, list, kwargs=None):
200         if not list:
201             return b""
202         if len(list) != kwargs[self.length_field]:
203             raise VPPSerializerValueError(
204                 'Variable length error, got: {} expected: {}'
205                 .format(len(list), kwargs[self.length_field]))
206         b = bytes()
207
208         # u8 array
209
210         if self.packer.size == 1:
211             return bytearray(list)
212
213         for e in list:
214             b += self.packer.pack(e)
215         return b
216
217     def unpack(self, data, offset=0, result=None, ntc=False):
218         # Return a list of arguments
219         total = 0
220
221         # u8 array
222         if self.packer.size == 1:
223             if result[self.index] == 0:
224                 return b'', 0
225             p = BaseTypes('u8', result[self.index])
226             return p.unpack(data, offset, ntc=ntc)
227
228         r = []
229         for e in range(result[self.index]):
230             x, size = self.packer.unpack(data, offset, ntc=ntc)
231             r.append(x)
232             offset += size
233             total += size
234         return r, total
235
236
237 class VLAList_legacy():
238     def __init__(self, name, field_type):
239         self.packer = types[field_type]
240         self.size = self.packer.size
241
242     def pack(self, list, kwargs=None):
243         if self.packer.size == 1:
244             return bytes(list)
245
246         b = bytes()
247         for e in list:
248             b += self.packer.pack(e)
249         return b
250
251     def unpack(self, data, offset=0, result=None, ntc=False):
252         total = 0
253         # Return a list of arguments
254         if (len(data) - offset) % self.packer.size:
255             raise VPPSerializerValueError(
256                 'Legacy Variable Length Array length mismatch.')
257         elements = int((len(data) - offset) / self.packer.size)
258         r = []
259         for e in range(elements):
260             x, size = self.packer.unpack(data, offset, ntc=ntc)
261             r.append(x)
262             offset += self.packer.size
263             total += size
264         return r, total
265
266
267 class VPPEnumType(object):
268     def __init__(self, name, msgdef):
269         self.size = types['u32'].size
270         e_hash = {}
271         for f in msgdef:
272             if type(f) is dict and 'enumtype' in f:
273                 if f['enumtype'] != 'u32':
274                     raise NotImplementedError
275                 continue
276             ename, evalue = f
277             e_hash[ename] = evalue
278         self.enum = IntEnum(name, e_hash)
279         types[name] = self
280
281     def __getattr__(self, name):
282         return self.enum[name]
283
284     def __nonzero__(self):
285         return True
286
287     def pack(self, data, kwargs=None):
288         return types['u32'].pack(data)
289
290     def unpack(self, data, offset=0, result=None, ntc=False):
291         x, size = types['u32'].unpack(data, offset)
292         return self.enum(x), size
293
294
295 class VPPUnionType(object):
296     def __init__(self, name, msgdef):
297         self.name = name
298         self.size = 0
299         self.maxindex = 0
300         fields = []
301         self.packers = collections.OrderedDict()
302         for i, f in enumerate(msgdef):
303             if type(f) is dict and 'crc' in f:
304                 self.crc = f['crc']
305                 continue
306             f_type, f_name = f
307             if f_type not in types:
308                 logger.debug('Unknown union type {}'.format(f_type))
309                 raise VPPSerializerValueError(
310                     'Unknown message type {}'.format(f_type))
311             fields.append(f_name)
312             size = types[f_type].size
313             self.packers[f_name] = types[f_type]
314             if size > self.size:
315                 self.size = size
316                 self.maxindex = i
317
318         types[name] = self
319         self.tuple = collections.namedtuple(name, fields, rename=True)
320
321     # Union of variable length?
322     def pack(self, data, kwargs=None):
323         if not data:
324             return b'\x00' * self.size
325
326         for k, v in data.items():
327             logger.debug("Key: {} Value: {}".format(k, v))
328             b = self.packers[k].pack(v, kwargs)
329             break
330         r = bytearray(self.size)
331         r[:len(b)] = b
332         return r
333
334     def unpack(self, data, offset=0, result=None, ntc=False):
335         r = []
336         maxsize = 0
337         for k, p in self.packers.items():
338             x, size = p.unpack(data, offset, ntc=ntc)
339             if size > maxsize:
340                 maxsize = size
341             r.append(x)
342         return self.tuple._make(r), maxsize
343
344
345 class VPPTypeAlias(object):
346     def __init__(self, name, msgdef):
347         self.name = name
348         t = vpp_get_type(msgdef['type'])
349         if not t:
350             raise ValueError()
351         if 'length' in msgdef:
352             if msgdef['length'] == 0:
353                 raise ValueError()
354             if msgdef['type'] == 'u8':
355                 self.packer = FixedList_u8(name, msgdef['type'],
356                                            msgdef['length'])
357                 self.size = self.packer.size
358             else:
359                 self.packer = FixedList(name, msgdef['type'], msgdef['length'])
360         else:
361             self.packer = t
362             self.size = t.size
363
364         types[name] = self
365
366     def pack(self, data, kwargs=None):
367         if data and conversion_required(data, self.name):
368             try:
369                 return conversion_packer(data, self.name)
370             # Python 2 and 3 raises different exceptions from inet_pton
371             except(OSError, socket.error, TypeError):
372                 pass
373
374         return self.packer.pack(data, kwargs)
375
376     def unpack(self, data, offset=0, result=None, ntc=False):
377         t, size = self.packer.unpack(data, offset, result, ntc=ntc)
378         if not ntc:
379             return conversion_unpacker(t, self.name), size
380         return t, size
381
382
383 class VPPType(object):
384     # Set everything up to be able to pack / unpack
385     def __init__(self, name, msgdef):
386         self.name = name
387         self.msgdef = msgdef
388         self.packers = []
389         self.fields = []
390         self.fieldtypes = []
391         self.field_by_name = {}
392         size = 0
393         for i, f in enumerate(msgdef):
394             if type(f) is dict and 'crc' in f:
395                 self.crc = f['crc']
396                 continue
397             f_type, f_name = f[:2]
398             self.fields.append(f_name)
399             self.field_by_name[f_name] = None
400             self.fieldtypes.append(f_type)
401             if f_type not in types:
402                 logger.debug('Unknown type {}'.format(f_type))
403                 raise VPPSerializerValueError(
404                     'Unknown message type {}'.format(f_type))
405             if len(f) == 3:  # list
406                 list_elements = f[2]
407                 if list_elements == 0:
408                     p = VLAList_legacy(f_name, f_type)
409                     self.packers.append(p)
410                 elif f_type == 'u8' or f_type == 'string':
411                     p = FixedList_u8(f_name, f_type, list_elements)
412                     self.packers.append(p)
413                     size += p.size
414                 else:
415                     p = FixedList(f_name, f_type, list_elements)
416                     self.packers.append(p)
417                     size += p.size
418             elif len(f) == 4:  # Variable length list
419                 length_index = self.fields.index(f[3])
420                 p = VLAList(f_name, f_type, f[3], length_index)
421                 self.packers.append(p)
422             else:
423                 self.packers.append(types[f_type])
424                 size += types[f_type].size
425
426         self.size = size
427         self.tuple = collections.namedtuple(name, self.fields, rename=True)
428         types[name] = self
429
430     def pack(self, data, kwargs=None):
431         if not kwargs:
432             kwargs = data
433         b = bytes()
434
435         # Try one of the format functions
436         if data and conversion_required(data, self.name):
437             return conversion_packer(data, self.name)
438
439         for i, a in enumerate(self.fields):
440             if data and type(data) is not dict and a not in data:
441                 raise VPPSerializerValueError(
442                     "Invalid argument: {} expected {}.{}".
443                     format(data, self.name, a))
444
445             # Defaulting to zero.
446             if not data or a not in data:  # Default to 0
447                 arg = None
448                 kwarg = None  # No default for VLA
449             else:
450                 arg = data[a]
451                 kwarg = kwargs[a] if a in kwargs else None
452             if isinstance(self.packers[i], VPPType):
453                 b += self.packers[i].pack(arg, kwarg)
454             else:
455                 b += self.packers[i].pack(arg, kwargs)
456
457         return b
458
459     def unpack(self, data, offset=0, result=None, ntc=False):
460         # Return a list of arguments
461         result = []
462         total = 0
463         for p in self.packers:
464             x, size = p.unpack(data, offset, result, ntc)
465             if type(x) is tuple and len(x) == 1:
466                 x = x[0]
467             result.append(x)
468             offset += size
469             total += size
470         t = self.tuple._make(result)
471         if not ntc:
472             t = conversion_unpacker(t, self.name)
473         return t, total
474
475
476 class VPPMessage(VPPType):
477     pass