packages feed

directory 1.3.0.2 → 1.3.1.0

raw patch · 18 files changed

+797/−221 lines, 18 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

+ System.Directory: createDirectoryLink :: FilePath -> FilePath -> IO ()
+ System.Directory: createFileLink :: FilePath -> FilePath -> IO ()
+ System.Directory: getSymbolicLinkTarget :: FilePath -> IO FilePath
+ System.Directory: removeDirectoryLink :: FilePath -> IO ()
+ System.Directory.Internal: CTimeSpec :: EpochTime -> CLong -> CTimeSpec
+ System.Directory.Internal: c_AT_FDCWD :: CInt
+ System.Directory.Internal: c_PATH_MAX :: Maybe Int
+ System.Directory.Internal: c_free :: Ptr a -> IO ()
+ System.Directory.Internal: c_realpath :: CString -> CString -> IO CString
+ System.Directory.Internal: c_utimensat :: CInt -> CString -> Ptr CTimeSpec -> CInt -> IO CInt
+ System.Directory.Internal: data CTimeSpec
+ System.Directory.Internal: toCTimeSpec :: POSIXTime -> CTimeSpec
+ System.Directory.Internal: utimeOmit :: CTimeSpec
+ System.Directory.Internal: withRealpath :: CString -> (CString -> IO a) -> IO a

Files

HsDirectoryConfig.h.in view
@@ -3,6 +3,9 @@ /* Filename extension of executable files */ #undef EXE_EXTENSION +/* Define to 1 if you have the `CreateSymbolicLinkW' function. */+#undef HAVE_CREATESYMBOLICLINKW+ /* Define to 1 if you have the <fcntl.h> header file. */ #undef HAVE_FCNTL_H 
System/Directory.hs view
@@ -52,10 +52,17 @@     , renamePath     , copyFile     , copyFileWithMetadata+    , getFileSize      , canonicalizePath     , makeAbsolute     , makeRelativeToCurrentDirectory++    -- * Existence tests+    , doesPathExist+    , doesFileExist+    , doesDirectoryExist+     , findExecutable     , findExecutables     , findExecutablesInDirectories@@ -65,15 +72,12 @@     , findFilesWith     , exeExtension -    , getFileSize--    -- * Existence tests-    , doesPathExist-    , doesFileExist-    , doesDirectoryExist-     -- * Symbolic links+    , createFileLink+    , createDirectoryLink+    , removeDirectoryLink     , pathIsSymbolicLink+    , getSymbolicLinkTarget      -- * Permissions @@ -115,6 +119,7 @@   , utcTimeToPOSIXSeconds   , POSIXTime   )+import qualified System.Directory.Internal.Config as Cfg #ifdef mingw32_HOST_OS import qualified System.Win32 as Win32 #else@@ -138,6 +143,28 @@ are relative to the current directory. -} +-- | A generator with side-effects.+newtype ListT m a = ListT (m (Maybe (a, ListT m a)))++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+ ----------------------------------------------------------------------------- -- Permissions @@ -418,7 +445,7 @@ getDirectoryType path =   (`ioeAddLocation` "getDirectoryType") `modifyIOError` do #ifdef mingw32_HOST_OS-    isDir <- withFileStatus "getDirectoryType" path isDirectory+    isDir <- withSymbolicLinkStatus "getDirectoryType" path isDirectory     if isDir       then do         isLink <- pathIsSymbolicLink path@@ -981,12 +1008,13 @@ -- working directory. -- -- Indirections include the two special directories @.@ and @..@, as well as--- any symbolic links.  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--- undergo 'normalise', but case canonicalization and indirection removal are--- skipped as they are impossible to do on a nonexistent path.+-- 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 undergo 'normalise', 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@@ -1000,11 +1028,11 @@ -- The results can be utterly wrong if the portions of the path change while -- this function is running. ----- Since symbolic links (and, on non-Windows systems, parent directories @..@)--- 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.+-- 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@@ -1017,11 +1045,14 @@ -- Similar to 'normalise', passing an empty path is equivalent to passing the -- current directory. ----- /Known bugs/: When the path contains an existing symbolic link, but the--- target of the link does not exist, then the path is not dereferenced (bug--- #64).  Symbolic link expansion is not performed on Windows XP or earlier--- due to the absence of @GetFinalPathNameByHandle@.+-- @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'. --@@ -1040,9 +1071,12 @@   where  #if defined(mingw32_HOST_OS)-    transform path =-      attemptRealpath getFinalPathName =<<-        (Win32.getFullPathName path `catchIOError` \ _ -> return path)+    transform = attemptRealpath getFinalPathName++    simplify path =+      Win32.getFullPathName path+        `catchIOError` \ _ ->+          return path #else     transform path = do       encoding <- getFileSystemEncoding@@ -1050,23 +1084,68 @@             GHC.withCString encoding path'               (`withRealpath` GHC.peekCString encoding)       attemptRealpath realpath path++    simplify = return #endif -    attemptRealpath realpath path =-      realpathPrefix realpath (reverse (zip prefixes suffixes)) path-      where segments = splitDirectories path-            prefixes = scanl1 (</>) segments-            suffixes = tail (scanr (</>) "" segments)+    -- allow up to 64 cycles before giving up+    attemptRealpath realpath =+      attemptRealpathWith (64 :: Int) Nothing realpath <=< simplify -    -- call realpath on the largest possible prefix-    realpathPrefix realpath ((prefix, suffix) : rest) path = do-      exist <- doesPathExist prefix-      if exist -- never call realpath on an inaccessible path-        then ((</> suffix) <$> realpath prefix)-             `catchIOError` \ _ -> realpathPrefix realpath rest path-        else realpathPrefix realpath rest path-    realpathPrefix _ _ path = return path+    -- 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 -> return 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+            [] -> return 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+            [] -> return fallback+            next : restSuffix -> do+              -- see if the 'next' segment is a symlink+              mTarget <- tryIOError (getSymbolicLinkTarget (p </> next))+              case mTarget of+                Left _ -> return fallback+                Right target -> do+                  -- if so, dereference it and restart the whole cycle+                  let mFallback' = Just (fromMaybe fallback mFallback)+                  path' <- simplify (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 -- 'normalise'd.  If the path is already absolute, the path is simply@@ -1119,24 +1198,28 @@     cur <- getCurrentDirectory     return $ makeRelative cur x --- | Given an executable file name, searches for such file in the--- directories listed in system PATH. The returned value is the path--- to the found executable or Nothing if an executable with the given--- name was not found. For example (findExecutable \"ghc\") gives you--- the path to GHC.+-- | 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' corresponds to the--- program that would be executed by 'System.Process.createProcess'--- when passed the same string (as a RawCommand, not a ShellCommand).+-- The path returned by @'findExecutable' name@ corresponds to the program+-- that would be executed by 'System.Process.createProcess' 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 'SearchPath',--- which may search other places before checking the directories in--- @PATH@.  Where it actually searches depends on registry settings,--- but notably includes the directory containing the current--- executable. See--- <http://msdn.microsoft.com/en-us/library/aa365527.aspx> for more--- details.+-- On Windows, 'findExecutable' calls the Win32 function+-- @<https://msdn.microsoft.com/en-us/library/aa365527.aspx SearchPath>@,+-- 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 = do #if defined(mingw32_HOST_OS)@@ -1146,13 +1229,17 @@     findFileWith isExecutable path (binary <.> exeExtension) #endif --- | Given a file name, searches for the file and returns a list of all--- occurences that are executable.+-- | 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 ocurrence, if any.  It uses the--- @SearchPath@ from the Win32 API, so the caveats noted in 'findExecutable'--- apply here as well.+-- On Windows, this /only returns the first ocurrence/, 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 = do@@ -1172,74 +1259,104 @@     return (splitSearchPath path) #endif --- | Given a file name, searches for the file on the given paths and returns a--- list of all occurences that are executable.+-- | 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 =     findFilesWith isExecutable path (binary <.> exeExtension) --- | Test whether a file is executable.+-- | Test whether a file has executable permissions. isExecutable :: FilePath -> IO Bool isExecutable file = do     perms <- getPermissions file     return (executable perms) --- | Search through the given set of directories for the given 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 (\_ -> return True) --- | Search through the given set of directories for the given file and--- returns a list of paths where the given file exists.+-- | 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 (\_ -> return True) --- | Search through the given set of directories for the given file and--- with the given property (usually permissions) and returns the file path--- where the given file exists and has the property.+-- | 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 = asumMaybeT (map (findFileWithIn f name) ds)+findFileWith f ds name = listTHead (findFilesWithLazy f ds name) --- | 'Data.Foldable.asum' for 'Control.Monad.Trans.Maybe.MaybeT', essentially.+-- | @findFilesWith predicate dirs name@ searches through the list of+-- directories (@dirs@) for files that have the given @name@ and satisfy the+-- given @predicate@ ands return the paths of those files.  The directories+-- are checked in a left-to-right order and the paths are returned in the same+-- order. ----- Returns the first 'Just' in the list or 'Nothing' if there aren't any.-asumMaybeT :: Monad m => [m (Maybe a)] -> m (Maybe a)-asumMaybeT = foldr attempt (return Nothing)-  where-    attempt mmx mx' = do-        mx <- mmx-        case mx of-            Nothing -> mx'-            Just _  -> return mx---- | Search through the given set of directories for the given file and--- with the given property (usually permissions) and returns a list of--- paths where the given file exists and has the property.+-- 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 = do-    mfiles <- mapM (findFileWithIn f name) ds-    return (catMaybes mfiles)+findFilesWith f ds name = listTToList (findFilesWithLazy f ds name) --- | Like 'findFileWith', but searches only a single directory.-findFileWithIn :: (FilePath -> IO Bool) -> String -> FilePath -> IO (Maybe FilePath)-findFileWithIn f name d = do-    let path = d </> name-    exist <- doesFileExist path-    if exist-        then do-            ok <- f path-            if ok-                then return (Just path)-                else return Nothing-        else return Nothing+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 []       = return Nothing+    find (d : ds) = do+      let p = d </> path+      found <- doesFileExist p `andM` f p+      if found+        then return (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 = Cfg.exeExtension+ -- | Similar to 'listDirectory', but always includes the special entries (@.@ -- and @..@).  (This applies to Windows as well.) --@@ -1470,13 +1587,108 @@ #endif    `catchIOError` \ _ -> return False --- | Check whether the path refers to a symbolic link.  On Windows, this tests--- for @FILE_ATTRIBUTE_REPARSE_POINT@.+-- | 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 requires privileges normally 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+-- @dwFlags@ set to zero.  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+#ifdef mingw32_HOST_OS+    createSymbolicLink False target link+#else+    Posix.createSymbolicLink target link+#endif++-- | 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 requires privileges normally 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+-- @dwFlags@ set to @SYMBOLIC_LINK_FLAG_DIRECTORY@.  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+#ifdef mingw32_HOST_OS+    createSymbolicLink True target link+#else+    createFileLink target link+#endif++-- | 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+#ifdef mingw32_HOST_OS+    removeDirectory path+#else+    removeFile path+#endif++-- | Check whether the path refers to a symbolic link.  An exception is thrown+-- if the path does not exist or is inaccessible.+--+-- 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@.+-- -- @since 1.3.0.0 pathIsSymbolicLink :: FilePath -> IO Bool pathIsSymbolicLink path =-  (`ioeAddLocation` "getDirectoryType") `modifyIOError` do+  (`ioeAddLocation` "pathIsSymbolicLink") `modifyIOError` do #ifdef mingw32_HOST_OS     isReparsePoint <$> Win32.getFileAttributes path   where@@ -1489,7 +1701,29 @@ 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 #ifdef mingw32_HOST_OS+    readSymbolicLink path+#else+    Posix.readSymbolicLink path+#endif++#ifdef mingw32_HOST_OS -- | Open the handle of an existing file or directory. openFileHandle :: String -> Win32.AccessMode -> IO Win32.HANDLE openFileHandle path mode = Win32.createFile path mode share Nothing@@ -1690,8 +1924,14 @@  #ifdef mingw32_HOST_OS withFileStatus :: String -> FilePath -> (Ptr CStat -> IO a) -> IO a-withFileStatus loc name f = do-  modifyIOError (`ioeSetFileName` name) $+withFileStatus loc name f =+  modifyIOError (`ioeSetFileName` name) $ do+    name' <- getFinalPathName name+    withSymbolicLinkStatus loc name' f++withSymbolicLinkStatus :: String -> FilePath -> (Ptr CStat -> IO a) -> IO a+withSymbolicLinkStatus loc name f = do+  modifyIOError (`ioeSetFileName` name) $ do     allocaBytes sizeof_stat $ \p ->       withFilePath (fileNameEndClean name) $ \s -> do         throwErrnoIfMinus1Retry_ loc (c_stat s p)
System/Directory/Internal.hs view
@@ -1,22 +1,29 @@ {-# LANGUAGE CPP #-}-{-# OPTIONS_HADDOCK hide #-}+-- |+-- 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 <HsDirectoryConfig.h>  module System.Directory.Internal-  ( module System.Directory.Internal.Config--#ifdef HAVE_UTIMENSAT-  , module System.Directory.Internal.C_utimensat-#endif+  (  #ifdef mingw32_HOST_OS-  , module System.Directory.Internal.Windows+    module System.Directory.Internal.Windows #else-  , module System.Directory.Internal.Posix+    module System.Directory.Internal.Posix #endif +#ifdef HAVE_UTIMENSAT+  , module System.Directory.Internal.C_utimensat+#endif+   ) where-import System.Directory.Internal.Config  #ifdef HAVE_UTIMENSAT import System.Directory.Internal.C_utimensat
System/Directory/Internal/Config.hs view
@@ -2,10 +2,6 @@ #include <HsDirectoryConfig.h> module System.Directory.Internal.Config where --- | 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 = EXE_EXTENSION -- We avoid using #const_str from hsc because it breaks cross-compilation
System/Directory/Internal/Prelude.hs view
@@ -1,10 +1,18 @@ {-# LANGUAGE CPP #-}-{-# OPTIONS_HADDOCK hide #-}+-- |+-- Stability: unstable+-- Portability: portable+--+-- Internal modules are always subject to change from version to version.+ module System.Directory.Internal.Prelude   ( module Prelude-#if !MIN_VERSION_base(4, 8, 0)+#if MIN_VERSION_base(4, 8, 0)+  , module Data.Void+#else   , module Control.Applicative   , module Data.Functor+  , Void #endif   , module Control.Arrow   , module Control.Concurrent@@ -29,7 +37,6 @@   , module System.Posix.Internals   , module System.Posix.Types   , module System.Timeout-  , Void   ) where #if !MIN_VERSION_base(4, 6, 0) import Prelude hiding (catch)@@ -137,6 +144,7 @@ import System.IO.Error   ( IOError   , catchIOError+  , illegalOperationErrorType   , ioeGetErrorString   , ioeGetErrorType   , ioeGetLocation@@ -145,6 +153,7 @@   , ioeSetLocation   , isAlreadyExistsError   , isDoesNotExistError+  , isIllegalOperation   , isPermissionError   , mkIOError   , modifyIOError
System/Directory/Internal/Windows.hsc view
@@ -14,9 +14,13 @@ #ifdef HAVE_SYS_STAT_H # include <sys/stat.h> #endif+#include <System/Directory/Internal/utility.h>+#include <System/Directory/Internal/windows.h> import Prelude () import System.Directory.Internal.Prelude-import System.FilePath (isRelative, normalise, splitDirectories)+import System.FilePath (isPathSeparator, isRelative, normalise,+                        pathSeparator, splitDirectories)+import qualified Data.List as List import qualified System.Win32 as Win32  win32_cSIDL_LOCAL_APPDATA :: Win32.CSIDL@@ -26,6 +30,9 @@ win32_cSIDL_LOCAL_APPDATA = (#const CSIDL_LOCAL_APPDATA) #endif +win32_eRROR_INVALID_FUNCTION :: Win32.ErrCode+win32_eRROR_INVALID_FUNCTION = 0x1+ win32_fILE_ATTRIBUTE_REPARSE_POINT :: Win32.FileAttributeOrFlag win32_fILE_ATTRIBUTE_REPARSE_POINT = (#const FILE_ATTRIBUTE_REPARSE_POINT) @@ -107,6 +114,151 @@     rawGetFinalPathName = win32_getLongPathName <=< win32_getShortPathName #endif +win32_fILE_FLAG_OPEN_REPARSE_POINT :: Win32.FileAttributeOrFlag+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+  -> Win32.DWORD+  -> (Ptr a, Int)+  -> (Ptr b, Int)+  -> Maybe Void+  -> IO (Either Win32.ErrCode Int)+deviceIoControl h code (inPtr, inSize) (outPtr, outSize) _ = do+  with 0 $ \ lenPtr -> do+    status <- c_DeviceIoControl h code inPtr (fromIntegral inSize) outPtr+                                (fromIntegral outSize) lenPtr nullPtr+    if not status+      then do+        Left <$> Win32.getLastError+      else+        Right . fromIntegral <$> peek lenPtr++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 path = modifyIOError (`ioeSetFileName` path) $ do+  let open = Win32.createFile (toExtendedLengthPath path)+                               0 shareMode Nothing Win32.oPEN_EXISTING+                               (Win32.fILE_FLAG_BACKUP_SEMANTICS .|.+                               win32_fILE_FLAG_OPEN_REPARSE_POINT) Nothing+  bracket open Win32.closeHandle $ \ h -> do+    win32_alloca_REPARSE_DATA_BUFFER $ \ ptrAndSize@(ptr, _) -> do+      result <- deviceIoControl h win32_fSCTL_GET_REPARSE_POINT+                                (nullPtr, 0) ptrAndSize Nothing+      case result of+        Left e | e == win32_eRROR_INVALID_FUNCTION -> do+                   let msg = "Incorrect function. The file system " <>+                             "might not support symbolic links."+                   throwIO (mkIOError illegalOperationErrorType+                                      "DeviceIoControl" Nothing Nothing+                            `ioeSetErrorString` msg)+               | otherwise -> Win32.failWith "DeviceIoControl" e+        Right _ -> return ()+      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+        _ -> throwIO (mkIOError InappropriateType+                                "readSymbolicLink" Nothing Nothing)+  where+    shareMode =+      win32_fILE_SHARE_DELETE .|.+      Win32.fILE_SHARE_READ   .|.+      Win32.fILE_SHARE_WRITE+    strip sn = fromMaybe sn (List.stripPrefix "\\??\\" sn)++-- | Normalise the path separators and prepend the @"\\\\?\\"@ prefix if+-- necessary or possible.+normaliseSeparators :: FilePath -> FilePath+normaliseSeparators path+  | isRelative path = normaliseSep <$> path+  | otherwise = toExtendedLengthPath path+  where normaliseSep c = if isPathSeparator c then pathSeparator else c+ -- | Add the @"\\\\?\\"@ prefix if necessary or possible. -- The path remains unchanged if the prefix is not added. toExtendedLengthPath :: FilePath -> FilePath@@ -158,8 +310,58 @@       r' <- getPathNameWithLen len       case r' of         Right s -> pure s-        Left _ -> ioError (mkIOError OtherError "" Nothing Nothing+        Left _ -> throwIO (mkIOError OtherError "" Nothing Nothing                            `ioeSetErrorString` "path changed unexpectedly")++win32_createSymbolicLink :: String -> String -> Bool -> IO ()+win32_createSymbolicLink link _target _isDir =+#ifdef HAVE_CREATESYMBOLICLINKW+  withCWString link $ \ pLink ->+  withCWString _target $ \ pTarget -> do+    let flags = if _isDir then win32_sYMBOLIC_LINK_FLAG_DIRECTORY else 0+    status <- c_CreateSymbolicLink pLink pTarget flags+    if status == 0+      then do+        e <- Win32.getLastError+        case () of+          _ | e == win32_eRROR_INVALID_FUNCTION -> do+                let msg = "Incorrect function. The underlying file system " <>+                          "might not support symbolic links."+                throwIO (mkIOError illegalOperationErrorType+                                   "CreateSymbolicLink" Nothing (Just link)+                         `ioeSetErrorString` msg)+            | e == win32_eRROR_PRIVILEGE_NOT_HELD -> do+                let msg = "A required privilege is not held by the client. " <>+                          "Creating symbolic links usually requires " <>+                          "administrative rights."+                throwIO (mkIOError permissionErrorType "CreateSymbolicLink"+                                   Nothing (Just link)+                         `ioeSetErrorString` msg)+            | otherwise -> Win32.failWith "CreateSymbolicLink" e+      else return ()+  where++win32_eRROR_PRIVILEGE_NOT_HELD :: Win32.ErrCode+win32_eRROR_PRIVILEGE_NOT_HELD = 0x522++win32_sYMBOLIC_LINK_FLAG_DIRECTORY :: Win32.DWORD+win32_sYMBOLIC_LINK_FLAG_DIRECTORY = 0x1++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)+  where unsupportedErrorMsg = "Not supported on Windows XP or older"+#endif++createSymbolicLink :: Bool -> String -> String -> IO ()+createSymbolicLink isDir target link = do+  -- toExtendedLengthPath ensures the target gets normalised properly+  win32_createSymbolicLink link (normaliseSeparators target) isDir  foreign import ccall unsafe "_wchmod"   c_wchmod :: CWString -> CMode -> IO CInt
+ System/Directory/Internal/windows.h view
@@ -0,0 +1,29 @@+#include <windows.h>++// define prototype to get size, offsets, and alignments+// (can't include <ntifs.h> because that only exists in WDK)+typedef struct {+    ULONG ReparseTag;+    USHORT ReparseDataLength;+    USHORT Reserved;+    union {+        struct {+            USHORT SubstituteNameOffset;+            USHORT SubstituteNameLength;+            USHORT PrintNameOffset;+            USHORT PrintNameLength;+            ULONG Flags;+            WCHAR PathBuffer[1];+        } SymbolicLinkReparseBuffer;+        struct {+            USHORT SubstituteNameOffset;+            USHORT SubstituteNameLength;+            USHORT PrintNameOffset;+            USHORT PrintNameLength;+            WCHAR PathBuffer[1];+        } MountPointReparseBuffer;+        struct {+            UCHAR DataBuffer[1];+        } GenericReparseBuffer;+    };+} HsDirectory_REPARSE_DATA_BUFFER;
changelog.md view
@@ -1,6 +1,32 @@ Changelog for the [`directory`][1] package ========================================== +## 1.3.1.0 (March 2017)++  * `findFile` (and similar functions): when an absolute path is given, the+    list of search directories is now completely ignored.  Previously, if the+    list was empty, `findFile` would always fail.+    ([#72](https://github.com/haskell/directory/issues/72))++  * For symbolic links on Windows, the following functions had previously+    interpreted paths as referring to the links themselves rather than their+    targets.  This was inconsistent with other platforms and has been fixed.+      * `getFileSize`+      * `doesPathExist`+      * `doesDirectoryExist`+      * `doesFileExist`++  * Fix incorrect location info in errors from `pathIsSymbolicLink`.++  * Add functions for symbolic link manipulation:+      * `createFileLink`+      * `createDirectoryLink`+      * `removeDirectoryLink`+      * `getSymbolicLinkTarget`++  * `canonicalizePath` can now resolve broken symbolic links too.+    ([#64](https://github.com/haskell/directory/issues/64))+ ## 1.3.0.2 (February 2017)    * [optimization] Increase internal buffer size of `copyFile`
configure view
@@ -3339,6 +3339,17 @@ fi done +for ac_func in CreateSymbolicLinkW+do :+  ac_fn_c_check_func "$LINENO" "CreateSymbolicLinkW" "ac_cv_func_CreateSymbolicLinkW"+if test "x$ac_cv_func_CreateSymbolicLinkW" = xyes; then :+  cat >>confdefs.h <<_ACEOF+#define HAVE_CREATESYMBOLICLINKW 1+_ACEOF++fi+done+ for ac_func in GetFinalPathNameByHandleW do :   ac_fn_c_check_func "$LINENO" "GetFinalPathNameByHandleW" "ac_cv_func_GetFinalPathNameByHandleW"
configure.ac view
@@ -31,6 +31,7 @@ AC_CHECK_HEADERS([fcntl.h limits.h sys/types.h sys/stat.h time.h])  AC_CHECK_FUNCS([utimensat])+AC_CHECK_FUNCS([CreateSymbolicLinkW]) AC_CHECK_FUNCS([GetFinalPathNameByHandleW])  # EXTEXT is defined automatically by AC_PROG_CC;
directory.cabal view
@@ -1,5 +1,5 @@ name:           directory-version:        1.3.0.2+version:        1.3.1.0 -- NOTE: Don't forget to update ./changelog.md license:        BSD3 license-file:   LICENSE@@ -46,8 +46,8 @@         System.Directory.Internal         System.Directory.Internal.Prelude     other-modules:-        System.Directory.Internal.Config         System.Directory.Internal.C_utimensat+        System.Directory.Internal.Config         System.Directory.Internal.Posix         System.Directory.Internal.Windows 
tests/CanonicalizePath.hs view
@@ -4,10 +4,6 @@ import System.FilePath ((</>), dropFileName, dropTrailingPathSeparator,                         normalise, takeFileName) import TestUtils-#ifdef mingw32_HOST_OS-import System.Directory.Internal (win32_getFinalPathNameByHandle)-import qualified System.Win32 as Win32-#endif  main :: TestEnv -> IO () main _t = do@@ -68,47 +64,35 @@   T(expectEq) () fooNon fooNon7   T(expectEq) () fooNon fooNon8 -  supportsSymbolicLinks <- do-#ifdef mingw32_HOST_OS-    hasSymbolicLinkPrivileges <--      (True <$ createSymbolicLink "_symlinktest_src" "_symlinktest_dst")-        -- only test if symbolic links can be created-        -- (usually disabled on Windows by group policy)-        `catchIOError` \ e ->-          if isPermissionError e-          then pure False-          else ioError e--    supportsGetFinalPathNameByHandle <--      (True <$ win32_getFinalPathNameByHandle Win32.nullHANDLE 0)-        `catchIOError` \ e ->-          case ioeGetErrorType e of-            UnsupportedOperation -> pure False-            _ -> pure True--    pure (hasSymbolicLinkPrivileges && supportsGetFinalPathNameByHandle)-#else-    pure True-#endif-+  supportsSymbolicLinks <- supportsSymlinks   when supportsSymbolicLinks $ do      let barQux = dot </> "bar" </> "qux" -    createSymbolicLink "../bar" "foo/bar"+    -- note: this also checks that "../bar" gets normalized to "..\\bar"+    --       since Windows does not like "/" in symbolic links targets+    createFileLink "../bar" "foo/bar"     T(expectEq) () bar =<< canonicalizePath "foo/bar"     T(expectEq) () barQux =<< canonicalizePath "foo/bar/qux" -    createSymbolicLink "foo" "lfoo"+    createDirectoryLink "foo" "lfoo"     T(expectEq) () foo =<< canonicalizePath "lfoo"     T(expectEq) () foo =<< canonicalizePath "lfoo/"     T(expectEq) () bar =<< canonicalizePath "lfoo/bar"     T(expectEq) () barQux =<< canonicalizePath "lfoo/bar/qux" -    -- FIXME: uncomment this test once #64 is fixed-    -- createSymbolicLink "../foo/non-existent" "foo/qux"-    -- qux <- canonicalizePath "foo/qux"-    -- T(expectEq) () qux (dot </> "../foo/non-existent")+    -- regression test for #64+    createFileLink "../foo/non-existent" "foo/qux"+    qux <- canonicalizePath "foo/qux"+    T(expectEq) () qux =<< canonicalizePath "foo/non-existent"++    -- make sure it can handle loops+    createFileLink "loop1" "loop2"+    createFileLink "loop2" "loop1"+    loop1 <- canonicalizePath "loop1"+    loop2 <- canonicalizePath "loop2"+    T(expectEq) () loop1 (normalise (dot </> "loop1"))+    T(expectEq) () loop2 (normalise (dot </> "loop2"))    caseInsensitive <-     (False <$ createDirectory "FOO")
tests/FindFile001.hs view
@@ -1,10 +1,51 @@ {-# LANGUAGE CPP #-} module FindFile001 where #include "util.inl"+import qualified Data.List as List import System.FilePath ((</>))  main :: TestEnv -> IO () main _t = do++  createDirectory "bar"+  createDirectory "qux"   writeFile "foo" ""-  found <- findFile ("." : undefined) "foo"-  T(expectEq) () found (Just ("." </> "foo"))+  writeFile ("bar" </> "foo") ""+  writeFile ("qux" </> "foo") ":3"++  -- make sure findFile is lazy enough+  T(expectEq) () (Just ("." </> "foo")) =<< findFile ("." : undefined) "foo"++  -- make sure relative paths work+  T(expectEq) () (Just ("." </> "bar" </> "foo")) =<<+    findFile ["."] ("bar" </> "foo")++  T(expectEq) () (Just ("." </> "foo")) =<< findFile [".", "bar"] ("foo")+  T(expectEq) () (Just ("bar" </> "foo")) =<< findFile ["bar", "."] ("foo")++  let f fn = (== ":3") <$> readFile fn+  for_ (List.permutations ["qux", "bar", "."]) $ \ ds -> do++    let (match, noMatch) = List.partition (== "qux") ds++    T(expectEq) ds (Just (List.head match </> "foo")) =<<+      findFileWith f ds "foo"++    T(expectEq) ds ((</> "foo") <$> match) =<< findFilesWith f ds "foo"++    T(expectEq) ds (Just (List.head noMatch </> "foo")) =<<+      findFileWith ((not <$>) . f) ds "foo"++    T(expectEq) ds ((</> "foo") <$> noMatch) =<<+      findFilesWith ((not <$>) . f) ds "foo"++    T(expectEq) ds Nothing =<< findFileWith (\ _ -> return False) ds "foo"++    T(expectEq) ds [] =<< findFilesWith (\ _ -> return False) ds "foo"++  -- make sure absolute paths are handled properly irrespective of 'dirs'+  -- https://github.com/haskell/directory/issues/72+  absPath <- makeAbsolute ("bar" </> "foo")+  absPath2 <- makeAbsolute ("bar" </> "nonexistent")+  T(expectEq) () (Just absPath) =<< findFile [] absPath+  T(expectEq) () Nothing =<< findFile [] absPath2
tests/PathIsSymbolicLink.hs view
@@ -5,14 +5,29 @@  main :: TestEnv -> IO () main _t = do-  success <- (createSymbolicLink "x" "y" >> return True)-#ifdef mingw32_HOST_OS-    -- only test if symbolic links can be created-    -- (usually disabled on Windows by group policy)-    `catchIOError` \ e ->-      if isPermissionError e-      then return False-      else ioError e-#endif-  when success $+  supportsSymbolicLinks <- supportsSymlinks+  when supportsSymbolicLinks $ do++    createFileLink "x" "y"+    createDirectoryLink "a" "b"+     T(expect) () =<< pathIsSymbolicLink "y"+    T(expect) () =<< pathIsSymbolicLink "b"+    T(expectEq) () "x" =<< getSymbolicLinkTarget "y"+    T(expectEq) () "a" =<< getSymbolicLinkTarget "b"+    T(expectEq) () False =<< doesFileExist "y"+    T(expectEq) () False =<< doesDirectoryExist "b"++    writeFile "x" ""+    createDirectory "a"++    T(expect) () =<< doesFileExist "y"+    T(expect) () =<< doesDirectoryExist "b"++    removeFile "y"+    removeDirectoryLink "b"++    T(expectIOErrorType) () isDoesNotExistError (pathIsSymbolicLink "y")+    T(expectIOErrorType) () isDoesNotExistError (pathIsSymbolicLink "b")+    T(expectEq) () False =<< doesFileExist "y"+    T(expectEq) () False =<< doesDirectoryExist "b"
tests/RemoveDirectoryRecursive001.hs view
@@ -26,9 +26,9 @@   createDirectoryIfMissing True (tmp "c")   writeFile (tmp "a/x/w/u") "foo"   writeFile (tmp "a/t")     "bar"-  tryCreateSymbolicLink (normalise "../a") (tmp "b/g")-  tryCreateSymbolicLink (normalise "../b") (tmp "c/h")-  tryCreateSymbolicLink (normalise "a")    (tmp "d")+  symlinkOrCopy (normalise "../a") (tmp "b/g")+  symlinkOrCopy (normalise "../b") (tmp "c/h")+  symlinkOrCopy (normalise "a")    (tmp "d")   modifyPermissions (tmp "c") (\ p -> p { writable = False })    ------------------------------------------------------------
tests/RemovePathForcibly.hs view
@@ -28,9 +28,9 @@   writeFile (tmp "a/x/w/u") "foo"   writeFile (tmp "a/t")     "bar"   writeFile (tmp "f/s")     "qux"-  tryCreateSymbolicLink (normalise "../a") (tmp "b/g")-  tryCreateSymbolicLink (normalise "../b") (tmp "c/h")-  tryCreateSymbolicLink (normalise "a")    (tmp "d")+  symlinkOrCopy (normalise "../a") (tmp "b/g")+  symlinkOrCopy (normalise "../b") (tmp "c/h")+  symlinkOrCopy (normalise "a")    (tmp "d")   setPermissions (tmp "f/s") emptyPermissions   setPermissions (tmp "f") emptyPermissions 
tests/TestUtils.hs view
@@ -1,33 +1,20 @@-{-# LANGUAGE CPP, ForeignFunctionInterface #-}+{-# LANGUAGE CPP #-}+-- | Utility functions specific to 'directory' tests module TestUtils   ( copyPathRecursive-  , createSymbolicLink   , modifyPermissions-  , tryCreateSymbolicLink+  , symlinkOrCopy+  , supportsSymlinks   ) where import Prelude () import System.Directory.Internal.Prelude import System.Directory-import System.FilePath ((</>))+import System.FilePath ((</>), normalise, takeDirectory) #ifdef mingw32_HOST_OS-import System.FilePath (takeDirectory)+import System.Directory.Internal (win32_getFinalPathNameByHandle) import qualified System.Win32 as Win32-#else-import System.Posix (createSymbolicLink) #endif -#ifdef 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-foreign import WINAPI unsafe "windows.h CreateSymbolicLinkW"-  c_CreateSymbolicLink :: Ptr CWchar -> Ptr CWchar -> CULong -> IO CUChar-#endif- -- | @'copyPathRecursive' path@ copies an existing file or directory at --   /path/ together with its contents and subdirectories. --@@ -49,37 +36,61 @@   permissions <- getPermissions path   setPermissions path (modify permissions) +-- | On Windows, the handler is called if symbolic links are unsupported or+-- the user lacks the necessary privileges to create them.  On other+-- platforms, the handler is never run.+handleSymlinkUnavail+  :: IO a                               -- ^ handler+  -> IO a                               -- ^ arbitrary action+  -> IO a+handleSymlinkUnavail _handler action = action #ifdef mingw32_HOST_OS-createSymbolicLink :: String -> String -> IO ()-createSymbolicLink target link =-  (`ioeSetLocation` "createSymbolicLink") `modifyIOError` do-    isDir <- (fromIntegral . fromEnum) `fmap`-             doesDirectoryExist (takeDirectory link </> target)-    let target' = fixSlash <$> target-    withCWString target' $ \ pTarget ->-      withCWString link $ \ pLink -> do-        status <- c_CreateSymbolicLink pLink pTarget isDir-        if status == 0-          then do-            errCode <- Win32.getLastError-            if errCode == c_ERROR_PRIVILEGE_NOT_HELD-              then ioError . (`ioeSetErrorString` permissionErrorMsg) $-                   mkIOError permissionErrorType "" Nothing (Just link)-              else Win32.failWith "createSymbolicLink" errCode-          else return ()-  where c_ERROR_PRIVILEGE_NOT_HELD = 0x522-        permissionErrorMsg = "no permission to create symbolic links"-        fixSlash '/' = '\\'-        fixSlash c   = c+  `catchIOError` \ e ->+    case ioeGetErrorType e of+      UnsupportedOperation -> _handler+      _ | isIllegalOperation e || isPermissionError e -> _handler+      _ -> ioError e #endif --- | Attempt to create a symbolic link.  On Windows, this falls back to---   copying if forbidden due to Group Policies.-tryCreateSymbolicLink :: FilePath -> FilePath -> IO ()-tryCreateSymbolicLink target link = createSymbolicLink target link+-- | Create a symbolic link.  On Windows, this falls back to copying if+-- forbidden by Group Policy or is not supported.  On other platforms, there+-- is no fallback.  Also, automatically detect if the source is a file or a+-- directory and create the appropriate type of link.+symlinkOrCopy :: FilePath -> FilePath -> IO ()+symlinkOrCopy target link = do+  let fullTarget = takeDirectory link </> target+  handleSymlinkUnavail (copyPathRecursive fullTarget link) $ do+    isDir <- doesDirectoryExist fullTarget+    (if isDir then createDirectoryLink else createFileLink)+      (normalise target)+      link++supportsSymlinks :: IO Bool+supportsSymlinks = do+  canCreate <- supportsLinkCreation+  canDeref <- supportsLinkDeref+  return (canCreate && canDeref)++-- | On Windows, test if symbolic link creation is supported and the user has+-- the necessary privileges to create them.  On other platforms, this always+-- returns 'True'.+supportsLinkCreation :: IO Bool+supportsLinkCreation = do+  let path = "_symlink_test.tmp"+  isSupported <- handleSymlinkUnavail (return False) $ do+    True <$ createFileLink path path+  when isSupported $ do+    removeFile path+  return isSupported++supportsLinkDeref :: IO Bool+supportsLinkDeref = do #ifdef mingw32_HOST_OS+    True <$ win32_getFinalPathNameByHandle Win32.nullHANDLE 0   `catchIOError` \ e ->-    if isPermissionError e-    then copyPathRecursive (takeDirectory link </> target) link-    else ioError e+    case ioeGetErrorType e of+      UnsupportedOperation -> return False+      _ -> return True+#else+    return True #endif
tests/Util.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE BangPatterns #-}+-- | A rudimentary testing framework module Util where import Prelude () import System.Directory.Internal.Prelude