-- | Miscellaneous utility functions for processing DNS data.
--
module Network.DNS.Utils (
    normalize
  , normalizeCase
  , normalizeRoot
  ) where

import qualified Data.ByteString.Char8 as BS (
    append
  , last
  , map
  , null
  , pack )
import Data.Char ( toLower )

import Network.DNS.Types ( Domain )


-- | Perform both 'normalizeCase' and 'normalizeRoot' on the given
--   'Domain'. When comparing DNS names taken from user input, this is
--   often necessary to avoid unexpected results.
--
--   /Examples/:
--
--   >>> let domain1 = BS.pack "ExAmPlE.COM"
--   >>> let domain2 = BS.pack "example.com."
--   >>> domain1 == domain2
--   False
--   >>> normalize domain1 == normalize domain2
--   True
--
--   The 'normalize' function should be idempotent:
--
--   >>> normalize (normalize domain1) == normalize domain1
--   True
--
--   Ensure that we don't crash on the empty 'Domain':
--
--   >>> import qualified Data.ByteString.Char8 as BS ( empty )
--   >>> normalize BS.empty
--   "."
--
normalize :: Domain -> Domain
normalize = normalizeCase . normalizeRoot


-- | Normalize the case of the given DNS name for comparisons.
--
--   According to RFC #1035, \"For all parts of the DNS that are part
--   of the official protocol, all comparisons between character
--   strings (e.g., labels, domain names, etc.) are done in a
--   case-insensitive manner.\" This function chooses to lowercase
--   its argument, but that should be treated as an implementation
--   detail if at all possible.
--
--   /Examples/:
--
--   >>> let domain1 = BS.pack "ExAmPlE.COM"
--   >>> let domain2 = BS.pack "exAMPle.com"
--   >>> domain1 == domain2
--   False
--   >>> normalizeCase domain1 == normalizeCase domain2
--   True
--
--   The 'normalizeCase' function should be idempotent:
--
--   >>> normalizeCase (normalizeCase domain2) == normalizeCase domain2
--   True
--
--   Ensure that we don't crash on the empty 'Domain':
--
--   >>> import qualified Data.ByteString.Char8 as BS ( empty )
--   >>> normalizeCase BS.empty
--   ""
--
normalizeCase :: Domain -> Domain
normalizeCase = BS.map toLower


-- | Normalize the given name by appending a trailing dot (the DNS
--   root) if one does not already exist.
--
--   Warning: this does not produce an equivalent DNS name! However,
--   users are often unaware of the effect that the absence of the
--   root will have. In user interface design, it may therefore be
--   wise to act as if the user supplied the trailing dot during
--   comparisons.
--
--   Per RFC #1034,
--
--   \"Since a complete domain name ends with the root label, this leads
--   to a printed form which ends in a dot. We use this property to
--   distinguish between:
--
--   * a character string which represents a complete domain name
--     (often called \'absolute\'). For example, \'poneria.ISI.EDU.\'
--
--   * a character string that represents the starting labels of a
--     domain name which is incomplete, and should be completed by
--     local software using knowledge of the local domain (often
--     called \'relative\'). For example, \'poneria\' used in the
--     ISI.EDU domain.
--
--   Relative names are either taken relative to a well known origin,
--   or to a list of domains used as a search list. Relative names
--   appear mostly at the user interface, where their interpretation
--   varies from implementation to implementation, and in master
--   files, where they are relative to a single origin domain name.\"
--
--   /Examples/:
--
--   >>> let domain1 = BS.pack "example.com"
--   >>> let domain2 = BS.pack "example.com."
--   >>> domain1 == domain2
--   False
--   >>> normalizeRoot domain1 == normalizeRoot domain2
--   True
--
--   The 'normalizeRoot' function should be idempotent:
--
--   >>> normalizeRoot (normalizeRoot domain1) == normalizeRoot domain1
--   True
--
--   Ensure that we don't crash on the empty 'Domain':
--
--   >>> import qualified Data.ByteString.Char8 as BS ( empty )
--   >>> normalizeRoot BS.empty
--   "."
--
normalizeRoot :: Domain -> Domain
normalizeRoot d
  | BS.null d = trailing_dot
  | BS.last d == '.' = d
  | otherwise = d `BS.append` trailing_dot
    where
      trailing_dot = BS.pack "."