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
      { Acc -> Set (Linenumber, Text)
archives :: Set.Set (Linenumber, Text.Text),
        Acc -> Set (Linenumber, Text)
extracted :: Set.Set (Linenumber, Text.Text)
      }
  | Empty

rule :: Rule Shell.ParsedShell
rule :: Rule ParsedShell
rule = forall a args.
(Linenumber -> State a -> Instruction args -> State a)
-> State a -> (State a -> Failures) -> Rule args
veryCustomRule Linenumber -> State Acc -> Instruction ParsedShell -> State Acc
check (forall a. a -> State a
emptyState Acc
Empty) State Acc -> Failures
markFailures
  where
    code :: RuleCode
code = RuleCode
"DL3010"
    severity :: DLSeverity
severity = DLSeverity
DLInfoC
    message :: Text
message = Text
"Use `ADD` for extracting archives into an image"

    check :: Linenumber -> State Acc -> Instruction ParsedShell -> State Acc
check Linenumber
_ State Acc
_ (From BaseImage
_) = forall a. a -> State a
emptyState Acc
Empty
    check Linenumber
line State Acc
st (Copy (CopyArgs NonEmpty SourcePath
srcs TargetPath
tgt) (CopyFlags Chown
_ Chmod
_ Link
_ CopySource
NoSource)) =
      State Acc
st forall a b. a -> (a -> b) -> b
|> forall a. (a -> a) -> State a -> State a
modify (Linenumber -> NonEmpty SourcePath -> TargetPath -> Acc -> Acc
rememberArchives Linenumber
line NonEmpty SourcePath
srcs TargetPath
tgt)
    check Linenumber
_ State Acc
st (Run (RunArgs Arguments ParsedShell
args RunFlags
_))
      | Acc Set (Linenumber, Text)
archives Set (Linenumber, Text)
_ <- forall a. State a -> a
state State Acc
st,
        Set (Linenumber, Text)
ex <- forall a b. (a -> b) -> Arguments a -> b
foldArguments (Set (Linenumber, Text) -> ParsedShell -> Set (Linenumber, Text)
getExtractedArchives Set (Linenumber, Text)
archives) Arguments ParsedShell
args =
          State Acc
st forall a b. a -> (a -> b) -> b
|> forall a. (a -> a) -> State a -> State a
modify (Set (Linenumber, Text) -> Acc -> Acc
markExtracted Set (Linenumber, Text)
ex)
      | Bool
otherwise = State Acc
st
    check Linenumber
_ State Acc
st Instruction ParsedShell
_ = State Acc
st

    markFailures :: State Acc -> Failures
    markFailures :: State Acc -> Failures
markFailures (State Failures
fails (Acc Set (Linenumber, Text)
_ Set (Linenumber, Text)
e)) =
      forall a b. (a -> b -> a) -> a -> Set b -> a
Set.foldl' forall a. Seq a -> a -> Seq a
(Seq.|>) Failures
fails (forall b a. Ord b => (a -> b) -> Set a -> Set b
Set.map (Linenumber, Text) -> CheckFailure
makeFail Set (Linenumber, Text)
e)
    markFailures State Acc
st = forall a. State a -> Failures
failures State Acc
st

    makeFail :: (Linenumber, Text.Text) -> CheckFailure
    makeFail :: (Linenumber, Text) -> CheckFailure
makeFail (Linenumber
line, Text
_) = CheckFailure {Linenumber
Text
RuleCode
DLSeverity
line :: Linenumber
message :: Text
severity :: DLSeverity
code :: RuleCode
line :: Linenumber
message :: Text
severity :: DLSeverity
code :: RuleCode
..}
{-# INLINEABLE rule #-}


extractsThisArchive :: (Linenumber, Text.Text) -> Shell.Command -> Bool
extractsThisArchive :: (Linenumber, Text) -> Command -> Bool
extractsThisArchive (Linenumber
_, Text
archive) Command
cmd =
  (Command -> Bool
isTarExtractCommand Command
cmd Bool -> Bool -> Bool
|| Command -> Bool
isUnzipCommand Command
cmd) Bool -> Bool -> Bool
&& Text
archive forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`elem` [Text]
arguments
  where
    arguments :: [Text]
arguments = forall a b. (a -> b) -> [a] -> [b]
map Text -> Text
basename forall a b. (a -> b) -> a -> b
$ Command -> [Text]
Shell.getArgsNoFlags Command
cmd

getExtractedArchives ::
  Set.Set (Linenumber, Text.Text) ->
  Shell.ParsedShell ->
  Set.Set (Linenumber, Text.Text)
getExtractedArchives :: Set (Linenumber, Text) -> ParsedShell -> Set (Linenumber, Text)
getExtractedArchives Set (Linenumber, Text)
archives ParsedShell
shell =
  forall a. (a -> Bool) -> Set a -> Set a
Set.filter
    (\(Linenumber, Text)
a -> forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
any ((Linenumber, Text) -> Command -> Bool
extractsThisArchive (Linenumber, Text)
a) [Command]
cmds)
    Set (Linenumber, Text)
archives
  where
    cmds :: [Command]
cmds = ParsedShell -> [Command]
Shell.presentCommands ParsedShell
shell

isTarExtractCommand :: Shell.Command -> Bool
isTarExtractCommand :: Command -> Bool
isTarExtractCommand cmd :: Command
cmd@(Shell.Command Text
name [CmdPart]
_ [CmdPart]
_) =
  Text
name forall a. Eq a => a -> a -> Bool
== Text
"tar" Bool -> Bool -> Bool
&& (forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
any forall {a}. (Eq a, IsString a) => a -> Bool
longExtractFlags [Text]
args Bool -> Bool -> Bool
|| forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
any Text -> Bool
shortExtractFlags [Text]
args)
  where
    longExtractFlags :: a -> Bool
longExtractFlags a
f = a
f forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`elem` [a
"--extract", a
"--get"]
    shortExtractFlags :: Text -> Bool
shortExtractFlags Text
f = Text
"-" Text -> Text -> Bool
`Text.isPrefixOf` Text
f Bool -> Bool -> Bool
&& Text
"x" Text -> Text -> Bool
`Text.isInfixOf` Text
f
    args :: [Text]
args = Command -> [Text]
Shell.getArgs Command
cmd

isUnzipCommand :: Shell.Command -> Bool
isUnzipCommand :: Command -> Bool
isUnzipCommand (Shell.Command Text
name [CmdPart]
_ [CmdPart]
_) =
  Text
name forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`elem`
    [ Text
"unzip",
      Text
"gunzip",
      Text
"bunzip2",
      Text
"unlzma",
      Text
"unxz",
      Text
"zgz",
      Text
"uncompress",
      Text
"zcat",
      Text
"gzcat"
    ]

markExtracted :: Set.Set (Linenumber, Text.Text) -> Acc -> Acc
markExtracted :: Set (Linenumber, Text) -> Acc -> Acc
markExtracted Set (Linenumber, Text)
_ Acc
Empty = Acc
Empty
markExtracted Set (Linenumber, Text)
exarcv Acc {Set (Linenumber, Text)
archives :: Set (Linenumber, Text)
archives :: Acc -> Set (Linenumber, Text)
archives, Set (Linenumber, Text)
extracted :: Set (Linenumber, Text)
extracted :: Acc -> Set (Linenumber, Text)
extracted} =
  Acc { Set (Linenumber, Text)
archives :: Set (Linenumber, Text)
archives :: Set (Linenumber, Text)
archives, extracted :: Set (Linenumber, Text)
extracted = forall a. Ord a => Set a -> Set a -> Set a
Set.union Set (Linenumber, Text)
exarcv Set (Linenumber, Text)
extracted }

rememberArchives ::
  Linenumber ->
  NonEmpty SourcePath ->
  TargetPath ->
  Acc ->
  Acc
rememberArchives :: Linenumber -> NonEmpty SourcePath -> TargetPath -> Acc -> Acc
rememberArchives Linenumber
line NonEmpty SourcePath
paths TargetPath
target Acc
Empty =
  if Text -> Bool
isArchive forall a b. (a -> b) -> a -> b
$ TargetPath -> Text
unTargetPath TargetPath
target
    then Acc
          { archives :: Set (Linenumber, Text)
archives = forall a. a -> Set a
Set.singleton (Linenumber
line, Text -> Text
basename forall a b. (a -> b) -> a -> b
$ TargetPath -> Text
unTargetPath TargetPath
target),
            extracted :: Set (Linenumber, Text)
extracted = forall a. Set a
Set.empty
          }
    else Acc
          { archives :: Set (Linenumber, Text)
archives =
              NonEmpty SourcePath
paths
                forall a b. a -> (a -> b) -> b
& forall (t :: * -> *) a. Foldable t => t a -> [a]
toList
                forall a b. a -> (a -> b) -> b
& forall a b. (a -> b) -> [a] -> [b]
map (Text -> Text
basename forall b c a. (b -> c) -> (a -> b) -> a -> c
. SourcePath -> Text
unSourcePath)
                forall a b. a -> (a -> b) -> b
& forall a. Ord a => [a] -> Set a
Set.fromList
                forall a b. a -> (a -> b) -> b
& forall a. (a -> Bool) -> Set a -> Set a
Set.filter Text -> Bool
isArchive
                forall a b. a -> (a -> b) -> b
& forall b a. Ord b => (a -> b) -> Set a -> Set b
Set.map (Linenumber
line,),
            extracted :: Set (Linenumber, Text)
extracted = forall a. Set a
Set.empty
          }
rememberArchives Linenumber
line NonEmpty SourcePath
paths TargetPath
target Acc {Set (Linenumber, Text)
archives :: Set (Linenumber, Text)
archives :: Acc -> Set (Linenumber, Text)
archives, Set (Linenumber, Text)
extracted :: Set (Linenumber, Text)
extracted :: Acc -> Set (Linenumber, Text)
extracted} =
  if Text -> Bool
isArchive forall a b. (a -> b) -> a -> b
$ TargetPath -> Text
unTargetPath TargetPath
target
    then Acc
          { archives :: Set (Linenumber, Text)
archives =
              forall a. Ord a => a -> Set a -> Set a
Set.insert (Linenumber
line, Text -> Text
basename forall a b. (a -> b) -> a -> b
$ TargetPath -> Text
unTargetPath TargetPath
target) Set (Linenumber, Text)
archives,
            Set (Linenumber, Text)
extracted :: Set (Linenumber, Text)
extracted :: Set (Linenumber, Text)
extracted
          }
    else Acc
          { archives :: Set (Linenumber, Text)
archives =
              NonEmpty SourcePath
paths
                forall a b. a -> (a -> b) -> b
& forall (t :: * -> *) a. Foldable t => t a -> [a]
toList
                forall a b. a -> (a -> b) -> b
& forall a b. (a -> b) -> [a] -> [b]
map (Text -> Text
basename forall b c a. (b -> c) -> (a -> b) -> a -> c
. SourcePath -> Text
unSourcePath)
                forall a b. a -> (a -> b) -> b
& forall a. Ord a => [a] -> Set a
Set.fromList
                forall a b. a -> (a -> b) -> b
& forall a. (a -> Bool) -> Set a -> Set a
Set.filter Text -> Bool
isArchive
                forall a b. a -> (a -> b) -> b
& forall b a. Ord b => (a -> b) -> Set a -> Set b
Set.map (Linenumber
line,)
                forall a b. a -> (a -> b) -> b
& forall a. Ord a => Set a -> Set a -> Set a
Set.union Set (Linenumber, Text)
archives,
            Set (Linenumber, Text)
extracted :: Set (Linenumber, Text)
extracted :: Set (Linenumber, Text)
extracted
          }

basename :: Text.Text -> Text.Text
basename :: Text -> Text
basename = (Char -> Bool) -> Text -> Text
Text.takeWhileEnd (\Char
c -> Char
c forall a. Eq a => a -> a -> Bool
/= Char
'/' Bool -> Bool -> Bool
&& Char
c forall a. Eq a => a -> a -> Bool
/= Char
'\\') forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> Text
dropQuotes

isArchive :: Text.Text -> Bool
isArchive :: Text -> Bool
isArchive Text
src =
  forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
any (Text -> Text -> Bool
`Text.isSuffixOf` Text -> Text
dropQuotes Text
src) [Text]
archiveFileFormatExtensions