doclib: added encoding options for doxygen
[senf.git] / doclib / pkgdraw
1 #!/usr/bin/python
2 #
3 # Usage:
4 #    pkgdraw <header> <outfile> [<parser names>...] [-- <cpp options>...]
5 #
6 # Extract packet structure from <header>. Write generated PNG diagram
7 # to <outfile>.  If <parser names> is given, it is a list names of
8 # parsers to generate diagrams for. All other parsers from the same
9 # header file will be skipped.
10 #
11 # <cpp options> are parsed to the C preprocessor while processing the
12 # header file.
13 #
14 # pkgdraw will interpret most SENF_PARSER statements, it does however
15 # *not* understand GOTO statements. Special comments may be added
16 # directly before or after a field to pass parameters to pkgdraw
17 #
18 #    SENF_PARSER_FIELD( id, senf::UInt16Parser ); //<pkgdraw: <option>, <option>...
19 #
20 # or
21 #
22 #    //>pkgdraw: <option>, <option>...
23 #    SENF_PARSER_FIELD( id, senf::UInt16Parser );
24 #
25 # <option> is any valid option:
26 #
27 #   hide                Completely skip this field (Helps with GOTO)
28 #   name=<name>         Sets the field name to <name>
29 #   size=<min>[-<max>]  Sets the field size in bits.
30 #
31
32 import sys, re, signal, tempfile, os, os.path, shutil, atexit
33
34 basedir=os.path.abspath(os.path.split(sys.argv[0])[0])
35
36 charsPerBit = 1.4
37
38 TEX_HEADER = r"""\documentclass{scrartcl}
39 \usepackage[german]{babel}
40 \usepackage[latin1]{inputenc}
41 \usepackage[T1]{fontenc}
42 \usepackage{ae,aecompl}
43
44 \usepackage{color}
45 \usepackage{bytefield}
46
47 \pagestyle{empty}
48
49 \begin{document}
50 \sffamily
51 """
52
53 PACKET_HEADER=r"""\begin{bytefield}{32}
54   \bitheader{0-31} \\
55 """
56
57 PACKET_FOOTER=r"""\end{bytefield}
58 \bigskip
59
60 """
61
62 TEX_FOOTER = r"""\end{document}
63 """
64
65 def formatField(width, start, size):
66     areas = []
67     sz = width - start
68     while size > 0:
69         if sz > size:
70             areas.append({'start': start,
71                           'size': size})
72             size = 0
73         else:
74             areas.append({'start': start,
75                           'size': sz})
76             size -= sz
77         sz = width
78         start = 0
79     for i in range(len(areas)-1):
80         if areas[i]['start'] < areas[i+1]['start']+areas[i+1]['size']:
81             areas[i]['bottom'] = False
82             areas[i+1]['top'] = False
83     return areas
84
85 def formatSimpleField(width, start, field):
86     areas = formatField(width, start, field['size'])
87     nameix = 0
88     namesz = 0
89     for i in range(len(areas)):
90         if areas[i]['size'] > namesz:
91             namesz = areas[i]['size']
92             nameix = i
93     areas[nameix]['name'] = field['name'][:int(areas[nameix]['size'] * charsPerBit)]
94     if field['name'] and len(areas) == 2 and areas[0].get('bottom',True):
95         if areas[0].get('name','') : ix = 1
96         else                       : ix = 0
97         if 6 <= int(areas[ix]['size'] * charsPerBit):
98             areas[ix]['name'] = '(cont)'
99     return areas
100     
101 def formatPacket(width, fields):
102     rows = [ [] ]
103     start = 0
104     for field in fields:
105         areas = []
106         if field.get('repeat', False):
107             if start > 0 and start < width:
108                 areas.append({ 'start': start, 'size': width-start, 'bottom': False,
109                                'right': False})
110             start = 0
111         if field.get('size',None):
112             areas.extend(formatSimpleField(width, start, field))
113             start = areas[-1]['start'] + areas[-1]['size']
114         elif field.get('minsize', None):
115             f = dict(field)
116             f['size'] = field['minsize']
117             areas.extend(formatSimpleField(width, start, f))
118             start = areas[-1]['start'] + areas[-1]['size']
119             if start >= width : start = 0
120             addareas = formatField(width, start, field['maxsize'] - field['minsize'])
121             for area in addareas:
122                 area['filled'] = True
123             areas += addareas
124             start = areas[-1]['start'] + areas[-1]['size']
125             if start > 0 and start < width:
126                 areas.append({ 'start': start, 'size': width-start, 'bottom': False,
127                                'right': False})
128             start = 0
129         else:
130             if start > 0 and start < width:
131                 areas.append({ 'start': start, 'size': width-start, 'bottom': False,
132                                'right': False})
133             areas.extend([ { 'start': 0, 'size': width, 'bottom': False,
134                              'name': field['name'] },
135                            { 'start': 0, 'size': width, 'skip': True },
136                            { 'start': 0, 'size': width, 'top': False } ])
137             start = 0
138         if field.get('optional', False):
139             for area in areas:
140                 area['optional'] = True
141             if start > 0 and start < width:
142                 areas.append({ 'start': start, 'size': width-start, 'bottom': False,
143                                'right': False})
144             start = 0
145         if field.get('repeat'):
146             if start > 0 and start < width:
147                 areas.append({ 'start': start, 'size': width-start, 'bottom': False,
148                                'right': False})
149             start = 0
150             areas.append({ 'start': 0, 'size': width, 'dots': True })
151             da = areas[(areas[0].get('right', True) and (0,) or (1,))[0]:-1]
152             for i in range(len(da)):
153                 if da[i].get('name','') :
154                     da[i] = dict(da[i])
155                     del da[i]['name']
156             areas.extend(da)
157         if start == width : start = 0
158         
159         while areas:
160             while areas and (not(rows[-1]) or rows[-1][-1]['start'] + rows[-1][-1]['size'] < width):
161                 if areas[0].get('right', True) == False:
162                     # This is a fillup field. Check, wether to draw top:
163                     if len(rows) <= 1:
164                         areas[0]['top'] = False
165                     elif rows[-2][-1].get('bottom', True) or not rows[-2][-1].get('right', True):
166                         areas[0]['top'] = False
167                 rows[-1].append(areas.pop(0))
168             if areas:
169                 rows.append([])
170     return rows
171
172 def texquote(s):
173     s = s.replace('_', '\\_')
174     return s
175
176 def makeTex(rows):
177     lines = []
178     for row in rows:
179         line = []
180         for area in row:
181             sides=""
182             if area.get('left',   True) : sides += "l"
183             if area.get('right',  True) : sides += "r"
184             if area.get('top',    True) : sides += "t"
185             if area.get('bottom', True) : sides += "b"
186             if sides == "lrtb" : sides = ""
187             else               : sides = "[%s]" % sides
188             if area.get('filled', False):
189                 line.append(r"\bitbox%s{%s}{\color[gray]{0.93}\rule{\width}{\height}}" % (sides, area['size']))
190             elif area.get('skip', False):
191                 line.append(r"\skippedwords")
192             elif area.get('dots', False):
193                 line.append(r"\wordbox[]{1}{$\vdots$\\[1ex]}")
194             else:
195                 name = texquote(area.get('name',''))
196                 if name and area.get('optional', False):
197                     name = "[%s]" % name
198                 line.append(r"\bitbox%s{%s}{\strut %s}" % (sides, area['size'], name))
199         lines.append(" & ".join(line))
200     return " \\\\\n".join(lines) + "\n"
201
202 COMMENT_RE = re.compile(r'//.*?$|/\*.*?\*/|"(?:\\.|[^\\"])*"', re.S | re.M)
203
204 def stripComments(text):
205     def replacer(match):
206         s = match.group(0)
207         if s.startswith('//<pkgdraw:') or s.startswith('//>pkgdraw:'):
208             return "@@" + s[2:]
209         if s.startswith('/'):
210             return ""
211         return s
212     return COMMENT_RE.sub(replacer, text)
213
214 SENF_INCLUDE_RE = re.compile(r"#\s*include\s*SENF_")
215
216 def quoteMacros(text):
217     return SENF_INCLUDE_RE.sub("PKGDRAW_", text).replace("SENF_PARSER_","PKGDRAW_PARSER_")
218
219 def cppExpand(text, cppopts, dir):
220     tmpf = tempfile.NamedTemporaryFile(dir=dir)
221     tmpf.write(text)
222     tmpf.flush()
223     cmd = "gcc %s -E -o - -x c++-header %s" % (" ".join(cppopts), tmpf.name)
224     return os.popen(cmd).read()
225     
226 FIELD_TYPES = {
227     'UInt8Parser' :  {'size': 8 },
228     'UInt16Parser' : {'size': 16 },
229     'UInt24Parser' : {'size': 24 },
230     'UInt32Parser' : {'size': 32 },
231     'UInt64Parser' : {'size': 64 },
232     'Int8Parser' : {'size': 8 },
233     'Int16Parser' : {'size': 16 },
234     'Int24Parser' : {'size': 24 },
235     'Int32Parser' : {'size': 32 },
236     'Int64Parser' : {'size': 64 },
237     'UInt16LSBParser' : {'size': 16 },
238     'UInt24LSBParser' : {'size': 24 },
239     'UInt32LSBParser' : {'size': 32 },
240     'UInt64LSBParser' : {'size': 64 },
241     'Int16LSBParser' : {'size': 16 },
242     'Int24LSBParser' : {'size': 24 },
243     'Int32LSBParser' : {'size': 32 },
244     'Int64LSBParser' : {'size': 64 },
245     'MACAddressParser': {'size': 48 },
246     'INet4AddressParser' : {'size': 32 },
247     'INet6AddressParser' : {'size': 128 },
248     'VoidPacketParser' : {'size': 0 },
249     }
250     
251 def parse_FIELD(args, flags):
252     args = [ arg.strip() for arg in args.split(',') ]
253     if len(args) != 2:
254         sys.stderr.write("Failed to parse FIELD: %s\n" % args)
255         return None
256     field = dict(FIELD_TYPES.get(args[1].split(':')[-1], {}))
257     field['name'] = args[0]
258     return field
259
260 def parse_PRIVATE_FIELD(args, flags):
261     return parse_FIELD(args, flags)
262
263 def parse_FIELD_RO(args, flags):
264     return parse_FIELD(args, flags)
265
266 def parse_BITFIELD(args, flags):
267     args = [ arg.strip() for arg in args.split(',') ]
268     if len(args) != 3:
269         sys.stderr.write("Failed to parse BITFIELD: %s\n" % args)
270         return None
271     try:
272         size = int(args[1])
273     except ValueError:
274         sys.stderr.write("Failed to parse BITFIELD: %s\n" % args)
275         return None
276     return { 'size' : size, 'name' : args[0] }
277
278 def parse_PRIVATE_BITFIELD(args, flags):
279     return parse_BITFIELD(args, flags)
280
281 def parse_BITFIELD_RO(args, flags):
282     return parse_BITFIELD(args, flags)
283
284 def parse_SKIP(args, flags):
285     args = args.split(',')[0]
286     try:
287         bytes = int(args.strip())
288     except ValueError:
289         sys.stderr.write("Failed to parse SKIP: %s\n" % args)
290         return None
291     return { 'size': 8*bytes, 'name': '' }
292
293 def parse_SKIP_BITS(args, flags):
294     try:
295         bits = int(args.strip())
296     except ValueError:
297         sys.stderr.write("Failed to parse SKIP_BITS: %s\n" % args)
298         return None
299     return { 'size': bits, 'name': '' }
300
301 def parse_VECTOR(args, flags):
302     args = [ arg.strip() for arg in args.split(',') ]
303     if len(args) < 3:
304         sys.stderr.write("Failed to aprse VECTOR: %s\n" % args)
305         return None
306     field = dict(FIELD_TYPES.get(args[-1].split(':')[-1], {}))
307     field['name'] = args[0]
308     field['repeat'] = True
309     return field
310
311 def parse_LIST(args, flags):
312     return parse_VECTOR(args, flags)
313
314 VARIANT_FIELD_RE_STR = r"""
315     \(\s*(?:
316         ([a-zA-Z0-9_:]+) |
317         id\(\s*
318             [a-zA-Z0-9_]+\s*,\s*
319             (?:
320                 ([a-zA-Z0-9_:]+) |
321                 key\(\s*[^,]*,\s*([a-zA-Z0-9_:]+)\s*\)
322             )\s*\) |
323         ids\(\s*
324             [a-zA-Z0-9_]+\s*,\s*
325             [a-zA-Z0-9_]+\s*,\s*
326             [a-zA-Z0-9_]+\s*,\s*
327             (?:
328                 ([a-zA-Z0-9_:]+) |
329                 key\(\s*[^,]*,\s*([a-zA-Z0-9_:]+)\s*\)
330             )\s*\) |
331         novalue\(\s*
332             [a-zA-Z0-9_]+\s*,\s*
333             (?:
334                 ([a-zA-Z0-9_:]+) |
335                 key\(\s*[^,]*,\s*([a-zA-Z0-9_:]+)\s*\)
336             )\s*\)
337     )\s*\)
338 """
339
340 VARIANT_FIELD_RE = re.compile(VARIANT_FIELD_RE_STR, re.X)
341 VARIANT_FIELDS_RE = re.compile(",\s*((?:%s\s*)+)$" % VARIANT_FIELD_RE_STR, re.X)
342
343 def parse_VARIANT(args, flags):
344     name = args.split(',',1)[0].strip()
345     fields_match = VARIANT_FIELDS_RE.search(args)
346     if not fields_match:
347         return { 'name': name }
348     fields_str = fields_match.group(1)
349     optional = False
350     minsize = None
351     maxsize = None
352     for field_match in VARIANT_FIELD_RE.finditer(fields_str):
353         parser = ([ group for group in field_match.groups() if group ] + [ None ])[0]
354         field = dict(FIELD_TYPES.get(parser.split(':')[-1], {}))
355         if field.has_key('minsize'):
356             if minsize is None or field['minsize'] < minsize:
357                 minsize = field['minsize']
358             if maxsize is None or field['maxsize'] > maxsize:
359                 maxsize = field['maxsize']
360         elif field.has_key('size'):
361             if field['size'] == 0:
362                 optional = True
363             else:
364                 if minsize is None or field['size'] < minsize:
365                     minsize = field['size']
366                 if maxsize is None or field['size'] > maxsize:
367                     maxsize = field['size']
368     if minsize is not None and minsize == maxsize:
369         return { 'name': name, 'size': minsize, 'optional': optional }
370     elif minsize is not None:
371         return { 'name': name, 'minsize': minsize, 'maxsize': maxsize, 'optional': optional }
372     else:
373         return { 'name': name, 'optional': optional }
374
375 def parse_PRIVATE_VARIANT(args, flags):
376     return parse_VARIANT(args, flags)
377
378 def parse_INIT(args, flags):
379     return None
380
381 PARSER_START_RE = re.compile(r"PKGDRAW_(FIXED_)?PARSER\s*\(\s*\)")
382 PARSER_END_RE = re.compile(r"PKGDRAW_PARSER_FINALIZE\s*\(([^)]*)\)\s*;")
383 PARSER_FIELD_RE = re.compile(r"(?:@@>pkgdraw:(.*)$\s*)?PKGDRAW_PARSER_([A-Z_]+)\s*\(([^;]*)\)\s*;(?:\s*@@<pkgdraw:(.*)$)?", re.M)
384
385 def scanPackets(data):
386     global FIELD_TYPES
387     
388     packets = {}
389     packetOrder = []
390     end = 0
391     while True:
392         start =  PARSER_START_RE.search(data, end)
393         if not start: return (packets, packetOrder)
394         start = start.end(0)
395         end = PARSER_END_RE.search(data, start)
396         if not end: return (packets, packetOrder)
397         name=end.group(1).strip()
398         end = end.start(0)
399         packets[name] = scanFields(data[start:end])
400         packetOrder.append(name)
401         minsize = maxsize = 0
402         for field in packets[name]:
403             if maxsize is not None:
404                 if field.get('repeat', False):
405                     maxsize = None
406                 elif field.get('size', None) is not None:
407                     maxsize += field['size']
408                 elif field.get('minsize', None) is not None:
409                     maxsize += field['maxsize']
410                 else:
411                     maxsize = None
412             if not field.get('optional', False):
413                 if field.get('size', None) is not None:
414                     minsize += field['size']
415                 elif field.get('minsize', None) is not None:
416                     minsize += field['minsize']
417         if minsize is not None and maxsize is not None:
418             if minsize == maxsize:
419                 FIELD_TYPES[name] = { 'size' : minsize }
420             else:
421                 FIELD_TYPES[name] = { 'minsize' : minsize, 'maxsize' : maxsize }
422
423 def scanFields(data):
424     fields = []
425     for match in PARSER_FIELD_RE.finditer(data):
426         tp = match.group(2)
427         flags = dict([ ([ arg.strip() for arg in flag.strip().split('=',1) ]+[True])[:2]
428                        for flag in ((match.group(1) or '')+(match.group(4) or '')).split(',') ])
429         if flags.has_key('hide') : continue
430         parser = globals().get("parse_%s" % tp, None)
431         if parser:
432             field = parser(match.group(3).strip(), flags)
433             if field:
434                 if flags.has_key('name') : field['name'] = flags['name']
435                 field['name'] = field['name'].strip('_')
436                 if flags.has_key('size'):
437                     if '-' in flags['size']:
438                         field['minsize'], field['maxsize'] = map(int, flags['size'].split('-',1))
439                         del field['size']
440                     else:
441                         field['size'] = int(flags['size'])
442                 if not field['name'] and fields and not fields[-1]['name'] \
443                        and field.has_key('size') and fields[-1].has_key('size'):
444                     fields[-1]['size'] += field['size']
445                 else:
446                     fields.append(field)
447         else:
448             sys.stderr.write("Unknown parser type: %s\n" % tp)
449     return fields
450
451 tmpdir = tempfile.mkdtemp(prefix="pkgdraw_")
452
453 def cleanup():
454     global tmpdir
455     shutil.rmtree(tmpdir)
456
457 signal.signal(signal.SIGINT, cleanup)
458 signal.signal(signal.SIGTERM, cleanup)
459 signal.signal(signal.SIGHUP, cleanup)
460 atexit.register(cleanup)
461
462 args = sys.argv[1:]
463 names = []
464 gccopts = []
465
466 if len(args)<2 or args[0] == '--' or args[1] == '--':
467     sys.stderr.write("Usage: %s <header> <outfile> [<parser names>...] [-- <cpp options>...]\n"
468                      % sys.argv[0])
469     sys.exit(1)
470
471 source = args.pop(0)
472 target = args.pop(0)
473
474 while args and args[0] != '--' : names.append(args.pop(0))
475 if args : gccopts = args[1:]
476
477 data, order = scanPackets(cppExpand(quoteMacros(stripComments(file(source).read())),
478                                     gccopts, os.path.dirname(source)))
479
480 texf = file(os.path.join(tmpdir, "fields.tex"),"w")
481 texf.write(TEX_HEADER)
482
483 if not names:
484     order.reverse()
485     names = order
486
487 for name in names:
488     texf.write("\\textbf{%s}\n\\bigskip\\par\n" % texquote(name))
489     texf.write(PACKET_HEADER)
490     texf.write(makeTex(formatPacket(32, data[name])))
491     texf.write(PACKET_FOOTER)
492     
493 texf.write(TEX_FOOTER)
494 texf.close()
495
496 if os.system("cd %s; %s/textogif -png -dpi 80 -res 0.25 fields >pkgdraw.log 2>&1"
497              % (tmpdir, basedir)) != 0:
498     sys.stderr.write("Conversion failed. See %s\n" % tmpdir)
499     os._exit(1)
500
501 file(target,"w").write(file(os.path.join(tmpdir, "fields.png")).read())