Use ByteString for paths instead of String

This commit is contained in:
Julian Ospald 2016-04-04 17:29:35 +02:00
parent c7229061d0
commit 491efe44a3
No known key found for this signature in database
GPG Key ID: 511B62C09D50CD28
3 changed files with 263 additions and 101 deletions

View File

@ -17,9 +17,8 @@ library
ghc-options: -Wall -O2 ghc-options: -Wall -O2
exposed-modules: HPath, HPath.Foreign, HPath.Internal exposed-modules: HPath, HPath.Foreign, HPath.Internal
build-depends: base >= 4 && <5 build-depends: base >= 4 && <5
, bytestring
, exceptions , exceptions
, filepath
, template-haskell
, deepseq , deepseq
test-suite test test-suite test

View File

@ -9,11 +9,13 @@
-- --
-- Support for well-typed paths. -- Support for well-typed paths.
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE EmptyDataDecls #-} {-# LANGUAGE EmptyDataDecls #-}
{-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE ForeignFunctionInterface #-} {-# LANGUAGE ForeignFunctionInterface #-}
{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_HADDOCK ignore-exports #-} {-# OPTIONS_HADDOCK ignore-exports #-}
module HPath module HPath
@ -23,35 +25,55 @@ module HPath
,Path ,Path
,Rel ,Rel
,Fn ,Fn
,PathParseException
-- * PatternSynonyms/ViewPatterns -- * PatternSynonyms/ViewPatterns
,pattern Path ,pattern Path
-- * Parsing -- * Path Parsing
,PathParseException
,parseAbs ,parseAbs
,parseFn ,parseFn
,parseRel ,parseRel
-- * Constructors -- * Path Conversion
,mkAbs ,canonicalizePath
,mkFn ,fromAbs
,mkRel ,fromRel
-- * Operations ,normalize
,toFilePath
-- * Path Operations
,(</>) ,(</>)
,basename ,basename
,dirname ,dirname
,isParentOf ,isParentOf
,getAllParents ,getAllParents
,stripDir ,stripDir
-- * Conversion -- * ByteString/Word8 constants
,canonicalizePath ,nullByte
,fromAbs ,pathDot
,fromRel ,pathDot'
,normalize ,pathSeparator
,toFilePath ,pathSeparator'
-- * ByteString operations
,addTrailingPathSeparator
,combine
,dropFileName
,dropTrailingPathSeparator
,dropWhileEnd
,joinPath
,normalise
,splitDirectories
,splitFileName
,splitPath
,stripPrefix
,takeDirectory
-- * Queries -- * Queries
,hasDot ,hasDot
,hasDoublePS ,hasDoublePS
,hasLeadingPathSeparator
,hasParentDir ,hasParentDir
,hasTrailingPathSeparator
,isAbsolute
,isFileName ,isFileName
,isRelative
,isValid
-- * String based functions -- * String based functions
,realPath ,realPath
) )
@ -60,18 +82,18 @@ module HPath
import Control.Exception (Exception) import Control.Exception (Exception)
import Control.Monad(void) import Control.Monad(void)
import Control.Monad.Catch (MonadThrow(..)) import Control.Monad.Catch (MonadThrow(..))
import Data.ByteString(ByteString)
import qualified Data.ByteString as B
import Data.Char(ord)
import Data.Data import Data.Data
import Data.List import qualified Data.List as L
import Data.Maybe import Data.Maybe
import Data.Word(Word8)
import Foreign.C.Error import Foreign.C.Error
import Foreign.C.String import Foreign.C.String
import GHC.Foreign as GHC
import GHC.IO.Encoding (getFileSystemEncoding)
import Foreign.Marshal.Alloc(allocaBytes) import Foreign.Marshal.Alloc(allocaBytes)
import Language.Haskell.TH
import HPath.Foreign import HPath.Foreign
import HPath.Internal import HPath.Internal
import qualified System.FilePath as FilePath
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- Types -- Types
@ -87,10 +109,10 @@ data Fn deriving (Typeable)
-- | Exception when parsing a location. -- | Exception when parsing a location.
data PathParseException data PathParseException
= InvalidAbs FilePath = InvalidAbs ByteString
| InvalidRel FilePath | InvalidRel ByteString
| InvalidFn FilePath | InvalidFn ByteString
| Couldn'tStripPrefixTPS FilePath FilePath | Couldn'tStripPrefixTPS ByteString ByteString
deriving (Show,Typeable) deriving (Show,Typeable)
instance Exception PathParseException instance Exception PathParseException
@ -103,18 +125,18 @@ instance RelC Fn
pattern Path x <- (MkPath x) pattern Path x <- (MkPath x)
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- Parsers -- Path Parsers
-- | Get a location for an absolute path. -- | Get a location for an absolute path.
-- --
-- Throws: 'PathParseException' -- Throws: 'PathParseException'
-- --
parseAbs :: MonadThrow m parseAbs :: MonadThrow m
=> FilePath -> m (Path Abs) => ByteString -> m (Path Abs)
parseAbs filepath = parseAbs filepath =
if FilePath.isAbsolute filepath && if isAbsolute filepath &&
not (null filepath) && not (B.null filepath) &&
FilePath.isValid filepath isValid filepath
then return (MkPath filepath) then return (MkPath filepath)
else throwM (InvalidAbs filepath) else throwM (InvalidAbs filepath)
@ -127,74 +149,44 @@ parseAbs filepath =
-- Throws: 'PathParseException' -- Throws: 'PathParseException'
-- --
parseRel :: MonadThrow m parseRel :: MonadThrow m
=> FilePath -> m (Path Rel) => ByteString -> m (Path Rel)
parseRel filepath = parseRel filepath =
if not (FilePath.isAbsolute filepath) && if not (isAbsolute filepath) &&
not (null filepath) && not (B.null filepath) &&
FilePath.isValid filepath isValid filepath
then return (MkPath filepath) then return (MkPath filepath)
else throwM (InvalidRel filepath) else throwM (InvalidRel filepath)
parseFn :: MonadThrow m parseFn :: MonadThrow m
=> FilePath -> m (Path Fn) => ByteString -> m (Path Fn)
parseFn filepath = parseFn filepath =
if not (FilePath.isAbsolute filepath) && if not (isAbsolute filepath) &&
not (null filepath) && not (B.null filepath) &&
isFileName filepath && isFileName filepath &&
FilePath.isValid filepath isValid filepath
then return (MkPath filepath) then return (MkPath filepath)
else throwM (InvalidFn filepath) else throwM (InvalidFn filepath)
--------------------------------------------------------------------------------
-- Constructors
-- | Make a 'Path Abs TPS'.
--
-- Remember: due to the nature of absolute paths this (e.g. @\/home\/foo@)
-- may compile on your platform, but it may not compile on another
-- platform (Windows).
mkAbs :: FilePath -> Q Exp
mkAbs s =
case parseAbs s of
Left err -> error (show err)
Right (MkPath str) ->
[|MkPath $(return (LitE (StringL str))) :: Path Abs|]
-- | Make a 'Path Rel TPS'.
mkRel :: FilePath -> Q Exp
mkRel s =
case parseRel s of
Left err -> error (show err)
Right (MkPath str) ->
[|MkPath $(return (LitE (StringL str))) :: Path Rel|]
-- | Make a 'Path Rel TPS'.
mkFn :: FilePath -> Q Exp
mkFn s =
case parseFn s of
Left err -> error (show err)
Right (MkPath str) ->
[|MkPath $(return (LitE (StringL str))) :: Path Fn|]
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- Conversion -- Path Conversion
-- | Convert to a 'FilePath' type. -- | Convert to a ByteString type.
-- --
-- All TPS data types have a trailing slash, so if you want no trailing -- All TPS data types have a trailing slash, so if you want no trailing
-- slash, you can use 'System.FilePath.dropTrailingPathSeparator' from -- slash, you can use 'System.FilePath.dropTrailingPathSeparator' from
-- the filepath package. -- the filepath package.
toFilePath :: Path b -> FilePath toFilePath :: Path b -> ByteString
toFilePath (MkPath l) = l toFilePath (MkPath l) = l
fromAbs :: Path Abs -> FilePath fromAbs :: Path Abs -> ByteString
fromAbs = toFilePath fromAbs = toFilePath
fromRel :: RelC r => Path r -> FilePath fromRel :: RelC r => Path r -> ByteString
fromRel = toFilePath fromRel = toFilePath
normalize :: Path t -> Path t normalize :: Path t -> Path t
normalize (MkPath l) = MkPath $ FilePath.normalise l normalize (MkPath l) = MkPath $ normalise l
-- | May fail on `realPath`. -- | May fail on `realPath`.
canonicalizePath :: Path Abs -> IO (Path Abs) canonicalizePath :: Path Abs -> IO (Path Abs)
@ -203,7 +195,7 @@ canonicalizePath (MkPath l) = do
return $ MkPath nl return $ MkPath nl
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- Operations -- Path Operations
-- | Append two paths. -- | Append two paths.
-- --
@ -214,9 +206,9 @@ canonicalizePath (MkPath l) = do
-- because this library is IO-agnostic and makes no assumptions about -- because this library is IO-agnostic and makes no assumptions about
-- file types. -- file types.
(</>) :: RelC r => Path b -> Path r -> Path b (</>) :: RelC r => Path b -> Path r -> Path b
(</>) (MkPath a) (MkPath b) = MkPath (a' ++ b) (</>) (MkPath a) (MkPath b) = MkPath (a' `B.append` b)
where where
a' = FilePath.addTrailingPathSeparator a a' = addTrailingPathSeparator a
-- | Strip directory from path, making it relative to that directory. -- | Strip directory from path, making it relative to that directory.
-- Throws 'Couldn'tStripPrefixDir' if directory is not a parent of the path. -- Throws 'Couldn'tStripPrefixDir' if directory is not a parent of the path.
@ -231,7 +223,7 @@ stripDir (MkPath p) (MkPath l) =
Just "" -> throwM (Couldn'tStripPrefixTPS p' l) Just "" -> throwM (Couldn'tStripPrefixTPS p' l)
Just ok -> return (MkPath ok) Just ok -> return (MkPath ok)
where where
p' = FilePath.addTrailingPathSeparator p p' = addTrailingPathSeparator p
-- | Is p a parent of the given location? Implemented in terms of -- | Is p a parent of the given location? Implemented in terms of
-- 'stripDir'. The bases must match. -- 'stripDir'. The bases must match.
@ -245,7 +237,7 @@ getAllParents (MkPath p) =
(MkPath "/") -> [] (MkPath "/") -> []
_ -> dirname np : getAllParents (dirname np) _ -> dirname np : getAllParents (dirname np)
where where
np = MkPath . FilePath.dropTrailingPathSeparator . FilePath.normalise $ p np = MkPath . dropTrailingPathSeparator . normalise $ p
-- | Extract the directory name of a path. -- | Extract the directory name of a path.
@ -255,7 +247,7 @@ getAllParents (MkPath p) =
-- @dirname (p \<\/> a) == dirname p@ -- @dirname (p \<\/> a) == dirname p@
-- --
dirname :: Path Abs -> Path Abs dirname :: Path Abs -> Path Abs
dirname (MkPath fp) = MkPath (FilePath.takeDirectory $ FilePath.dropTrailingPathSeparator fp) dirname (MkPath fp) = MkPath (takeDirectory $ dropTrailingPathSeparator fp)
-- | Extract the file part of a path. -- | Extract the file part of a path.
-- --
@ -267,35 +259,206 @@ dirname (MkPath fp) = MkPath (FilePath.takeDirectory $ FilePath.dropTrailingPath
-- Except when "/" is passed in which case the filename "." is returned. -- Except when "/" is passed in which case the filename "." is returned.
basename :: Path b -> Path Fn basename :: Path b -> Path Fn
basename (MkPath l) basename (MkPath l)
| not (FilePath.isAbsolute rl) = MkPath rl | not (isAbsolute rl) = MkPath rl
| otherwise = MkPath "." | otherwise = MkPath "."
where where
rl = last . FilePath.splitPath . FilePath.dropTrailingPathSeparator $ l rl = last . splitPath . dropTrailingPathSeparator $ l
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- Query functions -- ByteString/Word8 constants
pathSeparator :: Word8
pathSeparator = fromIntegral (ord '/')
pathSeparator' :: ByteString
pathSeparator' = "/"
pathDot :: Word8
pathDot = fromIntegral (ord '.')
pathDot' :: ByteString
pathDot' = "."
nullByte :: Word8
nullByte = fromIntegral (ord '\0')
--------------------------------------------------------------------------------
-- ByteString Operations
dropWhileEnd :: (Word8 -> Bool) -> ByteString -> ByteString
dropWhileEnd p = B.reverse . B.dropWhile p . B.reverse
dropTrailingPathSeparator :: ByteString -> ByteString
dropTrailingPathSeparator filepath =
if hasTrailingPathSeparator filepath
then let filepath' = dropWhileEnd (== pathSeparator) filepath
in if B.null filepath' then B.singleton . B.last $ filepath else filepath'
else filepath
addTrailingPathSeparator :: ByteString -> ByteString
addTrailingPathSeparator filepath
| B.null filepath = filepath
| filepath == pathSeparator' = filepath
| not (hasTrailingPathSeparator filepath)
= filepath `B.append` pathSeparator'
| otherwise = filepath
normalise :: ByteString -> ByteString
normalise filepath = result `B.append`
(if addPathSeparator then pathSeparator' else B.empty)
where
(drv, pth) = splitDrive filepath
result = joinDrive' (normaliseDrive drv) (f pth)
joinDrive' d p
| d == "" && p == "" = B.singleton pathDot
| otherwise = joinDrive d p
addPathSeparator = isDirPath pth && not (hasTrailingPathSeparator result)
isDirPath xs = hasTrailingPathSeparator xs
|| not (B.null xs) && B.last xs == pathDot
&& hasTrailingPathSeparator (B.init xs)
normaliseDrive p
| p == "" = ""
| otherwise = B.singleton pathSeparator
f = joinPath . dropDots . propSep . splitDirectories
propSep :: [ByteString] -> [ByteString]
propSep (x:xs)
| B.all (== pathSeparator) x = pathSeparator' : xs
| otherwise = x : xs
propSep [] = []
dropDots = filter (pathDot' /=)
splitPath :: ByteString -> [ByteString]
splitPath filepath = [drv | drv /= ""] ++ f pth
where
(drv, pth) = splitDrive filepath
f p
| p == "" = []
| otherwise = (a `B.append` c) : f d
where
(a, b) = B.break (== pathSeparator) p
(c, d) = splitDrive b
joinPath :: [ByteString] -> ByteString
joinPath = foldr combine ""
splitDrive :: ByteString -> (ByteString, ByteString)
splitDrive = B.span (== pathSeparator)
joinDrive :: ByteString -> ByteString -> ByteString
joinDrive = combineAlways
splitDirectories :: ByteString -> [ByteString]
splitDirectories = map dropTrailingPathSeparator . splitPath
combine :: ByteString -> ByteString -> ByteString
combine d p
| hasLeadingPathSeparator p = p
| otherwise = combineAlways d p
combineAlways :: ByteString -> ByteString -> ByteString
combineAlways d p
| B.null d = p
| B.null p = d
| hasTrailingPathSeparator d = d `B.append` p
| otherwise = d `B.append` B.singleton pathSeparator `B.append` p
takeDirectory :: ByteString -> ByteString
takeDirectory = dropTrailingPathSeparator . dropFileName
dropFileName :: ByteString -> ByteString
dropFileName = fst . splitFileName
splitFileName :: ByteString -> (ByteString, ByteString)
splitFileName filepath = (if B.null dir then "./" else dir, name)
where
(dir, name) = splitFileName_ filepath
splitFileName_ p = (drv `B.append` dir', file)
where
(drv, pth) = splitDrive p
(dir', file) = B.breakEnd (== pathSeparator) pth
stripPrefix :: ByteString -> ByteString -> Maybe ByteString
stripPrefix a b = B.pack `fmap` L.stripPrefix (B.unpack a) (B.unpack b)
--------------------------------------------------------------------------------
-- ByteString Query functions
-- | Helper function: check if the filepath has any parent directories in it. -- | Helper function: check if the filepath has any parent directories in it.
hasParentDir :: FilePath -> Bool hasParentDir :: ByteString -> Bool
hasParentDir filepath = hasParentDir filepath =
("/.." `isSuffixOf` filepath) || ("/.." `B.isSuffixOf` filepath) ||
("/../" `isInfixOf` filepath) || ("/../" `B.isInfixOf` filepath) ||
("../" `isPrefixOf` filepath) ("../" `B.isPrefixOf` filepath)
hasDot :: FilePath -> Bool hasDot :: ByteString -> Bool
hasDot filepath = hasDot filepath =
("/." `isSuffixOf` filepath) || ("/." `B.isSuffixOf` filepath) ||
("/./" `isInfixOf` filepath) || ("/./" `B.isInfixOf` filepath) ||
("./" `isPrefixOf` filepath) ("./" `B.isPrefixOf` filepath)
hasDoublePS :: FilePath -> Bool hasDoublePS :: ByteString -> Bool
hasDoublePS filepath = hasDoublePS filepath =
("//" `isInfixOf` filepath) ("//" `B.isInfixOf` filepath)
isFileName :: FilePath -> Bool
hasTrailingPathSeparator :: ByteString -> Bool
hasTrailingPathSeparator filepath
| B.null filepath = False
| B.last filepath == pathSeparator = True
| otherwise = False
hasLeadingPathSeparator :: ByteString -> Bool
hasLeadingPathSeparator filepath
| B.null filepath = False
| B.head filepath == pathSeparator = True
| otherwise = False
isFileName :: ByteString -> Bool
isFileName filepath = isFileName filepath =
not ("/" `isInfixOf` filepath) not ("/" `B.isInfixOf` filepath)
isAbsolute :: ByteString -> Bool
isAbsolute filepath
| B.null filepath = False
| B.head filepath == pathSeparator = True
| otherwise = False
isRelative :: ByteString -> Bool
isRelative = not . isAbsolute
isValid :: ByteString -> Bool
isValid filepath
| B.null filepath = False
| filepath == "" = False
| nullByte `B.elem` filepath = False
| otherwise = True
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
@ -307,12 +470,11 @@ foreign import ccall "realpath"
-- | return the canonicalized absolute pathname -- | return the canonicalized absolute pathname
-- --
-- like canonicalizePath, but uses realpath(3) -- like canonicalizePath, but uses realpath(3)
realPath :: String -> IO String realPath :: ByteString -> IO ByteString
realPath inp = do realPath inp =
encoding <- getFileSystemEncoding
allocaBytes pathMax $ \tmp -> do allocaBytes pathMax $ \tmp -> do
void $ GHC.withCString encoding inp void $ B.useAsCString inp
$ \cstr -> throwErrnoIfNull "realpath" $ \cstr -> throwErrnoIfNull "realpath"
$ c_realpath cstr tmp $ c_realpath cstr tmp
GHC.peekCString encoding tmp B.packCString tmp

View File

@ -8,6 +8,7 @@ module HPath.Internal
where where
import Control.DeepSeq (NFData (..)) import Control.DeepSeq (NFData (..))
import Data.ByteString (ByteString)
import Data.Data import Data.Data
-- | Path of some base and type. -- | Path of some base and type.
@ -19,7 +20,7 @@ import Data.Data
-- --
-- There are no duplicate -- There are no duplicate
-- path separators @\/\/@, no @..@, no @.\/@, no @~\/@, etc. -- path separators @\/\/@, no @..@, no @.\/@, no @~\/@, etc.
data Path b = MkPath FilePath data Path b = MkPath ByteString
deriving (Typeable) deriving (Typeable)
-- | String equality. -- | String equality.