netcfg/
interface.rs

1// Copyright 2018 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use either::Either;
6use serde::{Deserialize, Deserializer};
7use std::collections::{HashMap, HashSet};
8use std::sync::atomic::{AtomicU32, Ordering};
9
10use fidl_fuchsia_net_interfaces_admin as fnet_interfaces_admin;
11
12use crate::DeviceClass;
13
14const INTERFACE_PREFIX_WLAN: &str = "wlan";
15const INTERFACE_PREFIX_ETHERNET: &str = "eth";
16const INTERFACE_PREFIX_AP: &str = "ap";
17const INTERFACE_PREFIX_BLACKHOLE: &str = "blackhole";
18
19#[derive(PartialEq, Eq, Debug, Clone, Hash)]
20pub(crate) struct InterfaceNamingIdentifier {
21    pub(crate) mac: fidl_fuchsia_net_ext::MacAddress,
22}
23
24pub(crate) fn generate_identifier(
25    mac_address: &fidl_fuchsia_net_ext::MacAddress,
26) -> InterfaceNamingIdentifier {
27    InterfaceNamingIdentifier { mac: *mac_address }
28}
29
30// Get the NormalizedMac using the last octet of the MAC address. The offset
31// modifies the last_byte in an attempt to avoid naming conflicts.
32// For example, a MAC of `[0x1, 0x1, 0x1, 0x1, 0x1, 0x9]` with offset 0
33// becomes `9`.
34fn get_mac_identifier_from_octets(
35    octets: &[u8; 6],
36    interface_type: crate::InterfaceType,
37    offset: u8,
38) -> Result<u8, anyhow::Error> {
39    if offset == u8::MAX {
40        return Err(anyhow::format_err!(
41            "could not find unique identifier for mac={:?}, interface_type={:?}",
42            octets,
43            interface_type
44        ));
45    }
46
47    let last_byte = octets[octets.len() - 1];
48    let (identifier, _) = last_byte.overflowing_add(offset);
49    Ok(identifier)
50}
51
52// Get the normalized bus path for a topological path.
53// For example, a PCI device at `02:00.1` becomes `02001`.
54// At the time of writing, typical topological paths appear similar to:
55//
56// PCI:
57// "/dev/sys/platform/pt/PCI0/bus/02:00.0/02:00.0/e1000/ethernet"
58//
59// USB over PCI:
60// "/dev/sys/platform/pt/PCI0/bus/00:14.0/00:14.0/xhci/usb/007/ifc-000/<snip>/wlan/wlan-ethernet/ethernet"
61// 00:14:0 following "/PCI0/bus/" represents BDF (Bus Device Function)
62//
63// USB over DWC:
64// "/dev/sys/platform/05:00:18/usb-phy-composite/aml_usb_phy/dwc2/dwc2_phy/dwc2/usb-peripheral/function-000/cdc-eth-function/netdevice-migration/network-device"
65// 05:00:18 following "platform" represents
66// vid(vendor id):pid(product id):did(device id) and are defined in each board file
67//
68// SDIO
69// "/dev/sys/platform/05:00:6/aml-sd-emmc/sdio/broadcom-wlanphy/wlanphy"
70// 05:00:6 following "platform" represents
71// vid(vendor id):pid(product id):did(device id) and are defined in each board file
72//
73// Ethernet Jack for VIM2
74// "/dev/sys/platform/04:02:7/aml-ethernet/Designware-MAC/ethernet"
75//
76// VirtIo
77// "/dev/sys/platform/pt/PC00/bus/00:1e.0/00_1e_0/virtio-net/network-device"
78//
79// Since there is no real standard for topological paths, when no bus path can be found,
80// the function attempts to return one that is unlikely to conflict with any existing path
81// by assuming a bus path of ff:ff:ff, and decrementing from there. This permits
82// generating unique, well-formed names in cases where a matching path component can't be
83// found, while also being relatively recognizable as exceptional.
84fn get_normalized_bus_path_for_topo_path(topological_path: &str) -> String {
85    static PATH_UNIQ_MARKER: AtomicU32 = AtomicU32::new(0xffffff);
86    topological_path
87        .split("/")
88        .find(|pc| {
89            pc.len() >= 7 && pc.chars().all(|c| c.is_digit(16) || c == ':' || c == '.' || c == '_')
90        })
91        .and_then(|s| {
92            Some(s.replace(&[':', '.', '_'], "").trim_end_matches(|c| c == '0').to_string())
93        })
94        .unwrap_or_else(|| format!("{:01$x}", PATH_UNIQ_MARKER.fetch_sub(1, Ordering::SeqCst), 6))
95}
96
97#[derive(Debug)]
98pub struct InterfaceNamingConfig {
99    naming_rules: Vec<NamingRule>,
100    interfaces: HashMap<InterfaceNamingIdentifier, String>,
101}
102
103impl InterfaceNamingConfig {
104    pub(crate) fn from_naming_rules(naming_rules: Vec<NamingRule>) -> InterfaceNamingConfig {
105        InterfaceNamingConfig { naming_rules, interfaces: HashMap::new() }
106    }
107
108    /// Returns a stable interface name for the specified interface.
109    pub(crate) fn generate_stable_name(
110        &mut self,
111        topological_path: &str,
112        mac: &fidl_fuchsia_net_ext::MacAddress,
113        device_class: DeviceClass,
114    ) -> Result<(&str, InterfaceNamingIdentifier), NameGenerationError> {
115        let interface_naming_id = generate_identifier(mac);
116        let info = DeviceInfoRef { topological_path, mac, device_class };
117
118        // Interfaces that are named using the NormalizedMac naming rule are
119        // named to avoid MAC address final octet collisions. When a device
120        // with the same identifier is re-installed, re-attempt name generation
121        // since the MAC identifiers used may have changed.
122        match self.interfaces.remove(&interface_naming_id) {
123            Some(name) => log::info!(
124                "{name} already existed for this identifier\
125            {interface_naming_id:?}. inserting a new one."
126            ),
127            None => {
128                // This interface naming id will have a new entry
129            }
130        }
131
132        let generated_name = self.generate_name(&info)?;
133        if let Some(name) =
134            self.interfaces.insert(interface_naming_id.clone(), generated_name.clone())
135        {
136            log::error!(
137                "{name} was unexpectedly found for {interface_naming_id:?} \
138            when inserting a new name"
139            );
140        }
141
142        // Need to grab a reference to appease the borrow checker.
143        let generated_name = match self.interfaces.get(&interface_naming_id) {
144            Some(name) => Ok(name),
145            None => Err(NameGenerationError::GenerationError(anyhow::format_err!(
146                "expected to see name {generated_name} present since it was just added"
147            ))),
148        }?;
149
150        Ok((generated_name, interface_naming_id))
151    }
152
153    fn generate_name(&self, info: &DeviceInfoRef<'_>) -> Result<String, NameGenerationError> {
154        generate_name_from_naming_rules(&self.naming_rules, &self.interfaces, &info)
155    }
156}
157
158/// An error observed when generating a new name.
159#[derive(Debug)]
160pub enum NameGenerationError {
161    GenerationError(anyhow::Error),
162}
163
164#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Deserialize)]
165#[serde(deny_unknown_fields, rename_all = "lowercase")]
166pub enum BusType {
167    PCI,
168    SDIO,
169    USB,
170    Unknown,
171    VirtIo,
172}
173
174impl BusType {
175    // Retrieve the list of composition rules that comprise the default name
176    // for the interface based on BusType.
177    // Example names for the following default rules:
178    // * USB device: "ethx5"
179    // * PCI/SDIO device: "wlans5009"
180    fn get_default_name_composition_rules(&self) -> Vec<NameCompositionRule> {
181        match *self {
182            BusType::USB | BusType::Unknown => vec![
183                NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::DeviceClass },
184                NameCompositionRule::Static { value: String::from("x") },
185                NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::NormalizedMac },
186            ],
187            BusType::PCI | BusType::SDIO | BusType::VirtIo => vec![
188                NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::DeviceClass },
189                NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::BusType },
190                NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::BusPath },
191            ],
192        }
193    }
194}
195
196impl std::fmt::Display for BusType {
197    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
198        let name = match *self {
199            Self::PCI => "p",
200            Self::SDIO => "s",
201            Self::USB => "u",
202            Self::Unknown => "unk",
203            Self::VirtIo => "v",
204        };
205        write!(f, "{}", name)
206    }
207}
208
209// Extract the `BusType` for a device given the topological path.
210fn get_bus_type_for_topological_path(topological_path: &str) -> BusType {
211    let p = topological_path;
212
213    if p.contains("/PCI0") {
214        // A USB bus will require a bridge over a PCI controller, so a
215        // topological path for a USB bus should contain strings to represent
216        // PCI and USB.
217        if p.contains("/usb/") {
218            return BusType::USB;
219        }
220        return BusType::PCI;
221    } else if p.contains("/usb-peripheral/") {
222        // On VIM3 targets, the USB bus does not require a bridge over a PCI
223        // controller, so the bus path represents the USB type with a
224        // different string.
225        return BusType::USB;
226    } else if p.contains("/sdio/") {
227        return BusType::SDIO;
228    } else if p.contains("/virtio-net/") {
229        return BusType::VirtIo;
230    }
231
232    BusType::Unknown
233}
234
235fn deserialize_glob_pattern<'de, D>(deserializer: D) -> Result<glob::Pattern, D::Error>
236where
237    D: Deserializer<'de>,
238{
239    let buf = String::deserialize(deserializer)?;
240    glob::Pattern::new(&buf).map_err(serde::de::Error::custom)
241}
242
243/// The matching rules available for a `NamingRule`.
244#[derive(Debug, Deserialize, Eq, Hash, PartialEq)]
245#[serde(deny_unknown_fields, rename_all = "snake_case")]
246pub enum MatchingRule {
247    BusTypes(Vec<BusType>),
248    // TODO(https://fxbug.dev/42085144): Use a lightweight regex crate with the basic
249    // regex features to allow for more configurations than glob.
250    #[serde(deserialize_with = "deserialize_glob_pattern")]
251    TopologicalPath(glob::Pattern),
252    DeviceClasses(Vec<DeviceClass>),
253    // Signals whether this rule should match any interface.
254    Any(bool),
255}
256
257/// The matching rules available for a `ProvisoningRule`.
258#[derive(Debug, Deserialize, Eq, Hash, PartialEq)]
259#[serde(untagged)]
260pub enum ProvisioningMatchingRule {
261    // TODO(github.com/serde-rs/serde/issues/912): Use `other` once it supports
262    // deserializing into non-unit variants. `untagged` can only be applied
263    // to the entire enum, so `interface_name` is used as a field to ensure
264    // stability across configuration matching rules.
265    InterfaceName {
266        #[serde(rename = "interface_name", deserialize_with = "deserialize_glob_pattern")]
267        pattern: glob::Pattern,
268    },
269    Common(MatchingRule),
270}
271
272impl MatchingRule {
273    fn does_interface_match(&self, info: &DeviceInfoRef<'_>) -> Result<bool, anyhow::Error> {
274        match &self {
275            MatchingRule::BusTypes(type_list) => {
276                // Match the interface if the interface under comparison
277                // matches any of the types included in the list.
278                let bus_type = get_bus_type_for_topological_path(info.topological_path);
279                Ok(type_list.contains(&bus_type))
280            }
281            MatchingRule::TopologicalPath(pattern) => {
282                // Match the interface if the provided pattern finds any
283                // matches in the interface under comparison's
284                // topological path.
285                Ok(pattern.matches(info.topological_path))
286            }
287            MatchingRule::DeviceClasses(class_list) => {
288                // Match the interface if the interface under comparison
289                // matches any of the types included in the list.
290                Ok(class_list.contains(&info.device_class))
291            }
292            MatchingRule::Any(matches_any_interface) => Ok(*matches_any_interface),
293        }
294    }
295}
296
297impl ProvisioningMatchingRule {
298    fn does_interface_match(
299        &self,
300        info: &DeviceInfoRef<'_>,
301        interface_name: &str,
302    ) -> Result<bool, anyhow::Error> {
303        match &self {
304            ProvisioningMatchingRule::InterfaceName { pattern } => {
305                // Match the interface if the provided pattern finds any
306                // matches in the interface under comparison's name.
307                Ok(pattern.matches(interface_name))
308            }
309            ProvisioningMatchingRule::Common(matching_rule) => {
310                // Handle the other `MatchingRule`s the same as the naming
311                // policy matchers.
312                matching_rule.does_interface_match(info)
313            }
314        }
315    }
316}
317
318// TODO(https://fxbug.dev/42084785): Create dynamic naming rules
319// A naming rule that uses device information to produce a component of
320// the interface's name.
321#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, Deserialize)]
322#[serde(deny_unknown_fields, rename_all = "snake_case")]
323pub enum DynamicNameCompositionRule {
324    BusPath,
325    BusType,
326    DeviceClass,
327    // A unique value seeded by the final octet of the interface's MAC address.
328    NormalizedMac,
329}
330
331impl DynamicNameCompositionRule {
332    // `true` when a rule can be re-tried to produce a different name.
333    fn supports_retry(&self) -> bool {
334        match *self {
335            DynamicNameCompositionRule::BusPath
336            | DynamicNameCompositionRule::BusType
337            | DynamicNameCompositionRule::DeviceClass => false,
338            DynamicNameCompositionRule::NormalizedMac => true,
339        }
340    }
341
342    fn get_name(&self, info: &DeviceInfoRef<'_>, attempt_num: u8) -> Result<String, anyhow::Error> {
343        Ok(match *self {
344            DynamicNameCompositionRule::BusPath => {
345                get_normalized_bus_path_for_topo_path(info.topological_path)
346            }
347            DynamicNameCompositionRule::BusType => {
348                get_bus_type_for_topological_path(info.topological_path).to_string()
349            }
350            DynamicNameCompositionRule::DeviceClass => match info.device_class.into() {
351                crate::InterfaceType::WlanClient => INTERFACE_PREFIX_WLAN,
352                crate::InterfaceType::Ethernet => INTERFACE_PREFIX_ETHERNET,
353                crate::InterfaceType::WlanAp => INTERFACE_PREFIX_AP,
354                crate::InterfaceType::Blackhole => INTERFACE_PREFIX_BLACKHOLE,
355            }
356            .to_string(),
357            DynamicNameCompositionRule::NormalizedMac => {
358                let fidl_fuchsia_net_ext::MacAddress { octets } = info.mac;
359                let mac_identifier =
360                    get_mac_identifier_from_octets(octets, info.device_class.into(), attempt_num)?;
361                format!("{mac_identifier:x}")
362            }
363        })
364    }
365}
366
367// A rule that dictates a component of an interface's name. An interface's name
368// is determined by extracting the name of each rule, in order, and
369// concatenating the results.
370#[derive(Clone, Debug, Deserialize, PartialEq)]
371#[serde(deny_unknown_fields, rename_all = "lowercase", tag = "type")]
372pub enum NameCompositionRule {
373    Static { value: String },
374    Dynamic { rule: DynamicNameCompositionRule },
375    // The default name composition rules based on the device's BusType.
376    // Defined in `BusType::get_default_name_composition_rules`.
377    Default,
378}
379
380/// A rule that dictates how interfaces that align with the property matching
381/// rules should be named.
382#[derive(Debug, Deserialize, PartialEq)]
383#[serde(deny_unknown_fields, rename_all = "lowercase")]
384pub struct NamingRule {
385    /// A set of rules to check against an interface's properties. All rules
386    /// must apply for the naming scheme to take effect.
387    pub matchers: HashSet<MatchingRule>,
388    /// The rules to apply to the interface to produce the interface's name.
389    pub naming_scheme: Vec<NameCompositionRule>,
390}
391
392impl NamingRule {
393    // An interface's name is determined by extracting the name of each rule,
394    // in order, and concatenating the results. Returns an error if the
395    // interface name cannot be generated.
396    fn generate_name(
397        &self,
398        interfaces: &HashMap<InterfaceNamingIdentifier, String>,
399        info: &DeviceInfoRef<'_>,
400    ) -> Result<String, NameGenerationError> {
401        // When a bus type cannot be found for a path, use the USB
402        // default naming policy which uses a MAC address.
403        let bus_type = get_bus_type_for_topological_path(&info.topological_path);
404
405        // Expand any `Default` rules into the `Static` and `Dynamic` rules in a single vector.
406        // If this was being consumed once, we could avoid the call to `collect`. However, since we
407        // want to use it twice, we need to convert it to a form where the items can be itererated
408        // over without consuming them.
409        let expanded_rules = self
410            .naming_scheme
411            .iter()
412            .map(|rule| {
413                if let NameCompositionRule::Default = rule {
414                    Either::Right(bus_type.get_default_name_composition_rules().into_iter())
415                } else {
416                    Either::Left(std::iter::once(rule.clone()))
417                }
418            })
419            .flatten()
420            .collect::<Vec<_>>();
421
422        // Determine whether any rules present support retrying for a unique name.
423        let should_reattempt_on_conflict = expanded_rules.iter().any(|rule| {
424            if let NameCompositionRule::Dynamic { rule } = rule {
425                rule.supports_retry()
426            } else {
427                false
428            }
429        });
430
431        let mut attempt_num = 0u8;
432        loop {
433            let name = expanded_rules
434                .iter()
435                .map(|rule| match rule {
436                    NameCompositionRule::Static { value } => Ok(value.clone()),
437                    // Dynamic rules require the knowledge of `DeviceInfo` properties.
438                    NameCompositionRule::Dynamic { rule } => rule
439                        .get_name(info, attempt_num)
440                        .map_err(NameGenerationError::GenerationError),
441                    NameCompositionRule::Default => {
442                        unreachable!(
443                            "Default naming rules should have been pre-expanded. \
444                             Nested default rules are not supported."
445                        );
446                    }
447                })
448                .collect::<Result<String, NameGenerationError>>()?;
449
450            if interfaces.values().any(|existing_name| existing_name == &name) {
451                if should_reattempt_on_conflict {
452                    attempt_num += 1;
453                    // Try to generate another name with the modified attempt number.
454                    continue;
455                }
456
457                log::warn!(
458                    "name ({name}) already used for an interface installed by netcfg. \
459                 using name since it is possible that the interface using this name is no \
460                 longer active"
461                );
462            }
463            return Ok(name);
464        }
465    }
466
467    // An interface must align with all specified `MatchingRule`s.
468    fn does_interface_match(&self, info: &DeviceInfoRef<'_>) -> bool {
469        self.matchers.iter().all(|rule| rule.does_interface_match(info).unwrap_or_default())
470    }
471}
472
473// Find the first `NamingRule` that matches the device and attempt to
474// construct a name from the provided `NameCompositionRule`s.
475fn generate_name_from_naming_rules(
476    naming_rules: &[NamingRule],
477    interfaces: &HashMap<InterfaceNamingIdentifier, String>,
478    info: &DeviceInfoRef<'_>,
479) -> Result<String, NameGenerationError> {
480    // TODO(https://fxbug.dev/42086002): Consider adding an option to the rules to allow
481    // fallback rules when name generation fails.
482    // Use the first naming rule that matches the interface to enforce consistent
483    // interface names, even if there are other matching rules.
484    let fallback_rule = fallback_naming_rule();
485    let first_matching_rule =
486        naming_rules.iter().find(|rule| rule.does_interface_match(&info)).unwrap_or(
487            // When there are no `NamingRule`s that match the device,
488            // use a fallback rule that has the Default naming scheme.
489            &fallback_rule,
490        );
491
492    first_matching_rule.generate_name(interfaces, &info)
493}
494
495// Matches any device and uses the default naming rule.
496fn fallback_naming_rule() -> NamingRule {
497    NamingRule {
498        matchers: HashSet::from([MatchingRule::Any(true)]),
499        naming_scheme: vec![NameCompositionRule::Default],
500    }
501}
502
503/// The provision action to take if the matchers are satisfied.
504#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Default)]
505#[serde(deny_unknown_fields, rename_all = "lowercase")]
506pub struct ProvisioningAction {
507    /// The type of the provisioning.
508    pub provisioning: ProvisioningType,
509    /// Where the netstack managed routes should be installed.
510    pub netstack_managed_routes_designation: Option<NetstackManagedRoutesDesignation>,
511}
512
513/// Whether the interface should be provisioned locally by netcfg, or
514/// delegated. Provisioning is the set of events that occurs after
515/// interface enumeration, such as starting a DHCP client and assigning
516/// an IP to the interface. Provisioning actions work to support
517/// Internet connectivity.
518#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Default)]
519#[serde(deny_unknown_fields, rename_all = "lowercase")]
520pub enum ProvisioningType {
521    /// Netcfg will provision the interface
522    #[default]
523    Local,
524    /// Netcfg will not provision the interface. The provisioning
525    /// of the interface will occur elsewhere
526    Delegated,
527}
528
529/// Where the netstack managed routes should be stored.
530///
531/// Mirrors [`fnet_interfaces_admin::NetstackManagedRoutesDesignation`].
532#[derive(Copy, Clone, Debug, Deserialize, PartialEq)]
533#[serde(deny_unknown_fields, rename_all = "snake_case")]
534pub enum NetstackManagedRoutesDesignation {
535    Main,
536    InterfaceLocal,
537}
538
539impl From<NetstackManagedRoutesDesignation>
540    for fnet_interfaces_admin::NetstackManagedRoutesDesignation
541{
542    fn from(value: NetstackManagedRoutesDesignation) -> Self {
543        match value {
544            NetstackManagedRoutesDesignation::Main => Self::Main(fnet_interfaces_admin::Empty),
545            NetstackManagedRoutesDesignation::InterfaceLocal => {
546                Self::InterfaceLocal(fnet_interfaces_admin::Empty)
547            }
548        }
549    }
550}
551
552/// A rule that dictates how interfaces that align with the property matching
553/// rules should be provisioned.
554#[derive(Debug, Deserialize, PartialEq)]
555#[serde(deny_unknown_fields, rename_all = "lowercase")]
556pub struct ProvisioningRule {
557    /// A set of rules to check against an interface's properties. All rules
558    /// must apply for the provisioning action to take effect.
559    pub matchers: HashSet<ProvisioningMatchingRule>,
560    /// The provisioning policy that netcfg applies to a matching
561    /// interface.
562    #[serde(flatten)]
563    pub action: ProvisioningAction,
564}
565
566// A ref version of `devices::DeviceInfo` to avoid the need to clone data
567// unnecessarily. Devices without MAC are not supported yet, see
568// `add_new_device` in `lib.rs`. This makes mac into a required field for
569// ease of use.
570pub(super) struct DeviceInfoRef<'a> {
571    pub(super) device_class: DeviceClass,
572    pub(super) mac: &'a fidl_fuchsia_net_ext::MacAddress,
573    pub(super) topological_path: &'a str,
574}
575
576impl<'a> DeviceInfoRef<'a> {
577    pub(super) fn interface_type(&self) -> crate::InterfaceType {
578        let DeviceInfoRef { device_class, mac: _, topological_path: _ } = self;
579        (*device_class).into()
580    }
581
582    pub(super) fn is_wlan_ap(&self) -> bool {
583        let DeviceInfoRef { device_class, mac: _, topological_path: _ } = self;
584        match device_class {
585            DeviceClass::WlanAp => true,
586            DeviceClass::WlanClient
587            | DeviceClass::Virtual
588            | DeviceClass::Ethernet
589            | DeviceClass::Bridge
590            | DeviceClass::Ppp
591            | DeviceClass::Lowpan
592            | DeviceClass::Blackhole => false,
593        }
594    }
595}
596
597impl ProvisioningRule {
598    // An interface must align with all specified `MatchingRule`s.
599    fn does_interface_match(&self, info: &DeviceInfoRef<'_>, interface_name: &str) -> bool {
600        self.matchers
601            .iter()
602            .all(|rule| rule.does_interface_match(info, interface_name).unwrap_or_default())
603    }
604}
605
606// Find the first `ProvisioningRule` that matches the device and get
607// the associated `ProvisioningAction`. By default, use Local provisioning
608// so that Netcfg will provision interfaces unless configuration
609// indicates otherwise.
610pub(crate) fn find_provisioning_action_from_provisioning_rules(
611    provisioning_rules: &[ProvisioningRule],
612    info: &DeviceInfoRef<'_>,
613    interface_name: &str,
614) -> ProvisioningAction {
615    provisioning_rules
616        .iter()
617        .find_map(|rule| {
618            if rule.does_interface_match(&info, &interface_name) {
619                Some(rule.action)
620            } else {
621                None
622            }
623        })
624        .unwrap_or_default()
625}
626
627#[cfg(test)]
628mod tests {
629    use super::*;
630    use assert_matches::assert_matches;
631    use test_case::test_case;
632
633    // This is a lossy conversion between `InterfaceType` and `DeviceClass`
634    // that allows tests to use a `devices::DeviceInfo` struct instead of
635    // handling the fields individually.
636    fn device_class_from_interface_type(ty: crate::InterfaceType) -> DeviceClass {
637        match ty {
638            crate::InterfaceType::Ethernet => DeviceClass::Ethernet,
639            crate::InterfaceType::WlanClient => DeviceClass::WlanClient,
640            crate::InterfaceType::WlanAp => DeviceClass::WlanAp,
641            crate::InterfaceType::Blackhole => DeviceClass::Blackhole,
642        }
643    }
644
645    // usb interfaces
646    #[test_case(
647        "/dev/sys/platform/pt/PCI0/bus/00:14.0/00:14.0/xhci/usb/004/004/ifc-000/ax88179/ethernet",
648        [0x01, 0x01, 0x01, 0x01, 0x01, 0x01],
649        crate::InterfaceType::WlanClient,
650        "wlanx1";
651        "usb_wlan"
652    )]
653    #[test_case(
654        "/dev/sys/platform/pt/PCI0/bus/00:15.0/00:15.0/xhci/usb/004/004/ifc-000/ax88179/ethernet",
655        [0x02, 0x02, 0x02, 0x02, 0x02, 0x02],
656        crate::InterfaceType::Ethernet,
657        "ethx2";
658        "usb_eth"
659    )]
660    // pci interfaces
661    #[test_case(
662        "/dev/sys/platform/pt/PCI0/bus/00:14.0/00:14.0/ethernet",
663        [0x03, 0x03, 0x03, 0x03, 0x03, 0x03],
664        crate::InterfaceType::WlanClient,
665        "wlanp0014";
666        "pci_wlan"
667    )]
668    #[test_case(
669        "/dev/sys/platform/pt/PCI0/bus/00:15.0/00:14.0/ethernet",
670        [0x04, 0x04, 0x04, 0x04, 0x04, 0x04],
671        crate::InterfaceType::Ethernet,
672        "ethp0015";
673        "pci_eth"
674    )]
675    // platform interfaces (ethernet jack and sdio devices)
676    #[test_case(
677        "/dev/sys/platform/05:00:6/aml-sd-emmc/sdio/broadcom-wlanphy/wlanphy",
678        [0x05, 0x05, 0x05, 0x05, 0x05, 0x05],
679        crate::InterfaceType::WlanClient,
680        "wlans05006";
681        "platform_wlan"
682    )]
683    #[test_case(
684        "/dev/sys/platform/04:02:7/aml-ethernet/Designware-MAC/ethernet",
685        [0x07, 0x07, 0x07, 0x07, 0x07, 0x07],
686        crate::InterfaceType::Ethernet,
687        "ethx7";
688        "platform_eth"
689    )]
690    // unknown interfaces
691    #[test_case(
692        "/dev/sys/unknown",
693        [0x08, 0x08, 0x08, 0x08, 0x08, 0x08],
694        crate::InterfaceType::WlanClient,
695        "wlanx8";
696        "unknown_wlan1"
697    )]
698    #[test_case(
699        "unknown",
700        [0x09, 0x09, 0x09, 0x09, 0x09, 0x09],
701        crate::InterfaceType::WlanClient,
702        "wlanx9";
703        "unknown_wlan2"
704    )]
705    #[test_case(
706        "unknown",
707        [0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a],
708        crate::InterfaceType::WlanAp,
709        "apxa";
710        "unknown_ap"
711    )]
712    #[test_case(
713        "/dev/sys/platform/pt/PC00/bus/00:1e.0/00_1e_0/virtio-net/network-device",
714        [0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b],
715        crate::InterfaceType::Ethernet,
716        "ethv001e";
717        "virtio_attached_ethernet"
718    )]
719    // NB: name generation for blackhole interfaces is never expected to be invoked.
720    #[test_case(
721        "/dev/sys/platform/pt/PCI0/bus/00:15.0/00:15.0/xhci/usb/004/004/ifc-000/ax88179/ethernet",
722        [0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c],
723        crate::InterfaceType::Blackhole,
724        "blackholexc";
725        "usb_blackhole")]
726    fn test_generate_name(
727        topological_path: &'static str,
728        mac: [u8; 6],
729        interface_type: crate::InterfaceType,
730        want_name: &'static str,
731    ) {
732        let interface_naming_config = InterfaceNamingConfig::from_naming_rules(vec![]);
733        let name = interface_naming_config
734            .generate_name(&DeviceInfoRef {
735                device_class: device_class_from_interface_type(interface_type),
736                mac: &fidl_fuchsia_net_ext::MacAddress { octets: mac },
737                topological_path,
738            })
739            .expect("failed to generate the name");
740        assert_eq!(name, want_name);
741    }
742
743    struct StableNameTestCase {
744        topological_path: &'static str,
745        mac: [u8; 6],
746        interface_type: crate::InterfaceType,
747        want_name: &'static str,
748        expected_size: usize,
749    }
750
751    // Base case. Interface should be added to config.
752    #[test_case([StableNameTestCase {
753        topological_path: "/dev/sys/platform/pt/PCI0/bus/00:14.0_/00:14.0/ethernet",
754        mac: [0x01, 0x01, 0x01, 0x01, 0x01, 0x01],
755        interface_type: crate::InterfaceType::WlanClient,
756        want_name: "wlanp0014",
757        expected_size: 1 }];
758        "single_interface"
759    )]
760    // Test case that shares the same topo path and different MAC, but same
761    // last octet. Expect to see second interface added with different name.
762    #[test_case([StableNameTestCase {
763        topological_path: "/dev/sys/platform/pt/PCI0/bus/00:14.0_/00:14.0/ethernet",
764        mac: [0x01, 0x01, 0x01, 0x01, 0x01, 0x01],
765        interface_type: crate::InterfaceType::WlanClient,
766        want_name: "wlanp0014",
767        expected_size: 1}, StableNameTestCase {
768        topological_path: "/dev/sys/platform/pt/PCI0/bus/00:14.0_/00:14.0/ethernet",
769        mac: [0xFE, 0x01, 0x01, 0x01, 0x01, 0x01],
770        interface_type: crate::InterfaceType::WlanAp,
771        want_name: "app0014",
772        expected_size: 2 }];
773        "two_interfaces_same_topo_path_different_mac"
774    )]
775    #[test_case([StableNameTestCase {
776        topological_path: "/dev/sys/platform/pt/PCI0/bus/00:14.0_/00:14.0/ethernet",
777        mac: [0x01, 0x01, 0x01, 0x01, 0x01, 0x01],
778        interface_type: crate::InterfaceType::WlanClient,
779        want_name: "wlanp0014",
780        expected_size: 1}, StableNameTestCase {
781        topological_path: "/dev/sys/platform/pt/PCI0/bus/01:00.0/01:00.0/iwlwifi-wlan-softmac/wlan-ethernet/ethernet",
782        mac: [0xFE, 0x01, 0x01, 0x01, 0x01, 0x01],
783        interface_type: crate::InterfaceType::Ethernet,
784        want_name: "ethp01",
785        expected_size: 2 }];
786        "two_distinct_interfaces"
787    )]
788    // Test case that labels iwilwifi as ethernet, then changes the device
789    // class to wlan. The test should detect that the device class doesn't
790    // match the interface name, and overwrite with the new interface name
791    // that does match.
792    #[test_case([StableNameTestCase {
793        topological_path: "/dev/sys/platform/pt/PCI0/bus/01:00.0/01:00.0/iwlwifi-wlan-softmac/wlan-ethernet/ethernet",
794        mac: [0x01, 0x01, 0x01, 0x01, 0x01, 0x01],
795        interface_type: crate::InterfaceType::Ethernet,
796        want_name: "ethp01",
797        expected_size: 1 }, StableNameTestCase {
798        topological_path: "/dev/sys/platform/pt/PCI0/bus/01:00.0/01:00.0/iwlwifi-wlan-softmac/wlan-ethernet/ethernet",
799        mac: [0x01, 0x01, 0x01, 0x01, 0x01, 0x01],
800        interface_type: crate::InterfaceType::WlanClient,
801        want_name: "wlanp01",
802        expected_size: 1 }];
803        "two_interfaces_different_device_class"
804    )]
805    fn test_generate_stable_name(test_cases: impl IntoIterator<Item = StableNameTestCase>) {
806        let mut interface_naming_config = InterfaceNamingConfig::from_naming_rules(vec![]);
807
808        // query an existing interface with the same topo path and a different mac address
809        for (
810            _i,
811            StableNameTestCase { topological_path, mac, interface_type, want_name, expected_size },
812        ) in test_cases.into_iter().enumerate()
813        {
814            let (name, _identifier) = interface_naming_config
815                .generate_stable_name(
816                    topological_path,
817                    &fidl_fuchsia_net_ext::MacAddress { octets: mac },
818                    device_class_from_interface_type(interface_type),
819                )
820                .expect("failed to get the interface name");
821            assert_eq!(name, want_name);
822            // Ensure the number of interfaces we expect are present.
823            assert_eq!(interface_naming_config.interfaces.len(), expected_size);
824        }
825    }
826
827    #[test]
828    fn test_get_usb_255() {
829        let topo_usb = "/dev/pci-00:14.0-fidl/xhci/usb/004/004/ifc-000/ax88179/ethernet";
830
831        // test cases for 256 usb interfaces
832        let mut config = InterfaceNamingConfig::from_naming_rules(vec![]);
833        for n in 0u8..255u8 {
834            let octets = [n, 0x01, 0x01, 0x01, 0x01, 00];
835
836            let interface_naming_id =
837                generate_identifier(&fidl_fuchsia_net_ext::MacAddress { octets });
838
839            let name = config
840                .generate_name(&DeviceInfoRef {
841                    device_class: device_class_from_interface_type(
842                        crate::InterfaceType::WlanClient,
843                    ),
844                    mac: &fidl_fuchsia_net_ext::MacAddress { octets },
845                    topological_path: topo_usb,
846                })
847                .expect("failed to generate the name");
848            assert_eq!(name, format!("{}{:x}", "wlanx", n));
849            assert_matches!(config.interfaces.insert(interface_naming_id, name), None);
850        }
851
852        let octets = [0x00, 0x00, 0x01, 0x01, 0x01, 00];
853        assert!(config
854            .generate_name(&DeviceInfoRef {
855                device_class: device_class_from_interface_type(crate::InterfaceType::WlanClient),
856                mac: &fidl_fuchsia_net_ext::MacAddress { octets },
857                topological_path: topo_usb
858            },)
859            .is_err());
860    }
861
862    #[test]
863    fn test_get_usb_255_with_naming_rule() {
864        let topo_usb = "/dev/pci-00:14.0-fidl/xhci/usb/004/004/ifc-000/ax88179/ethernet";
865
866        let naming_rule = NamingRule {
867            matchers: HashSet::new(),
868            naming_scheme: vec![
869                NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::NormalizedMac },
870                NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::NormalizedMac },
871            ],
872        };
873
874        // test cases for 256 usb interfaces
875        let mut config = InterfaceNamingConfig::from_naming_rules(vec![naming_rule]);
876        for n in 0u8..255u8 {
877            let octets = [n, 0x01, 0x01, 0x01, 0x01, 00];
878            let interface_naming_id =
879                generate_identifier(&fidl_fuchsia_net_ext::MacAddress { octets });
880
881            let info = DeviceInfoRef {
882                device_class: DeviceClass::Ethernet,
883                mac: &fidl_fuchsia_net_ext::MacAddress { octets },
884                topological_path: topo_usb,
885            };
886
887            let name = config.generate_name(&info).expect("failed to generate the name");
888            // With only NormalizedMac as a NameCompositionRule, the name
889            // should simply be the NormalizedMac itself.
890            assert_eq!(name, format!("{n:x}{n:x}"));
891
892            assert_matches!(config.interfaces.insert(interface_naming_id, name), None);
893        }
894
895        let octets = [0x00, 0x00, 0x01, 0x01, 0x01, 00];
896        assert!(config
897            .generate_name(&DeviceInfoRef {
898                device_class: DeviceClass::Ethernet,
899                mac: &fidl_fuchsia_net_ext::MacAddress { octets },
900                topological_path: topo_usb
901            })
902            .is_err());
903    }
904
905    // Arbitrary values for devices::DeviceInfo for cases where DeviceInfo has
906    // no impact on the test.
907    fn default_device_info() -> DeviceInfoRef<'static> {
908        DeviceInfoRef {
909            device_class: DeviceClass::Ethernet,
910            mac: &fidl_fuchsia_net_ext::MacAddress { octets: [0x1, 0x1, 0x1, 0x1, 0x1, 0x1] },
911            topological_path: "",
912        }
913    }
914
915    #[test_case(
916        "/dev/sys/platform/pt/PCI0/bus/00:14.0_/00:14.0/ethernet",
917        vec![BusType::PCI],
918        BusType::PCI,
919        true,
920        "0014";
921        "pci_match"
922    )]
923    #[test_case(
924        "/dev/sys/platform/pt/PCI0/bus/00:14.0_/00:14.0/ethernet",
925        vec![BusType::USB, BusType::SDIO],
926        BusType::PCI,
927        false,
928        "0014";
929        "pci_no_match"
930    )]
931    #[test_case(
932        "/dev/sys/platform/pt/PCI0/bus/00:14.0/00:14.0/xhci/usb/004/004/ifc-000/ax88179/ethernet",
933        vec![BusType::USB],
934        BusType::USB,
935        true,
936        "0014";
937        "pci_usb_match"
938    )]
939    #[test_case(
940        "/dev/sys/platform/05:00:18/usb-phy-composite/aml_usb_phy/dwc2/dwc2_phy/dwc2/usb-peripheral/function-000/cdc-eth-function/netdevice-migration/network-device",
941        vec![BusType::USB],
942        BusType::USB,
943        true,
944        "050018";
945        "dwc_usb_match"
946    )]
947    // Same topological path as the case for USB, but with
948    // non-matching bus types. Ensure that even though PCI is
949    // present in the topological path, it does not match a PCI
950    // controller.
951    #[test_case(
952        "/dev/sys/platform/pt/PCI0/bus/00:14.0/00:14.0/xhci/usb/004/004/ifc-000/ax88179/ethernet",
953        vec![BusType::PCI, BusType::SDIO],
954        BusType::USB,
955        false,
956        "0014";
957        "usb_no_match"
958    )]
959    #[test_case(
960        "/dev/sys/platform/05:00:6/aml-sd-emmc/sdio/broadcom-wlanphy/wlanphy",
961        vec![BusType::SDIO],
962        BusType::SDIO,
963        true,
964        "05006";
965        "sdio_match"
966    )]
967    #[test_case(
968        "/dev/sys/platform/pt/PC00/bus/00:1e.0/00_1e_0/virtio-net/network-device",
969        vec![BusType::VirtIo],
970        BusType::VirtIo,
971        true,
972        "001e";
973        "virtio_match_alternate_location"
974    )]
975    #[test_case(
976        "/dev/sys/platform/pt/PC00/bus/<malformed>/00_1e_0/virtio-net/network-device",
977        vec![BusType::VirtIo],
978        BusType::VirtIo,
979        true,
980        "001e";
981        "virtio_matches_underscore_path"
982    )]
983    #[test_case(
984        "/dev/sys/platform/pt/PC00/bus/00:1e.1/00_1e_1/virtio-net/network-device",
985        vec![BusType::VirtIo],
986        BusType::VirtIo,
987        true,
988        "001e1";
989        "virtio_match_alternate_no_trim"
990    )]
991    #[test_case(
992        "/dev/sys/platform/pt/PC00/bus/<unrecognized_bus_path>/network-device",
993        vec![BusType::Unknown],
994        BusType::Unknown,
995        true,
996        "ffffff";
997        "unknown_bus_match_unrecognized"
998    )]
999    fn test_interface_matching_and_naming_by_bus_properties(
1000        topological_path: &'static str,
1001        bus_types: Vec<BusType>,
1002        expected_bus_type: BusType,
1003        want_match: bool,
1004        want_name: &'static str,
1005    ) {
1006        let device_info = DeviceInfoRef {
1007            topological_path: topological_path,
1008            // `device_class` and `mac` have no effect on `BusType`
1009            // matching, so we use arbitrary values.
1010            ..default_device_info()
1011        };
1012
1013        // Verify the `BusType` determined from the device's
1014        // topological path.
1015        let bus_type = get_bus_type_for_topological_path(&device_info.topological_path);
1016        assert_eq!(bus_type, expected_bus_type);
1017
1018        // Create a matching rule for the provided `BusType` list.
1019        let matching_rule = MatchingRule::BusTypes(bus_types);
1020        let does_interface_match = matching_rule.does_interface_match(&device_info).unwrap();
1021        assert_eq!(does_interface_match, want_match);
1022
1023        let name = get_normalized_bus_path_for_topo_path(&device_info.topological_path);
1024        assert_eq!(name, want_name);
1025
1026        // Ensure that calling again will decrement this. It's unfortunate to need to encode this
1027        // in the test itself, but each test runs separately, so we can't rely on static storage
1028        // between test invocations.
1029        if want_name == "ffffff" {
1030            let name = get_normalized_bus_path_for_topo_path(&device_info.topological_path);
1031            assert_eq!(name, "fffffe");
1032        }
1033    }
1034
1035    // Glob matches the number pattern of XX:XX in the path.
1036    #[test_case(
1037        "/dev/sys/platform/pt/PCI0/bus/00:14.0_/00:14.0/ethernet",
1038        r"*[0-9][0-9]:[0-9][0-9]*",
1039        true;
1040        "pattern_matches"
1041    )]
1042    #[test_case("pattern/will/match/anything", r"*", true; "pattern_matches_any")]
1043    // Glob checks for '00' after the colon but it will not find it.
1044    #[test_case(
1045        "/dev/sys/platform/pt/PCI0/bus/00:14.0_/00:14.0/ethernet",
1046        r"*[0-9][0-9]:00*",
1047        false;
1048        "no_matches"
1049    )]
1050    fn test_interface_matching_by_topological_path(
1051        topological_path: &'static str,
1052        glob_str: &'static str,
1053        want_match: bool,
1054    ) {
1055        let device_info = DeviceInfoRef {
1056            topological_path,
1057            // `device_class` and `mac` have no effect on `TopologicalPath`
1058            // matching, so we use arbitrary values.
1059            ..default_device_info()
1060        };
1061
1062        // Create a matching rule for the provided glob expression.
1063        let matching_rule = MatchingRule::TopologicalPath(glob::Pattern::new(glob_str).unwrap());
1064        let does_interface_match = matching_rule.does_interface_match(&device_info).unwrap();
1065        assert_eq!(does_interface_match, want_match);
1066    }
1067
1068    // Glob matches the default naming by MAC address.
1069    #[test_case(
1070        "ethx5",
1071        r"ethx[0-9]*",
1072        true;
1073        "pattern_matches"
1074    )]
1075    #[test_case("arbitraryname", r"*", true; "pattern_matches_any")]
1076    // Glob matches default naming by SDIO + bus path.
1077    #[test_case(
1078        "wlans1002",
1079        r"eths[0-9][0-9][0-9][0-9]*",
1080        false;
1081        "no_matches"
1082    )]
1083    fn test_interface_matching_by_interface_name(
1084        interface_name: &'static str,
1085        glob_str: &'static str,
1086        want_match: bool,
1087    ) {
1088        // Create a matching rule for the provided glob expression.
1089        let provisioning_matching_rule = ProvisioningMatchingRule::InterfaceName {
1090            pattern: glob::Pattern::new(glob_str).unwrap(),
1091        };
1092        let does_interface_match = provisioning_matching_rule
1093            .does_interface_match(&default_device_info(), interface_name)
1094            .unwrap();
1095        assert_eq!(does_interface_match, want_match);
1096    }
1097
1098    #[test_case(
1099        DeviceClass::Ethernet,
1100        vec![DeviceClass::Ethernet],
1101        true;
1102        "eth_match"
1103    )]
1104    #[test_case(
1105        DeviceClass::Ethernet,
1106        vec![DeviceClass::WlanClient, DeviceClass::WlanAp],
1107        false;
1108        "eth_no_match"
1109    )]
1110    #[test_case(
1111        DeviceClass::WlanClient,
1112        vec![DeviceClass::WlanClient],
1113        true;
1114        "wlan_match"
1115    )]
1116    #[test_case(
1117        DeviceClass::WlanClient,
1118        vec![DeviceClass::Ethernet, DeviceClass::WlanAp],
1119        false;
1120        "wlan_no_match"
1121    )]
1122    #[test_case(
1123        DeviceClass::WlanAp,
1124        vec![DeviceClass::WlanAp],
1125        true;
1126        "ap_match"
1127    )]
1128    #[test_case(
1129        DeviceClass::WlanAp,
1130        vec![DeviceClass::Ethernet, DeviceClass::WlanClient],
1131        false;
1132        "ap_no_match"
1133    )]
1134    fn test_interface_matching_by_device_class(
1135        device_class: DeviceClass,
1136        device_classes: Vec<DeviceClass>,
1137        want_match: bool,
1138    ) {
1139        let device_info = DeviceInfoRef { device_class, ..default_device_info() };
1140
1141        // Create a matching rule for the provided `DeviceClass` list.
1142        let matching_rule = MatchingRule::DeviceClasses(device_classes);
1143        let does_interface_match = matching_rule.does_interface_match(&device_info).unwrap();
1144        assert_eq!(does_interface_match, want_match);
1145    }
1146
1147    // The device information should not have any impact on whether the
1148    // interface matches, but we use Ethernet and Wlan as base cases
1149    // to ensure that all interfaces are accepted or all interfaces
1150    // are rejected.
1151    #[test_case(
1152        DeviceClass::Ethernet,
1153        "/dev/pci-00:15.0-fidl/xhci/usb/004/004/ifc-000/ax88179/ethernet"
1154    )]
1155    #[test_case(DeviceClass::WlanClient, "/dev/pci-00:14.0/ethernet")]
1156    fn test_interface_matching_by_any_matching_rule(
1157        device_class: DeviceClass,
1158        topological_path: &'static str,
1159    ) {
1160        let device_info = DeviceInfoRef {
1161            device_class,
1162            mac: &fidl_fuchsia_net_ext::MacAddress { octets: [0x1, 0x1, 0x1, 0x1, 0x1, 0x1] },
1163            topological_path,
1164        };
1165
1166        // Create a matching rule that should match any interface.
1167        let matching_rule = MatchingRule::Any(true);
1168        let does_interface_match = matching_rule.does_interface_match(&device_info).unwrap();
1169        assert!(does_interface_match);
1170
1171        // Create a matching rule that should reject any interface.
1172        let matching_rule = MatchingRule::Any(false);
1173        let does_interface_match = matching_rule.does_interface_match(&device_info).unwrap();
1174        assert!(!does_interface_match);
1175    }
1176
1177    #[test_case(
1178        DeviceInfoRef { device_class: DeviceClass::Ethernet, ..default_device_info() },
1179        vec![MatchingRule::DeviceClasses(vec![DeviceClass::WlanClient])],
1180        false;
1181        "false_single_rule"
1182    )]
1183    #[test_case(
1184        DeviceInfoRef { device_class: DeviceClass::Ethernet, ..default_device_info() },
1185        vec![MatchingRule::DeviceClasses(vec![DeviceClass::WlanClient]), MatchingRule::Any(true)],
1186        false;
1187        "false_one_rule_of_multiple"
1188    )]
1189    #[test_case(
1190        DeviceInfoRef { device_class: DeviceClass::Ethernet, ..default_device_info() },
1191        vec![MatchingRule::Any(true)],
1192        true;
1193        "true_single_rule"
1194    )]
1195    #[test_case(
1196        DeviceInfoRef { device_class: DeviceClass::Ethernet, ..default_device_info() },
1197        vec![MatchingRule::DeviceClasses(vec![DeviceClass::Ethernet]), MatchingRule::Any(true)],
1198        true;
1199        "true_multiple_rules"
1200    )]
1201    fn test_does_interface_match(
1202        info: DeviceInfoRef<'_>,
1203        matching_rules: Vec<MatchingRule>,
1204        want_match: bool,
1205    ) {
1206        let naming_rule =
1207            NamingRule { matchers: HashSet::from_iter(matching_rules), naming_scheme: Vec::new() };
1208        assert_eq!(naming_rule.does_interface_match(&info), want_match);
1209    }
1210
1211    #[test_case(
1212        DeviceInfoRef { device_class: DeviceClass::Ethernet, ..default_device_info() },
1213        "",
1214        vec![
1215            ProvisioningMatchingRule::Common(
1216                MatchingRule::DeviceClasses(vec![DeviceClass::WlanClient])
1217            )
1218        ],
1219        false;
1220        "false_single_rule"
1221    )]
1222    #[test_case(
1223        DeviceInfoRef { device_class: DeviceClass::WlanClient, ..default_device_info() },
1224        "wlanx5009",
1225        vec![
1226            ProvisioningMatchingRule::InterfaceName {
1227                pattern: glob::Pattern::new("ethx*").unwrap()
1228            },
1229            ProvisioningMatchingRule::Common(MatchingRule::Any(true))
1230        ],
1231        false;
1232        "false_one_rule_of_multiple"
1233    )]
1234    #[test_case(
1235        DeviceInfoRef { device_class: DeviceClass::Ethernet, ..default_device_info() },
1236        "",
1237        vec![ProvisioningMatchingRule::Common(MatchingRule::Any(true))],
1238        true;
1239        "true_single_rule"
1240    )]
1241    #[test_case(
1242        DeviceInfoRef { device_class: DeviceClass::Ethernet, ..default_device_info() },
1243        "wlanx5009",
1244        vec![
1245            ProvisioningMatchingRule::Common(
1246                MatchingRule::DeviceClasses(vec![DeviceClass::Ethernet])
1247            ),
1248            ProvisioningMatchingRule::InterfaceName {
1249                pattern: glob::Pattern::new("wlanx*").unwrap()
1250            }
1251        ],
1252        true;
1253        "true_multiple_rules"
1254    )]
1255    fn test_does_interface_match_provisioning_rule(
1256        info: DeviceInfoRef<'_>,
1257        interface_name: &str,
1258        matching_rules: Vec<ProvisioningMatchingRule>,
1259        want_match: bool,
1260    ) {
1261        let provisioning_rule = ProvisioningRule {
1262            matchers: HashSet::from_iter(matching_rules),
1263            action: ProvisioningAction {
1264                provisioning: ProvisioningType::Local,
1265                ..Default::default()
1266            },
1267        };
1268        assert_eq!(provisioning_rule.does_interface_match(&info, interface_name), want_match);
1269    }
1270
1271    #[test_case(
1272        vec![NameCompositionRule::Static { value: String::from("x") }],
1273        default_device_info(),
1274        "x";
1275        "single_static"
1276    )]
1277    #[test_case(
1278        vec![
1279            NameCompositionRule::Static { value: String::from("eth") },
1280            NameCompositionRule::Static { value: String::from("x") },
1281            NameCompositionRule::Static { value: String::from("100") },
1282        ],
1283        default_device_info(),
1284        "ethx100";
1285        "multiple_static"
1286    )]
1287    #[test_case(
1288        vec![NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::NormalizedMac }],
1289        DeviceInfoRef {
1290            mac: &fidl_fuchsia_net_ext::MacAddress { octets: [0x1, 0x1, 0x1, 0x1, 0x1, 0x1] },
1291            ..default_device_info()
1292        },
1293        "1";
1294        "normalized_mac"
1295    )]
1296    #[test_case(
1297        vec![
1298            NameCompositionRule::Static { value: String::from("eth") },
1299            NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::NormalizedMac },
1300        ],
1301        DeviceInfoRef {
1302            mac: &fidl_fuchsia_net_ext::MacAddress { octets: [0x1, 0x1, 0x1, 0x1, 0x1, 0x9] },
1303            ..default_device_info()
1304        },
1305        "eth9";
1306        "normalized_mac_with_static"
1307    )]
1308    #[test_case(
1309        vec![NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::DeviceClass }],
1310        DeviceInfoRef { device_class: DeviceClass::Ethernet, ..default_device_info() },
1311        "eth";
1312        "eth_device_class"
1313    )]
1314    #[test_case(
1315        vec![NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::DeviceClass }],
1316        DeviceInfoRef { device_class: DeviceClass::WlanClient, ..default_device_info() },
1317        "wlan";
1318        "wlan_device_class"
1319    )]
1320    #[test_case(
1321        vec![
1322            NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::DeviceClass },
1323            NameCompositionRule::Static { value: String::from("x") },
1324        ],
1325        DeviceInfoRef { device_class: DeviceClass::Ethernet, ..default_device_info() },
1326        "ethx";
1327        "device_class_with_static"
1328    )]
1329    #[test_case(
1330        vec![
1331            NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::DeviceClass },
1332            NameCompositionRule::Static { value: String::from("x") },
1333            NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::NormalizedMac },
1334        ],
1335        DeviceInfoRef {
1336            device_class: DeviceClass::WlanClient,
1337            mac: &fidl_fuchsia_net_ext::MacAddress { octets: [0x1, 0x1, 0x1, 0x1, 0x1, 0x8] },
1338            ..default_device_info()
1339        },
1340        "wlanx8";
1341        "device_class_with_static_with_normalized_mac"
1342    )]
1343    #[test_case(
1344        vec![
1345            NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::DeviceClass },
1346            NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::BusType },
1347            NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::BusPath },
1348        ],
1349        DeviceInfoRef {
1350            device_class: DeviceClass::Ethernet,
1351            topological_path: "/dev/sys/platform/pt/PCI0/bus/00:14.0_/00:14.0/ethernet",
1352            ..default_device_info()
1353        },
1354        "ethp0014";
1355        "device_class_with_pci_bus_type_with_bus_path"
1356    )]
1357    #[test_case(
1358        vec![
1359            NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::DeviceClass },
1360            NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::BusType },
1361            NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::BusPath },
1362        ],
1363        DeviceInfoRef {
1364            device_class: DeviceClass::Ethernet,
1365            topological_path: "/dev/sys/platform/pt/PCI0/bus/00:14.0/00:14.0/xhci/usb/004/004/ifc-000/ax88179/ethernet",
1366            ..default_device_info()
1367        },
1368        "ethu0014";
1369        "device_class_with_pci_usb_bus_type_with_bus_path"
1370    )]
1371    #[test_case(
1372        vec![
1373            NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::DeviceClass },
1374            NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::BusType },
1375            NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::BusPath },
1376        ],
1377        DeviceInfoRef {
1378            device_class: DeviceClass::Ethernet,
1379            topological_path: "/dev/sys/platform/05:00:18/usb-phy-composite/aml_usb_phy/dwc2/dwc2_phy/dwc2/usb-peripheral/function-000/cdc-eth-function/netdevice-migration/network-device",
1380            ..default_device_info()
1381        },
1382        "ethu050018";
1383        "device_class_with_dwc_usb_bus_type_with_bus_path"
1384    )]
1385    #[test_case(
1386        vec![NameCompositionRule::Default],
1387        DeviceInfoRef {
1388            device_class: DeviceClass::Ethernet,
1389            topological_path: "/dev/sys/platform/pt/PCI0/bus/00:14.0/00:14.0/xhci/usb/004/004/ifc-000/ax88179/ethernet",
1390            mac: &fidl_fuchsia_net_ext::MacAddress { octets: [0x1, 0x1, 0x1, 0x1, 0x1, 0x2] },
1391        },
1392        "ethx2";
1393        "default_usb_pci"
1394    )]
1395    #[test_case(
1396        vec![NameCompositionRule::Default],
1397        DeviceInfoRef {
1398            device_class: DeviceClass::Ethernet,
1399            topological_path: "/dev/sys/platform/05:00:18/usb-phy-composite/aml_usb_phy/dwc2/dwc2_phy/dwc2/usb-peripheral/function-000/cdc-eth-function/netdevice-migration/network-device",
1400            mac: &fidl_fuchsia_net_ext::MacAddress { octets: [0x1, 0x1, 0x1, 0x1, 0x1, 0x3] },
1401        },
1402        "ethx3";
1403        "default_usb_dwc"
1404    )]
1405    #[test_case(
1406        vec![NameCompositionRule::Default],
1407        DeviceInfoRef {
1408            device_class: DeviceClass::Ethernet,
1409            topological_path: "/dev/sys/platform/05:00:6/aml-sd-emmc/sdio/broadcom-wlanphy/wlanphy",
1410            ..default_device_info()
1411        },
1412        "eths05006";
1413        "default_sdio"
1414    )]
1415    fn test_naming_rules(
1416        composition_rules: Vec<NameCompositionRule>,
1417        info: DeviceInfoRef<'_>,
1418        expected_name: &'static str,
1419    ) {
1420        let naming_rule = NamingRule { matchers: HashSet::new(), naming_scheme: composition_rules };
1421
1422        let name = naming_rule.generate_name(&HashMap::new(), &info);
1423        assert_eq!(name.unwrap(), expected_name.to_owned());
1424    }
1425
1426    #[test]
1427    fn test_generate_name_from_naming_rule_interface_name_exists_no_reattempt() {
1428        let shared_interface_name = "x".to_owned();
1429        let mut interfaces = HashMap::new();
1430        assert_matches!(
1431            interfaces.insert(
1432                InterfaceNamingIdentifier {
1433                    mac: fidl_fuchsia_net_ext::MacAddress {
1434                        octets: [0x1, 0x1, 0x1, 0x1, 0x1, 0x1]
1435                    },
1436                },
1437                shared_interface_name.clone(),
1438            ),
1439            None
1440        );
1441
1442        let naming_rule = NamingRule {
1443            matchers: HashSet::new(),
1444            naming_scheme: vec![NameCompositionRule::Static {
1445                value: shared_interface_name.clone(),
1446            }],
1447        };
1448
1449        let name = naming_rule.generate_name(&interfaces, &default_device_info()).unwrap();
1450        assert_eq!(name, shared_interface_name);
1451    }
1452
1453    // This test is different from `test_get_usb_255_with_naming_rule` as this
1454    // test increments the last byte, ensuring that the offset is reset prior
1455    // to each name being generated.
1456    #[test]
1457    fn test_generate_name_from_naming_rule_many_unique_macs() {
1458        let topo_usb = "/dev/pci-00:14.0-fidl/xhci/usb/004/004/ifc-000/ax88179/ethernet";
1459
1460        let naming_rule = NamingRule {
1461            matchers: HashSet::new(),
1462            naming_scheme: vec![NameCompositionRule::Dynamic {
1463                rule: DynamicNameCompositionRule::NormalizedMac,
1464            }],
1465        };
1466
1467        // test cases for 256 usb interfaces
1468        let mut interfaces = HashMap::new();
1469
1470        for n in 0u8..255u8 {
1471            let octets = [0x01, 0x01, 0x01, 0x01, 0x01, n];
1472            let interface_naming_id =
1473                generate_identifier(&fidl_fuchsia_net_ext::MacAddress { octets });
1474            let info = DeviceInfoRef {
1475                device_class: DeviceClass::Ethernet,
1476                mac: &fidl_fuchsia_net_ext::MacAddress { octets },
1477                topological_path: topo_usb,
1478            };
1479
1480            let name =
1481                naming_rule.generate_name(&interfaces, &info).expect("failed to generate the name");
1482            assert_eq!(name, format!("{n:x}"));
1483
1484            assert_matches!(interfaces.insert(interface_naming_id, name.clone()), None);
1485        }
1486    }
1487
1488    #[test_case(true, "x"; "matches_first_rule")]
1489    #[test_case(false, "ethx1"; "fallback_default")]
1490    fn test_generate_name_from_naming_rules(match_first_rule: bool, expected_name: &'static str) {
1491        // Use an Ethernet device that is determined to have a USB bus type
1492        // from the topological path.
1493        let info = DeviceInfoRef {
1494            device_class: DeviceClass::Ethernet,
1495            mac: &fidl_fuchsia_net_ext::MacAddress { octets: [0x1, 0x1, 0x1, 0x1, 0x1, 0x1] },
1496            topological_path: "/dev/sys/platform/pt/PCI0/bus/00:14.0/00:14.0/xhci/usb/004/004/ifc-000/ax88179/ethernet"
1497        };
1498        let name = generate_name_from_naming_rules(
1499            &[
1500                NamingRule {
1501                    matchers: HashSet::from([MatchingRule::Any(match_first_rule)]),
1502                    naming_scheme: vec![NameCompositionRule::Static { value: String::from("x") }],
1503                },
1504                // Include an arbitrary rule that matches no interface
1505                // to ensure that it has no impact on the test.
1506                NamingRule {
1507                    matchers: HashSet::from([MatchingRule::Any(false)]),
1508                    naming_scheme: vec![NameCompositionRule::Static { value: String::from("y") }],
1509                },
1510            ],
1511            &HashMap::new(),
1512            &info,
1513        )
1514        .unwrap();
1515        assert_eq!(name, expected_name.to_owned());
1516    }
1517
1518    #[test_case(true, ProvisioningType::Delegated; "matches_first_rule")]
1519    #[test_case(false, ProvisioningType::Local; "fallback_default")]
1520    fn test_find_provisioning_action_from_provisioning_rules(
1521        match_first_rule: bool,
1522        expected: ProvisioningType,
1523    ) {
1524        let provisioning_action = find_provisioning_action_from_provisioning_rules(
1525            &[ProvisioningRule {
1526                matchers: HashSet::from([ProvisioningMatchingRule::Common(MatchingRule::Any(
1527                    match_first_rule,
1528                ))]),
1529                action: ProvisioningAction {
1530                    provisioning: ProvisioningType::Delegated,
1531                    ..Default::default()
1532                },
1533            }],
1534            &DeviceInfoRef {
1535                device_class: DeviceClass::WlanClient,
1536                mac: &fidl_fuchsia_net_ext::MacAddress { octets: [0x1, 0x1, 0x1, 0x1, 0x1, 0x1] },
1537                topological_path: "",
1538            },
1539            "wlans5009",
1540        );
1541        assert_eq!(provisioning_action.provisioning, expected);
1542    }
1543}