system-filepath-0.4.13: lib/Filesystem/Path/Internal.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveDataTypeable #-}
-- |
-- Module: Filesystem.Path.Internal
-- Copyright: 2010 John Millikin
-- License: MIT
--
-- Maintainer: jmillikin@gmail.com
-- Portability: portable
--
module Filesystem.Path.Internal where
import Prelude hiding (FilePath)
import Control.DeepSeq (NFData, rnf)
import qualified Control.Exception as Exc
import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as B8
import Data.Char (chr, ord)
import Data.Data (Data)
import Data.List (intersperse)
import Data.Ord (comparing)
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import Data.Text.Encoding.Error (UnicodeException)
import Data.Typeable (Typeable)
-------------------------------------------------------------------------------
-- File Paths
-------------------------------------------------------------------------------
type Chunk = String
type Directory = Chunk
type Basename = Chunk
type Extension = Chunk
data Root
= RootPosix
| RootWindowsVolume Char Bool
| RootWindowsCurrentVolume
| RootWindowsUnc String String Bool
| RootWindowsDoubleQMark
deriving (Eq, Ord, Data, Typeable, Show)
data FilePath = FilePath
{ pathRoot :: Maybe Root
, pathDirectories :: [Directory]
, pathBasename :: Maybe Basename
, pathExtensions :: [Extension]
}
deriving (Data, Typeable)
instance Eq FilePath where
x == y = compare x y == EQ
instance Ord FilePath where
compare = comparing (\p ->
(pathRoot p
, fmap unescape' (pathDirectories p)
, fmap unescape' (pathBasename p)
, fmap unescape' (pathExtensions p)
))
instance NFData Root where
rnf (RootWindowsVolume c extended) = rnf c `seq` rnf extended
rnf (RootWindowsUnc host share extended) = rnf host `seq` rnf share `seq` rnf extended
rnf _ = ()
instance NFData FilePath where
rnf p = rnf (pathRoot p) `seq` rnf (pathDirectories p) `seq` rnf (pathBasename p) `seq` rnf (pathExtensions p)
-- | A file path with no root, directory, or filename
empty :: FilePath
empty = FilePath Nothing [] Nothing []
dot :: Chunk
dot = "."
dots :: Chunk
dots = ".."
filenameChunk :: FilePath -> Chunk
filenameChunk p = concat (name:exts) where
name = maybe "" id (pathBasename p)
exts = case pathExtensions p of
[] -> []
exts' -> intersperse dot ("":exts')
rootChunk :: Maybe Root -> Chunk
rootChunk r = flip (maybe "") r $ \r' -> case r' of
RootPosix -> "/"
RootWindowsVolume c False -> c : ":\\"
RootWindowsVolume c True -> "\\\\?\\" ++ (c : ":\\")
RootWindowsCurrentVolume -> "\\"
RootWindowsUnc host share False -> "\\\\" ++ host ++ "\\" ++ share
RootWindowsUnc host share True -> "\\\\?\\UNC\\" ++ host ++ "\\" ++ share
RootWindowsDoubleQMark -> "\\??\\"
rootText :: Maybe Root -> T.Text
rootText = T.pack . rootChunk
directoryChunks :: FilePath -> [Chunk]
directoryChunks path = pathDirectories path ++ [filenameChunk path]
-------------------------------------------------------------------------------
-- Rules
-------------------------------------------------------------------------------
data Rules platformFormat = Rules
{ rulesName :: T.Text
-- | Check if a 'FilePath' is valid; it must not contain any illegal
-- characters, and must have a root appropriate to the current
-- 'Rules'.
, valid :: FilePath -> Bool
-- | Split a search path, such as @$PATH@ or @$PYTHONPATH@, into
-- a list of 'FilePath's.
, splitSearchPath :: platformFormat -> [FilePath]
-- | splitSearchPathString is like 'splitSearchPath', but takes a string
-- encoded in the format used by @System.IO@.
, splitSearchPathString :: String -> [FilePath]
-- | Attempt to convert a 'FilePath' to human‐readable text.
--
-- If the path is decoded successfully, the result is a 'Right'
-- containing the decoded text. Successfully decoded text can be
-- converted back to the original path using 'fromText'.
--
-- If the path cannot be decoded, the result is a 'Left' containing an
-- approximation of the original path. If displayed to the user, this
-- value should be accompanied by some warning that the path has an
-- invalid encoding. Approximated text cannot be converted back to the
-- original path.
--
-- This function ignores the user’s locale, and assumes all
-- file paths are encoded in UTF8. If you need to display file paths
-- with an unusual or obscure encoding, use 'encode' and then decode
-- them manually.
--
-- Since: 0.2
, toText :: FilePath -> Either T.Text T.Text
-- | Convert human‐readable text into a 'FilePath'.
--
-- This function ignores the user’s locale, and assumes all
-- file paths are encoded in UTF8. If you need to create file paths
-- with an unusual or obscure encoding, encode them manually and then
-- use 'decode'.
--
-- Since: 0.2
, fromText :: T.Text -> FilePath
-- | Convert a 'FilePath' to a platform‐specific format,
-- suitable for use with external OS functions.
--
-- Since: 0.3
, encode :: FilePath -> platformFormat
-- | Convert a 'FilePath' from a platform‐specific format,
-- suitable for use with external OS functions.
--
-- Since: 0.3
, decode :: platformFormat -> FilePath
-- | Attempt to convert a 'FilePath' to a string suitable for use with
-- functions in @System.IO@. The contents of this string are
-- platform‐dependent, and are not guaranteed to be
-- human‐readable. For converting 'FilePath's to a
-- human‐readable format, use 'toText'.
--
-- Since: 0.3.1
, encodeString :: FilePath -> String
-- | Attempt to parse a 'FilePath' from a string suitable for use
-- with functions in @System.IO@. Do not use this function for parsing
-- human‐readable paths, as the character set decoding is
-- platform‐dependent. For converting human‐readable
-- text to a 'FilePath', use 'fromText'.
--
-- Since: 0.3.1
, decodeString :: String -> FilePath
}
instance Show (Rules a) where
showsPrec d r = showParen (d > 10)
(showString "Rules " . shows (rulesName r))
escape :: T.Text -> Chunk
escape t = T.unpack t
unescape :: Chunk -> (T.Text, Bool)
unescape cs = if any (\c -> ord c >= 0xDC80 && ord c <= 0xDCFF) cs
then (T.pack (map (\c -> if ord c >= 0xDC80 && ord c <= 0xDCFF
then chr (ord c - 0xDC00)
else c) cs), False)
else (T.pack cs, True)
unescape' :: Chunk -> T.Text
unescape' = fst . unescape
unescapeBytes' :: Chunk -> B.ByteString
unescapeBytes' cs = if any (\c -> ord c >= 0xDC80 && ord c <= 0xDCFF) cs
then B8.concat (map (\c -> if ord c >= 0xDC80 && ord c <= 0xDCFF
then B8.singleton (chr (ord c - 0xDC00))
else TE.encodeUtf8 (T.singleton c)) cs)
else TE.encodeUtf8 (T.pack cs)
splitBy :: (a -> Bool) -> [a] -> [[a]]
splitBy p = loop where
loop xs = let
(chunk, rest) = break p xs
cont = chunk : loop (tail rest)
in if null rest then [chunk] else cont
textSplitBy :: (Char -> Bool) -> T.Text -> [T.Text]
#if MIN_VERSION_text(0,11,0)
textSplitBy = T.split
#else
textSplitBy = T.splitBy
#endif
parseFilename :: Chunk -> (Maybe Basename, [Extension])
parseFilename filename = parsed where
parsed = if null filename
then (Nothing, [])
else case span (== '.') filename of
(leadingDots, baseAndExts) -> case splitBy (== '.') baseAndExts of
[] -> (joinDots leadingDots "", [])
(name':exts') -> (joinDots leadingDots name', exts')
joinDots leadingDots base = case leadingDots ++ base of
[] -> Nothing
joined -> Just joined
maybeDecodeUtf8 :: B.ByteString -> Maybe T.Text
maybeDecodeUtf8 bytes = case TE.decodeUtf8' bytes of
Left _ -> Nothing
Right text -> Just text