File: Utils.hs

package info (click to toggle)
haskell-dns 4.2.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 380 kB
  • sloc: haskell: 3,298; ansic: 46; makefile: 2
file content (177 lines) | stat: -rw-r--r-- 6,040 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
-- | 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