packages feed

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

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

module Net.DNSBase.RData.CAA
    ( -- * Certification Authority Authorisation
      T_caa(..)
    , validCaaTag
    ) where

import qualified Data.ByteString.Short as SB

import Net.DNSBase.Internal.Util

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

-- | The @CAA@ resource record
-- ([RFC 8659 section 4.1](https://www.rfc-editor.org/rfc/rfc8659.html#section-4.1))
-- — three fields: an 8-bit flag byte, an ASCII-alphanumeric property
-- /tag/ (1..255 bytes), and the property's value (free-form bytes).
--
-- Tags are compared case-sensitively when comparing 'T_caa'
-- 'RData' objects.  CAs are required by RFC 8659 to handle tags
-- case-insensitively; that's a check for application-layer code,
-- not for the wire-format codec.  Use 'validCaaTag' to verify a
-- tag's syntactic constraints before encoding.
--
-- The 'Ord' instance compares the flag byte, then tag length,
-- then tag bytes, then value bytes — wire-encoding order, so it
-- agrees with the canonical RR-content ordering of
-- [RFC 4034 section 6.2](https://datatracker.ietf.org/doc/html/rfc4034#section-6.2).
data T_caa = T_CAA
    { caaFlags :: Word8
    , caaTag   :: ShortByteString
    , caaValue :: ShortByteString }
    deriving (Eq, Show)

instance Ord T_caa where
    a `compare` b = caaFlags  a `compare` caaFlags  b
                 <> tagLength a `compare` tagLength b
                 <> caaTag    a `compare` caaTag    b
                 <> caaValue  a `compare` caaValue  b
      where
        tagLength = SB.length . caaTag

instance Presentable T_caa where
    present T_CAA{..}
        = present caaFlags
          . presentSp caaTag
          . presentSp @DnsText (coerce caaValue)

instance KnownRData T_caa where
    rdType _ = CAA
    rdEncode T_CAA{..}
        | validCaaTag caaTag = putSizedBuilder $
                                   mbWord8 caaFlags
                                <> mbShortByteStringLen8 caaTag
                                <> mbShortByteString caaValue
        | otherwise         = failWith CantEncode
    rdDecode _ _ = const do
        caaFlags <- get8
        caaTag   <- getShortByteStringLen8
        when (not $ validCaaTag caaTag) $ failSGet "CAA tag not alphanumeric"
        caaValue <- getShortByteString
        pure $ RData T_CAA{..}

-- | Verify a CAA tag's syntactic constraints: non-empty and made
-- entirely of ASCII alphanumeric bytes (RFC 8659 section 4.2).  Required
-- before encoding; the decoder applies the same check and rejects
-- a wire-form 'T_caa' whose tag fails it.
validCaaTag :: ShortByteString -> Bool
validCaaTag = (&&) <$> not . SB.null <*> SB.all isalnum
  where
    isalnum w = w - 0x30 < 10 || (w .&. 0xdf) - 0x41 < 26