packages feed

dnsbase-1.0.2.3: internal/Net/DNSBase/Decode/Internal/Domain.hs

-- |
-- Module      : Net.DNSBase.Decode.Internal.Domain
-- Description : TBD
-- Copyright   : (c) Viktor Dukhovni, 2026
-- License     : BSD-3-Clause
-- Maintainer  : ietf-dane@dukhovni.org
-- Stability   : unstable
module Net.DNSBase.Decode.Internal.Domain
    ( getDomain
    , getDomainNC
    ) where

import qualified Data.ByteString as B
import qualified Data.ByteString.Unsafe as BU
import qualified Data.Primitive.ByteArray as A
import Control.Monad.ST (ST, runST)
import Data.ByteString.Internal (ByteString(..))
import Data.Primitive.ByteArray (MutableByteArray)

import Net.DNSBase.Decode.Internal.State
import Net.DNSBase.Internal.Domain
import Net.DNSBase.Internal.Util

-- | Wire-form length limit, including the terminal empty root label.
maxWireLen :: Int
maxWireLen = 255

-- | Initial allocation for the output buffer.  Most names fit, so
-- typical decodes avoid resizing; the few that exceed it grow by
-- @cap + cap \`shiftR\` 1@ (1.5x) up to 'maxWireLen'.
initCap :: Int
initCap = 32

-- | Parse a wire-form domain with \"No Compression\" (i.e. treat pointer
-- labels as invalid).
--
-- When defining the decoders for newly standardized RData types, it is
-- generally required to use this function to decode transparent domain
-- fields, as name compression is explicitly forbidden for domain fields
-- of future RData types (see 'getDomain' for reference)
getDomainNC :: SGet Domain
getDomainNC = decodeAt False

-- | Parse a wire-form domain with name compression (pointer labels)
-- allowed.
--
-- This function should only be used when decoding the owner name of
-- resource records, as well as for fields of the initial set of RData
-- types defined in [RFC 1035](https://tools.ietf.org/html/rfc1035) and
-- several others listed in section 4 of
-- [RFC 3597](https://tools.ietf.org/html/rfc3597#section-4), which also
-- states that future RData types MUST NOT use name compression.
getDomain :: SGet Domain
getDomain = getNameComp >>= decodeAt

-- The shared body of 'getDomain' and 'getDomainNC'.  Snapshots the
-- input packet and the SGet cursor, runs the pointer-following walk in
-- 'ST' over a growable 'MutableByteArray', and advances the SGet
-- cursor by however many bytes were consumed at the starting position
-- (the prefix labels plus either the terminal NUL byte or the 2-byte
-- pointer label that ended the inline portion).
decodeAt :: Bool -> SGet Domain
decodeAt allowPtr = do
    pkt   <- getPacket
    start <- getPosition
    case runST (decodeDomain allowPtr pkt start) of
        Right (consumed, dom) -> dom <$ skipNBytes consumed
        Left  msg             -> failSGet msg

-- The wire-form first octet of a label determines the interpretation
-- of the rest of the label.  @11XX_XXXX@ indicates a 14-bit compression
-- pointer composed of the low 6 bits of that octet and the entirety of
-- the next octet, while @00XX_XXXX@ encodes the length (<=63) of a
-- standard label.  @01XX_XXXX@ was proposed for extended labels but
-- remains experimental, and @10XX_XXXX@ is presently undefined.
-- Current status is in the
-- [IANA registry](https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-10).
--
-- The walk reads labels at @pos@ from the packet, copying their wire
-- bytes (including each leading length byte) into the growable output
-- buffer.  A pointer label switches reading to its target offset
-- without writing anything itself; the @firstPtr@ position remembers
-- the absolute offset of the /first/ pointer encountered, so the SGet
-- cursor advance at the end can charge the caller for the prefix plus
-- the 2-byte pointer label, not for any bytes read via the pointer.
--
-- Pointer cycles are prevented by the strict-decreasing @minPtr@
-- invariant: each pointer target must be strictly less than the
-- previous one (initially the starting offset).  Since pointer offsets
-- are non-negative and bounded, the chain terminates.
decodeDomain :: forall s. Bool -> ByteString -> Int
             -> ST s (Either String (Int, Domain))
decodeDomain allowPtr pkt@(BS fp _) !start = do
    buf <- A.newByteArray initCap
    runExceptT $ walk start start initCap buf 0 Nothing
  where
    !pktLen = B.length pkt

    walk :: Int                     -- ^ current read offset in packet
         -> Int                     -- ^ strict-decreasing pointer bound
         -> Int                     -- ^ current buffer capacity
         -> MutableByteArray s      -- ^ output buffer
         -> Int                     -- ^ current write position in buffer
         -> Maybe Int               -- ^ packet offset of first pointer, if any
         -> ExceptT String (ST s) (Int, Domain)
    walk !pos !minPtr !cap !buf !cursor !firstPtr = do
        when (pos >= pktLen) $
            throwE "domain name overruns message"
        let !vl = BU.unsafeIndex pkt pos
        if | vl == 0 -> do
               -- Terminal root label: write the NUL byte and freeze.
               sbs <- lift do
                   (buf', _) <- ensureCap buf cap cursor 1
                   A.writeByteArray @Word8 buf' cursor 0
                   unsafeMBAToSBS buf' (cursor + 1)
               let !consumed = case firstPtr of
                       Just p  -> p - start + 2
                       Nothing -> pos - start + 1
               pure (consumed, Domain_ sbs)
           | vl <= 63 -> do
               -- Inline label: copy its length byte plus content bytes.
               -- The @>=@ in the wire-length check reserves space for
               -- the terminal NUL the name will eventually need.
               let !labLen = fromIntegral vl
                   !need   = 1 + labLen
               when (cursor + need >= maxWireLen) $
                   throwE "domain name too long"
               when (pos + need > pktLen) $
                   throwE "label extends past end of message"
               (buf', cap') <- lift $ ensureCap buf cap cursor need
               lift $ copyFpToMBA (fp `plusForeignPtr` pos) buf' cursor need
               walk (pos + need) minPtr cap' buf' (cursor + need) firstPtr
           | vl >= 0b1100_0000 -> do
               -- Compression pointer: jump to the target, with the
               -- target offset constrained to be strictly less than
               -- the previous bound to rule out cycles.
               unless allowPtr $
                   throwE "domain name compression not allowed in current context"
               when (pos + 2 > pktLen) $
                   throwE "compression pointer extends past end of message"
               let !vl' = BU.unsafeIndex pkt (pos + 1)
                   !off = (fromIntegral (vl .&. 0x3f) `shiftL` 8)
                          .|. fromIntegral vl'
               when (off >= minPtr) $ throwE "invalid compression pointer"
               let !firstPtr' = case firstPtr of
                       Just _  -> firstPtr
                       Nothing -> Just pos
               walk off off cap buf cursor firstPtr'
           | otherwise -> throwE "unsupported label type"

-- Grow @buf@ when @cursor + need@ would overflow @cap@.  Growth is
-- @max needed (cap + cap \`shiftR\` 1)@ (1.5x), capped at 'maxWireLen'
-- so we never allocate beyond the wire-form limit.  Returns the
-- (possibly resized) buffer and its new capacity.
ensureCap :: MutableByteArray s -> Int -> Int -> Int
          -> ST s (MutableByteArray s, Int)
ensureCap buf cap cursor need
    | needed <= cap = pure (buf, cap)
    | otherwise     = do
        let !grown  = cap + cap `shiftR` 1
            !newCap = min maxWireLen (max needed grown)
        buf' <- A.resizeMutableByteArray buf newCap
        pure (buf', newCap)
  where
    !needed = cursor + need