-- | Interface to @zlib@ and @gzip@ compression for 'Bytestring's and 'Builder's {-# LANGUAGE GeneralizedNewtypeDeriving #-} module System.IO.Streams.Zlib ( -- * ByteString decompression gunzip , decompress -- * ByteString compression , gzip , compress -- * Builder compression , gzipBuilder , compressBuilder -- * Compression level , CompressionLevel(..) , defaultCompressionLevel ) where ------------------------------------------------------------------------------ import Data.ByteString (ByteString) import qualified Data.ByteString as S import Data.IORef (newIORef, readIORef, writeIORef) import Prelude hiding (read) ------------------------------------------------------------------------------ import Codec.Zlib (Deflate, Inflate, Popper, WindowBits (..), feedDeflate, feedInflate, finishDeflate, finishInflate, flushDeflate, flushInflate, initDeflate, initInflate) import Data.ByteString.Builder (Builder, byteString) import Data.ByteString.Builder.Extra (defaultChunkSize, flush) import Data.ByteString.Builder.Internal (newBuffer) ------------------------------------------------------------------------------ import System.IO.Streams.Builder (unsafeBuilderStream) import System.IO.Streams.Internal (InputStream, OutputStream, makeInputStream, makeOutputStream, read, write) ------------------------------------------------------------------------------ gzipBits :: WindowBits gzipBits = WindowBits 31 ------------------------------------------------------------------------------ compressBits :: WindowBits compressBits = WindowBits 15 ------------------------------------------------------------------------------ -- | Decompress an 'InputStream' of strict 'ByteString's from the @gzip@ format gunzip :: InputStream ByteString -> IO (InputStream ByteString) gunzip input = initInflate gzipBits >>= inflate input ------------------------------------------------------------------------------ -- | Decompress an 'InputStream' of strict 'ByteString's from the @zlib@ format decompress :: InputStream ByteString -> IO (InputStream ByteString) decompress input = initInflate compressBits >>= inflate input ------------------------------------------------------------------------------ -- Note: bytes pushed back to this input stream are not propagated back to the -- source InputStream. data IS = Input | Popper Popper | Done inflate :: InputStream ByteString -> Inflate -> IO (InputStream ByteString) inflate input state = do ref <- newIORef Input makeInputStream $ stream ref where stream ref = go where go = readIORef ref >>= \st -> case st of Input -> read input >>= maybe eof chunk Popper p -> pop p Done -> return Nothing eof = do x <- finishInflate state writeIORef ref Done if (not $ S.null x) then return $! Just x else return Nothing chunk s = if S.null s then do out <- flushInflate state return $! Just out else feedInflate state s >>= \popper -> do writeIORef ref $ Popper popper pop popper pop popper = popper >>= maybe backToInput (return . Just) backToInput = writeIORef ref Input >> read input >>= maybe eof chunk ------------------------------------------------------------------------------ deflateBuilder :: OutputStream Builder -> Deflate -> IO (OutputStream Builder) deflateBuilder stream state = do zippedStr <- makeOutputStream bytestringStream >>= \x -> deflate x state -- we can use unsafeBuilderStream here because zlib is going to consume the -- stream unsafeBuilderStream (newBuffer defaultChunkSize) zippedStr where bytestringStream x = write (fmap cvt x) stream cvt s | S.null s = flush | otherwise = byteString s ------------------------------------------------------------------------------ -- | Convert an 'OutputStream' that consumes compressed 'Builder's into an -- 'OutputStream' that consumes uncompressed 'Builder's in the @gzip@ format gzipBuilder :: CompressionLevel -> OutputStream Builder -> IO (OutputStream Builder) gzipBuilder level output = initDeflate (clamp level) gzipBits >>= deflateBuilder output ------------------------------------------------------------------------------ -- | Convert an 'OutputStream' that consumes compressed 'Builder's into an -- 'OutputStream' that consumes uncompressed 'Builder's in the @zlib@ format compressBuilder :: CompressionLevel -> OutputStream Builder -> IO (OutputStream Builder) compressBuilder level output = initDeflate (clamp level) compressBits >>= deflateBuilder output ------------------------------------------------------------------------------ deflate :: OutputStream ByteString -> Deflate -> IO (OutputStream ByteString) deflate output state = makeOutputStream stream where stream Nothing = popAll (finishDeflate state) >> write Nothing output stream (Just s) = do -- Empty string means flush if S.null s then do popAll (flushDeflate state) write (Just S.empty) output else feedDeflate state s >>= popAll popAll popper = go where go = popper >>= maybe (return $! ()) (\s -> write (Just s) output >> go) ------------------------------------------------------------------------------ -- | Parameter that defines the tradeoff between speed and compression ratio newtype CompressionLevel = CompressionLevel Int deriving (Read, Eq, Show, Num) ------------------------------------------------------------------------------ -- | A compression level that balances speed with compression ratio defaultCompressionLevel :: CompressionLevel defaultCompressionLevel = CompressionLevel 5 ------------------------------------------------------------------------------ clamp :: CompressionLevel -> Int clamp (CompressionLevel x) = min 9 (max x 0) ------------------------------------------------------------------------------ -- | Convert an 'OutputStream' that consumes compressed 'ByteString's into an -- 'OutputStream' that consumes uncompressed 'ByteString's in the @gzip@ format gzip :: CompressionLevel -> OutputStream ByteString -> IO (OutputStream ByteString) gzip level output = initDeflate (clamp level) gzipBits >>= deflate output ------------------------------------------------------------------------------ -- | Convert an 'OutputStream' that consumes compressed 'ByteString's into an -- 'OutputStream' that consumes uncompressed 'ByteString's in the @zlib@ format compress :: CompressionLevel -> OutputStream ByteString -> IO (OutputStream ByteString) compress level output = initDeflate (clamp level) compressBits >>= deflate output