934ef8743fc167e660eaa9994d6e3382b18e9c58
[confparser-old] / confparser.py
1 # -*- coding: iso-8859-1 -*-
2
3 #
4 # Configuration parser
5 # Copyright (C) 2005  Frédéric Jolliton <frederic@jolliton.com>
6 #
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 2 of the License, or
10 # (at your option) any later version.
11 #
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with this program; if not, write to the Free Software
19 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
20 #
21
22 import re
23 import types
24 import sys
25 import os
26 import stat
27 import errno
28
29 try :
30         import cPickle as pickle
31 except :
32         import pickle
33
34 import basicparser
35 import basicvalidator
36
37 class Error( Exception ) : pass
38
39 #
40 # Configuration parser check *syntax*.
41 # Validator check *structure*, and eventually some values.
42 #
43
44 #--[ Parser ]-----------------------------------------------------------------
45
46 def parseString( s ) :
47
48         if s.startswith( "'" ) :
49                 return s[ 1 : -1 ]
50         elif s.startswith( '"' ) :
51                 return s[ 1 : -1 ]
52         else :
53                 return s
54
55 def parseConf( p , meta = None ) :
56
57         def parseNode() :
58
59                 x , y = p.x , p.y
60                 #
61                 # Node name
62                 #
63                 t = p.next( 'keyword' )
64                 kw = t[ 1 ]
65                 #
66                 # Values
67                 #
68                 values = []
69                 while 1 :
70                         t = p.next( 'string' , '{' , ';' )
71                         if t[ 0 ] in [ '{' , ';' ] : break
72                         values.append( parseString( t[ 1 ] ) )
73                 #
74                 # Contents
75                 #
76                 subNodes = []
77                 if t[ 0 ] == '{' :
78                         subNodes = []
79                         while not p.snext( '}' ) :
80                                 r = parseNode()
81                                 subNodes.append( r )
82                 return ( kw , values , subNodes , ( y , x , meta ) )
83
84         nodes = []
85         #
86         # Parse the entire file
87         #
88         while not p.snext( 'eot' ) :
89                 r = parseNode()
90                 if not r : break
91                 nodes.append( r )
92         return ('__root__',None,nodes,None)
93
94 def parse( doc , filename = None ) :
95
96         tokenMatches = {
97                 'eot'     : '$' ,
98                 'blank'   : r'\s+' ,
99                 'keyword' : r'[_a-zA-Z][_a-zA-Z0-9]*' ,
100                 'string'  : r'[_a-zA-Z][_a-zA-Z0-9]*|\'(?:[^\\\']|\\.)*\'' ,
101                 'comment' : r'#[^\n]*(?:\n|$)' ,
102                 '{'       : '{' ,
103                 '}'       : '}' ,
104                 ';'       : ';'
105         }
106         p = basicparser.Parser( tokenMatches , doc )
107         p.ignore( 'blank' , 'comment' )
108         try :
109                 return parseConf( p , filename )
110         except basicparser.ParserError , e :
111                 msg = p.point()
112                 msg += str( e )
113                 raise Error( msg )
114
115 #--[ Validator ]--------------------------------------------------------------
116
117 #
118 # Check mail.filter configuration file structure.
119 #
120
121 from basicvalidator import *
122
123 #-----------------------------------------------------------------------------
124
125 class Keyword( Validator ) : pass
126
127 class Header( Validator ) :
128
129         allowedMatches = [ 'is' , 'contains' , 'match' ]
130
131         def check( self , values ) :
132
133                 if len( values ) == 2 :
134                         if values[ 1 ] != 'present' :
135                                 error( 'Invalid keyword %r. Expected \'present\'.' % values[ 1 ] )
136                 elif len( values ) == 3 :
137                         if values[ 1 ] not in self.allowedMatches :
138                                 error( '%r is not an allowed match type. Allowed matches type are: %r'
139                                         % ( values[ 1 ] , self.allowedMatches ) )
140                 else :
141                         error( '''header expect 2 or 3 arguments:
142 HEADER-NAME MATCH-TYPE MATCH-ARGUMENT
143 HEADER-NAME MATCH-FLAG''' )
144
145 #-----------------------------------------------------------------------------
146
147 class Logical( Validator , MixinNonEmpty ) :
148
149         def descend( self , item ) :
150
151                 self.children += 1
152                 return ruleValidator( item )
153
154 class Reject( Validator , MixinNonEmpty ) :
155
156         def descend( self , item ) :
157
158                 self.children += 1
159                 return ruleValidator( item )
160
161         def check( self , values ) :
162
163                 if len( values ) != 1 :
164                         error( 'reject CODE { .. }' )
165
166 class Folder( Validator , MixinNonEmpty ) :
167
168         def descend( self , item ) :
169
170                 self.children += 1
171                 return ruleValidator( item )
172
173         def check( self , values ) :
174
175                 if len( values ) != 1 :
176                         error( 'folder FOLDER-NAME { .. }' )
177
178 #-----------------------------------------------------------------------------
179
180 def ruleValidator( item ) :
181
182         if item in [ 'broken' , 'infected' , 'spam' , 'all' ] :
183                 return Keyword( item )
184         elif item in [ 'or' , 'and' , 'not' ] :
185                 return Logical( item )
186         elif item == 'header' :
187                 return Header( item )
188         else :
189                 error( 'unexpected keyword %r.' % item )
190
191 #-----------------------------------------------------------------------------
192
193 class Root( Validator ) :
194
195         def descend( self , item ) :
196
197                 if item == 'reject' :
198                         return Reject( item )
199                 elif item == 'folder' :
200                         return Folder( item )
201                 else :
202                         error( 'unexpected keyword %r.' % item )
203
204         def values( self , values ) :
205
206                 raise Exception( 'Internal error' )
207
208 #--[ Read&Write configuration ]-----------------------------------------------
209
210 def changedDate( filename ) :
211
212         try :
213                 return os.stat( filename )[ stat.ST_CTIME ]
214         except :
215                 return
216
217 #
218 # Return None | ( tree , isValid )
219 #
220 def readCachedConfiguration( filename ) :
221
222         cachedFilename = filename + '.cache'
223         #
224         # Check if cached file is older than the source.
225         #
226         dateCached = changedDate( cachedFilename )
227         if not dateCached : return
228         dateSource = changedDate( filename )
229         if not dateSource : return
230         if dateCached <= dateSource : return
231         #
232         #
233         #
234         try :
235                 r = pickle.load( open( cachedFilename ) )
236         except :
237                 return
238         return r
239
240 def writeCachedConfiguration( filename , tree , isValid ) :
241
242         try :
243                 pickle.dump( ( tree , isValid ) , open( filename + '.cache' , 'w' ) )
244         except :
245                 pass
246
247 def readConfiguration( filename ) :
248
249         try :
250                 #
251                 # 1. Read from cache file
252                 #
253                 r = readCachedConfiguration( filename )
254                 cached = False
255                 if r :
256                         conf , isValid = r
257                         cached = True
258                 else :
259                         isValid = False
260                         #
261                         # 2. Parse the file
262                         #
263                         conf = open( filename ).read()
264                         conf = parse( conf , filename )
265                 if not isValid :
266                         #
267                         # 3. Validate it
268                         #
269                         basicvalidator.checkConf( conf , Root )
270                 #
271                 # 4. Keep cached result
272                 #
273                 writeCachedConfiguration( filename , conf , isValid )
274         except IOError , e :
275                 if e[ 0 ] == errno.ENOENT :
276                         return None
277                 raise Exception( 'While reading file %s:\n%s' % ( filename , str( e ) ) )
278         except Exception , e :
279                 raise Exception( 'While reading file %s:\n%s' % ( filename , str( e ) ) )
280         return conf
281
282 #--[ Dump configuration tree ]------------------------------------------------
283
284 def printTreeInner( t , prt = sys.stdout.write , prefix = '' ) :
285
286         prt( prefix )
287         prt( t[ 0 ] )
288         for kw in t[ 1 ] :
289                 prt( ' ' + kw )
290         if t[ 2 ] :
291                 prt( ' {' )
292         else :
293                 prt( ' ;' )
294         if t[ 3 ] :
295                 prt( ' # ' )
296                 if t[ 3 ][ 2 ] :
297                         prt( '%s:' % t[ 3 ][ 2 ] )
298                 prt( '%s:%s' % ( t[ 3 ][ 0 ] , t[ 3 ][ 1 ] ) )
299         prt( '\n' )
300
301         if t[ 2 ] :
302                 for sub in t[ 2 ] :
303                         printTreeInner( sub , prt , prefix + '  ' )
304                 prt( prefix )
305                 prt( '}\n' )
306
307 def printTree( t ) :
308
309         for sub in t[ 2 ] or [] :
310                 printTreeInner( sub )