tricu

A lazy functional language based on Tree Calculus
Log | Files | Refs | README | LICENSE

Snapshot.hs (4919B)


      1 {-# LANGUAGE BangPatterns #-}
      2 {-# LANGUAGE RankNTypes #-}
      3 -- Internal authenticated transport for indexed codecs, not a store service.
      4 -- No paths, source trees or application publication policy are interpreted here.
      5 module ContentStore.Snapshot
      6   ( Snapshot, snapshotLength, readSnapshot
      7   , SnapshotError(..), withAuthenticatedSnapshot, withAuthenticatedSnapshotUsing
      8   ) where
      9 
     10 import ContentStore.Object (Domain(..), ObjectHash, hashToText, validateObjectHash)
     11 import Control.Concurrent.MVar
     12 import Control.Exception
     13 import Control.Monad (unless, when)
     14 import Crypto.Hash (Context, SHA256, hashInit, hashUpdate, hashFinalize)
     15 import qualified Data.ByteString as BS
     16 import Data.Text.Encoding (encodeUtf8)
     17 import System.Directory (removeFile)
     18 import System.IO
     19 
     20 -- The constructor and writable handle never escape this module. A Snapshot is
     21 -- valid only during the callback; its read operation serializes seek/read pairs.
     22 -- This is not confinement against other code with the process's OS authority.
     23 data Snapshot = Snapshot !Integer !(MVar Handle)
     24 
     25 snapshotLength :: Snapshot -> Integer
     26 snapshotLength (Snapshot size _) = size
     27 
     28 data SnapshotError = InvalidSnapshotBound | InvalidSnapshotDigest
     29   | SnapshotTruncated | SnapshotReaderExceededBound | SnapshotDigestMismatch
     30   | InvalidSnapshotRange
     31   deriving (Eq, Show)
     32 instance Exception SnapshotError
     33 
     34 chunkSize :: Int
     35 chunkSize = 4096
     36 
     37 -- Both limits are checked before allocating a file or consuming input. The
     38 -- caller supplies an EXACT frame length and a policy limit: neither an EOF probe
     39 -- nor any byte of the following frame is requested. A reader must return between
     40 -- zero and the requested number of bytes; zero before the boundary is truncation.
     41 --
     42 -- Disk: one unlinked file, at most size payload bytes (+ filesystem bookkeeping).
     43 -- Transfer memory: one bounded chunk and hash context, independent of size.
     44 -- Authentication finishes before invoking the decoder callback, against the same
     45 -- private bytes it will read, never a second pass over the original source.
     46 -- No fsync is necessary: this is a temporary snapshot, not durable publication.
     47 withAuthenticatedSnapshot :: FilePath -> Integer -> Integer -> Domain -> ObjectHash
     48   -> (Int -> IO BS.ByteString) -> (Snapshot -> IO a) -> IO a
     49 withAuthenticatedSnapshot = withAuthenticatedSnapshotUsing (\allocate use -> bracket allocate hClose use)
     50 
     51 -- Native structured IO supplies registry-backed lifetime management here, so a
     52 -- terminal fault/cancellation leaves cleanup to lexical settlement (including its
     53 -- diagnostic precedence), rather than allowing bracket cleanup to mask a fault.
     54 withAuthenticatedSnapshotUsing :: (forall a. IO Handle -> (Handle -> IO a) -> IO a)
     55   -> FilePath -> Integer -> Integer -> Domain -> ObjectHash
     56   -> (Int -> IO BS.ByteString) -> (Snapshot -> IO b) -> IO b
     57 withAuthenticatedSnapshotUsing lifetime directory limit size (Domain domain) expected receive consume = do
     58   when (limit < 0 || size < 0 || size > limit || limit > 2^(63 :: Int)-1) $
     59     throwIO InvalidSnapshotBound
     60   case validateObjectHash expected of
     61     Left _ -> throwIO InvalidSnapshotDigest
     62     Right () -> pure ()
     63   lifetime acquire $ \handle -> do
     64     let initial = hashUpdate (hashInit :: Context SHA256) (encodeUtf8 domain <> BS.singleton 0)
     65         copy !remaining !context
     66           | remaining == 0 = pure context
     67           | otherwise = do
     68               let wanted = fromInteger (min remaining (toInteger chunkSize))
     69               bytes <- receive wanted
     70               let count = BS.length bytes
     71               when (count > wanted) (throwIO SnapshotReaderExceededBound)
     72               when (count == 0) (throwIO SnapshotTruncated)
     73               BS.hPut handle bytes
     74               next <- evaluate (hashUpdate context bytes)
     75               copy (remaining - toInteger count) next
     76     context <- copy size initial
     77     unless (hashToText (hashFinalize context) == expected) (throwIO SnapshotDigestMismatch)
     78     hFlush handle
     79     mutex <- newMVar handle
     80     consume (Snapshot size mutex)
     81   where
     82     acquire = mask_ $ do
     83       (path, handle) <- openBinaryTempFile directory ".tricu-snapshot-"
     84       -- Unlink before reading any untrusted input. No named scratch artifact is
     85       -- retained after successful acquisition, including on process termination.
     86       (removeFile path >> hSetBuffering handle NoBuffering >> pure handle)
     87         `onException` hClose handle
     88 
     89 -- Bounded positional reads from the authenticated snapshot. The supplied offset
     90 -- is absolute; EOF is sticky by length, not inferred from the public input.
     91 readSnapshot :: Snapshot -> Integer -> Int -> IO BS.ByteString
     92 readSnapshot (Snapshot size mutex) offset count = do
     93   when (offset < 0 || offset > size || count < 0 || count > chunkSize) $
     94     throwIO InvalidSnapshotRange
     95   withMVar mutex $ \handle -> do
     96     hSeek handle AbsoluteSeek offset
     97     BS.hGet handle (fromInteger (min (toInteger count) (size - offset)))