dns-4.2.0: Network/DNS/Utils.hs
-- | Miscellaneous utility functions for processing DNS data.
--
module Network.DNS.Utils (
normalize
, normalizeCase
, normalizeRoot
, splitDomain
, splitMailbox
) where
import qualified Data.ByteString.Char8 as BS
import Data.Char (toLower)
import Network.DNS.Types.Internal (DNSError, Domain, Mailbox)
import Network.DNS.StateBinary (parseLabel)
-- | 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
-- >>> 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
-- >>> 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
-- >>> 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 "."
-- | Split a domain name in A-label form into its initial label and the rest of
-- the domain. Returns an error if the initial label is malformed. When no
-- more labels remain, the initial label will satisfy 'BS.null'.
--
-- This also decodes any escaped characters in the initial label, which may
-- therefore contain whitespace, binary data, or unescaped internal dots. To
-- reconstruct the original domain, the initial label may sometimes require
-- correct escaping of special characters.
--
-- ==== __Examples__
--
-- >>> import Data.ByteString.Char8 as BS
-- >>> splitDomain $ BS.pack "abc\\.def.xyz"
-- Right ("abc.def","xyz")
--
-- >>> splitDomain $ BS.pack ".abc.def.xyz"
-- Left (DecodeError "invalid domain: .abc.def.xyz")
--
splitDomain :: Domain -> Either DNSError (BS.ByteString, Domain)
splitDomain = parseLabel 0x2e
-- | Split a 'Mailbox' in A-label form into its initial label 'BS.ByteString'
-- (the /localpart/ of the email address) and the remaining 'Domain' (the
-- /domainpart/ of the email address, with a possible trailing @'.'@). Returns
-- an error if the initial label is malformed. When no more labels remain, the
-- initial label will satisfy 'BS.null'. The remaining labels can be obtained
-- by applying 'splitDomain' the returned domain part.
--
-- This also decodes any escaped characters in the initial label, which may
-- therefore contain whitespace, binary data, or unescaped internal dots. To
-- reconstruct the original mailbox, the initial label may sometimes require
-- correct escaping of special characters.
--
-- ==== __Example__
--
-- >>> import Data.ByteString.Char8 as BS
-- >>> splitMailbox $ BS.pack "Joe.Admin@example.com."
-- Right ("Joe.Admin","example.com.")
--
splitMailbox :: Mailbox -> Either DNSError (BS.ByteString, Domain)
splitMailbox = parseLabel 0x40