summaryrefslogtreecommitdiff
path: root/walint-cli
diff options
context:
space:
mode:
Diffstat (limited to 'walint-cli')
-rw-r--r--walint-cli/Main.hs108
-rw-r--r--walint-cli/Version.hs17
-rw-r--r--walint-cli/default.nix18
-rw-r--r--walint-cli/walint-cli.cabal27
4 files changed, 170 insertions, 0 deletions
diff --git a/walint-cli/Main.hs b/walint-cli/Main.hs
new file mode 100644
index 0000000..4bbc670
--- /dev/null
+++ b/walint-cli/Main.hs
@@ -0,0 +1,108 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards #-}
+
+module Main where
+
+import Universum
+
+import Data.Aeson (eitherDecodeFileStrict', encode)
+import Data.Aeson.Encode.Pretty (encodePretty)
+import Data.Aeson.KeyMap (coercionToHashMap)
+import WithCli (HasArguments, withCli)
+
+import CheckDir (recursiveCheckDir, resultIsFatal)
+import LintConfig (ConfigKind (..), LintConfig (..),
+ patchConfig)
+import System.Exit (ExitCode (ExitFailure))
+import Types (Level (..))
+import Util (printPretty)
+import qualified Version as V (version)
+import WriteRepo (writeAdjustedRepository)
+
+-- | the options this cli tool can take
+data Options = Options
+ { repository :: Maybe String
+ -- ^ path to the repository containing maps to lint
+ , entrypoint :: Maybe String
+ -- ^ entrypoint in that repository
+ , json :: Bool
+ -- ^ emit json if --json was given
+ , lintlevel :: Maybe Level
+ -- ^ maximum lint level to print
+ , pretty :: Bool
+ -- ^ pretty-print the json to make it human-readable
+ , out :: Maybe String
+ -- ^ path to write the (possibly adjusted) maps to after linting
+ , configFile :: Maybe FilePath
+ -- ^ path to a config file. Currently required.
+ , config :: Maybe (LintConfig Patch)
+ -- ^ a "patch" for the configuration file
+ , version :: Bool
+ , dot :: Bool
+ } deriving (Show, Generic, HasArguments)
+
+
+main :: IO ()
+main = withCli run
+
+run :: Options -> IO ()
+run Options { .. } = do
+ aesonWarning
+
+ if version then
+ putStrLn V.version
+ else do
+ let repo = fromMaybe "." repository
+ let entry = fromMaybe "main.json" entrypoint
+ let level = fromMaybe Suggestion lintlevel
+ configFile' <- case configFile of
+ Nothing -> do
+ hPutStrLn stderr ("option --config-file=FILEPATH required" :: Text)
+ exitFailure
+ Just path -> pure path
+
+ lintconfig <- eitherDecodeFileStrict' configFile' >>= \case
+ Left err -> error $ "config file invalid: " <> toText err
+ Right file -> pure (patchConfig file config)
+
+ lints <- recursiveCheckDir lintconfig repo entry
+
+ if json
+ then putText
+ $ decodeUtf8 (if pretty then encodePretty lints else encode lints)
+ else printPretty (level, lints)
+
+ case out of
+ Nothing
+ | resultIsFatal lintconfig lints -> exitWith (ExitFailure 1)
+ | otherwise -> exitSuccess
+ Just outpath -> do
+ c <- writeAdjustedRepository lintconfig repo outpath lints
+ unless json $
+ case c of
+ ExitFailure 1 ->
+ putTextLn "\nMap failed linting!"
+ ExitFailure 2 ->
+ putTextLn "\nOutpath already exists, not writing anything."
+ _ -> pass
+ exitWith c
+
+
+-- if Aesons's internal map and HashMap are the same type, then coercionToHashMap
+-- will contain a proof of that, and we can print a warning. Otherwise we're not
+-- using HashMaps in Aeson and everything is fine.
+--
+-- cf. https://frasertweedale.github.io/blog-fp/posts/2021-10-12-aeson-hash-flooding-protection.html
+aesonWarning :: IO ()
+aesonWarning = case coercionToHashMap of
+ Just _ -> hPutStrLn stderr
+ ("Warning: this program was compiled using an older version of the Aeson Library\n\
+ \used for parsing JSON, which is susceptible to hash flooding attacks.\n\
+ \n\
+ \Recompiling with a newer version is recommended when handling untrusted inputs.\n\
+ \n\
+ \See https://cs-syd.eu/posts/2021-09-11-json-vulnerability for details." :: Text)
+ _ -> pass
diff --git a/walint-cli/Version.hs b/walint-cli/Version.hs
new file mode 100644
index 0000000..1748512
--- /dev/null
+++ b/walint-cli/Version.hs
@@ -0,0 +1,17 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+
+module Version ( version ) where
+
+import Universum
+
+import qualified Language.Haskell.TH as TH
+import System.Process (readProcess)
+
+version :: String
+version = "walint generic 2022 (" <>
+ $(do
+ hash <- liftIO $ catchAny (readProcess "git" ["rev-parse", "HEAD"] "")
+ (\_ -> pure "[unknown]")
+ pure . TH.LitE . TH.StringL $ take 40 hash) ++
+ ")"
diff --git a/walint-cli/default.nix b/walint-cli/default.nix
new file mode 100644
index 0000000..938bc4f
--- /dev/null
+++ b/walint-cli/default.nix
@@ -0,0 +1,18 @@
+{ mkDerivation, aeson, aeson-pretty, base, bytestring
+, getopt-generics, lib, process, template-haskell, text, universum
+, walint
+}:
+mkDerivation {
+ pname = "walint-cli";
+ version = "0.1";
+ src = ./.;
+ isLibrary = false;
+ isExecutable = true;
+ executableHaskellDepends = [
+ aeson aeson-pretty base bytestring getopt-generics process
+ template-haskell text universum walint
+ ];
+ homepage = "https://stuebinm.eu/git/walint";
+ license = "unknown";
+ mainProgram = "walint";
+}
diff --git a/walint-cli/walint-cli.cabal b/walint-cli/walint-cli.cabal
new file mode 100644
index 0000000..6ca10bd
--- /dev/null
+++ b/walint-cli/walint-cli.cabal
@@ -0,0 +1,27 @@
+cabal-version: 3.0
+name: walint-cli
+version: 0.1
+author: stuebinm
+maintainer: stuebinm@disroot.org
+copyright: 2023 stuebinm
+homepage: https://stuebinm.eu/git/walint
+
+executable walint
+ main-is: Main.hs
+ other-modules:
+ Version
+ default-extensions:
+ NoImplicitPrelude
+ ghc-options: -Wall -Wno-name-shadowing -Wno-unticked-promoted-constructors
+ build-depends:
+ aeson
+ , aeson-pretty
+ , base
+ , bytestring
+ , getopt-generics
+ , process
+ , template-haskell
+ , text
+ , universum
+ , walint
+ default-language: GHC2021