path-0.9.6: src/OsPath/Internal/Include.hs
-- This template expects CPP definitions for:
-- PLATFORM_NAME = Posix | Windows
-- PLATFORM_PATH = PosixPath | WindowsPath
-- PLATFORM_PATH_SINGLE = 'PosixPath' | 'WindowsPath'
-- IS_WINDOWS = 0 | 1
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
-- | Internal types and functions.
module OsPath.Internal.PLATFORM_NAME
( -- * The Path type
Path(..)
, toOsPath
-- * Validation functions
, isValidAbsDir
, isValidAbsFile
, isValidRelDir
, isValidRelFile
, hasParentDir
-- * Normalizing functions
, normalizeLeadingSeps
, normalizeTrailingSeps
, normalizeAllSeps
#if IS_WINDOWS
, normalizeWindowsSeps
#endif
, normalizeDrive
, normalizeDir
, normalizeFile
-- * Other helper functions
, extSep
, pathSep
, relRoot
, isWindows
)
where
import Control.DeepSeq (NFData (..))
import GHC.Generics (Generic)
import Data.Data
import Data.Hashable
import qualified Language.Haskell.TH.Syntax as TH
import System.OsPath.PLATFORM_NAME (PLATFORM_PATH)
import qualified System.OsPath.PLATFORM_NAME as OsPath
import System.OsString.Compat.PLATFORM_NAME (PLATFORM_STRING)
import qualified System.OsString.Compat.PLATFORM_NAME as OsString
-- | Path of some base and type.
--
-- The type variables are:
--
-- * @b@ — base, the base location of the path; absolute or relative.
-- * @t@ — type, whether file or directory.
--
-- Internally it is a PLATFORM_PATH_SINGLE, which can be of two formats only:
--
-- 1. File format: @file.txt@, @foo\/bar.txt@, @\/foo\/bar.txt@
-- 2. Directory format: @foo\/@, @\/foo\/bar\/@
--
-- All directories end in a trailing separator. There are no duplicate
-- path separators @\/\/@, no @..@, no @.\/@, no @~\/@, etc.
newtype Path b t = Path PLATFORM_PATH
deriving (Data, Typeable, Generic)
-- | String equality.
--
-- The following property holds:
--
-- @show x == show y ≡ x == y@
instance Eq (Path b t) where
(==) (Path x) (Path y) = x == y
{-# INLINE (==) #-}
-- | 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
{-# INLINE compare #-}
-- | Same as 'show . OsPath.toOsPath'.
--
-- The following property holds:
--
-- @x == y ≡ show x == show y@
instance Show (Path b t) where
show = show . toOsPath
{-# INLINE show #-}
instance NFData (Path b t) where
rnf (Path x) = rnf x
{-# INLINE rnf #-}
instance Hashable (Path b t) where
-- A "." is represented as an empty string ("") internally. Hashing ""
-- results in a hash that is the same as the salt. To produce a more
-- reasonable hash we use "toFilePath" before hashing so that a "" gets
-- converted back to a ".".
hashWithSalt n path = hashWithSalt n (toOsPath path)
{-# INLINE hashWithSalt #-}
instance forall b t. (Typeable b, Typeable t) => TH.Lift (Path b t) where
lift (Path str) = do
let b = TH.ConT $ getTCName (Proxy :: Proxy b)
t = TH.ConT $ getTCName (Proxy :: Proxy t)
[| Path $(TH.lift str) :: Path $(pure b) $(pure t) |]
where
getTCName :: Typeable a => proxy a -> TH.Name
getTCName a = TH.Name occ flav
where
tc = typeRepTyCon (typeRep a)
occ = TH.OccName (tyConName tc)
flav = TH.NameG TH.TcClsName (TH.PkgName (tyConPackage tc)) (TH.ModName (tyConModule tc))
#if MIN_VERSION_template_haskell(2,17,0)
liftTyped = TH.unsafeCodeCoerce . TH.lift
#elif MIN_VERSION_template_haskell(2,16,0)
liftTyped = TH.unsafeTExpCoerce . TH.lift
#endif
-- | Convert to a PLATFORM_PATH type.
--
-- All directories have a trailing slash, so if you want no trailing
-- slash, you can use 'OsPath.dropTrailingPathSeparator' from
-- the filepath package.
toOsPath :: Path b t -> PLATFORM_PATH
toOsPath (Path ospath)
| OsString.null ospath = relRoot
| otherwise = ospath
--------------------------------------------------------------------------------
-- Validation functions
-- | Is the PLATFORM_PATH_SINGLE a valid absolute dir?
isValidAbsDir :: PLATFORM_PATH -> Bool
isValidAbsDir ospath =
OsPath.isAbsolute ospath &&
not (hasParentDir ospath) &&
OsPath.isValid ospath
-- | Is the PLATFORM_PATH_SINGLE a valid absolute file?
isValidAbsFile :: PLATFORM_PATH -> Bool
isValidAbsFile ospath =
OsPath.isAbsolute ospath &&
not (OsPath.hasTrailingPathSeparator ospath) &&
not (hasParentDir ospath) &&
OsPath.isValid ospath
-- | Is the PLATFORM_PATH_SINGLE a valid relative dir?
isValidRelDir :: PLATFORM_PATH -> Bool
isValidRelDir ospath =
not (OsPath.isAbsolute ospath) &&
not (OsString.null ospath) &&
not (hasParentDir ospath) &&
not (OsString.all OsPath.isPathSeparator ospath) &&
OsPath.isValid ospath
-- | Is the PLATFORM_PATH_SINGLE a valid relative file?
isValidRelFile :: PLATFORM_PATH -> Bool
isValidRelFile ospath =
not (OsPath.isAbsolute ospath) &&
not (OsString.null ospath) &&
not (hasParentDir ospath) &&
not (OsPath.hasTrailingPathSeparator ospath) &&
ospath /= [OsPath.pstr|.|] &&
OsPath.isValid ospath
-- | 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 :: PLATFORM_PATH -> Bool
hasParentDir ospath =
(ospath' == [OsString.pstr|..|]) ||
(prefix' `OsString.isPrefixOf` ospath') ||
(infix' `OsString.isInfixOf` ospath') ||
(suffix' `OsString.isSuffixOf` ospath')
where
prefix' = [OsString.pstr|..|] <> pathSep
infix' = pathSep <> [OsString.pstr|..|] <> pathSep
suffix' = pathSep <> [OsString.pstr|..|]
#if IS_WINDOWS
ospath' = OsString.map normSep ospath
normSep c
| OsPath.isPathSeparator c = OsPath.pathSeparator
| otherwise = c
#else
ospath' = ospath
#endif
--------------------------------------------------------------------------------
-- Normalizing functions
-- | Normalizes seps only at the beginning of a path.
normalizeLeadingSeps :: PLATFORM_PATH -> PLATFORM_PATH
normalizeLeadingSeps path = normLeadingSep <> rest
where (leadingSeps, rest) = OsString.span OsPath.isPathSeparator path
normLeadingSep
| OsString.null leadingSeps = OsString.empty
| otherwise = OsString.singleton OsPath.pathSeparator
-- | Normalizes seps only at the end of a path.
normalizeTrailingSeps :: PLATFORM_PATH -> PLATFORM_PATH
normalizeTrailingSeps path = rest <> normTrailingSep
where (rest, trailingSeps) = OsString.spanEnd OsPath.isPathSeparator path
normTrailingSep
| OsString.null trailingSeps = OsString.empty
| otherwise = OsString.singleton OsPath.pathSeparator
-- | Replaces consecutive path seps with single sep and replaces alt sep with
-- standard sep.
normalizeAllSeps :: PLATFORM_PATH -> PLATFORM_PATH
normalizeAllSeps = go OsString.empty
where go !acc ospath
| OsString.null ospath = acc
| otherwise =
let (leadingSeps, withoutLeadingSeps) =
OsString.span OsPath.isPathSeparator ospath
(name, rest) =
OsString.break OsPath.isPathSeparator withoutLeadingSeps
sep = if OsString.null leadingSeps
then OsString.empty
else OsString.singleton OsPath.pathSeparator
in go (acc <> sep <> name) rest
#if IS_WINDOWS
-- | Normalizes seps in whole path, but if there are 2+ seps at the beginning,
-- they are normalized to exactly 2 to preserve UNC and Unicode prefixed
-- paths.
normalizeWindowsSeps :: PLATFORM_PATH -> PLATFORM_PATH
normalizeWindowsSeps path = normLeadingSeps <> normalizeAllSeps rest
where (leadingSeps, rest) = OsString.span OsPath.isPathSeparator path
normLeadingSeps = OsString.replicate
(min 2 (OsString.length leadingSeps))
OsPath.pathSeparator
#endif
-- | Normalizes the drive of a PLATFORM_PATH_SINGLE.
normalizeDrive :: PLATFORM_PATH -> PLATFORM_PATH
#if IS_WINDOWS
normalizeDrive = normalizeTrailingSeps
#else
normalizeDrive = id
#endif
-- | Normalizes directory path with platform-specific rules.
normalizeDir :: PLATFORM_PATH -> PLATFORM_PATH
normalizeDir =
normalizeRelDir
. OsPath.addTrailingPathSeparator
. normalizeFile
where -- Represent a "." in relative dir path as "" internally so that it
-- composes without having to renormalize the path.
normalizeRelDir p
| p == relRoot = OsString.empty
| otherwise = p
-- | Applies platform-specific sep normalization following @OsPath.normalise@.
normalizeFile :: PLATFORM_PATH -> PLATFORM_PATH
#if IS_WINDOWS
normalizeFile = normalizeWindowsSeps . OsPath.normalise
#else
normalizeFile = normalizeLeadingSeps . OsPath.normalise
#endif
--------------------------------------------------------------------------------
-- Other helper functions
extSep :: PLATFORM_STRING
extSep = $(TH.lift (OsString.singleton OsPath.extSeparator))
pathSep :: PLATFORM_STRING
pathSep = $(TH.lift (OsString.singleton OsPath.pathSeparator))
-- | Normalized file path representation for the relative path root
relRoot :: PLATFORM_PATH
relRoot =
$(TH.lift ([OsPath.pstr|.|] <> OsString.singleton OsPath.pathSeparator))
isWindows :: Bool
#if IS_WINDOWS
isWindows = True
#else
isWindows = False
#endif
{-# INLINE isWindows #-}