1use std::fmt;
4use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
5use std::str::FromStr;
6
7#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
11#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
12pub enum IpRange {
13 V4 { start: Ipv4Addr, end: Ipv4Addr },
14 V6 { start: Ipv6Addr, end: Ipv6Addr },
15}
16
17#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
20pub enum IpFamily {
21 V4,
22 V6,
23}
24
25impl IpRange {
26 pub fn new(start: IpAddr, end: IpAddr) -> Result<Self, String> {
68 match (start, end) {
69 (IpAddr::V4(s), IpAddr::V4(e)) => {
70 if u32::from(s) > u32::from(e) {
71 return Err("IPv4 range start > end".into());
72 }
73 Ok(IpRange::V4 { start: s, end: e })
74 }
75 (IpAddr::V6(s), IpAddr::V6(e)) => {
76 if u128::from(s) > u128::from(e) {
77 return Err("IPv6 range start > end".into());
78 }
79 Ok(IpRange::V6 { start: s, end: e })
80 }
81 _ => Err("IP range start/end address families must match".into()),
82 }
83 }
84
85 pub fn new_v4(start: Ipv4Addr, end: Ipv4Addr) -> Result<Self, String> {
87 IpRange::new(IpAddr::V4(start), IpAddr::V4(end))
88 }
89
90 pub fn new_v6(start: Ipv6Addr, end: Ipv6Addr) -> Result<Self, String> {
92 IpRange::new(IpAddr::V6(start), IpAddr::V6(end))
93 }
94
95 pub fn family(&self) -> IpFamily {
97 match self {
98 IpRange::V4 { .. } => IpFamily::V4,
99 IpRange::V6 { .. } => IpFamily::V6,
100 }
101 }
102
103 pub fn as_v4(&self) -> Option<(Ipv4Addr, Ipv4Addr)> {
105 match self {
106 IpRange::V4 { start, end } => Some((*start, *end)),
107 _ => None,
108 }
109 }
110
111 pub fn as_v6(&self) -> Option<(Ipv6Addr, Ipv6Addr)> {
113 match self {
114 IpRange::V6 { start, end } => Some((*start, *end)),
115 _ => None,
116 }
117 }
118
119 pub fn start_v4(&self) -> Option<Ipv4Addr> {
121 self.as_v4().map(|(s, _)| s)
122 }
123
124 pub fn end_v4(&self) -> Option<Ipv4Addr> {
126 self.as_v4().map(|(_, e)| e)
127 }
128
129 pub fn start_v6(&self) -> Option<Ipv6Addr> {
131 self.as_v6().map(|(s, _)| s)
132 }
133
134 pub fn end_v6(&self) -> Option<Ipv6Addr> {
136 self.as_v6().map(|(_, e)| e)
137 }
138
139 pub fn contains_v4(&self, ip: Ipv4Addr) -> bool {
141 matches!(self, IpRange::V4 { .. }) && self.contains(IpAddr::V4(ip))
142 }
143
144 pub fn contains_v6(&self, ip: Ipv6Addr) -> bool {
146 matches!(self, IpRange::V6 { .. }) && self.contains(IpAddr::V6(ip))
147 }
148
149 pub fn parse(s: &str) -> Result<Self, String> {
183 let input = s.trim();
184
185 if input.is_empty() {
186 return Err("IP range string cannot be empty".into());
187 }
188
189 let (a, b) = input
190 .split_once('-')
191 .ok_or_else(|| "invalid IP range format; expected 'start-end'".to_string())?;
192
193 let a = a.trim();
194 let b = b.trim();
195
196 if a.is_empty() || b.is_empty() {
197 return Err("invalid IP range format; missing start or end".into());
198 }
199
200 let start: IpAddr = a
201 .parse()
202 .map_err(|_| "invalid IP range start".to_string())?;
203 let end: IpAddr = b.parse().map_err(|_| "invalid IP range end".to_string())?;
204
205 IpRange::new(start, end)
206 }
207
208 pub fn start(&self) -> IpAddr {
210 match self {
211 IpRange::V4 { start, .. } => IpAddr::V4(*start),
212 IpRange::V6 { start, .. } => IpAddr::V6(*start),
213 }
214 }
215
216 pub fn end(&self) -> IpAddr {
218 match self {
219 IpRange::V4 { end, .. } => IpAddr::V4(*end),
220 IpRange::V6 { end, .. } => IpAddr::V6(*end),
221 }
222 }
223
224 pub fn is_v4(&self) -> bool {
226 matches!(self, IpRange::V4 { .. })
227 }
228
229 pub fn is_v6(&self) -> bool {
231 matches!(self, IpRange::V6 { .. })
232 }
233
234 pub fn contains(&self, ip: IpAddr) -> bool {
236 match (self, ip) {
237 (IpRange::V4 { start, end }, IpAddr::V4(v)) => {
238 let s = u32::from(*start);
239 let e = u32::from(*end);
240 let x = u32::from(v);
241 s <= x && x <= e
242 }
243 (IpRange::V6 { start, end }, IpAddr::V6(v)) => {
244 let s = u128::from(*start);
245 let e = u128::from(*end);
246 let x = u128::from(v);
247 s <= x && x <= e
248 }
249 _ => false,
250 }
251 }
252}
253
254impl fmt::Debug for IpRange {
255 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
256 match self {
257 IpRange::V4 { start, end } => f
258 .debug_struct("IpRange")
259 .field("start", start)
260 .field("end", end)
261 .finish(),
262 IpRange::V6 { start, end } => f
263 .debug_struct("IpRange")
264 .field("start", start)
265 .field("end", end)
266 .finish(),
267 }
268 }
269}
270
271impl FromStr for IpRange {
272 type Err = String;
273
274 fn from_str(s: &str) -> Result<Self, Self::Err> {
275 IpRange::parse(s)
276 }
277}
278
279impl fmt::Display for IpRange {
280 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
281 match self {
282 IpRange::V4 { start, end } => write!(f, "{}-{}", start, end),
283 IpRange::V6 { start, end } => write!(f, "{}-{}", start, end),
284 }
285 }
286}
287
288#[cfg(test)]
289mod tests {
290 use super::IpRange;
291 use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
292 use std::str::FromStr;
293
294 #[test]
295 fn parse_rejects_mismatched_families() {
296 let err = IpRange::parse("192.0.2.1-2001:db8::1").unwrap_err();
297 assert!(err.contains("families"));
298 }
299
300 #[test]
301 fn contains_checks_are_inclusive() {
302 let start = Ipv4Addr::new(192, 0, 2, 10);
303 let end = Ipv4Addr::new(192, 0, 2, 12);
304 let range = IpRange::new(IpAddr::V4(start), IpAddr::V4(end)).unwrap();
305 assert!(range.contains(IpAddr::V4(start)));
306 assert!(range.contains(IpAddr::V4(end)));
307 assert!(!range.contains(IpAddr::V4(Ipv4Addr::new(192, 0, 2, 13))));
308 }
309
310 #[test]
311 fn from_str_round_trips_ipv6() {
312 let input = "2001:db8::1-2001:db8::5";
313 let parsed = IpRange::from_str(input).unwrap();
314 assert_eq!(parsed.to_string(), input);
315 assert!(parsed.contains(IpAddr::V6(Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 2))));
316 }
317}