packages feed

tahoe-great-black-swamp-types-0.5.0.0: src/Tahoe/Storage/Backend.hs

{- | Types involved in the implementation of the backend for a Tahoe-LAFS
 storage server.
-}
module Tahoe.Storage.Backend where

import Control.Exception (
    Exception,
 )
import Data.Aeson (
    FromJSON (..),
    FromJSONKey (..),
    ToJSON (..),
    Value (String),
    object,
    withObject,
    withText,
    (.:),
    (.=),
 )
import Data.ByteArray (constEq)
import qualified Data.ByteString as B
import qualified "base64-bytestring" Data.ByteString.Base64 as Base64
import Data.Hashable (Hashable (hashWithSalt))
import Data.Map.Merge.Strict (merge, preserveMissing, zipWithMatched)
import qualified Data.Map.Strict as Map
import qualified Data.Set as Set
import qualified Data.Text.Encoding as T
import GHC.Generics (
    Generic,
 )
import Network.HTTP.Types (
    ByteRanges,
 )

-- | A human-readable description of the backend software in use.
type ApplicationVersion = B.ByteString

-- | Give certain operational details about this storage server.
data Version1Parameters = Version1Parameters
    { maximumImmutableShareSize :: Size
    , maximumMutableShareSize :: Size
    , availableSpace :: Size
    }
    deriving (Show, Eq, Generic)

-- | Carry a version string and operational parameters.
data Version = Version
    { parameters :: Version1Parameters
    , applicationVersion :: ApplicationVersion
    }
    deriving (Show, Eq, Generic)

-- | The number of bytes in some data.
type Size = Integer

{- | A position in some data identified by a number of bytes into that data
 from the beginning.
-}
type Offset = Integer

{- | Some ranges of data, possibly.  Nothing conventionally refers to the
 entire data.
-}
type QueryRange = Maybe ByteRanges

{- | An opaque identifier for a single storage area.  Multiple storage objects
 may exist at one storage index if they have different share numbers.  TODO:
 This should probably be ByteString instead.
-}
type StorageIndex = String

-- | Some data.
type ShareData = B.ByteString

{- | The identifier of a distinct storage object, unique within the context of
 a particular storage index.  In practice, values are in the range [0..255]
 (endpoints included).
-}
newtype ShareNumber = ShareNumber Integer
    deriving
        ( Show
        , Eq
        , Ord
        , Generic
        )
    deriving newtype
        ( ToJSON
        , FromJSON
        , FromJSONKey
        )

instance Hashable ShareNumber where
    hashWithSalt i (ShareNumber num) = hashWithSalt i num

{- | A new type for which we can define our own CBOR serialisation rules.  The
 cborg library provides a Serialise instance for Set which is not compatible
 with the representation required by Tahoe-LAFS.
-}
newtype CBORSet a = CBORSet
    { getCBORSet :: Set.Set a
    }
    deriving newtype (ToJSON, FromJSON, Show, Eq)

-- | Describe a client-detected incidence of data corruption.
newtype CorruptionDetails = CorruptionDetails
    { reason :: String
    }
    deriving (Show, Eq, Generic)

{- | A secret shared between a client and this storage server for the purpose
 of authorizing certain operations related to immutable uploads.
-}
newtype UploadSecret = UploadSecret B.ByteString

instance Eq UploadSecret where
    (UploadSecret left) == (UploadSecret right) = constEq left right

{- | A secret shared between an SSK write capability holder and this storage
 server for the purpose of authorizing write operations on a mutable object.
-}
newtype WriteEnablerSecret = WriteEnablerSecret B.ByteString

instance Eq WriteEnablerSecret where
    (WriteEnablerSecret left) == (WriteEnablerSecret right) = constEq left right

-- | Describe tests, reads, and writes to perform on a mutable object.
data ReadTestWriteVectors = ReadTestWriteVectors
    { testWriteVectors :: Map.Map ShareNumber TestWriteVectors
    , readVector :: [ReadVector]
    }
    deriving (Show, Eq, Generic)

-- | Describe tests and writes to perform on a mutable object.
data TestWriteVectors = TestWriteVectors
    { test :: [TestVector]
    , write :: [WriteVector]
    , -- | If given, truncate or extend the object to the given size.  If
      -- necessary, fill new space with NUL.
      newLength :: Maybe Integer
    }
    deriving (Show, Eq, Generic, ToJSON, FromJSON)

-- | Describe a single test to perform on a mutable object.
data TestVector = TestVector
    { testOffset :: Offset
    , testSize :: Size
    , operator :: TestOperator
    , specimen :: ShareData
    }
    deriving (Show, Eq, Generic)

-- | Describe a single write to perform on a mutable object.
data WriteVector = WriteVector
    { writeOffset :: Offset
    , shareData :: ShareData
    }
    deriving (Show, Eq, Generic)

-- | Describe one read to perform on an immutable object.
data ReadVector = ReadVector
    { offset :: Offset
    , readSize :: Size
    }
    deriving (Show, Eq, Generic)

{- | The result of a request to read and/or write some data from/to some
 shares.
-}
data ReadTestWriteResult = ReadTestWriteResult
    { success :: Bool
    , readData :: ReadResult
    }
    deriving (Show, Eq, Generic)

-- | The result of a request to read some data from some shares.
type ReadResult = Map.Map ShareNumber [ShareData]

instance Semigroup TestWriteVectors where
    (TestWriteVectors testL writeL _) <> (TestWriteVectors testR writeR newLengthR) =
        TestWriteVectors (testL <> testR) (writeL <> writeR) newLengthR

instance Monoid TestWriteVectors where
    mempty = TestWriteVectors mempty mempty Nothing

instance Monoid ReadTestWriteVectors where
    mempty = ReadTestWriteVectors mempty []

instance Semigroup ReadTestWriteVectors where
    (ReadTestWriteVectors wv0 rv0) <> (ReadTestWriteVectors wv1 rv1) =
        ReadTestWriteVectors (merge preserveMissing preserveMissing (zipWithMatched $ \_ l r -> l <> r) wv0 wv1) (rv0 <> rv1)

{- | Create a ReadTestWriteVectors which performs one read at the given offset
 of the given size.
-}
readv :: Offset -> Size -> ReadTestWriteVectors
readv offset size = mempty{readVector = [ReadVector offset size]}

{- | Create a ReadTestWriteVectors which performs one write on the given share
 number at the given offset.
-}
writev :: ShareNumber -> Offset -> ShareData -> ReadTestWriteVectors
writev shareNum offset bytes = mempty{testWriteVectors = Map.singleton shareNum (mempty{write = [WriteVector offset bytes]})}

{- | Create a ReadTestWriteVectors which performs one test on the given share
 number using the given specimen.
-}
testv :: ShareNumber -> Offset -> ShareData -> ReadTestWriteVectors
testv shareNum offset specimen =
    mempty
        { testWriteVectors = Map.singleton shareNum (mempty{test = [TestVector offset (fromIntegral $ B.length specimen) Eq specimen]})
        }

-- | Describe some allocations for immutable objects requested by a client.
data AllocateBuckets = AllocateBuckets
    { shareNumbers :: [ShareNumber]
    , allocatedSize :: Size
    }
    deriving (Show, Eq, Generic)

{- | Describe the server's willingness to allocate space for some immutable
 objects.
-}
data AllocationResult = AllocationResult
    { alreadyHave :: [ShareNumber]
    , allocated :: [ShareNumber]
    }
    deriving (Show, Eq, Generic)

instance Semigroup AllocationResult where
    AllocationResult xa ya <> AllocationResult xb yb = AllocationResult (xa <> xb) (ya <> yb)

instance Monoid AllocationResult where
    mempty = AllocationResult mempty mempty

{- | One of several kinds of shared secrets for authorizing a client to
 perform various operations.
-}
data LeaseSecret = Renew B.ByteString | Cancel B.ByteString | Upload UploadSecret | Write WriteEnablerSecret

-- XXX Most of these operators have been removed from the spec.
data TestOperator
    = Lt
    | Le
    | Eq
    | Ne
    | Ge
    | Gt
    deriving (Show, Eq, Generic, ToJSON, FromJSON)

-- | A ByteString wrapper which can have its own serialization semantics.
newtype Base64 = Base64 {unBase64 :: B.ByteString}

-- | Serialize a ByteString by encoding with Base64.
instance ToJSON Base64 where
    toJSON = String . T.decodeLatin1 . Base64.encode . unBase64

-- | Deserialize a ByteString which has been encoded with Base64.
instance FromJSON Base64 where
    parseJSON =
        withText
            "Base64-encoded ByteString"
            ( \bs ->
                case Base64.decode $ T.encodeUtf8 bs of
                    Left err -> fail ("Base64 decoding failed: " <> err)
                    Right bytes -> pure $ Base64 bytes
            )

-- | Serialize to the JSON as specified by the specification.
instance ToJSON TestVector where
    toJSON (TestVector{..}) =
        object ["offset" .= testOffset, "size" .= testSize, "specimen" .= Base64 specimen]

-- | Deserialize from JSON as specified by the specification.
instance FromJSON TestVector where
    parseJSON = withObject "TestVector" $ \v ->
        TestVector <$> v .: "offset" <*> v .: "size" <*> pure Eq <*> (unBase64 <$> v .: "specimen")

-- | Serialize to the JSON as specified by the specification.
instance ToJSON WriteVector where
    toJSON (WriteVector{..}) =
        object ["offset" .= writeOffset, "data" .= Base64 shareData]

-- | Deserialize from JSON as specified by the specification.
instance FromJSON WriteVector where
    parseJSON = withObject "WriteVector" $ \v ->
        WriteVector <$> v .: "offset" <*> (unBase64 <$> v .: "data")

{- | Exceptional cases which might be encounted by various backend for
 operations related to immutable shares.
-}
data WriteImmutableError
    = -- | Used to reject an immutable allocate or upload with no upload secret.
      MissingUploadSecret
    | -- | Used to reject an immutable upload with an incorrect upload secret.
      IncorrectUploadSecret
    | -- | Used to reject an immutable allocate of a size that does not match
      -- existing shares with the same storage index.
      ShareSizeMismatch
    | -- | Used to reject an immutable allocate of a size greater than the
      -- maximum allowed by the server.
      MaximumShareSizeExceeded
        { maximumShareSizeExceededLimit :: Integer
        , maximumShareSizeExceededGiven :: Integer
        }
    | -- | Used to reject an immutable write to a share that is already
      -- completely written.
      ImmutableShareAlreadyWritten
    | -- | Used to reject an immutable write to a share which has not been
      -- allocated.
      ShareNotAllocated
    | -- | Used to reject an immutable write that overlaps with data that has
      -- already been written.
      ConflictingWrite
    deriving (Ord, Eq, Show)

instance Exception WriteImmutableError

{- | Exceptional cases which might be encounted by various backend for
 operations related to mutable shares.
-}
data WriteMutableError
    = -- | Used to reject a mutable write with no write enabler secret.
      MissingWriteEnablerSecret
    | -- | Used to reject a mutable write with an incorrect write enabler secret.
      IncorrectWriteEnablerSecret
    deriving (Ord, Eq, Show)

instance Exception WriteMutableError

class Backend b where
    version :: b -> IO Version

    -- | Update the lease expiration time on the shares associated with the
    -- given storage index.
    renewLease :: b -> StorageIndex -> [LeaseSecret] -> IO ()

    createImmutableStorageIndex :: b -> StorageIndex -> Maybe [LeaseSecret] -> AllocateBuckets -> IO AllocationResult

    -- May throw ImmutableShareAlreadyWritten
    -- XXX Return should indicate what remains to be written
    writeImmutableShare :: b -> StorageIndex -> ShareNumber -> Maybe [LeaseSecret] -> ShareData -> Maybe ByteRanges -> IO ()
    abortImmutableUpload :: b -> StorageIndex -> ShareNumber -> Maybe [LeaseSecret] -> IO ()
    adviseCorruptImmutableShare :: b -> StorageIndex -> ShareNumber -> CorruptionDetails -> IO ()
    getImmutableShareNumbers :: b -> StorageIndex -> IO (CBORSet ShareNumber)
    readImmutableShare :: b -> StorageIndex -> ShareNumber -> QueryRange -> IO ShareData

    -- | Read some ranges of all shares held and/or, if test conditions are
    -- met, overwrite some ranges of some shares.
    readvAndTestvAndWritev ::
        b ->
        -- | The storage index at which to operate.
        StorageIndex ->
        -- | A shared secret which the backend can use to authorize the writes.
        WriteEnablerSecret ->
        -- | The reads, tests, and writes to perform.
        ReadTestWriteVectors ->
        IO ReadTestWriteResult

    readMutableShare :: b -> StorageIndex -> ShareNumber -> QueryRange -> IO ShareData
    getMutableShareNumbers :: b -> StorageIndex -> IO (CBORSet ShareNumber)
    adviseCorruptMutableShare :: b -> StorageIndex -> ShareNumber -> CorruptionDetails -> IO ()