compose_lens/model/
hostname.rs1use super::Located;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct Hostname {
8 raw: Located<String>,
9 kind: HostnameKind,
10}
11
12impl Hostname {
13 pub(crate) fn parse(raw: Located<String>) -> Self {
14 let kind = if raw.value().contains('$') {
15 HostnameKind::Expression
16 } else if valid_hostname(raw.value()) {
17 HostnameKind::Resolved
18 } else {
19 HostnameKind::Invalid
20 };
21 Self { raw, kind }
22 }
23
24 #[must_use]
26 pub const fn raw(&self) -> &Located<String> {
27 &self.raw
28 }
29
30 #[must_use]
32 pub const fn kind(&self) -> &HostnameKind {
33 &self.kind
34 }
35
36 #[must_use]
38 pub const fn is_resolved(&self) -> bool {
39 matches!(self.kind, HostnameKind::Resolved)
40 }
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45#[non_exhaustive]
46pub enum HostnameKind {
47 Resolved,
49 Expression,
51 Invalid,
53}
54
55pub(crate) fn valid_hostname(value: &str) -> bool {
56 if !(1..=253).contains(&value.len()) || !value.is_ascii() {
57 return false;
58 }
59 value.split('.').all(|label| {
60 (1..=63).contains(&label.len())
61 && label.bytes().next().is_some_and(|byte| byte.is_ascii_alphanumeric())
62 && label.bytes().last().is_some_and(|byte| byte.is_ascii_alphanumeric())
63 && label.bytes().all(|byte| byte.is_ascii_alphanumeric() || byte == b'-')
64 })
65}
66
67#[cfg(test)]
68mod tests {
69 use super::{Hostname, HostnameKind, valid_hostname};
70 use crate::model::Located;
71 use crate::source::{SourceId, SourceSpan};
72
73 #[test]
74 fn validates_conservative_ascii_rfc_1123_hostnames() {
75 let label_63 = "a".repeat(63);
76 let maximum = format!("{label_63}.{label_63}.{label_63}.{}", "a".repeat(61));
77 for value in ["a", "3api", "API.Example-Corp.COM", maximum.as_str()] {
78 assert!(valid_hostname(value), "expected valid hostname {value}");
79 }
80 let label_64 = "a".repeat(64);
81 let too_long = format!("{maximum}.a");
82 for value in [
83 "",
84 ".",
85 "example.",
86 ".example",
87 "example..com",
88 "-example",
89 "example-",
90 "example_com",
91 "café.example",
92 label_64.as_str(),
93 too_long.as_str(),
94 ] {
95 assert!(!valid_hostname(value), "expected invalid hostname {value}");
96 }
97 }
98
99 #[test]
100 fn classifies_every_dollar_bearing_value_as_deferred() -> Result<(), &'static str> {
101 let value = "invalid_$_hostname";
102 let span = SourceSpan::new(SourceId::new(1), 0, value.len()).ok_or("valid test span expected")?;
103 let hostname = Hostname::parse(Located::new(value.to_owned(), span));
104 assert_eq!(hostname.raw().value(), value);
105 assert_eq!(hostname.kind(), &HostnameKind::Expression);
106 assert!(!hostname.is_resolved());
107 Ok(())
108 }
109}