cloudillo_types/
address.rs1use crate::prelude::*;
4use serde::{Deserialize, Serialize};
5use std::net::{Ipv4Addr, Ipv6Addr};
6use std::str::FromStr;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10pub enum AddressType {
11 Ipv4,
13 Ipv6,
15 Hostname,
17}
18
19impl std::fmt::Display for AddressType {
20 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21 match self {
22 AddressType::Ipv4 => write!(f, "ipv4"),
23 AddressType::Ipv6 => write!(f, "ipv6"),
24 AddressType::Hostname => write!(f, "hostname"),
25 }
26 }
27}
28
29pub fn parse_address_type(address: &str) -> ClResult<AddressType> {
33 if Ipv4Addr::from_str(address).is_ok() {
35 return Ok(AddressType::Ipv4);
36 }
37
38 if Ipv6Addr::from_str(address).is_ok() {
40 return Ok(AddressType::Ipv6);
41 }
42
43 if address.is_empty() {
47 return Err(Error::ValidationError("Address cannot be empty".to_string()));
48 }
49
50 if address.len() > 253 {
51 return Err(Error::ValidationError("Hostname too long (max 253 characters)".to_string()));
52 }
53
54 let valid_chars = |c: char| c.is_alphanumeric() || c == '.' || c == '-' || c == '_';
56 if !address.chars().all(valid_chars) {
57 return Err(Error::ValidationError(
58 "Invalid hostname characters (allowed: alphanumeric, dot, hyphen, underscore)"
59 .to_string(),
60 ));
61 }
62
63 for label in address.split('.') {
65 if label.is_empty() {
66 return Err(Error::ValidationError("Hostname labels cannot be empty".to_string()));
67 }
68 if label.starts_with('-') || label.ends_with('-') {
69 return Err(Error::ValidationError(
70 "Hostname labels cannot start or end with hyphen".to_string(),
71 ));
72 }
73 if label.len() > 63 {
74 return Err(Error::ValidationError(
75 "Hostname label too long (max 63 characters)".to_string(),
76 ));
77 }
78 }
79
80 Ok(AddressType::Hostname)
81}
82
83pub fn validate_address_type_consistency(addresses: &[Box<str>]) -> ClResult<Option<AddressType>> {
86 if addresses.is_empty() {
88 return Ok(None);
89 }
90
91 let first_type = parse_address_type(addresses[0].as_ref())?;
93
94 for (i, addr) in addresses.iter().enumerate().skip(1) {
96 let addr_type = parse_address_type(addr.as_ref())?;
97 if addr_type != first_type {
98 return Err(Error::ValidationError(format!(
99 "Address type mismatch: address[0] is {}, but address[{}] is {}",
100 first_type, i, addr_type
101 )));
102 }
103 }
104
105 Ok(Some(first_type))
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 #[test]
113 fn test_ipv4_detection() {
114 assert_eq!(parse_address_type("192.168.1.1").ok(), Some(AddressType::Ipv4));
115 assert_eq!(parse_address_type("203.0.113.42").ok(), Some(AddressType::Ipv4));
116 assert_eq!(parse_address_type("0.0.0.0").ok(), Some(AddressType::Ipv4));
117 assert_eq!(parse_address_type("255.255.255.255").ok(), Some(AddressType::Ipv4));
118 }
119
120 #[test]
121 fn test_ipv6_detection() {
122 assert_eq!(parse_address_type("2001:db8::1").ok(), Some(AddressType::Ipv6));
123 assert_eq!(parse_address_type("::1").ok(), Some(AddressType::Ipv6));
124 assert_eq!(parse_address_type("::").ok(), Some(AddressType::Ipv6));
125 assert_eq!(parse_address_type("fe80::1").ok(), Some(AddressType::Ipv6));
126 }
127
128 #[test]
129 fn test_hostname_detection() {
130 assert_eq!(parse_address_type("example.com").ok(), Some(AddressType::Hostname));
131 assert_eq!(parse_address_type("server.cloudillo.net").ok(), Some(AddressType::Hostname));
132 assert_eq!(parse_address_type("api-server").ok(), Some(AddressType::Hostname));
133 assert_eq!(parse_address_type("my_server").ok(), Some(AddressType::Hostname));
134 }
135
136 #[test]
137 fn test_hostname_validation_errors() {
138 assert!(parse_address_type("").is_err());
140
141 assert!(parse_address_type(&"a".repeat(254)).is_err());
143
144 assert!(parse_address_type("example.com/path").is_err());
146 assert!(parse_address_type("example@com").is_err());
147
148 assert!(parse_address_type("example..com").is_err());
150 assert!(parse_address_type(".example.com").is_err());
151 assert!(parse_address_type("example.com.").is_err());
152
153 assert!(parse_address_type("-example.com").is_err());
155 assert!(parse_address_type("example.com-").is_err());
156 assert!(parse_address_type("example.-com").is_err());
157
158 assert!(parse_address_type(&format!("{}.com", "a".repeat(64))).is_err());
160 }
161
162 #[test]
163 fn test_address_type_consistency_empty() {
164 let addresses: Vec<Box<str>> = vec![];
165 assert!(validate_address_type_consistency(&addresses).is_ok());
166 assert_eq!(validate_address_type_consistency(&addresses).ok(), Some(None));
167 }
168
169 #[test]
170 fn test_address_type_consistency_single() {
171 let addresses = vec!["192.168.1.1".into()];
172 let result = validate_address_type_consistency(&addresses);
173 assert!(result.is_ok());
174 assert_eq!(result.ok(), Some(Some(AddressType::Ipv4)));
175 }
176
177 #[test]
178 fn test_address_type_consistency_mixed() {
179 let addresses = vec!["192.168.1.1".into(), "2001:db8::1".into()];
180 assert!(validate_address_type_consistency(&addresses).is_err());
181 }
182}
183
184