packages feed

hadolint-2.0.0: src/Hadolint/Rule/DL3045.hs

module Hadolint.Rule.DL3045 (rule) where

import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as Text
import Hadolint.Rule
import Language.Docker.Syntax

-- This data encapsulates the name of a build stage. It may be None withing an
-- `ONBUILD` context.
data Stage
  = Stage {stage :: Text}
  | None
  deriving (Eq, Ord)

-- | The state here keeps the image name/alias of the current build stage
-- and a map from image names/aliases to a Bool, saving whether or
-- not a `WORKDIR` has been set in a build stage.
data Acc
  = Acc {current :: Stage, workdirSet :: Map Stage Bool}
  | Empty

rule :: Rule args
rule = customRule check (emptyState Empty)
  where
    code = "DL3045"
    severity = DLWarningC
    message = "`COPY` to a relative destination without `WORKDIR` set."

    check _ st (From from) = st |> modify (rememberStage from)
    check _ st (Workdir _) = st |> modify rememberWorkdir
    check line st (Copy (CopyArgs _ (TargetPath dest) _ _))
      | Acc s m <- state st, Just True <- Map.lookup s m = st -- workdir has been set
      | "/" `Text.isPrefixOf` Text.dropAround quotePredicate dest = st -- absolute dest. normal
      | ":\\" `Text.isPrefixOf` Text.drop 1 (Text.dropAround quotePredicate dest) = st -- absolute dest. windows
      | "$" `Text.isPrefixOf` Text.dropAround quotePredicate dest = st -- dest is a variable
      | otherwise = st |> addFail CheckFailure {..}
    check _ st _ = st
{-# INLINEABLE rule #-}

rememberStage :: BaseImage -> Acc -> Acc
rememberStage BaseImage {alias = Just als} Empty =
  Acc
    { current = Stage {stage = unImageAlias als},
      workdirSet = mempty
    }
rememberStage BaseImage {alias = Nothing, image} Empty =
  Acc
    { current = Stage {stage = imageName image},
      workdirSet = mempty
    }
rememberStage BaseImage {alias = Just als, image} Acc {..} =
  Acc
    { current = Stage {stage = unImageAlias als},
      workdirSet =
        let parentValue =
              Map.lookup (Stage { stage = imageName image}) workdirSet
                |> fromMaybe False
         in workdirSet
              |> Map.insert (Stage {stage = unImageAlias als}) parentValue
    }
rememberStage BaseImage {alias = Nothing, image} Acc {..} =
  Acc
    { current = Stage {stage = imageName image},
      workdirSet =
        let parentValue =
              Map.lookup (Stage {stage = imageName image}) workdirSet
                |> fromMaybe False
         in workdirSet
              |> Map.insert (Stage {stage = imageName image}) parentValue
    }

rememberWorkdir :: Acc -> Acc
rememberWorkdir Empty = Acc {current = None, workdirSet = Map.insert None True Map.empty}
rememberWorkdir Acc {..} = Acc {current, workdirSet = Map.insert current True workdirSet}

quotePredicate :: Char -> Bool
quotePredicate c
  | c == '"' = True
  | c == '\'' = True
  | otherwise = False