1 {-# LANGUAGE NoMonomorphismRestriction #-}
2 {-# LANGUAGE OverloadedStrings #-}
3 {-# LANGUAGE Rank2Types #-}
4 {-# LANGUAGE StrictData #-}
5 {-# LANGUAGE UndecidableInstances #-} -- for VP.Reifies instances
6 {-# OPTIONS_GHC -Wno-missing-signatures #-}
7 module Hjugement.CLI.Utils where
9 import Control.Arrow (left)
10 import Control.Applicative (Alternative(..))
11 import Control.Monad (Monad(..), forM_)
12 import Control.Monad.Trans.Maybe (MaybeT(..))
13 import Control.Monad.Trans.State.Strict (StateT(..), evalStateT)
14 import Data.Bits (setBit)
16 import Data.ByteString (ByteString)
17 import Data.Either (Either(..))
18 import Data.Foldable (Foldable)
19 import Data.Function (($), (.), id)
20 import Data.Functor ((<$>))
21 import Data.Maybe (Maybe(..), maybe)
22 import Data.Monoid (Monoid(..))
23 import Data.Semigroup (Semigroup(..))
24 import Data.Text (Text)
25 import Prelude (max, (-))
26 import Symantic.CLI as CLI
28 import Text.Show (Show(..))
29 import qualified Crypto.Hash as Crypto
30 import qualified Data.Aeson as JSON
31 import qualified Data.ByteArray as ByteArray
32 import qualified Data.ByteString as BS
33 import qualified Data.ByteString.Char8 as BS8
34 import qualified Data.ByteString.Lazy.Char8 as BSL8
35 import qualified Data.Text as Text
36 import qualified Data.Text.Encoding as T
37 import qualified Data.Text.Lazy as TL
38 import qualified Data.Text.Lazy.Builder as TLB
39 import qualified Data.Text.Lazy.Builder.Int as TLB
40 import qualified Lens.Family as Lens
41 import qualified Lens.Family.State.Strict as Lens
42 import qualified Pipes as Pip
43 import qualified Pipes.Aeson as PipJSON (DecodingError(..))
44 import qualified Pipes.Aeson.Unchecked as PipJSON
45 import qualified Pipes.ByteString as PipBS
46 import qualified Pipes.Group as Pip
47 import qualified Pipes.Parse as Pip
48 import qualified Pipes.Prelude as Pip
49 import qualified Pipes.Safe as Pip
50 import qualified Pipes.Safe.Prelude as Pip
51 import qualified Symantic.Document as Doc
52 import qualified System.Console.Terminal.Size as Console
53 import qualified System.Directory as IO
54 import qualified System.FilePath as FP
55 import qualified System.IO as IO
56 import qualified System.Posix as Posix
57 import qualified Voting.Protocol as VP
61 helps = help . Doc.justify
65 type Doc = Doc.Plain TLB.Builder
68 "Take cryptographic parameters from file "<>fileRef "FILE"<>"."
70 long "crypto" (var "FILE")
71 instance CLI.IOType VP.FFC
72 instance CLI.FromSegment VP.FFC where
73 fromSegment = JSON.eitherDecodeFileStrict'
74 instance VP.Reifies c VP.FFC => CLI.FromSegment (VP.E c) where
75 fromSegment = JSON.eitherDecodeFileStrict'
77 "UUID of the election."
79 long "uuid" (var "UUID")
80 instance CLI.IOType VP.UUID
81 instance CLI.FromSegment VP.UUID where
82 fromSegment = return . left show . VP.readUUID . Text.pack
83 instance CLI.IOType VP.Credential
84 instance CLI.FromSegment VP.Credential where
85 fromSegment = return . left show . VP.readCredential . Text.pack
86 instance IOType (VP.DecryptionShare ())
87 instance Outputable (VP.DecryptionShare ()) where
88 output decShare = output $ JSON.encode decShare<>"\n"
93 api_compact <.> response @Doc <!>
94 api_full <.> response @Doc
96 (api_compact <!> api_full) <.> response @Doc
100 then help "Print an uncommented grammar tree to help using this program."
102 tagged (TagShort 'h') (just False)
105 "Print a grammar tree to help using this program,\
106 \ along with explanations."
108 tagged (TagLong "help") (just True)
110 run_help lay = route :!: route
112 route helpInh_full = do
113 width <- Just . maybe 80 (max 0 . (\x -> x - 2) . Console.width) <$> Console.size
116 runLayout helpInh_full lay
118 -- * Type 'Global_Params'
119 data Global_Params = Global_Params
120 { global_dir :: IO.FilePath
128 "Use directory "<>ref"DIR"<>" for reading and writing election files."
130 toPermDefault (FP.takeDirectory "file")
131 ( tagged (TagLong "dir") (var "DIR") `alt`
132 env "HJUGEMENT_DIR" )
137 Pip.Effect (Pip.SafeT IO) a -> m a
138 runPipe = Pip.liftIO . Pip.runSafeT . Pip.runEffect
142 Pip.Effect (Pip.SafeT IO) (a, Either Doc ()) -> MaybeT m a
143 runPipeWithError p = do
146 Left err -> outputError err
154 Pip.Consumer (f Text) m r
155 writeFileLn fileMode filePath =
156 Pip.bracket open close $ \h ->
157 Pip.for Pip.cat $ \xs ->
159 forM_ xs $ BS8.hPutStr h . T.encodeUtf8
162 open = Pip.liftIO $ do
163 IO.createDirectoryIfMissing True $ FP.takeDirectory filePath
164 h <- IO.openFile filePath IO.WriteMode
166 close h = Pip.liftIO $ do
167 fd <- Posix.handleToFd h
168 Posix.setFdMode fd fileMode
177 writeJSON fileMode filePath =
178 Pip.bracket open close $ \h ->
179 Pip.for Pip.cat $ \a ->
181 BSL8.hPutStr h $ JSON.encode a
183 open = Pip.liftIO $ do
184 IO.createDirectoryIfMissing True $ FP.takeDirectory filePath
185 h <- IO.openFile filePath IO.WriteMode
187 close h = Pip.liftIO $ do
188 fd <- Posix.handleToFd h
189 Posix.setFdMode fd fileMode
197 Pip.Producer a m (Either Doc ())
199 let bytes = Pip.withFile filePath IO.ReadMode PipBS.fromHandle in
200 left handleError <$> Lens.view PipJSON.decoded bytes
202 handleError (err, _rest) =
204 PipJSON.AttoparsecError parsingErr ->
205 Doc.from filePath <> ": "<>
206 Doc.redder "parsing error"<>": "<>
207 Doc.from (show parsingErr) <> "\n"
208 PipJSON.FromJSONError decodingErr ->
209 Doc.from filePath <> ": "<>
210 Doc.redder "decoding error"<>": "<>
211 Doc.from decodingErr <> "\n"
216 IO.FilePath -> a -> m ()
217 saveJSON filePath a =
218 -- FIXME: abort or demand confirmation if the file exists
219 Pip.liftIO $ JSON.encodeFile filePath a
224 IO.FilePath -> MaybeT m a
226 Pip.liftIO (JSON.eitherDecodeFileStrict' filePath) >>= \case
227 Left err -> outputError $
228 Doc.from filePath<>": "<>
238 Pip.Parser a m r -> m r
239 readJSON' filePath fold =
240 let bytes = Pip.withFile filePath IO.ReadMode PipBS.fromHandle in
241 evalStateT (Lens.zoom PipJSON.decoded fold) bytes
247 Pip.FreeT (Pip.Producer (Either PipJSON.DecodingError a) m) m ()
248 readJSON'' filePath =
249 let bytes = Pip.withFile filePath IO.ReadMode PipBS.fromHandle in
250 parseMany (Pip.parsed_ PipJSON.decode) bytes
255 (Pip.Producer inp m r -> Pip.Producer a m (Pip.Producer inp m r)) ->
256 Pip.Producer inp m r ->
257 Pip.FreeT (Pip.Producer a m) m r
261 Pip.Producer inp m r ->
262 Pip.FreeT (Pip.Producer a m) m r
263 go0 p = Pip.FreeT $ do
265 Left r -> return $ Pip.Pure r
266 Right (inp, p') -> return $ Pip.Free $ go1 $ Pip.yield inp >> p'
268 Pip.Producer inp m r ->
269 Pip.Producer a m (Pip.FreeT (Pip.Producer a m) m r)
275 Outputable (OnHandle d) =>
276 (a -> d) -> Pip.Pipe a a m r
278 Pip.for Pip.cat $ \s -> do
280 output $ OnHandle IO.stderr (d s)
281 output $ OnHandle IO.stderr '\n'
284 outputInfo :: Pip.MonadIO m => Doc -> m ()
286 Pip.liftIO $ output $ OnHandle @Doc IO.stderr $
287 Doc.green "INFO"<>": "<>msg<>"\n"
293 Pip.liftIO $ output $ OnHandle @Doc IO.stderr $
294 Doc.redder "ERROR"<>": "<>msg<>"\n"