packages feed

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

-- |
-- Module      : Text.IDNA2008.Internal.Property
-- Description : IDNA2008 codepoint disposition lookup
-- Copyright   : (c) Viktor Dukhovni, 2026
-- License     : BSD-3-Clause
--
-- Maintainer  : ietf-dane@dukhovni.org
-- Stability   : unstable
--
-- The IDNA disposition of a Unicode codepoint per
-- <https://datatracker.ietf.org/doc/html/rfc5892 RFC 5892> (and as
-- summarised by the IANA IDNA Parameters registry).  Each codepoint
-- falls into exactly one of:
--
--   * @PVALID@      -- permitted in U-labels with no further conditions
--   * @CONTEXTJ@    -- permitted only when the per-codepoint contextual
--                     /joining/ rule is satisfied (currently U+200C
--                     ZWNJ and U+200D ZWJ; rules in RFC 5892 section A.1, section A.2)
--   * @CONTEXTO@    -- permitted only when the per-codepoint contextual
--                     /other/ rule is satisfied (rules in RFC 5892 section A.3+)
--   * @DISALLOWED@  -- not permitted in any U-label
--   * @UNASSIGNED@  -- not permitted; reserved for future Unicode
--                     assignments
--
-- The lookup is implemented as a binary search over a sorted array of
-- range starts.  Each entry says \"from this codepoint until the next
-- entry's start, the disposition is /D/\".  Total per-call cost is
-- @O(log N)@ array reads and zero allocations.
--
-- == Coverage
--
-- The full per-codepoint table is loaded from
-- "Text.IDNA2008.Internal.Property.Data", which is machine-generated
-- from the IANA @idna-tables-properties.csv@ registry.  As of
-- 2024-04-26 the most recent IANA-curated derived table is aligned with
-- /Unicode 12.0.0/; the IETF position is that applications which need
-- newer Unicode coverage may compute derived properties themselves per
-- RFC 5892 section 2 -- only the contextual rules are normative across versions.
--
-- Codepoints assigned in Unicode 12.1.0 or later that are not covered
-- by the 12.0.0 derivation will be reported as @UNASSIGNED@ here, which
-- this validator rejects as not-a-U-label.  That is consistent with the
-- IETF reference and conservative for DNS use; if you specifically need
-- to admit newer codepoints, regenerate the data module from a derived
-- table you compute against the desired Unicode version.
module Text.IDNA2008.Internal.Property
    ( -- * Disposition
      IdnaDisposition(..)
      -- * Lookup
    , idnaDisposition
    ) where

import Data.Bits (unsafeShiftR)
import Data.Primitive.ByteArray (indexByteArray)
import Data.Word (Word8, Word32)

import Text.IDNA2008.Internal.Property.Data
    ( rangeCount
    , rangeStarts
    , rangeTags
    )

----------------------------------------------------------------------
-- Disposition type
----------------------------------------------------------------------

-- | IDNA2008 codepoint disposition.
data IdnaDisposition
    = IdnaPVALID
    | IdnaCONTEXTJ
    | IdnaCONTEXTO
    | IdnaDISALLOWED
    | IdnaUNASSIGNED
    deriving (Eq, Show, Enum, Bounded)

-- | Map an internal 'Word8' tag back to a disposition.  Tags
-- @0..4@ correspond to the constructors in declaration order.
{-# INLINE word8ToDisp #-}
word8ToDisp :: Word8 -> IdnaDisposition
word8ToDisp w = case w of
    0 -> IdnaPVALID
    1 -> IdnaCONTEXTJ
    2 -> IdnaCONTEXTO
    3 -> IdnaDISALLOWED
    _ -> IdnaUNASSIGNED

----------------------------------------------------------------------
-- Public lookup
----------------------------------------------------------------------

-- | Return the IDNA2008 disposition of a codepoint.  Out-of-range
-- inputs (negative or greater than @0x10FFFF@) report
-- 'IdnaDISALLOWED'.
idnaDisposition :: Int -> IdnaDisposition
idnaDisposition !cp
    | cp < 0 || cp > 0x10FFFF = IdnaDISALLOWED
    | otherwise               = word8ToDisp (lookupTag cp)
{-# INLINE idnaDisposition #-}

----------------------------------------------------------------------
-- Binary search over the codegen'd range table
----------------------------------------------------------------------

-- | Binary search for the largest index @i@ in @[0 .. rangeCount-1]@
-- such that @rangeStarts[i] <= cp@; return the disposition tag at that
-- index.  Precondition: @cp >= 0 && cp <= 0x10FFFF@; combined with the
-- table starting at @0x000000@ this guarantees a hit.
{-# INLINE lookupTag #-}
lookupTag :: Int -> Word8
lookupTag !cp = go 0 (rangeCount - 1)
  where
    !target = fromIntegral cp :: Word32

    go !lo !hi
      | lo >= hi  = readTag lo
      | otherwise =
          let !mid     = (lo + hi + 1) `unsafeShiftR` 1
              !midKey  = readStart mid
          in if midKey <= target
               then go mid hi
               else go lo (mid - 1)

    readStart i = indexByteArray rangeStarts i :: Word32
    readTag   i = indexByteArray rangeTags   i :: Word8