1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum Format {
23 Uuid,
27 Email,
36 Hostname,
40 Ipv4,
45 Ipv6,
49}
50
51impl Format {
52 pub fn name(self) -> &'static str {
53 match self {
54 Format::Uuid => "uuid",
55 Format::Email => "email",
56 Format::Hostname => "hostname",
57 Format::Ipv4 => "ipv4",
58 Format::Ipv6 => "ipv6",
59 }
60 }
61
62 pub fn parse(name: &str) -> Option<Format> {
63 match name {
64 "uuid" => Some(Format::Uuid),
65 "email" => Some(Format::Email),
66 "hostname" => Some(Format::Hostname),
67 "ipv4" => Some(Format::Ipv4),
68 "ipv6" => Some(Format::Ipv6),
69 _ => None,
70 }
71 }
72
73 pub const ALL: [Format; 5] = [
76 Format::Uuid,
77 Format::Email,
78 Format::Hostname,
79 Format::Ipv4,
80 Format::Ipv6,
81 ];
82
83 pub fn matches(self, value: &str) -> bool {
84 match self {
85 Format::Uuid => uuid(value),
86 Format::Email => email(value),
87 Format::Hostname => hostname(value),
88 Format::Ipv4 => ipv4(value),
89 Format::Ipv6 => ipv6(value),
90 }
91 }
92}
93
94fn uuid(v: &str) -> bool {
95 let groups = [8, 4, 4, 4, 12];
96 let mut parts = v.split('-');
97 for len in groups {
98 match parts.next() {
99 Some(p) if p.len() == len && p.bytes().all(|b| b.is_ascii_hexdigit()) => {}
100 _ => return false,
101 }
102 }
103 parts.next().is_none()
104}
105
106fn email(v: &str) -> bool {
107 let Some(at) = v.rfind('@') else {
109 return false;
110 };
111 let (local, domain) = (&v[..at], &v[at + 1..]);
112
113 if local.is_empty() || local.len() > 64 {
114 return false;
115 }
116 if local
120 .bytes()
121 .any(|b| b <= b' ' || b == b'"' || b == b'\\' || b == b'@' || b == 0x7f)
122 {
123 return false;
124 }
125 if local.starts_with('.') || local.ends_with('.') || local.contains("..") {
126 return false;
127 }
128 hostname(domain) && domain.contains('.')
131}
132
133fn hostname(v: &str) -> bool {
134 if v.is_empty() || v.len() > 253 {
135 return false;
136 }
137 v.split('.').all(|label| {
138 !label.is_empty()
139 && label.len() <= 63
140 && !label.starts_with('-')
141 && !label.ends_with('-')
142 && label
143 .bytes()
144 .all(|b| b.is_ascii_alphanumeric() || b == b'-')
145 })
146}
147
148fn ipv4(v: &str) -> bool {
149 let mut octets = 0;
150 for part in v.split('.') {
151 octets += 1;
152 if octets > 4 || part.is_empty() || part.len() > 3 {
153 return false;
154 }
155 if !part.bytes().all(|b| b.is_ascii_digit()) {
156 return false;
157 }
158 if part.len() > 1 && part.starts_with('0') {
160 return false;
161 }
162 if part.parse::<u16>().map_or(true, |n| n > 255) {
163 return false;
164 }
165 }
166 octets == 4
167}
168
169fn ipv6(v: &str) -> bool {
170 if v.contains('%') {
173 return false;
174 }
175
176 let halves: Vec<&str> = v.split("::").collect();
178 let (head, tail, elided) = match halves.as_slice() {
179 [whole] => (*whole, "", false),
180 [before, after] => (*before, *after, true),
181 _ => return false,
182 };
183
184 let groups = |s: &str| -> Option<(usize, bool)> {
185 if s.is_empty() {
186 return Some((0, false));
187 }
188 let parts: Vec<&str> = s.split(':').collect();
189 let mut count = 0;
190 let mut trailing_v4 = false;
191 for (i, part) in parts.iter().enumerate() {
192 let last = i + 1 == parts.len();
193 if last && part.contains('.') {
195 if !ipv4(part) {
196 return None;
197 }
198 count += 2;
199 trailing_v4 = true;
200 continue;
201 }
202 if part.is_empty() || part.len() > 4 || !part.bytes().all(|b| b.is_ascii_hexdigit()) {
203 return None;
204 }
205 count += 1;
206 }
207 Some((count, trailing_v4))
208 };
209
210 let Some((left, left_v4)) = groups(head) else {
211 return false;
212 };
213 let Some((right, right_v4)) = groups(tail) else {
214 return false;
215 };
216 if left_v4 && (elided || !tail.is_empty()) {
218 return false;
219 }
220 let total = left + right;
221
222 if elided {
223 total < 8
225 } else {
226 total == 8 && !right_v4
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233
234 #[test]
235 fn uuids() {
236 assert!(Format::Uuid.matches("6ba7b810-9dad-11d1-80b4-00c04fd430c8"));
237 assert!(Format::Uuid.matches("6BA7B810-9DAD-11D1-80B4-00C04FD430C8"));
238 assert!(Format::Uuid.matches("00000000-0000-0000-0000-000000000000"));
241
242 assert!(!Format::Uuid.matches("6ba7b810-9dad-11d1-80b4-00c04fd430c"));
243 assert!(!Format::Uuid.matches("6ba7b8109dad11d180b400c04fd430c8"));
244 assert!(!Format::Uuid.matches("6ba7b810-9dad-11d1-80b4-00c04fd430c8-"));
245 assert!(!Format::Uuid.matches("gba7b810-9dad-11d1-80b4-00c04fd430c8"));
246 assert!(!Format::Uuid.matches(""));
247 }
248
249 #[test]
250 fn emails() {
251 assert!(Format::Email.matches("gabriel@example.com"));
252 assert!(Format::Email.matches("first.last+tag@sub.example.co.uk"));
253 assert!(Format::Email.matches("a@b.co"));
254
255 assert!(!Format::Email.matches("no-at-sign.example.com"));
256 assert!(!Format::Email.matches("@example.com"));
257 assert!(!Format::Email.matches("user@"));
258 assert!(!Format::Email.matches("user@localhost"));
259 assert!(!Format::Email.matches("user name@example.com"));
260 assert!(!Format::Email.matches(".user@example.com"));
261 assert!(!Format::Email.matches("user..name@example.com"));
262 }
263
264 #[test]
265 fn hostnames() {
266 assert!(Format::Hostname.matches("example.com"));
267 assert!(Format::Hostname.matches("localhost"));
268 assert!(Format::Hostname.matches("a-b.example.com"));
269
270 assert!(!Format::Hostname.matches(""));
271 assert!(!Format::Hostname.matches("-example.com"));
272 assert!(!Format::Hostname.matches("example-.com"));
273 assert!(!Format::Hostname.matches("exa mple.com"));
274 assert!(!Format::Hostname.matches("example..com"));
275 }
276
277 #[test]
278 fn ipv4_addresses() {
279 assert!(Format::Ipv4.matches("192.168.0.1"));
280 assert!(Format::Ipv4.matches("0.0.0.0"));
281 assert!(Format::Ipv4.matches("255.255.255.255"));
282
283 assert!(!Format::Ipv4.matches("256.0.0.1"));
284 assert!(!Format::Ipv4.matches("1.2.3"));
285 assert!(!Format::Ipv4.matches("1.2.3.4.5"));
286 assert!(!Format::Ipv4.matches("010.0.0.1"));
288 assert!(!Format::Ipv4.matches("1.2.3.-4"));
289 }
290
291 #[test]
292 fn ipv6_addresses() {
293 assert!(Format::Ipv6.matches("2001:0db8:85a3:0000:0000:8a2e:0370:7334"));
294 assert!(Format::Ipv6.matches("2001:db8:85a3::8a2e:370:7334"));
295 assert!(Format::Ipv6.matches("::1"));
296 assert!(Format::Ipv6.matches("::"));
297 assert!(Format::Ipv6.matches("::ffff:192.168.0.1"));
298
299 assert!(!Format::Ipv6.matches("2001:db8::85a3::7334"));
300 assert!(!Format::Ipv6.matches("2001:db8:85a3:0:0:8a2e:370"));
301 assert!(!Format::Ipv6.matches("gggg::1"));
302 assert!(!Format::Ipv6.matches("fe80::1%eth0"));
304 assert!(!Format::Ipv6.matches(""));
305 }
306
307 #[test]
308 fn names_round_trip() {
309 for f in Format::ALL {
310 assert_eq!(Format::parse(f.name()), Some(f));
311 }
312 assert_eq!(Format::parse("regex"), None);
313 }
314}