{-# LANGUAGE GeneralizedNewtypeDeriving, MultiParamTypeClasses #-} -- remove the two warnings from tagsoup {-# OPTIONS_GHC -fno-warn-warnings-deprecations #-} -- | Converting security protocol theories to a bunch of HTML files and images. module Scyther.Theory.Html ( GenerationInput(..) , theoryToHtml , evalHtmlMarkup ) where import Data.Char import Data.Maybe import qualified Data.Set as S import qualified Data.Map as M import Data.Time.Clock import Data.Time.Format () import Data.Traversable (sequenceA) import Control.Basics import Control.Monad.Identity import Control.Monad.State import Control.Monad.Label import Control.Concurrent.ManagedThreads import Text.Isar import Text.Dot import Text.HTML.TagSoup import Text.JSON import System.IO import System.Directory import System.FilePath import System.Timing import Extension.Prelude import Scyther.Protocol import Scyther.Sequent import Scyther.Theory import Scyther.Theory.Dot import Scyther.Theory.Pretty -- | Converts an indented string to the corresponding Html element. indentedToHtmlString :: String -> String indentedToHtmlString = unlines . map (addBreak . indent) . lines where addBreak = (++"
") indent = uncurry (++) . (concatMap (const " ") *** escapeText) . span isSpace escapeText = renderTags . parseTags ------------------------------------------------------------------------------ -- Html file generation ------------------------------------------------------------------------------ -- | Input for generation process that needs to be supplied from a caller of -- @theoryToHtml@. data GenerationInput = GenerationInput { giHeader :: String -- ^ Arbitrary html for the header , giTime :: UTCTime -- ^ Generation time. , giSystem :: String -- ^ Description of the system we run on. , giInputFile :: FilePath -- ^ Path to input file. , giTemplate :: FilePath -- ^ Path to template index. , giOutDir :: FilePath -- ^ Path to the output directory. , giMarkup :: HtmlMarkup Doc -- ^ Document representing theory to output. , giProofScript :: FilePath -> String -- ^ A function mapping the output file name to a -- string representation of Isabelle/HOL -- certificate. The generation time will be -- measured as the time it takes to write -- this string to the output file. , giDotTool :: String -- ^ 'dot' tool to use. , giCmdLine :: String -- ^ The command line that was used in this call to -- scyther-proof. , giIsabelle :: Maybe (FilePath -> IO (IO String, Maybe String)) -- ^ A checking function calling isabelle with -- the right parameters and returning an IO function -- for retrieving the logfile contents and an error -- message in case the check didn't succeed. } -- | Information about various paths relevant for generating the HTML output. data PathInfo = PathInfo { inputFileCopy :: FilePath -- ^ Path of input file copy. , proofScriptFile :: FilePath -- ^ Path of generated Isabelle proof script. , logFileCopy :: FilePath -- ^ Path of the copy of the logFile , outDir :: FilePath -- ^ Output directory. , imageDir :: FilePath -- ^ Relative directory for graphs. , filesDir :: FilePath -- ^ Relative directory for input and output files. } -- | Compute the path info from the generation input. pathInfo :: GenerationInput -> PathInfo pathInfo input = info where info = PathInfo { inputFileCopy = filesDir info takeFileName (giInputFile input) , proofScriptFile = filesDir info addExtension (takeBaseName (giInputFile input) ++ "_cert_auto") "thy" , logFileCopy = filesDir info "logfile" , outDir = giOutDir input , imageDir = "img" , filesDir = "files" } -- | Make a path that is specified relative to the output directory absolute. mkAbsolute :: PathInfo -> FilePath -> FilePath mkAbsolute info = (outDir info ) -- | Compute the list of absolute paths to directories required for generating -- this HTML output. requiredDirs :: PathInfo -> [FilePath] requiredDirs info = map (mkAbsolute info) [".", imageDir info, filesDir info] -- | Prepare information gathered during the generation of the resulting -- theories for exporting as JSON. jsGenerationInfo :: GenerationInput -> NominalDiffTime -- ^ Proof script generation time. -> Maybe (Bool, NominalDiffTime, FilePath) -- ^ Proof checking time and relative logfile path. -> JSObject JSValue jsGenerationInfo input genTime optCheckInfo = toJSObject $ [ ("header", showJSON . toJSString $ giHeader input) , ("time", showJSON . toJSString . show $ giTime input) , ("system", showJSON . toJSString $ giSystem input) , ("inputFile", showJSON . fileLink $ inputFileCopy paths) , ("proofScript", showJSON . fileLink $ proofScriptFile paths) , ("commandLine", showJSON . toJSString $ giCmdLine input) ] ++ checkInfo optCheckInfo where paths = pathInfo input fileLink file = (toJSString (takeFileName file), toJSString file) genTimeString = "generated in " ++ show genTime checkInfo Nothing = [ ("certificateStatus", showJSON . toJSString $ genTimeString) ] checkInfo (Just (success, checkTime, logFile)) = [ ( "certificateStatus", showJSON . toJSString $ genTimeString ++ status) , ( "logFile", showJSON (toJSString "logfile", toJSString logFile)) ] where status | success = ", successfully checked in " ++ show checkTime | otherwise = ", CHECK FAILED after " ++ show checkTime -- | Convert a security protocol theory to a HTML file visualizing it. theoryToHtml :: GenerationInput -> IO () theoryToHtml input = do putStrLn "" putStrLn $ " copying template to output directory: " ++ outDir paths mapM_ (createDirectoryIfMissing True) $ requiredDirs paths copyTemplate (giTemplate input) $ outDir paths copyFile (giInputFile input) (mkAbsolute paths $ inputFileCopy paths) -- timed proof script generation putStr " generating proof script: " >> hFlush stdout genTime <- timed_ $ writeAbsolute (proofScriptFile paths) (giProofScript input $ proofScriptFile paths) putStrLn $ show genTime -- timed isabelle check optCheckInfo <- case giIsabelle input of Nothing -> return Nothing Just machineCheck -> do putStr " checking proof script: " >> hFlush stdout ((logFileContents, optErrMsg), checkTime) <- timed $ machineCheck (mkAbsolute paths $ proofScriptFile paths) putStrLn $ show checkTime -- write log file copy contents <- logFileContents writeAbsolute (logFileCopy paths) contents return $ Just (isNothing optErrMsg, checkTime, logFileCopy paths) -- json output let thyJSON = mkThyJSON (jsGenerationInfo input genTime optCheckInfo) writeAbsolute "theory.js" (("scytherP_theory_JSON = "++) . show $ showJSObject thyJSON "") -- graph generation putStrLn " generating visualizations using GraphViz:" parMkGraphs . sortOn snd $ M.toList graphs where paths = pathInfo input writeAbsolute = writeFile . mkAbsolute paths (ppThy, (st, graphs)) = runHtmlMarkup $ giMarkup input thyStr = indentedToHtmlString (render ppThy) mkThyJSON :: JSObject JSValue -> JSObject JSValue mkThyJSON genInfo = toJSObject [ ("theory", showJSON $ toJSString thyStr) , ("generationInfo", showJSON $ genInfo ) , ("theoremDefs", showJSON $ htmlThmDefs st ) , ("theoremRefs", showJSON $ htmlThmRefs st ) , ("cases", showJSON $ htmlCases st ) , ("proofstates", showJSON $ htmlGraphRefs st ) , ("explanations", showJSON $ swapKeyValue $ htmlExpls st) ] swapKeyValue m = M.fromList [(y, x) | (x, y) <- M.toList m] -- create the graph corresponding to the given formula mkGraph (dotStr,idx) msgChan = do let outFile = mkAbsolute paths (imageDir paths ("graph_" ++ show idx)) dotFile = addExtension outFile "dot" pngFile = addExtension outFile "png" writeFile dotFile dotStr graphvizDotToPng (giDotTool input) dotFile pngFile msgChan removeFile dotFile -- | Convert a list of dot strings in parallel to png files, using the number of -- cores+1 parallel executions of the dot tool. parMkGraphs = parCmd_ display . map mkGraph where display n i msg = hPutStrLn stdout $ " ["++showPadded i++" of "++show n++"] "++msg where showPadded x = flushRight (length (show n)) (show x) -- | Copy all the files referenced in the template index file to the output -- directory. copyTemplate :: FilePath -- ^ Path of template index file. -> FilePath -- ^ Output directory. -> IO () copyTemplate templateFile targetDir = do let templateDir = takeDirectory templateFile template <- readFile templateFile let files = filter (not.null) $ lines template copy file = do let outPath = targetDir file createDirectoryIfMissing True (takeDirectory outPath) copyFile (templateDir file) outPath mapM_ copy files ------------------------------------------------------------------------------ -- Html Markup Output ------------------------------------------------------------------------------ -- Helper functions ------------------- -- | Name value pairs for attributes. type Attributes = [(String,String)] -- | Output a document surrounded by a tag. withTag :: Document d => String -- ^ Tag name -> Attributes -- ^ Name, value pairs for attributes (no escaping done!) -> d -- ^ Inner document -> d withTag tag atts d = zeroWidthText ("<" ++ tag ++ attsStr ++ ">") <> d <> zeroWidthText ("") where attsStr | null atts = "" | otherwise = concat [ " "++a++"=\""++v++"\"" | (a,v) <- atts ] -- | Output a document surrounded by a span tag. withSpan :: Document d => Attributes -> d -> d withSpan = withTag "span" -- Html Markup Monad -------------------- newtype GraphIdx = GraphIdx { getGraphIdx :: Int } deriving( Eq, Ord, Num, Enum, JSON ) instance Show GraphIdx where show = show . getGraphIdx type HtmlCase = (GraphIdx, [((String, Bool), GraphIdx)]) type HtmlSequent = GraphIdx -- type HtmlSequent = (GraphIdx, GraphIdx) data HtmlMarkupState = HtmlMarkupState { htmlCases :: M.Map Int HtmlCase , htmlThmDefs :: M.Map (String, String) HtmlSequent , htmlThmRefs :: M.Map Int HtmlSequent , htmlGraphRefs :: M.Map Int HtmlSequent , htmlExpls :: M.Map String Int , htmlNextExplRef :: Int } type GraphDesc = String newtype HtmlMarkup a = HtmlMarkup { unHtmlMarkup :: StateT HtmlMarkupState (ConsistentLabelsT GraphDesc GraphIdx Identity) a } deriving( Functor, Applicative, Monad ) instance MonadState HtmlMarkupState HtmlMarkup where get = HtmlMarkup get put = HtmlMarkup . put -- | Get a uniquely numbered reference to a graph given as a 'Dot' action. referenceGraph :: Dot a -> HtmlMarkup GraphIdx referenceGraph = HtmlMarkup . lift . label . showDot -- | Compute the next free index of a map of keys: 0 for the empty map and the -- maximal index plus 1 otherwise. nextIndex :: (Num k, Enum k) => M.Map k v -> k nextIndex = maybe 0 (succ.fst.fst) . M.maxViewWithKey -- | Insert the value at the 'nextIndex' of one of the maps underlying the -- 'HtmlMarkupState'. insertAtNextIndex :: (Ord k, Enum k, Num k) => (HtmlMarkupState -> M.Map k v) -- ^ Get the map. -> (M.Map k v -> HtmlMarkupState -> HtmlMarkupState) -- ^ Set the map. -> v -> HtmlMarkup k insertAtNextIndex getter setter c = HtmlMarkup $ do s <- get let cases = getter s next = nextIndex cases put (setter (M.insert next c cases) s) return next insertHtmlCase :: HtmlCase -> HtmlMarkup Int insertHtmlCase = insertAtNextIndex htmlCases (\x s->s {htmlCases = x}) insertGraphRef :: GraphIdx -> HtmlMarkup Int insertGraphRef = insertAtNextIndex htmlGraphRefs (\x s->s {htmlGraphRefs = x}) insertThmRef :: HtmlSequent -> HtmlMarkup Int insertThmRef = insertAtNextIndex htmlThmRefs (\x s->s {htmlThmRefs = x}) insertThmDef :: Theorem -> HtmlSequent -> HtmlMarkup (String, String) insertThmDef thm se = do let name = (protoName . seProto $ thmSequent thm, thmName thm) modify (\s -> s { htmlThmDefs = M.insert name se (htmlThmDefs s) }) return name insertExplanation :: String -> HtmlMarkup Int insertExplanation e = do expls <- gets htmlExpls case M.lookup e expls of Just ref -> return ref Nothing -> do st <- get let ref = htmlNextExplRef st put $ st { htmlNextExplRef = succ ref, htmlExpls = M.insert e ref expls } return ref htmlSequent :: Sequent -> HtmlMarkup HtmlSequent htmlSequent = referenceGraph . dotSequentMarked S.empty -- (,) <$> referenceGraph (dotPremise se) <*> referenceGraph (dotConclusion se) htmlCase :: Dot a -> [((String,Bool),Dot b)] -> HtmlMarkup HtmlCase htmlCase c scs = (,) <$> referenceGraph c <*> sequenceA [ (,) info <$> referenceGraph dot | (info, dot) <- scs ] emptyHtmlMarkupState :: HtmlMarkupState emptyHtmlMarkupState = HtmlMarkupState M.empty M.empty M.empty M.empty M.empty 0 runHtmlMarkup :: HtmlMarkup a -> (a, (HtmlMarkupState, M.Map GraphDesc GraphIdx)) runHtmlMarkup m = case runIdentity . flip runConsistentLabelsT [1..] . flip runStateT emptyHtmlMarkupState . unHtmlMarkup $ m of ((res, st), (_, sequentIdxs)) -> (res, (st, sequentIdxs)) evalHtmlMarkup :: HtmlMarkup a -> a evalHtmlMarkup = fst . runHtmlMarkup instance MarkupMonad HtmlMarkup where withGraph dot d = do ref <- insertGraphRef =<< referenceGraph dot withSpan [("id", "graphRef_"++show ref)] d withExplanation expl d = do ref <- insertExplanation expl withSpan [("name", "explRef_"++show ref)] d noteCases c ntcs tcs d = do let addTag isTrivial (name, dot) = ((name, isTrivial), dot) cases = map (addTag False) ntcs ++ map (addTag True) tcs ref <- insertHtmlCase =<< htmlCase c cases withSpan [("id", "caseRef_"++show ref)] d theoremRef proto thmname = do let pname = protoName proto defs <- gets htmlThmDefs case M.lookup (pname, thmname) defs of Just se -> do ref <- insertThmRef se withSpan [("id","thmRef_"++show ref)] (text thmname) Nothing -> text thmname theoremDef thm d = do (pname, thname) <- insertThmDef thm =<< htmlSequent (thmSequent thm) let anchor = pname ++ "_" ++ thname withTag "a" [("name",anchor)] d keyword tag d = withSpan [("class","kw-"++tag)] d