api: enforce vla is last and fixed string type
[vpp.git] / src / tools / vppapigen / vppapigen.py
index 9c6b21e..d0391cd 100755 (executable)
@@ -1,14 +1,16 @@
 #!/usr/bin/python3
 
-from __future__ import print_function
 import ply.lex as lex
 import ply.yacc as yacc
 import sys
 import argparse
+import keyword
 import logging
 import binascii
 import os
 
+log = logging.getLogger('vppapigen')
+
 # Ensure we don't leave temporary files around
 sys.dont_write_bytecode = True
 
@@ -77,9 +79,12 @@ class VPPAPILexer(object):
     t_ignore_LINE_COMMENT = '//.*'
 
     def t_NUM(self, t):
-        r'0[xX][0-9a-fA-F]+|-?\d+'
+        r'0[xX][0-9a-fA-F]+|-?\d+\.?\d*'
         base = 16 if t.value.startswith('0x') else 10
-        t.value = int(t.value, base)
+        if '.' in t.value:
+            t.value = float(t.value)
+        else:
+            t.value = int(t.value, base)
         return t
 
     def t_ID(self, t):
@@ -123,6 +128,7 @@ def crc_block_combine(block, crc):
     s = str(block).encode()
     return binascii.crc32(s, crc) & 0xffffffff
 
+
 class Service():
     def __init__(self, caller, reply, events=None, stream=False):
         self.caller = caller
@@ -146,6 +152,25 @@ class Typedef():
                 self.manual_endian = True
         global_type_add(name, self)
 
+        self.vla = False
+
+        for i, b in enumerate(block):
+            if isinstance(b, Array):
+                if b.length == 0:
+                    self.vla = True
+                    if i + 1 < len(block):
+                        raise ValueError(
+                            'VLA field "{}" must be the last '
+                            'field in message "{}"'
+                            .format(b.fieldname, name))
+            elif b.fieldtype == 'string':
+                self.vla = True
+                if i + 1 < len(block):
+                    raise ValueError(
+                        'VLA field "{}" must be the last '
+                        'field in message "{}"'
+                        .format(b.fieldname, name))
+
     def __repr__(self):
         return self.name + str(self.flags) + str(self.block)
 
@@ -153,12 +178,13 @@ class Typedef():
 class Using():
     def __init__(self, name, alias):
         self.name = name
+        self.vla = False
 
         if isinstance(alias, Array):
-            a = { 'type': alias.fieldtype,  # noqa: E201
-                  'length': alias.length }  # noqa: E202
+            a = {'type': alias.fieldtype,
+                 'length': alias.length}
         else:
-            a = { 'type': alias.fieldtype }  # noqa: E201,E202
+            a = {'type': alias.fieldtype}
         self.alias = a
         self.crc = str(alias).encode()
         global_type_add(name, self)
@@ -202,12 +228,29 @@ class Define():
             elif f == 'autoreply':
                 self.autoreply = True
 
-        for b in block:
+        for i, b in enumerate(block):
             if isinstance(b, Option):
                 if b[1] == 'singular' and b[2] == 'true':
                     self.singular = True
                 block.remove(b)
 
+            if isinstance(b, Array) and b.vla and i + 1 < len(block):
+                raise ValueError(
+                    'VLA field "{}" must be the last field in message "{}"'
+                    .format(b.fieldname, name))
+            elif b.fieldtype.startswith('vl_api_'):
+                if (global_types[b.fieldtype].vla and i + 1 < len(block)):
+                    raise ValueError(
+                        'VLA field "{}" must be the last '
+                        'field in message "{}"'
+                        .format(b.fieldname, name))
+            elif b.fieldtype == 'string' and b.length == 0:
+                if i + 1 < len(block):
+                    raise ValueError(
+                        'VLA field "{}" must be the last '
+                        'field in message "{}"'
+                        .format(b.fieldname, name))
+
     def __repr__(self):
         return self.name + str(self.flags) + str(self.block)
 
@@ -216,6 +259,7 @@ class Enum():
     def __init__(self, name, block, enumtype='u32'):
         self.name = name
         self.enumtype = enumtype
+        self.vla = False
 
         count = 0
         for i, b in enumerate(block):
@@ -245,12 +289,9 @@ class Import():
             f = os.path.join(dir, filename)
             if os.path.exists(f):
                 break
-        if sys.version[0] == '2':
-            with open(f) as fd:
-                self.result = parser.parse_file(fd, None)
-        else:
-            with open(f, encoding='utf-8') as fd:
-                self.result = parser.parse_file(fd, None)
+
+        with open(f, encoding='utf-8') as fd:
+            self.result = parser.parse_file(fd, None)
 
     def __repr__(self):
         return self.filename
@@ -269,16 +310,19 @@ class Option():
 
 
 class Array():
-    def __init__(self, fieldtype, name, length):
+    def __init__(self, fieldtype, name, length, modern_vla=False):
         self.type = 'Array'
         self.fieldtype = fieldtype
         self.fieldname = name
+        self.modern_vla = modern_vla
         if type(length) is str:
             self.lengthfield = length
             self.length = 0
+            self.vla = True
         else:
             self.length = length
             self.lengthfield = None
+            self.vla = False
 
     def __repr__(self):
         return str([self.fieldtype, self.fieldname, self.length,
@@ -289,6 +333,14 @@ class Field():
     def __init__(self, fieldtype, name, limit=None):
         self.type = 'Field'
         self.fieldtype = fieldtype
+
+        if self.fieldtype == 'string':
+            raise ValueError("The string type {!r} is an "
+                             "array type ".format(name))
+
+        if name in keyword.kwlist:
+            raise ValueError("Fieldname {!r} is a python keyword and is not "
+                             "accessible via the python API. ".format(name))
         self.fieldname = name
         self.limit = limit
 
@@ -444,7 +496,9 @@ class VPPAPIParser(object):
         '''define : flist DEFINE ID '{' block_statements_opt '}' ';' '''
         # Legacy typedef
         if 'typeonly' in p[1]:
-            p[0] = Typedef(p[3], p[1], p[5])
+            self._parse_error('legacy typedef. use typedef: {} {}[{}];'
+                              .format(p[1], p[2], p[4]),
+                              self._token_coord(p, 1))
         else:
             p[0] = Define(p[3], p[1], p[5])
 
@@ -513,13 +567,18 @@ class VPPAPIParser(object):
         if len(p) == 2:
             p[0] = p[1]
         else:
-            p[0] = { **p[1], **p[2] }
+            p[0] = {**p[1], **p[2]}
 
     def p_field_option(self, p):
-        '''field_option : ID '=' assignee ','
+        '''field_option : ID
+                        | ID '=' assignee ','
                         | ID '=' assignee
+
         '''
-        p[0] = { p[1]: p[3] }
+        if len(p) == 2:
+            p[0] = {p[1]: None}
+        else:
+            p[0] = {p[1]: p[3]}
 
     def p_declaration(self, p):
         '''declaration : type_specifier ID ';'
@@ -532,9 +591,14 @@ class VPPAPIParser(object):
             self._parse_error('ERROR')
         self.fields.append(p[2])
 
+    def p_declaration_array_vla(self, p):
+        '''declaration : type_specifier ID '[' ']' ';' '''
+        p[0] = Array(p[1], p[2], 0, modern_vla=True)
+
     def p_declaration_array(self, p):
         '''declaration : type_specifier ID '[' NUM ']' ';'
                        | type_specifier ID '[' ID ']' ';' '''
+
         if len(p) != 7:
             return self._parse_error(
                 'array: %s' % p.value,
@@ -762,6 +826,7 @@ def dirlist_add(dirs):
 def dirlist_get():
     return dirlist
 
+
 def foldup_blocks(block, crc):
     for b in block:
         # Look up CRC in user defined types
@@ -771,36 +836,36 @@ def foldup_blocks(block, crc):
             try:
                 crc = crc_block_combine(t.block, crc)
                 return foldup_blocks(t.block, crc)
-            except:
+            except AttributeError:
                 pass
     return crc
 
+
 def foldup_crcs(s):
     for f in s:
         f.crc = foldup_blocks(f.block,
                               binascii.crc32(f.crc))
 
+
 #
 # Main
 #
 def main():
+    if sys.version_info < (3, 5,):
+        log.exception('vppapigen requires a supported version of python. '
+                      'Please use version 3.5 or greater. '
+                      'Using {}'.format(sys.version))
+        return 1
+
     cliparser = argparse.ArgumentParser(description='VPP API generator')
     cliparser.add_argument('--pluginpath', default=""),
     cliparser.add_argument('--includedir', action='append'),
-    if sys.version[0] == '2':
-        cliparser.add_argument('--input', type=argparse.FileType('r'),
-                               default=sys.stdin)
-        cliparser.add_argument('--output', nargs='?',
-                               type=argparse.FileType('w'),
-                               default=sys.stdout)
-
-    else:
-        cliparser.add_argument('--input',
-                               type=argparse.FileType('r', encoding='UTF-8'),
-                               default=sys.stdin)
-        cliparser.add_argument('--output', nargs='?',
-                               type=argparse.FileType('w', encoding='UTF-8'),
-                               default=sys.stdout)
+    cliparser.add_argument('--input',
+                           type=argparse.FileType('r', encoding='UTF-8'),
+                           default=sys.stdin)
+    cliparser.add_argument('--output', nargs='?',
+                           type=argparse.FileType('w', encoding='UTF-8'),
+                           default=sys.stdout)
 
     cliparser.add_argument('output_module', nargs='?', default='C')
     cliparser.add_argument('--debug', action='store_true')
@@ -823,7 +888,6 @@ def main():
         logging.basicConfig(stream=sys.stdout, level=logging.WARNING)
     else:
         logging.basicConfig()
-    log = logging.getLogger('vppapigen')
 
     parser = VPPAPI(debug=args.debug, filename=filename, logger=log)
     parsed_objects = parser.parse_file(args.input, log)
@@ -870,7 +934,8 @@ def main():
     else:
         pluginpath = args.pluginpath + '/'
     if pluginpath == '':
-        raise Exception('Output plugin not found')
+        log.exception('Output plugin not found')
+        return 1
     module_path = '{}vppapigen_{}.py'.format(pluginpath,
                                              args.output_module.lower())
 
@@ -878,16 +943,19 @@ def main():
         plugin = SourceFileLoader(args.output_module,
                                   module_path).load_module()
     except Exception as err:
-        raise Exception('Error importing output plugin: {}, {}'
-                        .format(module_path, err))
+        log.exception('Error importing output plugin: {}, {}'
+                      .format(module_path, err))
+        return 1
 
     result = plugin.run(filename, s)
     if result:
         print(result, file=args.output)
     else:
-        raise Exception('Running plugin failed: {} {}'
-                        .format(filename, result))
+        log.exception('Running plugin failed: {} {}'
+                      .format(filename, result))
+        return 1
+    return 0
 
 
 if __name__ == '__main__':
-    main()
+    sys.exit(main())