Changes related to logs and error handling.
[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 ) != 3 :
114                         error( 'header expect 3 arguments: HEADER-NAME MATCH-TYPE MATCH-ARGUMENT.' )
115                 elif values[ 1 ] not in self.allowedMatches :
116                         error( '%r is not an allowed match type. Allowed matches type are: %r'
117                                 % ( values[ 1 ] , self.allowedMatches ) )
118
119 #-----------------------------------------------------------------------------
120
121 class Logical( Validator , MixinNonEmpty ) :
122
123         def descend( self , item ) :
124
125                 self.children += 1
126                 return ruleValidator( item )
127
128 class Reject( Validator , MixinNonEmpty ) :
129
130         def descend( self , item ) :
131
132                 self.children += 1
133                 return ruleValidator( item )
134
135         def check( self , values ) :
136
137                 if len( values ) != 1 :
138                         error( 'reject CODE { .. }' )
139
140 class Folder( Validator , MixinNonEmpty ) :
141
142         def descend( self , item ) :
143
144                 self.children += 1
145                 return ruleValidator( item )
146
147         def check( self , values ) :
148
149                 if len( values ) != 1 :
150                         error( 'folder FOLDER-NAME { .. }' )
151
152 #-----------------------------------------------------------------------------
153
154 def ruleValidator( item ) :
155
156         if item in [ 'broken' , 'infected' , 'spam' ] :
157                 return Keyword( item )
158         elif item in [ 'or' , 'and' , 'not' ] :
159                 return Logical( item )
160         elif item == 'header' :
161                 return Header( item )
162         else :
163                 error( 'unexpected keyword %r.' % item )
164
165 #-----------------------------------------------------------------------------
166
167 class Root( Validator ) :
168
169         def descend( self , item ) :
170
171                 if item == 'reject' :
172                         return Reject( item )
173                 elif item == 'folder' :
174                         return Folder( item )
175                 else :
176                         error( 'unexpected keyword %r.' % item )
177
178         def values( self , values ) :
179
180                 raise Exception( 'Internal error' )
181
182 #--[ Read&Write configuration ]-----------------------------------------------
183
184 def changedDate( filename ) :
185
186         try :
187                 return os.stat( filename )[ stat.ST_CTIME ]
188         except :
189                 return
190
191 #
192 # Return None | ( tree , isValid )
193 #
194 def readCachedConfiguration( filename ) :
195
196         cachedFilename = filename + '.cache'
197         #
198         # Check if cached file is older than the source.
199         #
200         dateCached = changedDate( cachedFilename )
201         if not dateCached : return
202         dateSource = changedDate( filename )
203         if not dateSource : return
204         if dateCached <= dateSource : return
205         #
206         #
207         #
208         try :
209                 r = pickle.load( open( cachedFilename ) )
210         except :
211                 return
212         return r
213
214 def writeCachedConfiguration( filename , tree , isValid ) :
215
216         try :
217                 pickle.dump( ( tree , isValid ) , open( filename + '.cache' , 'w' ) )
218         except :
219                 pass
220
221 def readConfiguration( filename ) :
222
223         try :
224                 #
225                 # 1. Read from cache file
226                 #
227                 r = readCachedConfiguration( filename )
228                 cached = False
229                 if r :
230                         conf , isValid = r
231                         cached = True
232                 else :
233                         isValid = False
234                         #
235                         # 2. Parse the file
236                         #
237                         conf = open( filename ).read()
238                         conf = parse( conf , filename )
239                 if not isValid :
240                         #
241                         # 3. Validate it
242                         #
243                         basicvalidator.checkConf( conf , Root )
244                 #
245                 # 4. Keep cached result
246                 #
247                 writeCachedConfiguration( filename , conf , isValid )
248         except Exception , e :
249                 raise Exception( 'While reading file %s:\n%s' % ( filename , str( e ) ) )
250
251 #--[ Dump configuration tree ]------------------------------------------------
252
253 def printTreeInner( t , prt = sys.stdout.write , prefix = '' ) :
254
255         prt( prefix )
256         prt( t[ 0 ] )
257         for kw in t[ 1 ] :
258                 prt( ' ' + kw )
259         if t[ 2 ] :
260                 prt( ' {' )
261         else :
262                 prt( ' ;' )
263         if t[ 3 ] :
264                 prt( ' # ' )
265                 if t[ 3 ][ 2 ] :
266                         prt( '%s:' % t[ 3 ][ 2 ] )
267                 prt( '%s:%s' % ( t[ 3 ][ 0 ] , t[ 3 ][ 1 ] ) )
268         prt( '\n' )
269
270         if t[ 2 ] :
271                 for sub in t[ 2 ] :
272                         printTreeInner( sub , prt , prefix + '  ' )
273                 prt( prefix )
274                 prt( '}\n' )
275
276 def printTree( t ) :
277
278         for sub in t[ 2 ] or [] :
279                 printTreeInner( sub )
280
281 def main() :
282
283         doc = open( 'fred.mf' ).read()
284         printTree( parse( doc , 'fred.mf' ) )
285
286 if __name__ == '__main__' :
287         main()