HORIZON HASKELLDocslts/ghc-10.0.xfc4e5982026-10-11Search names, modules, packages, or :: a typeCtrl K

GHC 10.0.0.20260917 · lts/ghc-10.0.x · fc4e598 · 2026-10-11

Modulestreamly-core-0.3.1Haskell2010

Streamly.Internal.FileSystem.WindowsPath

This module implements a WindowsPath type representing a file system path for Windows operating systems. The only assumption about the encoding of the path is that it maps the characters /, \ and . to Word16 representing their ASCII values. Operations are provided to encode and decode using UTF-16LE encoding.

This module has APIs that are equivalent to or can emulate all or most of the filepath package APIs. It has some differences from the filepath package:

  • Empty paths are not allowed. Paths are validated before construction.

  • The default Path type itself affords considerable safety regarding the distinction of rooted or non-rooted paths, it also allows distinguishing directory and file paths.

  • It is designed to provide flexible typing to provide compile time safety for rootednon-rooted paths and filedir paths. The Path type is just part of that typed path ecosystem. Though the default Path type itself should be enough for most cases.

  • It leverages the streamly array module for most of the heavy lifting, it is a thin wrapper on top of that, improving maintainability as well as providing better performance. We can have pinned and unpinned paths, also provide lower level operations for certain cases to interact more efficinetly with low level code.

  • 2 types
  • 1 class
  • 57 values

Setup

0 declarations

To execute the code examples provided in this module in ghci, please run the following commands first.

Example10 expressions
:m:set -XQuasiQuotesimport Control.Exception (SomeException, evaluate, try)import Data.Either (Either, isLeft)import Data.Maybe (fromJust, isNothing, isJust)import Data.Word (Word16)import Streamly.Data.Array (Array)import qualified Streamly.Data.Array as Arrayimport qualified Streamly.Data.Stream as Streamimport qualified Streamly.Unicode.Stream as Unicode

For APIs that have not been released yet.

Example2 expressions
import Streamly.Internal.FileSystem.WindowsPath (WindowsPath, path)import qualified Streamly.Internal.FileSystem.WindowsPath as Path

Utilities:

Example1 expression
fails x = isLeft <$> (try (evaluate x) :: IO (Either SomeException String))

Type

1 declaration
newtypenewtype WindowsPath
#

A type representing file system paths on Windows.

A WindowsPath is validated before construction unless unsafe constructors are used to create it. For validations performed by the safe construction methods see the fromChars function.

Note that in some cases the file system may perform unicode normalization on paths (e.g. Apple HFS), it may cause surprising results as the path used by the user may not have the same bytes as later returned by the file system.

Constructors

Instances9IsPath, …

Conversions

2 declarations
classclass IsPath a b where
#

If the type a b is a member of IsPath it means we know how to convert the type b to and from the base type a.

Methods

  • unsafeFromPath :: a -> b

    Like fromPath but does not check the properties of Path. The user is responsible to maintain the invariants enforced by the type b otherwise surprising behavior may result.

    This operation provides performance and simplicity when we know that the properties of the path are already verified, for example, when we get the path from the file system or from the OS APIs.

  • fromPath :: MonadThrow m => a -> m b

    Convert a base path type to other forms of well-typed paths. It may fail if the path does not satisfy the properties of the target type.

  • toPath :: b -> a

    Convert a well-typed path to the base path type. Never fails.

Instances18IsPath, …

Conversion to OsWord

2 declarations
valuecharToWord :: Char -> Word16
#

Unsafe, truncates the Char to Word8 on Posix and Word16 on Windows.

Validation

4 declarations
valuevalidatePath :: MonadThrow m => Array Word16 -> m ()
#

Checks whether the filepath is valid; i.e., whether the operating system permits such a path for listing or creating files. These validations are operating system specific and file system independent. Throws an exception with a detailed explanation if the path is invalid.

Example1 expression
isValid = isJust . Path.validatePath . Path.encodeString

General validations:

Example2 expressions
isValid ""FalseisValid "\0"False

Windows invalid characters:

Example4 expressions
isValid "c::"FalseisValid "c:\\x:y"FalseisValid "x*"FalseisValid "x\ty" -- control charactersFalse

Windows invalid path components:

Example7 expressions
isValid "pRn.txt"FalseisValid " pRn .txt"FalseisValid "c:\\x\\pRn"FalseisValid "c:\\x\\pRn.txt"FalseisValid "c:\\pRn\\x"FalseisValid "c:\\ pRn \\x"FalseisValid "pRn.x.txt"False

Windows drive root validations:

Example7 expressions
isValid "c:"TrueisValid "c:a\\b"TrueisValid "c:\\"TrueisValid "c:\\\\"FalseisValid "c:\\/"FalseisValid "c:\\\\x"FalseisValid "c:\\/x"False

Mixing path separators: >>> isValid "/x\y" True >>> isValid "\/" -- ? True >>> isValid "/\" -- ? True >>> isValid "\xy" -- ? True >>> isValid "x\y" -- ? True >>> isValid "x\y" -- ? True

Windows share path validations:

Example11 expressions
isValid "\\"TrueisValid "\\\\"FalseisValid "\\\\\\"FalseisValid "\\\\x"FalseisValid "\\\\x\\"TrueisValid "\\\\x\\y"TrueisValid "//x/y"TrueisValid "\\\\prn\\y"FalseisValid "\\\\x\\\\"FalseisValid "\\\\x\\\\x"FalseisValid "\\\\\\x"False

Windows short UNC path validations:

Example7 expressions
isValid "\\\\?\\c:"FalseisValid "\\\\?\\c:\\"TrueisValid "\\\\?\\c:x"FalseisValid "\\\\?\\c:\\\\" -- XXX validate thisFalseisValid "\\\\?\\c:\\x"TrueisValid "\\\\?\\c:\\\\\\"FalseisValid "\\\\?\\c:\\\\x"False

Windows long UNC path validations:

Example3 expressions
isValid "\\\\?\\UnC\\x" -- UnC treated as share nameTrueisValid "\\\\?\\UNC\\x" -- XXX fixFalseisValid "\\\\?\\UNC\\c:\\x"True

DOS local/global device namespace

Example2 expressions
isValid "\\\\.\\x"TrueisValid "\\\\??\\x"True

Other than these there may be maximum path component length and maximum path length restrictions enforced by the OS as well as the filesystem which we do not validate.

valuevalidatePath' :: MonadThrow m => Array Word16 -> m ()
#

Like validatePath but more strict. The path must refer to a file system object. For example, a share root itself is not a valid file system object. it must be followed by a non-empty path.

Example1 expression
isValid = isJust . Path.validatePath' . Path.encodeString
Example4 expressions
isValid "\\\\"FalseisValid "\\\\server\\"FalseisValid "\\\\server\\x"TrueisValid "\\\\?\\UNC\\server"False

Construction

8 declarations

Convert an encoded array of Word16 into a value of type WindowsPath. The path is validated using validatePath.

Each Word16 should be encoded such that:

  • The input does not contain a NUL word.

  • The Word16 is encoded with little-endian ordering.

  • Values from 1-128 are assumed to be ASCII characters.

Apart from the above, there are no restrictions on the encoding.

To bypass path validation checks, use unsafeFromArray.

Throws InvalidPath if validatePath fails on the resulting path.

Like fromString but a streaming operation.

Example1 expression
fromString = Path.fromChars . Stream.fromList

We do not sanitize the path i.e. we do not remove duplicate separators, redundant . segments, trailing separators etc because that would require unnecessary checks and modifications to the path which may not be used ever for any useful purpose, it is only needed for path equality and can be done during the equality check.

Unicode normalization is not done. If normalization is needed the user can normalize it and then use the fromArray API.

valueencodeString :: [Char] -> Array Word16
#

Create an array from a path string using strict UTF-16LE encoding. The path is not validated, therefore, it may not be valid according to validatePath.

Same as toArray . unsafeFromString.

valuereadArray :: [Char] -> WindowsPath
#

Read a raw array of Word16 as a path type.

Example1 expression
readArray = fromJust . Path.fromArray . read
Example2 expressions
arr :: Array Word16 = Path.encodeString "hello"Path.showArray $ (Path.readArray $ show arr :: Path.WindowsPath)"fromList [104,101,108,108,111]"

See also: showArray.

Statically Verified String Literals

1 declaration

Quasiquoters.

Statically Verified Strings

1 declaration

Template Haskell expression splices.

valuepathE :: String -> Q Exp
#

Generates a Haskell expression of type WindowsPath from a String. Equivalent to using fromString on the string passed.

Elimination

7 declarations

Separators

3 declarations
valueseparator :: Word16
#

The primary path separator word: / on POSIX and \ on Windows. Windows also supports / as a valid separator. Use isSeparator to check for any valid path separator.

valueisSeparator :: Word16 -> Bool
#

On POSIX, only / is a path separator, whereas on Windows both / and \ are valid separators.

Dir or non-dir paths

3 declarations

Remove all trailing path separators from the given Path.

Instead of this operation you may want to use eqPath with ignoreTrailingSeparators option.

This operation is careful not to alter the semantic meaning of the path. For example, on Windows:

  • Dropping the separator from "C:/" would change the meaning of the path from referring to the root of the C: drive to the current directory on C:.

  • If a path ends with a separator immediately after a colon (e.g., "C:/"), the separator will not be removed.

If the input path is invalid, the behavior is not fully guaranteed:

  • The separator may still be dropped.

  • In some cases, dropping the separator may make an invalid path valid (e.g., "C:\\" or "C:\/").

This operation may convert a path that implicitly refers to a directory into one that does not.

Typically, if the path is dir//, the result is dir. Special cases include:

  • On POSIX: dropping from "//" yields "/".

  • On Windows: dropping from "C://" results in "C:/".

Examples:

Example2 expressions
f = Path.toString . Path.dropTrailingSeparators . Path.fromString_f "./""."
> f "//"  -- On POSIX

"/"

Returns True if the path ends with a trailing separator.

This typically indicates that the path is a directory, though this is not guaranteed in all cases.

Example:

Example1 expression
Path.hasTrailingSeparator (Path.fromString_ "foo/")True
Example1 expression
Path.hasTrailingSeparator (Path.fromString_ "foo")False

Add a trailing path separator to a path if it doesn't already have one.

Instead of this operation you may want to use eqPath with ignoreTrailingSeparators option.

This function avoids modifying the path if doing so would change its meaning or make it invalid. For example, on Windows:

  • Adding a separator to "C:" would change it from referring to the current directory on the C: drive to the root directory.

  • Adding a separator to "\" could turn it into a UNC share path, which may not be intended.

  • If the path ends with a colon (e.g., "C:"), a separator is not added.

This operation typically makes the path behave like an implicit directory path.

Path Segment Types

2 declarations
valueisRooted :: WindowsPath -> Bool
#

A path that is attached to a root. "C:\" is considered an absolute root and "." as a dynamic root. ".." is not considered a root, "../x" or "x/y" are not rooted paths.

Absolute locations:

  • C:\ local drive

  • \\share\ UNC share

  • \\?\C:\ Long UNC local drive

  • \\?\UNC\ Long UNC remote server

  • \\.\ DOS local device namespace

  • \\??\ DOS global namespace

Relative locations:

  • \ relative to current drive root

  • .\ relative to current directory

  • C: current directory in drive

  • C:file relative to current directory in drive

Example1 expression
isRooted = Path.isRooted . fromJust . Path.fromString

Common to Windows and Posix:

Example4 expressions
isRooted "/"TrueisRooted "/x"TrueisRooted "."TrueisRooted "./x"True

Windows specific:

Example4 expressions
isRooted "c:"TrueisRooted "c:x"TrueisRooted "c:/"TrueisRooted "//x/y"True
valueisUnrooted :: WindowsPath -> Bool
#

A path that is not attached to a root e.g. a/b/c or ../b/c.

Example1 expression
isUnrooted = not . Path.isRooted
Example1 expression
isUnrooted = Path.isUnrooted . Path.fromString_
Example4 expressions
isUnrooted "x"TrueisUnrooted "x/y"TrueisUnrooted ".."TrueisUnrooted "../x"True

Joining

5 declarations

Like join but does not check if any of the path is empty or if the second path is rooted.

Example1 expression
f a b = Path.toString $ Path.unsafeJoin (Path.fromString_ a) (Path.fromString_ b)
Example4 expressions
f "x" "y""x\\y"f "x/" "y""x/y"f "x" "/y""x/y"f "x/" "/y""x/y"

Note "c:" and "/x" are both rooted paths, therefore, join cannot be used to join them. Similarly for joining "/x" and "/y". For these cases use unsafeJoin. unsafeJoin can be used as a replacement for the joinDrive function from the filepath package.

Example2 expressions
f "c:" "/x""c:/x"f "//x/" "/y""//x/y"

Append a WindowsPath to another. Fails if the second path refers to a rooted path. If you want to avoid runtime failure use the typesafe Streamly.FileSystem.WindowsPath.Seg module. Use unsafeJoin to avoid failure if you know it is ok to append the path.

Usually, append joins two paths using a separator between the paths. On Windows, joining a drive "c:" with path "x" does not add a separator between the two because "c:x" is different from "c:/x".

Note "c:" and "/x" are both rooted paths, therefore, join cannot be used to join them. Similarly for joining "/x" and "/y". For these cases use unsafeJoin.

Example1 expression
f a b = Path.toString $ Path.join a b
Example5 expressions
f [path|x|] [path|y|]"x\\y"f [path|x/|] [path|y|]"x/y"f [path|c:|] [path|x|]"c:x"f [path|c:/|] [path|x|]"c:/x"f [path|//x/|] [path|y|]"//x/y"
Example4 expressions
fails $ f [path|c:|] [path|/|]Truefails $ f [path|c:|] [path|/x|]Truefails $ f [path|c:/|] [path|/x|]Truefails $ f [path|//x/|] [path|/y|]True

A stricter version of join which requires the first path to be a directory like path i.e. with a trailing separator.

Example1 expression
f a b = Path.toString $ Path.joinDir a b
Example1 expression
fails $ f [path|x|] [path|y|]True

Join paths by path separator. Does not check if the paths being appended are rooted or branches. Note that splitting and joining may not give exactly the original path but an equivalent path.

Unimplemented

Splitting

6 declarations

Note: you can use unsafeJoin as a replacement for the joinDrive function in the filepath package.

If a path is rooted then separate the root and the remaining path, otherwise root is returned as empty. If the path is rooted then the non-root part is guaranteed to not start with a separator.

See Streamly.Internal.FileSystem.PosixPath module for common examples. We provide some Windows specific examples here.

Example2 expressions
toList (a,b) = (Path.toString a, fmap Path.toString b)split = fmap toList . Path.splitRoot . Path.fromString_
Example1 expression
split "c:"Just ("c:",Nothing)
Example1 expression
split "c:/"Just ("c:/",Nothing)
Example1 expression
split "//x/"Just ("//x/",Nothing)
Example1 expression
split "//x/y"Just ("//x/",Just "y")

Split the path components keeping separators between path components attached to the dir part. Redundant separators are removed, only the first one is kept, but separators are not changed to the default on Windows. Separators are not added either e.g. "." and ".." may not have trailing separators if the original path does not.

Example1 expression
split = Stream.toList . fmap Path.toString . Path.splitPath . Path.fromString_
Example1 expression
split "/x"["/","x"]
Example1 expression
split "/x/\\y"["/","x/","y"]
Example1 expression
split "\\x/\\y" -- this is not valid, multiple seps after share?["\\","x/","y"]

Split a path into components separated by the path separator. "." components in the path are ignored except when in the leading position. Trailing separators in non-root components are dropped.

Example1 expression
split = Stream.toList . fmap Path.toString . Path.splitPath_ . Path.fromString_
Example1 expression
split "c:x"["c:","x"]
Example1 expression
split "c:/" -- Note, c:/ is not the same as c:["c:/"]
Example1 expression
split "c:/x"["c:/","x"]
Example1 expression
split "//x/y/"["//x","y"]
Example1 expression
split "./a"[".","a"]
Example1 expression
split "c:./a"["c:","a"]
Example1 expression
split "a/."["a"]
Example1 expression
split "/x"["/","x"]
Example1 expression
split "/x/\\y"["/","x","y"]
Example1 expression
split "\\x/\\y"["\\","x","y"]

If the path does not look like a directory then return Just (Maybe dir, file) otherwise return Nothing. The path is not a directory if:

  • the path does not end with a separator

  • the path does not end with a . or .. component

Splits a single component path into Just (Nothing, path) if it does not look like a dir.

Example2 expressions
toList (a,b) = (fmap Path.toString a, Path.toString b)split = fmap toList . Path.splitFile . Path.fromString_
Example1 expression
split "/"Nothing
Example1 expression
split "."Nothing
Example1 expression
split "/."Nothing
Example1 expression
split ".."Nothing
> split "//" -- Posix

Nothing

Example1 expression
split "/home"Just (Just "/","home")
Example1 expression
split "./home"Just (Just "./","home")
Example1 expression
split "home"Just (Nothing,"home")
Example1 expression
split "x/"Nothing
Example1 expression
split "x/y"Just (Just "x/","y")
Example1 expression
split "x//y"Just (Just "x//","y")
Example1 expression
split "x/./y"Just (Just "x/./","y")

Split the path into the last component and rest of the path. Treats the entire root or share name, if present, as the first component.

Example2 expressions
basename = snd . Path.splitLast -- Posix basenamedirname = fst . Path.splitLast -- Posix dirname

Unimplemented

Extension

See Streamly.Internal.FileSystem.PosixPath module for detailed documentation and examples. We provide some Windows specific examples here.

Note: On Windows we cannot create a file named "prn." or "prn..". Thus it considers anything starting with and including the first "." as the extension and the part before it as the filename. Our definition considers "prn." as a filename without an extension.

Example2 expressions
toList (a,b) = (Path.toString a, Path.toString b)split = fmap toList . Path.splitExtension . Path.fromString_
Example1 expression
split "x:y"Nothing
Example1 expression
split "x:.y"Nothing

Drop the extension of a file if it has one.

Example1 expression
dropExtension p = maybe p fst $ Path.splitExtension p
Example1 expression
Path.toString $ Path.dropExtension [path|/home/user/file.txt|]"/home/user/file"

Add an extension to a file path. If a non-empty extension does not start with a leading dot then a dot is inserted, otherwise the extension is concatenated with the path.

It is an error to add an extension to a path with a trailing separator.

Unimplemented

Path View

Extracts the file name component (with extension) from a WindowsPath, if present.

Example2 expressions
takeFileName = fmap snd . Path.splitFilereplaceDirectory p x = fmap (flip Path.join x) (takeFileName p)
Example2 expressions
fmap Path.toString $ Path.takeFileName [path|/home/user/file.txt|]Just "file.txt"fmap Path.toString $ Path.takeFileName [path|/home/user/|]Nothing

See splitFile for more examples.

Returns the parent directory of the given WindowsPath, if any.

Example2 expressions
takeDirectory x = Path.splitFile x >>= fstreplaceFileName p x = fmap (flip Path.join x) (takeDirectory p)

To get an equivalent to takeDirectory from filepath use dropTrailingSeparators on the result.

Example2 expressions
fmap Path.toString $ Path.takeDirectory [path|/home/user/file.txt|]Just "/home/user/"fmap Path.toString $ Path.takeDirectory [path|file.txt|]Nothing

Take the extension of a file if it has one.

Example2 expressions
takeExtension = fmap snd . Path.splitExtensionhasExtension = isJust . Path.splitExtension
Example1 expression
fmap Path.toString $ Path.takeExtension [path|/home/user/file.txt|]Just ".txt"

See splitExtension for more examples.

Extracts the file name dropping the extension, if any, from a WindowsPath.

Example1 expression
takeFileBase = fmap Path.dropExtension . Path.takeFileName
Example4 expressions
fmap Path.toString $ Path.takeFileBase [path|/home/user/file.txt|]Just "file"fmap Path.toString $ Path.takeFileBase [path|/home/user/file|]Just "file"fmap Path.toString $ Path.takeFileBase [path|/home/user/.txt|]Just ".txt"fmap Path.toString $ Path.takeFileBase [path|/home/user/|]Nothing

See splitFile for more examples.

Equality

7 declarations
datadata EqCfg
#

Options for path comparison operation. By default path comparison uses a strict criteria for equality. The following options are provided to control the strictness.

The default configuration is as follows:

Example1 expression
:{defaultMod = ignoreTrailingSeparators False           . ignoreCase False           . allowRelativeEquality False:}

When set to False (default):

Example2 expressions
cfg = Path.ignoreTrailingSeparators Falseeq a b = Path.eqPath cfg (Path.fromString_ a) (Path.fromString_ b)
Example1 expression
eq "x/"  "x"False

When set to True:

Example2 expressions
cfg = Path.ignoreTrailingSeparators Trueeq a b = Path.eqPath cfg (Path.fromString_ a) (Path.fromString_ b)
Example1 expression
eq "x/"  "x"True

Default: False

Allow relative paths to be treated as equal. When this is False relative paths will never match even if they are literally equal e.g. "./x" will not match "./x" because the meaning of "." in both cases could be different depending on what the user meant by current directory in each case.

When set to False (default):

Example5 expressions
cfg = Path.allowRelativeEquality Falseeq a b = Path.eqPath cfg (Path.fromString_ a) (Path.fromString_ b)eq "."  "."Falseeq "./x"  "./x"Falseeq "./x"  "x"False

When set to False (default):

Example5 expressions
cfg = Path.allowRelativeEquality Trueeq a b = Path.eqPath cfg (Path.fromString_ a) (Path.fromString_ b)eq "."  "."Trueeq "./x"  "./x"Trueeq "./x"  "x"True
Example1 expression
eq "./x"  "././x"True

Default: False

valueeqPath :: (EqCfg -> EqCfg) -> WindowsPath -> WindowsPath -> Bool
#

See the eqPath documentation in the Streamly.Internal.FileSystem.PosixPath module for details.

On Windows, the following is different:

  • paths are normalized by replacing forward slash path separators by backslashes.

  • default configuration uses case-insensitive comparison.

Example1 expression
:{ eq a b = Path.eqPath id (Path.fromString_ a) (Path.fromString_ b):}

The cases that are different from Posix:

Example1 expression
eq "x\\y" "x/y"True
Example1 expression
eq "x"  "X"True
Example1 expression
eq "c:"  "C:"False
Example1 expression
eq "c:"  "c:"False
Example1 expression
eq "c:x"  "c:x"False
Example1 expression
:{ cfg = Path.ignoreTrailingSeparators True     . Path.ignoreCase True     . Path.allowRelativeEquality True eq a b = Path.eqPath cfg (Path.fromString_ a) (Path.fromString_ b):}
Example1 expression
eq "./x"  "x"True
Example1 expression
eq "X/"  "x"True
Example1 expression
eq "C:x"  "c:X"True
Example1 expression
eq ".\\x"  "./X"True
Example1 expression
eq "x//y"  "x/y"True
Example1 expression
eq "x/./y"  "x/y"True
Example1 expression
eq "x"  "x"True

Check two paths for byte level equality. This is the most strict path equality check.

Example1 expression
eqPath a b = Path.eqPathBytes (Path.fromString_ a) (Path.fromString_ b)
Example1 expression
eqPath "x//y"  "x//y"True
Example1 expression
eqPath "x//y"  "x/y"False
Example1 expression
eqPath "x/./y"  "x/y"False
Example1 expression
eqPath "x\\y" "x/y"False
Example1 expression
eqPath "./file"  "file"False
Example1 expression
eqPath "file/"  "file"False

Convert the path to an equivalent but standard format for reliable comparison. This can be implemented if required. Usually, the equality operations should be enough and this may not be needed.

Unimplemented