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