4 # pkgdraw <header> <outfile> [<parser names>...] [-- <cpp options>...]
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.
11 # <cpp options> are parsed to the C preprocessor while processing the
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
18 # SENF_PARSER_FIELD( id, senf::UInt16Parser ); //<pkgdraw: <option>, <option>...
22 # //>pkgdraw: <option>, <option>...
23 # SENF_PARSER_FIELD( id, senf::UInt16Parser );
25 # <option> is any valid option:
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.
32 import sys, re, signal, tempfile, os, os.path, shutil, atexit
34 basedir=os.path.abspath(os.path.split(sys.argv[0])[0])
38 TEX_HEADER = r"""\documentclass{scrartcl}
39 \usepackage[german]{babel}
40 \usepackage[latin1]{inputenc}
41 \usepackage[T1]{fontenc}
42 \usepackage{ae,aecompl}
45 \usepackage{bytefield}
53 PACKET_HEADER=r"""\begin{bytefield}{32}
57 PACKET_FOOTER=r"""\end{bytefield}
62 TEX_FOOTER = r"""\end{document}
65 def formatField(width, start, size):
70 areas.append({'start': start,
74 areas.append({'start': start,
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
85 def formatSimpleField(width, start, field):
86 areas = formatField(width, start, field['size'])
89 for i in range(len(areas)):
90 if areas[i]['size'] > namesz:
91 namesz = areas[i]['size']
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
97 if 6 <= int(areas[ix]['size'] * charsPerBit):
98 areas[ix]['name'] = '(cont)'
101 def formatPacket(width, fields):
106 if field.get('repeat', False):
107 if start > 0 and start < width:
108 areas.append({ 'start': start, 'size': width-start, 'bottom': False,
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):
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
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,
130 if start > 0 and start < width:
131 areas.append({ 'start': start, 'size': width-start, 'bottom': 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 } ])
138 if field.get('optional', False):
140 area['optional'] = True
141 if start > 0 and start < width:
142 areas.append({ 'start': start, 'size': width-start, 'bottom': False,
145 if field.get('repeat'):
146 if start > 0 and start < width:
147 areas.append({ 'start': start, 'size': width-start, 'bottom': False,
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','') :
157 if start == width : start = 0
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:
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))
173 s = s.replace('_', '\\_')
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]}")
195 name = texquote(area.get('name',''))
196 if name and area.get('optional', False):
198 line.append(r"\bitbox%s{%s}{\strut %s}" % (sides, area['size'], name))
199 lines.append(" & ".join(line))
200 return " \\\\\n".join(lines) + "\n"
202 COMMENT_RE = re.compile(r'//.*?$|/\*.*?\*/|"(?:\\.|[^\\"])*"', re.S | re.M)
204 def stripComments(text):
207 if s.startswith('//<pkgdraw:') or s.startswith('//>pkgdraw:'):
209 if s.startswith('/'):
212 return COMMENT_RE.sub(replacer, text)
214 SENF_INCLUDE_RE = re.compile(r"#\s*include\s*SENF_")
216 def quoteMacros(text):
217 return SENF_INCLUDE_RE.sub("PKGDRAW_", text).replace("SENF_PARSER_","PKGDRAW_PARSER_")
219 def cppExpand(text, cppopts, dir):
220 tmpf = tempfile.NamedTemporaryFile(dir=dir)
223 cmd = "gcc %s -E -o - -x c++-header %s" % (" ".join(cppopts), tmpf.name)
224 return os.popen(cmd).read()
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 },
251 def parse_FIELD(args, flags):
252 args = [ arg.strip() for arg in args.split(',') ]
254 sys.stderr.write("Failed to parse FIELD: %s\n" % args)
256 field = dict(FIELD_TYPES.get(args[1].split(':')[-1], {}))
257 field['name'] = args[0]
260 def parse_PRIVATE_FIELD(args, flags):
261 return parse_FIELD(args, flags)
263 def parse_FIELD_RO(args, flags):
264 return parse_FIELD(args, flags)
266 def parse_BITFIELD(args, flags):
267 args = [ arg.strip() for arg in args.split(',') ]
269 sys.stderr.write("Failed to parse BITFIELD: %s\n" % args)
274 sys.stderr.write("Failed to parse BITFIELD: %s\n" % args)
276 return { 'size' : size, 'name' : args[0] }
278 def parse_PRIVATE_BITFIELD(args, flags):
279 return parse_BITFIELD(args, flags)
281 def parse_BITFIELD_RO(args, flags):
282 return parse_BITFIELD(args, flags)
284 def parse_SKIP(args, flags):
285 args = args.split(',')[0]
287 bytes = int(args.strip())
289 sys.stderr.write("Failed to parse SKIP: %s\n" % args)
291 return { 'size': 8*bytes, 'name': '' }
293 def parse_SKIP_BITS(args, flags):
295 bits = int(args.strip())
297 sys.stderr.write("Failed to parse SKIP_BITS: %s\n" % args)
299 return { 'size': bits, 'name': '' }
301 def parse_VECTOR(args, flags):
302 args = [ arg.strip() for arg in args.split(',') ]
304 sys.stderr.write("Failed to aprse VECTOR: %s\n" % args)
306 field = dict(FIELD_TYPES.get(args[-1].split(':')[-1], {}))
307 field['name'] = args[0]
308 field['repeat'] = True
311 def parse_LIST(args, flags):
312 return parse_VECTOR(args, flags)
314 VARIANT_FIELD_RE_STR = r"""
321 key\(\s*[^,]*,\s*([a-zA-Z0-9_:]+)\s*\)
329 key\(\s*[^,]*,\s*([a-zA-Z0-9_:]+)\s*\)
335 key\(\s*[^,]*,\s*([a-zA-Z0-9_:]+)\s*\)
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)
343 def parse_VARIANT(args, flags):
344 name = args.split(',',1)[0].strip()
345 fields_match = VARIANT_FIELDS_RE.search(args)
347 return { 'name': name }
348 fields_str = fields_match.group(1)
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:
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 }
373 return { 'name': name, 'optional': optional }
375 def parse_PRIVATE_VARIANT(args, flags):
376 return parse_VARIANT(args, flags)
378 def parse_INIT(args, flags):
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)
385 def scanPackets(data):
392 start = PARSER_START_RE.search(data, end)
393 if not start: return (packets, packetOrder)
395 end = PARSER_END_RE.search(data, start)
396 if not end: return (packets, packetOrder)
397 name=end.group(1).strip()
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):
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']
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 }
421 FIELD_TYPES[name] = { 'minsize' : minsize, 'maxsize' : maxsize }
423 def scanFields(data):
425 for match in PARSER_FIELD_RE.finditer(data):
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)
432 field = parser(match.group(3).strip(), flags)
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))
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']
448 sys.stderr.write("Unknown parser type: %s\n" % tp)
451 tmpdir = tempfile.mkdtemp(prefix="pkgdraw_")
455 shutil.rmtree(tmpdir)
457 signal.signal(signal.SIGINT, cleanup)
458 signal.signal(signal.SIGTERM, cleanup)
459 signal.signal(signal.SIGHUP, cleanup)
460 atexit.register(cleanup)
466 if len(args)<2 or args[0] == '--' or args[1] == '--':
467 sys.stderr.write("Usage: %s <header> <outfile> [<parser names>...] [-- <cpp options>...]\n"
474 while args and args[0] != '--' : names.append(args.pop(0))
475 if args : gccopts = args[1:]
477 data, order = scanPackets(cppExpand(quoteMacros(stripComments(file(source).read())),
478 gccopts, os.path.dirname(source)))
480 texf = file(os.path.join(tmpdir, "fields.tex"),"w")
481 texf.write(TEX_HEADER)
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)
493 texf.write(TEX_FOOTER)
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)
501 file(target,"w").write(file(os.path.join(tmpdir, "fields.png")).read())