1{-# LANGUAGE TemplateHaskell #-}
4-- Module : Streamly.Internal.FileSystem.Path
5-- Copyright : (c) 2023 Composewell Technologies
7-- Maintainer : streamly@composewell.com
10-- Well typed and flexible file systems paths, preserving the OS and filesystem
13-- You can choose the level of type safety you want. 'Path' is the basic path
14-- type which can represent a file, directory, absolute or relative path with
15-- no restrictions. Depending on how much type safety you want you can choose
16-- appropriate type wrappers to wrap 'Path'. @File Path@ mandates the path to
17-- be a file whereas @Abs (File Path)@ mandates it to be an absolute path
18-- representing a file.
20-- You can upgrade or downgrade the safety. Whenever a less restrictive path
21-- type is converted to a more restrctive path type the conversion involves
22-- checks and it may fail. However, a more restrictive path type can be freely
23-- converted to a less restrictive one.
25-- See the @streamly-filepath@ package for interworking with the 'OsPath' type.
26-- The 'Path' type can be converted to and from 'OsPath' type at zero cost
27-- since the underlying representation of both is the same.
29-- Conventions: A trailing separator on a path indicates that it is a directory.
30-- However, the absence of a trailing separator does not convey any
31-- information, it could either be a directory or a file.
33-- You may also find the 'str' quasiquoter from "Streamly.Unicode.String" to be
34-- useful in creating paths.
36module Streamly.Internal.FileSystem.Path
55 -- * Statically Verified Literals
67 -- * Statically Verified Strings
85 -- Do we need to export the separator functions? They are not essential if
86 -- operations to split and combine paths are provided. If someone wants to
87 -- work on paths at low level then they know what they are.
95import Control.Exception (Exception)
96import Control.Monad.Catch (MonadThrow(..))
98import Data.Functor.Identity (Identity(..))
99import Data.Word (Word8)
100#if defined(mingw32_HOST_OS) || defined(__MINGW32__)
101import Data.Word (Word16)
103import Language.Haskell.TH (Q, Exp)
104import Language.Haskell.TH.Quote (QuasiQuoter)
105import Streamly.Internal.Data.Array (Array)
106import Streamly.Internal.Data.Stream (Stream)
107import System.IO.Unsafe (unsafePerformIO)
109import qualified Streamly.Internal.Data.Array as Array
110import qualified Streamly.Internal.Data.Fold as Fold
111import qualified Streamly.Internal.Data.MutArray as MutArray
112import qualified Streamly.Internal.Data.Stream as Stream
113import qualified Streamly.Internal.Unicode.Stream as Unicode
115import Prelude hiding (abs)
117#if defined(mingw32_HOST_OS) || defined(__MINGW32__)
118#define WORD_TYPE Word16
121#define WORD_TYPE Word8
125------------------------------------------------------------------------------
127------------------------------------------------------------------------------
129-- | Exceptions thrown by path operations.
132 | InvalidAbsPath String
133 | InvalidRelPath String
134 | InvalidFilePath String
135 | InvalidDirPath String
138instance Exception PathException
140-- XXX Path must not contain null char on Posix. System calls consider the path
141-- as null terminated.
142-- XXX Maintain the Array with null termination because Unix system calls
143-- require a null terminated string, also they return null terminated strings
144-- as paths. Implementation of path append will have to handle the null
145-- termination. Or we can choose to always copy the array when using it in
148-- XXX The eq instance needs to make sure that the paths are equivalent. If we
149-- normalize the paths we can do a byte comparison. However, on windows paths
150-- are case insensitive but the case is preserved, therefore, we cannot
151-- normalize and need to do case insensitive comparison.
153------------------------------------------------------------------------------
155------------------------------------------------------------------------------
157-- | A type representing file system paths for directories or files.
158newtype Path = Path (Array WORD_TYPE) -- deriving Eq
160-- Show instance prints raw bytes without any decoding for rountdtripping.
161-- Should we print this as a string instead, may be useful for ascii chars but
162-- utf8 encoded chars may be unprintable. Better use toString if you want to
163-- pretty print the path.
165instance Show Path where
166 show (Path x) = show x
169-- XXX Do we need a type for file or dir Name as names cannot have the
170-- separator char and there may be other restrictions on names? For example,
171-- length restriction. A file name cannot be "." or "..". We can use the types
172-- "File Name" and "Dir Name" to represent names. Also, file systems may put
173-- limits on names. Can have an IsName type class with members Name, (File
176-- | A type representing a file path.
177newtype File a = File a
179-- | A type representing a directory path.
182-- | A type representing absolute paths.
185-- | A type representing relative paths.
188------------------------------------------------------------------------------
190------------------------------------------------------------------------------
192-- | A member of 'IsPath' knows how to convert to and from the 'Path' type.
194 -- | Like 'fromPath' but does not check the properties of 'Path'. Provides
195 -- performance and simplicity when we know that the properties of the path
196 -- are already verified, for example, when we get the path from the file
197 -- system or the OS APIs.
198 fromPathUnsafe :: Path -> a
200 -- | Convert a raw 'Path' to other forms of well-typed paths. It may fail
201 -- if the path does not satisfy the properties of the target type.
203 -- Path components may have limits.
204 -- Total path length may have a limit.
205 fromPath :: MonadThrow m => Path -> m a
207 -- | Convert a well-typed path to a raw 'Path'. Never fails.
210instance IsPath Path where
215instance IsPath (File Path) where
216 fromPathUnsafe p = File p
217 fromPath p = pure (File p)
220instance IsPath (Dir Path) where
221 fromPathUnsafe p = Dir p
222 fromPath p = pure (Dir p)
225instance IsPath (Abs Path) where
226 fromPathUnsafe p = Abs p
227 fromPath p = pure (Abs p)
230instance IsPath (Rel Path) where
231 fromPathUnsafe p = Rel p
232 fromPath p = pure (Rel p)
235instance IsPath (Abs (File Path)) where
236 fromPathUnsafe p = Abs (File p)
237 fromPath p = pure (Abs (File p))
238 toPath (Abs (File p)) = p
240instance IsPath (Abs (Dir Path)) where
241 fromPathUnsafe p = Abs (Dir p)
242 fromPath p = pure (Abs (Dir p))
243 toPath (Abs (Dir p)) = p
245instance IsPath (Rel (File Path)) where
246 fromPathUnsafe p = Rel (File p)
247 fromPath p = pure (Rel (File p))
248 toPath (Rel (File p)) = p
250instance IsPath (Rel (Dir Path)) where
251 fromPathUnsafe p = Rel (Dir p)
252 fromPath p = pure (Rel (Dir p))
253 toPath (Rel (Dir p)) = p
255-- XXX Use rewrite rules to eliminate intermediate conversions for better
258-- | Convert a path type to another path type. This operation may fail with a
259-- 'PathException' when converting a less restrictive path type to a more
261adaptPath :: (MonadThrow m, IsPath a, IsPath b) => a -> m b
262adaptPath p = fromPath $ toPath p
264------------------------------------------------------------------------------
266------------------------------------------------------------------------------
268-- A chunk is essentially an untyped Array i.e. Array Word8. We can either use
269-- the term ByteArray for that or just Chunk. The latter is shorter and we have
270-- been using it consistently in streamly.
272-- | /Unsafe/: On Posix, a path cannot contain null characters. On Windows, the
273-- array passed must be a multiple of 2 bytes as the underlying representation
275{-# INLINE fromChunkUnsafe #-}
276fromChunkUnsafe :: Array Word8 -> Path
277fromChunkUnsafe arr = Path (Array.castUnsafe arr)
279-- | On Posix it may fail if the byte array contains null characters. On
280-- Windows the array passed must be a multiple of 2 bytes as the underlying
281-- representation uses 'Word16'.
283-- Throws 'InvalidPath'.
284fromChunk :: MonadThrow m => Array Word8 -> m Path
286 case Array.cast arr of
288 -- XXX Windows only message.
291 $ "Encoded path length " ++ show (Array.byteLength arr)
292 ++ " is not a multiple of 16-bit."
293 Just x -> pure (Path x)
295-- | Convert 'Path' to an array of bytes.
296toChunk :: Path -> Array Word8
297toChunk (Path arr) = Array.asBytes arr
299-- | Encode a Unicode char stream to 'Path' using strict UTF-8 encoding on
300-- Posix. On Posix it may fail if the stream contains null characters.
301-- TBD: Use UTF16LE on Windows.
302fromChars :: MonadThrow m => Stream Identity Char -> m Path
304 let n = runIdentity $ Stream.fold Fold.length s
305#if defined(mingw32_HOST_OS) || defined(__MINGW32__)
306 in pure $ Path (Array.fromPureStreamN n (Unicode.encodeUtf16le' s))
308 in pure $ Path (Array.fromPureStreamN n (Unicode.encodeUtf8' s))
311-- | Decode the path to a stream of Unicode chars using strict UTF-8 decoding
313-- TBD: Use UTF16LE on Windows.
314toChars :: Monad m => Path -> Stream m Char
316#if defined(mingw32_HOST_OS) || defined(__MINGW32__)
317 Unicode.decodeUtf16le' $ Array.read arr
319 Unicode.decodeUtf8' $ Array.read arr
322-- | Encode a Unicode string to 'Path' using strict UTF-8 encoding on Posix.
323-- On Posix it may fail if the stream contains null characters.
324-- TBD: Use UTF16LE on Windows.
325fromString :: MonadThrow m => [Char] -> m Path
326fromString = fromChars . Stream.fromList
328-- | Decode the path to a Unicode string using strict UTF-8 decoding on Posix.
329-- TBD: Use UTF16LE on Windows.
330toString :: Path -> [Char]
331toString = runIdentity . Stream.toList . toChars
333------------------------------------------------------------------------------
334-- Statically Verified Literals
335------------------------------------------------------------------------------
337-- XXX Build these on top of the str quasiquoter so that we get the
338-- interpolation for free.
340-- | Generates a 'Path' type from an interpolated string literal.
346-- | Generates an @Abs Path@ type from an interpolated string literal.
352-- | Generates an @Rel Path@ type from an interpolated string literal.
358-- | Generates an @Dir Path@ type from an interpolated string literal.
364-- | Generates an @File Path@ type from an interpolated string literal.
370-- | Generates an @Abs (Dir Path)@ type from an interpolated string literal.
376-- | Generates an @Rel (Dir Path)@ type from an interpolated string literal.
382-- | Generates an @Abs (File Path)@ type from an interpolated string literal.
385absfile :: QuasiQuoter
388-- | Generates an @Rel (File Path)@ type from an interpolated string literal.
391relfile :: QuasiQuoter
394------------------------------------------------------------------------------
395-- Statically Verified Strings
396------------------------------------------------------------------------------
398-- | Generates a 'Path' type.
401mkPath :: String -> Q Exp
404-- | Generates an @Abs Path@ type.
407mkAbs :: String -> Q Exp
410-- | Generates an @Rel Path@ type.
413mkRel :: String -> Q Exp
416-- | Generates an @Dir Path@ type.
419mkDir :: String -> Q Exp
422-- | Generates an @File Path@ type.
425mkFile :: String -> Q Exp
428-- | Generates an @Abs (Dir Path)@ type.
431mkAbsDir :: String -> Q Exp
434-- | Generates an @Rel (Dir Path)@ type.
437mkRelDir :: String -> Q Exp
440-- | Generates an @Abs (File Path)@ type.
443mkAbsFile :: String -> Q Exp
446-- | Generates an @Rel (File Path)@ type.
449mkRelFile :: String -> Q Exp
452------------------------------------------------------------------------------
454------------------------------------------------------------------------------
456separatorWord :: WORD_TYPE
457separatorWord = SEPARATOR
459-- Portable definition for exporting.
461-- | Primary path separator character, @/@ on Posix and @\\@ on Windows.
462-- Windows supports @/@ too as a separator. Please use 'isSeparator' for
463-- testing if a char is a separator char.
464primarySeparator :: Char
465primarySeparator = chr (SEPARATOR)
467-- | On Posix only @/@ is a path separator but in windows it could be either
469isSeparator :: Char -> Bool
470#if defined(mingw32_HOST_OS) || defined(__MINGW32__)
471isSeparator c = (c == '/') || (c == '\\')
473isSeparator = (== '/')
476-- If we append an absolute path it may fail with an error if the 'Path'
477-- implementation stores absolute path information (a leading separator char).
478-- However, the implementation may choose to store the path as a list of
479-- components in which case we cannot distinguish an absolute path from
482-- | Like 'extendDir' but for the less restrictive 'Path' type which will always
483-- create a syntactically valid 'Path' type but it may not be semantically valid
484-- because we may append an absolute path or we may append to a file path.
485-- The onus lies on the user to ensure that the first path is not a file and
486-- the second path is not absolute.
487extendPath :: Path -> Path -> Path
488extendPath (Path a) (Path b) =
489 let len = Array.byteLength a + 1 + Array.byteLength b
490 -- XXX Check the leading separator or drive identifier. However,
491 -- checking the drive letter may add an additional overhead (can it be
492 -- arbitrarily long?), if it is significant we may want to have a
493 -- separate combinePathChecked API for that.
495 -- Also, do not add a separator char if the first path has a trailing
497 newArr = unsafePerformIO $ do
498 arr <- MutArray.new len
499 arr1 <- MutArray.spliceUnsafe arr (Array.unsafeThaw a)
500 arr2 <- MutArray.snocUnsafe arr1 separatorWord
501 arr3 <- MutArray.spliceUnsafe arr2 (Array.unsafeThaw b)
502 return (Array.unsafeFreeze arr3)
505-- The only safety we need for paths is: (1) The first path can only be a Dir
506-- type path, and (2) second path can only be a Rel path.
507-- Can this be coerced to create unsafe versions?
509-- | Extend a directory path by appending a relative path to it. This is the
510-- equivalent to the @</>@ operator from the @filepath@ package.
511{-# INLINE extendDir #-}
512extendDir :: (IsPath (a (Dir Path)), IsPath b, IsPath (a b)) =>
513 (a (Dir Path)) -> Rel b -> a b
515 fromPathUnsafe $ extendPath (toPath a) (toPath b)
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list