module Autoproc.Transform (generate) where -- The purpose of this module is to define the transformations from -- condition expression to procmail representation. import qualified Autoproc.Procmail as Pm import qualified Autoproc.Classifier as Cf import Data.List (nub) -- This raises the question, Why not use RecipeFlag for CExp? The -- reason is that we are trying to separate the final representation -- (procmail) from the condition expression representation. So in the -- future if CExp flags change, we need only redefine this function. -- Similar logic applies to Act and Cond. -- Note: The above reasoning has saved me many times during development. transformFlag :: Cf.Flag -> Pm.RecipeFlag transformFlag Cf.Copy = Pm.Copy transformFlag Cf.Wait = Pm.Wait transformFlag Cf.IgnoreErrors = Pm.IgnoreErrors transformFlag (Cf.NeedLock b) = (Pm.NeedLock b) transformFlag Cf.Chain = Pm.Chain transformFlag Cf.CaseSensitive = Pm.CaseSensitive transformCond :: Cf.Cond -> [Pm.Condition] transformCond (Cf.Or _ _) = error "transformCond cannot handle Or." transformCond (Cf.And c1 c2) = transformCond c1 ++ transformCond c2 transformCond (Cf.Not c) = [Pm.Condition Pm.Invert c'] where [Pm.Condition _ c'] = transformCond c transformCond Cf.Always = [Pm.Condition Pm.Normal []] transformCond (Cf.CheckHeader s) = [Pm.Condition Pm.Normal s] transformCond (Cf.CheckBody s) = [Pm.Condition Pm.Normal s] transformCond (Cf.CheckMatch s) = [Pm.Condition (Pm.Var "$MATCH") s] transformAct :: Cf.Act -> Pm.Action transformAct (Cf.File s) = Pm.File s transformAct (Cf.Filter s) = Pm.Pipe s transformAct (Cf.Fwd es) = Pm.Forward (map unAddress es) where unAddress (Cf.Addr a) = a transformAct (Cf.Nest as) = Pm.Nest (map transform as) -- This pushes "not" as far down as possible. -- This helps us to reach a "normal" form distributeNot :: Cf.Cond -> Cf.Cond distributeNot (Cf.Not (Cf.And c1 c2)) = Cf.Or (distributeNot (Cf.Not c1)) (distributeNot (Cf.Not c2)) distributeNot (Cf.Not (Cf.Or c1 c2)) = Cf.And (distributeNot (Cf.Not c1)) (distributeNot (Cf.Not c2)) distributeNot (Cf.Not (Cf.Not c)) = distributeNot c distributeNot (Cf.And c1 c2) = Cf.And (distributeNot c1) (distributeNot c2) distributeNot (Cf.Or c1 c2) = Cf.Or (distributeNot c1) (distributeNot c2) distributeNot c = c -- Each call to factor moves the Or one step closer to the top. This must -- be called many times by repeated to reach a normal form. -- The goal here is to pull Or to the outside. -- We don't worry about not, because that should have been handled by -- distributeNot already. factor :: Cf.Cond -> Cf.Cond factor (Cf.And (Cf.Or c1 c2) c3) = (Cf.Or (Cf.And (factor c1) (factor c3)) (Cf.And (factor c2) (factor c3))) factor (Cf.And c1 (Cf.Or c2 c3)) = (Cf.Or (Cf.And (factor c1) (factor c2)) (Cf.And (factor c1) (factor c3))) factor (Cf.Or c1 c2) = (Cf.Or (factor c1) (factor c2)) factor (Cf.And c1 c2) = (Cf.And (factor c1) (factor c2)) factor c = c repeated :: (Cf.Cond -> Cf.Cond) -> Cf.Cond -> Cf.Cond repeated t c = loop c where loop c' = if c' == (t c') then c' else repeated t (t c') -- Procmail does not have a notion of Or, so we must put the -- conditions at the same level and repeat the action. This way, when -- one of the conditions becomes true, the action is performed. reduceOr :: Cf.CExp -> [Cf.CExp] reduceOr (Cf.CExp fs (Cf.Or c1 c2) a) = (reduceOr (Cf.CExp fs c1 a)) ++ (reduceOr (Cf.CExp fs c2 a)) reduceOr x = [x] -- 1. distrubuteNot -- 2. factor -- 3. reduceOr -- The result is a list of CExp, none of which have an Or in their conditions -- and not is only used on individual conditions simplify :: Cf.CExp -> [Cf.CExp] simplify (Cf.CExp fs c a) = reduceOr (Cf.CExp fs c'' a) where c' = repeated distributeNot c c'' = repeated factor c' -- This function assumses a simplified CExp, hence the first pattern match. transform :: Cf.CExp -> Pm.PExp transform (Cf.CExp _ (Cf.Or _ _) _) = error "use simplify." transform (Cf.CExp fs c a) = Pm.PExp (nub fs') (transformCond c) (transformAct a) where fs' = (if any' then [Pm.CheckHeader,Pm.CheckBody] else if body then [Pm.CheckBody] else if header then [Pm.CheckHeader] else [])++newFlags body = checksBody c header = checksHeader c any' = checksAny c newFlags = (if isFilter a then [Pm.Wait, Pm.PipeAsFilter] else [])++ (map transformFlag fs) isFilter :: Cf.Act -> Bool isFilter (Cf.Filter _) = True isFilter _ = False checksHeader :: Cf.Cond -> Bool checksHeader (Cf.And c1 c2) = checksHeader c1 || checksHeader c2 checksHeader (Cf.Or c1 c2) = checksHeader c1 || checksHeader c2 checksHeader (Cf.Not c) = checksHeader c checksHeader (Cf.CheckHeader _) = True checksHeader _ = False checksBody :: Cf.Cond -> Bool checksBody (Cf.And c1 c2) = checksBody c1 || checksBody c2 checksBody (Cf.Or c1 c2) = checksBody c1 || checksBody c2 checksBody (Cf.Not c) = checksBody c checksBody (Cf.CheckBody _) = True checksBody _ = False -- Perhaps checksEither is a better name? checksAny :: Cf.Cond -> Bool checksAny c = checksHeader c && checksBody c -- This is how to generate a procmail recipe from a single expression -- in the condition expression language. generate :: Cf.CExp -> [Pm.PExp] generate c = map transform (simplify c)