1 {-# LANGUAGE NoMonomorphismRestriction #-}
2 {-# LANGUAGE OverloadedStrings #-}
3 {-# LANGUAGE Rank2Types #-}
4 {-# LANGUAGE StrictData #-}
5 {-# LANGUAGE TypeApplications #-}
6 {-# LANGUAGE UndecidableInstances #-} -- for VP.Reifies instances
7 {-# OPTIONS_GHC -Wno-missing-signatures #-}
8 {-# OPTIONS_GHC -Wno-orphans #-}
9 module Hjugement.CLI.Utils where
11 import Control.Applicative (Applicative(..), Alternative(..))
12 import Control.Arrow (left)
13 import Control.Monad (Monad(..), forM_, when)
14 import Control.Monad.Trans.Maybe (MaybeT(..))
15 import Control.Monad.Trans.Except (runExceptT)
17 import Data.Either (Either(..))
18 import Data.Eq (Eq(..))
19 import Data.Foldable (Foldable)
20 import Data.Function (($), (.), id)
21 import Data.Functor ((<$>))
22 import Data.Maybe (Maybe(..), maybe)
23 import Data.Monoid (Monoid(..))
24 import Data.Ord (Ord(..))
25 import Data.Semigroup (Semigroup(..))
26 import Data.Text (Text)
27 import Prelude (min, max, (-))
28 import Symantic.CLI as CLI
30 import Text.Show (Show(..))
31 import qualified Data.Aeson as JSON
32 import qualified Data.ByteString.Char8 as BS8
33 import qualified Data.ByteString.Lazy.Char8 as BSL8
34 import qualified Data.Text as Text
35 import qualified Data.Text.Encoding as T
36 import qualified Data.Text.Lazy.Builder as TLB
37 import qualified Lens.Family as Lens
38 import qualified Pipes as Pip
39 import qualified Pipes.Aeson as PipJSON (DecodingError(..))
40 import qualified Pipes.Aeson.Unchecked as PipJSON
41 import qualified Pipes.ByteString as PipBS
42 import qualified Pipes.Safe as Pip
43 import qualified Pipes.Safe.Prelude as Pip
44 import qualified Symantic.Document as Doc
45 import qualified System.Console.Terminal.Size as Console
46 import qualified System.Directory as IO
47 import qualified System.FilePath as FP
48 import qualified System.IO as IO
49 import qualified System.Posix as Posix
50 import qualified Voting.Protocol as VP
53 con = Doc.between "\"" "\""
55 helps = help . Doc.justify
59 type Doc = Doc.Plain TLB.Builder
62 "Take cryptographic parameters from file "<>fileRef "FILE"<>"."
64 requiredTag "crypto" (var "FILE")
66 "Set the protocol version to use.\n"<>
67 "Defaults to the \"stable\" version.\n"<>
69 [ "stable == "<>Doc.from (show VP.stableVersion)
70 , "experimental == "<>Doc.from (show VP.experimentalVersion)
73 defaultTag "version" VP.stableVersion (
74 constant "stable" VP.stableVersion `alt`
75 constant "experimental" VP.experimentalVersion `alt`
78 instance CLI.IOType VP.Version
79 instance CLI.FromSegment VP.Version where
80 fromSegment = return . maybe (Left "invalid version string") Right . VP.readVersion
81 instance CLI.IOType VP.FFC
82 instance CLI.FromSegment VP.FFC where
83 fromSegment = JSON.eitherDecodeFileStrict'
84 instance VP.Reifies c VP.FFC => CLI.FromSegment (VP.E VP.FFC c) where
85 fromSegment = JSON.eitherDecodeFileStrict'
87 "UUID of the election."
89 requiredTag "uuid" (var "UUID")
90 instance CLI.IOType VP.UUID
91 instance CLI.FromSegment VP.UUID where
92 fromSegment = return . left show . VP.readUUID . Text.pack
93 instance CLI.IOType VP.Credential
94 instance CLI.FromSegment VP.Credential where
95 fromSegment = return . left show . VP.readCredential . Text.pack
96 instance IOType (VP.DecryptionShare VP.FFC () ())
97 instance Outputable (VP.DecryptionShare VP.FFC () ()) where
98 output decShare = output $ JSON.encode (decShare)<>"\n"
103 api_compact <.> response @Doc <!>
104 api_full <.> response @Doc
106 (api_compact <!> api_full) <.> response @Doc
110 then help "Print an uncommented grammar tree to help using this program."
115 "Print a grammar tree to help using this program,\
116 \ along with explanations."
118 tag "help" (just True)
120 run_help lay = route :!: route
122 route helpInh_full = do
123 width <- Just . maybe 80 (min 80 . max 0 . (\x -> x - 2) . Console.width)
127 runLayout helpInh_full lay
129 -- * Type 'Global_Params'
132 { global_stderr_prepend_newline :: Bool
133 , global_stderr_prepend_carriage :: Bool
134 , global_stderr_append_newline :: Bool
135 , global_dir :: IO.FilePath
136 , global_verbosity :: Verbosity
141 Global_Params False False True
143 <*> api_param_verbosity
145 "Use directory "<>ref"DIR"<>" for reading and writing election files.\n"<>
146 "Default to "<>con (Doc.from currDir)<>".\n"<>
147 "Can also be set via HJUGEMENT_DIR="<>ref "DIR"<>"."
149 toPermDefault currDir $
150 tag "dir" (var "DIR")
153 where currDir = FP.takeDirectory "file"
155 "Download election files from "<>ref"URL"<>"."
157 toPermutation $ tag "url" (var "URL")
159 -- * Type 'Verbosity'
167 instance IOType Verbosity
168 instance FromSegment Verbosity where
170 "error" -> return $ Right Verbosity_Error
171 "warning" -> return $ Right Verbosity_Warning
172 "info" -> return $ Right Verbosity_Info
173 "debug" -> return $ Right Verbosity_Debug
174 _ -> return $ Left "invalid verbosity"
176 api_param_verbosity =
178 \\nDefault to "<>con "info"<>"."
180 toPermDefault Verbosity_Info $
182 constant "error" Verbosity_Error `alt`
183 constant "warning" Verbosity_Warning `alt`
184 constant "info" Verbosity_Info `alt`
185 constant "debug" Verbosity_Debug
187 env "HJUGEMENT_VERBOSITY"
192 Pip.Effect (Pip.SafeT IO) a -> m a
193 runPipe = Pip.liftIO . Pip.runSafeT . Pip.runEffect
198 Pip.Effect (Pip.SafeT IO) (a, Either Doc ()) -> MaybeT m a
199 runPipeWithError glob p = do
202 Left err -> outputError glob err
211 Pip.Consumer (f Text) m r
212 writeFileLn glob fileMode filePath = do
213 Pip.liftIO $ outputDebug glob $ "writing " <> Doc.from filePath
214 Pip.bracket open close $ \h ->
215 Pip.for Pip.cat $ \xs ->
217 forM_ xs $ BS8.hPutStr h . T.encodeUtf8
220 open = Pip.liftIO $ do
221 IO.createDirectoryIfMissing True $ FP.takeDirectory filePath
222 IO.openFile filePath IO.WriteMode
223 close h = Pip.liftIO $ do
224 fd <- Posix.handleToFd h
225 Posix.setFdMode fd fileMode
235 writeJSON glob fileMode filePath = do
236 Pip.liftIO $ outputDebug glob $ "writing " <> Doc.from filePath
237 Pip.bracket open close $ \h ->
238 Pip.for Pip.cat $ \a ->
240 BSL8.hPutStrLn h $ JSON.encode a
242 open = Pip.liftIO $ do
243 IO.createDirectoryIfMissing True $ FP.takeDirectory filePath
244 IO.openFile filePath IO.WriteMode
245 close h = Pip.liftIO $ do
246 fd <- Posix.handleToFd h
247 Posix.setFdMode fd fileMode
256 Pip.Producer a m (Either Doc ())
257 readJSON glob filePath = do
258 Pip.liftIO $ outputDebug glob $ "reading " <> Doc.from filePath
259 let bytes = Pip.withFile filePath IO.ReadMode PipBS.fromHandle
260 left handleError <$> Lens.view PipJSON.decoded bytes
262 handleError (err, _rest) =
264 PipJSON.AttoparsecError parsingErr ->
265 Doc.from filePath <> ": "<>
266 Doc.redder "parsing error"<>": "<>
267 Doc.from (show parsingErr) <> "\n"
268 PipJSON.FromJSONError decodingErr ->
269 Doc.from filePath <> ": "<>
270 Doc.redder "decoding error"<>": "<>
271 Doc.from decodingErr <> "\n"
277 IO.FilePath -> a -> m ()
278 saveJSON glob filePath a =
279 -- FIXME: abort or demand confirmation if the file exists
281 outputDebug glob $ "saving " <> Doc.from filePath
282 JSON.encodeFile filePath a
288 IO.FilePath -> MaybeT m a
289 loadJSON glob filePath =
291 outputDebug glob $ "loading " <> Doc.from filePath
292 JSON.eitherDecodeFileStrict' filePath
294 Left err -> outputError glob $
295 Doc.from filePath<>": "<>
299 -- | TODO: abstract over @crypto@ in the continuation.
301 VP.ReifyCrypto crypto =>
302 JSON.FromJSON crypto =>
307 VP.Reifies v VP.Version =>
308 VP.GroupParams crypto c =>
309 VP.Election crypto v c -> MaybeT m r) ->
311 loadElection glob filePath k =
313 outputDebug glob $ "loading " <> Doc.from filePath
314 runExceptT $ VP.readElection filePath k
316 Left err -> outputError glob $
317 Doc.from filePath<>": "<>
327 Pip.Parser a m r -> m r
328 readJSON' filePath fold =
329 let bytes = Pip.withFile filePath IO.ReadMode PipBS.fromHandle in
330 evalStateT (Lens.zoom PipJSON.decoded fold) bytes
336 Pip.FreeT (Pip.Producer (Either PipJSON.DecodingError a) m) m ()
337 readJSON'' filePath =
338 let bytes = Pip.withFile filePath IO.ReadMode PipBS.fromHandle in
339 parseMany (Pip.parsed_ PipJSON.decode) bytes
344 (Pip.Producer inp m r -> Pip.Producer a m (Pip.Producer inp m r)) ->
345 Pip.Producer inp m r ->
346 Pip.FreeT (Pip.Producer a m) m r
350 Pip.Producer inp m r ->
351 Pip.FreeT (Pip.Producer a m) m r
352 go0 p = Pip.FreeT $ do
354 Left r -> return $ Pip.Pure r
355 Right (inp, p') -> return $ Pip.Free $ go1 $ Pip.yield inp >> p'
357 Pip.Producer inp m r ->
358 Pip.Producer a m (Pip.FreeT (Pip.Producer a m) m r)
365 (a -> Doc) -> Pip.Pipe a a m r
367 Pip.for Pip.cat $ \s -> do
368 Pip.liftIO $ outputInfo glob $ d s
371 outputMessage :: Pip.MonadIO m => Global_Params -> Doc -> Doc -> m ()
372 outputMessage Global_Params{..} hdr msg =
373 Pip.liftIO $ output $ OnHandle @Doc IO.stderr $
374 (if global_stderr_prepend_newline then Doc.newline else mempty) <>
375 (if global_stderr_prepend_carriage then "\r" else mempty) <>
377 (if global_stderr_append_newline then Doc.newline else mempty)
379 outputError :: Pip.MonadIO m => Global_Params -> Doc -> MaybeT m a
380 outputError glob@Global_Params{..} msg = do
381 when (Verbosity_Error <= global_verbosity) $ do
382 outputMessage glob (Doc.redder "ERROR") msg
385 outputWarning :: Pip.MonadIO m => Global_Params -> Doc -> m ()
386 outputWarning glob@Global_Params{..} msg = do
387 when (Verbosity_Warning <= global_verbosity) $ do
388 outputMessage glob (Doc.yellower "WARNING") msg
390 outputInfo :: Pip.MonadIO m => Global_Params -> Doc -> m ()
391 outputInfo glob@Global_Params{..} msg = do
392 when (Verbosity_Info <= global_verbosity) $ do
393 outputMessage glob (Doc.greener "info") msg
395 outputDebug :: Pip.MonadIO m => Global_Params -> Doc -> m ()
396 outputDebug glob@Global_Params{..} msg = do
397 when (Verbosity_Debug <= global_verbosity) $ do
398 outputMessage glob (Doc.magentaer "debug") msg