Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cfe626b6d4 | |||
| a946387330 | |||
| 1263fac7ec | |||
| 45b515d1db | |||
| a5360f29a3 | |||
| f1875da780 | |||
| 7e8c745e35 | |||
| 577ecf6750 | |||
| 8f7e5806e3 | |||
| c570505297 | |||
| 148eeb619f | |||
| 877d8c4089 | |||
| 8c1bd139c0 | |||
| bd71947b2a | |||
| 491efe44a3 | |||
|
|
c7229061d0 | ||
| 3a52a9ea4b | |||
| 3c3a2d2766 |
82
README.md
82
README.md
@@ -1,6 +1,7 @@
|
||||
# HPath
|
||||
|
||||
Support for well-typed paths in Haskell.
|
||||
Support for well-typed paths in Haskell. Also provides ByteString based filepath
|
||||
manipulation.
|
||||
|
||||
## Motivation
|
||||
|
||||
@@ -14,67 +15,28 @@ The library that came closest to my needs was
|
||||
but the API turned out to be oddly complicated for my use case, so I
|
||||
decided to fork it.
|
||||
|
||||
Similarly, [posix-paths](https://github.com/JohnLato/posix-paths)
|
||||
was exactly what I wanted for the low-level operations, but upstream seems dead,
|
||||
so it is forked as well and merged into this library.
|
||||
|
||||
## Differences to 'path'
|
||||
|
||||
I wasn't happy with the way it dealt with Dir vs File things. In his
|
||||
version of the library, a `Path b Dir` always ends with a trailing
|
||||
path separator and `Path b File` never ends with a trailing path separator.
|
||||
* 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...
|
||||
* trailing path separators will be preserved if they exist, no messing with that
|
||||
* uses safe ByteString for filepaths under the hood instead of unsafe String
|
||||
* fixes broken [dirname](https://github.com/chrisdone/path/issues/18)
|
||||
* renames dirname/filename to basename/dirname to match the POSIX shell functions
|
||||
* introduces a new `Path Fn` for safe filename guarantees and a `RelC` class
|
||||
* allows pattern matching via unidirectional PatternSynonym
|
||||
* uses simple doctest for testing
|
||||
* allows `~/` as relative path, because on posix level `~` is just a regular filename that does _NOT_ point to `$HOME`
|
||||
* remove TH, it sucks
|
||||
|
||||
IMO, it is nonsensical to make a Dir vs File distinction on path level,
|
||||
although it first seems nice.
|
||||
Some of the reasons are:
|
||||
* a path is just that: a path. It is completely disconnected from IO level
|
||||
and even if a `Dir`/`File` type theoretically allows us to say "this path
|
||||
ought to point to a file", there is literally zero guarantee that it will
|
||||
hold true at runtime. So this basically gives a false feeling of a
|
||||
type-safe file distinction.
|
||||
* it's imprecise about Dir vs File distinction, which makes it even worse,
|
||||
because a directory is also a file (just not a regular file). Add symlinks
|
||||
to that and the confusion is complete.
|
||||
* it makes the API oddly complicated for use cases where we basically don't
|
||||
care (yet) whether something turns out to be a directory or not
|
||||
## Differences to 'posix-paths'
|
||||
|
||||
Still, it comes also with a few perks:
|
||||
* it simplifies some functions, because they now have guarantees whether a
|
||||
path ends in a trailing path separator or not
|
||||
* it may be safer for interaction with other library functions, which behave
|
||||
differently depending on a trailing path separator (like probably shelly)
|
||||
|
||||
Not limited to, but also in order to fix my remarks without breaking any
|
||||
benefits, I did:
|
||||
* rename the `Dir`/`File` types to `TPS`/`NoTPS`, so it's clear we are only
|
||||
giving information about trailing path separators and not actual file
|
||||
types we don't know about yet
|
||||
* add a `MaybeTPS` type, which does not mess with trailing path separators
|
||||
and also gives no guarantees about them... then added `toNoTPS` and
|
||||
`toTPS` to allow type-safe conversion
|
||||
* make some functions accept more general types, so we don't unnecessarily
|
||||
force paths with trailing separators for `(</>)` for example... instead
|
||||
these functions now examine the paths to still have correct behavior.
|
||||
This is really minor overhead. You might say now "but then I can append
|
||||
filepath to filepath". Well, as I said... we don't know whether it's a
|
||||
"filepath" at all.
|
||||
* merge `filename` and `dirname` into `basename` and make `parent` be
|
||||
`dirname`, so the function names match the name of the POSIX ones,
|
||||
which do (almost) the same...
|
||||
* fix a bug in `basename` (formerly `dirname`) which broke the type
|
||||
guarantees
|
||||
* add a pattern synonym for easier pattern matching without exporting
|
||||
the internal Path constructor
|
||||
|
||||
## Consequences
|
||||
|
||||
So what does that mean? Well, it means that this library does not and
|
||||
cannot make any guarantees about what a filepath is meant for or what
|
||||
it might point to. And it doesn't pretend it can.
|
||||
|
||||
So when you strip the trailing path separator of a path that points to a
|
||||
directory and then shove it into some function which expects a regular
|
||||
file... then that function will very likely blow up. That's the nature of IO.
|
||||
There is no type that can save you from interfacing such low-level libraries.
|
||||
The filesystem is in constant change. What might have been a regular file
|
||||
2 seconds ago, can now be a directory or a symlink.
|
||||
That means you need a proper File type that is tied to your IO code.
|
||||
This is what [hsfm](https://github.com/hasufell/hsfm) does. It currently
|
||||
is not a library, maybe it will be in the future.
|
||||
* `hasTrailingPathSeparator` behaves in the same way as `System.FilePath`
|
||||
* `dropTrailingPathSeparator` behaves in the same way as `System.FilePath`
|
||||
* added various functions like `isValid`, `normalise` and `equalFilePath`
|
||||
* uses the `word8` package for save word8 literals instead of `OverloadedStrings`
|
||||
* has custom versions of `openFd` and `getDirectoryContents`
|
||||
|
||||
|
||||
90
benchmarks/Bench.hs
Normal file
90
benchmarks/Bench.hs
Normal file
@@ -0,0 +1,90 @@
|
||||
{-# LANGUAGE ForeignFunctionInterface #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
{-# LANGUAGE ViewPatterns #-}
|
||||
|
||||
{-# OPTIONS_GHC -Wall #-}
|
||||
import Control.Applicative
|
||||
import Control.Monad
|
||||
import System.Directory
|
||||
import System.FilePath ((</>))
|
||||
import System.Posix.ByteString.FilePath
|
||||
import System.Posix.Directory.ByteString as PosixBS
|
||||
import System.Posix.Directory.Traversals
|
||||
import qualified System.Posix.FilePath as PosixBS
|
||||
import System.Posix.Files.ByteString
|
||||
|
||||
import Control.Exception
|
||||
import qualified Data.ByteString.Char8 as BS
|
||||
|
||||
import System.Environment (getArgs, withArgs)
|
||||
import System.IO.Error
|
||||
import System.IO.Unsafe
|
||||
import System.Process (system)
|
||||
import Criterion.Main
|
||||
|
||||
|
||||
-- | Based on code from 'Real World Haskell', at
|
||||
-- http://book.realworldhaskell.org/read/io-case-study-a-library-for-searching-the-filesystem.html#id620419
|
||||
listFilesRecursive :: FilePath -> IO [FilePath]
|
||||
listFilesRecursive topdir = do
|
||||
names <- System.Directory.getDirectoryContents topdir
|
||||
let properNames = filter (`notElem` [".", ".."]) names
|
||||
paths <- forM properNames $ \name -> do
|
||||
let path = topdir </> name
|
||||
isDir <- doesDirectoryExist path
|
||||
if isDir
|
||||
then listFilesRecursive path
|
||||
else return [path]
|
||||
return (topdir : concat paths)
|
||||
|
||||
----------------------------------------------------------
|
||||
|
||||
getDirectoryContentsBS :: RawFilePath -> IO [RawFilePath]
|
||||
getDirectoryContentsBS path =
|
||||
modifyIOError ((`ioeSetFileName` (BS.unpack path)) .
|
||||
(`ioeSetLocation` "getDirectoryContentsBS")) $ do
|
||||
bracket
|
||||
(PosixBS.openDirStream path)
|
||||
PosixBS.closeDirStream
|
||||
loop
|
||||
where
|
||||
loop dirp = do
|
||||
e <- PosixBS.readDirStream dirp
|
||||
if BS.null e then return [] else do
|
||||
es <- loop dirp
|
||||
return (e:es)
|
||||
|
||||
|
||||
-- | similar to 'listFilesRecursive, but uses RawFilePaths
|
||||
listFilesRecursiveBS :: RawFilePath -> IO [RawFilePath]
|
||||
listFilesRecursiveBS topdir = do
|
||||
names <- getDirectoryContentsBS topdir
|
||||
let properNames = filter (`notElem` [".", ".."]) names
|
||||
paths <- forM properNames $ \name -> unsafeInterleaveIO $ do
|
||||
let path = PosixBS.combine topdir name
|
||||
isDir <- isDirectory <$> getFileStatus path
|
||||
if isDir
|
||||
then listFilesRecursiveBS path
|
||||
else return [path]
|
||||
return (topdir : concat paths)
|
||||
----------------------------------------------------------
|
||||
|
||||
|
||||
benchTraverse :: RawFilePath -> IO ()
|
||||
benchTraverse = traverseDirectory (\() p -> BS.putStrLn p) ()
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
args <- getArgs
|
||||
let (d,otherArgs) = case args of
|
||||
[] -> ("/usr/local",[])
|
||||
x:xs -> (x,xs)
|
||||
withArgs otherArgs $ defaultMain
|
||||
[ bench "traverse (FilePath)" $ nfIO $ listFilesRecursive d >>= mapM_ putStrLn
|
||||
, bench "traverse (RawFilePath)" $ nfIO $ listFilesRecursiveBS (BS.pack d) >>= mapM_ BS.putStrLn
|
||||
, bench "allDirectoryContents" $ nfIO $ allDirectoryContents (BS.pack d) >>= mapM_ BS.putStrLn
|
||||
, bench "allDirectoryContents'" $ nfIO $ allDirectoryContents' (BS.pack d) >>= mapM_ BS.putStrLn
|
||||
, bench "traverseDirectory" $ nfIO $ benchTraverse (BS.pack d)
|
||||
, bench "unix find" $ nfIO $ void $ system ("find " ++ d)
|
||||
]
|
||||
7
cbits/dirutils.c
Normal file
7
cbits/dirutils.c
Normal file
@@ -0,0 +1,7 @@
|
||||
#include "dirutils.h"
|
||||
unsigned int
|
||||
__posixdir_d_type(struct dirent* d)
|
||||
{
|
||||
return(d -> d_type);
|
||||
}
|
||||
|
||||
13
cbits/dirutils.h
Normal file
13
cbits/dirutils.h
Normal file
@@ -0,0 +1,13 @@
|
||||
#ifndef POSIXPATHS_CBITS_DIRUTILS_H
|
||||
#define POSIXPATHS_CBITS_DIRUTILS_H
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <dirent.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
extern unsigned int
|
||||
__posixdir_d_type(struct dirent* d)
|
||||
;
|
||||
#endif
|
||||
16
doctests-hpath.hs
Normal file
16
doctests-hpath.hs
Normal file
@@ -0,0 +1,16 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module Main where
|
||||
|
||||
import Control.Applicative
|
||||
|
||||
import Test.DocTest
|
||||
import Test.HUnit
|
||||
|
||||
main =
|
||||
doctest
|
||||
["-isrc"
|
||||
, "-XOverloadedStrings"
|
||||
, "src/HPath.hs"
|
||||
]
|
||||
|
||||
25
doctests-posix.hs
Normal file
25
doctests-posix.hs
Normal file
@@ -0,0 +1,25 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module Main where
|
||||
|
||||
import Control.Applicative
|
||||
import System.Posix.Directory.Traversals
|
||||
|
||||
import Test.DocTest
|
||||
import Test.HUnit
|
||||
|
||||
main = do
|
||||
doctest
|
||||
[ "-isrc"
|
||||
, "-XOverloadedStrings"
|
||||
, "System.Posix.FilePath"
|
||||
]
|
||||
runTestTT unitTests
|
||||
|
||||
|
||||
unitTests :: Test
|
||||
unitTests = test
|
||||
[ TestCase $ do
|
||||
r <- (==) <$> allDirectoryContents "." <*> allDirectoryContents' "."
|
||||
assertBool "allDirectoryContents == allDirectoryContents'" r
|
||||
]
|
||||
82
hpath.cabal
82
hpath.cabal
@@ -9,29 +9,75 @@ maintainer: Julian Ospald <hasufell@posteo.de>
|
||||
copyright: 2015–2016 FP Complete, Julian Ospald 2016
|
||||
category: Filesystem
|
||||
build-type: Simple
|
||||
cabal-version: >=1.8
|
||||
extra-source-files: README.md, CHANGELOG
|
||||
cabal-version: >=1.14
|
||||
extra-source-files: README.md
|
||||
CHANGELOG
|
||||
cbits/dirutils.h
|
||||
doctests.hs
|
||||
benchmarks/*.hs
|
||||
|
||||
library
|
||||
hs-source-dirs: src/
|
||||
default-language: Haskell2010
|
||||
ghc-options: -Wall -O2
|
||||
exposed-modules: HPath, HPath.Internal
|
||||
build-depends: base >= 4 && <5
|
||||
, exceptions
|
||||
, filepath
|
||||
, template-haskell
|
||||
c-sources: cbits/dirutils.c
|
||||
exposed-modules: HPath,
|
||||
HPath.Internal,
|
||||
System.Posix.Directory.Foreign,
|
||||
System.Posix.Directory.Traversals,
|
||||
System.Posix.FilePath
|
||||
build-depends: base >= 4.2 && <5
|
||||
, bytestring >= 0.9.2.0
|
||||
, deepseq
|
||||
, exceptions
|
||||
, hspec
|
||||
, unix >= 2.5
|
||||
, utf8-string
|
||||
, word8
|
||||
|
||||
test-suite test
|
||||
type: exitcode-stdio-1.0
|
||||
main-is: Main.hs
|
||||
hs-source-dirs: test
|
||||
build-depends: HUnit
|
||||
, base
|
||||
, hspec
|
||||
, mtl
|
||||
, hpath
|
||||
|
||||
test-suite doctests-hpath
|
||||
default-language: Haskell2010
|
||||
type: exitcode-stdio-1.0
|
||||
ghc-options: -threaded
|
||||
main-is: doctests-hpath.hs
|
||||
build-depends: base
|
||||
, HUnit
|
||||
, QuickCheck
|
||||
, doctest >= 0.8
|
||||
, hpath
|
||||
|
||||
test-suite doctests-posix
|
||||
default-language: Haskell2010
|
||||
type: exitcode-stdio-1.0
|
||||
ghc-options: -threaded
|
||||
main-is: doctests-posix.hs
|
||||
build-depends: base,
|
||||
bytestring,
|
||||
unix,
|
||||
hpath,
|
||||
doctest >= 0.8,
|
||||
HUnit,
|
||||
QuickCheck
|
||||
|
||||
benchmark bench.hs
|
||||
default-language: Haskell2010
|
||||
type: exitcode-stdio-1.0
|
||||
hs-source-dirs: benchmarks
|
||||
main-is: Bench.hs
|
||||
|
||||
build-depends:
|
||||
base,
|
||||
hpath,
|
||||
bytestring,
|
||||
unix,
|
||||
directory >= 1.1 && < 1.3,
|
||||
filepath >= 1.2 && < 1.4,
|
||||
process >= 1.0 && < 1.3,
|
||||
criterion >= 0.6 && < 0.9
|
||||
ghc-options: -O2
|
||||
|
||||
source-repository head
|
||||
type: git
|
||||
location: https://github.com/hasufell/hpath
|
||||
type: git
|
||||
location: https://github.com/hasufell/hpath
|
||||
|
||||
|
||||
641
src/HPath.hs
641
src/HPath.hs
@@ -9,7 +9,8 @@
|
||||
--
|
||||
-- Support for well-typed paths.
|
||||
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
|
||||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE DeriveDataTypeable #-}
|
||||
{-# LANGUAGE EmptyDataDecls #-}
|
||||
{-# LANGUAGE PatternSynonyms #-}
|
||||
@@ -19,54 +20,63 @@ module HPath
|
||||
(
|
||||
-- * Types
|
||||
Abs
|
||||
,NoTPS
|
||||
,Path
|
||||
,Rel
|
||||
,TPS
|
||||
,Fn
|
||||
,PathParseException
|
||||
-- * PatternSynonyms/ViewPatterns
|
||||
,pattern Path
|
||||
-- * Parsing
|
||||
,PathParseException
|
||||
,parseAbsMaybeTPS
|
||||
,parseAbsNoTPS
|
||||
,parseAbsTPS
|
||||
,parseRelMaybeTPS
|
||||
,parseRelNoTPS
|
||||
,parseRelTPS
|
||||
-- * Constructors
|
||||
,mkAbsMaybeTPS
|
||||
,mkAbsNoTPS
|
||||
,mkAbsTPS
|
||||
,mkRelMaybeTPS
|
||||
,mkRelNoTPS
|
||||
,mkRelTPS
|
||||
-- * Operations
|
||||
-- * Path Parsing
|
||||
,parseAbs
|
||||
,parseFn
|
||||
,parseRel
|
||||
-- * Path Conversion
|
||||
,canonicalizePath
|
||||
,fromAbs
|
||||
,fromRel
|
||||
,normalize
|
||||
,toFilePath
|
||||
-- * Path Operations
|
||||
,(</>)
|
||||
,basename
|
||||
,dirname
|
||||
,isParentOf
|
||||
,getAllParents
|
||||
,stripDir
|
||||
-- * Conversion
|
||||
,fromAbsMaybeTPS
|
||||
,fromAbsNoTPS
|
||||
,fromAbsTPS
|
||||
,fromRelMaybeTPS
|
||||
,fromRelNoTPS
|
||||
,fromRelTPS
|
||||
,toFilePath
|
||||
,toNoTPS
|
||||
,toTPS
|
||||
-- * Path IO helpers
|
||||
,withAbsPath
|
||||
,withRelPath
|
||||
,withFnPath
|
||||
-- * ByteString/Word8 constants
|
||||
,nullByte
|
||||
,pathDot
|
||||
,pathDot'
|
||||
,pathSeparator'
|
||||
-- * ByteString operations
|
||||
,fpToString
|
||||
,userStringToFP
|
||||
-- * ByteString Query functions
|
||||
,hiddenFile
|
||||
-- * Queries
|
||||
,hasParentDir
|
||||
,isFileName
|
||||
-- * String based functions
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Exception (Exception)
|
||||
import Control.Monad.Catch (MonadThrow(..))
|
||||
import Data.ByteString(ByteString)
|
||||
import qualified Data.ByteString as BS
|
||||
import Data.ByteString.UTF8 (fromString, toString)
|
||||
import Data.Data
|
||||
import Data.List
|
||||
import qualified Data.List as L
|
||||
import Data.Maybe
|
||||
import Language.Haskell.TH
|
||||
import Data.Word8
|
||||
import HPath.Internal
|
||||
import qualified System.FilePath as FilePath
|
||||
import System.Posix.FilePath hiding ((</>))
|
||||
import System.Posix.Directory.Traversals(realpath)
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Types
|
||||
@@ -77,259 +87,155 @@ data Abs deriving (Typeable)
|
||||
-- | A relative path; one without a root.
|
||||
data Rel deriving (Typeable)
|
||||
|
||||
-- | A path without trailing separator.
|
||||
data NoTPS deriving (Typeable)
|
||||
|
||||
-- | A path with trailing separator.
|
||||
data TPS deriving (Typeable)
|
||||
|
||||
-- | A path without any guarantee about whether it ends in a
|
||||
-- trailing path separators. Use `toTPS` and `toNoTPS`
|
||||
-- if that guarantee is required.
|
||||
data MaybeTPS deriving (Typeable)
|
||||
-- | A filename, without any '/'.
|
||||
data Fn deriving (Typeable)
|
||||
|
||||
-- | Exception when parsing a location.
|
||||
data PathParseException
|
||||
= InvalidAbsTPS FilePath
|
||||
| InvalidRelTPS FilePath
|
||||
| InvalidAbsNoTPS FilePath
|
||||
| InvalidRelNoTPS FilePath
|
||||
| InvalidAbsMaybeTPS FilePath
|
||||
| InvalidRelMaybeTPS FilePath
|
||||
| Couldn'tStripPrefixTPS FilePath FilePath
|
||||
= InvalidAbs ByteString
|
||||
| InvalidRel ByteString
|
||||
| InvalidFn ByteString
|
||||
| Couldn'tStripPrefixTPS ByteString ByteString
|
||||
deriving (Show,Typeable)
|
||||
instance Exception PathParseException
|
||||
|
||||
data PathException = RootDirHasNoBasename
|
||||
deriving (Show,Typeable)
|
||||
instance Exception PathException
|
||||
|
||||
instance RelC Rel
|
||||
instance RelC Fn
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- PatternSynonyms
|
||||
|
||||
pattern Path x <- (MkPath x)
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Parsers
|
||||
-- Path Parsers
|
||||
|
||||
-- | Get a location for an absolute path. Produces a normalized
|
||||
-- path which always ends in a path separator.
|
||||
|
||||
|
||||
-- | Get a location for an absolute path. Produces a normalised path.
|
||||
--
|
||||
-- Throws: 'PathParseException'
|
||||
--
|
||||
parseAbsTPS :: MonadThrow m
|
||||
=> FilePath -> m (Path Abs TPS)
|
||||
parseAbsTPS filepath =
|
||||
if FilePath.isAbsolute filepath &&
|
||||
not (null (normalizeTPS filepath)) &&
|
||||
not ("~/" `isPrefixOf` filepath) &&
|
||||
not (hasParentDir filepath) &&
|
||||
FilePath.isValid filepath
|
||||
then return (MkPath (normalizeTPS filepath))
|
||||
else throwM (InvalidAbsTPS filepath)
|
||||
-- >>> parseAbs "/abc" :: Maybe (Path Abs)
|
||||
-- Just "/abc"
|
||||
-- >>> parseAbs "/" :: Maybe (Path Abs)
|
||||
-- Just "/"
|
||||
-- >>> parseAbs "/abc/def" :: Maybe (Path Abs)
|
||||
-- Just "/abc/def"
|
||||
-- >>> parseAbs "/abc/def/.///" :: Maybe (Path Abs)
|
||||
-- Just "/abc/def/"
|
||||
-- >>> parseAbs "abc" :: Maybe (Path Abs)
|
||||
-- Nothing
|
||||
-- >>> parseAbs "" :: Maybe (Path Abs)
|
||||
-- Nothing
|
||||
-- >>> parseAbs "/abc/../foo" :: Maybe (Path Abs)
|
||||
-- Nothing
|
||||
parseAbs :: MonadThrow m
|
||||
=> ByteString -> m (Path Abs)
|
||||
parseAbs filepath =
|
||||
if isAbsolute filepath &&
|
||||
isValid filepath &&
|
||||
not (hasParentDir filepath)
|
||||
then return (MkPath $ normalise filepath)
|
||||
else throwM (InvalidAbs filepath)
|
||||
|
||||
-- | Get a location for a relative path. Produces a normalized
|
||||
-- path which always ends in a path separator.
|
||||
|
||||
-- | Get a location for a relative path. Produces a normalised
|
||||
-- path.
|
||||
--
|
||||
-- Note that @filepath@ may contain any number of @./@ but may not consist
|
||||
-- solely of @./@. It also may not contain a single @..@ anywhere.
|
||||
--
|
||||
-- Throws: 'PathParseException'
|
||||
--
|
||||
parseRelTPS :: MonadThrow m
|
||||
=> FilePath -> m (Path Rel TPS)
|
||||
parseRelTPS filepath =
|
||||
if not (FilePath.isAbsolute filepath) &&
|
||||
not (null filepath) &&
|
||||
not ("~/" `isPrefixOf` filepath) &&
|
||||
-- >>> parseRel "abc" :: Maybe (Path Rel)
|
||||
-- Just "abc"
|
||||
-- >>> parseRel "def/" :: Maybe (Path Rel)
|
||||
-- Just "def/"
|
||||
-- >>> parseRel "abc/def" :: Maybe (Path Rel)
|
||||
-- Just "abc/def"
|
||||
-- >>> parseRel "abc/def/." :: Maybe (Path Rel)
|
||||
-- Just "abc/def/"
|
||||
-- >>> parseRel "/abc" :: Maybe (Path Rel)
|
||||
-- Nothing
|
||||
-- >>> parseRel "" :: Maybe (Path Rel)
|
||||
-- Nothing
|
||||
-- >>> parseRel "abc/../foo" :: Maybe (Path Rel)
|
||||
-- Nothing
|
||||
-- >>> parseRel "." :: Maybe (Path Rel)
|
||||
-- Nothing
|
||||
-- >>> parseRel ".." :: Maybe (Path Rel)
|
||||
-- Nothing
|
||||
parseRel :: MonadThrow m
|
||||
=> ByteString -> m (Path Rel)
|
||||
parseRel filepath =
|
||||
if not (isAbsolute filepath) &&
|
||||
filepath /= pathDot' && filepath /= pathDoubleDot &&
|
||||
not (hasParentDir filepath) &&
|
||||
not (null (normalizeTPS filepath)) &&
|
||||
filepath /= "." && filepath /= ".." &&
|
||||
FilePath.isValid filepath
|
||||
then return (MkPath (normalizeTPS filepath))
|
||||
else throwM (InvalidRelTPS filepath)
|
||||
isValid filepath
|
||||
then return (MkPath $ normalise filepath)
|
||||
else throwM (InvalidRel filepath)
|
||||
|
||||
-- | Get a location for an absolute path, which must not end with a trailing
|
||||
-- path separator.
|
||||
|
||||
-- | Parses a filename. Filenames must not contain slashes.
|
||||
-- Excludes '.' and '..'.
|
||||
--
|
||||
-- Throws: 'PathParseException'
|
||||
--
|
||||
parseAbsNoTPS :: MonadThrow m
|
||||
=> FilePath -> m (Path Abs NoTPS)
|
||||
parseAbsNoTPS filepath =
|
||||
if FilePath.isAbsolute filepath &&
|
||||
not (FilePath.hasTrailingPathSeparator filepath) &&
|
||||
not ("~/" `isPrefixOf` filepath) &&
|
||||
not (hasParentDir filepath) &&
|
||||
not (null (normalizeNoTPS filepath)) &&
|
||||
FilePath.isValid filepath
|
||||
then return (MkPath (normalizeNoTPS filepath))
|
||||
else throwM (InvalidAbsNoTPS filepath)
|
||||
-- >>> parseFn "abc" :: Maybe (Path Fn)
|
||||
-- Just "abc"
|
||||
-- >>> parseFn "..." :: Maybe (Path Fn)
|
||||
-- Just "..."
|
||||
-- >>> parseFn "def/" :: Maybe (Path Fn)
|
||||
-- Nothing
|
||||
-- >>> parseFn "abc/def" :: Maybe (Path Fn)
|
||||
-- Nothing
|
||||
-- >>> parseFn "abc/def/." :: Maybe (Path Fn)
|
||||
-- Nothing
|
||||
-- >>> parseFn "/abc" :: Maybe (Path Fn)
|
||||
-- Nothing
|
||||
-- >>> parseFn "" :: Maybe (Path Fn)
|
||||
-- Nothing
|
||||
-- >>> parseFn "abc/../foo" :: Maybe (Path Fn)
|
||||
-- Nothing
|
||||
-- >>> parseFn "." :: Maybe (Path Fn)
|
||||
-- Nothing
|
||||
-- >>> parseFn ".." :: Maybe (Path Fn)
|
||||
-- Nothing
|
||||
parseFn :: MonadThrow m
|
||||
=> ByteString -> m (Path Fn)
|
||||
parseFn filepath =
|
||||
if isFileName filepath &&
|
||||
filepath /= pathDot' && filepath /= pathDoubleDot &&
|
||||
isValid filepath
|
||||
then return (MkPath filepath)
|
||||
else throwM (InvalidFn filepath)
|
||||
|
||||
-- | Get a location for a relative path, which must not end with a trailing
|
||||
-- path separator.
|
||||
--
|
||||
-- Note that @filepath@ may contain any number of @./@ but may not contain a
|
||||
-- single @..@ anywhere.
|
||||
--
|
||||
-- Throws: 'PathParseException'
|
||||
--
|
||||
parseRelNoTPS :: MonadThrow m
|
||||
=> FilePath -> m (Path Rel NoTPS)
|
||||
parseRelNoTPS filepath =
|
||||
if not (FilePath.isAbsolute filepath ||
|
||||
FilePath.hasTrailingPathSeparator filepath) &&
|
||||
not (null filepath) &&
|
||||
not ("~/" `isPrefixOf` filepath) &&
|
||||
not (hasParentDir filepath) &&
|
||||
not (null (normalizeNoTPS filepath)) &&
|
||||
filepath /= "." && filepath /= ".." &&
|
||||
FilePath.isValid filepath
|
||||
then return (MkPath (normalizeNoTPS filepath))
|
||||
else throwM (InvalidRelNoTPS filepath)
|
||||
|
||||
-- | Get a location for an absolute path that may or may not end in a trailing
|
||||
-- path separator. Use `toTPS` and `toNoTPS` if that guarantee is required.
|
||||
--
|
||||
-- Throws: 'PathParseException'
|
||||
--
|
||||
parseAbsMaybeTPS :: MonadThrow m
|
||||
=> FilePath -> m (Path Abs MaybeTPS)
|
||||
parseAbsMaybeTPS filepath =
|
||||
if FilePath.isAbsolute filepath &&
|
||||
not ("~/" `isPrefixOf` filepath) &&
|
||||
not (hasParentDir filepath) &&
|
||||
not (null (normalizeNoTPS filepath)) &&
|
||||
FilePath.isValid filepath
|
||||
then return (MkPath (normalizeNoTPS filepath))
|
||||
else throwM (InvalidAbsMaybeTPS filepath)
|
||||
|
||||
-- | Get a location for a relative path that may or may not end in a trailing
|
||||
-- path separator. Use `toTPS` and `toNoTPS` if that guarantee is required.
|
||||
--
|
||||
-- Note that @filepath@ may contain any number of @./@ but may not contain a
|
||||
-- single @..@ anywhere.
|
||||
--
|
||||
-- Throws: 'PathParseException'
|
||||
--
|
||||
parseRelMaybeTPS :: MonadThrow m
|
||||
=> FilePath -> m (Path Rel MaybeTPS)
|
||||
parseRelMaybeTPS filepath =
|
||||
if not (FilePath.isAbsolute filepath) &&
|
||||
not (null filepath) &&
|
||||
not ("~/" `isPrefixOf` filepath) &&
|
||||
not (hasParentDir filepath) &&
|
||||
not (null (normalizeNoTPS filepath)) &&
|
||||
filepath /= "." && filepath /= ".." &&
|
||||
FilePath.isValid filepath
|
||||
then return (MkPath (normalizeNoTPS filepath))
|
||||
else throwM (InvalidRelMaybeTPS filepath)
|
||||
|
||||
-- | Helper function: check if the filepath has any parent directories in it.
|
||||
-- This handles the logic of checking for different path separators on Windows.
|
||||
hasParentDir :: FilePath -> Bool
|
||||
hasParentDir filepath' =
|
||||
("/.." `isSuffixOf` filepath) ||
|
||||
("/../" `isInfixOf` filepath) ||
|
||||
("../" `isPrefixOf` filepath)
|
||||
where
|
||||
filepath =
|
||||
case FilePath.pathSeparator of
|
||||
'/' -> filepath'
|
||||
x -> map (\y -> if x == y then '/' else y) filepath'
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Constructors
|
||||
-- Path Conversion
|
||||
|
||||
-- | 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).
|
||||
mkAbsTPS :: FilePath -> Q Exp
|
||||
mkAbsTPS s =
|
||||
case parseAbsTPS s of
|
||||
Left err -> error (show err)
|
||||
Right (MkPath str) ->
|
||||
[|MkPath $(return (LitE (StringL str))) :: Path Abs TPS|]
|
||||
|
||||
-- | Make a 'Path Rel TPS'.
|
||||
mkRelTPS :: FilePath -> Q Exp
|
||||
mkRelTPS s =
|
||||
case parseRelTPS s of
|
||||
Left err -> error (show err)
|
||||
Right (MkPath str) ->
|
||||
[|MkPath $(return (LitE (StringL str))) :: Path Rel TPS|]
|
||||
|
||||
-- | Make a 'Path Abs NoTPS'.
|
||||
--
|
||||
-- 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).
|
||||
mkAbsNoTPS :: FilePath -> Q Exp
|
||||
mkAbsNoTPS s =
|
||||
case parseAbsNoTPS s of
|
||||
Left err -> error (show err)
|
||||
Right (MkPath str) ->
|
||||
[|MkPath $(return (LitE (StringL str))) :: Path Abs NoTPS|]
|
||||
|
||||
-- | Make a 'Path Rel NoTPS'.
|
||||
mkRelNoTPS :: FilePath -> Q Exp
|
||||
mkRelNoTPS s =
|
||||
case parseRelNoTPS s of
|
||||
Left err -> error (show err)
|
||||
Right (MkPath str) ->
|
||||
[|MkPath $(return (LitE (StringL str))) :: Path Rel NoTPS|]
|
||||
|
||||
-- | Make a 'Path Rel MaybeTPS'.
|
||||
mkAbsMaybeTPS :: FilePath -> Q Exp
|
||||
mkAbsMaybeTPS s =
|
||||
case parseAbsMaybeTPS s of
|
||||
Left err -> error (show err)
|
||||
Right (MkPath str) ->
|
||||
[|MkPath $(return (LitE (StringL str))) :: Path Abs MaybeTPS|]
|
||||
|
||||
-- | Make a 'Path Rel MaybeTPS'.
|
||||
mkRelMaybeTPS :: FilePath -> Q Exp
|
||||
mkRelMaybeTPS s =
|
||||
case parseRelMaybeTPS s of
|
||||
Left err -> error (show err)
|
||||
Right (MkPath str) ->
|
||||
[|MkPath $(return (LitE (StringL str))) :: Path Rel MaybeTPS|]
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Conversion
|
||||
|
||||
-- | Convert to a 'FilePath' type.
|
||||
--
|
||||
-- All TPS data types have a trailing slash, so if you want no trailing
|
||||
-- slash, you can use 'System.FilePath.dropTrailingPathSeparator' from
|
||||
-- the filepath package.
|
||||
toFilePath :: Path b t -> FilePath
|
||||
-- | Convert to a ByteString type.
|
||||
toFilePath :: Path b -> ByteString
|
||||
toFilePath (MkPath l) = l
|
||||
|
||||
fromAbsTPS :: Path Abs TPS -> FilePath
|
||||
fromAbsTPS = toFilePath
|
||||
fromAbs :: Path Abs -> ByteString
|
||||
fromAbs = toFilePath
|
||||
|
||||
fromRelTPS :: Path Rel TPS -> FilePath
|
||||
fromRelTPS = toFilePath
|
||||
fromRel :: RelC r => Path r -> ByteString
|
||||
fromRel = toFilePath
|
||||
|
||||
fromAbsNoTPS :: Path Abs NoTPS -> FilePath
|
||||
fromAbsNoTPS = toFilePath
|
||||
normalize :: Path t -> Path t
|
||||
normalize (MkPath l) = MkPath $ normalise l
|
||||
|
||||
fromRelNoTPS :: Path Rel NoTPS -> FilePath
|
||||
fromRelNoTPS = toFilePath
|
||||
|
||||
fromAbsMaybeTPS :: Path Abs MaybeTPS -> FilePath
|
||||
fromAbsMaybeTPS = toFilePath
|
||||
|
||||
fromRelMaybeTPS :: Path Rel MaybeTPS -> FilePath
|
||||
fromRelMaybeTPS = toFilePath
|
||||
|
||||
toTPS :: Path b MaybeTPS -> Path b TPS
|
||||
toTPS (MkPath l) = MkPath (FilePath.addTrailingPathSeparator l)
|
||||
|
||||
toNoTPS :: Path b MaybeTPS -> Path b NoTPS
|
||||
toNoTPS (MkPath l) = MkPath (FilePath.dropTrailingPathSeparator l)
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Operations
|
||||
-- Path Operations
|
||||
|
||||
-- | Append two paths.
|
||||
--
|
||||
@@ -339,31 +245,78 @@ toNoTPS (MkPath l) = MkPath (FilePath.dropTrailingPathSeparator l)
|
||||
-- Technically, the first argument can be a path that points to a non-directory,
|
||||
-- because this library is IO-agnostic and makes no assumptions about
|
||||
-- file types.
|
||||
(</>) :: Path b t1 -> Path Rel t2 -> Path b t2
|
||||
(</>) (MkPath a) (MkPath b) = MkPath (a' ++ b)
|
||||
--
|
||||
-- >>> (MkPath "/") </> (MkPath "file" :: Path Rel)
|
||||
-- "/file"
|
||||
-- >>> (MkPath "/path/to") </> (MkPath "file" :: Path Rel)
|
||||
-- "/path/to/file"
|
||||
-- >>> (MkPath "/") </> (MkPath "file/lal" :: Path Rel)
|
||||
-- "/file/lal"
|
||||
-- >>> (MkPath "/") </> (MkPath "file/" :: Path Rel)
|
||||
-- "/file/"
|
||||
(</>) :: RelC r => Path b -> Path r -> Path b
|
||||
(</>) (MkPath a) (MkPath b) = MkPath (a' `BS.append` b)
|
||||
where
|
||||
a' = FilePath.addTrailingPathSeparator a
|
||||
a' = if BS.last a == pathSeparator
|
||||
then a
|
||||
else addTrailingPathSeparator a
|
||||
|
||||
-- | Strip directory from path, making it relative to that directory.
|
||||
-- Throws 'Couldn'tStripPrefixDir' if directory is not a parent of the path.
|
||||
--
|
||||
-- The bases must match.
|
||||
--
|
||||
-- >>> (MkPath "/lal/lad") `stripDir` (MkPath "/lal/lad/fad") :: Maybe (Path Rel)
|
||||
-- Just "fad"
|
||||
-- >>> (MkPath "lal/lad") `stripDir` (MkPath "lal/lad/fad") :: Maybe (Path Rel)
|
||||
-- Just "fad"
|
||||
-- >>> (MkPath "/") `stripDir` (MkPath "/") :: Maybe (Path Rel)
|
||||
-- Nothing
|
||||
-- >>> (MkPath "/lal/lad/fad") `stripDir` (MkPath "/lal/lad") :: Maybe (Path Rel)
|
||||
-- Nothing
|
||||
-- >>> (MkPath "fad") `stripDir` (MkPath "fad") :: Maybe (Path Rel)
|
||||
-- Nothing
|
||||
stripDir :: MonadThrow m
|
||||
=> Path b t1 -> Path b t2 -> m (Path Rel t2)
|
||||
=> Path b -> Path b -> m (Path Rel)
|
||||
stripDir (MkPath p) (MkPath l) =
|
||||
case stripPrefix p' l of
|
||||
Nothing -> throwM (Couldn'tStripPrefixTPS p' l)
|
||||
Just "" -> throwM (Couldn'tStripPrefixTPS p' l)
|
||||
Just ok -> return (MkPath ok)
|
||||
Just ok -> if BS.null ok
|
||||
then throwM (Couldn'tStripPrefixTPS p' l)
|
||||
else return (MkPath ok)
|
||||
where
|
||||
p' = FilePath.addTrailingPathSeparator p
|
||||
p' = addTrailingPathSeparator p
|
||||
|
||||
-- | Is p a parent of the given location? Implemented in terms of
|
||||
-- 'stripDir'. The bases must match.
|
||||
isParentOf :: Path b t1 -> Path b t2 -> Bool
|
||||
isParentOf p l =
|
||||
isJust (stripDir p l)
|
||||
--
|
||||
-- >>> (MkPath "/lal/lad") `isParentOf` (MkPath "/lal/lad/fad")
|
||||
-- True
|
||||
-- >>> (MkPath "lal/lad") `isParentOf` (MkPath "lal/lad/fad")
|
||||
-- True
|
||||
-- >>> (MkPath "/") `isParentOf` (MkPath "/")
|
||||
-- False
|
||||
-- >>> (MkPath "/lal/lad/fad") `isParentOf` (MkPath "/lal/lad")
|
||||
-- False
|
||||
-- >>> (MkPath "fad") `isParentOf` (MkPath "fad")
|
||||
-- False
|
||||
isParentOf :: Path b -> Path b -> Bool
|
||||
isParentOf p l = isJust (stripDir p l :: Maybe (Path Rel))
|
||||
|
||||
|
||||
-- |Get all parents of a path.
|
||||
--
|
||||
-- >>> getAllParents (MkPath "/abs/def/dod")
|
||||
-- ["/abs/def","/abs","/"]
|
||||
-- >>> getAllParents (MkPath "/")
|
||||
-- []
|
||||
getAllParents :: Path Abs -> [Path Abs]
|
||||
getAllParents (MkPath p)
|
||||
| np == pathSeparator' = []
|
||||
| otherwise = dirname (MkPath np) : getAllParents (dirname $ MkPath np)
|
||||
where
|
||||
np = dropTrailingPathSeparator . normalise $ p
|
||||
|
||||
|
||||
-- | Extract the directory name of a path.
|
||||
--
|
||||
@@ -371,45 +324,169 @@ isParentOf p l =
|
||||
--
|
||||
-- @dirname (p \<\/> a) == dirname p@
|
||||
--
|
||||
dirname :: Path Abs t -> Path Abs TPS
|
||||
dirname (MkPath fp) = MkPath (normalizeTPS (FilePath.takeDirectory $ FilePath.dropTrailingPathSeparator fp))
|
||||
-- >>> dirname (MkPath "/abc/def/dod")
|
||||
-- "/abc/def"
|
||||
-- >>> dirname (MkPath "/")
|
||||
-- "/"
|
||||
dirname :: Path Abs -> Path Abs
|
||||
dirname (MkPath fp) = MkPath (takeDirectory $ dropTrailingPathSeparator fp)
|
||||
|
||||
-- | Extract the file part of a path.
|
||||
--
|
||||
-- Throws InvalidRelTPS if it's passed e.g. '/', because there is no
|
||||
-- basename for that and it would break the `Path Rel t` type.
|
||||
--
|
||||
-- The following properties hold:
|
||||
--
|
||||
-- @basename (p \<\/> a) == basename a@
|
||||
--
|
||||
basename :: MonadThrow m => Path b t -> m (Path Rel t)
|
||||
-- Throws: `PathException` if given the root path "/"
|
||||
--
|
||||
-- >>> basename (MkPath "/abc/def/dod") :: Maybe (Path Fn)
|
||||
-- Just "dod"
|
||||
-- >>> basename (MkPath "/") :: Maybe (Path Fn)
|
||||
-- Nothing
|
||||
basename :: MonadThrow m => Path b -> m (Path Fn)
|
||||
basename (MkPath l)
|
||||
| not (FilePath.isAbsolute rl) = return $ MkPath rl
|
||||
| otherwise = throwM (InvalidRelTPS rl)
|
||||
| not (isAbsolute rl) = return $ MkPath rl
|
||||
| otherwise = throwM RootDirHasNoBasename
|
||||
where
|
||||
rl = case FilePath.hasTrailingPathSeparator l of
|
||||
True -> last (FilePath.splitPath l)
|
||||
False -> normalizeNoTPS (FilePath.takeFileName l)
|
||||
rl = last . splitPath . dropTrailingPathSeparator $ l
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Internal functions
|
||||
-- Path IO helpers
|
||||
|
||||
-- | Internal use for normalizing a path while always adding
|
||||
-- a trailing path separator.
|
||||
normalizeTPS :: FilePath -> FilePath
|
||||
normalizeTPS =
|
||||
clean . FilePath.addTrailingPathSeparator . FilePath.normalise
|
||||
where clean "./" = ""
|
||||
clean ('/':'/':xs) = clean ('/':xs)
|
||||
clean x = x
|
||||
|
||||
-- | Internal use for normalizing a path without adding or removing
|
||||
-- a trailing path separator.
|
||||
normalizeNoTPS :: FilePath -> FilePath
|
||||
normalizeNoTPS =
|
||||
clean . FilePath.normalise
|
||||
where clean "./" = ""
|
||||
clean ('/':'/':xs) = clean ('/':xs)
|
||||
clean x = x
|
||||
-- | May fail on `realpath`.
|
||||
canonicalizePath :: Path Abs -> IO (Path Abs)
|
||||
canonicalizePath (MkPath l) = do
|
||||
nl <- realpath l
|
||||
return $ MkPath nl
|
||||
|
||||
|
||||
withAbsPath :: Path Abs -> (ByteString -> IO a) -> IO a
|
||||
withAbsPath (MkPath p) action = action p
|
||||
|
||||
|
||||
withRelPath :: Path Rel -> (ByteString -> IO a) -> IO a
|
||||
withRelPath (MkPath p) action = action p
|
||||
|
||||
|
||||
withFnPath :: Path Fn -> (ByteString -> IO a) -> IO a
|
||||
withFnPath (MkPath p) action = action p
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- ByteString Query functions
|
||||
|
||||
|
||||
-- | Whether the file is a hidden file.
|
||||
--
|
||||
-- >>> hiddenFile (MkPath ".foo")
|
||||
-- True
|
||||
-- >>> hiddenFile (MkPath "..foo.bar")
|
||||
-- True
|
||||
-- >>> hiddenFile (MkPath "...")
|
||||
-- True
|
||||
-- >>> hiddenFile (MkPath "dod")
|
||||
-- False
|
||||
-- >>> hiddenFile (MkPath "dod.bar")
|
||||
-- False
|
||||
hiddenFile :: Path Fn -> Bool
|
||||
hiddenFile (MkPath fp)
|
||||
| fp == pathDoubleDot = False
|
||||
| fp == pathDot' = False
|
||||
| otherwise = pathDot' `BS.isPrefixOf` fp
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- ByteString/Word8 constants
|
||||
|
||||
pathSeparator' :: ByteString
|
||||
pathSeparator' = BS.singleton pathSeparator
|
||||
|
||||
|
||||
pathDot :: Word8
|
||||
pathDot = _period
|
||||
|
||||
|
||||
pathDot' :: ByteString
|
||||
pathDot' = BS.singleton pathDot
|
||||
|
||||
|
||||
pathDoubleDot :: ByteString
|
||||
pathDoubleDot = pathDot `BS.cons` pathDot'
|
||||
|
||||
|
||||
nullByte :: Word8
|
||||
nullByte = _nul
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- ByteString Operations
|
||||
|
||||
|
||||
|
||||
-- |Uses UTF-8 decoding to convert the bytestring into a String.
|
||||
fpToString :: ByteString -> String
|
||||
fpToString = toString
|
||||
|
||||
|
||||
-- |Uses UTF-8 encoding to convert a user provided String into
|
||||
-- a ByteString, which represents a filepath.
|
||||
userStringToFP :: String -> ByteString
|
||||
userStringToFP = fromString
|
||||
|
||||
|
||||
#if MIN_VERSION_bytestring(0,10,8)
|
||||
#else
|
||||
stripPrefix :: ByteString -> ByteString -> Maybe ByteString
|
||||
stripPrefix a b = BS.pack `fmap` L.stripPrefix (BS.unpack a) (BS.unpack b)
|
||||
#endif
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- ByteString Query functions
|
||||
|
||||
-- | Helper function: check if the filepath has any parent directories in it.
|
||||
--
|
||||
-- >>> hasParentDir "/.."
|
||||
-- True
|
||||
-- >>> hasParentDir "foo/bar/.."
|
||||
-- True
|
||||
-- >>> hasParentDir "foo/../bar/."
|
||||
-- True
|
||||
-- >>> hasParentDir "foo/bar"
|
||||
-- False
|
||||
-- >>> hasParentDir "foo"
|
||||
-- False
|
||||
-- >>> hasParentDir ""
|
||||
-- False
|
||||
-- >>> hasParentDir ".."
|
||||
-- False
|
||||
hasParentDir :: ByteString -> Bool
|
||||
hasParentDir filepath =
|
||||
((pathSeparator `BS.cons` pathDoubleDot) `BS.isSuffixOf` filepath) ||
|
||||
((pathSeparator' `BS.append` pathDoubleDot `BS.append` pathSeparator')
|
||||
`BS.isInfixOf` filepath) ||
|
||||
((pathDoubleDot `BS.append` pathSeparator') `BS.isPrefixOf` filepath)
|
||||
|
||||
|
||||
-- | Is the given filename a valid filename?
|
||||
--
|
||||
-- >>> isFileName "lal"
|
||||
-- True
|
||||
-- >>> isFileName "."
|
||||
-- True
|
||||
-- >>> isFileName ".."
|
||||
-- True
|
||||
-- >>> isFileName ""
|
||||
-- False
|
||||
-- >>> isFileName "\0"
|
||||
-- False
|
||||
-- >>> isFileName "/random_ path:*"
|
||||
-- False
|
||||
isFileName :: ByteString -> Bool
|
||||
isFileName filepath =
|
||||
not (pathSeparator' `BS.isInfixOf` filepath) &&
|
||||
not (BS.null filepath) &&
|
||||
not (nullByte `BS.elem` filepath)
|
||||
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
-- | Internal types and functions.
|
||||
|
||||
module HPath.Internal
|
||||
(Path(..))
|
||||
(Path(..)
|
||||
,RelC)
|
||||
where
|
||||
|
||||
import Control.DeepSeq (NFData (..))
|
||||
import Data.ByteString (ByteString)
|
||||
import Data.Data
|
||||
|
||||
-- | Path of some base and type.
|
||||
@@ -18,7 +20,7 @@ import Data.Data
|
||||
--
|
||||
-- There are no duplicate
|
||||
-- path separators @\/\/@, no @..@, no @.\/@, no @~\/@, etc.
|
||||
data Path b t = MkPath FilePath
|
||||
data Path b = MkPath ByteString
|
||||
deriving (Typeable)
|
||||
|
||||
-- | String equality.
|
||||
@@ -26,7 +28,7 @@ data Path b t = MkPath FilePath
|
||||
-- The following property holds:
|
||||
--
|
||||
-- @show x == show y ≡ x == y@
|
||||
instance Eq (Path b t) where
|
||||
instance Eq (Path b) where
|
||||
(==) (MkPath x) (MkPath y) = x == y
|
||||
|
||||
-- | String ordering.
|
||||
@@ -34,7 +36,7 @@ instance Eq (Path b t) where
|
||||
-- The following property holds:
|
||||
--
|
||||
-- @show x \`compare\` show y ≡ x \`compare\` y@
|
||||
instance Ord (Path b t) where
|
||||
instance Ord (Path b) where
|
||||
compare (MkPath x) (MkPath y) = compare x y
|
||||
|
||||
-- | Same as 'Path.toFilePath'.
|
||||
@@ -42,9 +44,12 @@ instance Ord (Path b t) where
|
||||
-- The following property holds:
|
||||
--
|
||||
-- @x == y ≡ show x == show y@
|
||||
instance Show (Path b t) where
|
||||
instance Show (Path b) where
|
||||
show (MkPath x) = show x
|
||||
|
||||
instance NFData (Path b t) where
|
||||
instance NFData (Path b) where
|
||||
rnf (MkPath x) = rnf x
|
||||
|
||||
|
||||
class RelC m
|
||||
|
||||
|
||||
55
src/System/Posix/Directory/Foreign.hsc
Normal file
55
src/System/Posix/Directory/Foreign.hsc
Normal file
@@ -0,0 +1,55 @@
|
||||
module System.Posix.Directory.Foreign where
|
||||
|
||||
import Data.Bits
|
||||
import Data.List (foldl')
|
||||
import Foreign.C.Types
|
||||
|
||||
#include <limits.h>
|
||||
#include <stdlib.h>
|
||||
#include <dirent.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
newtype DirType = DirType Int deriving (Eq, Show)
|
||||
data Flags = Flags Int | UnsupportedFlag String deriving (Eq, Show)
|
||||
|
||||
unFlags :: Flags -> Int
|
||||
unFlags (Flags i) = i
|
||||
unFlags (UnsupportedFlag name) = error (name ++ " is not supported on this platform")
|
||||
|
||||
-- |Returns @True@ if posix-paths was compiled with support for the provided
|
||||
-- flag. (As of this writing, the only flag for which this check may be
|
||||
-- necessary is 'oCloexec'; all other flags will always yield @True@.)
|
||||
isSupported :: Flags -> Bool
|
||||
isSupported (Flags _) = True
|
||||
isSupported _ = False
|
||||
|
||||
-- |@O_CLOEXEC@ is not supported on every POSIX platform. Use
|
||||
-- @'isSupported' oCloexec@ to determine if support for @O_CLOEXEC@ was
|
||||
-- compiled into your version of posix-paths. (If not, using @oCloexec@ will
|
||||
-- throw an exception.)
|
||||
oCloexec :: Flags
|
||||
#ifdef O_CLOEXEC
|
||||
oCloexec = Flags #{const O_CLOEXEC}
|
||||
#else
|
||||
{-# WARNING oCloexec
|
||||
"This version of posix-paths was compiled without @O_CLOEXEC@ support." #-}
|
||||
oCloexec = UnsupportedFlag "O_CLOEXEC"
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
-- If these enum declarations occur earlier in the file, haddock
|
||||
-- gets royally confused about the above doc comments.
|
||||
-- Probably http://trac.haskell.org/haddock/ticket/138
|
||||
|
||||
#{enum DirType, DirType, DT_BLK, DT_CHR, DT_DIR, DT_FIFO, DT_LNK, DT_REG, DT_SOCK, DT_UNKNOWN}
|
||||
|
||||
#{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}
|
||||
|
||||
pathMax :: Int
|
||||
pathMax = #{const PATH_MAX}
|
||||
|
||||
unionFlags :: [Flags] -> CInt
|
||||
unionFlags = fromIntegral . foldl' ((. unFlags) . (.|.)) 0
|
||||
269
src/System/Posix/Directory/Traversals.hs
Normal file
269
src/System/Posix/Directory/Traversals.hs
Normal file
@@ -0,0 +1,269 @@
|
||||
{-# LANGUAGE ForeignFunctionInterface #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
{-# LANGUAGE ViewPatterns #-}
|
||||
|
||||
{-# OPTIONS_GHC -Wall #-}
|
||||
module System.Posix.Directory.Traversals (
|
||||
|
||||
getDirectoryContents
|
||||
, getDirectoryContents'
|
||||
|
||||
, allDirectoryContents
|
||||
, allDirectoryContents'
|
||||
, traverseDirectory
|
||||
|
||||
-- lower-level stuff
|
||||
, readDirEnt
|
||||
, packDirStream
|
||||
, unpackDirStream
|
||||
, openFd
|
||||
|
||||
, realpath
|
||||
) where
|
||||
|
||||
import Control.Applicative
|
||||
import Control.Monad
|
||||
import System.Posix.FilePath ((</>))
|
||||
import System.Posix.Directory.Foreign
|
||||
|
||||
import qualified System.Posix as Posix
|
||||
import System.IO.Error
|
||||
import Control.Exception
|
||||
import qualified Data.ByteString.Char8 as BS
|
||||
import System.Posix.ByteString.FilePath
|
||||
import System.Posix.Directory.ByteString as PosixBS
|
||||
import System.Posix.Files.ByteString
|
||||
|
||||
import System.IO.Unsafe
|
||||
import Unsafe.Coerce (unsafeCoerce)
|
||||
import Foreign.C.Error
|
||||
import Foreign.C.String
|
||||
import Foreign.C.Types
|
||||
import Foreign.Marshal.Alloc (alloca,allocaBytes)
|
||||
import Foreign.Ptr
|
||||
import Foreign.Storable
|
||||
|
||||
|
||||
|
||||
|
||||
----------------------------------------------------------
|
||||
|
||||
-- | Get all files from a directory and its subdirectories.
|
||||
--
|
||||
-- Upon entering a directory, 'allDirectoryContents' will get all entries
|
||||
-- strictly. However the returned list is lazy in that directories will only
|
||||
-- be accessed on demand.
|
||||
allDirectoryContents :: RawFilePath -> IO [RawFilePath]
|
||||
allDirectoryContents topdir = do
|
||||
namesAndTypes <- getDirectoryContents topdir
|
||||
let properNames = filter ((`notElem` [".", ".."]) . snd) namesAndTypes
|
||||
paths <- forM properNames $ \(typ,name) -> unsafeInterleaveIO $ do
|
||||
let path = topdir </> name
|
||||
case () of
|
||||
() | typ == dtDir -> allDirectoryContents path
|
||||
| typ == dtUnknown -> do
|
||||
isDir <- isDirectory <$> getFileStatus path
|
||||
if isDir
|
||||
then allDirectoryContents path
|
||||
else return [path]
|
||||
| otherwise -> return [path]
|
||||
return (topdir : concat paths)
|
||||
|
||||
-- | Get all files from a directory and its subdirectories strictly.
|
||||
allDirectoryContents' :: RawFilePath -> IO [RawFilePath]
|
||||
allDirectoryContents' = fmap reverse . traverseDirectory (\acc fp -> return (fp:acc)) []
|
||||
-- this uses traverseDirectory because it's more efficient than forcing the
|
||||
-- lazy version.
|
||||
|
||||
-- | Recursively apply the 'action' to the parent directory and all
|
||||
-- files/subdirectories.
|
||||
--
|
||||
-- This function allows for memory-efficient traversals.
|
||||
traverseDirectory :: (s -> RawFilePath -> IO s) -> s -> RawFilePath -> IO s
|
||||
traverseDirectory act s0 topdir = toploop
|
||||
where
|
||||
toploop = do
|
||||
isDir <- isDirectory <$> getFileStatus topdir
|
||||
s' <- act s0 topdir
|
||||
if isDir then actOnDirContents topdir s' loop
|
||||
else return s'
|
||||
loop typ path acc = do
|
||||
isDir <- case () of
|
||||
() | typ == dtDir -> return True
|
||||
| typ == dtUnknown -> isDirectory <$> getFileStatus path
|
||||
| otherwise -> return False
|
||||
if isDir
|
||||
then act acc path >>= \acc' -> actOnDirContents path acc' loop
|
||||
else act acc path
|
||||
|
||||
actOnDirContents :: RawFilePath
|
||||
-> b
|
||||
-> (DirType -> RawFilePath -> b -> IO b)
|
||||
-> IO b
|
||||
actOnDirContents pathRelToTop b f =
|
||||
modifyIOError ((`ioeSetFileName` (BS.unpack pathRelToTop)) .
|
||||
(`ioeSetLocation` "findBSTypRel")) $ do
|
||||
bracket
|
||||
(openDirStream pathRelToTop)
|
||||
(Posix.closeDirStream)
|
||||
(\dirp -> loop dirp b)
|
||||
where
|
||||
loop dirp b' = do
|
||||
(typ,e) <- readDirEnt dirp
|
||||
if (e == "")
|
||||
then return b'
|
||||
else do
|
||||
if (e == "." || e == "..")
|
||||
then loop dirp b'
|
||||
else f typ (pathRelToTop </> e) b' >>= loop dirp
|
||||
|
||||
|
||||
----------------------------------------------------------
|
||||
-- dodgy stuff
|
||||
|
||||
type CDir = ()
|
||||
type CDirent = ()
|
||||
|
||||
-- Posix doesn't export DirStream, so to re-use that type we need to use
|
||||
-- unsafeCoerce. It's just a newtype, so this is a legitimate usage.
|
||||
-- ugly trick.
|
||||
unpackDirStream :: DirStream -> Ptr CDir
|
||||
unpackDirStream = unsafeCoerce
|
||||
|
||||
packDirStream :: Ptr CDir -> DirStream
|
||||
packDirStream = unsafeCoerce
|
||||
|
||||
-- the __hscore_* functions are defined in the unix package. We can import them and let
|
||||
-- the linker figure it out.
|
||||
foreign import ccall unsafe "__hscore_readdir"
|
||||
c_readdir :: Ptr CDir -> Ptr (Ptr CDirent) -> IO CInt
|
||||
|
||||
foreign import ccall unsafe "__hscore_free_dirent"
|
||||
c_freeDirEnt :: Ptr CDirent -> IO ()
|
||||
|
||||
foreign import ccall unsafe "__hscore_d_name"
|
||||
c_name :: Ptr CDirent -> IO CString
|
||||
|
||||
foreign import ccall unsafe "__posixdir_d_type"
|
||||
c_type :: Ptr CDirent -> IO DirType
|
||||
|
||||
foreign import ccall "realpath"
|
||||
c_realpath :: CString -> CString -> IO CString
|
||||
|
||||
foreign import ccall unsafe "fdopendir"
|
||||
c_fdopendir :: Posix.Fd -> IO (Ptr ())
|
||||
|
||||
foreign import ccall unsafe "open"
|
||||
c_open :: CString -> CInt -> Posix.CMode -> IO CInt
|
||||
|
||||
----------------------------------------------------------
|
||||
-- less dodgy but still lower-level
|
||||
|
||||
|
||||
readDirEnt :: DirStream -> IO (DirType, RawFilePath)
|
||||
readDirEnt (unpackDirStream -> dirp) =
|
||||
alloca $ \ptr_dEnt -> loop ptr_dEnt
|
||||
where
|
||||
loop ptr_dEnt = do
|
||||
resetErrno
|
||||
r <- c_readdir dirp ptr_dEnt
|
||||
if (r == 0)
|
||||
then do
|
||||
dEnt <- peek ptr_dEnt
|
||||
if (dEnt == nullPtr)
|
||||
then return (dtUnknown,BS.empty)
|
||||
else do
|
||||
dName <- c_name dEnt >>= peekFilePath
|
||||
dType <- c_type dEnt
|
||||
c_freeDirEnt dEnt
|
||||
return (dType, dName)
|
||||
else do
|
||||
errno <- getErrno
|
||||
if (errno == eINTR)
|
||||
then loop ptr_dEnt
|
||||
else do
|
||||
let (Errno eo) = errno
|
||||
if (eo == 0)
|
||||
then return (dtUnknown,BS.empty)
|
||||
else throwErrno "readDirEnt"
|
||||
|
||||
|
||||
getDirectoryContents :: RawFilePath -> IO [(DirType, RawFilePath)]
|
||||
getDirectoryContents path =
|
||||
modifyIOError ((`ioeSetFileName` (BS.unpack path)) .
|
||||
(`ioeSetLocation` "System.Posix.Directory.Traversals.getDirectoryContents")) $ do
|
||||
bracket
|
||||
(PosixBS.openDirStream path)
|
||||
PosixBS.closeDirStream
|
||||
loop
|
||||
where
|
||||
loop dirp = do
|
||||
t@(_typ,e) <- readDirEnt dirp
|
||||
if BS.null e then return [] else do
|
||||
es <- loop dirp
|
||||
return (t:es)
|
||||
|
||||
|
||||
fdOpendir :: Posix.Fd -> IO DirStream
|
||||
fdOpendir fd =
|
||||
packDirStream <$> throwErrnoIfNull "fdOpendir" (c_fdopendir fd)
|
||||
|
||||
|
||||
getDirectoryContents' :: Posix.Fd -> IO [(DirType, RawFilePath)]
|
||||
getDirectoryContents' fd =
|
||||
bracket
|
||||
(fdOpendir fd)
|
||||
PosixBS.closeDirStream
|
||||
loop
|
||||
where
|
||||
loop dirp = do
|
||||
t@(_typ,e) <- readDirEnt dirp
|
||||
if BS.null e then return [] else do
|
||||
es <- loop dirp
|
||||
return (t:es)
|
||||
|
||||
|
||||
open_ :: CString
|
||||
-> Posix.OpenMode
|
||||
-> [Flags]
|
||||
-> Maybe Posix.FileMode
|
||||
-> IO Posix.Fd
|
||||
open_ str how optional_flags maybe_mode = do
|
||||
fd <- c_open str all_flags mode_w
|
||||
return (Posix.Fd fd)
|
||||
where
|
||||
all_flags = unionFlags $ optional_flags ++ [open_mode] ++ creat
|
||||
|
||||
|
||||
(creat, mode_w) = case maybe_mode of
|
||||
Nothing -> ([],0)
|
||||
Just x -> ([oCreat], x)
|
||||
|
||||
open_mode = case how of
|
||||
Posix.ReadOnly -> oRdonly
|
||||
Posix.WriteOnly -> oWronly
|
||||
Posix.ReadWrite -> oRdwr
|
||||
|
||||
|
||||
-- |Open and optionally create this file. See 'System.Posix.Files'
|
||||
-- for information on how to use the 'FileMode' type.
|
||||
openFd :: RawFilePath
|
||||
-> Posix.OpenMode
|
||||
-> [Flags]
|
||||
-> Maybe Posix.FileMode
|
||||
-> IO Posix.Fd
|
||||
openFd name how optional_flags maybe_mode =
|
||||
withFilePath name $ \str ->
|
||||
throwErrnoPathIfMinus1Retry "openFd" name $
|
||||
open_ str how optional_flags maybe_mode
|
||||
|
||||
|
||||
-- | return the canonicalized absolute pathname
|
||||
--
|
||||
-- like canonicalizePath, but uses realpath(3)
|
||||
realpath :: RawFilePath -> IO RawFilePath
|
||||
realpath inp = do
|
||||
allocaBytes pathMax $ \tmp -> do
|
||||
void $ BS.useAsCString inp $ \cstr -> throwErrnoIfNull "realpath" $ c_realpath cstr tmp
|
||||
BS.packCString tmp
|
||||
535
src/System/Posix/FilePath.hs
Normal file
535
src/System/Posix/FilePath.hs
Normal file
@@ -0,0 +1,535 @@
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
|
||||
{-# OPTIONS_GHC -Wall #-}
|
||||
|
||||
-- | The equivalent of "System.FilePath" on raw (byte string) file paths.
|
||||
--
|
||||
-- Not all functions of "System.FilePath" are implemented yet. Feel free to contribute!
|
||||
module System.Posix.FilePath (
|
||||
|
||||
pathSeparator
|
||||
, isPathSeparator
|
||||
, searchPathSeparator
|
||||
, isSearchPathSeparator
|
||||
, extSeparator
|
||||
, isExtSeparator
|
||||
|
||||
, splitExtension
|
||||
, takeExtension
|
||||
, replaceExtension
|
||||
, dropExtension
|
||||
, addExtension
|
||||
, hasExtension
|
||||
, (<.>)
|
||||
, splitExtensions
|
||||
, dropExtensions
|
||||
, takeExtensions
|
||||
|
||||
, splitFileName
|
||||
, takeFileName
|
||||
, replaceFileName
|
||||
, dropFileName
|
||||
, takeBaseName
|
||||
, replaceBaseName
|
||||
, takeDirectory
|
||||
, replaceDirectory
|
||||
, combine
|
||||
, (</>)
|
||||
, splitPath
|
||||
, joinPath
|
||||
, normalise
|
||||
, splitDirectories
|
||||
|
||||
, hasTrailingPathSeparator
|
||||
, addTrailingPathSeparator
|
||||
, dropTrailingPathSeparator
|
||||
|
||||
, isRelative
|
||||
, isAbsolute
|
||||
, isValid
|
||||
, equalFilePath
|
||||
|
||||
, module System.Posix.ByteString.FilePath
|
||||
) where
|
||||
|
||||
import Data.ByteString (ByteString)
|
||||
import qualified Data.ByteString as BS
|
||||
import System.Posix.ByteString.FilePath
|
||||
|
||||
import Data.Maybe (isJust)
|
||||
import Data.Word8
|
||||
|
||||
import Control.Arrow (second)
|
||||
|
||||
-- $setup
|
||||
-- >>> import Data.Char
|
||||
-- >>> import Test.QuickCheck
|
||||
-- >>> import Control.Applicative
|
||||
-- >>> import qualified Data.ByteString as BS
|
||||
-- >>> import Data.ByteString (ByteString)
|
||||
-- >>> instance Arbitrary ByteString where arbitrary = BS.pack <$> arbitrary
|
||||
-- >>> instance CoArbitrary ByteString where coarbitrary = coarbitrary . BS.unpack
|
||||
--
|
||||
-- >>> let _chr :: Word8 -> Char; _chr = chr . fromIntegral
|
||||
|
||||
|
||||
-- | Path separator character
|
||||
pathSeparator :: Word8
|
||||
pathSeparator = _slash
|
||||
|
||||
-- | Check if a character is the path separator
|
||||
--
|
||||
-- prop> \n -> (_chr n == '/') == isPathSeparator n
|
||||
isPathSeparator :: Word8 -> Bool
|
||||
isPathSeparator = (== pathSeparator)
|
||||
|
||||
-- | Search path separator
|
||||
searchPathSeparator :: Word8
|
||||
searchPathSeparator = _colon
|
||||
|
||||
-- | Check if a character is the search path separator
|
||||
--
|
||||
-- prop> \n -> (_chr n == ':') == isSearchPathSeparator n
|
||||
isSearchPathSeparator :: Word8 -> Bool
|
||||
isSearchPathSeparator = (== searchPathSeparator)
|
||||
|
||||
-- | File extension separator
|
||||
extSeparator :: Word8
|
||||
extSeparator = _period
|
||||
|
||||
-- | Check if a character is the file extension separator
|
||||
--
|
||||
-- prop> \n -> (_chr n == '.') == isExtSeparator n
|
||||
isExtSeparator :: Word8 -> Bool
|
||||
isExtSeparator = (== extSeparator)
|
||||
|
||||
------------------------
|
||||
-- extension stuff
|
||||
|
||||
-- | Split a 'RawFilePath' into a path+filename and extension
|
||||
--
|
||||
-- >>> splitExtension "file.exe"
|
||||
-- ("file",".exe")
|
||||
-- >>> splitExtension "file"
|
||||
-- ("file","")
|
||||
-- >>> splitExtension "/path/file.tar.gz"
|
||||
-- ("/path/file.tar",".gz")
|
||||
--
|
||||
-- prop> \path -> uncurry (BS.append) (splitExtension path) == path
|
||||
splitExtension :: RawFilePath -> (RawFilePath, ByteString)
|
||||
splitExtension x = if BS.null basename
|
||||
then (x,BS.empty)
|
||||
else (BS.append path (BS.init basename),BS.cons extSeparator fileExt)
|
||||
where
|
||||
(path,file) = splitFileNameRaw x
|
||||
(basename,fileExt) = BS.breakEnd isExtSeparator file
|
||||
|
||||
-- | Get the final extension from a 'RawFilePath'
|
||||
--
|
||||
-- >>> takeExtension "file.exe"
|
||||
-- ".exe"
|
||||
-- >>> takeExtension "file"
|
||||
-- ""
|
||||
-- >>> takeExtension "/path/file.tar.gz"
|
||||
-- ".gz"
|
||||
takeExtension :: RawFilePath -> ByteString
|
||||
takeExtension = snd . splitExtension
|
||||
|
||||
-- | Change a file's extension
|
||||
--
|
||||
-- prop> \path -> let ext = takeExtension path in replaceExtension path ext == path
|
||||
replaceExtension :: RawFilePath -> ByteString -> RawFilePath
|
||||
replaceExtension path ext = dropExtension path <.> ext
|
||||
|
||||
-- | Drop the final extension from a 'RawFilePath'
|
||||
--
|
||||
-- >>> dropExtension "file.exe"
|
||||
-- "file"
|
||||
-- >>> dropExtension "file"
|
||||
-- "file"
|
||||
-- >>> dropExtension "/path/file.tar.gz"
|
||||
-- "/path/file.tar"
|
||||
dropExtension :: RawFilePath -> RawFilePath
|
||||
dropExtension = fst . splitExtension
|
||||
|
||||
-- | Add an extension to a 'RawFilePath'
|
||||
--
|
||||
-- >>> addExtension "file" ".exe"
|
||||
-- "file.exe"
|
||||
-- >>> addExtension "file.tar" ".gz"
|
||||
-- "file.tar.gz"
|
||||
-- >>> addExtension "/path/" ".ext"
|
||||
-- "/path/.ext"
|
||||
addExtension :: RawFilePath -> ByteString -> RawFilePath
|
||||
addExtension file ext
|
||||
| BS.null ext = file
|
||||
| isExtSeparator (BS.head ext) = BS.append file ext
|
||||
| otherwise = BS.intercalate (BS.singleton extSeparator) [file, ext]
|
||||
|
||||
|
||||
-- | Operator version of 'addExtension'
|
||||
(<.>) :: RawFilePath -> ByteString -> RawFilePath
|
||||
(<.>) = addExtension
|
||||
|
||||
-- | Check if a 'RawFilePath' has an extension
|
||||
--
|
||||
-- >>> hasExtension "file"
|
||||
-- False
|
||||
-- >>> hasExtension "file.tar"
|
||||
-- True
|
||||
-- >>> hasExtension "/path.part1/"
|
||||
-- False
|
||||
hasExtension :: RawFilePath -> Bool
|
||||
hasExtension = isJust . BS.elemIndex extSeparator . takeFileName
|
||||
|
||||
-- | Split a 'RawFilePath' on the first extension
|
||||
--
|
||||
-- >>> splitExtensions "/path/file.tar.gz"
|
||||
-- ("/path/file",".tar.gz")
|
||||
--
|
||||
-- prop> \path -> uncurry addExtension (splitExtensions path) == path
|
||||
splitExtensions :: RawFilePath -> (RawFilePath, ByteString)
|
||||
splitExtensions x = if BS.null basename
|
||||
then (path,fileExt)
|
||||
else (BS.append path basename,fileExt)
|
||||
where
|
||||
(path,file) = splitFileNameRaw x
|
||||
(basename,fileExt) = BS.break isExtSeparator file
|
||||
|
||||
-- | Remove all extensions from a 'RawFilePath'
|
||||
--
|
||||
-- >>> dropExtensions "/path/file.tar.gz"
|
||||
-- "/path/file"
|
||||
dropExtensions :: RawFilePath -> RawFilePath
|
||||
dropExtensions = fst . splitExtensions
|
||||
|
||||
-- | Take all extensions from a 'RawFilePath'
|
||||
--
|
||||
-- >>> takeExtensions "/path/file.tar.gz"
|
||||
-- ".tar.gz"
|
||||
takeExtensions :: RawFilePath -> ByteString
|
||||
takeExtensions = snd . splitExtensions
|
||||
|
||||
------------------------
|
||||
-- more stuff
|
||||
|
||||
-- | Split a 'RawFilePath' into (path,file). 'combine' is the inverse
|
||||
--
|
||||
-- >>> splitFileName "path/file.txt"
|
||||
-- ("path/","file.txt")
|
||||
-- >>> splitFileName "path/"
|
||||
-- ("path/","")
|
||||
-- >>> splitFileName "file.txt"
|
||||
-- ("./","file.txt")
|
||||
--
|
||||
-- prop> \path -> uncurry combine (splitFileName path) == path || fst (splitFileName path) == "./"
|
||||
splitFileName :: RawFilePath -> (RawFilePath, RawFilePath)
|
||||
splitFileName x = if BS.null path
|
||||
then (dotSlash, file)
|
||||
else (path,file)
|
||||
where
|
||||
(path,file) = splitFileNameRaw x
|
||||
dotSlash = _period `BS.cons` (BS.singleton pathSeparator)
|
||||
|
||||
|
||||
-- | Get the file name
|
||||
--
|
||||
-- >>> takeFileName "path/file.txt"
|
||||
-- "file.txt"
|
||||
-- >>> takeFileName "path/"
|
||||
-- ""
|
||||
takeFileName :: RawFilePath -> RawFilePath
|
||||
takeFileName = snd . splitFileName
|
||||
|
||||
-- | Change the file name
|
||||
--
|
||||
-- prop> \path -> replaceFileName path (takeFileName path) == path
|
||||
replaceFileName :: RawFilePath -> ByteString -> RawFilePath
|
||||
replaceFileName x y = fst (splitFileNameRaw x) </> y
|
||||
|
||||
-- | Drop the file name
|
||||
--
|
||||
-- >>> dropFileName "path/file.txt"
|
||||
-- "path/"
|
||||
-- >>> dropFileName "file.txt"
|
||||
-- "./"
|
||||
dropFileName :: RawFilePath -> RawFilePath
|
||||
dropFileName = fst . splitFileName
|
||||
|
||||
-- | Get the file name, without a trailing extension
|
||||
--
|
||||
-- >>> takeBaseName "path/file.tar.gz"
|
||||
-- "file.tar"
|
||||
-- >>> takeBaseName ""
|
||||
-- ""
|
||||
takeBaseName :: RawFilePath -> ByteString
|
||||
takeBaseName = dropExtension . takeFileName
|
||||
|
||||
-- | Change the base name
|
||||
--
|
||||
-- >>> replaceBaseName "path/file.tar.gz" "bob"
|
||||
-- "path/bob.gz"
|
||||
--
|
||||
-- prop> \path -> replaceBaseName path (takeBaseName path) == path
|
||||
replaceBaseName :: RawFilePath -> ByteString -> RawFilePath
|
||||
replaceBaseName path name = combineRaw dir (name <.> ext)
|
||||
where
|
||||
(dir,file) = splitFileNameRaw path
|
||||
ext = takeExtension file
|
||||
|
||||
-- | Get the directory, moving up one level if it's already a directory
|
||||
--
|
||||
-- >>> takeDirectory "path/file.txt"
|
||||
-- "path"
|
||||
-- >>> takeDirectory "file"
|
||||
-- "."
|
||||
-- >>> takeDirectory "/path/to/"
|
||||
-- "/path/to"
|
||||
-- >>> takeDirectory "/path/to"
|
||||
-- "/path"
|
||||
takeDirectory :: RawFilePath -> RawFilePath
|
||||
takeDirectory x = case () of
|
||||
() | x == BS.singleton pathSeparator -> x
|
||||
| BS.null res && not (BS.null file) -> file
|
||||
| otherwise -> res
|
||||
where
|
||||
res = fst $ BS.spanEnd isPathSeparator file
|
||||
file = dropFileName x
|
||||
|
||||
-- | Change the directory component of a 'RawFilePath'
|
||||
--
|
||||
-- prop> \path -> replaceDirectory path (takeDirectory path) `equalFilePath` path || takeDirectory path == "."
|
||||
replaceDirectory :: RawFilePath -> ByteString -> RawFilePath
|
||||
replaceDirectory file dir = combineRaw dir (takeFileName file)
|
||||
|
||||
-- | Join two paths together
|
||||
--
|
||||
-- >>> combine "/" "file"
|
||||
-- "/file"
|
||||
-- >>> combine "/path/to" "file"
|
||||
-- "/path/to/file"
|
||||
-- >>> combine "file" "/absolute/path"
|
||||
-- "/absolute/path"
|
||||
combine :: RawFilePath -> RawFilePath -> RawFilePath
|
||||
combine a b | not (BS.null b) && isPathSeparator (BS.head b) = b
|
||||
| otherwise = combineRaw a b
|
||||
|
||||
-- | Operator version of combine
|
||||
(</>) :: RawFilePath -> RawFilePath -> RawFilePath
|
||||
(</>) = combine
|
||||
|
||||
-- | Split a path into a list of components:
|
||||
--
|
||||
-- >>> splitPath "/path/to/file.txt"
|
||||
-- ["/","path/","to/","file.txt"]
|
||||
--
|
||||
-- prop> \path -> BS.concat (splitPath path) == path
|
||||
splitPath :: RawFilePath -> [RawFilePath]
|
||||
splitPath = splitter
|
||||
where
|
||||
splitter x
|
||||
| BS.null x = []
|
||||
| otherwise = case BS.elemIndex pathSeparator x of
|
||||
Nothing -> [x]
|
||||
Just ix -> case BS.findIndex (not . isPathSeparator) $ BS.drop (ix+1) x of
|
||||
Nothing -> [x]
|
||||
Just runlen -> uncurry (:) . second splitter $ BS.splitAt (ix+1+runlen) x
|
||||
|
||||
-- | Like 'splitPath', but without trailing slashes
|
||||
--
|
||||
-- >>> splitDirectories "/path/to/file.txt"
|
||||
-- ["/","path","to","file.txt"]
|
||||
-- >>> splitDirectories ""
|
||||
-- []
|
||||
splitDirectories :: RawFilePath -> [RawFilePath]
|
||||
splitDirectories x
|
||||
| BS.null x = []
|
||||
| isPathSeparator (BS.head x) = let (root,rest) = BS.splitAt 1 x
|
||||
in root : splitter rest
|
||||
| otherwise = splitter x
|
||||
where
|
||||
splitter = filter (not . BS.null) . BS.split pathSeparator
|
||||
|
||||
-- | Join a split path back together
|
||||
--
|
||||
-- prop> \path -> joinPath (splitPath path) == path
|
||||
--
|
||||
-- >>> joinPath ["path","to","file.txt"]
|
||||
-- "path/to/file.txt"
|
||||
joinPath :: [RawFilePath] -> RawFilePath
|
||||
joinPath = foldr (</>) BS.empty
|
||||
|
||||
|
||||
-- |Normalise a file.
|
||||
--
|
||||
-- >>> normalise "/file/\\test////"
|
||||
-- "/file/\\test/"
|
||||
-- >>> normalise "/file/./test"
|
||||
-- "/file/test"
|
||||
-- >>> normalise "/test/file/../bob/fred/"
|
||||
-- "/test/file/../bob/fred/"
|
||||
-- >>> normalise "../bob/fred/"
|
||||
-- "../bob/fred/"
|
||||
-- >>> normalise "./bob/fred/"
|
||||
-- "bob/fred/"
|
||||
-- >>> normalise "./bob////.fred/./...///./..///#."
|
||||
-- "bob/.fred/.../../#."
|
||||
-- >>> normalise "."
|
||||
-- "."
|
||||
-- >>> normalise "./"
|
||||
-- "./"
|
||||
-- >>> normalise "./."
|
||||
-- "./"
|
||||
-- >>> normalise "/./"
|
||||
-- "/"
|
||||
-- >>> normalise "/"
|
||||
-- "/"
|
||||
-- >>> normalise "bob/fred/."
|
||||
-- "bob/fred/"
|
||||
-- >>> normalise "//home"
|
||||
-- "/home"
|
||||
normalise :: RawFilePath -> RawFilePath
|
||||
normalise filepath =
|
||||
result `BS.append`
|
||||
(if addPathSeparator
|
||||
then BS.singleton pathSeparator
|
||||
else BS.empty)
|
||||
where
|
||||
result = let n = f filepath
|
||||
in if BS.null n
|
||||
then BS.singleton _period
|
||||
else n
|
||||
addPathSeparator = isDirPath filepath &&
|
||||
not (hasTrailingPathSeparator result)
|
||||
isDirPath xs = hasTrailingPathSeparator xs
|
||||
|| not (BS.null xs) && BS.last xs == _period
|
||||
&& hasTrailingPathSeparator (BS.init xs)
|
||||
f = joinPath . dropDots . propSep . splitDirectories
|
||||
propSep :: [ByteString] -> [ByteString]
|
||||
propSep (x:xs)
|
||||
| BS.all (== pathSeparator) x = BS.singleton pathSeparator : xs
|
||||
| otherwise = x : xs
|
||||
propSep [] = []
|
||||
dropDots :: [ByteString] -> [ByteString]
|
||||
dropDots = filter (BS.singleton _period /=)
|
||||
|
||||
|
||||
------------------------
|
||||
-- trailing path separators
|
||||
|
||||
-- | Check if the last character of a 'RawFilePath' is '/'.
|
||||
--
|
||||
-- >>> hasTrailingPathSeparator "/path/"
|
||||
-- True
|
||||
-- >>> hasTrailingPathSeparator "/"
|
||||
-- True
|
||||
-- >>> hasTrailingPathSeparator "/path"
|
||||
-- False
|
||||
hasTrailingPathSeparator :: RawFilePath -> Bool
|
||||
hasTrailingPathSeparator x
|
||||
| BS.null x = False
|
||||
| otherwise = isPathSeparator $ BS.last x
|
||||
|
||||
-- | Add a trailing path separator.
|
||||
--
|
||||
-- >>> addTrailingPathSeparator "/path"
|
||||
-- "/path/"
|
||||
-- >>> addTrailingPathSeparator "/path/"
|
||||
-- "/path/"
|
||||
-- >>> addTrailingPathSeparator "/"
|
||||
-- "/"
|
||||
addTrailingPathSeparator :: RawFilePath -> RawFilePath
|
||||
addTrailingPathSeparator x = if hasTrailingPathSeparator x
|
||||
then x
|
||||
else x `BS.snoc` pathSeparator
|
||||
|
||||
-- | Remove a trailing path separator
|
||||
--
|
||||
-- >>> dropTrailingPathSeparator "/path/"
|
||||
-- "/path"
|
||||
-- >>> dropTrailingPathSeparator "/path////"
|
||||
-- "/path"
|
||||
-- >>> dropTrailingPathSeparator "/"
|
||||
-- "/"
|
||||
-- >>> dropTrailingPathSeparator "//"
|
||||
-- "/"
|
||||
dropTrailingPathSeparator :: RawFilePath -> RawFilePath
|
||||
dropTrailingPathSeparator x
|
||||
| x == BS.singleton pathSeparator = x
|
||||
| otherwise = if hasTrailingPathSeparator x
|
||||
then dropTrailingPathSeparator $ BS.init x
|
||||
else x
|
||||
|
||||
------------------------
|
||||
-- Filename/system stuff
|
||||
|
||||
-- | Check if a path is absolute
|
||||
--
|
||||
-- >>> isAbsolute "/path"
|
||||
-- True
|
||||
-- >>> isAbsolute "path"
|
||||
-- False
|
||||
-- >>> isAbsolute ""
|
||||
-- False
|
||||
isAbsolute :: RawFilePath -> Bool
|
||||
isAbsolute x
|
||||
| BS.length x > 0 = isPathSeparator (BS.head x)
|
||||
| otherwise = False
|
||||
|
||||
-- | Check if a path is relative
|
||||
--
|
||||
-- prop> \path -> isRelative path /= isAbsolute path
|
||||
isRelative :: RawFilePath -> Bool
|
||||
isRelative = not . isAbsolute
|
||||
|
||||
-- | Is a FilePath valid, i.e. could you create a file like it?
|
||||
--
|
||||
-- >>> isValid ""
|
||||
-- False
|
||||
-- >>> isValid "\0"
|
||||
-- False
|
||||
-- >>> isValid "/random_ path:*"
|
||||
-- True
|
||||
isValid :: RawFilePath -> Bool
|
||||
isValid filepath
|
||||
| BS.null filepath = False
|
||||
| _nul `BS.elem` filepath = False
|
||||
| otherwise = True
|
||||
|
||||
-- |Equality of two filepaths. The filepaths are normalised
|
||||
-- and trailing path separators are dropped.
|
||||
--
|
||||
-- >>> equalFilePath "foo" "foo"
|
||||
-- True
|
||||
-- >>> equalFilePath "foo" "foo/"
|
||||
-- True
|
||||
-- >>> equalFilePath "foo" "./foo"
|
||||
-- True
|
||||
-- >>> equalFilePath "foo" "/foo"
|
||||
-- False
|
||||
-- >>> equalFilePath "foo" "FOO"
|
||||
-- False
|
||||
-- >>> equalFilePath "foo" "../foo"
|
||||
-- False
|
||||
--
|
||||
-- prop> \p -> equalFilePath p p
|
||||
equalFilePath :: RawFilePath -> RawFilePath -> Bool
|
||||
equalFilePath p1 p2 = f p1 == f p2
|
||||
where
|
||||
f x = dropTrailingPathSeparator $ normalise x
|
||||
|
||||
------------------------
|
||||
-- internal stuff
|
||||
|
||||
-- Just split the input FileName without adding/normalizing or changing
|
||||
-- anything.
|
||||
splitFileNameRaw :: RawFilePath -> (RawFilePath, RawFilePath)
|
||||
splitFileNameRaw x = BS.breakEnd isPathSeparator x
|
||||
|
||||
-- | Combine two paths, assuming rhs is NOT absolute.
|
||||
combineRaw :: RawFilePath -> RawFilePath -> RawFilePath
|
||||
combineRaw a b | BS.null a = b
|
||||
| BS.null b = a
|
||||
| isPathSeparator (BS.last a) = BS.append a b
|
||||
| otherwise = BS.intercalate (BS.singleton pathSeparator) [a, b]
|
||||
|
||||
225
test/Main.hs
225
test/Main.hs
@@ -1,225 +0,0 @@
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
|
||||
-- | Test suite.
|
||||
|
||||
module Main where
|
||||
|
||||
import Control.Applicative
|
||||
import Control.Monad
|
||||
import Data.Maybe
|
||||
import Data.Monoid
|
||||
import HPath
|
||||
import HPath.Internal
|
||||
import Test.Hspec
|
||||
|
||||
-- | Test suite entry point, returns exit failure if any test fails.
|
||||
main :: IO ()
|
||||
main = hspec spec
|
||||
|
||||
-- | Test suite.
|
||||
spec :: Spec
|
||||
spec =
|
||||
do describe "Parsing: Path Abs Dir" parseAbsTPSSpec
|
||||
describe "Parsing: Path Rel Dir" parseRelTPSSpec
|
||||
describe "Parsing: Path Abs File" parseAbsNoTPSSpec
|
||||
describe "Parsing: Path Rel File" parseRelNoTPSSpec
|
||||
describe "Operations: (</>)" operationAppend
|
||||
describe "Operations: stripDir" operationStripDir
|
||||
describe "Operations: isParentOf" operationIsParentOf
|
||||
describe "Operations: dirname" operationDirname
|
||||
describe "Operations: basename" operationBasename
|
||||
describe "Restrictions" restrictions
|
||||
|
||||
-- | Restricting the input of any tricks.
|
||||
restrictions :: Spec
|
||||
restrictions =
|
||||
do parseFails "~/"
|
||||
parseFails "~/foo"
|
||||
parseFails "~/foo/bar"
|
||||
parseFails "../"
|
||||
parseFails ".."
|
||||
parseFails "."
|
||||
parseFails "/.."
|
||||
parseFails "/foo/../bar/"
|
||||
parseFails "/foo/bar/.."
|
||||
where parseFails x =
|
||||
it (show x ++ " should be rejected")
|
||||
(isNothing (void (parseAbsTPS x) <|>
|
||||
void (parseRelTPS x) <|>
|
||||
void (parseAbsNoTPS x) <|>
|
||||
void (parseRelNoTPS x)))
|
||||
|
||||
-- | The 'basename' operation.
|
||||
operationBasename :: Spec
|
||||
operationBasename =
|
||||
do it "basename ($(mkAbsTPS parent) </> basename $(mkRelNoTPS filename)) == $(mkRelNoTPS filename)"
|
||||
((basename =<< ($(mkAbsTPS "/home/hasufell/") </>)
|
||||
<$> basename $(mkRelNoTPS "bar.txt")) ==
|
||||
Just $(mkRelNoTPS "bar.txt"))
|
||||
it "basename ($(mkRelTPS parent) </> basename $(mkRelNoTPS filename)) == $(mkRelNoTPS filename)"
|
||||
((basename =<< ($(mkRelTPS "home/hasufell/") </>)
|
||||
<$> basename $(mkRelNoTPS "bar.txt")) ==
|
||||
Just $(mkRelNoTPS "bar.txt"))
|
||||
|
||||
-- | The 'dirname' operation.
|
||||
operationDirname :: Spec
|
||||
operationDirname =
|
||||
do it "dirname (parent </> child) == parent"
|
||||
(dirname ($(mkAbsTPS "/foo") </>
|
||||
$(mkRelTPS "bar")) ==
|
||||
$(mkAbsTPS "/foo"))
|
||||
it "dirname \"\" == \"\""
|
||||
(dirname $(mkAbsTPS "/") ==
|
||||
$(mkAbsTPS "/"))
|
||||
it "dirname (parent \"\") == \"\""
|
||||
(dirname (dirname $(mkAbsTPS "/")) ==
|
||||
$(mkAbsTPS "/"))
|
||||
|
||||
-- | The 'isParentOf' operation.
|
||||
operationIsParentOf :: Spec
|
||||
operationIsParentOf =
|
||||
do it "isParentOf parent (parent </> child)"
|
||||
(isParentOf
|
||||
$(mkAbsTPS "///bar/")
|
||||
($(mkAbsTPS "///bar/") </>
|
||||
$(mkRelNoTPS "bar/foo.txt")))
|
||||
it "isParentOf parent (parent </> child)"
|
||||
(isParentOf
|
||||
$(mkRelTPS "bar/")
|
||||
($(mkRelTPS "bar/") </>
|
||||
$(mkRelNoTPS "bob/foo.txt")))
|
||||
|
||||
-- | The 'stripDir' operation.
|
||||
operationStripDir :: Spec
|
||||
operationStripDir =
|
||||
do it "stripDir parent (parent </> child) = child"
|
||||
(stripDir $(mkAbsTPS "///bar/")
|
||||
($(mkAbsTPS "///bar/") </>
|
||||
$(mkRelNoTPS "bar/foo.txt")) ==
|
||||
Just $(mkRelNoTPS "bar/foo.txt"))
|
||||
it "stripDir parent (parent </> child) = child"
|
||||
(stripDir $(mkRelTPS "bar/")
|
||||
($(mkRelTPS "bar/") </>
|
||||
$(mkRelNoTPS "bob/foo.txt")) ==
|
||||
Just $(mkRelNoTPS "bob/foo.txt"))
|
||||
it "stripDir parent parent = _|_"
|
||||
(stripDir $(mkAbsTPS "/home/hasufell/foo")
|
||||
$(mkAbsTPS "/home/hasufell/foo") ==
|
||||
Nothing)
|
||||
|
||||
-- | The '</>' operation.
|
||||
operationAppend :: Spec
|
||||
operationAppend =
|
||||
do it "AbsDir + RelDir = AbsDir"
|
||||
($(mkAbsTPS "/home/") </>
|
||||
$(mkRelTPS "hasufell") ==
|
||||
$(mkAbsTPS "/home/hasufell/"))
|
||||
it "AbsDir + RelFile = AbsFile"
|
||||
($(mkAbsTPS "/home/") </>
|
||||
$(mkRelNoTPS "hasufell/test.txt") ==
|
||||
$(mkAbsNoTPS "/home/hasufell/test.txt"))
|
||||
it "RelDir + RelDir = RelDir"
|
||||
($(mkRelTPS "home/") </>
|
||||
$(mkRelTPS "hasufell") ==
|
||||
$(mkRelTPS "home/hasufell"))
|
||||
it "RelDir + RelFile = RelFile"
|
||||
($(mkRelTPS "home/") </>
|
||||
$(mkRelNoTPS "hasufell/test.txt") ==
|
||||
$(mkRelNoTPS "home/hasufell/test.txt"))
|
||||
|
||||
-- | Tests for the tokenizer.
|
||||
parseAbsTPSSpec :: Spec
|
||||
parseAbsTPSSpec =
|
||||
do failing ""
|
||||
failing "./"
|
||||
failing "~/"
|
||||
failing "foo.txt"
|
||||
succeeding "/" (MkPath "/")
|
||||
succeeding "//" (MkPath "/")
|
||||
succeeding "///foo//bar//mu/" (MkPath "/foo/bar/mu/")
|
||||
succeeding "///foo//bar////mu" (MkPath "/foo/bar/mu/")
|
||||
succeeding "///foo//bar/.//mu" (MkPath "/foo/bar/mu/")
|
||||
where failing x = parserTest parseAbsTPS x Nothing
|
||||
succeeding x with = parserTest parseAbsTPS x (Just with)
|
||||
|
||||
-- | Tests for the tokenizer.
|
||||
parseRelTPSSpec :: Spec
|
||||
parseRelTPSSpec =
|
||||
do failing ""
|
||||
failing "/"
|
||||
failing "//"
|
||||
failing "~/"
|
||||
failing "/"
|
||||
failing "./"
|
||||
failing "././"
|
||||
failing "//"
|
||||
failing "///foo//bar//mu/"
|
||||
failing "///foo//bar////mu"
|
||||
failing "///foo//bar/.//mu"
|
||||
succeeding "..." (MkPath ".../")
|
||||
succeeding "foo.bak" (MkPath "foo.bak/")
|
||||
succeeding "./foo" (MkPath "foo/")
|
||||
succeeding "././foo" (MkPath "foo/")
|
||||
succeeding "./foo/./bar" (MkPath "foo/bar/")
|
||||
succeeding "foo//bar//mu//" (MkPath "foo/bar/mu/")
|
||||
succeeding "foo//bar////mu" (MkPath "foo/bar/mu/")
|
||||
succeeding "foo//bar/.//mu" (MkPath "foo/bar/mu/")
|
||||
where failing x = parserTest parseRelTPS x Nothing
|
||||
succeeding x with = parserTest parseRelTPS x (Just with)
|
||||
|
||||
-- | Tests for the tokenizer.
|
||||
parseAbsNoTPSSpec :: Spec
|
||||
parseAbsNoTPSSpec =
|
||||
do failing ""
|
||||
failing "./"
|
||||
failing "~/"
|
||||
failing "./foo.txt"
|
||||
failing "/"
|
||||
failing "//"
|
||||
failing "///foo//bar//mu/"
|
||||
succeeding "/..." (MkPath "/...")
|
||||
succeeding "/foo.txt" (MkPath "/foo.txt")
|
||||
succeeding "///foo//bar////mu.txt" (MkPath "/foo/bar/mu.txt")
|
||||
succeeding "///foo//bar/.//mu.txt" (MkPath "/foo/bar/mu.txt")
|
||||
where failing x = parserTest parseAbsNoTPS x Nothing
|
||||
succeeding x with = parserTest parseAbsNoTPS x (Just with)
|
||||
|
||||
-- | Tests for the tokenizer.
|
||||
parseRelNoTPSSpec :: Spec
|
||||
parseRelNoTPSSpec =
|
||||
do failing ""
|
||||
failing "/"
|
||||
failing "//"
|
||||
failing "~/"
|
||||
failing "/"
|
||||
failing "./"
|
||||
failing "//"
|
||||
failing "///foo//bar//mu/"
|
||||
failing "///foo//bar////mu"
|
||||
failing "///foo//bar/.//mu"
|
||||
succeeding "..." (MkPath "...")
|
||||
succeeding "foo.txt" (MkPath "foo.txt")
|
||||
succeeding "./foo.txt" (MkPath "foo.txt")
|
||||
succeeding "././foo.txt" (MkPath "foo.txt")
|
||||
succeeding "./foo/./bar.txt" (MkPath "foo/bar.txt")
|
||||
succeeding "foo//bar//mu.txt" (MkPath "foo/bar/mu.txt")
|
||||
succeeding "foo//bar////mu.txt" (MkPath "foo/bar/mu.txt")
|
||||
succeeding "foo//bar/.//mu.txt" (MkPath "foo/bar/mu.txt")
|
||||
where failing x = parserTest parseRelNoTPS x Nothing
|
||||
succeeding x with = parserTest parseRelNoTPS x (Just with)
|
||||
|
||||
-- | Parser test.
|
||||
parserTest :: (Show a1,Show a,Eq a1)
|
||||
=> (a -> Maybe a1) -> a -> Maybe a1 -> SpecWith ()
|
||||
parserTest parser input expected =
|
||||
it ((case expected of
|
||||
Nothing -> "Failing: "
|
||||
Just{} -> "Succeeding: ") <>
|
||||
"Parsing " <>
|
||||
show input <>
|
||||
" " <>
|
||||
case expected of
|
||||
Nothing -> "should fail."
|
||||
Just x -> "should succeed with: " <> show x)
|
||||
(actual == expected)
|
||||
where actual = parser input
|
||||
Reference in New Issue
Block a user