You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

425 lines
12 KiB

  1. -- |
  2. -- Module : HPath
  3. -- Copyright : © 2015–2016 FP Complete, 2016 Julian Ospald
  4. -- License : BSD 3 clause
  5. --
  6. -- Maintainer : Julian Ospald <hasufell@posteo.de>
  7. -- Stability : experimental
  8. -- Portability : portable
  9. --
  10. -- Support for well-typed paths.
  11. {-# LANGUAGE BangPatterns #-}
  12. {-# LANGUAGE CPP #-}
  13. {-# LANGUAGE DeriveDataTypeable #-}
  14. {-# LANGUAGE EmptyDataDecls #-}
  15. #if __GLASGOW_HASKELL__ >= 708
  16. {-# LANGUAGE PatternSynonyms #-}
  17. #endif
  18. {-# LANGUAGE ScopedTypeVariables #-}
  19. module HPath
  20. (
  21. -- * Types
  22. Abs
  23. ,Path
  24. ,Rel
  25. ,Fn
  26. ,PathParseException
  27. ,PathException
  28. ,RelC
  29. #if __GLASGOW_HASKELL__ >= 708
  30. -- * PatternSynonyms/ViewPatterns
  31. ,pattern Path
  32. #endif
  33. -- * Path Parsing
  34. ,parseAbs
  35. ,parseFn
  36. ,parseRel
  37. -- * Path Conversion
  38. ,fromAbs
  39. ,fromRel
  40. ,toFilePath
  41. ,unsafeToString
  42. ,unsafeToString'
  43. -- * Path Operations
  44. ,(</>)
  45. ,basename
  46. ,dirname
  47. ,isParentOf
  48. ,getAllParents
  49. ,stripDir
  50. -- * Path IO helpers
  51. ,withAbsPath
  52. ,withRelPath
  53. ,withFnPath
  54. )
  55. where
  56. import Control.Exception (IOException, Exception, catch)
  57. import Control.Monad ((<$!>))
  58. import Control.Monad.Catch (MonadThrow(..))
  59. import Data.ByteString.Unsafe(unsafeUseAsCStringLen)
  60. #if MIN_VERSION_bytestring(0,10,8)
  61. import Data.ByteString(ByteString, stripPrefix)
  62. #else
  63. import Data.ByteString(ByteString)
  64. import qualified Data.List as L
  65. #endif
  66. import qualified Data.ByteString as BS
  67. import Data.Data
  68. import Data.Maybe
  69. import Data.Word8
  70. import GHC.Foreign(peekCStringLen)
  71. import GHC.IO.Encoding(getLocaleEncoding, TextEncoding)
  72. import HPath.Internal
  73. import System.IO.Unsafe(unsafePerformIO)
  74. import System.Posix.FilePath hiding ((</>))
  75. -- $setup
  76. -- >>> import GHC.IO.Encoding(utf8)
  77. --------------------------------------------------------------------------------
  78. -- Types
  79. -- | An absolute path.
  80. data Abs deriving (Typeable)
  81. -- | A relative path; one without a root.
  82. data Rel deriving (Typeable)
  83. -- | A filename, without any '/'.
  84. data Fn deriving (Typeable)
  85. -- | Exception when parsing a location.
  86. data PathParseException
  87. = InvalidAbs ByteString
  88. | InvalidRel ByteString
  89. | InvalidFn ByteString
  90. | Couldn'tStripPrefixTPS ByteString ByteString
  91. deriving (Show,Typeable)
  92. instance Exception PathParseException
  93. data PathException = RootDirHasNoBasename
  94. deriving (Show,Typeable)
  95. instance Exception PathException
  96. class RelC m
  97. instance RelC Rel
  98. instance RelC Fn
  99. --------------------------------------------------------------------------------
  100. -- PatternSynonyms
  101. #if __GLASGOW_HASKELL__ >= 710
  102. pattern Path :: ByteString -> Path a
  103. #endif
  104. #if __GLASGOW_HASKELL__ >= 708
  105. pattern Path x <- (MkPath x)
  106. #endif
  107. --------------------------------------------------------------------------------
  108. -- Path Parsers
  109. -- | Get a location for an absolute path. Produces a normalised path.
  110. --
  111. -- Throws: 'PathParseException'
  112. --
  113. -- >>> parseAbs "/abc" :: Maybe (Path Abs)
  114. -- Just "/abc"
  115. -- >>> parseAbs "/" :: Maybe (Path Abs)
  116. -- Just "/"
  117. -- >>> parseAbs "/abc/def" :: Maybe (Path Abs)
  118. -- Just "/abc/def"
  119. -- >>> parseAbs "/abc/def/.///" :: Maybe (Path Abs)
  120. -- Just "/abc/def/"
  121. -- >>> parseAbs "abc" :: Maybe (Path Abs)
  122. -- Nothing
  123. -- >>> parseAbs "" :: Maybe (Path Abs)
  124. -- Nothing
  125. -- >>> parseAbs "/abc/../foo" :: Maybe (Path Abs)
  126. -- Nothing
  127. parseAbs :: MonadThrow m
  128. => ByteString -> m (Path Abs)
  129. parseAbs filepath =
  130. if isAbsolute filepath &&
  131. isValid filepath &&
  132. not (hasParentDir filepath)
  133. then return (MkPath $ normalise filepath)
  134. else throwM (InvalidAbs filepath)
  135. -- | Get a location for a relative path. Produces a normalised
  136. -- path.
  137. --
  138. -- Note that @filepath@ may contain any number of @./@ but may not consist
  139. -- solely of @./@. It also may not contain a single @..@ anywhere.
  140. --
  141. -- Throws: 'PathParseException'
  142. --
  143. -- >>> parseRel "abc" :: Maybe (Path Rel)
  144. -- Just "abc"
  145. -- >>> parseRel "def/" :: Maybe (Path Rel)
  146. -- Just "def/"
  147. -- >>> parseRel "abc/def" :: Maybe (Path Rel)
  148. -- Just "abc/def"
  149. -- >>> parseRel "abc/def/." :: Maybe (Path Rel)
  150. -- Just "abc/def/"
  151. -- >>> parseRel "/abc" :: Maybe (Path Rel)
  152. -- Nothing
  153. -- >>> parseRel "" :: Maybe (Path Rel)
  154. -- Nothing
  155. -- >>> parseRel "abc/../foo" :: Maybe (Path Rel)
  156. -- Nothing
  157. -- >>> parseRel "." :: Maybe (Path Rel)
  158. -- Nothing
  159. -- >>> parseRel ".." :: Maybe (Path Rel)
  160. -- Nothing
  161. parseRel :: MonadThrow m
  162. => ByteString -> m (Path Rel)
  163. parseRel filepath =
  164. if not (isAbsolute filepath) &&
  165. filepath /= BS.singleton _period &&
  166. filepath /= BS.pack [_period, _period] &&
  167. not (hasParentDir filepath) &&
  168. isValid filepath
  169. then return (MkPath $ normalise filepath)
  170. else throwM (InvalidRel filepath)
  171. -- | Parses a filename. Filenames must not contain slashes.
  172. -- Excludes '.' and '..'.
  173. --
  174. -- Throws: 'PathParseException'
  175. --
  176. -- >>> parseFn "abc" :: Maybe (Path Fn)
  177. -- Just "abc"
  178. -- >>> parseFn "..." :: Maybe (Path Fn)
  179. -- Just "..."
  180. -- >>> parseFn "def/" :: Maybe (Path Fn)
  181. -- Nothing
  182. -- >>> parseFn "abc/def" :: Maybe (Path Fn)
  183. -- Nothing
  184. -- >>> parseFn "abc/def/." :: Maybe (Path Fn)
  185. -- Nothing
  186. -- >>> parseFn "/abc" :: Maybe (Path Fn)
  187. -- Nothing
  188. -- >>> parseFn "" :: Maybe (Path Fn)
  189. -- Nothing
  190. -- >>> parseFn "abc/../foo" :: Maybe (Path Fn)
  191. -- Nothing
  192. -- >>> parseFn "." :: Maybe (Path Fn)
  193. -- Nothing
  194. -- >>> parseFn ".." :: Maybe (Path Fn)
  195. -- Nothing
  196. parseFn :: MonadThrow m
  197. => ByteString -> m (Path Fn)
  198. parseFn filepath =
  199. if isFileName filepath &&
  200. filepath /= BS.singleton _period &&
  201. filepath /= BS.pack [_period, _period] &&
  202. isValid filepath
  203. then return (MkPath filepath)
  204. else throwM (InvalidFn filepath)
  205. --------------------------------------------------------------------------------
  206. -- Path Conversion
  207. -- | Convert any Path to a ByteString type.
  208. toFilePath :: Path b -> ByteString
  209. toFilePath (MkPath l) = l
  210. -- | Convert an absolute Path to a ByteString type.
  211. fromAbs :: Path Abs -> ByteString
  212. fromAbs = toFilePath
  213. -- | Convert a relative Path to a ByteString type.
  214. fromRel :: RelC r => Path r -> ByteString
  215. fromRel = toFilePath
  216. -- | This converts the underlying bytestring of the path to an unsafe
  217. -- FilePath by assuming the encoding of the current locale setting. This
  218. -- may be utterly wrong, but isn't particularly worse than what the
  219. -- base library does. Blows up on decoding errors.
  220. --
  221. -- >>> unsafeToString (MkPath "/lal/lad")
  222. -- "/lal/lad"
  223. -- >>> unsafeToString (MkPath "/")
  224. -- "/"
  225. -- >>> unsafeToString (MkPath "lad")
  226. -- "lad"
  227. -- >>> catch (Just <$> unsafeToString (MkPath "�")) (\(_ :: IOException) -> pure Nothing)
  228. -- Nothing
  229. unsafeToString :: Path b -> IO FilePath
  230. unsafeToString (MkPath p) = do
  231. enc <- getLocaleEncoding
  232. unsafeUseAsCStringLen p (peekCStringLen enc)
  233. -- | Same as @unsafeToString@, except requires the encoding
  234. -- to be passed explicitly. This uses 'unsafePerformIO' and
  235. -- returns 'Nothing' on decoding errors.
  236. --
  237. -- >>> unsafeToString' (MkPath "/lal/lad") utf8
  238. -- Just "/lal/lad"
  239. -- >>> unsafeToString' (MkPath "/") utf8
  240. -- Just "/"
  241. -- >>> unsafeToString' (MkPath "lad") utf8
  242. -- Just "lad"
  243. -- >>> unsafeToString' (MkPath "�") utf8
  244. -- Nothing
  245. unsafeToString' :: Path b -> TextEncoding -> Maybe FilePath
  246. unsafeToString' (MkPath !p) enc =
  247. unsafePerformIO $!
  248. catch (Just <$!> unsafeUseAsCStringLen p (peekCStringLen enc))
  249. (\(_ :: IOException) -> pure Nothing)
  250. --------------------------------------------------------------------------------
  251. -- Path Operations
  252. -- | Append two paths.
  253. --
  254. -- The second argument must always be a relative path, which ensures
  255. -- that undefinable things like `"/abc" </> "/def"` cannot happen.
  256. --
  257. -- Technically, the first argument can be a path that points to a non-directory,
  258. -- because this library is IO-agnostic and makes no assumptions about
  259. -- file types.
  260. --
  261. -- >>> (MkPath "/") </> (MkPath "file" :: Path Rel)
  262. -- "/file"
  263. -- >>> (MkPath "/path/to") </> (MkPath "file" :: Path Rel)
  264. -- "/path/to/file"
  265. -- >>> (MkPath "/") </> (MkPath "file/lal" :: Path Rel)
  266. -- "/file/lal"
  267. -- >>> (MkPath "/") </> (MkPath "file/" :: Path Rel)
  268. -- "/file/"
  269. (</>) :: RelC r => Path b -> Path r -> Path b
  270. (</>) (MkPath a) (MkPath b) = MkPath (a' `BS.append` b)
  271. where
  272. a' = if BS.last a == pathSeparator
  273. then a
  274. else addTrailingPathSeparator a
  275. -- | Strip directory from path, making it relative to that directory.
  276. -- Throws 'Couldn'tStripPrefixDir' if directory is not a parent of the path.
  277. --
  278. -- The bases must match.
  279. --
  280. -- >>> (MkPath "/lal/lad") `stripDir` (MkPath "/lal/lad/fad") :: Maybe (Path Rel)
  281. -- Just "fad"
  282. -- >>> (MkPath "lal/lad") `stripDir` (MkPath "lal/lad/fad") :: Maybe (Path Rel)
  283. -- Just "fad"
  284. -- >>> (MkPath "/") `stripDir` (MkPath "/") :: Maybe (Path Rel)
  285. -- Nothing
  286. -- >>> (MkPath "/lal/lad/fad") `stripDir` (MkPath "/lal/lad") :: Maybe (Path Rel)
  287. -- Nothing
  288. -- >>> (MkPath "fad") `stripDir` (MkPath "fad") :: Maybe (Path Rel)
  289. -- Nothing
  290. stripDir :: MonadThrow m
  291. => Path b -> Path b -> m (Path Rel)
  292. stripDir (MkPath p) (MkPath l) =
  293. case stripPrefix p' l of
  294. Nothing -> throwM (Couldn'tStripPrefixTPS p' l)
  295. Just ok -> if BS.null ok
  296. then throwM (Couldn'tStripPrefixTPS p' l)
  297. else return (MkPath ok)
  298. where
  299. p' = addTrailingPathSeparator p
  300. -- | Is p a parent of the given location? Implemented in terms of
  301. -- 'stripDir'. The bases must match.
  302. --
  303. -- >>> (MkPath "/lal/lad") `isParentOf` (MkPath "/lal/lad/fad")
  304. -- True
  305. -- >>> (MkPath "lal/lad") `isParentOf` (MkPath "lal/lad/fad")
  306. -- True
  307. -- >>> (MkPath "/") `isParentOf` (MkPath "/")
  308. -- False
  309. -- >>> (MkPath "/lal/lad/fad") `isParentOf` (MkPath "/lal/lad")
  310. -- False
  311. -- >>> (MkPath "fad") `isParentOf` (MkPath "fad")
  312. -- False
  313. isParentOf :: Path b -> Path b -> Bool
  314. isParentOf p l = isJust (stripDir p l :: Maybe (Path Rel))
  315. -- |Get all parents of a path.
  316. --
  317. -- >>> getAllParents (MkPath "/abs/def/dod")
  318. -- ["/abs/def","/abs","/"]
  319. -- >>> getAllParents (MkPath "/")
  320. -- []
  321. getAllParents :: Path Abs -> [Path Abs]
  322. getAllParents (MkPath p)
  323. | np == BS.singleton pathSeparator = []
  324. | otherwise = dirname (MkPath np) : getAllParents (dirname $ MkPath np)
  325. where
  326. np = dropTrailingPathSeparator . normalise $ p
  327. -- | Extract the directory name of a path.
  328. --
  329. -- >>> dirname (MkPath "/abc/def/dod")
  330. -- "/abc/def"
  331. -- >>> dirname (MkPath "/")
  332. -- "/"
  333. dirname :: Path Abs -> Path Abs
  334. dirname (MkPath fp) = MkPath (takeDirectory $ dropTrailingPathSeparator fp)
  335. -- | Extract the file part of a path.
  336. --
  337. --
  338. -- The following properties hold:
  339. --
  340. -- @basename (p \<\/> a) == basename a@
  341. --
  342. -- Throws: `PathException` if given the root path "/"
  343. --
  344. -- >>> basename (MkPath "/abc/def/dod") :: Maybe (Path Fn)
  345. -- Just "dod"
  346. -- >>> basename (MkPath "/abc/def/dod/") :: Maybe (Path Fn)
  347. -- Just "dod"
  348. -- >>> basename (MkPath "/") :: Maybe (Path Fn)
  349. -- Nothing
  350. basename :: MonadThrow m => Path b -> m (Path Fn)
  351. basename (MkPath l)
  352. | not (isAbsolute rl) = return $ MkPath rl
  353. | otherwise = throwM RootDirHasNoBasename
  354. where
  355. rl = last . splitPath . dropTrailingPathSeparator $ l
  356. --------------------------------------------------------------------------------
  357. -- Path IO helpers
  358. withAbsPath :: Path Abs -> (ByteString -> IO a) -> IO a
  359. withAbsPath (MkPath p) action = action p
  360. withRelPath :: Path Rel -> (ByteString -> IO a) -> IO a
  361. withRelPath (MkPath p) action = action p
  362. withFnPath :: Path Fn -> (ByteString -> IO a) -> IO a
  363. withFnPath (MkPath p) action = action p
  364. ------------------------
  365. -- ByteString helpers
  366. #if MIN_VERSION_bytestring(0,10,8)
  367. #else
  368. stripPrefix :: ByteString -> ByteString -> Maybe ByteString
  369. stripPrefix a b = BS.pack `fmap` L.stripPrefix (BS.unpack a) (BS.unpack b)
  370. #endif