67 Commits
0.3.0 ... dev

Author SHA1 Message Date
cfe626b6d4 Update README 2016-05-09 13:37:21 +02:00
a946387330 Merge posix-paths into hpath 2016-05-09 13:31:20 +02:00
1263fac7ec Update submodule 2016-05-02 19:10:40 +02:00
45b515d1db Add withAbsPath/withRelPath/withFnPath 2016-04-17 18:09:50 +02:00
a5360f29a3 Don't re-export functions 2016-04-16 19:21:19 +02:00
f1875da780 Use latest posix-paths
Pending upstream PR.
2016-04-16 19:12:23 +02:00
7e8c745e35 Clean up, rewrite stuff 2016-04-16 18:17:44 +02:00
577ecf6750 Add normalizeAbs, which may or may not be useful 2016-04-15 14:23:25 +02:00
8f7e5806e3 Be more strict
* don't rely on OverloadedString
* make basename fail if run on root dir "/"
* reject "." and ".." in filename parser
* reject ".." in path parsers
* normalise paths in parsers
2016-04-15 14:23:07 +02:00
c570505297 Add hiddenFile function 2016-04-10 21:50:52 +02:00
148eeb619f Implement String -> ByteString and vice versa conversion for filepaths 2016-04-05 00:55:25 +02:00
877d8c4089 Implement equalFilePath 2016-04-05 00:54:55 +02:00
8c1bd139c0 Readd TH constructors 2016-04-05 00:54:36 +02:00
bd71947b2a Use word8 package 2016-04-04 18:46:25 +02:00
491efe44a3 Use ByteString for paths instead of String 2016-04-04 17:29:35 +02:00
hasufell
c7229061d0 Fix encoding issues with realPath 2016-04-04 03:06:22 +02:00
3a52a9ea4b Add getAllParents 2016-04-03 16:20:38 +02:00
3c3a2d2766 Switch to new layout 2016-03-30 02:47:42 +02:00
d15e4b8ad9 Fork chrisdone's path library
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.

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

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
2016-03-08 22:53:42 +01:00
Chris Done
e2974d3152 Bump to 0.5.7 2016-03-06 17:43:49 +01:00
Chris Done
dbee82c69b Merge pull request #14 from felixonmars/patch-1
Fix a haddock parse error
2016-03-06 17:43:02 +01:00
Felix Yan
05271c94af Fix a haddock parse error
The line was triggering parse error when running haddock:

```
src/Path.hs:16:1:
    parse error on input ‘-- | A normalizing well-typed path type.’
```

Removing it works for me.
2016-03-05 13:44:08 +08:00
Chris Done
9d357b24c8 Remove redundant check for /= . or .. 2016-03-04 15:11:06 +01:00
Chris Done
62c681819c Update test suite 2016-03-04 15:09:21 +01:00
Chris Done
c41fa86d17 Bump to 0.5.6 2016-03-04 15:07:00 +01:00
Chris Done
04608e0e53 Reject only .. and . (#13) 2016-03-04 15:06:27 +01:00
Chris Done
47450b5dd6 Bump to 0.5.5 2016-03-04 14:50:28 +01:00
Chris Done
540c24f3a5 Use FilePath.isValid (#12) 2016-03-04 14:49:21 +01:00
Chris Done
b9b921db84 Bump to 0.5.4 2016-03-04 14:41:16 +01:00
Chris Done
2e109c86d8 Reject path consisting of all "." (fixes #13)
Ping @mrkkrp
2016-03-04 14:39:17 +01:00
Chris Done
04fa5d3ea8 Merge pull request #9 from magthe/ClarifyRelative
Clarify what limitations the RelX parsers have.
2016-03-04 14:29:52 +01:00
Chris Done
88eb4f32a8 Merge pull request #12 from mrkkrp/master
Improve description of the package
2016-01-31 11:37:59 +00:00
mrkkrp
01d0f5c968 Add standard header to the ‘Path’ module
The header is nice to have, it's displayed in Haddocks.
2016-01-28 18:10:27 +06:00
mrkkrp
c281f3755e Add year 2016 to copyright notices 2016-01-28 18:10:09 +06:00
mrkkrp
d6674c5ee1 Improve the ‘README.md’ file
Here I copied the blog post announcing the library, because it describes
it very well, and not everyone who discovers the library will know where
to look for such a comprehensive description.

http://chrisdone.com/posts/path-package

I've made two edits to that post to reflect new things:

1. On line 123 there is a mention of ‘fromAbsDir’ and other similar
   functions.

2. On line 363 I've put a link to my ‘path-io’ package that provides
   well-typed interface to ‘directory’ and ‘temporary’. I've written the
   package for my personal needs, because I was tired of the endless
   conversion and I wanted things like recursive copying of
   directories. When I published it, someone opened an issue asking to
   add some functions from Stack's ‘Path.IO’ — that's what I'm going to
   do. I expect it will be able to replace ‘Path.IO’ in Stack soon. I've
   talked to Stack maintainers and they like the package and have
   nothing against the switch.
2016-01-28 18:02:41 +06:00
mrkkrp
de73f8b4ea Improve synopsis and description 2016-01-28 17:47:15 +06:00
Chris Done
9fee9e42da Merge pull request #11 from mrkkrp/master
Fix a typo
2016-01-17 20:31:14 +00:00
mrkkrp
36cc9af231 Fix a typo 2016-01-16 17:22:34 +06:00
Chris Done
9af2a76b2e Merge pull request #10 from rimmington/nfdata
Add NFData instance for Path
2016-01-15 10:23:50 +00:00
Rhys
cd470f2d11 Add NFData instance for Path 2016-01-15 12:07:27 +11:00
Magnus Therning
b5cff315a1 Clarify what limitations the RelX parsers have.
Signed-off-by: Magnus Therning <magnus@therning.org>
2016-01-07 16:15:06 +01:00
Chris Done
28c4f7fe21 Bump to 0.5.3 2015-11-21 19:54:59 +01:00
Chris Done
95fab22567 Merge pull request #7 from mrkkrp/master
Various improvements
2015-11-21 19:53:16 +01:00
mrkkrp
00fabad1f4 Add type-safe synonyms of ‘toFilePath’
This helps to “double check” programmers' assumptions about what kind of
path he is converting into ‘FilePath’. Without these synonyms it's
possible to silently convert wrong type of path into ‘FilePath’.
2015-11-21 16:15:34 +06:00
mrkkrp
733fa04ac3 Write ‘isPrefixOf’ and friends in infix form 2015-11-21 16:14:54 +06:00
mrkkrp
339f7587a3 Remove unused language pragmas 2015-11-21 16:13:52 +06:00
mrkkrp
0476db8ebc Add ‘.stack-work’ to ‘.gitignore’ 2015-11-21 16:12:12 +06:00
Chris Done
57e886b71f Merge pull request #6 from mgsloan/properties-doc-fix
Improve a few property definitions
2015-10-08 09:13:13 -07:00
Michael Sloan
1841d7451c Improve a few property definitions
The properties for stripDir and parent used the name 'parent' as a
variable.

The properties for filename and dirname seemed to be wrong and also used
'parent' as a variable.
2015-10-08 03:00:50 -07:00
Chris Done
62278a77e6 Merge pull request #5 from glasserc/rel-file-tests
test: missing case for parseRelFile
2015-07-09 03:07:35 +02:00
Ethan Glasser-Camp
a4ed4cd504 test: missing case for parseRelFile 2015-07-08 20:28:15 -04:00
Chris Done
c637893aa8 Bump to 0.5.2 2015-06-21 12:19:11 +02:00
Chris Done
09996f1605 Merge pull request #4 from bergmark/no-derivegeneric
Remove unused DeriveGeneric extension, fixes GHC 7.0 build
2015-06-21 12:18:07 +02:00
Adam Bergmark
9f229ecef3 Remove unused DeriveGeneric extension, fixes GHC 7.0 build 2015-06-21 02:29:25 +02:00
Chris Done
44d9e02cc2 Add note in docs about dropTrailingPathSeparator 2015-06-18 08:58:01 +02:00
Chris Done
94a5252ff5 Bump to 0.5.1 2015-06-17 18:45:43 +02:00
Chris Done
df1e191517 Fix syntax error 2015-06-17 18:45:34 +02:00
Chris Done
30890ad3c2 Merge pull request #3 from snoyberg/master
Handle parent directory checks on Windows
2015-06-17 18:43:01 +02:00
Michael Snoyman
9ca83d66b8 Handle parent directory checks on Windows 2015-06-17 19:21:45 +03:00
Chris Done
4e1816392a Fix stripDir p p /= Nothing bug 2015-05-27 17:08:03 +02:00
Chris Done
8f3b169760 Bump to 0.4.0 2015-05-22 11:35:52 +02:00
Chris Done
ffdf4af243 Generalize stripDir to MonadThrow 2015-05-22 11:35:52 +02:00
Chris Done
87a56a93b8 Disallow .. 2015-05-22 11:35:52 +02:00
Chris Done
cb6f472524 Merge pull request #2 from kraai/patch-2
Add source repository information
2015-05-14 17:54:33 +02:00
Chris Done
bff31277e3 Merge pull request #1 from kraai/patch-1
Add ellipsis
2015-05-14 17:48:11 +02:00
kraai
b06a8c9528 Add source repository information 2015-05-14 15:47:04 +00:00
kraai
0c1dd7e493 Add ellipsis 2015-05-14 15:44:41 +00:00
18 changed files with 1682 additions and 525 deletions

3
.gitignore vendored
View File

@@ -7,3 +7,6 @@ cabal-dev/
TAGS
tags
*.tag
.stack-work/
.cabal-sandbox/
cabal.sandbox.config

View File

@@ -1,4 +1,33 @@
0.5.8:
* First version of the fork.
0.5.7:
* Fix haddock problem.
0.5.6:
* Reject only .. and .
0.5.5:
* Use filepath's isValid function for additional sanity checks
0.5.4:
* Disable parsing of path consisting only of "."
* Add NFData instance for Path
* Some typo/docs improvements
* Add standard headers to modules
0.5.3:
* Added conversion functions.
0.2.0:
* Rename parentAbs to simply parent.
* Add dirname.
0.3.0:
* Removed Generic instance.
0.4.0:
* Implemented stricter parsing, disabling use of "..".
* Made stripDir generic over MonadThrow
0.5.0:
* Fix stripDir p p /= Nothing bug.
0.5.2:
* Removed unused DeriveGeneric.

View File

@@ -1,4 +1,5 @@
Copyright (c) 2015, FP Complete
Copyright (c) 20152016, FP Complete
Copyright (c) 2016, Julian Ospald
All rights reserved.
Redistribution and use in source and binary forms, with or without

View File

@@ -1,4 +1,42 @@
path
=====
# HPath
Support for well-typed paths in Haskell. Also provides ByteString based filepath
manipulation.
## Motivation
The motivation came during development of
[hsfm](https://github.com/hasufell/hsfm)
which has a pretty strict File type, but lacks a strict Path type, e.g.
for user input.
The library that came closest to my needs was
[path](https://github.com/chrisdone/path),
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'
* 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
## Differences to 'posix-paths'
* `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`
Support for well-typed paths in Haskell.

90
benchmarks/Bench.hs Normal file
View 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
View 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
View 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
View 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
View 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
]

83
hpath.cabal Normal file
View File

@@ -0,0 +1,83 @@
name: hpath
version: 0.5.8
synopsis: Support for well-typed paths
description: Support for will-typed paths.
license: BSD3
license-file: LICENSE
author: Julian Ospald <hasufell@posteo.de>
maintainer: Julian Ospald <hasufell@posteo.de>
copyright: 20152016 FP Complete, Julian Ospald 2016
category: Filesystem
build-type: Simple
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
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 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

View File

@@ -1,31 +0,0 @@
name: path
version: 0.3.0
synopsis: Path
description: Path
license: BSD3
license-file: LICENSE
author: Chris Done
maintainer: chrisdone@fpcomplete.com
copyright: 2015 FP Complete
category: Filesystem
build-type: Simple
cabal-version: >=1.8
library
hs-source-dirs: src/
ghc-options: -Wall -O2
exposed-modules: Path, Path.Internal
build-depends: base >= 4 && <5
, exceptions
, filepath
, template-haskell
test-suite test
type: exitcode-stdio-1.0
main-is: Main.hs
hs-source-dirs: test
build-depends: HUnit
, base
, hspec
, mtl
, path

492
src/HPath.hs Normal file
View File

@@ -0,0 +1,492 @@
-- |
-- Module : HPath
-- Copyright : © 20152016 FP Complete, 2016 Julian Ospald
-- License : BSD 3 clause
--
-- Maintainer : Julian Ospald <hasufell@posteo.de>
-- Stability : experimental
-- Portability : portable
--
-- Support for well-typed paths.
{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE EmptyDataDecls #-}
{-# LANGUAGE PatternSynonyms #-}
{-# OPTIONS_HADDOCK ignore-exports #-}
module HPath
(
-- * Types
Abs
,Path
,Rel
,Fn
,PathParseException
-- * PatternSynonyms/ViewPatterns
,pattern Path
-- * Path Parsing
,parseAbs
,parseFn
,parseRel
-- * Path Conversion
,canonicalizePath
,fromAbs
,fromRel
,normalize
,toFilePath
-- * Path Operations
,(</>)
,basename
,dirname
,isParentOf
,getAllParents
,stripDir
-- * 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 qualified Data.List as L
import Data.Maybe
import Data.Word8
import HPath.Internal
import System.Posix.FilePath hiding ((</>))
import System.Posix.Directory.Traversals(realpath)
--------------------------------------------------------------------------------
-- Types
-- | An absolute path.
data Abs deriving (Typeable)
-- | A relative path; one without a root.
data Rel deriving (Typeable)
-- | A filename, without any '/'.
data Fn deriving (Typeable)
-- | Exception when parsing a location.
data PathParseException
= 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)
--------------------------------------------------------------------------------
-- Path Parsers
-- | Get a location for an absolute path. Produces a normalised path.
--
-- Throws: 'PathParseException'
--
-- >>> 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 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'
--
-- >>> 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) &&
isValid filepath
then return (MkPath $ normalise filepath)
else throwM (InvalidRel filepath)
-- | Parses a filename. Filenames must not contain slashes.
-- Excludes '.' and '..'.
--
-- Throws: 'PathParseException'
--
-- >>> 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)
--------------------------------------------------------------------------------
-- Path Conversion
-- | Convert to a ByteString type.
toFilePath :: Path b -> ByteString
toFilePath (MkPath l) = l
fromAbs :: Path Abs -> ByteString
fromAbs = toFilePath
fromRel :: RelC r => Path r -> ByteString
fromRel = toFilePath
normalize :: Path t -> Path t
normalize (MkPath l) = MkPath $ normalise l
--------------------------------------------------------------------------------
-- Path Operations
-- | Append two paths.
--
-- The second argument must always be a relative path, which ensures
-- that undefinable things like `"/abc" </> "/def"` cannot happen.
--
-- 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.
--
-- >>> (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' = 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 -> Path b -> m (Path Rel)
stripDir (MkPath p) (MkPath l) =
case stripPrefix p' l of
Nothing -> throwM (Couldn'tStripPrefixTPS p' l)
Just ok -> if BS.null ok
then throwM (Couldn'tStripPrefixTPS p' l)
else return (MkPath ok)
where
p' = addTrailingPathSeparator p
-- | Is p a parent of the given location? Implemented in terms of
-- 'stripDir'. The bases must match.
--
-- >>> (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.
--
-- The following properties hold:
--
-- @dirname (p \<\/> a) == dirname p@
--
-- >>> 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.
--
--
-- The following properties hold:
--
-- @basename (p \<\/> a) == basename a@
--
-- 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 (isAbsolute rl) = return $ MkPath rl
| otherwise = throwM RootDirHasNoBasename
where
rl = last . splitPath . dropTrailingPathSeparator $ l
--------------------------------------------------------------------------------
-- Path IO helpers
-- | 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)

View File

@@ -1,26 +1,26 @@
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
-- | Internal types and functions.
module Path.Internal
(Path(..))
module HPath.Internal
(Path(..)
,RelC)
where
import Control.DeepSeq (NFData (..))
import Data.ByteString (ByteString)
import Data.Data
-- | Path of some base and type.
--
-- Internally is a string. The string can be of two formats only:
--
-- 1. File format: @file.txt@, @foo\/bar.txt@, @\/foo\/bar.txt@
-- 2. Directory format: @foo\/@, @\/foo\/bar\/@
-- 1. without trailing path separator: @file.txt@, @foo\/bar.txt@, @\/foo\/bar.txt@
-- 2. with trailing path separator: @foo\/@, @\/foo\/bar\/@
--
-- All directories end in a trailing separator. There are no duplicate
-- There are no duplicate
-- path separators @\/\/@, no @..@, no @.\/@, no @~\/@, etc.
newtype Path b t = Path FilePath
data Path b = MkPath ByteString
deriving (Typeable)
-- | String equality.
@@ -28,21 +28,28 @@ newtype Path b t = Path FilePath
-- The following property holds:
--
-- @show x == show y ≡ x == y@
instance Eq (Path b t) where
(==) (Path x) (Path y) = x == y
instance Eq (Path b) where
(==) (MkPath x) (MkPath y) = x == y
-- | String ordering.
--
-- The following property holds:
--
-- @show x \`compare\` show y ≡ x \`compare\` y@
instance Ord (Path b t) where
compare (Path x) (Path y) = compare x y
instance Ord (Path b) where
compare (MkPath x) (MkPath y) = compare x y
-- | Same as 'Path.toFilePath'.
--
-- The following property holds:
--
-- @x == y ≡ show x == show y@
instance Show (Path b t) where
show (Path x) = show x
instance Show (Path b) where
show (MkPath x) = show x
instance NFData (Path b) where
rnf (MkPath x) = rnf x
class RelC m

View File

@@ -1,285 +0,0 @@
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE EmptyDataDecls #-}
-- | A normalizing well-typed path type.
module Path
(-- * Types
Path
,Abs
,Rel
,File
,Dir
-- * Parsing
,parseAbsDir
,parseRelDir
,parseAbsFile
,parseRelFile
,PathParseException
-- * Constructors
,mkAbsDir
,mkRelDir
,mkAbsFile
,mkRelFile
-- * Operations
,(</>)
,stripDir
,isParentOf
,parent
,filename
,dirname
-- * Conversion
,toFilePath
)
where
import Control.Exception (Exception)
import Control.Monad.Catch (MonadThrow(..))
import Data.Data
import Data.List
import Data.Maybe
import Language.Haskell.TH
import Path.Internal
import qualified System.FilePath as FilePath
--------------------------------------------------------------------------------
-- Types
-- | An absolute path.
data Abs deriving (Typeable)
-- | A relative path; one without a root.
data Rel deriving (Typeable)
-- | A file path.
data File deriving (Typeable)
-- | A directory path.
data Dir deriving (Typeable)
-- | Exception when parsing a location.
data PathParseException
= InvalidAbsDir FilePath
| InvalidRelDir FilePath
| InvalidAbsFile FilePath
| InvalidRelFile FilePath
deriving (Show,Typeable)
instance Exception PathParseException
--------------------------------------------------------------------------------
-- Parsers
-- | Get a location for an absolute directory. Produces a normalized
-- path which always ends in a path separator.
--
-- Throws: 'PathParseException'
--
parseAbsDir :: MonadThrow m
=> FilePath -> m (Path Abs Dir)
parseAbsDir filepath =
if FilePath.isAbsolute filepath &&
not (null (normalizeDir filepath)) &&
not (isPrefixOf "~/" filepath)
then return (Path (normalizeDir filepath))
else throwM (InvalidAbsDir filepath)
-- | Get a location for a relative directory. Produces a normalized
-- path which always ends in a path separator.
--
-- Throws: 'PathParseException'
--
parseRelDir :: MonadThrow m
=> FilePath -> m (Path Rel Dir)
parseRelDir filepath =
if not (FilePath.isAbsolute filepath) &&
not (null filepath) &&
not (isPrefixOf "~/" filepath) &&
not (null (normalizeDir filepath))
then return (Path (normalizeDir filepath))
else throwM (InvalidRelDir filepath)
-- | Get a location for an absolute file.
--
-- Throws: 'PathParseException'
--
parseAbsFile :: MonadThrow m
=> FilePath -> m (Path Abs File)
parseAbsFile filepath =
if FilePath.isAbsolute filepath &&
not (FilePath.hasTrailingPathSeparator filepath) &&
not (isPrefixOf "~/" filepath) &&
not (null (normalizeFile filepath))
then return (Path (normalizeFile filepath))
else throwM (InvalidAbsFile filepath)
-- | Get a location for a relative file.
--
-- Throws: 'PathParseException'
--
parseRelFile :: MonadThrow m
=> FilePath -> m (Path Rel File)
parseRelFile filepath =
if not (FilePath.isAbsolute filepath || FilePath.hasTrailingPathSeparator filepath) &&
not (null filepath) &&
not (isPrefixOf "~/" filepath) &&
not (null (normalizeFile filepath))
then return (Path (normalizeFile filepath))
else throwM (InvalidRelFile filepath)
--------------------------------------------------------------------------------
-- Constructors
-- | Make a 'Path Abs Dir'.
--
-- 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).
mkAbsDir :: FilePath -> Q Exp
mkAbsDir s =
case parseAbsDir s of
Left err -> error (show err)
Right (Path str) ->
[|Path $(return (LitE (StringL str))) :: Path Abs Dir|]
-- | Make a 'Path Rel Dir'.
mkRelDir :: FilePath -> Q Exp
mkRelDir s =
case parseRelDir s of
Left err -> error (show err)
Right (Path str) ->
[|Path $(return (LitE (StringL str))) :: Path Rel Dir|]
-- | Make a 'Path Abs File'.
--
-- 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).
mkAbsFile :: FilePath -> Q Exp
mkAbsFile s =
case parseAbsFile s of
Left err -> error (show err)
Right (Path str) ->
[|Path $(return (LitE (StringL str))) :: Path Abs File|]
-- | Make a 'Path Rel File'.
mkRelFile :: FilePath -> Q Exp
mkRelFile s =
case parseRelFile s of
Left err -> error (show err)
Right (Path str) ->
[|Path $(return (LitE (StringL str))) :: Path Rel File|]
--------------------------------------------------------------------------------
-- Conversion
-- | Convert to a 'FilePath' type.
toFilePath :: Path b t -> FilePath
toFilePath (Path l) = l
--------------------------------------------------------------------------------
-- Operations
-- | Append two paths.
--
-- The following cases are valid and the equalities hold:
--
-- @$(mkAbsDir x) \<\/> $(mkRelDir y) = $(mkAbsDir (x ++ \"/\" ++ y))@
--
-- @$(mkAbsDir x) \<\/> $(mkRelFile y) = $(mkAbsFile (x ++ \"/\" ++ y))@
--
-- @$(mkRelDir x) \<\/> $(mkRelDir y) = $(mkRelDir (x ++ \"/\" ++ y))@
--
-- @$(mkRelDir x) \<\/> $(mkRelFile y) = $(mkRelFile (x ++ \"/\" ++ y))@
--
-- The following are proven not possible to express:
--
-- @$(mkAbsFile …) \<\/> x@
--
-- @$(mkRelFile …) \<\/> x@
--
-- @x \<\/> $(mkAbsFile …)@
--
-- @x \<\/> $(mkAbsDir …)@
--
(</>) :: Path b Dir -> Path Rel t -> Path b t
(</>) (Path a) (Path b) = Path (a ++ b)
-- | Strip directory from path, making it relative to that directory.
-- Returns 'Nothing' if directory is not a parent of the path.
--
-- The following properties hold:
--
-- @stripDir parent (parent \<\/> child) = child@
--
-- Cases which are proven not possible:
--
-- @stripDir (a :: Path Abs …) (b :: Path Rel …)@
--
-- @stripDir (a :: Path Rel) (b :: Path Abs …)@
--
-- In other words the bases must match.
--
stripDir :: Path b Dir -> Path b t -> Maybe (Path Rel t)
stripDir (Path p) (Path l) =
fmap Path (stripPrefix p l)
-- | Is p a parent of the given location? Implemented in terms of
-- 'stripDir'. The bases must match.
isParentOf :: Path b Dir -> Path b t -> Bool
isParentOf p l =
isJust (stripDir p l)
-- | Take the absolute parent directory from the absolute path.
--
-- The following properties hold:
--
-- @parent (parent \<\/> child) == parent@
--
-- On the root, getting the parent is idempotent:
--
-- @parent (parent \"\/\") = \"\/\"@
--
parent :: Path Abs t -> Path Abs Dir
parent (Path fp) =
Path (normalizeDir (FilePath.takeDirectory (FilePath.dropTrailingPathSeparator fp)))
-- | Extract the file part of a path.
--
-- The following properties hold:
--
-- @filename (parent \<\/> filename a) == a@
--
filename :: Path b File -> Path Rel File
filename (Path l) =
Path (normalizeFile (FilePath.takeFileName l))
-- | Extract the last directory name of a path.
--
-- The following properties hold:
--
-- @dirname (parent \<\/> dirname a) == a@
--
dirname :: Path b Dir -> Path Rel Dir
dirname (Path l) =
Path (last (FilePath.splitPath l))
--------------------------------------------------------------------------------
-- Internal functions
-- | Internal use for normalizing a directory.
normalizeDir :: FilePath -> FilePath
normalizeDir =
clean . FilePath.addTrailingPathSeparator . FilePath.normalise
where clean "./" = ""
clean ('/':'/':xs) = clean ('/':xs)
clean x = x
-- | Internal use for normalizing a fileectory.
normalizeFile :: FilePath -> FilePath
normalizeFile =
clean . FilePath.normalise
where clean "./" = ""
clean ('/':'/':xs) = clean ('/':xs)
clean x = x

View 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

View 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

View 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]

View File

@@ -1,190 +0,0 @@
{-# LANGUAGE TemplateHaskell #-}
-- | Test suite.
module Main where
import Data.Monoid
import Path
import Path.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" parseAbsDirSpec
describe "Parsing: Path Rel Dir" parseRelDirSpec
describe "Parsing: Path Abs File" parseAbsFileSpec
describe "Parsing: Path Rel File" parseRelFileSpec
describe "Operations: (</>)" operationAppend
describe "Operations: stripDir" operationStripDir
describe "Operations: isParentOf" operationIsParentOf
describe "Operations: parent" operationParent
describe "Operations: filename" operationFilename
-- | The 'filename' operation.
operationFilename :: Spec
operationFilename =
do it "filename ($(mkAbsDir parent) </> filename $(mkRelFile filename)) == $(mkRelFile filename)"
(filename ($(mkAbsDir "/home/chris/") </>
filename $(mkRelFile "bar.txt")) ==
$(mkRelFile "bar.txt"))
it "filename ($(mkRelDir parent) </> filename $(mkRelFile filename)) == $(mkRelFile filename)"
(filename ($(mkRelDir "home/chris/") </>
filename $(mkRelFile "bar.txt")) ==
$(mkRelFile "bar.txt"))
-- | The 'parent' operation.
operationParent :: Spec
operationParent =
do it "parent (parent </> child) == parent"
(parent ($(mkAbsDir "/foo") </>
$(mkRelDir "bar")) ==
$(mkAbsDir "/foo"))
it "parent \"\" == \"\""
(parent $(mkAbsDir "/") ==
$(mkAbsDir "/"))
it "parent (parent \"\") == \"\""
(parent (parent $(mkAbsDir "/")) ==
$(mkAbsDir "/"))
-- | The 'isParentOf' operation.
operationIsParentOf :: Spec
operationIsParentOf =
do it "isParentOf parent (parent </> child)"
(isParentOf
$(mkAbsDir "///bar/")
($(mkAbsDir "///bar/") </>
$(mkRelFile "bar/foo.txt")))
it "isParentOf parent (parent </> child)"
(isParentOf
$(mkRelDir "bar/")
($(mkRelDir "bar/") </>
$(mkRelFile "bob/foo.txt")))
-- | The 'stripDir' operation.
operationStripDir :: Spec
operationStripDir =
do it "stripDir parent (parent </> child) = child"
(stripDir $(mkAbsDir "///bar/")
($(mkAbsDir "///bar/") </>
$(mkRelFile "bar/foo.txt")) ==
Just $(mkRelFile "bar/foo.txt"))
it "stripDir parent (parent </> child) = child"
(stripDir $(mkRelDir "bar/")
($(mkRelDir "bar/") </>
$(mkRelFile "bob/foo.txt")) ==
Just $(mkRelFile "bob/foo.txt"))
-- | The '</>' operation.
operationAppend :: Spec
operationAppend =
do it "AbsDir + RelDir = AbsDir"
($(mkAbsDir "/home/") </>
$(mkRelDir "chris") ==
$(mkAbsDir "/home/chris/"))
it "AbsDir + RelFile = AbsFile"
($(mkAbsDir "/home/") </>
$(mkRelFile "chris/test.txt") ==
$(mkAbsFile "/home/chris/test.txt"))
it "RelDir + RelDir = RelDir"
($(mkRelDir "home/") </>
$(mkRelDir "chris") ==
$(mkRelDir "home/chris"))
it "RelDir + RelFile = RelFile"
($(mkRelDir "home/") </>
$(mkRelFile "chris/test.txt") ==
$(mkRelFile "home/chris/test.txt"))
-- | Tests for the tokenizer.
parseAbsDirSpec :: Spec
parseAbsDirSpec =
do failing ""
failing "./"
failing "~/"
failing "foo.txt"
succeeding "/" (Path "/")
succeeding "//" (Path "/")
succeeding "///foo//bar//mu/" (Path "/foo/bar/mu/")
succeeding "///foo//bar////mu" (Path "/foo/bar/mu/")
succeeding "///foo//bar/.//mu" (Path "/foo/bar/mu/")
where failing x = parserTest parseAbsDir x Nothing
succeeding x with = parserTest parseAbsDir x (Just with)
-- | Tests for the tokenizer.
parseRelDirSpec :: Spec
parseRelDirSpec =
do failing ""
failing "/"
failing "//"
failing "~/"
failing "/"
failing "./"
failing "//"
failing "///foo//bar//mu/"
failing "///foo//bar////mu"
failing "///foo//bar/.//mu"
succeeding "foo.bak" (Path "foo.bak/")
succeeding "./foo" (Path "foo/")
succeeding "foo//bar//mu//" (Path "foo/bar/mu/")
succeeding "foo//bar////mu" (Path "foo/bar/mu/")
succeeding "foo//bar/.//mu" (Path "foo/bar/mu/")
where failing x = parserTest parseRelDir x Nothing
succeeding x with = parserTest parseRelDir x (Just with)
-- | Tests for the tokenizer.
parseAbsFileSpec :: Spec
parseAbsFileSpec =
do failing ""
failing "./"
failing "~/"
failing "./foo.txt"
failing "/"
failing "//"
failing "///foo//bar//mu/"
succeeding "/foo.txt" (Path "/foo.txt")
succeeding "///foo//bar////mu.txt" (Path "/foo/bar/mu.txt")
succeeding "///foo//bar/.//mu.txt" (Path "/foo/bar/mu.txt")
where failing x = parserTest parseAbsFile x Nothing
succeeding x with = parserTest parseAbsFile x (Just with)
-- | Tests for the tokenizer.
parseRelFileSpec :: Spec
parseRelFileSpec =
do failing ""
failing "/"
failing "//"
failing "~/"
failing "/"
failing "./"
failing "//"
failing "///foo//bar//mu/"
failing "///foo//bar////mu"
failing "///foo//bar/.//mu"
succeeding "foo.txt" (Path "foo.txt")
succeeding "./foo.txt" (Path "foo.txt")
succeeding "foo//bar//mu.txt" (Path "foo/bar/mu.txt")
succeeding "foo//bar////mu.txt" (Path "foo/bar/mu.txt")
succeeding "foo//bar/.//mu.txt" (Path "foo/bar/mu.txt")
where failing x = parserTest parseRelFile x Nothing
succeeding x with = parserTest parseRelFile 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