@@ -0,0 +1,483 @@
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.
36+module Streamly.Internal.FileSystem.Path
55+ -- * Statically Verified Literals
67+ -- * Statically Verified Strings
90+import Control.Exception (Exception)
91+import Control.Monad.Catch (MonadThrow(..))
92+import Data.Functor.Identity (Identity(..))
93+import Data.Word (Word8)
94+#if defined(mingw32_HOST_OS) || defined(__MINGW32__)
95+import Data.Word (Word16)
97+import Language.Haskell.TH (Q, Exp)
98+import Language.Haskell.TH.Quote (QuasiQuoter)
99+import Streamly.Internal.Data.Array (Array)
100+import Streamly.Internal.Data.Stream (Stream)
101+import System.IO.Unsafe (unsafePerformIO)
103+import qualified Streamly.Internal.Data.Array as Array
104+import qualified Streamly.Internal.Data.Fold as Fold
105+import qualified Streamly.Internal.Data.MutArray as MutArray
106+import qualified Streamly.Internal.Data.Stream as Stream
107+import qualified Streamly.Internal.Unicode.Stream as Unicode
109+import Prelude hiding (abs)
111+#if defined(mingw32_HOST_OS) || defined(__MINGW32__)
112+#define WORD_TYPE Word16
115+#define WORD_TYPE Word8
119+------------------------------------------------------------------------------
121+------------------------------------------------------------------------------
123+-- | Exceptions thrown by path operations.
126+ | InvalidAbsPath String
127+ | InvalidRelPath String
128+ | InvalidFilePath String
129+ | InvalidDirPath String
132+instance Exception PathException
134+-- XXX Path must not contain null char on Posix. System calls consider the path
135+-- as null terminated.
136+-- XXX Maintain the Array with null termination because Unix system calls
137+-- require a null terminated string, also they return null terminated strings
138+-- as paths. Implementation of path append will have to handle the null
139+-- termination. Or we can choose to always copy the array when using it in
142+-- XXX The eq instance needs to make sure that the paths are equivalent. If we
143+-- normalize the paths we can do a byte comparison. However, on windows paths
144+-- are case insensitive but the case is preserved, therefore, we cannot
145+-- normalize and need to do case insensitive comparison.
147+------------------------------------------------------------------------------
149+------------------------------------------------------------------------------
151+-- | A type representing file system paths for directories or files.
152+newtype Path = Path (Array WORD_TYPE) -- deriving Eq
154+-- XXX Do we need a type for file or dir Name as names cannot have the
155+-- separator char and there may be other restrictions on names? For example,
156+-- length restriction. A file name cannot be "." or "..". We can use the types
157+-- "File Name" and "Dir Name" to represent names. Also, file systems may put
158+-- limits on names. Can have an IsName type class with members Name, (File
159+-- Name), (Dir Name).
161+-- | A type representing a file path.
162+newtype File a = File a
164+-- | A type representing a directory path.
165+newtype Dir a = Dir a
167+-- | A type representing absolute paths.
168+newtype Abs a = Abs a
170+-- | A type representing relative paths.
171+newtype Rel a = Rel a
173+------------------------------------------------------------------------------
175+------------------------------------------------------------------------------
177+-- | A member of 'IsPath' knows how to convert to and from the 'Path' type.
179+ -- | Like 'fromPath' but does not check the properties of 'Path'. Provides
180+ -- performance and simplicity when we know that the properties of the path
181+ -- are already verified, for example, when we get the path from the file
182+ -- system or the OS APIs.
183+ fromPathUnsafe :: Path -> a
185+ -- | Convert a raw 'Path' to other forms of well-typed paths. It may fail
186+ -- if the path does not satisfy the properties of the target type.
188+ -- Path components may have limits.
189+ -- Total path length may have a limit.
190+ fromPath :: MonadThrow m => Path -> m a
192+ -- | Convert a well-typed path to a raw 'Path'. Never fails.
195+instance IsPath Path where
200+instance IsPath (File Path) where
201+ fromPathUnsafe p = File p
202+ fromPath p = pure (File p)
205+instance IsPath (Dir Path) where
206+ fromPathUnsafe p = Dir p
207+ fromPath p = pure (Dir p)
210+instance IsPath (Abs Path) where
211+ fromPathUnsafe p = Abs p
212+ fromPath p = pure (Abs p)
215+instance IsPath (Rel Path) where
216+ fromPathUnsafe p = Rel p
217+ fromPath p = pure (Rel p)
220+instance IsPath (Abs (File Path)) where
221+ fromPathUnsafe p = Abs (File p)
222+ fromPath p = pure (Abs (File p))
223+ toPath (Abs (File p)) = p
225+instance IsPath (Abs (Dir Path)) where
226+ fromPathUnsafe p = Abs (Dir p)
227+ fromPath p = pure (Abs (Dir p))
228+ toPath (Abs (Dir p)) = p
230+instance IsPath (Rel (File Path)) where
231+ fromPathUnsafe p = Rel (File p)
232+ fromPath p = pure (Rel (File p))
233+ toPath (Rel (File p)) = p
235+instance IsPath (Rel (Dir Path)) where
236+ fromPathUnsafe p = Rel (Dir p)
237+ fromPath p = pure (Rel (Dir p))
238+ toPath (Rel (Dir p)) = p
240+-- XXX Use rewrite rules to eliminate intermediate conversions for better
243+-- | Convert a path type to another path type. This operation may fail with a
244+-- 'PathException' when converting a less restrictive path type to a more
246+adaptPath :: (MonadThrow m, IsPath a, IsPath b) => a -> m b
247+adaptPath p = fromPath $ toPath p
249+------------------------------------------------------------------------------
251+------------------------------------------------------------------------------
253+-- A chunk is essentially an untyped Array i.e. Array Word8. We can either use
254+-- the term ByteArray for that or just Chunk. The latter is shorter and we have
255+-- been using it consistently in streamly.
257+-- | /Unsafe/: On Posix, a path cannot contain null characters. On Windows, the
258+-- array passed must be a multiple of 2 bytes as the underlying representation
260+{-# INLINE fromChunkUnsafe #-}
261+fromChunkUnsafe :: Array Word8 -> Path
262+fromChunkUnsafe arr = Path (Array.castUnsafe arr)
264+-- | On Posix it may fail if the byte array contains null characters. On
265+-- Windows the array passed must be a multiple of 2 bytes as the underlying
266+-- representation uses 'Word16'.
268+-- Throws 'InvalidPath'.
269+fromChunk :: MonadThrow m => Array Word8 -> m Path
271+ case Array.cast arr of
273+ -- XXX Windows only message.
276+ $ "Encoded path length " ++ show (Array.byteLength arr)
277+ ++ " is not a multiple of 16-bit."
278+ Just x -> pure (Path x)
280+-- | Convert 'Path' to an array of bytes.
281+toChunk :: Path -> Array Word8
282+toChunk (Path arr) = Array.asBytes arr
284+-- | Encode a Unicode char stream to 'Path' using strict UTF-8 encoding on
285+-- Posix. On Posix it may fail if the stream contains null characters.
286+-- TBD: Use UTF16LE on Windows.
287+fromChars :: MonadThrow m => Stream Identity Char -> m Path
289+ let n = runIdentity $ Stream.fold Fold.length s
290+#if defined(mingw32_HOST_OS) || defined(__MINGW32__)
291+ in pure $ Path (Array.fromPureStreamN n (Unicode.encodeUtf16le' s))
293+ in pure $ Path (Array.fromPureStreamN n (Unicode.encodeUtf8' s))
296+-- | Decode the path to a stream of Unicode chars using strict UTF-8 decoding
298+-- TBD: Use UTF16LE on Windows.
299+toChars :: Monad m => Path -> Stream m Char
301+#if defined(mingw32_HOST_OS) || defined(__MINGW32__)
302+ Unicode.decodeUtf16le' $ Array.read arr
304+ Unicode.decodeUtf8' $ Array.read arr
307+-- | Encode a Unicode string to 'Path' using strict UTF-8 encoding on Posix.
308+-- On Posix it may fail if the stream contains null characters.
309+-- TBD: Use UTF16LE on Windows.
310+fromString :: MonadThrow m => [Char] -> m Path
311+fromString = fromChars . Stream.fromList
313+-- | Decode the path to a Unicode string using strict UTF-8 decoding on Posix.
314+-- TBD: Use UTF16LE on Windows.
315+toString :: Path -> [Char]
316+toString = runIdentity . Stream.toList . toChars
318+------------------------------------------------------------------------------
319+-- Statically Verified Literals
320+------------------------------------------------------------------------------
322+-- XXX Build these on top of the str quasiquoter so that we get the
323+-- interpolation for free.
325+-- | Generates a 'Path' type from an interpolated string literal.
331+-- | Generates an @Abs Path@ type from an interpolated string literal.
337+-- | Generates an @Rel Path@ type from an interpolated string literal.
343+-- | Generates an @Dir Path@ type from an interpolated string literal.
349+-- | Generates an @File Path@ type from an interpolated string literal.
355+-- | Generates an @Abs (Dir Path)@ type from an interpolated string literal.
358+absdir :: QuasiQuoter
361+-- | Generates an @Rel (Dir Path)@ type from an interpolated string literal.
364+reldir :: QuasiQuoter
367+-- | Generates an @Abs (File Path)@ type from an interpolated string literal.
370+absfile :: QuasiQuoter
373+-- | Generates an @Rel (File Path)@ type from an interpolated string literal.
376+relfile :: QuasiQuoter
379+------------------------------------------------------------------------------
380+-- Statically Verified Strings
381+------------------------------------------------------------------------------
383+-- | Generates a 'Path' type.
386+mkPath :: String -> Q Exp
389+-- | Generates an @Abs Path@ type.
392+mkAbs :: String -> Q Exp
395+-- | Generates an @Rel Path@ type.
398+mkRel :: String -> Q Exp
401+-- | Generates an @Dir Path@ type.
404+mkDir :: String -> Q Exp
407+-- | Generates an @File Path@ type.
410+mkFile :: String -> Q Exp
413+-- | Generates an @Abs (Dir Path)@ type.
416+mkAbsDir :: String -> Q Exp
419+-- | Generates an @Rel (Dir Path)@ type.
422+mkRelDir :: String -> Q Exp
425+-- | Generates an @Abs (File Path)@ type.
428+mkAbsFile :: String -> Q Exp
429+mkAbsFile = undefined
431+-- | Generates an @Rel (File Path)@ type.
434+mkRelFile :: String -> Q Exp
435+mkRelFile = undefined
437+------------------------------------------------------------------------------
439+------------------------------------------------------------------------------
441+separator :: WORD_TYPE
442+separator = SEPARATOR
444+-- If we append an absolute path it may fail with an error if the 'Path'
445+-- implementation stores absolute path information (a leading separator char).
446+-- However, the implementation may choose to store the path as a list of
447+-- components in which case we cannot distinguish an absolute path from
450+-- | Like 'extendDir' but for the less restrictive 'Path' type which will always
451+-- create a syntactically valid 'Path' type but it may not be semantically valid
452+-- because we may append an absolute path or we may append to a file path.
453+-- The onus lies on the user to ensure that the first path is not a file and
454+-- the second path is not absolute.
455+extendPath :: Path -> Path -> Path
456+extendPath (Path a) (Path b) =
457+ let len = Array.byteLength a + 1 + Array.byteLength b
458+ -- XXX Check the leading separator or drive identifier. However,
459+ -- checking the drive letter may add an additional overhead (can it be
460+ -- arbitrarily long?), if it is significant we may want to have a
461+ -- separate combinePathChecked API for that.
463+ -- Also, do not add a separator char if the first path has a trailing
465+ newArr = unsafePerformIO $ do
466+ arr <- MutArray.new len
467+ arr1 <- MutArray.spliceUnsafe arr (Array.unsafeThaw a)
468+ arr2 <- MutArray.snocUnsafe arr1 separator
469+ arr3 <- MutArray.spliceUnsafe arr2 (Array.unsafeThaw b)
470+ return (Array.unsafeFreeze arr3)
473+-- The only safety we need for paths is: (1) The first path can only be a Dir
474+-- type path, and (2) second path can only be a Rel path.
475+-- Can this be coerced to create unsafe versions?
477+-- | Extend a directory path by appending a relative path to it. This is the
478+-- equivalent to the @</>@ operator from the @filepath@ package.
479+{-# INLINE extendDir #-}
480+extendDir :: (IsPath (a (Dir Path)), IsPath b, IsPath (a b)) =>
481+ (a (Dir Path)) -> Rel b -> a b
482+extendDir a (Rel b) =
483+ fromPathUnsafe $ extendPath (toPath a) (toPath b)