packages feed

capnp-0.10.0.0: lib/Capnp/Basics.hs

{-|
Module: Capnp.Basics
Description: Handling of "basic" capnp datatypes.

In particular

* 'Text' and 'Data' (which are primitive types in the schema language,
  but are both the same as @List(UInt8)@ on the wire).
* Lists of types other than those in "Capnp.Untyped".
  Whereas 'U.ListOf' only deals with low-level encodings of lists,
  this module's 'List' type can represent typed lists.
-}
{-# LANGUAGE DataKinds             #-}
{-# LANGUAGE FlexibleContexts      #-}
{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE GADTs                 #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies          #-}
module Capnp.Basics
    ( Text
    , Data(..)
    , ListElem(..)
    , MutListElem(..)
    , getData
    , getText
    , newData
    , newText
    , dataBytes
    , textBuffer
    , textBytes
    ) where

import Data.Word

import Control.Monad       (when, (>=>))
import Control.Monad.Catch (MonadThrow(throwM))

import qualified Data.ByteString as BS

import Capnp.Classes
    (FromPtr (..), ListElem (..), MutListElem (..), ToPtr (..))
import Capnp.Message          (Mutability (..))
import Data.Mutable           (Thaw (..))
import Internal.Gen.Instances ()

import qualified Capnp.Errors  as E
import qualified Capnp.Message as M
import qualified Capnp.Untyped as U


-- | A textual string (@Text@ in capnproto's schema language). On the wire,
-- this is NUL-terminated. The encoding should be UTF-8, but the library
-- /does not/ verify this; users of the library must do validation themselves, if
-- they care about this.
--
-- Rationale: validation would require doing an up-front pass over the data,
-- which runs counter to the overall design of capnproto.
newtype Text mut = Text (U.ListOf mut Word8)
-- The argument to the data constructor is the slice of the original message
-- containing the text, including the NUL terminator.

instance Thaw (Text 'Const) where
    type Mutable s (Text 'Const) = Text ('Mut s)
    thaw (Text l) = Text <$> thaw l
    unsafeThaw (Text l) = Text <$> unsafeThaw l
    freeze (Text l) = Text <$> freeze l
    unsafeFreeze (Text l) = Text <$> unsafeFreeze l

-- | A blob of bytes (@Data@ in capnproto's schema language). The argument
-- to the data constructor is a slice into the message, containing the raw
-- bytes.
newtype Data mut = Data (U.ListOf mut Word8)

instance Thaw (Data 'Const) where
    type Mutable s (Data 'Const) = Data ('Mut s)
    thaw (Data l) = Data <$> thaw l
    unsafeThaw (Data l) = Data <$> unsafeThaw l
    freeze (Data l) = Data <$> freeze l
    unsafeFreeze (Data l) = Data <$> unsafeFreeze l

-- | @'newData' msg len@ allocates a new data blob of length @len@ bytes
-- inside the message.
newData :: M.WriteCtx m s => M.Message ('Mut s) -> Int -> m (Data ('Mut s))
newData msg len = Data <$> U.allocList8 msg len

-- | Interpret a list of 'Word8' as a capnproto 'Data' value.
getData :: U.ReadCtx m mut => U.ListOf mut Word8 -> m (Data mut)
getData = pure . Data

-- | @'newText' msg len@ Allocates a new 'Text' inside the message. The
-- value has space for @len@ *bytes* (not characters).
newText :: M.WriteCtx m s => M.Message ('Mut s) -> Int -> m (Text ('Mut s))
newText msg len =
    Text <$> U.allocList8 msg (len+1)

-- | Interpret a list of 'Word8' as a capnproto 'Text' value.
--
-- This vaildates that the list is NUL-terminated, but not that it is valid
-- UTF-8. If it is not NUL-terminaed, a 'SchemaViolationError' is thrown.
getText :: U.ReadCtx m mut => U.ListOf mut Word8 -> m (Text mut)
getText list = do
    let len = U.length list
    when (len == 0) $ throwM $ E.SchemaViolationError
        "Text is not NUL-terminated (list of bytes has length 0)"
    lastByte <- U.index (len - 1) list
    when (lastByte /= 0) $ throwM $ E.SchemaViolationError $
        "Text is not NUL-terminated (last byte is " ++ show lastByte ++ ")"
    pure $ Text list

-- | Convert a 'Data' to a 'BS.ByteString'.
dataBytes :: U.ReadCtx m 'Const => Data 'Const -> m BS.ByteString
dataBytes (Data list) = U.rawBytes list

-- | Return the underlying buffer containing the text. This does not include the
-- null terminator.
textBuffer :: U.ReadCtx m mut => Text mut -> m (U.ListOf mut Word8)
textBuffer (Text list) = U.take (U.length list - 1) list

-- | Convert a 'Text' to a 'BS.ByteString', comprising the raw bytes of the text
-- (not counting the NUL terminator).
textBytes :: U.ReadCtx m 'Const => Text 'Const -> m BS.ByteString
textBytes = textBuffer >=> U.rawBytes

------------------- (Mut)ListElem instances for text and data ------------------

instance ListElem mut (Data mut) where
    newtype List mut (Data mut) = DataList (U.ListOf mut (Maybe (U.Ptr mut)))

    listFromPtr msg ptr = DataList <$> fromPtr msg ptr
    toUntypedList (DataList l) = U.ListPtr l

    length (DataList l) = U.length l
    index i (DataList l) = ptrListIndex i l

instance MutListElem s (Data ('Mut s)) where
    setIndex (Data e) i (DataList l) =
        U.setIndex (Just (U.PtrList (U.List8 e))) i l
    newList msg len = DataList <$> U.allocListPtr msg len

instance ListElem mut (Text mut) where
    newtype List mut (Text mut) = TextList (U.ListOf mut (Maybe (U.Ptr mut)))

    listFromPtr msg ptr = TextList <$> fromPtr msg ptr
    toUntypedList (TextList l) = U.ListPtr l

    length (TextList l) = U.length l
    index i (TextList l) = ptrListIndex i l

instance MutListElem s (Text ('Mut s)) where
    setIndex (Text e) i (TextList l) =
        U.setIndex (Just (U.PtrList (U.List8 e))) i l
    newList msg len = TextList <$> U.allocListPtr msg len

-- helper for the above instances.
ptrListIndex
    :: (U.ReadCtx m mut, FromPtr mut a)
    => Int -> U.ListOf mut (Maybe (U.Ptr mut)) -> m a
ptrListIndex i list = do
    ptr <- U.index i list
    fromPtr (U.message list) ptr

--------- To/FromPtr instances for Text and Data. These wrap lists of bytes. --------

instance FromPtr mut (Data mut) where
    fromPtr msg ptr = fromPtr msg ptr >>= getData
instance ToPtr s (Data ('Mut s)) where
    toPtr msg (Data l) = toPtr msg l

instance FromPtr mut (Text mut) where
    fromPtr msg ptr = case ptr of
        Just _ ->
            fromPtr msg ptr >>= getText
        Nothing -> do
            -- getText expects and strips off a NUL byte at the end of the
            -- string. In the case of a null pointer we just want to return
            -- the empty string, so we bypass it here.
            Data bytes <- fromPtr msg ptr
            pure $ Text bytes
instance ToPtr s (Text ('Mut s)) where
    toPtr msg (Text l) = toPtr msg l