Skip to main content

tanzim_validate/
net.rs

1use crate::error::{Error, ErrorKind};
2use crate::{Meta, Validator};
3use tanzim_value::{Value, ValueType};
4
5/// RFC 1123 hostname check: 1–253 chars, dot-separated labels of 1–63 chars made of
6/// ASCII letters, digits, and hyphens, with no leading or trailing hyphen per label.
7fn is_hostname(host: &str) -> bool {
8    if host.is_empty() || host.len() > 253 {
9        return false;
10    }
11    for label in host.split('.') {
12        let bytes = label.as_bytes();
13        if bytes.is_empty() || bytes.len() > 63 {
14            return false;
15        }
16        if bytes[0] == b'-' || bytes[bytes.len() - 1] == b'-' {
17            return false;
18        }
19        for &byte in bytes {
20            if !byte.is_ascii_alphanumeric() && byte != b'-' {
21                return false;
22            }
23        }
24    }
25    true
26}
27
28/// Borrow the inner string, or produce a `Type` error expecting a string.
29fn as_string(value: &mut Value) -> Result<&mut String, Error> {
30    match value {
31        Value::String(text) => Ok(text),
32        other => Err(Error::new(ErrorKind::Type {
33            expected: ValueType::String,
34            found: other.type_name(),
35        })),
36    }
37}
38
39/// (`net` feature) Accepts a hostname or an IP address literal.
40#[derive(Debug, Clone, Default)]
41pub struct Host {
42    meta: Meta,
43}
44
45impl Host {
46    pub fn new() -> Self {
47        Self {
48            meta: Meta::default(),
49        }
50    }
51
52    /// Attach human-facing metadata (name, description, examples, default, output conversion).
53    pub fn with_meta(mut self, meta: Meta) -> Self {
54        self.meta = meta;
55        self
56    }
57}
58
59crate::impl_meta_methods!(Host);
60
61impl Validator for Host {
62    fn meta(&self) -> &Meta {
63        &self.meta
64    }
65
66    fn meta_mut(&mut self) -> &mut Meta {
67        &mut self.meta
68    }
69
70    fn check(&self, value: &mut Value) -> Result<(), Error> {
71        let text = as_string(value)?;
72        if text.parse::<std::net::IpAddr>().is_ok() || is_hostname(text) {
73            Ok(())
74        } else {
75            Err(Error::new(ErrorKind::Format { expected: "host" }))
76        }
77    }
78}
79
80/// (`net` feature) Accepts a DNS domain name, normalizing it to lowercase.
81#[derive(Debug, Clone, Default)]
82pub struct Domain {
83    meta: Meta,
84    require_dot: bool,
85}
86
87impl Domain {
88    /// Attach human-facing metadata (name, description, examples, default, output conversion).
89    pub fn with_meta(mut self, meta: Meta) -> Self {
90        self.meta = meta;
91        self
92    }
93
94    pub fn new() -> Self {
95        Self::default()
96    }
97
98    /// Require at least one dot (reject bare labels like `localhost`).
99    pub fn require_dot(mut self) -> Self {
100        self.require_dot = true;
101        self
102    }
103}
104
105crate::impl_meta_methods!(Domain);
106
107impl Validator for Domain {
108    fn meta(&self) -> &Meta {
109        &self.meta
110    }
111
112    fn meta_mut(&mut self) -> &mut Meta {
113        &mut self.meta
114    }
115
116    fn check(&self, value: &mut Value) -> Result<(), Error> {
117        let text = as_string(value)?;
118        *text = text.to_lowercase();
119        if !is_hostname(text) || (self.require_dot && !text.contains('.')) {
120            return Err(Error::new(ErrorKind::Format { expected: "domain" }));
121        }
122        Ok(())
123    }
124}
125
126/// (`net` feature) Accepts an email address, normalizing the domain part to lowercase.
127#[derive(Debug, Clone, Default)]
128pub struct Email {
129    meta: Meta,
130}
131
132impl Email {
133    pub fn new() -> Self {
134        Self {
135            meta: Meta::default(),
136        }
137    }
138
139    /// Attach human-facing metadata (name, description, examples, default, output conversion).
140    pub fn with_meta(mut self, meta: Meta) -> Self {
141        self.meta = meta;
142        self
143    }
144}
145
146crate::impl_meta_methods!(Email);
147
148impl Validator for Email {
149    fn meta(&self) -> &Meta {
150        &self.meta
151    }
152
153    fn meta_mut(&mut self) -> &mut Meta {
154        &mut self.meta
155    }
156
157    fn check(&self, value: &mut Value) -> Result<(), Error> {
158        let text = as_string(value)?;
159        let (local, domain) = match text.rsplit_once('@') {
160            Some(parts) => parts,
161            None => return Err(Error::new(ErrorKind::Format { expected: "email" })),
162        };
163        if local.is_empty() || local.len() > 64 || !is_hostname(domain) || !domain.contains('.') {
164            return Err(Error::new(ErrorKind::Format { expected: "email" }));
165        }
166        *text = format!("{local}@{}", domain.to_lowercase());
167        Ok(())
168    }
169}
170
171/// (`net` feature) Accepts a TCP/UDP port number, coercing numeric strings and floats like [`crate::Integer`].
172#[derive(Debug, Clone)]
173pub struct Port {
174    meta: Meta,
175    allow_zero: bool,
176    privileged_ok: bool,
177}
178
179impl Default for Port {
180    fn default() -> Self {
181        Self {
182            meta: Meta::default(),
183            allow_zero: false,
184            privileged_ok: true,
185        }
186    }
187}
188
189impl Port {
190    /// Attach human-facing metadata (name, description, examples, default, output conversion).
191    pub fn with_meta(mut self, meta: Meta) -> Self {
192        self.meta = meta;
193        self
194    }
195
196    pub fn new() -> Self {
197        Self::default()
198    }
199
200    /// Permit port `0` (e.g. "pick any free port").
201    pub fn allow_zero(mut self) -> Self {
202        self.allow_zero = true;
203        self
204    }
205
206    /// When `false`, reject privileged ports below 1024.
207    pub fn privileged_ok(mut self, allowed: bool) -> Self {
208        self.privileged_ok = allowed;
209        self
210    }
211}
212
213crate::impl_meta_methods!(Port);
214
215impl Validator for Port {
216    fn meta(&self) -> &Meta {
217        &self.meta
218    }
219
220    fn meta_mut(&mut self) -> &mut Meta {
221        &mut self.meta
222    }
223
224    fn check(&self, value: &mut Value) -> Result<(), Error> {
225        let min = if self.allow_zero { 0 } else { 1 };
226        crate::Integer::new().range(min, 65535).validate(value)?;
227        let port = match value.as_int() {
228            Some(port) => port,
229            None => unreachable!("Integer validation produced a non-integer"),
230        };
231        if !self.privileged_ok && (1..1024).contains(&port) {
232            return Err(Error::new(ErrorKind::Format {
233                expected: "non-privileged port (>= 1024)",
234            }));
235        }
236        Ok(())
237    }
238}
239
240/// (`net` feature) Accepts an IP address literal.
241#[derive(Debug, Clone, Default)]
242pub struct IpAddr {
243    meta: Meta,
244    v4_only: bool,
245    v6_only: bool,
246}
247
248impl IpAddr {
249    /// Attach human-facing metadata (name, description, examples, default, output conversion).
250    pub fn with_meta(mut self, meta: Meta) -> Self {
251        self.meta = meta;
252        self
253    }
254
255    pub fn new() -> Self {
256        Self::default()
257    }
258
259    pub fn v4_only(mut self) -> Self {
260        self.v4_only = true;
261        self.v6_only = false;
262        self
263    }
264
265    pub fn v6_only(mut self) -> Self {
266        self.v6_only = true;
267        self.v4_only = false;
268        self
269    }
270}
271
272crate::impl_meta_methods!(IpAddr);
273
274impl Validator for IpAddr {
275    fn meta(&self) -> &Meta {
276        &self.meta
277    }
278
279    fn meta_mut(&mut self) -> &mut Meta {
280        &mut self.meta
281    }
282
283    fn check(&self, value: &mut Value) -> Result<(), Error> {
284        let text = as_string(value)?;
285        let parsed = match text.parse::<std::net::IpAddr>() {
286            Ok(parsed) => parsed,
287            Err(_) => {
288                return Err(Error::new(ErrorKind::Format {
289                    expected: "ip address",
290                }));
291            }
292        };
293        if self.v4_only && !parsed.is_ipv4() {
294            return Err(Error::new(ErrorKind::Format {
295                expected: "IPv4 address",
296            }));
297        }
298        if self.v6_only && !parsed.is_ipv6() {
299            return Err(Error::new(ErrorKind::Format {
300                expected: "IPv6 address",
301            }));
302        }
303        Ok(())
304    }
305}
306
307/// (`net` feature) Accepts a `host:port` socket address (IP or hostname host).
308#[derive(Debug, Clone, Default)]
309pub struct SocketAddr {
310    meta: Meta,
311}
312
313impl SocketAddr {
314    pub fn new() -> Self {
315        Self {
316            meta: Meta::default(),
317        }
318    }
319
320    /// Attach human-facing metadata (name, description, examples, default, output conversion).
321    pub fn with_meta(mut self, meta: Meta) -> Self {
322        self.meta = meta;
323        self
324    }
325}
326
327crate::impl_meta_methods!(SocketAddr);
328
329impl Validator for SocketAddr {
330    fn meta(&self) -> &Meta {
331        &self.meta
332    }
333
334    fn meta_mut(&mut self) -> &mut Meta {
335        &mut self.meta
336    }
337
338    fn check(&self, value: &mut Value) -> Result<(), Error> {
339        let text = as_string(value)?;
340        if text.parse::<std::net::SocketAddr>().is_ok() {
341            return Ok(());
342        }
343        // hostname:port form (std only parses ip:port)
344        if let Some((host, port)) = text.rsplit_once(':') {
345            let port_ok = match port.parse::<u16>() {
346                Ok(number) => number != 0,
347                Err(_) => false,
348            };
349            if port_ok && is_hostname(host) {
350                return Ok(());
351            }
352        }
353        Err(Error::new(ErrorKind::Format {
354            expected: "socket address",
355        }))
356    }
357}
358
359#[cfg(test)]
360mod tests {
361    use super::*;
362
363    fn string(text: &str) -> Value {
364        Value::String(text.to_string())
365    }
366
367    #[test]
368    fn host_accepts_name_and_ip() {
369        assert!(Host::new().validate(&mut string("example.com")).is_ok());
370        assert!(Host::new().validate(&mut string("127.0.0.1")).is_ok());
371        assert!(Host::new().validate(&mut string("bad_host!")).is_err());
372    }
373
374    #[test]
375    fn domain_lowercases_and_requires_dot() {
376        let mut value = string("Example.COM");
377        Domain::new().require_dot().validate(&mut value).unwrap();
378        assert_eq!(value, string("example.com"));
379        assert!(
380            Domain::new()
381                .require_dot()
382                .validate(&mut string("localhost"))
383                .is_err()
384        );
385    }
386
387    #[test]
388    fn email_validates_and_lowercases_domain() {
389        let mut value = string("User@Example.COM");
390        Email::new().validate(&mut value).unwrap();
391        assert_eq!(value, string("User@example.com"));
392        assert!(Email::new().validate(&mut string("nope")).is_err());
393    }
394
395    #[test]
396    fn port_range_and_privileged() {
397        let mut value = string("8080");
398        Port::new().validate(&mut value).unwrap();
399        assert_eq!(value, Value::Int(8080));
400        assert!(Port::new().validate(&mut Value::Int(0)).is_err());
401        assert!(
402            Port::new()
403                .allow_zero()
404                .validate(&mut Value::Int(0))
405                .is_ok()
406        );
407        assert!(
408            Port::new()
409                .privileged_ok(false)
410                .validate(&mut Value::Int(80))
411                .is_err()
412        );
413    }
414
415    #[test]
416    fn ip_addr_family_filter() {
417        assert!(
418            IpAddr::new()
419                .v4_only()
420                .validate(&mut string("10.0.0.1"))
421                .is_ok()
422        );
423        assert!(
424            IpAddr::new()
425                .v4_only()
426                .validate(&mut string("::1"))
427                .is_err()
428        );
429        assert!(IpAddr::new().v6_only().validate(&mut string("::1")).is_ok());
430    }
431
432    #[test]
433    fn socket_addr_forms() {
434        assert!(
435            SocketAddr::new()
436                .validate(&mut string("127.0.0.1:8080"))
437                .is_ok()
438        );
439        assert!(
440            SocketAddr::new()
441                .validate(&mut string("example.com:443"))
442                .is_ok()
443        );
444        assert!(
445            SocketAddr::new()
446                .validate(&mut string("example.com"))
447                .is_err()
448        );
449    }
450}