packages feed

elynx-0.2.1: app/Main.hs

{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE OverloadedStrings #-}

{- |
Module      :  Main
Description :  Validate elynx file
Copyright   :  (c) Dominik Schrempf 2020
License     :  GPL-3.0-or-later

Maintainer  :  dominik.schrempf@gmail.com
Stability   :  unstable
Portability :  portable

Creation date: Wed Apr 22 21:08:25 2020.

-}

module Main
  ( main
  )
where

import           Control.Monad
import           Data.Aeson
import qualified Data.Aeson.Types              as J
import qualified Data.ByteString.Char8         as B
import           Data.Maybe
import           Data.Version                   ( Version )
import           Options.Applicative
import           System.Environment             ( withProgName
                                                , withArgs
                                                )

import           ELynx.Tools

import           SLynx.SLynx                    ( slynx )
import qualified SLynx.Options                 as S
import           TLynx.TLynx                    ( tlynx )
import qualified TLynx.Options                 as T

import           Options

parseProgName :: Value -> J.Parser String
parseProgName = withObject "progName" $ \o -> o .: "progName"

-- TODO: Declaring program names here should not be necessary.
parseAllR :: String -> Value -> J.Parser AllReproductions
parseAllR "slynx" v =
  S <$> (parseJSON v :: J.Parser (Reproduction (Arguments S.CommandArguments)))
parseAllR "tlynx" v =
  T <$> (parseJSON v :: J.Parser (Reproduction (Arguments T.CommandArguments)))
parseAllR p _ =
  let err = unlines
        [ "Could not parse program name:"
        , p
        , "Do you use the correct ELynx version?"
        ]
  in  error err

parse :: Show a => [String] -> Parser a -> a
parse s p = fromMaybe
  (error $ "parse: could not parse command line arguments: " ++ show s)
  (getParseResult res)
  where res = execParserPure defaultPrefs (info p briefDesc) s

-- Does the command line fit the saved arguments?
checkArgs
  :: forall a
   . (Eq a, Show a, Reproducible a)
  => Reproduction a
  -> IO (Either String ())
checkArgs s = do
  let r   = reproducible s
      p   = parser @a
      as  = argsStr s
      res = parse as p
  return $ if res /= reproducible s
    then Left $ unlines
      ["Command line string and command arguments do not fit:", show as, show r]
    else Right ()

-- Does the file match the base 16 checksum?
checkFile :: FilePath -> B.ByteString -> IO (Either String ())
checkFile fp h = do
  h' <- hashFile fp
  return $ if h' == h
    then Right ()
    else Left $ unlines
      [ "SHA256 sum does not match for a file:"
      , fp ++ " has check sum " ++ B.unpack h'
      , "Stored check sum is " ++ B.unpack h
      ]

checkVersion :: Version -> Either String ()
checkVersion v = if v == version
  then Right ()
  else Left $ unlines
    [ "Versions differ:"
    , "Version in ELynx reproduction file: " ++ show v
    , "Version of current executable: " ++ show version
    ]

checkHash :: Reproducible a => Reproduction a -> Either String ()
checkHash r = if h == h'
  then Right ()
  else Left $ unlines
    [ "ELynx reproduction file has been changed:"
    , "Hash saved in file:        " ++ show h
    , "Hash calculated from file: " ++ show h'
    ]
 where
  h  = rHash r
  h' = Just $ getReproductionHash r

-- Check if command line arguments and files check sums are matching.
validate
  :: (Eq a, Show a, Reproducible a) => Reproduction a -> IO (Either String ())
validate s = do
  chA <- checkArgs s
  let chV = checkVersion (rVersion s)
      chH = checkHash s
  chFs <- zipWithM checkFile (files s) (map B.pack $ checkSums s)
  return $ sequence_ (chA : chV : chH : chFs)

validateAllReproductions :: AllReproductions -> IO (Either String ())
validateAllReproductions (S x) = validate x
validateAllReproductions (T x) = validate x

getAllR :: FilePath -> IO AllReproductions
getAllR fp = do
  eELynx <- eitherDecodeFileStrict fp :: IO (Either String Value)
  elynx  <- case eELynx of
    Left err -> do
      putStrLn "Failed decoding the ELynx reproduction file."
      putStrLn "The following error occurred:"
      error err
    Right val -> return val
  prog <- case J.parseEither parseProgName elynx of
    Left err -> do
      putStrLn "Failed getting command hash from ELynx reproduction file."
      putStrLn "The following error occurred:"
      error err
    Right prog -> return prog
  case J.parseEither (parseAllR prog) elynx of
    Left err -> do
      putStrLn "Failed parsing the ELynx reproduction file."
      putStrLn "The following error occurred:"
      error err
    Right repr -> return repr

-- Read and validate ELynx reproduction file. Check consistency of arguments and
-- input files.
runValidate :: ValidateArguments -> IO ()
runValidate a = do
  let fp = vElynxFile a
  repr <- getAllR fp
  val  <- validateAllReproductions repr
  case val of
    Left err -> do
      putStrLn "Failed validating the ELynx reproduction file."
      putStrLn "The following error occurred:"
      error err
    Right () -> putStrLn "Validation successful!"

runRedo :: RedoArguments -> IO ()
runRedo a = do
  let fp = rElynxFile a
  let f = rForce a
  when (f == Force False) $ do
    putStrLn "Validate ELynx reproduction file before reanalysis."
    putStrLn "Use the --force (-f) option to skip this test."
    runValidate (ValidateArguments fp)
  repr <- getAllR fp
  let as = getArgs repr
  as' <- if "-f" `notElem` as && "--force" `notElem` as
    then do
      putStrLn
        "Force option required to redo analysis. Add -f (force) to arguments."
      return $ "-f" : as
    else return as
  withProgName (getProgName repr) $ withArgs as' $ redo repr

setForce :: Arguments a -> Arguments a
setForce (Arguments g l) = Arguments g { forceReanalysis = Force True } l

redo :: AllReproductions -> IO ()
redo (S x) = slynx $ setForce $ reproducible x
redo (T x) = tlynx $ setForce $ reproducible x

main :: IO ()
main = do
  g <- execParser commandArguments
  case g of
    Validate a -> runValidate a
    Redo     a -> runRedo a