2011-08-22 20:14:12 +00:00
|
|
|
{- path manipulation
|
|
|
|
-
|
addurl --preserve-filename and a few related changes
* addurl --preserve-filename: New option, uses server-provided filename
without any sanitization, but with some security checking.
Not yet implemented for remotes other than the web.
* addurl, importfeed: Avoid adding filenames with leading '.', instead
it will be replaced with '_'.
This might be considered a security fix, but a CVE seems unwattanted.
It was possible for addurl to create a dotfile, which could change
behavior of some program. It was also possible for a web server to say
the file name was ".git" or "foo/.git". That would not overrwrite the
.git directory, but would cause addurl to fail; of course git won't
add "foo/.git".
sanitizeFilePath is too opinionated to remain in Utility, so moved it.
The changes to mkSafeFilePath are because it used sanitizeFilePath.
In particular:
isDrive will never succeed, because "c:" gets munged to "c_"
".." gets sanitized now
".git" gets sanitized now
It will never be null, because sanitizeFilePath keeps the length
the same, and splitDirectories never returns a null path.
Also, on the off chance a web server suggests a filename of "",
ignore that, rather than trying to save to such a filename, which would
fail in some way.
2020-05-08 20:09:29 +00:00
|
|
|
- Copyright 2010-2020 Joey Hess <id@joeyh.name>
|
2011-08-22 20:14:12 +00:00
|
|
|
-
|
2014-05-10 14:01:27 +00:00
|
|
|
- License: BSD-2-clause
|
2011-08-22 20:14:12 +00:00
|
|
|
-}
|
2013-04-14 16:51:05 +00:00
|
|
|
|
2020-10-28 18:18:09 +00:00
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
2017-12-31 20:08:31 +00:00
|
|
|
{-# LANGUAGE CPP #-}
|
2015-05-10 20:31:50 +00:00
|
|
|
{-# OPTIONS_GHC -fno-warn-tabs #-}
|
2011-08-22 20:14:12 +00:00
|
|
|
|
2019-11-21 19:38:06 +00:00
|
|
|
module Utility.Path (
|
|
|
|
simplifyPath,
|
|
|
|
parentDir,
|
|
|
|
upFrom,
|
|
|
|
dirContains,
|
|
|
|
segmentPaths,
|
2020-09-14 20:49:33 +00:00
|
|
|
segmentPaths',
|
2019-11-21 19:38:06 +00:00
|
|
|
runSegmentPaths,
|
2020-09-14 20:49:33 +00:00
|
|
|
runSegmentPaths',
|
2019-11-21 19:38:06 +00:00
|
|
|
dotfile,
|
|
|
|
splitShortExtensions,
|
2023-03-24 17:53:51 +00:00
|
|
|
splitShortExtensions',
|
2020-10-28 18:53:25 +00:00
|
|
|
relPathDirToFileAbs,
|
2021-02-02 23:01:45 +00:00
|
|
|
inSearchPath,
|
|
|
|
searchPath,
|
|
|
|
searchPathContents,
|
2019-11-21 19:38:06 +00:00
|
|
|
) where
|
2011-08-22 20:14:12 +00:00
|
|
|
|
2020-10-28 18:18:09 +00:00
|
|
|
import System.FilePath.ByteString
|
|
|
|
import qualified System.FilePath as P
|
|
|
|
import qualified Data.ByteString as B
|
2011-08-22 20:14:12 +00:00
|
|
|
import Data.List
|
|
|
|
import Data.Maybe
|
2021-02-02 23:06:33 +00:00
|
|
|
import Control.Monad
|
2011-08-25 04:28:55 +00:00
|
|
|
import Control.Applicative
|
2015-05-10 20:19:56 +00:00
|
|
|
import Prelude
|
2011-10-16 04:31:25 +00:00
|
|
|
|
|
|
|
import Utility.Monad
|
2020-03-05 17:52:30 +00:00
|
|
|
import Utility.SystemDirectory
|
2021-02-02 23:01:45 +00:00
|
|
|
import Utility.Exception
|
2011-08-22 20:14:12 +00:00
|
|
|
|
2020-11-10 15:21:03 +00:00
|
|
|
#ifdef mingw32_HOST_OS
|
|
|
|
import Data.Char
|
|
|
|
import Utility.FileSystemEncoding
|
|
|
|
#endif
|
|
|
|
|
2015-08-04 18:58:21 +00:00
|
|
|
{- Simplifies a path, removing any "." component, collapsing "dir/..",
|
|
|
|
- and removing the trailing path separator.
|
2014-02-07 21:10:51 +00:00
|
|
|
-
|
|
|
|
- On Windows, preserves whichever style of path separator might be used in
|
2020-10-28 18:18:09 +00:00
|
|
|
- the input RawFilePaths. This is done because some programs in Windows
|
2014-02-07 21:10:51 +00:00
|
|
|
- demand a particular path separator -- and which one actually varies!
|
|
|
|
-
|
|
|
|
- This does not guarantee that two paths that refer to the same location,
|
|
|
|
- and are both relative to the same location (or both absolute) will
|
2023-03-14 02:39:16 +00:00
|
|
|
- yield the same result. Run both through normalise from System.RawFilePath
|
2014-02-07 21:10:51 +00:00
|
|
|
- to ensure that.
|
|
|
|
-}
|
2020-10-28 18:18:09 +00:00
|
|
|
simplifyPath :: RawFilePath -> RawFilePath
|
2014-02-07 21:10:51 +00:00
|
|
|
simplifyPath path = dropTrailingPathSeparator $
|
|
|
|
joinDrive drive $ joinPath $ norm [] $ splitPath path'
|
|
|
|
where
|
|
|
|
(drive, path') = splitDrive path
|
|
|
|
|
|
|
|
norm c [] = reverse c
|
|
|
|
norm c (p:ps)
|
2015-08-04 18:58:21 +00:00
|
|
|
| p' == ".." && not (null c) && dropTrailingPathSeparator (c !! 0) /= ".." =
|
|
|
|
norm (drop 1 c) ps
|
2014-02-07 21:10:51 +00:00
|
|
|
| p' == "." = norm c ps
|
|
|
|
| otherwise = norm (p:c) ps
|
|
|
|
where
|
|
|
|
p' = dropTrailingPathSeparator p
|
|
|
|
|
2015-01-09 18:26:52 +00:00
|
|
|
{- takeDirectory "foo/bar/" is "foo/bar". This instead yields "foo" -}
|
2020-10-28 18:18:09 +00:00
|
|
|
parentDir :: RawFilePath -> RawFilePath
|
2015-01-09 18:26:52 +00:00
|
|
|
parentDir = takeDirectory . dropTrailingPathSeparator
|
|
|
|
|
|
|
|
{- Just the parent directory of a path, or Nothing if the path has no
|
2020-05-11 19:03:35 +00:00
|
|
|
- parent (ie for "/" or "." or "foo") -}
|
2020-10-28 18:18:09 +00:00
|
|
|
upFrom :: RawFilePath -> Maybe RawFilePath
|
2015-01-09 18:26:52 +00:00
|
|
|
upFrom dir
|
2015-01-20 21:23:23 +00:00
|
|
|
| length dirs < 2 = Nothing
|
2020-10-28 18:18:09 +00:00
|
|
|
| otherwise = Just $ joinDrive drive $
|
|
|
|
B.intercalate (B.singleton pathSeparator) $ init dirs
|
2012-12-13 04:24:19 +00:00
|
|
|
where
|
2017-05-16 03:32:17 +00:00
|
|
|
-- on Unix, the drive will be "/" when the dir is absolute,
|
|
|
|
-- otherwise ""
|
2013-05-12 18:58:46 +00:00
|
|
|
(drive, path) = splitDrive dir
|
2020-10-28 18:18:09 +00:00
|
|
|
dirs = filter (not . B.null) $ B.splitWith isPathSeparator path
|
2011-08-22 20:14:12 +00:00
|
|
|
|
2020-10-28 18:18:09 +00:00
|
|
|
{- Checks if the first RawFilePath is, or could be said to contain the second.
|
2011-08-22 20:14:12 +00:00
|
|
|
- For example, "foo/" contains "foo/bar". Also, "foo", "./foo", "foo/" etc
|
2023-03-14 02:39:16 +00:00
|
|
|
- are all equivalent.
|
2011-08-22 20:14:12 +00:00
|
|
|
-}
|
2020-10-28 18:18:09 +00:00
|
|
|
dirContains :: RawFilePath -> RawFilePath -> Bool
|
2019-12-20 22:01:29 +00:00
|
|
|
dirContains a b = a == b
|
|
|
|
|| a' == b'
|
2021-10-01 17:15:52 +00:00
|
|
|
|| (a'' `B.isPrefixOf` b' && avoiddotdotb)
|
2021-10-01 17:42:15 +00:00
|
|
|
|| a' == "." && normalise ("." </> b') == b' && nodotdot b'
|
2021-10-26 18:58:44 +00:00
|
|
|
|| dotdotcontains
|
2012-12-13 04:24:19 +00:00
|
|
|
where
|
|
|
|
a' = norm a
|
2021-10-01 17:15:52 +00:00
|
|
|
a'' = addTrailingPathSeparator a'
|
2012-12-13 04:24:19 +00:00
|
|
|
b' = norm b
|
2014-02-07 21:10:51 +00:00
|
|
|
norm = normalise . simplifyPath
|
2011-08-22 20:14:12 +00:00
|
|
|
|
2021-10-01 17:15:52 +00:00
|
|
|
{- This handles the case where a is ".." and b is "../..",
|
|
|
|
- which is not inside a. Similarly, "../.." does not contain
|
|
|
|
- "../../../". Due to the use of norm, cases like
|
|
|
|
- "../../foo/../../" get converted to eg "../../.." and
|
|
|
|
- so do not need to be handled specially here.
|
|
|
|
-
|
|
|
|
- When this is called, we already know that
|
|
|
|
- a'' is a prefix of b', so all that needs to be done is drop
|
|
|
|
- that prefix, and check if the next path component is ".."
|
|
|
|
-}
|
2021-10-01 17:42:15 +00:00
|
|
|
avoiddotdotb = nodotdot $ B.drop (B.length a'') b'
|
|
|
|
|
2021-10-26 18:58:44 +00:00
|
|
|
nodotdot p = all (not . isdotdot) (splitPath p)
|
|
|
|
|
|
|
|
isdotdot s = dropTrailingPathSeparator s == ".."
|
|
|
|
|
|
|
|
{- This handles the case where a is ".." or "../.." etc,
|
|
|
|
- and b is "foo" or "../foo" etc. The rule is that when
|
|
|
|
- a is entirely ".." components, b is under it when it starts
|
|
|
|
- with fewer ".." components.
|
|
|
|
-
|
|
|
|
- Due to the use of norm, cases like "../../foo/../../" get
|
|
|
|
- converted to eg "../../../" and so do not need to be handled
|
|
|
|
- specially here.
|
|
|
|
-}
|
|
|
|
dotdotcontains
|
|
|
|
| isAbsolute b' = False
|
|
|
|
| otherwise =
|
|
|
|
let aps = splitPath a'
|
|
|
|
bps = splitPath b'
|
|
|
|
in if all isdotdot aps
|
|
|
|
then length (takeWhile isdotdot bps) < length aps
|
|
|
|
else False
|
2021-10-01 17:15:52 +00:00
|
|
|
|
2012-11-25 21:54:08 +00:00
|
|
|
{- Given an original list of paths, and an expanded list derived from it,
|
2015-04-02 05:44:32 +00:00
|
|
|
- which may be arbitrarily reordered, generates a list of lists, where
|
|
|
|
- each sublist corresponds to one of the original paths.
|
|
|
|
-
|
|
|
|
- When the original path is a directory, any items in the expanded list
|
|
|
|
- that are contained in that directory will appear in its segment.
|
|
|
|
-
|
|
|
|
- The order of the original list of paths is attempted to be preserved in
|
|
|
|
- the order of the returned segments. However, doing so has a O^NM
|
|
|
|
- growth factor. So, if the original list has more than 100 paths on it,
|
|
|
|
- we stop preserving ordering at that point. Presumably a user passing
|
|
|
|
- that many paths in doesn't care too much about order of the later ones.
|
2011-09-19 05:37:04 +00:00
|
|
|
-}
|
2020-07-10 17:54:52 +00:00
|
|
|
segmentPaths :: (a -> RawFilePath) -> [RawFilePath] -> [a] -> [[a]]
|
2020-09-14 20:49:33 +00:00
|
|
|
segmentPaths = segmentPaths' (\_ r -> r)
|
|
|
|
|
|
|
|
segmentPaths' :: (Maybe RawFilePath -> a -> r) -> (a -> RawFilePath) -> [RawFilePath] -> [a] -> [[r]]
|
|
|
|
segmentPaths' f _ [] new = [map (f Nothing) new]
|
|
|
|
segmentPaths' f _ [i] new = [map (f (Just i)) new] -- optimisation
|
|
|
|
segmentPaths' f c (i:is) new =
|
|
|
|
map (f (Just i)) found : segmentPaths' f c is rest
|
2012-12-13 04:24:19 +00:00
|
|
|
where
|
2020-09-14 20:49:33 +00:00
|
|
|
(found, rest) = if length is < 100
|
|
|
|
then partition ini new
|
|
|
|
else break (not . ini) new
|
2020-10-28 18:18:09 +00:00
|
|
|
ini p = i `dirContains` c p
|
2011-09-19 05:37:04 +00:00
|
|
|
|
2012-11-25 21:54:08 +00:00
|
|
|
{- This assumes that it's cheaper to call segmentPaths on the result,
|
|
|
|
- than it would be to run the action separately with each path. In
|
|
|
|
- the case of git file list commands, that assumption tends to hold.
|
2011-09-19 05:37:04 +00:00
|
|
|
-}
|
2020-10-13 20:31:01 +00:00
|
|
|
runSegmentPaths :: (a -> RawFilePath) -> ([RawFilePath] -> IO [a]) -> [RawFilePath] -> IO [[a]]
|
|
|
|
runSegmentPaths c a paths = segmentPaths c paths <$> a paths
|
2011-10-16 04:31:25 +00:00
|
|
|
|
2020-10-13 20:31:01 +00:00
|
|
|
runSegmentPaths' :: (Maybe RawFilePath -> a -> r) -> (a -> RawFilePath) -> ([RawFilePath] -> IO [a]) -> [RawFilePath] -> IO [[r]]
|
|
|
|
runSegmentPaths' si c a paths = segmentPaths' si c paths <$> a paths
|
2020-09-14 20:49:33 +00:00
|
|
|
|
2012-01-03 04:09:09 +00:00
|
|
|
{- Checks if a filename is a unix dotfile. All files inside dotdirs
|
|
|
|
- count as dotfiles. -}
|
2020-10-28 18:18:09 +00:00
|
|
|
dotfile :: RawFilePath -> Bool
|
2012-01-03 04:09:09 +00:00
|
|
|
dotfile file
|
|
|
|
| f == "." = False
|
|
|
|
| f == ".." = False
|
|
|
|
| f == "" = False
|
2020-10-28 18:18:09 +00:00
|
|
|
| otherwise = "." `B.isPrefixOf` f || dotfile (takeDirectory file)
|
2012-12-13 04:24:19 +00:00
|
|
|
where
|
|
|
|
f = takeFileName file
|
2013-05-14 17:24:15 +00:00
|
|
|
|
2020-10-28 18:18:09 +00:00
|
|
|
{- Similar to splitExtensions, but knows that some things in RawFilePaths
|
2014-02-16 21:39:54 +00:00
|
|
|
- after a dot are too long to be extensions. -}
|
2020-10-28 18:18:09 +00:00
|
|
|
splitShortExtensions :: RawFilePath -> (RawFilePath, [B.ByteString])
|
2014-02-16 21:39:54 +00:00
|
|
|
splitShortExtensions = splitShortExtensions' 5 -- enough for ".jpeg"
|
2020-10-28 18:18:09 +00:00
|
|
|
splitShortExtensions' :: Int -> RawFilePath -> (RawFilePath, [B.ByteString])
|
2014-02-16 21:39:54 +00:00
|
|
|
splitShortExtensions' maxextension = go []
|
|
|
|
where
|
|
|
|
go c f
|
2020-10-28 18:18:09 +00:00
|
|
|
| len > 0 && len <= maxextension && not (B.null base) =
|
2014-02-16 21:39:54 +00:00
|
|
|
go (ext:c) base
|
|
|
|
| otherwise = (f, c)
|
|
|
|
where
|
|
|
|
(base, ext) = splitExtension f
|
2020-10-28 18:18:09 +00:00
|
|
|
len = B.length ext
|
2020-10-28 18:53:25 +00:00
|
|
|
|
2021-01-13 17:21:15 +00:00
|
|
|
{- This requires both paths to be absolute and normalized.
|
2020-10-28 18:53:25 +00:00
|
|
|
-
|
|
|
|
- On Windows, if the paths are on different drives,
|
|
|
|
- a relative path is not possible and the path is simply
|
|
|
|
- returned as-is.
|
|
|
|
-}
|
|
|
|
relPathDirToFileAbs :: RawFilePath -> RawFilePath -> RawFilePath
|
|
|
|
relPathDirToFileAbs from to
|
|
|
|
#ifdef mingw32_HOST_OS
|
|
|
|
| normdrive from /= normdrive to = to
|
|
|
|
#endif
|
|
|
|
| otherwise = joinPath $ dotdots ++ uncommon
|
|
|
|
where
|
|
|
|
pfrom = sp from
|
|
|
|
pto = sp to
|
|
|
|
sp = map dropTrailingPathSeparator . splitPath . dropDrive
|
|
|
|
common = map fst $ takeWhile same $ zip pfrom pto
|
|
|
|
same (c,d) = c == d
|
|
|
|
uncommon = drop numcommon pto
|
|
|
|
dotdots = replicate (length pfrom - numcommon) ".."
|
|
|
|
numcommon = length common
|
|
|
|
#ifdef mingw32_HOST_OS
|
2021-09-01 15:32:25 +00:00
|
|
|
normdrive = map toLower
|
|
|
|
-- Get just the drive letter, removing any leading
|
|
|
|
-- path separator, which takeDrive leaves on the drive
|
|
|
|
-- letter.
|
|
|
|
. dropWhileEnd (isPathSeparator . fromIntegral . ord)
|
|
|
|
. fromRawFilePath
|
|
|
|
. takeDrive
|
2020-10-28 18:53:25 +00:00
|
|
|
#endif
|
2021-02-02 23:01:45 +00:00
|
|
|
|
|
|
|
{- Checks if a command is available in PATH.
|
|
|
|
-
|
|
|
|
- The command may be fully-qualified, in which case, this succeeds as
|
|
|
|
- long as it exists. -}
|
|
|
|
inSearchPath :: String -> IO Bool
|
|
|
|
inSearchPath command = isJust <$> searchPath command
|
|
|
|
|
|
|
|
{- Finds a command in PATH and returns the full path to it.
|
|
|
|
-
|
|
|
|
- The command may be fully qualified already, in which case it will
|
|
|
|
- be returned if it exists.
|
|
|
|
-
|
|
|
|
- Note that this will find commands in PATH that are not executable.
|
|
|
|
-}
|
|
|
|
searchPath :: String -> IO (Maybe FilePath)
|
|
|
|
searchPath command
|
|
|
|
| P.isAbsolute command = check command
|
|
|
|
| otherwise = P.getSearchPath >>= getM indir
|
|
|
|
where
|
|
|
|
indir d = check $ d P.</> command
|
|
|
|
check f = firstM doesFileExist
|
|
|
|
#ifdef mingw32_HOST_OS
|
|
|
|
[f, f ++ ".exe"]
|
|
|
|
#else
|
|
|
|
[f]
|
|
|
|
#endif
|
|
|
|
|
|
|
|
{- Finds commands in PATH that match a predicate. Note that the predicate
|
|
|
|
- matches on the basename of the command, but the full path to it is
|
2021-02-02 23:06:33 +00:00
|
|
|
- returned.
|
|
|
|
-
|
|
|
|
- Note that this will find commands in PATH that are not executable.
|
|
|
|
-}
|
2021-02-02 23:01:45 +00:00
|
|
|
searchPathContents :: (FilePath -> Bool) -> IO [FilePath]
|
2021-02-02 23:06:33 +00:00
|
|
|
searchPathContents p =
|
|
|
|
filterM doesFileExist
|
|
|
|
=<< (concat <$> (P.getSearchPath >>= mapM go))
|
2021-02-02 23:01:45 +00:00
|
|
|
where
|
|
|
|
go d = map (d P.</>) . filter p
|
|
|
|
<$> catchDefaultIO [] (getDirectoryContents d)
|