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