--- /dev/null
+#! @PYTHON@
+###
+### Parse a profile definition file and emit a particular section
+###
+### (c) 2011 Mark Wooding
+###
+
+###----- Licensing notice ---------------------------------------------------
+###
+### This file is part of the distorted.org.uk key management suite.
+###
+### distorted-keys 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.
+###
+### distorted-keys 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
+### along with distorted-keys; if not, write to the Free Software Foundation,
+### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+from __future__ import with_statement
+
+import sys as SYS
+import os as OS
+import UserDict as UD
+import optparse as O
+from cStringIO import StringIO
+
+PACKAGE = "@PACKAGE@"
+VERSION = "@VERSION@"
+
+###--------------------------------------------------------------------------
+### Utilities.
+
+class struct (object):
+ def __init__(me, **kw):
+ me.__dict__.update(kw)
+
+class UserError (Exception): pass
+
+###--------------------------------------------------------------------------
+### Configuration section management.
+
+class prop (struct): pass
+
+class Section (object, UD.DictMixin):
+ """
+ A section of a profile configuration file.
+ """
+
+ ## States for depth-first traversal.
+ V_WHITE = 0 # Not yet visited.
+ V_GREY = 1 # Currently being visited.
+ V_BLACK = 2 # Visited previously and processed.
+
+ def __init__(me, name, dict = None):
+ """
+ Initialize a Section object.
+
+ The DICT provides the initial (direct) contents of the Section. The NAME
+ is used for presentation purposes, e.g., when reporting error conditions.
+ """
+ super(Section, me).__init__()
+ if dict is None: me._dict = {}
+ else: me._dict = dict
+ me._visited = me.V_WHITE
+ me.name = name
+ me.includes = set()
+ me.inferiors = set()
+ me.inherited = {}
+
+ ## Dictionary methods for UD.DictMixin. The built-in `dict' class provides
+ ## equality, and is therefore not hashable. By doing things this way, we
+ ## can retain reference equality and stay hashable, which will be useful
+ ## later.
+ def __getitem__(me, key):
+ return me._dict[key]
+ def __setitem__(me, key, value):
+ me._dict[key] = value
+ def __delitem__(me, key):
+ del me._dict[key]
+ def keys(me):
+ return me._dict.keys()
+ def __contains__(me, key):
+ return key in me._dict
+ def __iter__(me):
+ return me._dict.__iter__()
+ def iteritems(me):
+ return me._dict.iteritems()
+ def __repr__(me):
+ return 'Section(%r, %r)' % (me.name, me.inherited)
+
+ def transit(me, seen = None, path = None):
+ """
+ Visit the Section for the purposes of computing transitive inclusion.
+
+ If this completes successfully, the Section's inferiors slot is set up to
+ contain all of its (non-strict) inferiors. A section's inferiors consist
+ of itself, together with the union of the inferiors of all of its
+ included Sections.
+
+ If the Section's visited state is black, nothing happens; if it's white
+ then it will be coloured grey temporarily, and its included Sections
+ processed recursively; if it's grey to begin with then we have
+ encountered a cycle.
+
+ The SEEN dictionary and PATH list are used for detecting and reporting
+ cycles. The PATH contains a list of the currently grey Sections, in the
+ order in which they were encountered; SEEN maps Section names to their
+ indices in the PATH list.
+
+ It is possible to make this work in the presence of cycles, but it's more
+ effort than it's worth.
+ """
+
+ ## Extend the path to include us. This will be useful when reporting
+ ## cycles.
+ if seen is None: seen = {}
+ if path is None: path = []
+ path.append(me)
+
+ ## Already been here: nothing to do.
+ if me._visited == me.V_BLACK:
+ pass
+
+ ## We've found a cycle: report it to the user.
+ elif me._visited == me.V_GREY:
+ raise UserError, 'detected inclusion cycle:\n\t%s' % \
+ ' -> '.join(["`%s'" % s.name for s in path[seen[me]:]])
+
+ ## Not done this one yet: process my included Sections, and compute the
+ ## union of their inferiors.
+ else:
+ seen[me] = len(path) - 1
+ me._visited = me.V_GREY
+ me.inferiors = set([me])
+ for s in me.includes:
+ s.transit(seen, path)
+ me.inferiors.update(s.inferiors)
+ me._visited = me.V_BLACK
+
+ ## Remove myself from the path.
+ path.pop()
+
+ def inherit(me):
+ """
+ Compute the inherited properties for this Section.
+
+ A Section has an inherited property named P if any inferior has a direct
+ property named P. The value of the property is determined as follows.
+ Firstly, determine the set A of all inferiors which have a direct
+ property P. Secondly, determine a /reduced/ set containing only the
+ maximal elements of A: if R contains a pair of distinct inferiors I and J
+ such that I is an inferior of J, then R does not contain I; R contains
+ all elements A not so excluded. If all inferiors in R define the same
+ value for the property, then that is the value of the inherited property;
+ if two inferiors disagree, then the situation is erroneous.
+
+ Note that if a Section defines a direct property then it has an inherited
+ property with the same value: in this case, the reduced set is a
+ singleton.
+ """
+
+ ## First pass: for each property name, determine the reduced set of
+ ## inferiors defining that property, and the values they have for it.
+ ## Here, D maps property names to lists of `prop' records.
+ d = {}
+ for s in me.inferiors:
+
+ ## Work through the direct properties of inferior S.
+ for k, v in s.iteritems():
+
+ ## Ignore `special' properties.
+ if k.startswith('@'):
+ continue
+
+ ## Work through the current reduced set. Discard entries from
+ ## sections inferior to S. If an entry exists for a section T to
+ ## which S is inferior, then don't add S itself.
+ addp = True
+ pp = []
+ try:
+ for q in d[k]:
+ if s in q.source.inferiors:
+ addp = False
+ if q.source not in s.inferiors:
+ pp.append(q)
+ except KeyError:
+ pass
+ if addp:
+ pp.append(prop(value = v, source = s))
+ d[k] = pp
+
+ ## Second pass: check that the reduced set defines a unique value for
+ ## each inherited property.
+ for k, vv in d.iteritems():
+ c = {}
+
+ ## Build in C a dictionary mapping candidate values to lists of
+ ## inferiors asserting those values.
+ for p in vv:
+ c.setdefault(p.value, []).append(p.source)
+
+ ## Now C should have only one key. If not, C records enough
+ ## information that we can give a useful error report.
+ if len(c) != 1:
+ raise UserError, \
+ "inconsistent values for property `%s' in section `%s': %s" % \
+ (k, me.name,
+ ''.join(["\n\t`%s' via %s" %
+ (v, ', '.join(["`%s'" % s.name for s in ss]))
+ for v, ss in c.iteritems()]))
+
+ ## Insert the computed property value.
+ me.inherited[k] = c.keys()[0]
+
+ def expand(me, string, seen = None, path = None):
+ """
+ Expand placeholders in STRING and return the result.
+
+ A placeholder has the form $PROP or ${PROP} (the latter syntax identifies
+ the property name unambiguously), and is replaced by the value of the
+ (inherited) property named PROP. A token $$ is replaced with a single $.
+
+ The SEEN and PATH parameters work the same way as in the `transit'
+ method.
+ """
+
+ if seen is None: seen = {}
+ if path is None: path = []
+
+ ## Prepare stuff for the loop.
+ out = StringIO()
+ left = 0
+ n = len(string)
+
+ ## Pick out placeholders and expand them.
+ while True:
+
+ ## Find a placeholder.
+ dol = string.find('$', left)
+
+ ## None: commit the rest of the string and we're done.
+ if dol < 0:
+ out.write(string[left:])
+ break
+
+ ## Commit the portion before the placeholder.
+ out.write(string[left:dol])
+
+ ## Check for a trailing `$'. After this, we can be sure of at least
+ ## one more character.
+ if dol + 1 >= n:
+ prop = ''
+
+ ## If there's a left brace, find a right brace: the property name is
+ ## between them.
+ elif string[dol + 1] == '{':
+ ace = string.find('}', dol + 2)
+ if ace < 0:
+ raise UserError, \
+ "invalid placeholder (missing `}') in `%s'" % string
+ prop = string[dol + 2:ace]
+ left = ace + 1
+
+ ## If there's a dollar, just commit it and go round again.
+ elif string[dol + 1] == '$':
+ left = dol + 2
+ out.write('$')
+ continue
+
+ ## Otherwise take as many constituent characters as we can.
+ else:
+ left = dol + 1
+ while left < n and (string[left].isalnum() or string[left] in '-_'):
+ left += 1
+ prop = string[dol + 1:left].replace('-', '_')
+
+ ## If we came up empty, report an error.
+ if prop == '':
+ raise UserError, \
+ "invalid placeholder (empty name) in `%s'" % string
+
+ ## Extend the path: we're going to do a recursive expansion.
+ path.append(prop)
+
+ ## Report a cycle if we found one.
+ if prop in seen:
+ raise UserError, 'substitution cycle:\n\t%s' % \
+ (' -> '.join(["`%s'" % p for p in path[seen[prop]:]]))
+
+ ## Look up the raw value.
+ try:
+ value = me.inherited[prop]
+ except KeyError:
+ raise UserError, "unknown property `%s'" % prop
+
+ ## Recursively expand, and unwind the PATH and SEEN stuff.
+ seen[prop] = len(path) - 1
+ out.write(me.expand(value, seen, path))
+ path.pop()
+ del seen[prop]
+
+ ## Done: return the accumulated result.
+ return out.getvalue()
+
+def link(d):
+ """
+ Link together the Sections in D according to their inclusions.
+
+ If a Section S has an `@include' special property, then set S's `includes'
+ slot to be the set of sections named in that property's value. Then
+ compute the inferiors and inherited properties for all of the Sections.
+ """
+
+ ## Capture the global section.
+ g = d['@GLOBAL']
+
+ ## Walk through all of the sections.
+ for sect in d.itervalues():
+
+ ## If this isn't the global section, then add the global section as an
+ ## implicit inclusion.
+ if sect is not g:
+ sect.includes.add(g)
+
+ ## If there are explicit inclusions, then add them to the included set.
+ try:
+ inc = sect['@include']
+ except KeyError:
+ pass
+ else:
+ for s in inc.split():
+ try:
+ sect.includes.add(d[s])
+ except KeyError:
+ raise UserError, \
+ "unknown section `%s' included in `%s'" % (s, sect.name)
+
+ ## Compute the inferiors and inherited properties.
+ for sect in d.itervalues():
+ sect.transit()
+ for sect in d.itervalues():
+ sect.inherit()
+
+###--------------------------------------------------------------------------
+### Parsing input files.
+
+## Names of special properties. All of these begin with an `@' sign.
+SPECIALS = set(['@include'])
+
+def parse(filename, d):
+ """
+ Parse a profile file FILENAME, updating dictionary D.
+
+ Each entry in the dictionary maps a section name to the section's contents;
+ the contents are in turn represented as a dictionary mapping properties to
+ values. Inter-section references, defaults, and so on are not processed
+ here.
+ """
+
+ sect = '@GLOBAL'
+
+ with open(filename) as f:
+ n = 0
+ for line in f:
+ n += 1
+ line = line.strip()
+ if not line or line[0] in ';#':
+ continue
+ if line[0] == '[' and line[-1] == ']':
+ sect = line[1:-1]
+ continue
+
+ ## Parse an assignment.
+ eq = line.find('=')
+ colon = line.find(':')
+ if eq < 0 or 0 <= colon < eq: eq = colon
+ if eq < 0: raise UserError, '%s:%d: no assignment' % (filename, n)
+ name, value = line[:eq].strip(), line[eq + 1:].strip()
+
+ ## Check that the name is well-formed.
+ name = name.replace('-', '_')
+ if not (name and
+ (name in SPECIALS or
+ all(map(lambda ch: ch == '_' or ch.isalnum(), name)))):
+ raise UserError, "%s:%d: bad name `%s'" % (filename, n, name)
+
+ ## Store the assignment.
+ try:
+ d[sect][name] = value
+ except KeyError:
+ s = Section(sect)
+ d[sect] = s
+ s[name] = value
+
+###--------------------------------------------------------------------------
+### Main program.
+
+OP = O.OptionParser(
+ usage = '%prog FILE|DIRECTORY ... SECTION',
+ version = '%%prog, version %s' % VERSION,
+ description = '''\
+Parse the configurations FILE and DIRECTORY contents, and output the named
+SECTION as a sequence of simple assignments.
+''')
+
+def main(args):
+ try:
+
+ ## Check the arguments.
+ opts, args = OP.parse_args(args[1:])
+ if len(args) < 2:
+ OP.error('not enough positional parameters')
+ files = args[:-1]
+ sect = args[-1]
+
+ ## Read in the inputs.
+ d = { '@GLOBAL': Section('@GLOBAL') }
+ for f in files:
+
+ ## It's a directory: pick out the files contained.
+ if OS.path.isdir(f):
+ for sf in sorted(OS.listdir(f)):
+ if not all(map(lambda ch: ch in '_-' or ch.isalnum(), sf)):
+ continue
+ ff = OS.path.join(f, sf)
+ if not OS.path.isfile(ff):
+ continue
+ parse(ff, d)
+
+ ## Not a directory: just try to parse it.
+ else:
+ parse(f, d)
+
+ ## Print the contents.
+ link(d)
+ try:
+ s = d[sect]
+ except KeyError:
+ raise UserError, "unknown section `%s'" % sect
+ for k, v in s.inherited.iteritems():
+ print '%s=%s' % (k, s.expand(v))
+
+ ## Report errors for expected problems.
+ except UserError, e:
+ SYS.stderr.write('%s: %s\n' % (OP.get_prog_name(), e.args[0]))
+ SYS.exit(1)
+ except OSError, e:
+ SYS.stderr.write('%s: %s\n' % (OP.get_prog_name(), e.args[1]))
+ SYS.exit(1)
+ except IOError, e:
+ SYS.stderr.write('%s: %s: %s\n' %
+ (OP.get_prog_name(), e.filename, e.strerror))
+ SYS.exit(1)
+
+if __name__ == '__main__':
+ main(SYS.argv)
+
+###----- That's all, folks --------------------------------------------------