diff --git a/src/TahoeLAFS/Storage/API.hs b/src/TahoeLAFS/Storage/API.hs index ad2a9b13209c1452bc46cfe5dee69424ed510b37..b4b5bd0b35ac40816fd5e4cb117b972eb5fdc3c3 100644 --- a/src/TahoeLAFS/Storage/API.hs +++ b/src/TahoeLAFS/Storage/API.hs @@ -28,10 +28,9 @@ module TahoeLAFS.Storage.API ( WriteVector (..), ReadTestWriteVectors (..), ReadTestWriteResult (..), - ReadVectors, - ReadVector, + ReadVector (..), QueryRange, - TestVector (TestVector), + TestVector (..), ReadResult, CorruptionDetails (..), TestOperator (..), @@ -47,6 +46,9 @@ module TahoeLAFS.Storage.API ( leaseCancelSecretLength, CBOR, CBORSet (..), + readv, + writev, + testv, ) where import Codec.CBOR.Encoding (encodeBytes) @@ -71,9 +73,11 @@ import Data.Aeson.Types ( toJSONKeyText, ) import Data.Bifunctor (Bifunctor (bimap)) +import Data.ByteArray (constEq) import qualified Data.ByteString as B import qualified "base64-bytestring" Data.ByteString.Base64 as Base64 import qualified Data.Map as Map +import Data.Map.Merge.Strict (merge, preserveMissing, zipWithMatched) import Data.Map.Strict ( Map, ) @@ -411,6 +415,9 @@ instance ToHttpApiData ByteRanges where newtype UploadSecret = UploadSecret B.ByteString newtype WriteEnablerSecret = WriteEnablerSecret B.ByteString +instance Eq WriteEnablerSecret where + (WriteEnablerSecret left) == (WriteEnablerSecret right) = constEq left right + data LeaseSecret = Renew B.ByteString | Cancel B.ByteString | Upload UploadSecret | Write WriteEnablerSecret isUploadSecret :: LeaseSecret -> Bool @@ -523,18 +530,6 @@ type StorageAPI = type ReadResult = Map ShareNumber [ShareData] -data ReadVectors = ReadVectors - { shares :: [ShareNumber] - , readVectors :: [ReadVector] - } - deriving (Show, Eq, Generic) - -instance ToJSON ReadVectors where - toJSON = genericToJSON tahoeJSONOptions - -instance FromJSON ReadVectors where - parseJSON = genericParseJSON tahoeJSONOptions - data ReadTestWriteResult = ReadTestWriteResult { success :: Bool , readData :: ReadResult @@ -587,9 +582,35 @@ data TestWriteVectors = TestWriteVectors } deriving (Show, Eq, Generic, ToJSON, FromJSON) +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) + -- XXX This derived instance is surely not compatible with Tahoe-LAFS. instance Serialise TestWriteVectors +readv :: Offset -> Size -> ReadTestWriteVectors +readv offset size = mempty{readVector = [ReadVector offset size]} + +writev :: ShareNumber -> Offset -> ShareData -> ReadTestWriteVectors +writev shareNum offset bytes = mempty{testWriteVectors = Map.singleton shareNum (mempty{write = [WriteVector offset bytes]})} + +testv :: ShareNumber -> Offset -> ShareData -> ReadTestWriteVectors +testv shareNum offset specimen = + mempty + { testWriteVectors = Map.singleton shareNum (mempty{test = [TestVector offset (fromIntegral $ B.length specimen) Eq specimen]}) + } + -- XXX Most of these operators have been removed from the spec. data TestOperator = Lt @@ -620,7 +641,6 @@ data WriteVector = WriteVector } deriving (Show, Eq, Generic, ToJSON, FromJSON) --- XXX This derived instance is surely not compatible with Tahoe-LAFS. instance Serialise WriteVector api :: Proxy StorageAPI diff --git a/src/TahoeLAFS/Storage/Backend.hs b/src/TahoeLAFS/Storage/Backend.hs index dd99152eafbd481bab4162b8d8f3d891925e1f1f..5d772befe6ff8222a7668ae25212a8a2801f75ba 100644 --- a/src/TahoeLAFS/Storage/Backend.hs +++ b/src/TahoeLAFS/Storage/Backend.hs @@ -46,6 +46,7 @@ data WriteImmutableError | ImmutableShareAlreadyWritten | ShareNotAllocated | IncorrectUploadSecret + | IncorrectWriteEnablerSecret deriving (Ord, Eq, Show) instance Exception WriteImmutableError diff --git a/src/TahoeLAFS/Storage/Backend/Filesystem.hs b/src/TahoeLAFS/Storage/Backend/Filesystem.hs index 6b2babfe56dca1f9b076aa40d0fdf99acca72056..e42c0d04e51691e982ecc0089fe10d933603bdae 100644 --- a/src/TahoeLAFS/Storage/Backend/Filesystem.hs +++ b/src/TahoeLAFS/Storage/Backend/Filesystem.hs @@ -180,7 +180,7 @@ instance Backend FilesystemBackend where (ReadTestWriteVectors testWritev _readv) = do -- TODO implement readv and testv parts. -- TODO implement secrets - mapM_ (applyWriteVectors root storageIndex) $ toList testWritev + mapM_ applyWriteVectors $ toList testWritev return ReadTestWriteResult { success = True @@ -188,34 +188,30 @@ instance Backend FilesystemBackend where } where applyWriteVectors :: - FilePath -> - StorageIndex -> (ShareNumber, TestWriteVectors) -> IO () - applyWriteVectors _root _storageIndex (shareNumber', testWriteVectors) = - mapM_ (applyShareWrite root storageIndex shareNumber') (write testWriteVectors) + applyWriteVectors (shareNumber', testWriteVectors) = + mapM_ (applyShareWrite shareNumber') (write testWriteVectors) applyShareWrite :: - FilePath -> - StorageIndex -> ShareNumber -> WriteVector -> IO () - applyShareWrite _root _storageIndex shareNumber' (WriteVector offset shareData) = - let sharePath = pathOfShare root storageIndex shareNumber' - createParents = True - in do - createDirectoryIfMissing createParents $ takeDirectory sharePath - withBinaryFile sharePath ReadWriteMode (writeAtPosition offset shareData) + applyShareWrite shareNumber' (WriteVector offset shareData) = do + createDirectoryIfMissing createParents $ takeDirectory sharePath + withBinaryFile sharePath ReadWriteMode (writeAtPosition offset shareData) where - writeAtPosition :: - Offset -> - ShareData -> - Handle -> - IO () - writeAtPosition _offset shareData' handle = do - hSeek handle AbsoluteSeek offset - hPut handle shareData' + sharePath = pathOfShare root storageIndex shareNumber' + createParents = True + + writeAtPosition :: + Offset -> + ShareData -> + Handle -> + IO () + writeAtPosition offset shareData' handle = do + hSeek handle AbsoluteSeek offset + hPut handle shareData' -- Does the given backend have the complete share indicated? haveShare :: diff --git a/src/TahoeLAFS/Storage/Backend/Memory.hs b/src/TahoeLAFS/Storage/Backend/Memory.hs index 7efbd7da583cf6bde9c4b52711a4529372b5d32c..b10ee5413361df315fe5dece9d5f44247f34ee8d 100644 --- a/src/TahoeLAFS/Storage/Backend/Memory.hs +++ b/src/TahoeLAFS/Storage/Backend/Memory.hs @@ -3,11 +3,16 @@ module TahoeLAFS.Storage.Backend.Memory ( MemoryBackend (MemoryBackend), memoryBackend, + MutableShareSize (..), + shareDataSize, + toMutableShareSize, ) where import Control.Exception ( throw, + throwIO, ) +import Control.Foldl.ByteString (Word8) import Data.ByteArray (constEq) import qualified Data.ByteString as B import Data.IORef ( @@ -17,25 +22,21 @@ import Data.IORef ( newIORef, readIORef, ) -import Data.Map.Strict ( - Map, - adjust, - fromList, - insert, - keys, - lookup, - map, - toList, - ) +import Data.Map.Merge.Strict (merge, preserveMissing, zipWithMatched) import qualified Data.Map.Strict as Map -import Data.Maybe (isNothing) +import Data.Maybe (fromMaybe, isNothing) +import Data.Monoid (Last (Last, getLast)) import qualified Data.Set as Set +import Network.HTTP.Types (ByteRange (ByteRangeFrom, ByteRangeFromTo, ByteRangeSuffix)) import TahoeLAFS.Storage.API ( AllocateBuckets (AllocateBuckets), AllocationResult (..), CBORSet (..), + Offset, + QueryRange, ReadTestWriteResult (..), ReadTestWriteVectors (..), + ReadVector (ReadVector, offset, readSize), ShareData, ShareNumber, Size, @@ -44,12 +45,12 @@ import TahoeLAFS.Storage.API ( UploadSecret (UploadSecret), Version (..), Version1Parameters (..), - WriteEnablerSecret, + WriteEnablerSecret (WriteEnablerSecret), WriteVector (..), ) import TahoeLAFS.Storage.Backend ( Backend (..), - WriteImmutableError (ImmutableShareAlreadyWritten, IncorrectUploadSecret, ShareNotAllocated, ShareSizeMismatch), + WriteImmutableError (ImmutableShareAlreadyWritten, IncorrectUploadSecret, IncorrectWriteEnablerSecret, ShareNotAllocated, ShareSizeMismatch), withUploadSecret, ) import Prelude hiding ( @@ -57,18 +58,58 @@ import Prelude hiding ( map, ) -data Share = Complete ShareData | Uploading UploadSecret ShareData +data ImmutableShare = Complete ShareData | Uploading UploadSecret ShareData data Bucket = Bucket { bucketSize :: Size - , bucketShares :: Map ShareNumber Share + , bucketShares :: Map.Map ShareNumber ImmutableShare } -type ShareStorage = Map StorageIndex (Map ShareNumber ShareData) +data SecretProtected a = SecretProtected WriteEnablerSecret a + +readSecret :: SecretProtected a -> WriteEnablerSecret +readSecret (SecretProtected s _) = s + +readProtected :: SecretProtected a -> a +readProtected (SecretProtected _ p) = p + +{- | Apply a function in a SecretProtected to a value in a SecretProtected. The + result is in SecretProtected with the function's secret. + + This is almost liftA2 but it's not clear to me how to have lawful handling of + the secret. +-} +liftProtected2 :: (a -> a -> a) -> SecretProtected a -> SecretProtected a -> SecretProtected a +liftProtected2 f (SecretProtected secretL x) (SecretProtected _ y) = SecretProtected secretL (f x y) + +instance Functor SecretProtected where + fmap f (SecretProtected secret a) = SecretProtected secret (f a) + +type MutableShareStorage = Map.Map StorageIndex (SecretProtected (Map.Map ShareNumber [WriteVector])) + +data MutableShareSize = MutableShareSize Offset Size deriving (Show, Eq) + +instance Semigroup MutableShareSize where + (MutableShareSize writeOffsetL sizeL) <> (MutableShareSize writeOffsetR sizeR) = + MutableShareSize minOffset maxSize + where + minOffset = min writeOffsetL writeOffsetR + maxSize = max (writeOffsetL + sizeL) (writeOffsetR + sizeR) - minOffset + +instance Monoid MutableShareSize where + mempty = MutableShareSize 0 0 + +toMutableShareSize :: WriteVector -> MutableShareSize +toMutableShareSize (WriteVector offset bytes) = MutableShareSize offset (fromIntegral $ B.length bytes) + +shareDataSize :: [WriteVector] -> Size +shareDataSize writev = offset + size + where + (MutableShareSize offset size) = foldMap toMutableShareSize writev data MemoryBackend = MemoryBackend - { memoryBackendBuckets :: Map StorageIndex Bucket -- Completely or partially written immutable share data - , mutableShares :: ShareStorage -- Completely written mutable shares + { memoryBackendBuckets :: Map.Map StorageIndex Bucket -- Completely or partially written immutable share data + , mutableShares :: MutableShareStorage -- Completely written mutable shares } getShareNumbers :: StorageIndex -> MemoryBackend -> CBORSet ShareNumber @@ -125,13 +166,13 @@ abort :: (MemoryBackend, ()) abort storageIndex shareNumber (UploadSecret abortSecret) b@MemoryBackend{memoryBackendBuckets} = (b{memoryBackendBuckets = updated memoryBackendBuckets}, ()) where - updated :: Map StorageIndex Bucket -> Map StorageIndex Bucket + updated :: Map.Map StorageIndex Bucket -> Map.Map StorageIndex Bucket updated = Map.adjust abortIt storageIndex abortIt :: Bucket -> Bucket abortIt bucket@Bucket{bucketShares} = bucket{bucketShares = Map.update abortIt' shareNumber bucketShares} - abortIt' :: Share -> Maybe Share + abortIt' :: ImmutableShare -> Maybe ImmutableShare abortIt' (Uploading (UploadSecret existingSecret) _) = if constEq existingSecret abortSecret then Nothing else throw IncorrectUploadSecret abortIt' _ = throw ImmutableShareAlreadyWritten @@ -151,7 +192,7 @@ writeImm storageIndex shareNum (UploadSecret uploadSecret) newData b@MemoryBacke size = bucketSize <$> bucket updated = Map.adjust (\bkt -> bkt{bucketShares = Map.adjust writeToShare shareNum (bucketShares bkt)}) storageIndex memoryBackendBuckets - writeToShare :: Share -> Share + writeToShare :: ImmutableShare -> ImmutableShare writeToShare (Complete _) = throw ImmutableShareAlreadyWritten writeToShare (Uploading (UploadSecret existingSecret) existingData) | authorized = @@ -181,35 +222,59 @@ instance Backend (IORef MemoryBackend) where getMutableShareNumbers :: IORef MemoryBackend -> StorageIndex -> IO (CBORSet ShareNumber) getMutableShareNumbers backend storageIndex = do - shares' <- mutableShares <$> readIORef backend - return $ - CBORSet . Set.fromList $ - maybe [] keys $ - lookup storageIndex shares' + sharemap <- fmap readProtected . Map.lookup storageIndex . mutableShares <$> readIORef backend + return + . CBORSet + . Set.fromList + . maybe [] Map.keys + $ sharemap readvAndTestvAndWritev :: IORef MemoryBackend -> StorageIndex -> WriteEnablerSecret -> ReadTestWriteVectors -> IO ReadTestWriteResult readvAndTestvAndWritev backend storageIndex - _secrets - (ReadTestWriteVectors testWritev _readv) = do - -- TODO implement readv and testv parts. - -- TODO implement secret check - modifyIORef backend $ \m@MemoryBackend{mutableShares} -> m{mutableShares = addShares storageIndex (shares' testWritev) mutableShares} - return - ReadTestWriteResult - { success = True - , readData = mempty - } + secret + (ReadTestWriteVectors testWritev readv) = do + -- TODO implement testv parts. + + (CBORSet allShareNums) <- getMutableShareNumbers backend storageIndex + let queryRange = readvToQueryRange readv + + readData <- mapM (\shareNum -> (shareNum,) <$> readMutableShare' backend storageIndex shareNum queryRange) (Set.toList allShareNums) + outcome <- atomicModifyIORef' backend tryWrite + case outcome of + TestSuccess -> + return + ReadTestWriteResult + { readData = Map.fromList readData + , success = True + } + TestFail -> + return + ReadTestWriteResult + { readData = Map.fromList readData + , success = False + } + SecretMismatch -> + throwIO IncorrectWriteEnablerSecret where - shares' :: - Map ShareNumber TestWriteVectors -> - [(ShareNumber, ShareData)] - shares' testWritevs = - [ (shareNumber, shareData writev) - | (shareNumber, testWritev') <- toList testWritevs - , writev <- write testWritev' - ] + readvToQueryRange :: [ReadVector] -> QueryRange + -- readvToQueryRange [] = Nothing + readvToQueryRange rv = Just (go rv) + where + go [] = [] + go (r : rs) = ByteRangeFromTo off end : go rs + where + off = offset r + end = off + readSize r - 1 + + tryWrite m@MemoryBackend{mutableShares} = + case addShares storageIndex secret mutableShares (Map.map write testWritev) of + Nothing -> (m, SecretMismatch) + Just newShares -> (m{mutableShares = newShares}, TestSuccess) + + readMutableShare backend storageIndex shareNum queryRange = + B.concat <$> readMutableShare' backend storageIndex shareNum queryRange createImmutableStorageIndex backend storageIndex secrets (AllocateBuckets shareNums size) = withUploadSecret secrets $ \secret -> @@ -231,9 +296,9 @@ instance Backend (IORef MemoryBackend) where readImmutableShare backend storageIndex shareNum _qr = do buckets <- memoryBackendBuckets <$> readIORef backend - case lookup storageIndex buckets of + case Map.lookup storageIndex buckets of Nothing -> pure mempty - Just bucket -> case lookup shareNum (bucketShares bucket) of + Just bucket -> case Map.lookup shareNum (bucketShares bucket) of Just (Complete shareData) -> pure shareData _ -> pure mempty @@ -241,25 +306,88 @@ totalShareSize :: MemoryBackend -> IO Size totalShareSize backend = do let imm = memoryBackendBuckets backend mut = mutableShares backend - let immSize = sum $ map bucketTotalSize imm - let mutSize = sum $ map length mut + let immSize = sum $ Map.map bucketTotalSize imm + let mutSize = sum $ Map.map (length . readProtected) mut return $ toInteger $ immSize + fromIntegral mutSize bucketTotalSize :: Bucket -> Size bucketTotalSize Bucket{bucketSize, bucketShares} = bucketSize * fromIntegral (Map.size bucketShares) -addShares :: StorageIndex -> [(ShareNumber, ShareData)] -> ShareStorage -> ShareStorage -addShares _storageIndex [] shareStorage = shareStorage -addShares storageIndex ((shareNumber, shareData) : rest) shareStorage = - let added = case lookup storageIndex shareStorage of - Nothing -> - insert storageIndex (fromList [(shareNumber, shareData)]) shareStorage - Just _shares -> - adjust addShare' storageIndex shareStorage - where - addShare' = insert shareNumber shareData - in addShares storageIndex rest added +addShare :: StorageIndex -> WriteEnablerSecret -> ShareNumber -> [WriteVector] -> MutableShareStorage -> MutableShareStorage +addShare storageIndex secret shareNum writev = + Map.insertWith (liftProtected2 f) storageIndex newShare + where + f :: Map.Map ShareNumber [WriteVector] -> Map.Map ShareNumber [WriteVector] -> Map.Map ShareNumber [WriteVector] + f = merge preserveMissing preserveMissing (zipWithMatched (const (<>))) + + newShare = SecretProtected secret (Map.singleton shareNum writev) + +addShares :: StorageIndex -> WriteEnablerSecret -> MutableShareStorage -> Map.Map ShareNumber [WriteVector] -> Maybe MutableShareStorage +addShares storageIndex secret existing updates + | isNothing existingSecret = Just go + | existingSecret == Just secret = Just go + | otherwise = Nothing + where + go = Map.foldrWithKey (addShare storageIndex secret) existing updates + + existingSecret = readSecret <$> Map.lookup storageIndex existing memoryBackend :: IO (IORef MemoryBackend) memoryBackend = do newIORef $ MemoryBackend mempty mempty + +readMutableShare' :: IORef MemoryBackend -> StorageIndex -> ShareNumber -> QueryRange -> IO [ShareData] +readMutableShare' backend storageIndex shareNum queryRange = do + storage <- mutableShares <$> readIORef backend + pure $ doOneRead <$> rv storage <*> pure storage + where + rv :: MutableShareStorage -> [ReadVector] + rv storage = queryRangeToReadVector storage queryRange + + getShareData storage = + Map.lookup storageIndex storage >>= Map.lookup shareNum . readProtected + + doOneRead :: ReadVector -> MutableShareStorage -> ShareData + doOneRead readv storage = + maybe "" (readOneVector readv) (getShareData storage) + + queryRangeToReadVector :: MutableShareStorage -> QueryRange -> [ReadVector] + queryRangeToReadVector storage Nothing = [ReadVector 0 size] + where + size = maybe 0 shareDataSize (getShareData storage) + queryRangeToReadVector storage (Just ranges) = toReadVector <$> ranges + where + toReadVector (ByteRangeFrom start) = ReadVector start size + where + size = maybe 0 shareDataSize (getShareData storage) + toReadVector (ByteRangeFromTo start end) = ReadVector start (end - start + 1) + toReadVector (ByteRangeSuffix len) = ReadVector (size - len) len + where + size = maybe 0 shareDataSize (getShareData storage) + + readOneVector :: ReadVector -> [WriteVector] -> ShareData + readOneVector ReadVector{offset, readSize} wv = + B.pack (extractBytes <$> positions) + where + positions = [offset .. (offset + readSize - 1)] + + extractBytes :: Integer -> Word8 + extractBytes p = fromMaybe 0 (go wv) + where + -- New writes are added to the end of the list so give the Last + -- write precedence over others. + go = getLast . foldMap (Last . byteFromShare p) + + byteFromShare :: Integer -> WriteVector -> Maybe Word8 + byteFromShare p (WriteVector off bytes) + | p >= off && p < off + fromIntegral (B.length bytes) = Just (B.index bytes (fromIntegral $ p - off)) + | otherwise = Nothing + +-- | Internal type tracking the result of an attempted mutable write. +data WriteResult + = -- | The test condition succeeded and the write was performed. + TestSuccess + | -- | The test condition failed and the write was not performed. + TestFail + | -- | The supplied secret was incorrect and the write was not performed. + SecretMismatch diff --git a/tahoe-great-black-swamp.cabal b/tahoe-great-black-swamp.cabal index a644e9c3340ef6764ef917f857f7eefa3956f0d1..26b37f6958b472a9df7b89b704a6b0d1ad12edf2 100644 --- a/tahoe-great-black-swamp.cabal +++ b/tahoe-great-black-swamp.cabal @@ -217,6 +217,7 @@ test-suite http-tests , cborg >=0.2.4 && <0.3 , connection >=0.3 && <0.4 , data-default-class >=0.1 && <0.2 + , data-interval , hspec <2.12 , hspec-expectations <0.9 , hspec-wai <0.12 @@ -224,6 +225,7 @@ test-suite http-tests , network >=3.1 && <3.2 , network-simple-tls >=0.4 && <0.5 , QuickCheck <2.15 + , quickcheck-classes >=0.6 && <0.7 , quickcheck-instances <0.4 , serialise >=0.2.3 && <0.3 , servant >=0.16.2 && <0.21 diff --git a/test/Lib.hs b/test/Lib.hs index d33432877382756c98ce145fad3b5e70614ed04f..e4a21217bb1a3364e4dab8848daa84761ca028aa 100644 --- a/test/Lib.hs +++ b/test/Lib.hs @@ -1,4 +1,5 @@ {-# LANGUAGE OverloadedStrings #-} +{-# OPTIONS_GHC -Wno-orphans #-} module Lib ( gen10String, @@ -22,10 +23,12 @@ import Data.ByteString ( import qualified Data.Text as Text +import qualified Data.Map.Strict as Map import Test.QuickCheck ( Arbitrary (arbitrary), Gen, - NonNegative (getNonNegative), + NonNegative (NonNegative, getNonNegative), + Positive (getPositive), shuffle, sublistOf, suchThatMap, @@ -36,8 +39,16 @@ import Test.QuickCheck ( import Test.QuickCheck.Instances.ByteString () import TahoeLAFS.Storage.API ( + Offset, + ReadTestWriteResult (ReadTestWriteResult), + ReadTestWriteVectors (ReadTestWriteVectors), + ReadVector (ReadVector), ShareNumber (..), StorageIndex, + TestOperator (Eq), + TestVector (TestVector), + TestWriteVectors (TestWriteVectors), + WriteVector (..), ) gen10String :: Gen String @@ -65,7 +76,7 @@ b32decode :: String -> ByteString b32decode base32 = Base32.toBytes b32table $ Base32.fromText b32table $ Text.pack base32 -newtype ShareNumbers = ShareNumbers [ShareNumber] deriving (Eq, Ord, Show) +newtype ShareNumbers = ShareNumbers {getShareNumbers :: [ShareNumber]} deriving (Eq, Ord, Show) {- | An Arbitrary instance that guarantees ShareNumbers are unique and non-empty (without invoking discard). @@ -77,3 +88,24 @@ instance Arbitrary ShareNumbers where arbitrary >>= (shuffle . enumFromTo 0) . getNonNegative >>= \(num : rest) -> (num :) <$> sublistOf rest + +instance Arbitrary ShareNumber where + arbitrary = ShareNumber <$> arbNonNeg + +instance Arbitrary ReadTestWriteVectors where + arbitrary = ReadTestWriteVectors <$> arbitrary <*> arbitrary + +instance Arbitrary TestWriteVectors where + arbitrary = TestWriteVectors <$> arbitrary <*> arbitrary <*> arbitrary + +instance Arbitrary TestVector where + arbitrary = TestVector <$> arbNonNeg <*> arbNonNeg <*> pure Eq <*> arbitrary + +instance Arbitrary WriteVector where + arbitrary = WriteVector <$> arbNonNeg <*> arbitrary + +instance Arbitrary ReadVector where + arbitrary = ReadVector <$> arbNonNeg <*> (getPositive <$> arbitrary) + +arbNonNeg :: Gen Offset +arbNonNeg = getNonNegative <$> arbitrary diff --git a/test/SemanticSpec.hs b/test/SemanticSpec.hs index c2fd18421c0b3282b6fb34b2538a573fec7c63bb..4a9092e78b9d16397780c38d0c354c245122428d 100644 --- a/test/SemanticSpec.hs +++ b/test/SemanticSpec.hs @@ -22,6 +22,7 @@ import GHC.Word ( Word8, ) +import qualified Data.Map.Strict as Map import qualified Data.Set as Set import System.Directory ( @@ -38,14 +39,22 @@ import Test.Hspec ( context, describe, it, + shouldBe, shouldThrow, ) - import Test.QuickCheck ( + Gen, + NonEmptyList (getNonEmpty), Positive (..), Property, + chooseInteger, + counterexample, forAll, + ioProperty, + oneof, property, + vector, + (==>), ) import Test.QuickCheck.Monadic ( @@ -53,26 +62,30 @@ import Test.QuickCheck.Monadic ( run, ) -import Data.ByteString ( - ByteString, - length, - map, - ) +import qualified Data.ByteString as B import TahoeLAFS.Storage.API ( AllocateBuckets (AllocateBuckets), AllocationResult (AllocationResult), CBORSet (..), LeaseSecret (..), + Offset, + ReadTestWriteResult (readData, success), + ReadTestWriteVectors, + ReadVector (ReadVector), ShareData, ShareNumber (ShareNumber), Size, StorageIndex, + TestWriteVectors, UploadSecret (UploadSecret), WriteEnablerSecret (WriteEnablerSecret), + WriteVector (WriteVector), allocated, alreadyHave, + readv, toInteger, + writev, ) import TahoeLAFS.Storage.Backend ( @@ -82,6 +95,7 @@ import TahoeLAFS.Storage.Backend ( getImmutableShareNumbers, getMutableShareNumbers, readImmutableShare, + readvAndTestvAndWritev, writeImmutableShare ), WriteImmutableError (..), @@ -90,7 +104,6 @@ import TahoeLAFS.Storage.Backend ( import Data.IORef (IORef) --- We also get the Arbitrary ShareNumber instance from here. import Lib ( ShareNumbers (..), genStorageIndex, @@ -98,16 +111,23 @@ import Lib ( import TahoeLAFS.Storage.Backend.Memory ( MemoryBackend (..), + MutableShareSize (MutableShareSize), memoryBackend, + shareDataSize, + toMutableShareSize, ) +import Data.Data (Proxy (Proxy)) +import Data.Interval (Boundary (Closed, Open), Extended (Finite), Interval, interval, lowerBound, upperBound) +import qualified Data.IntervalSet as IS import TahoeLAFS.Storage.Backend.Filesystem ( FilesystemBackend (FilesystemBackend), ) +import Test.QuickCheck.Classes (Laws (..), semigroupMonoidLaws) -permuteShare :: ByteString -> ShareNumber -> ByteString +permuteShare :: B.ByteString -> ShareNumber -> B.ByteString permuteShare seed number = - Data.ByteString.map xor' seed + B.map xor' seed where xor' :: Word8 -> Word8 xor' = xor $ fromInteger $ toInteger number @@ -149,11 +169,11 @@ immutableWriteAndEnumerateShares :: IO b -> StorageIndex -> ShareNumbers -> - ByteString -> + B.ByteString -> Property immutableWriteAndEnumerateShares makeBackend storageIndex (ShareNumbers shareNumbers) shareSeed = monadicIO $ do let permutedShares = Prelude.map (permuteShare shareSeed) shareNumbers - size = fromIntegral (Data.ByteString.length shareSeed) + size = fromIntegral (B.length shareSeed) allocate = AllocateBuckets shareNumbers size run $ withBackend makeBackend $ \backend -> do @@ -172,10 +192,10 @@ immutableWriteAndRewriteShare :: IO b -> StorageIndex -> ShareNumbers -> - ByteString -> + B.ByteString -> Property immutableWriteAndRewriteShare makeBackend storageIndex (ShareNumbers shareNumbers) shareSeed = monadicIO $ do - let size = fromIntegral (Data.ByteString.length shareSeed) + let size = fromIntegral (B.length shareSeed) allocate = AllocateBuckets shareNumbers size aShareNumber = head shareNumbers aShare = permuteShare shareSeed aShareNumber @@ -196,11 +216,11 @@ immutableWriteAndReadShare :: IO b -> StorageIndex -> ShareNumbers -> - ByteString -> + B.ByteString -> Property immutableWriteAndReadShare makeBackend storageIndex (ShareNumbers shareNumbers) shareSeed = monadicIO $ do let permutedShares = Prelude.map (permuteShare shareSeed) shareNumbers - let size = fromIntegral (Data.ByteString.length shareSeed) + let size = fromIntegral (B.length shareSeed) let allocate = AllocateBuckets shareNumbers size run $ withBackend makeBackend $ \backend -> do @@ -219,7 +239,7 @@ mutableWriteAndEnumerateShares :: IO b -> StorageIndex -> ShareNumbers -> - ByteString -> + B.ByteString -> Property mutableWriteAndEnumerateShares makeBackend storageIndex (ShareNumbers shareNumbers) shareSeed = monadicIO $ do let permutedShares = Prelude.map (permuteShare shareSeed) shareNumbers @@ -231,9 +251,17 @@ mutableWriteAndEnumerateShares makeBackend storageIndex (ShareNumbers shareNumbe when (readShareNumbers /= Set.fromList shareNumbers) $ fail (show readShareNumbers ++ " /= " ++ show shareNumbers) --- The specification for a storage backend. +-- | Create a Spec that checks the given Laws. +lawsCheck :: Laws -> Spec +lawsCheck Laws{lawsTypeclass, lawsProperties} = + describe lawsTypeclass $ + mapM_ oneLawProp lawsProperties + where + oneLawProp (lawName, lawProp) = it lawName lawProp + +-- | The specification for a storage backend. storageSpec :: (Backend b, Mess b) => IO b -> Spec -storageSpec makeBackend = +storageSpec makeBackend = do context "v1" $ do context "immutable" $ do describe "allocate a storage index" $ @@ -292,11 +320,122 @@ storageSpec makeBackend = property $ forAll genStorageIndex (mutableWriteAndEnumerateShares makeBackend) + it "rejects an update with the wrong write enabler" $ + forAll genStorageIndex $ \storageIndex shareNum (secret, wrongSecret) (shareData, junkData) offset -> + (secret /= wrongSecret) + && (shareData /= junkData) + && (B.length shareData > 0) + && (B.length junkData > 0) + ==> monadicIO + . run + . withBackend makeBackend + $ \backend -> do + first <- readvAndTestvAndWritev backend storageIndex (WriteEnablerSecret secret) (writev shareNum offset shareData) + success first `shouldBe` True + readvAndTestvAndWritev backend storageIndex (WriteEnablerSecret wrongSecret) (writev shareNum offset junkData) + `shouldThrow` (== IncorrectWriteEnablerSecret) + third <- readvAndTestvAndWritev backend storageIndex (WriteEnablerSecret secret) (readv offset (fromIntegral $ B.length shareData)) + readData third `shouldBe` Map.singleton shareNum [shareData] + + it "overwrites older data with newer data" $ + property $ \storageIndex (readVectors :: NonEmptyList ReadVector) secret shareNum -> do + let is = readVectorToIntervalSet (getNonEmpty readVectors) + sp = IS.span is + (lower, upper) = toFiniteBounds sp + size = upper - lower + bs <- B.pack <$> vector (fromIntegral size) + writeVectors <- writesThatResultIn bs lower size + pure $ + counterexample ("write vectors: " <> show writeVectors) $ + ioProperty $ + withBackend makeBackend $ \backend -> do + let x = foldMap (\(WriteVector off shareData) -> writev shareNum off shareData) writeVectors + writeResult <- readvAndTestvAndWritev backend storageIndex (WriteEnablerSecret secret) x + success writeResult `shouldBe` True + + let y = foldMap (\(ReadVector off sz) -> readv off sz) (getNonEmpty readVectors) + readResult <- readvAndTestvAndWritev backend storageIndex (WriteEnablerSecret secret) y + Map.map B.concat (readData readResult) + `shouldBe` Map.singleton shareNum (B.concat $ extractRead lower bs <$> getNonEmpty readVectors) + +extractRead :: Integral a => a -> B.ByteString -> ReadVector -> B.ByteString +extractRead lower bs (ReadVector offset size) = B.take (fromIntegral size) . B.drop (fromIntegral offset - fromIntegral lower) $ bs + +toFiniteBounds :: Show r => Interval r -> (r, r) +toFiniteBounds i = (lower, upper) + where + lower = toFinite (lowerBound i) + upper = toFinite (upperBound i) + + toFinite n = case n of + Finite r -> r + e -> error ("Non-finite bound " <> show e) + +readVectorToIntervalSet :: [ReadVector] -> IS.IntervalSet Integer +readVectorToIntervalSet rvs = foldr IS.insert IS.empty (f <$> rvs) + where + f (ReadVector offset size) = interval (Finite offset, Closed) (Finite $ offset + size, Open) + +writesThatResultIn :: B.ByteString -> Offset -> Size -> Gen [WriteVector] +writesThatResultIn "" _ _ = pure [] +writesThatResultIn bs offset size = + oneof + [ -- The whole thing as one write + pure [WriteVector offset bs] + , -- Or divide and conquer arbitrarily + do + prefixLen <- chooseInteger (0, fromIntegral $ B.length bs) + pfx <- writesThatResultIn (B.take (fromIntegral prefixLen) bs) offset prefixLen + sfx <- writesThatResultIn (B.drop (fromIntegral prefixLen) bs) (offset + prefixLen) (size - prefixLen) + pure $ pfx <> sfx + , -- Or write some other random somewhere in this range first, to + -- later be overwritten. + (:) <$> (WriteVector <$> chooseInteger (offset, offset + size) <*> (chooseInteger (1, size) >>= bytes)) <*> writesThatResultIn bs offset size + ] + +bytes :: Integer -> Gen B.ByteString +bytes len = B.pack <$> vector (fromIntegral len) + spec :: Spec spec = do - Test.Hspec.context "memory" $ storageSpec memoryBackend - - Test.Hspec.context "filesystem" $ storageSpec filesystemBackend + context "utilities" $ do + describe "MutableShareStorage" $ do + it "finds the larger size for some cases" $ do + toMutableShareSize (WriteVector 0 "x") <> toMutableShareSize (WriteVector 1 "x") + `shouldBe` MutableShareSize 0 2 + + toMutableShareSize (WriteVector 0 "Hello") <> toMutableShareSize (WriteVector 1 "bye") + `shouldBe` MutableShareSize 0 5 + + toMutableShareSize (WriteVector 0 "x") <> toMutableShareSize (WriteVector 3 "x") + `shouldBe` MutableShareSize 0 4 + + toMutableShareSize (WriteVector 0 "Hello") <> toMutableShareSize (WriteVector 3 "world") + `shouldBe` MutableShareSize 0 8 + + describe "shareDataSize" $ do + it "converts list of WriteVector to a size" $ do + shareDataSize [WriteVector 2 "foo", WriteVector 10 "quux"] + `shouldBe` 14 + shareDataSize [WriteVector 0 "foobar", WriteVector 2 "q"] + `shouldBe` 6 + shareDataSize [] + `shouldBe` 0 + shareDataSize [WriteVector 2 "foo", WriteVector 3 "quux"] + `shouldBe` 7 + + describe "TestWriteVectors" + . lawsCheck + . semigroupMonoidLaws + $ (Proxy :: Proxy TestWriteVectors) + + describe "ReadTestWriteVectors" + . lawsCheck + . semigroupMonoidLaws + $ (Proxy :: Proxy ReadTestWriteVectors) + + context "memory" $ storageSpec memoryBackend + context "filesystem" $ storageSpec filesystemBackend anUploadSecret :: LeaseSecret anUploadSecret = Upload $ UploadSecret "anuploadsecret"