packages feed

idna2008-1.0.0.0: internal/Text/IDNA2008/Internal/LabelInfo.hs

-- |
-- Module      : Text.IDNA2008.Internal.LabelInfo
-- Description : Per-label classification result.
-- Copyright   : (c) Viktor Dukhovni, 2026
-- License     : BSD-3-Clause
--
-- Maintainer  : ietf-dane@dukhovni.org
-- Stability   : unstable
--
-- A 'LabelInfo' is the parser's per-label classification result,
-- paired with the wire-form @Domain@ in the @parseDomain*@ return
-- tuple.  Opaque; inspect via the accessors:
--
--   * 'getLabelForms' returns the list of forms in label order.
--   * 'getLabelFormCount' returns the number of labels.
--   * 'getLabelForm' indexes the array; out-of-range indices
--     (including any index for the zero-label root domain's
--     'LabelInfo') return 'NoLabel'.
--   * 'allLabelFormsIn' asks whether every label belongs to a
--     given 'LabelFormSet'.
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE PatternSynonyms #-}

module Text.IDNA2008.Internal.LabelInfo
    ( -- * Opaque per-label result
      LabelInfo
    , mkLabelInfo

      -- * Accessors
    , getLabelForms
    , getLabelFormCount
    , getLabelForm

      -- * Conformance to a permitted-set
    , allLabelFormsIn
    ) where

import qualified Data.ByteString.Short as SBS
import Control.Monad.ST (ST, runST)
import Data.Array.Byte (ByteArray(..))
import Data.Bits ((.|.), (.&.), unsafeShiftL, unsafeShiftR)
import Data.ByteString.Short (ShortByteString)
import Data.ByteString.Short (ShortByteString(SBS))
import Data.Primitive.ByteArray
    ( MutableByteArray
    , newByteArray
    , readByteArray
    , setByteArray
    , unsafeFreezeByteArray
    , writeByteArray
    )
import Data.Word (Word8)

import Text.IDNA2008.Internal.LabelForm ( LabelForm(..) )
import qualified Text.IDNA2008.Internal.LabelFormSet as LFS
import Text.IDNA2008.Internal.LabelFormSet (LabelFormSet)

-- | The parser's per-label classification result.  Opaque;
-- inspect via the accessors.
data LabelInfo = LabelInfo
    {-# UNPACK #-} !Word8
    {-# UNPACK #-} !ShortByteString

instance Show LabelInfo where
    show info = show (getLabelForms info)

-- | Number of packed bytes needed to hold @n@ four-bit values.
packedBytes :: Int -> Int
packedBytes !n = (n + 1) `unsafeShiftR` 1
{-# INLINE packedBytes #-}

-- | Build a 'LabelInfo' from a list of 'LabelForm' values in
-- label order.  Internal-only; not re-exported by the public
-- API.  A 'LabelInfo' is normally produced by the parser, not
-- constructed by hand; the caller is expected to honour the DNS
-- 127-label cap, since the stored count is a 'Word8'.
mkLabelInfo :: [LabelForm] -> LabelInfo
mkLabelInfo forms =
    let !n = length forms
    in LabelInfo (fromIntegral n) (packForms n forms)

-- | Pack a list of 'LabelForm' tags into a 'ShortByteString',
-- one nibble per label.
packForms :: Int -> [LabelForm] -> ShortByteString
packForms !n forms = runST do
    let !nb = packedBytes n
    mba <- newByteArray nb
    setByteArray mba 0 nb (0 :: Word8)
    let loop !_ []     = pure ()
        loop !i (f:fs) = do
            writeForm mba i f
            loop (i + 1) fs
    loop 0 forms
    (ByteArray frozen) <- unsafeFreezeByteArray mba
    pure (SBS frozen)

-- | Write a single 'LabelForm' at logical position @i@ into the
-- mutable buffer.  Assumes the buffer is zero-initialised at
-- the relevant nibble; the write OR-merges the four bits in.
-- Even indices land in the low nibble of byte @i/2@; odd
-- indices in the high nibble.
writeForm :: forall s. MutableByteArray s -> Int -> LabelForm -> ST s ()
writeForm mba i (LabelForm_ form) = do
    let !w      = form .&. 0x0F
        !byteIx = i `unsafeShiftR` 1
        !nibOff = (i .&. 1) `unsafeShiftL` 2  -- 0 (low) or 4 (high)
    cur <- readByteArray @Word8 mba byteIx
    writeByteArray mba byteIx (cur .|. (w `unsafeShiftL` nibOff))

-- | Read the four bits at logical position @i@ from the packed
-- buffer.  Assumes @0 <= i < count@.
readForm :: ShortByteString -> Int -> Word8
readForm sbs i =
    let !byteIx = i `unsafeShiftR` 1
        !nibOff = (i .&. 1) `unsafeShiftL` 2  -- 0 (low) or 4 (high)
        !byte   = SBS.index sbs byteIx
    in (byte `unsafeShiftR` nibOff) .&. 0x0F
{-# INLINE readForm #-}

-- | The list of 'LabelForm' values, in input label order.
-- Returns an empty list for the root domain's 'LabelInfo'.
-- Lazy: callers may consume a prefix without forcing the rest.
getLabelForms :: LabelInfo -> [LabelForm]
getLabelForms (LabelInfo n sbs) =
    [ LabelForm_ (readForm sbs i) | i <- [0 .. fromIntegral n - 1] ]

-- | The number of labels.  Zero for the root domain; capped at
-- 127 in practice (the DNS limit on labels per name).
getLabelFormCount :: LabelInfo -> Int
getLabelFormCount (LabelInfo n _) = fromIntegral n
{-# INLINE getLabelFormCount #-}

-- | Indexed access.  Returns 'NoLabel' for any index outside
-- @[0, 'getLabelFormCount' - 1]@, including all indices when
-- the count is zero (the root domain).
getLabelForm :: LabelInfo -> Int -> LabelForm
getLabelForm (LabelInfo n sbs) i
    | i < 0 || i >= fromIntegral n = NoLabel
    | otherwise                    = LabelForm_ (readForm sbs i)
{-# INLINE getLabelForm #-}

-- | Does every label's form match the given 'LabelFormSet'?
--
-- Vacuously returns 'True' for the root domain's 'LabelInfo'.
-- Applications that want to reject the root domain can check the
-- value of 'getLabelFormCount', or check whether the label list
-- returned by 'Text.IDNA2008.toLabels' is 'null'.
allLabelFormsIn :: LabelInfo -- ^ The per-label forms
                -> LabelFormSet -- ^ The target label form set
                -> Bool
allLabelFormsIn info allowed =
    all (`LFS.memberLabelFormSet` allowed) (getLabelForms info)