packages feed

snappy-hs-0.1.1.0: src/Snappy.hs

{-# LANGUAGE BangPatterns      #-}
{-# LANGUAGE MagicHash         #-}
{-# LANGUAGE Strict            #-}
{-# LANGUAGE UnboxedTuples     #-}
{-# LANGUAGE UnliftedNewtypes   #-}

module Snappy (
    compress,
    decompress,
    DecodeError (..),
) where

import Control.Exception (Exception)
import Data.Bits (unsafeShiftL, unsafeShiftR, (.&.), (.|.))
import Data.ByteString.Internal (ByteString (..))
import Data.Word (Word64)
import Foreign.ForeignPtr (mallocForeignPtrBytes, withForeignPtr)
import Foreign.Marshal.Utils (copyBytes, fillBytes)
import Foreign.Ptr (Ptr)
import Foreign.Storable (sizeOf)
import GHC.Exts
    ( Addr#
    , Int (..)
    , MutableByteArray#
    , Ptr (..)
    , RealWorld
    , (+#)
    , eqWord8#
    , indexWord8OffAddr#
    , isTrue#
    , newByteArray#
    , or#
    , plusAddr#
    , readIntArray#
    , readWord8OffAddr#
    , setByteArray#
    , uncheckedShiftL#
    , word8ToWord#
    , wordToWord32#
    , writeIntArray#
    , writeWord8OffAddr#
    )
import GHC.IO (IO (..))
import GHC.Word (Word32 (W32#), Word8 (W8#))
import System.IO.Unsafe (unsafeDupablePerformIO)


decompress :: ByteString -> Either DecodeError ByteString
decompress (BS srcFp srcLen)
    | srcLen == 0 = Left EmptyInput
    | otherwise = unsafeDupablePerformIO $
        withForeignPtr srcFp $ \(Ptr srcAddr#) -> do
            vr <- parseVarint srcAddr# srcLen
            case vr of
                Left e -> pure (Left e)
                Right (outLen, hdrLen) -> do
                    outFp <- mallocForeignPtrBytes (max 1 outLen)
                    withForeignPtr outFp $ \(Ptr outAddr#) -> do
                        r <- decodeLoop srcAddr# hdrLen srcLen outAddr# 0 outLen
                        case r of
                            Left e  -> pure (Left e)
                            Right _ -> pure (Right (BS outFp outLen))

compress :: ByteString -> ByteString
compress (BS srcFp srcLen) = unsafeDupablePerformIO $
    withForeignPtr srcFp $ \(Ptr srcAddr#) -> do
        let maxOut = 10 + srcLen + (srcLen `div` 6) + 64
        outFp <- mallocForeignPtrBytes maxOut
        withForeignPtr outFp $ \(Ptr outAddr#) -> do
            hdrLen <- writeVarint outAddr# (fromIntegral srcLen :: Word64)
            written <-
                if srcLen < 4
                    then writeLiteral srcAddr# 0 srcLen outAddr# hdrLen
                    else do
                        let !tableSize  = 1 `unsafeShiftL` 15
                            !tableBytes = tableSize * sizeOfInt
                        tbl <- newZeroedByteArray tableBytes
                        compressLoop srcAddr# srcLen tbl outAddr# hdrLen 0 0
            pure (BS outFp written)


{-# INLINE parseVarint #-}
parseVarint :: Addr# -> Int -> IO (Either DecodeError (Int, Int))
parseVarint !addr# !srcLen = go 0 0 0
  where
    go :: Word64 -> Int -> Int -> IO (Either DecodeError (Int, Int))
    go !acc !s !n
        | n > 10    = pure (Left VarIntTooLong)
        | s > 63    = pure (Left Overflow)
        | n >= srcLen = pure (Left UnexpectedEOF)
        | otherwise =
            let b = fromIntegral (ixByte addr# n) :: Word64
            in if b < 0x80
                then pure (Right (fromIntegral (acc .|. (b `unsafeShiftL` s)), n + 1))
                else go
                    (acc .|. ((b .&. 0x7f) `unsafeShiftL` s))
                    (s + 7)
                    (n + 1)

{-# INLINE writeVarint #-}
writeVarint :: Addr# -> Word64 -> IO Int
writeVarint !addr# = go 0
  where
    go !off !x
        | x < 0x80 = do
            writeW8 addr# off (fromIntegral x :: Word8)
            pure (off + 1)
        | otherwise = do
            writeW8 addr# off (fromIntegral (0x80 .|. (x .&. 0x7f)) :: Word8)
            go (off + 1) (x `unsafeShiftR` 7)

decodeLoop
    :: Addr# -> Int -> Int   -- src base, current offset, end offset
    -> Addr# -> Int -> Int   -- dst base, current offset, capacity
    -> IO (Either DecodeError Int)
decodeLoop !srcAddr# !srcOff0 !srcEnd !outAddr# = go srcOff0
  where
    go :: Int -> Int -> Int -> IO (Either DecodeError Int)
    go !srcOff !outOff !outLen
        | srcOff >= srcEnd = pure (Right outOff)
        | otherwise = do
            let !tag    = ixByte srcAddr# srcOff
                !srcOff1 = srcOff + 1
            case tag .&. 0x03 of
                0 -> do
                    let !hdr = fromIntegral (tag `unsafeShiftR` 2) :: Int
                    if hdr < 60
                        then do
                            let !litLen = hdr + 1
                            if srcOff1 + litLen > srcEnd || outOff + litLen > outLen
                                then pure (Left UnexpectedEOF)
                                else do
                                    copyBytes (atOff outAddr# outOff) (atOff srcAddr# srcOff1) litLen
                                    go (srcOff1 + litLen) (outOff + litLen) outLen
                        else do
                            let !nb = hdr - 59
                            if nb < 1 || nb > 4
                                then pure (Left UnsupportedLiteralLength)
                                else if srcOff1 + nb > srcEnd
                                    then pure (Left UnexpectedEOF)
                                    else do
                                        lm1 <- readLEn srcAddr# srcOff1 nb
                                        let !litLen  = lm1 + 1
                                            !srcOff2 = srcOff1 + nb
                                        if srcOff2 + litLen > srcEnd || outOff + litLen > outLen
                                            then pure (Left UnexpectedEOF)
                                            else do
                                                copyBytes (atOff outAddr# outOff) (atOff srcAddr# srcOff2) litLen
                                                go (srcOff2 + litLen) (outOff + litLen) outLen
                1 -> do
                    if srcOff1 >= srcEnd
                        then pure (Left UnexpectedEOF)
                        else do
                            let !lo    = fromIntegral (ixByte srcAddr# srcOff1) :: Int
                                !cpLen = (fromIntegral ((tag `unsafeShiftR` 2) .&. 0x07) :: Int) + 4
                                !offHi = fromIntegral (tag `unsafeShiftR` 5) :: Int
                                !offset = (offHi `unsafeShiftL` 8) .|. lo
                            if offset <= 0 || offset > outOff
                                then pure (Left InvalidOffset)
                                else do
                                    copyOverlap outAddr# outOff offset cpLen
                                    go (srcOff1 + 1) (outOff + cpLen) outLen
                2 -> do
                    if srcOff1 + 2 > srcEnd
                        then pure (Left UnexpectedEOF)
                        else do
                            off <- readLEn srcAddr# srcOff1 2
                            let !cpLen = (fromIntegral ((tag `unsafeShiftR` 2) .&. 0x3f) :: Int) + 1
                            if off <= 0 || off > outOff
                                then pure (Left InvalidOffset)
                                else do
                                    copyOverlap outAddr# outOff off cpLen
                                    go (srcOff1 + 2) (outOff + cpLen) outLen
                3 -> do
                    if srcOff1 + 4 > srcEnd
                        then pure (Left UnexpectedEOF)
                        else do
                            off <- readLEn srcAddr# srcOff1 4
                            let !cpLen = (fromIntegral ((tag `unsafeShiftR` 2) .&. 0x3f) :: Int) + 1
                            if off <= 0 || off > outOff
                                then pure (Left InvalidOffset)
                                else do
                                    copyOverlap outAddr# outOff off cpLen
                                    go (srcOff1 + 4) (outOff + cpLen) outLen
                _ -> pure (Left InvalidTag)

compressLoop
    :: Addr# -> Int -> HashTable
    -> Addr# -> Int -> Int -> Int
    -> IO Int
compressLoop !src !srcLen !tbl !dst !dstOff0 = go dstOff0
  where
    go :: Int -> Int -> Int -> IO Int
    go !dstOff !i !litStart
        | i + 3 >= srcLen = do
            let !remLen = srcLen - litStart
            if remLen > 0
                then writeLiteral src litStart remLen dst dstOff
                else pure dstOff
        | otherwise = do
            let !w  = readU32LE src i
                !h  = hash32 w
            prev0 <- readTbl tbl h
            writeTbl tbl h (i + 1)
            let !prev = prev0 - 1
            if prev < 0
                then go dstOff (i + 1) litStart
                else do
                    let !pw = readU32LE src prev
                    if pw /= w
                        then go dstOff (i + 1) litStart
                        else do
                            dstOff' <-
                                if i > litStart
                                    then writeLiteral src litStart (i - litStart) dst dstOff
                                    else pure dstOff
                            let !extra    = extendMatch src (i + 4) (prev + 4) srcLen
                                !matchLen = 4 + extra
                                !offset   = i - prev
                            dstOff'' <- writeCopies offset matchLen dst dstOff'
                            let !i' = i + matchLen
                            go dstOff'' i' i'

-- | Extend a match as far as possible; pure since source is immutable.
extendMatch :: Addr# -> Int -> Int -> Int -> Int
extendMatch addr# a0 b0 limit = go 0
  where
    go :: Int -> Int
    go !n
        | a0 + n >= limit || b0 + n >= limit = n
        | otherwise =
            let !(I# an#) = a0 + n
                !(I# bn#) = b0 + n
            in if isTrue# (eqWord8#
                    (indexWord8OffAddr# addr# an#)
                    (indexWord8OffAddr# addr# bn#))
               then go (n + 1)
               else n
{-# INLINE extendMatch #-}


{-# INLINE readLEn #-}
readLEn :: Addr# -> Int -> Int -> IO Int
readLEn !addr# !off !n
    | n == 1 = pure $!
        fromIntegral (ixByte addr# off)
    | n == 2 = pure $!
           fromIntegral (ixByte addr# off)
        .|. (fromIntegral (ixByte addr# (off + 1)) `unsafeShiftL` 8)
    | n == 3 = pure $!
           fromIntegral (ixByte addr# off)
        .|. (fromIntegral (ixByte addr# (off + 1)) `unsafeShiftL` 8)
        .|. (fromIntegral (ixByte addr# (off + 2)) `unsafeShiftL` 16)
    | n == 4 = pure $!
           fromIntegral (ixByte addr# off)
        .|. (fromIntegral (ixByte addr# (off + 1)) `unsafeShiftL` 8)
        .|. (fromIntegral (ixByte addr# (off + 2)) `unsafeShiftL` 16)
        .|. (fromIntegral (ixByte addr# (off + 3)) `unsafeShiftL` 24)
    | otherwise = pure $! go 0 0
  where
    go !acc !k
        | k >= n    = acc
        | otherwise =
            go (acc .|. (fromIntegral (ixByte addr# (off + k)) `unsafeShiftL` (k * 8))) (k + 1)

-- | Read a 32-bit little-endian word; pure since source is immutable.
{-# INLINE readU32LE #-}
readU32LE :: Addr# -> Int -> Word32
readU32LE addr# (I# i#) =
    let b0 = word8ToWord# (indexWord8OffAddr# addr# i#)
        b1 = word8ToWord# (indexWord8OffAddr# addr# (i# +# 1#))
        b2 = word8ToWord# (indexWord8OffAddr# addr# (i# +# 2#))
        b3 = word8ToWord# (indexWord8OffAddr# addr# (i# +# 3#))
        w  = b0 `or#` (b1 `uncheckedShiftL#` 8#)
                `or#` (b2 `uncheckedShiftL#` 16#)
                `or#` (b3 `uncheckedShiftL#` 24#)
    in W32# (wordToWord32# w)

{-# INLINE hash32 #-}
hash32 :: Word32 -> Int
hash32 !w =
    let !kMul = 0x1e35a7bd :: Word32
     in fromIntegral ((w * kMul) `unsafeShiftR` (32 - 15))

writeLiteral :: Addr# -> Int -> Int -> Addr# -> Int -> IO Int
writeLiteral _ _ 0 _ !dstOff = pure dstOff
writeLiteral !src !off !len !dst !dstOff
    | len <= 60 = do
        let !tag = fromIntegral (((len - 1) `unsafeShiftL` 2) .|. 0x00) :: Word8
        writeW8 dst dstOff tag
        copyBytes (atOff dst (dstOff + 1)) (atOff src off) len
        pure (dstOff + 1 + len)
    | otherwise = do
        let !l1  = len - 1
            !nb  = bytesFor l1
            !tag = fromIntegral (((59 + nb) `unsafeShiftL` 2) .|. 0x00) :: Word8
        writeW8 dst dstOff tag
        writeLEn dst (dstOff + 1) nb (fromIntegral l1 :: Word32)
        copyBytes (atOff dst (dstOff + 1 + nb)) (atOff src off) len
        pure (dstOff + 1 + nb + len)

writeCopies :: Int -> Int -> Addr# -> Int -> IO Int
writeCopies !offset = go
  where
    go :: Int -> Addr# -> Int -> IO Int
    go !remLen !dst !dstOff
        | remLen <= 0 = pure dstOff
        | offset <= 0x7FF && remLen >= 4 = do
            let !c = min 11 remLen
            dstOff' <- writeCopy1 c offset dst dstOff
            go (remLen - c) dst dstOff'
        | offset <= 0xFFFF = do
            let !c = min 64 remLen
            dstOff' <- writeCopy2 c offset dst dstOff
            go (remLen - c) dst dstOff'
        | otherwise = do
            let !c = min 64 remLen
            dstOff' <- writeCopy4 c offset dst dstOff
            go (remLen - c) dst dstOff'
{-# INLINE writeCopies #-}

{-# INLINE writeCopy1 #-}
writeCopy1 :: Int -> Int -> Addr# -> Int -> IO Int
writeCopy1 !len !off !dst !dstOff = do
    let !lenField = (len - 4) .&. 0x7
        !offHi    = (off `unsafeShiftR` 8) .&. 0x7
        !tag      = fromIntegral ((lenField `unsafeShiftL` 2) .|. (offHi `unsafeShiftL` 5) .|. 0x01) :: Word8
        !lo       = fromIntegral (off .&. 0xFF) :: Word8
    writeW8 dst dstOff tag
    writeW8 dst (dstOff + 1) lo
    pure (dstOff + 2)

{-# INLINE writeCopy2 #-}
writeCopy2 :: Int -> Int -> Addr# -> Int -> IO Int
writeCopy2 !len !off !dst !dstOff = do
    let !tag = fromIntegral (((len - 1) `unsafeShiftL` 2) .|. 0x02) :: Word8
    writeW8 dst dstOff tag
    writeLEn dst (dstOff + 1) 2 (fromIntegral off :: Word32)
    pure (dstOff + 3)

{-# INLINE writeCopy4 #-}
writeCopy4 :: Int -> Int -> Addr# -> Int -> IO Int
writeCopy4 !len !off !dst !dstOff = do
    let !tag = fromIntegral (((len - 1) `unsafeShiftL` 2) .|. 0x03) :: Word8
    writeW8 dst dstOff tag
    writeLEn dst (dstOff + 1) 4 (fromIntegral off :: Word32)
    pure (dstOff + 5)

{-# INLINE writeLEn #-}
writeLEn :: Addr# -> Int -> Int -> Word32 -> IO ()
writeLEn !dst !off !n !v = go 0
  where
    go !k
        | k >= n    = pure ()
        | otherwise = do
            writeW8 dst (off + k) (fromIntegral ((v `unsafeShiftR` (8 * k)) .&. 0xFF) :: Word8)
            go (k + 1)

{-# INLINE bytesFor #-}
bytesFor :: Int -> Int
bytesFor x
    | x < 0x100     = 1
    | x < 0x10000   = 2
    | x < 0x1000000 = 3
    | otherwise      = 4

{-# INLINE copyOverlap #-}
copyOverlap :: Addr# -> Int -> Int -> Int -> IO ()
copyOverlap !base !outOff !offset !len
    | offset >= len =
        copyBytes (atOff base outOff) (atOff base srcStart) len
    | offset == 1 = do
        b <- readW8 base srcStart
        fillBytes (atOff base outOff) b len
    | otherwise = go 0
  where
    !srcStart = outOff - offset
    go !i
        | i >= len  = pure ()
        | otherwise = do
            b <- readW8 base (srcStart + (i `rem` offset))
            writeW8 base (outOff + i) b
            go (i + 1)

sizeOfInt :: Int
sizeOfInt = sizeOf (0 :: Int)
{-# INLINE sizeOfInt #-}

-- | Lifted wrapper around an unlifted 'MutableByteArray#'.
data HashTable = HashTable (MutableByteArray# RealWorld)

data DecodeError
    = EmptyInput
    | Overflow
    | VarIntTooLong
    | UnexpectedEOF
    | UnsupportedLiteralLength
    | InvalidOffset
    | InvalidTag
    deriving (Show, Eq)

instance Exception DecodeError

-- | Allocate a GC-managed byte array, zero-initialised.
newZeroedByteArray :: Int -> IO HashTable
newZeroedByteArray (I# n#) = IO $ \s0 ->
    case newByteArray# n# s0 of
        (# s1, arr #) -> case setByteArray# arr 0# n# 0# s1 of
            s2 -> (# s2, HashTable arr #)
{-# INLINE newZeroedByteArray #-}

-- | Read the Int at slot index @i@ (each slot is one machine Int).
readTbl :: HashTable -> Int -> IO Int
readTbl (HashTable arr) (I# i#) = IO $ \s ->
    case readIntArray# arr i# s of
        (# s', v# #) -> (# s', I# v# #)
{-# INLINE readTbl #-}

-- | Write an Int to slot index @i@.
writeTbl :: HashTable -> Int -> Int -> IO ()
writeTbl (HashTable arr) (I# i#) (I# v#) = IO $ \s ->
    case writeIntArray# arr i# v# s of
        s' -> (# s', () #)
{-# INLINE writeTbl #-}

-- | Pure read: one byte from an immutable foreign address.
ixByte :: Addr# -> Int -> Word8
ixByte addr# (I# i#) = W8# (indexWord8OffAddr# addr# i#)
{-# INLINE ixByte #-}

-- | Mutable read: one byte from a (possibly mutable) address.
readW8 :: Addr# -> Int -> IO Word8
readW8 addr# (I# i#) = IO $ \s ->
    case readWord8OffAddr# addr# i# s of
        (# s', b# #) -> (# s', W8# b# #)
{-# INLINE readW8 #-}

-- | Write one byte to an address.
writeW8 :: Addr# -> Int -> Word8 -> IO ()
writeW8 addr# (I# i#) (W8# b#) = IO $ \s ->
    case writeWord8OffAddr# addr# i# b# s of
        s' -> (# s', () #)
{-# INLINE writeW8 #-}

-- | Construct a @Ptr@ from a base @Addr#@ plus an @Int@ byte offset.
atOff :: Addr# -> Int -> Ptr a
atOff addr# (I# i#) = Ptr (addr# `plusAddr#` i#)
{-# INLINE atOff #-}