1#[cfg(not(feature = "std"))]
4use alloc::{string::String, vec::Vec};
5use bytes::BufMut;
6use commonware_codec::{
7 Buf, EncodeSize, Error as CodecError, MaxEncodeSize, RangeCfg, Read as CodecRead,
8 Write as CodecWrite, varint::UInt,
9};
10use thiserror::Error;
11
12pub const MAX_HOSTNAME_LEN: usize = 253;
17
18pub const MAX_HOSTNAME_LABEL_LEN: usize = 63;
20
21#[derive(Debug, Clone, PartialEq, Eq, Error)]
23pub enum Error {
24 #[error("hostname is empty")]
25 Empty,
26 #[error("hostname exceeds maximum length of {MAX_HOSTNAME_LEN} characters")]
27 TooLong,
28 #[error("hostname label exceeds maximum length of {MAX_HOSTNAME_LABEL_LEN} characters")]
29 LabelTooLong,
30 #[error("hostname contains empty label")]
31 EmptyLabel,
32 #[error("hostname contains invalid character")]
33 InvalidCharacter,
34 #[error("hostname label starts with hyphen")]
35 LabelStartsWithHyphen,
36 #[error("hostname label ends with hyphen")]
37 LabelEndsWithHyphen,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
49pub struct Hostname(String);
50
51impl Hostname {
52 pub fn new(hostname: impl Into<String>) -> Result<Self, Error> {
54 let hostname = hostname.into();
55 Self::validate(&hostname)?;
56 Ok(Self(hostname))
57 }
58
59 fn validate(hostname: &str) -> Result<(), Error> {
61 if hostname.is_empty() {
62 return Err(Error::Empty);
63 }
64
65 if hostname.len() > MAX_HOSTNAME_LEN {
66 return Err(Error::TooLong);
67 }
68
69 for label in hostname.split('.') {
70 Self::validate_label(label)?;
71 }
72
73 Ok(())
74 }
75
76 fn validate_label(label: &str) -> Result<(), Error> {
78 if label.is_empty() {
79 return Err(Error::EmptyLabel);
80 }
81
82 if label.len() > MAX_HOSTNAME_LABEL_LEN {
83 return Err(Error::LabelTooLong);
84 }
85
86 for c in label.chars() {
87 if !c.is_ascii_alphanumeric() && c != '-' {
88 return Err(Error::InvalidCharacter);
89 }
90 }
91
92 if label.starts_with('-') {
93 return Err(Error::LabelStartsWithHyphen);
94 }
95 if label.ends_with('-') {
96 return Err(Error::LabelEndsWithHyphen);
97 }
98
99 Ok(())
100 }
101
102 pub fn as_str(&self) -> &str {
104 &self.0
105 }
106
107 pub fn into_string(self) -> String {
109 self.0
110 }
111}
112
113impl AsRef<str> for Hostname {
114 fn as_ref(&self) -> &str {
115 &self.0
116 }
117}
118
119impl core::fmt::Display for Hostname {
120 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
121 write!(f, "{}", self.0)
122 }
123}
124
125impl TryFrom<String> for Hostname {
126 type Error = Error;
127
128 fn try_from(value: String) -> Result<Self, Self::Error> {
129 Self::new(value)
130 }
131}
132
133impl TryFrom<&str> for Hostname {
134 type Error = Error;
135
136 fn try_from(value: &str) -> Result<Self, Self::Error> {
137 Self::new(value)
138 }
139}
140
141impl CodecWrite for Hostname {
142 #[inline]
143 fn write(&self, buf: &mut impl BufMut) {
144 self.0.as_bytes().write(buf);
145 }
146}
147
148impl EncodeSize for Hostname {
149 #[inline]
150 fn encode_size(&self) -> usize {
151 self.0.as_bytes().encode_size()
152 }
153}
154
155impl MaxEncodeSize for Hostname {
156 const MAX_ENCODE_SIZE: usize = UInt::<u32>::MAX_ENCODE_SIZE + MAX_HOSTNAME_LEN;
157}
158
159impl CodecRead for Hostname {
160 type Cfg = ();
161
162 #[inline]
163 fn read_cfg(buf: &mut impl Buf, _: &()) -> Result<Self, CodecError> {
164 let bytes = Vec::<u8>::read_cfg(buf, &(RangeCfg::new(..=MAX_HOSTNAME_LEN), ()))?;
165 let hostname = String::from_utf8(bytes)
166 .map_err(|_| CodecError::Invalid("Hostname", "invalid UTF-8"))?;
167 Self::new(hostname).map_err(|_| CodecError::Invalid("Hostname", "invalid hostname"))
168 }
169}
170
171#[macro_export]
189macro_rules! hostname {
190 ($s:expr) => {
191 $crate::Hostname::new($s).expect("invalid hostname")
192 };
193}
194
195#[cfg(feature = "arbitrary")]
196impl arbitrary::Arbitrary<'_> for Hostname {
197 fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
198 let num_labels: u8 = u.int_in_range(1..=4)?;
199 let mut labels = Vec::with_capacity(num_labels as usize);
200
201 for _ in 0..num_labels {
202 let label_len: u8 = u.int_in_range(1..=10)?;
203 let label: String = (0..label_len)
204 .map(|i| {
205 if i == 0 || i == label_len - 1 {
206 u.choose(&['a', 'b', 'c', 'd', 'e', '1', '2', '3'])
207 } else {
208 u.choose(&['a', 'b', 'c', 'd', 'e', '1', '2', '3', '-'])
209 }
210 })
211 .collect::<Result<_, _>>()?;
212 labels.push(label);
213 }
214
215 let hostname = labels.join(".");
216 Ok(Self(hostname))
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 #[test]
225 fn test_hostname_valid() {
226 assert!(Hostname::new("localhost").is_ok());
228 assert!(Hostname::new("example").is_ok());
229 assert!(Hostname::new("a").is_ok());
230
231 assert!(Hostname::new("example.com").is_ok());
233 assert!(Hostname::new("sub.example.com").is_ok());
234 assert!(Hostname::new("deep.sub.example.com").is_ok());
235
236 assert!(Hostname::new("my-host").is_ok());
238 assert!(Hostname::new("my-example-host.com").is_ok());
239 assert!(Hostname::new("a-b-c.d-e-f.com").is_ok());
240
241 assert!(Hostname::new("123").is_ok());
243 assert!(Hostname::new("123.456").is_ok());
244 assert!(Hostname::new("host1.example2.com").is_ok());
245 assert!(Hostname::new("1host.2example.3com").is_ok());
246
247 assert!(Hostname::new("Example.COM").is_ok());
249 assert!(Hostname::new("MyHost.Example.Com").is_ok());
250 }
251
252 #[test]
253 fn test_hostname_invalid_empty() {
254 assert!(matches!(Hostname::new("").unwrap_err(), Error::Empty));
255 }
256
257 #[test]
258 fn test_hostname_invalid_too_long() {
259 let long_label = "a".repeat(63);
262 let long_hostname = format!("{long_label}.{long_label}.{long_label}.{long_label}");
263 assert_eq!(long_hostname.len(), 255);
264 assert!(matches!(
265 Hostname::new(&long_hostname).unwrap_err(),
266 Error::TooLong
267 ));
268
269 let short_label = "a".repeat(61);
272 let valid_long = format!("{long_label}.{long_label}.{long_label}.{short_label}");
273 assert_eq!(valid_long.len(), 253);
274 assert!(Hostname::new(&valid_long).is_ok());
275 }
276
277 #[test]
278 fn test_hostname_invalid_label_too_long() {
279 let long_label = "a".repeat(64);
281 assert!(matches!(
282 Hostname::new(&long_label).unwrap_err(),
283 Error::LabelTooLong
284 ));
285
286 let valid_label = "a".repeat(63);
288 assert!(Hostname::new(&valid_label).is_ok());
289 }
290
291 #[test]
292 fn test_hostname_invalid_empty_label() {
293 assert!(matches!(
295 Hostname::new(".example.com").unwrap_err(),
296 Error::EmptyLabel
297 ));
298
299 assert!(matches!(
301 Hostname::new("example.com.").unwrap_err(),
302 Error::EmptyLabel
303 ));
304
305 assert!(matches!(
307 Hostname::new("example..com").unwrap_err(),
308 Error::EmptyLabel
309 ));
310 }
311
312 #[test]
313 fn test_hostname_invalid_characters() {
314 assert!(matches!(
316 Hostname::new("my_host.com").unwrap_err(),
317 Error::InvalidCharacter
318 ));
319
320 assert!(matches!(
322 Hostname::new("my host.com").unwrap_err(),
323 Error::InvalidCharacter
324 ));
325
326 assert!(matches!(
328 Hostname::new("host@example.com").unwrap_err(),
329 Error::InvalidCharacter
330 ));
331 assert!(matches!(
332 Hostname::new("host!.com").unwrap_err(),
333 Error::InvalidCharacter
334 ));
335
336 assert!(matches!(
338 Hostname::new("hôst.com").unwrap_err(),
339 Error::InvalidCharacter
340 ));
341 }
342
343 #[test]
344 fn test_hostname_invalid_hyphen_position() {
345 assert!(matches!(
347 Hostname::new("-example.com").unwrap_err(),
348 Error::LabelStartsWithHyphen
349 ));
350 assert!(matches!(
351 Hostname::new("example.-sub.com").unwrap_err(),
352 Error::LabelStartsWithHyphen
353 ));
354
355 assert!(matches!(
357 Hostname::new("example-.com").unwrap_err(),
358 Error::LabelEndsWithHyphen
359 ));
360 assert!(matches!(
361 Hostname::new("example.sub-.com").unwrap_err(),
362 Error::LabelEndsWithHyphen
363 ));
364
365 assert!(matches!(
367 Hostname::new("-").unwrap_err(),
368 Error::LabelStartsWithHyphen
369 ));
370 }
371
372 #[test]
373 fn test_hostname_try_from() {
374 let hostname: Result<Hostname, _> = "example.com".to_string().try_into();
376 assert!(hostname.is_ok());
377
378 let hostname: Result<Hostname, _> = "example.com".try_into();
380 assert!(hostname.is_ok());
381
382 let hostname: Result<Hostname, _> = "invalid..host".try_into();
384 assert!(hostname.is_err());
385 }
386
387 #[test]
388 fn test_hostname_display_and_as_ref() {
389 let hostname = Hostname::new("example.com").unwrap();
390 assert_eq!(format!("{hostname}"), "example.com");
391 assert_eq!(hostname.as_ref(), "example.com");
392 assert_eq!(hostname.as_str(), "example.com");
393 }
394
395 #[test]
396 fn test_hostname_into_string() {
397 let hostname = Hostname::new("example.com").unwrap();
398 let s: String = hostname.into_string();
399 assert_eq!(s, "example.com");
400 }
401
402 #[test]
403 fn test_hostname_macro() {
404 let h = hostname!("example.com");
405 assert_eq!(h.as_str(), "example.com");
406 }
407
408 #[test]
409 fn test_max_encode_size() {
410 let host = [
413 "a".repeat(63),
414 "b".repeat(63),
415 "c".repeat(63),
416 "d".repeat(61),
417 ]
418 .join(".");
419 assert_eq!(host.len(), MAX_HOSTNAME_LEN);
420 let hostname = Hostname::new(host).unwrap();
421 assert_eq!(Hostname::MAX_ENCODE_SIZE, 258);
422 assert!(hostname.encode_size() <= Hostname::MAX_ENCODE_SIZE);
423 }
424
425 #[cfg(feature = "arbitrary")]
426 mod conformance {
427 use super::*;
428 use commonware_codec::conformance::CodecConformance;
429
430 commonware_conformance::conformance_tests! {
431 CodecConformance<Hostname>,
432 }
433 }
434}