From 66b3f4887275951d102821666c34aa45361b5a1c Mon Sep 17 00:00:00 2001 From: Julian Ospald Date: Sun, 8 May 2022 20:19:46 +0200 Subject: [PATCH] Add AFPP support --- System/Directory.hs | 1559 +--------------- System/Directory/AbstractFilePath.hs | 142 ++ System/Directory/Internal/AbstractFilePath.hs | 30 + System/Directory/Internal/Common.hs | 324 +--- .../Internal/Common/AbstractFilePath.hs | 51 + System/Directory/Internal/Common/Template.hs | 312 ++++ .../Internal/Config/AbstractFilePath.hs | 13 + System/Directory/Internal/Posix.hs | 28 + .../Internal/Posix/AbstractFilePath.hs | 43 + .../Internal/{Posix.hsc => Posix/Template.hs} | 180 +- System/Directory/Internal/PosixFFI.hsc | 28 + System/Directory/Internal/Windows.hs | 39 + .../Internal/Windows/AbstractFilePath.hs | 81 + .../{Windows.hsc => Windows/Template.hs} | 409 ++--- System/Directory/Internal/WindowsFFI.hsc | 92 + .../Internal/WindowsFFI/AbstractFilePath.hsc | 96 + .../Directory/Internal/WindowsFFI/Common.hsc | 68 + System/Directory/Template.hs | 1561 +++++++++++++++++ System/File/AbstractFilePath.hs | 30 + System/File/Common.hs | 82 + System/File/PlatformFilePath.hs | 28 + System/File/Posix.hs | 32 + System/File/Windows.hs | 111 ++ cabal.project | 14 + directory.cabal | 36 +- 25 files changed, 3134 insertions(+), 2255 deletions(-) create mode 100644 System/Directory/AbstractFilePath.hs create mode 100644 System/Directory/Internal/AbstractFilePath.hs create mode 100644 System/Directory/Internal/Common/AbstractFilePath.hs create mode 100644 System/Directory/Internal/Common/Template.hs create mode 100644 System/Directory/Internal/Config/AbstractFilePath.hs create mode 100644 System/Directory/Internal/Posix.hs create mode 100644 System/Directory/Internal/Posix/AbstractFilePath.hs rename System/Directory/Internal/{Posix.hsc => Posix/Template.hs} (58%) create mode 100644 System/Directory/Internal/PosixFFI.hsc create mode 100644 System/Directory/Internal/Windows.hs create mode 100644 System/Directory/Internal/Windows/AbstractFilePath.hs rename System/Directory/Internal/{Windows.hsc => Windows/Template.hs} (54%) create mode 100644 System/Directory/Internal/WindowsFFI.hsc create mode 100644 System/Directory/Internal/WindowsFFI/AbstractFilePath.hsc create mode 100644 System/Directory/Internal/WindowsFFI/Common.hsc create mode 100644 System/Directory/Template.hs create mode 100644 System/File/AbstractFilePath.hs create mode 100644 System/File/Common.hs create mode 100644 System/File/PlatformFilePath.hs create mode 100644 System/File/Posix.hs create mode 100644 System/File/Windows.hs create mode 100644 cabal.project diff --git a/System/Directory.hs b/System/Directory.hs index 1defc9e0..cec489fe 100644 --- a/System/Directory.hs +++ b/System/Directory.hs @@ -128,1559 +128,8 @@ import System.FilePath ) import Data.Time (UTCTime) import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds) +import Data.String ( fromString ) -{- $intro -A directory contains a series of entries, each of which is a named -reference to a file system object (file, directory etc.). Some -entries may be hidden, inaccessible, or have some administrative -function (e.g. @.@ or @..@ under -), but in -this standard all such entries are considered to form part of the -directory contents. Entries in sub-directories are not, however, -considered to form part of the directory contents. - -Each file system object is referenced by a /path/. There is -normally at least one absolute path to each file system object. In -some operating systems, it may also be possible to have paths which -are relative to the current directory. - -Unless otherwise documented: - -* 'IO' operations in this package may throw any 'IOError'. No other types of - exceptions shall be thrown. - -* The list of possible 'IOErrorType's in the API documentation is not - exhaustive. The full list may vary by platform and/or evolve over time. - --} - ------------------------------------------------------------------------------ --- Permissions - -{- $permissions - -directory offers a limited (and quirky) interface for reading and setting file -and directory permissions; see 'getPermissions' and 'setPermissions' for a -discussion of their limitations. Because permissions are very difficult to -implement portably across different platforms, users who wish to do more -sophisticated things with permissions are advised to use other, -platform-specific libraries instead. For example, if you are only interested -in permissions on POSIX-like platforms, - -offers much more flexibility. - - The 'Permissions' type is used to record whether certain operations are - permissible on a file\/directory. 'getPermissions' and 'setPermissions' - get and set these permissions, respectively. Permissions apply both to - files and directories. For directories, the executable field will be - 'False', and for files the searchable field will be 'False'. Note that - directories may be searchable without being readable, if permission has - been given to use them as part of a path, but not to examine the - directory contents. - -Note that to change some, but not all permissions, a construct on the following lines must be used. - -> makeReadable f = do -> p <- getPermissions f -> setPermissions f (p {readable = True}) - --} - -emptyPermissions :: Permissions -emptyPermissions = Permissions { - readable = False, - writable = False, - executable = False, - searchable = False - } - -setOwnerReadable :: Bool -> Permissions -> Permissions -setOwnerReadable b p = p { readable = b } - -setOwnerWritable :: Bool -> Permissions -> Permissions -setOwnerWritable b p = p { writable = b } - -setOwnerExecutable :: Bool -> Permissions -> Permissions -setOwnerExecutable b p = p { executable = b } - -setOwnerSearchable :: Bool -> Permissions -> Permissions -setOwnerSearchable b p = p { searchable = b } - --- | Get the permissions of a file or directory. --- --- On Windows, the 'writable' permission corresponds to the "read-only" --- attribute. The 'executable' permission is set if the file extension is of --- an executable file type. The 'readable' permission is always set. --- --- On POSIX systems, this returns the result of @access@. --- --- The operation may fail with: --- --- * 'isPermissionError' if the user is not permitted to access the --- permissions, or --- --- * 'isDoesNotExistError' if the file or directory does not exist. -getPermissions :: FilePath -> IO Permissions -getPermissions path = - (`ioeAddLocation` "getPermissions") `modifyIOError` do - getAccessPermissions (emptyToCurDir path) - --- | Set the permissions of a file or directory. --- --- On Windows, this is only capable of changing the 'writable' permission, --- which corresponds to the "read-only" attribute. Changing the other --- permissions has no effect. --- --- On POSIX systems, this sets the /owner/ permissions. --- --- The operation may fail with: --- --- * 'isPermissionError' if the user is not permitted to set the permissions, --- or --- --- * 'isDoesNotExistError' if the file or directory does not exist. -setPermissions :: FilePath -> Permissions -> IO () -setPermissions path p = - (`ioeAddLocation` "setPermissions") `modifyIOError` do - setAccessPermissions (emptyToCurDir path) p - --- | Copy the permissions of one file to another. This reproduces the --- permissions more accurately than using 'getPermissions' followed by --- 'setPermissions'. --- --- On Windows, this copies only the read-only attribute. --- --- On POSIX systems, this is equivalent to @stat@ followed by @chmod@. -copyPermissions :: FilePath -> FilePath -> IO () -copyPermissions src dst = - (`ioeAddLocation` "copyPermissions") `modifyIOError` do - m <- getFileMetadata src - copyPermissionsFromMetadata m dst - -copyPermissionsFromMetadata :: Metadata -> FilePath -> IO () -copyPermissionsFromMetadata m dst = do - -- instead of setFileMode, setFilePermissions is used here - -- this is to retain backward compatibility in copyPermissions - setFilePermissions dst (modeFromMetadata m) - ------------------------------------------------------------------------------ --- Implementation - -{- |@'createDirectory' dir@ creates a new directory @dir@ which is -initially empty, or as near to empty as the operating system -allows. - -The operation may fail with: - -* 'isPermissionError' -The process has insufficient privileges to perform the operation. -@[EROFS, EACCES]@ - -* 'isAlreadyExistsError' -The operand refers to a directory that already exists. -@ [EEXIST]@ - -* @HardwareFault@ -A physical I\/O error has occurred. -@[EIO]@ - -* @InvalidArgument@ -The operand is not a valid directory name. -@[ENAMETOOLONG, ELOOP]@ - -* 'isDoesNotExistError' -There is no path to the directory. -@[ENOENT, ENOTDIR]@ - -* 'System.IO.isFullError' -Insufficient resources (virtual memory, process file descriptors, -physical disk space, etc.) are available to perform the operation. -@[EDQUOT, ENOSPC, ENOMEM, EMLINK]@ - -* @InappropriateType@ -The path refers to an existing non-directory object. -@[EEXIST]@ - --} - -createDirectory :: FilePath -> IO () -createDirectory = createDirectoryInternal - --- | @'createDirectoryIfMissing' parents dir@ creates a new directory --- @dir@ if it doesn\'t exist. If the first argument is 'True' --- the function will also create all parent directories if they are missing. -createDirectoryIfMissing :: Bool -- ^ Create its parents too? - -> FilePath -- ^ The path to the directory you want to make - -> IO () -createDirectoryIfMissing create_parents path0 - | create_parents = createDirs (parents path0) - | otherwise = createDirs (take 1 (parents path0)) - where - parents = reverse . scanl1 () . splitDirectories . simplify - - createDirs [] = pure () - createDirs (dir:[]) = createDir dir ioError - createDirs (dir:dirs) = - createDir dir $ \_ -> do - createDirs dirs - createDir dir ioError - - createDir dir notExistHandler = do - r <- tryIOError (createDirectory dir) - case r of - Right () -> pure () - Left e - | isDoesNotExistError e -> notExistHandler e - -- createDirectory (and indeed POSIX mkdir) does not distinguish - -- between a dir already existing and a file already existing. So we - -- check for it here. Unfortunately there is a slight race condition - -- here, but we think it is benign. It could report an exception in - -- the case that the dir did exist but another process deletes the - -- directory and creates a file in its place before we can check - -- that the directory did indeed exist. - -- We also follow this path when we get a permissions error, as - -- trying to create "." when in the root directory on Windows - -- fails with - -- CreateDirectory ".": permission denied (Access is denied.) - -- This caused GHCi to crash when loading a module in the root - -- directory. - | isAlreadyExistsError e - || isPermissionError e -> do - canIgnore <- pathIsDirectory dir - `catchIOError` \ _ -> - pure (isAlreadyExistsError e) - unless canIgnore (ioError e) - | otherwise -> ioError e - - -{- | @'removeDirectory' dir@ removes an existing directory /dir/. The -implementation may specify additional constraints which must be -satisfied before a directory can be removed (e.g. the directory has to -be empty, or may not be in use by other processes). It is not legal -for an implementation to partially remove a directory unless the -entire directory is removed. A conformant implementation need not -support directory removal in all situations (e.g. removal of the root -directory). - -The operation may fail with: - -* @HardwareFault@ -A physical I\/O error has occurred. -@[EIO]@ - -* @InvalidArgument@ -The operand is not a valid directory name. -@[ENAMETOOLONG, ELOOP]@ - -* 'isDoesNotExistError' -The directory does not exist. -@[ENOENT, ENOTDIR]@ - -* 'isPermissionError' -The process has insufficient privileges to perform the operation. -@[EROFS, EACCES, EPERM]@ - -* @UnsatisfiedConstraints@ -Implementation-dependent constraints are not satisfied. -@[EBUSY, ENOTEMPTY, EEXIST]@ - -* @UnsupportedOperation@ -The implementation does not support removal in this situation. -@[EINVAL]@ - -* @InappropriateType@ -The operand refers to an existing non-directory object. -@[ENOTDIR]@ - --} - -removeDirectory :: FilePath -> IO () -removeDirectory = removePathInternal True - --- | @'removeDirectoryRecursive' dir@ removes an existing directory /dir/ --- together with its contents and subdirectories. Within this directory, --- symbolic links are removed without affecting their targets. --- --- On Windows, the operation fails if /dir/ is a directory symbolic link. --- --- This operation is reported to be flaky on Windows so retry logic may --- be advisable. See: https://github.com/haskell/directory/pull/108 -removeDirectoryRecursive :: FilePath -> IO () -removeDirectoryRecursive path = - (`ioeAddLocation` "removeDirectoryRecursive") `modifyIOError` do - m <- getSymbolicLinkMetadata path - case fileTypeFromMetadata m of - Directory -> - removeContentsRecursive path - DirectoryLink -> - ioError (err `ioeSetErrorString` "is a directory symbolic link") - _ -> - ioError (err `ioeSetErrorString` "not a directory") - where err = mkIOError InappropriateType "" Nothing (Just path) - --- | @removePathRecursive path@ removes an existing file or directory at --- /path/ together with its contents and subdirectories. Symbolic links are --- removed without affecting their the targets. --- --- This operation is reported to be flaky on Windows so retry logic may --- be advisable. See: https://github.com/haskell/directory/pull/108 -removePathRecursive :: FilePath -> IO () -removePathRecursive path = - (`ioeAddLocation` "removePathRecursive") `modifyIOError` do - m <- getSymbolicLinkMetadata path - case fileTypeFromMetadata m of - Directory -> removeContentsRecursive path - DirectoryLink -> removeDirectory path - _ -> removeFile path - --- | @removeContentsRecursive dir@ removes the contents of the directory --- /dir/ recursively. Symbolic links are removed without affecting their the --- targets. --- --- This operation is reported to be flaky on Windows so retry logic may --- be advisable. See: https://github.com/haskell/directory/pull/108 -removeContentsRecursive :: FilePath -> IO () -removeContentsRecursive path = - (`ioeAddLocation` "removeContentsRecursive") `modifyIOError` do - cont <- listDirectory path - traverse_ removePathRecursive [path x | x <- cont] - removeDirectory path - --- | Removes a file or directory at /path/ together with its contents and --- subdirectories. Symbolic links are removed without affecting their --- targets. If the path does not exist, nothing happens. --- --- Unlike other removal functions, this function will also attempt to delete --- files marked as read-only or otherwise made unremovable due to permissions. --- As a result, if the removal is incomplete, the permissions or attributes on --- the remaining files may be altered. If there are hard links in the --- directory, then permissions on all related hard links may be altered. --- --- If an entry within the directory vanishes while @removePathForcibly@ is --- running, it is silently ignored. --- --- If an exception occurs while removing an entry, @removePathForcibly@ will --- still try to remove as many entries as it can before failing with an --- exception. The first exception that it encountered is re-thrown. --- --- @since 1.2.7.0 -removePathForcibly :: FilePath -> IO () -removePathForcibly path = - (`ioeAddLocation` "removePathForcibly") `modifyIOError` do - makeRemovable path `catchIOError` \ _ -> pure () - ignoreDoesNotExistError $ do - m <- getSymbolicLinkMetadata path - case fileTypeFromMetadata m of - DirectoryLink -> removeDirectory path - Directory -> do - names <- listDirectory path - sequenceWithIOErrors_ $ - [ removePathForcibly (path name) | name <- names ] ++ - [ removeDirectory path ] - _ -> removeFile path - where - - ignoreDoesNotExistError :: IO () -> IO () - ignoreDoesNotExistError action = - () <$ tryIOErrorType isDoesNotExistError action - - makeRemovable :: FilePath -> IO () - makeRemovable p = do - perms <- getPermissions p - setPermissions path perms{ readable = True - , searchable = True - , writable = True } - -{- |'removeFile' /file/ removes the directory entry for an existing file -/file/, where /file/ is not itself a directory. The -implementation may specify additional constraints which must be -satisfied before a file can be removed (e.g. the file may not be in -use by other processes). - -The operation may fail with: - -* @HardwareFault@ -A physical I\/O error has occurred. -@[EIO]@ - -* @InvalidArgument@ -The operand is not a valid file name. -@[ENAMETOOLONG, ELOOP]@ - -* 'isDoesNotExistError' -The file does not exist. -@[ENOENT, ENOTDIR]@ - -* 'isPermissionError' -The process has insufficient privileges to perform the operation. -@[EROFS, EACCES, EPERM]@ - -* @UnsatisfiedConstraints@ -Implementation-dependent constraints are not satisfied. -@[EBUSY]@ - -* @InappropriateType@ -The operand refers to an existing directory. -@[EPERM, EINVAL]@ - --} - -removeFile :: FilePath -> IO () -removeFile = removePathInternal False - -{- |@'renameDirectory' old new@ changes the name of an existing -directory from /old/ to /new/. If the /new/ directory -already exists, it is atomically replaced by the /old/ directory. -If the /new/ directory is neither the /old/ directory nor an -alias of the /old/ directory, it is removed as if by -'removeDirectory'. A conformant implementation need not support -renaming directories in all situations (e.g. renaming to an existing -directory, or across different physical devices), but the constraints -must be documented. - -On Win32 platforms, @renameDirectory@ fails if the /new/ directory already -exists. - -The operation may fail with: - -* @HardwareFault@ -A physical I\/O error has occurred. -@[EIO]@ - -* @InvalidArgument@ -Either operand is not a valid directory name. -@[ENAMETOOLONG, ELOOP]@ - -* 'isDoesNotExistError' -The original directory does not exist, or there is no path to the target. -@[ENOENT, ENOTDIR]@ - -* 'isPermissionError' -The process has insufficient privileges to perform the operation. -@[EROFS, EACCES, EPERM]@ - -* 'System.IO.isFullError' -Insufficient resources are available to perform the operation. -@[EDQUOT, ENOSPC, ENOMEM, EMLINK]@ - -* @UnsatisfiedConstraints@ -Implementation-dependent constraints are not satisfied. -@[EBUSY, ENOTEMPTY, EEXIST]@ - -* @UnsupportedOperation@ -The implementation does not support renaming in this situation. -@[EINVAL, EXDEV]@ - -* @InappropriateType@ -Either path refers to an existing non-directory object. -@[ENOTDIR, EISDIR]@ - --} - -renameDirectory :: FilePath -> FilePath -> IO () -renameDirectory opath npath = - (`ioeAddLocation` "renameDirectory") `modifyIOError` do - -- XXX this test isn't performed atomically with the following rename - isDir <- pathIsDirectory opath - when (not isDir) $ do - ioError . (`ioeSetErrorString` "not a directory") $ - (mkIOError InappropriateType "renameDirectory" Nothing (Just opath)) - renamePath opath npath - -{- |@'renameFile' old new@ changes the name of an existing file system -object from /old/ to /new/. If the /new/ object already exists, it is -replaced by the /old/ object. Neither path may refer to an existing -directory. A conformant implementation need not support renaming files -in all situations (e.g. renaming across different physical devices), but -the constraints must be documented. - -On Windows, this calls @MoveFileEx@ with @MOVEFILE_REPLACE_EXISTING@ set, -which is not guaranteed to be atomic -(). - -On other platforms, this operation is atomic. - -The operation may fail with: - -* @HardwareFault@ -A physical I\/O error has occurred. -@[EIO]@ - -* @InvalidArgument@ -Either operand is not a valid file name. -@[ENAMETOOLONG, ELOOP]@ - -* 'isDoesNotExistError' -The original file does not exist, or there is no path to the target. -@[ENOENT, ENOTDIR]@ - -* 'isPermissionError' -The process has insufficient privileges to perform the operation. -@[EROFS, EACCES, EPERM]@ - -* 'System.IO.isFullError' -Insufficient resources are available to perform the operation. -@[EDQUOT, ENOSPC, ENOMEM, EMLINK]@ - -* @UnsatisfiedConstraints@ -Implementation-dependent constraints are not satisfied. -@[EBUSY]@ - -* @UnsupportedOperation@ -The implementation does not support renaming in this situation. -@[EXDEV]@ - -* @InappropriateType@ -Either path refers to an existing directory. -@[ENOTDIR, EISDIR, EINVAL, EEXIST, ENOTEMPTY]@ - --} - -renameFile :: FilePath -> FilePath -> IO () -renameFile opath npath = - (`ioeAddLocation` "renameFile") `modifyIOError` do - -- XXX the tests are not performed atomically with the rename - checkNotDir opath - renamePath opath npath - -- The underlying rename implementation can throw odd exceptions when the - -- destination is a directory. For example, Windows typically throws a - -- permission error, while POSIX systems may throw a resource busy error - -- if one of the paths refers to the current directory. In these cases, - -- we check if the destination is a directory and, if so, throw an - -- InappropriateType error. - `catchIOError` \ err -> do - checkNotDir npath - ioError err - where checkNotDir path = do - m <- tryIOError (getSymbolicLinkMetadata path) - case fileTypeIsDirectory . fileTypeFromMetadata <$> m of - Right True -> ioError . (`ioeSetErrorString` "is a directory") $ - mkIOError InappropriateType "" Nothing (Just path) - _ -> pure () - --- | Rename a file or directory. If the destination path already exists, it --- is replaced atomically. The destination path must not point to an existing --- directory. A conformant implementation need not support renaming files in --- all situations (e.g. renaming across different physical devices), but the --- constraints must be documented. --- --- The operation may fail with: --- --- * @HardwareFault@ --- A physical I\/O error has occurred. --- @[EIO]@ --- --- * @InvalidArgument@ --- Either operand is not a valid file name. --- @[ENAMETOOLONG, ELOOP]@ --- --- * 'isDoesNotExistError' --- The original file does not exist, or there is no path to the target. --- @[ENOENT, ENOTDIR]@ --- --- * 'isPermissionError' --- The process has insufficient privileges to perform the operation. --- @[EROFS, EACCES, EPERM]@ --- --- * 'System.IO.isFullError' --- Insufficient resources are available to perform the operation. --- @[EDQUOT, ENOSPC, ENOMEM, EMLINK]@ --- --- * @UnsatisfiedConstraints@ --- Implementation-dependent constraints are not satisfied. --- @[EBUSY]@ --- --- * @UnsupportedOperation@ --- The implementation does not support renaming in this situation. --- @[EXDEV]@ --- --- * @InappropriateType@ --- Either the destination path refers to an existing directory, or one of the --- parent segments in the destination path is not a directory. --- @[ENOTDIR, EISDIR, EINVAL, EEXIST, ENOTEMPTY]@ --- --- @since 1.2.7.0 -renamePath :: FilePath -- ^ Old path - -> FilePath -- ^ New path - -> IO () -renamePath opath npath = - (`ioeAddLocation` "renamePath") `modifyIOError` do - renamePathInternal opath npath - --- | Copy a file with its permissions. If the destination file already exists, --- it is replaced atomically. Neither path may refer to an existing --- directory. No exceptions are thrown if the permissions could not be --- copied. -copyFile :: FilePath -- ^ Source filename - -> FilePath -- ^ Destination filename - -> IO () -copyFile fromFPath toFPath = - (`ioeAddLocation` "copyFile") `modifyIOError` do - atomicCopyFileContents fromFPath toFPath - (ignoreIOExceptions . copyPermissions fromFPath) - --- | Copy the contents of a source file to a destination file, replacing the --- destination file atomically via @withReplacementFile@, resetting the --- attributes of the destination file to the defaults. -atomicCopyFileContents :: FilePath -- ^ Source filename - -> FilePath -- ^ Destination filename - -> (FilePath -> IO ()) -- ^ Post-action - -> IO () -atomicCopyFileContents fromFPath toFPath postAction = - (`ioeAddLocation` "atomicCopyFileContents") `modifyIOError` do - withReplacementFile toFPath postAction $ \ hTo -> do - copyFileToHandle fromFPath hTo - --- | A helper function useful for replacing files in an atomic manner. The --- function creates a temporary file in the directory of the destination file, --- opens it, performs the main action with its handle, closes it, performs the --- post-action with its path, and finally replaces the destination file with --- the temporary file. If an error occurs during any step of this process, --- the temporary file is removed and the destination file remains untouched. -withReplacementFile :: FilePath -- ^ Destination file - -> (FilePath -> IO ()) -- ^ Post-action - -> (Handle -> IO a) -- ^ Main action - -> IO a -withReplacementFile path postAction action = - (`ioeAddLocation` "withReplacementFile") `modifyIOError` do - mask $ \ restore -> do - (tmpFPath, hTmp) <- openBinaryTempFile (takeDirectory path) - ".copyFile.tmp" - (`onException` ignoreIOExceptions (removeFile tmpFPath)) $ do - r <- (`onException` ignoreIOExceptions (hClose hTmp)) $ do - restore (action hTmp) - hClose hTmp - restore (postAction tmpFPath) - renameFile tmpFPath path - pure r - --- | Copy a file with its associated metadata. If the destination file --- already exists, it is overwritten. There is no guarantee of atomicity in --- the replacement of the destination file. Neither path may refer to an --- existing directory. If the source and/or destination are symbolic links, --- the copy is performed on the targets of the links. --- --- On Windows, it behaves like the Win32 function --- , --- which copies various kinds of metadata including file attributes and --- security resource properties. --- --- On Unix-like systems, permissions, access time, and modification time are --- preserved. If possible, the owner and group are also preserved. Note that --- the very act of copying can change the access time of the source file, --- hence the access times of the two files may differ after the operation --- completes. --- --- @since 1.2.6.0 -copyFileWithMetadata :: FilePath -- ^ Source file - -> FilePath -- ^ Destination file - -> IO () -copyFileWithMetadata src dst = - (`ioeAddLocation` "copyFileWithMetadata") `modifyIOError` - copyFileWithMetadataInternal copyPermissionsFromMetadata - copyTimesFromMetadata - src - dst - -copyTimesFromMetadata :: Metadata -> FilePath -> IO () -copyTimesFromMetadata st dst = do - let atime = accessTimeFromMetadata st - let mtime = modificationTimeFromMetadata st - setFileTimes dst (Just atime, Just mtime) - --- | Make a path absolute, normalize the path, and remove as many indirections --- from it as possible. Any trailing path separators are discarded via --- 'dropTrailingPathSeparator'. Additionally, on Windows the letter case of --- the path is canonicalized. --- --- __Note__: This function is a very big hammer. If you only need an absolute --- path, 'makeAbsolute' is sufficient for removing dependence on the current --- working directory. --- --- Indirections include the two special directories @.@ and @..@, as well as --- any symbolic links (and junction points on Windows). The input path need --- not point to an existing file or directory. Canonicalization is performed --- on the longest prefix of the path that points to an existing file or --- directory. The remaining portion of the path that does not point to an --- existing file or directory will still be normalized, but case --- canonicalization and indirection removal are skipped as they are impossible --- to do on a nonexistent path. --- --- Most programs should not worry about the canonicity of a path. In --- particular, despite the name, the function does not truly guarantee --- canonicity of the returned path due to the presence of hard links, mount --- points, etc. --- --- If the path points to an existing file or directory, then the output path --- shall also point to the same file or directory, subject to the condition --- that the relevant parts of the file system do not change while the function --- is still running. In other words, the function is definitively not atomic. --- The results can be utterly wrong if the portions of the path change while --- this function is running. --- --- Since some indirections (symbolic links on all systems, @..@ on non-Windows --- systems, and junction points on Windows) are dependent on the state of the --- existing filesystem, the function can only make a conservative attempt by --- removing such indirections from the longest prefix of the path that still --- points to an existing file or directory. --- --- Note that on Windows parent directories @..@ are always fully expanded --- before the symbolic links, as consistent with the rest of the Windows API --- (such as @GetFullPathName@). In contrast, on POSIX systems parent --- directories @..@ are expanded alongside symbolic links from left to right. --- To put this more concretely: if @L@ is a symbolic link for @R/P@, then on --- Windows @L\\..@ refers to @.@, whereas on other operating systems @L/..@ --- refers to @R@. --- --- Similar to 'System.FilePath.normalise', passing an empty path is equivalent --- to passing the current directory. --- --- @canonicalizePath@ can resolve at least 64 indirections in a single path, --- more than what is supported by most operating systems. Therefore, it may --- return the fully resolved path even though the operating system itself --- would have long given up. --- --- On Windows XP or earlier systems, junction expansion is not performed due --- to their lack of @GetFinalPathNameByHandle@. --- --- /Changes since 1.2.3.0:/ The function has been altered to be more robust --- and has the same exception behavior as 'makeAbsolute'. --- --- /Changes since 1.3.0.0:/ The function no longer preserves the trailing path --- separator. File symbolic links that appear in the middle of a path are --- properly dereferenced. Case canonicalization and symbolic link expansion --- are now performed on Windows. --- -canonicalizePath :: FilePath -> IO FilePath -canonicalizePath = \ path -> - ((`ioeAddLocation` "canonicalizePath") . - (`ioeSetFileName` path)) `modifyIOError` do - -- simplify does more stuff, like upper-casing the drive letter - dropTrailingPathSeparator . simplify <$> - (canonicalizePathWith attemptRealpath =<< prependCurrentDirectory path) - where - - -- allow up to 64 cycles before giving up - attemptRealpath realpath = - attemptRealpathWith (64 :: Int) Nothing realpath - <=< canonicalizePathSimplify - - -- n is a counter to make sure we don't run into an infinite loop; we - -- don't try to do any cycle detection here because an adversary could DoS - -- any arbitrarily clever algorithm - attemptRealpathWith n mFallback realpath path = - case mFallback of - -- too many indirections ... giving up. - Just fallback | n <= 0 -> pure fallback - -- either mFallback == Nothing (first attempt) - -- or n > 0 (still have some attempts left) - _ -> realpathPrefix (reverse (zip prefixes suffixes)) - - where - - segments = splitDirectories path - prefixes = scanl1 () segments - suffixes = tail (scanr () "" segments) - - -- try to call realpath on the largest possible prefix - realpathPrefix candidates = - case candidates of - [] -> pure path - (prefix, suffix) : rest -> do - exist <- doesPathExist prefix - if not exist - -- never call realpath on an inaccessible path - -- (to avoid bugs in system realpath implementations) - -- try a smaller prefix instead - then realpathPrefix rest - else do - mp <- tryIOError (realpath prefix) - case mp of - -- realpath failed: try a smaller prefix instead - Left _ -> realpathPrefix rest - -- realpath succeeded: fine-tune the result - Right p -> realpathFurther (p suffix) p suffix - - -- by now we have a reasonable fallback value that we can use if we - -- run into too many indirections; the fallback value is the same - -- result that we have been returning in versions prior to 1.3.1.0 - -- (this is essentially the fix to #64) - realpathFurther fallback p suffix = - case splitDirectories suffix of - [] -> pure fallback - next : restSuffix -> do - -- see if the 'next' segment is a symlink - mTarget <- tryIOError (getSymbolicLinkTarget (p next)) - case mTarget of - Left _ -> pure fallback - Right target -> do - -- if so, dereference it and restart the whole cycle - let mFallback' = Just (fromMaybe fallback mFallback) - path' <- canonicalizePathSimplify - (p target joinPath restSuffix) - attemptRealpathWith (n - 1) mFallback' realpath path' - --- | Convert a path into an absolute path. If the given path is relative, the --- current directory is prepended and then the combined result is normalized. --- If the path is already absolute, the path is simply normalized. The --- function preserves the presence or absence of the trailing path separator --- unless the path refers to the root directory @/@. --- --- If the path is already absolute, the operation never fails. Otherwise, the --- operation may fail with the same exceptions as 'getCurrentDirectory'. --- --- @since 1.2.2.0 --- -makeAbsolute :: FilePath -> IO FilePath -makeAbsolute path = - ((`ioeAddLocation` "makeAbsolute") . - (`ioeSetFileName` path)) `modifyIOError` do - matchTrailingSeparator path . simplify <$> prependCurrentDirectory path - --- | Add or remove the trailing path separator in the second path so as to --- match its presence in the first path. --- --- (internal API) -matchTrailingSeparator :: FilePath -> FilePath -> FilePath -matchTrailingSeparator path - | hasTrailingPathSeparator path = addTrailingPathSeparator - | otherwise = dropTrailingPathSeparator - --- | Construct a path relative to the current directory, similar to --- 'makeRelative'. --- --- The operation may fail with the same exceptions as 'getCurrentDirectory'. -makeRelativeToCurrentDirectory :: FilePath -> IO FilePath -makeRelativeToCurrentDirectory x = do - (`makeRelative` x) <$> getCurrentDirectory - --- | Given the name or path of an executable file, 'findExecutable' searches --- for such a file in a list of system-defined locations, which generally --- includes @PATH@ and possibly more. The full path to the executable is --- returned if found. For example, @(findExecutable \"ghc\")@ would normally --- give you the path to GHC. --- --- The path returned by @'findExecutable' name@ corresponds to the program --- that would be executed by --- @@ --- when passed the same string (as a @RawCommand@, not a @ShellCommand@), --- provided that @name@ is not a relative path with more than one segment. --- --- On Windows, 'findExecutable' calls the Win32 function --- @@, --- which may search other places before checking the directories in the @PATH@ --- environment variable. Where it actually searches depends on registry --- settings, but notably includes the directory containing the current --- executable. --- --- On non-Windows platforms, the behavior is equivalent to 'findFileWith' --- using the search directories from the @PATH@ environment variable and --- testing each file for executable permissions. Details can be found in the --- documentation of 'findFileWith'. -findExecutable :: String -> IO (Maybe FilePath) -findExecutable binary = - listTHead - (findExecutablesLazyInternal findExecutablesInDirectoriesLazy binary) - --- | Search for executable files in a list of system-defined locations, which --- generally includes @PATH@ and possibly more. --- --- On Windows, this /only returns the first occurrence/, if any. Its behavior --- is therefore equivalent to 'findExecutable'. --- --- On non-Windows platforms, the behavior is equivalent to --- 'findExecutablesInDirectories' using the search directories from the @PATH@ --- environment variable. Details can be found in the documentation of --- 'findExecutablesInDirectories'. --- --- @since 1.2.2.0 -findExecutables :: String -> IO [FilePath] -findExecutables binary = - listTToList - (findExecutablesLazyInternal findExecutablesInDirectoriesLazy binary) - --- | Given a name or path, 'findExecutable' appends the 'exeExtension' to the --- query and searches for executable files in the list of given search --- directories and returns all occurrences. --- --- The behavior is equivalent to 'findFileWith' using the given search --- directories and testing each file for executable permissions. Details can --- be found in the documentation of 'findFileWith'. --- --- Unlike other similarly named functions, 'findExecutablesInDirectories' does --- not use @SearchPath@ from the Win32 API. The behavior of this function on --- Windows is therefore equivalent to those on non-Windows platforms. --- --- @since 1.2.4.0 -findExecutablesInDirectories :: [FilePath] -> String -> IO [FilePath] -findExecutablesInDirectories path binary = - listTToList (findExecutablesInDirectoriesLazy path binary) - -findExecutablesInDirectoriesLazy :: [FilePath] -> String -> ListT IO FilePath -findExecutablesInDirectoriesLazy path binary = - findFilesWithLazy isExecutable path (binary <.> exeExtension) - --- | Test whether a file has executable permissions. -isExecutable :: FilePath -> IO Bool -isExecutable file = executable <$> getPermissions file - --- | Search through the given list of directories for the given file. --- --- The behavior is equivalent to 'findFileWith', returning only the first --- occurrence. Details can be found in the documentation of 'findFileWith'. -findFile :: [FilePath] -> String -> IO (Maybe FilePath) -findFile = findFileWith (\ _ -> pure True) - --- | Search through the given list of directories for the given file and --- returns all paths where the given file exists. --- --- The behavior is equivalent to 'findFilesWith'. Details can be found in the --- documentation of 'findFilesWith'. --- --- @since 1.2.1.0 -findFiles :: [FilePath] -> String -> IO [FilePath] -findFiles = findFilesWith (\ _ -> pure True) - --- | Search through a given list of directories for a file that has the given --- name and satisfies the given predicate and return the path of the first --- occurrence. The directories are checked in a left-to-right order. --- --- This is essentially a more performant version of 'findFilesWith' that --- always returns the first result, if any. Details can be found in the --- documentation of 'findFilesWith'. --- --- @since 1.2.6.0 -findFileWith :: (FilePath -> IO Bool) -> [FilePath] -> String -> IO (Maybe FilePath) -findFileWith f ds name = listTHead (findFilesWithLazy f ds name) - --- | @findFilesWith predicate dirs name@ searches through the list of --- directories (@dirs@) for files that have the given @name@ and satisfy the --- given @predicate@ and returns the paths of those files. The directories --- are checked in a left-to-right order and the paths are returned in the same --- order. --- --- If the @name@ is a relative path, then for every search directory @dir@, --- the function checks whether @dir '' name@ exists and satisfies the --- predicate. If so, @dir '' name@ is returned as one of the results. In --- other words, the returned paths can be either relative or absolute --- depending on the search directories were used. If there are no search --- directories, no results are ever returned. --- --- If the @name@ is an absolute path, then the function will return a single --- result if the file exists and satisfies the predicate and no results --- otherwise. This is irrespective of what search directories were given. --- --- @since 1.2.1.0 -findFilesWith :: (FilePath -> IO Bool) -> [FilePath] -> String -> IO [FilePath] -findFilesWith f ds name = listTToList (findFilesWithLazy f ds name) - -findFilesWithLazy - :: (FilePath -> IO Bool) -> [FilePath] -> String -> ListT IO FilePath -findFilesWithLazy f dirs path - -- make sure absolute paths are handled properly irrespective of 'dirs' - -- https://github.com/haskell/directory/issues/72 - | isAbsolute path = ListT (find [""]) - | otherwise = ListT (find dirs) - - where - - find [] = pure Nothing - find (d : ds) = do - let p = d path - found <- doesFileExist p `andM` f p - if found - then pure (Just (p, ListT (find ds))) - else find ds - --- | Filename extension for executable files (including the dot if any) --- (usually @\"\"@ on POSIX systems and @\".exe\"@ on Windows or OS\/2). --- --- @since 1.2.4.0 -exeExtension :: String -exeExtension = exeExtensionInternal - --- | Similar to 'listDirectory', but always includes the special entries (@.@ --- and @..@). (This applies to Windows as well.) --- --- The operation may fail with the same exceptions as 'listDirectory'. -getDirectoryContents :: FilePath -> IO [FilePath] -getDirectoryContents path = - ((`ioeSetFileName` path) . - (`ioeAddLocation` "getDirectoryContents")) `modifyIOError` do - getDirectoryContentsInternal path - --- | @'listDirectory' dir@ returns a list of /all/ entries in /dir/ without --- the special entries (@.@ and @..@). --- --- The operation may fail with: --- --- * @HardwareFault@ --- A physical I\/O error has occurred. --- @[EIO]@ --- --- * @InvalidArgument@ --- The operand is not a valid directory name. --- @[ENAMETOOLONG, ELOOP]@ --- --- * 'isDoesNotExistError' --- The directory does not exist. --- @[ENOENT, ENOTDIR]@ --- --- * 'isPermissionError' --- The process has insufficient privileges to perform the operation. --- @[EACCES]@ --- --- * 'System.IO.isFullError' --- Insufficient resources are available to perform the operation. --- @[EMFILE, ENFILE]@ --- --- * @InappropriateType@ --- The path refers to an existing non-directory object. --- @[ENOTDIR]@ --- --- @since 1.2.5.0 --- -listDirectory :: FilePath -> IO [FilePath] -listDirectory path = filter f <$> getDirectoryContents path - where f filename = filename /= "." && filename /= ".." - --- | Obtain the current working directory as an absolute path. --- --- In a multithreaded program, the current working directory is a global state --- shared among all threads of the process. Therefore, when performing --- filesystem operations from multiple threads, it is highly recommended to --- use absolute rather than relative paths (see: 'makeAbsolute'). --- --- Note that 'getCurrentDirectory' is not guaranteed to return the same path --- received by 'setCurrentDirectory'. On POSIX systems, the path returned will --- always be fully dereferenced (not contain any symbolic links). For more --- information, refer to the documentation of --- . --- --- The operation may fail with: --- --- * @HardwareFault@ --- A physical I\/O error has occurred. --- @[EIO]@ --- --- * 'isDoesNotExistError' --- There is no path referring to the working directory. --- @[EPERM, ENOENT, ESTALE...]@ --- --- * 'isPermissionError' --- The process has insufficient privileges to perform the operation. --- @[EACCES]@ --- --- * 'System.IO.isFullError' --- Insufficient resources are available to perform the operation. --- --- * @UnsupportedOperation@ --- The operating system has no notion of current working directory. --- -getCurrentDirectory :: IO FilePath -getCurrentDirectory = - (`ioeAddLocation` "getCurrentDirectory") `modifyIOError` do - specializeErrorString - "Current working directory no longer exists" - isDoesNotExistError - getCurrentDirectoryInternal - --- | Change the working directory to the given path. --- --- In a multithreaded program, the current working directory is a global state --- shared among all threads of the process. Therefore, when performing --- filesystem operations from multiple threads, it is highly recommended to --- use absolute rather than relative paths (see: 'makeAbsolute'). --- --- The operation may fail with: --- --- * @HardwareFault@ --- A physical I\/O error has occurred. --- @[EIO]@ --- --- * @InvalidArgument@ --- The operand is not a valid directory name. --- @[ENAMETOOLONG, ELOOP]@ --- --- * 'isDoesNotExistError' --- The directory does not exist. --- @[ENOENT, ENOTDIR]@ --- --- * 'isPermissionError' --- The process has insufficient privileges to perform the operation. --- @[EACCES]@ --- --- * @UnsupportedOperation@ --- The operating system has no notion of current working directory, or the --- working directory cannot be dynamically changed. --- --- * @InappropriateType@ --- The path refers to an existing non-directory object. --- @[ENOTDIR]@ --- -setCurrentDirectory :: FilePath -> IO () -setCurrentDirectory = setCurrentDirectoryInternal - --- | Run an 'IO' action with the given working directory and restore the --- original working directory afterwards, even if the given action fails due --- to an exception. --- --- The operation may fail with the same exceptions as 'getCurrentDirectory' --- and 'setCurrentDirectory'. --- --- @since 1.2.3.0 --- -withCurrentDirectory :: FilePath -- ^ Directory to execute in - -> IO a -- ^ Action to be executed - -> IO a -withCurrentDirectory dir action = - bracket getCurrentDirectory setCurrentDirectory $ \ _ -> do - setCurrentDirectory dir - action - --- | Obtain the size of a file in bytes. --- --- @since 1.2.7.0 -getFileSize :: FilePath -> IO Integer -getFileSize path = - (`ioeAddLocation` "getFileSize") `modifyIOError` do - fileSizeFromMetadata <$> getFileMetadata path - --- | Test whether the given path points to an existing filesystem object. If --- the user lacks necessary permissions to search the parent directories, this --- function may return false even if the file does actually exist. --- --- @since 1.2.7.0 -doesPathExist :: FilePath -> IO Bool -doesPathExist path = do - (True <$ getFileMetadata path) - `catchIOError` \ _ -> - pure False - -{- |The operation 'doesDirectoryExist' returns 'True' if the argument file -exists and is either a directory or a symbolic link to a directory, -and 'False' otherwise. --} - -doesDirectoryExist :: FilePath -> IO Bool -doesDirectoryExist path = do - pathIsDirectory path - `catchIOError` \ _ -> - pure False - -{- |The operation 'doesFileExist' returns 'True' -if the argument file exists and is not a directory, and 'False' otherwise. --} - -doesFileExist :: FilePath -> IO Bool -doesFileExist path = do - (not <$> pathIsDirectory path) - `catchIOError` \ _ -> - pure False - -pathIsDirectory :: FilePath -> IO Bool -pathIsDirectory path = - (`ioeAddLocation` "pathIsDirectory") `modifyIOError` do - fileTypeIsDirectory . fileTypeFromMetadata <$> getFileMetadata path - --- | Create a /file/ symbolic link. The target path can be either absolute or --- relative and need not refer to an existing file. The order of arguments --- follows the POSIX convention. --- --- To remove an existing file symbolic link, use 'removeFile'. --- --- Although the distinction between /file/ symbolic links and /directory/ --- symbolic links does not exist on POSIX systems, on Windows this is an --- intrinsic property of every symbolic link and cannot be changed without --- recreating the link. A file symbolic link that actually points to a --- directory will fail to dereference and vice versa. Moreover, creating --- symbolic links on Windows may require privileges unavailable to users --- outside the Administrators group. Portable programs that use symbolic --- links should take both into consideration. --- --- On Windows, the function is implemented using @CreateSymbolicLink@. Since --- 1.3.3.0, the @SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE@ flag is included --- if supported by the operating system. On POSIX, the function uses @symlink@ --- and is therefore atomic. --- --- Windows-specific errors: This operation may fail with 'permissionErrorType' --- if the user lacks the privileges to create symbolic links. It may also --- fail with 'illegalOperationErrorType' if the file system does not support --- symbolic links. --- --- @since 1.3.1.0 -createFileLink - :: FilePath -- ^ path to the target file - -> FilePath -- ^ path of the link to be created - -> IO () -createFileLink target link = - (`ioeAddLocation` "createFileLink") `modifyIOError` do - createSymbolicLink False target link - --- | Create a /directory/ symbolic link. The target path can be either --- absolute or relative and need not refer to an existing directory. The --- order of arguments follows the POSIX convention. --- --- To remove an existing directory symbolic link, use 'removeDirectoryLink'. --- --- Although the distinction between /file/ symbolic links and /directory/ --- symbolic links does not exist on POSIX systems, on Windows this is an --- intrinsic property of every symbolic link and cannot be changed without --- recreating the link. A file symbolic link that actually points to a --- directory will fail to dereference and vice versa. Moreover, creating --- symbolic links on Windows may require privileges unavailable to users --- outside the Administrators group. Portable programs that use symbolic --- links should take both into consideration. --- --- On Windows, the function is implemented using @CreateSymbolicLink@ with --- @SYMBOLIC_LINK_FLAG_DIRECTORY@. Since 1.3.3.0, the --- @SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE@ flag is also included if --- supported by the operating system. On POSIX, this is an alias for --- 'createFileLink' and is therefore atomic. --- --- Windows-specific errors: This operation may fail with 'permissionErrorType' --- if the user lacks the privileges to create symbolic links. It may also --- fail with 'illegalOperationErrorType' if the file system does not support --- symbolic links. --- --- @since 1.3.1.0 -createDirectoryLink - :: FilePath -- ^ path to the target directory - -> FilePath -- ^ path of the link to be created - -> IO () -createDirectoryLink target link = - (`ioeAddLocation` "createDirectoryLink") `modifyIOError` do - createSymbolicLink True target link - --- | Remove an existing /directory/ symbolic link. --- --- On Windows, this is an alias for 'removeDirectory'. On POSIX systems, this --- is an alias for 'removeFile'. --- --- See also: 'removeFile', which can remove an existing /file/ symbolic link. --- --- @since 1.3.1.0 -removeDirectoryLink :: FilePath -> IO () -removeDirectoryLink path = - (`ioeAddLocation` "removeDirectoryLink") `modifyIOError` do - removePathInternal linkToDirectoryIsDirectory path - --- | Check whether an existing @path@ is a symbolic link. If @path@ is a --- regular file or directory, 'False' is returned. If @path@ does not exist --- or is otherwise inaccessible, an exception is thrown (see below). --- --- On Windows, this checks for @FILE_ATTRIBUTE_REPARSE_POINT@. In addition to --- symbolic links, the function also returns true on junction points. On --- POSIX systems, this checks for @S_IFLNK@. --- --- The operation may fail with: --- --- * 'isDoesNotExistError' if the symbolic link does not exist; or --- --- * 'isPermissionError' if the user is not permitted to read the symbolic --- link. --- --- @since 1.3.0.0 -pathIsSymbolicLink :: FilePath -> IO Bool -pathIsSymbolicLink path = - ((`ioeAddLocation` "pathIsSymbolicLink") . - (`ioeSetFileName` path)) `modifyIOError` do - fileTypeIsLink . fileTypeFromMetadata <$> getSymbolicLinkMetadata path - -{-# DEPRECATED isSymbolicLink "Use 'pathIsSymbolicLink' instead" #-} -isSymbolicLink :: FilePath -> IO Bool -isSymbolicLink = pathIsSymbolicLink - --- | Retrieve the target path of either a file or directory symbolic link. --- The returned path may not be absolute, may not exist, and may not even be a --- valid path. --- --- On Windows systems, this calls @DeviceIoControl@ with --- @FSCTL_GET_REPARSE_POINT@. In addition to symbolic links, the function --- also works on junction points. On POSIX systems, this calls @readlink@. --- --- Windows-specific errors: This operation may fail with --- 'illegalOperationErrorType' if the file system does not support symbolic --- links. --- --- @since 1.3.1.0 -getSymbolicLinkTarget :: FilePath -> IO FilePath -getSymbolicLinkTarget path = - (`ioeAddLocation` "getSymbolicLinkTarget") `modifyIOError` do - readSymbolicLink path - --- | Obtain the time at which the file or directory was last accessed. --- --- The operation may fail with: --- --- * 'isPermissionError' if the user is not permitted to read --- the access time; or --- --- * 'isDoesNotExistError' if the file or directory does not exist. --- --- Caveat for POSIX systems: This function returns a timestamp with sub-second --- resolution only if this package is compiled against @unix-2.6.0.0@ or later --- and the underlying filesystem supports them. --- --- @since 1.2.3.0 --- -getAccessTime :: FilePath -> IO UTCTime -getAccessTime path = - (`ioeAddLocation` "getAccessTime") `modifyIOError` do - accessTimeFromMetadata <$> getFileMetadata (emptyToCurDir path) - --- | Obtain the time at which the file or directory was last modified. --- --- The operation may fail with: --- --- * 'isPermissionError' if the user is not permitted to read --- the modification time; or --- --- * 'isDoesNotExistError' if the file or directory does not exist. --- --- Caveat for POSIX systems: This function returns a timestamp with sub-second --- resolution only if this package is compiled against @unix-2.6.0.0@ or later --- and the underlying filesystem supports them. --- -getModificationTime :: FilePath -> IO UTCTime -getModificationTime path = - (`ioeAddLocation` "getModificationTime") `modifyIOError` do - modificationTimeFromMetadata <$> getFileMetadata (emptyToCurDir path) - --- | Change the time at which the file or directory was last accessed. --- --- The operation may fail with: --- --- * 'isPermissionError' if the user is not permitted to alter the --- access time; or --- --- * 'isDoesNotExistError' if the file or directory does not exist. --- --- Some caveats for POSIX systems: --- --- * Not all systems support @utimensat@, in which case the function can only --- emulate the behavior by reading the modification time and then setting --- both the access and modification times together. On systems where --- @utimensat@ is supported, the access time is set atomically with --- nanosecond precision. --- --- * If compiled against a version of @unix@ prior to @2.7.0.0@, the function --- would not be able to set timestamps with sub-second resolution. In this --- case, there would also be loss of precision in the modification time. --- --- @since 1.2.3.0 --- -setAccessTime :: FilePath -> UTCTime -> IO () -setAccessTime path atime = - (`ioeAddLocation` "setAccessTime") `modifyIOError` do - setFileTimes path (Just atime, Nothing) - --- | Change the time at which the file or directory was last modified. --- --- The operation may fail with: --- --- * 'isPermissionError' if the user is not permitted to alter the --- modification time; or --- --- * 'isDoesNotExistError' if the file or directory does not exist. --- --- Some caveats for POSIX systems: --- --- * Not all systems support @utimensat@, in which case the function can only --- emulate the behavior by reading the access time and then setting both the --- access and modification times together. On systems where @utimensat@ is --- supported, the modification time is set atomically with nanosecond --- precision. --- --- * If compiled against a version of @unix@ prior to @2.7.0.0@, the function --- would not be able to set timestamps with sub-second resolution. In this --- case, there would also be loss of precision in the access time. --- --- @since 1.2.3.0 --- -setModificationTime :: FilePath -> UTCTime -> IO () -setModificationTime path mtime = - (`ioeAddLocation` "setModificationTime") `modifyIOError` do - setFileTimes path (Nothing, Just mtime) - -setFileTimes :: FilePath -> (Maybe UTCTime, Maybe UTCTime) -> IO () -setFileTimes _ (Nothing, Nothing) = return () -setFileTimes path (atime, mtime) = - ((`ioeAddLocation` "setFileTimes") . - (`ioeSetFileName` path)) `modifyIOError` do - setTimes (emptyToCurDir path) - (utcTimeToPOSIXSeconds <$> atime, utcTimeToPOSIXSeconds <$> mtime) - -{- | Returns the current user's home directory. - -The directory returned is expected to be writable by the current user, -but note that it isn't generally considered good practice to store -application-specific data here; use 'getXdgDirectory' or -'getAppUserDataDirectory' instead. - -On Unix, 'getHomeDirectory' behaves as follows: - -* Returns $HOME env variable if set (including to an empty string). -* Otherwise uses home directory returned by `getpwuid_r` using the UID of the current proccesses user. This basically reads the /etc/passwd file. An empty home directory field is considered valid. - -On Windows, the system is queried for a suitable path; a typical path might be @C:\/Users\//\/@. - -The operation may fail with: - -* @UnsupportedOperation@ -The operating system has no notion of home directory. - -* 'isDoesNotExistError' -The home directory for the current user does not exist, or -cannot be found. --} -getHomeDirectory :: IO FilePath -getHomeDirectory = - (`ioeAddLocation` "getHomeDirectory") `modifyIOError` do - getHomeDirectoryInternal - --- | Obtain the paths to special directories for storing user-specific --- application data, configuration, and cache files, conforming to the --- . --- Compared with 'getAppUserDataDirectory', this function provides a more --- fine-grained hierarchy as well as greater flexibility for the user. --- --- On Windows, 'XdgData' and 'XdgConfig' usually map to the same directory --- unless overridden. --- --- Refer to the docs of 'XdgDirectory' for more details. --- --- The second argument is usually the name of the application. Since it --- will be integrated into the path, it must consist of valid path --- characters. Note: if the second argument is an absolute path, it will --- just return the second argument. --- --- Note: The directory may not actually exist, in which case you would need --- to create it with file mode @700@ (i.e. only accessible by the owner). --- --- As of 1.3.5.0, the environment variable is ignored if set to a relative --- path, per revised XDG Base Directory Specification. See --- . --- --- @since 1.2.3.0 -getXdgDirectory :: XdgDirectory -- ^ which special directory - -> FilePath -- ^ a relative path that is appended - -- to the path; if empty, the base - -- path is returned - -> IO FilePath -getXdgDirectory xdgDir suffix = - (`ioeAddLocation` "getXdgDirectory") `modifyIOError` do - simplify . ( suffix) <$> do - env <- lookupEnv $ case xdgDir of - XdgData -> "XDG_DATA_HOME" - XdgConfig -> "XDG_CONFIG_HOME" - XdgCache -> "XDG_CACHE_HOME" - XdgState -> "XDG_STATE_HOME" - case env of - Just path | isAbsolute path -> pure path - _ -> getXdgDirectoryFallback getHomeDirectory xdgDir - --- | Similar to 'getXdgDirectory' but retrieves the entire list of XDG --- directories. --- --- On Windows, 'XdgDataDirs' and 'XdgConfigDirs' usually map to the same list --- of directories unless overridden. --- --- Refer to the docs of 'XdgDirectoryList' for more details. -getXdgDirectoryList :: XdgDirectoryList -- ^ which special directory list - -> IO [FilePath] -getXdgDirectoryList xdgDirs = - (`ioeAddLocation` "getXdgDirectoryList") `modifyIOError` do - env <- lookupEnv $ case xdgDirs of - XdgDataDirs -> "XDG_DATA_DIRS" - XdgConfigDirs -> "XDG_CONFIG_DIRS" - case env of - Nothing -> getXdgDirectoryListFallback xdgDirs - Just paths -> pure (splitSearchPath paths) - --- | Obtain the path to a special directory for storing user-specific --- application data (traditional Unix location). Newer applications may --- prefer the the XDG-conformant location provided by 'getXdgDirectory' --- (). --- --- The argument is usually the name of the application. Since it will be --- integrated into the path, it must consist of valid path characters. --- --- * On Unix-like systems, the path is @~\/./\/@. --- * On Windows, the path is @%APPDATA%\//\/@ --- (e.g. @C:\/Users\//\/\/AppData\/Roaming\//\/@) --- --- Note: the directory may not actually exist, in which case you would need --- to create it. It is expected that the parent directory exists and is --- writable. --- --- The operation may fail with: --- --- * @UnsupportedOperation@ --- The operating system has no notion of application-specific data --- directory. --- --- * 'isDoesNotExistError' --- The home directory for the current user does not exist, or cannot be --- found. --- -getAppUserDataDirectory :: FilePath -- ^ a relative path that is appended - -- to the path - -> IO FilePath -getAppUserDataDirectory appName = do - (`ioeAddLocation` "getAppUserDataDirectory") `modifyIOError` do - getAppUserDataDirectoryInternal appName - -{- | Returns the current user's document directory. - -The directory returned is expected to be writable by the current user, -but note that it isn't generally considered good practice to store -application-specific data here; use 'getXdgDirectory' or -'getAppUserDataDirectory' instead. - -On Unix, 'getUserDocumentsDirectory' returns the value of the @HOME@ -environment variable. On Windows, the system is queried for a -suitable path; a typical path might be @C:\/Users\//\/\/Documents@. - -The operation may fail with: - -* @UnsupportedOperation@ -The operating system has no notion of document directory. - -* 'isDoesNotExistError' -The document directory for the current user does not exist, or -cannot be found. --} -getUserDocumentsDirectory :: IO FilePath -getUserDocumentsDirectory = do - (`ioeAddLocation` "getUserDocumentsDirectory") `modifyIOError` do - getUserDocumentsDirectoryInternal - -{- | Returns the current directory for temporary files. - -On Unix, 'getTemporaryDirectory' returns the value of the @TMPDIR@ -environment variable or \"\/tmp\" if the variable isn\'t defined. -On Windows, the function checks for the existence of environment variables in -the following order and uses the first path found: - -* -TMP environment variable. - -* -TEMP environment variable. - -* -USERPROFILE environment variable. - -* -The Windows directory - -The operation may fail with: - -* @UnsupportedOperation@ -The operating system has no notion of temporary directory. - -The function doesn\'t verify whether the path exists. --} -getTemporaryDirectory :: IO FilePath -getTemporaryDirectory = getTemporaryDirectoryInternal +#define FILEPATH FilePath +#define STRING String +#include "Directory/Template.hs" diff --git a/System/Directory/AbstractFilePath.hs b/System/Directory/AbstractFilePath.hs new file mode 100644 index 00000000..ae1183f2 --- /dev/null +++ b/System/Directory/AbstractFilePath.hs @@ -0,0 +1,142 @@ +{-# LANGUAGE CPP #-} + +#if !MIN_VERSION_base(4, 8, 0) +-- In base-4.8.0 the Foreign module became Safe +{-# LANGUAGE Trustworthy #-} +#endif + +----------------------------------------------------------------------------- +-- | +-- Module : System.Directory.AbstractFilePath +-- Copyright : (c) The University of Glasgow 2001 +-- License : BSD-style (see the file libraries/base/LICENSE) +-- +-- Maintainer : libraries@haskell.org +-- Stability : stable +-- Portability : portable +-- +-- System-independent interface to directory manipulation. +-- +----------------------------------------------------------------------------- + +module System.Directory.AbstractFilePath + ( + -- $intro + + -- * Types + AbstractFilePath + , OsString + + -- * Actions on directories + , createDirectory + , createDirectoryIfMissing + , removeDirectory + , removeDirectoryRecursive + , removePathForcibly + , renameDirectory + , listDirectory + , getDirectoryContents + -- ** Current working directory + , getCurrentDirectory + , setCurrentDirectory + , withCurrentDirectory + + -- * Pre-defined directories + , getHomeDirectory + , XdgDirectory(..) + , getXdgDirectory + , XdgDirectoryList(..) + , getXdgDirectoryList + , getAppUserDataDirectory + , getUserDocumentsDirectory + , getTemporaryDirectory + + -- * Actions on files + , removeFile + , renameFile + , renamePath + , copyFile + , copyFileWithMetadata + , getFileSize + + , canonicalizePath + , makeAbsolute + , makeRelativeToCurrentDirectory + + -- * Existence tests + , doesPathExist + , doesFileExist + , doesDirectoryExist + + , findExecutable + , findExecutables + , findExecutablesInDirectories + , findFile + , findFiles + , findFileWith + , findFilesWith + , exeExtension + + -- * Symbolic links + , createFileLink + , createDirectoryLink + , removeDirectoryLink + , pathIsSymbolicLink + , getSymbolicLinkTarget + + -- * Permissions + + -- $permissions + + , Permissions + , emptyPermissions + , readable + , writable + , executable + , searchable + , setOwnerReadable + , setOwnerWritable + , setOwnerExecutable + , setOwnerSearchable + + , getPermissions + , setPermissions + , copyPermissions + + -- * Timestamps + + , getAccessTime + , getModificationTime + , setAccessTime + , setModificationTime + + -- * Deprecated + , isSymbolicLink + + ) where +import Prelude () +import System.Directory.Internal.AbstractFilePath +import System.Directory.Internal.Prelude hiding (lookupEnv) +import System.AbstractFilePath + ( (<.>) + , () + , addTrailingPathSeparator + , dropTrailingPathSeparator + , hasTrailingPathSeparator + , isAbsolute + , joinPath + , makeRelative + , splitDirectories + , splitSearchPath + , takeDirectory + , AbstractFilePath + , OsString + ) +import Data.Time (UTCTime) +import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds) +import Data.String ( fromString ) + + +#define FILEPATH AbstractFilePath +#define STRING OsString +#include "Template.hs" diff --git a/System/Directory/Internal/AbstractFilePath.hs b/System/Directory/Internal/AbstractFilePath.hs new file mode 100644 index 00000000..0477316d --- /dev/null +++ b/System/Directory/Internal/AbstractFilePath.hs @@ -0,0 +1,30 @@ +{-# LANGUAGE CPP #-} +-- | +-- Stability: unstable +-- Portability: unportable +-- +-- Internal modules are always subject to change from version to version. +-- The contents of this module are also platform-dependent, hence what is +-- shown in the Hackage documentation may differ from what is actually +-- available on your system. + +#include + +module System.Directory.Internal.AbstractFilePath + ( module System.Directory.Internal.Common.AbstractFilePath + +#if defined(mingw32_HOST_OS) + , module System.Directory.Internal.Windows.AbstractFilePath +#else + , module System.Directory.Internal.Posix.AbstractFilePath +#endif + + ) where + +import System.Directory.Internal.Common.AbstractFilePath + +#if defined(mingw32_HOST_OS) +import System.Directory.Internal.Windows.AbstractFilePath +#else +import System.Directory.Internal.Posix.AbstractFilePath +#endif diff --git a/System/Directory/Internal/Common.hs b/System/Directory/Internal/Common.hs index f043b7c7..0a181e0b 100644 --- a/System/Directory/Internal/Common.hs +++ b/System/Directory/Internal/Common.hs @@ -1,310 +1,38 @@ -module System.Directory.Internal.Common where -import Prelude () -import System.Directory.Internal.Prelude -import System.FilePath - ( addTrailingPathSeparator - , hasTrailingPathSeparator - , isPathSeparator - , isRelative - , joinDrive - , joinPath - , normalise - , pathSeparator - , pathSeparators - , splitDirectories - , splitDrive - ) - --- | A generator with side-effects. -newtype ListT m a = ListT { unListT :: m (Maybe (a, ListT m a)) } - -emptyListT :: Applicative m => ListT m a -emptyListT = ListT (pure Nothing) - -maybeToListT :: Applicative m => m (Maybe a) -> ListT m a -maybeToListT m = ListT (((\ x -> (x, emptyListT)) <$>) <$> m) - -listToListT :: Applicative m => [a] -> ListT m a -listToListT [] = emptyListT -listToListT (x : xs) = ListT (pure (Just (x, listToListT xs))) - -liftJoinListT :: Monad m => m (ListT m a) -> ListT m a -liftJoinListT m = ListT (m >>= unListT) - -listTHead :: Functor m => ListT m a -> m (Maybe a) -listTHead (ListT m) = (fst <$>) <$> m - -listTToList :: Monad m => ListT m a -> m [a] -listTToList (ListT m) = do - mx <- m - case mx of - Nothing -> return [] - Just (x, m') -> do - xs <- listTToList m' - return (x : xs) - -andM :: Monad m => m Bool -> m Bool -> m Bool -andM mx my = do - x <- mx - if x - then my - else return x - -sequenceWithIOErrors_ :: [IO ()] -> IO () -sequenceWithIOErrors_ actions = go (Right ()) actions - where - - go :: Either IOError () -> [IO ()] -> IO () - go (Left e) [] = ioError e - go (Right ()) [] = pure () - go s (m : ms) = s `seq` do - r <- tryIOError m - go (thenEither s r) ms +{-# LANGUAGE CPP #-} - -- equivalent to (*>) for Either, defined here to retain compatibility - -- with base prior to 4.3 - thenEither :: Either b a -> Either b a -> Either b a - thenEither x@(Left _) _ = x - thenEither _ y = y - --- | Similar to 'try' but only catches a specify kind of 'IOError' as --- specified by the predicate. -tryIOErrorType :: (IOError -> Bool) -> IO a -> IO (Either IOError a) -tryIOErrorType check action = do - result <- tryIOError action - case result of - Left err -> if check err then pure (Left err) else throwIO err - Right val -> pure (Right val) - --- | Attempt to perform the given action, silencing any IO exception thrown by --- it. -ignoreIOExceptions :: IO () -> IO () -ignoreIOExceptions io = io `catchIOError` (\_ -> pure ()) - -specializeErrorString :: String -> (IOError -> Bool) -> IO a -> IO a -specializeErrorString str errType action = do - mx <- tryIOErrorType errType action - case mx of - Left e -> throwIO (ioeSetErrorString e str) - Right x -> pure x - -ioeAddLocation :: IOError -> String -> IOError -ioeAddLocation e loc = do - ioeSetLocation e newLoc - where - newLoc = loc <> if null oldLoc then "" else ":" <> oldLoc - oldLoc = ioeGetLocation e - --- | Given a list of path segments, expand @.@ and @..@. The path segments --- must not contain path separators. -expandDots :: [FilePath] -> [FilePath] -expandDots = reverse . go [] - where - go ys' xs' = - case xs' of - [] -> ys' - x : xs -> - case x of - "." -> go ys' xs - ".." -> - case ys' of - [] -> go (x : ys') xs - ".." : _ -> go (x : ys') xs - _ : ys -> go ys xs - _ -> go (x : ys') xs - --- | Convert to the right kind of slashes. -normalisePathSeps :: FilePath -> FilePath -normalisePathSeps p = (\ c -> if isPathSeparator c then pathSeparator else c) <$> p - --- | Remove redundant trailing slashes and pick the right kind of slash. -normaliseTrailingSep :: FilePath -> FilePath -normaliseTrailingSep path = do - let path' = reverse path - let (sep, path'') = span isPathSeparator path' - let addSep = if null sep then id else (pathSeparator :) - reverse (addSep path'') - --- | Convert empty paths to the current directory, otherwise leave it --- unchanged. -emptyToCurDir :: FilePath -> FilePath -emptyToCurDir "" = "." -emptyToCurDir path = path - --- | Similar to 'normalise' but empty paths stay empty. -simplifyPosix :: FilePath -> FilePath -simplifyPosix "" = "" -simplifyPosix path = normalise path +module System.Directory.Internal.Common where --- | Similar to 'normalise' but: --- --- * empty paths stay empty, --- * parent dirs (@..@) are expanded, and --- * paths starting with @\\\\?\\@ are preserved. --- --- The goal is to preserve the meaning of paths better than 'normalise'. -simplifyWindows :: FilePath -> FilePath -simplifyWindows "" = "" -simplifyWindows path = - case drive' of - "\\\\?\\" -> drive' <> subpath - _ -> simplifiedPath - where - simplifiedPath = joinDrive drive' subpath' - (drive, subpath) = splitDrive path - drive' = upperDrive (normaliseTrailingSep (normalisePathSeps drive)) - subpath' = appendSep . avoidEmpty . prependSep . joinPath . - stripPardirs . expandDots . skipSeps . - splitDirectories $ subpath +import qualified System.Directory.Internal.Prelude as P - upperDrive d = case d of - c : ':' : s | isAlpha c && all isPathSeparator s -> toUpper c : ':' : s - _ -> d - skipSeps = filter (not . (`elem` (pure <$> pathSeparators))) - stripPardirs | pathIsAbsolute || subpathIsAbsolute = dropWhile (== "..") - | otherwise = id - prependSep | subpathIsAbsolute = (pathSeparator :) - | otherwise = id - avoidEmpty | not pathIsAbsolute - && (null drive || hasTrailingPathSep) -- prefer "C:" over "C:." - = emptyToCurDir - | otherwise = id - appendSep p | hasTrailingPathSep - && not (pathIsAbsolute && null p) - = addTrailingPathSeparator p - | otherwise = p - pathIsAbsolute = not (isRelative path) - subpathIsAbsolute = any isPathSeparator (take 1 subpath) - hasTrailingPathSep = hasTrailingPathSeparator subpath +#define SYSTEM_FILEPATH_MODULE System.FilePath +#define FILEPATH FilePath +#include "Common/Template.hs" -data FileType = File - | SymbolicLink -- ^ POSIX: either file or directory link; Windows: file link - | Directory - | DirectoryLink -- ^ Windows only: directory link - deriving (Bounded, Enum, Eq, Ord, Read, Show) +unpack :: FilePath -> [Char] +unpack = id --- | Check whether the given 'FileType' is considered a directory by the --- operating system. This affects the choice of certain functions --- e.g. 'System.Directory.removeDirectory' vs 'System.Directory.removeFile'. -fileTypeIsDirectory :: FileType -> Bool -fileTypeIsDirectory Directory = True -fileTypeIsDirectory DirectoryLink = True -fileTypeIsDirectory _ = False +pack :: [Char] -> FilePath +pack = id --- | Return whether the given 'FileType' is a link. -fileTypeIsLink :: FileType -> Bool -fileTypeIsLink SymbolicLink = True -fileTypeIsLink DirectoryLink = True -fileTypeIsLink _ = False +-- UNSAFE... only use this with ascii, not unknown input +unsafeFromChar' :: Char -> Char +unsafeFromChar' = id -data Permissions - = Permissions - { readable :: Bool - , writable :: Bool - , executable :: Bool - , searchable :: Bool - } deriving (Eq, Ord, Read, Show) +toChar :: Char -> Char +toChar = id --- | Truncate the destination file and then copy the contents of the source --- file to the destination file. If the destination file already exists, its --- attributes shall remain unchanged. Otherwise, its attributes are reset to --- the defaults. -copyFileContents :: FilePath -- ^ Source filename - -> FilePath -- ^ Destination filename - -> IO () -copyFileContents fromFPath toFPath = - (`ioeAddLocation` "copyFileContents") `modifyIOError` do - withBinaryFile toFPath WriteMode $ \ hTo -> - copyFileToHandle fromFPath hTo +unpackPlatform :: FilePath -> FilePath +unpackPlatform = id --- | Copy all data from a file to a handle. -copyFileToHandle :: FilePath -- ^ Source file - -> Handle -- ^ Destination handle - -> IO () -copyFileToHandle fromFPath hTo = - (`ioeAddLocation` "copyFileToHandle") `modifyIOError` do - withBinaryFile fromFPath ReadMode $ \ hFrom -> - copyHandleData hFrom hTo +packPlatform :: FilePath -> FilePath +packPlatform = id --- | Copy data from one handle to another until end of file. -copyHandleData :: Handle -- ^ Source handle - -> Handle -- ^ Destination handle - -> IO () -copyHandleData hFrom hTo = - (`ioeAddLocation` "copyData") `modifyIOError` do - allocaBytes bufferSize go - where - bufferSize = 131072 -- 128 KiB, as coreutils `cp` uses as of May 2014 (see ioblksize.h) - go buffer = do - count <- hGetBuf hFrom buffer bufferSize - when (count > 0) $ do - hPutBuf hTo buffer count - go buffer +-- for errors only, never fails +decodeFilepathFuzzy :: FilePath -> FilePath +decodeFilepathFuzzy = id --- | Special directories for storing user-specific application data, --- configuration, and cache files, as specified by the --- . --- --- Note: On Windows, 'XdgData' and 'XdgConfig' usually map to the same --- directory. --- --- @since 1.2.3.0 -data XdgDirectory - = XdgData - -- ^ For data files (e.g. images). - -- It uses the @XDG_DATA_HOME@ environment variable. - -- On non-Windows systems, the default is @~\/.local\/share@. - -- On Windows, the default is @%APPDATA%@ - -- (e.g. @C:\/Users\//\/\/AppData\/Roaming@). - -- Can be considered as the user-specific equivalent of @\/usr\/share@. - | XdgConfig - -- ^ For configuration files. - -- It uses the @XDG_CONFIG_HOME@ environment variable. - -- On non-Windows systems, the default is @~\/.config@. - -- On Windows, the default is @%APPDATA%@ - -- (e.g. @C:\/Users\//\/\/AppData\/Roaming@). - -- Can be considered as the user-specific equivalent of @\/etc@. - | XdgCache - -- ^ For non-essential files (e.g. cache). - -- It uses the @XDG_CACHE_HOME@ environment variable. - -- On non-Windows systems, the default is @~\/.cache@. - -- On Windows, the default is @%LOCALAPPDATA%@ - -- (e.g. @C:\/Users\//\/\/AppData\/Local@). - -- Can be considered as the user-specific equivalent of @\/var\/cache@. - | XdgState - -- ^ For data that should persist between (application) restarts, - -- but that is not important or portable enough to the user that it - -- should be stored in 'XdgData'. - -- It uses the @XDG_STATE_HOME@ environment variable. - -- On non-Windows sytems, the default is @~\/.local\/state@. On - -- Windows, the default is @%LOCALAPPDATA%@ - -- (e.g. @C:\/Users\//\/\/AppData\/Local@). - -- - -- @since 1.3.7.0 - deriving (Bounded, Enum, Eq, Ord, Read, Show) +toString :: FilePath -> IO FilePath +toString = pure --- | Search paths for various application data, as specified by the --- . --- --- The list of paths is split using 'System.FilePath.searchPathSeparator', --- which on Windows is a semicolon. --- --- Note: On Windows, 'XdgDataDirs' and 'XdgConfigDirs' usually yield the same --- result. --- --- @since 1.3.2.0 -data XdgDirectoryList - = XdgDataDirs - -- ^ For data files (e.g. images). - -- It uses the @XDG_DATA_DIRS@ environment variable. - -- On non-Windows systems, the default is @\/usr\/local\/share\/@ and - -- @\/usr\/share\/@. - -- On Windows, the default is @%PROGRAMDATA%@ or @%ALLUSERSPROFILE%@ - -- (e.g. @C:\/ProgramData@). - | XdgConfigDirs - -- ^ For configuration files. - -- It uses the @XDG_CONFIG_DIRS@ environment variable. - -- On non-Windows systems, the default is @\/etc\/xdg@. - -- On Windows, the default is @%PROGRAMDATA%@ or @%ALLUSERSPROFILE%@ - -- (e.g. @C:\/ProgramData@). - deriving (Bounded, Enum, Eq, Ord, Read, Show) +fromStringIO :: String -> IO String +fromStringIO = pure diff --git a/System/Directory/Internal/Common/AbstractFilePath.hs b/System/Directory/Internal/Common/AbstractFilePath.hs new file mode 100644 index 00000000..26c318c4 --- /dev/null +++ b/System/Directory/Internal/Common/AbstractFilePath.hs @@ -0,0 +1,51 @@ +{-# LANGUAGE CPP #-} + +module System.Directory.Internal.Common.AbstractFilePath where + +import System.AbstractFilePath (unpackAFP, packAFP, unsafeFromChar) +import qualified System.AbstractFilePath as AFP + +import qualified System.File.AbstractFilePath as P + +import System.OsString.Internal.Types +import System.AbstractFilePath.Types +import System.AbstractFilePath.Data.ByteString.Short.Decode + +#define SYSTEM_FILEPATH_MODULE System.AbstractFilePath +#define FILEPATH AbstractFilePath +#include "Template.hs" + +unpack :: AbstractFilePath -> [OsChar] +unpack = unpackAFP + +pack :: [OsChar] -> AbstractFilePath +pack = packAFP + +-- UNSAFE... only use this with ascii, not unknown input +unsafeFromChar' :: Char -> OsChar +unsafeFromChar' = unsafeFromChar + +toChar :: OsChar -> Char +toChar = AFP.toChar + +unpackPlatform :: AbstractFilePath -> PlatformFilePath +unpackPlatform (OsString p) = p + +packPlatform :: PlatformFilePath -> AbstractFilePath +packPlatform = OsString + +-- for errors only, never fails +decodeFilepathFuzzy :: AbstractFilePath -> FilePath +#if defined(mingw32_HOST_OS) +decodeFilepathFuzzy (OsString (WS fp)) = decodeUtf8With lenientDecode fp +#else +decodeFilepathFuzzy (OsString (PS fp)) = decodeUtf8With lenientDecode fp +#endif + + +toString :: AbstractFilePath -> IO FilePath +toString = AFP.fromAbstractFilePathIO + +fromStringIO :: String -> IO AbstractFilePath +fromStringIO = AFP.toAbstractFilePathIO + diff --git a/System/Directory/Internal/Common/Template.hs b/System/Directory/Internal/Common/Template.hs new file mode 100644 index 00000000..d0881d15 --- /dev/null +++ b/System/Directory/Internal/Common/Template.hs @@ -0,0 +1,312 @@ +import Prelude () +import Data.String ( fromString ) +import System.Directory.Internal.Prelude +import SYSTEM_FILEPATH_MODULE + ( addTrailingPathSeparator + , hasTrailingPathSeparator + , isPathSeparator + , isRelative + , joinDrive + , joinPath + , normalise + , pathSeparator + , pathSeparators + , splitDirectories + , splitDrive + ) + +-- | A generator with side-effects. +newtype ListT m a = ListT { unListT :: m (Maybe (a, ListT m a)) } + +emptyListT :: Applicative m => ListT m a +emptyListT = ListT (pure Nothing) + +maybeToListT :: Applicative m => m (Maybe a) -> ListT m a +maybeToListT m = ListT (((\ x -> (x, emptyListT)) <$>) <$> m) + +listToListT :: Applicative m => [a] -> ListT m a +listToListT [] = emptyListT +listToListT (x : xs) = ListT (pure (Just (x, listToListT xs))) + +liftJoinListT :: Monad m => m (ListT m a) -> ListT m a +liftJoinListT m = ListT (m >>= unListT) + +listTHead :: Functor m => ListT m a -> m (Maybe a) +listTHead (ListT m) = (fst <$>) <$> m + +listTToList :: Monad m => ListT m a -> m [a] +listTToList (ListT m) = do + mx <- m + case mx of + Nothing -> return [] + Just (x, m') -> do + xs <- listTToList m' + return (x : xs) + +andM :: Monad m => m Bool -> m Bool -> m Bool +andM mx my = do + x <- mx + if x + then my + else return x + +sequenceWithIOErrors_ :: [IO ()] -> IO () +sequenceWithIOErrors_ actions = go (Right ()) actions + where + + go :: Either IOError () -> [IO ()] -> IO () + go (Left e) [] = ioError e + go (Right ()) [] = pure () + go s (m : ms) = s `seq` do + r <- tryIOError m + go (thenEither s r) ms + + -- equivalent to (*>) for Either, defined here to retain compatibility + -- with base prior to 4.3 + thenEither :: Either b a -> Either b a -> Either b a + thenEither x@(Left _) _ = x + thenEither _ y = y + +-- | Similar to 'try' but only catches a specify kind of 'IOError' as +-- specified by the predicate. +tryIOErrorType :: (IOError -> Bool) -> IO a -> IO (Either IOError a) +tryIOErrorType check action = do + result <- tryIOError action + case result of + Left err -> if check err then pure (Left err) else throwIO err + Right val -> pure (Right val) + +-- | Attempt to perform the given action, silencing any IO exception thrown by +-- it. +ignoreIOExceptions :: IO () -> IO () +ignoreIOExceptions io = io `catchIOError` (\_ -> pure ()) + +specializeErrorString :: String -> (IOError -> Bool) -> IO a -> IO a +specializeErrorString str errType action = do + mx <- tryIOErrorType errType action + case mx of + Left e -> throwIO (ioeSetErrorString e str) + Right x -> pure x + +ioeAddLocation :: IOError -> String -> IOError +ioeAddLocation e loc = do + ioeSetLocation e newLoc + where + newLoc = loc <> if null oldLoc then "" else ":" <> oldLoc + oldLoc = ioeGetLocation e + +-- | Given a list of path segments, expand @.@ and @..@. The path segments +-- must not contain path separators. +expandDots :: [FILEPATH] -> [FILEPATH] +expandDots = reverse . go [] + where + go ys' xs' = + case xs' of + [] -> ys' + x : xs -> + if | x == fromString "." -> go ys' xs + | x == fromString ".." -> + case ys' of + [] -> go (x : ys') xs + y : ys -> if y == fromString ".." then go (x : ys') xs else go ys xs + | otherwise -> go (x : ys') xs + +-- | Convert to the right kind of slashes. +normalisePathSeps :: FILEPATH -> FILEPATH +normalisePathSeps p = pack $ (\ c -> if isPathSeparator c then pathSeparator else c) <$> unpack p + +-- | Remove redundant trailing slashes and pick the right kind of slash. +normaliseTrailingSep :: FILEPATH -> FILEPATH +normaliseTrailingSep (unpack -> path) = pack $ do + let path' = reverse path + let (sep, path'') = span isPathSeparator path' + let addSep = if null sep then id else (pathSeparator :) + reverse (addSep path'') + +-- | Convert empty paths to the current directory, otherwise leave it +-- unchanged. +emptyToCurDir :: FILEPATH -> FILEPATH +emptyToCurDir path + | null (unpack path) = fromString "." + | otherwise = path + +-- | Similar to 'normalise' but empty paths stay empty. +simplifyPosix :: FILEPATH -> FILEPATH +simplifyPosix path + | null (unpack path) = pack [] + | otherwise = normalise path + +-- | Similar to 'normalise' but: +-- +-- * empty paths stay empty, +-- * parent dirs (@..@) are expanded, and +-- * paths starting with @\\\\?\\@ are preserved. +-- +-- The goal is to preserve the meaning of paths better than 'normalise'. +simplifyWindows :: FILEPATH -> FILEPATH +simplifyWindows path + | null (unpack path) = pack [] + | otherwise = + if drive' == fromString "\\\\?\\" then drive' <> subpath else simplifiedPath + where + simplifiedPath = joinDrive drive' subpath' + (drive, subpath) = splitDrive path + drive' = upperDrive (normaliseTrailingSep (normalisePathSeps drive)) + subpath' = appendSep . avoidEmpty . prependSep . joinPath . + stripPardirs . expandDots . skipSeps . + splitDirectories $ subpath + + upperDrive d = case unpack d of + c : k : s | isAlpha (toChar c) + , (toChar k) == ':' + -- unsafeFromChar' is safe here, all chars are ascii + , all isPathSeparator s -> pack (unsafeFromChar' (toUpper (toChar c)) : unsafeFromChar' ':' : s) + _ -> d + skipSeps = fmap pack . filter (not . (`elem` (pure <$> pathSeparators))) . fmap unpack + stripPardirs | pathIsAbsolute || subpathIsAbsolute = dropWhile (== fromString "..") + | otherwise = id + prependSep | subpathIsAbsolute = (pack [pathSeparator] <>) + | otherwise = id + avoidEmpty | not pathIsAbsolute + && (null (unpack drive) || hasTrailingPathSep) -- prefer "C:" over "C:." + = emptyToCurDir + | otherwise = id + appendSep p | hasTrailingPathSep + && not (pathIsAbsolute && null (unpack p)) + = addTrailingPathSeparator p + | otherwise = p + pathIsAbsolute = not (isRelative path) + subpathIsAbsolute = any isPathSeparator (take 1 $ unpack subpath) + hasTrailingPathSep = hasTrailingPathSeparator subpath + +data FileType = File + | SymbolicLink -- ^ POSIX: either file or directory link; Windows: file link + | Directory + | DirectoryLink -- ^ Windows only: directory link + deriving (Bounded, Enum, Eq, Ord, Read, Show) + +-- | Check whether the given 'FileType' is considered a directory by the +-- operating system. This affects the choice of certain functions +-- e.g. 'System.Directory.removeDirectory' vs 'System.Directory.removeFile'. +fileTypeIsDirectory :: FileType -> Bool +fileTypeIsDirectory Directory = True +fileTypeIsDirectory DirectoryLink = True +fileTypeIsDirectory _ = False + +-- | Return whether the given 'FileType' is a link. +fileTypeIsLink :: FileType -> Bool +fileTypeIsLink SymbolicLink = True +fileTypeIsLink DirectoryLink = True +fileTypeIsLink _ = False + +data Permissions + = Permissions + { readable :: Bool + , writable :: Bool + , executable :: Bool + , searchable :: Bool + } deriving (Eq, Ord, Read, Show) + +-- | Truncate the destination file and then copy the contents of the source +-- file to the destination file. If the destination file already exists, its +-- attributes shall remain unchanged. Otherwise, its attributes are reset to +-- the defaults. +copyFileContents :: FILEPATH -- ^ Source filename + -> FILEPATH -- ^ Destination filename + -> IO () +copyFileContents fromFPath toFPath = + (`ioeAddLocation` "copyFileContents") `modifyIOError` do + P.withBinaryFile toFPath WriteMode $ \ hTo -> + copyFileToHandle fromFPath hTo + +-- | Copy all data from a file to a handle. +copyFileToHandle :: FILEPATH -- ^ Source file + -> Handle -- ^ Destination handle + -> IO () +copyFileToHandle fromFPath hTo = + (`ioeAddLocation` "copyFileToHandle") `modifyIOError` do + P.withBinaryFile fromFPath ReadMode $ \ hFrom -> + copyHandleData hFrom hTo + +-- | Copy data from one handle to another until end of file. +copyHandleData :: Handle -- ^ Source handle + -> Handle -- ^ Destination handle + -> IO () +copyHandleData hFrom hTo = + (`ioeAddLocation` "copyData") `modifyIOError` do + allocaBytes bufferSize go + where + bufferSize = 131072 -- 128 KiB, as coreutils `cp` uses as of May 2014 (see ioblksize.h) + go buffer = do + count <- hGetBuf hFrom buffer bufferSize + when (count > 0) $ do + hPutBuf hTo buffer count + go buffer + +-- | Special directories for storing user-specific application data, +-- configuration, and cache files, as specified by the +-- . +-- +-- Note: On Windows, 'XdgData' and 'XdgConfig' usually map to the same +-- directory. +-- +-- @since 1.2.3.0 +data XdgDirectory + = XdgData + -- ^ For data files (e.g. images). + -- It uses the @XDG_DATA_HOME@ environment variable. + -- On non-Windows systems, the default is @~\/.local\/share@. + -- On Windows, the default is @%APPDATA%@ + -- (e.g. @C:\/Users\//\/\/AppData\/Roaming@). + -- Can be considered as the user-specific equivalent of @\/usr\/share@. + | XdgConfig + -- ^ For configuration files. + -- It uses the @XDG_CONFIG_HOME@ environment variable. + -- On non-Windows systems, the default is @~\/.config@. + -- On Windows, the default is @%APPDATA%@ + -- (e.g. @C:\/Users\//\/\/AppData\/Roaming@). + -- Can be considered as the user-specific equivalent of @\/etc@. + | XdgCache + -- ^ For non-essential files (e.g. cache). + -- It uses the @XDG_CACHE_HOME@ environment variable. + -- On non-Windows systems, the default is @~\/.cache@. + -- On Windows, the default is @%LOCALAPPDATA%@ + -- (e.g. @C:\/Users\//\/\/AppData\/Local@). + -- Can be considered as the user-specific equivalent of @\/var\/cache@. + | XdgState + -- ^ For data that should persist between (application) restarts, + -- but that is not important or portable enough to the user that it + -- should be stored in 'XdgData'. + -- It uses the @XDG_STATE_HOME@ environment variable. + -- On non-Windows sytems, the default is @~\/.local\/state@. On + -- Windows, the default is @%LOCALAPPDATA%@ + -- (e.g. @C:\/Users\//\/\/AppData\/Local@). + -- + -- @since 1.3.7.0 + deriving (Bounded, Enum, Eq, Ord, Read, Show) + +-- | Search paths for various application data, as specified by the +-- . +-- +-- The list of paths is split using 'System.FilePath.searchPathSeparator', +-- which on Windows is a semicolon. +-- +-- Note: On Windows, 'XdgDataDirs' and 'XdgConfigDirs' usually yield the same +-- result. +-- +-- @since 1.3.2.0 +data XdgDirectoryList + = XdgDataDirs + -- ^ For data files (e.g. images). + -- It uses the @XDG_DATA_DIRS@ environment variable. + -- On non-Windows systems, the default is @\/usr\/local\/share\/@ and + -- @\/usr\/share\/@. + -- On Windows, the default is @%PROGRAMDATA%@ or @%ALLUSERSPROFILE%@ + -- (e.g. @C:\/ProgramData@). + | XdgConfigDirs + -- ^ For configuration files. + -- It uses the @XDG_CONFIG_DIRS@ environment variable. + -- On non-Windows systems, the default is @\/etc\/xdg@. + -- On Windows, the default is @%PROGRAMDATA%@ or @%ALLUSERSPROFILE%@ + -- (e.g. @C:\/ProgramData@). + deriving (Bounded, Enum, Eq, Ord, Read, Show) diff --git a/System/Directory/Internal/Config/AbstractFilePath.hs b/System/Directory/Internal/Config/AbstractFilePath.hs new file mode 100644 index 00000000..649b5729 --- /dev/null +++ b/System/Directory/Internal/Config/AbstractFilePath.hs @@ -0,0 +1,13 @@ +{-# LANGUAGE CPP #-} +module System.Directory.Internal.Config.AbstractFilePath where +#include + +import System.OsString.Internal.Types +import Data.String ( fromString ) + +exeExtension :: OsString +exeExtension = fromString EXE_EXTENSION +-- We avoid using #const_str from hsc because it breaks cross-compilation +-- builds, so we use this ugly workaround where we simply paste the C string +-- literal directly in here. This will probably break if the EXE_EXTENSION +-- contains strange characters, but hopefully no sane OS would ever do that. diff --git a/System/Directory/Internal/Posix.hs b/System/Directory/Internal/Posix.hs new file mode 100644 index 00000000..8c98270f --- /dev/null +++ b/System/Directory/Internal/Posix.hs @@ -0,0 +1,28 @@ +{-# LANGUAGE CPP #-} + +module System.Directory.Internal.Posix where + +#if !defined(mingw32_HOST_OS) +import System.Directory.Internal.PosixFFI +import System.Directory.Internal.Common +import System.FilePath ((), isRelative, splitSearchPath) +import qualified GHC.Foreign as GHC +import System.Directory.Internal.Prelude (lookupEnv, getEnv) +import qualified System.Posix.Directory as Posix +import qualified System.Posix.Files as Posix +import System.Directory.Internal.Config (exeExtension) + +#define FILEPATH FilePath +#define STRING String +#include "Posix/Template.hs" + +canonicalizePathWith :: ((FilePath -> IO FilePath) -> FilePath -> IO FilePath) + -> FilePath + -> IO FilePath +canonicalizePathWith attemptRealpath path = do + encoding <- getFileSystemEncoding + let realpath path' = + GHC.withCString encoding path' (`withRealpath` GHC.peekCString encoding) + attemptRealpath realpath path + +#endif diff --git a/System/Directory/Internal/Posix/AbstractFilePath.hs b/System/Directory/Internal/Posix/AbstractFilePath.hs new file mode 100644 index 00000000..114a2bfa --- /dev/null +++ b/System/Directory/Internal/Posix/AbstractFilePath.hs @@ -0,0 +1,43 @@ +{-# LANGUAGE CPP #-} + +module System.Directory.Internal.Posix.AbstractFilePath where + +#if !defined(mingw32_HOST_OS) +import System.Directory.Internal.PosixFFI +import qualified System.AbstractFilePath.Data.ByteString.Short as L +import System.Directory.Internal.Common.AbstractFilePath + +import System.AbstractFilePath +import System.OsString.Internal.Types +import System.AbstractFilePath.Posix (fromPlatformStringIO) +import System.Directory.Internal.Config.AbstractFilePath (exeExtension) + +import qualified System.Posix.Env.PosixString as PS +import qualified System.Posix.Directory.PosixFilePath as Posix +import qualified System.Posix.Files.PosixString as Posix + +import System.IO.Error (doesNotExistErrorType) + +#define FILEPATH AbstractFilePath +#define STRING OsString +#include "Template.hs" + +lookupEnv :: OsString -> IO (Maybe OsString) +lookupEnv (OsString name@(PS _)) = fmap OsString <$> PS.getEnv name + +getEnv :: OsString -> IO OsString +getEnv (OsString name@(PS _)) = do + env <- PS.getEnv name + pn <- fromPlatformStringIO name + case env of + Nothing -> throwIO (mkIOError doesNotExistErrorType ("Env var '" <> pn <> "' could not be found!") Nothing Nothing) + Just value -> pure (OsString value) + + +canonicalizePathWith :: ((AbstractFilePath -> IO AbstractFilePath) -> AbstractFilePath -> IO AbstractFilePath) + -> AbstractFilePath + -> IO AbstractFilePath +canonicalizePathWith attemptRealpath path = do + let realpath (OsString (PS sb)) = L.useAsCString sb (\cstr -> withRealpath cstr (fmap (OsString . PS) . L.packCString)) + attemptRealpath realpath path +#endif diff --git a/System/Directory/Internal/Posix.hsc b/System/Directory/Internal/Posix/Template.hs similarity index 58% rename from System/Directory/Internal/Posix.hsc rename to System/Directory/Internal/Posix/Template.hs index 45b19c65..63f63df7 100644 --- a/System/Directory/Internal/Posix.hsc +++ b/System/Directory/Internal/Posix/Template.hs @@ -1,41 +1,29 @@ -module System.Directory.Internal.Posix where -#include -#if !defined(mingw32_HOST_OS) -#ifdef HAVE_LIMITS_H -# include -#endif import Prelude () -import System.Directory.Internal.Prelude +import System.Directory.Internal.Prelude hiding (lookupEnv, getEnv) #ifdef HAVE_UTIMENSAT import System.Directory.Internal.C_utimensat #endif -import System.Directory.Internal.Common -import System.Directory.Internal.Config (exeExtension) +import Data.String ( fromString ) import Data.Time (UTCTime) import Data.Time.Clock.POSIX (POSIXTime) -import System.FilePath ((), isRelative, splitSearchPath) import qualified Data.Time.Clock.POSIX as POSIXTime -import qualified GHC.Foreign as GHC -import qualified System.Posix as Posix +import qualified System.Posix.Types as Posix import qualified System.Posix.User as PU -createDirectoryInternal :: FilePath -> IO () -createDirectoryInternal path = Posix.createDirectory path 0o777 +createDirectoryInternal :: FILEPATH -> IO () +createDirectoryInternal path = Posix.createDirectory (unpackPlatform path) 0o777 -removePathInternal :: Bool -> FilePath -> IO () -removePathInternal True = Posix.removeDirectory -removePathInternal False = Posix.removeLink +removePathInternal :: Bool -> FILEPATH -> IO () +removePathInternal True = Posix.removeDirectory . unpackPlatform +removePathInternal False = Posix.removeLink . unpackPlatform -renamePathInternal :: FilePath -> FilePath -> IO () -renamePathInternal = Posix.rename +renamePathInternal :: FILEPATH -> FILEPATH -> IO () +renamePathInternal f t = Posix.rename (unpackPlatform f) (unpackPlatform t) -- | On POSIX, equivalent to 'simplifyPosix'. -simplify :: FilePath -> FilePath +simplify :: FILEPATH -> FILEPATH simplify = simplifyPosix --- we use the 'free' from the standard library here since it's not entirely --- clear whether Haskell's 'free' corresponds to the same one -foreign import ccall unsafe "free" c_free :: Ptr a -> IO () c_PATH_MAX :: Maybe Int #ifdef PATH_MAX @@ -49,16 +37,6 @@ c_PATH_MAX | c_PATH_MAX' > toInteger maxValue = Nothing c_PATH_MAX = Nothing #endif -#if !defined(HAVE_REALPATH) - -c_realpath :: CString -> CString -> IO CString -c_realpath _ _ = throwIO (mkIOError UnsupportedOperation "platform does not support realpath" Nothing Nothing) - -#else - -foreign import ccall "realpath" c_realpath :: CString -> CString -> IO CString - -#endif withRealpath :: CString -> (CString -> IO a) -> IO a withRealpath path action = case c_PATH_MAX of @@ -71,33 +49,24 @@ withRealpath path action = case c_PATH_MAX of allocaBytes (pathMax + 1) (realpath >=> action) where realpath = throwErrnoIfNull "" . c_realpath path -canonicalizePathWith :: ((FilePath -> IO FilePath) -> FilePath -> IO FilePath) - -> FilePath - -> IO FilePath -canonicalizePathWith attemptRealpath path = do - encoding <- getFileSystemEncoding - let realpath path' = - GHC.withCString encoding path' (`withRealpath` GHC.peekCString encoding) - attemptRealpath realpath path - -canonicalizePathSimplify :: FilePath -> IO FilePath +canonicalizePathSimplify :: FILEPATH -> IO FILEPATH canonicalizePathSimplify = pure -findExecutablesLazyInternal :: ([FilePath] -> String -> ListT IO FilePath) - -> String - -> ListT IO FilePath +findExecutablesLazyInternal :: ([FILEPATH] -> STRING -> ListT IO FILEPATH) + -> STRING + -> ListT IO FILEPATH findExecutablesLazyInternal findExecutablesInDirectoriesLazy binary = liftJoinListT $ do path <- getPath pure (findExecutablesInDirectoriesLazy path binary) -exeExtensionInternal :: String +exeExtensionInternal :: STRING exeExtensionInternal = exeExtension -getDirectoryContentsInternal :: FilePath -> IO [FilePath] +getDirectoryContentsInternal :: FILEPATH -> IO [FILEPATH] getDirectoryContentsInternal path = - bracket - (Posix.openDirStream path) + fmap packPlatform <$> bracket + (Posix.openDirStream $ unpackPlatform path) Posix.closeDirStream start where @@ -105,12 +74,12 @@ getDirectoryContentsInternal path = where loop acc = do e <- Posix.readDirStream dirp - if null e + if e == fromString "" then pure (acc []) else loop (acc . (e:)) -getCurrentDirectoryInternal :: IO FilePath -getCurrentDirectoryInternal = Posix.getWorkingDirectory +getCurrentDirectoryInternal :: IO FILEPATH +getCurrentDirectoryInternal = packPlatform <$> Posix.getWorkingDirectory -- | Convert a path into an absolute path. If the given path is relative, the -- current directory is prepended and the path may or may not be simplified. @@ -121,33 +90,33 @@ getCurrentDirectoryInternal = Posix.getWorkingDirectory -- operation may throw exceptions. -- -- Empty paths are treated as the current directory. -prependCurrentDirectory :: FilePath -> IO FilePath +prependCurrentDirectory :: FILEPATH -> IO FILEPATH prependCurrentDirectory path | isRelative path = ((`ioeAddLocation` "prependCurrentDirectory") . - (`ioeSetFileName` path)) `modifyIOError` do + (`ioeSetFileName` decodeFilepathFuzzy path)) `modifyIOError` do ( path) <$> getCurrentDirectoryInternal | otherwise = pure path -setCurrentDirectoryInternal :: FilePath -> IO () -setCurrentDirectoryInternal = Posix.changeWorkingDirectory +setCurrentDirectoryInternal :: FILEPATH -> IO () +setCurrentDirectoryInternal = Posix.changeWorkingDirectory . unpackPlatform linkToDirectoryIsDirectory :: Bool linkToDirectoryIsDirectory = False -createSymbolicLink :: Bool -> FilePath -> FilePath -> IO () -createSymbolicLink _ = Posix.createSymbolicLink +createSymbolicLink :: Bool -> FILEPATH -> FILEPATH -> IO () +createSymbolicLink _ f t = Posix.createSymbolicLink (unpackPlatform f) (unpackPlatform t) -readSymbolicLink :: FilePath -> IO FilePath -readSymbolicLink = Posix.readSymbolicLink +readSymbolicLink :: FILEPATH -> IO FILEPATH +readSymbolicLink = fmap packPlatform . Posix.readSymbolicLink . unpackPlatform type Metadata = Posix.FileStatus -getSymbolicLinkMetadata :: FilePath -> IO Metadata -getSymbolicLinkMetadata = Posix.getSymbolicLinkStatus +getSymbolicLinkMetadata :: FILEPATH -> IO Metadata +getSymbolicLinkMetadata = Posix.getSymbolicLinkStatus . unpackPlatform -getFileMetadata :: FilePath -> IO Metadata -getFileMetadata = Posix.getFileStatus +getFileMetadata :: FILEPATH -> IO Metadata +getFileMetadata = Posix.getFileStatus . unpackPlatform fileTypeFromMetadata :: Metadata -> FileType fileTypeFromMetadata stat @@ -197,19 +166,19 @@ setWriteMode :: Bool -> Mode -> Mode setWriteMode False m = m .&. complement allWriteMode setWriteMode True m = m .|. allWriteMode -setFileMode :: FilePath -> Mode -> IO () -setFileMode = Posix.setFileMode +setFileMode :: FILEPATH -> Mode -> IO () +setFileMode = Posix.setFileMode . unpackPlatform -setFilePermissions :: FilePath -> Mode -> IO () +setFilePermissions :: FILEPATH -> Mode -> IO () setFilePermissions = setFileMode -getAccessPermissions :: FilePath -> IO Permissions +getAccessPermissions :: FILEPATH -> IO Permissions getAccessPermissions path = do m <- getFileMetadata path let isDir = fileTypeIsDirectory (fileTypeFromMetadata m) - r <- Posix.fileAccess path True False False - w <- Posix.fileAccess path False True False - x <- Posix.fileAccess path False False True + r <- Posix.fileAccess (unpackPlatform path) True False False + w <- Posix.fileAccess (unpackPlatform path) False True False + x <- Posix.fileAccess (unpackPlatform path) False False True pure Permissions { readable = r , writable = w @@ -217,7 +186,7 @@ getAccessPermissions path = do , searchable = x && isDir } -setAccessPermissions :: FilePath -> Permissions -> IO () +setAccessPermissions :: FILEPATH -> Permissions -> IO () setAccessPermissions path (Permissions r w e s) = do m <- getFileMetadata path setFileMode path (modifyBit (e || s) Posix.ownerExecuteMode . @@ -229,35 +198,35 @@ setAccessPermissions path (Permissions r w e s) = do modifyBit False b m = m .&. complement b modifyBit True b m = m .|. b -copyOwnerFromStatus :: Posix.FileStatus -> FilePath -> IO () +copyOwnerFromStatus :: Posix.FileStatus -> FILEPATH -> IO () copyOwnerFromStatus st dst = do - Posix.setOwnerAndGroup dst (Posix.fileOwner st) (-1) + Posix.setOwnerAndGroup (unpackPlatform dst) (Posix.fileOwner st) (-1) -copyGroupFromStatus :: Posix.FileStatus -> FilePath -> IO () +copyGroupFromStatus :: Posix.FileStatus -> FILEPATH -> IO () copyGroupFromStatus st dst = do - Posix.setOwnerAndGroup dst (-1) (Posix.fileGroup st) + Posix.setOwnerAndGroup (unpackPlatform dst) (-1) (Posix.fileGroup st) -tryCopyOwnerAndGroupFromStatus :: Posix.FileStatus -> FilePath -> IO () +tryCopyOwnerAndGroupFromStatus :: Posix.FileStatus -> FILEPATH -> IO () tryCopyOwnerAndGroupFromStatus st dst = do ignoreIOExceptions (copyOwnerFromStatus st dst) ignoreIOExceptions (copyGroupFromStatus st dst) -copyFileWithMetadataInternal :: (Metadata -> FilePath -> IO ()) - -> (Metadata -> FilePath -> IO ()) - -> FilePath - -> FilePath +copyFileWithMetadataInternal :: (Metadata -> FILEPATH -> IO ()) + -> (Metadata -> FILEPATH -> IO ()) + -> FILEPATH + -> FILEPATH -> IO () copyFileWithMetadataInternal copyPermissionsFromMetadata copyTimesFromMetadata src dst = do - st <- Posix.getFileStatus src + st <- Posix.getFileStatus (unpackPlatform src) copyFileContents src dst tryCopyOwnerAndGroupFromStatus st dst copyPermissionsFromMetadata st dst copyTimesFromMetadata st dst -setTimes :: FilePath -> (Maybe POSIXTime, Maybe POSIXTime) -> IO () +setTimes :: FILEPATH -> (Maybe POSIXTime, Maybe POSIXTime) -> IO () #ifdef HAVE_UTIMENSAT setTimes path' (atime', mtime') = withFilePath path' $ \ path'' -> @@ -275,9 +244,10 @@ setTimes path' (atime', mtime') = do (fromMaybe (POSIXTime.utcTimeToPOSIXSeconds atimeOld) atime') (fromMaybe (POSIXTime.utcTimeToPOSIXSeconds mtimeOld) mtime') -setFileTimes' :: FilePath -> POSIXTime -> POSIXTime -> IO () +setFileTimes' :: + FILEPATH -> POSIXTime -> POSIXTime -> IO () # if MIN_VERSION_unix(2, 7, 0) -setFileTimes' = Posix.setFileTimesHiRes +setFileTimes' pth atime' mtime' = Posix.setFileTimesHiRes (unpackPlatform pth) atime' mtime' # else setFileTimes' pth atime' mtime' = Posix.setFileTimes pth @@ -287,40 +257,40 @@ setFileTimes' pth atime' mtime' = #endif -- | Get the contents of the @PATH@ environment variable. -getPath :: IO [FilePath] -getPath = splitSearchPath <$> getEnv "PATH" +getPath :: IO [FILEPATH] +getPath = splitSearchPath <$> getEnv (fromString "PATH") -- | $HOME is preferred, because the user has control over it. However, POSIX -- doesn't define it as a mandatory variable, so fall back to `getpwuid_r`. -getHomeDirectoryInternal :: IO FilePath +getHomeDirectoryInternal :: IO FILEPATH getHomeDirectoryInternal = do - e <- lookupEnv "HOME" + e <- lookupEnv (fromString "HOME") case e of Just fp -> pure fp - Nothing -> PU.homeDirectory <$> (PU.getEffectiveUserID >>= PU.getUserEntryForID) + -- TODO: fromString here is bad, but unix's System.Posix.User.UserEntry does not have ByteString/OsString variants + Nothing -> fromString <$> (PU.homeDirectory <$> (PU.getEffectiveUserID >>= PU.getUserEntryForID)) -getXdgDirectoryFallback :: IO FilePath -> XdgDirectory -> IO FilePath +getXdgDirectoryFallback :: IO FILEPATH -> XdgDirectory -> IO FILEPATH getXdgDirectoryFallback getHomeDirectory xdgDir = do (<$> getHomeDirectory) $ flip () $ case xdgDir of - XdgData -> ".local/share" - XdgConfig -> ".config" - XdgCache -> ".cache" - XdgState -> ".local/state" + XdgData -> fromString ".local/share" + XdgConfig -> fromString ".config" + XdgCache -> fromString ".cache" + XdgState -> fromString ".local/state" -getXdgDirectoryListFallback :: XdgDirectoryList -> IO [FilePath] +getXdgDirectoryListFallback :: XdgDirectoryList -> IO [FILEPATH] getXdgDirectoryListFallback xdgDirs = pure $ case xdgDirs of - XdgDataDirs -> ["/usr/local/share/", "/usr/share/"] - XdgConfigDirs -> ["/etc/xdg"] + XdgDataDirs -> [fromString "/usr/local/share/", fromString "/usr/share/"] + XdgConfigDirs -> [fromString "/etc/xdg"] -getAppUserDataDirectoryInternal :: FilePath -> IO FilePath +getAppUserDataDirectoryInternal :: FILEPATH -> IO FILEPATH getAppUserDataDirectoryInternal appName = - (\ home -> home <> ('/' : '.' : appName)) <$> getHomeDirectoryInternal + (\ home -> home <> (fromString "/" <> fromString "." <> appName)) <$> getHomeDirectoryInternal -getUserDocumentsDirectoryInternal :: IO FilePath +getUserDocumentsDirectoryInternal :: IO FILEPATH getUserDocumentsDirectoryInternal = getHomeDirectoryInternal -getTemporaryDirectoryInternal :: IO FilePath -getTemporaryDirectoryInternal = fromMaybe "/tmp" <$> lookupEnv "TMPDIR" +getTemporaryDirectoryInternal :: IO FILEPATH +getTemporaryDirectoryInternal = fromMaybe (fromString "/tmp") <$> lookupEnv (fromString "TMPDIR") -#endif diff --git a/System/Directory/Internal/PosixFFI.hsc b/System/Directory/Internal/PosixFFI.hsc new file mode 100644 index 00000000..12434683 --- /dev/null +++ b/System/Directory/Internal/PosixFFI.hsc @@ -0,0 +1,28 @@ +{-# LANGUAGE CPP #-} + +module System.Directory.Internal.PosixFFI where + +#include +#if !defined(mingw32_HOST_OS) +#ifdef HAVE_LIMITS_H +# include +#endif +import Prelude () +import System.Directory.Internal.Prelude + + +-- we use the 'free' from the standard library here since it's not entirely +-- clear whether Haskell's 'free' corresponds to the same one +foreign import ccall unsafe "free" c_free :: Ptr a -> IO () + +#if !defined(HAVE_REALPATH) + +c_realpath :: CString -> CString -> IO CString +c_realpath _ _ = throwIO (mkIOError UnsupportedOperation "platform does not support realpath" Nothing Nothing) + +#else + +foreign import ccall "realpath" c_realpath :: CString -> CString -> IO CString + +#endif +#endif diff --git a/System/Directory/Internal/Windows.hs b/System/Directory/Internal/Windows.hs new file mode 100644 index 00000000..ed6ba599 --- /dev/null +++ b/System/Directory/Internal/Windows.hs @@ -0,0 +1,39 @@ +{-# LANGUAGE CPP #-} +module System.Directory.Internal.Windows where + +#if defined(mingw32_HOST_OS) + +#define FILEPATH FilePath +#define STRING String + +import Data.String (fromString) +import qualified System.Win32.Types as Win32 +import qualified System.Win32.Shell as Win32 +import qualified System.Win32.File as Win32 +import qualified System.Win32.Info as Win32 +import System.Directory.Internal.WindowsFFI +import System.Directory.Internal.WindowsFFI.Common +import System.Directory.Internal.Common +import System.Directory.Internal.Config (exeExtension) +import System.FilePath + ( () + , isPathSeparator + , isRelative + , pathSeparator + , splitDirectories + , takeExtension + ) + +#include "Windows/Template.hs" + +fromPlatformPath :: FilePath -> FilePath +fromPlatformPath = id + +toPlatformPath :: FilePath -> FilePath +toPlatformPath = id + +peekCWLen :: (Ptr CWchar, Int) -> IO String +peekCWLen = peekCWStringLen + + +#endif diff --git a/System/Directory/Internal/Windows/AbstractFilePath.hs b/System/Directory/Internal/Windows/AbstractFilePath.hs new file mode 100644 index 00000000..bec789a5 --- /dev/null +++ b/System/Directory/Internal/Windows/AbstractFilePath.hs @@ -0,0 +1,81 @@ +{-# LANGUAGE CPP #-} + +module System.Directory.Internal.Windows.AbstractFilePath where + + +#if defined(mingw32_HOST_OS) + +#if defined(mingw32_HOST_OS) +# if defined(i386_HOST_ARCH) +# define WINDOWS_CCONV stdcall +# elif defined(x86_64_HOST_ARCH) +# define WINDOWS_CCONV ccall +# else +# error Unknown mingw32 arch +# endif +#endif + +#include "HsBaseConfig.h" + +#define FILEPATH AbstractFilePath +#define STRING OsString + +import Data.String (fromString) +import qualified System.AbstractFilePath.Data.ByteString.Short.Word16 as L +import qualified System.Win32.WindowsString.Types as Win32 +import qualified System.Win32.WindowsString.Shell as Win32 +import qualified System.Win32.WindowsString.File as Win32 +import qualified System.Win32.WindowsString.Info as Win32 +import System.AbstractFilePath.Types +import System.OsString.Internal.Types +import System.Directory.Internal.WindowsFFI.AbstractFilePath +import System.Directory.Internal.WindowsFFI.Common +import System.Directory.Internal.Common.AbstractFilePath +import System.Directory.Internal.Config.AbstractFilePath (exeExtension) +import System.AbstractFilePath + ( () + , isPathSeparator + , isRelative + , pathSeparator + , splitDirectories + , takeExtension + ) +import Foreign.Ptr (castPtr) + +#include "Template.hs" + +#if defined(i386_HOST_ARCH) +# define WINDOWS_CCONV stdcall +#elif defined(x86_64_HOST_ARCH) +# define WINDOWS_CCONV ccall +#else +# error Unknown mingw32 arch +#endif + + +lookupEnv :: AbstractFilePath -> IO (Maybe AbstractFilePath) +lookupEnv (OsString fp) = fmap OsString <$> lookupEnv' fp + +lookupEnv' :: WindowsString -> IO (Maybe WindowsString) +lookupEnv' (WS name) = L.useAsCWString name $ \s -> try_size (castPtr s) 256 + where + try_size s size = allocaArray (fromIntegral size) $ \p_value -> do + res <- c_GetEnvironmentVariable s p_value size + case res of + 0 -> return Nothing + _ | res > size -> try_size s res -- Rare: size increased between calls to GetEnvironmentVariable + | otherwise -> L.packCWString (castPtr p_value) >>= return . Just . WS + +foreign import WINDOWS_CCONV unsafe "windows.h GetEnvironmentVariableW" + c_GetEnvironmentVariable :: Win32.LPWSTR -> Win32.LPWSTR -> Win32.DWORD -> IO Win32.DWORD + +fromPlatformPath :: WindowsFilePath -> AbstractFilePath +fromPlatformPath = OsString + +toPlatformPath :: AbstractFilePath -> WindowsFilePath +toPlatformPath (OsString fp) = fp + +peekCWLen :: (Ptr CWchar, Int) -> IO AbstractFilePath +peekCWLen (ptr, i) = fmap (OsString . WS) . L.packCWStringLen $ (castPtr ptr, i) + +#endif diff --git a/System/Directory/Internal/Windows.hsc b/System/Directory/Internal/Windows/Template.hs similarity index 54% rename from System/Directory/Internal/Windows.hsc rename to System/Directory/Internal/Windows/Template.hs index 6bc76ec9..3ebb9ca2 100644 --- a/System/Directory/Internal/Windows.hsc +++ b/System/Directory/Internal/Windows/Template.hs @@ -1,87 +1,43 @@ -{-# LANGUAGE CPP #-} -module System.Directory.Internal.Windows where -#include -#if defined(mingw32_HOST_OS) -##if defined(i386_HOST_ARCH) -## define WINAPI stdcall -##elif defined(x86_64_HOST_ARCH) -## define WINAPI ccall -##else -## error unknown architecture -##endif -#include -#include -#include -#include import Prelude () +import Data.List (stripPrefix) import System.Directory.Internal.Prelude -import System.Directory.Internal.Common -import System.Directory.Internal.Config (exeExtension) import Data.Time (UTCTime) import Data.Time.Clock.POSIX (POSIXTime, posixSecondsToUTCTime) -import System.FilePath - ( () - , isPathSeparator - , isRelative - , pathSeparator - , splitDirectories - , takeExtension - ) -import qualified Data.List as List -import qualified System.Win32 as Win32 - -createDirectoryInternal :: FilePath -> IO () +import qualified System.Win32.Time as Win32 + +createDirectoryInternal :: FILEPATH -> IO () createDirectoryInternal path = - (`ioeSetFileName` path) `modifyIOError` do + (`ioeSetFileName` decodeFilepathFuzzy path) `modifyIOError` do path' <- furnishPath path - Win32.createDirectory path' Nothing + Win32.createDirectory (unpackPlatform path') Nothing -removePathInternal :: Bool -> FilePath -> IO () +removePathInternal :: Bool -> FILEPATH -> IO () removePathInternal isDir path = - (`ioeSetFileName` path) `modifyIOError` do + (`ioeSetFileName` decodeFilepathFuzzy path) `modifyIOError` do furnishPath path - >>= if isDir then Win32.removeDirectory else Win32.deleteFile + >>= (if isDir then Win32.removeDirectory else Win32.deleteFile) . unpackPlatform -renamePathInternal :: FilePath -> FilePath -> IO () +renamePathInternal :: FILEPATH -> FILEPATH -> IO () renamePathInternal opath npath = - (`ioeSetFileName` opath) `modifyIOError` do + (`ioeSetFileName` decodeFilepathFuzzy opath) `modifyIOError` do opath' <- furnishPath opath npath' <- furnishPath npath -#if MIN_VERSION_Win32(2, 6, 0) - Win32.moveFileEx opath' (Just npath') Win32.mOVEFILE_REPLACE_EXISTING -#else - Win32.moveFileEx opath' npath' Win32.mOVEFILE_REPLACE_EXISTING -#endif + Win32.moveFileEx (unpackPlatform opath') (Just $ unpackPlatform npath') Win32.mOVEFILE_REPLACE_EXISTING -copyFileWithMetadataInternal :: (Metadata -> FilePath -> IO ()) - -> (Metadata -> FilePath -> IO ()) - -> FilePath - -> FilePath +copyFileWithMetadataInternal :: (Metadata -> FILEPATH -> IO ()) + -> (Metadata -> FILEPATH -> IO ()) + -> FILEPATH + -> FILEPATH -> IO () copyFileWithMetadataInternal _ _ src dst = - (`ioeSetFileName` src) `modifyIOError` do + (`ioeSetFileName` decodeFilepathFuzzy src) `modifyIOError` do src' <- furnishPath src dst' <- furnishPath dst - Win32.copyFile src' dst' False + Win32.copyFile (unpackPlatform src') (unpackPlatform dst') False win32_cSIDL_LOCAL_APPDATA :: Win32.CSIDL -#if MIN_VERSION_Win32(2, 3, 1) win32_cSIDL_LOCAL_APPDATA = Win32.cSIDL_LOCAL_APPDATA -#else -win32_cSIDL_LOCAL_APPDATA = (#const CSIDL_LOCAL_APPDATA) -#endif - -win32_cSIDL_COMMON_APPDATA :: Win32.CSIDL -win32_cSIDL_COMMON_APPDATA = (#const CSIDL_COMMON_APPDATA) -win32_eRROR_INVALID_FUNCTION :: Win32.ErrCode -win32_eRROR_INVALID_FUNCTION = (#const ERROR_INVALID_FUNCTION) - -win32_eRROR_INVALID_PARAMETER :: Win32.ErrCode -win32_eRROR_INVALID_PARAMETER = (#const ERROR_INVALID_PARAMETER) - -win32_eRROR_PRIVILEGE_NOT_HELD :: Win32.ErrCode -win32_eRROR_PRIVILEGE_NOT_HELD = (#const ERROR_PRIVILEGE_NOT_HELD) win32_sYMBOLIC_LINK_FLAG_DIRECTORY :: Win32.DWORD win32_sYMBOLIC_LINK_FLAG_DIRECTORY = 0x1 @@ -90,18 +46,10 @@ win32_sYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE :: Win32.DWORD win32_sYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE = 0x2 win32_fILE_ATTRIBUTE_REPARSE_POINT :: Win32.FileAttributeOrFlag -#if MIN_VERSION_Win32(2, 4, 0) win32_fILE_ATTRIBUTE_REPARSE_POINT = Win32.fILE_ATTRIBUTE_REPARSE_POINT -#else -win32_fILE_ATTRIBUTE_REPARSE_POINT = (#const FILE_ATTRIBUTE_REPARSE_POINT) -#endif win32_fILE_SHARE_DELETE :: Win32.ShareMode -#if MIN_VERSION_Win32(2, 3, 1) win32_fILE_SHARE_DELETE = Win32.fILE_SHARE_DELETE -- added in 2.3.0.2 -#else -win32_fILE_SHARE_DELETE = (#const FILE_SHARE_DELETE) -#endif maxShareMode :: Win32.ShareMode maxShareMode = @@ -109,60 +57,24 @@ maxShareMode = Win32.fILE_SHARE_READ .|. Win32.fILE_SHARE_WRITE -win32_getLongPathName, win32_getShortPathName :: FilePath -> IO FilePath -#if MIN_VERSION_Win32(2, 4, 0) -win32_getLongPathName = Win32.getLongPathName -win32_getShortPathName = Win32.getShortPathName -#else -win32_getLongPathName path = - ((`ioeSetLocation` "GetLongPathName") . - (`ioeSetFileName` path)) `modifyIOError` do - withCWString path $ \ ptrPath -> do - getPathNameWith (c_GetLongPathName ptrPath) - -win32_getShortPathName path = - ((`ioeSetLocation` "GetShortPathName") . - (`ioeSetFileName` path)) `modifyIOError` do - withCWString path $ \ ptrPath -> do - getPathNameWith (c_GetShortPathName ptrPath) - -foreign import WINAPI unsafe "windows.h GetLongPathNameW" - c_GetLongPathName - :: Ptr CWchar - -> Ptr CWchar - -> Win32.DWORD - -> IO Win32.DWORD - -foreign import WINAPI unsafe "windows.h GetShortPathNameW" - c_GetShortPathName - :: Ptr CWchar - -> Ptr CWchar - -> Win32.DWORD - -> IO Win32.DWORD -#endif +win32_getLongPathName, win32_getShortPathName :: FILEPATH -> IO FILEPATH +win32_getLongPathName = fmap packPlatform . Win32.getLongPathName . unpackPlatform +win32_getShortPathName = fmap packPlatform . Win32.getShortPathName . unpackPlatform -win32_getFinalPathNameByHandle :: Win32.HANDLE -> Win32.DWORD -> IO FilePath +win32_getFinalPathNameByHandle :: Win32.HANDLE -> Win32.DWORD -> IO FILEPATH win32_getFinalPathNameByHandle _h _flags = (`ioeSetLocation` "GetFinalPathNameByHandle") `modifyIOError` do #ifdef HAVE_GETFINALPATHNAMEBYHANDLEW getPathNameWith $ \ ptr len -> do c_GetFinalPathNameByHandle _h ptr len _flags -foreign import WINAPI unsafe "windows.h GetFinalPathNameByHandleW" - c_GetFinalPathNameByHandle - :: Win32.HANDLE - -> Ptr CWchar - -> Win32.DWORD - -> Win32.DWORD - -> IO Win32.DWORD - #else throwIO (mkIOError UnsupportedOperation "platform does not support GetFinalPathNameByHandle" Nothing Nothing) #endif -getFinalPathName :: FilePath -> IO FilePath +getFinalPathName :: FILEPATH -> IO FILEPATH getFinalPathName = (fromExtendedLengthPath <$>) . rawGetFinalPathName . toExtendedLengthPath where @@ -182,75 +94,6 @@ win32_fILE_FLAG_OPEN_REPARSE_POINT = 0x00200000 win32_fSCTL_GET_REPARSE_POINT :: Win32.DWORD win32_fSCTL_GET_REPARSE_POINT = 0x900a8 -win32_iO_REPARSE_TAG_MOUNT_POINT, win32_iO_REPARSE_TAG_SYMLINK :: CULong -win32_iO_REPARSE_TAG_MOUNT_POINT = (#const IO_REPARSE_TAG_MOUNT_POINT) -win32_iO_REPARSE_TAG_SYMLINK = (#const IO_REPARSE_TAG_SYMLINK) - -win32_mAXIMUM_REPARSE_DATA_BUFFER_SIZE :: Win32.DWORD -win32_mAXIMUM_REPARSE_DATA_BUFFER_SIZE = - (#const MAXIMUM_REPARSE_DATA_BUFFER_SIZE) - -win32_sYMLINK_FLAG_RELATIVE :: CULong -win32_sYMLINK_FLAG_RELATIVE = 0x00000001 - -data Win32_REPARSE_DATA_BUFFER - = Win32_MOUNT_POINT_REPARSE_DATA_BUFFER String String - -- ^ substituteName printName - | Win32_SYMLINK_REPARSE_DATA_BUFFER String String Bool - -- ^ substituteName printName isRelative - | Win32_GENERIC_REPARSE_DATA_BUFFER - -win32_alloca_REPARSE_DATA_BUFFER - :: ((Ptr Win32_REPARSE_DATA_BUFFER, Int) -> IO a) -> IO a -win32_alloca_REPARSE_DATA_BUFFER action = - allocaBytesAligned size align $ \ ptr -> - action (ptr, size) - where size = fromIntegral win32_mAXIMUM_REPARSE_DATA_BUFFER_SIZE - -- workaround (hsc2hs for GHC < 8.0 don't support #{alignment ...}) - align = #{size char[alignof(HsDirectory_REPARSE_DATA_BUFFER)]} - -win32_peek_REPARSE_DATA_BUFFER - :: Ptr Win32_REPARSE_DATA_BUFFER -> IO Win32_REPARSE_DATA_BUFFER -win32_peek_REPARSE_DATA_BUFFER p = do - tag <- #{peek HsDirectory_REPARSE_DATA_BUFFER, ReparseTag} p - case () of - _ | tag == win32_iO_REPARSE_TAG_MOUNT_POINT -> do - let buf = #{ptr HsDirectory_REPARSE_DATA_BUFFER, - MountPointReparseBuffer.PathBuffer} p - sni <- #{peek HsDirectory_REPARSE_DATA_BUFFER, - MountPointReparseBuffer.SubstituteNameOffset} p - sns <- #{peek HsDirectory_REPARSE_DATA_BUFFER, - MountPointReparseBuffer.SubstituteNameLength} p - sn <- peekName buf sni sns - pni <- #{peek HsDirectory_REPARSE_DATA_BUFFER, - MountPointReparseBuffer.PrintNameOffset} p - pns <- #{peek HsDirectory_REPARSE_DATA_BUFFER, - MountPointReparseBuffer.PrintNameLength} p - pn <- peekName buf pni pns - pure (Win32_MOUNT_POINT_REPARSE_DATA_BUFFER sn pn) - | tag == win32_iO_REPARSE_TAG_SYMLINK -> do - let buf = #{ptr HsDirectory_REPARSE_DATA_BUFFER, - SymbolicLinkReparseBuffer.PathBuffer} p - sni <- #{peek HsDirectory_REPARSE_DATA_BUFFER, - SymbolicLinkReparseBuffer.SubstituteNameOffset} p - sns <- #{peek HsDirectory_REPARSE_DATA_BUFFER, - SymbolicLinkReparseBuffer.SubstituteNameLength} p - sn <- peekName buf sni sns - pni <- #{peek HsDirectory_REPARSE_DATA_BUFFER, - SymbolicLinkReparseBuffer.PrintNameOffset} p - pns <- #{peek HsDirectory_REPARSE_DATA_BUFFER, - SymbolicLinkReparseBuffer.PrintNameLength} p - pn <- peekName buf pni pns - flags <- #{peek HsDirectory_REPARSE_DATA_BUFFER, - SymbolicLinkReparseBuffer.Flags} p - pure (Win32_SYMLINK_REPARSE_DATA_BUFFER sn pn - (flags .&. win32_sYMLINK_FLAG_RELATIVE /= 0)) - | otherwise -> pure Win32_GENERIC_REPARSE_DATA_BUFFER - where - peekName :: Ptr CWchar -> CUShort -> CUShort -> IO String - peekName buf offset size = - peekCWStringLen ( buf `plusPtr` fromIntegral offset - , fromIntegral size `div` sizeOf (0 :: CWchar) ) deviceIoControl :: Win32.HANDLE @@ -267,23 +110,11 @@ deviceIoControl h code (inPtr, inSize) (outPtr, outSize) _ = do then Right . fromIntegral <$> peek lenPtr else Left <$> Win32.getLastError -foreign import WINAPI unsafe "windows.h DeviceIoControl" - c_DeviceIoControl - :: Win32.HANDLE - -> Win32.DWORD - -> Ptr a - -> Win32.DWORD - -> Ptr b - -> Win32.DWORD - -> Ptr Win32.DWORD - -> Ptr Void - -> IO Win32.BOOL - -readSymbolicLink :: FilePath -> IO FilePath +readSymbolicLink :: FILEPATH -> IO FILEPATH readSymbolicLink path = - (`ioeSetFileName` path) `modifyIOError` do + (`ioeSetFileName` decodeFilepathFuzzy path) `modifyIOError` do path' <- furnishPath path - let open = Win32.createFile path' 0 maxShareMode Nothing Win32.oPEN_EXISTING + let open = Win32.createFile (toPlatformPath path') 0 maxShareMode Nothing Win32.oPEN_EXISTING (Win32.fILE_FLAG_BACKUP_SEMANTICS .|. win32_fILE_FLAG_OPEN_REPARSE_POINT) Nothing bracket open Win32.closeHandle $ \ h -> do @@ -301,40 +132,42 @@ readSymbolicLink path = Right _ -> pure () rData <- win32_peek_REPARSE_DATA_BUFFER ptr strip <$> case rData of - Win32_MOUNT_POINT_REPARSE_DATA_BUFFER sn _ -> pure sn - Win32_SYMLINK_REPARSE_DATA_BUFFER sn _ _ -> pure sn + Win32_MOUNT_POINT_REPARSE_DATA_BUFFER sn _ -> pure (fromPlatformPath sn) + Win32_SYMLINK_REPARSE_DATA_BUFFER sn _ _ -> pure (fromPlatformPath sn) _ -> throwIO (mkIOError InappropriateType "readSymbolicLink" Nothing Nothing) where - strip sn = fromMaybe sn (List.stripPrefix "\\??\\" sn) + strip sn = fromMaybe sn (pack <$> stripPrefix (unpack $ fromString "\\??\\") (unpack sn)) + -- | On Windows, equivalent to 'simplifyWindows'. -simplify :: FilePath -> FilePath +simplify :: FILEPATH -> FILEPATH simplify = simplifyWindows -- | Normalise the path separators and prepend the @"\\\\?\\"@ prefix if -- necessary or possible. This is used for symbolic links targets because -- they can't handle forward slashes. -normaliseSeparators :: FilePath -> FilePath +normaliseSeparators :: FILEPATH -> FILEPATH normaliseSeparators path - | isRelative path = normaliseSep <$> path + | isRelative path = pack (normaliseSep <$> unpack path) | otherwise = toExtendedLengthPath path where normaliseSep c = if isPathSeparator c then pathSeparator else c -- | 'simplify' the path and prepend the @"\\\\?\\"@ if possible. This -- function can sometimes be used to bypass the @MAX_PATH@ length restriction -- in Windows API calls. -toExtendedLengthPath :: FilePath -> FilePath +toExtendedLengthPath :: FILEPATH -> FILEPATH toExtendedLengthPath path | isRelative path = simplifiedPath | otherwise = - case simplifiedPath of + case toChar <$> simplifiedPath' of '\\' : '?' : '?' : '\\' : _ -> simplifiedPath '\\' : '\\' : '?' : '\\' : _ -> simplifiedPath '\\' : '\\' : '.' : '\\' : _ -> simplifiedPath - '\\' : subpath@('\\' : _) -> "\\\\?\\UNC" <> subpath - _ -> "\\\\?\\" <> simplifiedPath + '\\' : '\\' : _ -> fromString "\\\\?\\UNC" <> pack (tail simplifiedPath') + _ -> fromString "\\\\?\\" <> simplifiedPath where simplifiedPath = simplify path + simplifiedPath' = unpack simplifiedPath -- | Make a path absolute and convert to an extended length path, if possible. -- @@ -342,7 +175,7 @@ toExtendedLengthPath path -- -- This function never fails. If it doesn't understand the path, it just -- returns the path unchanged. -furnishPath :: FilePath -> IO FilePath +furnishPath :: FILEPATH -> IO FILEPATH furnishPath path = (toExtendedLengthPath <$> rawPrependCurrentDirectory path) `catchIOError` \ _ -> @@ -350,33 +183,43 @@ furnishPath path = -- | Strip the @"\\\\?\\"@ prefix if possible. -- The prefix is kept if the meaning of the path would otherwise change. -fromExtendedLengthPath :: FilePath -> FilePath +fromExtendedLengthPath :: FILEPATH -> FILEPATH fromExtendedLengthPath ePath = - case ePath of - '\\' : '\\' : '?' : '\\' : path -> + case ePath' of + c1 : c2 : c3 : c4 : path + | toChar c1 == '\\' + , toChar c2 == '\\' + , toChar c3 == '?' + , toChar c4 == '\\' -> case path of - 'U' : 'N' : 'C' : subpath@('\\' : _) -> "\\" <> subpath - drive : ':' : subpath + c5 : c6 : c7 : c8 : rest + | toChar c5 == 'U' + , toChar c6 == 'N' + , toChar c7 == 'C' + , toChar c8 == '\\' -> fromString "\\\\" <> pack rest + drive : col : subpath -- if the path is not "regular", then the prefix is necessary -- to ensure the path is interpreted literally - | isAlpha drive && isAscii drive && isPathRegular subpath -> path + | toChar col == ':' + , isAlpha (toChar drive) && isAscii (toChar drive) && isPathRegular subpath -> pack path _ -> ePath _ -> ePath where + ePath' = unpack ePath isPathRegular path = - not ('/' `elem` path || - "." `elem` splitDirectories path || - ".." `elem` splitDirectories path) + not ('/' `elem` (toChar <$> path) || + fromString "." `elem` splitDirectories (pack path) || + fromString ".." `elem` splitDirectories (pack path)) -getPathNameWith :: (Ptr CWchar -> Win32.DWORD -> IO Win32.DWORD) -> IO FilePath +getPathNameWith :: (Ptr CWchar -> Win32.DWORD -> IO Win32.DWORD) -> IO FILEPATH getPathNameWith cFunc = do let getPathNameWithLen len = do allocaArray (fromIntegral len) $ \ ptrPathOut -> do len' <- Win32.failIfZero "" (cFunc ptrPathOut len) if len' <= len - then Right <$> peekCWStringLen (ptrPathOut, fromIntegral len') + then Right <$> peekCWLen (ptrPathOut, fromIntegral len') else pure (Left len') - r <- getPathNameWithLen ((#const MAX_PATH) * (#size wchar_t)) + r <- getPathNameWithLen max_path_len case r of Right s -> pure s Left len -> do @@ -386,64 +229,60 @@ getPathNameWith cFunc = do Left _ -> throwIO (mkIOError OtherError "" Nothing Nothing `ioeSetErrorString` "path changed unexpectedly") -canonicalizePathWith :: ((FilePath -> IO FilePath) -> FilePath -> IO FilePath) - -> FilePath - -> IO FilePath +canonicalizePathWith :: ((FILEPATH -> IO FILEPATH) -> FILEPATH -> IO FILEPATH) + -> FILEPATH + -> IO FILEPATH canonicalizePathWith attemptRealpath = attemptRealpath getFinalPathName -canonicalizePathSimplify :: FilePath -> IO FilePath +canonicalizePathSimplify :: FILEPATH -> IO FILEPATH canonicalizePathSimplify path = getFullPathName path `catchIOError` \ _ -> pure path -searchPathEnvForExes :: String -> IO (Maybe FilePath) -searchPathEnvForExes binary = Win32.searchPath Nothing binary $ -#if MIN_VERSION_Win32(2, 6, 0) - Just -#endif - exeExtension +searchPathEnvForExes :: STRING -> IO (Maybe FILEPATH) +searchPathEnvForExes binary = fmap fromPlatformPath <$> (Win32.searchPath Nothing (toPlatformPath binary) $ Just (toPlatformPath exeExtension)) -findExecutablesLazyInternal :: ([FilePath] -> String -> ListT IO FilePath) - -> String - -> ListT IO FilePath +findExecutablesLazyInternal :: ([FILEPATH] -> STRING -> ListT IO FILEPATH) + -> STRING + -> ListT IO FILEPATH findExecutablesLazyInternal _ = maybeToListT . searchPathEnvForExes -exeExtensionInternal :: String +exeExtensionInternal :: STRING exeExtensionInternal = exeExtension -getDirectoryContentsInternal :: FilePath -> IO [FilePath] +getDirectoryContentsInternal :: FILEPATH -> IO [FILEPATH] getDirectoryContentsInternal path = do - query <- furnishPath (path "*") + query <- furnishPath (path fromString "*") bracket - (Win32.findFirstFile query) + (Win32.findFirstFile $ toPlatformPath query) (\ (h, _) -> Win32.findClose h) (\ (h, fdat) -> loop h fdat []) where -- we needn't worry about empty directories: a directory always -- has at least "." and ".." entries - loop :: Win32.HANDLE -> Win32.FindData -> [FilePath] -> IO [FilePath] + loop :: Win32.HANDLE -> Win32.FindData -> [FILEPATH] -> IO [FILEPATH] loop h fdat acc = do filename <- Win32.getFindDataFileName fdat more <- Win32.findNextFile h fdat if more - then loop h fdat (filename : acc) - else pure (filename : acc) + then loop h fdat (fromPlatformPath filename : acc) + else pure (fromPlatformPath filename : acc) -- no need to reverse, ordering is undefined -getCurrentDirectoryInternal :: IO FilePath -getCurrentDirectoryInternal = Win32.getCurrentDirectory +getCurrentDirectoryInternal :: IO FILEPATH +getCurrentDirectoryInternal = fromPlatformPath <$> Win32.getCurrentDirectory -getFullPathName :: FilePath -> IO FilePath +getFullPathName :: FILEPATH -> IO FILEPATH getFullPathName path = - fromExtendedLengthPath <$> Win32.getFullPathName (toExtendedLengthPath path) + (fromExtendedLengthPath . fromPlatformPath) <$> Win32.getFullPathName (toPlatformPath $ toExtendedLengthPath path) -- | Similar to 'prependCurrentDirectory' but fails for empty paths. -rawPrependCurrentDirectory :: FilePath -> IO FilePath +rawPrependCurrentDirectory :: FILEPATH -> IO FILEPATH rawPrependCurrentDirectory path | isRelative path = ((`ioeAddLocation` "prependCurrentDirectory") . - (`ioeSetFileName` path)) `modifyIOError` do + (`ioeSetFileName` decodeFilepathFuzzy path)) `modifyIOError` do getFullPathName path | otherwise = pure path @@ -456,15 +295,15 @@ rawPrependCurrentDirectory path -- operation may throw exceptions. -- -- Empty paths are treated as the current directory. -prependCurrentDirectory :: FilePath -> IO FilePath +prependCurrentDirectory :: FILEPATH -> IO FILEPATH prependCurrentDirectory = rawPrependCurrentDirectory . emptyToCurDir -- SetCurrentDirectory does not support long paths even with the \\?\ prefix -- https://ghc.haskell.org/trac/ghc/ticket/13373#comment:6 -setCurrentDirectoryInternal :: FilePath -> IO () -setCurrentDirectoryInternal = Win32.setCurrentDirectory +setCurrentDirectoryInternal :: FILEPATH -> IO () +setCurrentDirectoryInternal = Win32.setCurrentDirectory . toPlatformPath -createSymbolicLinkUnpriv :: String -> String -> Bool -> IO () +createSymbolicLinkUnpriv :: STRING -> STRING -> Bool -> IO () createSymbolicLinkUnpriv link _target _isDir = #ifdef HAVE_CREATESYMBOLICLINKW withCWString link $ \ pLink -> @@ -488,7 +327,7 @@ createSymbolicLinkUnpriv link _target _isDir = "Creating symbolic links usually requires " <> "administrative rights." throwIO (mkIOError permissionErrorType "CreateSymbolicLink" - Nothing (Just link) + Nothing (Just $ decodeFilepathFuzzy link) `ioeSetErrorString` msg) | e == win32_eRROR_INVALID_PARAMETER && unpriv /= 0 -> @@ -497,44 +336,40 @@ createSymbolicLinkUnpriv link _target _isDir = call pLink pTarget flags 0 | otherwise -> Win32.failWith "CreateSymbolicLink" e -foreign import WINAPI unsafe "windows.h CreateSymbolicLinkW" - c_CreateSymbolicLink - :: Ptr CWchar -> Ptr CWchar -> Win32.DWORD -> IO Win32.BYTE - #else throwIO . (`ioeSetErrorString` unsupportedErrorMsg) $ mkIOError UnsupportedOperation "CreateSymbolicLink" - Nothing (Just link) + Nothing (Just $ decodeFilepathFuzzy link) where unsupportedErrorMsg = "Not supported on Windows XP or older" #endif linkToDirectoryIsDirectory :: Bool linkToDirectoryIsDirectory = True -createSymbolicLink :: Bool -> FilePath -> FilePath -> IO () +createSymbolicLink :: Bool -> FILEPATH -> FILEPATH -> IO () createSymbolicLink isDir target link = - (`ioeSetFileName` link) `modifyIOError` do + (`ioeSetFileName` decodeFilepathFuzzy link) `modifyIOError` do -- normaliseSeparators ensures the target gets normalised properly link' <- furnishPath link createSymbolicLinkUnpriv link' (normaliseSeparators target) isDir type Metadata = Win32.BY_HANDLE_FILE_INFORMATION -getSymbolicLinkMetadata :: FilePath -> IO Metadata +getSymbolicLinkMetadata :: FILEPATH -> IO Metadata getSymbolicLinkMetadata path = - (`ioeSetFileName` path) `modifyIOError` do + (`ioeSetFileName` decodeFilepathFuzzy path) `modifyIOError` do path' <- furnishPath path - let open = Win32.createFile path' 0 maxShareMode Nothing Win32.oPEN_EXISTING + let open = Win32.createFile (toPlatformPath path') 0 maxShareMode Nothing Win32.oPEN_EXISTING (Win32.fILE_FLAG_BACKUP_SEMANTICS .|. win32_fILE_FLAG_OPEN_REPARSE_POINT) Nothing bracket open Win32.closeHandle $ \ h -> do Win32.getFileInformationByHandle h -getFileMetadata :: FilePath -> IO Metadata +getFileMetadata :: FILEPATH -> IO Metadata getFileMetadata path = - (`ioeSetFileName` path) `modifyIOError` do + (`ioeSetFileName` decodeFilepathFuzzy path) `modifyIOError` do path' <- furnishPath path - let open = Win32.createFile path' 0 maxShareMode Nothing Win32.oPEN_EXISTING + let open = Win32.createFile (toPlatformPath path') 0 maxShareMode Nothing Win32.oPEN_EXISTING Win32.fILE_FLAG_BACKUP_SEMANTICS Nothing bracket open Win32.closeHandle $ \ h -> do Win32.getFileInformationByHandle h @@ -575,25 +410,18 @@ posixToWindowsTime :: POSIXTime -> Win32.FILETIME posixToWindowsTime t = Win32.FILETIME $ truncate (t * 10000000 + windowsPosixEpochDifference) -setTimes :: FilePath -> (Maybe POSIXTime, Maybe POSIXTime) -> IO () +setTimes :: FILEPATH -> (Maybe POSIXTime, Maybe POSIXTime) -> IO () setTimes path' (atime', mtime') = bracket (openFileHandle path' Win32.gENERIC_WRITE) Win32.closeHandle $ \ handle -> -#if MIN_VERSION_Win32(2,12,0) Win32.setFileTime handle Nothing (posixToWindowsTime <$> atime') (posixToWindowsTime <$> mtime') -#else - maybeWith with (posixToWindowsTime <$> atime') $ \ atime'' -> - maybeWith with (posixToWindowsTime <$> mtime') $ \ mtime'' -> - Win32.failIf_ not "" $ - Win32.c_SetFileTime handle nullPtr atime'' mtime'' -#endif -- | Open the handle of an existing file or directory. -openFileHandle :: String -> Win32.AccessMode -> IO Win32.HANDLE +openFileHandle :: STRING -> Win32.AccessMode -> IO Win32.HANDLE openFileHandle path mode = - (`ioeSetFileName` path) `modifyIOError` do + (`ioeSetFileName` decodeFilepathFuzzy path) `modifyIOError` do path' <- furnishPath path - Win32.createFile path' mode maxShareMode Nothing + Win32.createFile (toPlatformPath path') mode maxShareMode Nothing Win32.oPEN_EXISTING flags Nothing where flags = Win32.fILE_ATTRIBUTE_NORMAL .|. Win32.fILE_FLAG_BACKUP_SEMANTICS -- required for directories @@ -610,26 +438,26 @@ setWriteMode :: Bool -> Mode -> Mode setWriteMode False m = m .|. Win32.fILE_ATTRIBUTE_READONLY setWriteMode True m = m .&. complement Win32.fILE_ATTRIBUTE_READONLY -setFileMode :: FilePath -> Mode -> IO () +setFileMode :: FILEPATH -> Mode -> IO () setFileMode path mode = - (`ioeSetFileName` path) `modifyIOError` do + (`ioeSetFileName` decodeFilepathFuzzy path) `modifyIOError` do path' <- furnishPath path - Win32.setFileAttributes path' mode + Win32.setFileAttributes (toPlatformPath path') mode -- | A restricted form of 'setFileMode' that only sets the permission bits. -- For Windows, this means only the "read-only" attribute is affected. -setFilePermissions :: FilePath -> Mode -> IO () +setFilePermissions :: FILEPATH -> Mode -> IO () setFilePermissions path m = do m' <- modeFromMetadata <$> getFileMetadata path setFileMode path ((m' .&. complement Win32.fILE_ATTRIBUTE_READONLY) .|. (m .&. Win32.fILE_ATTRIBUTE_READONLY)) -getAccessPermissions :: FilePath -> IO Permissions +getAccessPermissions :: FILEPATH -> IO Permissions getAccessPermissions path = do m <- getFileMetadata path let isDir = fileTypeIsDirectory (fileTypeFromMetadata m) let w = hasWriteMode (modeFromMetadata m) - let x = (toLower <$> takeExtension path) + let x = (toLower . toChar <$> (unpack (takeExtension path))) `elem` [".bat", ".cmd", ".com", ".exe"] pure Permissions { readable = True @@ -638,19 +466,19 @@ getAccessPermissions path = do , searchable = isDir } -setAccessPermissions :: FilePath -> Permissions -> IO () +setAccessPermissions :: FILEPATH -> Permissions -> IO () setAccessPermissions path Permissions{writable = w} = do setFilePermissions path (setWriteMode w 0) -getFolderPath :: Win32.CSIDL -> IO FilePath -getFolderPath what = Win32.sHGetFolderPath nullPtr what nullPtr 0 +getFolderPath :: Win32.CSIDL -> IO FILEPATH +getFolderPath what = fromPlatformPath <$> (Win32.sHGetFolderPath nullPtr what nullPtr 0) -getHomeDirectoryInternal :: IO FilePath +getHomeDirectoryInternal :: IO FILEPATH getHomeDirectoryInternal = getFolderPath Win32.cSIDL_PROFILE `catchIOError` \ _ -> getFolderPath Win32.cSIDL_WINDOWS -getXdgDirectoryFallback :: IO FilePath -> XdgDirectory -> IO FilePath +getXdgDirectoryFallback :: IO FILEPATH -> XdgDirectory -> IO FILEPATH getXdgDirectoryFallback _ xdgDir = do case xdgDir of XdgData -> getFolderPath Win32.cSIDL_APPDATA @@ -658,19 +486,18 @@ getXdgDirectoryFallback _ xdgDir = do XdgCache -> getFolderPath win32_cSIDL_LOCAL_APPDATA XdgState -> getFolderPath win32_cSIDL_LOCAL_APPDATA -getXdgDirectoryListFallback :: XdgDirectoryList -> IO [FilePath] +getXdgDirectoryListFallback :: XdgDirectoryList -> IO [FILEPATH] getXdgDirectoryListFallback _ = pure <$> getFolderPath win32_cSIDL_COMMON_APPDATA -getAppUserDataDirectoryInternal :: FilePath -> IO FilePath +getAppUserDataDirectoryInternal :: FILEPATH -> IO FILEPATH getAppUserDataDirectoryInternal appName = - (\ appData -> appData <> ('\\' : appName)) + (\ appData -> appData <> (fromString "\\" <> appName)) <$> getXdgDirectoryFallback getHomeDirectoryInternal XdgData -getUserDocumentsDirectoryInternal :: IO FilePath +getUserDocumentsDirectoryInternal :: IO FILEPATH getUserDocumentsDirectoryInternal = getFolderPath Win32.cSIDL_PERSONAL -getTemporaryDirectoryInternal :: IO FilePath -getTemporaryDirectoryInternal = Win32.getTemporaryDirectory +getTemporaryDirectoryInternal :: IO FILEPATH +getTemporaryDirectoryInternal = fromPlatformPath <$> Win32.getTemporaryDirectory -#endif diff --git a/System/Directory/Internal/WindowsFFI.hsc b/System/Directory/Internal/WindowsFFI.hsc new file mode 100644 index 00000000..68e094a6 --- /dev/null +++ b/System/Directory/Internal/WindowsFFI.hsc @@ -0,0 +1,92 @@ +{-# LANGUAGE CPP #-} +module System.Directory.Internal.WindowsFFI where +#include +#if defined(mingw32_HOST_OS) +##if defined(i386_HOST_ARCH) +## define WINAPI stdcall +##elif defined(x86_64_HOST_ARCH) +## define WINAPI ccall +##else +## error unknown architecture +##endif +#include +#include +#include +#include + +import Prelude () +import System.Directory.Internal.Prelude +import qualified System.Win32 as Win32 + +win32_mAXIMUM_REPARSE_DATA_BUFFER_SIZE :: Win32.DWORD +win32_mAXIMUM_REPARSE_DATA_BUFFER_SIZE = + (#const MAXIMUM_REPARSE_DATA_BUFFER_SIZE) + +win32_iO_REPARSE_TAG_MOUNT_POINT, win32_iO_REPARSE_TAG_SYMLINK :: CULong +win32_iO_REPARSE_TAG_MOUNT_POINT = (#const IO_REPARSE_TAG_MOUNT_POINT) +win32_iO_REPARSE_TAG_SYMLINK = (#const IO_REPARSE_TAG_SYMLINK) + +win32_sYMLINK_FLAG_RELATIVE :: CULong +win32_sYMLINK_FLAG_RELATIVE = 0x00000001 + +data Win32_REPARSE_DATA_BUFFER + = Win32_MOUNT_POINT_REPARSE_DATA_BUFFER String String + -- ^ substituteName printName + | Win32_SYMLINK_REPARSE_DATA_BUFFER String String Bool + -- ^ substituteName printName isRelative + | Win32_GENERIC_REPARSE_DATA_BUFFER + + +win32_alloca_REPARSE_DATA_BUFFER + :: ((Ptr Win32_REPARSE_DATA_BUFFER, Int) -> IO a) -> IO a +win32_alloca_REPARSE_DATA_BUFFER action = + allocaBytesAligned size align $ \ ptr -> + action (ptr, size) + where size = fromIntegral win32_mAXIMUM_REPARSE_DATA_BUFFER_SIZE + -- workaround (hsc2hs for GHC < 8.0 don't support #{alignment ...}) + align = #{size char[alignof(HsDirectory_REPARSE_DATA_BUFFER)]} + +peekName :: Ptr CWchar -> CUShort -> CUShort -> IO String +peekName buf offset size = + peekCWStringLen ( buf `plusPtr` fromIntegral offset + , fromIntegral size `div` sizeOf (0 :: CWchar) ) + +win32_peek_REPARSE_DATA_BUFFER + :: Ptr Win32_REPARSE_DATA_BUFFER -> IO Win32_REPARSE_DATA_BUFFER +win32_peek_REPARSE_DATA_BUFFER p = do + tag <- #{peek HsDirectory_REPARSE_DATA_BUFFER, ReparseTag} p + case () of + _ | tag == win32_iO_REPARSE_TAG_MOUNT_POINT -> do + let buf = #{ptr HsDirectory_REPARSE_DATA_BUFFER, + MountPointReparseBuffer.PathBuffer} p + sni <- #{peek HsDirectory_REPARSE_DATA_BUFFER, + MountPointReparseBuffer.SubstituteNameOffset} p + sns <- #{peek HsDirectory_REPARSE_DATA_BUFFER, + MountPointReparseBuffer.SubstituteNameLength} p + sn <- peekName buf sni sns + pni <- #{peek HsDirectory_REPARSE_DATA_BUFFER, + MountPointReparseBuffer.PrintNameOffset} p + pns <- #{peek HsDirectory_REPARSE_DATA_BUFFER, + MountPointReparseBuffer.PrintNameLength} p + pn <- peekName buf pni pns + pure (Win32_MOUNT_POINT_REPARSE_DATA_BUFFER sn pn) + | tag == win32_iO_REPARSE_TAG_SYMLINK -> do + let buf = #{ptr HsDirectory_REPARSE_DATA_BUFFER, + SymbolicLinkReparseBuffer.PathBuffer} p + sni <- #{peek HsDirectory_REPARSE_DATA_BUFFER, + SymbolicLinkReparseBuffer.SubstituteNameOffset} p + sns <- #{peek HsDirectory_REPARSE_DATA_BUFFER, + SymbolicLinkReparseBuffer.SubstituteNameLength} p + sn <- peekName buf sni sns + pni <- #{peek HsDirectory_REPARSE_DATA_BUFFER, + SymbolicLinkReparseBuffer.PrintNameOffset} p + pns <- #{peek HsDirectory_REPARSE_DATA_BUFFER, + SymbolicLinkReparseBuffer.PrintNameLength} p + pn <- peekName buf pni pns + flags <- #{peek HsDirectory_REPARSE_DATA_BUFFER, + SymbolicLinkReparseBuffer.Flags} p + pure (Win32_SYMLINK_REPARSE_DATA_BUFFER sn pn + (flags .&. win32_sYMLINK_FLAG_RELATIVE /= 0)) + | otherwise -> pure Win32_GENERIC_REPARSE_DATA_BUFFER + +#endif diff --git a/System/Directory/Internal/WindowsFFI/AbstractFilePath.hsc b/System/Directory/Internal/WindowsFFI/AbstractFilePath.hsc new file mode 100644 index 00000000..61379e57 --- /dev/null +++ b/System/Directory/Internal/WindowsFFI/AbstractFilePath.hsc @@ -0,0 +1,96 @@ +{-# LANGUAGE CPP #-} + +module System.Directory.Internal.WindowsFFI.AbstractFilePath where + +#include +#if defined(mingw32_HOST_OS) +##if defined(i386_HOST_ARCH) +## define WINAPI stdcall +##elif defined(x86_64_HOST_ARCH) +## define WINAPI ccall +##else +## error unknown architecture +##endif +#include +#include +#include +#include + +import System.OsString.Internal.Types +import System.AbstractFilePath.Data.ByteString.Short.Word16 (packCWStringLen) +import Prelude () +import System.Directory.Internal.Prelude +import qualified System.Win32 as Win32 + +data Win32_REPARSE_DATA_BUFFER + = Win32_MOUNT_POINT_REPARSE_DATA_BUFFER WindowsString WindowsString + -- ^ substituteName printName + | Win32_SYMLINK_REPARSE_DATA_BUFFER WindowsString WindowsString Bool + -- ^ substituteName printName isRelative + | Win32_GENERIC_REPARSE_DATA_BUFFER + +win32_mAXIMUM_REPARSE_DATA_BUFFER_SIZE :: Win32.DWORD +win32_mAXIMUM_REPARSE_DATA_BUFFER_SIZE = + (#const MAXIMUM_REPARSE_DATA_BUFFER_SIZE) + +win32_iO_REPARSE_TAG_MOUNT_POINT, win32_iO_REPARSE_TAG_SYMLINK :: CULong +win32_iO_REPARSE_TAG_MOUNT_POINT = (#const IO_REPARSE_TAG_MOUNT_POINT) +win32_iO_REPARSE_TAG_SYMLINK = (#const IO_REPARSE_TAG_SYMLINK) + +win32_sYMLINK_FLAG_RELATIVE :: CULong +win32_sYMLINK_FLAG_RELATIVE = 0x00000001 + +win32_alloca_REPARSE_DATA_BUFFER + :: ((Ptr Win32_REPARSE_DATA_BUFFER, Int) -> IO a) -> IO a +win32_alloca_REPARSE_DATA_BUFFER action = + allocaBytesAligned size align $ \ ptr -> + action (ptr, size) + where size = fromIntegral win32_mAXIMUM_REPARSE_DATA_BUFFER_SIZE + -- workaround (hsc2hs for GHC < 8.0 don't support #{alignment ...}) + align = #{size char[alignof(HsDirectory_REPARSE_DATA_BUFFER)]} + +win32_peek_REPARSE_DATA_BUFFER + :: Ptr Win32_REPARSE_DATA_BUFFER -> IO Win32_REPARSE_DATA_BUFFER +win32_peek_REPARSE_DATA_BUFFER p = do + tag <- #{peek HsDirectory_REPARSE_DATA_BUFFER, ReparseTag} p + case () of + _ | tag == win32_iO_REPARSE_TAG_MOUNT_POINT -> do + let buf = #{ptr HsDirectory_REPARSE_DATA_BUFFER, + MountPointReparseBuffer.PathBuffer} p + sni <- #{peek HsDirectory_REPARSE_DATA_BUFFER, + MountPointReparseBuffer.SubstituteNameOffset} p + sns <- #{peek HsDirectory_REPARSE_DATA_BUFFER, + MountPointReparseBuffer.SubstituteNameLength} p + sn <- peekName buf sni sns + pni <- #{peek HsDirectory_REPARSE_DATA_BUFFER, + MountPointReparseBuffer.PrintNameOffset} p + pns <- #{peek HsDirectory_REPARSE_DATA_BUFFER, + MountPointReparseBuffer.PrintNameLength} p + pn <- peekName buf pni pns + pure (Win32_MOUNT_POINT_REPARSE_DATA_BUFFER sn pn) + | tag == win32_iO_REPARSE_TAG_SYMLINK -> do + let buf = #{ptr HsDirectory_REPARSE_DATA_BUFFER, + SymbolicLinkReparseBuffer.PathBuffer} p + sni <- #{peek HsDirectory_REPARSE_DATA_BUFFER, + SymbolicLinkReparseBuffer.SubstituteNameOffset} p + sns <- #{peek HsDirectory_REPARSE_DATA_BUFFER, + SymbolicLinkReparseBuffer.SubstituteNameLength} p + sn <- peekName buf sni sns + pni <- #{peek HsDirectory_REPARSE_DATA_BUFFER, + SymbolicLinkReparseBuffer.PrintNameOffset} p + pns <- #{peek HsDirectory_REPARSE_DATA_BUFFER, + SymbolicLinkReparseBuffer.PrintNameLength} p + pn <- peekName buf pni pns + flags <- #{peek HsDirectory_REPARSE_DATA_BUFFER, + SymbolicLinkReparseBuffer.Flags} p + pure (Win32_SYMLINK_REPARSE_DATA_BUFFER sn pn + (flags .&. win32_sYMLINK_FLAG_RELATIVE /= 0)) + | otherwise -> pure Win32_GENERIC_REPARSE_DATA_BUFFER + +peekName :: Ptr CWchar -> CUShort -> CUShort -> IO WindowsString +peekName buf offset size = WS <$> + packCWStringLen ( buf `plusPtr` fromIntegral offset + , fromIntegral size `div` sizeOf (0 :: CWchar) ) + + +#endif diff --git a/System/Directory/Internal/WindowsFFI/Common.hsc b/System/Directory/Internal/WindowsFFI/Common.hsc new file mode 100644 index 00000000..291e76c3 --- /dev/null +++ b/System/Directory/Internal/WindowsFFI/Common.hsc @@ -0,0 +1,68 @@ +{-# LANGUAGE CPP #-} +module System.Directory.Internal.WindowsFFI.Common where +#include +#if defined(mingw32_HOST_OS) +##if defined(i386_HOST_ARCH) +## define WINAPI stdcall +##elif defined(x86_64_HOST_ARCH) +## define WINAPI ccall +##else +## error unknown architecture +##endif +#include +#include +#include +#include +import Prelude () +import System.Directory.Internal.Prelude +import GHC.Word (Word32) +import qualified System.Win32 as Win32 + + +#ifdef HAVE_GETFINALPATHNAMEBYHANDLEW +foreign import WINAPI unsafe "windows.h GetFinalPathNameByHandleW" + c_GetFinalPathNameByHandle + :: Win32.HANDLE + -> Ptr CWchar + -> Win32.DWORD + -> Win32.DWORD + -> IO Win32.DWORD + +#endif + + +foreign import WINAPI unsafe "windows.h DeviceIoControl" + c_DeviceIoControl + :: Win32.HANDLE + -> Win32.DWORD + -> Ptr a + -> Win32.DWORD + -> Ptr b + -> Win32.DWORD + -> Ptr Win32.DWORD + -> Ptr Void + -> IO Win32.BOOL + + +#ifdef HAVE_CREATESYMBOLICLINKW +foreign import WINAPI unsafe "windows.h CreateSymbolicLinkW" + c_CreateSymbolicLink + :: Ptr CWchar -> Ptr CWchar -> Win32.DWORD -> IO Win32.BYTE +#endif + +win32_cSIDL_COMMON_APPDATA :: Win32.CSIDL +win32_cSIDL_COMMON_APPDATA = (#const CSIDL_COMMON_APPDATA) + +win32_eRROR_INVALID_FUNCTION :: Win32.ErrCode +win32_eRROR_INVALID_FUNCTION = (#const ERROR_INVALID_FUNCTION) + +win32_eRROR_INVALID_PARAMETER :: Win32.ErrCode +win32_eRROR_INVALID_PARAMETER = (#const ERROR_INVALID_PARAMETER) + +win32_eRROR_PRIVILEGE_NOT_HELD :: Win32.ErrCode +win32_eRROR_PRIVILEGE_NOT_HELD = (#const ERROR_PRIVILEGE_NOT_HELD) + +max_path_len :: Word32 +max_path_len = (#const MAX_PATH) * (#size wchar_t) + +#endif diff --git a/System/Directory/Template.hs b/System/Directory/Template.hs new file mode 100644 index 00000000..aeb5020a --- /dev/null +++ b/System/Directory/Template.hs @@ -0,0 +1,1561 @@ + +{- $intro +A directory contains a series of entries, each of which is a named +reference to a file system object (file, directory etc.). Some +entries may be hidden, inaccessible, or have some administrative +function (e.g. @.@ or @..@ under +), but in +this standard all such entries are considered to form part of the +directory contents. Entries in sub-directories are not, however, +considered to form part of the directory contents. + +Each file system object is referenced by a /path/. There is +normally at least one absolute path to each file system object. In +some operating systems, it may also be possible to have paths which +are relative to the current directory. + +Unless otherwise documented: + +* 'IO' operations in this package may throw any 'IOError'. No other types of + exceptions shall be thrown. + +* The list of possible 'IOErrorType's in the API documentation is not + exhaustive. The full list may vary by platform and/or evolve over time. + +-} + +----------------------------------------------------------------------------- +-- Permissions + +{- $permissions + +directory offers a limited (and quirky) interface for reading and setting file +and directory permissions; see 'getPermissions' and 'setPermissions' for a +discussion of their limitations. Because permissions are very difficult to +implement portably across different platforms, users who wish to do more +sophisticated things with permissions are advised to use other, +platform-specific libraries instead. For example, if you are only interested +in permissions on POSIX-like platforms, + +offers much more flexibility. + + The 'Permissions' type is used to record whether certain operations are + permissible on a file\/directory. 'getPermissions' and 'setPermissions' + get and set these permissions, respectively. Permissions apply both to + files and directories. For directories, the executable field will be + 'False', and for files the searchable field will be 'False'. Note that + directories may be searchable without being readable, if permission has + been given to use them as part of a path, but not to examine the + directory contents. + +Note that to change some, but not all permissions, a construct on the following lines must be used. + +> makeReadable f = do +> p <- getPermissions f +> setPermissions f (p {readable = True}) + +-} + +emptyPermissions :: Permissions +emptyPermissions = Permissions { + readable = False, + writable = False, + executable = False, + searchable = False + } + +setOwnerReadable :: Bool -> Permissions -> Permissions +setOwnerReadable b p = p { readable = b } + +setOwnerWritable :: Bool -> Permissions -> Permissions +setOwnerWritable b p = p { writable = b } + +setOwnerExecutable :: Bool -> Permissions -> Permissions +setOwnerExecutable b p = p { executable = b } + +setOwnerSearchable :: Bool -> Permissions -> Permissions +setOwnerSearchable b p = p { searchable = b } + +-- | Get the permissions of a file or directory. +-- +-- On Windows, the 'writable' permission corresponds to the "read-only" +-- attribute. The 'executable' permission is set if the file extension is of +-- an executable file type. The 'readable' permission is always set. +-- +-- On POSIX systems, this returns the result of @access@. +-- +-- The operation may fail with: +-- +-- * 'isPermissionError' if the user is not permitted to access the +-- permissions, or +-- +-- * 'isDoesNotExistError' if the file or directory does not exist. +getPermissions :: FILEPATH -> IO Permissions +getPermissions path = + (`ioeAddLocation` "getPermissions") `modifyIOError` do + getAccessPermissions (emptyToCurDir path) + +-- | Set the permissions of a file or directory. +-- +-- On Windows, this is only capable of changing the 'writable' permission, +-- which corresponds to the "read-only" attribute. Changing the other +-- permissions has no effect. +-- +-- On POSIX systems, this sets the /owner/ permissions. +-- +-- The operation may fail with: +-- +-- * 'isPermissionError' if the user is not permitted to set the permissions, +-- or +-- +-- * 'isDoesNotExistError' if the file or directory does not exist. +setPermissions :: FILEPATH -> Permissions -> IO () +setPermissions path p = + (`ioeAddLocation` "setPermissions") `modifyIOError` do + setAccessPermissions (emptyToCurDir path) p + +-- | Copy the permissions of one file to another. This reproduces the +-- permissions more accurately than using 'getPermissions' followed by +-- 'setPermissions'. +-- +-- On Windows, this copies only the read-only attribute. +-- +-- On POSIX systems, this is equivalent to @stat@ followed by @chmod@. +copyPermissions :: FILEPATH -> FILEPATH -> IO () +copyPermissions src dst = + (`ioeAddLocation` "copyPermissions") `modifyIOError` do + m <- getFileMetadata src + copyPermissionsFromMetadata m dst + +copyPermissionsFromMetadata :: Metadata -> FILEPATH -> IO () +copyPermissionsFromMetadata m dst = do + -- instead of setFileMode, setFilePermissions is used here + -- this is to retain backward compatibility in copyPermissions + setFilePermissions dst (modeFromMetadata m) + +----------------------------------------------------------------------------- +-- Implementation + +{- |@'createDirectory' dir@ creates a new directory @dir@ which is +initially empty, or as near to empty as the operating system +allows. + +The operation may fail with: + +* 'isPermissionError' +The process has insufficient privileges to perform the operation. +@[EROFS, EACCES]@ + +* 'isAlreadyExistsError' +The operand refers to a directory that already exists. +@ [EEXIST]@ + +* @HardwareFault@ +A physical I\/O error has occurred. +@[EIO]@ + +* @InvalidArgument@ +The operand is not a valid directory name. +@[ENAMETOOLONG, ELOOP]@ + +* 'isDoesNotExistError' +There is no path to the directory. +@[ENOENT, ENOTDIR]@ + +* 'System.IO.isFullError' +Insufficient resources (virtual memory, process file descriptors, +physical disk space, etc.) are available to perform the operation. +@[EDQUOT, ENOSPC, ENOMEM, EMLINK]@ + +* @InappropriateType@ +The path refers to an existing non-directory object. +@[EEXIST]@ + +-} + +createDirectory :: FILEPATH -> IO () +createDirectory = createDirectoryInternal + +-- | @'createDirectoryIfMissing' parents dir@ creates a new directory +-- @dir@ if it doesn\'t exist. If the first argument is 'True' +-- the function will also create all parent directories if they are missing. +createDirectoryIfMissing :: Bool -- ^ Create its parents too? + -> FILEPATH -- ^ The path to the directory you want to make + -> IO () +createDirectoryIfMissing create_parents path0 + | create_parents = createDirs (parents path0) + | otherwise = createDirs (take 1 (parents path0)) + where + parents = reverse . scanl1 () . splitDirectories . simplify + + createDirs [] = pure () + createDirs (dir:[]) = createDir dir ioError + createDirs (dir:dirs) = + createDir dir $ \_ -> do + createDirs dirs + createDir dir ioError + + createDir dir notExistHandler = do + r <- tryIOError (createDirectory dir) + case r of + Right () -> pure () + Left e + | isDoesNotExistError e -> notExistHandler e + -- createDirectory (and indeed POSIX mkdir) does not distinguish + -- between a dir already existing and a file already existing. So we + -- check for it here. Unfortunately there is a slight race condition + -- here, but we think it is benign. It could report an exception in + -- the case that the dir did exist but another process deletes the + -- directory and creates a file in its place before we can check + -- that the directory did indeed exist. + -- We also follow this path when we get a permissions error, as + -- trying to create "." when in the root directory on Windows + -- fails with + -- CreateDirectory ".": permission denied (Access is denied.) + -- This caused GHCi to crash when loading a module in the root + -- directory. + | isAlreadyExistsError e + || isPermissionError e -> do + canIgnore <- pathIsDirectory dir + `catchIOError` \ _ -> + pure (isAlreadyExistsError e) + unless canIgnore (ioError e) + | otherwise -> ioError e + + +{- | @'removeDirectory' dir@ removes an existing directory /dir/. The +implementation may specify additional constraints which must be +satisfied before a directory can be removed (e.g. the directory has to +be empty, or may not be in use by other processes). It is not legal +for an implementation to partially remove a directory unless the +entire directory is removed. A conformant implementation need not +support directory removal in all situations (e.g. removal of the root +directory). + +The operation may fail with: + +* @HardwareFault@ +A physical I\/O error has occurred. +@[EIO]@ + +* @InvalidArgument@ +The operand is not a valid directory name. +@[ENAMETOOLONG, ELOOP]@ + +* 'isDoesNotExistError' +The directory does not exist. +@[ENOENT, ENOTDIR]@ + +* 'isPermissionError' +The process has insufficient privileges to perform the operation. +@[EROFS, EACCES, EPERM]@ + +* @UnsatisfiedConstraints@ +Implementation-dependent constraints are not satisfied. +@[EBUSY, ENOTEMPTY, EEXIST]@ + +* @UnsupportedOperation@ +The implementation does not support removal in this situation. +@[EINVAL]@ + +* @InappropriateType@ +The operand refers to an existing non-directory object. +@[ENOTDIR]@ + +-} + +removeDirectory :: FILEPATH -> IO () +removeDirectory = removePathInternal True + +-- | @'removeDirectoryRecursive' dir@ removes an existing directory /dir/ +-- together with its contents and subdirectories. Within this directory, +-- symbolic links are removed without affecting their targets. +-- +-- On Windows, the operation fails if /dir/ is a directory symbolic link. +-- +-- This operation is reported to be flaky on Windows so retry logic may +-- be advisable. See: https://github.com/haskell/directory/pull/108 +removeDirectoryRecursive :: FILEPATH -> IO () +removeDirectoryRecursive path = + (`ioeAddLocation` "removeDirectoryRecursive") `modifyIOError` do + m <- getSymbolicLinkMetadata path + case fileTypeFromMetadata m of + Directory -> + removeContentsRecursive path + DirectoryLink -> + ioError (err `ioeSetErrorString` "is a directory symbolic link") + _ -> + ioError (err `ioeSetErrorString` "not a directory") + where err = mkIOError InappropriateType "" Nothing (Just $ decodeFilepathFuzzy path) + +-- | @removePathRecursive path@ removes an existing file or directory at +-- /path/ together with its contents and subdirectories. Symbolic links are +-- removed without affecting their the targets. +-- +-- This operation is reported to be flaky on Windows so retry logic may +-- be advisable. See: https://github.com/haskell/directory/pull/108 +removePathRecursive :: FILEPATH -> IO () +removePathRecursive path = + (`ioeAddLocation` "removePathRecursive") `modifyIOError` do + m <- getSymbolicLinkMetadata path + case fileTypeFromMetadata m of + Directory -> removeContentsRecursive path + DirectoryLink -> removeDirectory path + _ -> removeFile path + +-- | @removeContentsRecursive dir@ removes the contents of the directory +-- /dir/ recursively. Symbolic links are removed without affecting their the +-- targets. +-- +-- This operation is reported to be flaky on Windows so retry logic may +-- be advisable. See: https://github.com/haskell/directory/pull/108 +removeContentsRecursive :: FILEPATH -> IO () +removeContentsRecursive path = + (`ioeAddLocation` "removeContentsRecursive") `modifyIOError` do + cont <- listDirectory path + traverse_ removePathRecursive [path x | x <- cont] + removeDirectory path + +-- | Removes a file or directory at /path/ together with its contents and +-- subdirectories. Symbolic links are removed without affecting their +-- targets. If the path does not exist, nothing happens. +-- +-- Unlike other removal functions, this function will also attempt to delete +-- files marked as read-only or otherwise made unremovable due to permissions. +-- As a result, if the removal is incomplete, the permissions or attributes on +-- the remaining files may be altered. If there are hard links in the +-- directory, then permissions on all related hard links may be altered. +-- +-- If an entry within the directory vanishes while @removePathForcibly@ is +-- running, it is silently ignored. +-- +-- If an exception occurs while removing an entry, @removePathForcibly@ will +-- still try to remove as many entries as it can before failing with an +-- exception. The first exception that it encountered is re-thrown. +-- +-- @since 1.2.7.0 +removePathForcibly :: FILEPATH -> IO () +removePathForcibly path = + (`ioeAddLocation` "removePathForcibly") `modifyIOError` do + makeRemovable path `catchIOError` \ _ -> pure () + ignoreDoesNotExistError $ do + m <- getSymbolicLinkMetadata path + case fileTypeFromMetadata m of + DirectoryLink -> removeDirectory path + Directory -> do + names <- listDirectory path + sequenceWithIOErrors_ $ + [ removePathForcibly (path name) | name <- names ] ++ + [ removeDirectory path ] + _ -> removeFile path + where + + ignoreDoesNotExistError :: IO () -> IO () + ignoreDoesNotExistError action = + () <$ tryIOErrorType isDoesNotExistError action + + makeRemovable :: FILEPATH -> IO () + makeRemovable p = do + perms <- getPermissions p + setPermissions path perms{ readable = True + , searchable = True + , writable = True } + +{- |'removeFile' /file/ removes the directory entry for an existing file +/file/, where /file/ is not itself a directory. The +implementation may specify additional constraints which must be +satisfied before a file can be removed (e.g. the file may not be in +use by other processes). + +The operation may fail with: + +* @HardwareFault@ +A physical I\/O error has occurred. +@[EIO]@ + +* @InvalidArgument@ +The operand is not a valid file name. +@[ENAMETOOLONG, ELOOP]@ + +* 'isDoesNotExistError' +The file does not exist. +@[ENOENT, ENOTDIR]@ + +* 'isPermissionError' +The process has insufficient privileges to perform the operation. +@[EROFS, EACCES, EPERM]@ + +* @UnsatisfiedConstraints@ +Implementation-dependent constraints are not satisfied. +@[EBUSY]@ + +* @InappropriateType@ +The operand refers to an existing directory. +@[EPERM, EINVAL]@ + +-} + +removeFile :: FILEPATH -> IO () +removeFile = removePathInternal False + +{- |@'renameDirectory' old new@ changes the name of an existing +directory from /old/ to /new/. If the /new/ directory +already exists, it is atomically replaced by the /old/ directory. +If the /new/ directory is neither the /old/ directory nor an +alias of the /old/ directory, it is removed as if by +'removeDirectory'. A conformant implementation need not support +renaming directories in all situations (e.g. renaming to an existing +directory, or across different physical devices), but the constraints +must be documented. + +On Win32 platforms, @renameDirectory@ fails if the /new/ directory already +exists. + +The operation may fail with: + +* @HardwareFault@ +A physical I\/O error has occurred. +@[EIO]@ + +* @InvalidArgument@ +Either operand is not a valid directory name. +@[ENAMETOOLONG, ELOOP]@ + +* 'isDoesNotExistError' +The original directory does not exist, or there is no path to the target. +@[ENOENT, ENOTDIR]@ + +* 'isPermissionError' +The process has insufficient privileges to perform the operation. +@[EROFS, EACCES, EPERM]@ + +* 'System.IO.isFullError' +Insufficient resources are available to perform the operation. +@[EDQUOT, ENOSPC, ENOMEM, EMLINK]@ + +* @UnsatisfiedConstraints@ +Implementation-dependent constraints are not satisfied. +@[EBUSY, ENOTEMPTY, EEXIST]@ + +* @UnsupportedOperation@ +The implementation does not support renaming in this situation. +@[EINVAL, EXDEV]@ + +* @InappropriateType@ +Either path refers to an existing non-directory object. +@[ENOTDIR, EISDIR]@ + +-} + +renameDirectory :: FILEPATH -> FILEPATH -> IO () +renameDirectory opath npath = + (`ioeAddLocation` "renameDirectory") `modifyIOError` do + -- XXX this test isn't performed atomically with the following rename + isDir <- pathIsDirectory opath + when (not isDir) $ do + ioError . (`ioeSetErrorString` "not a directory") $ + (mkIOError InappropriateType "renameDirectory" Nothing (Just $ decodeFilepathFuzzy opath)) + renamePath opath npath + +{- |@'renameFile' old new@ changes the name of an existing file system +object from /old/ to /new/. If the /new/ object already exists, it is +replaced by the /old/ object. Neither path may refer to an existing +directory. A conformant implementation need not support renaming files +in all situations (e.g. renaming across different physical devices), but +the constraints must be documented. + +On Windows, this calls @MoveFileEx@ with @MOVEFILE_REPLACE_EXISTING@ set, +which is not guaranteed to be atomic +(). + +On other platforms, this operation is atomic. + +The operation may fail with: + +* @HardwareFault@ +A physical I\/O error has occurred. +@[EIO]@ + +* @InvalidArgument@ +Either operand is not a valid file name. +@[ENAMETOOLONG, ELOOP]@ + +* 'isDoesNotExistError' +The original file does not exist, or there is no path to the target. +@[ENOENT, ENOTDIR]@ + +* 'isPermissionError' +The process has insufficient privileges to perform the operation. +@[EROFS, EACCES, EPERM]@ + +* 'System.IO.isFullError' +Insufficient resources are available to perform the operation. +@[EDQUOT, ENOSPC, ENOMEM, EMLINK]@ + +* @UnsatisfiedConstraints@ +Implementation-dependent constraints are not satisfied. +@[EBUSY]@ + +* @UnsupportedOperation@ +The implementation does not support renaming in this situation. +@[EXDEV]@ + +* @InappropriateType@ +Either path refers to an existing directory. +@[ENOTDIR, EISDIR, EINVAL, EEXIST, ENOTEMPTY]@ + +-} + +renameFile :: FILEPATH -> FILEPATH -> IO () +renameFile opath npath = + (`ioeAddLocation` "renameFile") `modifyIOError` do + -- XXX the tests are not performed atomically with the rename + checkNotDir opath + renamePath opath npath + -- The underlying rename implementation can throw odd exceptions when the + -- destination is a directory. For example, Windows typically throws a + -- permission error, while POSIX systems may throw a resource busy error + -- if one of the paths refers to the current directory. In these cases, + -- we check if the destination is a directory and, if so, throw an + -- InappropriateType error. + `catchIOError` \ err -> do + checkNotDir npath + ioError err + where checkNotDir path = do + m <- tryIOError (getSymbolicLinkMetadata path) + case fileTypeIsDirectory . fileTypeFromMetadata <$> m of + Right True -> ioError . (`ioeSetErrorString` "is a directory") $ + mkIOError InappropriateType "" Nothing (Just $ decodeFilepathFuzzy path) + _ -> pure () + +-- | Rename a file or directory. If the destination path already exists, it +-- is replaced atomically. The destination path must not point to an existing +-- directory. A conformant implementation need not support renaming files in +-- all situations (e.g. renaming across different physical devices), but the +-- constraints must be documented. +-- +-- The operation may fail with: +-- +-- * @HardwareFault@ +-- A physical I\/O error has occurred. +-- @[EIO]@ +-- +-- * @InvalidArgument@ +-- Either operand is not a valid file name. +-- @[ENAMETOOLONG, ELOOP]@ +-- +-- * 'isDoesNotExistError' +-- The original file does not exist, or there is no path to the target. +-- @[ENOENT, ENOTDIR]@ +-- +-- * 'isPermissionError' +-- The process has insufficient privileges to perform the operation. +-- @[EROFS, EACCES, EPERM]@ +-- +-- * 'System.IO.isFullError' +-- Insufficient resources are available to perform the operation. +-- @[EDQUOT, ENOSPC, ENOMEM, EMLINK]@ +-- +-- * @UnsatisfiedConstraints@ +-- Implementation-dependent constraints are not satisfied. +-- @[EBUSY]@ +-- +-- * @UnsupportedOperation@ +-- The implementation does not support renaming in this situation. +-- @[EXDEV]@ +-- +-- * @InappropriateType@ +-- Either the destination path refers to an existing directory, or one of the +-- parent segments in the destination path is not a directory. +-- @[ENOTDIR, EISDIR, EINVAL, EEXIST, ENOTEMPTY]@ +-- +-- @since 1.2.7.0 +renamePath :: FILEPATH -- ^ Old path + -> FILEPATH -- ^ New path + -> IO () +renamePath opath npath = + (`ioeAddLocation` "renamePath") `modifyIOError` do + renamePathInternal opath npath + +-- | Copy a file with its permissions. If the destination file already exists, +-- it is replaced atomically. Neither path may refer to an existing +-- directory. No exceptions are thrown if the permissions could not be +-- copied. +copyFile :: FILEPATH -- ^ Source filename + -> FILEPATH -- ^ Destination filename + -> IO () +copyFile fromFPath toFPath = + (`ioeAddLocation` "copyFile") `modifyIOError` do + atomicCopyFileContents fromFPath toFPath + (ignoreIOExceptions . copyPermissions fromFPath) + +-- | Copy the contents of a source file to a destination file, replacing the +-- destination file atomically via @withReplacementFile@, resetting the +-- attributes of the destination file to the defaults. +atomicCopyFileContents :: FILEPATH -- ^ Source filename + -> FILEPATH -- ^ Destination filename + -> (FILEPATH -> IO ()) -- ^ Post-action + -> IO () +atomicCopyFileContents fromFPath toFPath postAction = + (`ioeAddLocation` "atomicCopyFileContents") `modifyIOError` do + withReplacementFile toFPath postAction $ \ hTo -> do + copyFileToHandle fromFPath hTo + +-- | A helper function useful for replacing files in an atomic manner. The +-- function creates a temporary file in the directory of the destination file, +-- opens it, performs the main action with its handle, closes it, performs the +-- post-action with its path, and finally replaces the destination file with +-- the temporary file. If an error occurs during any step of this process, +-- the temporary file is removed and the destination file remains untouched. +withReplacementFile :: FILEPATH -- ^ Destination file + -> (FILEPATH -> IO ()) -- ^ Post-action + -> (Handle -> IO a) -- ^ Main action + -> IO a +withReplacementFile path postAction action = + (`ioeAddLocation` "withReplacementFile") `modifyIOError` do + mask $ \ restore -> do + -- TODO: AFPP doesn't support openBinaryTempFile yet, + -- so we have to use this (sad) workaround + -- (on unix, converts using filesystem encoding, on windows + -- converts with UTF-16LE) + d <- toString (takeDirectory path) + (tmpFPath', hTmp) <- openBinaryTempFile d ".copyFile.tmp" + tmpFPath <- fromStringIO tmpFPath' + (`onException` ignoreIOExceptions (removeFile tmpFPath)) $ do + r <- (`onException` ignoreIOExceptions (hClose hTmp)) $ do + restore (action hTmp) + hClose hTmp + restore (postAction tmpFPath) + renameFile tmpFPath path + pure r + +-- | Copy a file with its associated metadata. If the destination file +-- already exists, it is overwritten. There is no guarantee of atomicity in +-- the replacement of the destination file. Neither path may refer to an +-- existing directory. If the source and/or destination are symbolic links, +-- the copy is performed on the targets of the links. +-- +-- On Windows, it behaves like the Win32 function +-- , +-- which copies various kinds of metadata including file attributes and +-- security resource properties. +-- +-- On Unix-like systems, permissions, access time, and modification time are +-- preserved. If possible, the owner and group are also preserved. Note that +-- the very act of copying can change the access time of the source file, +-- hence the access times of the two files may differ after the operation +-- completes. +-- +-- @since 1.2.6.0 +copyFileWithMetadata :: FILEPATH -- ^ Source file + -> FILEPATH -- ^ Destination file + -> IO () +copyFileWithMetadata src dst = + (`ioeAddLocation` "copyFileWithMetadata") `modifyIOError` + copyFileWithMetadataInternal copyPermissionsFromMetadata + copyTimesFromMetadata + src + dst + +copyTimesFromMetadata :: Metadata -> FILEPATH -> IO () +copyTimesFromMetadata st dst = do + let atime = accessTimeFromMetadata st + let mtime = modificationTimeFromMetadata st + setFileTimes dst (Just atime, Just mtime) + +-- | Make a path absolute, normalize the path, and remove as many indirections +-- from it as possible. Any trailing path separators are discarded via +-- 'dropTrailingPathSeparator'. Additionally, on Windows the letter case of +-- the path is canonicalized. +-- +-- __Note__: This function is a very big hammer. If you only need an absolute +-- path, 'makeAbsolute' is sufficient for removing dependence on the current +-- working directory. +-- +-- Indirections include the two special directories @.@ and @..@, as well as +-- any symbolic links (and junction points on Windows). The input path need +-- not point to an existing file or directory. Canonicalization is performed +-- on the longest prefix of the path that points to an existing file or +-- directory. The remaining portion of the path that does not point to an +-- existing file or directory will still be normalized, but case +-- canonicalization and indirection removal are skipped as they are impossible +-- to do on a nonexistent path. +-- +-- Most programs should not worry about the canonicity of a path. In +-- particular, despite the name, the function does not truly guarantee +-- canonicity of the returned path due to the presence of hard links, mount +-- points, etc. +-- +-- If the path points to an existing file or directory, then the output path +-- shall also point to the same file or directory, subject to the condition +-- that the relevant parts of the file system do not change while the function +-- is still running. In other words, the function is definitively not atomic. +-- The results can be utterly wrong if the portions of the path change while +-- this function is running. +-- +-- Since some indirections (symbolic links on all systems, @..@ on non-Windows +-- systems, and junction points on Windows) are dependent on the state of the +-- existing filesystem, the function can only make a conservative attempt by +-- removing such indirections from the longest prefix of the path that still +-- points to an existing file or directory. +-- +-- Note that on Windows parent directories @..@ are always fully expanded +-- before the symbolic links, as consistent with the rest of the Windows API +-- (such as @GetFullPathName@). In contrast, on POSIX systems parent +-- directories @..@ are expanded alongside symbolic links from left to right. +-- To put this more concretely: if @L@ is a symbolic link for @R/P@, then on +-- Windows @L\\..@ refers to @.@, whereas on other operating systems @L/..@ +-- refers to @R@. +-- +-- Similar to 'System.FilePath.normalise', passing an empty path is equivalent +-- to passing the current directory. +-- +-- @canonicalizePath@ can resolve at least 64 indirections in a single path, +-- more than what is supported by most operating systems. Therefore, it may +-- return the fully resolved path even though the operating system itself +-- would have long given up. +-- +-- On Windows XP or earlier systems, junction expansion is not performed due +-- to their lack of @GetFinalPathNameByHandle@. +-- +-- /Changes since 1.2.3.0:/ The function has been altered to be more robust +-- and has the same exception behavior as 'makeAbsolute'. +-- +-- /Changes since 1.3.0.0:/ The function no longer preserves the trailing path +-- separator. File symbolic links that appear in the middle of a path are +-- properly dereferenced. Case canonicalization and symbolic link expansion +-- are now performed on Windows. +-- +canonicalizePath :: FILEPATH -> IO FILEPATH +canonicalizePath = \ path -> + ((`ioeAddLocation` "canonicalizePath") . + (`ioeSetFileName` decodeFilepathFuzzy path)) `modifyIOError` do + -- simplify does more stuff, like upper-casing the drive letter + dropTrailingPathSeparator . simplify <$> + (canonicalizePathWith attemptRealpath =<< prependCurrentDirectory path) + where + + -- allow up to 64 cycles before giving up + attemptRealpath realpath = + attemptRealpathWith (64 :: Int) Nothing realpath + <=< canonicalizePathSimplify + + -- n is a counter to make sure we don't run into an infinite loop; we + -- don't try to do any cycle detection here because an adversary could DoS + -- any arbitrarily clever algorithm + attemptRealpathWith n mFallback realpath path = + case mFallback of + -- too many indirections ... giving up. + Just fallback | n <= 0 -> pure fallback + -- either mFallback == Nothing (first attempt) + -- or n > 0 (still have some attempts left) + _ -> realpathPrefix (reverse (zip prefixes suffixes)) + + where + + segments = splitDirectories path + prefixes = scanl1 () segments + suffixes = tail (scanr () (fromString "") segments) + + -- try to call realpath on the largest possible prefix + realpathPrefix candidates = + case candidates of + [] -> pure path + (prefix, suffix) : rest -> do + exist <- doesPathExist prefix + if not exist + -- never call realpath on an inaccessible path + -- (to avoid bugs in system realpath implementations) + -- try a smaller prefix instead + then realpathPrefix rest + else do + mp <- tryIOError (realpath prefix) + case mp of + -- realpath failed: try a smaller prefix instead + Left _ -> realpathPrefix rest + -- realpath succeeded: fine-tune the result + Right p -> realpathFurther (p suffix) p suffix + + -- by now we have a reasonable fallback value that we can use if we + -- run into too many indirections; the fallback value is the same + -- result that we have been returning in versions prior to 1.3.1.0 + -- (this is essentially the fix to #64) + realpathFurther fallback p suffix = + case splitDirectories suffix of + [] -> pure fallback + next : restSuffix -> do + -- see if the 'next' segment is a symlink + mTarget <- tryIOError (getSymbolicLinkTarget (p next)) + case mTarget of + Left _ -> pure fallback + Right target -> do + -- if so, dereference it and restart the whole cycle + let mFallback' = Just (fromMaybe fallback mFallback) + path' <- canonicalizePathSimplify + (p target joinPath restSuffix) + attemptRealpathWith (n - 1) mFallback' realpath path' + +-- | Convert a path into an absolute path. If the given path is relative, the +-- current directory is prepended and then the combined result is normalized. +-- If the path is already absolute, the path is simply normalized. The +-- function preserves the presence or absence of the trailing path separator +-- unless the path refers to the root directory @/@. +-- +-- If the path is already absolute, the operation never fails. Otherwise, the +-- operation may fail with the same exceptions as 'getCurrentDirectory'. +-- +-- @since 1.2.2.0 +-- +makeAbsolute :: FILEPATH -> IO FILEPATH +makeAbsolute path = + ((`ioeAddLocation` "makeAbsolute") . + (`ioeSetFileName` decodeFilepathFuzzy path)) `modifyIOError` do + matchTrailingSeparator path . simplify <$> prependCurrentDirectory path + +-- | Add or remove the trailing path separator in the second path so as to +-- match its presence in the first path. +-- +-- (internal API) +matchTrailingSeparator :: FILEPATH -> FILEPATH -> FILEPATH +matchTrailingSeparator path + | hasTrailingPathSeparator path = addTrailingPathSeparator + | otherwise = dropTrailingPathSeparator + +-- | Construct a path relative to the current directory, similar to +-- 'makeRelative'. +-- +-- The operation may fail with the same exceptions as 'getCurrentDirectory'. +makeRelativeToCurrentDirectory :: FILEPATH -> IO FILEPATH +makeRelativeToCurrentDirectory x = do + (`makeRelative` x) <$> getCurrentDirectory + +-- | Given the name or path of an executable file, 'findExecutable' searches +-- for such a file in a list of system-defined locations, which generally +-- includes @PATH@ and possibly more. The full path to the executable is +-- returned if found. For example, @(findExecutable \"ghc\")@ would normally +-- give you the path to GHC. +-- +-- The path returned by @'findExecutable' name@ corresponds to the program +-- that would be executed by +-- @@ +-- when passed the same string (as a @RawCommand@, not a @ShellCommand@), +-- provided that @name@ is not a relative path with more than one segment. +-- +-- On Windows, 'findExecutable' calls the Win32 function +-- @@, +-- which may search other places before checking the directories in the @PATH@ +-- environment variable. Where it actually searches depends on registry +-- settings, but notably includes the directory containing the current +-- executable. +-- +-- On non-Windows platforms, the behavior is equivalent to 'findFileWith' +-- using the search directories from the @PATH@ environment variable and +-- testing each file for executable permissions. Details can be found in the +-- documentation of 'findFileWith'. +findExecutable :: STRING -> IO (Maybe FILEPATH) +findExecutable binary = + listTHead + (findExecutablesLazyInternal findExecutablesInDirectoriesLazy binary) + +-- | Search for executable files in a list of system-defined locations, which +-- generally includes @PATH@ and possibly more. +-- +-- On Windows, this /only returns the first occurrence/, if any. Its behavior +-- is therefore equivalent to 'findExecutable'. +-- +-- On non-Windows platforms, the behavior is equivalent to +-- 'findExecutablesInDirectories' using the search directories from the @PATH@ +-- environment variable. Details can be found in the documentation of +-- 'findExecutablesInDirectories'. +-- +-- @since 1.2.2.0 +findExecutables :: STRING -> IO [FILEPATH] +findExecutables binary = + listTToList + (findExecutablesLazyInternal findExecutablesInDirectoriesLazy binary) + +-- | Given a name or path, 'findExecutable' appends the 'exeExtension' to the +-- query and searches for executable files in the list of given search +-- directories and returns all occurrences. +-- +-- The behavior is equivalent to 'findFileWith' using the given search +-- directories and testing each file for executable permissions. Details can +-- be found in the documentation of 'findFileWith'. +-- +-- Unlike other similarly named functions, 'findExecutablesInDirectories' does +-- not use @SearchPath@ from the Win32 API. The behavior of this function on +-- Windows is therefore equivalent to those on non-Windows platforms. +-- +-- @since 1.2.4.0 +findExecutablesInDirectories :: [FILEPATH] -> STRING -> IO [FILEPATH] +findExecutablesInDirectories path binary = + listTToList (findExecutablesInDirectoriesLazy path binary) + +findExecutablesInDirectoriesLazy :: [FILEPATH] -> STRING -> ListT IO FILEPATH +findExecutablesInDirectoriesLazy path binary = + findFilesWithLazy isExecutable path (binary <.> exeExtension) + +-- | Test whether a file has executable permissions. +isExecutable :: FILEPATH -> IO Bool +isExecutable file = executable <$> getPermissions file + +-- | Search through the given list of directories for the given file. +-- +-- The behavior is equivalent to 'findFileWith', returning only the first +-- occurrence. Details can be found in the documentation of 'findFileWith'. +findFile :: [FILEPATH] -> STRING -> IO (Maybe FILEPATH) +findFile = findFileWith (\ _ -> pure True) + +-- | Search through the given list of directories for the given file and +-- returns all paths where the given file exists. +-- +-- The behavior is equivalent to 'findFilesWith'. Details can be found in the +-- documentation of 'findFilesWith'. +-- +-- @since 1.2.1.0 +findFiles :: [FILEPATH] -> STRING -> IO [FILEPATH] +findFiles = findFilesWith (\ _ -> pure True) + +-- | Search through a given list of directories for a file that has the given +-- name and satisfies the given predicate and return the path of the first +-- occurrence. The directories are checked in a left-to-right order. +-- +-- This is essentially a more performant version of 'findFilesWith' that +-- always returns the first result, if any. Details can be found in the +-- documentation of 'findFilesWith'. +-- +-- @since 1.2.6.0 +findFileWith :: (FILEPATH -> IO Bool) -> [FILEPATH] -> STRING -> IO (Maybe FILEPATH) +findFileWith f ds name = listTHead (findFilesWithLazy f ds name) + +-- | @findFilesWith predicate dirs name@ searches through the list of +-- directories (@dirs@) for files that have the given @name@ and satisfy the +-- given @predicate@ and returns the paths of those files. The directories +-- are checked in a left-to-right order and the paths are returned in the same +-- order. +-- +-- If the @name@ is a relative path, then for every search directory @dir@, +-- the function checks whether @dir '' name@ exists and satisfies the +-- predicate. If so, @dir '' name@ is returned as one of the results. In +-- other words, the returned paths can be either relative or absolute +-- depending on the search directories were used. If there are no search +-- directories, no results are ever returned. +-- +-- If the @name@ is an absolute path, then the function will return a single +-- result if the file exists and satisfies the predicate and no results +-- otherwise. This is irrespective of what search directories were given. +-- +-- @since 1.2.1.0 +findFilesWith :: (FILEPATH -> IO Bool) -> [FILEPATH] -> STRING -> IO [FILEPATH] +findFilesWith f ds name = listTToList (findFilesWithLazy f ds name) + +findFilesWithLazy + :: (FILEPATH -> IO Bool) -> [FILEPATH] -> STRING -> ListT IO FILEPATH +findFilesWithLazy f dirs path + -- make sure absolute paths are handled properly irrespective of 'dirs' + -- https://github.com/haskell/directory/issues/72 + | isAbsolute path = ListT (find [fromString ""]) + | otherwise = ListT (find dirs) + + where + + find [] = pure Nothing + find (d : ds) = do + let p = d path + found <- doesFileExist p `andM` f p + if found + then pure (Just (p, ListT (find ds))) + else find ds + +-- | Filename extension for executable files (including the dot if any) +-- (usually @\"\"@ on POSIX systems and @\".exe\"@ on Windows or OS\/2). +-- +-- @since 1.2.4.0 +exeExtension :: STRING +exeExtension = exeExtensionInternal + +-- | Similar to 'listDirectory', but always includes the special entries (@.@ +-- and @..@). (This applies to Windows as well.) +-- +-- The operation may fail with the same exceptions as 'listDirectory'. +getDirectoryContents :: FILEPATH -> IO [FILEPATH] +getDirectoryContents path = + ((`ioeSetFileName` decodeFilepathFuzzy path) . + (`ioeAddLocation` "getDirectoryContents")) `modifyIOError` do + getDirectoryContentsInternal path + +-- | @'listDirectory' dir@ returns a list of /all/ entries in /dir/ without +-- the special entries (@.@ and @..@). +-- +-- The operation may fail with: +-- +-- * @HardwareFault@ +-- A physical I\/O error has occurred. +-- @[EIO]@ +-- +-- * @InvalidArgument@ +-- The operand is not a valid directory name. +-- @[ENAMETOOLONG, ELOOP]@ +-- +-- * 'isDoesNotExistError' +-- The directory does not exist. +-- @[ENOENT, ENOTDIR]@ +-- +-- * 'isPermissionError' +-- The process has insufficient privileges to perform the operation. +-- @[EACCES]@ +-- +-- * 'System.IO.isFullError' +-- Insufficient resources are available to perform the operation. +-- @[EMFILE, ENFILE]@ +-- +-- * @InappropriateType@ +-- The path refers to an existing non-directory object. +-- @[ENOTDIR]@ +-- +-- @since 1.2.5.0 +-- +listDirectory :: FILEPATH -> IO [FILEPATH] +listDirectory path = filter f <$> getDirectoryContents path + where f filename = filename /= fromString "." && filename /= fromString ".." + +-- | Obtain the current working directory as an absolute path. +-- +-- In a multithreaded program, the current working directory is a global state +-- shared among all threads of the process. Therefore, when performing +-- filesystem operations from multiple threads, it is highly recommended to +-- use absolute rather than relative paths (see: 'makeAbsolute'). +-- +-- Note that 'getCurrentDirectory' is not guaranteed to return the same path +-- received by 'setCurrentDirectory'. On POSIX systems, the path returned will +-- always be fully dereferenced (not contain any symbolic links). For more +-- information, refer to the documentation of +-- . +-- +-- The operation may fail with: +-- +-- * @HardwareFault@ +-- A physical I\/O error has occurred. +-- @[EIO]@ +-- +-- * 'isDoesNotExistError' +-- There is no path referring to the working directory. +-- @[EPERM, ENOENT, ESTALE...]@ +-- +-- * 'isPermissionError' +-- The process has insufficient privileges to perform the operation. +-- @[EACCES]@ +-- +-- * 'System.IO.isFullError' +-- Insufficient resources are available to perform the operation. +-- +-- * @UnsupportedOperation@ +-- The operating system has no notion of current working directory. +-- +getCurrentDirectory :: IO FILEPATH +getCurrentDirectory = + (`ioeAddLocation` "getCurrentDirectory") `modifyIOError` do + specializeErrorString + "Current working directory no longer exists" + isDoesNotExistError + getCurrentDirectoryInternal + +-- | Change the working directory to the given path. +-- +-- In a multithreaded program, the current working directory is a global state +-- shared among all threads of the process. Therefore, when performing +-- filesystem operations from multiple threads, it is highly recommended to +-- use absolute rather than relative paths (see: 'makeAbsolute'). +-- +-- The operation may fail with: +-- +-- * @HardwareFault@ +-- A physical I\/O error has occurred. +-- @[EIO]@ +-- +-- * @InvalidArgument@ +-- The operand is not a valid directory name. +-- @[ENAMETOOLONG, ELOOP]@ +-- +-- * 'isDoesNotExistError' +-- The directory does not exist. +-- @[ENOENT, ENOTDIR]@ +-- +-- * 'isPermissionError' +-- The process has insufficient privileges to perform the operation. +-- @[EACCES]@ +-- +-- * @UnsupportedOperation@ +-- The operating system has no notion of current working directory, or the +-- working directory cannot be dynamically changed. +-- +-- * @InappropriateType@ +-- The path refers to an existing non-directory object. +-- @[ENOTDIR]@ +-- +setCurrentDirectory :: FILEPATH -> IO () +setCurrentDirectory = setCurrentDirectoryInternal + +-- | Run an 'IO' action with the given working directory and restore the +-- original working directory afterwards, even if the given action fails due +-- to an exception. +-- +-- The operation may fail with the same exceptions as 'getCurrentDirectory' +-- and 'setCurrentDirectory'. +-- +-- @since 1.2.3.0 +-- +withCurrentDirectory :: FILEPATH -- ^ Directory to execute in + -> IO a -- ^ Action to be executed + -> IO a +withCurrentDirectory dir action = + bracket getCurrentDirectory setCurrentDirectory $ \ _ -> do + setCurrentDirectory dir + action + +-- | Obtain the size of a file in bytes. +-- +-- @since 1.2.7.0 +getFileSize :: FILEPATH -> IO Integer +getFileSize path = + (`ioeAddLocation` "getFileSize") `modifyIOError` do + fileSizeFromMetadata <$> getFileMetadata path + +-- | Test whether the given path points to an existing filesystem object. If +-- the user lacks necessary permissions to search the parent directories, this +-- function may return false even if the file does actually exist. +-- +-- @since 1.2.7.0 +doesPathExist :: FILEPATH -> IO Bool +doesPathExist path = do + (True <$ getFileMetadata path) + `catchIOError` \ _ -> + pure False + +{- |The operation 'doesDirectoryExist' returns 'True' if the argument file +exists and is either a directory or a symbolic link to a directory, +and 'False' otherwise. +-} + +doesDirectoryExist :: FILEPATH -> IO Bool +doesDirectoryExist path = do + pathIsDirectory path + `catchIOError` \ _ -> + pure False + +{- |The operation 'doesFileExist' returns 'True' +if the argument file exists and is not a directory, and 'False' otherwise. +-} + +doesFileExist :: FILEPATH -> IO Bool +doesFileExist path = do + (not <$> pathIsDirectory path) + `catchIOError` \ _ -> + pure False + +pathIsDirectory :: FILEPATH -> IO Bool +pathIsDirectory path = + (`ioeAddLocation` "pathIsDirectory") `modifyIOError` do + fileTypeIsDirectory . fileTypeFromMetadata <$> getFileMetadata path + +-- | Create a /file/ symbolic link. The target path can be either absolute or +-- relative and need not refer to an existing file. The order of arguments +-- follows the POSIX convention. +-- +-- To remove an existing file symbolic link, use 'removeFile'. +-- +-- Although the distinction between /file/ symbolic links and /directory/ +-- symbolic links does not exist on POSIX systems, on Windows this is an +-- intrinsic property of every symbolic link and cannot be changed without +-- recreating the link. A file symbolic link that actually points to a +-- directory will fail to dereference and vice versa. Moreover, creating +-- symbolic links on Windows may require privileges unavailable to users +-- outside the Administrators group. Portable programs that use symbolic +-- links should take both into consideration. +-- +-- On Windows, the function is implemented using @CreateSymbolicLink@. Since +-- 1.3.3.0, the @SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE@ flag is included +-- if supported by the operating system. On POSIX, the function uses @symlink@ +-- and is therefore atomic. +-- +-- Windows-specific errors: This operation may fail with 'permissionErrorType' +-- if the user lacks the privileges to create symbolic links. It may also +-- fail with 'illegalOperationErrorType' if the file system does not support +-- symbolic links. +-- +-- @since 1.3.1.0 +createFileLink + :: FILEPATH -- ^ path to the target file + -> FILEPATH -- ^ path of the link to be created + -> IO () +createFileLink target link = + (`ioeAddLocation` "createFileLink") `modifyIOError` do + createSymbolicLink False target link + +-- | Create a /directory/ symbolic link. The target path can be either +-- absolute or relative and need not refer to an existing directory. The +-- order of arguments follows the POSIX convention. +-- +-- To remove an existing directory symbolic link, use 'removeDirectoryLink'. +-- +-- Although the distinction between /file/ symbolic links and /directory/ +-- symbolic links does not exist on POSIX systems, on Windows this is an +-- intrinsic property of every symbolic link and cannot be changed without +-- recreating the link. A file symbolic link that actually points to a +-- directory will fail to dereference and vice versa. Moreover, creating +-- symbolic links on Windows may require privileges unavailable to users +-- outside the Administrators group. Portable programs that use symbolic +-- links should take both into consideration. +-- +-- On Windows, the function is implemented using @CreateSymbolicLink@ with +-- @SYMBOLIC_LINK_FLAG_DIRECTORY@. Since 1.3.3.0, the +-- @SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE@ flag is also included if +-- supported by the operating system. On POSIX, this is an alias for +-- 'createFileLink' and is therefore atomic. +-- +-- Windows-specific errors: This operation may fail with 'permissionErrorType' +-- if the user lacks the privileges to create symbolic links. It may also +-- fail with 'illegalOperationErrorType' if the file system does not support +-- symbolic links. +-- +-- @since 1.3.1.0 +createDirectoryLink + :: FILEPATH -- ^ path to the target directory + -> FILEPATH -- ^ path of the link to be created + -> IO () +createDirectoryLink target link = + (`ioeAddLocation` "createDirectoryLink") `modifyIOError` do + createSymbolicLink True target link + +-- | Remove an existing /directory/ symbolic link. +-- +-- On Windows, this is an alias for 'removeDirectory'. On POSIX systems, this +-- is an alias for 'removeFile'. +-- +-- See also: 'removeFile', which can remove an existing /file/ symbolic link. +-- +-- @since 1.3.1.0 +removeDirectoryLink :: FILEPATH -> IO () +removeDirectoryLink path = + (`ioeAddLocation` "removeDirectoryLink") `modifyIOError` do + removePathInternal linkToDirectoryIsDirectory path + +-- | Check whether an existing @path@ is a symbolic link. If @path@ is a +-- regular file or directory, 'False' is returned. If @path@ does not exist +-- or is otherwise inaccessible, an exception is thrown (see below). +-- +-- On Windows, this checks for @FILE_ATTRIBUTE_REPARSE_POINT@. In addition to +-- symbolic links, the function also returns true on junction points. On +-- POSIX systems, this checks for @S_IFLNK@. +-- +-- The operation may fail with: +-- +-- * 'isDoesNotExistError' if the symbolic link does not exist; or +-- +-- * 'isPermissionError' if the user is not permitted to read the symbolic +-- link. +-- +-- @since 1.3.0.0 +pathIsSymbolicLink :: FILEPATH -> IO Bool +pathIsSymbolicLink path = + ((`ioeAddLocation` "pathIsSymbolicLink") . + (`ioeSetFileName` decodeFilepathFuzzy path)) `modifyIOError` do + fileTypeIsLink . fileTypeFromMetadata <$> getSymbolicLinkMetadata path + +{-# DEPRECATED isSymbolicLink "Use 'pathIsSymbolicLink' instead" #-} +isSymbolicLink :: FILEPATH -> IO Bool +isSymbolicLink = pathIsSymbolicLink + +-- | Retrieve the target path of either a file or directory symbolic link. +-- The returned path may not be absolute, may not exist, and may not even be a +-- valid path. +-- +-- On Windows systems, this calls @DeviceIoControl@ with +-- @FSCTL_GET_REPARSE_POINT@. In addition to symbolic links, the function +-- also works on junction points. On POSIX systems, this calls @readlink@. +-- +-- Windows-specific errors: This operation may fail with +-- 'illegalOperationErrorType' if the file system does not support symbolic +-- links. +-- +-- @since 1.3.1.0 +getSymbolicLinkTarget :: FILEPATH -> IO FILEPATH +getSymbolicLinkTarget path = + (`ioeAddLocation` "getSymbolicLinkTarget") `modifyIOError` do + readSymbolicLink path + +-- | Obtain the time at which the file or directory was last accessed. +-- +-- The operation may fail with: +-- +-- * 'isPermissionError' if the user is not permitted to read +-- the access time; or +-- +-- * 'isDoesNotExistError' if the file or directory does not exist. +-- +-- Caveat for POSIX systems: This function returns a timestamp with sub-second +-- resolution only if this package is compiled against @unix-2.6.0.0@ or later +-- and the underlying filesystem supports them. +-- +-- @since 1.2.3.0 +-- +getAccessTime :: FILEPATH -> IO UTCTime +getAccessTime path = + (`ioeAddLocation` "getAccessTime") `modifyIOError` do + accessTimeFromMetadata <$> getFileMetadata (emptyToCurDir path) + +-- | Obtain the time at which the file or directory was last modified. +-- +-- The operation may fail with: +-- +-- * 'isPermissionError' if the user is not permitted to read +-- the modification time; or +-- +-- * 'isDoesNotExistError' if the file or directory does not exist. +-- +-- Caveat for POSIX systems: This function returns a timestamp with sub-second +-- resolution only if this package is compiled against @unix-2.6.0.0@ or later +-- and the underlying filesystem supports them. +-- +getModificationTime :: FILEPATH -> IO UTCTime +getModificationTime path = + (`ioeAddLocation` "getModificationTime") `modifyIOError` do + modificationTimeFromMetadata <$> getFileMetadata (emptyToCurDir path) + +-- | Change the time at which the file or directory was last accessed. +-- +-- The operation may fail with: +-- +-- * 'isPermissionError' if the user is not permitted to alter the +-- access time; or +-- +-- * 'isDoesNotExistError' if the file or directory does not exist. +-- +-- Some caveats for POSIX systems: +-- +-- * Not all systems support @utimensat@, in which case the function can only +-- emulate the behavior by reading the modification time and then setting +-- both the access and modification times together. On systems where +-- @utimensat@ is supported, the access time is set atomically with +-- nanosecond precision. +-- +-- * If compiled against a version of @unix@ prior to @2.7.0.0@, the function +-- would not be able to set timestamps with sub-second resolution. In this +-- case, there would also be loss of precision in the modification time. +-- +-- @since 1.2.3.0 +-- +setAccessTime :: FILEPATH -> UTCTime -> IO () +setAccessTime path atime = + (`ioeAddLocation` "setAccessTime") `modifyIOError` do + setFileTimes path (Just atime, Nothing) + +-- | Change the time at which the file or directory was last modified. +-- +-- The operation may fail with: +-- +-- * 'isPermissionError' if the user is not permitted to alter the +-- modification time; or +-- +-- * 'isDoesNotExistError' if the file or directory does not exist. +-- +-- Some caveats for POSIX systems: +-- +-- * Not all systems support @utimensat@, in which case the function can only +-- emulate the behavior by reading the access time and then setting both the +-- access and modification times together. On systems where @utimensat@ is +-- supported, the modification time is set atomically with nanosecond +-- precision. +-- +-- * If compiled against a version of @unix@ prior to @2.7.0.0@, the function +-- would not be able to set timestamps with sub-second resolution. In this +-- case, there would also be loss of precision in the access time. +-- +-- @since 1.2.3.0 +-- +setModificationTime :: FILEPATH -> UTCTime -> IO () +setModificationTime path mtime = + (`ioeAddLocation` "setModificationTime") `modifyIOError` do + setFileTimes path (Nothing, Just mtime) + +setFileTimes :: FILEPATH -> (Maybe UTCTime, Maybe UTCTime) -> IO () +setFileTimes _ (Nothing, Nothing) = return () +setFileTimes path (atime, mtime) = + ((`ioeAddLocation` "setFileTimes") . + (`ioeSetFileName` decodeFilepathFuzzy path)) `modifyIOError` do + setTimes (emptyToCurDir path) + (utcTimeToPOSIXSeconds <$> atime, utcTimeToPOSIXSeconds <$> mtime) + +{- | Returns the current user's home directory. + +The directory returned is expected to be writable by the current user, +but note that it isn't generally considered good practice to store +application-specific data here; use 'getXdgDirectory' or +'getAppUserDataDirectory' instead. + +On Unix, 'getHomeDirectory' behaves as follows: + +* Returns $HOME env variable if set (including to an empty string). +* Otherwise uses home directory returned by `getpwuid_r` using the UID of the current proccesses user. This basically reads the /etc/passwd file. An empty home directory field is considered valid. + +On Windows, the system is queried for a suitable path; a typical path might be @C:\/Users\//\/@. + +The operation may fail with: + +* @UnsupportedOperation@ +The operating system has no notion of home directory. + +* 'isDoesNotExistError' +The home directory for the current user does not exist, or +cannot be found. +-} +getHomeDirectory :: IO FILEPATH +getHomeDirectory = + (`ioeAddLocation` "getHomeDirectory") `modifyIOError` do + getHomeDirectoryInternal + +-- | Obtain the paths to special directories for storing user-specific +-- application data, configuration, and cache files, conforming to the +-- . +-- Compared with 'getAppUserDataDirectory', this function provides a more +-- fine-grained hierarchy as well as greater flexibility for the user. +-- +-- On Windows, 'XdgData' and 'XdgConfig' usually map to the same directory +-- unless overridden. +-- +-- Refer to the docs of 'XdgDirectory' for more details. +-- +-- The second argument is usually the name of the application. Since it +-- will be integrated into the path, it must consist of valid path +-- characters. Note: if the second argument is an absolute path, it will +-- just return the second argument. +-- +-- Note: The directory may not actually exist, in which case you would need +-- to create it with file mode @700@ (i.e. only accessible by the owner). +-- +-- As of 1.3.5.0, the environment variable is ignored if set to a relative +-- path, per revised XDG Base Directory Specification. See +-- . +-- +-- @since 1.2.3.0 +getXdgDirectory :: XdgDirectory -- ^ which special directory + -> FILEPATH -- ^ a relative path that is appended + -- to the path; if empty, the base + -- path is returned + -> IO FILEPATH +getXdgDirectory xdgDir suffix = + (`ioeAddLocation` "getXdgDirectory") `modifyIOError` do + simplify . ( suffix) <$> do + env <- lookupEnv $ case xdgDir of + XdgData -> fromString "XDG_DATA_HOME" + XdgConfig -> fromString "XDG_CONFIG_HOME" + XdgCache -> fromString "XDG_CACHE_HOME" + XdgState -> fromString "XDG_STATE_HOME" + case env of + Just path | isAbsolute path -> pure path + _ -> getXdgDirectoryFallback getHomeDirectory xdgDir + +-- | Similar to 'getXdgDirectory' but retrieves the entire list of XDG +-- directories. +-- +-- On Windows, 'XdgDataDirs' and 'XdgConfigDirs' usually map to the same list +-- of directories unless overridden. +-- +-- Refer to the docs of 'XdgDirectoryList' for more details. +getXdgDirectoryList :: XdgDirectoryList -- ^ which special directory list + -> IO [FILEPATH] +getXdgDirectoryList xdgDirs = + (`ioeAddLocation` "getXdgDirectoryList") `modifyIOError` do + env <- lookupEnv $ case xdgDirs of + XdgDataDirs -> fromString "XDG_DATA_DIRS" + XdgConfigDirs -> fromString "XDG_CONFIG_DIRS" + case env of + Nothing -> getXdgDirectoryListFallback xdgDirs + Just paths -> pure (splitSearchPath paths) + +-- | Obtain the path to a special directory for storing user-specific +-- application data (traditional Unix location). Newer applications may +-- prefer the the XDG-conformant location provided by 'getXdgDirectory' +-- (). +-- +-- The argument is usually the name of the application. Since it will be +-- integrated into the path, it must consist of valid path characters. +-- +-- * On Unix-like systems, the path is @~\/./\/@. +-- * On Windows, the path is @%APPDATA%\//\/@ +-- (e.g. @C:\/Users\//\/\/AppData\/Roaming\//\/@) +-- +-- Note: the directory may not actually exist, in which case you would need +-- to create it. It is expected that the parent directory exists and is +-- writable. +-- +-- The operation may fail with: +-- +-- * @UnsupportedOperation@ +-- The operating system has no notion of application-specific data +-- directory. +-- +-- * 'isDoesNotExistError' +-- The home directory for the current user does not exist, or cannot be +-- found. +-- +getAppUserDataDirectory :: FILEPATH -- ^ a relative path that is appended + -- to the path + -> IO FILEPATH +getAppUserDataDirectory appName = do + (`ioeAddLocation` "getAppUserDataDirectory") `modifyIOError` do + getAppUserDataDirectoryInternal appName + +{- | Returns the current user's document directory. + +The directory returned is expected to be writable by the current user, +but note that it isn't generally considered good practice to store +application-specific data here; use 'getXdgDirectory' or +'getAppUserDataDirectory' instead. + +On Unix, 'getUserDocumentsDirectory' returns the value of the @HOME@ +environment variable. On Windows, the system is queried for a +suitable path; a typical path might be @C:\/Users\//\/\/Documents@. + +The operation may fail with: + +* @UnsupportedOperation@ +The operating system has no notion of document directory. + +* 'isDoesNotExistError' +The document directory for the current user does not exist, or +cannot be found. +-} +getUserDocumentsDirectory :: IO FILEPATH +getUserDocumentsDirectory = do + (`ioeAddLocation` "getUserDocumentsDirectory") `modifyIOError` do + getUserDocumentsDirectoryInternal + +{- | Returns the current directory for temporary files. + +On Unix, 'getTemporaryDirectory' returns the value of the @TMPDIR@ +environment variable or \"\/tmp\" if the variable isn\'t defined. +On Windows, the function checks for the existence of environment variables in +the following order and uses the first path found: + +* +TMP environment variable. + +* +TEMP environment variable. + +* +USERPROFILE environment variable. + +* +The Windows directory + +The operation may fail with: + +* @UnsupportedOperation@ +The operating system has no notion of temporary directory. + +The function doesn\'t verify whether the path exists. +-} +getTemporaryDirectory :: IO FILEPATH +getTemporaryDirectory = getTemporaryDirectoryInternal diff --git a/System/File/AbstractFilePath.hs b/System/File/AbstractFilePath.hs new file mode 100644 index 00000000..8a0c4af5 --- /dev/null +++ b/System/File/AbstractFilePath.hs @@ -0,0 +1,30 @@ +{-# LANGUAGE CPP #-} + +module System.File.AbstractFilePath where + +#if defined(mingw32_HOST_OS) || defined(__MINGW32__) +#define CTOR WS +import qualified System.File.Windows as P +#else +#define CTOR PS +import qualified System.File.Posix as P +#endif + +import Control.Exception (bracket) +import System.IO (IOMode(..), Handle, hSetBinaryMode, hClose) +import System.AbstractFilePath +import System.OsString.Internal.Types + +import qualified Data.ByteString as BS +import qualified Data.ByteString.Lazy as BSL + +#define FILE_PATH AbstractFilePath +#include "Common.hs" + +-- | Open a file and return the 'Handle'. +openFile :: AbstractFilePath -> IOMode -> IO Handle +openFile (OsString fp) = P.openFile fp + +-- | Open an existing file and return the 'Handle'. +openExistingFile :: AbstractFilePath -> IOMode -> IO Handle +openExistingFile (OsString fp) = P.openExistingFile fp diff --git a/System/File/Common.hs b/System/File/Common.hs new file mode 100644 index 00000000..6b591180 --- /dev/null +++ b/System/File/Common.hs @@ -0,0 +1,82 @@ + +-- | Like 'openFile', but open the file in binary mode. +-- On Windows, reading a file in text mode (which is the default) +-- will translate CRLF to LF, and writing will translate LF to CRLF. +-- This is usually what you want with text files. With binary files +-- this is undesirable; also, as usual under Microsoft operating systems, +-- text mode treats control-Z as EOF. Binary mode turns off all special +-- treatment of end-of-line and end-of-file characters. +-- (See also 'System.IO.hSetBinaryMode'.) + +-- On POSIX systems, 'openBinaryFile' is an /interruptible operation/ as +-- described in "Control.Exception". +openBinaryFile :: FILE_PATH -> IOMode -> IO Handle +openBinaryFile fp iomode = do + h <- openFile fp iomode + hSetBinaryMode h True + pure h + +-- | Run an action on a file. +-- +-- The 'Handle' is automatically closed afther the action. +withFile :: FILE_PATH -> IOMode -> (Handle -> IO r) -> IO r +withFile fp iomode action = bracket + (openFile fp iomode) + hClose + action + +withBinaryFile :: FILE_PATH -> IOMode -> (Handle -> IO r) -> IO r +withBinaryFile fp iomode action = bracket + (openBinaryFile fp iomode) + hClose + action + +-- | Run an action on a file. +-- +-- The 'Handle' is not automatically closed to allow lazy IO. Use this +-- with caution. +withFile' + :: FILE_PATH -> IOMode -> (Handle -> IO r) -> IO r +withFile' fp iomode action = do + h <- openFile fp iomode + action h + +withBinaryFile' + :: FILE_PATH -> IOMode -> (Handle -> IO r) -> IO r +withBinaryFile' fp iomode action = do + h <- openBinaryFile fp iomode + action h + +-- | The 'readFile' function reads a file and returns the contents of the file +-- as a 'ByteString'. The file is read lazily, on demand. +readFile :: FILE_PATH -> IO BSL.ByteString +readFile fp = withFile' fp ReadMode BSL.hGetContents + +-- | The 'readFile'' function reads a file and returns the contents of the file +-- as a 'ByteString'. The file is fully read before being returned. +readFile' + :: FILE_PATH -> IO BS.ByteString +readFile' fp = withFile fp ReadMode BS.hGetContents + +-- | The computation 'writeFile' @file str@ function writes the lazy 'ByteString' @str@, +-- to the file @file@. +writeFile :: FILE_PATH -> BSL.ByteString -> IO () +writeFile fp contents = withFile fp WriteMode (`BSL.hPut` contents) + +-- | The computation 'writeFile' @file str@ function writes the strict 'ByteString' @str@, +-- to the file @file@. +writeFile' + :: FILE_PATH -> BS.ByteString -> IO () +writeFile' fp contents = withFile fp WriteMode (`BS.hPut` contents) + +-- | The computation 'appendFile' @file str@ function appends the lazy 'ByteString' @str@, +-- to the file @file@. +appendFile :: FILE_PATH -> BSL.ByteString -> IO () +appendFile fp contents = withFile fp AppendMode (`BSL.hPut` contents) + +-- | The computation 'appendFile' @file str@ function appends the strict 'ByteString' @str@, +-- to the file @file@. +appendFile' + :: FILE_PATH -> BS.ByteString -> IO () +appendFile' fp contents = withFile fp AppendMode (`BS.hPut` contents) + diff --git a/System/File/PlatformFilePath.hs b/System/File/PlatformFilePath.hs new file mode 100644 index 00000000..12501553 --- /dev/null +++ b/System/File/PlatformFilePath.hs @@ -0,0 +1,28 @@ +{-# LANGUAGE CPP #-} + +module System.File.PlatformFilePath where + +#if defined(mingw32_HOST_OS) || defined(__MINGW32__) +import qualified System.File.Windows as P +#else +import qualified System.File.Posix as P +#endif + +import Control.Exception (bracket) +import System.IO (IOMode(..), Handle, hSetBinaryMode, hClose) +import System.AbstractFilePath.Types + +import qualified Data.ByteString as BS +import qualified Data.ByteString.Lazy as BSL + +#define FILE_PATH PlatformFilePath +#include "Common.hs" + +-- | Open a file and return the 'Handle'. +openFile :: PlatformFilePath -> IOMode -> IO Handle +openFile fp = P.openFile fp + +-- | Open an existing file and return the 'Handle'. +openExistingFile :: PlatformFilePath -> IOMode -> IO Handle +openExistingFile fp = P.openExistingFile fp + diff --git a/System/File/Posix.hs b/System/File/Posix.hs new file mode 100644 index 00000000..a2b9864e --- /dev/null +++ b/System/File/Posix.hs @@ -0,0 +1,32 @@ +module System.File.Posix where + +import System.IO (IOMode(..), Handle) +import System.Posix.IO.PosixString + ( defaultFileFlags, + fdToHandle, + openFd, + OpenFileFlags(noctty, nonBlock, creat, append, trunc), + OpenMode(ReadWrite, ReadOnly, WriteOnly) ) +import System.AbstractFilePath.Posix ( PosixFilePath ) + +-- | Open a file and return the 'Handle'. +openFile :: PosixFilePath -> IOMode -> IO Handle +openFile fp iomode = fdToHandle =<< case iomode of + ReadMode -> open ReadOnly df + WriteMode -> open WriteOnly df { trunc = True } + AppendMode -> open WriteOnly df { append = True } + ReadWriteMode -> open ReadWrite df + where + open = openFd fp + df = defaultFileFlags { noctty = True, nonBlock = True, creat = Just 0o666 } + +-- | Open an existing file and return the 'Handle'. +openExistingFile :: PosixFilePath -> IOMode -> IO Handle +openExistingFile fp iomode = fdToHandle =<< case iomode of + ReadMode -> open ReadOnly df + WriteMode -> open WriteOnly df { trunc = True } + AppendMode -> open WriteOnly df { append = True } + ReadWriteMode -> open ReadWrite df + where + open = openFd fp + df = defaultFileFlags { noctty = True, nonBlock = True, creat = Nothing } diff --git a/System/File/Windows.hs b/System/File/Windows.hs new file mode 100644 index 00000000..f865db18 --- /dev/null +++ b/System/File/Windows.hs @@ -0,0 +1,111 @@ +{-# LANGUAGE CPP #-} + +module System.File.Windows where + +import Control.Exception (bracketOnError) +import Data.Bits +import System.IO (IOMode(..), Handle) +import System.AbstractFilePath.Windows ( WindowsFilePath ) + +import qualified System.Win32 as Win32 +import qualified System.Win32.WindowsString.File as WS +import Control.Monad (when, void) +#if defined(__IO_MANAGER_WINIO__) +import GHC.IO.SubSystem +#endif + +-- | Open a file and return the 'Handle'. +openFile :: WindowsFilePath -> IOMode -> IO Handle +openFile fp iomode = bracketOnError + (WS.createFile + fp + accessMode + shareMode + Nothing + createMode +#if defined(__IO_MANAGER_WINIO__) + (case ioSubSystem of + IoPOSIX -> Win32.fILE_ATTRIBUTE_NORMAL + IoNative -> Win32.fILE_ATTRIBUTE_NORMAL .|. Win32.fILE_FLAG_OVERLAPPED + ) +#else + Win32.fILE_ATTRIBUTE_NORMAL +#endif + Nothing) + Win32.closeHandle + toHandle + where + toHandle h = do + when (iomode == AppendMode ) $ void $ Win32.setFilePointerEx h 0 Win32.fILE_END + Win32.hANDLEToHandle h + accessMode = case iomode of + ReadMode -> Win32.gENERIC_READ + WriteMode -> Win32.gENERIC_WRITE + AppendMode -> Win32.gENERIC_WRITE .|. Win32.fILE_APPEND_DATA + ReadWriteMode -> Win32.gENERIC_READ .|. Win32.gENERIC_WRITE + + createMode = case iomode of + ReadMode -> Win32.oPEN_ALWAYS + WriteMode -> Win32.cREATE_ALWAYS + AppendMode -> Win32.oPEN_ALWAYS + ReadWriteMode -> Win32.oPEN_ALWAYS + + shareMode = case iomode of + ReadMode -> maxShareMode + WriteMode -> writeShareMode + AppendMode -> writeShareMode + ReadWriteMode -> maxShareMode + +maxShareMode :: Win32.ShareMode +maxShareMode = + Win32.fILE_SHARE_DELETE .|. + Win32.fILE_SHARE_READ .|. + Win32.fILE_SHARE_WRITE + +writeShareMode :: Win32.ShareMode +writeShareMode = + Win32.fILE_SHARE_DELETE .|. + Win32.fILE_SHARE_READ + + -- | Open an existing file and return the 'Handle'. +openExistingFile :: WindowsFilePath -> IOMode -> IO Handle +openExistingFile fp iomode = bracketOnError + (WS.createFile + fp + accessMode + shareMode + Nothing + createMode +#if defined(__IO_MANAGER_WINIO__) + (case ioSubSystem of + IoPOSIX -> Win32.fILE_ATTRIBUTE_NORMAL + IoNative -> Win32.fILE_ATTRIBUTE_NORMAL .|. Win32.fILE_FLAG_OVERLAPPED + ) +#else + Win32.fILE_ATTRIBUTE_NORMAL +#endif + Nothing) + Win32.closeHandle + toHandle + where + toHandle h = do + when (iomode == AppendMode ) $ void $ Win32.setFilePointerEx h 0 Win32.fILE_END + Win32.hANDLEToHandle h + accessMode = case iomode of + ReadMode -> Win32.gENERIC_READ + WriteMode -> Win32.gENERIC_WRITE + AppendMode -> Win32.gENERIC_WRITE .|. Win32.fILE_APPEND_DATA + ReadWriteMode -> Win32.gENERIC_READ .|. Win32.gENERIC_WRITE + + createMode = case iomode of + ReadMode -> Win32.oPEN_EXISTING + WriteMode -> Win32.tRUNCATE_EXISTING + AppendMode -> Win32.oPEN_EXISTING + ReadWriteMode -> Win32.oPEN_EXISTING + + shareMode = case iomode of + ReadMode -> maxShareMode + WriteMode -> writeShareMode + AppendMode -> writeShareMode + ReadWriteMode -> maxShareMode + diff --git a/cabal.project b/cabal.project new file mode 100644 index 00000000..443201dc --- /dev/null +++ b/cabal.project @@ -0,0 +1,14 @@ +packages: . + https://hackage.haskell.org/package/filepath-2.0.0.0/candidate/filepath-2.0.0.0.tar.gz + +source-repository-package + type: git + location: https://github.com/hasufell/unix.git + tag: c0fd56ccf26fb30307bc87b1130fabe8a2641b87 + +source-repository-package + type: git + location: https://github.com/hasufell/Win32.git + tag: a2138f8ce5c5f0fec27c17e4599b37b9abb83fb9 + +allow-newer: filepath diff --git a/directory.cabal b/directory.cabal index 09c4a550..cd29653a 100644 --- a/directory.cabal +++ b/directory.cabal @@ -1,3 +1,4 @@ +cabal-version: 2.0 name: directory version: 1.3.7.1 license: BSD3 @@ -10,7 +11,6 @@ description: directories in a portable way. category: System build-type: Configure -cabal-version: >= 1.10 tested-with: GHC>=7.4.1 extra-tmp-files: @@ -23,6 +23,10 @@ extra-source-files: HsDirectoryConfig.h.in README.md System/Directory/Internal/*.h + System/Directory/Template.hs + System/Directory/Internal/Common/Template.hs + System/Directory/Internal/Posix/Template.hs + System/Directory/Internal/Windows/Template.hs changelog.md configure configure.ac @@ -36,31 +40,51 @@ source-repository head Library default-language: Haskell2010 + default-extensions: + MultiWayIf + ViewPatterns other-extensions: CPP Trustworthy exposed-modules: System.Directory + System.Directory.AbstractFilePath System.Directory.Internal + System.Directory.Internal.AbstractFilePath System.Directory.Internal.Prelude other-modules: System.Directory.Internal.C_utimensat + System.Directory.Internal.Common.AbstractFilePath System.Directory.Internal.Common System.Directory.Internal.Config + System.Directory.Internal.Config.AbstractFilePath System.Directory.Internal.Posix + System.Directory.Internal.PosixFFI + System.Directory.Internal.Posix.AbstractFilePath System.Directory.Internal.Windows + System.Directory.Internal.WindowsFFI + System.Directory.Internal.WindowsFFI.Common + System.Directory.Internal.WindowsFFI.AbstractFilePath + System.Directory.Internal.Windows.AbstractFilePath + System.File.AbstractFilePath + System.File.PlatformFilePath include-dirs: . build-depends: - base >= 4.5 && < 4.18, - time >= 1.4 && < 1.13, - filepath >= 1.3 && < 1.5 + base >= 4.5 && < 4.18, + bytestring >= 0.9.2 && < 0.14, + time >= 1.4 && < 1.13, + filepath ^>= 2.0 if os(windows) - build-depends: Win32 >= 2.2.2 && < 2.14 + build-depends: Win32 ^>= 2.13.2.0 + other-modules: + System.File.Windows else - build-depends: unix >= 2.5.1 && < 2.9 + build-depends: unix ^>= 2.8.0.0 + other-modules: + System.File.Posix ghc-options: -Wall