snappy-c-0.1.0: src/Codec/Compression/SnappyC/Framed.hs
-- |
-- Module : Codec.Compression.SnappyC.Framed
-- Copyright : (c) 2024 Finley McIlwaine
-- License : BSD-3-Clause (see LICENSE)
--
-- Maintainer : Finley McIlwaine <finley@well-typed.com>
--
-- Frame format Snappy compression/decompression.
-- See the framing format description here:
-- <https://github.com/google/snappy/blob/main/framing_format.txt>
--
-- Intended for qualified import:
--
-- > import Codec.Compression.SnappyC.Framed qualified as Snappy
module Codec.Compression.SnappyC.Framed
( -- * Compression
compress
-- ** Compression with custom parameters
, EncodeParams(..)
, FrameSize -- Opaque
, Threshold(..)
, compressWithParams
, defaultFrameSize
, customFrameSize
, unFrameSize
-- * Decompression
, DecodeFailure(..)
, decompress
, decompress'
-- ** Decompression with custom parameters
, DecodeParams(..)
, decompressWithParams
, decompressWithParams'
-- * Low-level incremental API
-- ** Compression
, Encoder -- Opaque
, initializeEncoder
, finalizeEncoder
, compressStep
-- ** Decompression
, Decoder -- Opaque
, initializeDecoder
, finalizeDecoder
, decompressStep
, decompressStep'
) where
import Codec.Compression.SnappyC.Internal.Buffer qualified as Buffer
import Codec.Compression.SnappyC.Internal.FrameFormat
import Codec.Compression.SnappyC.Internal.Util
import Data.ByteString.Internal qualified as Strict (ByteString)
import Data.ByteString.Lazy qualified as Lazy (ByteString)
import Data.ByteString.Lazy qualified as BS.Lazy
import Data.Default
import GHC.Stack
-------------------------------------------------------------------------------
-- Compression
-------------------------------------------------------------------------------
-- | Compress the input using [Snappy](https://github.com/google/snappy/).
--
-- The output stream is in Snappy frame format.
compress :: BS.Lazy.ByteString -> BS.Lazy.ByteString
compress = compressWithParams def
-- | Compress the input using [Snappy](https://github.com/google/snappy/) with
-- the given 'EncodeParams'.
--
-- The output stream is in Snappy frame format.
compressWithParams :: EncodeParams -> Lazy.ByteString -> Lazy.ByteString
compressWithParams ps =
BS.Lazy.fromChunks
. (streamId :)
. go initialEncoder
. BS.Lazy.toChunks
where
streamId :: Strict.ByteString
initialEncoder :: Encoder
(streamId, initialEncoder) = initializeEncoder
-- Loop invariant: The 'Encoder' has strictly less than 'chunkSize' in it.
go ::
Encoder
-> [Strict.ByteString]
-> [Strict.ByteString]
go encoder =
\case
[] ->
finalizeEncoder ps encoder
(c:cs) ->
-- The encoded chunks are available to the caller before we process
-- the rest of the chunks.
case compressStep ps encoder c of
(encodedChunks, encoder') ->
encodedChunks ++ go encoder' cs
-- | Append the data to the 'Encoder' buffer and do as much compression as
-- possible.
--
-- __Postconditions:__
--
-- * The resulting 'Encoder' will not have more data in its buffer than the
-- 'chunkSize' in the 'EncodeParams'.
-- * Each encoded frame will hold /exactly/ one `chunkSize` worth of
-- uncompressed data.
compressStep ::
EncodeParams
-> Encoder
-> Strict.ByteString
-> ([Strict.ByteString], Encoder)
compressStep ps (Encoder b) bs =
let
EncodeResult{..} = encodeBuffered ps (Encoder $ b `Buffer.append` bs)
in
(encodeResultEncoded, encodeResultEncoder)
-------------------------------------------------------------------------------
-- Decompression
-------------------------------------------------------------------------------
-- | Decompress the input using [Snappy](https://github.com/google/snappy/).
--
-- The input stream is expected to be in the official Snappy frame format.
--
-- __Note:__ The extra laziness of this function (compared to `decompress'`)
-- comes at the cost of potential exceptions during decompression.
decompress :: HasCallStack => Lazy.ByteString -> Lazy.ByteString
decompress = decompressWithParams def
-- | Decompress the input using [Snappy](https://github.com/google/snappy/) with
-- the given 'DecodeParams'.
--
-- The input stream is expected to be in the official Snappy frame format.
--
-- __Note:__ The extra laziness of this function (compared to
-- `decompressWithParams'`) comes at the cost of potential exceptions during
-- decompression.
decompressWithParams ::
HasCallStack
=> DecodeParams
-> Lazy.ByteString
-> Lazy.ByteString
decompressWithParams dps =
BS.Lazy.fromChunks
. go initializeDecoder
. BS.Lazy.toChunks
where
go ::
Decoder
-> [Strict.ByteString]
-> [Strict.ByteString]
go decoder =
\case
[] ->
throwLeft (finalizeDecoder decoder) `seq` []
(c:cs) ->
let
(decompressed, decoder') = decompressStep dps decoder c
in
decompressed ++ go decoder' cs
-- | Append the data to the 'Decoder' buffer and do as much decompression as
-- possible.
--
-- Throws an exception if any 'DecodeFailure' occurs.
decompressStep ::
HasCallStack
=> DecodeParams
-> Decoder
-> Strict.ByteString
-> ([Strict.ByteString], Decoder)
decompressStep dps d@Decoder{..} bs =
let
DecodeResult{..} =
throwLeft $
decodeBuffered
dps
d { decoderBuffer = decoderBuffer `Buffer.append` bs }
in
( decodeResultDecoded
, decodeResultDecoder
)
-- | Decompress the input using [Snappy](https://github.com/google/snappy/).
--
-- The input stream is expected to be in the official Snappy frame format.
-- Evaluates to a 'DecodeFailure' if the input stream is ill-formed.
--
-- __WARNING:__ This function is not as lazy as you might hope. To determine
-- whether the result is a 'DecodeFailure', it must load the entire source
-- 'Lazy.ByteString' into memory during decompression. Use either
-- 'decompressWithParams' or the incremental `decompressStep'` instead. If you
-- are truly okay with the extra memory overhead, you may ignore this warning.
{-# DEPRECATED decompress' "Consider using decompress or decompressStep' instead" #-}
decompress' :: Lazy.ByteString -> Either DecodeFailure Lazy.ByteString
decompress' = decompressWithParams' def
-- | Decompress the input using [Snappy](https://github.com/google/snappy/) with
-- the given 'DecodeParams'.
--
-- The input stream is expected to be in the official Snappy frame format.
-- Evaluates to a 'DecodeFailure' if the input stream is ill-formed.
--
-- __WARNING:__ This function is not as lazy as you might hope. To determine
-- whether the result is a 'DecodeFailure', it must load the entire source
-- 'Lazy.ByteString' into memory during decompression. Use either
-- 'decompressWithParams' or the incremental `decompressStep'` instead. If you
-- are truly okay with the extra memory overhead, you may ignore this warning.
{-# DEPRECATED decompressWithParams' "Consider using decompressWithParams or decompressStep' instead" #-}
decompressWithParams' ::
DecodeParams
-> Lazy.ByteString
-> Either DecodeFailure Lazy.ByteString
decompressWithParams' dps compressed = do
decompressedChunks <- go initializeDecoder $ BS.Lazy.toChunks compressed
return $ BS.Lazy.fromChunks decompressedChunks
where
go ::
Decoder
-> [Strict.ByteString]
-> Either DecodeFailure [Strict.ByteString]
go decoder =
\case
[] -> do
finalizeDecoder decoder
return []
(c:cs) -> do
(decompressed, decompressor') <- decompressStep' dps decoder c
(decompressed ++) <$> go decompressor' cs
-- | Append the data to the 'Decoder' buffer and do as much decompression as
-- possible.
--
-- __Note:__ This function is not as lazy as 'decompressStep', since it must
-- completely decode the given chunk before providing a result.
decompressStep' ::
DecodeParams
-> Decoder
-> Strict.ByteString
-> Either DecodeFailure ([Strict.ByteString], Decoder)
decompressStep' dps d@Decoder{..} bs = do
DecodeResult{..} <-
decodeBuffered dps d { decoderBuffer = decoderBuffer `Buffer.append` bs }
return (decodeResultDecoded, decodeResultDecoder)