packages feed

Z-IO-1.0.0.0: Z/IO/BIO/Base.hs

{-|
Module      : Z.IO.BIO.Base
Description : Composable IO Loops
Copyright   : (c) Dong Han, 2017-2020
License     : BSD
Maintainer  : winterland1989@gmail.com
Stability   : experimental
Portability : non-portable

This module provides 'BIO' (block IO) type to facilitate writing streaming programs. A 'BIO' node usually:

  * Process input in unit of block(or item).
  * Running in constant spaces, which means the memory usage won't accumulate.
  * Keep some state in IO, which is sealed in 'BIO' closure.

-}
module Z.IO.BIO.Base (
  -- * The BIO type
    BIO, pattern EOF, Source, Sink
  -- ** Basic combinators
  , appendSource, concatSource, concatSource'
  , joinSink, fuseSink
  -- * Run BIO chain
  , discard
  , step, step_
  , run, run_
  , runBlock, runBlock_, unsafeRunBlock
  , runBlocks, runBlocks_, unsafeRunBlocks
  -- * Make new BIO
  , fromPure, fromIO
  , filter, filterIO
  -- * Use with fold
  , fold', foldIO'
  -- ** Source
  , initSourceFromFile
  , initSourceFromFile'
  , sourceFromIO
  , sourceFromList
  , sourceFromBuffered
  , sourceTextFromBuffered
  , sourceJSONFromBuffered
  , sourceParserFromBuffered
  , sourceParseChunkFromBuffered
  -- ** Sink
  , sinkToIO
  , sinkToList
  , initSinkToFile
  , sinkToBuffered
  , sinkBuilderToBuffered
  -- ** Bytes specific
  , newReChunk
  , newUTF8Decoder
  , newParser, newMagicSplitter, newLineSplitter
  , newBase64Encoder, newBase64Decoder
  , hexEncode
  , newHexDecoder
  -- ** Generic BIO
  , counter
  , seqNum
  , newGrouping
  , ungrouping
  , consumed
  ) where

import           Prelude                hiding (filter)
import           Control.Concurrent.MVar
import           Control.Concurrent.STM
import qualified Control.Foldl          as L
import           Control.Monad
import           Control.Monad.IO.Class
import           Data.Bits              ((.|.))
import           Data.IORef
import qualified Data.List              as List
import           Data.Void
import           Data.Word
import           System.IO.Unsafe       (unsafePerformIO)
import qualified Z.Data.Array           as A
import qualified Z.Data.Builder         as B
import           Z.Data.CBytes          (CBytes)
import qualified Z.Data.JSON            as JSON
import qualified Z.Data.Parser          as P
import           Z.Data.PrimRef
import qualified Z.Data.Text            as T
import qualified Z.Data.Text.UTF8Codec  as T
import qualified Z.Data.Vector          as V
import qualified Z.Data.Vector.Base     as V
import           Z.Data.Vector.Base64
import qualified Z.Data.Vector.Hex      as Hex
import           Z.IO.Buffered
import           Z.IO.Exception
import qualified Z.IO.FileSystem.Base   as FS
import           Z.IO.Resource

-- | A 'BIO'(blocked IO) node.
--
-- A 'BIO' node is a push based stream transformer. It can be used to describe different kinds of IO
-- devices:
--
--  * @BIO inp out@ describe an IO state machine(e.g. z_stream in zlib),
--    which takes some input in block, then outputs.
--  * @type Source out = BIO Void out@ described an IO source, which never takes input,
--    but gives output until EOF by looping.
--  * @type Sink inp = BIO inp Void@ described an IO sink, which takes input and perform some IO effects,
--    such as writing to terminal or files.
--
-- You can connect these 'BIO' nodes with '>|>', which connect left node's output to right node's input,
-- and return a new 'BIO' node with left node's input type and right node's output type.
--
-- You can run a 'BIO' node in different ways:
--
--   * 'step'\/'step_' to supply a single chunk of input and step the BIO node.
--   * 'run'\/'run_' will supply EOF directly, which will effectively pull all values from source,
--     and push to sink until source reaches EOF.
--   * 'runBlock'\/'runBlock_' will supply a single block of input as whole input and run the BIO node.
--   * 'runBlocks'\/'runBlocks_' will supply a list of blocks as whole input and run the BIO node.
--
-- Note 'BIO' usually contains some IO states, you can consider it as an opaque 'IORef':
--
--   * You shouldn't use a 'BIO' node across multiple 'BIO' chain unless the state can be reset.
--   * You shouldn't use a 'BIO' node across multiple threads unless document states otherwise.
--
-- 'BIO' is simply a convenient way to construct single-thread streaming computation, to use 'BIO'
-- in multiple threads, check "Z.IO.BIO.Concurrent" module.
--
type BIO inp out = (Maybe out -> IO ())     -- ^ Pass 'EOF' to indicate current node reaches EOF
                -> Maybe inp                -- ^ 'EOF' indicates upstream reaches EOF
                -> IO ()

-- | Patterns for more meaningful pattern matching.
pattern EOF :: Maybe a
pattern EOF = Nothing

-- | Type alias for 'BIO' node which never takes input.
--
-- Note when implement a 'Source', you should assume 'EOF' argument is supplied only once, and you
-- should loop to call downstream continuation with all available chunks, then write a final 'EOF'
-- to indicate EOF.
type Source x = BIO Void x

-- | Type alias for 'BIO' node which only takes input and perform effects.
--
-- Note when implement a 'Sink', you should assume 'EOF' argument is supplied only once(when upstream
-- reaches EOF), you do not need to call downstream continuation before EOF, and
-- do a flush(also write a final 'EOF') when upstream reach EOF.
type Sink x = BIO x ()

-- | Connect two 'BIO' source, after first reach EOF, draw elements from second.
appendSource :: HasCallStack => Source a -> Source a -> Source a
{-# INLINABLE appendSource #-}
b1 `appendSource` b2 = \ k _ ->
    b1 (\ y ->
        case y of Just _ -> k y
                  _      -> b2 k EOF) EOF

-- | Fuse two 'BIO' sinks, i.e. everything written to the fused sink will be written to left and right sink.
--
-- Flush result 'BIO' will effectively flush both sink.
joinSink :: HasCallStack => Sink out -> Sink out -> Sink out
{-# INLINABLE joinSink #-}
b1 `joinSink` b2 = \ k mx ->
    case mx of
        Just _ -> do
            b1 discard mx
            b2 discard mx
        _ -> do
            b1 discard EOF
            b2 discard EOF
            k EOF

-- | Fuse a list of 'BIO' sinks, everything written to the fused sink will be written to every sink in the list.
--
-- Flush result 'BIO' will effectively flush every sink in the list.
fuseSink :: HasCallStack => [Sink out] -> Sink out
{-# INLINABLE fuseSink #-}
fuseSink ss = \ k mx ->
    case mx of
        Just _ -> mapM_ (\ s -> s discard mx) ss
        _ -> do
            mapM_ (\ s -> s discard mx) ss
            k EOF

-- | Connect list of 'BIO' sources, after one reach EOF, draw element from next.
concatSource :: HasCallStack => [Source a] -> Source a
{-# INLINABLE concatSource #-}
concatSource = List.foldl' appendSource emptySource

-- | A 'Source' directly write EOF to downstream.
emptySource :: Source a
{-# INLINABLE emptySource #-}
emptySource = \ k _ -> k EOF

-- | Connect list of 'BIO' sources, after one reach EOF, draw element from next.
concatSource' :: HasCallStack => Source (Source a) -> Source a
{-# INLINABLE concatSource' #-}
concatSource' ssrc = \ k _ -> ssrc (\ msrc ->
    case msrc of
        Just src -> src (\ mx ->
            case mx of Just _ -> k mx
                       _ -> return ()) EOF
        _ -> k EOF) EOF

-------------------------------------------------------------------------------
-- Run BIO

-- | Discards a value.
discard :: a -> IO ()
{-# INLINABLE discard #-}
discard _ = return ()

-- | Supply a single chunk of input to a 'BIO' and collect result.
step :: HasCallStack => BIO inp out -> inp -> IO [out]
{-# INLINABLE step #-}
step bio inp = do
    accRef <- newIORef []
    bio (mapM_  $ \ x -> modifyIORef' accRef (x:)) (Just inp)
    reverse <$> readIORef accRef

-- | Supply a single chunk of input to a 'BIO' without collecting result.
step_ :: HasCallStack => BIO inp out -> inp -> IO ()
{-# INLINABLE step_ #-}
step_ bio = bio discard . Just

-- | Run a 'BIO' loop without providing input.
--
-- When used on 'Source', it starts the streaming loop.
-- When used on 'Sink', it performs a flush.
run_ :: HasCallStack => BIO inp out -> IO ()
{-# INLINABLE run_ #-}
run_ bio = bio discard EOF

-- | Run a 'BIO' loop without providing input, and collect result.
--
-- When used on 'Source', it will collect all input chunks.
run :: HasCallStack => BIO inp out -> IO [out]
{-# INLINABLE run #-}
run bio = do
    accRef <- newIORef []
    bio (mapM_  $ \ x -> modifyIORef' accRef (x:)) EOF
    reverse <$> readIORef accRef

-- | Run a strict fold over a source with 'L.Fold'.
fold' :: L.Fold a b -> Source a -> IO b
{-# INLINABLE fold' #-}
fold' (L.Fold s i e) bio = do
    iref <- newIORef i
    bio (mapM_  (\ x -> modifyIORef' iref (\ i' -> s i' x))) Nothing
    e <$> readIORef iref

-- | Run a strict fold over a source with 'L.FoldM'.
foldIO' :: L.FoldM IO a b -> Source a -> IO b
{-# INLINABLE foldIO' #-}
foldIO' (L.FoldM s i e) bio = do
    iref <- newIORef =<< i
    bio (mapM_  (\ x -> do
        i' <- readIORef iref
        !x' <- s i' x
        writeIORef iref x')) Nothing
    e =<< readIORef iref

-- | Run a 'BIO' loop with a single chunk of input and EOF, and collect result.
--
runBlock :: HasCallStack => BIO inp out -> inp -> IO [out]
{-# INLINABLE runBlock #-}
runBlock bio inp = do
    accRef <- newIORef []
    bio (mapM_  $ \ x -> modifyIORef' accRef (x:)) (Just inp)
    bio (mapM_  $ \ x -> modifyIORef' accRef (x:)) EOF
    reverse <$> readIORef accRef

-- | Run a 'BIO' loop with a single chunk of input and EOF, without collecting result.
--
runBlock_ :: HasCallStack => BIO inp out -> inp -> IO ()
{-# INLINABLE runBlock_ #-}
runBlock_ bio inp = do
    bio discard (Just inp)
    bio discard EOF

-- | Wrap 'runBlock' into a pure interface.
--
-- You can wrap a stateful BIO computation(including the creation of 'BIO' node),
-- when you can guarantee a computation is pure, e.g. compressing, decoding, etc.
unsafeRunBlock :: HasCallStack => IO (BIO inp out) -> inp -> [out]
{-# INLINABLE unsafeRunBlock #-}
unsafeRunBlock new inp = unsafePerformIO (new >>= \ bio -> runBlock bio inp)

-- | Supply blocks of input and EOF to a 'BIO', and collect results.
--
-- Note many 'BIO' node will be closed or not be able to take new input after drained.
runBlocks :: HasCallStack => BIO inp out -> [inp] -> IO [out]
{-# INLINABLE runBlocks #-}
runBlocks bio inps = do
    accRef <- newIORef []
    forM_ inps $ bio (mapM_  $ \ x -> modifyIORef' accRef (x:)) . Just
    bio (mapM_  $ \ x -> modifyIORef' accRef (x:)) EOF
    reverse <$> readIORef accRef

-- | Supply blocks of input and EOF to a 'BIO', without collecting results.
--
-- Note many 'BIO' node will be closed or not be able to take new input after drained.
runBlocks_ :: HasCallStack => BIO inp out -> [inp] -> IO ()
{-# INLINABLE runBlocks_ #-}
runBlocks_ bio inps = do
    forM_ inps $ bio discard . Just
    bio discard EOF

-- | Wrap 'runBlocks' into a pure interface.
--
-- Similar to 'unsafeRunBlock', but with a list of input blocks.
unsafeRunBlocks :: HasCallStack => IO (BIO inp out) -> [inp] -> [out]
{-# INLINABLE unsafeRunBlocks #-}
unsafeRunBlocks new inps = unsafePerformIO (new >>= \ bio -> runBlocks bio inps)

-------------------------------------------------------------------------------
-- Source

-- | Source a list(or any 'Foldable') from memory.
--
sourceFromList :: Foldable f => f a -> Source a
{-# INLINABLE sourceFromList #-}
sourceFromList xs0 = \ k _ -> do
    mapM_ (k . Just) xs0
    k EOF

-- | Turn a 'BufferedInput' into 'BIO' source, map EOF to EOF.
--
sourceFromBuffered :: HasCallStack => BufferedInput -> Source V.Bytes
{-# INLINABLE sourceFromBuffered #-}
sourceFromBuffered i = \ k _ -> loop k
  where
    loop k = do
        x <- readBuffer i
        if V.null x then k EOF else k (Just x) >> loop k

-- | Turn a `IO` action into 'Source'
sourceFromIO :: HasCallStack => IO (Maybe a) -> Source a
{-# INLINABLE sourceFromIO #-}
sourceFromIO io = \ k _ -> loop k
  where
    loop k = do
        x <- io
        case x of
            Just _ -> k x >> loop k
            _      -> k EOF

-- | Turn a UTF8 encoded 'BufferedInput' into 'BIO' source, map EOF to EOF.
--
sourceTextFromBuffered :: HasCallStack => BufferedInput -> Source T.Text
{-# INLINABLE sourceTextFromBuffered #-}
sourceTextFromBuffered i = \ k _ -> loop k
  where
    loop k = do
        x <- readBufferText i
        if T.null x then k EOF else k (Just x) >> loop k

-- | Turn a 'JSON' encoded 'BufferedInput' into 'BIO' source, ignoring any
-- whitespaces bewteen JSON objects. If EOF reached, then return 'EOF'.
-- Throw 'OtherError' with name "EJSON" if JSON value is not parsed or converted.
--
sourceJSONFromBuffered :: forall a. (JSON.JSON a, HasCallStack) => BufferedInput -> Source a
{-# INLINABLE sourceJSONFromBuffered #-}
sourceJSONFromBuffered = sourceParseChunkFromBuffered JSON.decodeChunk

-- | Turn buffered input device into a packet source, throw 'OtherError' with name @EPARSE@ if parsing fail.
sourceParserFromBuffered :: HasCallStack => P.Parser a -> BufferedInput -> Source a
{-# INLINABLE sourceParserFromBuffered #-}
sourceParserFromBuffered p = sourceParseChunkFromBuffered (P.parseChunk p)

-- | Turn buffered input device into a packet source, throw 'OtherError' with name @EPARSE@ if parsing fail.
sourceParseChunkFromBuffered :: (HasCallStack, T.Print e)
                              => (V.Bytes -> P.Result e a) -> BufferedInput -> Source a
{-# INLINABLE sourceParseChunkFromBuffered #-}
sourceParseChunkFromBuffered pc bi = \ k _ ->
    let loopA = do
            bs <- readBuffer bi
            if V.null bs
            then k EOF
            else loopB bs
        loopB bs = do
            (rest, r) <- P.parseChunks pc (readBuffer bi) bs
            case r of Right v -> k (Just v)
                      Left e  -> throwOtherError "EPARSE" (T.toText e)
            if V.null rest
            then loopA
            else loopB rest
    in loopA

-- | Turn a file into a 'V.Bytes' source.
initSourceFromFile :: HasCallStack => CBytes -> Resource (Source V.Bytes)
{-# INLINABLE initSourceFromFile #-}
initSourceFromFile p = do
    f <- FS.initFile p FS.O_RDONLY FS.DEFAULT_FILE_MODE
    liftIO (sourceFromBuffered <$> newBufferedInput f)

-- | Turn a file into a 'V.Bytes' source with given chunk size.
initSourceFromFile' :: HasCallStack => CBytes -> Int -> Resource (Source V.Bytes)
{-# INLINABLE initSourceFromFile' #-}
initSourceFromFile' p bufSiz = do
    f <- FS.initFile p FS.O_RDONLY FS.DEFAULT_FILE_MODE
    liftIO (sourceFromBuffered <$> newBufferedInput' bufSiz f)

--------------------------------------------------------------------------------
-- Sink

-- | Turn a 'BufferedOutput' into a 'V.Bytes' sink.
sinkToBuffered :: HasCallStack => BufferedOutput -> Sink V.Bytes
{-# INLINABLE sinkToBuffered #-}
sinkToBuffered bo = \ k mbs ->
    case mbs of
        Just bs -> writeBuffer bo bs >> k (Just ())
        _       -> flushBuffer bo >> k EOF

-- | Turn a 'BufferedOutput' into a 'B.Builder' sink.
--
sinkBuilderToBuffered :: HasCallStack => BufferedOutput -> Sink (B.Builder a)
{-# INLINABLE sinkBuilderToBuffered #-}
sinkBuilderToBuffered bo = \ k mbs ->
    case mbs of
        Just bs -> writeBuilder bo bs >> k (Just ())
        _       -> flushBuffer bo >> k EOF

-- | Turn a file into a 'V.Bytes' sink.
--
-- Note the file will be opened in @'FS.O_APPEND' .|. 'FS.O_CREAT' .|. 'FS.O_WRONLY'@ mode,
-- bytes will be written after the end of the original file if there'are old bytes.
initSinkToFile :: HasCallStack => CBytes -> Resource (Sink V.Bytes)
{-# INLINABLE initSinkToFile #-}
initSinkToFile p = do
    f <- FS.initFile p (FS.O_APPEND .|. FS.O_CREAT .|. FS.O_WRONLY) FS.DEFAULT_FILE_MODE
    liftIO (sinkToBuffered <$> newBufferedOutput f)

-- | Turn an `IO` action into 'BIO' sink.
--
sinkToIO :: HasCallStack => (a -> IO ()) -> Sink a
{-# INLINABLE sinkToIO #-}
sinkToIO f = \ k ma ->
    case ma of
        Just a -> f a >> k (Just ())
        _ -> k EOF

-- | Turn an `IO` action(and a flush action), into 'BIO' sink.
--
sinkToIO' :: HasCallStack => (a -> IO ()) -> IO () -> Sink a
{-# INLINABLE sinkToIO' #-}
sinkToIO' f flush = \ k ma ->
    case ma of
        Just a -> f a >> k (Just ())
        _ -> flush >> k EOF

-- | Sink to a list in memory.
--
-- The 'MVar' will be empty during sinking, and will be filled after sink receives an EOF.
sinkToList :: IO (MVar [a], Sink a)
sinkToList = do
    xsRef <- newIORef []
    rRef <- newEmptyMVar
    return (rRef, sinkToIO' (\ x -> modifyIORef xsRef (x:))
                            (do modifyIORef xsRef reverse
                                xs <- readIORef xsRef
                                putMVar rRef xs))

--------------------------------------------------------------------------------
-- s

-- | BIO node from a pure function.
--
-- BIO node made with this funtion are stateless, thus can be reused across chains.
fromPure :: (a -> b) -> BIO a b
{-# INLINABLE fromPure #-}
fromPure f = \ k x -> k (f <$> x)

-- | BIO node from an IO function.
--
-- BIO node made with this funtion may not be stateless, it depends on if the IO function use
-- IO state.
fromIO :: HasCallStack => (a -> IO b) -> BIO a b
{-# INLINABLE fromIO #-}
fromIO f = \ k x ->
    case x of Just x' -> f x' >>= k . Just
              _ -> k EOF

-- | BIO node from a pure filter.
--
-- BIO node made with this funtion are stateless, thus can be reused across chains.
filter ::  (a -> Bool) -> BIO a a
{-# INLINABLE filter #-}
filter f k = go
  where
    go (Just a) = when (f a) $ k (Just a)
    go Nothing = k Nothing

-- | BIO node from an impure filter.
--
-- BIO node made with this funtion may not be stateless, it depends on if the IO function use
filterIO ::  (a -> IO Bool) -> BIO a a
{-# INLINABLE filterIO #-}
filterIO f k = go
  where
    go (Just a) = do
        mbool <- f a
        when mbool $ k (Just a)
    go Nothing = k Nothing

-- | Make a chunk size divider.
--
-- A divider size divide each chunk's size to the nearest multiplier to granularity,
-- last trailing chunk is directly returned.
newReChunk :: Int                -- ^ chunk granularity
           -> IO (BIO V.Bytes V.Bytes)
{-# INLINABLE newReChunk #-}
newReChunk n = do
    trailingRef <- newIORef V.empty
    return $ \ k mbs ->
        case mbs of
            Just bs -> do
                trailing <- readIORef trailingRef
                let chunk =  trailing `V.append` bs
                    l = V.length chunk
                if l >= n
                then do
                    let l' = l - (l `rem` n)
                        (chunk', rest) = V.splitAt l' chunk
                    writeIORef trailingRef rest
                    k (Just chunk')
                else writeIORef trailingRef chunk
            _ -> do
                trailing <- readIORef trailingRef
                unless (V.null trailing) $ do
                    writeIORef trailingRef V.empty
                    k (Just trailing)
                k EOF

-- | Read buffer and parse with 'Parser'.
--
-- This function will turn a 'Parser' into a 'BIO', throw 'OtherError' with name @EPARSE@ if parsing fail.
--
newParser :: HasCallStack => P.Parser a -> IO (BIO V.Bytes a)
{-# INLINABLE newParser #-}
newParser p = do
    -- type LastParseState = Maybe (V.Bytes -> P.Result)
    resultRef <- newIORef EOF
    return $ \ k mbs -> do
        let loop f chunk = case f chunk of
                P.Success a trailing -> do
                    k (Just a)
                    unless (V.null trailing) (loop f trailing)
                P.Partial f' ->
                    writeIORef resultRef (Just f')
                P.Failure e _ ->
                    throwOtherError "EPARSE" (T.toText e)
        lastResult <- readIORef resultRef
        case mbs of
            Just bs -> do
                let f = case lastResult of
                        Just x    -> x
                        _         -> P.parseChunk p
                loop f bs
            _ ->
                case lastResult of
                    Just f -> loop f V.empty
                    _ -> k EOF

-- | Make a new UTF8 decoder, which decode bytes streams into text streams.
--
-- If there're invalid UTF8 bytes, an 'OtherError' with name 'EINVALIDUTF8' will be thrown.`
--
-- Note this node is supposed to be used with preprocess node such as decompressor, parser, etc.
-- where bytes boundary cannot be controlled, UTF8 decoder will concat trailing bytes from last block to next one.
-- Use this node directly with 'sourceFromBuffered' will not be as efficient as directly use
-- 'sourceTextFromBuffered', because 'BufferedInput' provides push back capability,
-- trailing bytes can be pushed back to reading buffer then returned with next block input together.
--
newUTF8Decoder :: HasCallStack => IO (BIO V.Bytes T.Text)
{-# INLINABLE newUTF8Decoder #-}
newUTF8Decoder = do
    trailingRef <- newIORef V.empty
    return $ \ k mbs -> do
        case mbs of
            Just bs -> do
                trailing <- readIORef trailingRef
                let chunk =  trailing `V.append` bs
                    (V.PrimVector arr s l) = chunk
                if l > 0 && T.decodeCharLen arr s <= l
                then do
                    let (i, _) = V.findR (\ w -> w >= 0b11000000 || w <= 0b01111111) chunk
                    if (i == -1)
                    then throwOtherError "EINVALIDUTF8" "invalid UTF8 bytes"
                    else do
                        if T.decodeCharLen arr (s + i) > l - i
                        then do
                            writeIORef trailingRef (V.fromArr arr (s+i) (l-i))
                            k (Just (T.validate (V.fromArr arr s i)))
                        else do
                            writeIORef trailingRef V.empty
                            k (Just (T.validate chunk))
                else writeIORef trailingRef chunk

            _ -> do
                trailing <- readIORef trailingRef
                if V.null trailing
                then k EOF
                else throwOtherError "EINVALIDUTF8" "invalid UTF8 bytes"

-- | Make a new stream splitter based on magic byte.
--
newMagicSplitter :: Word8 -> IO (BIO V.Bytes V.Bytes)
{-# INLINABLE newMagicSplitter #-}
newMagicSplitter magic = do
    trailingRef <- newIORef V.empty
    return $ \ k mx ->
        case mx of
            Just bs -> do
                trailing <- readIORef trailingRef
                let loop chunk = case V.elemIndex magic chunk of
                        Just i -> do
                            -- TODO: looping
                            let (line, rest) = V.splitAt (i+1) chunk
                            k (Just line)
                            loop rest
                        _ -> writeIORef trailingRef chunk
                loop (trailing `V.append` bs)
            _ -> do
                chunk <- readIORef trailingRef
                unless (V.null chunk) $ do
                    writeIORef trailingRef V.empty
                    k (Just chunk)
                k EOF

-- | Make a new stream splitter based on linefeed(@\r\n@ or @\n@).
--
-- The result bytes doesn't contain linefeed.
newLineSplitter :: IO (BIO V.Bytes V.Bytes)
{-# INLINABLE newLineSplitter #-}
newLineSplitter = do
    s <- newMagicSplitter 10
    return (s . fromPure dropLineEnd)
  where
    dropLineEnd bs@(V.PrimVector arr s l) =
        case bs `V.indexMaybe` (l-2) of
            Just r | r == 13   -> V.PrimVector arr s (l-2)
                   | otherwise -> V.PrimVector arr s (l-1)
            _ | V.head bs == 10 -> V.PrimVector arr s (l-1)
              | otherwise -> V.PrimVector arr s l

-- | Make a new base64 encoder node.
newBase64Encoder :: IO (BIO V.Bytes V.Bytes)
{-# INLINABLE newBase64Encoder #-}
newBase64Encoder = do
    re <- newReChunk 3
    return (re . fromPure base64Encode)

-- | Make a new base64 decoder node.
newBase64Decoder :: HasCallStack => IO (BIO V.Bytes V.Bytes)
{-# INLINABLE newBase64Decoder #-}
newBase64Decoder = do
    re <- newReChunk 4
    return (re . fromPure base64Decode')

-- | Make a hex encoder node.
--
-- Hex encoder is stateless, it can be reused across chains.
hexEncode :: Bool   -- ^ uppercase?
          -> BIO V.Bytes V.Bytes
{-# INLINABLE hexEncode #-}
hexEncode upper = fromPure (Hex.hexEncode upper)

-- | Make a new hex decoder node.
newHexDecoder :: IO (BIO V.Bytes V.Bytes)
{-# INLINABLE newHexDecoder #-}
newHexDecoder = do
    re <- newReChunk 2
    return (re . fromPure Hex.hexDecode')

-- | Make a new BIO node which counts items flow throught it.
--
-- 'Counter' is increased atomically, it's safe to read \/ reset the counter from other threads.
counter :: Counter -> BIO a a
{-# INLINABLE counter #-}
counter c = fromIO inc
  where
    inc x = do
        atomicAddCounter_ c 1
        return x

-- | Make a new BIO node which counts items, and label item with a sequence number.
--
-- 'Counter' is increased atomically, it's safe to read \/ reset the counter from other threads.
seqNum :: Counter -> BIO a (Int, a)
{-# INLINABLE seqNum #-}
seqNum c = fromIO inc
  where
    inc x = do
        i <- atomicAddCounter c 1
        return (i, x)

-- | Make a BIO node grouping items into fixed size arrays.
--
-- Trailing items are directly returned.
newGrouping :: V.Vec v a => Int -> IO (BIO a (v a))
{-# INLINABLE newGrouping #-}
newGrouping n
    | n < 1 =  newGrouping 1
    | otherwise = do
        c <- newCounter 0
        arrRef <- newIORef =<< A.newArr n
        return $ \ k mx ->
            case mx of
                Just x -> do
                    i <- readPrimRef c
                    if i == n - 1
                    then do
                        marr <- readIORef arrRef
                        A.writeArr marr i x
                        writePrimRef c 0
                        writeIORef arrRef =<< A.newArr n
                        arr <- A.unsafeFreezeArr marr
                        k . Just $! V.fromArr arr 0 n
                    else do
                        marr <- readIORef arrRef
                        A.writeArr marr i x
                        writePrimRef c (i+1)
                _ -> do
                    i <- readPrimRef c
                    if i /= 0
                    then do
                        writePrimRef c 0
                        marr <- readIORef arrRef
                        A.shrinkMutableArr marr i
                        arr <- A.unsafeFreezeArr marr
                        k . Just $! V.fromArr arr 0 i
                    else k EOF

-- | A BIO node flatten items.
--
ungrouping :: BIO (V.Vector a) a
{-# INLINABLE ungrouping #-}
ungrouping = \ k mx ->
    case mx of
        Just x -> V.traverse_ (k . Just) x
        _      -> k EOF

-- | A BIO node which write 'True' to 'IORef' when 'EOF' is reached.
consumed :: TVar Bool -> BIO a a
{-# INLINABLE consumed #-}
consumed ref = \ k mx -> case mx of
    Just _ -> k mx
    _ -> do atomically (writeTVar ref True)
            k EOF