Skip to main content

cloudillo_types/
address.rs

1//! Address type detection and validation for IPv4, IPv6, and hostnames
2
3use crate::prelude::*;
4use serde::{Deserialize, Serialize};
5use std::net::{Ipv4Addr, Ipv6Addr};
6use std::str::FromStr;
7
8/// Type of address
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10pub enum AddressType {
11	/// IPv4 address (e.g., 192.168.1.1)
12	Ipv4,
13	/// IPv6 address (e.g., 2001:db8::1)
14	Ipv6,
15	/// Hostname/domain name (e.g., example.com)
16	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
29/// Parse and determine the type of an address (IPv4, IPv6, or hostname)
30///
31/// Returns the AddressType if the address is valid, otherwise returns an error
32pub fn parse_address_type(address: &str) -> ClResult<AddressType> {
33	// Try to parse as IPv4
34	if Ipv4Addr::from_str(address).is_ok() {
35		return Ok(AddressType::Ipv4);
36	}
37
38	// Try to parse as IPv6
39	if Ipv6Addr::from_str(address).is_ok() {
40		return Ok(AddressType::Ipv6);
41	}
42
43	// Validate as hostname
44	// Basic hostname validation: must be non-empty, contain only alphanumeric, dots, hyphens, underscores
45	// and must not start or end with a hyphen or dot
46	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	// Check valid hostname characters
55	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	// Check labels (parts between dots)
64	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
83/// Validate that all addresses are the same type
84/// Returns the common type if all match, or an error if they're mixed
85pub fn validate_address_type_consistency(addresses: &[Box<str>]) -> ClResult<Option<AddressType>> {
86	// Empty is fine
87	if addresses.is_empty() {
88		return Ok(None);
89	}
90
91	// Parse first address
92	let first_type = parse_address_type(addresses[0].as_ref())?;
93
94	// Check all subsequent addresses match the first type
95	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		// Empty
139		assert!(parse_address_type("").is_err());
140
141		// Too long
142		assert!(parse_address_type(&"a".repeat(254)).is_err());
143
144		// Invalid characters
145		assert!(parse_address_type("example.com/path").is_err());
146		assert!(parse_address_type("example@com").is_err());
147
148		// Empty labels
149		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		// Label with hyphen at start/end
154		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		// Label too long (>63 chars)
159		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// vim: ts=4