tricu

An interpreted language for exploring Tree Calculus
Log | Files | Refs | README | LICENSE

Filesystem.hs (5719B)


      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 IOV2.Filesystem
      5   ( Status(..), statPath, createDirectory, removeDirectory, removeFile, renamePath
      6   , syncDirectory
      7   , openDirectory, directoryBackend, nameLimit
      8   ) where
      9 import qualified IOV2.Descriptor as Descriptor
     10 import System.Posix.IO (openFd, OpenMode(ReadOnly), defaultFileFlags, nonBlock)
     11 import System.Posix.Unistd (fileSynchronise)
     12 import qualified IOV2.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)