packages feed

language-docker-16.0.0: src/Language/Docker/Parser/From.hs

module Language.Docker.Parser.From
  ( parseFrom,
  )
where

import qualified Data.Text as T
import Language.Docker.Parser.Prelude
import Language.Docker.Syntax

parseRegistry :: (?esc :: Char) => Parser Registry
parseRegistry = do
  domain <- someUnlessExpanded "a domain name" (== '.')
  void $ char '.'
  tld <- someUnlessExpanded "a TLD" (== '/')
  void $ char '/'
  return $ Registry (domain <> "." <> tld)

parsePlatform :: (?esc :: Char) => Parser Platform
parsePlatform = do
  void $ string "--platform="
  p <- someUnless "the platform for the FROM image" (== ' ')
  requiredWhitespace
  return p

-- | Like someUnless, but a ${...} parameter expansion is opaque: a ':' or '@'
-- inside it (the :- :? :+ operators) is not read as the tag or digest
-- separator. One linear pass, bounded to the current line.
someUnlessExpanded :: (?esc :: Char) => String -> (Char -> Bool) -> Parser Text
someUnlessExpanded name predicate =
  mconcat <$> some (variableExpansion <|> literalRun) <?> name
  where
    literalRun = someUnless name (\c -> predicate c || c == '$')

-- | A '$' and, when it opens one, the balanced ${...} that follows. The scan
-- stops at whitespace, so an unterminated '${' stays literal text bounded like
-- any image reference (it cannot swallow a same-line 'AS' alias or a later
-- instruction, nor backtrack). Fragments are collected in a list and joined
-- once, so deep nesting stays linear.
variableExpansion :: Parser Text
variableExpansion = do
  void $ char '$'
  brace <- optional (char '{')
  case brace of
    Nothing -> return "$"
    Just _ -> ("${" <>) <$> braces [] (1 :: Int)
  where
    braces acc depth = do
      piece <- takeWhileP Nothing (\c -> c `notElem` ['{', '}', ' ', '\t', '\n'])
      next <- optional (char '{' <|> char '}')
      case next of
        Just '{' -> braces ("{" : piece : acc) $! depth + 1
        Just '}'
          | depth <= 1 -> return $ mconcat (reverse ("}" : piece : acc))
          | otherwise -> braces ("}" : piece : acc) $! depth - 1
        _ -> return $ mconcat (reverse (piece : acc))

parseBaseImage :: (?esc :: Char) => (Text -> Parser (Maybe Tag)) -> Parser BaseImage
parseBaseImage tagParser = do
  maybePlatform <- (Just <$> try parsePlatform) <|> return Nothing
  notFollowedBy (string "--")
  regName <- (Just <$> try parseRegistry) <|> return Nothing
  name <- someUnlessExpanded "the image name with a tag" (\c -> c == '@' || c == ':')
  maybeTag <- tagParser name <|> return Nothing
  maybeDigest <- (Just <$> try parseDigest) <|> return Nothing
  maybeAlias <- (Just <$> try (requiredWhitespace *> imageAlias)) <|> return Nothing
  return $ BaseImage (Image regName name) maybeTag maybeDigest maybeAlias maybePlatform

taggedImage :: (?esc :: Char) => Parser BaseImage
taggedImage = parseBaseImage tagParser
  where
    tagParser _ = do
      void $ char ':'
      t <- someUnlessExpanded "the image tag" (\c -> c == '@' || c == ':')
      return (Just . Tag $ t)

parseDigest :: (?esc :: Char) => Parser Digest
parseDigest = do
  void $ char '@'
  d <- someUnless "the image digest" (== '@')
  return $ Digest d

untaggedImage :: (?esc :: Char) => Parser BaseImage
untaggedImage = parseBaseImage notInvalidTag
  where
    notInvalidTag :: Text -> Parser (Maybe Tag)
    notInvalidTag name = do
      try (notFollowedBy $ string ":") <?> "no ':' or a valid image tag string (example: "
        ++ T.unpack name
        ++ ":valid-tag)"
      return Nothing

imageAlias :: (?esc :: Char) => Parser ImageAlias
imageAlias = do
  void (try (reserved "AS") <?> "'AS' followed by the image alias")
  aka <- someUnless "the image alias" (== '\n')
  return $ ImageAlias aka

baseImage :: (?esc :: Char) => Parser BaseImage
baseImage = try taggedImage <|> untaggedImage

parseFrom :: (?esc :: Char) => Parser (Instruction Text)
parseFrom = do
  reserved "FROM"
  From <$> baseImage