wlan_sme/client/
scan.rs

1// Copyright 2021 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 crate::Error;
6use crate::client::inspect;
7use fuchsia_inspect::NumericProperty;
8use ieee80211::{Bssid, Ssid};
9use log::warn;
10use std::collections::{HashMap, HashSet, hash_map};
11use std::mem;
12use std::sync::{Arc, LazyLock};
13use wlan_common::bss::BssDescription;
14use wlan_common::channel::{Cbw, Channel};
15use wlan_common::ie::IesMerger;
16use {
17    fidl_fuchsia_wlan_common as fidl_common, fidl_fuchsia_wlan_mlme as fidl_mlme,
18    fidl_fuchsia_wlan_sme as fidl_sme,
19};
20
21const PASSIVE_SCAN_CHANNEL_MS: u32 = 200;
22const ACTIVE_SCAN_PROBE_DELAY_MS: u32 = 5;
23const ACTIVE_SCAN_CHANNEL_MS: u32 = 75;
24
25// A "user"-initiated scan request for the purpose of discovering available networks
26#[derive(Debug, PartialEq)]
27pub struct DiscoveryScan<T> {
28    tokens: Vec<T>,
29    scan_request: fidl_sme::ScanRequest,
30}
31
32impl<T> DiscoveryScan<T> {
33    pub fn new(token: T, scan_request: fidl_sme::ScanRequest) -> Self {
34        Self { tokens: vec![token], scan_request }
35    }
36
37    pub fn matches(&self, scan: &DiscoveryScan<T>) -> bool {
38        self.scan_request == scan.scan_request
39    }
40
41    pub fn merges(&mut self, mut scan: DiscoveryScan<T>) {
42        self.tokens.append(&mut scan.tokens)
43    }
44}
45
46pub struct ScanScheduler<T> {
47    // The currently running scan. We assume that MLME can handle a single concurrent scan
48    // regardless of its own state.
49    current: ScanState<T>,
50    // Pending discovery requests from the user
51    pending_discovery: Vec<DiscoveryScan<T>>,
52    device_info: Arc<fidl_mlme::DeviceInfo>,
53    spectrum_management_support: fidl_common::SpectrumManagementSupport,
54    last_mlme_txn_id: u64,
55}
56
57#[derive(Debug)]
58enum ScanState<T> {
59    NotScanning,
60    ScanningToDiscover {
61        cmd: DiscoveryScan<T>,
62        mlme_txn_id: u64,
63        bss_map: HashMap<Bssid, (fidl_common::BssDescription, IesMerger)>,
64    },
65}
66
67#[derive(Debug)]
68pub struct ScanEnd<T> {
69    pub tokens: Vec<T>,
70    pub result_code: fidl_mlme::ScanResultCode,
71    pub bss_description_list: Vec<BssDescription>,
72}
73
74impl<T> ScanScheduler<T> {
75    pub fn new(
76        device_info: Arc<fidl_mlme::DeviceInfo>,
77        spectrum_management_support: fidl_common::SpectrumManagementSupport,
78    ) -> Self {
79        ScanScheduler {
80            current: ScanState::NotScanning,
81            pending_discovery: Vec::new(),
82            device_info,
83            spectrum_management_support,
84            last_mlme_txn_id: 0,
85        }
86    }
87
88    // Initiate a "discovery" scan. The scan might or might not begin immediately.
89    // The request can be merged with any pending or ongoing requests.
90    // If a ScanRequest is returned, the caller is responsible for forwarding it to MLME.
91    pub fn enqueue_scan_to_discover(
92        &mut self,
93        s: DiscoveryScan<T>,
94    ) -> Option<fidl_mlme::ScanRequest> {
95        if let ScanState::ScanningToDiscover { cmd, .. } = &mut self.current
96            && cmd.matches(&s)
97        {
98            cmd.merges(s);
99            return None;
100        }
101        if let Some(scan_cmd) = self.pending_discovery.iter_mut().find(|cmd| cmd.matches(&s)) {
102            scan_cmd.merges(s);
103            return None;
104        }
105        self.pending_discovery.push(s);
106        self.start_next_scan()
107    }
108
109    // Should be called for every OnScanResult event received from MLME.
110    pub fn on_mlme_scan_result(&mut self, msg: fidl_mlme::ScanResult) -> Result<(), Error> {
111        match &mut self.current {
112            ScanState::NotScanning => Err(Error::ScanResultNotScanning),
113            ScanState::ScanningToDiscover { mlme_txn_id, .. } if *mlme_txn_id != msg.txn_id => {
114                Err(Error::ScanResultWrongTxnId)
115            }
116            ScanState::ScanningToDiscover { bss_map, .. } => {
117                maybe_insert_bss(bss_map, msg.bss);
118                Ok(())
119            }
120        }
121    }
122
123    // Should be called for every OnScanEnd event received from MLME.
124    // If a ScanRequest is returned, the caller is responsible for forwarding it to MLME.
125    pub fn on_mlme_scan_end(
126        &mut self,
127        msg: fidl_mlme::ScanEnd,
128        sme_inspect: &Arc<inspect::SmeTree>,
129    ) -> Result<(ScanEnd<T>, Option<fidl_mlme::ScanRequest>), Error> {
130        match mem::replace(&mut self.current, ScanState::NotScanning) {
131            ScanState::NotScanning => Err(Error::ScanEndNotScanning),
132            ScanState::ScanningToDiscover { mlme_txn_id, .. } if mlme_txn_id != msg.txn_id => {
133                Err(Error::ScanEndWrongTxnId)
134            }
135            ScanState::ScanningToDiscover { cmd, bss_map, .. } => {
136                let scan_end = ScanEnd {
137                    tokens: cmd.tokens,
138                    result_code: msg.code,
139                    bss_description_list: convert_bss_map(bss_map, None::<Ssid>, sme_inspect),
140                };
141
142                let request = self.start_next_scan();
143                Ok((scan_end, request))
144            }
145        }
146    }
147
148    fn start_next_scan(&mut self) -> Option<fidl_mlme::ScanRequest> {
149        let has_pending = !self.pending_discovery.is_empty();
150        (matches!(self.current, ScanState::NotScanning) && has_pending).then(|| {
151            self.last_mlme_txn_id += 1;
152            let scan_cmd = self.pending_discovery.remove(0);
153            let request = new_discovery_scan_request(
154                self.last_mlme_txn_id,
155                &scan_cmd,
156                &self.device_info,
157                self.spectrum_management_support,
158            );
159            self.current = ScanState::ScanningToDiscover {
160                cmd: scan_cmd,
161                mlme_txn_id: self.last_mlme_txn_id,
162                bss_map: HashMap::new(),
163            };
164            request
165        })
166    }
167}
168
169fn maybe_insert_bss(
170    bss_map: &mut HashMap<Bssid, (fidl_common::BssDescription, IesMerger)>,
171    mut fidl_bss: fidl_common::BssDescription,
172) {
173    let mut ies = vec![];
174    std::mem::swap(&mut ies, &mut fidl_bss.ies);
175
176    match bss_map.entry(Bssid::from(fidl_bss.bssid)) {
177        hash_map::Entry::Occupied(mut entry) => {
178            let (existing_bss, ies_merger) = entry.get_mut();
179
180            if (fidl_bss.channel.primary != existing_bss.channel.primary)
181                && (fidl_bss.rssi_dbm < existing_bss.rssi_dbm)
182            {
183                // Assume `fidl_bss` is from an "echo" Beacon frame from the same BSSID
184                return;
185            }
186
187            ies_merger.merge(&ies[..]);
188            if ies_merger.buffer_overflow() {
189                warn!(
190                    "Not merging some IEs due to running out of buffer. BSSID: {}",
191                    Bssid::from(fidl_bss.bssid)
192                );
193            }
194            *existing_bss = fidl_bss;
195        }
196        hash_map::Entry::Vacant(entry) => {
197            let _ = entry.insert((fidl_bss, IesMerger::new(ies)));
198        }
199    }
200}
201
202fn convert_bss_map(
203    bss_map: HashMap<Bssid, (fidl_common::BssDescription, IesMerger)>,
204    ssid_selector: Option<Ssid>,
205    sme_inspect: &Arc<inspect::SmeTree>,
206) -> Vec<BssDescription> {
207    let bss_description_list =
208        bss_map.into_iter().filter_map(|(_bssid, (mut bss, mut ies_merger))| {
209            let _ = sme_inspect.scan_merge_ie_failures.add(ies_merger.merge_ie_failures() as u64);
210
211            let mut ies = ies_merger.finalize();
212            std::mem::swap(&mut ies, &mut bss.ies);
213            let bss: Option<BssDescription> = bss.try_into().ok();
214            if bss.is_none() {
215                let _ = sme_inspect.scan_discard_fidl_bss.add(1);
216            }
217            bss
218        });
219
220    match ssid_selector {
221        None => bss_description_list.collect(),
222        Some(ssid) => bss_description_list.filter(|v| v.ssid == ssid).collect(),
223    }
224}
225
226fn new_scan_request(
227    mlme_txn_id: u64,
228    scan_request: fidl_sme::ScanRequest,
229    ssid_list: Vec<Ssid>,
230    device_info: &fidl_mlme::DeviceInfo,
231    spectrum_management_support: fidl_common::SpectrumManagementSupport,
232) -> fidl_mlme::ScanRequest {
233    let scan_req = fidl_mlme::ScanRequest {
234        txn_id: mlme_txn_id,
235        scan_type: fidl_mlme::ScanTypes::Passive,
236        probe_delay: 0,
237        // TODO(https://fxbug.dev/42169913): SME silently ignores unsupported channels
238        channel_list: get_operating_channels_for_scan(
239            device_info,
240            spectrum_management_support,
241            &scan_request,
242        ),
243        ssid_list: ssid_list.into_iter().map(Ssid::into).collect(),
244        min_channel_time: PASSIVE_SCAN_CHANNEL_MS,
245        max_channel_time: PASSIVE_SCAN_CHANNEL_MS,
246    };
247    match scan_request {
248        fidl_sme::ScanRequest::Active(active_scan_params) => fidl_mlme::ScanRequest {
249            scan_type: fidl_mlme::ScanTypes::Active,
250            ssid_list: active_scan_params.ssids,
251            probe_delay: ACTIVE_SCAN_PROBE_DELAY_MS,
252            min_channel_time: ACTIVE_SCAN_CHANNEL_MS,
253            max_channel_time: ACTIVE_SCAN_CHANNEL_MS,
254            ..scan_req
255        },
256        fidl_sme::ScanRequest::Passive(_) => scan_req,
257    }
258}
259
260fn new_discovery_scan_request<T>(
261    mlme_txn_id: u64,
262    discovery_scan: &DiscoveryScan<T>,
263    device_info: &fidl_mlme::DeviceInfo,
264    spectrum_management_support: fidl_common::SpectrumManagementSupport,
265) -> fidl_mlme::ScanRequest {
266    new_scan_request(
267        mlme_txn_id,
268        discovery_scan.scan_request.clone(),
269        vec![],
270        device_info,
271        spectrum_management_support,
272    )
273}
274
275/// Returns channels at the intersection of
276///
277///   - CANDIDATE_OPERATING_CHANNELS
278///   - This device's operating channels.
279///   - The requested channels (for an active scan only).
280///
281/// When a device does not support DFS, 5 GHz channels are excluded for active scans.
282/// Every 5 GHz channel requires DFS support in at least one regulatory domain, or is otherwise
283/// not allowed in some regulatory domain. This function cautiously excludes 5 GHz channels
284/// for active scans on those devices to ensure accordance with each the regulatory domain's DFS
285/// requirements. The wlan-sme library is the common component in every WLAN interface and
286/// is therefore a sensible place for this filter.
287///
288/// TODO(https://fxbug.dev/42144530): Known quirks about this implementation.
289fn get_operating_channels_for_scan(
290    device_info: &fidl_mlme::DeviceInfo,
291    spectrum_management_support: fidl_common::SpectrumManagementSupport,
292    scan_request: &fidl_sme::ScanRequest,
293) -> Vec<u8> {
294    let mut operating_channels: HashSet<u8> = HashSet::new();
295    for band in &device_info.bands {
296        operating_channels.extend(&band.operating_channels);
297    }
298
299    let requested_channels = match scan_request {
300        fidl_sme::ScanRequest::Active(options) => &options.channels[..],
301        _ => &[],
302    };
303    let channels: Vec<u8> = CANDIDATE_OPERATING_CHANNELS
304        .iter()
305        .filter(|channel| operating_channels.contains(&channel.primary))
306        .filter(|channel| {
307            // Avoid active scans on 5 GHz channels on a non-DFS device. There is no 5 GHz
308            // channel that is valid in all regulatory domains.
309            if let &fidl_sme::ScanRequest::Passive(_) = scan_request {
310                return true;
311            };
312            if channel.is_5ghz() {
313                return spectrum_management_support.dfs.supported;
314            };
315            true
316        })
317        .filter(|channel| {
318            // If this is an active scan and there are any channels specified by the caller,
319            // only include those channels.
320            if !requested_channels.is_empty() {
321                return requested_channels.contains(&channel.primary);
322            }
323            true
324        })
325        .map(|channel| channel.primary)
326        .collect();
327
328    if channels.is_empty() {
329        if !requested_channels.is_empty() {
330            warn!("All channels are filtered out. Requested channels: {:?}", requested_channels);
331        } else {
332            warn!("All channels are filtered out.");
333        };
334    }
335
336    channels
337}
338
339// The following constructs the Channel list at runtime once and leaks its contents
340// as a static reference. Firmware will reject channels if they are not allowed by
341// the current regulatory region.
342static CANDIDATE_OPERATING_CHANNELS: LazyLock<&'static [Channel]> = LazyLock::new(|| {
343    #[rustfmt::skip]
344    let channels = vec![
345        // 2.4 GHz
346        1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
347        // 5 GHz
348        36, 40, 44, 48, 52, 56, 60, 64, 100, 104, 108,
349        112, 116, 120, 124, 128, 132, 136, 140, 144,
350        149, 153, 157, 161, 165,
351    ];
352
353    channels.iter().map(|primary| Channel::new(*primary, Cbw::Cbw20)).collect::<Vec<_>>().leak()
354});
355
356#[cfg(test)]
357mod tests {
358    use super::*;
359    use crate::test_utils;
360    use assert_matches::assert_matches;
361    use fuchsia_inspect::Inspector;
362
363    use ieee80211::MacAddr;
364    use regex::bytes::Regex;
365    use std::fmt::Write;
366    use std::sync::LazyLock;
367    use test_case::test_case;
368    use wlan_common::test_utils::fake_capabilities::fake_5ghz_band_capability;
369    use wlan_common::test_utils::fake_features::fake_spectrum_management_support_empty;
370    use wlan_common::{fake_bss_description, fake_fidl_bss_description};
371
372    static CLIENT_ADDR: LazyLock<MacAddr> =
373        LazyLock::new(|| [0x7A, 0xE7, 0x76, 0xD9, 0xF2, 0x67].into());
374
375    fn passive_discovery_scan(token: i32) -> DiscoveryScan<i32> {
376        DiscoveryScan::new(token, fidl_sme::ScanRequest::Passive(fidl_sme::PassiveScanRequest {}))
377    }
378
379    #[test]
380    fn discovery_scan() {
381        let mut sched = create_sched();
382        let (_inspector, sme_inspect) = sme_inspect();
383        let req = sched
384            .enqueue_scan_to_discover(passive_discovery_scan(10))
385            .expect("expected a ScanRequest");
386        let txn_id = req.txn_id;
387        sched
388            .on_mlme_scan_result(fidl_mlme::ScanResult {
389                txn_id,
390                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
391                bss: fidl_common::BssDescription {
392                    bssid: [1; 6],
393                    ..fake_fidl_bss_description!(Open, ssid: Ssid::try_from("foo").unwrap())
394                },
395            })
396            .expect("expect scan result received");
397        assert_matches!(
398            sched.on_mlme_scan_result(fidl_mlme::ScanResult {
399                txn_id: txn_id + 100, // mismatching transaction id
400                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
401                bss: fidl_common::BssDescription {
402                    bssid: [2; 6],
403                    ..fake_fidl_bss_description!(Open, ssid: Ssid::try_from("bar").unwrap())
404                },
405            },),
406            Err(Error::ScanResultWrongTxnId)
407        );
408        sched
409            .on_mlme_scan_result(fidl_mlme::ScanResult {
410                txn_id,
411                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
412                bss: fidl_common::BssDescription {
413                    bssid: [3; 6],
414                    ..fake_fidl_bss_description!(Open, ssid: Ssid::try_from("qux").unwrap())
415                },
416            })
417            .expect("expect scan result received");
418        let (scan_end, mlme_req) = assert_matches!(
419            sched.on_mlme_scan_end(
420                fidl_mlme::ScanEnd { txn_id, code: fidl_mlme::ScanResultCode::Success },
421                &sme_inspect,
422            ),
423            Ok((scan_end, mlme_req)) => (scan_end, mlme_req)
424        );
425        assert!(mlme_req.is_none());
426        let (tokens, bss_description_list) = assert_matches!(
427            scan_end,
428            ScanEnd {
429                tokens,
430                result_code: fidl_mlme::ScanResultCode::Success,
431                bss_description_list
432            } => (tokens, bss_description_list),
433            "expected discovery scan to be completed successfully"
434        );
435        assert_eq!(vec![10], tokens);
436        let mut ssid_list =
437            bss_description_list.into_iter().map(|bss| bss.ssid).collect::<Vec<_>>();
438        ssid_list.sort();
439        assert_eq!(vec![Ssid::try_from("foo").unwrap(), Ssid::try_from("qux").unwrap()], ssid_list);
440    }
441
442    #[test_case(vec![
443        fake_fidl_bss_description!(Open, ssid: Ssid::try_from("bar").unwrap()),
444        fake_fidl_bss_description!(Open, ssid: Ssid::try_from("baz").unwrap()),
445    ], vec![fake_bss_description!(Open, ssid: Ssid::try_from("baz").unwrap())] ;
446                "when latest BSS Description is new")]
447    #[test_case(vec![
448        fake_fidl_bss_description!(Open, rssi_dbm: -36, channel: Channel::new(149, Cbw::Cbw20)),
449        fake_fidl_bss_description!(Open, rssi_dbm: -84, channel: Channel::new(165, Cbw::Cbw20)),
450    ], vec![fake_bss_description!(Open, rssi_dbm: -36, channel: Channel::new(149, Cbw::Cbw20))] ;
451                "when strong signal is first")]
452    #[test_case(vec![
453        fake_fidl_bss_description!(Open, rssi_dbm: -84, channel: Channel::new(64, Cbw::Cbw20)),
454        fake_fidl_bss_description!(Open, rssi_dbm: -36, channel: Channel::new(50, Cbw::Cbw20)),
455        fake_fidl_bss_description!(Open, rssi_dbm: -80, channel: Channel::new(36, Cbw::Cbw20)),
456    ], vec![fake_bss_description!(Open, rssi_dbm: -36, channel: Channel::new(50, Cbw::Cbw20))];
457                "when strong signal is middle")]
458    #[test_case(vec![
459        fake_fidl_bss_description!(Open, rssi_dbm: -84, channel: Channel::new(64, Cbw::Cbw20)),
460        fake_fidl_bss_description!(Open, rssi_dbm: -80, channel: Channel::new(36, Cbw::Cbw20)),
461        fake_fidl_bss_description!(Open, rssi_dbm: -36, channel: Channel::new(50, Cbw::Cbw20)),
462    ], vec![fake_bss_description!(Open, rssi_dbm: -36, channel: Channel::new(50, Cbw::Cbw20))];
463                "when strong signal is last")]
464    #[test_case(vec![
465        fake_fidl_bss_description!(Open, rssi_dbm: -84, ssid: Ssid::try_from("bar").unwrap(),
466                                   channel: Channel::new(149, Cbw::Cbw20)),
467        fake_fidl_bss_description!(Open, rssi_dbm: -36, ssid: Ssid::try_from("bar").unwrap(),
468                                   channel: Channel::new(165, Cbw::Cbw20)),
469        fake_fidl_bss_description!(Open, rssi_dbm: -40, ssid: Ssid::try_from("baz").unwrap(),
470                                   channel: Channel::new(165, Cbw::Cbw20)),
471    ], vec![fake_bss_description!(Open, rssi_dbm: -40, ssid: Ssid::try_from("baz").unwrap(),
472                                  channel: Channel::new(165, Cbw::Cbw20))];
473                "overwrite latest chosen channel")]
474    fn deduplicate_by_bssid(
475        bss_description_list_from_mlme: Vec<fidl_common::BssDescription>,
476        returned_bss_description_list: Vec<BssDescription>,
477    ) {
478        let mut sched = create_sched();
479        let (_inspector, sme_inspect) = sme_inspect();
480        let req = sched
481            .enqueue_scan_to_discover(passive_discovery_scan(10))
482            .expect("expected a ScanRequest");
483        let txn_id = req.txn_id;
484        for bss in bss_description_list_from_mlme {
485            sched
486                .on_mlme_scan_result(fidl_mlme::ScanResult {
487                    txn_id,
488                    timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
489                    bss,
490                })
491                .expect("expect scan result received");
492        }
493        let (scan_end, mlme_req) = assert_matches!(
494            sched.on_mlme_scan_end(
495                fidl_mlme::ScanEnd { txn_id, code: fidl_mlme::ScanResultCode::Success },
496                &sme_inspect,
497            ),
498            Ok((scan_end, mlme_req)) => (scan_end, mlme_req)
499        );
500        assert!(mlme_req.is_none());
501        let (tokens, bss_description_list) = assert_matches!(
502            scan_end,
503            ScanEnd {
504                tokens,
505                result_code: fidl_mlme::ScanResultCode::Success,
506                bss_description_list
507            } => (tokens, bss_description_list),
508            "expected discovery scan to be completed successfully"
509        );
510        assert_eq!(vec![10], tokens);
511        assert_eq!(bss_description_list, returned_bss_description_list);
512    }
513
514    #[test]
515    fn discovery_scan_merge_ies() {
516        let mut sched = create_sched();
517        let (_inspector, sme_inspect) = sme_inspect();
518        let req = sched
519            .enqueue_scan_to_discover(passive_discovery_scan(10))
520            .expect("expected a ScanRequest");
521        let txn_id = req.txn_id;
522
523        let mut bss = fake_fidl_bss_description!(Open, ssid: Ssid::try_from("ssid").unwrap());
524        // Add an extra IE so we can distinguish this result.
525        let ie_marker1 = &[0xdd, 0x07, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee];
526        bss.ies.extend_from_slice(ie_marker1);
527        sched
528            .on_mlme_scan_result(fidl_mlme::ScanResult {
529                txn_id,
530                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
531                bss,
532            })
533            .expect("expect scan result received");
534
535        let mut bss = fake_fidl_bss_description!(Open, ssid: Ssid::try_from("ssid").unwrap());
536        // Add an extra IE so we can distinguish this result.
537        let ie_marker2 = &[0xdd, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff];
538        bss.ies.extend_from_slice(ie_marker2);
539        sched
540            .on_mlme_scan_result(fidl_mlme::ScanResult {
541                txn_id,
542                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
543                bss,
544            })
545            .expect("expect scan result received");
546        let (scan_end, mlme_req) = assert_matches!(
547            sched.on_mlme_scan_end(
548                fidl_mlme::ScanEnd { txn_id, code: fidl_mlme::ScanResultCode::Success },
549                &sme_inspect,
550            ),
551            Ok((scan_end, mlme_req)) => (scan_end, mlme_req)
552        );
553        assert!(mlme_req.is_none());
554        let (tokens, bss_description_list) = assert_matches!(
555            scan_end,
556            ScanEnd {
557                tokens,
558                result_code: fidl_mlme::ScanResultCode::Success,
559                bss_description_list
560            } => (tokens, bss_description_list),
561            "expected discovery scan to be completed successfully"
562        );
563        assert_eq!(vec![10], tokens);
564
565        assert_eq!(bss_description_list.len(), 1);
566        // Verify that both IEs are processed.
567        assert!(slice_contains(bss_description_list[0].ies(), ie_marker1));
568        assert!(slice_contains(bss_description_list[0].ies(), ie_marker2));
569    }
570
571    fn slice_contains(slice: &[u8], subslice: &[u8]) -> bool {
572        // https://github.com/rust-lang/regex/issues/451#issuecomment-367987989
573        let re = {
574            let mut re_string = String::with_capacity(6 + subslice.len() * 4);
575            re_string += "(?-u:";
576            for b in subslice {
577                write!(re_string, "\\x{b:02X}").unwrap();
578            }
579            re_string += ")";
580            Regex::new(&re_string).unwrap()
581        };
582        re.is_match(slice)
583    }
584
585    #[test_case(&[1, 2, 3], &[] => true; "vacuous")]
586    #[test_case(&[1, 2, 3], &[1u8] => true; "one byte")]
587    #[test_case(&[1, 2, 3], &[2u8, 3] => true; "multiple bytes")]
588    #[test_case(&[1, 1, 1], &[1u8, 1] => true; "multiple matches")]
589    #[test_case(&[1, 2, 3], &[0u8] => false; "no match")]
590    #[test_case(&[1, 2, 3], &[1u8, 2, 3, 4] => false; "too large")]
591    #[test_case(&[0x87, 0x77, 0x78], &[0x77, 0x77] => false; "misaligned match")]
592    fn slice_contains_test(slice: &[u8], subslice: &[u8]) -> bool {
593        slice_contains(slice, subslice)
594    }
595
596    #[test]
597    fn test_passive_discovery_scan_args() {
598        let mut sched = create_sched();
599        let req = sched
600            .enqueue_scan_to_discover(passive_discovery_scan(10))
601            .expect("expected a ScanRequest");
602        assert_eq!(req.txn_id, 1);
603        assert_eq!(req.scan_type, fidl_mlme::ScanTypes::Passive);
604        assert_eq!(
605            req.channel_list.into_iter().collect::<HashSet<_>>(),
606            CANDIDATE_OPERATING_CHANNELS.iter().map(|c| c.primary).collect::<HashSet<_>>()
607        );
608        assert_eq!(req.ssid_list, Vec::<Vec<u8>>::new());
609        assert_eq!(req.probe_delay, 0);
610        assert_eq!(req.min_channel_time, 200);
611        assert_eq!(req.max_channel_time, 200);
612    }
613
614    #[test_case(true, HashSet::from([1, 36, 165]); "dfs_enabled")]
615    #[test_case(false, HashSet::from([1]); "dfs_disabled")]
616    fn test_active_discovery_scan_args_empty(dfs_supported: bool, expected_channels: HashSet<u8>) {
617        let device_info = device_info_with_channel(vec![1, 36, 165]);
618        let mut spectrum_management = fake_spectrum_management_support_empty();
619        if dfs_supported {
620            spectrum_management.dfs.supported = true;
621        }
622        let mut sched: ScanScheduler<i32> =
623            ScanScheduler::new(Arc::new(device_info), spectrum_management);
624        let scan_cmd = DiscoveryScan::new(
625            10,
626            fidl_sme::ScanRequest::Active(fidl_sme::ActiveScanRequest {
627                ssids: vec![],
628                channels: vec![],
629            }),
630        );
631        let req = sched.enqueue_scan_to_discover(scan_cmd).expect("expected a ScanRequest");
632
633        assert_eq!(req.txn_id, 1);
634        assert_eq!(req.scan_type, fidl_mlme::ScanTypes::Active);
635        assert_eq!(req.channel_list.into_iter().collect::<HashSet<_>>(), expected_channels);
636        assert_eq!(req.ssid_list, Vec::<Vec<u8>>::new());
637        assert_eq!(req.probe_delay, 5);
638        assert_eq!(req.min_channel_time, 75);
639        assert_eq!(req.max_channel_time, 75);
640    }
641
642    #[test]
643    fn test_active_discovery_scan_args_filled() {
644        let device_info = device_info_with_channel(vec![1, 36, 165]);
645        let mut sched: ScanScheduler<i32> =
646            ScanScheduler::new(Arc::new(device_info), fake_spectrum_management_support_empty());
647        let ssid1: Vec<u8> = Ssid::try_from("ssid1").unwrap().into();
648        let ssid2: Vec<u8> = Ssid::try_from("ssid2").unwrap().into();
649        let scan_cmd = DiscoveryScan::new(
650            10,
651            fidl_sme::ScanRequest::Active(fidl_sme::ActiveScanRequest {
652                ssids: vec![ssid1.clone(), ssid2.clone()],
653                // TODO(https://fxbug.dev/42169913): SME silently ignores unsupported channels
654                channels: vec![1, 20, 100],
655            }),
656        );
657        let req = sched.enqueue_scan_to_discover(scan_cmd).expect("expected a ScanRequest");
658
659        assert_eq!(req.txn_id, 1);
660        assert_eq!(req.scan_type, fidl_mlme::ScanTypes::Active);
661        assert_eq!(req.channel_list, vec![1]);
662        assert_eq!(req.ssid_list, vec![ssid1, ssid2]);
663        assert_eq!(req.probe_delay, 5);
664        assert_eq!(req.min_channel_time, 75);
665        assert_eq!(req.max_channel_time, 75);
666    }
667
668    #[test]
669    fn test_discovery_scans_dedupe_single_group() {
670        let mut sched = create_sched();
671        let (_inspector, sme_inspect) = sme_inspect();
672
673        // Post one scan command, expect a message to MLME
674        let mlme_req = sched
675            .enqueue_scan_to_discover(passive_discovery_scan(10))
676            .expect("expected a ScanRequest");
677        let txn_id = mlme_req.txn_id;
678
679        // Report a scan result
680        sched
681            .on_mlme_scan_result(fidl_mlme::ScanResult {
682                txn_id,
683                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
684                bss: fidl_common::BssDescription {
685                    bssid: [1; 6],
686                    ..fake_fidl_bss_description!(Open, ssid: Ssid::try_from("foo").unwrap())
687                },
688            })
689            .expect("expect scan result received");
690
691        // Post another command. It should not issue another request to the MLME since
692        // there is already an on-going one
693        assert!(sched.enqueue_scan_to_discover(passive_discovery_scan(20)).is_none());
694
695        // Report another scan result and the end of the scan transaction
696        sched
697            .on_mlme_scan_result(fidl_mlme::ScanResult {
698                txn_id,
699                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
700                bss: fidl_common::BssDescription {
701                    bssid: [2; 6],
702                    ..fake_fidl_bss_description!(Open, ssid: Ssid::try_from("bar").unwrap())
703                },
704            })
705            .expect("expect scan result received");
706        let (scan_end, mlme_req) = assert_matches!(
707            sched.on_mlme_scan_end(
708                fidl_mlme::ScanEnd { txn_id, code: fidl_mlme::ScanResultCode::Success },
709                &sme_inspect,
710            ),
711            Ok((scan_end, mlme_req)) => (scan_end, mlme_req)
712        );
713
714        // We don't expect another request to the MLME
715        assert!(mlme_req.is_none());
716
717        // Expect a discovery result with both tokens and both SSIDs
718        assert_discovery_scan_result(
719            scan_end,
720            vec![10, 20],
721            vec![Ssid::try_from("bar").unwrap(), Ssid::try_from("foo").unwrap()],
722        );
723    }
724
725    #[test]
726    fn test_discovery_scans_dedupe_multiple_groups() {
727        let mut sched = create_sched();
728        let (_inspector, sme_inspect) = sme_inspect();
729
730        // Post a passive scan command, expect a message to MLME
731        let mlme_req = sched
732            .enqueue_scan_to_discover(passive_discovery_scan(10))
733            .expect("expected a ScanRequest");
734        let txn_id = mlme_req.txn_id;
735
736        // Post an active scan command, which should be enqueued until the previous one finishes
737        let scan_cmd = DiscoveryScan::new(
738            20,
739            fidl_sme::ScanRequest::Active(fidl_sme::ActiveScanRequest {
740                ssids: vec![],
741                channels: vec![],
742            }),
743        );
744        assert!(sched.enqueue_scan_to_discover(scan_cmd).is_none());
745
746        // Post a passive scan command. It should be merged with the ongoing one and so should not
747        // issue another request to MLME
748        assert!(sched.enqueue_scan_to_discover(passive_discovery_scan(30)).is_none());
749
750        // Post an active scan command. It should be merged with the active scan command that's
751        // still enqueued, and so should not issue another request to MLME
752        let scan_cmd = DiscoveryScan::new(
753            40,
754            fidl_sme::ScanRequest::Active(fidl_sme::ActiveScanRequest {
755                ssids: vec![],
756                channels: vec![],
757            }),
758        );
759        assert!(sched.enqueue_scan_to_discover(scan_cmd).is_none());
760
761        // Report scan result and scan end
762        sched
763            .on_mlme_scan_result(fidl_mlme::ScanResult {
764                txn_id,
765                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
766                bss: fidl_common::BssDescription {
767                    bssid: [1; 6],
768                    ..fake_fidl_bss_description!(Open, ssid: Ssid::try_from("foo").unwrap())
769                },
770            })
771            .expect("expect scan result received");
772        let (scan_end, mlme_req) = assert_matches!(
773            sched.on_mlme_scan_end(
774                fidl_mlme::ScanEnd { txn_id, code: fidl_mlme::ScanResultCode::Success },
775                &sme_inspect,
776            ),
777            Ok((scan_end, mlme_req)) => (scan_end, mlme_req)
778        );
779
780        // Expect discovery result with 1st and 3rd tokens
781        assert_discovery_scan_result(scan_end, vec![10, 30], vec![Ssid::try_from("foo").unwrap()]);
782
783        // Next mlme_req should be an active scan request
784        assert!(mlme_req.is_some());
785        let mlme_req = mlme_req.unwrap();
786        assert_eq!(mlme_req.scan_type, fidl_mlme::ScanTypes::Active);
787        let txn_id = mlme_req.txn_id;
788
789        // Report scan result and scan end
790        sched
791            .on_mlme_scan_result(fidl_mlme::ScanResult {
792                txn_id,
793                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
794                bss: fidl_common::BssDescription {
795                    bssid: [2; 6],
796                    ..fake_fidl_bss_description!(Open, ssid: Ssid::try_from("bar").unwrap())
797                },
798            })
799            .expect("expect scan result received");
800        let (scan_end, mlme_req) = assert_matches!(
801            sched.on_mlme_scan_end(
802                fidl_mlme::ScanEnd { txn_id, code: fidl_mlme::ScanResultCode::Success },
803                &sme_inspect,
804            ),
805            Ok((scan_end, mlme_req)) => (scan_end, mlme_req)
806        );
807
808        // Expect discovery result with 2nd and 4th tokens
809        assert_discovery_scan_result(scan_end, vec![20, 40], vec![Ssid::try_from("bar").unwrap()]);
810
811        // We don't expect another request to the MLME
812        assert!(mlme_req.is_none());
813    }
814
815    #[test]
816    fn test_discovery_scan_result_wrong_txn_id() {
817        let mut sched = create_sched();
818
819        // Post a passive scan command, expect a message to MLME
820        let mlme_req = sched
821            .enqueue_scan_to_discover(passive_discovery_scan(10))
822            .expect("expected a ScanRequest");
823        let txn_id = mlme_req.txn_id;
824
825        // Report scan result with wrong txn id
826        assert_matches!(
827            sched.on_mlme_scan_result(fidl_mlme::ScanResult {
828                txn_id: txn_id + 1,
829                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
830                bss: fidl_common::BssDescription {
831                    bssid: [1; 6],
832                    ..fake_fidl_bss_description!(Open, ssid: Ssid::try_from("foo").unwrap())
833                },
834            },),
835            Err(Error::ScanResultWrongTxnId)
836        );
837    }
838
839    #[test]
840    fn test_discovery_scan_result_not_scanning() {
841        let mut sched = create_sched();
842        assert_matches!(
843            sched.on_mlme_scan_result(fidl_mlme::ScanResult {
844                txn_id: 0,
845                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
846                bss: fidl_common::BssDescription {
847                    bssid: [1; 6],
848                    ..fake_fidl_bss_description!(Open, ssid: Ssid::try_from("foo").unwrap())
849                },
850            },),
851            Err(Error::ScanResultNotScanning)
852        );
853    }
854
855    #[test]
856    fn test_discovery_scan_end_wrong_txn_id() {
857        let mut sched = create_sched();
858        let (_inspector, sme_inspect) = sme_inspect();
859
860        // Post a passive scan command, expect a message to MLME
861        let mlme_req = sched
862            .enqueue_scan_to_discover(passive_discovery_scan(10))
863            .expect("expected a ScanRequest");
864        let txn_id = mlme_req.txn_id;
865
866        assert_matches!(
867            sched.on_mlme_scan_end(
868                fidl_mlme::ScanEnd { txn_id: txn_id + 1, code: fidl_mlme::ScanResultCode::Success },
869                &sme_inspect,
870            ),
871            Err(Error::ScanEndWrongTxnId)
872        );
873    }
874
875    #[test]
876    fn test_discovery_scan_end_not_scanning() {
877        let mut sched = create_sched();
878        let (_inspector, sme_inspect) = sme_inspect();
879        assert_matches!(
880            sched.on_mlme_scan_end(
881                fidl_mlme::ScanEnd { txn_id: 0, code: fidl_mlme::ScanResultCode::Success },
882                &sme_inspect,
883            ),
884            Err(Error::ScanEndNotScanning)
885        );
886    }
887
888    fn assert_discovery_scan_result(
889        scan_end: ScanEnd<i32>,
890        expected_tokens: Vec<i32>,
891        expected_ssids: Vec<Ssid>,
892    ) {
893        let (tokens, bss_description_list) = assert_matches!(
894            scan_end,
895            ScanEnd {
896                tokens,
897                result_code: fidl_mlme::ScanResultCode::Success,
898                bss_description_list
899            } => (tokens, bss_description_list),
900            "expected discovery scan to be completed successfully"
901        );
902        assert_eq!(tokens, expected_tokens);
903        let mut ssid_list =
904            bss_description_list.into_iter().map(|bss| bss.ssid.clone()).collect::<Vec<_>>();
905        ssid_list.sort();
906        assert_eq!(ssid_list, expected_ssids);
907    }
908
909    fn create_sched() -> ScanScheduler<i32> {
910        ScanScheduler::new(
911            Arc::new(test_utils::fake_device_info(*CLIENT_ADDR)),
912            fake_spectrum_management_support_empty(),
913        )
914    }
915
916    fn device_info_with_channel(operating_channels: Vec<u8>) -> fidl_mlme::DeviceInfo {
917        fidl_mlme::DeviceInfo {
918            bands: vec![fidl_mlme::BandCapability {
919                operating_channels,
920                ..fake_5ghz_band_capability()
921            }],
922            ..test_utils::fake_device_info(*CLIENT_ADDR)
923        }
924    }
925
926    fn sme_inspect() -> (Inspector, Arc<inspect::SmeTree>) {
927        let inspector = Inspector::default();
928        let sme_inspect = Arc::new(inspect::SmeTree::new(
929            inspector.clone(),
930            inspector.root().create_child("usme"),
931            &test_utils::fake_device_info([1u8; 6].into()),
932            &fake_spectrum_management_support_empty(),
933        ));
934        (inspector, sme_inspect)
935    }
936}