From b7e5aa06ec192af281f7acb38f7cf8c8d8363dc8 Mon Sep 17 00:00:00 2001 Message-Id: From: Mark Wooding Date: Sat, 26 May 2018 13:55:02 +0100 Subject: [PATCH] peerdb/tripe-newpeers.in (MyConfigParser): Abandon Python `ConfigParser'. Organization: Straylight/Edgeware From: Mark Wooding Instead, just parse the input by hand. This makes the behaviour easier to specify properly. The language accepted is now actually as described in the manpage, rather than also, say, stripping `;' comments (but not `#' comments) from assignment and continuation lines, or interpreting a `""' as an empty value. Fortunately, the rest of the program doesn't make much use of the `ConfigParser' protocol, so this isn't missed. --- peerdb/tripe-newpeers.in | 129 ++++++++++++++++++++++++++++++++++----- 1 file changed, 113 insertions(+), 16 deletions(-) diff --git a/peerdb/tripe-newpeers.in b/peerdb/tripe-newpeers.in index 59fd85fa..37c0a341 100644 --- a/peerdb/tripe-newpeers.in +++ b/peerdb/tripe-newpeers.in @@ -28,13 +28,13 @@ 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 +from cStringIO import StringIO ###-------------------------------------------------------------------------- ### Utilities. @@ -103,12 +103,38 @@ class BulkResolver (object): ###-------------------------------------------------------------------------- ### 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. RX_REF = RX.compile(r'(?x) \$ \( ([^)]+) \)') ## Match a $[HOST] name resolution reference; group 1 is the HOST. 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]) @@ -137,11 +163,11 @@ class MissingKeyException (Exception): def __str__(me): return "Key `%s' not found in section `%s'" % (me.key, me.sec) -class MyConfigParser (CP.RawConfigParser): +class MyConfigParser (object): """ A more advanced configuration parser. - This has three 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 @@ -153,10 +179,11 @@ class MyConfigParser (CP.RawConfigParser): * It recognizes `$[HOST]' name-resolver requests and handles them correctly. + * Its parsing behaviour is well-defined. + 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. @@ -169,9 +196,81 @@ class MyConfigParser (CP.RawConfigParser): """ Initialize a new, empty configuration parser. """ - CP.RawConfigParser.__init__(me) + me._sectmap = dict() me._resolver = BulkResolver() + def parse(me, f): + """ + Parse configuration from a file F. + """ + + ## 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[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] = dict() + 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)) + + else: + ## Something else. + + raise ConfigSyntaxError(f.name, lno, 'incomprehensible line') + + ## Don't forget to commit any final value material. + flush() + + def sections(me): + """Yield the known section names.""" + return me._sectmap.iterkeys() + def resolve(me): """ Works out all of the hostnames which need resolving and resolves them. @@ -179,7 +278,7 @@ class MyConfigParser (CP.RawConfigParser): Until you call this, attempts to fetch configuration items which need to resolve hostnames will fail! """ - for sec in me.sections(): + for sec in me._sectmap.iterkeys(): for key, value in me.items(sec, resolvep = False): for match in RX_RESOLVE.finditer(value): me._resolver.prepare(match.group(1)) @@ -241,15 +340,14 @@ class MyConfigParser (CP.RawConfigParser): if threadp: raise InheritanceCycleError(key, path[:]) ## See whether the answer is ready waiting for us. - try: v = CP.RawConfigParser.get(me, sec, key) - except CP.NoOptionError: pass + try: v = me._sectmap[sec][key] + except KeyError: pass else: return v, path[:] ## No, apparently, not. Find out our list of parents. try: - parents = CP.RawConfigParser.get(me, sec, '@inherit').\ - replace(',', ' ').split() - except CP.NoOptionError: + parents = me._sectmap[sec]['@inherit'].replace(',', ' ').split() + except KeyError: parents = [] ## Initially we have no idea. @@ -286,8 +384,7 @@ class MyConfigParser (CP.RawConfigParser): ## Special handling for the `name' key. if key == 'name': - try: value = CP.RawConfigParser.get(me, sec, key) - except CP.NoOptionError: value = sec + value = me._sectmap[sec].get('name', sec) else: value, _ = me._get(sec, key) if value is None: @@ -316,7 +413,7 @@ class MyConfigParser (CP.RawConfigParser): if sec in visited: continue visited[sec] = True - for key, value in CP.RawConfigParser.items(me, sec): + for key, value in me._sectmap[sec].iteritems(): if key == '@inherit': stack += value.replace(',', ' ').split() else: d[key] = None @@ -382,7 +479,7 @@ def getconf(args): """ conf = MyConfigParser() for f in inputiter(args): - conf.readfp(f) + conf.parse(f) conf.resolve() return conf -- [mdw]