packages feed

dnsbase-1.0.0.0: internal/Net/DNSBase/Decode/Internal/State.hs

-- |
-- Module      : Net.DNSBase.Decode.Internal.State
-- Description : Decoder 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 #-}

module Net.DNSBase.Decode.Internal.State
    (
    -- * DNS message element parser
      SGet
    -- * Internal state accessors
    , getPosition
    , getPacket
    , getChrono
    , getNameComp
    -- ** Deduplication support
    , getLastOwner
    , getLastCname
    , setLastOwner
    , setLastCname
    -- ** Setting a non-default error context
    , setDecodeSection
    , setDecodeTriple
    , setDecodeSource
    , local
    -- * Generic low-level decoders
    , get8
    , get16
    , get32
    , get64
    , getInt8
    , getInt16
    -- * DNS-specific low-level decoders
    , getIPv4
    , getIPv4Net
    , getIPv6
    , getIPv6Net
    , getDnsTime
    -- * Octet-string decoders
    , skipNBytes
    , getNBytes
    , getShortByteString
    , getShortNByteString
    , getShortByteStringLen8
    , getShortByteStringLen16
    , getUtf8Text
    , getUtf8TextLen8
    , getUtf8TextLen16
    -- * Sequence decoders
    , getVarWidthSequence
    , getFixedWidthSequence
    -- * Decoder sandboxing
    , seekSGet
    , fitSGet
    -- * Decoder failure
    , failSGet
    -- * Decoder driver
    , decodeAtWith
    ) where

import qualified Data.ByteString as B
import qualified Data.ByteString.Short as SB
import qualified Data.ByteString.Unsafe as B
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import Data.ByteString.Internal (ByteString(..))

import Net.DNSBase.Internal.Domain
import Net.DNSBase.Internal.Error
import Net.DNSBase.Internal.Util

-----------

-- | The 'SGet' monad internal reader environment
data SGetEnv = SGetEnv
    { psPacket   :: ByteString
    , psChrono   :: Int64
    , psNameComp :: Bool
    , psSection  :: DnsSection
    , psTriple   :: Maybe DnsTriple
    , psSource   :: Maybe MessageSource
    }

-- | The 'SGet' monad internal state
data SGetState = SGetState
    { psOffset    :: Int
    , psLength    :: Int
    , psLastOwner :: Domain
    , psLastCname :: Domain
    }

-- | Abort the decoder with a 'DecodeError' carrying the given
-- diagnostic message and the current 'DecodeContext' (message
-- section, RR triple, and source address) drawn from the reader
-- environment.  Used by RR-data parsers when the wire bytes
-- don't conform to the expected shape.
failSGet :: String -> SGet a
failSGet msg = do
    SGetEnv { psSection = decodeSection
            , psTriple  = decodeTriple
            , psSource  = decodeSource } <- ask
    throw $ DecodeError DecodeContext {..} msg

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

-- | Consumes and returns a 'B.ByteString' of length @n@ from the buffer
--
-- Fails if this would back-track or over-run.
getNByteString :: Int -> SGet ByteString
getNByteString n | n == 0 = pure B.empty
getNByteString n | n > 0  = do
    s <- get
    when (psLength s < n) do failSGet "requested bytecount exceeds available"
    modify' \t -> t { psOffset = psOffset s + n
                    , psLength = psLength s - n}
    (BS fp _) <- asks psPacket
    pure $! BS (fp `plusForeignPtr` psOffset s) n
getNByteString _ = failSGet "negative bytecount requested"
{-# INLINE getNByteString #-}

-- | Consumes and discards @n@ bytes of input from the buffer
skipNBytes :: Int -> SGet ()
skipNBytes n | n >= 0 = do
    s <- get
    when (psLength s < n) do
        failSGet "requested skip bytecount exceeds available"
    when (n > 0) do
        modify' $ \t -> t { psOffset = psOffset s + n
                          , psLength = psLength s - n }
skipNBytes _ = failSGet "negative skip bytecount requested"
{-# INLINE skipNBytes #-}

-- | Returns the current position relative to the start of the internal buffer
getPosition :: SGet Int
getPosition = gets psOffset
{-# INLINE getPosition #-}

-- | Returns the entire contents of the internal buffer
getPacket :: SGet ByteString
getPacket = asks psPacket
{-# INLINE getPacket #-}

-- | Returns the epoch-relative time passed to 'decodeAtWith'
getChrono :: SGet Int64
getChrono = asks psChrono
{-# INLINE getChrono #-}

-- | Returns whether name (de)compression is applicable to the input buffer.
--
-- Generally true for full DNS messages, and false for data blobs encoded in
-- isolation.
getNameComp :: SGet Bool
getNameComp = asks psNameComp
{-# INLINE getNameComp #-}

getLastOwner, getLastCname :: SGet Domain
setLastOwner, setLastCname :: Domain -> SGet Domain
getLastOwner = gets psLastOwner
getLastCname = gets psLastCname
setLastOwner d = d <$ modify' \ s -> s { psLastOwner = d }
setLastCname d = d <$ modify' \ s -> s { psLastCname = d }
{-# INLINE getLastOwner #-}
{-# INLINE getLastCname #-}
{-# INLINE setLastOwner #-}
{-# INLINE setLastCname #-}

-- | Set message section for error reporting.
setDecodeSection :: DnsSection -> SGetEnv -> SGetEnv
setDecodeSection s env = env {psSection = s}

-- | Set current RRSet name, type, class for error reporting.
setDecodeTriple :: DnsTriple -> SGetEnv -> SGetEnv
setDecodeTriple t env = env {psTriple = Just t}

-- | Set message source for error reporting.
setDecodeSource :: MessageSource -> SGetEnv -> SGetEnv
setDecodeSource s env = env {psSource = Just s}

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

-- | Consumes one octet and returns it as a 'Word8'
get8 :: SGet Word8
get8 = B.unsafeIndex <$> asks psPacket <*> (gets psOffset <* skipNBytes 1)
{-# INLINE get8 #-}

-- | Load a 16-bit big-endian word.
get16 :: SGet Word16
get16 = word16be <$> getNByteString 2
{-# INLINE get16 #-}

-- | Load a 32-bit big-endian word.
get32 :: SGet Word32
get32 = word32be <$> getNByteString 4
{-# INLINE get32 #-}

-- | Load a 64-bit big-endian word.
get64 :: SGet Word64
get64 = word64be <$> getNByteString 8
{-# INLINE get64 #-}

-- | Consumes one octet and returns it as an 'Int'
getInt8 :: SGet Int
getInt8 = fromIntegral <$> get8
{-# INLINE getInt8 #-}

-- | Consumes two octets and returns them as an 'Int'
-- computed using network byte order
getInt16 :: SGet Int
getInt16 = fromIntegral <$> get16
{-# INLINE getInt16 #-}

--  Not implemented, risks sign overflow on 32-bit systems.
--
--  -- | Consumes four octets and returns them as an 'Int'
--  -- computed using network byte order
--  getInt32 :: SGet Int
--  getInt32 = fromIntegral <$> get32
--  {-# INLINE getInt32 #-}

----

-- | Reads 4 octets and returns them as an 'IPv4' address
getIPv4 :: SGet IPv4
getIPv4 = toIPv4w <$> get32
{-# INLINE getIPv4 #-}

-- | Reads 16 octets and returns them as an 'IPv6' address
getIPv6 :: SGet IPv6
getIPv6 = toIPv6w <$> ((,,,) <$> get32 <*> get32 <*> get32 <*> get32)
{-# INLINE getIPv6 #-}

-- | Reads up to four octets and returns them as an 'IPv4'
-- address padded as needed with trailing 0x0 bytes.
getIPv4Net :: Int -> SGet IPv4
getIPv4Net n | n >= 0 && n <= 4 =
    getNByteString n >>= \ (BS fp _) -> pure $! unsafePerformFPIO fp \ptr -> do
        allocaBytesAligned 4 4 $ \buf -> do
            fillBytes buf 0 4
            copyBytes buf ptr n
            w <- toBE byteSwap32 <$> peek (castPtr buf)
            pure $ toIPv4w w
getIPv4Net _ = failSGet "invalid IPv4 prefix length"

-- | Reads up to 16 octets and returns them as an 'IPv6' address
-- padded as needed with trailing 0x0 bytes.
getIPv6Net :: Int -> SGet IPv6
getIPv6Net n | n >= 0 && n <= 16 =
    getNByteString n >>= \ (BS fp _) -> pure $! unsafePerformFPIO fp \ptr -> do
        allocaBytesAligned 16 4 $ \buf -> do
            fillBytes buf 0 16
            copyBytes buf ptr n
            w0 <- toBE byteSwap32 <$> peekElemOff (castPtr buf) 0
            w1 <- toBE byteSwap32 <$> peekElemOff (castPtr buf) 1
            w2 <- toBE byteSwap32 <$> peekElemOff (castPtr buf) 2
            w3 <- toBE byteSwap32 <$> peekElemOff (castPtr buf) 3
            pure $ toIPv6w (w0,w1,w2,w3)
getIPv6Net _ = failSGet "invalid IPv6 prefix length"

-- | Converts a 32-bit circle-arithmetic DNS time to an absolute 64-bit DNS
-- timestamp that lies within a 31-bit band of the parser state's reference
-- timestamp.
getDnsTime :: SGet Int64
getDnsTime = dnsTime <$> get32 <*> getChrono
  where
    dnsTime :: Word32 -- ^ DNS circle-arithmetic timestamp
            -> Int64  -- ^ reference epoch time
            -> Int64  -- ^ absolute DNS timestamp
    dnsTime tdns tnow =
        let delta = tdns - fromIntegral tnow
         in if delta > 0x7FFF_FFFF -- tdns is in the past?
               then tnow - (0x1_0000_0000 - fromIntegral delta)
               else tnow + fromIntegral delta
{-# INLINE getDnsTime #-}

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

-- | Consumes and returns @n@ bytes of input from the buffer.
getNBytes :: Int -> SGet [Word8]
getNBytes n = B.unpack <$> getNByteString n
{-# INLINE getNBytes #-}

-- | Decodes a sequence of values with a fixed wire-form byte-width.
getFixedWidthSequence :: Int    -- ^ Number of octets to encode one value
                      -> SGet a -- ^ Decoder for a single value
                      -> Int    -- ^ Total number of octets in the sequence
                      -> SGet [a]
getFixedWidthSequence wdth getOne len@((`quotRem` wdth) -> (cnt, 0)) =
  fitSGet len $ replicateM cnt getOne
getFixedWidthSequence _ _ _ =
  failSGet "sequence length not multiple of element size"
{-# INLINE getFixedWidthSequence #-}

-- | Decodes a sequence of values with a variable wire-form byte-width.
getVarWidthSequence :: SGet a   -- ^ Decoder for a single value
                    -> Int      -- ^ Total number of octets in the sequence
                    -> SGet [a]
getVarWidthSequence getOne = fitSGet <$> id <*> go
  where
    go n | n > 0 = do
      pos0 <- getPosition
      x    <- getOne
      used <- (subtract pos0) <$> getPosition
      (x : ) <$> go (n - used)
    go 0 = pure []
    go _ = failSGet "last sequence element read past limit"
{-# INLINE getVarWidthSequence #-}

-- | Consumes the rest of the buffer as 'SB.ShortByteString'.
getShortByteString :: SGet ShortByteString
getShortByteString = SB.toShort <$> (getNByteString =<< gets psLength)
{-# INLINE getShortByteString #-}

-- | Consumes and returns a 'SB.ShortByteString' of length @n@ from the buffer.
getShortNByteString :: Int -> SGet ShortByteString
getShortNByteString n = SB.toShort <$> getNByteString n
{-# INLINE getShortNByteString #-}

-- | Read a ShortByteString whose length is determined by an 8-bit prefix.
getShortByteStringLen8 :: SGet ShortByteString
getShortByteStringLen8 = getInt8 >>= getShortNByteString
{-# INLINE getShortByteStringLen8 #-}

-- | Read a ShortByteString whose length is determined by a 16-bit prefix.
getShortByteStringLen16 :: SGet ShortByteString
getShortByteStringLen16 = getInt16 >>= getShortNByteString
{-# INLINE getShortByteStringLen16 #-}

-- | Read a UTF8-encoded text string of the given length.
getUtf8Text :: Int -> SGet T.Text
getUtf8Text len = T.decodeUtf8' <$> getNByteString len >>= \ case
    Right txt -> pure txt
    Left  err -> failSGet $ show err
{-# INLINE getUtf8Text #-}

-- | Read a UTF8-encoded text string preceded by an explicit 8-bit length.
getUtf8TextLen8 :: SGet T.Text
getUtf8TextLen8 = getInt8 >>= T.decodeUtf8' <.> getNByteString >>= \ case
    Right txt -> pure txt
    Left  err -> failSGet $ show err
{-# INLINE getUtf8TextLen8 #-}

-- | Read a UTF8-encoded text string preceded by an explicit 16-bit length.
getUtf8TextLen16 :: SGet T.Text
getUtf8TextLen16 = getInt16 >>= T.decodeUtf8' <.> getNByteString >>= \ case
    Right txt -> pure txt
    Left  err -> failSGet $ show err
{-# INLINE getUtf8TextLen16 #-}

-- | Seek to a given offset and run a parser that can consume at
-- most the given number of bytes.  The caller's state remains
-- unchanged.  Used exclusively for decoding DNS message name
-- compression.
seekSGet :: Word16 -> SGet a -> SGet a
seekSGet pos parser = do
    let off = fromIntegral pos
    len <- B.length <$> getPacket
    when (off > len) do
        failSGet "seek attempt beyond end of buffer"
    env   <- ask
    state <- gets \ s -> s { psOffset = off
                           , psLength = len - off }
    case runSGet parser env state of
        Right (ret, _) -> pure ret
        Left err       -> throw err

-- | Runs a parser on an initial segment of the unread input.
-- Consumes exactly the specified number of bytes or fails.
fitSGet :: Int -> SGet a -> SGet a
fitSGet len parser | len >= 0 = do
    s <- get
    when (psLength s < len) do
        failSGet "requested skip bytecount exceeds available"
    when (len > 0) do
        modify' $ \t -> t { psOffset = psOffset s + len
                          , psLength = psLength s - len }
    env <- ask
    case runSGet parser env s { psLength = len } of
        Right (ret, t)
            | psLength t == 0 -> pure $! ret
            | otherwise       -> failSGet "element shorter than indicated size"
        Left err -> throw err
fitSGet _ _ = failSGet "negative sanbox buffer size"
{-# INLINE fitSGet #-}

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

-- | Run a decoder with a given epoch offset over specified input
decodeAtWith :: Int64       -- ^ Current absolute offset from epoch
             -> Bool        -- ^ Support name compression?
             -> SGet a      -- ^ Decoder to run
             -> ByteString  -- ^ Buffer to run decoder over
             -> Either DNSError a
decodeAtWith t nc parser inp =
    evalSGet parser SGetEnv{..} SGetState{..}
  where
    psPacket     = inp
    psChrono     = t
    psNameComp   = nc
    psSection    = DnsNonSection
    psTriple     = Nothing
    psSource     = Nothing
    psOffset     = 0
    psLength     = B.length inp
    psLastOwner  = RootDomain
    psLastCname  = RootDomain

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

-- | Minimal Reader + State + Except Monad.
newtype SGet a = SGet { runSGet :: SGetEnv -> SGetState -> Either DNSError (a, SGetState) }

evalSGet :: SGet a -> SGetEnv -> SGetState -> Either DNSError a
evalSGet m = \r s -> fst <$> runSGet m r s
{-# INLINE evalSGet #-}

instance Functor SGet where
    fmap f m = SGet $ \r s -> do
        (a, t) <- runSGet m r s
        pure (f a, t)
    {-# INLINE fmap #-}

instance Applicative SGet where
    pure a = SGet $ \_ s -> pure (a, s)
    {-# INLINE pure #-}
    mf <*> ma = SGet $ \r s -> do
        (f, t) <- runSGet mf r s
        (a, u) <- runSGet ma r t
        pure (f a, u)
    {-# INLINE (<*>) #-}
    liftA2 f ma mb = SGet $ \r s -> do
        (a, t) <- runSGet ma r s
        (b, u) <- runSGet mb r t
        pure (f a b, u)
    {-# INLINE liftA2 #-}
    ma *> mb = SGet $ \r s -> do
        (_, t) <- runSGet ma r s
        runSGet mb r t
    {-# INLINE (*>) #-}
    ma <* mb = SGet $ \r s -> do
        (a, t) <- runSGet ma r s
        (_, u) <- runSGet mb r t
        pure (a, u)
    {-# INLINE (<*) #-}

instance Monad SGet where
    ma >>= f = SGet $ \r s -> do
        (a, t) <- runSGet ma r s
        runSGet (f a) r t
    {-# INLINE (>>=) #-}

ask :: SGet SGetEnv
ask  = SGet $ \r s -> pure (r, s)
{-# INLINE ask #-}

asks :: (SGetEnv -> a) -> SGet a
asks f = SGet $ \r s -> pure (f r, s)
{-# INLINE asks #-}

get :: SGet SGetState
get = SGet $ \_ s -> pure (s, s)
{-# INLINE get #-}

gets :: (SGetState -> a) -> SGet a
gets f = SGet $ \_ s -> pure (f s, s)
{-# INLINE gets #-}

local :: (SGetEnv -> SGetEnv) -> SGet a -> SGet a
local f m = SGet $ \ r s -> runSGet m (f r) s
{-# INLINE local #-}

modify' :: (SGetState -> SGetState) -> SGet ()
modify' f = SGet $ \_ s -> let !t = f s in pure ((), t)
{-# INLINE modify' #-}

throw :: DNSError -> SGet a
throw e = SGet $ \_ _ -> Left e
{-# INLINE throw #-}