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