X-Git-Url: https://git.distorted.org.uk/~mdw/distorted-keys/blobdiff_plain/599c8f754492a6555503fedb8a2662ec229fb8e8..c47f2aba7d705252c660ba1ad0931fbb93122d80:/extract-profile.in diff --git a/extract-profile.in b/extract-profile.in new file mode 100644 index 0000000..1d19fc2 --- /dev/null +++ b/extract-profile.in @@ -0,0 +1,465 @@ +#! @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 --------------------------------------------------