ebml 0.1.0.0 → 0.1.1.0
raw patch · 10 files changed
+291/−203 lines, 10 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
Files
- CHANGELOG.md +6/−1
- app/Main.hs +5/−3
- ebml.cabal +7/−3
- src/Codec/EBML.hs +2/−2
- src/Codec/EBML/Decoder.hs +105/−0
- src/Codec/EBML/Get.hs +0/−105
- src/Codec/EBML/Header.hs +0/−33
- src/Codec/EBML/Matroska.hs +32/−0
- src/Codec/EBML/Stream.hs +66/−52
- test/Spec.hs +68/−4
CHANGELOG.md view
@@ -1,5 +1,10 @@ # Changelog -## 0.0.1.0+## 0.1.1.0++- Improve the 'feedReader' implementation to ensure the initialization frame is fully read.+- 'feedReader' now returns an error when called with an empty buffer.++## 0.1.0.0 - Initial version
app/Main.hs view
@@ -1,12 +1,13 @@ module Main (main) where -import Codec.EBML qualified as EBML import Data.ByteString qualified as BS import Data.Text qualified as Text import Data.Text.IO qualified import System.Environment (getArgs) import System.IO (Handle, IOMode (ReadMode), stdin, withBinaryFile) +import Codec.EBML qualified as EBML+ main :: IO () main = getArgs >>= \case@@ -25,8 +26,9 @@ putStr "Reading 2048 bytes... " buf <- BS.hGet handl 2048 case EBML.feedReader buf ir of- Left e -> error (Text.unpack e)- Right (Nothing, _) | buf == "" -> putStrLn "Done!"+ Left e+ | buf == "" -> putStrLn "Done!"+ | otherwise -> error (Text.unpack e) Right (mFrame, newIR) -> do case mFrame of Nothing -> putStrLn "Need more data"
ebml.cabal view
@@ -5,11 +5,15 @@ -- see: https://github.com/sol/hpack name: ebml-version: 0.1.0.0+version: 0.1.1.0 synopsis: A pure EBML parser description: Pure decoder for the Extensible Binary Meta Language (EBML) format. . Use this library to parse mkv/webm file or split a webm stream segments.+ .+ Checkout a motivating use-case presented in this blog post: [Broadcasting a WebM stream using MSE](https://tristancacqueray.github.io/blog/broadcasting-webm).+ .+ Read the haskell-ebml demo app for simple usage: [Main.hs](https://github.com/TristanCacqueray/haskell-ebml/blob/main/app/Main.hs). category: Codec homepage: https://github.com/TristanCacqueray/haskell-ebml#readme bug-reports: https://github.com/TristanCacqueray/haskell-ebml/issues@@ -31,9 +35,9 @@ exposed-modules: Codec.EBML other-modules:+ Codec.EBML.Decoder Codec.EBML.Element- Codec.EBML.Get- Codec.EBML.Header+ Codec.EBML.Matroska Codec.EBML.Pretty Codec.EBML.Schema Codec.EBML.Stream
src/Codec/EBML.hs view
@@ -65,9 +65,9 @@ import Data.Text (Text) import Data.Text qualified as Text +import Codec.EBML.Decoder import Codec.EBML.Element-import Codec.EBML.Get-import Codec.EBML.Header+import Codec.EBML.Matroska import Codec.EBML.Pretty import Codec.EBML.Schema import Codec.EBML.Stream
+ src/Codec/EBML/Decoder.hs view
@@ -0,0 +1,105 @@+module Codec.EBML.Decoder where++import Data.Binary.Get (Get, bytesRead, getByteString, isEmpty, lookAheadM)+import Data.Text.Encoding (decodeUtf8)+import Data.Word (Word64)++import Codec.EBML.Element+import Codec.EBML.Schema+import Data.Bits (Bits)++getElement :: EBMLSchemas -> Get EBMLElement+getElement schemas = do+ elth <- getElementHeader+ getElementValue schemas elth++getElementValue :: EBMLSchemas -> EBMLElementHeader -> Get EBMLElement+getElementValue schemas elth = do+ -- here is a good place to add traceM debug+ val <- case lookupSchema elth.eid schemas of+ Nothing -> getBinary elth+ Just schema -> schema.decode schemas elth+ pure $ EBMLElement elth val++getDocument :: EBMLSchemas -> Get EBMLDocument+getDocument schemas = EBMLDocument <$> go+ where+ go = do+ empty <- isEmpty+ if empty+ then pure []+ else do+ elt <- getElement schemas+ elts <- go+ pure (elt : elts)++getBinary :: EBMLElementHeader -> Get EBMLValue+getBinary elth = case elth.size of+ Nothing -> fail "Invalid binary header size"+ Just sz -> EBMLBinary <$> getByteString (fromIntegral sz)++getText :: EBMLElementHeader -> Get EBMLValue+getText elth = case elth.size of+ Nothing -> fail "Invalid text header size"+ Just sz -> EBMLText . decodeUtf8 <$> getByteString (fromIntegral sz)++getUnsignedInteger :: EBMLElementHeader -> Get EBMLValue+getUnsignedInteger elth = EBMLUnsignedInteger <$> getInt elth.size++getInteger :: EBMLElementHeader -> Get EBMLValue+getInteger elth = EBMLUnsignedInteger <$> getInt elth.size++getInt :: (Bits a, Integral a) => Maybe Word64 -> Get a+getInt size = getVar sz 0+ where+ -- TODO: check the value is in the [0..8] range+ sz = maybe 0 fromIntegral size++getRoot :: EBMLSchemas -> EBMLElementHeader -> Get EBMLValue+getRoot schemas elth = case elth.size of+ Nothing -> EBMLRoot <$> getUntil schemas elth.eid+ Just sz -> getRootFixed schemas sz++getUntil :: EBMLSchemas -> EBMLID -> Get [EBMLElement]+getUntil schemas eid = go+ where+ getChild :: Get (Maybe EBMLElement)+ getChild = do+ -- This is not exactly correct. The rfc-8794 spec (chapter 6.2) says we should decode until+ -- any valid parent or global element. Because the EBMLSchema doesn't yet contain this information,+ -- and because in practice such unknown-sized element are segment/cluster, we simply decode until+ -- we find a matching element.+ elth <- getElementHeader+ if elth.eid == eid+ then pure Nothing+ else Just <$> getElementValue schemas elth++ go = do+ empty <- isEmpty+ if empty+ then pure []+ else goGet++ goGet =+ lookAheadM getChild >>= \case+ Just elt -> do+ elts <- go+ pure (elt : elts)+ Nothing -> pure []++getRootFixed :: EBMLSchemas -> Word64 -> Get EBMLValue+getRootFixed schemas sz = do+ startPosition <- bytesRead+ let maxPosition = startPosition + fromIntegral sz+ getChilds = do+ currentPosition <- bytesRead+ if+ | currentPosition > maxPosition ->+ fail $ "Element decode position " <> show currentPosition <> " exceed parent size " <> show sz+ | currentPosition == maxPosition ->+ pure []+ | otherwise -> do+ elt <- getElement schemas+ elts <- getChilds+ pure (elt : elts)+ EBMLRoot <$> getChilds
− src/Codec/EBML/Get.hs
@@ -1,105 +0,0 @@-module Codec.EBML.Get where--import Data.Binary.Get (Get, bytesRead, getByteString, isEmpty, lookAheadM)-import Data.Text.Encoding (decodeUtf8)-import Data.Word (Word64)--import Codec.EBML.Element-import Codec.EBML.Schema-import Data.Bits (Bits)--getElement :: EBMLSchemas -> Get EBMLElement-getElement schemas = do- elth <- getElementHeader- getElementValue schemas elth--getElementValue :: EBMLSchemas -> EBMLElementHeader -> Get EBMLElement-getElementValue schemas elth = do- -- here is a good place to add traceM debug- val <- case lookupSchema elth.eid schemas of- Nothing -> getBinary elth- Just schema -> schema.decode schemas elth- pure $ EBMLElement elth val--getDocument :: EBMLSchemas -> Get EBMLDocument-getDocument schemas = EBMLDocument <$> go- where- go = do- empty <- isEmpty- if empty- then pure []- else do- elt <- getElement schemas- elts <- go- pure (elt : elts)--getBinary :: EBMLElementHeader -> Get EBMLValue-getBinary elth = case elth.size of- Nothing -> fail "Invalid binary header size"- Just sz -> EBMLBinary <$> getByteString (fromIntegral sz)--getText :: EBMLElementHeader -> Get EBMLValue-getText elth = case elth.size of- Nothing -> fail "Invalid text header size"- Just sz -> EBMLText . decodeUtf8 <$> getByteString (fromIntegral sz)--getUnsignedInteger :: EBMLElementHeader -> Get EBMLValue-getUnsignedInteger elth = EBMLUnsignedInteger <$> getInt elth.size--getInteger :: EBMLElementHeader -> Get EBMLValue-getInteger elth = EBMLUnsignedInteger <$> getInt elth.size--getInt :: (Bits a, Integral a) => Maybe Word64 -> Get a-getInt size = getVar sz 0- where- -- TODO: check the value is in the [0..8] range- sz = maybe 0 fromIntegral size--getRoot :: EBMLSchemas -> EBMLElementHeader -> Get EBMLValue-getRoot schemas elth = case elth.size of- Nothing -> EBMLRoot <$> getUntil schemas elth.eid- Just sz -> getRootFixed schemas sz--getUntil :: EBMLSchemas -> EBMLID -> Get [EBMLElement]-getUntil schemas eid = go- where- getChild :: Get (Maybe EBMLElement)- getChild = do- -- This is not exactly correct. The rfc-8794 spec (chapter 6.2) says we should decode until- -- any valid parent or global element. Because the EBMLSchema doesn't yet contain this information,- -- and because in practice such unknown-sized element are segment/cluster, we simply decode until- -- we find a matching element.- elth <- getElementHeader- if elth.eid == eid- then pure Nothing- else Just <$> getElementValue schemas elth-- go = do- empty <- isEmpty- if empty- then pure []- else goGet-- goGet =- lookAheadM getChild >>= \case- Just elt -> do- elts <- go- pure (elt : elts)- Nothing -> pure []--getRootFixed :: EBMLSchemas -> Word64 -> Get EBMLValue-getRootFixed schemas sz = do- startPosition <- bytesRead- let maxPosition = startPosition + fromIntegral sz- getChilds = do- currentPosition <- bytesRead- if- | currentPosition > maxPosition ->- fail $ "Element decode position " <> show currentPosition <> " exceed parent size " <> show sz- | currentPosition == maxPosition ->- pure []- | otherwise -> do- elt <- getElement schemas- elts <- getChilds- pure (elt : elts)- EBMLRoot <$> getChilds
− src/Codec/EBML/Header.hs
@@ -1,33 +0,0 @@--- | Header schema definition, see: https://github.com/ietf-wg-cellar/ebml-specification/blob/master/specification.markdown#ebml-header-elements-module Codec.EBML.Header where--import Data.Text (Text)--import Codec.EBML.Element-import Codec.EBML.Get-import Codec.EBML.Schema--schemaHeader :: [EBMLSchema]-schemaHeader =- [ EBMLSchema "EBML" 0x1A45DFA3 getRoot- , EBMLSchema "DocType" 0x4282 (const getText)- , EBMLSchema "Segment" 0x18538067 getRoot- , EBMLSchema "Info" 0x1549A966 getRoot- , EBMLSchema "Cluster" 0x1F43B675 getRoot- ]- <> map fromUints uints--fromUints :: (Text, EBMLID) -> EBMLSchema-fromUints (n, i) = EBMLSchema n i (const getUnsignedInteger)--uints :: [(Text, EBMLID)]-uints =- [ ("EBMLVersion", 0x4286)- , ("EBMLReadVersion", 0x42F7)- , ("EBMLMaxIDLength", 0x42F2)- , ("EBMLMaxSizeLength", 0x42F3)- , ("DocTypeVersion", 0x4287)- , ("DocTypeReadVersion", 0x4285)- , ("TimestampScale", 0x2AD7B1)- , ("Timestamp", 0xE7)- ]
+ src/Codec/EBML/Matroska.hs view
@@ -0,0 +1,32 @@+module Codec.EBML.Matroska where++import Data.Text (Text)++import Codec.EBML.Decoder+import Codec.EBML.Element+import Codec.EBML.Schema++schemaHeader :: [EBMLSchema]+schemaHeader =+ [ EBMLSchema "EBML" 0x1A45DFA3 getRoot+ , EBMLSchema "DocType" 0x4282 (const getText)+ , EBMLSchema "Segment" 0x18538067 getRoot+ , EBMLSchema "Info" 0x1549A966 getRoot+ , EBMLSchema "Cluster" 0x1F43B675 getRoot+ ]+ <> map fromUints uints++fromUints :: (Text, EBMLID) -> EBMLSchema+fromUints (n, i) = EBMLSchema n i (const getUnsignedInteger)++uints :: [(Text, EBMLID)]+uints =+ [ ("EBMLVersion", 0x4286)+ , ("EBMLReadVersion", 0x42F7)+ , ("EBMLMaxIDLength", 0x42F2)+ , ("EBMLMaxSizeLength", 0x42F3)+ , ("DocTypeVersion", 0x4287)+ , ("DocTypeReadVersion", 0x4285)+ , ("TimestampScale", 0x2AD7B1)+ , ("Timestamp", 0xE7)+ ]
src/Codec/EBML/Stream.hs view
@@ -1,14 +1,40 @@+{- | This module contains the incremental decoder logic to process a continuous stream.++Here is an example stream layout:++> | EBML | SIZE | ELT | ELTA | ELT | ... | SEGMENT | USIZE | ELT | ELTB | ... |+> | CLUSTER | USIZE | ELT | ELTC | ... | CLUSTER | USIZE | ELT | ELT | ... |++There are two difficulties:++- The element are not aligned, a segment id can start at position 15.+- Unknown sized element, such as segment and cluster, need to use a look-ahead to ensure it is completed.++Here are the main scenarios:++- The initial buffers does not contains the begining of a media segment.+ In that case we need to accumulate the data to provide the complete initialization segments.++- The buffer contains multiple segments. In that case we need to find the last one, e.g. the most recent.++- The buffer ends on the middle of the cluster id, e.g. "...\x1f\x43".+ In that case we need to wait for the next buffer to confirm a new media segment exists.+ We also need to returns the end of the previous buffer, so that the media segment does start with "\x1f\x43...".+ This is somehow already managed by the 'Data.Binary.Get.runGetIncremental'.++Checkout the 'testIncrementalLookahead' case in the test/Spec.hs module that validates these scenarios.+-} module Codec.EBML.Stream (StreamReader, newStreamReader, StreamFrame (..), feedReader) where -import Control.Monad (void, when)+import Control.Monad (when) import Data.Binary.Get qualified as Get import Data.ByteString qualified as BS import Data.Text (Text) import Data.Text qualified as Text +import Codec.EBML.Decoder import Codec.EBML.Element-import Codec.EBML.Get-import Codec.EBML.Header+import Codec.EBML.Matroska import Codec.EBML.Schema import Codec.EBML.WebM qualified as WebM @@ -22,12 +48,8 @@ -- | Create a stream reader with 'newStreamReader', and decode media segments with 'feedReader'. data StreamReader = StreamReader- { acc :: [BS.ByteString]- -- ^ Accumulate data in case the header is not completed in the first buffer.- , consumed :: Int- -- ^ Keep track of the decoder position accross multiple buffers.- , header :: Maybe BS.ByteString- -- ^ The stream initialization segments.+ { header :: Either (Int, [BS.ByteString]) BS.ByteString+ -- ^ The stream initialization segments, either an accumulator (read bytes, list of buffer), either the full segments. , decoder :: Get.Decoder () -- ^ The current decoder. }@@ -35,6 +57,23 @@ streamSchema :: EBMLSchemas streamSchema = compileSchemas schemaHeader +-- | Read elements until the first cluster eid.+getUntilNextCluster :: Get.Get [EBMLElement]+getUntilNextCluster =+ Get.lookAheadM getNonCluster >>= \case+ Just elt -> do+ elts <- getUntilNextCluster+ pure (elt : elts)+ Nothing -> pure []+ where+ getNonCluster = do+ eid <- getElementID+ if eid == 0x1F43B675+ then pure Nothing+ else do+ elth <- EBMLElementHeader eid <$> getMaybeDataSize+ Just <$> getElementValue streamSchema elth+ -- | Read the initialization frame. getInitialization :: Get.Get () getInitialization = do@@ -47,7 +86,7 @@ segmentHead <- getElementHeader when (segmentHead.eid /= 0x18538067) do fail $ "Invalid segment: " <> show segmentHead- elts <- getUntil streamSchema 0x1F43B675+ elts <- getUntilNextCluster case WebM.decodeSegment elts of Right _webmDocument -> pure () Left err -> fail (Text.unpack err)@@ -58,66 +97,41 @@ clusterHead <- getElementHeader when (clusterHead.eid /= 0x1F43B675) do fail $ "Invalid cluster: " <> show clusterHead- getClusterBody--getClusterBody :: Get.Get ()-getClusterBody = do- elts <- getUntil streamSchema 0x1F43B675+ elts <- getUntilNextCluster case elts of (elt : _) | elt.header.eid == 0xE7 -> pure () _ -> fail "Cluster first element is not a timestamp" -getClusterRemaining :: Get.Get ()-getClusterRemaining = do- elth <- getElementHeader- if elth.eid == 0x1F43B675- then -- This is in fact a new cluster, get its body- getClusterBody- else -- This is a cluster left-over, let's keep on reading until a new start- void (getUntil streamSchema 0x1F43B675)- -- | Initialize a stream reader. newStreamReader :: StreamReader-newStreamReader = StreamReader [] 0 Nothing (Get.runGetIncremental getInitialization)+newStreamReader = StreamReader (Left (0, [])) (Get.runGetIncremental getInitialization) -- | Feed data into a stream reader. Returns either an error, or maybe a new 'StreamFrame' and an updated StreamReader. feedReader :: BS.ByteString -> StreamReader -> Either Text (Maybe StreamFrame, StreamReader) feedReader = go Nothing where -- This is the end- go Nothing "" sr = case Get.pushEndOfInput sr.decoder of- Get.Fail _ _ s -> Left (Text.pack s)- Get.Partial _ -> Left "Missing data"- Get.Done "" _ _ -> Right (Nothing, sr)- Get.Done{} -> Left "Left-over data"+ go Nothing "" _ = Left "empty buffer" -- Feed the decoder go mFrame bs sr = case Get.pushChunk sr.decoder bs of Get.Fail _ _ s -> Left (Text.pack s)- newDecoder@(Get.Partial _) ->- let newAcc = case sr.header of- Nothing -> bs : sr.acc- -- We don't need to accumulate data once the header is known.- Just _ -> []- newSR = StreamReader newAcc (sr.consumed + BS.length bs) sr.header newDecoder- in Right (mFrame, newSR)- Get.Done leftover consumed _- | BS.null leftover ->- -- We might have ended on a in-cluster element, use the remainingDecoder next time- Right (mFrame, newIR remainingDecoder)- | otherwise ->- -- There might be a new frame after, keep on decoding- go newFrame leftover (newIR segmentDecoder)+ -- More data is needed.+ newDecoder@(Get.Partial _) -> Right (mFrame, newSR) where+ -- Accumulate the buffer for the initialization segments if needed.+ newHeader = case sr.header of+ Left (consumed, acc) -> Left (consumed + BS.length bs, bs : acc)+ Right _ -> sr.header+ newSR = StreamReader newHeader newDecoder+ Get.Done leftover consumed _ -> go newFrame leftover newSR+ where -- The header is either the one already parsed, or the current complete decoded buffer. newHeader = case sr.header of- Just header -> header- Nothing ->- let currentPos = fromIntegral consumed - sr.consumed- in mconcat $ reverse (BS.take currentPos bs : sr.acc)+ Left (prevConsumed, acc) ->+ let currentPos = fromIntegral consumed - prevConsumed+ in mconcat $ reverse (BS.take currentPos bs : acc)+ Right header -> header -- The new frame starts after what was decoded. newFrame = Just (StreamFrame newHeader leftover)- newIR = StreamReader [] 0 (Just newHeader)-- remainingDecoder = Get.runGetIncremental getClusterRemaining- segmentDecoder = Get.runGetIncremental getCluster+ newSR = StreamReader (Right newHeader) (Get.runGetIncremental getCluster)
test/Spec.hs view
@@ -39,6 +39,7 @@ , "0010 0000 0000 0000 0000 0010" , "0001 0000 0000 0000 0000 0000 0000 0010" ]+ , testCase "Incremental lookahead" (pure ()) ] testVarInts :: String -> BS.ByteString@@ -60,6 +61,7 @@ "Integration tests" [ testGroup "sample" (decodeFile sampleFile 53054906 4458 49 0 2794) , testGroup "stream" (decodeFile streamFile 3499 260 6 6 1006)+ , testCase "Incremental lookahead" testIncrementalLookahead ] where decodeFile bs size headerSize clusterCount clusterTs1 clusterTs2@@ -81,10 +83,10 @@ let go buf sr acc = let (cur, next) = BS.splitAt 256 buf in case EBML.feedReader cur sr of- Left e -> error (Text.unpack e)- Right (mFrame, nextSR)- | cur == "" -> reverse newAcc- | otherwise -> go next nextSR newAcc+ Left e+ | cur == "" -> reverse acc+ | otherwise -> error (Text.unpack e)+ Right (mFrame, nextSR) -> go next nextSR newAcc where newAcc = maybe acc (: acc) mFrame frames = go bs EBML.newStreamReader []@@ -95,3 +97,65 @@ forM_ frames $ \frame -> do BS.take 4 frame.media @?= "\x1f\x43\xb6\x75" ]++ testIncrementalLookahead = do+ let eltA = 0x10+ segment = 0x2b+ eltB = 0x43+ cluster1 = 0x104+ eltC = 0x113+ cluster2 = 0x36b+ -- validate the addresses+ BS.take 4 (BS.drop segment streamFile) @?= "\x18\x53\x80\x67"+ BS.take 4 (BS.drop cluster1 streamFile) @?= "\x1f\x43\xb6\x75"+ BS.take 4 (BS.drop cluster2 streamFile) @?= "\x1f\x43\xb6\x75"+ let getIDat pos =+ let elth = runGet EBML.getElementHeader (BS.fromStrict $ BS.drop pos streamFile)+ in elth.eid+ getIDat eltA @?= EBML.EBMLID 0x42f7+ getIDat eltB @?= EBML.EBMLID 0x1549a966+ getIDat eltC @?= EBML.EBMLID 0xa3++ -- validate the first two media cluster+ let c1 = "\x1f\x43\xb6\x75\x01\xff\xff\xff\xff\xff\xff\xff\xe7\x81\x06\xa3"+ c2 = "\x1f\x43\xb6\x75\x01\xff\xff\xff\xff\xff\xff\xff\xe7\x82\x03\xee\xa3"+ BS.drop cluster1 streamFile `checkPrefix` c1+ BS.drop cluster2 streamFile `checkPrefix` c2++ -- Test stream reader with pathological buffer size+ let runIncrementalTest size = testIncremental size EBML.newStreamReader streamFile [c1, c2]+ -- Make sure the first two cluster are covered.+ forM_ [1, 2, 7] runIncrementalTest+ forM_ [eltA, segment, eltB, cluster1, eltC] $ \pos -> do+ runIncrementalTest pos+ forM_ [15, 8, 7, 4, 2, 1] $ \offset -> do+ runIncrementalTest (pos - offset)+ runIncrementalTest (pos + offset)++ -- Until c2 can be parsed, the latest media segment must be c1+ testIncremental cluster2 EBML.newStreamReader streamFile [c1]+ forM_ [1, 2, 3] $ \offset -> do+ testIncremental (cluster2 - offset) EBML.newStreamReader streamFile [c1]+ testIncremental (cluster2 + offset) EBML.newStreamReader streamFile [c1]++ -- The buffer contains both clusters, the latest media segment must be c2+ forM_ [4, 5, 8, 16, 24] $ \offset -> do+ testIncremental (cluster2 + offset) EBML.newStreamReader streamFile [c2]++checkPrefix :: HasCallStack => BS.ByteString -> BS.ByteString -> Assertion+checkPrefix b1 b2 = BS.take (BS.length b2) b1 @?= BS.take (BS.length b1) b2++testIncremental :: HasCallStack => Int -> EBML.StreamReader -> BS.ByteString -> [BS.ByteString] -> IO ()+testIncremental _ _ _ [] = pure ()+testIncremental _ _ "" _ = error "Reached the end of file"+testIncremental size sr buf clusters@(x : xs) = do+ let (chunk, nextBuf) = BS.splitAt size buf+ case EBML.feedReader chunk sr of+ Left e -> error (Text.unpack e)+ Right (mFrame, nextSR) -> do+ nextClusters <- case mFrame of+ Nothing -> pure clusters+ Just f -> do+ (f.media <> nextBuf) `checkPrefix` x+ pure xs+ testIncremental size nextSR nextBuf nextClusters