Skip to main content

karbon_framework/validation/constraints/string/
ip.rs

1use crate::validation::constraints::{Constraint, ConstraintResult, ConstraintViolation};
2
3/// IP address version to validate against.
4#[derive(Debug, Clone, Copy, PartialEq)]
5pub enum IpVersion {
6    V4,
7    V6,
8    All,
9}
10
11/// Validates that a value is a valid IP address.
12pub struct Ip {
13    pub message: String,
14    pub version: IpVersion,
15}
16
17impl Default for Ip {
18    fn default() -> Self {
19        Self {
20            message: "This value is not a valid IP address.".to_string(),
21            version: IpVersion::All,
22        }
23    }
24}
25
26impl Ip {
27    pub fn new() -> Self {
28        Self::default()
29    }
30
31    pub fn v4() -> Self {
32        Self {
33            version: IpVersion::V4,
34            ..Self::default()
35        }
36    }
37
38    pub fn v6() -> Self {
39        Self {
40            version: IpVersion::V6,
41            ..Self::default()
42        }
43    }
44
45    pub fn with_message(mut self, message: impl Into<String>) -> Self {
46        self.message = message.into();
47        self
48    }
49
50    fn is_valid_ipv4(value: &str) -> bool {
51        let parts: Vec<&str> = value.split('.').collect();
52        if parts.len() != 4 {
53            return false;
54        }
55        for part in parts {
56            if part.is_empty() || part.len() > 3 {
57                return false;
58            }
59            // No leading zeros (except "0" itself)
60            if part.len() > 1 && part.starts_with('0') {
61                return false;
62            }
63            match part.parse::<u16>() {
64                Ok(n) if n <= 255 => {}
65                _ => return false,
66            }
67        }
68        true
69    }
70
71    fn is_valid_ipv6(value: &str) -> bool {
72        if value.is_empty() {
73            return false;
74        }
75
76        // Check for triple colon (invalid)
77        if value.contains(":::") {
78            return false;
79        }
80
81        // Handle :: abbreviation
82        let double_colon_count = value.matches("::").count();
83        if double_colon_count > 1 {
84            return false;
85        }
86
87        let groups: Vec<&str> = value.split(':').collect();
88
89        if double_colon_count == 1 {
90            if groups.len() > 8 {
91                return false;
92            }
93        } else if groups.len() != 8 {
94            return false;
95        }
96
97        for group in &groups {
98            if group.is_empty() {
99                continue; // Part of ::
100            }
101            if group.len() > 4 {
102                return false;
103            }
104            if !group.chars().all(|c| c.is_ascii_hexdigit()) {
105                return false;
106            }
107        }
108
109        true
110    }
111
112    fn is_valid(&self, value: &str) -> bool {
113        match self.version {
114            IpVersion::V4 => Self::is_valid_ipv4(value),
115            IpVersion::V6 => Self::is_valid_ipv6(value),
116            IpVersion::All => Self::is_valid_ipv4(value) || Self::is_valid_ipv6(value),
117        }
118    }
119}
120
121impl Constraint for Ip {
122    fn validate(&self, value: &str) -> ConstraintResult {
123        if !self.is_valid(value) {
124            return Err(ConstraintViolation::new(
125                self.name(),
126                &self.message,
127                value,
128            ));
129        }
130        Ok(())
131    }
132
133    fn name(&self) -> &'static str {
134        "Ip"
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn test_valid_ipv4() {
144        let constraint = Ip::v4();
145        assert!(constraint.validate("127.0.0.1").is_ok());
146        assert!(constraint.validate("192.168.1.1").is_ok());
147        assert!(constraint.validate("0.0.0.0").is_ok());
148        assert!(constraint.validate("255.255.255.255").is_ok());
149    }
150
151    #[test]
152    fn test_invalid_ipv4() {
153        let constraint = Ip::v4();
154        assert!(constraint.validate("256.0.0.1").is_err());
155        assert!(constraint.validate("1.2.3").is_err());
156        assert!(constraint.validate("1.2.3.4.5").is_err());
157        assert!(constraint.validate("01.02.03.04").is_err()); // leading zeros
158        assert!(constraint.validate("abc.def.ghi.jkl").is_err());
159    }
160
161    #[test]
162    fn test_valid_ipv6() {
163        let constraint = Ip::v6();
164        assert!(constraint.validate("::1").is_ok());
165        assert!(constraint.validate("2001:0db8:85a3:0000:0000:8a2e:0370:7334").is_ok());
166        assert!(constraint.validate("fe80::1").is_ok());
167        assert!(constraint.validate("::").is_ok());
168    }
169
170    #[test]
171    fn test_invalid_ipv6() {
172        let constraint = Ip::v6();
173        assert!(constraint.validate("127.0.0.1").is_err());
174        assert!(constraint.validate(":::1").is_err());
175        assert!(constraint.validate("2001:db8::85a3::7334").is_err()); // double ::
176    }
177
178    #[test]
179    fn test_all_versions() {
180        let constraint = Ip::new();
181        assert!(constraint.validate("127.0.0.1").is_ok());
182        assert!(constraint.validate("::1").is_ok());
183        assert!(constraint.validate("not-an-ip").is_err());
184    }
185}