on master help sign in
515 lines 16 kB history raw
1{-# LANGUAGE TemplateHaskell #-}
3-- |
4-- Module : Streamly.Internal.FileSystem.Path
5-- Copyright : (c) 2023 Composewell Technologies
6-- License : BSD3
7-- Maintainer : streamly@composewell.com
8-- Portability : GHC
9--
10-- Well typed and flexible file systems paths, preserving the OS and filesystem
11-- encoding.
12--
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.
19--
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.
24--
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.
35--
36module Streamly.Internal.FileSystem.Path
37 (
38 -- * Path Types
39 Path (..)
40 , File
41 , Dir
42 , Abs
43 , Rel
45 -- * Conversions
46 , IsPath (..)
47 , adaptPath
49 -- * Construction
50 , fromChunk
51 , fromChunkUnsafe
52 , fromString
53 , fromChars
55 -- * Statically Verified Literals
56 -- quasiquoters
57 , path
58 , abs
59 , rel
60 , dir
61 , file
62 , absdir
63 , reldir
64 , absfile
65 , relfile
67 -- * Statically Verified Strings
68 -- TH macros
69 , mkPath
70 , mkAbs
71 , mkRel
72 , mkDir
73 , mkFile
74 , mkAbsDir
75 , mkRelDir
76 , mkAbsFile
77 , mkRelFile
79 -- * Elimination
80 , toChunk
81 , toString
82 , toChars
84 -- * Operations
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.
88 , primarySeparator
89 , isSeparator
90 , extendPath
91 , extendDir
92 )
93where
95import Control.Exception (Exception)
96import Control.Monad.Catch (MonadThrow(..))
97import Data.Char (chr)
98import Data.Functor.Identity (Identity(..))
99import Data.Word (Word8)
100#if defined(mingw32_HOST_OS) || defined(__MINGW32__)
101import Data.Word (Word16)
102#endif
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
119#define SEPARATOR 92
120#else
121#define WORD_TYPE Word8
122#define SEPARATOR 47
123#endif
125------------------------------------------------------------------------------
126-- Exceptions
127------------------------------------------------------------------------------
129-- | Exceptions thrown by path operations.
130data PathException =
131 InvalidPath String
132 | InvalidAbsPath String
133 | InvalidRelPath String
134 | InvalidFilePath String
135 | InvalidDirPath String
136 deriving (Show,Eq)
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
146-- system calls.
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------------------------------------------------------------------------------
154-- Types
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.
164{-
165instance Show Path where
166 show (Path x) = show x
167-}
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
174-- Name), (Dir Name).
176-- | A type representing a file path.
177newtype File a = File a
179-- | A type representing a directory path.
180newtype Dir a = Dir a
182-- | A type representing absolute paths.
183newtype Abs a = Abs a
185-- | A type representing relative paths.
186newtype Rel a = Rel a
188------------------------------------------------------------------------------
189-- Conversions
190------------------------------------------------------------------------------
192-- | A member of 'IsPath' knows how to convert to and from the 'Path' type.
193class IsPath a where
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.
202 --
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.
208 toPath :: a -> Path
210instance IsPath Path where
211 fromPathUnsafe = id
212 fromPath = pure
213 toPath = id
215instance IsPath (File Path) where
216 fromPathUnsafe p = File p
217 fromPath p = pure (File p)
218 toPath (File p) = p
220instance IsPath (Dir Path) where
221 fromPathUnsafe p = Dir p
222 fromPath p = pure (Dir p)
223 toPath (Dir p) = p
225instance IsPath (Abs Path) where
226 fromPathUnsafe p = Abs p
227 fromPath p = pure (Abs p)
228 toPath (Abs p) = p
230instance IsPath (Rel Path) where
231 fromPathUnsafe p = Rel p
232 fromPath p = pure (Rel p)
233 toPath (Rel p) = 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
256-- efficiency.
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
260-- restrictive one.
261adaptPath :: (MonadThrow m, IsPath a, IsPath b) => a -> m b
262adaptPath p = fromPath $ toPath p
264------------------------------------------------------------------------------
265-- Construction
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
274-- uses 'Word16'.
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'.
282--
283-- Throws 'InvalidPath'.
284fromChunk :: MonadThrow m => Array Word8 -> m Path
285fromChunk arr =
286 case Array.cast arr of
287 Nothing ->
288 -- XXX Windows only message.
289 throwM
290 $ InvalidPath
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
303fromChars s =
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))
307#else
308 in pure $ Path (Array.fromPureStreamN n (Unicode.encodeUtf8' s))
309#endif
311-- | Decode the path to a stream of Unicode chars using strict UTF-8 decoding
312-- on Posix.
313-- TBD: Use UTF16LE on Windows.
314toChars :: Monad m => Path -> Stream m Char
315toChars (Path arr) =
316#if defined(mingw32_HOST_OS) || defined(__MINGW32__)
317 Unicode.decodeUtf16le' $ Array.read arr
318#else
319 Unicode.decodeUtf8' $ Array.read arr
320#endif
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.
341--
342-- /Unimplemented/
343path :: QuasiQuoter
344path = undefined
346-- | Generates an @Abs Path@ type from an interpolated string literal.
347--
348-- /Unimplemented/
349abs :: QuasiQuoter
350abs = undefined
352-- | Generates an @Rel Path@ type from an interpolated string literal.
353--
354-- /Unimplemented/
355rel :: QuasiQuoter
356rel = undefined
358-- | Generates an @Dir Path@ type from an interpolated string literal.
359--
360-- /Unimplemented/
361dir :: QuasiQuoter
362dir = undefined
364-- | Generates an @File Path@ type from an interpolated string literal.
365--
366-- /Unimplemented/
367file :: QuasiQuoter
368file = undefined
370-- | Generates an @Abs (Dir Path)@ type from an interpolated string literal.
371--
372-- /Unimplemented/
373absdir :: QuasiQuoter
374absdir = undefined
376-- | Generates an @Rel (Dir Path)@ type from an interpolated string literal.
377--
378-- /Unimplemented/
379reldir :: QuasiQuoter
380reldir = undefined
382-- | Generates an @Abs (File Path)@ type from an interpolated string literal.
383--
384-- /Unimplemented/
385absfile :: QuasiQuoter
386absfile = undefined
388-- | Generates an @Rel (File Path)@ type from an interpolated string literal.
389--
390-- /Unimplemented/
391relfile :: QuasiQuoter
392relfile = undefined
394------------------------------------------------------------------------------
395-- Statically Verified Strings
396------------------------------------------------------------------------------
398-- | Generates a 'Path' type.
399--
400-- /Unimplemented/
401mkPath :: String -> Q Exp
402mkPath = undefined
404-- | Generates an @Abs Path@ type.
405--
406-- /Unimplemented/
407mkAbs :: String -> Q Exp
408mkAbs = undefined
410-- | Generates an @Rel Path@ type.
411--
412-- /Unimplemented/
413mkRel :: String -> Q Exp
414mkRel = undefined
416-- | Generates an @Dir Path@ type.
417--
418-- /Unimplemented/
419mkDir :: String -> Q Exp
420mkDir = undefined
422-- | Generates an @File Path@ type.
423--
424-- /Unimplemented/
425mkFile :: String -> Q Exp
426mkFile = undefined
428-- | Generates an @Abs (Dir Path)@ type.
429--
430-- /Unimplemented/
431mkAbsDir :: String -> Q Exp
432mkAbsDir = undefined
434-- | Generates an @Rel (Dir Path)@ type.
435--
436-- /Unimplemented/
437mkRelDir :: String -> Q Exp
438mkRelDir = undefined
440-- | Generates an @Abs (File Path)@ type.
441--
442-- /Unimplemented/
443mkAbsFile :: String -> Q Exp
444mkAbsFile = undefined
446-- | Generates an @Rel (File Path)@ type.
447--
448-- /Unimplemented/
449mkRelFile :: String -> Q Exp
450mkRelFile = undefined
452------------------------------------------------------------------------------
453-- Operations
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
468-- @/@ or @\\@.
469isSeparator :: Char -> Bool
470#if defined(mingw32_HOST_OS) || defined(__MINGW32__)
471isSeparator c = (c == '/') || (c == '\\')
472#else
473isSeparator = (== '/')
474#endif
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
480-- relative.
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.
494 --
495 -- Also, do not add a separator char if the first path has a trailing
496 -- separator.
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)
503 in Path newArr
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
514extendDir a (Rel b) =
515 fromPathUnsafe $ extendPath (toPath a) (toPath b)