doclib/fix-links: open files in utf-8 mode
[senf.git] / doclib / pkgdraw
index c0fd746..895330f 100755 (executable)
@@ -1,4 +1,33 @@
 #!/usr/bin/python
+#
+# Usage:
+#    pkgdraw <header> <outfile> [<parser names>...] [-- <cpp options>...]
+#
+# Extract packet structure from <header>. Write generated PNG diagram
+# to <outfile>.  If <parser names> is given, it is a list names of
+# parsers to generate diagrams for. All other parsers from the same
+# header file will be skipped.
+#
+# <cpp options> are parsed to the C preprocessor while processing the
+# header file.
+#
+# pkgdraw will interpret most SENF_PARSER statements, it does however
+# *not* understand GOTO statements. Special comments may be added
+# directly before or after a field to pass parameters to pkgdraw
+#
+#    SENF_PARSER_FIELD( id, senf::UInt16Parser ); //<pkgdraw: <option>, <option>...
+#
+# or
+#
+#    //>pkgdraw: <option>, <option>...
+#    SENF_PARSER_FIELD( id, senf::UInt16Parser );
+#
+# <option> is any valid option:
+#
+#   hide                Completely skip this field (Helps with GOTO)
+#   name=<name>         Sets the field name to <name>
+#   size=<min>[-<max>]  Sets the field size in bits.
+#
 
 import sys, re, signal, tempfile, os, os.path, shutil, atexit
 
@@ -11,7 +40,6 @@ TEX_HEADER = r"""\documentclass{scrartcl}
 \usepackage[latin1]{inputenc}
 \usepackage[T1]{fontenc}
 \usepackage{ae,aecompl}
-\usepackage[active]{srcltx}
 
 \usepackage{color}
 \usepackage{bytefield}
@@ -63,7 +91,7 @@ def formatSimpleField(width, start, field):
             namesz = areas[i]['size']
             nameix = i
     areas[nameix]['name'] = field['name'][:int(areas[nameix]['size'] * charsPerBit)]
-    if len(areas) == 2 and areas[0].get('bottom',True):
+    if field['name'] and len(areas) == 2 and areas[0].get('bottom',True):
         if areas[0].get('name','') : ix = 1
         else                       : ix = 0
         if 6 <= int(areas[ix]['size'] * charsPerBit):
@@ -110,6 +138,10 @@ def formatPacket(width, fields):
         if field.get('optional', False):
             for area in areas:
                 area['optional'] = True
+            if start > 0 and start < width:
+                areas.append({ 'start': start, 'size': width-start, 'bottom': False,
+                               'right': False})
+            start = 0
         if field.get('repeat'):
             if start > 0 and start < width:
                 areas.append({ 'start': start, 'size': width-start, 'bottom': False,
@@ -130,7 +162,7 @@ def formatPacket(width, fields):
                     # This is a fillup field. Check, wether to draw top:
                     if len(rows) <= 1:
                         areas[0]['top'] = False
-                    elif rows[-2][-1].get('bottom', True):
+                    elif rows[-2][-1].get('bottom', True) or not rows[-2][-1].get('right', True):
                         areas[0]['top'] = False
                 rows[-1].append(areas.pop(0))
             if areas:
@@ -154,7 +186,7 @@ def makeTex(rows):
             if sides == "lrtb" : sides = ""
             else               : sides = "[%s]" % sides
             if area.get('filled', False):
-                line.append(r"\bitbox%s{%s}{\color[gray]{0.7}\rule{\width}{\height}}" % (sides, area['size']))
+                line.append(r"\bitbox%s{%s}{\color[gray]{0.93}\rule{\width}{\height}}" % (sides, area['size']))
             elif area.get('skip', False):
                 line.append(r"\skippedwords")
             elif area.get('dots', False):
@@ -173,11 +205,23 @@ def stripComments(text):
     def replacer(match):
         s = match.group(0)
         if s.startswith('//<pkgdraw:') or s.startswith('//>pkgdraw:'):
-            return s
+            return "@@" + s[2:]
         if s.startswith('/'):
             return ""
         return s
     return COMMENT_RE.sub(replacer, text)
+
+SENF_INCLUDE_RE = re.compile(r"#\s*include\s*SENF_")
+
+def quoteMacros(text):
+    return SENF_INCLUDE_RE.sub("PKGDRAW_", text).replace("SENF_PARSER_","PKGDRAW_PARSER_")
+
+def cppExpand(text, cppopts, dir):
+    tmpf = tempfile.NamedTemporaryFile(dir=dir)
+    tmpf.write(text)
+    tmpf.flush()
+    cmd = "gcc %s -E -o - -x c++-header %s" % (" ".join(cppopts), tmpf.name)
+    return os.popen(cmd).read()
     
 FIELD_TYPES = {
     'UInt8Parser' :  {'size': 8 },
@@ -334,22 +378,47 @@ def parse_PRIVATE_VARIANT(args, flags):
 def parse_INIT(args, flags):
     return None
 
-PARSER_START_RE = re.compile(r"#\s*include\s+SENF_(FIXED_)?PARSER\s*\(\s*\)")
-PARSER_END_RE = re.compile(r"SENF_PARSER_FINALIZE\s*\(([^)]*)\)\s*;")
-PARSER_FIELD_RE = re.compile(r"(?://>pkgdraw:(.*)$\s*)?SENF_PARSER_([A-Z_]+)\s*\(([^;]*)\)\s*;(?:\s*//<pkgdraw:(.*)$)?", re.M)
+PARSER_START_RE = re.compile(r"PKGDRAW_(FIXED_)?PARSER\s*\(\s*\)")
+PARSER_END_RE = re.compile(r"PKGDRAW_PARSER_FINALIZE\s*\(([^)]*)\)\s*;")
+PARSER_FIELD_RE = re.compile(r"(?:@@>pkgdraw:(.*)$\s*)?PKGDRAW_PARSER_([A-Z_]+)\s*\(([^;]*)\)\s*;(?:\s*@@<pkgdraw:(.*)$)?", re.M)
 
 def scanPackets(data):
+    global FIELD_TYPES
+    
     packets = {}
+    packetOrder = []
     end = 0
     while True:
         start =  PARSER_START_RE.search(data, end)
-        if not start: return packets
+        if not start: return (packets, packetOrder)
         start = start.end(0)
         end = PARSER_END_RE.search(data, start)
-        if not end: return packets
+        if not end: return (packets, packetOrder)
         name=end.group(1).strip()
         end = end.start(0)
         packets[name] = scanFields(data[start:end])
+        packetOrder.append(name)
+        minsize = maxsize = 0
+        for field in packets[name]:
+            if maxsize is not None:
+                if field.get('repeat', False):
+                    maxsize = None
+                elif field.get('size', None) is not None:
+                    maxsize += field['size']
+                elif field.get('minsize', None) is not None:
+                    maxsize += field['maxsize']
+                else:
+                    maxsize = None
+            if not field.get('optional', False):
+                if field.get('size', None) is not None:
+                    minsize += field['size']
+                elif field.get('minsize', None) is not None:
+                    minsize += field['minsize']
+        if minsize is not None and maxsize is not None:
+            if minsize == maxsize:
+                FIELD_TYPES[name] = { 'size' : minsize }
+            else:
+                FIELD_TYPES[name] = { 'minsize' : minsize, 'maxsize' : maxsize }
 
 def scanFields(data):
     fields = []
@@ -364,7 +433,17 @@ def scanFields(data):
             if field:
                 if flags.has_key('name') : field['name'] = flags['name']
                 field['name'] = field['name'].strip('_')
-                fields.append(field)
+                if flags.has_key('size'):
+                    if '-' in flags['size']:
+                        field['minsize'], field['maxsize'] = map(int, flags['size'].split('-',1))
+                        del field['size']
+                    else:
+                        field['size'] = int(flags['size'])
+                if not field['name'] and fields and not fields[-1]['name'] \
+                       and field.has_key('size') and fields[-1].has_key('size'):
+                    fields[-1]['size'] += field['size']
+                else:
+                    fields.append(field)
         else:
             sys.stderr.write("Unknown parser type: %s\n" % tp)
     return fields
@@ -380,16 +459,30 @@ signal.signal(signal.SIGTERM, cleanup)
 signal.signal(signal.SIGHUP, cleanup)
 atexit.register(cleanup)
 
-data = scanPackets(stripComments(sys.stdin.read()))
+args = sys.argv[1:]
+names = []
+gccopts = []
+
+if len(args)<2 or args[0] == '--' or args[1] == '--':
+    sys.stderr.write("Usage: %s <header> <outfile> [<parser names>...] [-- <cpp options>...]\n"
+                     % sys.argv[0])
+    sys.exit(1)
+
+source = args.pop(0)
+target = args.pop(0)
+
+while args and args[0] != '--' : names.append(args.pop(0))
+if args : gccopts = args[1:]
+
+data, order = scanPackets(cppExpand(quoteMacros(stripComments(file(source).read())),
+                                    gccopts, os.path.dirname(source)))
 
 texf = file(os.path.join(tmpdir, "fields.tex"),"w")
 texf.write(TEX_HEADER)
 
-if len(sys.argv) > 1:
-    names = sys.argv[1:]
-else:
-    names = data.keys()
-    names.sort()
+if not names:
+    order.reverse()
+    names = order
 
 for name in names:
     texf.write("\\textbf{%s}\n\\bigskip\\par\n" % texquote(name))
@@ -405,4 +498,4 @@ if os.system("cd %s; %s/textogif -png -dpi 80 -res 0.25 fields >pkgdraw.log 2>&1
     sys.stderr.write("Conversion failed. See %s\n" % tmpdir)
     os._exit(1)
 
-sys.stdout.write(file(os.path.join(tmpdir, "fields.png")).read())
+file(target,"w").write(file(os.path.join(tmpdir, "fields.png")).read())