packages feed

mariadb-haskell-0.1.0.0: src/Database/MySQL/Protocol/Packet.hs

{-# OPTIONS_GHC -funbox-strict-fields #-}

{-|
Module      : Database.MySQL.Protocol.Packet
Description : MariaDB packet framing, replies and limits
Copyright   : (c) Winterland, 2016
License     : BSD
Maintainer  : Anton Antich <anton.antich@gmail.com>
Stability   : experimental
Portability : PORTABLE

MySQL packet decoder&encoder, and varities utility.

-}

module Database.MySQL.Protocol.Packet where

import           Control.Exception     (Exception (..), throwIO)
import           Data.Binary.Parser
import           Data.Binary.Put
import           Data.Binary           (Binary(..), encode)
import           Data.Bits
import qualified Data.ByteString       as B
import           Data.ByteString.Char8 as BC
import qualified Data.ByteString.Lazy  as L
import           Data.Int.Int24
import           Data.Int
import           Data.Word
import           Data.Typeable
import           Data.Word.Word24

--------------------------------------------------------------------------------
-- | MySQL packet type
--
data Packet = Packet
    { pLen  :: !Int64
    , pSeqN :: !Word8
    , pBody :: !L.ByteString
    } deriving (Show, Eq)

putPacket :: Packet -> Put
putPacket (Packet len seqN body)  = do
    putWord24le (fromIntegral len)
    putWord8 seqN
    putLazyByteString body
{-# INLINE putPacket #-}

getPacket :: Get Packet
getPacket = do
    len <- fromIntegral <$> getWord24le
    seqN <- getWord8
    body <- getLazyByteString len
    return (Packet len seqN body)
{-# INLINE getPacket #-}

instance Binary Packet where
    put = putPacket
    {-# INLINE put #-}
    get = getPacket
    {-# INLINE get #-}

isERR :: Packet -> Bool
isERR p = not (L.null (pBody p)) && L.head (pBody p) == 0xFF
{-# INLINE isERR #-}

isOK :: Packet -> Bool
isOK p = not (L.null (pBody p)) && L.head (pBody p) == 0x00
{-# INLINE isOK #-}

-- | Recognize a short legacy EOF. The 0xfe marker also starts an eight-byte
-- length-encoded row value, so checking its first byte alone would truncate
-- large rows. Negotiated OK terminators are handled by the connection layer.
isEOF :: Packet -> Bool
isEOF p = pLen p > 0 && pLen p < 9 && not (L.null (pBody p)) && L.head (pBody p) == 0xFE
{-# INLINE isEOF #-}

-- | Is this an AuthMoreData packet? (first byte 0x01)
-- Used during authentication handshake for caching_sha2_password.
isAuthMoreData :: Packet -> Bool
isAuthMoreData p = not (L.null (pBody p)) && L.head (pBody p) == 0x01
{-# INLINE isAuthMoreData #-}

-- | Is this an AuthSwitchRequest packet? (first byte 0xFE)
-- Same marker as EOF but used in authentication context.
isAuthSwitch :: Packet -> Bool
isAuthSwitch p = not (L.null (pBody p)) && L.head (pBody p) == 0xFE
{-# INLINE isAuthSwitch #-}

-- | Is there more packet to be read?
--
--  https://dev.mysql.com/doc/internals/en/status-flags.html
isThereMore :: OK -> Bool
isThereMore p  = okStatus p .&. 0x08 /= 0
{-# INLINE isThereMore #-}

-- | Decode a Binary payload in IO, permitting a separately parsed trailer.
-- Fixed row/metadata structures should use 'getFromPacketExact' instead.
decodeFromPacket :: Binary a => Packet -> IO a
decodeFromPacket = getFromPacket get
{-# INLINE decodeFromPacket #-}

-- | Decode with a supplied parser, permitting unconsumed bytes and forcing the
-- result to WHNF. Row readers additionally force their values under ownership;
-- WHNF alone does not force every element of a returned list.
getFromPacket :: Get a -> Packet -> IO a
getFromPacket g (Packet _ _ body) = case parseDetailLazy g body of
    Left  (buf, offset, errmsg) -> throwIO (DecodePacketFailed buf offset errmsg)
    Right (_,   _,      r     ) -> return $! r
{-# INLINE getFromPacket #-}

-- | A binary parser rejected a packet, with its buffered bytes, consumed offset
-- and parser message. Live command/stream guards retire an incomplete exchange;
-- low-level callers decide how to handle independent packet data.
data DecodePacketException = DecodePacketFailed ByteString ByteOffset String
  deriving (Typeable, Show)
instance Exception DecodePacketException

encodeToPacket :: Binary a => Word8 -> a -> Packet
encodeToPacket seqN payload =
    let s = encode payload
        l = L.length s
    in Packet l seqN s
{-# INLINE encodeToPacket #-}

putToPacket :: Word8 -> Put -> Packet
putToPacket seqN payload =
    let s = runPut payload
        l = L.length s
    in Packet l seqN s
{-# INLINE putToPacket #-}

--------------------------------------------------------------------------------
-- OK, ERR, EOF

-- | You may get interested in t'OK' packet because it provides information about
-- successful operations.
--
data OK = OK
    { okAffectedRows :: !Word64      -- ^ affected row number
    , okLastInsertID :: !Word64      -- ^ last insert's ID
    , okStatus       :: !Word16
    , okWarningCnt   :: !Word16
    } deriving (Show, Eq)

-- | Decode the fixed OK prefix after its marker has been identified. Optional
-- info/session-tracking trailers are consumed by the connection layer.
getOK :: Get OK
getOK = OK <$ skipN 1
           <*> getLenEncWord64
           <*> getLenEncWord64
           <*> getWord16le
           <*> getWord16le
{-# INLINE getOK #-}

-- | Encode the fixed OK prefix; negotiated tracking envelopes are separate.
putOK :: OK -> Put
putOK (OK row lid stat wcnt) = do
    putWord8 0x00
    putLenEncWord64 row
    putLenEncWord64 lid
    putWord16le stat
    putWord16le wcnt
{-# INLINE putOK #-}

instance Binary OK where
    get = getOK
    {-# INLINE get #-}
    put = putOK
    {-# INLINE put #-}

data ERR = ERR
    { errCode  :: !Word16
    , errState :: !ByteString
    , errMsg   :: !ByteString
    } deriving (Show, Eq)

getERR :: Get ERR
getERR = ERR <$  skipN 1
             <*> getWord16le
             <*  skipN 1
             <*> getByteString 5
             <*> getRemainingByteString
{-# INLINE getERR #-}

putERR :: ERR -> Put
putERR (ERR code stat msg) = do
    putWord8 0xFF
    putWord16le code
    putWord8 35 -- '#'
    putByteString stat
    putByteString msg
{-# INLINE putERR #-}

instance Binary ERR where
    get = getERR
    {-# INLINE get #-}
    put = putERR
    {-# INLINE put #-}

data EOF = EOF
    { eofWarningCnt :: !Word16
    , eofStatus     :: !Word16
    } deriving (Show, Eq)

getEOF :: Get EOF
getEOF = EOF <$  skipN 1
             <*> getWord16le
             <*> getWord16le
{-# INLINE getEOF #-}

putEOF :: EOF -> Put
putEOF (EOF wcnt stat) = do
    putWord8 0xFE
    putWord16le wcnt
    putWord16le stat
{-# INLINE putEOF #-}

instance Binary EOF where
    get = getEOF
    {-# INLINE get #-}
    put = putEOF
    {-# INLINE put #-}

--------------------------------------------------------------------------------
--  Helpers

getByteStringNul :: Get ByteString
getByteStringNul = L.toStrict <$> getLazyByteStringNul
{-# INLINE getByteStringNul #-}

getRemainingByteString :: Get ByteString
getRemainingByteString = L.toStrict <$> getRemainingLazyByteString
{-# INLINE getRemainingByteString #-}

putLenEncBytes :: ByteString -> Put
putLenEncBytes c = do
    putLenEncInt (B.length c)
    putByteString c
{-# INLINE putLenEncBytes #-}

getLenEncBytes :: Get ByteString
getLenEncBytes = getLenEncInt >>= getByteString
{-# INLINE getLenEncBytes #-}

-- | Decode a non-NULL unsigned length-encoded value. OK counters and insert IDs
-- use the full Word64 range even when the host's Int is narrower.
getLenEncWord64 :: Get Word64
getLenEncWord64 = do
    tag <- getWord8
    case tag of
        0xfc -> fromIntegral <$> getWord16le
        0xfd -> fromIntegral <$> getWord24le
        0xfe -> getWord64le
        _ | tag < 0xfb -> pure (fromIntegral tag)
          | otherwise -> fail "NULL or invalid non-null length integer"
{-# INLINE getLenEncWord64 #-}

-- | Decode a length that will index memory, rejecting values outside host Int.
getLenEncInt :: Get Int
getLenEncInt = getWord8 >>= getLenEncIntAfterTag

-- | Length decoder after a row parser has already consumed its NULL/length tag.
-- Sharing that read avoids a second peek and Get wrapper on every text field.
getLenEncIntAfterTag :: Word8 -> Get Int
getLenEncIntAfterTag tag
    | tag < 0xfb = pure (fromIntegral tag)
    | tag == 0xfc = fromIntegral <$> getWord16le
    | tag == 0xfd = fromIntegral <$> getWord24le
    | tag == 0xfe = do
        n <- getWord64le
        if n > fromIntegral (maxBound :: Int) then fail "length exceeds machine Int" else pure (fromIntegral n)
    | otherwise = fail "NULL or invalid non-null length integer"
{-# INLINE getLenEncIntAfterTag #-}
{-# INLINE getLenEncInt #-}

-- | Encode a non-NULL unsigned value using the shortest protocol representation.
putLenEncWord64 :: Word64 -> Put
putLenEncWord64 n
    | n < 251 = putWord8 (fromIntegral n)
    | n < 65536 = putWord8 0xfc >> putWord16le (fromIntegral n)
    | n < 16777216 = putWord8 0xfd >> putWord24le (fromIntegral n)
    | otherwise = putWord8 0xfe >> putWord64le n
{-# INLINE putLenEncWord64 #-}

putLenEncInt :: Int -> Put
putLenEncInt n | n < 0 = error "negative protocol length"
               | otherwise = putLenEncWord64 (fromIntegral n)
{-# INLINE putLenEncInt #-}

putWord24le :: Word32 -> Put
putWord24le v = do
    putWord16le $ fromIntegral v
    putWord8 $ fromIntegral (v `shiftR` 16)
{-# INLINE putWord24le #-}

getWord24le :: Get Word32
getWord24le = do
    a <- fromIntegral <$> getWord16le
    b <- fromIntegral <$> getWord8
    return $! a .|. (b `shiftL` 16)
{-# INLINE getWord24le #-}

putWord48le :: Word64 -> Put
putWord48le v = do
    putWord32le $ fromIntegral v
    putWord16le $ fromIntegral (v `shiftR` 32)
{-# INLINE putWord48le #-}

getWord48le :: Get Word64
getWord48le = do
    a <- fromIntegral <$> getWord32le
    b <- fromIntegral <$> getWord16le
    return $! a .|. (b `shiftL` 32)
{-# INLINE getWord48le #-}

getWord24be :: Get Word24
getWord24be = do
    a <- fromIntegral <$> getWord16be
    b <- fromIntegral <$> getWord8
    return $! b .|. (a `shiftL` 8)
{-# INLINE getWord24be #-}

getInt24be :: Get Int24
getInt24be = do
    a <- fromIntegral <$> getWord16be
    b <- fromIntegral <$> getWord8
    return $! fromIntegral $ (b .|. (a `shiftL` 8) :: Word24)
{-# INLINE getInt24be #-}

getWord40be, getWord48be, getWord56be :: Get Word64
getWord40be = do
    a <- fromIntegral <$> getWord32be
    b <- fromIntegral <$> getWord8
    return $! (a `shiftL` 8) .|. b
getWord48be = do
    a <- fromIntegral <$> getWord32be
    b <- fromIntegral <$> getWord16be
    return $! (a `shiftL` 16) .|. b
getWord56be = do
    a <- fromIntegral <$> getWord32be
    b <- fromIntegral <$> getWord24be
    return $! (a `shiftL` 24) .|. b
{-# INLINE getWord40be #-}
{-# INLINE getWord48be #-}
{-# INLINE getWord56be #-}

-- | Decode a fixed row/metadata structure with no trailing bytes. Compare the
-- consumed offset with the complete logical length: a parser may stop before
-- requesting a later lazy ByteString chunk, so its returned buffer alone cannot
-- establish full consumption. OK envelopes use their separate trailer parser.
getFromPacketExact :: Get a -> Packet -> IO a
getFromPacketExact parser (Packet lengthBytes _ body) = case parseDetailLazy parser body of
    Left (buffer,offset,message) -> throwIO (DecodePacketFailed buffer offset message)
    Right (_,offset,value)
        | offset == lengthBytes -> pure value
        | otherwise -> throwIO (DecodePacketFailed B.empty offset "trailing bytes in fixed protocol structure")
{-# INLINE getFromPacketExact #-}