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