packages feed

dnsbase-1.0.2.3: internal/Net/DNSBase/Encode/Internal/State.hs

-- |
-- Module      : Net.DNSBase.Encode.Internal.State
-- Description : Encoder state monad and wire-format primitives
-- Copyright   : (c) IIJ Innovation Institute Inc., 2009
--               (c) Viktor Dukhovni, 2020-2026
-- License     : BSD-3-Clause
-- Maintainer  : ietf-dane@dukhovni.org
-- Stability   : unstable
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE GeneralisedNewtypeDeriving #-}

module Net.DNSBase.Encode.Internal.State
    ( EncodeErr(..)
    , ErrorContext
    , SPut, SPutM, localSPut
    , buildCompressed
    , encodeCompressed
    , buildVerbatim
    , encodeVerbatim
    , putDomain
    , putWireForm
    , put8
    , put16
    , put32
    , put64
    , putIPv4
    , putIPv6
    , putByteString
    , putByteStringLen8
    , putByteStringLen16
    , putShortByteString
    , putShortByteStringLen8
    , putShortByteStringLen16
    , putUtf8Text
    , putUtf8TextLen8
    , putUtf8TextLen16
    , putSizedBuilder
    , putReplicate
    -- Encoder state mutation
    , passLen
    , failWith
    , setContext
    , encoderOffset
    , setQNameHint
    , setLastOwnerHint
    ) where

import qualified Control.Monad.Trans.RWS.CPS
        as R ( RWST, evalRWST, ask, get, gets, local
             , pass, put, tell )
import qualified Control.Monad.STE as STE
import qualified Control.Monad.STE.Internal as STE
import qualified Data.ByteString as B
import qualified Data.ByteString.Builder as B
import qualified Data.ByteString.Builder.Extra as B
import qualified Data.ByteString.Lazy as LB
import qualified Data.ByteString.Short as SB
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Data.Text.Unsafe as T
import GHC.ST as G (ST(..))

import qualified Net.DNSBase.Internal.NameComp as NC
import Net.DNSBase.Internal.Domain
import Net.DNSBase.Encode.Internal.Metric
import Net.DNSBase.Internal.Error
import Net.DNSBase.Internal.Util

----------------------------------------------------------------

stToSTE :: G.ST s a -> STE.STE e s a
stToSTE = coerce

----------------------------------------------------------------

-- | Encoder state.  The QNAME and last-owner hints use 'RootDomain'
-- as the unset sentinel.
data EncState s = EncState
    { encOffset       :: Int
    , encDoNC         :: Bool
    , encNCTree       :: NC.NCMap s
    , encQName        :: Domain
    , encQNameOff     :: Int
    , encLastOwner    :: Domain
    , encLastOwnerOff :: Int
    }

-- | Initial encoder state.
encInit :: Bool -- ^ If "True", DNS name compression is enabled
        -> STE.STE e s (EncState s)
encInit donamecomp = do
    nc <- stToSTE NC.empty
    pure $ EncState 0 donamecomp nc RootDomain 0 RootDomain 0

----------------------------------------------------------------

type ErrorContext r = (Typeable r, Show r, Eq r)

buildSPut :: ErrorContext r
        => (forall s. SPut s r)
        -> Bool
        -> Either (EncodeErr (Maybe r)) (Int, Builder)
buildSPut m donc = STE.handleSTE id do
    st <- encInit donc
    evalSPutM (m >> gets encOffset) Nothing st

-- | Execute the composed 'Builder' endomorphisms to encode a packet of the
-- cumulative length.
runSPut :: ErrorContext r
        => (forall s. SPut s r)
        -> Bool
        -> Either (EncodeErr (Maybe r)) ByteString
runSPut m donc = do
    (len, builder) <- buildSPut m donc
    pure $ LB.toStrict
         $ B.toLazyByteStringWith (strat len) mempty builder
  where
    strat len = B.untrimmedStrategy len len

-- | Perform a stateful encoding with DNS name compression.  The initial error
-- context is "Nothing".  Specific values can be provided during the
-- computation by using 'localSPut'.
buildCompressed :: ErrorContext r
                => (forall s. SPut s r)
                -> Either (EncodeErr (Maybe r)) Builder
buildCompressed m = snd <$> buildSPut m True

-- | Perform a stateful encoding with DNS name compression.  The initial error
-- context is "Nothing".  Specific values can be provided during the
-- computation by using 'localSPut'.
encodeCompressed :: ErrorContext r
                 => (forall s. SPut s r)
                 -> Either (EncodeErr (Maybe r)) ByteString
encodeCompressed m = runSPut m True

-- | Perform a stateful encoding without DNS name compression.  The initial
-- error context is "Nothing".  Specific values can be provided during the
-- computation by using 'localSPut'.
buildVerbatim :: ErrorContext r
              => (forall s. SPut s r)
              -> Either (EncodeErr (Maybe r)) Builder
buildVerbatim m = snd <$> buildSPut m False

-- | Perform a stateful encoding without DNS name compression.  The initial
-- error context is "Nothing".  Specific values can be provided during the
-- computation by using 'localSPut'.
encodeVerbatim :: ErrorContext r
               => (forall s. SPut s r)
               -> Either (EncodeErr (Maybe r)) ByteString
encodeVerbatim m = runSPut m False

-- | Encode a domain with possible name compression if the entire name fits in
-- the first 16K of the output.
putDomain :: ErrorContext r => Domain -> SPut s r
putDomain domain = do
    EncState{..} <- get
    let !sb   = shortBytes domain
        !wlen = SB.length sb - 1
    if wlen > 0 && encDoNC
        then case tryHintMatch domain encQName encQNameOff
                               encLastOwner encLastOwnerOff of
            Just ptr -> put16 ptr
            Nothing  -> do
                m <- liftSPut . stToSTE $
                         NC.lookupAndRegister sb encOffset encNCTree
                case m of
                    Nothing      -> putWireForm domain
                    Just (hb, p) -> do
                        putShortByteStringPrefix hb sb
                        put16 p
        else putWireForm domain

-- QNAME is checked before the last-owner hint: its offset always
-- points at verbatim bytes, whereas the last-owner offset may point
-- at a head-bytes-plus-pointer emission, so preferring QNAME keeps
-- pointer chains one hop shorter when both hints match.
tryHintMatch
    :: Domain -> Domain -> Int -> Domain -> Int -> Maybe Word16
tryHintMatch dom qn qOff lo loOff
    | qn == dom = Just (mkPtr qOff)
    | lo == dom = Just (mkPtr loOff)
    | otherwise = Nothing
  where
    mkPtr off = fromIntegral (0xC000 .|. off)
{-# INLINE tryHintMatch #-}

-- | Record the QNAME compression hint.
setQNameHint :: ErrorContext r => Domain -> Int -> SPut s r
setQNameHint !dom !off = do
    s <- get
    put $! s { encQName = dom, encQNameOff = off }

-- | Record the last-RR-owner compression hint.  The offset is
-- updated only when the owner changes, so consecutive RRs sharing
-- the same owner all point at the first emission rather than
-- chaining through each other.
setLastOwnerHint :: ErrorContext r => Domain -> Int -> SPut s r
setLastOwnerHint !dom !off = do
    s <- get
    when (encLastOwner s /= dom) $
        put $! s { encLastOwner = dom, encLastOwnerOff = off }

-- | The encoder offset at which the next emitted byte will land.
encoderOffset :: SPutM s r Int
encoderOffset = gets encOffset
{-# INLINE encoderOffset #-}

-- | Encode a domain name verbatim, without name compression.
putWireForm :: ErrorContext r => Domain -> SPut s r
putWireForm = encVar (SB.length . shortBytes) (B.shortByteString . shortBytes)
{-# INLINE putWireForm #-}

----------------------------------------------------------------

pattern MaxPos :: Int
pattern MaxPos = 0xffff

{-# INLINE addPos #-}
addPos :: ErrorContext r => Int -> SPut s r
addPos n = do
    !s@EncState{ encOffset = pos } <- get
    let !pos' = pos + n
    when (n > MaxPos || pos' > MaxPos) do
        ask >>= liftSPut . STE.throwSTE . EncodeTooLong
    put $! s { encOffset = pos' }

{-# INLINE encFix #-}
encFix :: ErrorContext r => Int -> (a -> Builder) -> a -> SPut s r
encFix size enc a = addPos size >> tell (enc a)

{-# INLINE encVar #-}
encVar :: ErrorContext r => (a -> Int) -> (a -> Builder) -> a -> SPut s r
encVar getSize enc a = encFix (getSize a) enc a

----------------------------------------------------------------

-- | Write a single octet.
put8 :: ErrorContext r => Word8 -> SPut s r
put8 = encFix 1 B.word8
{-# INLINE put8 #-}

-- | Write a big-endian 16-bit word.
put16 :: ErrorContext r => Word16 -> SPut s r
put16 = encFix 2 B.word16BE
{-# INLINE put16 #-}

-- | Write a big-endian 32-bit word.
put32 :: ErrorContext r => Word32 -> SPut s r
put32 = encFix 4 B.word32BE
{-# INLINE put32 #-}

-- | Write a big-endian 64-bit word.
put64 :: ErrorContext r => Word64 -> SPut s r
put64 = encFix 8 B.word64BE
{-# INLINE put64 #-}

-- | Write the four octets of an IPv4 address in network order.
putIPv4 :: ErrorContext r => IPv4 -> SPut s r
putIPv4 = put32 . fromIPv4w
{-# INLINE putIPv4 #-}

-- | Write the sixteen octets of an IPv6 address in network order.
putIPv6 :: ErrorContext r => IPv6 -> SPut s r
putIPv6 ip6 =
    putSizedBuilder $! mbWord32 w0
                    <> mbWord32 w1
                    <> mbWord32 w2
                    <> mbWord32 w3
  where
    (w0, w1, w2, w3) = fromIPv6w ip6
{-# INLINE putIPv6 #-}

-- | Write the bytes of a 'ByteString' verbatim, with no length prefix.
putByteString :: ErrorContext r => ByteString -> SPut s r
putByteString b =
    unless (B.null b) $ encVar B.length B.byteString b

-- | Write the bytes of a 'ShortByteString' verbatim, with no length prefix.
putShortByteString :: ErrorContext r => ShortByteString -> SPut s r
putShortByteString b =
    unless (SB.null b) $ encVar SB.length B.shortByteString b

-- | Write the first @n@ bytes of a 'ShortByteString' verbatim.
putShortByteStringPrefix :: ErrorContext r => Int -> ShortByteString -> SPut s r
putShortByteStringPrefix !n !sbs
    | n > 0  = addPos n >> tell (shortByteStringSliceB 0 n sbs)
    | n == 0 = pure ()
    | otherwise = failWith CantEncode -- should never happen

-- | Write a DNS /character-string/: an 8-bit length prefix followed
-- by the bytes.  Fails with 'EncodeTooLong' if the input exceeds 255 bytes.
putByteStringLen8 :: ErrorContext r => ByteString -> SPut s r
putByteStringLen8 bs@(B.length -> len) | len <= 0xff = do
    addPos (len + 1)
    tell $ B.word8 (iw8 len) <> B.byteString bs
putByteStringLen8 _ =
    failWith EncodeTooLong

-- | 'ShortByteString' counterpart of 'putByteStringLen8'.
putShortByteStringLen8 :: ErrorContext r => ShortByteString -> SPut s r
putShortByteStringLen8 bs@(SB.length -> len) | len <= 0xff = do
    addPos $ len + 1
    tell $ B.word8 (iw8 len) <> B.shortByteString bs
putShortByteStringLen8 _ = failWith EncodeTooLong

-- | Write a 16-bit-length-prefixed 'ByteString'.  Fails with
-- 'EncodeTooLong' if the input exceeds 65535 bytes.
putByteStringLen16 :: ErrorContext r => ByteString -> SPut s r
putByteStringLen16 bs@(B.length -> len) | len <= 0xffff = do
    addPos $ len + 2
    tell $ B.word16BE (iw16 len) <> B.byteString bs
putByteStringLen16 _ = failWith EncodeTooLong

-- | 'ShortByteString' counterpart of 'putByteStringLen16'.
putShortByteStringLen16 :: ErrorContext r => ShortByteString -> SPut s r
putShortByteStringLen16 bs@(SB.length -> len) | len <= 0xffff = do
    addPos $ len + 2
    tell $ B.word16BE (iw16 len) <> B.shortByteString bs
putShortByteStringLen16 _ = failWith EncodeTooLong

-- | Write the UTF-8 encoding of a 'Text' verbatim, with no
-- length prefix.
putUtf8Text :: ErrorContext r => Text -> SPut s r
putUtf8Text t =
    unless (T.null t) $ encVar T.lengthWord8 T.encodeUtf8Builder t

-- | Write a UTF-8-encoded 'Text' with an 8-bit length prefix
-- (length in /bytes/, not codepoints).  Fails with 'EncodeTooLong'
-- if the UTF-8 encoding exceeds 255 bytes.
putUtf8TextLen8 :: ErrorContext r => Text -> SPut s r
putUtf8TextLen8 t@(T.lengthWord8-> len) | len <= 0xff = do
    addPos $ len + 1
    tell $ B.word8 (iw8 len) <> T.encodeUtf8Builder t
putUtf8TextLen8 _ = failWith EncodeTooLong

-- | Write a UTF-8-encoded 'Text' with a 16-bit length prefix
-- (length in /bytes/).  Fails with 'EncodeTooLong' if the UTF-8
-- encoding exceeds 65535 bytes.
putUtf8TextLen16 :: ErrorContext r => Text -> SPut s r
putUtf8TextLen16 t@(T.lengthWord8-> len) | len <= 0xffff = do
    addPos $ len + 2
    tell $ B.word16BE (iw16 len) <> T.encodeUtf8Builder t
putUtf8TextLen16 _ = failWith EncodeTooLong

iw8 :: Int -> Word8
iw8 = fromIntegral
{-# INLINE iw8 #-}

iw16 :: Int -> Word16
iw16 = fromIntegral
{-# INLINE iw16 #-}

-- | Write @n@ copies of the byte @w@ (used for fixed-width
-- zero-padded fields).
putReplicate :: ErrorContext r => Word8 -> Word8 -> SPut s r
putReplicate n w =
    encFix (fromEnum n) B.lazyByteString $
        LB.replicate (fromIntegral n) w

-- | Write a length-tracked 'SizedBuilder' verbatim, advancing the
-- encoder offset by the builder's recorded length.  Fails with
-- 'EncodeTooLong' when the builder itself carries the overflow
-- sentinel.
putSizedBuilder :: ErrorContext r => SizedBuilder -> SPut s r
putSizedBuilder (SizedBuilder len b) = addPos len >> tell b
putSizedBuilder _                    = failWith EncodeTooLong
{-# INLINE putSizedBuilder #-}

------------------------------------------

-- | Wrap a writer with an outer 16-bit length prefix that is
-- computed automatically from the writer's output.  Used for
-- @RDLENGTH@ and the various length-prefixed sub-fields of RR
-- data (SVCB option values, EDNS option values, and so on).
passLen :: ErrorContext r => SPutM s r a -> SPutM s r a
passLen m = pass $ do
        pos <- addPos 2 >> gets encOffset
        x <- m
        len <- subtract pos <$> gets encOffset
        return (x, prependLen len)

prependLen :: Int -> B.Builder -> B.Builder
prependLen = mappend . B.word16BE . fromIntegral

-- | Abort the encoder with the given error, attaching the current
-- 'ErrorContext' from the reader environment.  Used by individual
-- writers when an out-of-range or otherwise invalid value cannot
-- be serialised.
failWith :: ErrorContext r => (forall a. ErrorContext a => a -> EncodeErr a) -> SPut s r
failWith f = ask >>= liftSPut . STE.throwSTE . f

-- | Run a sub-encoder with the given context value installed, so
-- that any 'failWith' inside reports its error against that
-- context.  Used to label sections of the encode (RR header, RR
-- data, EDNS option, ...) so that error messages identify which
-- field went wrong.
setContext :: ErrorContext r => r -> SPutM s r a -> SPutM s r a
setContext r = localSPut (const $ Just r)
{-# INLINE setContext #-}

----------------------------------------------------------------

type BaseM s r = STE.STE (EncodeErr (Maybe r)) s
type RWST s r = R.RWST (Maybe r) Builder (EncState s) (BaseM s r)

-- | Encode an output packet in the ST monad, with @r@ as an
-- optional error context (typically the RData being encoded, when
-- applicable).
type SPut s r = SPutM s r ()

-- | The underlying SPut monad
newtype SPutM s r a = SPutM { _unSPutM :: RWST s r a }

instance Functor (SPutM s r) where
    fmap :: forall a b. (a -> b) -> SPutM s r a -> SPutM s r b
    fmap = coerce (fmap :: (a -> b) -> RWST s r a -> RWST s r b)
    {-# INLINE fmap #-}

    (<$) :: forall a b. a -> SPutM s r b -> SPutM s r a
    (<$) = coerce ((<$) :: a -> RWST s r b -> RWST s r a)
    {-# INLINE (<$) #-}

instance Applicative (SPutM s r) where
    pure :: forall a. a -> SPutM s r a
    pure = coerce (pure :: a -> RWST s r a)
    {-# INLINE pure #-}

    liftA2 :: forall a b c. (a -> b -> c) -> SPutM s r a -> SPutM s r b -> SPutM s r c
    liftA2 = coerce (liftA2 :: (a -> b -> c) -> RWST s r a -> RWST s r b -> RWST s r c)
    {-# INLINE liftA2 #-}

    (<*>) :: forall a b. SPutM s r (a -> b) -> SPutM s r a -> SPutM s r b
    (<*>) = coerce ((<*>) :: RWST s r (a -> b) -> RWST s r a -> RWST s r b)
    {-# INLINE (<*>) #-}

    (*>) :: forall a b. SPutM s r a -> SPutM s r b -> SPutM s r b
    (*>) = coerce ((*>) :: RWST s r a -> RWST s r b -> RWST s r b)
    {-# INLINE (*>) #-}

    (<*) :: forall a b. SPutM s r a -> SPutM s r b -> SPutM s r a
    (<*) = coerce ((<*) :: RWST s r a -> RWST s r b -> RWST s r a)
    {-# INLINE (<*) #-}

instance Monad (SPutM s r) where
    (>>=) :: forall a b. SPutM s r a -> (a -> SPutM s r b) -> SPutM s r b
    (>>=) = coerce ((>>=) :: RWST s r a -> (a -> RWST s r b) -> RWST s r b)
    {-# INLINE (>>=) #-}

evalSPutM :: forall s r a. (forall t. SPutM t r a) -> (Maybe r) -> (EncState s) -> BaseM s r (a, Builder)
evalSPutM m = coerce (R.evalRWST (coerce m :: (forall t. RWST t r a)) :: Maybe r -> EncState s -> BaseM s r (a, Builder))
{-# INLINE evalSPutM #-}

liftSPut :: forall s r a. BaseM s r a -> SPutM s r a
liftSPut = coerce (lift :: BaseM s r a -> RWST s r a)

ask :: forall s r. SPutM s r (Maybe r)
ask = coerce (R.ask :: RWST s r (Maybe r))
{-# INLINE ask #-}

get :: forall s r. SPutM s r (EncState s)
get = coerce (R.get :: RWST s r (EncState s))
{-# INLINE get #-}

gets :: forall s r a. (EncState s -> a) -> SPutM s r a
gets = coerce (R.gets :: (EncState s -> a) -> RWST s r a)
{-# INLINE gets #-}

-- | Run the encoder with a modified context
localSPut :: forall s r a. (Maybe r -> Maybe r) -> SPutM s r a -> SPutM s r a
localSPut = coerce (R.local :: (Maybe r -> Maybe r) -> RWST s r a -> RWST s r a)
{-# INLINE localSPut #-}

pass :: forall s r a. SPutM s r (a, Builder -> Builder) -> SPutM s r a
pass = coerce (R.pass :: RWST s r (a, Builder -> Builder) -> RWST s r a)
{-# INLINE pass #-}

put :: forall s r. EncState s -> SPutM s r ()
put = coerce (R.put :: EncState s -> RWST s r ())
{-# INLINE put #-}

tell :: forall s r. Builder -> SPutM s r ()
tell = coerce (R.tell :: Builder -> RWST s r ())
{-# INLINE tell #-}