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