Skip to content

Commit e07e089

Browse files
committed
Merge branch 'openTempFile'
2 parents a4a0464 + 98a4d1b commit e07e089

File tree

9 files changed

+357
-44
lines changed

9 files changed

+357
-44
lines changed

.cirrus.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
task:
22
name: FreeBSD
33
freebsd_instance:
4-
image_family: freebsd-13-2
4+
image_family: freebsd-14-0
55
install_script: pkg install -y ghc hs-cabal-install git autoconf
66
script:
77
- cabal update
@@ -26,7 +26,7 @@ task:
2626
name: NetBSD
2727
compute_engine_instance:
2828
image_project: pg-ci-images
29-
image: family/pg-ci-netbsd-vanilla-9-3
29+
image: family/pg-ci-netbsd-vanilla-10-0
3030
platform: netbsd
3131
install_script:
3232
- export PKG_PATH="http://cdn.NetBSD.org/pub/pkgsrc/packages/NetBSD/$(uname -p)/$(uname -r|cut -f '1 2' -d.)/All/"

.github/workflows/test.yaml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
fail-fast: false
1717
matrix:
1818
os: [ubuntu-latest]
19-
ghc: ['8.6', '8.8', '8.10', '9.0', '9.2', '9.4', '9.6', '9.8']
19+
ghc: ['8.8', '8.10', '9.0', '9.2', '9.4', '9.6', '9.8']
2020
include:
2121
- os: macOS-latest
2222
ghc: '9.4'
@@ -49,11 +49,13 @@ jobs:
4949
run: |
5050
set -eux
5151
[ -e ~/.ghcup/env ] && . ~/.ghcup/env
52+
ghcup install ghc --set ${{ matrix.ghc }}
5253
echo ${{ matrix.ghc }}
5354
echo $(ghc --numeric-version)
5455
cabal update
55-
cabal build --enable-tests
56-
cabal test --test-show-details=direct
56+
cabal configure --enable-tests --test-show-details=direct
57+
cabal build
58+
cabal test
5759
cabal haddock
5860
cabal check
5961
cabal sdist

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Revision history for file-io
22

3+
## 0.1.3 -- 2024-??-??
4+
5+
* add `openTempFile` , `openBinaryTempFile` , `openTempFileWithDefaultPermissions` and `openBinaryTempFileWithDefaultPermissions` wrt [#2](https://github.com/hasufell/file-io/issues/2)
6+
37
## 0.1.2 -- 2024-05-27
48

59
* expose internals via `.Internal` modules

System/File/OsPath.hs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ module System.File.OsPath (
2323
, appendFile'
2424
, openFile
2525
, openExistingFile
26+
, openTempFile
27+
, openBinaryTempFile
28+
, openTempFileWithDefaultPermissions
29+
, openBinaryTempFileWithDefaultPermissions
2630
) where
2731

2832

System/File/OsPath/Internal.hs

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1+
{-# LANGUAGE CPP #-}
12
{-# LANGUAGE TypeApplications #-}
23
{-# LANGUAGE BangPatterns #-}
4+
{-# LANGUAGE ViewPatterns #-}
5+
{-# LANGUAGE QuasiQuotes #-}
36

47
module System.File.OsPath.Internal where
58

69

710
import qualified System.File.Platform as P
811

9-
import Prelude ((.), ($), String, IO, ioError, pure, either, const, flip, Maybe(..), fmap, (<$>), id, Bool(..), FilePath, (++), return, show, (>>=))
12+
import Prelude ((.), ($), String, IO, ioError, pure, either, const, flip, Maybe(..), fmap, (<$>), id, Bool(..), FilePath, (++), return, show, (>>=), (==), otherwise, userError)
1013
import GHC.IO (catchException)
1114
import GHC.IO.Exception (IOException(..))
1215
import GHC.IO.Handle (hClose_help)
@@ -15,14 +18,20 @@ import GHC.IO.Handle.Types (Handle__, Handle(..))
1518
import Control.Concurrent.MVar
1619
import Control.Monad (void, when)
1720
import Control.DeepSeq (force)
18-
import Control.Exception (SomeException, try, evaluate, mask, onException)
21+
import Control.Exception (SomeException, try, evaluate, mask, onException, throwIO)
1922
import System.IO (IOMode(..), hSetBinaryMode, hClose)
2023
import System.IO.Unsafe (unsafePerformIO)
2124
import System.OsPath as OSP
2225
import System.OsString.Internal.Types
2326

2427
import qualified Data.ByteString as BS
2528
import qualified Data.ByteString.Lazy as BSL
29+
import System.Posix.Types (CMode)
30+
#if MIN_VERSION_filepath(1, 5, 0)
31+
import qualified System.OsString as OSS
32+
#else
33+
import Data.Coerce
34+
#endif
2635

2736
-- | Like 'openFile', but open the file in binary mode.
2837
-- On Windows, reading a file in text mode (which is the default)
@@ -127,6 +136,56 @@ openFileWithCloseOnExec osfp iomode = augmentError "openFileWithCloseOnExec" osf
127136
openExistingFileWithCloseOnExec :: OsPath -> IOMode -> IO Handle
128137
openExistingFileWithCloseOnExec osfp iomode = augmentError "openExistingFileWithCloseOnExec" osfp $ withOpenFile' osfp iomode False True True pure False
129138

139+
140+
-- | The function creates a temporary file in ReadWrite mode.
141+
-- The created file isn\'t deleted automatically, so you need to delete it manually.
142+
--
143+
-- The file is created with permissions such that only the current
144+
-- user can read\/write it.
145+
--
146+
-- With some exceptions (see below), the file will be created securely
147+
-- in the sense that an attacker should not be able to cause
148+
-- openTempFile to overwrite another file on the filesystem using your
149+
-- credentials, by putting symbolic links (on Unix) in the place where
150+
-- the temporary file is to be created. On Unix the @O_CREAT@ and
151+
-- @O_EXCL@ flags are used to prevent this attack, but note that
152+
-- @O_EXCL@ is sometimes not supported on NFS filesystems, so if you
153+
-- rely on this behaviour it is best to use local filesystems only.
154+
--
155+
-- @since 0.1.3
156+
openTempFile :: OsPath -- ^ Directory in which to create the file
157+
-> OsString -- ^ File name template. If the template is \"foo.ext\" then
158+
-- the created file will be \"fooXXX.ext\" where XXX is some
159+
-- random number. Note that this should not contain any path
160+
-- separator characters. On Windows, the template prefix may
161+
-- be truncated to 3 chars, e.g. \"foobar.ext\" will be
162+
-- \"fooXXX.ext\".
163+
-> IO (OsPath, Handle)
164+
openTempFile tmp_dir template = openTempFile' "openTempFile" tmp_dir template False 0o600
165+
166+
-- | Like 'openTempFile', but opens the file in binary mode. See 'openBinaryFile' for more comments.
167+
--
168+
-- @since 0.1.3
169+
openBinaryTempFile :: OsPath -> OsString -> IO (OsPath, Handle)
170+
openBinaryTempFile tmp_dir template
171+
= openTempFile' "openBinaryTempFile" tmp_dir template True 0o600
172+
173+
-- | Like 'openTempFile', but uses the default file permissions
174+
--
175+
-- @since 0.1.3
176+
openTempFileWithDefaultPermissions :: OsPath -> OsString
177+
-> IO (OsPath, Handle)
178+
openTempFileWithDefaultPermissions tmp_dir template
179+
= openTempFile' "openTempFileWithDefaultPermissions" tmp_dir template False 0o666
180+
181+
-- | Like 'openBinaryTempFile', but uses the default file permissions
182+
--
183+
-- @since 0.1.3
184+
openBinaryTempFileWithDefaultPermissions :: OsPath -> OsString
185+
-> IO (OsPath, Handle)
186+
openBinaryTempFileWithDefaultPermissions tmp_dir template
187+
= openTempFile' "openBinaryTempFileWithDefaultPermissions" tmp_dir template True 0o666
188+
130189
-- ---------------------------------------------------------------------------
131190
-- Internals
132191

@@ -173,3 +232,29 @@ addFilePathToIOError fun fp ioe = unsafePerformIO $ do
173232
augmentError :: String -> OsPath -> IO a -> IO a
174233
augmentError str osfp = flip catchException (ioError . addFilePathToIOError str osfp)
175234

235+
236+
openTempFile' :: String -> OsPath -> OsString -> Bool -> CMode
237+
-> IO (OsPath, Handle)
238+
openTempFile' loc (OsString tmp_dir) template@(OsString tmpl) binary mode
239+
| any_ (== OSP.pathSeparator) template
240+
= throwIO $ userError $ "openTempFile': Template string must not contain path separator characters: " ++ P.lenientDecode tmpl
241+
| otherwise = do
242+
(fp, hdl) <- P.findTempName (prefix, suffix) loc tmp_dir mode
243+
when binary $ hSetBinaryMode hdl True
244+
pure (OsString fp, hdl)
245+
where
246+
-- We split off the last extension, so we can use .foo.ext files
247+
-- for temporary files (hidden on Unix OSes). Unfortunately we're
248+
-- below filepath in the hierarchy here.
249+
(OsString prefix, OsString suffix) = OSP.splitExtension template
250+
251+
#if MIN_VERSION_filepath(1, 5, 0)
252+
any_ :: (OsChar -> Bool) -> OsString -> Bool
253+
any_ = OSS.any
254+
255+
#else
256+
any_ :: (OsChar -> Bool) -> OsString -> Bool
257+
any_ = coerce P.any_
258+
259+
#endif
260+

file-io.cabal

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
cabal-version: 2.4
22
name: file-io
3-
version: 0.1.2
3+
version: 0.1.3
44
synopsis: Basic file IO operations via 'OsPath'
55
description: Basic file IO operations like Prelude, but for 'OsPath'.
66
homepage: https://github.com/hasufell/file-io
@@ -17,8 +17,7 @@ tested-with: GHC==9.8.1,
1717
GHC==9.2.8,
1818
GHC==9.0.2,
1919
GHC==8.10.7,
20-
GHC==8.8.4,
21-
GHC==8.6.5
20+
GHC==8.8.4
2221

2322
source-repository head
2423
type: git
@@ -42,7 +41,7 @@ library
4241

4342
hs-source-dirs: .
4443
build-depends:
45-
, base >=4.12 && <5
44+
, base >=4.13.0.0 && <5
4645
, bytestring >=0.11.3.0
4746
, deepseq
4847

@@ -66,7 +65,7 @@ test-suite T15
6665
main-is: T15.hs
6766
type: exitcode-stdio-1.0
6867
default-language: Haskell2010
69-
build-depends: base, tasty, tasty-hunit, file-io, filepath, temporary
68+
build-depends: base >=4.13.0.0 && <5, tasty, tasty-hunit, file-io, filepath, temporary
7069
ghc-options: -Wall -threaded -rtsopts "-with-rtsopts=-N10"
7170
if os(windows)
7271
build-depends: Win32 >=2.13.3.0
@@ -77,40 +76,40 @@ test-suite T15Win
7776
type: exitcode-stdio-1.0
7877
default-language: Haskell2010
7978
if os(windows)
80-
build-depends: base, tasty, tasty-hunit, file-io, filepath, temporary, Win32 >=2.13.3.0
79+
build-depends: base >=4.13.0.0 && <5, tasty, tasty-hunit, file-io, filepath, temporary, Win32 >=2.13.3.0
8180
else
82-
build-depends: base
81+
build-depends: base >=4.13.0.0 && <5
8382
ghc-options: -Wall -threaded -rtsopts "-with-rtsopts=-N10"
8483

8584
test-suite T14
8685
hs-source-dirs: tests
8786
main-is: T14.hs
8887
type: exitcode-stdio-1.0
8988
default-language: Haskell2010
90-
build-depends: base, file-io, filepath, temporary
89+
build-depends: base >=4.13.0.0 && <5, file-io, filepath, temporary
9190
ghc-options: -Wall
9291

9392
test-suite T8
9493
hs-source-dirs: tests
9594
main-is: T8.hs
9695
type: exitcode-stdio-1.0
9796
default-language: Haskell2010
98-
build-depends: base, bytestring, file-io, filepath, temporary
97+
build-depends: base >=4.13.0.0 && <5, bytestring, file-io, filepath, temporary
9998
ghc-options: -Wall -threaded
10099

101100
test-suite CLC237
102101
hs-source-dirs: tests
103102
main-is: CLC237.hs
104103
type: exitcode-stdio-1.0
105104
default-language: Haskell2010
106-
build-depends: base, file-io, filepath, temporary
105+
build-depends: base >=4.13.0.0 && <5, file-io, filepath, temporary
107106
ghc-options: -Wall
108107

109108
test-suite Properties
110109
hs-source-dirs: tests
111110
main-is: Properties.hs
112111
type: exitcode-stdio-1.0
113112
default-language: Haskell2010
114-
build-depends: base, bytestring, tasty, tasty-hunit, file-io, filepath, temporary
113+
build-depends: base >=4.13.0.0 && <5, bytestring, tasty, tasty-hunit, file-io, filepath, temporary
115114
ghc-options: -Wall -threaded -rtsopts "-with-rtsopts=-N10"
116115

posix/System/File/Platform.hs

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
{-# LANGUAGE CPP #-}
2+
{-# LANGUAGE TupleSections #-}
13
{-# LANGUAGE TypeApplications #-}
4+
{-# LANGUAGE PackageImports #-}
25

36
module System.File.Platform where
47

8+
import Data.Either (fromRight)
59
import Control.Exception (try, onException, SomeException)
610
import GHC.IO.Handle.FD (fdToHandle')
711
import System.IO (IOMode(..), Handle)
@@ -10,10 +14,28 @@ import System.Posix.IO.PosixString
1014
( defaultFileFlags,
1115
openFd,
1216
closeFd,
13-
OpenFileFlags(noctty, nonBlock, creat, append, trunc, cloexec),
17+
OpenFileFlags(noctty, nonBlock, creat, append, trunc, cloexec, exclusive),
1418
OpenMode(ReadWrite, ReadOnly, WriteOnly) )
15-
import System.OsPath.Posix ( PosixPath )
19+
import System.OsPath.Posix ( PosixPath, PosixString, (</>) )
1620
import qualified System.OsPath.Posix as PS
21+
import Data.IORef (IORef, newIORef)
22+
import System.Posix (CMode)
23+
import System.IO (utf8, latin1)
24+
import System.IO.Unsafe (unsafePerformIO)
25+
import System.Posix.Internals (c_getpid)
26+
import GHC.IORef (atomicModifyIORef'_)
27+
import Foreign.C (getErrno, eEXIST, errnoToIOError)
28+
29+
#if MIN_VERSION_filepath(1, 5, 0)
30+
import "os-string" System.OsString.Internal.Types (PosixString(..), PosixChar(..))
31+
import qualified "os-string" System.OsString.Data.ByteString.Short as BC
32+
#else
33+
import Data.Coerce (coerce)
34+
import "filepath" System.OsString.Internal.Types (PosixString(..), PosixChar(..))
35+
import qualified "filepath" System.OsPath.Data.ByteString.Short as BC
36+
#endif
37+
import System.CPUTime (cpuTimePrecision, getCPUTime)
38+
import Text.Printf (printf)
1739

1840
-- | Open a file and return the 'Handle'.
1941
openFile :: PosixPath -> IOMode -> IO Handle
@@ -43,7 +65,7 @@ openExistingFile_ df fp iomode = fdToHandle_ iomode fp =<< case iomode of
4365

4466
fdToHandle_ :: IOMode -> PosixPath -> Fd -> IO Handle
4567
fdToHandle_ iomode fp (Fd fd) = (`onException` closeFd (Fd fd)) $ do
46-
fp' <- either (const (fmap PS.toChar . PS.unpack $ fp)) id <$> try @SomeException (PS.decodeFS fp)
68+
fp' <- fromRight (fmap PS.toChar . PS.unpack $ fp) <$> try @SomeException (PS.decodeFS fp)
4769
fdToHandle' fd Nothing False fp' iomode True
4870

4971
openFileWithCloseOnExec :: PosixPath -> IOMode -> IO Handle
@@ -58,3 +80,55 @@ defaultFileFlags' = defaultFileFlags { noctty = True, nonBlock = True }
5880
defaultExistingFileFlags :: OpenFileFlags
5981
defaultExistingFileFlags = defaultFileFlags { noctty = True, nonBlock = True, creat = Nothing }
6082

83+
findTempName :: (PosixString, PosixString)
84+
-> String
85+
-> PosixPath
86+
-> CMode
87+
-> IO (PosixPath, Handle)
88+
findTempName (prefix, suffix) loc tmp_dir mode = go
89+
where
90+
go = do
91+
rs <- rand_string
92+
let filename = prefix <> rs <> suffix
93+
filepath = tmp_dir </> filename
94+
fd <- openTempFile_ filepath mode
95+
if fd < 0
96+
then do
97+
errno <- getErrno
98+
case errno of
99+
_ | errno == eEXIST -> go
100+
_ -> do
101+
let tmp_dir' = lenientDecode tmp_dir
102+
ioError (errnoToIOError loc errno Nothing (Just tmp_dir'))
103+
else fmap (filepath,) $ fdToHandle_ ReadWriteMode filepath fd
104+
105+
openTempFile_ :: PosixPath -> CMode -> IO Fd
106+
openTempFile_ fp cmode = openFd fp ReadWrite defaultFileFlags' { creat = Just cmode, nonBlock = True, noctty = True, exclusive = True }
107+
108+
tempCounter :: IORef Int
109+
tempCounter = unsafePerformIO $ newIORef 0
110+
{-# NOINLINE tempCounter #-}
111+
112+
-- build large digit-alike number
113+
rand_string :: IO PosixString
114+
rand_string = do
115+
r1 <- fromIntegral @_ @Int <$> c_getpid
116+
(r2, _) <- atomicModifyIORef'_ tempCounter (+1)
117+
r3 <- (`quot` cpuTimePrecision) <$> getCPUTime
118+
return $ PS.pack $ fmap (PS.unsafeFromChar) (printf "%x-%x-%x" r1 r2 r3)
119+
120+
lenientDecode :: PosixString -> String
121+
lenientDecode ps = let utf8' = PS.decodeWith utf8 ps
122+
latin1' = PS.decodeWith latin1 ps
123+
in case (utf8', latin1') of
124+
(Right s, ~_) -> s
125+
(_, Right s) -> s
126+
(Left _, Left _) -> error "lenientDecode: failed to decode"
127+
128+
#if !MIN_VERSION_filepath(1, 5, 0)
129+
130+
any_ :: (PosixChar -> Bool) -> PosixString -> Bool
131+
any_ = coerce BC.any
132+
133+
#endif
134+

0 commit comments

Comments
 (0)