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.
 
 
 
 

473 lines
13 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 CPP #-}
  12. {-# LANGUAGE DeriveDataTypeable #-}
  13. {-# LANGUAGE EmptyDataDecls #-}
  14. #if __GLASGOW_HASKELL__ >= 708
  15. {-# LANGUAGE PatternSynonyms #-}
  16. #endif
  17. {-# LANGUAGE QuasiQuotes #-}
  18. {-# LANGUAGE TemplateHaskell #-}
  19. module HPath
  20. (
  21. -- * Types
  22. Abs
  23. ,Path
  24. ,Rel
  25. ,PathParseException
  26. ,PathException
  27. #if __GLASGOW_HASKELL__ >= 708
  28. -- * PatternSynonyms/ViewPatterns
  29. ,pattern Path
  30. #endif
  31. -- * Path Construction
  32. ,parseAbs
  33. ,parseRel
  34. ,parseAny
  35. ,rootPath
  36. -- * Path Conversion
  37. ,fromAbs
  38. ,fromRel
  39. ,toFilePath
  40. ,fromAny
  41. -- * Path Operations
  42. ,(</>)
  43. ,basename
  44. ,dirname
  45. ,getAllParents
  46. ,getAllComponents
  47. ,getAllComponentsAfterRoot
  48. ,stripDir
  49. -- * Path Examination
  50. ,isParentOf
  51. ,isRootPath
  52. -- * Path IO helpers
  53. ,withAbsPath
  54. ,withRelPath
  55. -- * Quasiquoters
  56. ,abs
  57. ,rel
  58. )
  59. where
  60. import Control.Exception (Exception)
  61. import Control.Monad.Catch (MonadThrow(..))
  62. #if MIN_VERSION_bytestring(0,10,8)
  63. import Data.ByteString(ByteString, stripPrefix)
  64. #else
  65. import Data.ByteString(ByteString)
  66. import qualified Data.List as L
  67. #endif
  68. import qualified Data.ByteString as BS
  69. import Data.ByteString.UTF8
  70. import Data.Data
  71. import Data.Maybe
  72. import Data.Word8
  73. import HPath.Internal
  74. import Language.Haskell.TH
  75. import Language.Haskell.TH.Syntax (Exp(..), Lift(..), lift)
  76. import Language.Haskell.TH.Quote (QuasiQuoter(..))
  77. import Prelude hiding (abs, any)
  78. import System.Posix.FilePath hiding ((</>))
  79. --------------------------------------------------------------------------------
  80. -- Types
  81. -- | An absolute path.
  82. data Abs deriving (Typeable)
  83. -- | A relative path; one without a root.
  84. data Rel deriving (Typeable)
  85. -- | Exception when parsing a location.
  86. data PathParseException
  87. = InvalidAbs ByteString
  88. | InvalidRel ByteString
  89. | Couldn'tStripPrefixTPS ByteString ByteString
  90. deriving (Show,Typeable)
  91. instance Exception PathParseException
  92. data PathException = RootDirHasNoBasename
  93. deriving (Show,Typeable)
  94. instance Exception PathException
  95. --------------------------------------------------------------------------------
  96. -- PatternSynonyms
  97. #if __GLASGOW_HASKELL__ >= 710
  98. pattern Path :: ByteString -> Path a
  99. #endif
  100. #if __GLASGOW_HASKELL__ >= 708
  101. pattern Path x <- (MkPath x)
  102. #endif
  103. --------------------------------------------------------------------------------
  104. -- Path Parsers
  105. -- | Get a location for an absolute path. Produces a normalised path.
  106. --
  107. -- Throws: 'PathParseException'
  108. --
  109. -- >>> parseAbs "/abc" :: Maybe (Path Abs)
  110. -- Just "/abc"
  111. -- >>> parseAbs "/" :: Maybe (Path Abs)
  112. -- Just "/"
  113. -- >>> parseAbs "/abc/def" :: Maybe (Path Abs)
  114. -- Just "/abc/def"
  115. -- >>> parseAbs "/abc/def/.///" :: Maybe (Path Abs)
  116. -- Just "/abc/def"
  117. -- >>> parseAbs "abc" :: Maybe (Path Abs)
  118. -- Nothing
  119. -- >>> parseAbs "" :: Maybe (Path Abs)
  120. -- Nothing
  121. -- >>> parseAbs "/abc/../foo" :: Maybe (Path Abs)
  122. -- Nothing
  123. parseAbs :: MonadThrow m
  124. => ByteString -> m (Path Abs)
  125. parseAbs filepath =
  126. if isAbsolute filepath &&
  127. isValid filepath &&
  128. not (hasParentDir filepath)
  129. then return (MkPath . dropTrailingPathSeparator . normalise $ filepath)
  130. else throwM (InvalidAbs filepath)
  131. -- | Get a location for a relative path. Produces a normalised
  132. -- path.
  133. --
  134. -- Note that @filepath@ may contain any number of @./@ but may not consist
  135. -- solely of @./@. It also may not contain a single @..@ anywhere.
  136. --
  137. -- Throws: 'PathParseException'
  138. --
  139. -- >>> parseRel "abc" :: Maybe (Path Rel)
  140. -- Just "abc"
  141. -- >>> parseRel "def/" :: Maybe (Path Rel)
  142. -- Just "def"
  143. -- >>> parseRel "abc/def" :: Maybe (Path Rel)
  144. -- Just "abc/def"
  145. -- >>> parseRel "abc/def/." :: Maybe (Path Rel)
  146. -- Just "abc/def"
  147. -- >>> parseRel "/abc" :: Maybe (Path Rel)
  148. -- Nothing
  149. -- >>> parseRel "" :: Maybe (Path Rel)
  150. -- Nothing
  151. -- >>> parseRel "abc/../foo" :: Maybe (Path Rel)
  152. -- Nothing
  153. -- >>> parseRel "." :: Maybe (Path Rel)
  154. -- Nothing
  155. -- >>> parseRel ".." :: Maybe (Path Rel)
  156. -- Nothing
  157. parseRel :: MonadThrow m
  158. => ByteString -> m (Path Rel)
  159. parseRel filepath =
  160. if not (isAbsolute filepath) &&
  161. filepath /= BS.singleton _period &&
  162. filepath /= BS.pack [_period, _period] &&
  163. not (hasParentDir filepath) &&
  164. isValid filepath
  165. then return (MkPath . dropTrailingPathSeparator . normalise $ filepath)
  166. else throwM (InvalidRel filepath)
  167. -- | Parses a path, whether it's relative or absolute. Will lose
  168. -- information on whether it's relative or absolute. If you need to know,
  169. -- reparse it.
  170. --
  171. -- Filenames must not contain slashes.
  172. -- Excludes '.' and '..'.
  173. --
  174. -- Throws: 'PathParseException'
  175. --
  176. -- >>> parseAny "/abc" :: Maybe (Either (Path Abs) (Path Rel))
  177. -- Just (Left "/abc")
  178. -- >>> parseAny "..." :: Maybe (Either (Path Abs) (Path Rel))
  179. -- Just (Right "...")
  180. -- >>> parseAny "abc/def" :: Maybe (Either (Path Abs) (Path Rel))
  181. -- Just (Right "abc/def")
  182. -- >>> parseAny "abc/def/." :: Maybe (Either (Path Abs) (Path Rel))
  183. -- Just (Right "abc/def")
  184. -- >>> parseAny "/abc" :: Maybe (Either (Path Abs) (Path Rel))
  185. -- Just (Left "/abc")
  186. -- >>> parseAny "" :: Maybe (Either (Path Abs) (Path Rel))
  187. -- Nothing
  188. -- >>> parseAny "abc/../foo" :: Maybe (Either (Path Abs) (Path Rel))
  189. -- Nothing
  190. -- >>> parseAny "." :: Maybe (Either (Path Abs) (Path Rel))
  191. -- Nothing
  192. -- >>> parseAny ".." :: Maybe (Either (Path Abs) (Path Rel))
  193. -- Nothing
  194. parseAny :: MonadThrow m => ByteString -> m (Either (Path Abs) (Path Rel))
  195. parseAny filepath = case parseAbs filepath of
  196. Just p -> pure $ Left p
  197. Nothing -> case parseRel filepath of
  198. Just p -> pure $ Right p
  199. Nothing -> throwM (InvalidRel filepath)
  200. rootPath :: Path Abs
  201. rootPath = (MkPath (BS.singleton _slash))
  202. --------------------------------------------------------------------------------
  203. -- Path Conversion
  204. -- | Convert any Path to a ByteString type.
  205. toFilePath :: Path b -> ByteString
  206. toFilePath (MkPath l) = l
  207. -- | Convert an absolute Path to a ByteString type.
  208. fromAbs :: Path Abs -> ByteString
  209. fromAbs = toFilePath
  210. -- | Convert a relative Path to a ByteString type.
  211. fromRel :: Path Rel -> ByteString
  212. fromRel = toFilePath
  213. fromAny :: Either (Path Abs) (Path Rel) -> ByteString
  214. fromAny = either toFilePath toFilePath
  215. --------------------------------------------------------------------------------
  216. -- Path Operations
  217. -- | Append two paths.
  218. --
  219. -- The second argument must always be a relative path, which ensures
  220. -- that undefinable things like `"/abc" </> "/def"` cannot happen.
  221. --
  222. -- Technically, the first argument can be a path that points to a non-directory,
  223. -- because this library is IO-agnostic and makes no assumptions about
  224. -- file types.
  225. --
  226. -- >>> (MkPath "/") </> (MkPath "file" :: Path Rel)
  227. -- "/file"
  228. -- >>> (MkPath "/path/to") </> (MkPath "file" :: Path Rel)
  229. -- "/path/to/file"
  230. -- >>> (MkPath "/") </> (MkPath "file/lal" :: Path Rel)
  231. -- "/file/lal"
  232. -- >>> (MkPath "/") </> (MkPath "file" :: Path Rel)
  233. -- "/file"
  234. (</>) :: Path b -> Path Rel -> Path b
  235. (</>) (MkPath a) (MkPath b) = MkPath (a' `BS.append` b)
  236. where
  237. a' = if hasTrailingPathSeparator a
  238. then a
  239. else addTrailingPathSeparator a
  240. -- | Strip directory from path, making it relative to that directory.
  241. -- Throws 'Couldn'tStripPrefixDir' if directory is not a parent of the path.
  242. --
  243. -- The bases must match.
  244. --
  245. -- >>> (MkPath "/lal/lad") `stripDir` (MkPath "/lal/lad/fad") :: Maybe (Path Rel)
  246. -- Just "fad"
  247. -- >>> (MkPath "lal/lad") `stripDir` (MkPath "lal/lad/fad") :: Maybe (Path Rel)
  248. -- Just "fad"
  249. -- >>> (MkPath "/") `stripDir` (MkPath "/") :: Maybe (Path Rel)
  250. -- Nothing
  251. -- >>> (MkPath "/lal/lad/fad") `stripDir` (MkPath "/lal/lad") :: Maybe (Path Rel)
  252. -- Nothing
  253. -- >>> (MkPath "fad") `stripDir` (MkPath "fad") :: Maybe (Path Rel)
  254. -- Nothing
  255. stripDir :: MonadThrow m
  256. => Path b -> Path b -> m (Path Rel)
  257. stripDir (MkPath p) (MkPath l) =
  258. case stripPrefix p' l of
  259. Nothing -> throwM (Couldn'tStripPrefixTPS p' l)
  260. Just ok -> if BS.null ok
  261. then throwM (Couldn'tStripPrefixTPS p' l)
  262. else return (MkPath ok)
  263. where
  264. p' = addTrailingPathSeparator p
  265. -- |Get all parents of a path.
  266. --
  267. -- >>> getAllParents (MkPath "/abs/def/dod")
  268. -- ["/abs/def","/abs","/"]
  269. -- >>> getAllParents (MkPath "/foo")
  270. -- ["/"]
  271. -- >>> getAllParents (MkPath "/")
  272. -- []
  273. getAllParents :: Path Abs -> [Path Abs]
  274. getAllParents (MkPath p)
  275. | np == BS.singleton pathSeparator = []
  276. | otherwise = dirname (MkPath np) : getAllParents (dirname $ MkPath np)
  277. where
  278. np = normalise p
  279. -- | Gets all path components.
  280. --
  281. -- >>> getAllComponents (MkPath "abs/def/dod")
  282. -- ["abs","def","dod"]
  283. -- >>> getAllComponents (MkPath "abs")
  284. -- ["abs"]
  285. getAllComponents :: Path Rel -> [Path Rel]
  286. getAllComponents (MkPath p) = fmap MkPath . splitDirectories $ p
  287. -- | Gets all path components after the "/" root directory.
  288. --
  289. -- >>> getAllComponentsAfterRoot (MkPath "/abs/def/dod")
  290. -- ["abs","def","dod"]
  291. -- >>> getAllComponentsAfterRoot (MkPath "/abs")
  292. -- ["abs"]
  293. getAllComponentsAfterRoot :: Path Abs -> [Path Rel]
  294. getAllComponentsAfterRoot p = getAllComponents (fromJust $ stripDir rootPath p)
  295. -- | Extract the directory name of a path.
  296. --
  297. -- >>> dirname (MkPath "/abc/def/dod")
  298. -- "/abc/def"
  299. -- >>> dirname (MkPath "/")
  300. -- "/"
  301. dirname :: Path Abs -> Path Abs
  302. dirname (MkPath fp) = MkPath (takeDirectory fp)
  303. -- | Extract the file part of a path.
  304. --
  305. --
  306. -- The following properties hold:
  307. --
  308. -- @basename (p \<\/> a) == basename a@
  309. --
  310. -- Throws: `PathException` if given the root path "/"
  311. --
  312. -- >>> basename (MkPath "/abc/def/dod") :: Maybe (Path Rel)
  313. -- Just "dod"
  314. -- >>> basename (MkPath "abc/def/dod") :: Maybe (Path Rel)
  315. -- Just "dod"
  316. -- >>> basename (MkPath "dod") :: Maybe (Path Rel)
  317. -- Just "dod"
  318. -- >>> basename (MkPath "/") :: Maybe (Path Rel)
  319. -- Nothing
  320. basename :: MonadThrow m => Path b -> m (Path Rel)
  321. basename (MkPath l)
  322. | not (isAbsolute rl) = return $ MkPath rl
  323. | otherwise = throwM RootDirHasNoBasename
  324. where
  325. rl = last . splitPath $ l
  326. --------------------------------------------------------------------------------
  327. -- Path Examination
  328. -- | Is p a parent of the given location? Implemented in terms of
  329. -- 'stripDir'. The bases must match.
  330. --
  331. -- >>> (MkPath "/lal/lad") `isParentOf` (MkPath "/lal/lad/fad")
  332. -- True
  333. -- >>> (MkPath "lal/lad") `isParentOf` (MkPath "lal/lad/fad")
  334. -- True
  335. -- >>> (MkPath "/") `isParentOf` (MkPath "/")
  336. -- False
  337. -- >>> (MkPath "/lal/lad/fad") `isParentOf` (MkPath "/lal/lad")
  338. -- False
  339. -- >>> (MkPath "fad") `isParentOf` (MkPath "fad")
  340. -- False
  341. isParentOf :: Path b -> Path b -> Bool
  342. isParentOf p l = isJust (stripDir p l :: Maybe (Path Rel))
  343. -- | Check whether the given Path is the root "/" path.
  344. --
  345. -- >>> isRootPath (MkPath "/lal/lad")
  346. -- False
  347. -- >>> isRootPath (MkPath "/")
  348. -- True
  349. isRootPath :: Path Abs -> Bool
  350. isRootPath = (== rootPath)
  351. --------------------------------------------------------------------------------
  352. -- Path IO helpers
  353. withAbsPath :: Path Abs -> (ByteString -> IO a) -> IO a
  354. withAbsPath (MkPath p) action = action p
  355. withRelPath :: Path Rel -> (ByteString -> IO a) -> IO a
  356. withRelPath (MkPath p) action = action p
  357. ------------------------
  358. -- ByteString helpers
  359. #if MIN_VERSION_bytestring(0,10,8)
  360. #else
  361. stripPrefix :: ByteString -> ByteString -> Maybe ByteString
  362. stripPrefix a b = BS.pack `fmap` L.stripPrefix (BS.unpack a) (BS.unpack b)
  363. #endif
  364. ------------------------
  365. -- QuasiQuoters
  366. instance Lift (Path a) where
  367. lift (MkPath bs) = AppE <$> [| MkPath . BS.pack |] <*> lift (BS.unpack bs)
  368. qq :: (ByteString -> Q Exp) -> QuasiQuoter
  369. qq quoteExp' =
  370. QuasiQuoter
  371. { quoteExp = (\s -> quoteExp' . fromString $ s)
  372. , quotePat = \_ ->
  373. fail "illegal QuasiQuote (allowed as expression only, used as a pattern)"
  374. , quoteType = \_ ->
  375. fail "illegal QuasiQuote (allowed as expression only, used as a type)"
  376. , quoteDec = \_ ->
  377. fail "illegal QuasiQuote (allowed as expression only, used as a declaration)"
  378. }
  379. mkAbs :: ByteString -> Q Exp
  380. mkAbs = either (error . show) lift . parseAbs
  381. mkRel :: ByteString -> Q Exp
  382. mkRel = either (error . show) lift . parseRel
  383. -- | Quasiquote an absolute Path. This accepts Unicode Chars and will encode as UTF-8.
  384. --
  385. -- >>> [abs|/etc/profile|] :: Path Abs
  386. -- "/etc/profile"
  387. -- >>> [abs|/|] :: Path Abs
  388. -- "/"
  389. -- >>> [abs|/|] :: Path Abs
  390. -- "/\239\131\144"
  391. abs :: QuasiQuoter
  392. abs = qq mkAbs
  393. -- | Quasiquote a relative Path. This accepts Unicode Chars and will encode as UTF-8.
  394. --
  395. -- >>> [rel|etc|] :: Path Rel
  396. -- "etc"
  397. -- >>> [rel|bar/baz|] :: Path Rel
  398. -- "bar/baz"
  399. -- >>> [rel||] :: Path Rel
  400. -- "\239\131\144"
  401. rel :: QuasiQuoter
  402. rel = qq mkRel