-- | Wire format for multistream-select.
--
-- Every message is: <varint-length><UTF-8 payload>\n
-- The length includes the trailing newline byte.
module Network.LibP2P.MultistreamSelect.Wire
  ( encodeMessage
  , decodeMessage
  , multistreamHeader
  , naMessage
  ) where

import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import Data.Text (Text)
import qualified Data.Text.Encoding as TE
import Network.LibP2P.Core.Varint (decodeUvarint, encodeUvarint)

-- | The multistream-select protocol header.
multistreamHeader :: Text
multistreamHeader :: Text
multistreamHeader = Text
"/multistream/1.0.0"

-- | The "not available" response.
naMessage :: Text
naMessage :: Text
naMessage = Text
"na"

-- | Encode a multistream-select message (protocol ID or command).
-- Produces: <varint(len+1)><UTF-8 encoded text><0x0a>
encodeMessage :: Text -> ByteString
encodeMessage :: Text -> ByteString
encodeMessage Text
msg =
  let payload :: ByteString
payload = Text -> ByteString
TE.encodeUtf8 Text
msg ByteString -> ByteString -> ByteString
forall a. Semigroup a => a -> a -> a
<> Word8 -> ByteString
BS.singleton Word8
0x0a
      len :: Word64
len = Int -> Word64
forall a b. (Integral a, Num b) => a -> b
fromIntegral (ByteString -> Int
BS.length ByteString
payload)
   in Word64 -> ByteString
encodeUvarint Word64
len ByteString -> ByteString -> ByteString
forall a. Semigroup a => a -> a -> a
<> ByteString
payload

-- | Decode a multistream-select message from bytes.
-- Returns the text content (without newline) and remaining bytes.
decodeMessage :: ByteString -> Either String (Text, ByteString)
decodeMessage :: ByteString -> Either String (Text, ByteString)
decodeMessage ByteString
bs = do
  (len, rest1) <- ByteString -> Either String (Word64, ByteString)
decodeUvarint ByteString
bs
  let msgLen = Word64 -> Int
forall a b. (Integral a, Num b) => a -> b
fromIntegral Word64
len :: Int
  if BS.length rest1 < msgLen
    then Left "decodeMessage: not enough bytes"
    else
      let (payload, rest2) = BS.splitAt msgLen rest1
       in if BS.null payload || BS.last payload /= 0x0a
            then Left "decodeMessage: message does not end with newline"
            else
              case TE.decodeUtf8' (BS.init payload) of
                Left UnicodeException
err -> String -> Either String (Text, ByteString)
forall a b. a -> Either a b
Left (String -> Either String (Text, ByteString))
-> String -> Either String (Text, ByteString)
forall a b. (a -> b) -> a -> b
$ String
"decodeMessage: invalid UTF-8: " String -> String -> String
forall a. Semigroup a => a -> a -> a
<> UnicodeException -> String
forall a. Show a => a -> String
show UnicodeException
err
                Right Text
text -> (Text, ByteString) -> Either String (Text, ByteString)
forall a b. b -> Either a b
Right (Text
text, ByteString
rest2)
  where
    -- BS.init is safe here because we checked non-null above