packages feed

kademlia-1.0.0.0: src/Network/Kademlia/Protocol/Parsing.hs

{-|
Module      : Network.Kademlia.Protocol.Parsing
Description : Implementation of the protocol parsing

Network.Kademlia.Protocol.Parsing implements the actual protocol parsing.

It made sense to split it off Network.Kademlia.Protocol as it made both cleaner
and more readable.
-}

module Network.Kademlia.Protocol.Parsing where

import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as C
import Control.Monad (liftM, liftM2)
import Control.Monad.Trans (lift)
import Control.Monad.State
import Control.Monad.Trans.Except
import Text.Read (readMaybe)
import Data.Word (Word8, Word16)
import Data.Bits (shiftL)

import Network.Kademlia.Types

type Parse = ExceptT String (State B.ByteString)

-- | Parse a signal from a ByteString
--
--   (This needs to be supplied a Peer, to be able to create a complete Signal)
parse :: (Serialize i, Serialize a) => Peer -> B.ByteString -> Either String (Signal i a)
parse peer = evalState (runExceptT $ parseSignal peer)

-- | Parses the parsable parts of a signal
parseSignal :: (Serialize i, Serialize a) => Peer -> Parse (Signal i a)
parseSignal peer = do
    cId <- parseCommandId
    id <- parseSerialize
    cmd <- parseCommand cId
    let node = Node peer id
    return $ Signal node cmd

-- | Parses a Serialize
parseSerialize :: (Serialize a) => Parse a
parseSerialize = do
    bs <- lift get
    case fromBS bs of
        Left err -> throwE err
        Right (id, rest) -> do
            lift . put $ rest
            return id

-- | Parses a CommandId
parseCommandId :: Parse Int
parseCommandId = do
    bs <- lift get
    case B.uncons bs of
        Nothing         -> throwE "uncons returned Nothing"
        Just (id, rest) -> do
            lift . put $ rest
            return $ fromIntegral id

-- | Splits after a certain character
parseSplit :: Char -> Parse B.ByteString
parseSplit c = do
    bs <- lift get
    if B.null bs
        then throwE "ByteString empty"
        else do
            let (result, rest) = C.span (/=c) bs
            lift . put $ rest
            return result

-- | Skips one character
skipCharacter :: Parse ()
skipCharacter = do
    bs <- lift get
    if B.null bs
        then throwE "ByteString empty"
        else lift . put $ B.drop 1 bs

-- | Parses an Int
parseInt :: Parse Int
parseInt = do
    bs <- lift get
    case C.readInt bs of
        Nothing -> throwE "Failed to parse an Int"
        Just (n, rest) -> do
            lift . put $ rest
            return n

-- | Parses two Word8s from a ByteString into one Word16
parseWord16 :: Parse Word16
parseWord16 = do
    bs <- lift  get
    if B.length bs < 2
        then throwE "ByteString to short"
        else do
            let (words, rest) = B.splitAt 2 bs
            lift . put $ rest
            return . joinWords . B.unpack $ words
    where
        joinWords [a, b] = (toWord16 a `shiftL` 8) + toWord16 b

        toWord16 :: Word8 -> Word16
        toWord16 = fromIntegral

-- | Parses a Node's info
parseNode :: (Serialize i) => Parse (Node i)
parseNode = do
    id <- parseSerialize
    host <- parseSplit ' '
    skipCharacter
    port <- parseWord16
    let peer = Peer (C.unpack host) (fromIntegral port)
    return $ Node peer id

-- | Parses a trailing k-bucket
parseKBucket :: (Serialize i) => Parse (KBucket i)
parseKBucket = liftM2 (:) parseNode parseKBucket
                   `catchE` \_ -> return []

-- | Parses the rest of a command corresponding to an id
parseCommand :: (Serialize i, Serialize a) => Int -> Parse (Command i a)
parseCommand 0 = return PING
parseCommand 1 = return PONG
parseCommand 2 = liftM2 STORE parseSerialize parseSerialize
parseCommand 3 = FIND_NODE `liftM` parseSerialize
parseCommand 4 = liftM2 RETURN_NODES  parseSerialize parseKBucket
parseCommand 5 = FIND_VALUE `liftM` parseSerialize
parseCommand 6 = liftM2 RETURN_VALUE parseSerialize parseSerialize
parseCommand _ = throwE "Invalid id"