packages feed

dnsbase-1.0.0.0: internal/Net/DNSBase/EDNS/Internal/Option.hs

-- |
-- Module      : Net.DNSBase.EDNS.Internal.Option
-- Description : TBD
-- Copyright   : (c) Viktor Dukhovni, 2026
-- License     : BSD-3-Clause
-- Maintainer  : ietf-dane@dukhovni.org
-- Stability   : unstable
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DefaultSignatures #-}
module Net.DNSBase.EDNS.Internal.Option
    ( -- * EDNS option class
      KnownEdnsOption(..)
    , EdnsOption(..)
    , fromOption
    , monoOption
    , optionCode
    , putOption
    -- * EDNS option Controls
    , OptionCtl
    , optCtlSet
    , optCtlAdd
    , emptyOptionCtl
    , applyOptionCtl
    -- * Extensibility
    , OptionCodec(..)
    ) where

import Data.List (sortOn)

import Net.DNSBase.Decode.Internal.State
import Net.DNSBase.EDNS.Internal.OptNum
import Net.DNSBase.Encode.Internal.State
import Net.DNSBase.Internal.Present
import Net.DNSBase.Internal.Util

-- | EDNS option class with conversion to/from opaque 'EdnsOption' form.
class (Typeable a, Eq a, Show a, Presentable a) => KnownEdnsOption a where
    -- | The codec-consumed extension value for option type @a@.
    -- Defaults to @()@.  Options with a non-trivial extension
    -- (currently only 'Net.DNSBase.EDNS.Option.EDE.O_ede', whose extension carries the
    -- info-code name registry) supply their own associated-type
    -- definition.
    type OptionExtensionVal a :: Type
    type OptionExtensionVal a = ()

    -- | The library's built-in starting 'OptionExtensionVal' for option
    -- 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
    -- @'OptionExtensionVal' a ~ ()@ options the class default applies.
    optionExtensionVal :: forall b -> b ~ a => OptionExtensionVal a
    default optionExtensionVal :: (OptionExtensionVal a ~ ())
                               => forall b -> b ~ a
                               => OptionExtensionVal a
    optionExtensionVal _ = ()

    -- | The EDNS option number
    optNum     :: forall b -> b ~ a => OptNum

    -- | CPS option number presentation form builder.
    -- Most useful for new option values not yet known to the library.
    optPres    :: forall b -> b ~ a => Builder -> Builder

    -- | Encoder of option data to wire form
    optEncode  :: forall s r. (Typeable r, Eq r, Show r)
               => a -- ^ The value to encode
               -> SPut s r

    -- | Decoder from wire form
    optDecode  :: forall b
               -> b ~ a
               -- ^ The type to decode
               => OptionExtensionVal b
               -- ^ Its extension value
               -> Int
               -- ^ The encoded data length
               -> SGet EdnsOption

    optPres t = present $ optNum t
    {-# INLINE optPres #-}


-- | Known EDNS option Proxy + 'OptionExtensionVal' pair, paralleling
-- 'Net.DNSBase.Internal.RData.RDataCodec' on the RR-type side.
data OptionCodec where
    OptionCodec :: KnownEdnsOption a
                => Proxy a
                -> OptionExtensionVal a
                -> OptionCodec

-- | Existentially quantified type-opaque 'KnownEdnsOption', with heterogeneous
-- equality.
data EdnsOption = forall a. KnownEdnsOption a => EdnsOption a

-- | Extract specific 'KnownEdnsOption' from existential wrapping
fromOption :: forall a. KnownEdnsOption a => EdnsOption -> Maybe a
fromOption (EdnsOption a) = cast a
{-# INLINE fromOption #-}

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

instance Presentable EdnsOption where
    present (EdnsOption a)  = present a
    {-# INLINE present #-}

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

-- | The 16-bit 'Net.DNSBase.EDNS.OptNum.OptNum' codepoint of the option wrapped inside a
-- 'EdnsOption'.  Useful for filtering or grouping a heterogeneous
-- option list without unpacking each value.
optionCode :: EdnsOption -> OptNum
optionCode (EdnsOption (_ :: a)) = optNum a
{-# INLINE optionCode #-}

-- | Filter a 'Foldable' of 'EdnsOption' down to the values of a
-- single type @a@, dropping any entries whose option-code does
-- not match (or whose value is held under an 'Net.DNSBase.EDNS.Option.Opaque.OpaqueOption' even
-- though @a@ is registered for that code).  Use a type
-- application to select the target type, e.g. @'monoOption' \@'Net.DNSBase.EDNS.Option.EDE.O_ede'
-- ednsOptions@.
monoOption :: forall a t. (KnownEdnsOption a, Foldable t) => t EdnsOption -> [a]
monoOption = foldr (maybe id (:) . fromOption) []
{-# INLINE monoOption #-}

-- | Wire-form encoder for a 'EdnsOption': writes the 16-bit
-- option code followed by the length-prefixed value bytes
-- produced by the underlying 'optEncode' method.  Used by the
-- OPT pseudo-RR encoder to serialise each option in turn.
putOption :: forall s r. (Typeable r, Eq r, Show r)
          => EdnsOption -> SPut s r
putOption (EdnsOption (o :: a)) = do
    put16 $ coerce (optNum a)
    passLen (optEncode o)
{-# INLINE putOption #-}

--------

-- | The list of EDNS options carried in the OPT pseudo-RR of an
-- outgoing query.  Callers do not build an 'OptionCtl' directly;
-- they build an endomorphism @'OptionCtl' -> 'OptionCtl'@ out of
-- 'optCtlAdd' and 'optCtlSet' and pass it to the 'Net.DNSBase.Resolver.EdnsOptionCtl'
-- pattern of 'Net.DNSBase.Resolver.QueryControls'.  The endomorphism is applied to the
-- resolver's ambient option list at send time, so callers see the
-- final list as a delta on top of whatever the resolver already
-- had configured.
newtype OptionCtl = OptionCtl { fromOptionCtl :: [EdnsOption] }
    deriving (Eq, Show)

-- | Replace the option list outright with the supplied options.
-- Use this when the per-call configuration should override
-- anything the resolver had set; combine with 'optCtlAdd' for
-- partial overrides.
optCtlSet :: [EdnsOption] -> OptionCtl -> OptionCtl
optCtlSet opts _ = OptionCtl (sortOn optionCode opts)

-- | Add the supplied options to the existing list.  When the new
-- and old options share an @OPTCODE@, the new entries replace the
-- old ones with that code.
optCtlAdd :: [EdnsOption] -> OptionCtl -> OptionCtl
optCtlAdd opts (OptionCtl opts') = OptionCtl $ lbMerge (sortOn optionCode opts) opts'
  where
    -- left-biased merge that omits duplicate opcodes in the second argument only
    lbMerge [] ys = ys
    lbMerge xs [] = xs
    lbMerge xs@(x:xt) ys@(y:yt) =
        case compare (optionCode x) (optionCode y) of
            LT -> x : lbMerge xt ys
            GT -> y : lbMerge xs yt
            EQ -> lbMerge xs yt

-- | The empty option list.  Useful as the seed when running an
-- 'OptionCtl' endomorphism to inspect what it would produce.
emptyOptionCtl :: OptionCtl
emptyOptionCtl = OptionCtl []

-- | Apply an 'OptionCtl' endomorphism to a plain list of options.
-- Used by the resolver to fold per-call EDNS-option tweaks into
-- the option list it is about to put on the wire.
applyOptionCtl :: (OptionCtl -> OptionCtl) -> [EdnsOption] -> [EdnsOption]
applyOptionCtl f opts = fromOptionCtl $ f (OptionCtl opts)