hadolint-2.8.0: src/Hadolint/Rule/DL3010.hs
module Hadolint.Rule.DL3010 (rule) where
import Data.Foldable (toList)
import Data.Function ((&))
import Data.List.NonEmpty (NonEmpty)
import qualified Data.Set as Set
import qualified Data.Sequence as Seq
import qualified Data.Text as Text
import Hadolint.Rule
import qualified Hadolint.Shell as Shell
import Language.Docker.Syntax
data Acc
= Acc
{ archives :: Set.Set (Linenumber, Text.Text),
extracted :: Set.Set (Linenumber, Text.Text)
}
| Empty
rule :: Rule Shell.ParsedShell
rule = veryCustomRule check (emptyState Empty) markFailures
where
code = "DL3010"
severity = DLInfoC
message = "Use `ADD` for extracting archives into an image"
check _ _ (From _) = emptyState Empty
check line st (Copy (CopyArgs srcs tgt _ _ NoSource)) =
st |> modify (rememberArchives line srcs tgt)
check _ st (Run (RunArgs args _))
| Acc archives _ <- state st,
ex <- foldArguments (getExtractedArchives archives) args =
st |> modify (markExtracted ex)
| otherwise = st
check _ st _ = st
markFailures :: State Acc -> Failures
markFailures (State fails (Acc _ e)) =
Set.foldl' (Seq.|>) fails (Set.map makeFail e)
markFailures st = failures st
makeFail :: (Linenumber, Text.Text) -> CheckFailure
makeFail (line, _) = CheckFailure {..}
{-# INLINEABLE rule #-}
extractsThisArchive :: (Linenumber, Text.Text) -> Shell.Command -> Bool
extractsThisArchive (_, archive) cmd =
(isTarExtractCommand cmd || isUnzipCommand cmd) && archive `elem` arguments
where
arguments = map basename $ Shell.getArgsNoFlags cmd
getExtractedArchives ::
Set.Set (Linenumber, Text.Text) ->
Shell.ParsedShell ->
Set.Set (Linenumber, Text.Text)
getExtractedArchives archives shell =
Set.filter
(\a -> any (extractsThisArchive a) cmds)
archives
where
cmds = Shell.presentCommands shell
isTarExtractCommand :: Shell.Command -> Bool
isTarExtractCommand cmd@(Shell.Command name _ _) =
name == "tar" && (any longExtractFlags args || any shortExtractFlags args)
where
longExtractFlags f = f `elem` ["--extract", "--get"]
shortExtractFlags f = "-" `Text.isPrefixOf` f && "x" `Text.isInfixOf` f
args = Shell.getArgs cmd
isUnzipCommand :: Shell.Command -> Bool
isUnzipCommand (Shell.Command name _ _) =
name `elem`
[ "unzip",
"gunzip",
"bunzip2",
"unlzma",
"unxz",
"zgz",
"uncompress",
"zcat",
"gzcat"
]
markExtracted :: Set.Set (Linenumber, Text.Text) -> Acc -> Acc
markExtracted _ Empty = Empty
markExtracted exarcv Acc {archives, extracted} =
Acc { archives, extracted = Set.union exarcv extracted }
rememberArchives ::
Linenumber ->
NonEmpty SourcePath ->
TargetPath ->
Acc ->
Acc
rememberArchives line paths target Empty =
if isArchive $ unTargetPath target
then Acc
{ archives = Set.singleton (line, basename $ unTargetPath target),
extracted = Set.empty
}
else Acc
{ archives =
paths
& toList
& map (basename . unSourcePath)
& Set.fromList
& Set.filter isArchive
& Set.map (line,),
extracted = Set.empty
}
rememberArchives line paths target Acc {archives, extracted} =
if isArchive $ unTargetPath target
then Acc
{ archives =
Set.insert (line, basename $ unTargetPath target) archives,
extracted
}
else Acc
{ archives =
paths
& toList
& map (basename . unSourcePath)
& Set.fromList
& Set.filter isArchive
& Set.map (line,)
& Set.union archives,
extracted
}
basename :: Text.Text -> Text.Text
basename = Text.takeWhileEnd (\c -> c /= '/' && c /= '\\') . dropQuotes
isArchive :: Text.Text -> Bool
isArchive src =
any (`Text.isSuffixOf` dropQuotes src) archiveFileFormatExtensions