From 9ac10a6a7de501b78be343d5371f396b56fff6a8 Mon Sep 17 00:00:00 2001 From: Julian Ospald Date: Fri, 6 Apr 2018 16:42:40 +0200 Subject: [PATCH] Add file reading functions --- src/HPath/IO.hs | 88 +++++++++++++++++++++++++++++++- test/HPath/IO/ReadFileEOFSpec.hs | 86 +++++++++++++++++++++++++++++++ test/HPath/IO/ReadFileSpec.hs | 86 +++++++++++++++++++++++++++++++ test/Utils.hs | 18 +++++++ 4 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 test/HPath/IO/ReadFileEOFSpec.hs create mode 100644 test/HPath/IO/ReadFileSpec.hs diff --git a/src/HPath/IO.hs b/src/HPath/IO.hs index e5ad7fc..65b4f4e 100644 --- a/src/HPath/IO.hs +++ b/src/HPath/IO.hs @@ -60,6 +60,9 @@ module HPath.IO -- * File renaming/moving , renameFile , moveFile + -- * File reading + , readFile + , readFileEOF -- * File permissions , newFilePerms , newDirPerms @@ -97,6 +100,17 @@ import Data.ByteString ( ByteString ) +import Data.ByteString.Builder + ( + Builder + , byteString + , toLazyByteString + ) +import qualified Data.ByteString.Lazy as L +import Data.ByteString.Unsafe + ( + unsafePackCStringFinalizer + ) import Data.Foldable ( for_ @@ -112,6 +126,10 @@ import Data.Maybe ( catMaybes ) +import Data.Monoid + ( + (<>) + ) import Data.Word ( Word8 @@ -145,7 +163,7 @@ import GHC.IO.Exception import HPath import HPath.Internal import HPath.IO.Errors -import Prelude hiding (readFile) +import Prelude hiding (readFile, writeFile) import System.IO.Error ( catchIOError @@ -845,6 +863,74 @@ moveFile from to cm = do + -------------------- + --[ File Reading ]-- + -------------------- + + +-- |Read the given file at once into memory as a strict ByteString. +-- Symbolic links are followed, no sanity checks on file size +-- or file type. File must exist. +-- +-- Note: the size of the file is determined in advance, as to only +-- have one allocation. +-- +-- Safety/reliability concerns: +-- +-- * since amount of bytes to read is determined in advance, +-- the file might be read partially only if something else is +-- appending to it while reading +-- * the whole file is read into memory! +-- +-- Throws: +-- +-- - `InappropriateType` if file is not a regular file or a symlink +-- - `PermissionDenied` if we cannot read the file or the directory +-- containting it +-- - `NoSuchThing` if the file does not exist +readFile :: Path Abs -> IO ByteString +readFile p = withAbsPath p $ \fp -> + bracket (openFd fp SPI.ReadOnly [] Nothing) (SPI.closeFd) $ \fd -> do + stat <- PF.getFdStatus fd + let fsize = PF.fileSize stat + SPB.fdRead fd (fromIntegral fsize) + + +-- |Read the given file in chunks of size `8192` into memory until +-- `fread` returns 0. Returns a lazy ByteString, because it uses +-- Builders under the hood. +-- +-- Safety/reliability concerns: +-- +-- * the whole file is read into memory! +-- +-- Throws: +-- +-- - `InappropriateType` if file is not a regular file or a symlink +-- - `PermissionDenied` if we cannot read the file or the directory +-- containting it +-- - `NoSuchThing` if the file does not exist +readFileEOF :: Path Abs -> IO L.ByteString +readFileEOF p = withAbsPath p $ \fp -> + bracket (openFd fp SPI.ReadOnly [] Nothing) (SPI.closeFd) $ \fd -> + allocaBytes (fromIntegral bufSize) $ \buf -> read' fd buf mempty + where + bufSize :: CSize + bufSize = 8192 + read' :: Fd -> Ptr Word8 -> Builder -> IO L.ByteString + read' fd buf builder = do + size <- SPB.fdReadBuf fd buf bufSize + if size == 0 + then return $ toLazyByteString builder + else do + readBS <- unsafePackCStringFinalizer buf + (fromIntegral size) + mempty + read' fd buf (builder <> byteString readBS) + + + + ----------------------- --[ File Permissions]-- diff --git a/test/HPath/IO/ReadFileEOFSpec.hs b/test/HPath/IO/ReadFileEOFSpec.hs new file mode 100644 index 0000000..cb519f5 --- /dev/null +++ b/test/HPath/IO/ReadFileEOFSpec.hs @@ -0,0 +1,86 @@ +{-# LANGUAGE OverloadedStrings #-} + + +module HPath.IO.ReadFileEOFSpec where + + +import Test.Hspec +import System.IO.Error + ( + ioeGetErrorType + ) +import GHC.IO.Exception + ( + IOErrorType(..) + ) +import System.Process +import Utils + + + +upTmpDir :: IO () +upTmpDir = do + setTmpDir "ReadFileEOFSpec" + createTmpDir + +setupFiles :: IO () +setupFiles = do + createRegularFile' "fileWithContent" + createRegularFile' "fileWithoutContent" + createSymlink' "inputFileSymL" "fileWithContent" + createDir' "alreadyExistsD" + createRegularFile' "noPerms" + noPerms "noPerms" + createDir' "noPermsD" + createRegularFile' "noPermsD/inputFile" + noPerms "noPermsD" + writeFile' "fileWithContent" "Blahfaselgagaga" + + +cleanupFiles :: IO () +cleanupFiles = do + deleteFile' "fileWithContent" + deleteFile' "fileWithoutContent" + deleteFile' "inputFileSymL" + deleteDir' "alreadyExistsD" + normalFilePerms "noPerms" + deleteFile' "noPerms" + normalDirPerms "noPermsD" + deleteFile' "noPermsD/inputFile" + deleteDir' "noPermsD" + + +spec :: Spec +spec = beforeAll_ (upTmpDir >> setupFiles) $ afterAll_ cleanupFiles $ + describe "HPath.IO.readFileEOF" $ do + + -- successes -- + it "readFileEOF (Strict) file with content, everything clear" $ do + out <- readFileEOF' "fileWithContent" + out `shouldBe` "Blahfaselgagaga" + + it "readFileEOF (Strict) symlink, everything clear" $ do + out <- readFileEOF' "inputFileSymL" + out `shouldBe` "Blahfaselgagaga" + + it "readFileEOF (Strict) empty file, everything clear" $ do + out <- readFileEOF' "fileWithoutContent" + out `shouldBe` "" + + + -- posix failures -- + it "readFileEOF (Strict) directory, wrong file type" $ do + readFileEOF' "alreadyExistsD" + `shouldThrow` (\e -> ioeGetErrorType e == InappropriateType) + + it "readFileEOF (Strict) file, no permissions" $ do + readFileEOF' "noPerms" + `shouldThrow` (\e -> ioeGetErrorType e == PermissionDenied) + + it "readFileEOF (Strict) file, no permissions on dir" $ do + readFileEOF' "noPermsD/inputFile" + `shouldThrow` (\e -> ioeGetErrorType e == PermissionDenied) + + it "readFileEOF (Strict) file, no such file" $ do + readFileEOF' "lalala" + `shouldThrow` (\e -> ioeGetErrorType e == NoSuchThing) diff --git a/test/HPath/IO/ReadFileSpec.hs b/test/HPath/IO/ReadFileSpec.hs new file mode 100644 index 0000000..b844c30 --- /dev/null +++ b/test/HPath/IO/ReadFileSpec.hs @@ -0,0 +1,86 @@ +{-# LANGUAGE OverloadedStrings #-} + + +module HPath.IO.ReadFileSpec where + + +import Test.Hspec +import System.IO.Error + ( + ioeGetErrorType + ) +import GHC.IO.Exception + ( + IOErrorType(..) + ) +import System.Process +import Utils + + + +upTmpDir :: IO () +upTmpDir = do + setTmpDir "ReadFileSpec" + createTmpDir + +setupFiles :: IO () +setupFiles = do + createRegularFile' "fileWithContent" + createRegularFile' "fileWithoutContent" + createSymlink' "inputFileSymL" "fileWithContent" + createDir' "alreadyExistsD" + createRegularFile' "noPerms" + noPerms "noPerms" + createDir' "noPermsD" + createRegularFile' "noPermsD/inputFile" + noPerms "noPermsD" + writeFile' "fileWithContent" "Blahfaselgagaga" + + +cleanupFiles :: IO () +cleanupFiles = do + deleteFile' "fileWithContent" + deleteFile' "fileWithoutContent" + deleteFile' "inputFileSymL" + deleteDir' "alreadyExistsD" + normalFilePerms "noPerms" + deleteFile' "noPerms" + normalDirPerms "noPermsD" + deleteFile' "noPermsD/inputFile" + deleteDir' "noPermsD" + + +spec :: Spec +spec = beforeAll_ (upTmpDir >> setupFiles) $ afterAll_ cleanupFiles $ + describe "HPath.IO.readFile" $ do + + -- successes -- + it "readFile (Strict) file with content, everything clear" $ do + out <- readFile' "fileWithContent" + out `shouldBe` "Blahfaselgagaga" + + it "readFile (Strict) symlink, everything clear" $ do + out <- readFile' "inputFileSymL" + out `shouldBe` "Blahfaselgagaga" + + it "readFile (Strict) empty file, everything clear" $ do + out <- readFile' "fileWithoutContent" + out `shouldBe` "" + + + -- posix failures -- + it "readFile (Strict) directory, wrong file type" $ do + readFile' "alreadyExistsD" + `shouldThrow` (\e -> ioeGetErrorType e == InappropriateType) + + it "readFile (Strict) file, no permissions" $ do + readFile' "noPerms" + `shouldThrow` (\e -> ioeGetErrorType e == PermissionDenied) + + it "readFile (Strict) file, no permissions on dir" $ do + readFile' "noPermsD/inputFile" + `shouldThrow` (\e -> ioeGetErrorType e == PermissionDenied) + + it "readFile (Strict) file, no such file" $ do + readFile' "lalala" + `shouldThrow` (\e -> ioeGetErrorType e == NoSuchThing) diff --git a/test/Utils.hs b/test/Utils.hs index 506de68..5216c16 100644 --- a/test/Utils.hs +++ b/test/Utils.hs @@ -28,6 +28,7 @@ import Data.IORef ) import HPath.IO import HPath.IO.Errors +import Prelude hiding (readFile) import Data.Maybe ( fromJust @@ -46,6 +47,7 @@ import Data.ByteString ( ByteString ) +import qualified Data.ByteString.Lazy as L import System.Posix.Files.ByteString ( groupExecuteMode @@ -243,6 +245,12 @@ normalDirPerms path = withTmpDir path $ \p -> setFileMode (P.fromAbs p) newDirPerms +normalFilePerms :: ByteString -> IO () +{-# NOINLINE normalFilePerms #-} +normalFilePerms path = + withTmpDir path $ \p -> setFileMode (P.fromAbs p) newFilePerms + + getFileType' :: ByteString -> IO FileType {-# NOINLINE getFileType' #-} getFileType' path = withTmpDir path getFileType @@ -288,3 +296,13 @@ allDirectoryContents' :: ByteString -> IO [ByteString] allDirectoryContents' ip = withTmpDir ip $ \p -> DT.allDirectoryContents' (P.fromAbs p) + +readFile' :: ByteString -> IO ByteString +{-# NOINLINE readFile' #-} +readFile' p = withTmpDir p readFile + + +readFileEOF' :: ByteString -> IO L.ByteString +{-# NOINLINE readFileEOF' #-} +readFileEOF' p = withTmpDir p readFileEOF +