ByteStream.hs (4706B)
1 {-# LANGUAGE BangPatterns #-} 2 -- Native bulk bytes; source owns paths, digest selection and response policy. 3 module IO.ByteStream (snapshot, snapshotWith, transfer) where 4 5 import qualified IO.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)