2 -- Module : Distribution.PackageDescription.Check.Paths
3 -- Copyright : Lennart Kolmodin 2008, Francesco Ariis 2023
6 -- Maintainer : cabal-devel@haskell.org
7 -- Portability : portable
9 -- Functions to check filepaths, directories, globs, etc.
10 module Distribution
.PackageDescription
.Check
.Paths
13 , fileExtensionSupportedLanguage
14 , isGoodRelativeDirectoryPath
15 , isGoodRelativeFilePath
20 import Distribution
.Compat
.Prelude
23 import Distribution
.PackageDescription
.Check
.Common
24 import Distribution
.PackageDescription
.Check
.Monad
25 import Distribution
.Simple
.CCompiler
26 import Distribution
.Simple
.Glob
28 , explainGlobSyntaxError
32 import Distribution
.Simple
.Utils
hiding (findPackageDesc
, notice
)
33 import System
.FilePath (splitDirectories
, splitPath
, takeExtension
)
35 import qualified System
.FilePath.Windows
as FilePath.Windows
(isValid
)
37 fileExtensionSupportedLanguage
:: FilePath -> Bool
38 fileExtensionSupportedLanguage path
=
41 extension
= takeExtension path
42 isHaskell
= extension `
elem`
[".hs", ".lhs"]
43 isC
= isJust (filenameCDialect extension
)
45 -- Boolean: are absolute paths allowed?
48 => Bool -- Can be absolute path?
49 -> CabalField
-- .cabal field that we are checking.
50 -> PathKind
-- Path type.
53 checkPath isAbs title kind path
= do
56 (PackageBuildWarning
$ RelativeOutside title path
)
59 (PackageDistInexcusable
$ DistPoint
(Just title
) path
)
60 checkPackageFileNamesWithGlob kind path
62 -- Skip if "can be absolute path".
64 (not isAbs
&& isAbsoluteOnAnyPlatform path
)
65 (PackageDistInexcusable
$ AbsolutePath title path
)
70 (PackageDistInexcusable
$ BadRelativePath title path e
)
72 checkWindowsPath
(kind
== PathKindGlob
) path
74 isOutsideTree wpath
= case splitDirectories wpath
of
76 "." : ".." : _
-> True
79 -- These are not paths, but globs...
80 grl wfp PathKindFile
= isGoodRelativeFilePath wfp
81 grl wfp PathKindGlob
= isGoodRelativeGlob wfp
82 grl wfp PathKindDirectory
= isGoodRelativeDirectoryPath wfp
84 -- | Is a 'FilePath' inside `dist`, `dist-newstyle` and friends?
85 isInsideDist
:: FilePath -> Bool
87 case map lowercase
(splitDirectories path
) of
89 "." : "dist" : _
-> True
90 "dist-newstyle" : _
-> True
91 "." : "dist-newstyle" : _
-> True
94 checkPackageFileNamesWithGlob
97 -> FilePath -- Filepath or possibly a glob pattern.
99 checkPackageFileNamesWithGlob kind fp
= do
100 checkWindowsPath
(kind
== PathKindGlob
) fp
105 => Bool -- Is it a glob pattern?
108 checkWindowsPath isGlob path
=
110 (not . FilePath.Windows
.isValid
$ escape isGlob path
)
111 (PackageDistInexcusable
$ InvalidOnWin
[path
])
113 -- Force a relative name to catch invalid file names like "f:oo" which
114 -- otherwise parse as file "oo" in the current directory on the 'f' drive.
115 escape
:: Bool -> String -> String
116 escape wisGlob wpath
=
118 -- Glob paths will be expanded before being dereferenced, so asterisks
119 -- shouldn't count against them.
121 map (\c
-> if c
== '*' && wisGlob
then 'x
' else c
) wpath
123 -- | Check a file name is valid for the portable POSIX tar format.
125 -- The POSIX tar format has a restriction on the length of file names. It is
126 -- unfortunately not a simple restriction like a maximum length. The exact
127 -- restriction is that either the whole path be 100 characters or less, or it
128 -- be possible to split the path on a directory separator such that the first
129 -- part is 155 characters or less and the second part 100 characters or less.
130 checkTarPath
:: Monad m
=> FilePath -> CheckM m
()
132 |
length path
> 255 = tellP longPath
133 |
otherwise = case pack nameMax
(reverse (splitPath path
)) of
134 Left err
-> tellP err
135 Right
[] -> return ()
136 Right
(h
: rest
) -> case pack prefixMax remainder
of
137 Left err
-> tellP err
138 Right
[] -> return ()
139 Right
(_
: _
) -> tellP noSplit
141 -- drop the '/' between the name and prefix:
142 remainder
= safeInit h
: rest
144 nameMax
, prefixMax
:: Int
148 pack _
[] = Left emptyName
150 | n
> maxLen
= Left longName
151 |
otherwise = Right
(pack
' maxLen n cs
)
155 pack
' maxLen n
(c
: cs
)
156 | n
' <= maxLen
= pack
' maxLen n
' cs
161 longPath
= PackageDistInexcusable
(FilePathTooLong path
)
162 longName
= PackageDistInexcusable
(FilePathNameTooLong path
)
163 noSplit
= PackageDistInexcusable
(FilePathSplitTooLong path
)
164 emptyName
= PackageDistInexcusable FilePathEmpty
166 -- `checkGlob` checks glob patterns and returns good ones for further
170 => CabalField
-- .cabal field we are checking.
171 -> FilePath -- glob filepath pattern
172 -> CheckM m
(Maybe Glob
)
173 checkGlob title pat
= do
174 ver
<- asksCM ccSpecVersion
176 -- Glob sanity check.
177 case parseFileGlob ver pat
of
180 ( PackageDistInexcusable
$
181 GlobSyntaxError title
(explainGlobSyntaxError pat e
)
185 -- \* Miscellaneous checks on sane glob.
186 -- Checks for recursive glob in root.
188 (isRecursiveInRoot wglob
)
189 ( PackageDistSuspiciousWarn
$
190 RecursiveGlobInRoot title pat
194 -- | Whether a path is a good relative path. We aren't worried about perfect
195 -- cross-platform compatibility here; this function just checks the paths in
196 -- the (local) @.cabal@ file, while only Hackage needs the portability.
198 -- >>> let test fp = putStrLn $ show (isGoodRelativeDirectoryPath fp) ++ "; " ++ show (isGoodRelativeFilePath fp)
200 -- Note that "foo./bar.hs" would be invalid on Windows.
202 -- >>> traverse_ test ["foo/bar/quu", "a/b.hs", "foo./bar.hs"]
207 -- Trailing slash is not allowed for files, for directories it is ok.
210 -- Nothing; Just "trailing slash"
212 -- Leading @./@ is fine, but @.@ and @./@ are not valid files.
214 -- >>> traverse_ test [".", "./", "./foo/bar"]
215 -- Nothing; Just "trailing dot segment"
216 -- Nothing; Just "trailing slash"
219 -- Lastly, not good file nor directory cases:
221 -- >>> traverse_ test ["", "/tmp/src", "foo//bar", "foo/.", "foo/./bar", "foo/../bar"]
222 -- Just "empty path"; Just "empty path"
223 -- Just "posix absolute path"; Just "posix absolute path"
224 -- Just "empty path segment"; Just "empty path segment"
225 -- Just "trailing same directory segment: ."; Just "trailing same directory segment: ."
226 -- Just "same directory segment: ."; Just "same directory segment: ."
227 -- Just "parent directory segment: .."; Just "parent directory segment: .."
229 -- For the last case, 'isGoodRelativeGlob' doesn't warn:
231 -- >>> traverse_ (print . isGoodRelativeGlob) ["foo/../bar"]
232 -- Just "parent directory segment: .."
233 isGoodRelativeFilePath
:: FilePath -> Maybe String
234 isGoodRelativeFilePath
= state0
237 state0
[] = Just
"empty path"
239 | c
== '.' = state1 cs
240 | c
== '/' = Just
"posix absolute path"
241 |
otherwise = state5 cs
244 state1
[] = Just
"trailing dot segment"
246 | c
== '.' = state4 cs
247 | c
== '/' = state2 cs
248 |
otherwise = state5 cs
250 -- after ./ or after / between segments
251 state2
[] = Just
"trailing slash"
253 | c
== '.' = state3 cs
254 | c
== '/' = Just
"empty path segment"
255 |
otherwise = state5 cs
257 -- after non-first segment's .
258 state3
[] = Just
"trailing same directory segment: ."
260 | c
== '.' = state4 cs
261 | c
== '/' = Just
"same directory segment: ."
262 |
otherwise = state5 cs
265 state4
[] = Just
"trailing parent directory segment: .."
267 | c
== '.' = state5 cs
268 | c
== '/' = Just
"parent directory segment: .."
269 |
otherwise = state5 cs
271 -- in a segment which is ok.
274 | c
== '.' = state5 cs
275 | c
== '/' = state2 cs
276 |
otherwise = state5 cs
278 -- | See 'isGoodRelativeFilePath'.
280 -- This is barebones function. We check whether the glob is a valid file
281 -- by replacing stars @*@ with @x@ses.
282 isGoodRelativeGlob
:: FilePath -> Maybe String
283 isGoodRelativeGlob
= isGoodRelativeFilePath
. map f
288 -- | See 'isGoodRelativeFilePath'.
289 isGoodRelativeDirectoryPath
:: FilePath -> Maybe String
290 isGoodRelativeDirectoryPath
= state0
293 state0
[] = Just
"empty path"
295 | c
== '.' = state5 cs
296 | c
== '/' = Just
"posix absolute path"
297 |
otherwise = state4 cs
299 -- after initial ./ or after / between segments
302 | c
== '.' = state2 cs
303 | c
== '/' = Just
"empty path segment"
304 |
otherwise = state4 cs
306 -- after non-first setgment's .
307 state2
[] = Just
"trailing same directory segment: ."
309 | c
== '.' = state3 cs
310 | c
== '/' = Just
"same directory segment: ."
311 |
otherwise = state4 cs
314 state3
[] = Just
"trailing parent directory segment: .."
316 | c
== '.' = state4 cs
317 | c
== '/' = Just
"parent directory segment: .."
318 |
otherwise = state4 cs
320 -- in a segment which is ok.
323 | c
== '.' = state4 cs
324 | c
== '/' = state1 cs
325 |
otherwise = state4 cs
328 state5
[] = Nothing
-- "."
330 | c
== '.' = state3 cs
331 | c
== '/' = state1 cs
332 |
otherwise = state4 cs
334 -- [Note: Good relative paths]
336 -- Using @kleene@ we can define an extended regex:
339 -- import Algebra.Lattice
341 -- import Kleene.ERE (ERE (..), intersections)
343 -- data C = CDot | CSlash | CChar
344 -- deriving (Eq, Ord, Enum, Bounded, Show)
346 -- reservedR :: ERE C
347 -- reservedR = notChar CSlash
349 -- pathPieceR :: ERE C
350 -- pathPieceR = intersections
352 -- , ERENot (string [CDot])
353 -- , ERENot (string [CDot,CDot])
356 -- filePathR :: ERE C
357 -- filePathR = optional (string [CDot, CSlash]) <> pathPieceR <> star (char CSlash <> pathPieceR)
360 -- dirPathR = (char CDot \/ filePathR) <> optional (char CSlash)
362 -- plus :: ERE C -> ERE C
363 -- plus r = r <> star r
365 -- optional :: ERE C -> ERE C
366 -- optional r = mempty \/ r
369 -- Results in following state machine for @filePathR@
377 -- | x <= CSlash -> 2
390 -- | x <= CSlash -> 2
411 -- | x <= CSlash -> 1
415 -- | x <= CSlash -> 1