zstd-0.1.1.2: Codec/Compression/Zstd/Streaming.hs
-- Copyright (c) 2016-present, Facebook, Inc.
-- All rights reserved.
--
-- This source code is licensed under the BSD-style license found in
-- the LICENSE file in the root directory of this source tree. An
-- additional grant of patent rights can be found in the PATENTS file
-- in the same directory.
{-# LANGUAGE MultiWayIf #-}
-- |
-- Module : Codec.Compression.Zstd.Streaming
-- Copyright : (c) 2016-present, Facebook, Inc. All rights reserved.
--
-- License : BSD3
-- Maintainer : bryano@fb.com
-- Stability : experimental
-- Portability : GHC
--
-- Streaming compression and decompression support for zstd.
module Codec.Compression.Zstd.Streaming
(
Result(..)
, compress
, decompress
, maxCLevel
) where
import Codec.Compression.Zstd.FFI hiding (compress, decompress)
import Codec.Compression.Zstd.FFI.Types (peekPos)
import qualified Data.ByteString as B
import Data.ByteString.Internal (ByteString(..), mallocByteString)
import Foreign.Marshal.Alloc (finalizerFree, malloc)
import Foreign.C.Types (CSize)
import Foreign.ForeignPtr
import Foreign.ForeignPtr.Unsafe (unsafeForeignPtrToPtr)
import Foreign.Storable (poke)
import Foreign.Ptr (Ptr, plusPtr)
import Data.Word (Word8)
-- | The result of a streaming compression or decompression step.
data Result
= Produce ByteString (IO Result)
-- ^ A single frame of transformed data, and an action that when
-- executed will yield the next step in the streaming operation.
-- The action is ephemeral; you should discard it as soon as you
-- use it.
| Consume (ByteString -> IO Result)
-- ^ Provide the function with more input for the streaming
-- operation to continue. This function is ephemeral. You should
-- call it exactly once, and discard it immediately after you call
-- it.
--
-- To signal the end of a stream of data, supply an 'B.empty'
-- input.
| Error String String
-- ^ An error has occurred. If an error occurs, the streaming
-- operation cannot continue.
| Done ByteString
-- ^ The streaming operation has ended. This payload may be
-- empty. If it is not, it must be written out.
--
-- A non-empty payload consists of a frame epilogue, possibly
-- preceded by any data left over from the final streaming step.
instance Show Result where
show (Produce bs _) = "Produce " ++ show bs ++ " _"
show (Consume _) = "Consume _"
show (Error n d) = "Error " ++ show n ++ " " ++ show d
show (Done bs) = "Done " ++ show bs
-- | Begin a streaming compression operation.
--
-- The initial result will be either an 'Error' or a 'Consume'.
compress :: Int
-- ^ Compression level. Must be >= 1 and <= 'maxCLevel'.
-> IO Result
compress level
| level < 1 || level > maxCLevel =
return (Error "compress" "unsupported compression level")
| otherwise =
streaming
createCStream
p_freeCStream
outSize
(\cs -> initCStream cs (fromIntegral level))
compressStream
finish
where
outSize = fromIntegral cstreamOutSize
finish cfp obfp opos dfp = do
let cptr = unsafeForeignPtrToPtr cfp
obuf = unsafeForeignPtrToPtr obfp
check "endStream" (endStream cptr obuf) $ \leftover -> do
touchForeignPtr cfp
touchForeignPtr obfp
if | leftover <= 0 -> do -- leftover will never be <0, but compiler does not know that
opos1 <- fromIntegral `fmap` peekPos obuf
Done `fmap` shrink outSize dfp opos1
| leftover > 0 -> do
opos1 <- fromIntegral `fmap` peekPos obuf
dfp1 <- mallocByteString (fromIntegral leftover)
poke obuf (buffer (unsafeForeignPtrToPtr dfp1) leftover)
touchForeignPtr obfp
bs <- shrink outSize dfp opos1
return (Produce bs (finish cfp obfp 0 dfp1))
type ConsumeBlock ctx io = Ptr ctx -> Ptr (Buffer Out)
-> Ptr (Buffer In) -> IO CSize
type Finish ctx io = ForeignPtr ctx -> ForeignPtr (Buffer Out)
-> Int -> ForeignPtr Word8 -> IO Result
streaming :: IO (Ptr ctx)
-> FinalizerPtr ctx
-> Int
-> (Ptr ctx -> IO CSize)
-> ConsumeBlock ctx io
-> Finish ctx io
-> IO Result
streaming createStream freeStream outSize initStream consumeBlock finish = do
cx <- checkAlloc "createStream" createStream
cxfp <- newForeignPtr freeStream cx
check "initStream" (initStream cx) $ \_ -> do
ibfp <- newForeignPtr finalizerFree =<< malloc
obfp <- newForeignPtr finalizerFree =<< malloc
dfp <- newOutput obfp
advanceInput cxfp ibfp obfp 0 dfp
where
advanceInput cxfp ibfp obfp opos dfp = do
let prompt (PS fp off len)
| len == 0 = finish cxfp obfp opos dfp
| otherwise = do
withForeignPtr fp $ \sp0 ->
withForeignPtr ibfp $ \ibuf ->
poke ibuf (buffer (sp0 `plusPtr` off) (fromIntegral len))
consume cxfp ibfp 0 len obfp 0 dfp fp
return (Consume prompt)
newOutput obfp = do
dfp <- mallocByteString outSize
withForeignPtr dfp $ \dp ->
withForeignPtr obfp $ \obuf ->
poke obuf (buffer dp (fromIntegral outSize))
return dfp
consume cxfp ibfp ipos ilen obfp opos dfp fp = do
if | fromIntegral ipos == ilen -> advanceInput cxfp ibfp obfp opos dfp
| opos == outSize -> do
let go = do
ndfp <- newOutput obfp
consume cxfp ibfp ipos ilen obfp 0 ndfp fp
return (Produce (PS dfp 0 opos) go)
| otherwise -> do
let obuf = unsafeForeignPtrToPtr obfp
ibuf = unsafeForeignPtrToPtr ibfp
check "consumeBlock"
(withForeignPtr cxfp $ \cptr ->
consumeBlock cptr obuf ibuf <* touchForeignPtr fp) $ \_ -> do
opos1 <- fromIntegral `fmap` peekPos obuf
ipos1 <- peekPos ibuf
touchForeignPtr obfp
touchForeignPtr ibfp
consume cxfp ibfp ipos1 ilen obfp opos1 dfp fp
-- | Begin a streaming decompression operation.
--
-- The initial result will be either an 'Error' or a 'Consume'.
decompress :: IO Result
decompress =
streaming
createDStream
p_freeDStream
outSize
initDStream
decompressStream
finish
where
outSize = fromIntegral dstreamOutSize
finish _cxfp _obfp opos dfp = Done `fmap` shrink outSize dfp opos
shrink :: Int -> ForeignPtr Word8 -> Int -> IO B.ByteString
shrink capacity dfp opos
| opos == 0 = return B.empty
| let unused = capacity - opos
in unused >= 1024 || unused > capacity `rem` 8
= return (B.copy (PS dfp 0 opos))
| otherwise = return (PS dfp 0 opos)
buffer :: Ptr a -> CSize -> Buffer io
buffer ptr size = Buffer ptr size 0
check :: String -> IO CSize -> (CSize -> IO Result) -> IO Result
check name act onSuccess = do
ret <- act
if isError ret
then return (Error name (getErrorName ret))
else onSuccess ret