use crate::network::{AdapterSnapshot, IpVersion};
use std::collections::HashMap;
use std::net::IpAddr;
use std::time::SystemTime;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum IpChangeKind {
Added,
Removed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ChangeKind {
Added,
Removed,
#[default]
Both,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IpChange {
pub adapter: String,
pub address: IpAddr,
pub timestamp: SystemTime,
pub kind: IpChangeKind,
}
impl IpChange {
#[must_use]
pub fn new(
adapter: impl Into<String>,
address: IpAddr,
timestamp: SystemTime,
kind: IpChangeKind,
) -> Self {
Self {
adapter: adapter.into(),
address,
timestamp,
kind,
}
}
#[must_use]
pub fn added(adapter: impl Into<String>, address: IpAddr, timestamp: SystemTime) -> Self {
Self::new(adapter, address, timestamp, IpChangeKind::Added)
}
#[must_use]
pub fn removed(adapter: impl Into<String>, address: IpAddr, timestamp: SystemTime) -> Self {
Self::new(adapter, address, timestamp, IpChangeKind::Removed)
}
#[must_use]
pub const fn is_added(&self) -> bool {
matches!(self.kind, IpChangeKind::Added)
}
#[must_use]
pub const fn is_removed(&self) -> bool {
matches!(self.kind, IpChangeKind::Removed)
}
#[must_use]
pub const fn is_ipv4(&self) -> bool {
self.address.is_ipv4()
}
#[must_use]
pub const fn is_ipv6(&self) -> bool {
self.address.is_ipv6()
}
#[must_use]
pub const fn matches_version(&self, version: IpVersion) -> bool {
match version {
IpVersion::V4 => self.address.is_ipv4(),
IpVersion::V6 => self.address.is_ipv6(),
IpVersion::Both => true,
}
}
}
#[must_use]
pub fn filter_by_version(changes: Vec<IpChange>, version: IpVersion) -> Vec<IpChange> {
match version {
IpVersion::Both => changes,
IpVersion::V4 | IpVersion::V6 => changes
.into_iter()
.filter(|c| c.matches_version(version))
.collect(),
}
}
#[must_use]
pub fn filter_by_change_kind(changes: Vec<IpChange>, kind: ChangeKind) -> Vec<IpChange> {
match kind {
ChangeKind::Both => changes,
ChangeKind::Added => changes.into_iter().filter(IpChange::is_added).collect(),
ChangeKind::Removed => changes.into_iter().filter(IpChange::is_removed).collect(),
}
}
#[must_use]
pub fn diff(
old: &[AdapterSnapshot],
new: &[AdapterSnapshot],
timestamp: SystemTime,
) -> Vec<IpChange> {
let old_by_name: HashMap<&str, &AdapterSnapshot> =
old.iter().map(|a| (a.name.as_str(), a)).collect();
let new_by_name: HashMap<&str, &AdapterSnapshot> =
new.iter().map(|a| (a.name.as_str(), a)).collect();
let mut changes = Vec::new();
for (name, old_adapter) in &old_by_name {
match new_by_name.get(name) {
Some(new_adapter) => {
diff_adapter_addresses(&mut changes, name, old_adapter, new_adapter, timestamp);
}
None => {
add_all_addresses_as_removed(&mut changes, name, old_adapter, timestamp);
}
}
}
for (name, new_adapter) in &new_by_name {
if !old_by_name.contains_key(name) {
add_all_addresses_as_added(&mut changes, name, new_adapter, timestamp);
}
}
changes
}
fn diff_adapter_addresses(
changes: &mut Vec<IpChange>,
adapter_name: &str,
old: &AdapterSnapshot,
new: &AdapterSnapshot,
timestamp: SystemTime,
) {
for addr in &old.ipv4_addresses {
if !new.ipv4_addresses.contains(addr) {
changes.push(IpChange::removed(
adapter_name,
IpAddr::V4(*addr),
timestamp,
));
}
}
for addr in &new.ipv4_addresses {
if !old.ipv4_addresses.contains(addr) {
changes.push(IpChange::added(adapter_name, IpAddr::V4(*addr), timestamp));
}
}
for addr in &old.ipv6_addresses {
if !new.ipv6_addresses.contains(addr) {
changes.push(IpChange::removed(
adapter_name,
IpAddr::V6(*addr),
timestamp,
));
}
}
for addr in &new.ipv6_addresses {
if !old.ipv6_addresses.contains(addr) {
changes.push(IpChange::added(adapter_name, IpAddr::V6(*addr), timestamp));
}
}
}
fn add_all_addresses_as_removed(
changes: &mut Vec<IpChange>,
adapter_name: &str,
adapter: &AdapterSnapshot,
timestamp: SystemTime,
) {
for addr in &adapter.ipv4_addresses {
changes.push(IpChange::removed(
adapter_name,
IpAddr::V4(*addr),
timestamp,
));
}
for addr in &adapter.ipv6_addresses {
changes.push(IpChange::removed(
adapter_name,
IpAddr::V6(*addr),
timestamp,
));
}
}
fn add_all_addresses_as_added(
changes: &mut Vec<IpChange>,
adapter_name: &str,
adapter: &AdapterSnapshot,
timestamp: SystemTime,
) {
for addr in &adapter.ipv4_addresses {
changes.push(IpChange::added(adapter_name, IpAddr::V4(*addr), timestamp));
}
for addr in &adapter.ipv6_addresses {
changes.push(IpChange::added(adapter_name, IpAddr::V6(*addr), timestamp));
}
}
#[cfg(test)]
#[path = "change_tests.rs"]
mod tests;