ddns_a/monitor/
change.rs

1//! IP change detection types and functions.
2
3use crate::network::{AdapterSnapshot, IpVersion};
4use std::collections::HashMap;
5use std::net::IpAddr;
6use std::time::SystemTime;
7
8/// The kind of IP address change.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub enum IpChangeKind {
11    /// An IP address was added to an adapter.
12    Added,
13    /// An IP address was removed from an adapter.
14    Removed,
15}
16
17/// Filter for change kinds.
18///
19/// Determines which types of IP address changes to report.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
21pub enum ChangeKind {
22    /// Report only IP addresses that were added
23    Added,
24    /// Report only IP addresses that were removed
25    Removed,
26    /// Report both added and removed IP addresses (default)
27    #[default]
28    Both,
29}
30
31/// An IP address change event.
32///
33/// Represents a single IP address being added or removed from a network adapter.
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct IpChange {
36    /// The name of the adapter where the change occurred.
37    pub adapter: String,
38    /// The IP address that was added or removed.
39    pub address: IpAddr,
40    /// The timestamp when the change was detected.
41    pub timestamp: SystemTime,
42    /// Whether the address was added or removed.
43    pub kind: IpChangeKind,
44}
45
46impl IpChange {
47    /// Creates a new IP change event.
48    #[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    /// Creates an "added" change event.
64    #[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    /// Creates a "removed" change event.
70    #[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    /// Returns true if this is an "added" change.
76    #[must_use]
77    pub const fn is_added(&self) -> bool {
78        matches!(self.kind, IpChangeKind::Added)
79    }
80
81    /// Returns true if this is a "removed" change.
82    #[must_use]
83    pub const fn is_removed(&self) -> bool {
84        matches!(self.kind, IpChangeKind::Removed)
85    }
86
87    /// Returns true if this change involves an IPv4 address.
88    #[must_use]
89    pub const fn is_ipv4(&self) -> bool {
90        self.address.is_ipv4()
91    }
92
93    /// Returns true if this change involves an IPv6 address.
94    #[must_use]
95    pub const fn is_ipv6(&self) -> bool {
96        self.address.is_ipv6()
97    }
98
99    /// Returns true if this change matches the specified IP version filter.
100    #[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/// Filters IP changes by the specified IP version.
111///
112/// Returns only changes that match the specified version:
113/// - `V4`: only IPv4 changes
114/// - `V6`: only IPv6 changes
115/// - `Both`: all changes (no filtering)
116///
117/// # Arguments
118///
119/// * `changes` - The changes to filter
120/// * `version` - The IP version filter to apply
121#[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/// Filters IP changes by the specified change kind.
133///
134/// Returns only changes that match the specified kind:
135/// - `Added`: only "added" changes
136/// - `Removed`: only "removed" changes
137/// - `Both`: all changes (no filtering)
138///
139/// # Arguments
140///
141/// * `changes` - The changes to filter
142/// * `kind` - The change kind filter to apply
143#[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/// Compares two adapter snapshots and returns a list of IP changes.
153///
154/// This is a pure function that detects which IP addresses were added or removed
155/// between two points in time. The comparison is done per-adapter by name.
156///
157/// # Arguments
158///
159/// * `old` - The previous state of network adapters
160/// * `new` - The current state of network adapters
161/// * `timestamp` - The timestamp to assign to all detected changes
162///
163/// # Returns
164///
165/// A vector of [`IpChange`] events. The order is not guaranteed.
166///
167/// # Algorithm
168///
169/// For each adapter (matched by name):
170/// 1. Find addresses in `new` but not in `old` → `Added`
171/// 2. Find addresses in `old` but not in `new` → `Removed`
172///
173/// Adapters that exist only in `old` have all their addresses marked as `Removed`.
174/// Adapters that exist only in `new` have all their addresses marked as `Added`.
175#[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    // Process adapters that exist in old
189    for (name, old_adapter) in &old_by_name {
190        match new_by_name.get(name) {
191            Some(new_adapter) => {
192                // Adapter exists in both - compare addresses
193                diff_adapter_addresses(&mut changes, name, old_adapter, new_adapter, timestamp);
194            }
195            None => {
196                // Adapter removed - all addresses are removed
197                add_all_addresses_as_removed(&mut changes, name, old_adapter, timestamp);
198            }
199        }
200    }
201
202    // Process adapters that only exist in new
203    for (name, new_adapter) in &new_by_name {
204        if !old_by_name.contains_key(name) {
205            // New adapter - all addresses are added
206            add_all_addresses_as_added(&mut changes, name, new_adapter, timestamp);
207        }
208    }
209
210    changes
211}
212
213/// Compares addresses between old and new snapshots of the same adapter.
214fn diff_adapter_addresses(
215    changes: &mut Vec<IpChange>,
216    adapter_name: &str,
217    old: &AdapterSnapshot,
218    new: &AdapterSnapshot,
219    timestamp: SystemTime,
220) {
221    // Check IPv4 addresses
222    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    // Check IPv6 addresses
238    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
254/// Adds all addresses from an adapter as "removed" changes.
255fn 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
277/// Adds all addresses from an adapter as "added" changes.
278fn 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;