tricu

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

ByteStream.hs (4710B)


      1 {-# LANGUAGE BangPatterns #-}
      2 -- Native bulk bytes; source owns paths, digest selection and response policy.
      3 module IOV2.ByteStream (snapshot, snapshotWith, transfer) where
      4 
      5 import qualified IOV2.Resources as R
      6 import Control.Exception
      7 import Control.Monad (unless, when)
      8 import Crypto.Hash (Context, SHA256, hashInit, hashUpdate, hashFinalize)
      9 import qualified Data.ByteString as BS
     10 import Data.IORef
     11 import System.Directory (removeFile)
     12 import System.IO
     13 
     14 -- A whole-stream SHA-256 snapshot, including an EOF probe at the byte budget.
     15 -- Register before consuming input. The exposed file has no write/resize method;
     16 -- only the private handle can prepare it, before its token is returned.
     17 snapshot :: R.Runtime -> R.Scope -> R.Token -> FilePath -> Integer -> String
     18   -> IO (Either R.Problem (R.Token, Integer))
     19 snapshot = snapshotWith hClose
     20 
     21 snapshotWith :: (Handle -> IO ()) -> R.Runtime -> R.Scope -> R.Token -> FilePath
     22   -> Integer -> String -> IO (Either R.Problem (R.Token, Integer))
     23 snapshotWith release runtime scope reader directory limit expected
     24   | limit < 0 || limit > R.positionLimit = pure (Left (fault "invalid-bound" "invalid byte limit" "none"))
     25   | length expected /= 64 || any (`notElem` ("0123456789abcdef" :: String)) expected =
     26       pure (Left (fault "invalid-argument" "expected lowercase SHA-256" "none"))
     27   | null directory = pure (Left (fault "invalid-argument" "empty snapshot directory" "none"))
     28   | otherwise = R.withReaderLease runtime scope reader (\receive -> mask $ \restore -> do
     29       slot <- newIORef Nothing
     30       acquired <- R.acquire runtime scope $ do
     31         (path,h) <- openBinaryTempFile directory ".tricu-bytes-"
     32         (do
     33           removeFile path
     34           hSetBuffering h NoBuffering
     35           writeIORef slot (Just h)
     36           pure (R.File (R.FileOps (Just (BS.hGetSome h)) Nothing
     37             (Just (hSeek h AbsoluteSeek)) (Just (hTell h)) (hFileSize h)
     38             Nothing Nothing (release h)))) `onException` hClose h
     39       token <- either R.resourceFault pure acquired
     40       Just h <- readIORef slot
     41       -- A lease protects the registered spool throughout preparation. Exceptions
     42       -- leave it to scope settlement, retaining cancellation/cleanup precedence.
     43       result <- restore $ R.withReaderLease runtime scope token $ \_ -> do
     44         let copy !count !context = do
     45               allowInterrupt
     46               bytes <- receive (fromInteger (min (toInteger R.chunkLimit) (limit-count+1)))
     47               if BS.null bytes then pure (count,context) else do
     48                 let nextCount = count + toInteger (BS.length bytes)
     49                 when (nextCount > limit) (R.resourceFault (fault "invalid-bound" "stream exceeds byte limit" "unknown"))
     50                 BS.hPut h bytes
     51                 next <- evaluate (hashUpdate context bytes)
     52                 copy nextCount next
     53         (count,context) <- copy 0 (hashInit :: Context SHA256)
     54         unless (show (hashFinalize context) == expected)
     55           (R.resourceFault (fault "integrity-failed" "SHA-256 mismatch" "unknown"))
     56         hSeek h AbsoluteSeek 0
     57         pure count
     58       case result of
     59         Right count -> pure (token,count)
     60         Left primary -> do
     61           closed <- R.closeResource runtime scope token
     62           R.resourceFault $ case closed of
     63             Right () -> primary
     64             Left cleanup -> primary { R.detail = take 240 (R.detail primary) ++ "; cleanup: " ++ take 240 (show cleanup) })
     65   where fault category detail progress = R.Problem category "snapshot" detail progress
     66 
     67 -- Exact byte count, starting at the current cursor; no EOF probe. Both leases
     68 -- cover the transfer. A failure may follow partial writes: callers close rather
     69 -- than retrying or emitting a second response. No encoded byte trees are built.
     70 transfer :: R.Runtime -> R.Scope -> R.Token -> R.Token -> Integer -> IO (Either R.Problem ())
     71 transfer runtime scope reader writer count
     72   | count < 0 || count > R.positionLimit = pure (Left (R.Problem "invalid-bound" "transfer" "invalid byte count" "none"))
     73   | otherwise = R.withReaderLease runtime scope reader (\receive ->
     74       R.withWriterLease runtime scope writer (\send -> do
     75         let sendAll bytes = unless (BS.null bytes) $ do
     76               n <- send bytes
     77               sendAll (BS.drop n bytes)
     78             copy !remaining = when (remaining > 0) $ do
     79               allowInterrupt
     80               bytes <- receive (fromInteger (min remaining (toInteger R.chunkLimit)))
     81               when (BS.null bytes) (R.resourceFault (R.Problem "invalid-data" "transfer" "unexpected EOF" "unknown"))
     82               sendAll bytes
     83               copy (remaining - toInteger (BS.length bytes))
     84         copy count) >>= either R.resourceFault pure)