zlib-bytes-0.1.0.2: src/Zlib/Raw.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE CApiFFI #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE UnboxedTuples #-}
{-# LANGUAGE UnliftedFFITypes #-}
{-# LANGUAGE ViewPatterns #-}
module Zlib.Raw
( Zlib
, runZlib
, decompress
, ZlibError (..)
) where
import Control.Exception (Exception)
import Control.Monad.Except (ExceptT, MonadError (catchError, throwError), runExceptT)
import Control.Monad.Reader (ReaderT, asks, runReaderT)
import Control.Monad.ST (ST, runST)
import Control.Monad.Trans.Class (lift)
import Data.Bytes (Bytes)
import Data.Bytes.Chunks (Chunks (ChunksCons, ChunksNil))
import Data.Primitive.ByteArray (MutableByteArray (MutableByteArray), newPinnedByteArray)
import Data.Word (Word8)
import Foreign.C.Types (CInt (CInt))
import Foreign.Ptr (Ptr)
import GHC.Exts (MutableByteArray#, touch#)
import GHC.IO (IO (IO), unsafeIOToST)
import qualified Data.Bytes as Bytes
import qualified Data.Bytes.Chunks as Chunks
import qualified Data.Primitive.ByteArray as BA
-- FIXME there are kinda two monads: ZlibCompress, ZlibDecompress
-- so far, I've only done the latter
newtype Zlib s a = Zlib {unZlib :: ReaderT (Stream s) (ExceptT ZlibError (ST s)) a}
deriving (Functor, Applicative, Monad)
instance MonadError ZlibError (Zlib s) where
throwError exn = Zlib (throwError exn)
catchError try handle = Zlib (unZlib try `catchError` (unZlib . handle))
newtype Stream s = Stream
{ unStream :: MutableByteArray s
}
-- TODO: In GHC 8.10+, use with# instead of touch# so that the
-- noinline pragma is not needed.
runZlib :: (forall s. Zlib s a) -> Bytes -> Either ZlibError a
{-# NOINLINE runZlib #-}
runZlib action inp = runST $ runExceptT $ do
let pinnedInp = Bytes.pin inp
stream <- newStream pinnedInp
v <- runReaderT (unZlib action) stream `catchError` (\exn -> delStream stream >> throwError exn)
_ <- delStream stream
Bytes.touch pinnedInp
pure v
------------ Idiomatic FFI Calls ------------
type PreZlib s a = ExceptT ZlibError (ST s) a
-- Precondition: Bytes are pinned.
-- Postcondition: Call touch on the argument after calling this function.
newStream :: Bytes -> PreZlib s (Stream s)
newStream pinnedInp = do
let inpP = Bytes.contents pinnedInp
inpLen = Bytes.length pinnedInp
MutableByteArray stream# <- newPinnedByteArray sizeofStream
ret <- lift . unsafeIOToST $ initDecompress stream# inpP inpLen
let stream =
Stream
{ unStream = MutableByteArray stream#
}
case ret of
Z_OK -> pure stream
Z_MEM_ERROR -> errorWithoutStackTrace "zlib: out of memory"
Z_VERSION_ERROR -> errorWithoutStackTrace "zlib: incompatible version"
Z_STREAM_ERROR -> throwError InvalidInitParameters
_ -> errorWithoutStackTrace ("unknown error produced by zlib: " ++ show ret)
delStream :: Stream s -> PreZlib s ()
delStream stream = do
let !(MutableByteArray stream#) = unStream stream
ret <- lift . unsafeIOToST $ inflateEnd stream#
case ret of
Z_OK -> pure ()
Z_STREAM_ERROR -> throwError InvalidStreamState
_ -> errorWithoutStackTrace ("unknown error produced by zlib: " ++ show ret)
-- TODO couldn't I resize the output buffer rather than use chunks?
-- probably more useful for an unsliced version
decompress :: Zlib s Chunks
decompress = Zlib $ loop ChunksNil
where
-- TODO adapt chunkSize based on input remaining and estimated compression ratio
chunkSize = 32 * 1024 :: Int
loop acc = do
!(MutableByteArray stream#) <- asks unStream
!oBuf@(MutableByteArray oBuf#) <- newPinnedByteArray chunkSize
ret <- lift . lift . unsafeIOToST $ do
r <- decompressChunk stream# oBuf# chunkSize
-- This call to touch# is not really necessary since GHC cannot
-- possibly have any insight into what ret is, but it is prudent
-- to include it here anyway.
touchMutableByteArray# oBuf#
pure r
case ret of
Z_OK -> do
out <- Bytes.fromByteArray <$> BA.unsafeFreezeByteArray oBuf
let acc' = ChunksCons out acc
loop acc'
Z_STREAM_END -> do
out <- Bytes.fromByteArray <$> BA.unsafeFreezeByteArray oBuf
outRestCInt <- lift . lift . unsafeIOToST $ availOut stream#
let outRest = fromIntegral @CInt @Int outRestCInt
let outLen = chunkSize - outRest
pure $ Chunks.reverse $ case outLen of
0 -> acc
_ -> ChunksCons (Bytes.unsafeTake outLen out) acc
Z_NEED_DICT -> errorWithoutStackTrace "zlib: preset dictionary is needed to decompress"
Z_DATA_ERROR -> throwError DataCorrupt
Z_STREAM_ERROR -> throwError InvalidStreamState
Z_MEM_ERROR -> errorWithoutStackTrace "zlib: out of memory"
Z_BUF_ERROR -> throwError BufferTooSmall
_ -> errorWithoutStackTrace ("unknown error produced by zlib: " ++ show ret)
touchMutableByteArray# :: MutableByteArray# s -> IO ()
touchMutableByteArray# x = IO (\s -> (# touch# x s, () #))
------------ Idiomatic Error Handling ------------
data ZlibError
= InvalidInitParameters -- corresponds to Z_STREAM_ERROR
| InvalidStreamState -- corresponds to Z_STREAM_ERROR
| DataCorrupt -- corresponds to Z_DATA_ERROR
| BufferTooSmall -- corresponds to Z_BUF_ERROR
deriving (Show)
instance Exception ZlibError
pattern Z_BUF_ERROR :: CInt
pattern Z_BUF_ERROR <- ((== z_BUF_ERROR) -> True)
where
Z_BUF_ERROR = z_BUF_ERROR
pattern Z_DATA_ERROR :: CInt
pattern Z_DATA_ERROR <- ((== z_DATA_ERROR) -> True)
where
Z_DATA_ERROR = z_DATA_ERROR
pattern Z_MEM_ERROR :: CInt
pattern Z_MEM_ERROR <- ((== z_MEM_ERROR) -> True)
where
Z_MEM_ERROR = z_MEM_ERROR
pattern Z_NEED_DICT :: CInt
pattern Z_NEED_DICT <- ((== z_NEED_DICT) -> True)
where
Z_NEED_DICT = z_NEED_DICT
pattern Z_OK :: CInt
pattern Z_OK <- ((== z_OK) -> True)
where
Z_OK = z_OK
pattern Z_STREAM_END :: CInt
pattern Z_STREAM_END <- ((== z_STREAM_END) -> True)
where
Z_STREAM_END = z_STREAM_END
pattern Z_STREAM_ERROR :: CInt
pattern Z_STREAM_ERROR <- ((== z_STREAM_ERROR) -> True)
where
Z_STREAM_ERROR = z_STREAM_ERROR
pattern Z_VERSION_ERROR :: CInt
pattern Z_VERSION_ERROR <- ((== z_VERSION_ERROR) -> True)
where
Z_VERSION_ERROR = z_VERSION_ERROR
------------ Raw Foreign Imports ------------
foreign import capi "zlib.h value Z_BUF_ERROR" z_BUF_ERROR :: CInt
foreign import capi "zlib.h value Z_DATA_ERROR" z_DATA_ERROR :: CInt
foreign import capi "zlib.h value Z_MEM_ERROR" z_MEM_ERROR :: CInt
foreign import capi "zlib.h value Z_NEED_DICT" z_NEED_DICT :: CInt
foreign import capi "zlib.h value Z_OK" z_OK :: CInt
foreign import capi "zlib.h value Z_STREAM_END" z_STREAM_END :: CInt
foreign import capi "zlib.h value Z_STREAM_ERROR" z_STREAM_ERROR :: CInt
foreign import capi "zlib.h value Z_VERSION_ERROR" z_VERSION_ERROR :: CInt
foreign import capi "hs_zlib.h value hs_sizeofStream" sizeofStream :: Int
foreign import ccall unsafe "hs_initDecompress"
initDecompress ::
MutableByteArray# s ->
Ptr Word8 ->
Int ->
IO CInt
foreign import ccall unsafe "hs_decompressChunk"
decompressChunk ::
MutableByteArray# s ->
MutableByteArray# s ->
Int ->
IO CInt
foreign import ccall unsafe "hs_avail_out"
availOut ::
MutableByteArray# s ->
IO CInt
foreign import ccall unsafe "inflateEnd"
inflateEnd ::
MutableByteArray# s ->
IO CInt