1 -- This file is part of Diohsc
2 -- Copyright (C) 2020 Martin Bays <mbays@sdf.org>
4 -- This program is free software: you can redistribute it and/or modify
5 -- it under the terms of version 3 of the GNU General Public License as
6 -- published by the Free Software Foundation, or any later version.
8 -- You should have received a copy of the GNU General Public License
9 -- along with this program. If not, see http://www.gnu.org/licenses/.
11 {-# LANGUAGE GeneralizedNewtypeDeriving #-}
13 module RunExternal
where
15 import Control
.Concurrent
(forkIO
)
16 import Control
.Monad
(void
)
17 import Control
.Monad
.Catch
(bracket_, finally
)
18 import Control
.Monad
.State
(State
, put
, runState
)
19 import Data
.Char (isAlphaNum)
20 import System
.Environment
(setEnv
, unsetEnv
)
21 import System
.Exit
(ExitCode)
23 import System
.IO.Temp
(withTempFile
)
26 import qualified Data
.ByteString
.Lazy
as BL
33 -- |Wrapper to ensure we don't accidentally allow use of shell commands in
35 newtype RestrictedIO a
= RestrictedIO
(IO a
)
36 deriving (Functor
,Applicative
,Monad
)
38 runRestrictedIO
:: RestrictedIO a
-> IO a
39 runRestrictedIO
(RestrictedIO m
) = m
42 subPercentOrAppend
:: String -> String -> String
43 subPercentOrAppend sub str
=
44 let (s
',subbed
) = subPercent str `runState`
False
45 in if subbed
then s
' else s
' ++ (' ':shellQuote sub
)
47 -- |based on specification for $BROWSER in 'man 1 man'
48 subPercent
:: String -> State
Bool String
49 subPercent
"" = return []
50 subPercent
('%':'%':s
) = ('%':) <$> subPercent s
51 subPercent
('%':'c
':s
) = (':':) <$> subPercent s
52 subPercent
('%':'s
':s
) = put
True >> (sub
++) <$> subPercent s
53 subPercent
(c
:s
) = (c
:) <$> subPercent s
55 |
all shellSafe s
&& not (null s
) = s
56 |
otherwise = '\'' : substAll
'\'' "'\\''" s
<> "'"
57 shellSafe c
= isAlphaNum c || c `
elem`
".,_-+="
59 |
(s
',_
:t
) <- break (== c
) s
= s
' <> r
<> substAll c r t
62 confirmShell
:: String -> String -> IO Bool
63 confirmShell desc cmd
= promptYN
True False $
64 desc
++ " following shell command?: " ++ withBoldStr cmd
66 pipeToCmdLazily
:: String -> [String] -> [(String,String)] -> BL
.ByteString
-> RestrictedIO
()
67 pipeToCmdLazily cmd cArgs
= pipeLazily
$ proc cmd cArgs
69 pipeToShellLazily
:: String -> [(String,String)] -> BL
.ByteString
-> RestrictedIO
()
70 pipeToShellLazily
= pipeLazily
. shell
72 filterShell
:: String -> [(String,String)] -> BL
.ByteString
-> RestrictedIO BL
.ByteString
73 filterShell
= filterProcess
. shell
75 withExtraEnv
:: [(String,String)] -> IO a
-> IO a
76 withExtraEnv envir
= bracket_
77 (mapM_ (uncurry setEnv
) envir
)
78 (mapM_ (unsetEnv
. fst) envir
)
80 pipeLazily
:: CreateProcess
-> [(String,String)] -> BL
.ByteString
-> RestrictedIO
()
81 pipeLazily cp envir b
= RestrictedIO
. withExtraEnv envir
$ do
82 (Just inp
, _
, _
, pid
) <- createProcess
$
83 cp
{ std_in
= CreatePipe
, std_out
= Inherit
}
84 hSetBuffering inp NoBuffering
85 ignoreIOErr
. finally
(BL
.hPut inp b
) . void
$ do
89 filterProcess
:: CreateProcess
-> [(String,String)] -> BL
.ByteString
-> RestrictedIO BL
.ByteString
90 filterProcess cp envir b
= RestrictedIO
. withExtraEnv envir
$ do
91 (Just inp
, Just outp
, _
, pid
) <- createProcess
$
92 cp
{ std_in
= CreatePipe
, std_out
= CreatePipe
}
93 hSetBuffering inp NoBuffering
94 hSetBuffering outp NoBuffering
95 _
<- forkIO
$ ignoreIOErr
. finally
(BL
.hPut inp b
) . void
$ do
100 runMailcap
:: Bool -> String -> String -> String -> BL
.ByteString
-> RestrictedIO
()
101 runMailcap noConfirm action dir mimetype b
=
102 RestrictedIO
. withTempFile dir
"runtmp" $ \path h
-> do
105 let cArgs
= ["--action=" ++ action
, mimetype
++ ":" ++ path
]
106 rawSystem
"run-mailcap" ("--norun":cArgs
) >>
107 (if noConfirm
then return True else promptYN
True False "Run this command?") >>?
108 void
$ rawSystem
"run-mailcap" cArgs
110 runShellCmd
:: String -> [(String,String)] -> RestrictedIO
ExitCode
111 runShellCmd cmd envir
= RestrictedIO
. withExtraEnv envir
$ spawnCommand cmd
>>= waitForProcess
113 shellOnData
:: Bool -> String -> FilePath -> [(String,String)] -> BL
.ByteString
-> RestrictedIO
()
114 shellOnData noConfirm cmd dir envir b
= RestrictedIO
. withTempFile dir
"runtmp" $ \path h
->
115 let cmd
' = subPercentOrAppend path cmd
116 in (if noConfirm
then return True else confirmShell
"Run" cmd
') >>? void
$ do
117 BL
.hPut h b
>> hClose h
118 runRestrictedIO
$ runShellCmd cmd
' envir