1use std::error::Error as StdError;
3use std::fmt;
4
5#[derive(Clone, Eq, Hash, PartialEq, Debug)]
7pub struct DnsName(String);
8
9impl<'a> DnsName {
10 pub fn borrow(&'a self) -> DnsNameRef<'a> {
12 DnsNameRef(self.as_ref())
13 }
14
15 pub fn try_from_ascii(bytes: &[u8]) -> Result<Self, InvalidDnsNameError> {
17 String::from_utf8(bytes.to_vec())
20 .map_err(|_| InvalidDnsNameError)
21 .and_then(Self::try_from)
22 }
23}
24
25impl TryFrom<String> for DnsName {
26 type Error = InvalidDnsNameError;
27
28 fn try_from(value: String) -> Result<Self, Self::Error> {
29 validate(value.as_bytes())?;
30 Ok(Self(value))
31 }
32}
33
34impl AsRef<str> for DnsName {
35 fn as_ref(&self) -> &str {
36 AsRef::<str>::as_ref(&self.0)
37 }
38}
39
40#[derive(Eq, Hash, PartialEq, Debug)]
42pub struct DnsNameRef<'a>(&'a str);
43
44impl<'a> DnsNameRef<'a> {
45 pub fn to_owned(&'a self) -> DnsName {
47 DnsName(self.0.to_string())
48 }
49
50 pub fn to_lowercase_owned(&'a self) -> DnsName {
53 DnsName(self.0.to_lowercase())
54 }
55}
56
57impl<'a> TryFrom<&'a str> for DnsNameRef<'a> {
58 type Error = InvalidDnsNameError;
59
60 fn try_from(value: &'a str) -> Result<Self, Self::Error> {
61 validate(value.as_bytes())?;
62 Ok(DnsNameRef(value))
63 }
64}
65
66impl<'a> AsRef<str> for DnsNameRef<'a> {
67 fn as_ref(&self) -> &str {
68 self.0
69 }
70}
71
72#[derive(Debug)]
75pub struct InvalidDnsNameError;
76
77impl fmt::Display for InvalidDnsNameError {
78 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
79 f.write_str("invalid dns name")
80 }
81}
82
83impl StdError for InvalidDnsNameError {}
84
85fn validate(input: &[u8]) -> Result<(), InvalidDnsNameError> {
86 use State::*;
87 let mut state = Start;
88
89 const MAX_LABEL_LENGTH: usize = 63;
91
92 const MAX_NAME_LENGTH: usize = 253;
94
95 if input.len() > MAX_NAME_LENGTH {
96 return Err(InvalidDnsNameError);
97 }
98
99 for ch in input {
100 state = match (state, ch) {
101 (Start | Next | NextAfterNumericOnly | Hyphen { .. }, b'.') => {
102 return Err(InvalidDnsNameError)
103 }
104 (Subsequent { .. }, b'.') => Next,
105 (NumericOnly { .. }, b'.') => NextAfterNumericOnly,
106 (Subsequent { len } | NumericOnly { len } | Hyphen { len }, _)
107 if len >= MAX_LABEL_LENGTH =>
108 {
109 return Err(InvalidDnsNameError)
110 }
111 (Start | Next | NextAfterNumericOnly, b'0'..=b'9') => NumericOnly { len: 1 },
112 (NumericOnly { len }, b'0'..=b'9') => NumericOnly { len: len + 1 },
113 (Start | Next | NextAfterNumericOnly, b'a'..=b'z' | b'A'..=b'Z' | b'_') => {
114 Subsequent { len: 1 }
115 }
116 (Subsequent { len } | NumericOnly { len } | Hyphen { len }, b'-') => {
117 Hyphen { len: len + 1 }
118 }
119 (
120 Subsequent { len } | NumericOnly { len } | Hyphen { len },
121 b'a'..=b'z' | b'A'..=b'Z' | b'_' | b'0'..=b'9',
122 ) => Subsequent { len: len + 1 },
123 _ => return Err(InvalidDnsNameError),
124 };
125 }
126
127 if matches!(
128 state,
129 Start | Hyphen { .. } | NumericOnly { .. } | NextAfterNumericOnly
130 ) {
131 return Err(InvalidDnsNameError);
132 }
133
134 Ok(())
135}
136
137enum State {
138 Start,
139 Next,
140 NumericOnly { len: usize },
141 NextAfterNumericOnly,
142 Subsequent { len: usize },
143 Hyphen { len: usize },
144}
145
146#[cfg(test)]
147mod test {
148 static TESTS: &[(&str, bool)] = &[
149 ("", false),
150 ("localhost", true),
151 ("LOCALHOST", true),
152 (".localhost", false),
153 ("..localhost", false),
154 ("1.2.3.4", false),
155 ("127.0.0.1", false),
156 ("absolute.", true),
157 ("absolute..", false),
158 ("multiple.labels.absolute.", true),
159 ("foo.bar.com", true),
160 ("infix-hyphen-allowed.com", true),
161 ("-prefixhypheninvalid.com", false),
162 ("suffixhypheninvalid--", false),
163 ("suffixhypheninvalid-.com", false),
164 ("foo.lastlabelendswithhyphen-", false),
165 ("infix_underscore_allowed.com", true),
166 ("_prefixunderscorevalid.com", true),
167 ("labelendswithnumber1.bar.com", true),
168 ("xn--bcher-kva.example", true),
169 (
170 "sixtythreesixtythreesixtythreesixtythreesixtythreesixtythreesix.com",
171 true,
172 ),
173 (
174 "sixtyfoursixtyfoursixtyfoursixtyfoursixtyfoursixtyfoursixtyfours.com",
175 false,
176 ),
177 (
178 "012345678901234567890123456789012345678901234567890123456789012.com",
179 true,
180 ),
181 (
182 "0123456789012345678901234567890123456789012345678901234567890123.com",
183 false,
184 ),
185 (
186 "01234567890123456789012345678901234567890123456789012345678901-.com",
187 false,
188 ),
189 (
190 "012345678901234567890123456789012345678901234567890123456789012-.com",
191 false,
192 ),
193 ("numeric-only-final-label.1", false),
194 ("numeric-only-final-label.absolute.1.", false),
195 ("1starts-with-number.com", true),
196 ("1Starts-with-number.com", true),
197 ("1.2.3.4.com", true),
198 ("123.numeric-only-first-label", true),
199 ("a123b.com", true),
200 ("numeric-only-middle-label.4.com", true),
201 ("1000-sans.badssl.com", true),
202 ("twohundredandfiftythreecharacters.twohundredandfiftythreecharacters.twohundredandfiftythreecharacters.twohundredandfiftythreecharacters.twohundredandfiftythreecharacters.twohundredandfiftythreecharacters.twohundredandfiftythreecharacters.twohundredandfi", true),
203 ("twohundredandfiftyfourcharacters.twohundredandfiftyfourcharacters.twohundredandfiftyfourcharacters.twohundredandfiftyfourcharacters.twohundredandfiftyfourcharacters.twohundredandfiftyfourcharacters.twohundredandfiftyfourcharacters.twohundredandfiftyfourc", false),
204 ];
205
206 #[test]
207 fn test_validation() {
208 for (input, expected) in TESTS {
209 println!("test: {:?} expected valid? {:?}", input, expected);
210 let name_ref = super::DnsNameRef::try_from(*input);
211 assert_eq!(*expected, name_ref.is_ok());
212 let name = super::DnsName::try_from(input.to_string());
213 assert_eq!(*expected, name.is_ok());
214 }
215 }
216
217 #[test]
218 fn error_is_debug() {
219 assert_eq!(
220 format!("{:?}", super::InvalidDnsNameError),
221 "InvalidDnsNameError"
222 );
223 }
224
225 #[test]
226 fn error_is_display() {
227 assert_eq!(
228 format!("{}", super::InvalidDnsNameError),
229 "invalid dns name"
230 );
231 }
232
233 #[test]
234 fn dns_name_is_debug() {
235 let example = super::DnsName::try_from("example.com".to_string()).unwrap();
236 assert_eq!(format!("{:?}", example), "DnsName(\"example.com\")");
237 }
238
239 #[test]
240 fn dns_name_traits() {
241 let example = super::DnsName::try_from("example.com".to_string()).unwrap();
242 assert_eq!(example, example); use std::collections::HashSet;
245 let mut h = HashSet::<super::DnsName>::new();
246 h.insert(example);
247 }
248
249 #[test]
250 fn try_from_ascii_rejects_bad_utf8() {
251 assert_eq!(
252 format!("{:?}", super::DnsName::try_from_ascii(b"\x80")),
253 "Err(InvalidDnsNameError)"
254 );
255 }
256
257 #[test]
258 fn dns_name_ref_is_debug() {
259 let example = super::DnsNameRef::try_from("example.com").unwrap();
260 assert_eq!(format!("{:?}", example), "DnsNameRef(\"example.com\")");
261 }
262
263 #[test]
264 fn dns_name_ref_traits() {
265 let example = super::DnsNameRef::try_from("example.com").unwrap();
266 assert_eq!(example, example); use std::collections::HashSet;
269 let mut h = HashSet::<super::DnsNameRef>::new();
270 h.insert(example);
271 }
272}