From cbe0caf78508b3700ba0a288d20360b0dc3c73fd Mon Sep 17 00:00:00 2001 From: Julian Ospald Date: Sun, 21 Jul 2024 18:42:38 +0800 Subject: [PATCH 01/10] Support openTempFile and friends, fixes #2 --- System/File/OsPath.hs | 4 ++ System/File/OsPath/Internal.hs | 88 +++++++++++++++++++++++++++- posix/System/File/Platform.hs | 59 ++++++++++++++++++- tests/Properties.hs | 46 ++++++++++++++- windows/System/File/Platform.hsc | 99 +++++++++++++++++++++++++++----- 5 files changed, 276 insertions(+), 20 deletions(-) diff --git a/System/File/OsPath.hs b/System/File/OsPath.hs index cfbc507..6d8057f 100644 --- a/System/File/OsPath.hs +++ b/System/File/OsPath.hs @@ -23,6 +23,10 @@ module System.File.OsPath ( , appendFile' , openFile , openExistingFile +, openTempFile +, openBinaryTempFile +, openTempFileWithDefaultPermissions +, openBinaryTempFileWithDefaultPermissions ) where diff --git a/System/File/OsPath/Internal.hs b/System/File/OsPath/Internal.hs index f7e9cce..1b346e2 100644 --- a/System/File/OsPath/Internal.hs +++ b/System/File/OsPath/Internal.hs @@ -1,12 +1,14 @@ {-# LANGUAGE TypeApplications #-} {-# LANGUAGE BangPatterns #-} +{-# LANGUAGE ViewPatterns #-} +{-# LANGUAGE QuasiQuotes #-} module System.File.OsPath.Internal where import qualified System.File.Platform as P -import Prelude ((.), ($), String, IO, ioError, pure, either, const, flip, Maybe(..), fmap, (<$>), id, Bool(..), FilePath, (++), return, show, (>>=)) +import Prelude ((.), ($), String, IO, ioError, pure, either, const, flip, Maybe(..), fmap, (<$>), id, Bool(..), FilePath, (++), return, show, (>>=), (==), otherwise, errorWithoutStackTrace) import GHC.IO (catchException) import GHC.IO.Exception (IOException(..)) import GHC.IO.Handle (hClose_help) @@ -18,11 +20,15 @@ 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.OsString (osstr) import System.OsPath as OSP import System.OsString.Internal.Types import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy as BSL +import qualified System.OsString as OSS +import System.Posix.Types (CMode) +import GHC.Base (failIO) -- | Like 'openFile', but open the file in binary mode. -- On Windows, reading a file in text mode (which is the default) @@ -127,6 +133,56 @@ openFileWithCloseOnExec osfp iomode = augmentError "openFileWithCloseOnExec" osf openExistingFileWithCloseOnExec :: OsPath -> IOMode -> IO Handle openExistingFileWithCloseOnExec osfp iomode = augmentError "openExistingFileWithCloseOnExec" osfp $ withOpenFile' osfp iomode False True True pure False + +-- | The function creates a temporary file in ReadWrite mode. +-- The created file isn\'t deleted automatically, so you need to delete it manually. +-- +-- The file is created with permissions such that only the current +-- user can read\/write it. +-- +-- With some exceptions (see below), the file will be created securely +-- in the sense that an attacker should not be able to cause +-- openTempFile to overwrite another file on the filesystem using your +-- credentials, by putting symbolic links (on Unix) in the place where +-- the temporary file is to be created. On Unix the @O_CREAT@ and +-- @O_EXCL@ flags are used to prevent this attack, but note that +-- @O_EXCL@ is sometimes not supported on NFS filesystems, so if you +-- rely on this behaviour it is best to use local filesystems only. +-- +-- @since 0.1.3 +openTempFile :: OsPath -- ^ Directory in which to create the file + -> OsString -- ^ File name template. If the template is \"foo.ext\" then + -- the created file will be \"fooXXX.ext\" where XXX is some + -- random number. Note that this should not contain any path + -- separator characters. On Windows, the template prefix may + -- be truncated to 3 chars, e.g. \"foobar.ext\" will be + -- \"fooXXX.ext\". + -> IO (OsPath, Handle) +openTempFile tmp_dir template = openTempFile' "openTempFile" tmp_dir template False 0o600 + +-- | Like 'openTempFile', but opens the file in binary mode. See 'openBinaryFile' for more comments. +-- +-- @since 0.1.3 +openBinaryTempFile :: OsPath -> OsString -> IO (OsPath, Handle) +openBinaryTempFile tmp_dir template + = openTempFile' "openBinaryTempFile" tmp_dir template True 0o600 + +-- | Like 'openTempFile', but uses the default file permissions +-- +-- @since 0.1.3 +openTempFileWithDefaultPermissions :: OsPath -> OsString + -> IO (OsPath, Handle) +openTempFileWithDefaultPermissions tmp_dir template + = openTempFile' "openTempFileWithDefaultPermissions" tmp_dir template False 0o666 + +-- | Like 'openBinaryTempFile', but uses the default file permissions +-- +-- @since 0.1.3 +openBinaryTempFileWithDefaultPermissions :: OsPath -> OsString + -> IO (OsPath, Handle) +openBinaryTempFileWithDefaultPermissions tmp_dir template + = openTempFile' "openBinaryTempFileWithDefaultPermissions" tmp_dir template True 0o666 + -- --------------------------------------------------------------------------- -- Internals @@ -173,3 +229,33 @@ addFilePathToIOError fun fp ioe = unsafePerformIO $ do augmentError :: String -> OsPath -> IO a -> IO a augmentError str osfp = flip catchException (ioError . addFilePathToIOError str osfp) + +openTempFile' :: String -> OsPath -> OsString -> Bool -> CMode + -> IO (OsPath, Handle) +openTempFile' loc (OsString tmp_dir) template@(OsString tmpl) binary mode + | OSS.any (== OSP.pathSeparator) template + = failIO $ "openTempFile': Template string must not contain path separator characters: " ++ P.lenientDecode tmpl + | otherwise = do + (fp, hdl) <- P.findTempName (prefix, suffix) loc tmp_dir mode + when binary $ hSetBinaryMode hdl True + pure (OsString fp, hdl) + where + -- We split off the last extension, so we can use .foo.ext files + -- for temporary files (hidden on Unix OSes). Unfortunately we're + -- below filepath in the hierarchy here. + (OsString prefix, OsString suffix) = + case OSS.break (== OSS.unsafeFromChar '.') $ OSS.reverse template of + -- First case: template contains no '.'s. Just re-reverse it. + (rev_suffix, [osstr||]) -> (OSS.reverse rev_suffix, OSS.empty) + -- Second case: template contains at least one '.'. Strip the + -- dot from the prefix and prepend it to the suffix (if we don't + -- do this, the unique number will get added after the '.' and + -- thus be part of the extension, which is wrong.) + (rev_suffix, xs) + | (h:rest) <- OSS.unpack xs + , h == unsafeFromChar '.' -> (OSS.reverse (OSS.pack rest), OSS.cons (unsafeFromChar '.') $ OSS.reverse rev_suffix) + -- Otherwise, something is wrong, because (break (== '.')) should + -- always return a pair with either the empty string or a string + -- beginning with '.' as the second component. + _ -> errorWithoutStackTrace "bug in System.IO.openTempFile" + diff --git a/posix/System/File/Platform.hs b/posix/System/File/Platform.hs index 760f22b..3d81726 100644 --- a/posix/System/File/Platform.hs +++ b/posix/System/File/Platform.hs @@ -1,7 +1,9 @@ +{-# LANGUAGE TupleSections #-} {-# LANGUAGE TypeApplications #-} module System.File.Platform where +import Data.Either (fromRight) import Control.Exception (try, onException, SomeException) import GHC.IO.Handle.FD (fdToHandle') import System.IO (IOMode(..), Handle) @@ -10,10 +12,17 @@ import System.Posix.IO.PosixString ( defaultFileFlags, openFd, closeFd, - OpenFileFlags(noctty, nonBlock, creat, append, trunc, cloexec), + OpenFileFlags(noctty, nonBlock, creat, append, trunc, cloexec, exclusive), OpenMode(ReadWrite, ReadOnly, WriteOnly) ) -import System.OsPath.Posix ( PosixPath ) +import System.OsPath.Posix ( PosixPath, PosixString, () ) import qualified System.OsPath.Posix as PS +import Data.IORef (IORef, newIORef) +import System.Posix (CMode) +import System.IO (utf8, latin1) +import System.IO.Unsafe (unsafePerformIO) +import System.Posix.Internals (c_getpid) +import GHC.IORef (atomicModifyIORef'_) +import Foreign.C (getErrno, eEXIST, errnoToIOError) -- | Open a file and return the 'Handle'. openFile :: PosixPath -> IOMode -> IO Handle @@ -43,7 +52,7 @@ openExistingFile_ df fp iomode = fdToHandle_ iomode fp =<< case iomode of 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) + fp' <- fromRight (fmap PS.toChar . PS.unpack $ fp) <$> try @SomeException (PS.decodeFS fp) fdToHandle' fd Nothing False fp' iomode True openFileWithCloseOnExec :: PosixPath -> IOMode -> IO Handle @@ -58,3 +67,47 @@ defaultFileFlags' = defaultFileFlags { noctty = True, nonBlock = True } defaultExistingFileFlags :: OpenFileFlags defaultExistingFileFlags = defaultFileFlags { noctty = True, nonBlock = True, creat = Nothing } +findTempName :: (PosixString, PosixString) + -> String + -> PosixPath + -> CMode + -> IO (PosixPath, Handle) +findTempName (prefix, suffix) loc tmp_dir mode = go + where + go = do + rs <- rand_string + let filename = prefix <> rs <> suffix + filepath = tmp_dir filename + fd <- openTempFile_ filepath mode + if fd < 0 + then do + errno <- getErrno + case errno of + _ | errno == eEXIST -> go + _ -> do + let tmp_dir' = lenientDecode tmp_dir + ioError (errnoToIOError loc errno Nothing (Just tmp_dir')) + else fmap (filepath,) $ fdToHandle_ ReadWriteMode filepath fd + + openTempFile_ :: PosixPath -> CMode -> IO Fd + openTempFile_ fp cmode = openFd fp ReadWrite defaultFileFlags' { creat = Just cmode, nonBlock = True, noctty = True, exclusive = True } + +tempCounter :: IORef Int +tempCounter = unsafePerformIO $ newIORef 0 +{-# NOINLINE tempCounter #-} + +-- build large digit-alike number +rand_string :: IO PosixString +rand_string = do + r1 <- c_getpid + (r2, _) <- atomicModifyIORef'_ tempCounter (+1) + return $ PS.pack $ fmap (PS.unsafeFromChar) (show r1 ++ "-" ++ show r2) + +lenientDecode :: PosixString -> String +lenientDecode ps = let utf8' = PS.decodeWith utf8 ps + latin1' = PS.decodeWith latin1 ps + in case (utf8', latin1') of + (Right s, ~_) -> s + (_, Right s) -> s + (Left _, Left _) -> error "lenientDecode: failed to decode" + diff --git a/tests/Properties.hs b/tests/Properties.hs index 6c71131..61fd56f 100644 --- a/tests/Properties.hs +++ b/tests/Properties.hs @@ -10,7 +10,7 @@ import Control.Exception import qualified System.FilePath as FP import Test.Tasty import Test.Tasty.HUnit -import System.OsPath ((), osp) +import System.OsPath ((), osp, OsPath, OsString) import qualified System.OsPath as OSP import qualified System.File.OsPath as OSP import GHC.IO.Exception (IOErrorType(..), IOException(..)) @@ -40,6 +40,18 @@ main = defaultMain $ testGroup "All" , testCase "openExistingFile yes (Write)" existingFile2' , testCase "openExistingFile yes (Append)" existingFile3' , testCase "openExistingFile yes (ReadWrite)" existingFile4' + , testCase "openTempFile" (openTempFile2 OSP.openTempFile) + , testCase "openTempFile (reopen file)" (openTempFile1 OSP.openTempFile) + , testCase "openTempFile (filepaths different)" (openTempFile3 OSP.openTempFile) + , testCase "openBinaryTempFile" (openTempFile2 OSP.openBinaryTempFile) + , testCase "openBinaryTempFile (reopen file)" (openTempFile1 OSP.openBinaryTempFile) + , testCase "openBinaryTempFile (filepaths different)" (openTempFile3 OSP.openBinaryTempFile) + , testCase "openTempFileWithDefaultPermissions" (openTempFile2 OSP.openTempFileWithDefaultPermissions) + , testCase "openTempFileWithDefaultPermissions (reopen file)" (openTempFile1 OSP.openTempFileWithDefaultPermissions) + , testCase "openTempFileWithDefaultPermissions (filepaths different)" (openTempFile3 OSP.openTempFileWithDefaultPermissions) + , testCase "openBinaryTempFileWithDefaultPermissions" (openTempFile2 OSP.openBinaryTempFileWithDefaultPermissions) + , testCase "openBinaryTempFileWithDefaultPermissions (reopen file)" (openTempFile1 OSP.openBinaryTempFileWithDefaultPermissions) + , testCase "openBinaryTempFileWithDefaultPermissions (filepaths different)" (openTempFile3 OSP.openBinaryTempFileWithDefaultPermissions) ] ] @@ -215,6 +227,38 @@ existingFile4' = do pure (c, c') Right ("tx", "bootx") @=? r +openTempFile1 :: (OsPath -> OsString -> IO (OsPath, Handle)) -> Assertion +openTempFile1 open = do + withSystemTempDirectory "test" $ \baseDir' -> do + baseDir <- OSP.encodeFS baseDir' + let file = [osp|foo.ext|] + (fp, h') <- open baseDir file + hClose h' + r <- try @IOException $ do + OSP.openExistingFile fp ReadWriteMode >>= \h -> BS.hPut h "boo" >> hClose h + OSP.readFile fp + Right "boo" @=? r + +openTempFile2 :: (OsPath -> OsString -> IO (OsPath, Handle)) -> Assertion +openTempFile2 open = do + withSystemTempDirectory "test" $ \baseDir' -> do + baseDir <- OSP.encodeFS baseDir' + let file = [osp|foo.ext|] + (fp, h) <- open baseDir file + r <- try @IOException $ do + BS.hPut h "boo" >> hClose h + OSP.readFile fp + Right "boo" @=? r + +openTempFile3 :: (OsPath -> OsString -> IO (OsPath, Handle)) -> Assertion +openTempFile3 open = do + withSystemTempDirectory "test" $ \baseDir' -> do + baseDir <- OSP.encodeFS baseDir' + let file = [osp|foo.ext|] + (fp, _) <- open baseDir file + (fp', _) <- open baseDir file + (fp /= fp') @? "Filepaths are different" + compareIOError :: forall a . (Eq a, Show a, HasCallStack) => IOException -> Either IOException a -> Assertion compareIOError el (Left lel) = lel { ioe_handle = Nothing diff --git a/windows/System/File/Platform.hsc b/windows/System/File/Platform.hsc index 85b34d3..cf375af 100644 --- a/windows/System/File/Platform.hsc +++ b/windows/System/File/Platform.hsc @@ -1,5 +1,6 @@ {-# LANGUAGE CPP #-} {-# LANGUAGE TypeApplications #-} +{-# LANGUAGE QuasiQuotes #-} module System.File.Platform where @@ -9,10 +10,13 @@ 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 System.OsString.Encoding +import qualified System.OsString.Windows as WS hiding (decodeFS) +import System.OsString.Windows ( pstr, WindowsString ) import qualified System.Win32 as Win32 import qualified System.Win32.WindowsString.File as WS +import System.Win32.WindowsString.Types (withTString, withFilePath, peekTString) import Control.Monad (when, void) #if defined(__IO_MANAGER_WINIO__) import GHC.IO.SubSystem @@ -20,6 +24,17 @@ import GHC.IO.SubSystem import GHC.IO.Handle.FD (fdToHandle') #include #endif +import GHC.IORef (atomicModifyIORef'_) +import Foreign.C (getErrno, errnoToIOError) +import Data.IORef (IORef, newIORef) +import Foreign.C.String +import Foreign.Ptr +import Foreign.Marshal.Alloc +import Foreign.Marshal.Utils (with) +import Foreign.Storable +import System.Posix.Types (CMode) +import System.IO.Unsafe (unsafePerformIO) +import System.Posix.Internals (c_getpid, o_EXCL) -- | Open a file and return the 'Handle'. openFile :: WindowsPath -> IOMode -> IO Handle @@ -40,19 +55,8 @@ openFile fp iomode = bracketOnError #endif Nothing) Win32.closeHandle - toHandle + (toHandle fp iomode) 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 @@ -101,9 +105,9 @@ openExistingFile fp iomode = bracketOnError #endif Nothing) Win32.closeHandle - toHandle + toHandle' where - toHandle h = do + toHandle' h = do when (iomode == AppendMode ) $ void $ Win32.setFilePointerEx h 0 Win32.fILE_END Win32.hANDLEToHandle h accessMode = case iomode of @@ -135,3 +139,68 @@ openFileWithCloseOnExec = openFile openExistingFileWithCloseOnExec :: WindowsPath -> IOMode -> IO Handle openExistingFileWithCloseOnExec = openExistingFile +findTempName :: (WindowsString, WindowsString) + -> String + -> WindowsPath + -> CMode + -> IO (WindowsPath, Handle) +findTempName (prefix, suffix) loc tmp_dir mode = go + where + go = do + let label = if WS.null prefix then [pstr|ghc|] else prefix + withFilePath tmp_dir $ \c_tmp_dir -> + withTString label $ \c_template -> + withTString suffix $ \c_suffix -> + with nullPtr $ \c_ptr -> do + res <- c_createUUIDTempFileErrNo c_tmp_dir c_template c_suffix c_ptr + if not res + then do errno <- getErrno + ioError (errnoToIOError loc errno Nothing (Just $ lenientDecode tmp_dir)) + else do c_p <- peek c_ptr + filename <- peekTString c_p + free c_p + let flags = fromIntegral mode .&. o_EXCL + handleResultsWinIO filename (flags == o_EXCL) + + handleResultsWinIO filename excl = do + h <- (if excl then openExistingFile else openFile) filename ReadWriteMode + return (filename, h) + +foreign import ccall "__createUUIDTempFileErrNo" c_createUUIDTempFileErrNo + :: CWString -> CWString -> CWString -> Ptr CWString -> IO Bool + + + +tempCounter :: IORef Int +tempCounter = unsafePerformIO $ newIORef 0 +{-# NOINLINE tempCounter #-} + +-- build large digit-alike number +rand_string :: IO WindowsPath +rand_string = do + r1 <- c_getpid + (r2, _) <- atomicModifyIORef'_ tempCounter (+1) + return $ WS.pack $ fmap (WS.unsafeFromChar) (show r1 ++ "-" ++ show r2) + +lenientDecode :: WindowsString -> String +lenientDecode ws = let utf16le' = WS.decodeWith utf16le_b ws + ucs2' = WS.decodeWith ucs2le ws + in case (utf16le', ucs2') of + (Right s, ~_) -> s + (_, Right s) -> s + (Left _, Left _) -> error "lenientDecode: failed to decode" + + +toHandle :: WindowsPath -> IOMode -> Win32.HANDLE -> IO Handle +#if defined(__IO_MANAGER_WINIO__) +toHandle _ iomode h = (`onException` Win32.closeHandle h) $ do + when (iomode == AppendMode ) $ void $ Win32.setFilePointerEx h 0 Win32.fILE_END + Win32.hANDLEToHandle h +#else +toHandle fp iomode 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 + From 8ec0a0cabeb6acf1b20dbf8116fba847421b288c Mon Sep 17 00:00:00 2001 From: Julian Ospald Date: Mon, 22 Jul 2024 19:02:26 +0800 Subject: [PATCH 02/10] Use portable toHandle for openExistingFile too --- windows/System/File/Platform.hsc | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/windows/System/File/Platform.hsc b/windows/System/File/Platform.hsc index cf375af..abaeb04 100644 --- a/windows/System/File/Platform.hsc +++ b/windows/System/File/Platform.hsc @@ -105,11 +105,8 @@ openExistingFile fp iomode = bracketOnError #endif Nothing) Win32.closeHandle - toHandle' + (toHandle fp iomode) 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 From dd819a8ecf3fe997972e5449bc440f57dded6218 Mon Sep 17 00:00:00 2001 From: Julian Ospald Date: Mon, 22 Jul 2024 19:24:55 +0800 Subject: [PATCH 03/10] Bump to 0.1.3 --- CHANGELOG.md | 4 ++++ file-io.cabal | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63ad789..1650470 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Revision history for file-io +## 0.1.3 -- 2024-??-?? + +* add `openTempFile` , `openBinaryTempFile` , `openTempFileWithDefaultPermissions` and `openBinaryTempFileWithDefaultPermissions` wrt [#2](https://github.com/hasufell/file-io/issues/2) + ## 0.1.2 -- 2024-05-27 * expose internals via `.Internal` modules diff --git a/file-io.cabal b/file-io.cabal index ba341ec..f379c50 100644 --- a/file-io.cabal +++ b/file-io.cabal @@ -1,6 +1,6 @@ cabal-version: 2.4 name: file-io -version: 0.1.2 +version: 0.1.3 synopsis: Basic file IO operations via 'OsPath' description: Basic file IO operations like Prelude, but for 'OsPath'. homepage: https://github.com/hasufell/file-io From e73e24c71500ad2f93bab300ab993f4c3763b9ff Mon Sep 17 00:00:00 2001 From: Julian Ospald Date: Mon, 22 Jul 2024 19:36:09 +0800 Subject: [PATCH 04/10] Fix build with GHC-8.8 --- System/File/OsPath/Internal.hs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/System/File/OsPath/Internal.hs b/System/File/OsPath/Internal.hs index 1b346e2..13026b8 100644 --- a/System/File/OsPath/Internal.hs +++ b/System/File/OsPath/Internal.hs @@ -8,7 +8,7 @@ module System.File.OsPath.Internal where import qualified System.File.Platform as P -import Prelude ((.), ($), String, IO, ioError, pure, either, const, flip, Maybe(..), fmap, (<$>), id, Bool(..), FilePath, (++), return, show, (>>=), (==), otherwise, errorWithoutStackTrace) +import Prelude ((.), ($), String, IO, ioError, pure, either, const, flip, Maybe(..), fmap, (<$>), id, Bool(..), FilePath, (++), return, show, (>>=), (==), otherwise, errorWithoutStackTrace, userError) import GHC.IO (catchException) import GHC.IO.Exception (IOException(..)) import GHC.IO.Handle (hClose_help) @@ -17,7 +17,7 @@ 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 Control.Exception (SomeException, try, evaluate, mask, onException, throwIO) import System.IO (IOMode(..), hSetBinaryMode, hClose) import System.IO.Unsafe (unsafePerformIO) import System.OsString (osstr) @@ -28,7 +28,6 @@ import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy as BSL import qualified System.OsString as OSS import System.Posix.Types (CMode) -import GHC.Base (failIO) -- | Like 'openFile', but open the file in binary mode. -- On Windows, reading a file in text mode (which is the default) @@ -234,7 +233,7 @@ openTempFile' :: String -> OsPath -> OsString -> Bool -> CMode -> IO (OsPath, Handle) openTempFile' loc (OsString tmp_dir) template@(OsString tmpl) binary mode | OSS.any (== OSP.pathSeparator) template - = failIO $ "openTempFile': Template string must not contain path separator characters: " ++ P.lenientDecode tmpl + = throwIO $ userError $ "openTempFile': Template string must not contain path separator characters: " ++ P.lenientDecode tmpl | otherwise = do (fp, hdl) <- P.findTempName (prefix, suffix) loc tmp_dir mode when binary $ hSetBinaryMode hdl True From 31438b4c1dc3fbcfd83f72e68adf588de2c35b3f Mon Sep 17 00:00:00 2001 From: Julian Ospald Date: Mon, 22 Jul 2024 19:36:23 +0800 Subject: [PATCH 05/10] Drop support for GHC <= 8.6 --- .github/workflows/test.yaml | 2 +- file-io.cabal | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index d20f6db..3e05cc9 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -16,7 +16,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - ghc: ['8.6', '8.8', '8.10', '9.0', '9.2', '9.4', '9.6', '9.8'] + ghc: ['8.8', '8.10', '9.0', '9.2', '9.4', '9.6', '9.8'] include: - os: macOS-latest ghc: '9.4' diff --git a/file-io.cabal b/file-io.cabal index f379c50..f33d30d 100644 --- a/file-io.cabal +++ b/file-io.cabal @@ -17,8 +17,7 @@ tested-with: GHC==9.8.1, GHC==9.2.8, GHC==9.0.2, GHC==8.10.7, - GHC==8.8.4, - GHC==8.6.5 + GHC==8.8.4 source-repository head type: git @@ -42,7 +41,7 @@ library hs-source-dirs: . build-depends: - , base >=4.12 && <5 + , base >=4.13.0.0 && <5 , bytestring >=0.11.3.0 , deepseq From 05528546e549af36c71f60e7a05014647934ac5b Mon Sep 17 00:00:00 2001 From: Julian Ospald Date: Mon, 22 Jul 2024 20:09:32 +0800 Subject: [PATCH 06/10] Fix build with GHC >= 9.6 --- System/File/OsPath/Internal.hs | 42 +++++++++++++++++++++++++++----- posix/System/File/Platform.hs | 27 ++++++++++++++++++++ windows/System/File/Platform.hsc | 40 +++++++++++++++++++++++++++--- 3 files changed, 100 insertions(+), 9 deletions(-) diff --git a/System/File/OsPath/Internal.hs b/System/File/OsPath/Internal.hs index 13026b8..6acf28e 100644 --- a/System/File/OsPath/Internal.hs +++ b/System/File/OsPath/Internal.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE CPP #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE BangPatterns #-} {-# LANGUAGE ViewPatterns #-} @@ -8,7 +9,7 @@ module System.File.OsPath.Internal where import qualified System.File.Platform as P -import Prelude ((.), ($), String, IO, ioError, pure, either, const, flip, Maybe(..), fmap, (<$>), id, Bool(..), FilePath, (++), return, show, (>>=), (==), otherwise, errorWithoutStackTrace, userError) +import Prelude ((.), ($), String, IO, ioError, pure, either, const, flip, Maybe(..), fmap, (<$>), id, Bool(..), FilePath, (++), return, show, (>>=), (==), otherwise, errorWithoutStackTrace, userError, mempty) import GHC.IO (catchException) import GHC.IO.Exception (IOException(..)) import GHC.IO.Handle (hClose_help) @@ -20,7 +21,6 @@ import Control.DeepSeq (force) import Control.Exception (SomeException, try, evaluate, mask, onException, throwIO) import System.IO (IOMode(..), hSetBinaryMode, hClose) import System.IO.Unsafe (unsafePerformIO) -import System.OsString (osstr) import System.OsPath as OSP import System.OsString.Internal.Types @@ -28,6 +28,9 @@ import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy as BSL import qualified System.OsString as OSS import System.Posix.Types (CMode) +#if !MIN_VERSION_filepath(1, 5, 0) +import Data.Coerce +#endif -- | Like 'openFile', but open the file in binary mode. -- On Windows, reading a file in text mode (which is the default) @@ -232,7 +235,7 @@ augmentError str osfp = flip catchException (ioError . addFilePathToIOError str openTempFile' :: String -> OsPath -> OsString -> Bool -> CMode -> IO (OsPath, Handle) openTempFile' loc (OsString tmp_dir) template@(OsString tmpl) binary mode - | OSS.any (== OSP.pathSeparator) template + | any_ (== OSP.pathSeparator) template = throwIO $ userError $ "openTempFile': Template string must not contain path separator characters: " ++ P.lenientDecode tmpl | otherwise = do (fp, hdl) <- P.findTempName (prefix, suffix) loc tmp_dir mode @@ -243,18 +246,45 @@ openTempFile' loc (OsString tmp_dir) template@(OsString tmpl) binary mode -- for temporary files (hidden on Unix OSes). Unfortunately we're -- below filepath in the hierarchy here. (OsString prefix, OsString suffix) = - case OSS.break (== OSS.unsafeFromChar '.') $ OSS.reverse template of + case break_ (== OSS.unsafeFromChar '.') $ reverse_ template of -- First case: template contains no '.'s. Just re-reverse it. - (rev_suffix, [osstr||]) -> (OSS.reverse rev_suffix, OSS.empty) + (rev_suffix, xs) + | xs == mempty -> (reverse_ rev_suffix, mempty) -- Second case: template contains at least one '.'. Strip the -- dot from the prefix and prepend it to the suffix (if we don't -- do this, the unique number will get added after the '.' and -- thus be part of the extension, which is wrong.) (rev_suffix, xs) | (h:rest) <- OSS.unpack xs - , h == unsafeFromChar '.' -> (OSS.reverse (OSS.pack rest), OSS.cons (unsafeFromChar '.') $ OSS.reverse rev_suffix) + , h == unsafeFromChar '.' -> (reverse_ (OSS.pack rest), cons_ (unsafeFromChar '.') $ reverse_ rev_suffix) -- Otherwise, something is wrong, because (break (== '.')) should -- always return a pair with either the empty string or a string -- beginning with '.' as the second component. _ -> errorWithoutStackTrace "bug in System.IO.openTempFile" +#if MIN_VERSION_filepath(1, 5, 0) +any_ :: (OsChar -> Bool) -> OsString -> Bool +any_ = OSS.any + +cons_ :: OsChar -> OsString -> OsString +cons_ = OSS.cons + +break_ :: (OsChar -> Bool) -> OsString -> (OsString, OsString) +break_ = OSS.break + +reverse_ :: OsString -> OsString +reverse_ = OSS.reverse +#else +any_ :: (OsChar -> Bool) -> OsString -> Bool +any_ = coerce P.any_ + +cons_ :: OsChar -> OsString -> OsString +cons_ = coerce P.cons_ + +break_ :: (OsChar -> Bool) -> OsString -> (OsString, OsString) +break_ = coerce P.break_ + +reverse_ :: OsString -> OsString +reverse_ = coerce P.reverse_ +#endif + diff --git a/posix/System/File/Platform.hs b/posix/System/File/Platform.hs index 3d81726..85a28a2 100644 --- a/posix/System/File/Platform.hs +++ b/posix/System/File/Platform.hs @@ -1,5 +1,7 @@ +{-# LANGUAGE CPP #-} {-# LANGUAGE TupleSections #-} {-# LANGUAGE TypeApplications #-} +{-# LANGUAGE PackageImports #-} module System.File.Platform where @@ -24,6 +26,15 @@ import System.Posix.Internals (c_getpid) import GHC.IORef (atomicModifyIORef'_) import Foreign.C (getErrno, eEXIST, errnoToIOError) +#if MIN_VERSION_filepath(1, 5, 0) +import "os-string" System.OsString.Internal.Types (PosixString(..), PosixChar(..)) +import qualified "os-string" System.OsString.Data.ByteString.Short as BC +#else +import Data.Coerce (coerce) +import "filepath" System.OsString.Internal.Types (PosixString(..), PosixChar(..)) +import qualified "filepath" System.OsPath.Data.ByteString.Short as BC +#endif + -- | Open a file and return the 'Handle'. openFile :: PosixPath -> IOMode -> IO Handle openFile = openFile_ defaultFileFlags' @@ -111,3 +122,19 @@ lenientDecode ps = let utf8' = PS.decodeWith utf8 ps (_, Right s) -> s (Left _, Left _) -> error "lenientDecode: failed to decode" +#if !MIN_VERSION_filepath(1, 5, 0) + +break_ :: (PosixChar -> Bool) -> PosixString -> (PosixString, PosixString) +break_ = coerce BC.break + +reverse_ :: PosixString -> PosixString +reverse_ = coerce BC.reverse + +any_ :: (PosixChar -> Bool) -> PosixString -> Bool +any_ = coerce BC.any + +cons_ :: PosixChar -> PosixString -> PosixString +cons_ = coerce BC.cons + +#endif + diff --git a/windows/System/File/Platform.hsc b/windows/System/File/Platform.hsc index abaeb04..93cd88f 100644 --- a/windows/System/File/Platform.hsc +++ b/windows/System/File/Platform.hsc @@ -1,6 +1,7 @@ {-# LANGUAGE CPP #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE PackageImports #-} module System.File.Platform where @@ -11,12 +12,14 @@ import System.OsPath.Windows ( WindowsPath ) import qualified System.OsPath.Windows as WS import Foreign.C.Types -import System.OsString.Encoding import qualified System.OsString.Windows as WS hiding (decodeFS) import System.OsString.Windows ( pstr, WindowsString ) import qualified System.Win32 as Win32 import qualified System.Win32.WindowsString.File as WS -import System.Win32.WindowsString.Types (withTString, withFilePath, peekTString) +import System.Win32.WindowsString.Types (withTString, peekTString) +#if MIN_VERSION_Win32(2, 14, 0) +import System.Win32.WindowsString.Types (withFilePath) +#endif import Control.Monad (when, void) #if defined(__IO_MANAGER_WINIO__) import GHC.IO.SubSystem @@ -36,6 +39,17 @@ import System.Posix.Types (CMode) import System.IO.Unsafe (unsafePerformIO) import System.Posix.Internals (c_getpid, o_EXCL) +#if MIN_VERSION_filepath(1, 5, 0) +import System.OsString.Encoding +import "os-string" System.OsString.Internal.Types (WindowsString(..), WindowsChar(..)) +import qualified "os-string" System.OsString.Data.ByteString.Short as BC +#else +import Data.Coerce (coerce) +import System.OsPath.Encoding +import "filepath" System.OsString.Internal.Types (WindowsString(..), WindowsChar(..)) +import qualified "filepath" System.OsPath.Data.ByteString.Short.Word16 as BC +#endif + -- | Open a file and return the 'Handle'. openFile :: WindowsPath -> IOMode -> IO Handle openFile fp iomode = bracketOnError @@ -144,8 +158,12 @@ findTempName :: (WindowsString, WindowsString) findTempName (prefix, suffix) loc tmp_dir mode = go where go = do - let label = if WS.null prefix then [pstr|ghc|] else prefix + let label = if prefix == mempty then [pstr|ghc|] else prefix +#if MIN_VERSION_Win32(2, 14, 0) withFilePath tmp_dir $ \c_tmp_dir -> +#else + withTString tmp_dir $ \c_tmp_dir -> +#endif withTString label $ \c_template -> withTString suffix $ \c_suffix -> with nullPtr $ \c_ptr -> do @@ -201,3 +219,19 @@ toHandle fp iomode h = (`onException` Win32.closeHandle h) $ do fdToHandle' fd Nothing False fp' iomode True #endif +#if !MIN_VERSION_filepath(1, 5, 0) + +break_ :: (WindowsChar -> Bool) -> WindowsString -> (WindowsString, WindowsString) +break_ = coerce BC.break + +reverse_ :: WindowsString -> WindowsString +reverse_ = coerce BC.reverse + +any_ :: (WindowsChar -> Bool) -> WindowsString -> Bool +any_ = coerce BC.any + +cons_ :: WindowsChar -> WindowsString -> WindowsString +cons_ = coerce BC.cons + +#endif + From 31e4073db1e5e06b9d21d3ccbe49b3373a20f02d Mon Sep 17 00:00:00 2001 From: Julian Ospald Date: Mon, 22 Jul 2024 20:34:43 +0800 Subject: [PATCH 07/10] Fix CI --- .cirrus.yml | 4 ++-- file-io.cabal | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index 5bf9e01..67238da 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -1,7 +1,7 @@ task: name: FreeBSD freebsd_instance: - image_family: freebsd-13-2 + image_family: freebsd-14-0 install_script: pkg install -y ghc hs-cabal-install git autoconf script: - cabal update @@ -26,7 +26,7 @@ task: name: NetBSD compute_engine_instance: image_project: pg-ci-images - image: family/pg-ci-netbsd-vanilla-9-3 + image: family/pg-ci-netbsd-vanilla-10-0 platform: netbsd install_script: - export PKG_PATH="http://cdn.NetBSD.org/pub/pkgsrc/packages/NetBSD/$(uname -p)/$(uname -r|cut -f '1 2' -d.)/All/" diff --git a/file-io.cabal b/file-io.cabal index f33d30d..c1091f6 100644 --- a/file-io.cabal +++ b/file-io.cabal @@ -65,7 +65,7 @@ test-suite T15 main-is: T15.hs type: exitcode-stdio-1.0 default-language: Haskell2010 - build-depends: base, tasty, tasty-hunit, file-io, filepath, temporary + build-depends: base >=4.13.0.0 && <5, 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 @@ -76,9 +76,9 @@ test-suite T15Win 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 + build-depends: base >=4.13.0.0 && <5, tasty, tasty-hunit, file-io, filepath, temporary, Win32 >=2.13.3.0 else - build-depends: base + build-depends: base >=4.13.0.0 && <5 ghc-options: -Wall -threaded -rtsopts "-with-rtsopts=-N10" test-suite T14 @@ -86,7 +86,7 @@ test-suite T14 main-is: T14.hs type: exitcode-stdio-1.0 default-language: Haskell2010 - build-depends: base, file-io, filepath, temporary + build-depends: base >=4.13.0.0 && <5, file-io, filepath, temporary ghc-options: -Wall test-suite T8 @@ -94,7 +94,7 @@ test-suite T8 main-is: T8.hs type: exitcode-stdio-1.0 default-language: Haskell2010 - build-depends: base, bytestring, file-io, filepath, temporary + build-depends: base >=4.13.0.0 && <5, bytestring, file-io, filepath, temporary ghc-options: -Wall -threaded test-suite CLC237 @@ -102,7 +102,7 @@ test-suite CLC237 main-is: CLC237.hs type: exitcode-stdio-1.0 default-language: Haskell2010 - build-depends: base, file-io, filepath, temporary + build-depends: base >=4.13.0.0 && <5, file-io, filepath, temporary ghc-options: -Wall test-suite Properties @@ -110,6 +110,6 @@ test-suite Properties main-is: Properties.hs type: exitcode-stdio-1.0 default-language: Haskell2010 - build-depends: base, bytestring, tasty, tasty-hunit, file-io, filepath, temporary + build-depends: base >=4.13.0.0 && <5, bytestring, tasty, tasty-hunit, file-io, filepath, temporary ghc-options: -Wall -threaded -rtsopts "-with-rtsopts=-N10" From f733c80c98a8971064c4f3404add0cbb3f38b182 Mon Sep 17 00:00:00 2001 From: Julian Ospald Date: Tue, 23 Jul 2024 20:38:12 +0800 Subject: [PATCH 08/10] Fix windows runners --- .github/workflows/test.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 3e05cc9..ce1c268 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -49,11 +49,13 @@ jobs: run: | set -eux [ -e ~/.ghcup/env ] && . ~/.ghcup/env + ghcup install ghc --set ${{ matrix.ghc }} echo ${{ matrix.ghc }} echo $(ghc --numeric-version) cabal update - cabal build --enable-tests - cabal test --test-show-details=direct + cabal configure --enable-tests --test-show-details=direct + cabal build + cabal test cabal haddock cabal check cabal sdist From 030e3af88ece5625fb4817bf702676d6bd66d397 Mon Sep 17 00:00:00 2001 From: Julian Ospald Date: Wed, 24 Jul 2024 20:29:06 +0800 Subject: [PATCH 09/10] Apply review suggestions --- System/File/OsPath/Internal.hs | 40 ++++---------------------------- posix/System/File/Platform.hs | 16 ++++--------- windows/System/File/Platform.hsc | 16 ++++--------- 3 files changed, 15 insertions(+), 57 deletions(-) diff --git a/System/File/OsPath/Internal.hs b/System/File/OsPath/Internal.hs index 6acf28e..e057d90 100644 --- a/System/File/OsPath/Internal.hs +++ b/System/File/OsPath/Internal.hs @@ -9,7 +9,7 @@ module System.File.OsPath.Internal where import qualified System.File.Platform as P -import Prelude ((.), ($), String, IO, ioError, pure, either, const, flip, Maybe(..), fmap, (<$>), id, Bool(..), FilePath, (++), return, show, (>>=), (==), otherwise, errorWithoutStackTrace, userError, mempty) +import Prelude ((.), ($), String, IO, ioError, pure, either, const, flip, Maybe(..), fmap, (<$>), id, Bool(..), FilePath, (++), return, show, (>>=), (==), otherwise, userError) import GHC.IO (catchException) import GHC.IO.Exception (IOException(..)) import GHC.IO.Handle (hClose_help) @@ -26,9 +26,10 @@ import System.OsString.Internal.Types import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy as BSL -import qualified System.OsString as OSS import System.Posix.Types (CMode) -#if !MIN_VERSION_filepath(1, 5, 0) +#if MIN_VERSION_filepath(1, 5, 0) +import qualified System.OsString as OSS +#else import Data.Coerce #endif @@ -245,46 +246,15 @@ openTempFile' loc (OsString tmp_dir) template@(OsString tmpl) binary mode -- We split off the last extension, so we can use .foo.ext files -- for temporary files (hidden on Unix OSes). Unfortunately we're -- below filepath in the hierarchy here. - (OsString prefix, OsString suffix) = - case break_ (== OSS.unsafeFromChar '.') $ reverse_ template of - -- First case: template contains no '.'s. Just re-reverse it. - (rev_suffix, xs) - | xs == mempty -> (reverse_ rev_suffix, mempty) - -- Second case: template contains at least one '.'. Strip the - -- dot from the prefix and prepend it to the suffix (if we don't - -- do this, the unique number will get added after the '.' and - -- thus be part of the extension, which is wrong.) - (rev_suffix, xs) - | (h:rest) <- OSS.unpack xs - , h == unsafeFromChar '.' -> (reverse_ (OSS.pack rest), cons_ (unsafeFromChar '.') $ reverse_ rev_suffix) - -- Otherwise, something is wrong, because (break (== '.')) should - -- always return a pair with either the empty string or a string - -- beginning with '.' as the second component. - _ -> errorWithoutStackTrace "bug in System.IO.openTempFile" + (OsString prefix, OsString suffix) = OSP.splitExtension template #if MIN_VERSION_filepath(1, 5, 0) any_ :: (OsChar -> Bool) -> OsString -> Bool any_ = OSS.any -cons_ :: OsChar -> OsString -> OsString -cons_ = OSS.cons - -break_ :: (OsChar -> Bool) -> OsString -> (OsString, OsString) -break_ = OSS.break - -reverse_ :: OsString -> OsString -reverse_ = OSS.reverse #else any_ :: (OsChar -> Bool) -> OsString -> Bool any_ = coerce P.any_ -cons_ :: OsChar -> OsString -> OsString -cons_ = coerce P.cons_ - -break_ :: (OsChar -> Bool) -> OsString -> (OsString, OsString) -break_ = coerce P.break_ - -reverse_ :: OsString -> OsString -reverse_ = coerce P.reverse_ #endif diff --git a/posix/System/File/Platform.hs b/posix/System/File/Platform.hs index 85a28a2..2d1e3df 100644 --- a/posix/System/File/Platform.hs +++ b/posix/System/File/Platform.hs @@ -34,6 +34,8 @@ import Data.Coerce (coerce) import "filepath" System.OsString.Internal.Types (PosixString(..), PosixChar(..)) import qualified "filepath" System.OsPath.Data.ByteString.Short as BC #endif +import System.CPUTime (cpuTimePrecision, getCPUTime) +import Text.Printf (printf) -- | Open a file and return the 'Handle'. openFile :: PosixPath -> IOMode -> IO Handle @@ -110,9 +112,10 @@ tempCounter = unsafePerformIO $ newIORef 0 -- build large digit-alike number rand_string :: IO PosixString rand_string = do - r1 <- c_getpid + r1 <- fromIntegral @_ @Int <$> c_getpid (r2, _) <- atomicModifyIORef'_ tempCounter (+1) - return $ PS.pack $ fmap (PS.unsafeFromChar) (show r1 ++ "-" ++ show r2) + r3 <- (`quot` cpuTimePrecision) <$> getCPUTime + return $ PS.pack $ fmap (PS.unsafeFromChar) (printf "%x-%x-%x" r1 r2 r3) lenientDecode :: PosixString -> String lenientDecode ps = let utf8' = PS.decodeWith utf8 ps @@ -124,17 +127,8 @@ lenientDecode ps = let utf8' = PS.decodeWith utf8 ps #if !MIN_VERSION_filepath(1, 5, 0) -break_ :: (PosixChar -> Bool) -> PosixString -> (PosixString, PosixString) -break_ = coerce BC.break - -reverse_ :: PosixString -> PosixString -reverse_ = coerce BC.reverse - any_ :: (PosixChar -> Bool) -> PosixString -> Bool any_ = coerce BC.any -cons_ :: PosixChar -> PosixString -> PosixString -cons_ = coerce BC.cons - #endif diff --git a/windows/System/File/Platform.hsc b/windows/System/File/Platform.hsc index 93cd88f..02bdafc 100644 --- a/windows/System/File/Platform.hsc +++ b/windows/System/File/Platform.hsc @@ -35,9 +35,11 @@ import Foreign.Ptr import Foreign.Marshal.Alloc import Foreign.Marshal.Utils (with) import Foreign.Storable +import System.CPUTime (cpuTimePrecision, getCPUTime) import System.Posix.Types (CMode) import System.IO.Unsafe (unsafePerformIO) import System.Posix.Internals (c_getpid, o_EXCL) +import Text.Printf (printf) #if MIN_VERSION_filepath(1, 5, 0) import System.OsString.Encoding @@ -193,9 +195,10 @@ tempCounter = unsafePerformIO $ newIORef 0 -- build large digit-alike number rand_string :: IO WindowsPath rand_string = do - r1 <- c_getpid + r1 <- fromIntegral @_ @Int <$> c_getpid (r2, _) <- atomicModifyIORef'_ tempCounter (+1) - return $ WS.pack $ fmap (WS.unsafeFromChar) (show r1 ++ "-" ++ show r2) + r3 <- (`quot` cpuTimePrecision) <$> getCPUTime + return $ WS.pack $ fmap (WS.unsafeFromChar) (printf "%x-%x-%x" r1 r2 r3) lenientDecode :: WindowsString -> String lenientDecode ws = let utf16le' = WS.decodeWith utf16le_b ws @@ -221,17 +224,8 @@ toHandle fp iomode h = (`onException` Win32.closeHandle h) $ do #if !MIN_VERSION_filepath(1, 5, 0) -break_ :: (WindowsChar -> Bool) -> WindowsString -> (WindowsString, WindowsString) -break_ = coerce BC.break - -reverse_ :: WindowsString -> WindowsString -reverse_ = coerce BC.reverse - any_ :: (WindowsChar -> Bool) -> WindowsString -> Bool any_ = coerce BC.any -cons_ :: WindowsChar -> WindowsString -> WindowsString -cons_ = coerce BC.cons - #endif From 98a4d1ba00d0afbe0d2d98643fd036bf4b583dad Mon Sep 17 00:00:00 2001 From: Julian Ospald Date: Thu, 8 Aug 2024 18:07:35 +0800 Subject: [PATCH 10/10] Fix flaky tests --- tests/Properties.hs | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/tests/Properties.hs b/tests/Properties.hs index 61fd56f..e719b3d 100644 --- a/tests/Properties.hs +++ b/tests/Properties.hs @@ -1,4 +1,5 @@ {-# LANGUAGE CPP #-} +{-# LANGUAGE BangPatterns #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TypeApplications #-} @@ -124,8 +125,9 @@ concFile = do baseDir <- OSP.encodeFS baseDir' let fp = baseDir [osp|foo|] OSP.writeFile fp "" - _ <- OSP.openFile fp ReadMode + !h <- OSP.openFile fp ReadMode r <- try @IOException $ OSP.withFile fp WriteMode $ \h' -> do BS.hPut h' "test" + _ <- try @IOException $ BS.hPut h "" IOError Nothing fileLockedType "withFile" fileLockedMsg Nothing (Just $ baseDir' FP. "foo") @==? r concFile2 :: Assertion @@ -134,8 +136,9 @@ concFile2 = do baseDir <- OSP.encodeFS baseDir' let fp = baseDir [osp|foo|] OSP.writeFile fp "h" - _ <- OSP.openFile fp ReadMode + !h <- OSP.openFile fp ReadMode r <- try @IOException $ OSP.withFile fp ReadMode BS.hGetContents + _ <- try @IOException $ BS.hPut h "" Right "h" @=? r concFile3 :: Assertion @@ -144,8 +147,9 @@ concFile3 = do baseDir <- OSP.encodeFS baseDir' let fp = baseDir [osp|foo|] OSP.writeFile fp "" - _ <- OSP.openFile fp WriteMode + !h <- OSP.openFile fp ReadMode r <- try @IOException $ OSP.withFile fp WriteMode (flip BS.hPut "test") + _ <- try @IOException $ BS.hPut h "" IOError Nothing fileLockedType "withFile" fileLockedMsg Nothing (Just $ baseDir' FP. "foo") @==? r existingFile :: Assertion @@ -221,9 +225,10 @@ existingFile4' = do OSP.openExistingFile fp ReadWriteMode >>= \h -> do hSetBuffering h NoBuffering BS.hPut h "boo" - c <- BS.hGetSome h 5 + !c <- BS.hGetSome h 5 hSeek h AbsoluteSeek 0 - c' <- BS.hGetSome h 5 + !c' <- BS.hGetSome h 5 + hClose h pure (c, c') Right ("tx", "bootx") @=? r @@ -232,7 +237,7 @@ openTempFile1 open = do withSystemTempDirectory "test" $ \baseDir' -> do baseDir <- OSP.encodeFS baseDir' let file = [osp|foo.ext|] - (fp, h') <- open baseDir file + (!fp, h') <- open baseDir file hClose h' r <- try @IOException $ do OSP.openExistingFile fp ReadWriteMode >>= \h -> BS.hPut h "boo" >> hClose h @@ -255,8 +260,10 @@ openTempFile3 open = do withSystemTempDirectory "test" $ \baseDir' -> do baseDir <- OSP.encodeFS baseDir' let file = [osp|foo.ext|] - (fp, _) <- open baseDir file - (fp', _) <- open baseDir file + (!fp, h) <- open baseDir file + (!fp', h') <- open baseDir file + hClose h + hClose h' (fp /= fp') @? "Filepaths are different"