{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE Trustworthy #-}
module Codec.Compression.Brotli
(
compress
, decompress
, BrotliException(..)
, compressWith
, decompressWith
, CompressStream(..)
, compressIO
, compressST
, DecompressStream(..)
, decompressIO
, decompressST
, BrotliDecoderErrorCode(..)
, showBrotliDecoderErrorCode
, defaultCompressParams
, CompressParams
, compressLevel
, compressWindowSize
, compressMode
, compressSizeHint
, CompressionLevel(..)
, CompressionWindowSize(..)
, CompressionMode(..)
, defaultDecompressParams
, DecompressParams
, decompressDisableRingBufferReallocation
) where
import Control.Applicative
import Prelude
import Control.Exception
import Control.Monad
import Control.Monad.ST (stToIO)
import Control.Monad.ST.Lazy (ST, runST, strictToLazyST)
import qualified Control.Monad.ST.Strict as ST.Strict (ST)
import Control.Monad.ST.Unsafe (unsafeIOToST)
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BSL
import qualified Data.ByteString.Lazy.Internal as BSL
import Data.Monoid (Monoid (mempty))
import Data.Typeable (Typeable)
import GHC.IO (noDuplicate)
import LibBrotli
newtype BrotliException = BrotliException String deriving (Typeable,Show)
instance Exception BrotliException
compress :: BSL.ByteString -> BSL.ByteString
compress = compressWith defaultCompressParams
compressWith :: CompressParams -> BSL.ByteString -> BSL.ByteString
compressWith parms input = runST (compress' input)
where
compress' :: BSL.ByteString -> ST s BSL.ByteString
compress' ibs0 = loop ibs0 =<< compressST parms
where
loop BSL.Empty CompressStreamEnd =
pure BSL.Empty
loop (BSL.Chunk _ _) CompressStreamEnd =
throwST (BrotliException "internal error")
loop BSL.Empty (CompressInputRequired _ supply) =
loop BSL.Empty =<< supply BS.empty
loop (BSL.Chunk c bs') (CompressInputRequired _ supply) =
loop bs' =<< supply c
loop ibs (CompressOutputAvailable oc next) = do
obs <- loop ibs =<< next
pure (BSL.chunk oc obs)
{-# NOINLINE compressWith #-}
decompress :: BSL.ByteString -> BSL.ByteString
decompress = decompressWith defaultDecompressParams
decompressWith :: DecompressParams -> BSL.ByteString -> BSL.ByteString
decompressWith parms input = runST (decompress' input)
where
decompress' :: BSL.ByteString -> ST s BSL.ByteString
decompress' ibs0 = loop ibs0 =<< decompressST parms
where
loop BSL.Empty (DecompressStreamEnd rest)
| BS.null rest = pure BSL.Empty
| otherwise = throwST (BrotliException "extra trailing data")
loop (BSL.Chunk _ _) (DecompressStreamEnd _) =
throwST (BrotliException "extra trailing data")
loop _ (DecompressStreamError ec) =
throwST ec
loop BSL.Empty (DecompressInputRequired supply) =
loop BSL.Empty =<< supply BS.empty
loop (BSL.Chunk c bs') (DecompressInputRequired supply) =
loop bs' =<< supply c
loop ibs (DecompressOutputAvailable oc next) = do
obs <- loop ibs =<< next
pure (BSL.chunk oc obs)
{-# NOINLINE decompressWith #-}
data CompressStream m =
CompressInputRequired (m (CompressStream m))
(ByteString -> m (CompressStream m))
| CompressOutputAvailable !ByteString (m (CompressStream m))
| CompressStreamEnd
compressIO :: CompressParams -> IO (CompressStream IO)
compressIO parms = stToIO (newBrotliEncoder parms)
>>= maybe (throwIO (BrotliException "failed to initialize encoder")) go
where
bUFSIZ = 32752
go ls = pure inputRequired
where
inputRequired = CompressInputRequired goFlush (withChunk goFinish goInput)
unexpectedState = throwIO (BrotliException "internal error (unexpected state)")
encoderFailure = throwIO (BrotliException "encoder failure")
internalError = throwIO (BrotliException "internal error")
goInput chunk = do
(rc, unused) <- stToIO (runBrotliEncoder ls chunk BrotliEncOpProcess)
let chunk' = BS.drop used chunk
used = BS.length chunk - unused
case rc of
BSFail -> encoderFailure
BSInternalError -> internalError
BSFinished -> unexpectedState
BSNeedsInput -> do unless (used > 0) internalError
withChunk (pure inputRequired) goInput chunk'
BSHasOutput -> drainOutput (withChunk (pure inputRequired) goInput chunk') unexpectedState
goFlush = do
(rc, 0) <- stToIO (runBrotliEncoder ls mempty BrotliEncOpFlush)
case rc of
BSFail -> encoderFailure
BSInternalError -> internalError
BSFinished -> unexpectedState
BSNeedsInput -> unexpectedState
BSHasOutput -> drainOutput (pure inputRequired) unexpectedState
goFinish = do
(rc, 0) <- stToIO (runBrotliEncoder ls mempty BrotliEncOpFinish)
case rc of
BSFail -> encoderFailure
BSInternalError -> internalError
BSFinished -> do
!() <- stToIO (finalizeBrotliEncoder ls)
pure CompressStreamEnd
BSNeedsInput -> unexpectedState
BSHasOutput -> drainOutput unexpectedState (pure CompressStreamEnd)
drainOutput needsInputCont finishedCont = do
(rc, obuf) <- stToIO (readBrotliEncoder ls bUFSIZ)
case rc of
BSFail -> encoderFailure
BSInternalError -> internalError
BSHasOutput -> do
pure (CompressOutputAvailable obuf (drainOutput needsInputCont finishedCont))
BSNeedsInput -> do
pure (CompressOutputAvailable obuf needsInputCont)
BSFinished -> do
!() <- stToIO (finalizeBrotliEncoder ls)
pure (CompressOutputAvailable obuf finishedCont)
compressST :: CompressParams -> ST s (CompressStream (ST s))
compressST parms = strictToLazyST (newBrotliEncoder parms)
>>= maybe (throwST (BrotliException "failed to initialize encoder")) go
where
bUFSIZ = 32752
go ls = pure inputRequired
where
inputRequired = CompressInputRequired goFlush (withChunk goFinish goInput)
unexpectedState = throwST (BrotliException "internal error (unexpected state)")
encoderFailure = throwST (BrotliException "encoder failure")
internalError = throwST (BrotliException "internal error")
goInput :: ByteString -> ST s (CompressStream (ST s))
goInput chunk = do
(rc, unused) <- strictToLazyST (noDuplicateST >> runBrotliEncoder ls chunk BrotliEncOpProcess)
let chunk' = BS.drop used chunk
used = BS.length chunk - unused
case rc of
BSFail -> encoderFailure
BSInternalError -> internalError
BSFinished -> unexpectedState
BSNeedsInput -> do unless (used > 0) internalError
withChunk (pure inputRequired) goInput chunk'
BSHasOutput -> drainOutput (withChunk (pure inputRequired) goInput chunk') unexpectedState
goFlush :: ST s (CompressStream (ST s))
goFlush = do
(rc, 0) <- strictToLazyST (noDuplicateST >> runBrotliEncoder ls mempty BrotliEncOpFlush)
case rc of
BSFail -> encoderFailure
BSInternalError -> internalError
BSFinished -> unexpectedState
BSNeedsInput -> unexpectedState
BSHasOutput -> drainOutput (pure inputRequired) unexpectedState
goFinish :: ST s (CompressStream (ST s))
goFinish = do
(rc, 0) <- strictToLazyST (noDuplicateST >> runBrotliEncoder ls mempty BrotliEncOpFinish)
case rc of
BSFail -> encoderFailure
BSInternalError -> internalError
BSFinished -> do
!() <- strictToLazyST (noDuplicateST >> finalizeBrotliEncoder ls)
pure CompressStreamEnd
BSNeedsInput -> unexpectedState
BSHasOutput -> drainOutput unexpectedState (pure CompressStreamEnd)
drainOutput :: ST s (CompressStream (ST s)) -> ST s (CompressStream (ST s)) -> ST s (CompressStream (ST s))
drainOutput needsInputCont finishedCont = do
(rc, obuf) <- strictToLazyST (noDuplicateST >> readBrotliEncoder ls bUFSIZ)
case rc of
BSFail -> encoderFailure
BSInternalError -> internalError
BSHasOutput -> do
pure (CompressOutputAvailable obuf (drainOutput needsInputCont finishedCont))
BSNeedsInput -> do
pure (CompressOutputAvailable obuf needsInputCont)
BSFinished -> do
!() <- strictToLazyST (noDuplicateST >> finalizeBrotliEncoder ls)
pure (CompressOutputAvailable obuf finishedCont)
data DecompressStream m =
DecompressInputRequired (ByteString -> m (DecompressStream m))
| DecompressOutputAvailable !ByteString (m (DecompressStream m))
| DecompressStreamEnd ByteString
| DecompressStreamError !BrotliDecoderErrorCode
decompressIO :: DecompressParams -> IO (DecompressStream IO)
decompressIO parms = stToIO (newBrotliDecoder parms)
>>= maybe (throwIO (BrotliException "failed to initialize decoder")) go
where
bUFSIZ = 32752
go ls = pure inputRequired
where
inputRequired = DecompressInputRequired (withChunk goFinish goInput)
unexpectedState = throwIO (BrotliException "internal error (unexpected state)")
internalError = throwIO (BrotliException "internal error")
truncatedError = DecompressStreamError (BrotliDecoderErrorCode 2)
goInput chunk = do
(rc, ecode, unused) <- stToIO (runBrotliDecoder ls chunk)
let chunk' = BS.drop used chunk
used = BS.length chunk - unused
case rc of
BSFail -> pure (DecompressStreamError ecode)
BSInternalError -> internalError
BSFinished -> pure (DecompressStreamEnd chunk')
BSNeedsInput -> do
unless (used > 0) internalError
withChunk (pure inputRequired) goInput chunk'
BSHasOutput -> drainOutput (withChunk (pure inputRequired) goInput chunk')
(pure (DecompressStreamEnd chunk'))
goFinish = do
(rc, ecode, 0) <- stToIO (runBrotliDecoder ls mempty)
case rc of
BSFail -> pure (DecompressStreamError ecode)
BSInternalError -> internalError
BSFinished -> do
!() <- stToIO (finalizeBrotliDecoder ls)
pure (DecompressStreamEnd mempty)
BSNeedsInput -> pure truncatedError
BSHasOutput -> drainOutput (pure truncatedError)
(pure (DecompressStreamEnd mempty))
drainOutput needsInputCont finishedCont = do
(rc, obuf) <- stToIO (readBrotliDecoder ls bUFSIZ)
case rc of
BSFail -> unexpectedState
BSInternalError -> internalError
BSHasOutput ->
pure (DecompressOutputAvailable obuf (drainOutput needsInputCont finishedCont))
BSNeedsInput ->
pure (DecompressOutputAvailable obuf needsInputCont)
BSFinished -> do
!() <- stToIO (finalizeBrotliDecoder ls)
pure (DecompressOutputAvailable obuf finishedCont)
decompressST :: DecompressParams -> ST s (DecompressStream (ST s))
decompressST parms = strictToLazyST (newBrotliDecoder parms)
>>= maybe (throwST (BrotliException "failed to initialize decoder")) go
where
bUFSIZ = 32752
go ls = pure inputRequired
where
inputRequired = DecompressInputRequired (withChunk goFinish goInput)
unexpectedState = throwST (BrotliException "internal error (unexpected state)")
internalError = throwST (BrotliException "internal error")
truncatedError = DecompressStreamError (BrotliDecoderErrorCode 2)
goInput :: ByteString -> ST s (DecompressStream (ST s))
goInput chunk = do
(rc, ecode, unused) <- strictToLazyST (noDuplicateST >> runBrotliDecoder ls chunk)
let chunk' = BS.drop used chunk
used = BS.length chunk - unused
case rc of
BSFail -> pure (DecompressStreamError ecode)
BSInternalError -> internalError
BSFinished -> pure (DecompressStreamEnd chunk')
BSNeedsInput -> do
unless (used > 0) internalError
withChunk (pure inputRequired) goInput chunk'
BSHasOutput -> drainOutput (withChunk (pure inputRequired) goInput chunk')
(pure (DecompressStreamEnd chunk'))
goFinish :: ST s (DecompressStream (ST s))
goFinish = do
(rc, ecode, 0) <- strictToLazyST (noDuplicateST >> runBrotliDecoder ls mempty)
case rc of
BSFail -> pure (DecompressStreamError ecode)
BSInternalError -> internalError
BSFinished -> do
!() <- strictToLazyST (noDuplicateST >> finalizeBrotliDecoder ls)
pure (DecompressStreamEnd mempty)
BSNeedsInput -> pure truncatedError
BSHasOutput -> drainOutput (pure truncatedError)
(pure (DecompressStreamEnd mempty))
drainOutput :: ST s (DecompressStream (ST s)) -> ST s (DecompressStream (ST s)) -> ST s (DecompressStream (ST s))
drainOutput needsInputCont finishedCont = do
(rc, obuf) <- strictToLazyST (noDuplicateST >> readBrotliDecoder ls bUFSIZ)
case rc of
BSFail -> unexpectedState
BSInternalError -> internalError
BSHasOutput ->
pure (DecompressOutputAvailable obuf (drainOutput needsInputCont finishedCont))
BSNeedsInput ->
pure (DecompressOutputAvailable obuf needsInputCont)
BSFinished -> do
!() <- strictToLazyST (noDuplicateST >> finalizeBrotliDecoder ls)
pure (DecompressOutputAvailable obuf finishedCont)
withChunk :: t -> (ByteString -> t) -> ByteString -> t
withChunk emptyChunk nemptyChunk chunk
| BS.null chunk = emptyChunk
| otherwise = nemptyChunk chunk
noDuplicateST :: ST.Strict.ST s ()
noDuplicateST = unsafeIOToST noDuplicate
throwST :: Exception e => e -> ST s a
throwST = throw