ghcup-0.2.6.0: lib/GHCup/Input/Parsers/Domain.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-|
Module : GHCup.Input.Parsers.Domain
Description : Domain parsers and bashism features for the metadata
Copyright : (c) Julian Ospald, 2026
License : LGPL-3.0
Maintainer : hasufell@hasufell.de
Stability : experimental
Portability : portable
-}
module GHCup.Input.Parsers.Domain where
import GHCup.Types
import GHCup.Errors
import Data.Bifunctor (bimap)
import Data.Versions ( Version, version )
import Control.Applicative ( Alternative ((<|>), many) )
import Data.Maybe
import Data.Void
import Text.Regex.Posix
import Data.Variant.Excepts
import qualified Data.Map.Strict as M
import qualified Data.Text as T
import qualified Text.Megaparsec as MP
evalDomainVal :: M.Map DomainVariable String -> (DomainVariable, Maybe Substitution) -> Maybe String
evalDomainVal env (dvar, msubst) = do
var <- M.lookup dvar env
pure $ maybe var (substitute var) msubst
where
mkRegex = makeRegexOpts compBlank execBlank
stripSuffix (T.pack -> suff) (T.pack -> t) = fmap T.unpack $ T.stripSuffix suff t
stripPrefix (T.pack -> prev) (T.pack -> t) = fmap T.unpack $ T.stripPrefix prev t
breakOn (T.pack -> needle) (T.pack -> haystack) = bimap T.unpack T.unpack $ T.breakOn needle haystack
replace (T.pack -> needle) (T.pack -> replacement) (T.pack -> haystack) = T.unpack $ T.replace needle replacement haystack
substitute var (Replace needle replacement)
| null matchedNeedle = var
| otherwise =
let (pre, rest) = breakOn matchedNeedle var
in case stripPrefix matchedNeedle rest of
Nothing -> var
Just rest' -> pre <> replacement <> rest'
where
matchedNeedle = match (mkRegex (globToRegex needle)) var
substitute var (ReplaceAll needle replacement)
| null matchedNeedle = var
| otherwise = replace matchedNeedle replacement var
where
matchedNeedle = match (mkRegex (globToRegex needle)) var
substitute var (StripPrefix prefix)
= let matchedPrefix = match (mkRegex ("^" <> globToRegex prefix)) var
in fromMaybe var (stripPrefix matchedPrefix var)
substitute var (StripSuffix suffix)
= let matchedSuffix = match (mkRegex (globToRegex suffix <> "$")) var
in fromMaybe var (stripSuffix matchedSuffix var)
-- https://www.man7.org/linux/man-pages/man7/glob.7.html
globToRegex :: String -> String
globToRegex = go
where
go :: String -> String
go ('\\':'*':xs) = '\\' : '*' : go xs
go ('\\':'?':xs) = '?' : go xs
go ('*':xs) = '.' : '*' : go xs
go ('?':xs) = '.' : go xs
go ('[':'!':']':xs) = '\\' : '[' : '!' : ']' : go xs
go ('[':'!':xs) = '[' : '^' : charClass xs
go ('[':']':xs) = '[' : '\\' : ']' : charClass xs
go ('[':xs) = '[' : charClass xs
go (x:xs)
| x `elem` specialChars = '\\' : x : go xs
| otherwise = x : go xs
go [] = []
charClass :: String -> String
charClass ('\\':']':xs) = '\\' : ']' : charClass xs
charClass (']':xs) = ']' : go xs
charClass (x:xs)
| x `elem` specialChars = '\\' : x : charClass xs
| otherwise = x : charClass xs
charClass [] = []
-- https://stackoverflow.com/a/400316
specialChars :: String
specialChars = ".^$*[\\"
domainParserP :: MP.Parsec Void String (DomainVariable, Maybe Substitution)
domainParserP = (,) <$> (MP.chunk "${" *> var) <*> MP.optional (MP.try subst) <* MP.chunk "}"
where
subst = MP.try replaceP <|> MP.try replaceAllP <|> MP.try stripSuffixP <|> MP.try stripPrefixP
replaceP = Replace <$> (MP.chunk "/" *> MP.some escaped <* MP.chunk "/") <*> MP.many escaped
replaceAllP = ReplaceAll <$> (MP.chunk "//" *> MP.some escaped <* MP.chunk "/") <*> MP.many escaped
stripPrefixP = StripPrefix <$> (MP.chunk "#" *> MP.some escaped)
stripSuffixP = StripSuffix <$> (MP.chunk "%" *> MP.some escaped)
escaped :: MP.Parsec Void String Char
escaped = do
t <- MP.lookAhead MP.anySingle
if | t == '\\'
-> MP.chunk "\\" *> MP.anySingle
| otherwise
-> MP.noneOf ("/%#}{$" :: String)
var = (TARGETFN <$ MP.try (MP.chunk "TARGETFN"))
<|> (PKGVER <$ MP.try (MP.chunk "PKGVER"))
<|> (PREFIX <$ MP.try (MP.chunk "PREFIX"))
<|> (TMPDIR <$ MP.try (MP.chunk "TMPDIR"))
throwOnParseError :: (Monad m, Show a) => Either a b -> Excepts '[ParseError] m b
throwOnParseError = either (throwE . ParseError . show) pure
domainParser :: M.Map DomainVariable String -> MP.Parsec Void String String
domainParser env = domainParser' env []
domainParser' :: M.Map DomainVariable String -> [DomainVariable] -> MP.Parsec Void String String
domainParser' env skip = concat <$> many anyOrKnownVars
where
anyOrKnownVars :: MP.Parsec Void String String
anyOrKnownVars = do
(MP.try (Just <$> MP.lookAhead ((,,) <$> MP.anySingle <*> MP.anySingle <*> MP.anySingle)) <|> pure Nothing) >>= \case
Just ('\\', '$', '{') -> do
_ <- MP.anySingle
fmap (:[]) MP.anySingle
Just ('$', '{', _) -> do
(matched, r@(dvar, _)) <- MP.match domainParserP
case evalDomainVal env r of
Just x -> pure x
Nothing
| dvar `elem` skip -> pure matched
| otherwise -> fail $ "Variable " <> show dvar <> " not found in env"
_ -> fmap (:[]) MP.anySingle
domainParserVer :: M.Map DomainVariable String -> MP.Parsec Void String [Either Char Version]
domainParserVer env = many anyOrKnownVars
where
anyOrKnownVars :: MP.Parsec Void String (Either Char Version)
anyOrKnownVars = do
(MP.try (Just <$> MP.lookAhead ((,,) <$> MP.anySingle <*> MP.anySingle <*> MP.anySingle)) <|> pure Nothing) >>= \case
Just ('\\', '$', '{') -> do
_ <- MP.anySingle
fmap Left MP.anySingle
Just ('$', '{', _) -> do
r <- domainParserP
v <- maybe (fail "Variable not found in env") pure (evalDomainVal env r)
v' <- either (fail . show) pure $ version (T.pack v)
pure (Right v')
_ -> fmap Left MP.anySingle