Packets: Add packet diagrams
[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('repeat'):
111             if start > 0 and start < width:
112                 areas.append({ 'start': start, 'size': width-start, 'bottom': False,
113                                'right': False})
114             start = 0
115             areas.append({ 'start': 0, 'size': width, 'dots': True })
116             da = areas[(areas[0].get('right', True) and (0,) or (1,))[0]:-1]
117             for i in range(len(da)):
118                 if da[i].get('name','') :
119                     da[i] = dict(da[i])
120                     del da[i]['name']
121             areas.extend(da)
122         if start == width : start = 0
123         
124         while areas:
125             while areas and (not(rows[-1]) or rows[-1][-1]['start'] + rows[-1][-1]['size'] < width):
126                 if areas[0].get('right', True) == False:
127                     # This is a fillup field. Check, wether to draw top:
128                     if len(rows) <= 1:
129                         areas[0]['top'] = False
130                     elif rows[-2][-1].get('bottom', True):
131                         areas[0]['top'] = False
132                 rows[-1].append(areas.pop(0))
133             if areas:
134                 rows.append([])
135     return rows
136
137 def texquote(s):
138     s = s.replace('_', '\\_')
139     return s
140
141 def makeTex(rows):
142     lines = []
143     for row in rows:
144         line = []
145         for area in row:
146             sides=""
147             if area.get('left',   True) : sides += "l"
148             if area.get('right',  True) : sides += "r"
149             if area.get('top',    True) : sides += "t"
150             if area.get('bottom', True) : sides += "b"
151             if sides == "lrtb" : sides = ""
152             else               : sides = "[%s]" % sides
153             if area.get('filled', False):
154                 line.append(r"\bitbox%s{%s}{\color[gray]{0.7}\rule{\width}{\height}}" % (sides, area['size']))
155             elif area.get('skip', False):
156                 line.append(r"\skippedwords")
157             elif area.get('dots', False):
158                 line.append(r"\wordbox[]{1}{$\vdots$\\[1ex]}")
159             else:
160                 line.append(r"\bitbox%s{%s}{\strut %s}"
161                             % (sides, area['size'], texquote(area.get('name',''))))
162         lines.append(" & ".join(line))
163     return " \\\\\n".join(lines) + "\n"
164
165 FIELD_TYPES = {
166     'UInt8Parser' :  {'size': 8 },
167     'UInt16Parser' : {'size': 16 },
168     'UInt24Parser' : {'size': 24 },
169     'UInt32Parser' : {'size': 32 },
170     'UInt64Parser' : {'size': 64 },
171     'Int8Parser' : {'size': 8 },
172     'Int16Parser' : {'size': 16 },
173     'Int24Parser' : {'size': 24 },
174     'Int32Parser' : {'size': 32 },
175     'Int64Parser' : {'size': 64 },
176     'MACAddressParser': {'size': 48 },
177     'INet4AddressParser' : {'size': 32 },
178     'INet6AddressParser' : {'size': 128 },
179     }
180     
181 def parse_FIELD(args, flags):
182     args = [ arg.strip() for arg in args.split(',') ]
183     if len(args) != 2:
184         sys.stderr.write("Failed to parse FIELD: %s" % args)
185         return None
186     field = dict(FIELD_TYPES.get(args[1].split(':')[-1], {}))
187     field['name'] = args[0]
188     return field
189
190 def parse_PRIVATE_FIELD(args, flags):
191     return parse_FIELD(args, flags)
192
193 def parse_FIELD_RO(args, flags):
194     return parse_FIELD(args, flags)
195
196 def parse_BITFIELD(args, flags):
197     args = [ arg.strip() for arg in args.split(',') ]
198     if len(args) != 3:
199         sys.stderr.write("Failed to parse BITFIELD: %s" % args)
200         return None
201     try:
202         size = int(args[1])
203     except ValueError:
204         sys.stderr.write("Failed to parse BITFIELD: %s" % args)
205         return None
206     return { 'size' : size, 'name' : args[0] }
207
208 def parse_PRIVATE_BITFIELD(args, flags):
209     return parse_BITFIELD(args, flags)
210
211 def parse_BITFIELD_RO(args, flags):
212     return parse_BITFIELD(args, flags)
213
214 def parse_SKIP(args, flags):
215     args = args.split(',')[0]
216     try:
217         bytes = int(args.strip())
218     except ValueError:
219         sys.stderr.write("Failed to parse SKIP: %s" % args)
220         return None
221     return { 'size': 8*bytes, 'name': '' }
222
223 def parse_SKIP_BITS(args, flags):
224     try:
225         bits = int(args.strip())
226     except ValueError:
227         sys.stderr.write("Failed to parse SKIP_BITS: %s" % args)
228         return None
229     return { 'size': bits, 'name': '' }
230
231 def parse_VECTOR(args, flags):
232     args = [ arg.strip() for arg in args.split(',') ]
233     if len(args) < 3:
234         sys.stderr.write("Failed to aprse VECTOR: %s" % args)
235         return None
236     field = dict(FIELD_TYPES.get(args[-1].split(':')[-1], {}))
237     field['name'] = args[0]
238     field['repeat'] = True
239     return field
240
241 def parse_LIST(args, flags):
242     return parse_VECTOR(args, flags)
243
244 def parse_VARIANT(args, flags):
245     return { 'name': args.split(',',1)[0].strip() }
246
247 def parse_INIT(args, flags):
248     return None
249
250 PARSER_START_RE = re.compile(r"#\s*include\s+SENF_(FIXED_)?PARSER\s*\(\s*\)")
251 PARSER_END_RE = re.compile(r"SENF_PARSER_FINALIZE\s*\(([^)]*)\)\s*;")
252 PARSER_FIELD_RE = re.compile(r"(?://>pkgdraw:(.*)$\s*)?SENF_PARSER_([A-Z_]+)\s*\(([^;]*)\)\s*;(?:\s*//<pkgdraw:(.*)$)?", re.M)
253
254 def scanPackets(data):
255     packets = {}
256     end = 0
257     while True:
258         start =  PARSER_START_RE.search(data, end)
259         if not start: return packets
260         start = start.end(0)
261         end = PARSER_END_RE.search(data, start)
262         if not end: return packets
263         name=end.group(1).strip()
264         end = end.start(0)
265         packets[name] = scanFields(data[start:end])
266
267 def scanFields(data):
268     fields = []
269     for match in PARSER_FIELD_RE.finditer(data):
270         tp = match.group(2)
271         flags = dict([ ([ arg.strip() for arg in flag.strip().split('=',1) ]+[True])[:2]
272                        for flag in ((match.group(1) or '')+(match.group(4) or '')).split(',') ])
273         if flags.has_key('hide') : continue
274         parser = globals().get("parse_%s" % tp, None)
275         if parser:
276             field = parser(match.group(3).strip(), flags)
277             if field:
278                 if flags.has_key('name') : field['name'] = flags['name']
279                 field['name'] = field['name'].strip('_')
280                 fields.append(field)
281         else:
282             sys.stderr.write("Unknown parser type: %s\n" % tp)
283     return fields
284
285 tmpdir = tempfile.mkdtemp(prefix="pkgdraw_")
286
287 def cleanup():
288     global tmpdir
289     shutil.rmtree(tmpdir)
290
291 signal.signal(signal.SIGINT, cleanup)
292 signal.signal(signal.SIGTERM, cleanup)
293 signal.signal(signal.SIGHUP, cleanup)
294 #atexit.register(cleanup)
295
296 data = scanPackets(sys.stdin.read())
297
298 texf = file(os.path.join(tmpdir, "fields.tex"),"w")
299 texf.write(TEX_HEADER)
300
301 if len(sys.argv) > 1:
302     names = sys.argv[1:]
303 else:
304     names = data.keys()
305     names.sort()
306
307 for name in names:
308     texf.write("\\textbf{%s}\n\\bigskip\n\n" % texquote(name))
309     texf.write(PACKET_HEADER)
310     texf.write(makeTex(formatPacket(32, data[name])))
311     texf.write(PACKET_FOOTER)
312     
313 texf.write(TEX_FOOTER)
314 texf.close()
315
316 if os.system("cd %s; %s/textogif -png -dpi 80 -res 0.25 fields >pkgdraw.log 2>&1"
317              % (tmpdir, basedir)) != 0:
318     sys.stderr.write("Conversion failed. See %s\n" % tmpdir)
319     os._exit(1)
320
321 sys.stdout.write(file(os.path.join(tmpdir, "fields.png")).read())