3 ### Parse a profile definition file and emit a particular section
5 ### (c) 2011 Mark Wooding
8 ###----- Licensing notice ---------------------------------------------------
10 ### This file is part of the distorted.org.uk key management suite.
12 ### distorted-keys is free software; you can redistribute it and/or modify
13 ### it under the terms of the GNU General Public License as published by
14 ### the Free Software Foundation; either version 2 of the License, or
15 ### (at your option) any later version.
17 ### distorted-keys is distributed in the hope that it will be useful,
18 ### but WITHOUT ANY WARRANTY; without even the implied warranty of
19 ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 ### GNU General Public License for more details.
22 ### You should have received a copy of the GNU General Public License
23 ### along with distorted-keys; if not, write to the Free Software Foundation,
24 ### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
26 from __future__ import with_statement
32 from cStringIO import StringIO
37 ###--------------------------------------------------------------------------
40 class struct (object):
41 def __init__(me, **kw):
42 me.__dict__.update(kw)
44 class UserError (Exception): pass
46 ###--------------------------------------------------------------------------
47 ### Configuration section management.
49 class prop (struct): pass
51 class Section (object, UD.DictMixin):
53 A section of a profile configuration file.
56 ## States for depth-first traversal.
57 V_WHITE = 0 # Not yet visited.
58 V_GREY = 1 # Currently being visited.
59 V_BLACK = 2 # Visited previously and processed.
61 def __init__(me, name, dict = None):
63 Initialize a Section object.
65 The DICT provides the initial (direct) contents of the Section. The NAME
66 is used for presentation purposes, e.g., when reporting error conditions.
68 super(Section, me).__init__()
69 if dict is None: me._dict = {}
71 me._visited = me.V_WHITE
77 ## Dictionary methods for UD.DictMixin. The built-in `dict' class provides
78 ## equality, and is therefore not hashable. By doing things this way, we
79 ## can retain reference equality and stay hashable, which will be useful
81 def __getitem__(me, key):
83 def __setitem__(me, key, value):
85 def __delitem__(me, key):
88 return me._dict.keys()
89 def __contains__(me, key):
90 return key in me._dict
92 return me._dict.__iter__()
94 return me._dict.iteritems()
96 return 'Section(%r, %r)' % (me.name, me.inherited)
98 def transit(me, seen = None, path = None):
100 Visit the Section for the purposes of computing transitive inclusion.
102 If this completes successfully, the Section's inferiors slot is set up to
103 contain all of its (non-strict) inferiors. A section's inferiors consist
104 of itself, together with the union of the inferiors of all of its
107 If the Section's visited state is black, nothing happens; if it's white
108 then it will be coloured grey temporarily, and its included Sections
109 processed recursively; if it's grey to begin with then we have
112 The SEEN dictionary and PATH list are used for detecting and reporting
113 cycles. The PATH contains a list of the currently grey Sections, in the
114 order in which they were encountered; SEEN maps Section names to their
115 indices in the PATH list.
117 It is possible to make this work in the presence of cycles, but it's more
118 effort than it's worth.
121 ## Extend the path to include us. This will be useful when reporting
123 if seen is None: seen = {}
124 if path is None: path = []
127 ## Already been here: nothing to do.
128 if me._visited == me.V_BLACK:
131 ## We've found a cycle: report it to the user.
132 elif me._visited == me.V_GREY:
133 raise UserError, 'detected inclusion cycle:\n\t%s' % \
134 ' -> '.join(["`%s'" % s.name for s in path[seen[me]:]])
136 ## Not done this one yet: process my included Sections, and compute the
137 ## union of their inferiors.
139 seen[me] = len(path) - 1
140 me._visited = me.V_GREY
141 me.inferiors = set([me])
142 for s in me.includes:
143 s.transit(seen, path)
144 me.inferiors.update(s.inferiors)
145 me._visited = me.V_BLACK
147 ## Remove myself from the path.
152 Compute the inherited properties for this Section.
154 A Section has an inherited property named P if any inferior has a direct
155 property named P. The value of the property is determined as follows.
156 Firstly, determine the set A of all inferiors which have a direct
157 property P. Secondly, determine a /reduced/ set containing only the
158 maximal elements of A: if R contains a pair of distinct inferiors I and J
159 such that I is an inferior of J, then R does not contain I; R contains
160 all elements A not so excluded. If all inferiors in R define the same
161 value for the property, then that is the value of the inherited property;
162 if two inferiors disagree, then the situation is erroneous.
164 Note that if a Section defines a direct property then it has an inherited
165 property with the same value: in this case, the reduced set is a
169 ## First pass: for each property name, determine the reduced set of
170 ## inferiors defining that property, and the values they have for it.
171 ## Here, D maps property names to lists of `prop' records.
173 for s in me.inferiors:
175 ## Work through the direct properties of inferior S.
176 for k, v in s.iteritems():
178 ## Ignore `special' properties.
179 if k.startswith('@'):
182 ## Work through the current reduced set. Discard entries from
183 ## sections inferior to S. If an entry exists for a section T to
184 ## which S is inferior, then don't add S itself.
189 if s in q.source.inferiors:
191 if q.source not in s.inferiors:
196 pp.append(prop(value = v, source = s))
199 ## Second pass: check that the reduced set defines a unique value for
200 ## each inherited property.
201 for k, vv in d.iteritems():
204 ## Build in C a dictionary mapping candidate values to lists of
205 ## inferiors asserting those values.
207 c.setdefault(p.value, []).append(p.source)
209 ## Now C should have only one key. If not, C records enough
210 ## information that we can give a useful error report.
213 "inconsistent values for property `%s' in section `%s': %s" % \
215 ''.join(["\n\t`%s' via %s" %
216 (v, ', '.join(["`%s'" % s.name for s in ss]))
217 for v, ss in c.iteritems()]))
219 ## Insert the computed property value.
220 me.inherited[k] = c.keys()[0]
222 def expand(me, string, seen = None, path = None):
224 Expand placeholders in STRING and return the result.
226 A placeholder has the form $PROP or ${PROP} (the latter syntax identifies
227 the property name unambiguously), and is replaced by the value of the
228 (inherited) property named PROP. A token $$ is replaced with a single $.
230 The SEEN and PATH parameters work the same way as in the `transit'
234 if seen is None: seen = {}
235 if path is None: path = []
237 ## Prepare stuff for the loop.
242 ## Pick out placeholders and expand them.
245 ## Find a placeholder.
246 dol = string.find('$', left)
248 ## None: commit the rest of the string and we're done.
250 out.write(string[left:])
253 ## Commit the portion before the placeholder.
254 out.write(string[left:dol])
256 ## Check for a trailing `$'. After this, we can be sure of at least
257 ## one more character.
261 ## If there's a left brace, find a right brace: the property name is
263 elif string[dol + 1] == '{':
264 ace = string.find('}', dol + 2)
267 "invalid placeholder (missing `}') in `%s'" % string
268 prop = string[dol + 2:ace]
271 ## If there's a dollar, just commit it and go round again.
272 elif string[dol + 1] == '$':
277 ## Otherwise take as many constituent characters as we can.
280 if left < n and string[left] == '@':
282 while left < n and (string[left].isalnum() or string[left] in '%-_'):
284 prop = string[dol + 1:left]
286 ## If we came up empty, report an error.
289 "invalid placeholder (empty name) in `%s'" % string
291 ## Extend the path: we're going to do a recursive expansion.
292 prop = prop.replace('-', '_')
295 ## Report a cycle if we found one.
297 raise UserError, 'substitution cycle:\n\t%s' % \
298 (' -> '.join(["`%s'" % p for p in path[seen[prop]:]]))
300 ## Look up the raw value.
305 value = me.inherited[prop]
307 raise UserError, "unknown property `%s'" % prop
309 ## Recursively expand, and unwind the PATH and SEEN stuff.
310 seen[prop] = len(path) - 1
311 out.write(me.expand(value, seen, path))
315 ## Done: return the accumulated result.
316 return out.getvalue()
320 Link together the Sections in D according to their inclusions.
322 If a Section S has an `@include' special property, then set S's `includes'
323 slot to be the set of sections named in that property's value. Then
324 compute the inferiors and inherited properties for all of the Sections.
327 ## Capture the global section.
330 ## Walk through all of the sections.
331 for sect in d.itervalues():
333 ## If this isn't the global section, then add the global section as an
334 ## implicit inclusion.
338 ## If there are explicit inclusions, then add them to the included set.
340 inc = sect['@include']
344 for s in inc.split():
346 sect.includes.add(d[s])
349 "unknown section `%s' included in `%s'" % (s, sect.name)
351 ## Compute the inferiors and inherited properties.
352 for sect in d.itervalues():
354 for sect in d.itervalues():
357 ###--------------------------------------------------------------------------
358 ### Parsing input files.
360 ## Names of special properties. All of these begin with an `@' sign.
361 SPECIALS = set(['@include'])
363 def parse(filename, d):
365 Parse a profile file FILENAME, updating dictionary D.
367 Each entry in the dictionary maps a section name to the section's contents;
368 the contents are in turn represented as a dictionary mapping properties to
369 values. Inter-section references, defaults, and so on are not processed
375 with open(filename) as f:
380 if not line or line[0] in ';#':
382 if line[0] == '[' and line[-1] == ']':
385 d[sect] = Section(sect)
388 ## Parse an assignment.
390 colon = line.find(':')
391 if eq < 0 or 0 <= colon < eq: eq = colon
392 if eq < 0: raise UserError, '%s:%d: no assignment' % (filename, n)
393 name, value = line[:eq].strip(), line[eq + 1:].strip()
395 ## Check that the name is well-formed.
396 name = name.replace('-', '_')
399 all(map(lambda ch: ch in '%_' or ch.isalnum(), name)))):
400 raise UserError, "%s:%d: bad name `%s'" % (filename, n, name)
402 ## Store the assignment.
403 d[sect][name] = value
405 ###--------------------------------------------------------------------------
409 usage = '%prog SECTION FILE|DIRECTORY ...',
410 version = '%%prog, %s version %s' % (PACKAGE, VERSION),
412 Parse the configurations FILE and DIRECTORY contents, and output the named
413 SECTION as a sequence of simple assignments.
419 ## Check the arguments.
420 opts, args = OP.parse_args(args[1:])
422 OP.error('not enough positional parameters')
426 ## Read in the inputs.
427 d = { '@GLOBAL': Section('@GLOBAL') }
430 ## It's a directory: pick out the files contained.
432 for sf in sorted(OS.listdir(f)):
433 if not all(map(lambda ch: ch in '_-' or ch.isalnum(), sf)):
435 ff = OS.path.join(f, sf)
436 if not OS.path.isfile(ff):
440 ## Not a directory: just try to parse it.
444 ## Print the contents.
449 raise UserError, "unknown section `%s'" % sect
450 for k, v in s.inherited.iteritems():
451 if '%' in k: continue
452 print '%s=%s' % (k, s.expand(v))
454 ## Report errors for expected problems.
456 SYS.stderr.write('%s: %s\n' % (OP.get_prog_name(), e.args[0]))
459 SYS.stderr.write('%s: %s\n' % (OP.get_prog_name(), e.args[1]))
462 SYS.stderr.write('%s: %s: %s\n' %
463 (OP.get_prog_name(), e.filename, e.strerror))
466 if __name__ == '__main__':
469 ###----- That's all, folks --------------------------------------------------