packages feed

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 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