1use crate::network::{AdapterSnapshot, IpVersion};
4use std::collections::HashMap;
5use std::net::IpAddr;
6use std::time::SystemTime;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub enum IpChangeKind {
11 Added,
13 Removed,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
21pub enum ChangeKind {
22 Added,
24 Removed,
26 #[default]
28 Both,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct IpChange {
36 pub adapter: String,
38 pub address: IpAddr,
40 pub timestamp: SystemTime,
42 pub kind: IpChangeKind,
44}
45
46impl IpChange {
47 #[must_use]
49 pub fn new(
50 adapter: impl Into<String>,
51 address: IpAddr,
52 timestamp: SystemTime,
53 kind: IpChangeKind,
54 ) -> Self {
55 Self {
56 adapter: adapter.into(),
57 address,
58 timestamp,
59 kind,
60 }
61 }
62
63 #[must_use]
65 pub fn added(adapter: impl Into<String>, address: IpAddr, timestamp: SystemTime) -> Self {
66 Self::new(adapter, address, timestamp, IpChangeKind::Added)
67 }
68
69 #[must_use]
71 pub fn removed(adapter: impl Into<String>, address: IpAddr, timestamp: SystemTime) -> Self {
72 Self::new(adapter, address, timestamp, IpChangeKind::Removed)
73 }
74
75 #[must_use]
77 pub const fn is_added(&self) -> bool {
78 matches!(self.kind, IpChangeKind::Added)
79 }
80
81 #[must_use]
83 pub const fn is_removed(&self) -> bool {
84 matches!(self.kind, IpChangeKind::Removed)
85 }
86
87 #[must_use]
89 pub const fn is_ipv4(&self) -> bool {
90 self.address.is_ipv4()
91 }
92
93 #[must_use]
95 pub const fn is_ipv6(&self) -> bool {
96 self.address.is_ipv6()
97 }
98
99 #[must_use]
101 pub const fn matches_version(&self, version: IpVersion) -> bool {
102 match version {
103 IpVersion::V4 => self.address.is_ipv4(),
104 IpVersion::V6 => self.address.is_ipv6(),
105 IpVersion::Both => true,
106 }
107 }
108}
109
110#[must_use]
122pub fn filter_by_version(changes: Vec<IpChange>, version: IpVersion) -> Vec<IpChange> {
123 match version {
124 IpVersion::Both => changes,
125 IpVersion::V4 | IpVersion::V6 => changes
126 .into_iter()
127 .filter(|c| c.matches_version(version))
128 .collect(),
129 }
130}
131
132#[must_use]
144pub fn filter_by_change_kind(changes: Vec<IpChange>, kind: ChangeKind) -> Vec<IpChange> {
145 match kind {
146 ChangeKind::Both => changes,
147 ChangeKind::Added => changes.into_iter().filter(IpChange::is_added).collect(),
148 ChangeKind::Removed => changes.into_iter().filter(IpChange::is_removed).collect(),
149 }
150}
151
152#[must_use]
176pub fn diff(
177 old: &[AdapterSnapshot],
178 new: &[AdapterSnapshot],
179 timestamp: SystemTime,
180) -> Vec<IpChange> {
181 let old_by_name: HashMap<&str, &AdapterSnapshot> =
182 old.iter().map(|a| (a.name.as_str(), a)).collect();
183 let new_by_name: HashMap<&str, &AdapterSnapshot> =
184 new.iter().map(|a| (a.name.as_str(), a)).collect();
185
186 let mut changes = Vec::new();
187
188 for (name, old_adapter) in &old_by_name {
190 match new_by_name.get(name) {
191 Some(new_adapter) => {
192 diff_adapter_addresses(&mut changes, name, old_adapter, new_adapter, timestamp);
194 }
195 None => {
196 add_all_addresses_as_removed(&mut changes, name, old_adapter, timestamp);
198 }
199 }
200 }
201
202 for (name, new_adapter) in &new_by_name {
204 if !old_by_name.contains_key(name) {
205 add_all_addresses_as_added(&mut changes, name, new_adapter, timestamp);
207 }
208 }
209
210 changes
211}
212
213fn diff_adapter_addresses(
215 changes: &mut Vec<IpChange>,
216 adapter_name: &str,
217 old: &AdapterSnapshot,
218 new: &AdapterSnapshot,
219 timestamp: SystemTime,
220) {
221 for addr in &old.ipv4_addresses {
223 if !new.ipv4_addresses.contains(addr) {
224 changes.push(IpChange::removed(
225 adapter_name,
226 IpAddr::V4(*addr),
227 timestamp,
228 ));
229 }
230 }
231 for addr in &new.ipv4_addresses {
232 if !old.ipv4_addresses.contains(addr) {
233 changes.push(IpChange::added(adapter_name, IpAddr::V4(*addr), timestamp));
234 }
235 }
236
237 for addr in &old.ipv6_addresses {
239 if !new.ipv6_addresses.contains(addr) {
240 changes.push(IpChange::removed(
241 adapter_name,
242 IpAddr::V6(*addr),
243 timestamp,
244 ));
245 }
246 }
247 for addr in &new.ipv6_addresses {
248 if !old.ipv6_addresses.contains(addr) {
249 changes.push(IpChange::added(adapter_name, IpAddr::V6(*addr), timestamp));
250 }
251 }
252}
253
254fn add_all_addresses_as_removed(
256 changes: &mut Vec<IpChange>,
257 adapter_name: &str,
258 adapter: &AdapterSnapshot,
259 timestamp: SystemTime,
260) {
261 for addr in &adapter.ipv4_addresses {
262 changes.push(IpChange::removed(
263 adapter_name,
264 IpAddr::V4(*addr),
265 timestamp,
266 ));
267 }
268 for addr in &adapter.ipv6_addresses {
269 changes.push(IpChange::removed(
270 adapter_name,
271 IpAddr::V6(*addr),
272 timestamp,
273 ));
274 }
275}
276
277fn add_all_addresses_as_added(
279 changes: &mut Vec<IpChange>,
280 adapter_name: &str,
281 adapter: &AdapterSnapshot,
282 timestamp: SystemTime,
283) {
284 for addr in &adapter.ipv4_addresses {
285 changes.push(IpChange::added(adapter_name, IpAddr::V4(*addr), timestamp));
286 }
287 for addr in &adapter.ipv6_addresses {
288 changes.push(IpChange::added(adapter_name, IpAddr::V6(*addr), timestamp));
289 }
290}
291
292#[cfg(test)]
293#[path = "change_tests.rs"]
294mod tests;