packages feed

age-0.0.1.0: src/Data/Conduit/Base64.hs

{-# LANGUAGE CPP #-}

module Data.Conduit.Base64
  ( conduitEncodeBase64
  , conduitDecodeBase64
  ) where

import Control.Exception ( assert )
import Control.Monad.Except ( ExceptT, throwError )
import Control.Monad.Trans.Class ( MonadTrans (lift) )
import Data.Text ( Text )
#if MIN_VERSION_base64(1,0,0)
import qualified Data.Base64.Types as B64
#endif
import Data.ByteString ( ByteString )
import qualified Data.ByteString as BS
import qualified Data.ByteString.Base64 as B64
import Data.Conduit ( ConduitT, await, yield )
import Data.MonoTraversable ( olength )
import Prelude

conduitEncodeBase64 :: Monad m => ConduitT ByteString ByteString m ()
conduitEncodeBase64 =
  codeWith 3
#if MIN_VERSION_base64(1,0,0)
    (Right . B64.extractBase64 . B64.encodeBase64')
#else
    (Right . B64.encodeBase64')
#endif
    (error "conduitEncodeBase64: impossible: base64 encoding cannot fail")

conduitDecodeBase64 :: Monad m => ConduitT ByteString ByteString (ExceptT Text m) ()
conduitDecodeBase64 =
  codeWith 4
#if MIN_VERSION_base64(1,0,0)
    B64.decodeBase64Untyped
#else
    B64.decodeBase64
#endif
    (lift . throwError)

codeWith ::
  Monad m =>
  Int ->
  -- | Encoding or decoding function.
  (ByteString -> Either e ByteString) ->
  -- | Error handling function.
  (e -> ConduitT ByteString ByteString m ()) ->
  ConduitT ByteString ByteString m ()
codeWith size f handleErr =
    loop
  where
    loop = await >>= maybe (return ()) push

    loopWith bs
        | BS.null bs = loop
        | otherwise = await >>= maybe (finish bs) (pushWith bs)

    finish bs =
        case f bs of
            Left err -> handleErr err
            Right x -> yield x

    push bs = do
        let (x, y) = BS.splitAt (len - (len `mod` size)) bs
        if BS.null x
            then loopWith y
            else do
                case f x of
                    Left err -> handleErr err
                    Right x' -> yield x' >> loopWith y
      where
        len = olength bs

    pushWith bs1 bs2 | BS.length bs1 + BS.length bs2 < size = loopWith (BS.append bs1 bs2)
    pushWith bs1 bs2 = assertion1 $ assertion2 $ assertion3 $
        case f bs1' of
            Left err -> handleErr err
            Right toYield -> yield toYield >> push y
      where
        m = BS.length bs1 `mod` size
        (x, y) = BS.splitAt (size - m) bs2
        bs1' = mappend bs1 x

        assertion1 = assert $ olength bs1 < size
        assertion2 = assert $ olength bs1' `mod` size == 0
        assertion3 = assert $ olength bs1' > 0