Browse Source

Merge posix-paths into hpath

Julian Ospald 3 years ago
parent
commit
a946387330
No known key found for this signature in database

+ 0
- 3
.gitmodules View File

@@ -1,3 +0,0 @@
1
-[submodule "3rdparty/posix-paths"]
2
-	path = 3rdparty/posix-paths
3
-	url = https://github.com/hasufell/posix-paths.git

+ 0
- 1
3rdparty/posix-paths

@@ -1 +0,0 @@
1
-Subproject commit 5338c03af0a6efeb3914123e9ff085387c3151f9

+ 11
- 2
README.md View File

@@ -1,6 +1,7 @@
1 1
 # HPath
2 2
 
3
-Support for well-typed paths in Haskell.
3
+Support for well-typed paths in Haskell. Also provides ByteString based filepath
4
+manipulation.
4 5
 
5 6
 ## Motivation
6 7
 
@@ -14,9 +15,13 @@ The library that came closest to my needs was
14 15
 but the API turned out to be oddly complicated for my use case, so I
15 16
 decided to fork it.
16 17
 
18
+Similarly, [posix-paths](https://github.com/JohnLato/posix-paths)
19
+was exactly what I wanted for the low-level operations, but upstream seems dead,
20
+so it is forked as well and merged into this library.
21
+
17 22
 ## Differences to 'path'
18 23
 
19
-* doesn't attempt to fake IO-related types into the path, so whether a path points to a file or directory is up to your IO-code to decide... this should be a library that is used _with_ a proper IO File Type
24
+* doesn't attempt to fake IO-related information into the path, so whether a path points to a file or directory is up to your IO-code to decide...
20 25
 * trailing path separators will be preserved if they exist, no messing with that
21 26
 * uses safe ByteString for filepaths under the hood instead of unsafe String
22 27
 * fixes broken [dirname](https://github.com/chrisdone/path/issues/18)
@@ -27,3 +32,7 @@ decided to fork it.
27 32
 * allows `~/` as relative path, because on posix level `~` is just a regular filename that does _NOT_ point to `$HOME`
28 33
 * remove TH, it sucks
29 34
 
35
+## Differences to 'posix-paths'
36
+
37
+* `hasTrailingPathSeparator` behaves in the same way as `System.FilePath`
38
+* `dropTrailingPathSeparator` behaves in the same way as `System.FilePath`

+ 90
- 0
benchmarks/Bench.hs View File

@@ -0,0 +1,90 @@
1
+{-# LANGUAGE ForeignFunctionInterface #-}
2
+{-# LANGUAGE OverloadedStrings #-}
3
+{-# LANGUAGE TupleSections #-}
4
+{-# LANGUAGE ViewPatterns #-}
5
+
6
+{-# OPTIONS_GHC -Wall #-}
7
+import Control.Applicative
8
+import Control.Monad
9
+import System.Directory
10
+import System.FilePath ((</>))
11
+import System.Posix.ByteString.FilePath
12
+import System.Posix.Directory.ByteString as PosixBS
13
+import System.Posix.Directory.Traversals
14
+import qualified System.Posix.FilePath as PosixBS
15
+import System.Posix.Files.ByteString
16
+
17
+import Control.Exception
18
+import qualified Data.ByteString.Char8 as BS
19
+
20
+import System.Environment (getArgs, withArgs)
21
+import System.IO.Error
22
+import System.IO.Unsafe
23
+import System.Process (system)
24
+import Criterion.Main
25
+
26
+
27
+-- | Based on code from 'Real World Haskell', at
28
+-- http://book.realworldhaskell.org/read/io-case-study-a-library-for-searching-the-filesystem.html#id620419
29
+listFilesRecursive :: FilePath -> IO [FilePath]
30
+listFilesRecursive topdir = do
31
+    names <- System.Directory.getDirectoryContents topdir
32
+    let properNames = filter (`notElem` [".", ".."]) names
33
+    paths <- forM properNames $ \name -> do
34
+        let path = topdir </> name
35
+        isDir <- doesDirectoryExist path
36
+        if isDir
37
+            then listFilesRecursive path
38
+            else return [path]
39
+    return (topdir : concat paths)
40
+
41
+----------------------------------------------------------
42
+
43
+getDirectoryContentsBS :: RawFilePath -> IO [RawFilePath]
44
+getDirectoryContentsBS path = 
45
+  modifyIOError ((`ioeSetFileName` (BS.unpack path)) .
46
+                 (`ioeSetLocation` "getDirectoryContentsBS")) $ do
47
+    bracket
48
+      (PosixBS.openDirStream path)
49
+      PosixBS.closeDirStream
50
+      loop
51
+ where
52
+  loop dirp = do
53
+     e <- PosixBS.readDirStream dirp
54
+     if BS.null e then return [] else do
55
+       es <- loop dirp
56
+       return (e:es)
57
+
58
+
59
+-- | similar to 'listFilesRecursive, but uses RawFilePaths
60
+listFilesRecursiveBS :: RawFilePath -> IO [RawFilePath]
61
+listFilesRecursiveBS topdir = do
62
+    names <- getDirectoryContentsBS topdir
63
+    let properNames = filter (`notElem` [".", ".."]) names
64
+    paths <- forM properNames $ \name -> unsafeInterleaveIO $ do
65
+        let path = PosixBS.combine topdir name
66
+        isDir <- isDirectory <$> getFileStatus path
67
+        if isDir
68
+            then listFilesRecursiveBS path
69
+            else return [path]
70
+    return (topdir : concat paths)
71
+----------------------------------------------------------
72
+
73
+
74
+benchTraverse :: RawFilePath -> IO ()
75
+benchTraverse = traverseDirectory (\() p -> BS.putStrLn p) ()
76
+
77
+main :: IO ()
78
+main = do
79
+  args <- getArgs
80
+  let (d,otherArgs) = case args of
81
+          []   -> ("/usr/local",[])
82
+          x:xs -> (x,xs)
83
+  withArgs otherArgs $ defaultMain
84
+    [ bench "traverse (FilePath)"      $ nfIO $ listFilesRecursive d >>= mapM_ putStrLn
85
+    , bench "traverse (RawFilePath)"   $ nfIO $ listFilesRecursiveBS (BS.pack d) >>= mapM_ BS.putStrLn
86
+    , bench "allDirectoryContents"     $ nfIO $ allDirectoryContents (BS.pack d) >>= mapM_ BS.putStrLn
87
+    , bench "allDirectoryContents'"    $ nfIO $ allDirectoryContents' (BS.pack d) >>= mapM_ BS.putStrLn
88
+    , bench "traverseDirectory"        $ nfIO $ benchTraverse (BS.pack d)
89
+    , bench "unix find"                $ nfIO $ void $ system ("find " ++ d)
90
+    ]

+ 7
- 0
cbits/dirutils.c View File

@@ -0,0 +1,7 @@
1
+#include "dirutils.h"
2
+unsigned int
3
+    __posixdir_d_type(struct dirent* d)
4
+    {
5
+      return(d -> d_type);
6
+    }
7
+

+ 13
- 0
cbits/dirutils.h View File

@@ -0,0 +1,13 @@
1
+#ifndef POSIXPATHS_CBITS_DIRUTILS_H
2
+#define POSIXPATHS_CBITS_DIRUTILS_H
3
+
4
+#include <stdlib.h>
5
+#include <dirent.h>
6
+#include <sys/types.h>
7
+#include <sys/stat.h>
8
+#include <fcntl.h>
9
+
10
+extern unsigned int
11
+    __posixdir_d_type(struct dirent* d)
12
+    ;
13
+#endif

doctests.hs → doctests-hpath.hs View File

@@ -2,12 +2,14 @@
2 2
 
3 3
 module Main where
4 4
 
5
+import Control.Applicative
6
+
5 7
 import Test.DocTest
6 8
 import Test.HUnit
7 9
 
8 10
 main =
9 11
     doctest
10
-      [ "-isrc"
12
+      ["-isrc"
11 13
       , "-XOverloadedStrings"
12 14
       , "src/HPath.hs"
13 15
       ]

+ 25
- 0
doctests-posix.hs View File

@@ -0,0 +1,25 @@
1
+{-# LANGUAGE OverloadedStrings #-}
2
+
3
+module Main where
4
+
5
+import Control.Applicative
6
+import System.Posix.Directory.Traversals
7
+
8
+import Test.DocTest
9
+import Test.HUnit
10
+
11
+main = do
12
+    doctest
13
+      [ "-isrc"
14
+      , "-XOverloadedStrings"
15
+      , "System.Posix.FilePath"
16
+      ]
17
+    runTestTT unitTests
18
+
19
+
20
+unitTests :: Test
21
+unitTests = test
22
+    [ TestCase $ do
23
+        r <- (==) <$> allDirectoryContents "." <*> allDirectoryContents' "."
24
+        assertBool "allDirectoryContents == allDirectoryContents'" r
25
+    ]

+ 50
- 11
hpath.cabal View File

@@ -9,34 +9,73 @@ maintainer:          Julian Ospald <hasufell@posteo.de>
9 9
 copyright:           2015–2016 FP Complete, Julian Ospald 2016
10 10
 category:            Filesystem
11 11
 build-type:          Simple
12
-cabal-version:       >=1.8
13
-extra-source-files:  README.md, CHANGELOG
12
+cabal-version:       >=1.14
13
+extra-source-files:  README.md
14
+                     CHANGELOG
15
+                     cbits/dirutils.h
16
+                     doctests.hs
17
+                     benchmarks/*.hs
14 18
 
15 19
 library
16 20
   hs-source-dirs:    src/
21
+  default-language:  Haskell2010
17 22
   ghc-options:       -Wall -O2
18
-  exposed-modules:   HPath, HPath.Internal
19
-  build-depends:     base >= 4 && <5
20
-                   , HUnit
21
-                   , bytestring
23
+  c-sources:         cbits/dirutils.c
24
+  exposed-modules:   HPath,
25
+                     HPath.Internal,
26
+                     System.Posix.Directory.Foreign,
27
+                     System.Posix.Directory.Traversals,
28
+                     System.Posix.FilePath
29
+  build-depends:     base >= 4.2 && <5
30
+                   , bytestring >= 0.9.2.0
22 31
                    , deepseq
23 32
                    , exceptions
24 33
                    , hspec
25
-                   , posix-paths
34
+                   , unix >= 2.5
26 35
                    , utf8-string
27 36
                    , word8
28 37
 
29 38
 
30
-test-suite doctests
39
+test-suite doctests-hpath
40
+  default-language:  Haskell2010
31 41
   type:              exitcode-stdio-1.0
32 42
   ghc-options:       -threaded
33
-  main-is:           doctests.hs
43
+  main-is:           doctests-hpath.hs
34 44
   build-depends:     base
35 45
                    , HUnit
36
-                   , bytestring
46
+                   , QuickCheck
37 47
                    , doctest >= 0.8
38 48
                    , hpath
39
-                   , posix-paths
49
+
50
+test-suite doctests-posix
51
+  default-language:  Haskell2010
52
+  type:              exitcode-stdio-1.0
53
+  ghc-options:       -threaded
54
+  main-is:           doctests-posix.hs
55
+  build-depends:     base,
56
+                     bytestring,
57
+                     unix,
58
+                     hpath,
59
+                     doctest >= 0.8,
60
+                     HUnit,
61
+                     QuickCheck
62
+
63
+benchmark bench.hs
64
+  default-language: Haskell2010
65
+  type: exitcode-stdio-1.0
66
+  hs-source-dirs: benchmarks
67
+  main-is:        Bench.hs
68
+
69
+  build-depends:
70
+      base,
71
+      hpath,
72
+      bytestring,
73
+      unix,
74
+      directory  >= 1.1 && < 1.3,
75
+      filepath   >= 1.2 && < 1.4,
76
+      process    >= 1.0 && < 1.3,
77
+      criterion  >= 0.6 && < 0.9
78
+  ghc-options: -O2
40 79
 
41 80
 source-repository head
42 81
   type:     git

+ 55
- 0
src/System/Posix/Directory/Foreign.hsc View File

@@ -0,0 +1,55 @@
1
+module System.Posix.Directory.Foreign where
2
+
3
+import Data.Bits
4
+import Data.List (foldl')
5
+import Foreign.C.Types
6
+
7
+#include <limits.h>
8
+#include <stdlib.h>
9
+#include <dirent.h>
10
+#include <sys/types.h>
11
+#include <sys/stat.h>
12
+#include <fcntl.h>
13
+
14
+newtype DirType = DirType Int deriving (Eq, Show)
15
+data Flags = Flags Int | UnsupportedFlag String deriving (Eq, Show)
16
+
17
+unFlags :: Flags -> Int
18
+unFlags (Flags i) = i
19
+unFlags (UnsupportedFlag name) = error (name ++ " is not supported on this platform")
20
+
21
+-- |Returns @True@ if posix-paths was compiled with support for the provided
22
+-- flag. (As of this writing, the only flag for which this check may be
23
+-- necessary is 'oCloexec'; all other flags will always yield @True@.)
24
+isSupported :: Flags -> Bool
25
+isSupported (Flags _) = True
26
+isSupported _ = False
27
+
28
+-- |@O_CLOEXEC@ is not supported on every POSIX platform. Use
29
+-- @'isSupported' oCloexec@ to determine if support for @O_CLOEXEC@ was
30
+-- compiled into your version of posix-paths. (If not, using @oCloexec@ will
31
+-- throw an exception.)
32
+oCloexec :: Flags
33
+#ifdef O_CLOEXEC
34
+oCloexec = Flags #{const O_CLOEXEC}
35
+#else
36
+{-# WARNING oCloexec
37
+    "This version of posix-paths was compiled without @O_CLOEXEC@ support." #-}
38
+oCloexec = UnsupportedFlag "O_CLOEXEC"
39
+#endif
40
+
41
+
42
+
43
+-- If these enum declarations occur earlier in the file, haddock
44
+-- gets royally confused about the above doc comments.
45
+-- Probably http://trac.haskell.org/haddock/ticket/138
46
+
47
+#{enum DirType, DirType, DT_BLK, DT_CHR, DT_DIR, DT_FIFO, DT_LNK, DT_REG, DT_SOCK, DT_UNKNOWN}
48
+
49
+#{enum Flags, Flags, O_APPEND, O_ASYNC, O_CREAT, O_DIRECTORY, O_EXCL, O_NOCTTY, O_NOFOLLOW, O_NONBLOCK, O_RDONLY, O_WRONLY, O_RDWR, O_SYNC, O_TRUNC}
50
+
51
+pathMax :: Int
52
+pathMax = #{const PATH_MAX}
53
+
54
+unionFlags :: [Flags] -> CInt
55
+unionFlags = fromIntegral . foldl' ((. unFlags) . (.|.)) 0

+ 269
- 0
src/System/Posix/Directory/Traversals.hs View File

@@ -0,0 +1,269 @@
1
+{-# LANGUAGE ForeignFunctionInterface #-}
2
+{-# LANGUAGE OverloadedStrings #-}
3
+{-# LANGUAGE TupleSections #-}
4
+{-# LANGUAGE ViewPatterns #-}
5
+
6
+{-# OPTIONS_GHC -Wall #-}
7
+module System.Posix.Directory.Traversals (
8
+
9
+  getDirectoryContents
10
+, getDirectoryContents'
11
+
12
+, allDirectoryContents
13
+, allDirectoryContents'
14
+, traverseDirectory
15
+
16
+-- lower-level stuff
17
+, readDirEnt
18
+, packDirStream
19
+, unpackDirStream
20
+, openFd
21
+
22
+, realpath
23
+) where
24
+
25
+import Control.Applicative
26
+import Control.Monad
27
+import System.Posix.FilePath ((</>))
28
+import System.Posix.Directory.Foreign
29
+
30
+import qualified System.Posix as Posix
31
+import System.IO.Error
32
+import Control.Exception
33
+import qualified Data.ByteString.Char8 as BS
34
+import System.Posix.ByteString.FilePath
35
+import System.Posix.Directory.ByteString as PosixBS
36
+import System.Posix.Files.ByteString
37
+
38
+import System.IO.Unsafe
39
+import Unsafe.Coerce (unsafeCoerce)
40
+import Foreign.C.Error
41
+import Foreign.C.String
42
+import Foreign.C.Types
43
+import Foreign.Marshal.Alloc (alloca,allocaBytes)
44
+import Foreign.Ptr
45
+import Foreign.Storable
46
+
47
+
48
+
49
+
50
+----------------------------------------------------------
51
+
52
+-- | Get all files from a directory and its subdirectories.
53
+--
54
+-- Upon entering a directory, 'allDirectoryContents' will get all entries
55
+-- strictly.  However the returned list is lazy in that directories will only
56
+-- be accessed on demand.
57
+allDirectoryContents :: RawFilePath -> IO [RawFilePath]
58
+allDirectoryContents topdir = do
59
+    namesAndTypes <- getDirectoryContents topdir
60
+    let properNames = filter ((`notElem` [".", ".."]) . snd) namesAndTypes
61
+    paths <- forM properNames $ \(typ,name) -> unsafeInterleaveIO $ do
62
+        let path = topdir </> name
63
+        case () of
64
+            () | typ == dtDir -> allDirectoryContents path
65
+               | typ == dtUnknown -> do
66
+                    isDir <- isDirectory <$> getFileStatus path
67
+                    if isDir
68
+                        then allDirectoryContents path
69
+                        else return [path]
70
+               | otherwise -> return [path]
71
+    return (topdir : concat paths)
72
+
73
+-- | Get all files from a directory and its subdirectories strictly.
74
+allDirectoryContents' :: RawFilePath -> IO [RawFilePath]
75
+allDirectoryContents' = fmap reverse . traverseDirectory (\acc fp -> return (fp:acc)) []
76
+-- this uses traverseDirectory because it's more efficient than forcing the
77
+-- lazy version.
78
+
79
+-- | Recursively apply the 'action' to the parent directory and all
80
+-- files/subdirectories.
81
+--
82
+-- This function allows for memory-efficient traversals.
83
+traverseDirectory :: (s -> RawFilePath -> IO s) -> s -> RawFilePath -> IO s
84
+traverseDirectory act s0 topdir = toploop
85
+  where
86
+    toploop = do
87
+        isDir <- isDirectory <$> getFileStatus topdir
88
+        s' <- act s0 topdir
89
+        if isDir then actOnDirContents topdir s' loop
90
+                 else return s'
91
+    loop typ path acc = do
92
+        isDir <- case () of
93
+            () | typ == dtDir     -> return True
94
+               | typ == dtUnknown -> isDirectory <$> getFileStatus path
95
+               | otherwise        -> return False
96
+        if isDir
97
+          then act acc path >>= \acc' -> actOnDirContents path acc' loop
98
+          else act acc path
99
+
100
+actOnDirContents :: RawFilePath
101
+                 -> b
102
+                 -> (DirType -> RawFilePath -> b -> IO b)
103
+                 -> IO b
104
+actOnDirContents pathRelToTop b f =
105
+  modifyIOError ((`ioeSetFileName` (BS.unpack pathRelToTop)) .
106
+                 (`ioeSetLocation` "findBSTypRel")) $ do
107
+    bracket
108
+      (openDirStream pathRelToTop)
109
+      (Posix.closeDirStream)
110
+      (\dirp -> loop dirp b)
111
+ where
112
+  loop dirp b' = do
113
+    (typ,e) <- readDirEnt dirp
114
+    if (e == "")
115
+      then return b'
116
+      else do
117
+          if (e == "." || e == "..")
118
+              then loop dirp b'
119
+              else f typ (pathRelToTop </> e) b' >>= loop dirp
120
+
121
+
122
+----------------------------------------------------------
123
+-- dodgy stuff
124
+
125
+type CDir = ()
126
+type CDirent = ()
127
+
128
+-- Posix doesn't export DirStream, so to re-use that type we need to use
129
+-- unsafeCoerce.  It's just a newtype, so this is a legitimate usage.
130
+-- ugly trick.
131
+unpackDirStream :: DirStream -> Ptr CDir
132
+unpackDirStream = unsafeCoerce
133
+
134
+packDirStream :: Ptr CDir -> DirStream
135
+packDirStream = unsafeCoerce
136
+
137
+-- the __hscore_* functions are defined in the unix package.  We can import them and let
138
+-- the linker figure it out.
139
+foreign import ccall unsafe "__hscore_readdir"
140
+  c_readdir  :: Ptr CDir -> Ptr (Ptr CDirent) -> IO CInt
141
+
142
+foreign import ccall unsafe "__hscore_free_dirent"
143
+  c_freeDirEnt  :: Ptr CDirent -> IO ()
144
+
145
+foreign import ccall unsafe "__hscore_d_name"
146
+  c_name :: Ptr CDirent -> IO CString
147
+
148
+foreign import ccall unsafe "__posixdir_d_type"
149
+  c_type :: Ptr CDirent -> IO DirType
150
+
151
+foreign import ccall "realpath"
152
+  c_realpath :: CString -> CString -> IO CString
153
+
154
+foreign import ccall unsafe "fdopendir"
155
+  c_fdopendir :: Posix.Fd -> IO (Ptr ())
156
+
157
+foreign import ccall unsafe "open"
158
+   c_open :: CString -> CInt -> Posix.CMode -> IO CInt
159
+
160
+----------------------------------------------------------
161
+-- less dodgy but still lower-level
162
+
163
+
164
+readDirEnt :: DirStream -> IO (DirType, RawFilePath)
165
+readDirEnt (unpackDirStream -> dirp) =
166
+  alloca $ \ptr_dEnt  -> loop ptr_dEnt
167
+ where
168
+  loop ptr_dEnt = do
169
+    resetErrno
170
+    r <- c_readdir dirp ptr_dEnt
171
+    if (r == 0)
172
+       then do
173
+         dEnt <- peek ptr_dEnt
174
+         if (dEnt == nullPtr)
175
+            then return (dtUnknown,BS.empty)
176
+            else do
177
+                 dName <- c_name dEnt >>= peekFilePath
178
+                 dType <- c_type dEnt
179
+                 c_freeDirEnt dEnt
180
+                 return (dType, dName)
181
+       else do
182
+         errno <- getErrno
183
+         if (errno == eINTR)
184
+            then loop ptr_dEnt
185
+            else do
186
+                 let (Errno eo) = errno
187
+                 if (eo == 0)
188
+                    then return (dtUnknown,BS.empty)
189
+                    else throwErrno "readDirEnt"
190
+
191
+
192
+getDirectoryContents :: RawFilePath -> IO [(DirType, RawFilePath)]
193
+getDirectoryContents path =
194
+  modifyIOError ((`ioeSetFileName` (BS.unpack path)) .
195
+                 (`ioeSetLocation` "System.Posix.Directory.Traversals.getDirectoryContents")) $ do
196
+    bracket
197
+      (PosixBS.openDirStream path)
198
+      PosixBS.closeDirStream
199
+      loop
200
+ where
201
+  loop dirp = do
202
+     t@(_typ,e) <- readDirEnt dirp
203
+     if BS.null e then return [] else do
204
+       es <- loop dirp
205
+       return (t:es)
206
+
207
+
208
+fdOpendir :: Posix.Fd -> IO DirStream
209
+fdOpendir fd =
210
+    packDirStream <$> throwErrnoIfNull "fdOpendir" (c_fdopendir fd)
211
+
212
+
213
+getDirectoryContents' :: Posix.Fd -> IO [(DirType, RawFilePath)]
214
+getDirectoryContents' fd =
215
+    bracket
216
+      (fdOpendir fd)
217
+      PosixBS.closeDirStream
218
+      loop
219
+ where
220
+  loop dirp = do
221
+     t@(_typ,e) <- readDirEnt dirp
222
+     if BS.null e then return [] else do
223
+       es <- loop dirp
224
+       return (t:es)
225
+
226
+
227
+open_  :: CString
228
+       -> Posix.OpenMode
229
+       -> [Flags]
230
+       -> Maybe Posix.FileMode
231
+       -> IO Posix.Fd
232
+open_ str how optional_flags maybe_mode = do
233
+    fd <- c_open str all_flags mode_w
234
+    return (Posix.Fd fd)
235
+  where
236
+    all_flags  = unionFlags $ optional_flags ++ [open_mode] ++ creat
237
+
238
+
239
+    (creat, mode_w) = case maybe_mode of
240
+                        Nothing -> ([],0)
241
+                        Just x  -> ([oCreat], x)
242
+
243
+    open_mode = case how of
244
+                   Posix.ReadOnly  -> oRdonly
245
+                   Posix.WriteOnly -> oWronly
246
+                   Posix.ReadWrite -> oRdwr
247
+
248
+
249
+-- |Open and optionally create this file.  See 'System.Posix.Files'
250
+-- for information on how to use the 'FileMode' type.
251
+openFd :: RawFilePath
252
+       -> Posix.OpenMode
253
+       -> [Flags]
254
+       -> Maybe Posix.FileMode
255
+       -> IO Posix.Fd
256
+openFd name how optional_flags maybe_mode =
257
+   withFilePath name $ \str ->
258
+     throwErrnoPathIfMinus1Retry "openFd" name $
259
+       open_ str how optional_flags maybe_mode
260
+
261
+
262
+-- | return the canonicalized absolute pathname
263
+--
264
+-- like canonicalizePath, but uses realpath(3)
265
+realpath :: RawFilePath -> IO RawFilePath
266
+realpath inp = do
267
+    allocaBytes pathMax $ \tmp -> do
268
+        void $ BS.useAsCString inp $ \cstr -> throwErrnoIfNull "realpath" $ c_realpath cstr tmp
269
+        BS.packCString tmp

+ 535
- 0
src/System/Posix/FilePath.hs View File

@@ -0,0 +1,535 @@
1
+{-# LANGUAGE TupleSections #-}
2
+
3
+{-# OPTIONS_GHC -Wall #-}
4
+
5
+-- | The equivalent of "System.FilePath" on raw (byte string) file paths.
6
+--
7
+-- Not all functions of "System.FilePath" are implemented yet. Feel free to contribute!
8
+module System.Posix.FilePath (
9
+
10
+  pathSeparator
11
+, isPathSeparator
12
+, searchPathSeparator
13
+, isSearchPathSeparator
14
+, extSeparator
15
+, isExtSeparator
16
+
17
+, splitExtension
18
+, takeExtension
19
+, replaceExtension
20
+, dropExtension
21
+, addExtension
22
+, hasExtension
23
+, (<.>)
24
+, splitExtensions
25
+, dropExtensions
26
+, takeExtensions
27
+
28
+, splitFileName
29
+, takeFileName
30
+, replaceFileName
31
+, dropFileName
32
+, takeBaseName
33
+, replaceBaseName
34
+, takeDirectory
35
+, replaceDirectory
36
+, combine
37
+, (</>)
38
+, splitPath
39
+, joinPath
40
+, normalise
41
+, splitDirectories
42
+
43
+, hasTrailingPathSeparator
44
+, addTrailingPathSeparator
45
+, dropTrailingPathSeparator
46
+
47
+, isRelative
48
+, isAbsolute
49
+, isValid
50
+, equalFilePath
51
+
52
+, module System.Posix.ByteString.FilePath
53
+) where
54
+
55
+import           Data.ByteString (ByteString)
56
+import qualified Data.ByteString as BS
57
+import           System.Posix.ByteString.FilePath
58
+
59
+import           Data.Maybe (isJust)
60
+import           Data.Word8
61
+
62
+import           Control.Arrow (second)
63
+
64
+-- $setup
65
+-- >>> import Data.Char
66
+-- >>> import Test.QuickCheck
67
+-- >>> import Control.Applicative
68
+-- >>> import qualified Data.ByteString as BS
69
+-- >>> import Data.ByteString (ByteString)
70
+-- >>> instance Arbitrary ByteString where arbitrary = BS.pack <$> arbitrary
71
+-- >>> instance CoArbitrary ByteString where coarbitrary = coarbitrary . BS.unpack
72
+--
73
+-- >>> let _chr :: Word8 -> Char; _chr = chr . fromIntegral
74
+
75
+
76
+-- | Path separator character
77
+pathSeparator :: Word8
78
+pathSeparator = _slash
79
+
80
+-- | Check if a character is the path separator
81
+--
82
+-- prop> \n ->  (_chr n == '/') == isPathSeparator n
83
+isPathSeparator :: Word8 -> Bool
84
+isPathSeparator = (== pathSeparator)
85
+
86
+-- | Search path separator
87
+searchPathSeparator :: Word8
88
+searchPathSeparator = _colon
89
+
90
+-- | Check if a character is the search path separator
91
+--
92
+-- prop> \n -> (_chr n == ':') == isSearchPathSeparator n
93
+isSearchPathSeparator :: Word8 -> Bool
94
+isSearchPathSeparator = (== searchPathSeparator)
95
+
96
+-- | File extension separator
97
+extSeparator :: Word8
98
+extSeparator = _period
99
+
100
+-- | Check if a character is the file extension separator
101
+--
102
+-- prop> \n -> (_chr n == '.') == isExtSeparator n
103
+isExtSeparator :: Word8 -> Bool
104
+isExtSeparator = (== extSeparator)
105
+
106
+------------------------
107
+-- extension stuff
108
+
109
+-- | Split a 'RawFilePath' into a path+filename and extension
110
+--
111
+-- >>> splitExtension "file.exe"
112
+-- ("file",".exe")
113
+-- >>> splitExtension "file"
114
+-- ("file","")
115
+-- >>> splitExtension "/path/file.tar.gz"
116
+-- ("/path/file.tar",".gz")
117
+--
118
+-- prop> \path -> uncurry (BS.append) (splitExtension path) == path
119
+splitExtension :: RawFilePath -> (RawFilePath, ByteString)
120
+splitExtension x = if BS.null basename
121
+    then (x,BS.empty)
122
+    else (BS.append path (BS.init basename),BS.cons extSeparator fileExt)
123
+  where
124
+    (path,file) = splitFileNameRaw x
125
+    (basename,fileExt) = BS.breakEnd isExtSeparator file
126
+
127
+-- | Get the final extension from a 'RawFilePath'
128
+--
129
+-- >>> takeExtension "file.exe"
130
+-- ".exe"
131
+-- >>> takeExtension "file"
132
+-- ""
133
+-- >>> takeExtension "/path/file.tar.gz"
134
+-- ".gz"
135
+takeExtension :: RawFilePath -> ByteString
136
+takeExtension = snd . splitExtension
137
+
138
+-- | Change a file's extension
139
+--
140
+-- prop> \path -> let ext = takeExtension path in replaceExtension path ext == path
141
+replaceExtension :: RawFilePath -> ByteString -> RawFilePath
142
+replaceExtension path ext = dropExtension path <.> ext
143
+
144
+-- | Drop the final extension from a 'RawFilePath'
145
+--
146
+-- >>> dropExtension "file.exe"
147
+-- "file"
148
+-- >>> dropExtension "file"
149
+-- "file"
150
+-- >>> dropExtension "/path/file.tar.gz"
151
+-- "/path/file.tar"
152
+dropExtension :: RawFilePath -> RawFilePath
153
+dropExtension = fst . splitExtension
154
+
155
+-- | Add an extension to a 'RawFilePath'
156
+--
157
+-- >>> addExtension "file" ".exe"
158
+-- "file.exe"
159
+-- >>> addExtension "file.tar" ".gz"
160
+-- "file.tar.gz"
161
+-- >>> addExtension "/path/" ".ext"
162
+-- "/path/.ext"
163
+addExtension :: RawFilePath -> ByteString -> RawFilePath
164
+addExtension file ext
165
+    | BS.null ext = file
166
+    | isExtSeparator (BS.head ext) = BS.append file ext
167
+    | otherwise = BS.intercalate (BS.singleton extSeparator) [file, ext]
168
+
169
+
170
+-- | Operator version of 'addExtension'
171
+(<.>) :: RawFilePath -> ByteString -> RawFilePath
172
+(<.>) = addExtension
173
+
174
+-- | Check if a 'RawFilePath' has an extension
175
+--
176
+-- >>> hasExtension "file"
177
+-- False
178
+-- >>> hasExtension "file.tar"
179
+-- True
180
+-- >>> hasExtension "/path.part1/"
181
+-- False
182
+hasExtension :: RawFilePath -> Bool
183
+hasExtension = isJust . BS.elemIndex extSeparator . takeFileName
184
+
185
+-- | Split a 'RawFilePath' on the first extension
186
+--
187
+-- >>> splitExtensions "/path/file.tar.gz"
188
+-- ("/path/file",".tar.gz")
189
+--
190
+-- prop> \path -> uncurry addExtension (splitExtensions path) == path
191
+splitExtensions :: RawFilePath -> (RawFilePath, ByteString)
192
+splitExtensions x = if BS.null basename
193
+    then (path,fileExt)
194
+    else (BS.append path basename,fileExt)
195
+  where
196
+    (path,file) = splitFileNameRaw x
197
+    (basename,fileExt) = BS.break isExtSeparator file
198
+
199
+-- | Remove all extensions from a 'RawFilePath'
200
+--
201
+-- >>> dropExtensions "/path/file.tar.gz"
202
+-- "/path/file"
203
+dropExtensions :: RawFilePath -> RawFilePath
204
+dropExtensions = fst . splitExtensions
205
+
206
+-- | Take all extensions from a 'RawFilePath'
207
+--
208
+-- >>> takeExtensions "/path/file.tar.gz"
209
+-- ".tar.gz"
210
+takeExtensions :: RawFilePath -> ByteString
211
+takeExtensions = snd . splitExtensions
212
+
213
+------------------------
214
+-- more stuff
215
+
216
+-- | Split a 'RawFilePath' into (path,file).  'combine' is the inverse
217
+--
218
+-- >>> splitFileName "path/file.txt"
219
+-- ("path/","file.txt")
220
+-- >>> splitFileName "path/"
221
+-- ("path/","")
222
+-- >>> splitFileName "file.txt"
223
+-- ("./","file.txt")
224
+--
225
+-- prop> \path -> uncurry combine (splitFileName path) == path || fst (splitFileName path) == "./"
226
+splitFileName :: RawFilePath -> (RawFilePath, RawFilePath)
227
+splitFileName x = if BS.null path
228
+    then (dotSlash, file)
229
+    else (path,file)
230
+  where
231
+    (path,file) = splitFileNameRaw x
232
+    dotSlash = _period `BS.cons` (BS.singleton pathSeparator)
233
+
234
+
235
+-- | Get the file name
236
+--
237
+-- >>> takeFileName "path/file.txt"
238
+-- "file.txt"
239
+-- >>> takeFileName "path/"
240
+-- ""
241
+takeFileName :: RawFilePath -> RawFilePath
242
+takeFileName = snd . splitFileName
243
+
244
+-- | Change the file name
245
+--
246
+-- prop> \path -> replaceFileName path (takeFileName path) == path
247
+replaceFileName :: RawFilePath -> ByteString -> RawFilePath
248
+replaceFileName x y = fst (splitFileNameRaw x) </> y
249
+
250
+-- | Drop the file name
251
+--
252
+-- >>> dropFileName "path/file.txt"
253
+-- "path/"
254
+-- >>> dropFileName "file.txt"
255
+-- "./"
256
+dropFileName :: RawFilePath -> RawFilePath
257
+dropFileName = fst . splitFileName
258
+
259
+-- | Get the file name, without a trailing extension
260
+--
261
+-- >>> takeBaseName "path/file.tar.gz"
262
+-- "file.tar"
263
+-- >>> takeBaseName ""
264
+-- ""
265
+takeBaseName :: RawFilePath -> ByteString
266
+takeBaseName = dropExtension . takeFileName
267
+
268
+-- | Change the base name
269
+--
270
+-- >>> replaceBaseName "path/file.tar.gz" "bob"
271
+-- "path/bob.gz"
272
+--
273
+-- prop> \path -> replaceBaseName path (takeBaseName path) == path
274
+replaceBaseName :: RawFilePath -> ByteString -> RawFilePath
275
+replaceBaseName path name = combineRaw dir (name <.> ext)
276
+  where
277
+    (dir,file) = splitFileNameRaw path
278
+    ext = takeExtension file
279
+
280
+-- | Get the directory, moving up one level if it's already a directory
281
+--
282
+-- >>> takeDirectory "path/file.txt"
283
+-- "path"
284
+-- >>> takeDirectory "file"
285
+-- "."
286
+-- >>> takeDirectory "/path/to/"
287
+-- "/path/to"
288
+-- >>> takeDirectory "/path/to"
289
+-- "/path"
290
+takeDirectory :: RawFilePath -> RawFilePath
291
+takeDirectory x = case () of
292
+    () | x == BS.singleton pathSeparator -> x
293
+       | BS.null res && not (BS.null file) -> file
294
+       | otherwise -> res
295
+  where
296
+    res = fst $ BS.spanEnd isPathSeparator file
297
+    file = dropFileName x
298
+
299
+-- | Change the directory component of a 'RawFilePath'
300
+--
301
+-- prop> \path -> replaceDirectory path (takeDirectory path) `equalFilePath` path || takeDirectory path == "."
302
+replaceDirectory :: RawFilePath -> ByteString -> RawFilePath
303
+replaceDirectory file dir = combineRaw dir (takeFileName file)
304
+
305
+-- | Join two paths together
306
+--
307
+-- >>> combine "/" "file"
308
+-- "/file"
309
+-- >>> combine "/path/to" "file"
310
+-- "/path/to/file"
311
+-- >>> combine "file" "/absolute/path"
312
+-- "/absolute/path"
313
+combine :: RawFilePath -> RawFilePath -> RawFilePath
314
+combine a b | not (BS.null b) && isPathSeparator (BS.head b) = b
315
+            | otherwise = combineRaw a b
316
+
317
+-- | Operator version of combine
318
+(</>) :: RawFilePath -> RawFilePath -> RawFilePath
319
+(</>) = combine
320
+
321
+-- | Split a path into a list of components:
322
+--
323
+-- >>> splitPath "/path/to/file.txt"
324
+-- ["/","path/","to/","file.txt"]
325
+--
326
+-- prop> \path -> BS.concat (splitPath path) == path
327
+splitPath :: RawFilePath -> [RawFilePath]
328
+splitPath = splitter
329
+  where
330
+    splitter x
331
+      | BS.null x = []
332
+      | otherwise = case BS.elemIndex pathSeparator x of
333
+            Nothing -> [x]
334
+            Just ix -> case BS.findIndex (not . isPathSeparator) $ BS.drop (ix+1) x of
335
+                          Nothing -> [x]
336
+                          Just runlen -> uncurry (:) . second splitter $ BS.splitAt (ix+1+runlen) x
337
+
338
+-- | Like 'splitPath', but without trailing slashes
339
+--
340
+-- >>> splitDirectories "/path/to/file.txt"
341
+-- ["/","path","to","file.txt"]
342
+-- >>> splitDirectories ""
343
+-- []
344
+splitDirectories :: RawFilePath -> [RawFilePath]
345
+splitDirectories x
346
+    | BS.null x = []
347
+    | isPathSeparator (BS.head x) = let (root,rest) = BS.splitAt 1 x
348
+                                    in root : splitter rest
349
+    | otherwise = splitter x
350
+  where
351
+    splitter = filter (not . BS.null) . BS.split pathSeparator
352
+
353
+-- | Join a split path back together
354
+--
355
+-- prop> \path -> joinPath (splitPath path) == path
356
+--
357
+-- >>> joinPath ["path","to","file.txt"]
358
+-- "path/to/file.txt"
359
+joinPath :: [RawFilePath] -> RawFilePath
360
+joinPath = foldr (</>) BS.empty
361
+
362
+
363
+-- |Normalise a file.
364
+--
365
+-- >>> normalise "/file/\\test////"
366
+-- "/file/\\test/"
367
+-- >>> normalise "/file/./test"
368
+-- "/file/test"
369
+-- >>> normalise "/test/file/../bob/fred/"
370
+-- "/test/file/../bob/fred/"
371
+-- >>> normalise "../bob/fred/"
372
+-- "../bob/fred/"
373
+-- >>> normalise "./bob/fred/"
374
+-- "bob/fred/"
375
+-- >>> normalise "./bob////.fred/./...///./..///#."
376
+-- "bob/.fred/.../../#."
377
+-- >>> normalise "."
378
+-- "."
379
+-- >>> normalise "./"
380
+-- "./"
381
+-- >>> normalise "./."
382
+-- "./"
383
+-- >>> normalise "/./"
384
+-- "/"
385
+-- >>> normalise "/"
386
+-- "/"
387
+-- >>> normalise "bob/fred/."
388
+-- "bob/fred/"
389
+-- >>> normalise "//home"
390
+-- "/home"
391
+normalise :: RawFilePath -> RawFilePath
392
+normalise filepath =
393
+  result `BS.append`
394
+  (if addPathSeparator
395
+       then BS.singleton pathSeparator
396
+       else BS.empty)
397
+  where
398
+    result = let n = f filepath
399
+             in if BS.null n
400
+                then BS.singleton _period
401
+                else n
402
+    addPathSeparator = isDirPath filepath &&
403
+      not (hasTrailingPathSeparator result)
404
+    isDirPath xs = hasTrailingPathSeparator xs
405
+        || not (BS.null xs) && BS.last xs == _period
406
+           && hasTrailingPathSeparator (BS.init xs)
407
+    f = joinPath . dropDots . propSep . splitDirectories
408
+    propSep :: [ByteString] -> [ByteString]
409
+    propSep (x:xs)
410
+      | BS.all (== pathSeparator) x = BS.singleton pathSeparator : xs
411
+      | otherwise                   = x : xs
412
+    propSep [] = []
413
+    dropDots :: [ByteString] -> [ByteString]
414
+    dropDots = filter (BS.singleton _period /=)
415
+
416
+
417
+------------------------
418
+-- trailing path separators
419
+
420
+-- | Check if the last character of a 'RawFilePath' is '/'.
421
+--
422
+-- >>> hasTrailingPathSeparator "/path/"
423
+-- True
424
+-- >>> hasTrailingPathSeparator "/"
425
+-- True
426
+-- >>> hasTrailingPathSeparator "/path"
427
+-- False
428
+hasTrailingPathSeparator :: RawFilePath -> Bool
429
+hasTrailingPathSeparator x
430
+  | BS.null x = False
431
+  | otherwise = isPathSeparator $ BS.last x
432
+
433
+-- | Add a trailing path separator.
434
+--
435
+-- >>> addTrailingPathSeparator "/path"
436
+-- "/path/"
437
+-- >>> addTrailingPathSeparator "/path/"
438
+-- "/path/"
439
+-- >>> addTrailingPathSeparator "/"
440
+-- "/"
441
+addTrailingPathSeparator :: RawFilePath -> RawFilePath
442
+addTrailingPathSeparator x = if hasTrailingPathSeparator x
443
+    then x
444
+    else x `BS.snoc` pathSeparator
445
+
446
+-- | Remove a trailing path separator
447
+--
448
+-- >>> dropTrailingPathSeparator "/path/"
449
+-- "/path"
450
+-- >>> dropTrailingPathSeparator "/path////"
451
+-- "/path"
452
+-- >>> dropTrailingPathSeparator "/"
453
+-- "/"
454
+-- >>> dropTrailingPathSeparator "//"
455
+-- "/"
456
+dropTrailingPathSeparator :: RawFilePath -> RawFilePath
457
+dropTrailingPathSeparator x
458
+  | x == BS.singleton pathSeparator = x
459
+  | otherwise = if hasTrailingPathSeparator x
460
+                  then dropTrailingPathSeparator $ BS.init x
461
+                  else x
462
+
463
+------------------------
464
+-- Filename/system stuff
465
+
466
+-- | Check if a path is absolute
467
+--
468
+-- >>> isAbsolute "/path"
469
+-- True
470
+-- >>> isAbsolute "path"
471
+-- False
472
+-- >>> isAbsolute ""
473
+-- False
474
+isAbsolute :: RawFilePath -> Bool
475
+isAbsolute x
476
+    | BS.length x > 0 = isPathSeparator (BS.head x)
477
+    | otherwise = False
478
+
479
+-- | Check if a path is relative
480
+--
481
+-- prop> \path -> isRelative path /= isAbsolute path
482
+isRelative :: RawFilePath -> Bool
483
+isRelative = not . isAbsolute
484
+
485
+-- | Is a FilePath valid, i.e. could you create a file like it?
486
+--
487
+-- >>> isValid ""
488
+-- False
489
+-- >>> isValid "\0"
490
+-- False
491
+-- >>> isValid "/random_ path:*"
492
+-- True
493
+isValid :: RawFilePath -> Bool
494
+isValid filepath
495
+  | BS.null filepath        = False
496
+  | _nul `BS.elem` filepath = False
497
+  | otherwise               = True
498
+
499
+-- |Equality of two filepaths. The filepaths are normalised
500
+-- and trailing path separators are dropped.
501
+--
502
+-- >>> equalFilePath "foo" "foo"
503
+-- True
504
+-- >>> equalFilePath "foo" "foo/"
505
+-- True
506
+-- >>> equalFilePath "foo" "./foo"
507
+-- True
508
+-- >>> equalFilePath "foo" "/foo"
509
+-- False
510
+-- >>> equalFilePath "foo" "FOO"
511
+-- False
512
+-- >>> equalFilePath "foo" "../foo"
513
+-- False
514
+--
515
+-- prop> \p -> equalFilePath p p
516
+equalFilePath :: RawFilePath -> RawFilePath -> Bool
517
+equalFilePath p1 p2 = f p1 == f p2
518
+  where
519
+    f x = dropTrailingPathSeparator $ normalise x
520
+
521
+------------------------
522
+-- internal stuff
523
+
524
+-- Just split the input FileName without adding/normalizing or changing
525
+-- anything.
526
+splitFileNameRaw :: RawFilePath -> (RawFilePath, RawFilePath)
527
+splitFileNameRaw x = BS.breakEnd isPathSeparator x
528
+
529
+-- | Combine two paths, assuming rhs is NOT absolute.
530
+combineRaw :: RawFilePath -> RawFilePath -> RawFilePath
531
+combineRaw a b | BS.null a = b
532
+                  | BS.null b = a
533
+                  | isPathSeparator (BS.last a) = BS.append a b
534
+                  | otherwise = BS.intercalate (BS.singleton pathSeparator) [a, b]
535
+