{-# LANGUAGE GeneralizedNewtypeDeriving, DeriveGeneric, OverloadedStrings, ScopedTypeVariables #-} module Database.PlistBuddy.Audit ( auditOn, auditOff, hashcode, recover, findTrail ) where import Control.Concurrent import Control.Exception import Control.Monad.Reader import Control.Monad.Except import Data.Char(isSpace) import Data.Text(Text) import Data.ByteString (ByteString) import qualified Data.ByteString.Base16 as B16 import qualified Data.ByteString.Lazy as LB import Database.PlistBuddy.Types import System.IO import System.Process import Data.Time import GHC.Generics import qualified Crypto.Hash.MD5 as MD5 import Debug.Trace auditOn :: FilePath -> Plist -> IO Plist auditOn auditFile plist = do -- if there is no file, then this creates an empty file first au <- openFile auditFile AppendMode -- state what we are auditing, with blank line(s) -- in case the previous line was incomplete hPutStr au $ "\n\n" ++ take 72 (cycle "-") ++ "\n" t <- getCurrentTime h <- hashcode (plist_file plist) issue au $ t :! Start h -- and append to the audit file let up u = do o <- hIsOpen au if o then do t <- getCurrentTime issue au $ t :! u case u of Exit -> hClose au _ -> return () else return () -- putStrLn $ "audit log failure: " ++ show u ++ "\n" return $ plist { plist_trail = up, plist_launder = hClose au } -- | Turn off audit. auditOff :: Plist -> IO () auditOff = plist_launder hashcode :: FilePath -> IO ByteString hashcode fileName = do bs <- LB.readFile fileName return $! B16.encode $! MD5.hashlazy bs issue :: Show a => Handle -> a -> IO () issue h u = do hPutStr h $ show u ++ "\n" hFlush h maybeRead :: Read a => String -> Maybe a maybeRead str = case reads str of [(r,rest)] | all isSpace rest -> return r _ -> Nothing -- | Find the list of 'PlistBuddy' commands to recover the plist. -- Be careful when running recover with the audit capability turned on; it can duplicate -- the audit trail, because recovery is also write. (This should not break anything) recover :: FilePath -> IO [AuditTrail] recover auditFile = do txt <- readFile auditFile let trails = [ v | Just (_ :! v) <- map maybeRead $ lines $ txt ] return $ runTrails trails runTrails :: [Trail] -> [AuditTrail] runTrails [] = [] runTrails (inst :rest) = case inst of Save bs -> runTrails' bs [] rest Start bs -> runTrails' bs [] rest _ -> runTrails rest -- find a 'Save/Start' checkpoint where runTrails' :: ByteString -> RList Trail -> [Trail] -> [AuditTrail] runTrails' bs [] [] = [] runTrails' bs insts [] = [AuditTrail bs (reverse insts) Nothing] runTrails' bs insts (inst : rest) = case inst of Save bs' -> mkTrail (Just bs') $ runTrails' bs' [] rest -- save is start of next trail Start bs' -> mkTrail Nothing $ runTrails' bs' [] rest -- start ignores trail before, because of no save Revert -> runTrails' bs [] rest -- revert wipes all unsaved instructions Exit -> runTrails rest -- abandon the changes; start trail again Clear _ -> runTrails' bs [inst] rest -- anything *before* clear is now lost _ -> runTrails' bs (inst : insts) rest -- Set / Add / Delete where mkTrail done k = if null insts then k else (AuditTrail bs (reverse insts) done) : k type RList a = [a] -- a reversed list, often a stack -- Find the last trail with this hashcode findTrail :: ByteString -> [AuditTrail] -> [Trail] findTrail bs trails = combine $ dropMe trails where combine [] = [] combine ( AuditTrail bs ts (Just bs') : AuditTrail bs'' ts2 done : more) | bs' == bs'' = combine (AuditTrail bs (ts ++ ts2) done : more) combine (AuditTrail bs ts _ : _) = ts takeMe [] acc = reverse acc takeMe (x@(AuditTrail bs' _ _) : xs) acc | bs == bs' = takeMe xs [x] --- restart | otherwise = takeMe xs (x : acc) dropMe [] = [] dropMe (x@(AuditTrail bs' _ _) : xs) | bs == bs' = takeMe xs [x] | otherwise = dropMe xs