1use crate::context::{State, Update, UpdateErr};
6use crate::node::{Kind, Node, Validator};
7use crate::value::Value;
8use regex::Regex;
9use std::sync::{Arc, OnceLock};
10
11fn re(cell: &'static OnceLock<Regex>, src: &str) -> &'static Regex {
12 cell.get_or_init(|| Regex::new(src).unwrap())
13}
14
15fn email_re() -> &'static Regex {
19 static RE: OnceLock<Regex> = OnceLock::new();
20 re(
21 &RE,
22 "^[A-Za-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[A-Za-z0-9!#$%&'*+/=?^_`{|}~-]+)*\
23 @(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\\.)+[A-Za-z]{2,63}$",
24 )
25}
26
27pub(crate) fn is_email(s: &str) -> bool {
28 s.len() <= 254 && s.find('@').map(|i| i <= 64).unwrap_or(false) && email_re().is_match(s)
29}
30
31fn url_re() -> &'static Regex {
34 static RE: OnceLock<Regex> = OnceLock::new();
35 re(
36 &RE,
37 r"^[A-Za-z][A-Za-z0-9+.-]*://(?:[^ \t\r\n/?#@]+@)?(?:\[[0-9A-Fa-f:.]+\]|[^ \t\r\n/?#@:\[\]]+)(?::\d{1,5})?(?:[/?#][^ \t\r\n]*)?$",
38 )
39}
40
41fn uuid_re() -> &'static Regex {
43 static RE: OnceLock<Regex> = OnceLock::new();
44 re(
45 &RE,
46 r"^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$",
47 )
48}
49
50fn ipv4_re() -> &'static Regex {
52 static RE: OnceLock<Regex> = OnceLock::new();
53 re(
54 &RE,
55 r"^(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$",
56 )
57}
58
59fn hex4_re() -> &'static Regex {
60 static RE: OnceLock<Regex> = OnceLock::new();
61 re(&RE, r"^[0-9A-Fa-f]{1,4}$")
62}
63
64pub(crate) fn is_ipv4(s: &str) -> bool {
65 ipv4_re().is_match(s)
66}
67
68pub(crate) fn is_ipv6(s: &str) -> bool {
72 let parts: Vec<&str> = s.split("::").collect();
73 if parts.len() > 2 {
74 return false;
75 }
76 let head: Vec<&str> = if parts[0].is_empty() {
77 Vec::new()
78 } else {
79 parts[0].split(':').collect()
80 };
81 let tail: Vec<&str> = if parts.len() == 2 && !parts[1].is_empty() {
82 parts[1].split(':').collect()
83 } else {
84 Vec::new()
85 };
86 let groups: Vec<&str> = head.iter().chain(tail.iter()).copied().collect();
87
88 let mut count = 0;
89 for (i, g) in groups.iter().enumerate() {
90 if hex4_re().is_match(g) {
91 count += 1;
92 } else if i == groups.len() - 1 && (parts.len() == 1 || head.len() <= i) && is_ipv4(g) {
93 count += 2;
95 } else {
96 return false;
97 }
98 }
99 if parts.len() == 2 {
100 count <= 7
101 } else {
102 count == 8
103 }
104}
105
106#[derive(Clone, Copy)]
108pub(crate) struct Format {
109 pub name: &'static str,
111 pub what: &'static str,
113 pub valid: fn(&str) -> bool,
114}
115
116pub(crate) const FMT_EMAIL: Format = Format {
117 name: "Email",
118 what: "email address",
119 valid: is_email,
120};
121pub(crate) const FMT_URL: Format = Format {
122 name: "Url",
123 what: "URL",
124 valid: |s| url_re().is_match(s),
125};
126pub(crate) const FMT_UUID: Format = Format {
127 name: "Uuid",
128 what: "UUID",
129 valid: |s| uuid_re().is_match(s),
130};
131pub(crate) const FMT_DATE_TIME: Format = Format {
132 name: "DateTime",
133 what: "ISO 8601 date-time",
134 valid: crate::coerce::is_iso_date_time,
135};
136pub(crate) const FMT_IP: Format = Format {
137 name: "Ip",
138 what: "IP address",
139 valid: |s| is_ipv4(s) || is_ipv6(s),
140};
141pub(crate) const FMT_IPV4: Format = Format {
142 name: "Ipv4",
143 what: "IPv4 address",
144 valid: is_ipv4,
145};
146pub(crate) const FMT_IPV6: Format = Format {
147 name: "Ipv6",
148 what: "IPv6 address",
149 valid: is_ipv6,
150};
151
152pub(crate) fn format_validator(f: Format) -> Validator {
156 Validator {
157 name: f.name.to_string(),
158 func: Arc::new(move |state: &mut State<'_>, update: &mut Update| {
159 if state.absent || crate::builders::type_will_fail(state.node, state.value) {
160 return true;
161 }
162 if let Value::Str(s) = state.value {
163 if (f.valid)(s) {
164 return true;
165 }
166 }
167 update.err = Some(UpdateErr::Text(format!(
168 "Value \"$VALUE\" for property \"$PATH\" is not a valid {}.",
169 f.what
170 )));
171 update.why = Some(f.name.to_string());
172 update.mark = 0;
173 false
174 }),
175 args: Vec::new(),
176 suffix: Some(f.name.to_string()),
177 inner: None,
178 }
179}
180
181pub(crate) fn with_format(mut n: Node, f: Format) -> Node {
184 if n.kind == Kind::Any {
185 n = crate::builders::type_(Kind::String, n);
186 }
187 n.befores.push(format_validator(f));
188 n
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 #[test]
196 fn ipv6_forms() {
197 assert!(is_ipv6("::1"));
198 assert!(is_ipv6("2001:db8::8a2e:370:7334"));
199 assert!(is_ipv6("2001:0db8:0000:0000:0000:ff00:0042:8329"));
200 assert!(is_ipv6("::ffff:192.168.1.1"));
201 assert!(is_ipv6("64:ff9b::192.0.2.33"));
202 assert!(!is_ipv6("1:2:3:4:5:6:7:8:9"));
203 assert!(!is_ipv6("1::2::3"));
204 assert!(!is_ipv6("192.168.1.1::1"));
205 assert!(!is_ipv6("2001:db8::8a2e:370:7334:1:2:3"));
206 assert!(!is_ipv6("gggg::1"));
207 assert!(!is_ipv6("1:2:3:4:5:6:7"));
208 assert!(is_ipv6("1:2:3:4:5:6:1.2.3.4"));
209 assert!(!is_ipv6("1:2:3:4:5:6:7:1.2.3.4"));
210 }
211
212 #[test]
213 fn email_limits() {
214 assert!(is_email("a@b.co"));
215 assert!(!is_email("a"));
216 let long_local = format!("{}@b.co", "a".repeat(65));
217 assert!(!is_email(&long_local));
218 let long = format!("a@{}.co", "b".repeat(260));
219 assert!(!is_email(&long));
220 }
221}