packages feed

compact-socket-0.1.0.0: src/Data/Compact/Socket.hs

{-# LANGUAGE CPP, MultiWayIf, OverloadedStrings #-}

-- | A not-very-smart default implementation of transmitting
-- compact regions over sockets.
module Data.Compact.Socket where

import           Control.Applicative
import           Control.Arrow                  (first)
import           Control.DeepSeq                (NFData)
import           Control.Monad
import           Data.Bits
import qualified Data.ByteString                as B
import qualified Data.ByteString.Builder        as BB
import qualified Data.ByteString.Lazy           as LB
import qualified Data.ByteString.Unsafe         as B
import           Data.Char                      (isSpace)
import           Data.Compact                   as C
import           Data.IORef
import           Data.Maybe                     (fromMaybe)
import           Data.Monoid
import           Data.Word
import           Foreign.C.Types
import           Foreign.Marshal.Utils          (copyBytes)
import           Foreign.Ptr                    (Ptr, castPtr, plusPtr,
                                                 ptrToWordPtr, wordPtrToPtr)
import           Network.Socket.ByteString      as B
import qualified Network.Socket.ByteString.Lazy as LB
import           Network.Socket.Internal
import           Network.Socket
import           System.Directory               (doesFileExist,
                                                 getTemporaryDirectory)
import           System.FilePath                (splitExtension, (</>))
import           System.IO                      (hGetContents)
import           System.IO.Unsafe               (unsafeInterleaveIO)

--------------------------------------------------------------------
-- * Sending/receiving length prefixed ByteStrings(lazy and strict).

-- | Call @send@ with a length-prefixed version of @bs@.
sendLenPfx :: (LB.ByteString -> IO ()) -> LB.ByteString -> IO ()
sendLenPfx send bs =
    send $ BB.toLazyByteString $
      BB.word64BE (fromIntegral $ LB.length bs) <> BB.lazyByteString bs

-- | Send a length-prefixed bytestring over a 'Socket'.
sendLenPfxSock :: Socket -> LB.ByteString -> IO ()
sendLenPfxSock = sendLenPfx . LB.sendAll

-- | Try to read length prefixed message. Second return value is extra data
-- read after reading length prefixed message.
recvLenPfx :: IO LB.ByteString -> IO (LB.ByteString, LB.ByteString)
recvLenPfx recv = pfxLoop ""
  where
    pfxLoop :: LB.ByteString -> IO (LB.ByteString, LB.ByteString)
    pfxLoop bs
      | LB.length bs >= 8 = do
          let ([w1,w2,w3,w4,w5,w6,w7,w8], rest) = first LB.unpack $ LB.splitAt 8 bs
              pfx = mkWord64 w1 w2 w3 w4 w5 w6 w7 w8
          loop pfx rest
      | otherwise = do
          bs' <- recv
          if LB.null bs' then return ("", "") else pfxLoop bs'

    loop :: Word64 -> LB.ByteString -> IO (LB.ByteString, LB.ByteString)
    loop pfx bs
      | pfx == fromIntegral (LB.length bs) = return (bs, "")
      | pfx <  fromIntegral (LB.length bs) = do
          let (msg, extra) = LB.splitAt (fromIntegral pfx) bs
          return (msg, extra)
      | otherwise = do
          bytes <- recv
          if LB.null bytes
            then error "Socket is closed before reading the whole message"
            else loop pfx (bs <> bytes)

recvLenPfxSock :: Socket -> IO (LB.ByteString, LB.ByteString)
recvLenPfxSock = recvLenPfx . flip LB.recv 8192

-- | 'recv' messages until the socket is closed. Returns lazy 'ByteString'.
recvAll :: Socket -> IO LB.ByteString
recvAll sock = do
    bs <- LB.recv sock 8192
    if LB.null bs then return bs else (bs <>) <$> recvAll sock

recvAllInterleave :: Socket -> IO LB.ByteString
recvAllInterleave sock = do
  bs <- LB.recv sock 8192
  if LB.null bs then return bs else (bs <>) <$> unsafeInterleaveIO (recvAllInterleave sock)

mkWord32 :: Word8 -> Word8 -> Word8 -> Word8 -> Word32
mkWord32 w1 w2 w3 w4 =
    fromIntegral w1 `shiftL` 24
  + fromIntegral w2 `shiftL` 16
  + fromIntegral w3 `shiftL` 8
  + fromIntegral w4
{-# INLINE mkWord32 #-}

mkWord64 :: Word8 -> Word8 -> Word8 -> Word8 -> Word8 -> Word8 -> Word8 -> Word8 -> Word64
mkWord64 w1 w2 w3 w4 w5 w6 w7 w8 =
    fromIntegral (mkWord32 w1 w2 w3 w4) `shiftL` 32
  + fromIntegral (mkWord32 w5 w6 w7 w8)
{-# INLINE mkWord64 #-}

----------------------------------------
-- * Compact <-> ByteString convresions.

compactToBS :: NFData a => C.Compact a -> IO LB.ByteString
compactToBS c = C.withCompactPtrs c $ \(C.SerializedCompact blocks root) -> do
    bss <- forM blocks $ \(ptr, len) -> B.unsafePackCStringLen (castPtr ptr, fromIntegral len)
    return . BB.toLazyByteString . mconcat $
      -- root pointer
      BB.int64BE (fromIntegral $ ptrToWordPtr root)
      -- (ptr, length, bytes) triplets
      : zipWith mkPair blocks bss
  where
    mkPair :: (Ptr a, Word) -> B.ByteString -> BB.Builder
    mkPair (ptr, len) bs =
         BB.word64BE (fromIntegral $ ptrToWordPtr ptr)
      <> BB.word64BE (fromIntegral len)
      <> BB.byteString bs

bsToCompact :: B.ByteString -> IO (C.Compact a)
bsToCompact bs = do
    let (rootPtr, r) = first (wordPtrToPtr . fromIntegral . mkW64 . B.unpack) $ B.splitAt 8 bs
        blocks       = parseBlocks r
        pws          = map (\(p, w, _) -> (p, w)) blocks
        bss          = map (\(_, _, b) -> b) blocks
    fromMaybe (error "Can't generate Compact from bytes.") <$>
#ifdef CNF_DONT_USE_TRUSTED
#warning "compiling with compactImportByteStrings"
      C.compactImportByteStrings        (C.SerializedCompact pws rootPtr) bss
#else
#warning "compiling with compactImportByteStringsTrusted"
      C.compactImportByteStringsTrusted (C.SerializedCompact pws rootPtr) bss
#endif
  where
    parseBlocks :: B.ByteString -> [(Ptr a, Word, B.ByteString)]
    parseBlocks ""  = []
    parseBlocks bbs =
      let (ptr,   r  ) = first (wordPtrToPtr . fromIntegral . mkW64 . B.unpack) $ B.splitAt 8 bbs
          (len,   r' ) = first (fromIntegral . mkW64 . B.unpack) $ B.splitAt 8 r
          (bytes, r'') = B.splitAt (fromIntegral len) r'
          rest         = parseBlocks r''
      in (ptr, len, bytes) : rest

--------------------------------------------------------------------
-- * Utilities for sending and receiving Compacts without converting
-- Compact blocks to ByteStrings first.

-- | Send a compact region over the network via 'Socket'.
sendCompact :: NFData a => Socket -> Compact a -> IO ()
sendCompact sock@(MkSocket s _ _ _ _) c =
  C.withCompactPtrs c $ \(C.SerializedCompact blocks root) -> do
    -- we first send root and [(ptr, len)] using standard method
    sendLenPfxSock sock $ mkInfoMsg root blocks
    -- we then send blocks, skipping `network` API and ByteString
    -- conversions.
    forM_ blocks $ \(ptr, len) -> send s (castPtr ptr) (fromIntegral len) 0
  where
    send :: CInt -> Ptr a -> CSize -> CInt -> IO CInt
    send s ptr len mode = do
      sent <- throwSocketErrorWaitWrite sock "sendCompact" $ c_send s ptr len mode
      if fromIntegral sent < len
        then do
          sent' <- send s (ptr `plusPtr` fromIntegral sent) (len - fromIntegral sent) mode
          return (sent + sent')
        else return sent

    mkInfoMsg :: Ptr a -> [(Ptr b, Word)] -> LB.ByteString
    mkInfoMsg root blocks = BB.toLazyByteString . mconcat $
      -- root pointer
      BB.int64BE (fromIntegral $ ptrToWordPtr root)
      -- (ptr, length) pairs
      : map mkPair blocks

    mkPair :: (Ptr a, Word) -> BB.Builder
    mkPair (ptr, len) =
      BB.word64BE (fromIntegral $ ptrToWordPtr ptr) <> BB.word64BE (fromIntegral len)

-- | Receive a compact region being sent on 'Socket'.
recvCompact :: Socket -> IO (Compact a)
recvCompact sock = do
    -- receive info msg first
    (sc, extra) <- recvInfoMsg
    extraRef <- newIORef (LB.toStrict extra)
    fromMaybe (error "Can't generate Compact from bytes.") <$>
      C.compactImportTrusted sc (\ptr size ->
        recv' extraRef (fromIntegral size) (castPtr ptr))
  where
    recvInfoMsg :: IO (C.SerializedCompact a, LB.ByteString)
    recvInfoMsg = do
      (bytes', extra) <- recvLenPfxSock sock
      let bytes = LB.toStrict bytes'
      let (ptr, r) = first (wordPtrToPtr . fromIntegral . mkW64 . B.unpack) $ B.splitAt 8 bytes
          rest     = parseBlocks r
      return (C.SerializedCompact rest ptr, extra)

    recv' :: IORef B.ByteString -> Int -> Ptr Word8 -> IO ()
    recv' _ 0 _ = return ()
    recv' extra size ptr = do
      extra' <- readIORef extra
      if | B.null extra' -> do
             recvd <- recvInner sock size ptr
             when (recvd == 0) $ error "socket closed"
             unless (recvd == size) $
               recv' extra (size - recvd) (ptr `plusPtr` recvd)
         | B.length extra' > fromIntegral size -> do
             let (b, rest) = B.splitAt (fromIntegral size) extra'
             writeIORef extra rest
             writeBS b ptr
             return ()
         | otherwise -> do
             writeBS extra' ptr
             writeIORef extra ""
             recv' extra (size - fromIntegral (B.length extra'))
                         (ptr `plusPtr` fromIntegral (B.length extra'))

    writeBS :: B.ByteString -> Ptr Word8 -> IO ()
    writeBS bs ptr = B.unsafeUseAsCStringLen bs $ \(cPtr, len) ->
      copyBytes ptr (castPtr cPtr) len
    {-# INLINE writeBS #-}

    parseBlocks :: B.ByteString -> [(Ptr a, Word)]
    parseBlocks ""  = []
    parseBlocks bbs =
      let (ptr, r ) = first (wordPtrToPtr . fromIntegral . mkW64 . B.unpack) $ B.splitAt 8 bbs
          (len, r') = first (fromIntegral . mkW64 . B.unpack) $ B.splitAt 8 r
          rest      = parseBlocks r'
      in (ptr, len) : rest

compactLen :: NFData a => Compact a -> IO Word
compactLen c =
    C.withCompactPtrs c $  \(SerializedCompact blocks _) -> return $ sum (map snd blocks)

---------
-- * Misc

mkW64 :: [Word8] -> Word64
mkW64 [w1, w2, w3, w4, w5, w6, w7, w8] =
    fromIntegral w1 `shiftL` 56
  + fromIntegral w2 `shiftL` 48
  + fromIntegral w3 `shiftL` 40
  + fromIntegral w4 `shiftL` 32
  + fromIntegral w5 `shiftL` 24
  + fromIntegral w6 `shiftL` 16
  + fromIntegral w7 `shiftL` 8
  + fromIntegral w8
mkW64 l = error $ "mkW64 is called with " ++ show (length l) ++ " elements."
{-# INLINE mkW64 #-}

foreign import ccall unsafe "send"
  c_send :: CInt -> Ptr a -> CSize -> CInt -> IO CInt

foreign import ccall unsafe "recv"
  c_recv :: CInt -> Ptr CChar -> CSize -> CInt -> IO CInt

recvInner :: Socket -> Int -> Ptr Word8 -> IO Int
recvInner sock@(MkSocket s _ _ _ _) nbytes ptr =
    fmap fromIntegral $
        throwSocketErrorWaitRead sock "recv" $
            c_recv s (castPtr ptr) (fromIntegral nbytes) 0