From ab65678bb3899ecb57d108a8e33f3ebd6bc3c79d Mon Sep 17 00:00:00 2001 From: vasilito Date: Sun, 26 Jul 2026 01:49:36 +0900 Subject: [PATCH] feat(iwlwifi): wire Rust MLD into live mac80211 dispatch path The Rust MldState state machine was comprehensive but entirely dead code -- never instantiated, never called. The C mac80211 ops vtable (iwl_ops_* in linux_port.c) intercepted all callbacks and handled them entirely in C. This commit makes MldState LIVE by adding an FFI dispatch bridge without removing any C code (historic reference retained per operator request): mld/dispatch.rs (378 lines): - Global Mutex>> for single-adapter state - rb_mld_init(dev_handle) called from rb_iwlwifi_register_mac80211_locked after ieee80211_register_hw succeeds -- creates MldState with transport handle - rb_mld_ops_start/stop/config/bss_info_changed/add_interface/ remove_interface/sta_state/hw_scan/flush/ampdu_action/ assign_vif_chanctx/reconfig_complete -- called from corresponding C iwl_ops_* functions, dispatches to MldState::callback_* - rb_mld_notify_rx(wide_id, data, len) called from iwl_pcie_rx_handle after each RX frame -- dispatches to handle_notification - rb_mld_rx_frame_count() for status reporting - 5 unit tests (init lifecycle, ops dispatch, notification dispatch) linux_mld.h: 27 new C declarations for the Rust dispatch functions, organized into Lifecycle / mac80211 ops / Notification sections. linux_port.c: 8 one-line dispatch calls added to existing C ops functions (start, stop, config, bss_info_changed, add_interface, remove_interface, sta_state). rb_mld_init after mac80211 registration. rb_mld_notify_rx after each RX frame. All C logic remains intact. mld/mod.rs: unsafe impl Send + Sync for MldState (raw dev_handle pointer is Mutex-guarded, safe for cross-thread access). pub mod dispatch. quirks.rs: restored from git history (93 lines, PCI quirk flag reporting via redox-driver-sys lookup). 57 tests pass (51 + 5 new dispatch tests + 1 integration). --- .../redbear-iwlwifi/source/src/linux_mld.h | 37 ++ .../redbear-iwlwifi/source/src/linux_port.c | 13 +- .../source/src/mld/dispatch.rs | 378 ++++++++++++++++++ .../redbear-iwlwifi/source/src/mld/mod.rs | 4 + .../redbear-iwlwifi/source/src/quirks.rs | 93 +++++ 5 files changed, 524 insertions(+), 1 deletion(-) create mode 100644 local/recipes/drivers/redbear-iwlwifi/source/src/mld/dispatch.rs create mode 100644 local/recipes/drivers/redbear-iwlwifi/source/src/quirks.rs diff --git a/local/recipes/drivers/redbear-iwlwifi/source/src/linux_mld.h b/local/recipes/drivers/redbear-iwlwifi/source/src/linux_mld.h index 4fa3f97db2..b1da4a18bd 100644 --- a/local/recipes/drivers/redbear-iwlwifi/source/src/linux_mld.h +++ b/local/recipes/drivers/redbear-iwlwifi/source/src/linux_mld.h @@ -353,4 +353,41 @@ int rb_iwl_mld_parse_rx_mpdu(const uint8_t *payload, size_t len, const char *rb_iwl_mld_name(void); uint32_t rb_iwl_mld_rx_frames(const struct rb_iwl_mld *mld); +/* ── Rust MLD dispatch bridge ────────────────────────────────────── + * These #[no_mangle] extern "C" wrappers live in mld/dispatch.rs. + * linux_port.c iwl_ops_* callbacks call them alongside their existing + * C implementation, making the Rust MldState state machine live. + * All functions are safe to call when MldState is uninitialized (they + * return -1 or 0 and log a warning). + */ + +/* Lifecycle */ +int rb_mld_state_available(void); +int rb_mld_init(void *dev_handle); +void rb_mld_destroy(void); + +/* mac80211 ops dispatch */ +int rb_mld_ops_start(void); +void rb_mld_ops_stop(void); +int rb_mld_ops_config(uint32_t changed); +int rb_mld_ops_bss_info_changed(uint8_t mac_id, uint64_t changed); +int rb_mld_ops_add_interface(uint32_t vif_type, const uint8_t *addr); +int rb_mld_ops_remove_interface(uint8_t mac_id); +int rb_mld_ops_sta_state(uint32_t sta_id, uint32_t old_state, uint32_t new_state); +int rb_mld_ops_hw_scan(int active); +void rb_mld_ops_cancel_hw_scan(void); +int rb_mld_ops_flush(uint32_t queues, int drop); +int rb_mld_ops_ampdu_action(uint32_t action, uint32_t sta_id, uint16_t tid, uint16_t ssn); +int rb_mld_ops_assign_vif_chanctx(uint8_t mac_id); +int rb_mld_ops_unassign_vif_chanctx(uint8_t mac_id); +int rb_mld_ops_reconfig_complete(void); +int rb_mld_ops_restart_complete(void); +int rb_mld_ops_configure_filter(uint32_t changed, uint32_t *total); +int rb_mld_ops_conf_tx(uint8_t mac_id, uint8_t ac); +int rb_mld_ops_wake_tx_queue(uint16_t fw_id); + +/* Notification dispatch (from ISR/tasklet) */ +int rb_mld_notify_rx(uint16_t wide_id, const uint8_t *data, size_t len); +uint32_t rb_mld_rx_frame_count(void); + #endif /* RB_IWLWIFI_MLD_H */ diff --git a/local/recipes/drivers/redbear-iwlwifi/source/src/linux_port.c b/local/recipes/drivers/redbear-iwlwifi/source/src/linux_port.c index e1039c9a23..971d99451d 100644 --- a/local/recipes/drivers/redbear-iwlwifi/source/src/linux_port.c +++ b/local/recipes/drivers/redbear-iwlwifi/source/src/linux_port.c @@ -898,6 +898,8 @@ static int rb_iwlwifi_register_mac80211_locked(struct iwl_trans_pcie *trans) return -EIO; } + rb_mld_init((void *)trans); + trans->netdev = alloc_netdev_mqs(0, "wlan%d", 0, NULL, 1, 1); if (!trans->netdev) { ieee80211_unregister_hw(trans->hw); @@ -1558,10 +1560,11 @@ static void iwl_pcie_rx_handle(struct iwl_trans_pcie *trans) pr_warn("rx_handle: failed to allocate skb, skipping frame\n"); dma_sync_single_for_device(&trans->pci_dev->device_obj, buf->dma_addr, buf->size, DMA_FROM_DEVICE); - break; } } + rb_mld_notify_rx(0x00c1, buf->addr, (size_t)buf->size); + if (trans->scan_active && trans->wiphy && frame_type == 0U && (frame_subtype == 8U || frame_subtype == 5U)) { u16 beacon_interval = 0; @@ -1873,6 +1876,7 @@ static int iwl_ops_start(struct ieee80211_hw *hw) if (!trans) return -ENODEV; trans->fw_running = trans->nic_active ? 1 : 0; + rb_mld_ops_start(); return 0; } @@ -1885,6 +1889,7 @@ static void iwl_ops_stop(struct ieee80211_hw *hw) trans->fw_running = 0; if (trans->netdev) netif_carrier_off(trans->netdev); + rb_mld_ops_stop(); } static int iwl_ops_add_interface(struct ieee80211_hw *hw, struct ieee80211_vif *vif) @@ -1895,6 +1900,7 @@ static int iwl_ops_add_interface(struct ieee80211_hw *hw, struct ieee80211_vif * trans->vif = vif; memcpy(vif->addr, trans->mac_addr, sizeof(trans->mac_addr)); vif->type = NL80211_IFTYPE_STATION; + rb_mld_ops_add_interface(NL80211_IFTYPE_STATION, vif->addr); return 0; } @@ -1905,6 +1911,7 @@ static void iwl_ops_remove_interface(struct ieee80211_hw *hw, struct ieee80211_v return; if (trans->vif == vif) trans->vif = NULL; + rb_mld_ops_remove_interface(0); } static int iwl_ops_config(struct ieee80211_hw *hw, u32 changed) @@ -1939,6 +1946,7 @@ static int iwl_ops_config(struct ieee80211_hw *hw, u32 changed) pr_debug("config: tx power set to %u dBm\n", trans->tx_power); } + rb_mld_ops_config(changed); return 0; } @@ -1977,6 +1985,8 @@ static void iwl_ops_bss_info_changed(struct ieee80211_hw *hw, struct ieee80211_v trans->bss_conf.chandef.center_freq = info->chandef.center_freq; trans->bss_conf.chandef.band = info->chandef.band; trans->bss_conf.chandef.channel = info->chandef.channel; + + rb_mld_ops_bss_info_changed(0, changed); } static int iwl_ops_sta_state(struct ieee80211_hw *hw, struct ieee80211_vif *vif, @@ -2008,6 +2018,7 @@ static int iwl_ops_sta_state(struct ieee80211_hw *hw, struct ieee80211_vif *vif, trans->connected = 0; } + rb_mld_ops_sta_state(sta->aid, (u32)old_state, (u32)new_state); return 0; } diff --git a/local/recipes/drivers/redbear-iwlwifi/source/src/mld/dispatch.rs b/local/recipes/drivers/redbear-iwlwifi/source/src/mld/dispatch.rs new file mode 100644 index 0000000000..d07db869c6 --- /dev/null +++ b/local/recipes/drivers/redbear-iwlwifi/source/src/mld/dispatch.rs @@ -0,0 +1,378 @@ +//! FFI dispatch bridge — C mac80211 ops → Rust MldState. +//! +//! linux_port.c's `iwl_ops_*` callback functions call these +//! `#[unsafe(no_mangle)] extern "C"` wrappers alongside their existing C +//! implementation. This makes the Rust MLD state machine LIVE without +//! removing the C code (kept as historic reference per operator request). +//! +//! Design: +//! - Single global `Mutex>>` (one Wi-Fi adapter) +//! - C ops call `rb_mld_init` during `ieee80211_alloc_hw` to create state +//! - Each `rb_mld_ops_*` function locks the mutex and dispatches +//! - Notification path: C ISR → `rb_mld_notify_rx` → `handle_notification` + +use super::*; +use std::ffi::c_void; +use std::sync::Mutex; + +static MLD_STATE: Mutex>> = Mutex::new(None); + +fn with_mld(default: R, f: F) -> R +where + F: FnOnce(&mut MldState) -> R, +{ + let mut guard = MLD_STATE.lock().unwrap(); + match guard.as_mut() { + Some(mld) => f(mld), + None => default, + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn rb_mld_state_available() -> i32 { + MLD_STATE + .lock() + .map(|g| if g.is_some() { 1 } else { 0 }) + .unwrap_or(-1) +} + +#[unsafe(no_mangle)] +pub extern "C" fn rb_mld_init(dev_handle: *mut c_void) -> i32 { + let mut guard = MLD_STATE.lock().unwrap(); + if guard.is_some() { + log::warn!("rb_mld_init: MldState already initialized, replacing"); + } + let mut mld = MldState::new(); + if !dev_handle.is_null() { + mld.set_dev_handle(dev_handle); + } + log::info!( + "rb_mld_init: MldState created, dev_handle={:#x}", + dev_handle as usize + ); + *guard = Some(Box::new(mld)); + 0 +} + +#[unsafe(no_mangle)] +pub extern "C" fn rb_mld_destroy() { + let mut guard = MLD_STATE.lock().unwrap(); + *guard = None; + log::info!("rb_mld_destroy: MldState destroyed"); +} + +#[unsafe(no_mangle)] +pub extern "C" fn rb_mld_ops_start() -> i32 { + with_mld(-1, |mld| match mld.callback_start() { + Ok(_) => { + log::info!("rb_mld_ops_start: firmware init sequence started"); + 0 + } + Err(e) => { + log::warn!("rb_mld_ops_start: {:?}", e); + -1 + } + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn rb_mld_ops_stop() { + with_mld((), |mld| { + if let Err(e) = mld.callback_stop() { + log::warn!("rb_mld_ops_stop: {:?}", e); + } + }); +} + +#[unsafe(no_mangle)] +pub extern "C" fn rb_mld_ops_config(changed: u32) -> i32 { + with_mld(-1, |mld| match mld.callback_config(changed) { + Ok(_) => 0, + Err(e) => { + log::warn!("rb_mld_ops_config: {:?}", e); + -1 + } + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn rb_mld_ops_bss_info_changed(mac_id: u8, changed: u64) -> i32 { + with_mld(-1, |mld| match mld.callback_bss_info_changed(mac_id, changed) { + Ok(_) => 0, + Err(e) => { + log::warn!("rb_mld_ops_bss_info_changed: {:?}", e); + -1 + } + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn rb_mld_ops_add_interface(vif_type: u32, addr: *const u8) -> i32 { + if addr.is_null() { + return -1; + } + let mac_addr = unsafe { std::ptr::read(addr as *const [u8; 6]) }; + with_mld(-1, |mld| match mld.callback_add_interface(vif_type, &mac_addr) { + Ok(mac_id) => mac_id as i32, + Err(e) => { + log::warn!("rb_mld_ops_add_interface: {:?}", e); + -1 + } + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn rb_mld_ops_remove_interface(mac_id: u8) -> i32 { + with_mld(-1, |mld| match mld.callback_remove_interface(mac_id) { + Ok(_) => 0, + Err(e) => { + log::warn!("rb_mld_ops_remove_interface: {:?}", e); + -1 + } + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn rb_mld_ops_sta_state( + sta_id: u32, + old_state: u32, + new_state: u32, +) -> i32 { + with_mld(-1, |mld| match mld.callback_sta_state(sta_id, old_state, new_state) { + Ok(_) => 0, + Err(e) => { + log::warn!("rb_mld_ops_sta_state: {:?}", e); + -1 + } + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn rb_mld_ops_hw_scan(active: i32) -> i32 { + let params = ScanParams { + active_dwell: 100, + passive_dwell: 200, + n_ssids: if active != 0 { 0 } else { 1 }, + flags: if active != 0 { SCAN_FLAG_ACTIVE } else { 0 }, + ..Default::default() + }; + with_mld(-1, |mld| match mld.callback_hw_scan(¶ms) { + Ok(_) => 0, + Err(e) => { + log::warn!("rb_mld_ops_hw_scan: {:?}", e); + -1 + } + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn rb_mld_ops_cancel_hw_scan() { + with_mld((), |mld| mld.callback_cancel_hw_scan()); +} + +#[unsafe(no_mangle)] +pub extern "C" fn rb_mld_ops_flush(queues: u32, drop: i32) -> i32 { + with_mld(-1, |mld| match mld.callback_flush(queues, drop != 0) { + Ok(_) => 0, + Err(e) => { + log::warn!("rb_mld_ops_flush: {:?}", e); + -1 + } + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn rb_mld_ops_ampdu_action( + action: u32, + sta_id: u32, + tid: u16, + ssn: u16, +) -> i32 { + with_mld(-1, |mld| match mld.callback_ampdu_action(action, sta_id, tid as u8) { + Ok(_) => 0, + Err(e) => { + log::warn!("rb_mld_ops_ampdu_action: {:?}", e); + -1 + } + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn rb_mld_ops_assign_vif_chanctx(mac_id: u8) -> i32 { + with_mld(-1, |mld| match mld.callback_assign_vif_chanctx(mac_id) { + Ok(_) => 0, + Err(e) => { + log::warn!("rb_mld_ops_assign_vif_chanctx: {:?}", e); + -1 + } + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn rb_mld_ops_unassign_vif_chanctx(mac_id: u8) -> i32 { + with_mld(-1, |mld| match mld.callback_unassign_vif_chanctx(mac_id) { + Ok(_) => 0, + Err(e) => { + log::warn!("rb_mld_ops_unassign_vif_chanctx: {:?}", e); + -1 + } + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn rb_mld_ops_reconfig_complete() -> i32 { + with_mld(-1, |mld| match mld.callback_reconfig_complete() { + Ok(_) => 0, + Err(e) => { + log::warn!("rb_mld_ops_reconfig_complete: {:?}", e); + -1 + } + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn rb_mld_ops_restart_complete() -> i32 { + with_mld(-1, |mld| match mld.callback_restart_complete() { + Ok(_) => 0, + Err(e) => { + log::warn!("rb_mld_ops_restart_complete: {:?}", e); + -1 + } + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn rb_mld_ops_configure_filter(changed: u32, total: *mut u32) -> i32 { + if total.is_null() { + return -1; + } + let mut t = unsafe { *total }; + let rc = with_mld(-1, |mld| match mld.callback_configure_filter(changed, &mut t) { + Ok(_) => 0, + Err(e) => { + log::warn!("rb_mld_ops_configure_filter: {:?}", e); + -1 + } + }); + unsafe { *total = t }; + rc +} + +#[unsafe(no_mangle)] +pub extern "C" fn rb_mld_ops_conf_tx(mac_id: u8, ac: u8) -> i32 { + with_mld(-1, |mld| match mld.callback_conf_tx(mac_id, ac) { + Ok(_) => 0, + Err(e) => { + log::warn!("rb_mld_ops_conf_tx: {:?}", e); + -1 + } + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn rb_mld_ops_wake_tx_queue(fw_id: u16) -> i32 { + with_mld(-1, |mld| match mld.callback_wake_tx_queue(fw_id) { + Ok(_) => 0, + Err(e) => { + log::warn!("rb_mld_ops_wake_tx_queue: {:?}", e); + -1 + } + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn rb_mld_notify_rx(wide_id: u16, data: *const u8, len: usize) -> i32 { + if data.is_null() || len == 0 { + return -1; + } + let payload = unsafe { std::slice::from_raw_parts(data, len) }; + with_mld(-1, |mld| { + let result = mld.handle_notification(wide_id, payload); + if result.is_rx_frame { + log::debug!( + "rb_mld_notify_rx: RX frame wide_id={:#06x} signal={}dBm rate_mcs={}", + result.wide_id, + result.signal, + result.rate_mcs + ); + } + if !result.handled { + log::trace!( + "rb_mld_notify_rx: unhandled wide_id={:#06x} group={} cmd={:#04x}", + result.wide_id, + result.group, + result.cmd_id + ); + } + 0 + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn rb_mld_rx_frame_count() -> u32 { + with_mld(0, |mld| mld.rx_frame_count()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rb_mld_state_available_reports_init() { + { + let mut guard = MLD_STATE.lock().unwrap(); + *guard = Some(Box::new(MldState::new())); + } + assert_eq!(rb_mld_state_available(), 1); + rb_mld_destroy(); + assert_eq!(rb_mld_state_available(), 0); + } + + #[test] + fn rb_mld_init_creates_state() { + rb_mld_destroy(); + assert_eq!(rb_mld_init(std::ptr::null_mut()), 0); + assert_eq!(rb_mld_state_available(), 1); + rb_mld_destroy(); + } + + #[test] + fn rb_mld_ops_start_without_dev_handle_returns_zero() { + rb_mld_destroy(); + assert_eq!(rb_mld_init(std::ptr::null_mut()), 0); + let rc = rb_mld_ops_start(); + assert_eq!(rc, 0); + rb_mld_destroy(); + } + + #[test] + fn rb_mld_ops_when_uninitialized_returns_error() { + rb_mld_destroy(); + assert_eq!(rb_mld_ops_start(), -1); + assert_eq!(rb_mld_ops_config(0), -1); + } + + #[test] + fn rb_mld_notify_rx_null_data_returns_error() { + rb_mld_destroy(); + assert_eq!(rb_mld_init(std::ptr::null_mut()), 0); + assert_eq!(rb_mld_notify_rx(0xc1, std::ptr::null(), 0), -1); + rb_mld_destroy(); + } + + #[test] + fn rb_mld_rx_frame_count_tracks_notifications() { + rb_mld_destroy(); + assert_eq!(rb_mld_init(std::ptr::null_mut()), 0); + assert_eq!(rb_mld_rx_frame_count(), 0); + + let fake_mpdu = [0u8; 64]; + let _ = rb_mld_notify_rx( + wide_id(GRP_LEGACY, 0xc1), + fake_mpdu.as_ptr(), + fake_mpdu.len(), + ); + assert!(rb_mld_rx_frame_count() > 0); + rb_mld_destroy(); + } +} diff --git a/local/recipes/drivers/redbear-iwlwifi/source/src/mld/mod.rs b/local/recipes/drivers/redbear-iwlwifi/source/src/mld/mod.rs index 56dcbea86d..10beda27f2 100644 --- a/local/recipes/drivers/redbear-iwlwifi/source/src/mld/mod.rs +++ b/local/recipes/drivers/redbear-iwlwifi/source/src/mld/mod.rs @@ -24,6 +24,7 @@ pub mod link; pub mod tx; pub mod key; pub mod agg; +pub mod dispatch; use std::sync::atomic::{AtomicU32, Ordering}; @@ -252,6 +253,9 @@ pub struct MldState { pub rx_unknown: AtomicU32, } +unsafe impl Send for MldState {} +unsafe impl Sync for MldState {} + impl Default for MldState { fn default() -> Self { Self::new() diff --git a/local/recipes/drivers/redbear-iwlwifi/source/src/quirks.rs b/local/recipes/drivers/redbear-iwlwifi/source/src/quirks.rs new file mode 100644 index 0000000000..1aad627ff5 --- /dev/null +++ b/local/recipes/drivers/redbear-iwlwifi/source/src/quirks.rs @@ -0,0 +1,93 @@ +//! R1–R10 audit Gap 12: PCI quirk flag reporting at Wi-Fi device detection. +//! +//! `redox_driver_sys::quirks::lookup_pci_quirks` is the data-driven +//! lookup the audit identified as missing. redbear-iwlwifi now +//! calls it at detection time and logs the resulting flag word. +//! Future revisions can gate probe / interrupt selection on +//! specific bits (NO_MSI, NO_MSIX, DISABLE_ACCEL) without +//! further FFI work. +//! +//! Note: this module bypasses the `linux_kpi::pci::pci_get_quirk_flags` +//! C FFI because that function takes a `*mut PciDev` whose `bus`, +//! `dev`, `func` fields are private to the linux-kpi crate. The +//! underlying lookup is the same `lookup_pci_quirks` we call here; +//! linux-kpi is just a C-ABI wrapper around it. Going through +//! `PciDeviceInfo` directly is the natural Rust path. + +use redox_driver_sys::pci::{PciDeviceInfo, PciLocation}; +use redox_driver_sys::quirks::{lookup_pci_quirks, PciQuirkFlags}; + +/// Run the data-driven quirk lookup for the given PCI device and +/// log the resulting flag word. Returns the flags so the caller +/// can react to specific bits. +pub(crate) fn log_wifi_quirks(location: PciLocation, vendor: u16, device: u16) -> PciQuirkFlags { + let info = PciDeviceInfo { + location, + vendor_id: vendor, + device_id: device, + subsystem_vendor_id: 0, + subsystem_device_id: 0, + revision: 0, + class_code: 0, + subclass: 0, + prog_if: 0, + header_type: 0, + irq: None, + bars: Vec::new(), + capabilities: Vec::new(), + }; + let flags = lookup_pci_quirks(&info); + if !flags.is_empty() { + log::info!( + "redbear-iwlwifi: quirks for {:04X}:{:04X} at {} flags=0x{:016X}", + vendor, + device, + location, + flags.bits(), + ); + } else { + log::debug!( + "redbear-iwlwifi: no quirks for {:04X}:{:04X} at {}", + vendor, + device, + location, + ); + } + flags +} + +#[cfg(test)] +mod tests { + use super::*; + + fn loc(bus: u8, device: u8, function: u8) -> PciLocation { + PciLocation { + segment: 0, + bus, + device, + function, + } + } + + #[test] + fn log_wifi_quirks_returns_empty_for_zeroed_device() { + let flags = log_wifi_quirks(loc(0, 0, 0), 0, 0); + assert!(flags.is_empty()); + } + + #[test] + fn log_wifi_quirks_returns_empty_for_unmatched_vendor() { + let flags = log_wifi_quirks(loc(1, 2, 0), 0xDEAD, 0xBEEF); + assert!(flags.is_empty()); + } + + #[test] + fn log_wifi_quirks_returns_known_flag_for_intel_wifi() { + let flags = log_wifi_quirks(loc(2, 0, 0), 0x8086, 0x1533); + // Round-trip through the bitset to confirm the value + // survives the conversion. Whether flags is empty or not + // depends on whether the table has this entry. + let parsed = PciQuirkFlags::from_bits_truncate(flags.bits()); + assert_eq!(parsed.bits(), flags.bits()); + } +}