peerdb/tripe-newpeers.in: Abolish `ConfigSection.has_option'.
[tripe] / peerdb / tripe-newpeers.in
index aa9ccfc..304d34a 100644 (file)
 ###
 ### This file is part of Trivial IP Encryption (TrIPE).
 ###
 ###
 ### This file is part of Trivial IP Encryption (TrIPE).
 ###
-### TrIPE is free software; you can redistribute it and/or modify
-### it under the terms of the GNU General Public License as published by
-### the Free Software Foundation; either version 2 of the License, or
-### (at your option) any later version.
+### TrIPE is free software: you can redistribute it and/or modify it under
+### the terms of the GNU General Public License as published by the Free
+### Software Foundation; either version 3 of the License, or (at your
+### option) any later version.
 ###
 ###
-### TrIPE is distributed in the hope that it will be useful,
-### but WITHOUT ANY WARRANTY; without even the implied warranty of
-### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-### GNU General Public License for more details.
+### TrIPE is distributed in the hope that it will be useful, but WITHOUT
+### ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+### FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+### for more details.
 ###
 ### You should have received a copy of the GNU General Public License
 ###
 ### You should have received a copy of the GNU General Public License
-### along with TrIPE; if not, write to the Free Software Foundation,
-### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+### along with TrIPE.  If not, see <https://www.gnu.org/licenses/>.
 
 VERSION = '@VERSION@'
 
 ###--------------------------------------------------------------------------
 ### External dependencies.
 
 
 VERSION = '@VERSION@'
 
 ###--------------------------------------------------------------------------
 ### External dependencies.
 
-import ConfigParser as CP
 import mLib as M
 from optparse import OptionParser
 import cdb as CDB
 from sys import stdin, stdout, exit, argv
 import re as RX
 import os as OS
 import mLib as M
 from optparse import OptionParser
 import cdb as CDB
 from sys import stdin, stdout, exit, argv
 import re as RX
 import os as OS
+from cStringIO import StringIO
 
 ###--------------------------------------------------------------------------
 ### Utilities.
 
 ###--------------------------------------------------------------------------
 ### Utilities.
@@ -93,7 +92,7 @@ class BulkResolver (object):
     """
     addr = me._namemap[host]
     if addr is None:
     """
     addr = me._namemap[host]
     if addr is None:
-      raise KeyError, host
+      raise KeyError(host)
     return addr
 
   def _resolved(me, host, addr):
     return addr
 
   def _resolved(me, host, addr):
@@ -104,17 +103,223 @@ class BulkResolver (object):
 ###--------------------------------------------------------------------------
 ### The configuration parser.
 
 ###--------------------------------------------------------------------------
 ### The configuration parser.
 
+## Match a comment or empty line.
+RX_COMMENT = RX.compile(r'(?x) ^ \s* (?: $ | [;#])')
+
+## Match a section group header.
+RX_GRPHDR = RX.compile(r'(?x) ^ \s* \[ (.*) \] \s* $')
+
+## Match an assignment line.
+RX_ASSGN = RX.compile(r'''(?x) ^
+        ([^\s:=] (?: [^:=]* [^\s:=])?)
+        \s* [:=] \s*
+        (| \S | \S.*\S)
+        \s* $''')
+
+## Match a continuation line.
+RX_CONT = RX.compile(r'''(?x) ^ \s+
+        (| \S | \S.*\S)
+        \s* $''')
+
 ## Match a $(VAR) configuration variable reference; group 1 is the VAR.
 ## Match a $(VAR) configuration variable reference; group 1 is the VAR.
-r_ref = RX.compile(r'\$\(([^)]+)\)')
+RX_REF = RX.compile(r'(?x) \$ \( ([^)]+) \)')
 
 ## Match a $[HOST] name resolution reference; group 1 is the HOST.
 
 ## Match a $[HOST] name resolution reference; group 1 is the HOST.
-r_resolve = RX.compile(r'\$\[([^]]+)\]')
+RX_RESOLVE = RX.compile(r'(?x) \$ \[ ([^]]+) \]')
+
+class ConfigSyntaxError (Exception):
+  def __init__(me, fname, lno, msg):
+    me.fname = fname
+    me.lno = lno
+    me.msg = msg
+  def __str__(me):
+    return '%s:%d: %s' % (me.fname, me.lno, me.msg)
+
+def _fmt_path(path):
+  return ' -> '.join(["`%s'" % hop for hop in path])
+
+class AmbiguousOptionError (Exception):
+  def __init__(me, key, patha, vala, pathb, valb):
+    me.key = key
+    me.patha, me.vala = patha, vala
+    me.pathb, me.valb = pathb, valb
+  def __str__(me):
+    return "Ambiguous answer resolving key `%s': " \
+        "path %s yields `%s' but %s yields `%s'" % \
+        (me.key, _fmt_path(me.patha), me.vala, _fmt_path(me.pathb), me.valb)
+
+class InheritanceCycleError (Exception):
+  def __init__(me, key, path):
+    me.key = key
+    me.path = path
+  def __str__(me):
+    return "Found a cycle %s looking up key `%s'" % \
+        (_fmt_path(me.path), me.key)
+
+class MissingSectionException (Exception):
+  def __init__(me, sec):
+    me.key = key
+  def __str__(me):
+    return "Section `%s' not found" % (me.sec)
+
+class MissingKeyException (Exception):
+  def __init__(me, sec, key):
+    me.sec = sec
+    me.key = key
+  def __str__(me):
+    return "Key `%s' not found in section `%s'" % (me.key, me.sec)
+
+class ConfigSection (object):
+  """
+  A section in a configuration parser.
 
 
-class MyConfigParser (CP.RawConfigParser):
+  This is where a lot of the nitty-gritty stuff actually happens.  The
+  `MyConfigParser' knows a lot about the internals of this class, which saves
+  on building a complicated interface.
+  """
+
+  def __init__(me, name, cp):
+    """Initialize a new, empty section with a given NAME and parent CP."""
+    me.name = name
+    me._itemmap = dict()
+    me._cp = cp
+
+  def _expand(me, string, resolvep):
+    """
+    Expands $(...) and (optionally) $[...] placeholders in STRING.
+
+    RESOLVEP is a boolean switch: do we bother to tax the resolver or not?
+    This is turned off by MyConfigParser's resolve() method while it's
+    collecting hostnames to be resolved.
+    """
+    string = RX_REF.sub \
+             (lambda m: me.get(m.group(1), resolvep), string)
+    if resolvep:
+      string = RX_RESOLVE.sub(lambda m: me._cp._resolver.lookup(m.group(1)),
+                              string)
+    return string
+
+  def _get(me, key, map = None, path = None):
+    """
+    Low-level option-fetching method.
+
+    Fetch the value for the named KEY in this section, or maybe (recursively)
+    a section which it inherits from.
+
+    Returns a pair VALUE, PATH.  The value is not expanded; nor do we check
+    for the special `name' key.  The caller is expected to do these things.
+    Returns None if no value could be found.
+    """
+
+    ## If we weren't given a memoization map or path, then we'd better make
+    ## one.
+    if map is None: map = {}
+    if path is None: path = []
+
+    ## Extend the path to cover us, but remember to remove us again when
+    ## we've finished.  If we need to pass the current path back upwards,
+    ## then remember to take a copy.
+    path.append(me.name)
+    try:
+
+      ## If we've been this way before on another pass through then return
+      ## the value we found then.  If we're still thinking about it then
+      ## we've found a cycle.
+      try: threadp, value = map[me.name]
+      except KeyError: pass
+      else:
+        if threadp: raise InheritanceCycleError(key, path[:])
+
+      ## See whether the answer is ready waiting for us.
+      try: v = me._itemmap[key]
+      except KeyError: pass
+      else: return v, path[:]
+
+      ## No, apparently, not.  Find out our list of parents.
+      try:
+        parents = [me._cp.section(p) for p in
+                   me._itemmap['@inherit'].replace(',', ' ').split()]
+      except KeyError:
+        parents = []
+
+      ## Initially we have no idea.
+      value = None
+      winner = None
+
+      ## Go through our parents and ask them what they think.
+      map[me.name] = True, None
+      for p in parents:
+
+        ## See whether we get an answer.  If not, keep on going.
+        v, pp = p._get(key, map, path)
+        if v is None: continue
+
+        ## If we got an answer, check that it matches any previous ones.
+        if value is None:
+          value = v
+          winner = pp
+        elif value != v:
+          raise AmbiguousOptionError(key, winner, value, pp, v)
+
+      ## That's the best we could manage.
+      map[me.name] = False, value
+      return value, winner
+
+    finally:
+      ## Remove us from the path again.
+      path.pop()
+
+  def get(me, key, resolvep = True):
+    """
+    Retrieve the value of KEY from this section.
+    """
+
+    ## Special handling for the `name' key.
+    if key == 'name':
+      value = me._itemmap.get('name', me.name)
+    else:
+      value, _ = me._get(key)
+      if value is None:
+        raise MissingKeyException(me.name, key)
+
+    ## Expand the value and return it.
+    return me._expand(value, resolvep)
+
+  def items(me, resolvep = True):
+    """
+    Return a list of (NAME, VALUE) items in this section.
+
+    This extends the default method by handling the inheritance chain.
+    """
+
+    ## Initialize for a depth-first walk of the inheritance graph.
+    d = {}
+    visited = {}
+    stack = [me.name]
+
+    ## Visit nodes, collecting their keys.  Don't believe the values:
+    ## resolving inheritance is too hard to do like this.
+    while stack:
+      sec = me._cp.section(stack.pop())
+      if sec.name in visited: continue
+      visited[sec.name] = True
+
+      for key, value in sec._itemmap.iteritems():
+        if key == '@inherit': stack += value.replace(',', ' ').split()
+        else: d[key] = None
+
+    ## Now collect the values for the known keys, one by one.
+    items = []
+    for key in d: items.append((key, me.get(key, resolvep)))
+
+    ## And we're done.
+    return items
+
+class MyConfigParser (object):
   """
   A more advanced configuration parser.
 
   """
   A more advanced configuration parser.
 
-  This has two major enhancements over the standard ConfigParser which are
+  This has four major enhancements over the standard ConfigParser which are
   relevant to us.
 
     * It recognizes `@inherits' keys and follows them when expanding a
   relevant to us.
 
     * It recognizes `@inherits' keys and follows them when expanding a
@@ -126,15 +331,19 @@ class MyConfigParser (CP.RawConfigParser):
     * It recognizes `$[HOST]' name-resolver requests and handles them
       correctly.
 
     * It recognizes `$[HOST]' name-resolver requests and handles them
       correctly.
 
+    * Its parsing behaviour is well-defined.
+
   Use:
 
   Use:
 
-    1. Call read(FILENAME) and/or read(FP, [FILENAME]) to slurp in the
-       configuration data.
+    1. Call parse(FILENAME) to slurp in the configuration data.
 
     2. Call resolve() to collect the hostnames which need to be resolved and
        actually do the name resolution.
 
 
     2. Call resolve() to collect the hostnames which need to be resolved and
        actually do the name resolution.
 
-    3. Call get(SECTION, ITEM) to collect the results, or items(SECTION) to
+    3. Call sections() to get a list of the configuration sections, or
+       section(NAME) to find a named section.
+
+    4. Call get(ITEM) on a section to collect the results, or items() to
        iterate over them.
   """
 
        iterate over them.
   """
 
@@ -142,100 +351,98 @@ class MyConfigParser (CP.RawConfigParser):
     """
     Initialize a new, empty configuration parser.
     """
     """
     Initialize a new, empty configuration parser.
     """
-    CP.RawConfigParser.__init__(me)
+    me._sectmap = dict()
     me._resolver = BulkResolver()
 
     me._resolver = BulkResolver()
 
-  def resolve(me):
-    """
-    Works out all of the hostnames which need resolving and resolves them.
-
-    Until you call this, attempts to fetch configuration items which need to
-    resolve hostnames will fail!
-    """
-    for sec in me.sections():
-      for key, value in me.items(sec, resolvep = False):
-        for match in r_resolve.finditer(value):
-          me._resolver.prepare(match.group(1))
-    me._resolver.run()
-
-  def _expand(me, sec, string, resolvep):
-    """
-    Expands $(...) and (optionally) $[...] placeholders in STRING.
-
-    The SEC is the configuration section from which to satisfy $(...)
-    requests.  RESOLVEP is a boolean switch: do we bother to tax the resolver
-    or not?  This is turned off by the resolve() method while it's collecting
-    hostnames to be resolved.
-    """
-    string = r_ref.sub \
-             (lambda m: me.get(sec, m.group(1), resolvep), string)
-    if resolvep:
-      string = r_resolve.sub(lambda m: me._resolver.lookup(m.group(1)),
-                             string)
-    return string
-
-  def has_option(me, sec, key):
+  def parse(me, f):
     """
     """
-    Decide whether section SEC has a configuration key KEY.
-
-    This version of the method properly handles the @inherit key.
+    Parse configuration from a file F.
     """
     """
-    return CP.RawConfigParser.has_option(me, sec, key) or \
-           (CP.RawConfigParser.has_option(me, sec, '@inherit') and
-            me.has_option(CP.RawConfigParser.get(me, sec, '@inherit'), key))
 
 
-  def _get(me, basesec, sec, key, resolvep):
-    """
-    Low-level option-fetching method.
+    ## Initial parser state.
+    sect = None
+    key = None
+    val = None
+    lno = 0
+
+    ## An unpleasant hack.  Python makes it hard to capture a value in a
+    ## variable and examine it in a single action, and this is the best that
+    ## I came up with.
+    m = [None]
+    def match(rx): m[0] = rx.match(line); return m[0]
+
+    ## Commit a key's value when we've determined that there are no further
+    ## continuation lines.
+    def flush():
+      if key is not None: sect._itemmap[key] = val.getvalue()
+
+    ## Work through all of the input lines.
+    for line in f:
+      lno += 1
+
+      if match(RX_COMMENT):
+        ## A comment or a blank line.  Nothing doing.  (This means that we
+        ## leave out blank lines which look like they might be continuation
+        ## lines.)
+
+        pass
+
+      elif match(RX_GRPHDR):
+        ## A section header.  Flush out any previous value and set up the new
+        ## group.
+
+        flush()
+        name = m[0].group(1)
+        try: sect = me._sectmap[name]
+        except KeyError: sect = me._sectmap[name] = ConfigSection(name, me)
+        key = None
+
+      elif match(RX_ASSGN):
+        ## A new assignment.  Flush out the old one, and set up to store this
+        ## one.
+
+        if sect is None:
+          raise ConfigSyntaxError(f.name, lno, 'no active section to update')
+        flush()
+        key = m[0].group(1)
+        val = StringIO(); val.write(m[0].group(2))
+
+      elif match(RX_CONT):
+        ## A continuation line.  Accumulate the value.
+
+        if key is None:
+          raise ConfigSyntaxError(f.name, lno, 'no config value to continue')
+        val.write('\n'); val.write(m[0].group(1))
 
 
-    Fetch the value for the named KEY from section SEC, or maybe
-    (recursively) the section which SEC inherits from.
+      else:
+        ## Something else.
 
 
-    The result is expanded, by _expend; RESOLVEP is passed to _expand to
-    control whether $[...] should be expanded in the result.
+        raise ConfigSyntaxError(f.name, lno, 'incomprehensible line')
 
 
-    The BASESEC is the section for which the original request was made.  This
-    will be different from SEC if we're recursing up the inheritance chain.
+    ## Don't forget to commit any final value material.
+    flush()
 
 
-    We also provide the default value for `name' here.
-    """
-    try:
-      raw = CP.RawConfigParser.get(me, sec, key)
-    except CP.NoOptionError:
-      if key == 'name':
-        raw = basesec
-      elif CP.RawConfigParser.has_option(me, sec, '@inherit'):
-        raw = me._get(basesec,
-                      CP.RawConfigParser.get(me, sec, '@inherit'),
-                      key,
-                      resolvep)
-      else:
-        raise
-    return me._expand(basesec, raw, resolvep)
+  def section(me, name):
+    """Return a ConfigSection with the given NAME."""
+    try: return me._sectmap[name]
+    except KeyError: raise MissingSectionException(name)
 
 
-  def get(me, sec, key, resolvep = True):
-    """
-    Retrieve the value of KEY from section SEC.
-    """
-    return me._get(sec, sec, key, resolvep)
+  def sections(me):
+    """Yield the known sections."""
+    return me._sectmap.itervalues()
 
 
-  def items(me, sec, resolvep = True):
+  def resolve(me):
     """
     """
-    Return a list of (NAME, VALUE) items in section SEC.
+    Works out all of the hostnames which need resolving and resolves them.
 
 
-    This extends the default method by handling the inheritance chain.
+    Until you call this, attempts to fetch configuration items which need to
+    resolve hostnames will fail!
     """
     """
-    d = {}
-    basesec = sec
-    while sec:
-      next = None
-      for key, value in CP.RawConfigParser.items(me, sec):
-        if key == '@inherit':
-          next = value
-        elif not key.startswith('@') and key not in d:
-          d[key] = me._expand(basesec, value, resolvep)
-      sec = next
-    return d.items()
+    for sec in me.sections():
+      for key, value in sec.items(resolvep = False):
+        for match in RX_RESOLVE.finditer(value):
+          me._resolver.prepare(match.group(1))
+    me._resolver.run()
 
 ###--------------------------------------------------------------------------
 ### Command-line handling.
 
 ###--------------------------------------------------------------------------
 ### Command-line handling.
@@ -292,7 +499,7 @@ def getconf(args):
   """
   conf = MyConfigParser()
   for f in inputiter(args):
   """
   conf = MyConfigParser()
   for f in inputiter(args):
-    conf.readfp(f)
+    conf.parse(f)
   conf.resolve()
   return conf
 
   conf.resolve()
   return conf
 
@@ -303,20 +510,22 @@ def output(conf, cdb):
   This is where the special `user' and `auto' database entries get set.
   """
   auto = []
   This is where the special `user' and `auto' database entries get set.
   """
   auto = []
-  for sec in conf.sections():
-    if sec.startswith('@'):
+  for sec in sorted(conf.sections(), key = lambda sec: sec.name):
+    if sec.name.startswith('@'):
       continue
       continue
-    elif sec.startswith('$'):
-      label = sec
+    elif sec.name.startswith('$'):
+      label = sec.name
     else:
     else:
-      label = 'P%s' % sec
-      if conf.has_option(sec, 'auto') and \
-         conf.get(sec, 'auto') in ('y', 'yes', 't', 'true', '1', 'on'):
-        auto.append(sec)
-      if conf.has_option(sec, 'user'):
-        cdb.add('U%s' % conf.get(sec, 'user'), sec)
+      label = 'P%s' % sec.name
+      try: a = sec.get('auto')
+      except MissingKeyException: pass
+      else:
+        if a in ('y', 'yes', 't', 'true', '1', 'on'): auto.append(sec.name)
+      try: u = sec.get('user')
+      except MissingKeyException: pass
+      else: cdb.add('U%s' % u)
     url = M.URLEncode(laxp = True, semip = True)
     url = M.URLEncode(laxp = True, semip = True)
-    for key, value in conf.items(sec):
+    for key, value in sorted(sec.items(), key = lambda (k, v): k):
       if not key.startswith('@'):
         url.encode(key, ' '.join(M.split(value)[0]))
     cdb.add(label, url.result)
       if not key.startswith('@'):
         url.encode(key, ' '.join(M.split(value)[0]))
     cdb.add(label, url.result)