1# FRR CLI preprocessor (DEFPY)
2#
3# Copyright (C) 2017  David Lamparter for NetDEF, Inc.
4#
5# This program is free software; you can redistribute it and/or modify it
6# under the terms of the GNU General Public License as published by the Free
7# Software Foundation; either version 2 of the License, or (at your option)
8# any later version.
9#
10# This program is distributed in the hope that it will be useful, but WITHOUT
11# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
13# more details.
14#
15# You should have received a copy of the GNU General Public License along
16# with this program; see the file COPYING; if not, write to the Free Software
17# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
18
19import clippy, traceback, sys, os
20from collections import OrderedDict
21from functools import reduce
22from pprint import pprint
23from string import Template
24from io import StringIO
25
26# the various handlers generate output C code for a particular type of
27# CLI token, choosing the most useful output C type.
28
29class RenderHandler(object):
30    def __init__(self, token):
31        pass
32    def combine(self, other):
33        if type(self) == type(other):
34            return other
35        return StringHandler(None)
36
37    deref = ''
38    drop_str = False
39    canfail = True
40    canassert = False
41
42class StringHandler(RenderHandler):
43    argtype = 'const char *'
44    decl = Template('const char *$varname = NULL;')
45    code = Template('$varname = (argv[_i]->type == WORD_TKN) ? argv[_i]->text : argv[_i]->arg;')
46    drop_str = True
47    canfail = False
48    canassert = True
49
50class LongHandler(RenderHandler):
51    argtype = 'long'
52    decl = Template('long $varname = 0;')
53    code = Template('''\
54char *_end;
55$varname = strtol(argv[_i]->arg, &_end, 10);
56_fail = (_end == argv[_i]->arg) || (*_end != '\\0');''')
57
58# A.B.C.D/M (prefix_ipv4) and
59# X:X::X:X/M (prefix_ipv6) are "compatible" and can merge into a
60# struct prefix:
61
62class PrefixBase(RenderHandler):
63    def combine(self, other):
64        if type(self) == type(other):
65            return other
66        if isinstance(other, PrefixBase):
67            return PrefixGenHandler(None)
68        return StringHandler(None)
69    deref = '&'
70class Prefix4Handler(PrefixBase):
71    argtype = 'const struct prefix_ipv4 *'
72    decl = Template('struct prefix_ipv4 $varname = { };')
73    code = Template('_fail = !str2prefix_ipv4(argv[_i]->arg, &$varname);')
74class Prefix6Handler(PrefixBase):
75    argtype = 'const struct prefix_ipv6 *'
76    decl = Template('struct prefix_ipv6 $varname = { };')
77    code = Template('_fail = !str2prefix_ipv6(argv[_i]->arg, &$varname);')
78class PrefixEthHandler(PrefixBase):
79    argtype = 'struct prefix_eth *'
80    decl = Template('struct prefix_eth $varname = { };')
81    code = Template('_fail = !str2prefix_eth(argv[_i]->arg, &$varname);')
82class PrefixGenHandler(PrefixBase):
83    argtype = 'const struct prefix *'
84    decl = Template('struct prefix $varname = { };')
85    code = Template('_fail = !str2prefix(argv[_i]->arg, &$varname);')
86
87# same for IP addresses.  result is union sockunion.
88class IPBase(RenderHandler):
89    def combine(self, other):
90        if type(self) == type(other):
91            return other
92        if type(other) in [IP4Handler, IP6Handler, IPGenHandler]:
93            return IPGenHandler(None)
94        return StringHandler(None)
95class IP4Handler(IPBase):
96    argtype = 'struct in_addr'
97    decl = Template('struct in_addr $varname = { INADDR_ANY };')
98    code = Template('_fail = !inet_aton(argv[_i]->arg, &$varname);')
99class IP6Handler(IPBase):
100    argtype = 'struct in6_addr'
101    decl = Template('struct in6_addr $varname = {};')
102    code = Template('_fail = !inet_pton(AF_INET6, argv[_i]->arg, &$varname);')
103class IPGenHandler(IPBase):
104    argtype = 'const union sockunion *'
105    decl = Template('''union sockunion s__$varname = { .sa.sa_family = AF_UNSPEC }, *$varname = NULL;''')
106    code = Template('''\
107if (argv[_i]->text[0] == 'X') {
108	s__$varname.sa.sa_family = AF_INET6;
109	_fail = !inet_pton(AF_INET6, argv[_i]->arg, &s__$varname.sin6.sin6_addr);
110	$varname = &s__$varname;
111} else {
112	s__$varname.sa.sa_family = AF_INET;
113	_fail = !inet_aton(argv[_i]->arg, &s__$varname.sin.sin_addr);
114	$varname = &s__$varname;
115}''')
116    canassert = True
117
118def mix_handlers(handlers):
119    def combine(a, b):
120        if a is None:
121            return b
122        return a.combine(b)
123    return reduce(combine, handlers, None)
124
125handlers = {
126    'WORD_TKN':         StringHandler,
127    'VARIABLE_TKN':     StringHandler,
128    'RANGE_TKN':        LongHandler,
129    'IPV4_TKN':         IP4Handler,
130    'IPV4_PREFIX_TKN':  Prefix4Handler,
131    'IPV6_TKN':         IP6Handler,
132    'IPV6_PREFIX_TKN':  Prefix6Handler,
133    'MAC_TKN':          PrefixEthHandler,
134    'MAC_PREFIX_TKN':   PrefixEthHandler,
135}
136
137# core template invoked for each occurence of DEFPY.
138#
139# the "#if $..." bits are there to keep this template unified into one
140# common form, without requiring a more advanced template engine (e.g.
141# jinja2)
142templ = Template('''/* $fnname => "$cmddef" */
143DEFUN_CMD_FUNC_DECL($fnname)
144#define funcdecl_$fnname static int ${fnname}_magic(\\
145	const struct cmd_element *self __attribute__ ((unused)),\\
146	struct vty *vty __attribute__ ((unused)),\\
147	int argc __attribute__ ((unused)),\\
148	struct cmd_token *argv[] __attribute__ ((unused))$argdefs)
149funcdecl_$fnname;
150DEFUN_CMD_FUNC_TEXT($fnname)
151{
152#if $nonempty /* anything to parse? */
153	int _i;
154#if $canfail /* anything that can fail? */
155	unsigned _fail = 0, _failcnt = 0;
156#endif
157$argdecls
158	for (_i = 0; _i < argc; _i++) {
159		if (!argv[_i]->varname)
160			continue;
161#if $canfail /* anything that can fail? */
162		_fail = 0;
163#endif
164$argblocks
165#if $canfail /* anything that can fail? */
166		if (_fail)
167			vty_out (vty, "%% invalid input for %s: %s\\n",
168				   argv[_i]->varname, argv[_i]->arg);
169		_failcnt += _fail;
170#endif
171	}
172#if $canfail /* anything that can fail? */
173	if (_failcnt)
174		return CMD_WARNING;
175#endif
176#endif
177$argassert
178	return ${fnname}_magic(self, vty, argc, argv$arglist);
179}
180
181''')
182
183# invoked for each named parameter
184argblock = Template('''
185		if (!strcmp(argv[_i]->varname, \"$varname\")) {$strblock
186			$code
187		}''')
188
189def get_always_args(token, always_args, args = [], stack = []):
190    if token in stack:
191        return
192    if token.type == 'END_TKN':
193        for arg in list(always_args):
194            if arg not in args:
195                always_args.remove(arg)
196        return
197
198    stack = stack + [token]
199    if token.type in handlers and token.varname is not None:
200        args = args + [token.varname]
201    for nexttkn in token.next():
202        get_always_args(nexttkn, always_args, args, stack)
203
204class Macros(dict):
205    def load(self, filename):
206        filedata = clippy.parse(filename)
207        for entry in filedata['data']:
208            if entry['type'] != 'PREPROC':
209                continue
210            ppdir = entry['line'].lstrip().split(None, 1)
211            if ppdir[0] != 'define' or len(ppdir) != 2:
212                continue
213            ppdef = ppdir[1].split(None, 1)
214            name = ppdef[0]
215            if '(' in name:
216                continue
217            val = ppdef[1] if len(ppdef) == 2 else ''
218
219            val = val.strip(' \t\n\\')
220            if name in self:
221                sys.stderr.write('warning: macro %s redefined!\n' % (name))
222            self[name] = val
223
224def process_file(fn, ofd, dumpfd, all_defun, macros):
225    errors = 0
226    filedata = clippy.parse(fn)
227
228    for entry in filedata['data']:
229        if entry['type'].startswith('DEFPY') or (all_defun and entry['type'].startswith('DEFUN')):
230            if len(entry['args'][0]) != 1:
231                sys.stderr.write('%s:%d: DEFPY function name not parseable (%r)\n' % (fn, entry['lineno'], entry['args'][0]))
232                errors += 1
233                continue
234
235            cmddef = entry['args'][2]
236            cmddefx = []
237            for i in cmddef:
238                while i in macros:
239                    i = macros[i]
240                if i.startswith('"') and i.endswith('"'):
241                    cmddefx.append(i[1:-1])
242                    continue
243
244                sys.stderr.write('%s:%d: DEFPY command string not parseable (%r)\n' % (fn, entry['lineno'], cmddef))
245                errors += 1
246                cmddefx = None
247                break
248            if cmddefx is None:
249                continue
250            cmddef = ''.join([i for i in cmddefx])
251
252            graph = clippy.Graph(cmddef)
253            args = OrderedDict()
254            always_args = set()
255            for token, depth in clippy.graph_iterate(graph):
256                if token.type not in handlers:
257                    continue
258                if token.varname is None:
259                    continue
260                arg = args.setdefault(token.varname, [])
261                arg.append(handlers[token.type](token))
262                always_args.add(token.varname)
263
264            get_always_args(graph.first(), always_args)
265
266            #print('-' * 76)
267            #pprint(entry)
268            #clippy.dump(graph)
269            #pprint(args)
270
271            params = { 'cmddef': cmddef, 'fnname': entry['args'][0][0] }
272            argdefs = []
273            argdecls = []
274            arglist = []
275            argblocks = []
276            argassert = []
277            doc = []
278            canfail = 0
279
280            def do_add(handler, basename, varname, attr = ''):
281                argdefs.append(',\\\n\t%s %s%s' % (handler.argtype, varname, attr))
282                argdecls.append('\t%s\n' % (handler.decl.substitute({'varname': varname}).replace('\n', '\n\t')))
283                arglist.append(', %s%s' % (handler.deref, varname))
284                if basename in always_args and handler.canassert:
285                    argassert.append('''\tif (!%s) {
286\t\tvty_out(vty, "Internal CLI error [%%s]\\n", "%s");
287\t\treturn CMD_WARNING;
288\t}\n''' % (varname, varname))
289                if attr == '':
290                    at = handler.argtype
291                    if not at.startswith('const '):
292                        at = '. . . ' + at
293                    doc.append('\t%-26s %s  %s' % (at, 'alw' if basename in always_args else 'opt', varname))
294
295            for varname in args.keys():
296                handler = mix_handlers(args[varname])
297                #print(varname, handler)
298                if handler is None: continue
299                do_add(handler, varname, varname)
300                code = handler.code.substitute({'varname': varname}).replace('\n', '\n\t\t\t')
301                if handler.canfail:
302                    canfail = 1
303                strblock = ''
304                if not handler.drop_str:
305                    do_add(StringHandler(None), varname, '%s_str' % (varname), ' __attribute__ ((unused))')
306                    strblock = '\n\t\t\t%s_str = argv[_i]->arg;' % (varname)
307                argblocks.append(argblock.substitute({'varname': varname, 'strblock': strblock, 'code': code}))
308
309            if dumpfd is not None:
310                if len(arglist) > 0:
311                    dumpfd.write('"%s":\n%s\n\n' % (cmddef, '\n'.join(doc)))
312                else:
313                    dumpfd.write('"%s":\n\t---- no magic arguments ----\n\n' % (cmddef))
314
315            params['argdefs'] = ''.join(argdefs)
316            params['argdecls'] = ''.join(argdecls)
317            params['arglist'] = ''.join(arglist)
318            params['argblocks'] = ''.join(argblocks)
319            params['canfail'] = canfail
320            params['nonempty'] = len(argblocks)
321            params['argassert'] = ''.join(argassert)
322            ofd.write(templ.substitute(params))
323
324    return errors
325
326if __name__ == '__main__':
327    import argparse
328
329    argp = argparse.ArgumentParser(description = 'FRR CLI preprocessor in Python')
330    argp.add_argument('--all-defun', action = 'store_const', const = True,
331            help = 'process DEFUN() statements in addition to DEFPY()')
332    argp.add_argument('--show', action = 'store_const', const = True,
333            help = 'print out list of arguments and types for each definition')
334    argp.add_argument('-o', type = str, metavar = 'OUTFILE',
335            help = 'output C file name')
336    argp.add_argument('cfile', type = str)
337    args = argp.parse_args()
338
339    dumpfd = None
340    if args.o is not None:
341        ofd = StringIO()
342        if args.show:
343            dumpfd = sys.stdout
344    else:
345        ofd = sys.stdout
346        if args.show:
347            dumpfd = sys.stderr
348
349    basepath = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
350
351    macros = Macros()
352    macros.load('lib/route_types.h')
353    macros.load(os.path.join(basepath, 'lib/command.h'))
354    macros.load(os.path.join(basepath, 'bgpd/bgp_vty.h'))
355    # sigh :(
356    macros['PROTO_REDIST_STR'] = 'FRR_REDIST_STR_ISISD'
357
358    errors = process_file(args.cfile, ofd, dumpfd, args.all_defun, macros)
359    if errors != 0:
360        sys.exit(1)
361
362    if args.o is not None:
363        clippy.wrdiff(args.o, ofd, [args.cfile, os.path.realpath(__file__), sys.executable])
364