lz4-bytes-0.2.1.0: src/Lz4/Frame.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE BinaryLiterals #-}
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE UnliftedFFITypes #-}
{- | Compress a contiguous sequence of bytes into an LZ4 frame
containing a single block.
-}
module Lz4.Frame
( -- * Compression
compressHighlyU
, compressU
-- * Decompression
, decompressU
) where
import Lz4.Internal (requiredBufferSize,c_hs_compress_HC,c_hs_compress_fast,c_hs_decompress_safe,DecompressionContext)
import Lz4.Internal (c_LZ4F_createDecompressionContext, c_hs_decompress_frame)
import Lz4.Internal (c_LZ4F_freeDecompressionContext)
import Foreign.C.Types (CSize)
import Foreign.Ptr (Ptr)
import Foreign.Storable (peek)
import GHC.Exts (RealWorld)
import Control.Monad (when)
import Control.Monad.ST (runST)
import Data.Bits ((.&.))
import Data.Bytes.Types (Bytes (Bytes))
import Data.Int (Int32)
import Data.Primitive (ByteArray (..), MutableByteArray (..), MutablePrimArray)
import Data.Word (Word8, Word32)
import GHC.IO (unsafeIOToST,unsafeDupablePerformIO)
import qualified Data.Primitive as PM
import qualified Data.Primitive.ByteArray.LittleEndian as LE
import qualified Data.Bytes as Bytes
-- | Decompress an LZ4 frame. The caller must know the exact size
-- of the decompressed byte array.
--
-- Note: This currently fails if any of the optional headers are used.
-- It is difficult to find examples of lz4 frames that actually use
-- any of these. Open a PR with an example of an lz4 frame that fails
-- to decode if you find one.
--
-- Error codes from the C library lz4 are returned in the Left data
-- constructor.
decompressU ::
Int -- ^ The exact size of the decompressed bytes
-> Bytes -- ^ Compressed bytes
-> Either Int ByteArray
decompressU !decompressedSize (Bytes arr@(ByteArray arr# ) off len) = do
when (len < 11) (Left 10000)
when (indexWord8 arr off /= 0x04) (Left 10001)
when (indexWord8 arr (off + 1) /= 0x22) (Left 10002)
when (indexWord8 arr (off + 2) /= 0x4D) (Left 10003)
when (indexWord8 arr (off + 3) /= 0x18) (Left 10004)
let !flag = indexWord8 arr (off + 4)
-- when (flag /= 0b0110_0000) Nothing
-- Here is the code that would read the size hint from the bd. However,
-- there is no reason to use this since this function takes the actual
-- size as an argument. We ignore the checksum at position off+6 as well.
-- let !bd = indexWord8 arr (off + 5)
-- maximumDecompressedSize <- case bd of
-- 0b0111_0000 -> pure 4194304
-- 0b0110_0000 -> pure 1048576
-- 0b0101_0000 -> pure 262144
-- 0b0100_0000 -> pure 65536
-- _ -> Nothing
-- when (maximumDecompressedSize < decompressedSize) Nothing
if | flag == 0b0110_0000
, !compressedSize <- LE.indexUnalignedByteArray arr (off + 7) :: Word32
, !compressedSizeI <- fromIntegral (compressedSize .&. 0x7fff_ffff) :: Int
, compressedSizeI + (4 + 3 + 4 + 4) == len -> do
-- When there is only one block and there aren't any checksums,
-- we can just decompress the single block and allocating and
-- freeing a context. This case is pretty common in practice.
let !offPost = off + 11 + compressedSizeI
when (indexWord8 arr offPost /= 0x00) (Left 10005)
when (indexWord8 arr (offPost + 1) /= 0x00) (Left 10006)
when (indexWord8 arr (offPost + 2) /= 0x00) (Left 10007)
when (indexWord8 arr (offPost + 3) /= 0x00) (Left 10008)
case compressedSize .&. 0x8000_0000 of
0 -> runST $ do
dst@(MutableByteArray dst# ) <- PM.newByteArray decompressedSize
actualSz <- unsafeIOToST (c_hs_decompress_safe arr# (off + 11) dst# 0 compressedSizeI decompressedSize)
-- Note: actualSz will be negative if decompression fails. That's fine.
if actualSz == decompressedSize
then do
dst' <- PM.unsafeFreezeByteArray dst
pure (Right dst')
else pure (Left 10009)
_ -> do
-- When the upper bit of the size is set, it means that the data in
-- the block is uncompressed. This code path is not tested in the test
-- suite, and I cannot find examples of this feature used in the wild.
-- If anyone knows of an example, open a PR.
when (decompressedSize /= compressedSizeI) (Left 10010)
when (decompressedSize + 15 /= len) (Left 10011)
Right $! Bytes.toByteArrayClone (Bytes arr (off + 11) decompressedSize)
| otherwise -> unsafeDupablePerformIO $ do
dst@(MutableByteArray dst# ) <- PM.newByteArray decompressedSize
dctxPtrMba :: MutablePrimArray RealWorld (Ptr DecompressionContext) <- PM.newPinnedPrimArray 1
let !dctxPtrPtr = PM.mutablePrimArrayContents dctxPtrMba
x <- c_LZ4F_createDecompressionContext dctxPtrPtr 100
case x of
0 -> do
dctxPtr <- peek dctxPtrPtr
r <- c_hs_decompress_frame dctxPtr arr# off len dst# decompressedSize
_ <- c_LZ4F_freeDecompressionContext dctxPtr
case r of
0 -> Right <$> PM.unsafeFreezeByteArray dst
_ -> pure (Left r)
_ -> errorWithoutStackTrace "Lz4.Frame.decompressU: failed to create decompression context"
{- | Use HC compression to produce a frame with a single block.
All optional fields (checksums, content sizes, and dictionary IDs)
are omitted.
-}
compressHighlyU ::
-- | Compression level (Use 9 if uncertain)
Int ->
-- | Bytes to compress
Bytes ->
ByteArray
{-# inline compressHighlyU #-}
compressHighlyU = compressGeneralU 1
compressU ::
-- | Acceleration factor (Use 1 if uncertain, higher values increase speed and reduce compression)
Int ->
-- | Bytes to compress
Bytes ->
ByteArray
{-# inline compressU #-}
compressU = compressGeneralU 0
compressGeneralU ::
-- Is HC (0 means fast, anything else means HC)
Word32 ->
-- Compression level (meanings vary for HC and fast)
Int ->
-- Bytes to compress
Bytes ->
ByteArray
{-# noinline compressGeneralU #-}
compressGeneralU !isHc !lvl (Bytes (ByteArray arr) off0 len0) = runST do
let maxSz = requiredBufferSize len0 + 15
dst@(MutableByteArray dst#) <- PM.newByteArray maxSz
-- -- First 4 bytes: magic identifier
PM.writeByteArray dst 0 (0x04 :: Word8)
PM.writeByteArray dst 1 (0x22 :: Word8)
PM.writeByteArray dst 2 (0x4D :: Word8)
PM.writeByteArray dst 3 (0x18 :: Word8)
-- Next 3 bytes: frame descriptor
PM.writeByteArray dst 4 (0b0110_0000 :: Word8)
if
| len0 <= 65_536 -> do
PM.writeByteArray dst 5 (0b0100_0000 :: Word8)
PM.writeByteArray dst 6 (0x82 :: Word8)
| len0 <= 262_144 -> do
PM.writeByteArray dst 5 (0b0101_0000 :: Word8)
PM.writeByteArray dst 6 (0xFB :: Word8)
| len0 <= 1_048_576 -> do
PM.writeByteArray dst 5 (0b0110_0000 :: Word8)
PM.writeByteArray dst 6 (0x51 :: Word8)
| otherwise -> do
PM.writeByteArray dst 5 (0b0111_0000 :: Word8)
PM.writeByteArray dst 6 (0x73 :: Word8)
let handleBlocks !ix !inputOff !inputRemaining = do
let !inputClipped = min inputRemaining 4_194_304
actualSz <- case isHc of
0 -> unsafeIOToST (c_hs_compress_fast arr inputOff dst# (ix + 4) inputClipped maxSz lvl)
_ -> unsafeIOToST (c_hs_compress_HC arr inputOff dst# (ix + 4) inputClipped maxSz lvl)
case actualSz of
0 -> errorWithoutStackTrace "Lz4.Frame.compressHighlyU: compression failed"
_ -> do
LE.writeUnalignedByteArray dst ix (fromIntegral actualSz :: Int32)
let inputRemaining' = inputRemaining - inputClipped
let inputOff' = inputOff + inputClipped
if inputRemaining' > 0
then do
handleBlocks (ix + 4 + actualSz) inputOff' inputRemaining'
else do
LE.writeUnalignedByteArray dst (actualSz + ix + 4) (0 :: Word32)
PM.shrinkMutableByteArray dst (actualSz + ix + 8)
PM.unsafeFreezeByteArray dst
handleBlocks 7 off0 len0
indexWord8 :: ByteArray -> Int -> Word8
{-# inline indexWord8 #-}
indexWord8 = PM.indexByteArray