packages feed

dnsbase-1.0.0.0: internal/Net/DNSBase/Internal/RData.hs

-- |
-- Module      : Net.DNSBase.Internal.RData
-- Description : TBD
-- Copyright   : (c) Viktor Dukhovni, 2026
-- License     : BSD-3-Clause
-- Maintainer  : ietf-dane@dukhovni.org
-- Stability   : unstable
{-# LANGUAGE RecordWildCards #-}

{-# LANGUAGE DefaultSignatures #-}
module Net.DNSBase.Internal.RData
    ( -- * RData class
      RData(..)
    , fromRData
    , monoRData
    , rdataType
    , rdataEncode
    , rdataEncodeCanonical
      -- ** Opaque RData
    , OpaqueRData(..)
    , opaqueRData
    , toOpaqueRData
      -- ** Extensibility
    , KnownRData(..)
    , RDataCodec(..)
    , RDataMap
    ) where

import qualified Data.ByteString as B
import qualified Data.ByteString.Short as SB
import Data.IntMap (IntMap)

import Net.DNSBase.Decode.Internal.State
import Net.DNSBase.Encode.Internal.State
import Net.DNSBase.Internal.Bytes
import Net.DNSBase.Internal.Nat16
import Net.DNSBase.Internal.Present
import Net.DNSBase.Internal.RRTYPE
import Net.DNSBase.Internal.Util

-- | Abstract DNS Resource Record (type-specific) data.
--
-- The decoding, encoding and presentation functions are responsible for just
-- the value, presentation of the associated RR type defaults to the built-in
-- names, for novel types override 'rdTypePres'.
--
-- The 'Show' instance is typically derived, and will output the type
-- constructor (its output strives to produce syntactically valid Haskell
-- values), in contrast with 'Presentable' which produces RFC-standard
-- presentation forms.
class ( Typeable a, Eq a, Ord a, Show a, Presentable a
      ) => KnownRData a where
    -- | The codec-consumed extension value for type @a@.  Defaults
    -- to @()@.  Types with non-trivial extension data (SVCB and
    -- HTTPS, which carry the SvcParam decoder map) supply their
    -- own associated-type definition.
    type RDataExtensionVal a :: Type
    type RDataExtensionVal a = ()

    -- | The library's built-in starting 'RDataExtensionVal' for type
    -- @a@.  Used as the baseline when the library installs its
    -- built-in registration for @a@, and as the starting point
    -- when the user extends the codec for @a@.  For
    -- @'RDataExtensionVal' a ~ ()@ types the class default applies.
    rdataExtensionVal :: forall b -> b ~ a => RDataExtensionVal a
    default rdataExtensionVal :: (RDataExtensionVal a ~ ())
                              => forall b -> b ~ a => RDataExtensionVal a
    rdataExtensionVal _ = ()

    rdType     :: forall b -> b ~ a => RRTYPE
    rdTypePres :: forall b -> b ~ a => Builder -> Builder
    rdDecode   :: forall b -> b ~ a => RDataExtensionVal a -> Int -> SGet RData
    -- Default encoding
    rdEncode   :: a -> SPut s RData
    -- Canonical encoding for DNSSEC validation.
    cnEncode   :: a -> SPut s RData
    cnEncode    = rdEncode

    -- | Override for user-friendly types for non-built-in types added at
    -- runtime (as part of resolver configuration).  Otherwise, defaults to
    -- @TYPE@/number/.
    rdTypePres _ = present $ rdType a
    {-# INLINE rdTypePres #-}

-- | Wrapper around any concrete 'KnownRData' type.
--
-- Its presentation form includes both the type and the value, space-separated.
-- The underlying concrete types present just their values.
data RData = forall a. KnownRData a => RData a

-- | Recover a typed RR payload from the existential 'RData' wrapper.
-- Returns @'Just' x@ when the dynamic payload's type matches the
-- caller's expected type @a@, and 'Nothing' otherwise.
--
-- The target type is selected by the result-side pattern; once
-- there's a concrete constructor on the @Just@ side, the
-- @'KnownRData' a@ constraint is resolved without an explicit type
-- ascription.  A typical use is a view-pattern dispatch that
-- handles two or more RR types at once:
--
-- > evalIP :: (IP -> a) -> RData -> Maybe a
-- > evalIP f (fromRData -> Just (T_A    ip)) = Just $! f (IPv4 ip)
-- > evalIP f (fromRData -> Just (T_AAAA ip)) = Just $! f (IPv6 ip)
-- > evalIP _ _                               = Nothing
--
-- 'fromRData' is the right tool when the value in hand is already
-- an 'RData'.  If you instead have an 'Net.DNSBase.RR.RR' (or a list of them, as
-- returned by 'Net.DNSBase.Lookup.lookupAnswers'), 'Net.DNSBase.RR.rrDataCast' is the convenience
-- composition @'fromRData' . 'Net.DNSBase.RR.rrData'@.  And 'monoRData' performs
-- the filter-and-cast over a 'Foldable' container in one step.
fromRData  :: forall a. KnownRData a => RData -> Maybe a
fromRData (RData a) = cast a
{-# INLINE fromRData #-}

instance Show RData where
    showsPrec p (RData a) = showsP p $ showString "RData " . shows' a

-- | Presents the type and value, space-separated.
instance Presentable RData where
    present (RData (a :: t)) = rdTypePres t . presentSp a

-- | Known RData Proxy + Codec parameter pair
data RDataCodec where
    RDataCodec :: KnownRData a
              => Proxy a
              -> RDataExtensionVal a
              -> RDataCodec

-- | Map associating a type-specific length-aware 'RData' decoder
-- to each 'Net.DNSBase.RRTYPE.RRTYPE'
type RDataMap = IntMap RDataCodec

-- | Filter a 'Foldable' of 'RData' down to the elements whose
-- payload type matches the caller's target @a@, returning a
-- monomorphic list of those typed values.  Elements with a
-- different payload type are dropped.
--
-- For example, the @T_mx@ payloads from a mixed 'RData' list:
--
-- > mxs :: [RData] -> [T_mx]
-- > mxs = monoRData
--
-- Equivalent to @'Data.Maybe.mapMaybe' 'fromRData' . 'Data.Foldable.toList'@,
-- but in one fused pass.  See 'fromRData' for the single-element
-- cast, and 'Net.DNSBase.RR.rrDataCast' for the 'Net.DNSBase.RR.RR'-input analogue.
monoRData :: forall a t. (KnownRData a, Foldable t) => t RData -> [a]
monoRData = foldr (maybe id (:) . fromRData) []
{-# INLINE monoRData #-}

{-# INLINE rdataType #-}
rdataType :: RData -> RRTYPE
rdataType (RData (_ :: t)) = rdType t

instance Eq RData where
    (RData (_a :: a)) == (RData (_b :: b)) =
        case teq a b of
            Just Refl -> _a == _b
            _         -> False

-- | Compare RData first by RRtype number, then by content.
-- When two RRtype numbers match, but the data types nevertheless differ, order
-- opaque type after non-opaque.  In the unlikely case of two non-opaque types
-- with the same RRtype, compare their opaque encodings (this could throw an
-- error if one of the objects is not encodable, perhaps because encoding would
-- be too long).
instance Ord RData where
    ra@(RData (_a :: a)) `compare` rb@(RData (_b :: b)) =
        compare (rdType a) (rdType b)
        <> if | Just Refl <- teq a b -> compare _a _b
              | isOpaque (rdType a) ra -> GT
              | isOpaque (rdType b) rb -> LT
              | otherwise -> ocmp (toOpaqueRData ra) (toOpaqueRData rb)
      where
        ocmp (Right oa) (Right ob) = compare oa ob
        ocmp (Left e)   _          = error $ show e
        ocmp _          (Left e)   = error $ show e

-- | Perform a default encoding of the contained 'KnownRData'.
rdataEncode :: RData -> SPut s RData
rdataEncode rd@(RData a) = setContext rd $ rdEncode a

-- | Perform a canonical encoding of the contained 'KnownRData'.
rdataEncodeCanonical :: RData -> SPut s RData
rdataEncodeCanonical rd@(RData a) = setContext rd $ cnEncode a

-- | Opaque 'RData', for RRTYPEs not known at runtime
--
data OpaqueRData n = Nat16 n => OpaqueRData ShortByteString
deriving instance Eq (OpaqueRData n)
deriving instance Ord (OpaqueRData n)
instance Show (OpaqueRData n) where
    showsPrec p (OpaqueRData bs) = showsP p $
        showString "OpaqueRData @"
        . shows (natToWord16 n) . showChar ' '
        . shows @Bytes16 (coerce bs)

instance Presentable (OpaqueRData n) where
    present (OpaqueRData val) =
        present "\\#"
        . presentSp (SB.length val)
        . present16 val
      where
        present16 = presentSp @Bytes16 . coerce

instance Nat16 n => KnownRData (OpaqueRData n) where
    rdType _ = RRTYPE $ natToWord16 n
    rdTypePres _ = present "TYPE"
                 . present (natToWord16 n)
    rdEncode (OpaqueRData bs) = putShortByteString bs
    rdDecode _ _ = RData . OpaqueRData @n <.> getShortNByteString

-- | Create opaque RData from its type number and Bytes16 value
opaqueRData :: Word16 -> ShortByteString -> RData
opaqueRData w bs = withNat16 w go
  where
    go :: forall (n :: Nat) -> Nat16 n => RData
    go n = RData $ (OpaqueRData bs :: OpaqueRData n)

-- | Convert 'RData' to its /opaque/ equivalent of the same RRtype.
-- 'OpaqueRData' values will be returned as-is.  Otherwise, this will attempt
-- to encode the record without name compression, the encoding may fail, in
-- which case the return value will be 'Nothing'.
--
toOpaqueRData :: RData -> Either (EncodeErr (Maybe RData)) RData
toOpaqueRData rd@(rdataType -> rt) = withNat16 (coerce rt) go
  where
    go :: forall (n :: Nat) -> Nat16 n => Either (EncodeErr (Maybe RData)) RData
    go n | isOpaque rt rd = Right rd
         | otherwise
           = RData . mkopaque <$> encodeVerbatim do rdataEncode rd
             where
               mkopaque :: B.ByteString -> OpaqueRData n
               mkopaque bs = OpaqueRData $ SB.toShort bs

-- | Check whether the given 'RData' is opaque of given RRtype.
--
isOpaque :: RRTYPE -> RData -> Bool
isOpaque rt rd = withNat16 (coerce rt) go
  where
    go :: forall (n :: Nat) -> Nat16 n => Bool
    go n = isJust (fromRData rd :: Maybe (OpaqueRData n))