Filesystem.hs (5713B)
1 {-# LANGUAGE ScopedTypeVariables #-} 2 -- Path operations are observations/mutations, not authorization or transactions. 3 -- Directory cursors never materialize a listing or retain previously returned names. 4 module IO.Filesystem 5 ( Status(..), statPath, createDirectory, removeDirectory, removeFile, renamePath 6 , syncDirectory 7 , openDirectory, directoryBackend, nameLimit 8 ) where 9 import qualified IO.Descriptor as Descriptor 10 import System.Posix.IO (openFd, OpenMode(ReadOnly), defaultFileFlags, nonBlock) 11 import System.Posix.Unistd (fileSynchronise) 12 import qualified IO.Resources as R 13 import qualified System.Posix.Files as F 14 import qualified System.Posix.Directory as D 15 import Control.Exception 16 import Control.Monad (when, unless) 17 import Data.IORef 18 import Data.Char (ord) 19 20 data Status = Regular Integer | Directory | Symlink | Other deriving (Eq, Show) 21 nameLimit :: Int 22 nameLimit = 4096 23 24 pathOperation :: String -> Bool -> [FilePath] -> IO a -> IO (Either R.Problem a) 25 pathOperation op mutating paths action 26 | any (\p -> null p || '\0' `elem` p) paths = 27 pure (Left (R.Problem "invalid-argument" op "empty or NUL-containing path" "none")) 28 | otherwise = (Right <$> action) `catch` \(e :: IOException) -> do 29 let problem = R.ioProblem op e 30 pure (Left (problem {R.progress=if mutating then "unknown" else "none"})) 31 32 -- Following the final symlink is an explicit choice; intermediate components 33 -- always follow the host path resolver. Neither form pins later path operations. 34 statPath :: Bool -> FilePath -> IO (Either R.Problem Status) 35 statPath follow path = pathOperation op False [path] $ do 36 status <- (if follow then F.getFileStatus else F.getSymbolicLinkStatus) path 37 if F.isRegularFile status then do 38 let size = toInteger (F.fileSize status) 39 if size >= 0 && size <= R.positionLimit then pure (Regular size) 40 else ioError (userError "file size outside supported range") 41 else pure (if F.isDirectory status then Directory 42 else if F.isSymbolicLink status then Symlink else Other) 43 where op = if follow then "stat" else "lstat" 44 45 createDirectory, removeDirectory, removeFile :: FilePath -> IO (Either R.Problem ()) 46 createDirectory path = pathOperation "create" True [path] (D.createDirectory path 0o777) 47 removeDirectory path = pathOperation "remove" True [path] (D.removeDirectory path) 48 removeFile path = pathOperation "remove-file" True [path] (F.removeLink path) 49 renamePath :: FilePath -> FilePath -> IO (Either R.Problem ()) 50 renamePath old new = pathOperation "rename" True [old,new] (F.rename old new) 51 52 -- Synchronize an opened directory, not an unchecked path after verification. 53 -- Nonblocking open avoids hanging on a FIFO supplied where a directory belongs. 54 syncDirectory :: R.Runtime -> R.Scope -> FilePath -> IO (Either R.Problem ()) 55 syncDirectory runtime scope path 56 | null path || '\0' `elem` path = pure (Left (R.Problem "invalid-argument" "sync" "empty or NUL-containing path" "none")) 57 | otherwise = R.withTemporary runtime scope "sync" (do 58 fd <- openFd path ReadOnly (defaultFileFlags {nonBlock=True}) 59 release <- Descriptor.closeOnce fd 60 pure (fd,release)) $ \fd -> do 61 status <- F.getFdStatus fd 62 unless (F.isDirectory status) (R.resourceFault (R.Problem "not-directory" "sync" "path is not a directory" "none")) 63 fileSynchronise fd 64 65 openDirectory :: R.Runtime -> R.Scope -> FilePath -> IO (Either R.Problem R.Token) 66 openDirectory runtime scope path 67 | null path || '\0' `elem` path = pure (Left (R.Problem "invalid-argument" "open" "empty or NUL-containing path" "none")) 68 | otherwise = R.acquire runtime scope $ 69 bracketOnError (D.openDirStream path) D.closeDirStream $ \stream -> 70 directoryBackend (D.readDirStream stream) (D.closeDirStream stream) 71 72 data Cursor = Active | Ended | Poisoned 73 -- Injectable backend callbacks exercise failure/cancellation at the actual cursor 74 -- boundary. The registry supplies the lease; these callbacks are not standalone 75 -- concurrent iterators. An uncertain advance cannot be implicitly retried. 76 directoryBackend :: IO FilePath -> IO () -> IO R.Backend 77 directoryBackend readName close = do 78 state <- newIORef Active 79 owned <- newIORef True 80 let fault category detail = R.resourceFault (R.Problem category "next" detail "unknown") 81 validName = walk nameLimit 82 where 83 walk _ [] = pure () 84 walk 0 _ = fault "invalid-name" "directory name exceeds scalar limit" 85 walk n (c:rest) 86 | c == '/' || c == '\0' = fault "invalid-name" "invalid directory component" 87 | ord c > 0x10ffff || (ord c >= 0xd800 && ord c <= 0xdfff) = fault "invalid-encoding" "directory name is not Unicode scalar text" 88 | otherwise = walk (n-1) rest 89 advance budget = do 90 name <- readName 91 case name of 92 "" -> writeIORef state Ended >> pure Nothing 93 "." -> skip budget 94 ".." -> skip budget 95 _ -> validName name >> pure (Just name) 96 skip 0 = fault "invalid-state" "directory dot-entry scan exceeded bound" 97 skip n = advance (n-1) 98 next = mask $ \restore -> do 99 phase <- readIORef state 100 case phase of 101 Ended -> pure Nothing 102 Poisoned -> fault "invalid-state" "directory cursor failed; reopen explicitly" 103 Active -> restore (advance (2 :: Int)) `onException` writeIORef state Poisoned 104 release = mask_ $ do 105 -- closedir owns a raw pointer: never retry an uncertain close against a 106 -- potentially freed DIR*. Registry teardown can revisit async failures. 107 wasOwned <- atomicModifyIORef' owned (\old -> (False,old)) 108 when wasOwned close 109 pure (R.Directory next release)