xrefcheck-0.3.0: src/Xrefcheck/System.hs
{- SPDX-FileCopyrightText: 2019 Serokell <https://serokell.io>
-
- SPDX-License-Identifier: MPL-2.0
-}
module Xrefcheck.System
( askWithinCI
, RelPosixLink (..)
, (</>)
, mkRelPosixLink
, filePathFromRoot
, getIntermediateDirs
, hasBackslash
, takeDirectory
, takeExtension
, CanonicalRelPosixLink (unCanonicalRelPosixLink)
, hasUnexpanededParentIndirections
, canonicalizeRelPosixLink
, CanonicalRelGlobPattern (unCanonicalRelGlobPattern)
, matchesGlobPatterns
, mkCanonicalRelGlobPattern
, PrintUnixPaths(..)
, mkPathForPrinting
) where
import Universum
import Data.Aeson (FromJSON (..), withText)
import Data.Char qualified as C
import Data.Reflection (Given (..))
import Data.Text qualified as T
import Fmt (Buildable)
import System.Console.Pretty (Pretty)
import System.Environment (lookupEnv)
import System.FilePath qualified as FP
import System.FilePath.Glob qualified as Glob
import System.FilePath.Posix qualified as FPP
import Text.Interpolation.Nyan (int, rmode')
-- | Heuristics to check whether we are running within CI.
-- Check the respective env variable which is usually set in all CIs.
askWithinCI :: IO Bool
askWithinCI = lookupEnv "CI" <&> \case
Just "1" -> True
Just (map C.toLower -> "true") -> True
_ -> False
-- | Relative file path with POSIX path separators.
--
-- This type exist in contrast to 'FilePath' which, in this project,
-- is used for platform-dependent file paths and related filesystem
-- IO operations.
--
-- Note that `RelPosixLink` may contain `\` characters, but they are
-- considered as part of the filename instead of denoting a path
-- separator.
newtype RelPosixLink = RelPosixLink
{ unRelPosixLink :: Text
} deriving newtype (Show, Eq, Ord, NFData, Buildable, Pretty)
-- | Create a POSIX file path from a platform-dependent one.
mkRelPosixLink :: FilePath -> RelPosixLink
mkRelPosixLink = RelPosixLink
. withPathSeparator FPP.pathSeparator
. fromString
-- | Join two 'RelPosixLink's.
(</>) :: RelPosixLink -> RelPosixLink -> RelPosixLink
RelPosixLink a </> RelPosixLink b =
let a' = fromMaybe a $ T.stripSuffix "/" a
b' = fromMaybe b $ T.stripPrefix "./" a
in case (a', b') of
("", _) -> RelPosixLink b
(".", _) -> RelPosixLink b
_ -> RelPosixLink $ a' <> "/" <> b'
-- Get the platform-dependent file path from a 'RelPosixLink'
-- considered as relative to another given platform-dependent
-- 'FilePath'.
--
-- In Windows, every `\` occurrence will be replaced by `/`.
filePathFromRoot :: FilePath -> RelPosixLink -> FilePath
filePathFromRoot rootPath = (rootPath FP.</>)
. toString
. withPathSeparator FP.pathSeparator
. unRelPosixLink
-- | 'FilePath.takeDirectory' version for 'RelPosixLink'.
takeDirectory :: RelPosixLink -> RelPosixLink
takeDirectory = RelPosixLink
. fromString
. FPP.takeDirectory
. toString
. unRelPosixLink
-- | 'FilePath.takeExtension' version for 'RelPosixLink'.
takeExtension :: RelPosixLink -> String
takeExtension = FPP.takeExtension
. toString
. unRelPosixLink
-- | 'Check if a 'RelPosixLink' contains any backslash.
hasBackslash :: RelPosixLink -> Bool
hasBackslash = ('\\' `elem`)
. unRelPosixLink
-- | Get the list of directories between a 'RelPosixLink' and its
-- relative root.
getIntermediateDirs :: RelPosixLink -> [RelPosixLink]
getIntermediateDirs link = fmap RelPosixLink $
case T.splitOn "/" $ unRelPosixLink $ takeDirectory link of
[] -> []
["."] -> [""]
[".."] -> ["", ".."]
d : ds -> scanl (\a b -> a <> "/" <> b) d ds
-- | Relative POSIX file path with some normalizations applied.
--
-- It should be created from a 'RelPosixLink' via
-- 'canonicalizeRelPosixLink'.
newtype CanonicalRelPosixLink = UnsafeCanonicalRelPosixLink
{ unCanonicalRelPosixLink :: RelPosixLink
} deriving newtype (Show, Eq, Ord, NFData, Buildable, Pretty)
-- | Canonicalize a 'RelPosixLink'.
--
-- Applies the following normalizations:
--
-- * Drop trailing path separator.
--
-- * Expand '.' and '..' indirections syntactically.
--
canonicalizeRelPosixLink :: RelPosixLink -> CanonicalRelPosixLink
canonicalizeRelPosixLink = UnsafeCanonicalRelPosixLink
. RelPosixLink
. expandPosixIndirections
. dropTrailingPosixPathSeparator
. withPathSeparator FPP.pathSeparator
. unRelPosixLink
-- | Check if a 'CanonicalRelPosixLink' passes through its relative root when
-- expanding indirections.
hasUnexpanededParentIndirections :: CanonicalRelPosixLink -> Bool
hasUnexpanededParentIndirections = elem ".."
. T.splitOn "/"
. unRelPosixLink
. unCanonicalRelPosixLink
-- | Relative Glob pattern with some normalizations applied.
--
-- It should be created via 'mkCanonicalRelGlobPattern'.
newtype CanonicalRelGlobPattern = UnsafeCanonicalRelGlobPattern
{ unCanonicalRelGlobPattern :: Glob.Pattern
}
-- | Create a CanonicalRelGlobPattern from a 'ToString' instance value that
-- represents a POSIX glob pattern.
--
-- Applies the following normalizations:
--
-- * Drop trailing path separator.
--
-- * FilePath.Posix.normalise.
--
-- * Expand '.' and '..' indirections syntactically.
--
mkCanonicalRelGlobPattern :: ToString s => s -> Either String CanonicalRelGlobPattern
mkCanonicalRelGlobPattern path = do
let spath = toString path
unless (FPP.isRelative spath) $ Left $
"Expected a relative glob pattern, but got " <> spath
-- Checking correctness of glob, e.g. "a[b" is incorrect
case Glob.tryCompileWith globCompileOptions (normalise spath) of
Right pat -> return $ UnsafeCanonicalRelGlobPattern pat
Left err -> Left
[int||
Glob pattern compilation failed.
Error message is:
#{err}
The syntax for glob patterns is described here:
https://hackage.haskell.org/package/Glob/docs/System-FilePath-Glob.html#v:compile
Special characters in file names can be escaped using square brackets, e.g. <a> -> [<]a[>].
|]
where
normalise = toString
. expandPosixIndirections
. fromString
. FPP.normalise
. FPP.dropTrailingPathSeparator
-- Checks if a 'CanonicalRelPosixLink' matches some of the given
-- 'CanonicalRelGlobPattern's.
--
-- They are considered as relative to the same root.
matchesGlobPatterns :: [CanonicalRelGlobPattern] -> CanonicalRelPosixLink -> Bool
matchesGlobPatterns globPatterns file = or
[ Glob.match pat . toString . unRelPosixLink . unCanonicalRelPosixLink $ file
| UnsafeCanonicalRelGlobPattern pat <- globPatterns
]
instance FromJSON CanonicalRelGlobPattern where
parseJSON = withText "Repo-relative glob pattern" $
either fail pure . mkCanonicalRelGlobPattern
-- | Glob compilation options we use.
globCompileOptions :: Glob.CompOptions
globCompileOptions = Glob.compDefault{Glob.errorRecovery = False}
dropTrailingPosixPathSeparator :: Text -> Text
dropTrailingPosixPathSeparator p = fromMaybe p
$ T.stripSuffix "/" p
-- Expand '.' and '..' in paths with Posix path separators.
expandPosixIndirections :: Text -> Text
expandPosixIndirections = T.intercalate "/"
. reverse
. expand 0
. reverse
. T.split (FPP.isPathSeparator)
where
expand :: Int -> [Text] -> [Text]
expand acc (".." : xs) = expand (acc + 1) xs
expand acc ("." : xs) = expand acc xs
expand 0 (x : xs) = x : expand 0 xs
expand acc (_ : xs) = expand (acc - 1) xs
expand acc [] = replicate acc ".."
-- Expand '.' and '..' in paths with system-specific path separators.
expandPathIndirections :: FilePath -> FilePath
expandPathIndirections = FP.joinPath
. reverse
. expand 0
. reverse
. map FP.dropTrailingPathSeparator
. FP.splitPath
where
expand :: Int -> [FilePath] -> [FilePath]
expand acc (".." : xs) = expand (acc + 1) xs
expand acc ("." : xs) = expand acc xs
expand 0 (x : xs) = x : expand 0 xs
expand acc (_ : xs) = expand (acc - 1) xs
expand acc [] = replicate acc ".."
withPathSeparator :: Char -> Text -> Text
withPathSeparator pathSep = T.map replaceSeparator
where
replaceSeparator :: Char -> Char
replaceSeparator c
| FP.isPathSeparator c = pathSep
| otherwise = c
newtype PrintUnixPaths = PrintUnixPaths Bool
mkPathForPrinting :: Given PrintUnixPaths => FilePath -> String
mkPathForPrinting = replaceSeparator . expandPathIndirections
where
replaceSeparator :: FilePath -> String
replaceSeparator = case given of
PrintUnixPaths True -> map (\c -> if c == FP.pathSeparator then '/' else c)
PrintUnixPaths False -> id