packages feed

idna2008-1.0.0.0: src/Text/IDNA2008.hs

-- |
-- Module      : Text.IDNA2008
-- Description : Strict IDNA2008 parser and renderer.
-- Copyright   : (c) Viktor Dukhovni, 2026
-- License     : BSD-3-Clause
--
-- Maintainer  : ietf-dane@dukhovni.org
-- Stability   : unstable
--
-- Given a domain name as the user typed it — possibly mixing
-- Latin, Greek, Hebrew, Arabic, CJK, or any other Unicode script
-- — this library checks that every label is well-formed, encodes
-- any non-ASCII labels into their ACE-prefixed Punycode
-- form for the wire, classifies each label, and (optionally)
-- renders the parsed name back into display form.
--
-- == Quick start
--
-- > {-# LANGUAGE OverloadedStrings #-}
-- > module Main(main) where
-- > import qualified Data.Text.IO as T
-- > import Text.IDNA2008
-- >
-- > main :: IO ()
-- > main = do
-- >     -- Strict-IDN parse + render two ways.  'mkDomain' uses
-- >     -- 'idnLabelForms' and 'defaultIdnaFlags'.
-- >     case mkDomain "www.αβγ.gr" of
-- >         Right dom -> do
-- >             T.putStrLn $ domainToUnicode dom
-- >             T.putStrLn $ domainToAscii  dom
-- >         Left err -> print err
-- >
-- >     -- The full parser returns the wire-form 'Domain' /and/ the
-- >     -- per-label classification.  'allLabelForms' admits every
-- >     -- label class a DNS zone file might carry plus 'ULABEL',
-- >     -- so '*' and '_tcp' coexist with the Unicode label.
-- >     case parseDomain allLabelForms "*._tcp.αβγ.gr" of
-- >         Right (_dom, info) -> print (getLabelForms info)
-- >         Left err           -> print err
--
-- Which produces:
--
-- > ghci> main
-- > www.αβγ.gr
-- > www.xn--mxacd.gr
-- > [WILDLABEL,ATTRLEAF,ULABEL,LDH]
--
-- == Strictness
--
-- This library implements /strict/ IDNA2008 (RFCs 5891-5894
-- including RFC 5893 Bidi rules and RFC 3492 Punycode).  Optional
-- RFC5895 mappings are available when parsing text inputs.  A
-- basic set of CJK mappings from UTS #46 can also be enabled, and
-- implies the full set of RFC5895 mappings.
--
-- == Terminology: \"encode\" and \"decode\"
--
-- The library uses /encode/ and /decode/ in the IDNA-spec
-- sense, centred on the Punycode transformation between a
-- U-label and its ACE-prefixed (A-label) encoded form:
--
--   * 'parseDomainOpts' (presentation -> wire) is the /encoding/
--     side: non-ASCII labels that arrive in U-label form are
--     Punycode-encoded to their ACE-prefixed forms used "on the wire"
--     in DNS queries.
--
--   * 'unparseDomainOpts' (wire -> presentation) is the /decoding/
--     side: ACE-prefixed labels are Punycode-decoded back to their
--     U-label forms for display.
--
-- This terminology is natural for transformations between
-- U-labels and A-labels.  ASCII LDH labels pass through both
-- directions byte-for-byte.  The /encoding/ of 'OCTET' labels
-- (non-LDH bytes admitted as raw octets) to wire form requires
-- decoding backslash-escaped characters or @\\DDD@ decimal
-- triples from their presentation form, and /decoding/ of these
-- labels from wire form to presentation form requires encoding
-- many non-LDH octets as escaped characters or decimal triples.
module Text.IDNA2008
    ( -- * Domain names
      Domain(..)
    , toLabels
    , isValidWireForm
    , wireBytes
    , wireBytesShort

      -- * Parsing presentation form
      -- $parsing

      -- ** Returning wire form plus per-label classification
    , parseDomain
    , parseDomainOpts
    , parseDomainUtf8
    , parseDomainShort

      -- ** Domain-only convenience wrappers
      -- $convenience
    , mkDomain
    , mkDomainStr
    , mkDomainUtf8
    , mkDomainShort

      -- ** Compile-time literals
      -- $literals
    , dnLit

      -- * Rendering wire form back to presentation form
    , domainToAscii
    , domainToUnicode
    , labelToAscii
    , labelToUnicode
    , unparseDomainOpts
    , unparseLabelOpts

      -- * Per-label classification result
      -- $labelinfo
    , LabelInfo
    , getLabelForms
    , getLabelFormCount
    , getLabelForm
    , allLabelFormsIn

      -- * Label form singletons
      -- $labelform
    , LabelForm
        ( LDH
        , RLDH
        , FAKEA
        , ALABEL
        , ULABEL
        , ATTRLEAF
        , OCTET
        , WILDLABEL
        , LAXULABEL
        , NoLabel
        )

      -- * Label form sets
      -- $labelformset
    , LabelFormSet
    , (<+>)
    , (<->)
    , labelFormToSet
    , memberLabelFormSet
    , withoutLabelFormSet
    , allLabelForms
    , idnLabelForms
    , hostnameLabelForms

      -- * Parser flags
      -- $flags
    , IdnaFlags
        ( ALABELCHECK
        , NFCCHECK
        , EMOJIOK
        , MAPDOTS
        , MAPNFC
        , MAPCASE
        , MAPWIDTH
        , BIDICHECK
        , ASCIIFALLBACK
        , MAPUTS46
        )
    , allIdnaMappings
    , defaultIdnaFlags
    , meetsIdnaFlags
    , withoutIdnaFlags

      -- * Errors
    , IdnaError(..)
    , LabelReason(..)
    , AceReason(..)
    , BidiRuleViolation(..)

      -- * CLI option parsers
      -- $cli
    , parseIdnaFlags
    , parseIdnaFlagsStr
    , parseLabelFormSet
    , parseLabelFormSetStr
    ) where

import Text.IDNA2008.Internal.Error
    ( AceReason(..)
    , BidiRuleViolation(..)
    , IdnaError(..)
    , LabelReason(..)
    )
import Text.IDNA2008.Internal.Flags
    ( IdnaFlags
        ( ALABELCHECK
        , NFCCHECK
        , EMOJIOK
        , MAPDOTS
        , MAPNFC
        , MAPCASE
        , MAPWIDTH
        , BIDICHECK
        , ASCIIFALLBACK
        , MAPUTS46
        )
    , allIdnaMappings
    , defaultIdnaFlags
    , meetsIdnaFlags
    , parseIdnaFlags
    , parseIdnaFlagsStr
    , withoutIdnaFlags
    )
import Text.IDNA2008.Internal.LabelForm
    ( LabelForm
        ( LDH
        , RLDH
        , FAKEA
        , ALABEL
        , ULABEL
        , ATTRLEAF
        , OCTET
        , WILDLABEL
        , LAXULABEL
        , NoLabel
        )
    )
import Text.IDNA2008.Internal.LabelFormSet
    ( LabelFormSet
    , (<+>)
    , (<->)
    , labelFormToSet
    , allLabelForms
    , hostnameLabelForms
    , idnLabelForms
    , memberLabelFormSet
    , parseLabelFormSet
    , parseLabelFormSetStr
    , withoutLabelFormSet
    )
import Text.IDNA2008.Internal.LabelInfo
    ( LabelInfo
    , getLabelForm
    , getLabelFormCount
    , getLabelForms
    , allLabelFormsIn
    )
import Text.IDNA2008.Internal.Parse
    ( Domain(..)
    , dnLit
    , domainToAscii
    , domainToUnicode
    , labelToAscii
    , labelToUnicode
    , unparseDomainOpts
    , unparseLabelOpts
    , mkDomain
    , mkDomainShort
    , mkDomainStr
    , mkDomainUtf8
    , parseDomain
    , parseDomainOpts
    , parseDomainShort
    , parseDomainUtf8
    , isValidWireForm
    , toLabels
    , wireBytes
    , wireBytesShort
    )

-- $parsing
-- The four parsing entry points share a shape: pass the
-- 'LabelFormSet' the application accepts (typically
-- 'idnLabelForms' or 'hostnameLabelForms') plus the input,
-- get back either a parse error or the parsed 'Domain' paired
-- with the per-label classification ('LabelInfo').  They differ
-- only in input type, chosen to match what the caller already
-- has on hand:
--
-- * 'parseDomain' takes 'Data.Text.Text'.  Since
--   'Data.Text.Text' enforces well-formed UTF-8 at
--   construction, the parser views the underlying byte array
--   directly with no extra copy or re-validation.
-- * 'parseDomainOpts' is the same as 'parseDomain' but takes
--   an explicit 'IdnaFlags' bitmask (the other three use
--   'defaultIdnaFlags').
-- * 'parseDomainUtf8' takes a UTF-8-encoded
--   'Data.ByteString.ByteString' — the convenient entry
--   point when the caller already has raw bytes (a network
--   read, a file, a protocol decoder) and would otherwise
--   pay a 'Data.Text.Encoding.decodeUtf8' just to reach
--   'parseDomain'.  One copy is unavoidable because
--   'Data.ByteString.ByteString' is pinned and the parser
--   wants an unpinned buffer.
-- * 'parseDomainShort' is the zero-copy variant for callers
--   who already have a 'Data.ByteString.Short.ShortByteString'.
--
-- The 'Data.ByteString.ByteString' and
-- 'Data.ByteString.Short.ShortByteString' variants are /not/
-- assumed to be well-formed UTF-8; the parser performs a
-- strict RFC 3629 decode and reports @'ErrInvalidUtf8'@ on any
-- ill-formed sequence.

-- $convenience
-- The 'mkDomain' convenience functions use defaults for both the
-- allowed label forms and the validation flags.  They return just
-- the wire-form 'Domain' without the list of label classifications.

-- $literals
-- Compile-time literal domains via Template Haskell, by way of
-- the 'dnLit' splice primitive.  An invalid name becomes a
-- compile-time error rather than a runtime failure.

-- $labelinfo
-- 'LabelInfo' is the parser's per-label classification result.
-- The representation is opaque; inspect via the accessors.
--
-- * 'getLabelForms' returns the list in label order.
-- * 'getLabelFormCount' returns the label count.
-- * 'getLabelForm' indexes the list starting at @0@ for the first
--    label.
-- * 'allLabelFormsIn' tests whether every label belongs to a
--   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'.

-- $labelform
-- Each label falls into one of nine classifications:
--
-- * 'LDH' — a valid label of letters, digits, and hyphens.
-- * 'RLDH' — a legacy reserved label with @--@ at positions 3-4.
-- * 'FAKEA' — an ACE-prefixed label that isn't a valid A-label.
-- * 'ALABEL' — an ACE-prefixed label that encodes a valid IDN label.
-- * 'ULABEL' — a non-ASCII label that can be part of a valid IDN.
-- * 'ATTRLEAF' — an underscore-prefixed label (e.g. @_25._tcp@).
-- * 'OCTET' — a label with characters outside the LDH alphabet.
-- * 'WILDLABEL' — the DNS wildcard label @*@.
-- * 'LAXULABEL' — a U-label that fails strict IDN validation;
--   not admitted unless the caller explicitly opts in.
--
-- A tenth sentinel ('NoLabel') is returned by 'getLabelForm' for
-- out-of-range indices (every index for the root domain which has
-- zero labels).  The parser never produces 'NoLabel' for a real
-- label, and it is not a member of any 'LabelFormSet'.

-- $labelformset
-- 'LabelFormSet' is the parser's @allowed@ argument: a set of
-- 'LabelForm' values the parser is willing to admit.

-- $flags
-- 'IdnaFlags' is a bitmask of parser options.
--
-- The default ('defaultIdnaFlags') is 'ALABELCHECK' '<>'
-- 'NFCCHECK' '<>' 'BIDICHECK': enforce strict A-label round-trip,
-- require NFC, and apply the cross-label Bidi rules.  Additional
-- non-default flags include:
--
-- * 'MAPCASE' - Fold input to lower case.
-- * 'MAPDOTS' - Accept additional Unicode label separators.
-- * 'MAPWIDTH' - Map wide/narrow codepoints.
-- * 'MAPNFC' - Normalise input to composed form (NFC).
-- * 'MAPUTS46' - A small set of UTS #46 mappings plus all the above.
-- * 'EMOJIOK' - Accept most emoji in labels.
-- * 'ASCIIFALLBACK' - Decode to A-labels when a domain violates BIDI rules

-- $cli
-- Convenience functions for command-line option parsers.
--
-- Parse comma-separated lists with @+@\/@-@ prefixes and accept
-- either an exact match or any unambiguous three-or-more letter
-- prefix.