file-io 0.1.0.2 → 0.1.1
raw patch · 12 files changed
+722/−140 lines, 12 filesdep +deepseqdep +file-iodep +tastydep ~basedep ~bytestringdep ~filepathPVP ok
version bump matches the API change (PVP)
Dependencies added: deepseq, file-io, tasty, tasty-hunit, temporary
Dependency ranges changed: base, bytestring, filepath
API changes (from Hackage documentation)
Files
- CHANGELOG.md +7/−0
- System/File/OsPath.hs +95/−24
- file-io.cabal +64/−2
- posix/System/File/Platform.hs +15/−3
- tests/CLC237.hs +25/−0
- tests/Properties.hs +251/−0
- tests/T14.hs +22/−0
- tests/T15.hs +24/−0
- tests/T15Win.hs +62/−0
- tests/T8.hs +26/−0
- windows/System/File/Platform.hs +0/−111
- windows/System/File/Platform.hsc +131/−0
CHANGELOG.md view
@@ -1,5 +1,12 @@ # Revision history for file-io +## 0.1.1 -- 2024-01-20++* fix a severe bug on windows, where `readFile` may create a missing file, wrt [#14](https://github.com/hasufell/file-io/issues/14)+* fix a concurrency bug on windows with `readFile`, wrt [#15](https://github.com/hasufell/file-io/issues/15)+* make sure to set `ioe_filename` in `IOException` wrt [#17](https://github.com/hasufell/file-io/issues/17)+* Make `openFile` and friends exception safe wrt [#8](https://github.com/hasufell/file-io/issues/8)+ ## 0.1.0.2 -- 2023-12-11 * support `os-string` package and newer `filepath`
System/File/OsPath.hs view
@@ -1,10 +1,38 @@-module System.File.OsPath where+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE BangPatterns #-} +module System.File.OsPath (+ openBinaryFile+, withFile+, withBinaryFile+, withFile'+, withBinaryFile'+, readFile+, readFile'+, writeFile+, writeFile'+, appendFile+, appendFile'+, openFile+, openExistingFile+) where++ import qualified System.File.Platform as P -import Control.Exception (bracket)-import System.IO (IOMode(..), Handle, hSetBinaryMode, hClose)-import System.OsPath+import Prelude ((.), ($), String, IO, ioError, pure, either, const, flip, Maybe(..), fmap, (<$>), id, Bool(..), FilePath, (++), return, show, (>>=))+import GHC.IO (catchException)+import GHC.IO.Exception (IOException(..))+import GHC.IO.Handle (hClose_help)+import GHC.IO.Handle.Internals (debugIO)+import GHC.IO.Handle.Types (Handle__, Handle(..))+import Control.Concurrent.MVar+import Control.Monad (void, when)+import Control.DeepSeq (force)+import Control.Exception (SomeException, try, evaluate, mask, onException)+import System.IO (IOMode(..), hSetBinaryMode, hClose)+import System.IO.Unsafe (unsafePerformIO)+import System.OsPath as OSP import System.OsString.Internal.Types import qualified Data.ByteString as BS@@ -22,25 +50,21 @@ -- On POSIX systems, 'openBinaryFile' is an /interruptible operation/ as -- described in "Control.Exception". openBinaryFile :: OsPath -> IOMode -> IO Handle-openBinaryFile fp iomode = do- h <- openFile fp iomode- hSetBinaryMode h True- pure h+openBinaryFile osfp iomode = augmentError "openBinaryFile" osfp $ withOpenFile' osfp iomode True False pure False + -- | Run an action on a file. -- -- The 'Handle' is automatically closed afther the action. withFile :: OsPath -> IOMode -> (Handle -> IO r) -> IO r-withFile fp iomode action = bracket- (openFile fp iomode)- hClose- action+withFile osfp iomode act = (augmentError "withFile" osfp+ $ withOpenFile' osfp iomode False False (try . act) True)+ >>= either ioError pure withBinaryFile :: OsPath -> IOMode -> (Handle -> IO r) -> IO r-withBinaryFile fp iomode action = bracket- (openBinaryFile fp iomode)- hClose- action+withBinaryFile osfp iomode act = (augmentError "withBinaryFile" osfp+ $ withOpenFile' osfp iomode True False (try . act) True)+ >>= either ioError pure -- | Run an action on a file. --@@ -48,15 +72,15 @@ -- with caution. withFile' :: OsPath -> IOMode -> (Handle -> IO r) -> IO r-withFile' fp iomode action = do- h <- openFile fp iomode- action h+withFile' osfp iomode act = (augmentError "withFile'" osfp+ $ withOpenFile' osfp iomode False False (try . act) False)+ >>= either ioError pure withBinaryFile' :: OsPath -> IOMode -> (Handle -> IO r) -> IO r-withBinaryFile' fp iomode action = do- h <- openBinaryFile fp iomode- action h+withBinaryFile' osfp iomode act = (augmentError "withBinaryFile'" osfp+ $ withOpenFile' osfp iomode True False (try . act) False)+ >>= either ioError pure -- | The 'readFile' function reads a file and returns the contents of the file -- as a 'ByteString'. The file is read lazily, on demand.@@ -93,8 +117,55 @@ -- | Open a file and return the 'Handle'. openFile :: OsPath -> IOMode -> IO Handle-openFile (OsString fp) = P.openFile fp+openFile osfp iomode = augmentError "openFile" osfp $ withOpenFile' osfp iomode False False pure False + -- | Open an existing file and return the 'Handle'. openExistingFile :: OsPath -> IOMode -> IO Handle-openExistingFile (OsString fp) = P.openExistingFile fp+openExistingFile osfp iomode = augmentError "openExistingFile" osfp $ withOpenFile' osfp iomode False True pure False+++-- ---------------------------------------------------------------------------+-- Internals++handleFinalizer :: FilePath -> MVar Handle__ -> IO ()+handleFinalizer _fp m = do+ handle_ <- takeMVar m+ (handle_', _) <- hClose_help handle_+ putMVar m handle_'+ return ()++type HandleFinalizer = FilePath -> MVar Handle__ -> IO ()++-- | Add a finalizer to a 'Handle'. Specifically, the finalizer+-- will be added to the 'MVar' of a file handle or the write-side+-- 'MVar' of a duplex handle. See Handle Finalizers for details.+addHandleFinalizer :: Handle -> HandleFinalizer -> IO ()+addHandleFinalizer hndl finalizer = do+ debugIO $ "Registering finalizer: " ++ show filepath+ void $ mkWeakMVar mv (finalizer filepath mv)+ where+ !(filepath, !mv) = case hndl of+ FileHandle fp m -> (fp, m)+ DuplexHandle fp _ write_m -> (fp, write_m)++withOpenFile' :: OsPath -> IOMode -> Bool -> Bool -> (Handle -> IO r) -> Bool -> IO r+withOpenFile' (OsString fp) iomode binary existing action close_finally = mask $ \restore -> do+ hndl <- if existing+ then P.openExistingFile fp iomode+ else P.openFile fp iomode+ addHandleFinalizer hndl handleFinalizer+ when binary $ hSetBinaryMode hndl True+ r <- restore (action hndl) `onException` hClose hndl+ when close_finally $ hClose hndl+ pure r++addFilePathToIOError :: String -> OsPath -> IOException -> IOException+addFilePathToIOError fun fp ioe = unsafePerformIO $ do+ fp' <- either (const (fmap OSP.toChar . OSP.unpack $ fp)) id <$> try @SomeException (OSP.decodeFS fp)+ fp'' <- evaluate $ force fp'+ pure $ ioe{ ioe_location = fun, ioe_filename = Just fp'' }++augmentError :: String -> OsPath -> IO a -> IO a+augmentError str osfp = flip catchException (ioError . addFilePathToIOError str osfp)+
file-io.cabal view
@@ -1,6 +1,6 @@ cabal-version: 2.4 name: file-io-version: 0.1.0.2+version: 0.1.1 synopsis: Basic file IO operations via 'OsPath' description: Basic file IO operations like Prelude, but for 'OsPath'. homepage: https://github.com/hasufell/file-io@@ -12,6 +12,13 @@ category: System extra-source-files: CHANGELOG.md+tested-with: GHC==9.8.1,+ GHC==9.4.8,+ GHC==9.2.8,+ GHC==9.0.2,+ GHC==8.10.7,+ GHC==8.8.4,+ GHC==8.6.5 source-repository head type: git@@ -35,8 +42,9 @@ hs-source-dirs: . build-depends:- , base >=4.10 && <5+ , base >=4.12 && <5 , bytestring >=0.11.3.0+ , deepseq if flag(os-string) build-depends: filepath >= 1.5.0.0, os-string >= 2.0.0@@ -50,3 +58,57 @@ other-modules: System.File.Platform ghc-options: -Wall++test-suite T15+ hs-source-dirs: tests+ main-is: T15.hs+ type: exitcode-stdio-1.0+ default-language: Haskell2010+ build-depends: base, tasty, tasty-hunit, file-io, filepath, temporary+ ghc-options: -Wall -threaded -rtsopts "-with-rtsopts=-N10"+ if os(windows)+ build-depends: Win32 >=2.13.3.0++test-suite T15Win+ hs-source-dirs: tests+ main-is: T15Win.hs+ type: exitcode-stdio-1.0+ default-language: Haskell2010+ if os(windows)+ build-depends: base, tasty, tasty-hunit, file-io, filepath, temporary, Win32 >=2.13.3.0+ else+ build-depends: base+ ghc-options: -Wall -threaded -rtsopts "-with-rtsopts=-N10"++test-suite T14+ hs-source-dirs: tests+ main-is: T14.hs+ type: exitcode-stdio-1.0+ default-language: Haskell2010+ build-depends: base, file-io, filepath, temporary+ ghc-options: -Wall++test-suite T8+ hs-source-dirs: tests+ main-is: T8.hs+ type: exitcode-stdio-1.0+ default-language: Haskell2010+ build-depends: base, bytestring, file-io, filepath, temporary+ ghc-options: -Wall -threaded++test-suite CLC237+ hs-source-dirs: tests+ main-is: CLC237.hs+ type: exitcode-stdio-1.0+ default-language: Haskell2010+ build-depends: base, file-io, filepath, temporary+ ghc-options: -Wall++test-suite Properties+ hs-source-dirs: tests+ main-is: Properties.hs+ type: exitcode-stdio-1.0+ default-language: Haskell2010+ build-depends: base, bytestring, tasty, tasty-hunit, file-io, filepath, temporary+ ghc-options: -Wall -threaded -rtsopts "-with-rtsopts=-N10"+
posix/System/File/Platform.hs view
@@ -1,17 +1,23 @@+{-# LANGUAGE TypeApplications #-}+ module System.File.Platform where +import Control.Exception (try, onException, SomeException)+import GHC.IO.Handle.FD (fdToHandle') import System.IO (IOMode(..), Handle)+import System.Posix.Types (Fd(..)) import System.Posix.IO.PosixString ( defaultFileFlags,- fdToHandle, openFd,+ closeFd, OpenFileFlags(noctty, nonBlock, creat, append, trunc), OpenMode(ReadWrite, ReadOnly, WriteOnly) ) import System.OsPath.Posix ( PosixPath )+import qualified System.OsPath.Posix as PS -- | Open a file and return the 'Handle'. openFile :: PosixPath -> IOMode -> IO Handle-openFile fp iomode = fdToHandle =<< case iomode of+openFile fp iomode = fdToHandle_ iomode fp =<< case iomode of ReadMode -> open ReadOnly df WriteMode -> open WriteOnly df { trunc = True, creat = Just 0o666 } AppendMode -> open WriteOnly df { append = True, creat = Just 0o666 }@@ -22,7 +28,7 @@ -- | Open an existing file and return the 'Handle'. openExistingFile :: PosixPath -> IOMode -> IO Handle-openExistingFile fp iomode = fdToHandle =<< case iomode of+openExistingFile fp iomode = fdToHandle_ iomode fp =<< case iomode of ReadMode -> open ReadOnly df WriteMode -> open WriteOnly df { trunc = True } AppendMode -> open WriteOnly df { append = True }@@ -30,3 +36,9 @@ where open = openFd fp df = defaultFileFlags { noctty = True, nonBlock = True, creat = Nothing }++fdToHandle_ :: IOMode -> PosixPath -> Fd -> IO Handle+fdToHandle_ iomode fp (Fd fd) = (`onException` closeFd (Fd fd)) $ do+ fp' <- either (const (fmap PS.toChar . PS.unpack $ fp)) id <$> try @SomeException (PS.decodeFS fp)+ fdToHandle' fd Nothing False fp' iomode True+
+ tests/CLC237.hs view
@@ -0,0 +1,25 @@+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++module Main where++import Control.Exception+import System.OsPath ((</>), osp)+import qualified System.OsPath as OSP+import qualified System.File.OsPath as OSP+import GHC.IO.Exception (IOErrorType(..), IOException(..))+import System.IO+import System.IO.Temp++-- Test that the action in 'withFile' does not inherit the filepath annotation+-- See https://github.com/haskell/core-libraries-committee/issues/237+main :: IO ()+main = withSystemTempDirectory "tar-test" $ \baseDir' -> do+ baseDir <- OSP.encodeFS baseDir'+ res <- try @IOException $ OSP.withFile (baseDir </> [osp|foo|]) WriteMode $ \_ -> fail "test"+ case res of+ Left (IOError Nothing UserError "" "test" Nothing Nothing) -> pure ()+ Left e -> print e >> fail "Unexpected error"+ Right _ -> fail "Unexpected success"+
+ tests/Properties.hs view
@@ -0,0 +1,251 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Main where++import Control.Exception+import qualified System.FilePath as FP+import Test.Tasty+import Test.Tasty.HUnit+import System.OsPath ((</>), osp)+import qualified System.OsPath as OSP+import qualified System.File.OsPath as OSP+import GHC.IO.Exception (IOErrorType(..), IOException(..))+import System.IO+import System.IO.Temp+import qualified Data.ByteString as BS+++main :: IO ()+main = defaultMain $ testGroup "All"+ [ testGroup "System.File.OsPath"+ [ testCase "readFile . writeFile" writeFileReadFile+ , testCase "readFile . writeFile . writeFile" writeWriteFileReadFile+ , testCase "readFile . appendFile . writeFile" appendFileReadFile+ , testCase "iomode: ReadFile does not allow write" iomodeReadFile+ , testCase "iomode: WriteFile does not allow read" iomodeWriteFile+ , testCase "iomode: AppendMode does not allow read" iomodeAppendFile+ , testCase "iomode: ReadWriteMode does allow everything" iomodeAppendFile+ , testCase "concurrency: open multiple handles (read and write)" concFile+ , testCase "concurrency: open multiple handles (read and read)" concFile2+ , testCase "concurrency: open multiple handles (write and write)" concFile3+ , testCase "openExistingFile no (Read)" existingFile+ , testCase "openExistingFile no (Write)" existingFile2+ , testCase "openExistingFile no (Append)" existingFile3+ , testCase "openExistingFile no (ReadWrite)" existingFile4+ , testCase "openExistingFile yes (Read)" existingFile'+ , testCase "openExistingFile yes (Write)" existingFile2'+ , testCase "openExistingFile yes (Append)" existingFile3'+ , testCase "openExistingFile yes (ReadWrite)" existingFile4'+ ]+ ]++writeFileReadFile :: Assertion+writeFileReadFile = do+ withSystemTempDirectory "test" $ \baseDir' -> do+ baseDir <- OSP.encodeFS baseDir'+ OSP.writeFile (baseDir </> [osp|foo|]) "test"+ contents <- OSP.readFile (baseDir </> [osp|foo|])+ "test" @=? contents++writeWriteFileReadFile :: Assertion+writeWriteFileReadFile = do+ withSystemTempDirectory "test" $ \baseDir' -> do+ baseDir <- OSP.encodeFS baseDir'+ OSP.writeFile (baseDir </> [osp|foo|]) "lol"+ OSP.writeFile (baseDir </> [osp|foo|]) "test"+ contents <- OSP.readFile (baseDir </> [osp|foo|])+ "test" @=? contents++appendFileReadFile :: Assertion+appendFileReadFile = do+ withSystemTempDirectory "test" $ \baseDir' -> do+ baseDir <- OSP.encodeFS baseDir'+ OSP.writeFile (baseDir </> [osp|foo|]) "test"+ OSP.appendFile (baseDir </> [osp|foo|]) "test"+ contents <- OSP.readFile (baseDir </> [osp|foo|])+ "testtest" @=? contents++iomodeReadFile :: Assertion+iomodeReadFile = do+ withSystemTempDirectory "test" $ \baseDir' -> do+ baseDir <- OSP.encodeFS baseDir'+ OSP.writeFile (baseDir </> [osp|foo|]) ""+ r <- try @IOException $ OSP.withFile (baseDir </> [osp|foo|]) ReadMode $ \h -> BS.hPut h "test"+ IOError Nothing IllegalOperation "hPutBuf" "handle is not open for writing" Nothing (Just $ baseDir' FP.</> "foo")+ @==? r++iomodeWriteFile :: Assertion+iomodeWriteFile = do+ withSystemTempDirectory "test" $ \baseDir' -> do+ baseDir <- OSP.encodeFS baseDir'+ OSP.writeFile (baseDir </> [osp|foo|]) ""+ r <- try @IOException $ OSP.withFile (baseDir </> [osp|foo|]) WriteMode $ \h -> BS.hGetContents h+ IOError Nothing IllegalOperation "hGetBuf" "handle is not open for reading" Nothing (Just $ baseDir' FP.</> "foo")+ @==? r++iomodeAppendFile :: Assertion+iomodeAppendFile = do+ withSystemTempDirectory "test" $ \baseDir' -> do+ baseDir <- OSP.encodeFS baseDir'+ OSP.writeFile (baseDir </> [osp|foo|]) ""+ r <- try @IOException $ OSP.withFile (baseDir </> [osp|foo|]) AppendMode $ \h -> BS.hGetContents h+ IOError Nothing IllegalOperation "hGetBuf" "handle is not open for reading" Nothing (Just $ baseDir' FP.</> "foo")+ @==? r++iomodeReadWriteFile :: Assertion+iomodeReadWriteFile = do+ withSystemTempDirectory "test" $ \baseDir' -> do+ baseDir <- OSP.encodeFS baseDir'+ OSP.writeFile (baseDir </> [osp|foo|]) ""+ r <- try @IOException $ OSP.withFile (baseDir </> [osp|foo|]) ReadWriteMode $ \h -> do+ BS.hPut h "test"+ BS.hGetContents h+ Right "testtest" @=? r++concFile :: Assertion+concFile = do+ withSystemTempDirectory "test" $ \baseDir' -> do+ baseDir <- OSP.encodeFS baseDir'+ let fp = baseDir </> [osp|foo|]+ OSP.writeFile fp ""+ _ <- OSP.openFile fp ReadMode+ r <- try @IOException $ OSP.withFile fp WriteMode $ \h' -> do BS.hPut h' "test"+ IOError Nothing fileLockedType "withFile" fileLockedMsg Nothing (Just $ baseDir' FP.</> "foo") @==? r++concFile2 :: Assertion+concFile2 = do+ withSystemTempDirectory "test" $ \baseDir' -> do+ baseDir <- OSP.encodeFS baseDir'+ let fp = baseDir </> [osp|foo|]+ OSP.writeFile fp "h"+ _ <- OSP.openFile fp ReadMode+ r <- try @IOException $ OSP.withFile fp ReadMode BS.hGetContents+ Right "h" @=? r++concFile3 :: Assertion+concFile3 = do+ withSystemTempDirectory "test" $ \baseDir' -> do+ baseDir <- OSP.encodeFS baseDir'+ let fp = baseDir </> [osp|foo|]+ OSP.writeFile fp ""+ _ <- OSP.openFile fp WriteMode+ r <- try @IOException $ OSP.withFile fp WriteMode (flip BS.hPut "test")+ IOError Nothing fileLockedType "withFile" fileLockedMsg Nothing (Just $ baseDir' FP.</> "foo") @==? r++existingFile :: Assertion+existingFile = do+ withSystemTempDirectory "test" $ \baseDir' -> do+ baseDir <- OSP.encodeFS baseDir'+ let fp = baseDir </> [osp|foo|]+ r <- try @IOException $ OSP.openExistingFile fp ReadMode+ IOError Nothing NoSuchThing "openExistingFile" noSuchFileMsg Nothing (Just $ baseDir' FP.</> "foo") @==? r++existingFile2 :: Assertion+existingFile2 = do+ withSystemTempDirectory "test" $ \baseDir' -> do+ baseDir <- OSP.encodeFS baseDir'+ let fp = baseDir </> [osp|foo|]+ r <- try @IOException $ OSP.openExistingFile fp WriteMode+ IOError Nothing NoSuchThing "openExistingFile" noSuchFileMsg Nothing (Just $ baseDir' FP.</> "foo") @==? r++existingFile3 :: Assertion+existingFile3 = do+ withSystemTempDirectory "test" $ \baseDir' -> do+ baseDir <- OSP.encodeFS baseDir'+ let fp = baseDir </> [osp|foo|]+ r <- try @IOException $ OSP.openExistingFile fp AppendMode+ IOError Nothing NoSuchThing "openExistingFile" noSuchFileMsg Nothing (Just $ baseDir' FP.</> "foo") @==? r++existingFile4 :: Assertion+existingFile4 = do+ withSystemTempDirectory "test" $ \baseDir' -> do+ baseDir <- OSP.encodeFS baseDir'+ let fp = baseDir </> [osp|foo|]+ r <- try @IOException $ OSP.openExistingFile fp AppendMode+ IOError Nothing NoSuchThing "openExistingFile" noSuchFileMsg Nothing (Just $ baseDir' FP.</> "foo") @==? r++existingFile' :: Assertion+existingFile' = do+ withSystemTempDirectory "test" $ \baseDir' -> do+ baseDir <- OSP.encodeFS baseDir'+ let fp = baseDir </> [osp|foo|]+ OSP.writeFile fp "test"+ r <- try @IOException $ (OSP.openExistingFile fp ReadMode >>= BS.hGetContents)+ Right "test" @=? r++existingFile2' :: Assertion+existingFile2' = do+ withSystemTempDirectory "test" $ \baseDir' -> do+ baseDir <- OSP.encodeFS baseDir'+ let fp = baseDir </> [osp|foo|]+ OSP.writeFile fp "test"+ r <- try @IOException $ do+ OSP.openExistingFile fp WriteMode >>= \h -> BS.hPut h "boo" >> hClose h+ OSP.readFile (baseDir </> [osp|foo|])+ Right "boo" @=? r++existingFile3' :: Assertion+existingFile3' = do+ withSystemTempDirectory "test" $ \baseDir' -> do+ baseDir <- OSP.encodeFS baseDir'+ let fp = baseDir </> [osp|foo|]+ OSP.writeFile fp "test"+ r <- try @IOException $ do+ OSP.openExistingFile fp AppendMode >>= \h -> BS.hPut h "boo" >> hClose h+ OSP.readFile (baseDir </> [osp|foo|])+ Right "testboo" @=? r++existingFile4' :: Assertion+existingFile4' = do+ withSystemTempDirectory "test" $ \baseDir' -> do+ baseDir <- OSP.encodeFS baseDir'+ let fp = baseDir </> [osp|foo|]+ OSP.writeFile fp "testx"+ r <- try @IOException $+ OSP.openExistingFile fp ReadWriteMode >>= \h -> do+ hSetBuffering h NoBuffering+ BS.hPut h "boo"+ c <- BS.hGetSome h 5+ hSeek h AbsoluteSeek 0+ c' <- BS.hGetSome h 5+ pure (c, c')+ Right ("tx", "bootx") @=? r+++compareIOError :: forall a . (Eq a, Show a, HasCallStack) => IOException -> Either IOException a -> Assertion+compareIOError el (Left lel) = lel { ioe_handle = Nothing+ , ioe_errno = Nothing+ } @?=+ el { ioe_handle = Nothing+ , ioe_errno = Nothing+ }+compareIOError el (Right rel) = Right rel @?= (Left el :: Either IOException a)++(@==?) :: forall a . (Eq a, Show a, HasCallStack) => IOException -> Either IOException a -> Assertion+(@==?) = compareIOError++noSuchFileMsg :: String+#if defined(mingw32_HOST_OS) || defined(__MINGW32__)+noSuchFileMsg = "The system cannot find the file specified."+#else+noSuchFileMsg = "No such file or directory"+#endif++fileLockedMsg :: String+#if defined(mingw32_HOST_OS) || defined(__MINGW32__)+fileLockedMsg = "The process cannot access the file because it is being used by another process."+#else+fileLockedMsg = "file is locked"+#endif++fileLockedType :: IOErrorType+#if defined(mingw32_HOST_OS) || defined(__MINGW32__)+fileLockedType = PermissionDenied+#else+fileLockedType = ResourceBusy+#endif+
+ tests/T14.hs view
@@ -0,0 +1,22 @@+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++module Main where++import Control.Exception+import System.OsPath ((</>), osp)+import qualified System.OsPath as OSP+import qualified System.File.OsPath as OSP+import System.IO.Temp++-- Test that 'readFile' does not create a file+-- https://github.com/hasufell/file-io/issues/14+main :: IO ()+main = withSystemTempDirectory "tar-test" $ \baseDir' -> do+ baseDir <- OSP.encodeFS baseDir'+ res <- try @SomeException $ OSP.readFile (baseDir </> [osp|foo|])+ case res of+ Left e -> print e >> return ()+ Right _ -> fail "Unexpectedly found file 'foo'"+
+ tests/T15.hs view
@@ -0,0 +1,24 @@+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE OverloadedStrings #-}++module Main where++import Test.Tasty+import Test.Tasty.HUnit+import System.OsPath ((</>), osp)+import qualified System.OsPath as OSP+import qualified System.File.OsPath as OSP+import System.IO+import System.IO.Temp++-- Test that we can read concurrently without file lock+-- https://github.com/hasufell/file-io/issues/15+main :: IO ()+main = withSystemTempDirectory "tar-test" $ \baseDir' -> do+ baseDir <- OSP.encodeFS baseDir'+ OSP.writeFile (baseDir </> [osp|foo|]) ""+ defaultMain $ testGroup "All"+ [ testGroup "System.File.OsPath"+ $ map (\i -> testCase ("foo " <> show i) (OSP.openFile (baseDir </> [osp|foo|]) ReadMode >>= hClose)) ([0..99] :: [Int])+ ]+
+ tests/T15Win.hs view
@@ -0,0 +1,62 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE OverloadedStrings #-}++module Main where++#if defined(mingw32_HOST_OS) || defined(__MINGW32__)++import Test.Tasty+import Test.Tasty.HUnit+import qualified System.File.PlatformPath as PFP+import System.IO+import System.IO.Temp++import Control.Exception (bracketOnError)+import Data.Bits+import System.OsPath.Windows ( WindowsPath, pstr )+import qualified System.OsPath.Windows as WS++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++-- Test that we can read concurrently without file lock+-- https://github.com/hasufell/file-io/issues/15+main :: IO ()+main = withSystemTempDirectory "tar-test" $ \baseDir' -> do+ baseDir <- WS.encodeFS baseDir'+ PFP.writeFile (baseDir WS.</> [pstr|foo|]) ""+ defaultMain $ testGroup "All"+ [ testGroup "System.File.OsPath (Windows)" $+ (map (\i -> testCase ("foo (Win32 API) " <> show i) (openFile32 (baseDir WS.</> [pstr|foo|]) ReadMode >>= Win32.closeHandle)) ([0..99] :: [Int]))+ ]++openFile32 :: WindowsPath -> IOMode -> IO Win32.HANDLE+openFile32 fp iomode =+ WS.createFile+ fp+ Win32.gENERIC_READ+ Win32.fILE_SHARE_READ+ Nothing+ Win32.oPEN_EXISTING+#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++#else++main :: IO ()+main = putStrLn "Skipping test on windows"++#endif+
+ tests/T8.hs view
@@ -0,0 +1,26 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}++module Main where++import Control.Concurrent+import Control.Monad+import System.File.OsPath+import System.OsPath+import System.IO.Temp++import qualified Data.ByteString.Lazy as BL+import qualified System.OsPath as OSP+import qualified System.File.OsPath as OSP++main :: IO ()+main = withSystemTempDirectory "test" $ \baseDir' -> do+ let fn = [osp|test.txt|]+ baseDir <- OSP.encodeFS baseDir'+ let fp = baseDir OSP.</> fn+ OSP.writeFile fp ""++ replicateM_ 100000 $ do+ thr <- forkIO (System.File.OsPath.readFile fp >>= BL.putStr)+ threadDelay 1+ void $ killThread thr
− windows/System/File/Platform.hs
@@ -1,111 +0,0 @@-{-# LANGUAGE CPP #-}--module System.File.Platform where--import Control.Exception (bracketOnError)-import Data.Bits-import System.IO (IOMode(..), Handle)-import System.OsPath.Windows ( WindowsPath )--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 :: WindowsPath -> 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 :: WindowsPath -> 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-
+ windows/System/File/Platform.hsc view
@@ -0,0 +1,131 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE TypeApplications #-}++module System.File.Platform where++import Control.Exception (bracketOnError, try, SomeException, onException)+import Data.Bits+import System.IO (IOMode(..), Handle)+import System.OsPath.Windows ( WindowsPath )+import qualified System.OsPath.Windows as WS+import Foreign.C.Types+import Foreign.Ptr (ptrToIntPtr)++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+#else+import GHC.IO.Handle.FD (fdToHandle')+#include <fcntl.h>+#endif++-- | Open a file and return the 'Handle'.+openFile :: WindowsPath -> 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+#if defined(__IO_MANAGER_WINIO__)+ toHandle h = (`onException` Win32.closeHandle h) $ do+ when (iomode == AppendMode ) $ void $ Win32.setFilePointerEx h 0 Win32.fILE_END+ Win32.hANDLEToHandle h+#else+ toHandle h = (`onException` Win32.closeHandle h) $ do+ when (iomode == AppendMode ) $ void $ Win32.setFilePointerEx h 0 Win32.fILE_END+ fd <- _open_osfhandle (fromIntegral (ptrToIntPtr h)) (#const _O_BINARY)+ fp' <- either (const (fmap WS.toChar . WS.unpack $ fp)) id <$> try @SomeException (WS.decodeFS fp)+ fdToHandle' fd Nothing False fp' iomode True+#endif+ 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.cREATE_ALWAYS+ AppendMode -> Win32.oPEN_ALWAYS+ ReadWriteMode -> Win32.oPEN_ALWAYS++ shareMode = case iomode of+ ReadMode -> Win32.fILE_SHARE_READ+ 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 :: WindowsPath -> 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 -> Win32.fILE_SHARE_READ+ WriteMode -> writeShareMode+ AppendMode -> writeShareMode+ ReadWriteMode -> maxShareMode++#if !defined(__IO_MANAGER_WINIO__)+foreign import ccall "_open_osfhandle"+ _open_osfhandle :: CIntPtr -> CInt -> IO CInt+#endif+