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(ð));
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(ð)); // 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(ð0));
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}