packages feed

dnsbase-1.0.0.0: src/Net/DNSBase/RData/TXT.hs

{-|
Module      : Net.DNSBase.RData.TXT
Description : Text-payload RR types (TXT, HINFO, NULL)
Copyright   : (c) Viktor Dukhovni, 2026
License     : BSD-3-Clause
Maintainer  : ietf-dane@dukhovni.org
Stability   : unstable

Three RFC 1035 RR types carrying byte-string payloads.
'T_txt' is the general-purpose record holding one or more
character-strings — used by SPF, DKIM, DMARC, and many ad-hoc
TXT conventions.  'T_hinfo' was defined to describe a host's
hardware and operating system but is rarely used in modern zone
data.  'T_null' is opaque arbitrary bytes; primarily a
historical placeholder, presented using the generic RFC 3597
syntax.
-}
{-# LANGUAGE RecordWildCards #-}

module Net.DNSBase.RData.TXT
    ( T_txt(..)
    , T_hinfo(..)
    , T_null(..)
    ) where

import qualified Data.ByteString.Short as SB

import Net.DNSBase.Internal.Util

import Net.DNSBase.Bytes
import Net.DNSBase.Decode.State
import Net.DNSBase.Encode.State
import Net.DNSBase.Present
import Net.DNSBase.RData
import Net.DNSBase.RRTYPE
import Net.DNSBase.Text

-- | The @TXT@ resource record
-- ([RFC 1035 section 3.3.14](https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.14))
-- — a non-empty list of byte-strings, each at most 255 bytes long.
-- Most TXT-record conventions (SPF, DKIM, DMARC, ...) concatenate
-- the strings on read, but the wire format preserves the boundaries.
--
-- The constructor does not enforce the per-string 255-byte limit;
-- encoding fails if any individual string exceeds it.  Values
-- decoded from wire form are always within the limit by
-- construction.
--
-- The 'Ord' instance compares the strings as DNS
-- character-strings (length-prefixed lexicographic), agreeing
-- with the canonical wire-form ordering of
-- [RFC 4034 section 6.2](https://datatracker.ietf.org/doc/html/rfc4034#section-6.2).
newtype T_txt = T_TXT (NonEmpty ShortByteString) -- ^ One or more character-strings
    deriving (Eq, Show)

instance Ord T_txt where
    compare = comparing asDnsText
      where
        asDnsText :: T_txt -> NonEmpty DnsText
        asDnsText = coerce

instance Presentable T_txt where
    present (T_TXT (str :| strs)) =
        pfst str
        . flip (foldr pnxt) strs
      where
        pfst = present @DnsText . coerce
        pnxt = presentSp @DnsText . coerce

instance KnownRData T_txt where
    rdType _ = TXT
    {-# INLINE rdType #-}
    rdEncode (T_TXT strs) =
        mapM_ encodeCharString strs
    rdDecode _ _ len = do
        pos0 <- getPosition
        str  <- getShortByteStringLen8
        used <- subtract pos0 <$> getPosition
        rest <- getVarWidthSequence getShortByteStringLen8 (len - used)
        pure $ RData $ T_TXT $ str :| rest

-- | The @HINFO@ resource record
-- ([RFC 1035 section 3.3.2](https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.2))
-- — host information: a /CPU/ character-string and an /OS/
-- character-string describing the named host's hardware and
-- operating system.  Rarely used in modern zone data; RFC 8482
-- reuses the type code as a placeholder answer for @ANY@ queries.
--
-- > +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
-- > /                      CPU                      /
-- > +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
-- > /                       OS                      /
-- > +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
--
-- The 'Ord' instance compares both fields as DNS
-- character-strings, giving canonical ordering
-- ([RFC 4034 section 6.2](https://datatracker.ietf.org/doc/html/rfc4034#section-6.2)).
data T_hinfo = T_HINFO
    { hinfoCPU :: ShortByteString
    , hinfoOS  :: ShortByteString
    } deriving (Eq, Show)

instance Ord T_hinfo where
    a `compare` b = hinfoCPU a `strCompare` hinfoCPU b
                 <> hinfoOS  a `strCompare` hinfoOS  b
      where
        strCompare = comparing DnsText

instance Presentable T_hinfo where
    present T_HINFO{..} =
        present     @DnsText (coerce hinfoCPU)
        . presentSp @DnsText (coerce hinfoOS)

instance KnownRData T_hinfo where
    rdType _ = HINFO
    {-# INLINE rdType #-}
    rdEncode T_HINFO{..} = do
        encodeCharString hinfoCPU
        encodeCharString hinfoOS
    rdDecode _ _ = const do
        RData <$.> T_HINFO <$> getShortByteStringLen8 <*> getShortByteStringLen8

-- | The @NULL@ resource record
-- ([RFC 1035 section 3.3.10](https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.10))
-- — arbitrary opaque bytes (up to 65535).  Rarely seen on the
-- wire; presented using the generic
-- [RFC 3597 section 5](https://datatracker.ietf.org/doc/html/rfc3597#section-5)
-- syntax (@\\\# /n/ /hex/@).
--
-- > +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
-- > /                  <anything>                   /
-- > /                                               /
-- > +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
--
-- Derived 'Ord' is canonical
-- ([RFC 4034 section 6.2](https://datatracker.ietf.org/doc/html/rfc4034#section-6.2)).
newtype T_null = T_NULL Bytes16
    deriving (Eq, Ord, Show)

instance Presentable T_null where
    present (T_NULL val) =
        present @String "\\# "
        . present (SB.length $ coerce val)
        . presentSp val

instance KnownRData T_null where
    rdType _ = NULL
    {-# INLINE rdType #-}
    rdEncode = putShortByteString . coerce
    rdDecode _ _ = RData . T_NULL . coerce <.> getShortNByteString

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

-- | Encode a DNS /character-string/ (explicit one byte length).
encodeCharString :: ShortByteString -> SPut s RData
encodeCharString = putShortByteStringLen8