ddns_a/network/
filter.rs

1//! Adapter filtering for selective monitoring.
2//!
3//! This module provides traits and types for filtering network adapters
4//! based on various criteria (name patterns, adapter kind, etc.).
5//!
6//! # Design
7//!
8//! - **Pure Matchers**: [`KindFilter`] and [`NameRegexFilter`] only answer
9//!   "does this adapter match?" without include/exclude semantics.
10//! - **Filter Chain**: [`FilterChain`] combines matchers with correct semantics:
11//!   - Exclude filters: AND logic (must pass ALL excludes)
12//!   - Include filters: OR logic (pass ANY include, empty = match all)
13//! - **Decorator**: [`FilteredFetcher`] applies filtering transparently
14//!   to any [`AddressFetcher`] implementation.
15
16use std::collections::HashSet;
17
18use regex::Regex;
19
20use super::{AdapterKind, AdapterSnapshot, AddressFetcher, FetchError};
21
22/// Trait for filtering network adapters.
23///
24/// Implementations determine which adapters should be included in monitoring.
25/// Filters are composable via [`FilterChain`].
26///
27/// # Thread Safety
28///
29/// Filters must be `Send + Sync` to support concurrent access in async contexts.
30pub trait AdapterFilter: Send + Sync {
31    /// Returns `true` if the adapter should be included, `false` to filter it out.
32    fn matches(&self, adapter: &AdapterSnapshot) -> bool;
33}
34
35// ============================================================================
36// KindFilter - Pure matcher by adapter kind
37// ============================================================================
38
39/// Filters adapters by their kind (pure matcher, no include/exclude semantics).
40///
41/// This filter matches adapters whose kind is contained in the specified set.
42/// Use with [`FilterChain`] to apply include/exclude logic.
43///
44/// # Examples
45///
46/// ```
47/// use ddns_a::network::filter::{KindFilter, AdapterFilter};
48/// use ddns_a::network::{AdapterSnapshot, AdapterKind};
49///
50/// // Match wireless and ethernet adapters
51/// let filter = KindFilter::new([AdapterKind::Wireless, AdapterKind::Ethernet]);
52///
53/// let eth = AdapterSnapshot::new("eth0", AdapterKind::Ethernet, vec![], vec![]);
54/// let loopback = AdapterSnapshot::new("lo", AdapterKind::Loopback, vec![], vec![]);
55///
56/// assert!(filter.matches(&eth));
57/// assert!(!filter.matches(&loopback));
58/// ```
59#[derive(Debug, Clone)]
60pub struct KindFilter {
61    kinds: HashSet<AdapterKind>,
62}
63
64impl KindFilter {
65    /// Creates a kind filter matching any of the specified kinds.
66    #[must_use]
67    pub fn new(kinds: impl IntoIterator<Item = AdapterKind>) -> Self {
68        Self {
69            kinds: kinds.into_iter().collect(),
70        }
71    }
72
73    /// Returns true if no kinds are configured (matches nothing).
74    #[must_use]
75    pub fn is_empty(&self) -> bool {
76        self.kinds.is_empty()
77    }
78
79    /// Returns the number of kinds in the filter.
80    #[must_use]
81    pub fn len(&self) -> usize {
82        self.kinds.len()
83    }
84
85    /// Returns a reference to the set of kinds.
86    #[must_use]
87    #[allow(clippy::missing_const_for_fn)] // HashSet is not const-compatible
88    pub fn kinds(&self) -> &HashSet<AdapterKind> {
89        &self.kinds
90    }
91}
92
93impl AdapterFilter for KindFilter {
94    fn matches(&self, adapter: &AdapterSnapshot) -> bool {
95        self.kinds.contains(&adapter.kind)
96    }
97}
98
99// ============================================================================
100// FilterChain - Include OR / Exclude AND semantics
101// ============================================================================
102
103/// Filter chain with correct include/exclude semantics.
104///
105/// Evaluation order:
106/// 1. **Exclude filters (AND)**: Any match → reject. Adapter must pass ALL excludes.
107/// 2. **Include filters (OR)**: Any match → accept. Adapter needs to pass ANY include.
108///    Empty includes = match all (passthrough).
109///
110/// # Examples
111///
112/// ```
113/// use ddns_a::network::filter::{FilterChain, KindFilter, AdapterFilter};
114/// use ddns_a::network::{AdapterSnapshot, AdapterKind};
115///
116/// let chain = FilterChain::new()
117///     .exclude(KindFilter::new([AdapterKind::Loopback]))
118///     .include(KindFilter::new([AdapterKind::Wireless, AdapterKind::Ethernet]));
119///
120/// let eth = AdapterSnapshot::new("eth0", AdapterKind::Ethernet, vec![], vec![]);
121/// let virtual_adapter = AdapterSnapshot::new("vm0", AdapterKind::Virtual, vec![], vec![]);
122/// let loopback = AdapterSnapshot::new("lo", AdapterKind::Loopback, vec![], vec![]);
123///
124/// assert!(chain.matches(&eth));       // Included by kind
125/// assert!(!chain.matches(&virtual_adapter)); // Not in include kinds
126/// assert!(!chain.matches(&loopback)); // Excluded
127/// ```
128#[derive(Default)]
129pub struct FilterChain {
130    includes: Vec<Box<dyn AdapterFilter>>,
131    excludes: Vec<Box<dyn AdapterFilter>>,
132}
133
134impl FilterChain {
135    /// Creates an empty filter chain (matches all adapters).
136    #[must_use]
137    pub fn new() -> Self {
138        Self::default()
139    }
140
141    /// Adds an include filter (OR semantics).
142    ///
143    /// Adapters matching ANY include filter will be accepted
144    /// (after passing all exclude filters).
145    #[must_use]
146    pub fn include<F: AdapterFilter + 'static>(mut self, filter: F) -> Self {
147        self.includes.push(Box::new(filter));
148        self
149    }
150
151    /// Adds an exclude filter (AND semantics - must not match ANY).
152    ///
153    /// Adapters matching ANY exclude filter will be rejected,
154    /// regardless of include filters.
155    #[must_use]
156    pub fn exclude<F: AdapterFilter + 'static>(mut self, filter: F) -> Self {
157        self.excludes.push(Box::new(filter));
158        self
159    }
160
161    /// Returns the number of include filters.
162    #[must_use]
163    pub fn include_count(&self) -> usize {
164        self.includes.len()
165    }
166
167    /// Returns the number of exclude filters.
168    #[must_use]
169    pub fn exclude_count(&self) -> usize {
170        self.excludes.len()
171    }
172
173    /// Returns true if no filters are configured.
174    #[must_use]
175    pub fn is_empty(&self) -> bool {
176        self.includes.is_empty() && self.excludes.is_empty()
177    }
178}
179
180impl AdapterFilter for FilterChain {
181    fn matches(&self, adapter: &AdapterSnapshot) -> bool {
182        // 1. Any exclude match → reject
183        if self.excludes.iter().any(|f| f.matches(adapter)) {
184            return false;
185        }
186
187        // 2. No includes = all pass; otherwise any include match → accept
188        self.includes.is_empty() || self.includes.iter().any(|f| f.matches(adapter))
189    }
190}
191
192impl std::fmt::Debug for FilterChain {
193    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
194        f.debug_struct("FilterChain")
195            .field("include_count", &self.includes.len())
196            .field("exclude_count", &self.excludes.len())
197            .finish()
198    }
199}
200
201// ============================================================================
202// NameRegexFilter - Pure matcher by name pattern
203// ============================================================================
204
205/// Filters adapters by name pattern (pure matcher, no include/exclude semantics).
206///
207/// This filter simply checks if the adapter name matches the regex pattern.
208/// Use with [`FilterChain`] to apply include/exclude logic.
209///
210/// # Examples
211///
212/// ```
213/// use ddns_a::network::filter::{NameRegexFilter, AdapterFilter};
214/// use ddns_a::network::{AdapterSnapshot, AdapterKind};
215///
216/// let filter = NameRegexFilter::new(r"^eth").unwrap();
217///
218/// let eth0 = AdapterSnapshot::new("eth0", AdapterKind::Ethernet, vec![], vec![]);
219/// let wlan0 = AdapterSnapshot::new("wlan0", AdapterKind::Wireless, vec![], vec![]);
220///
221/// assert!(filter.matches(&eth0));
222/// assert!(!filter.matches(&wlan0));
223/// ```
224#[derive(Debug)]
225pub struct NameRegexFilter {
226    pattern: Regex,
227}
228
229impl NameRegexFilter {
230    /// Creates a name filter with the given regex pattern.
231    ///
232    /// # Errors
233    ///
234    /// Returns an error if the regex pattern is invalid.
235    pub fn new(pattern: &str) -> Result<Self, regex::Error> {
236        Ok(Self {
237            pattern: Regex::new(pattern)?,
238        })
239    }
240
241    /// Returns a reference to the regex pattern.
242    #[must_use]
243    #[allow(clippy::missing_const_for_fn)] // Regex is not a const type
244    pub fn pattern(&self) -> &Regex {
245        &self.pattern
246    }
247}
248
249impl AdapterFilter for NameRegexFilter {
250    fn matches(&self, adapter: &AdapterSnapshot) -> bool {
251        self.pattern.is_match(&adapter.name)
252    }
253}
254
255// ============================================================================
256// FilteredFetcher - Decorator for AddressFetcher
257// ============================================================================
258
259/// A fetcher decorator that applies a filter to results.
260///
261/// This wraps any [`AddressFetcher`] and filters the returned adapters
262/// using the provided [`AdapterFilter`].
263///
264/// # Type Parameters
265///
266/// - `F`: The inner fetcher type (implements [`AddressFetcher`])
267/// - `A`: The filter type (implements [`AdapterFilter`])
268///
269/// # Examples
270///
271/// ```ignore
272/// use ddns_a::network::filter::{FilteredFetcher, FilterChain, KindFilter};
273/// use ddns_a::network::{AdapterKind, platform::WindowsFetcher};
274///
275/// let filter = FilterChain::new()
276///     .exclude(KindFilter::new([AdapterKind::Virtual, AdapterKind::Loopback]));
277/// let fetcher = FilteredFetcher::new(WindowsFetcher::new(), filter);
278/// let adapters = fetcher.fetch()?; // Only non-virtual, non-loopback adapters
279/// ```
280#[derive(Debug)]
281pub struct FilteredFetcher<F, A> {
282    inner: F,
283    filter: A,
284}
285
286impl<F, A> FilteredFetcher<F, A> {
287    /// Creates a new filtered fetcher.
288    #[must_use]
289    pub const fn new(inner: F, filter: A) -> Self {
290        Self { inner, filter }
291    }
292
293    /// Returns a reference to the inner fetcher.
294    pub const fn inner(&self) -> &F {
295        &self.inner
296    }
297
298    /// Returns a reference to the filter.
299    pub const fn filter(&self) -> &A {
300        &self.filter
301    }
302
303    /// Consumes the filtered fetcher and returns the inner fetcher.
304    pub fn into_inner(self) -> F {
305        self.inner
306    }
307}
308
309impl<F: AddressFetcher, A: AdapterFilter> AddressFetcher for FilteredFetcher<F, A> {
310    fn fetch(&self) -> Result<Vec<AdapterSnapshot>, FetchError> {
311        let snapshots = self.inner.fetch()?;
312        Ok(snapshots
313            .into_iter()
314            .filter(|adapter| self.filter.matches(adapter))
315            .collect())
316    }
317}
318
319// Blanket implementation: any &T where T: AdapterFilter also implements AdapterFilter
320impl<T: AdapterFilter + ?Sized> AdapterFilter for &T {
321    fn matches(&self, adapter: &AdapterSnapshot) -> bool {
322        (*self).matches(adapter)
323    }
324}
325
326// Box<dyn AdapterFilter> implements AdapterFilter
327impl AdapterFilter for Box<dyn AdapterFilter> {
328    fn matches(&self, adapter: &AdapterSnapshot) -> bool {
329        self.as_ref().matches(adapter)
330    }
331}