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.String (IsString(..))
24 import Data.Semigroup (Semigroup(..))
25 import Data.Text (Text)
26 import Prelude (min, max, (-))
27 import Symantic.CLI as CLI
29 import Text.Show (Show(..))
30 import qualified Crypto.Hash as Crypto
31 import qualified Data.Aeson as JSON
32 import qualified Data.ByteArray as ByteArray
33 import qualified Data.ByteString as BS
34 import qualified Data.ByteString.Char8 as BS8
35 import qualified Data.ByteString.Lazy.Char8 as BSL8
36 import qualified Data.Text as Text
37 import qualified Data.Text.Encoding as T
38 import qualified Data.Text.Lazy as TL
39 import qualified Data.Text.Lazy.Builder as TLB
40 import qualified Data.Text.Lazy.Builder.Int as TLB
41 import qualified Lens.Family as Lens
42 import qualified Lens.Family.State.Strict as Lens
43 import qualified Pipes as Pip
44 import qualified Pipes.Aeson as PipJSON (DecodingError(..))
45 import qualified Pipes.Aeson.Unchecked as PipJSON
46 import qualified Pipes.ByteString as PipBS
47 import qualified Pipes.Group as Pip
48 import qualified Pipes.Parse as Pip
49 import qualified Pipes.Prelude as Pip
50 import qualified Pipes.Safe as Pip
51 import qualified Pipes.Safe.Prelude as Pip
52 import qualified Symantic.Document as Doc
53 import qualified System.Console.Terminal.Size as Console
54 import qualified System.Directory as IO
55 import qualified System.FilePath as FP
56 import qualified System.IO as IO
57 import qualified System.Posix as Posix
58 import qualified Voting.Protocol as VP
62 helps = help . Doc.justify
66 type Doc = Doc.Plain TLB.Builder
69 "Take cryptographic parameters from file "<>fileRef "FILE"<>"."
71 requiredTag "crypto" (var "FILE")
72 instance CLI.IOType VP.FFC
73 instance CLI.FromSegment VP.FFC where
74 fromSegment = JSON.eitherDecodeFileStrict'
75 instance VP.Reifies c VP.FFC => CLI.FromSegment (VP.E c) where
76 fromSegment = JSON.eitherDecodeFileStrict'
78 "UUID of the election."
80 requiredTag "uuid" (var "UUID")
81 instance CLI.IOType VP.UUID
82 instance CLI.FromSegment VP.UUID where
83 fromSegment = return . left show . VP.readUUID . Text.pack
84 instance CLI.IOType VP.Credential
85 instance CLI.FromSegment VP.Credential where
86 fromSegment = return . left show . VP.readCredential . Text.pack
87 instance IOType (VP.DecryptionShare ())
88 instance Outputable (VP.DecryptionShare ()) where
89 output decShare = output $ JSON.encode decShare<>"\n"
94 api_compact <.> response @Doc <!>
95 api_full <.> response @Doc
97 (api_compact <!> api_full) <.> response @Doc
101 then help "Print an uncommented grammar tree to help using this program."
106 "Print a grammar tree to help using this program,\
107 \ along with explanations."
109 tag "help" (just True)
111 run_help lay = route :!: route
113 route helpInh_full = do
114 width <- Just . maybe 80 (min 80 . max 0 . (\x -> x - 2) . Console.width)
118 runLayout helpInh_full lay
120 -- * Type 'Global_Params'
121 data Global_Params = Global_Params
122 { global_dir :: IO.FilePath
130 "Use directory "<>ref"DIR"<>" for reading and writing election files.\n"<>
131 "Default to "<>fileRef (Doc.from currDir)<>".\n"<>
132 "Can also be set via HJUGEMENT_DIR="<>ref "DIR"<>"."
134 toPermDefault currDir $
135 tag "dir" (var "DIR")
138 where currDir = FP.takeDirectory "file"
140 "Download election files from "<>ref"URL"<>"."
142 toPermutation $ tag "url" (var "URL")
147 Pip.Effect (Pip.SafeT IO) a -> m a
148 runPipe = Pip.liftIO . Pip.runSafeT . Pip.runEffect
152 Pip.Effect (Pip.SafeT IO) (a, Either Doc ()) -> MaybeT m a
153 runPipeWithError p = do
156 Left err -> outputError err
164 Pip.Consumer (f Text) m r
165 writeFileLn fileMode filePath =
166 Pip.bracket open close $ \h ->
167 Pip.for Pip.cat $ \xs ->
169 forM_ xs $ BS8.hPutStr h . T.encodeUtf8
172 open = Pip.liftIO $ do
173 IO.createDirectoryIfMissing True $ FP.takeDirectory filePath
174 h <- IO.openFile filePath IO.WriteMode
176 close h = Pip.liftIO $ do
177 fd <- Posix.handleToFd h
178 Posix.setFdMode fd fileMode
187 writeJSON fileMode filePath =
188 Pip.bracket open close $ \h ->
189 Pip.for Pip.cat $ \a ->
191 BSL8.hPutStr h $ JSON.encode a
193 open = Pip.liftIO $ do
194 IO.createDirectoryIfMissing True $ FP.takeDirectory filePath
195 h <- IO.openFile filePath IO.WriteMode
197 close h = Pip.liftIO $ do
198 fd <- Posix.handleToFd h
199 Posix.setFdMode fd fileMode
207 Pip.Producer a m (Either Doc ())
209 let bytes = Pip.withFile filePath IO.ReadMode PipBS.fromHandle in
210 left handleError <$> Lens.view PipJSON.decoded bytes
212 handleError (err, _rest) =
214 PipJSON.AttoparsecError parsingErr ->
215 Doc.from filePath <> ": "<>
216 Doc.redder "parsing error"<>": "<>
217 Doc.from (show parsingErr) <> "\n"
218 PipJSON.FromJSONError decodingErr ->
219 Doc.from filePath <> ": "<>
220 Doc.redder "decoding error"<>": "<>
221 Doc.from decodingErr <> "\n"
226 IO.FilePath -> a -> m ()
227 saveJSON filePath a =
228 -- FIXME: abort or demand confirmation if the file exists
229 Pip.liftIO $ JSON.encodeFile filePath a
234 IO.FilePath -> MaybeT m a
236 Pip.liftIO (JSON.eitherDecodeFileStrict' filePath) >>= \case
237 Left err -> outputError $
238 Doc.from filePath<>": "<>
248 Pip.Parser a m r -> m r
249 readJSON' filePath fold =
250 let bytes = Pip.withFile filePath IO.ReadMode PipBS.fromHandle in
251 evalStateT (Lens.zoom PipJSON.decoded fold) bytes
257 Pip.FreeT (Pip.Producer (Either PipJSON.DecodingError a) m) m ()
258 readJSON'' filePath =
259 let bytes = Pip.withFile filePath IO.ReadMode PipBS.fromHandle in
260 parseMany (Pip.parsed_ PipJSON.decode) bytes
265 (Pip.Producer inp m r -> Pip.Producer a m (Pip.Producer inp m r)) ->
266 Pip.Producer inp m r ->
267 Pip.FreeT (Pip.Producer a m) m r
271 Pip.Producer inp m r ->
272 Pip.FreeT (Pip.Producer a m) m r
273 go0 p = Pip.FreeT $ do
275 Left r -> return $ Pip.Pure r
276 Right (inp, p') -> return $ Pip.Free $ go1 $ Pip.yield inp >> p'
278 Pip.Producer inp m r ->
279 Pip.Producer a m (Pip.FreeT (Pip.Producer a m) m r)
285 Outputable (OnHandle d) =>
286 (a -> d) -> Pip.Pipe a a m r
288 Pip.for Pip.cat $ \s -> do
290 output $ OnHandle IO.stderr (d s)
291 output $ OnHandle IO.stderr '\n'
294 outputInfo :: Pip.MonadIO m => Doc -> m ()
296 Pip.liftIO $ output $ OnHandle @Doc IO.stderr $
297 Doc.green "INFO"<>": "<>msg<>"\n"
303 Pip.liftIO $ output $ OnHandle @Doc IO.stderr $
304 Doc.redder "ERROR"<>": "<>msg<>"\n"