module Hadolint.Config.Configfile
  ( getConfigFromFile
  )
where

import Control.Monad (when, filterM)
import Data.Maybe (listToMaybe)
import Data.YAML as Yaml
import qualified Data.ByteString as Bytes
import Hadolint.Config.Configuration (PartialConfiguration (..))
import System.Directory
  ( XdgDirectory (..),
    doesFileExist,
    getCurrentDirectory,
    getAppUserDataDirectory,
    getUserDocumentsDirectory,
    getXdgDirectory,
  )
import System.FilePath ((</>))
import System.IO (hPrint, stderr)


getConfigFromFile ::
  Maybe FilePath -> Bool -> IO (Either String PartialConfiguration)
getConfigFromFile :: Maybe FilePath -> Bool -> IO (Either FilePath PartialConfiguration)
getConfigFromFile Maybe FilePath
maybeExplicitPath Bool
verbose = do
  maybePath <- Maybe FilePath -> IO (Maybe FilePath)
getConfig Maybe FilePath
maybeExplicitPath
  when verbose $ hPrint stderr $ getFilePathDescription maybePath
  case maybePath of
    Maybe FilePath
Nothing -> Either FilePath PartialConfiguration
-> IO (Either FilePath PartialConfiguration)
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return (Either FilePath PartialConfiguration
 -> IO (Either FilePath PartialConfiguration))
-> Either FilePath PartialConfiguration
-> IO (Either FilePath PartialConfiguration)
forall a b. (a -> b) -> a -> b
$ PartialConfiguration -> Either FilePath PartialConfiguration
forall a b. b -> Either a b
Right PartialConfiguration
forall a. Monoid a => a
mempty
    Just FilePath
path -> FilePath -> IO (Either FilePath PartialConfiguration)
readConfig FilePath
path

readConfig :: FilePath -> IO (Either String PartialConfiguration)
readConfig :: FilePath -> IO (Either FilePath PartialConfiguration)
readConfig FilePath
path = do
  contents <- FilePath -> IO ByteString
Bytes.readFile FilePath
path
  return $ case Yaml.decode1Strict contents of
    Left (Pos
_, FilePath
err) -> FilePath -> Either FilePath PartialConfiguration
forall a b. a -> Either a b
Left (FilePath -> FilePath -> FilePath
formatError FilePath
err FilePath
path)
    Right PartialConfiguration
config -> PartialConfiguration -> Either FilePath PartialConfiguration
forall a b. b -> Either a b
Right PartialConfiguration
config

getFilePathDescription :: Maybe FilePath -> String
getFilePathDescription :: Maybe FilePath -> FilePath
getFilePathDescription Maybe FilePath
Nothing =
  FilePath
"No configuration was specified. Using default configuration"
getFilePathDescription (Just FilePath
filepath) = FilePath
"Configuration file used: " FilePath -> FilePath -> FilePath
forall a. [a] -> [a] -> [a]
++ FilePath
filepath

formatError :: String -> String -> String
formatError :: FilePath -> FilePath -> FilePath
formatError FilePath
err FilePath
config =
  [FilePath] -> FilePath
Prelude.unlines
    [ FilePath
"Error parsing your config file in  '" FilePath -> FilePath -> FilePath
forall a. [a] -> [a] -> [a]
++ FilePath
config FilePath -> FilePath -> FilePath
forall a. [a] -> [a] -> [a]
++ FilePath
"':",
      FilePath
"It should contain one of the keys 'override', 'ignored'",
      FilePath
"or 'trustedRegistries'. For example:\n",
      FilePath
"ignored:",
      FilePath
"\t- DL3000",
      FilePath
"\t- SC1099\n\n",
      FilePath
"The key 'override' should contain only lists with names 'error',",
      FilePath
"'warning', 'info' or 'style', which each name rules to override the",
      FilePath
"severity on. For example:\n",
      FilePath
"override:",
      FilePath
"\terror:",
      FilePath
"\t\t- DL3008\n\n",
      FilePath
"The key 'trustedRegistries' should contain the names of the allowed",
      FilePath
"docker registries:\n",
      FilePath
"trustedRegistries:",
      FilePath
"\t- docker.io",
      FilePath
"\t- my-company.com",
      FilePath
"",
      FilePath
err
    ]

-- | Gets the configuration file which Hadolint uses
getConfig :: Maybe FilePath -> IO (Maybe FilePath)
getConfig :: Maybe FilePath -> IO (Maybe FilePath)
getConfig Maybe FilePath
maybeConfig =
  case Maybe FilePath
maybeConfig of
    Maybe FilePath
Nothing -> IO (Maybe FilePath)
findConfig
    Maybe FilePath
_ -> Maybe FilePath -> IO (Maybe FilePath)
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return Maybe FilePath
maybeConfig

-- | If no configuration file path was given on the command line, Hadolint
-- searches these locations or their equivalents on MacOS/Windows:
--  - $(pwd)/.hadolint.{yaml|yml}
--  - $HOME/.config/hadolint.{yaml|yml}
--  - $HOME/.hadolint/{hadolint|config}.{yaml|yml}
--  - $HOME/.hadolint.{yaml|yml}
-- The first file found is used, all other are ignored.
findConfig :: IO (Maybe FilePath)
findConfig :: IO (Maybe FilePath)
findConfig = do
  filesInCWD <- (FilePath -> IO FilePath) -> [FilePath] -> IO [FilePath]
forall (t :: * -> *) (f :: * -> *) a b.
(Traversable t, Applicative f) =>
(a -> f b) -> t a -> f (t b)
forall (f :: * -> *) a b.
Applicative f =>
(a -> f b) -> [a] -> f [b]
traverse
                  (\FilePath
filePath -> (FilePath -> FilePath -> FilePath
</> FilePath
filePath) (FilePath -> FilePath) -> IO FilePath -> IO FilePath
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> IO FilePath
getCurrentDirectory)
                  [FilePath]
hiddenConfigs
  filesInXdgConfig <- traverse (getXdgDirectory XdgConfig) visibleConfigs
  filesInAppData <- traverse
                      (\FilePath
fp -> (FilePath -> FilePath -> FilePath
</> FilePath
fp) (FilePath -> FilePath) -> IO FilePath -> IO FilePath
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> FilePath -> IO FilePath
getAppUserDataDirectory FilePath
"hadolint")
                      (visibleConfigs <> moreConfigs)
  filesInHome <- traverse
                   (\FilePath
fp -> (FilePath -> FilePath -> FilePath
</> FilePath
fp) (FilePath -> FilePath) -> IO FilePath -> IO FilePath
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> IO FilePath
getUserDocumentsDirectory)
                   hiddenConfigs
  listToMaybe
    <$> filterM
          doesFileExist
          (filesInCWD <> filesInXdgConfig <> filesInAppData <> filesInHome)
  where
    hiddenConfigs :: [FilePath]
hiddenConfigs = [FilePath
".hadolint.yaml", FilePath
".hadolint.yml"]
    visibleConfigs :: [FilePath]
visibleConfigs = [FilePath
"hadolint.yaml", FilePath
"hadolint.yml"]
    moreConfigs :: [FilePath]
moreConfigs = [FilePath
"config.yaml", FilePath
"config.yml"]