#! @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 if left < n and string[left] == '@': left += 1 while left < n and (string[left].isalnum() or string[left] in '%-_'): left += 1 prop = string[dol + 1:left] ## 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. prop = prop.replace('-', '_') 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. if prop == '@name': value = me.name else: 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] if sect not in d: d[sect] = Section(sect) 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 in '%_' or ch.isalnum(), name)))): raise UserError, "%s:%d: bad name `%s'" % (filename, n, name) ## Store the assignment. d[sect][name] = value ###-------------------------------------------------------------------------- ### Main program. OP = O.OptionParser( usage = '%prog SECTION FILE|DIRECTORY ...', 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') sect = args[0] files = 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(): if '%' in k: continue 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 --------------------------------------------------