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::client::inspect;
6use crate::Error;
7use fuchsia_inspect::NumericProperty;
8use ieee80211::{Bssid, Ssid};
9use log::warn;
10use std::collections::{hash_map, HashMap, HashSet};
11use std::mem;
12use std::sync::Arc;
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            if cmd.matches(&s) {
97                cmd.merges(s);
98                return None;
99            }
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 (ref mut existing_bss, ref mut 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_channels_to_scan(device_info, spectrum_management_support, &scan_request),
239        ssid_list: ssid_list.into_iter().map(Ssid::into).collect(),
240        min_channel_time: PASSIVE_SCAN_CHANNEL_MS,
241        max_channel_time: PASSIVE_SCAN_CHANNEL_MS,
242    };
243    match scan_request {
244        fidl_sme::ScanRequest::Active(active_scan_params) => fidl_mlme::ScanRequest {
245            scan_type: fidl_mlme::ScanTypes::Active,
246            ssid_list: active_scan_params.ssids,
247            probe_delay: ACTIVE_SCAN_PROBE_DELAY_MS,
248            min_channel_time: ACTIVE_SCAN_CHANNEL_MS,
249            max_channel_time: ACTIVE_SCAN_CHANNEL_MS,
250            ..scan_req
251        },
252        fidl_sme::ScanRequest::Passive(_) => scan_req,
253    }
254}
255
256fn new_discovery_scan_request<T>(
257    mlme_txn_id: u64,
258    discovery_scan: &DiscoveryScan<T>,
259    device_info: &fidl_mlme::DeviceInfo,
260    spectrum_management_support: fidl_common::SpectrumManagementSupport,
261) -> fidl_mlme::ScanRequest {
262    new_scan_request(
263        mlme_txn_id,
264        discovery_scan.scan_request.clone(),
265        vec![],
266        device_info,
267        spectrum_management_support,
268    )
269}
270
271// TODO(https://fxbug.dev/42169913): SME silently ignores unsupported channels
272/// Get channels to scan depending on device's capability and scan type. If scan type is passive,
273/// or if scan type is active but the device handles DFS channels, then the channels returned by
274/// this function are the intersection of device's supported channels and Fuchsia supported
275/// channels. If scan type is active and the device doesn't handle DFS channels, then the return
276/// value excludes DFS channels.
277///
278/// Example:
279///
280/// Suppose that Fuchsia supported channels are [1, 2, 52], and 1, 2 are non-DFS channels while
281/// 112 is DFS channel. Also suppose that device's supported channels are [1, 52] as parameter
282/// to fidl_mlme::DeviceInfo below.
283///
284/// ScanType | Device handles DFS | Return values
285/// ---------+--------------------+-----------------
286/// Passive  | Y                  | [1, 52]
287/// Passive  | N                  | [1, 52]
288/// Active   | Y                  | [1, 52]
289/// Active   | N                  | [1]
290fn get_channels_to_scan(
291    device_info: &fidl_mlme::DeviceInfo,
292    spectrum_management_support: fidl_common::SpectrumManagementSupport,
293    scan_request: &fidl_sme::ScanRequest,
294) -> Vec<u8> {
295    let mut operating_channels: HashSet<u8> = HashSet::new();
296    for band in &device_info.bands {
297        operating_channels.extend(&band.operating_channels);
298    }
299
300    let requested_channels = match scan_request {
301        fidl_sme::ScanRequest::Active(options) => &options.channels[..],
302        _ => &[],
303    };
304    let channels: Vec<u8> = SUPPORTED_20_MHZ_CHANNELS
305        .iter()
306        .filter(|channel| operating_channels.contains(channel))
307        .filter(|channel| {
308            if let &fidl_sme::ScanRequest::Passive(_) = scan_request {
309                return true;
310            };
311            if spectrum_management_support.dfs.supported {
312                return true;
313            };
314            !Channel::new(**channel, Cbw::Cbw20).is_dfs()
315        })
316        .filter(|chan| {
317            // If this is an active scan and there are any channels specified by the caller,
318            // only include those channels.
319            if !requested_channels.is_empty() {
320                return requested_channels.contains(chan);
321            }
322            true
323        })
324        .copied()
325        .collect();
326
327    if channels.is_empty() {
328        if !requested_channels.is_empty() {
329            warn!("All channels are filtered out. Requested channels: {:?}", requested_channels);
330        } else {
331            warn!("All channels are filtered out.");
332        };
333    }
334
335    channels
336}
337
338// TODO(65792): Evaluate options for where and how we select what channels to scan.
339// Firmware will reject channels if they are not allowed by the current regulatory region.
340const SUPPORTED_20_MHZ_CHANNELS: &[u8] = &[
341    // 5GHz UNII-1
342    36, 40, 44, 48, // 5GHz UNII-2 Middle
343    52, 56, 60, 64, // 5GHz UNII-2 Extended
344    100, 104, 108, 112, 116, 120, 124, 128, 132, 136, 140, 144, // 5GHz UNII-3
345    149, 153, 157, 161, 165, // 5GHz
346    1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, // 2GHz
347];
348
349#[cfg(test)]
350mod tests {
351    use super::*;
352    use crate::test_utils;
353    use fuchsia_inspect::Inspector;
354
355    use ieee80211::MacAddr;
356    use lazy_static::lazy_static;
357    use regex::bytes::Regex;
358    use std::fmt::Write;
359    use test_case::test_case;
360    use wlan_common::test_utils::fake_capabilities::fake_5ghz_band_capability;
361    use wlan_common::test_utils::fake_features::fake_spectrum_management_support_empty;
362    use wlan_common::{assert_variant, fake_bss_description, fake_fidl_bss_description};
363
364    lazy_static! {
365        static ref CLIENT_ADDR: MacAddr = [0x7A, 0xE7, 0x76, 0xD9, 0xF2, 0x67].into();
366    }
367
368    fn passive_discovery_scan(token: i32) -> DiscoveryScan<i32> {
369        DiscoveryScan::new(token, fidl_sme::ScanRequest::Passive(fidl_sme::PassiveScanRequest {}))
370    }
371
372    #[test]
373    fn discovery_scan() {
374        let mut sched = create_sched();
375        let (_inspector, sme_inspect) = sme_inspect();
376        let req = sched
377            .enqueue_scan_to_discover(passive_discovery_scan(10))
378            .expect("expected a ScanRequest");
379        let txn_id = req.txn_id;
380        sched
381            .on_mlme_scan_result(fidl_mlme::ScanResult {
382                txn_id,
383                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
384                bss: fidl_common::BssDescription {
385                    bssid: [1; 6],
386                    ..fake_fidl_bss_description!(Open, ssid: Ssid::try_from("foo").unwrap())
387                },
388            })
389            .expect("expect scan result received");
390        assert_variant!(
391            sched.on_mlme_scan_result(fidl_mlme::ScanResult {
392                txn_id: txn_id + 100, // mismatching transaction id
393                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
394                bss: fidl_common::BssDescription {
395                    bssid: [2; 6],
396                    ..fake_fidl_bss_description!(Open, ssid: Ssid::try_from("bar").unwrap())
397                },
398            },),
399            Err(Error::ScanResultWrongTxnId)
400        );
401        sched
402            .on_mlme_scan_result(fidl_mlme::ScanResult {
403                txn_id,
404                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
405                bss: fidl_common::BssDescription {
406                    bssid: [3; 6],
407                    ..fake_fidl_bss_description!(Open, ssid: Ssid::try_from("qux").unwrap())
408                },
409            })
410            .expect("expect scan result received");
411        let (scan_end, mlme_req) = assert_variant!(
412            sched.on_mlme_scan_end(
413                fidl_mlme::ScanEnd { txn_id, code: fidl_mlme::ScanResultCode::Success },
414                &sme_inspect,
415            ),
416            Ok((scan_end, mlme_req)) => (scan_end, mlme_req)
417        );
418        assert!(mlme_req.is_none());
419        let (tokens, bss_description_list) = assert_variant!(
420            scan_end,
421            ScanEnd {
422                tokens,
423                result_code: fidl_mlme::ScanResultCode::Success,
424                bss_description_list
425            } => (tokens, bss_description_list),
426            "expected discovery scan to be completed successfully"
427        );
428        assert_eq!(vec![10], tokens);
429        let mut ssid_list =
430            bss_description_list.into_iter().map(|bss| bss.ssid).collect::<Vec<_>>();
431        ssid_list.sort();
432        assert_eq!(vec![Ssid::try_from("foo").unwrap(), Ssid::try_from("qux").unwrap()], ssid_list);
433    }
434
435    #[test_case(vec![
436        fake_fidl_bss_description!(Open, ssid: Ssid::try_from("bar").unwrap()),
437        fake_fidl_bss_description!(Open, ssid: Ssid::try_from("baz").unwrap()),
438    ], vec![fake_bss_description!(Open, ssid: Ssid::try_from("baz").unwrap())] ;
439                "when latest BSS Description is new")]
440    #[test_case(vec![
441        fake_fidl_bss_description!(Open, rssi_dbm: -36, channel: Channel::new(149, Cbw::Cbw20)),
442        fake_fidl_bss_description!(Open, rssi_dbm: -84, channel: Channel::new(165, Cbw::Cbw20)),
443    ], vec![fake_bss_description!(Open, rssi_dbm: -36, channel: Channel::new(149, Cbw::Cbw20))] ;
444                "when strong signal is first")]
445    #[test_case(vec![
446        fake_fidl_bss_description!(Open, rssi_dbm: -84, channel: Channel::new(64, Cbw::Cbw20)),
447        fake_fidl_bss_description!(Open, rssi_dbm: -36, channel: Channel::new(50, Cbw::Cbw20)),
448        fake_fidl_bss_description!(Open, rssi_dbm: -80, channel: Channel::new(36, Cbw::Cbw20)),
449    ], vec![fake_bss_description!(Open, rssi_dbm: -36, channel: Channel::new(50, Cbw::Cbw20))];
450                "when strong signal is middle")]
451    #[test_case(vec![
452        fake_fidl_bss_description!(Open, rssi_dbm: -84, channel: Channel::new(64, Cbw::Cbw20)),
453        fake_fidl_bss_description!(Open, rssi_dbm: -80, channel: Channel::new(36, Cbw::Cbw20)),
454        fake_fidl_bss_description!(Open, rssi_dbm: -36, channel: Channel::new(50, Cbw::Cbw20)),
455    ], vec![fake_bss_description!(Open, rssi_dbm: -36, channel: Channel::new(50, Cbw::Cbw20))];
456                "when strong signal is last")]
457    #[test_case(vec![
458        fake_fidl_bss_description!(Open, rssi_dbm: -84, ssid: Ssid::try_from("bar").unwrap(),
459                                   channel: Channel::new(149, Cbw::Cbw20)),
460        fake_fidl_bss_description!(Open, rssi_dbm: -36, ssid: Ssid::try_from("bar").unwrap(),
461                                   channel: Channel::new(165, Cbw::Cbw20)),
462        fake_fidl_bss_description!(Open, rssi_dbm: -40, ssid: Ssid::try_from("baz").unwrap(),
463                                   channel: Channel::new(165, Cbw::Cbw20)),
464    ], vec![fake_bss_description!(Open, rssi_dbm: -40, ssid: Ssid::try_from("baz").unwrap(),
465                                  channel: Channel::new(165, Cbw::Cbw20))];
466                "overwrite latest chosen channel")]
467    fn deduplicate_by_bssid(
468        bss_description_list_from_mlme: Vec<fidl_common::BssDescription>,
469        returned_bss_description_list: Vec<BssDescription>,
470    ) {
471        let mut sched = create_sched();
472        let (_inspector, sme_inspect) = sme_inspect();
473        let req = sched
474            .enqueue_scan_to_discover(passive_discovery_scan(10))
475            .expect("expected a ScanRequest");
476        let txn_id = req.txn_id;
477        for bss in bss_description_list_from_mlme {
478            sched
479                .on_mlme_scan_result(fidl_mlme::ScanResult {
480                    txn_id,
481                    timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
482                    bss,
483                })
484                .expect("expect scan result received");
485        }
486        let (scan_end, mlme_req) = assert_variant!(
487            sched.on_mlme_scan_end(
488                fidl_mlme::ScanEnd { txn_id, code: fidl_mlme::ScanResultCode::Success },
489                &sme_inspect,
490            ),
491            Ok((scan_end, mlme_req)) => (scan_end, mlme_req)
492        );
493        assert!(mlme_req.is_none());
494        let (tokens, bss_description_list) = assert_variant!(
495            scan_end,
496            ScanEnd {
497                tokens,
498                result_code: fidl_mlme::ScanResultCode::Success,
499                bss_description_list
500            } => (tokens, bss_description_list),
501            "expected discovery scan to be completed successfully"
502        );
503        assert_eq!(vec![10], tokens);
504        assert_eq!(bss_description_list, returned_bss_description_list);
505    }
506
507    #[test]
508    fn discovery_scan_merge_ies() {
509        let mut sched = create_sched();
510        let (_inspector, sme_inspect) = sme_inspect();
511        let req = sched
512            .enqueue_scan_to_discover(passive_discovery_scan(10))
513            .expect("expected a ScanRequest");
514        let txn_id = req.txn_id;
515
516        let mut bss = fake_fidl_bss_description!(Open, ssid: Ssid::try_from("ssid").unwrap());
517        // Add an extra IE so we can distinguish this result.
518        let ie_marker1 = &[0xdd, 0x07, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee];
519        bss.ies.extend_from_slice(ie_marker1);
520        sched
521            .on_mlme_scan_result(fidl_mlme::ScanResult {
522                txn_id,
523                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
524                bss,
525            })
526            .expect("expect scan result received");
527
528        let mut bss = fake_fidl_bss_description!(Open, ssid: Ssid::try_from("ssid").unwrap());
529        // Add an extra IE so we can distinguish this result.
530        let ie_marker2 = &[0xdd, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff];
531        bss.ies.extend_from_slice(ie_marker2);
532        sched
533            .on_mlme_scan_result(fidl_mlme::ScanResult {
534                txn_id,
535                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
536                bss,
537            })
538            .expect("expect scan result received");
539        let (scan_end, mlme_req) = assert_variant!(
540            sched.on_mlme_scan_end(
541                fidl_mlme::ScanEnd { txn_id, code: fidl_mlme::ScanResultCode::Success },
542                &sme_inspect,
543            ),
544            Ok((scan_end, mlme_req)) => (scan_end, mlme_req)
545        );
546        assert!(mlme_req.is_none());
547        let (tokens, bss_description_list) = assert_variant!(
548            scan_end,
549            ScanEnd {
550                tokens,
551                result_code: fidl_mlme::ScanResultCode::Success,
552                bss_description_list
553            } => (tokens, bss_description_list),
554            "expected discovery scan to be completed successfully"
555        );
556        assert_eq!(vec![10], tokens);
557
558        assert_eq!(bss_description_list.len(), 1);
559        // Verify that both IEs are processed.
560        assert!(slice_contains(bss_description_list[0].ies(), ie_marker1));
561        assert!(slice_contains(bss_description_list[0].ies(), ie_marker2));
562    }
563
564    fn slice_contains(slice: &[u8], subslice: &[u8]) -> bool {
565        // https://github.com/rust-lang/regex/issues/451#issuecomment-367987989
566        let re = {
567            let mut re_string = String::with_capacity(6 + subslice.len() * 4);
568            re_string += "(?-u:";
569            for b in subslice {
570                write!(re_string, "\\x{:02X}", b).unwrap();
571            }
572            re_string += ")";
573            Regex::new(&re_string).unwrap()
574        };
575        re.is_match(slice)
576    }
577
578    #[test_case(&[1, 2, 3], &[] => true; "vacuous")]
579    #[test_case(&[1, 2, 3], &[1u8] => true; "one byte")]
580    #[test_case(&[1, 2, 3], &[2u8, 3] => true; "multiple bytes")]
581    #[test_case(&[1, 1, 1], &[1u8, 1] => true; "multiple matches")]
582    #[test_case(&[1, 2, 3], &[0u8] => false; "no match")]
583    #[test_case(&[1, 2, 3], &[1u8, 2, 3, 4] => false; "too large")]
584    #[test_case(&[0x87, 0x77, 0x78], &[0x77, 0x77] => false; "misaligned match")]
585    fn slice_contains_test(slice: &[u8], subslice: &[u8]) -> bool {
586        slice_contains(slice, subslice)
587    }
588
589    #[test]
590    fn test_passive_discovery_scan_args() {
591        let mut sched = create_sched();
592        let req = sched
593            .enqueue_scan_to_discover(passive_discovery_scan(10))
594            .expect("expected a ScanRequest");
595        assert_eq!(req.txn_id, 1);
596        assert_eq!(req.scan_type, fidl_mlme::ScanTypes::Passive);
597        assert_eq!(req.channel_list, Vec::<u8>::new());
598        assert_eq!(req.ssid_list, Vec::<Vec<u8>>::new());
599        assert_eq!(req.probe_delay, 0);
600        assert_eq!(req.min_channel_time, 200);
601        assert_eq!(req.max_channel_time, 200);
602    }
603
604    #[test]
605    fn test_active_discovery_scan_args_empty() {
606        let device_info = device_info_with_channel(vec![1, 36, 165]);
607        let mut sched: ScanScheduler<i32> =
608            ScanScheduler::new(Arc::new(device_info), fake_spectrum_management_support_empty());
609        let scan_cmd = DiscoveryScan::new(
610            10,
611            fidl_sme::ScanRequest::Active(fidl_sme::ActiveScanRequest {
612                ssids: vec![],
613                channels: vec![],
614            }),
615        );
616        let req = sched.enqueue_scan_to_discover(scan_cmd).expect("expected a ScanRequest");
617
618        assert_eq!(req.txn_id, 1);
619        assert_eq!(req.scan_type, fidl_mlme::ScanTypes::Active);
620        assert_eq!(req.channel_list, vec![36, 165, 1]);
621        assert_eq!(req.ssid_list, Vec::<Vec<u8>>::new());
622        assert_eq!(req.probe_delay, 5);
623        assert_eq!(req.min_channel_time, 75);
624        assert_eq!(req.max_channel_time, 75);
625    }
626
627    #[test]
628    fn test_active_discovery_scan_args_filled() {
629        let device_info = device_info_with_channel(vec![1, 36, 165]);
630        let mut sched: ScanScheduler<i32> =
631            ScanScheduler::new(Arc::new(device_info), fake_spectrum_management_support_empty());
632        let ssid1: Vec<u8> = Ssid::try_from("ssid1").unwrap().into();
633        let ssid2: Vec<u8> = Ssid::try_from("ssid2").unwrap().into();
634        let scan_cmd = DiscoveryScan::new(
635            10,
636            fidl_sme::ScanRequest::Active(fidl_sme::ActiveScanRequest {
637                ssids: vec![ssid1.clone(), ssid2.clone()],
638                // TODO(https://fxbug.dev/42169913): SME silently ignores unsupported channels
639                channels: vec![1, 20, 100],
640            }),
641        );
642        let req = sched.enqueue_scan_to_discover(scan_cmd).expect("expected a ScanRequest");
643
644        assert_eq!(req.txn_id, 1);
645        assert_eq!(req.scan_type, fidl_mlme::ScanTypes::Active);
646        assert_eq!(req.channel_list, vec![1]);
647        assert_eq!(req.ssid_list, vec![ssid1, ssid2]);
648        assert_eq!(req.probe_delay, 5);
649        assert_eq!(req.min_channel_time, 75);
650        assert_eq!(req.max_channel_time, 75);
651    }
652
653    #[test]
654    fn test_discovery_scans_dedupe_single_group() {
655        let mut sched = create_sched();
656        let (_inspector, sme_inspect) = sme_inspect();
657
658        // Post one scan command, expect a message to MLME
659        let mlme_req = sched
660            .enqueue_scan_to_discover(passive_discovery_scan(10))
661            .expect("expected a ScanRequest");
662        let txn_id = mlme_req.txn_id;
663
664        // Report a scan result
665        sched
666            .on_mlme_scan_result(fidl_mlme::ScanResult {
667                txn_id,
668                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
669                bss: fidl_common::BssDescription {
670                    bssid: [1; 6],
671                    ..fake_fidl_bss_description!(Open, ssid: Ssid::try_from("foo").unwrap())
672                },
673            })
674            .expect("expect scan result received");
675
676        // Post another command. It should not issue another request to the MLME since
677        // there is already an on-going one
678        assert!(sched.enqueue_scan_to_discover(passive_discovery_scan(20)).is_none());
679
680        // Report another scan result and the end of the scan transaction
681        sched
682            .on_mlme_scan_result(fidl_mlme::ScanResult {
683                txn_id,
684                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
685                bss: fidl_common::BssDescription {
686                    bssid: [2; 6],
687                    ..fake_fidl_bss_description!(Open, ssid: Ssid::try_from("bar").unwrap())
688                },
689            })
690            .expect("expect scan result received");
691        let (scan_end, mlme_req) = assert_variant!(
692            sched.on_mlme_scan_end(
693                fidl_mlme::ScanEnd { txn_id, code: fidl_mlme::ScanResultCode::Success },
694                &sme_inspect,
695            ),
696            Ok((scan_end, mlme_req)) => (scan_end, mlme_req)
697        );
698
699        // We don't expect another request to the MLME
700        assert!(mlme_req.is_none());
701
702        // Expect a discovery result with both tokens and both SSIDs
703        assert_discovery_scan_result(
704            scan_end,
705            vec![10, 20],
706            vec![Ssid::try_from("bar").unwrap(), Ssid::try_from("foo").unwrap()],
707        );
708    }
709
710    #[test]
711    fn test_discovery_scans_dedupe_multiple_groups() {
712        let mut sched = create_sched();
713        let (_inspector, sme_inspect) = sme_inspect();
714
715        // Post a passive scan command, expect a message to MLME
716        let mlme_req = sched
717            .enqueue_scan_to_discover(passive_discovery_scan(10))
718            .expect("expected a ScanRequest");
719        let txn_id = mlme_req.txn_id;
720
721        // Post an active scan command, which should be enqueued until the previous one finishes
722        let scan_cmd = DiscoveryScan::new(
723            20,
724            fidl_sme::ScanRequest::Active(fidl_sme::ActiveScanRequest {
725                ssids: vec![],
726                channels: vec![],
727            }),
728        );
729        assert!(sched.enqueue_scan_to_discover(scan_cmd).is_none());
730
731        // Post a passive scan command. It should be merged with the ongoing one and so should not
732        // issue another request to MLME
733        assert!(sched.enqueue_scan_to_discover(passive_discovery_scan(30)).is_none());
734
735        // Post an active scan command. It should be merged with the active scan command that's
736        // still enqueued, and so should not issue another request to MLME
737        let scan_cmd = DiscoveryScan::new(
738            40,
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        // Report scan result and scan end
747        sched
748            .on_mlme_scan_result(fidl_mlme::ScanResult {
749                txn_id,
750                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
751                bss: fidl_common::BssDescription {
752                    bssid: [1; 6],
753                    ..fake_fidl_bss_description!(Open, ssid: Ssid::try_from("foo").unwrap())
754                },
755            })
756            .expect("expect scan result received");
757        let (scan_end, mlme_req) = assert_variant!(
758            sched.on_mlme_scan_end(
759                fidl_mlme::ScanEnd { txn_id, code: fidl_mlme::ScanResultCode::Success },
760                &sme_inspect,
761            ),
762            Ok((scan_end, mlme_req)) => (scan_end, mlme_req)
763        );
764
765        // Expect discovery result with 1st and 3rd tokens
766        assert_discovery_scan_result(scan_end, vec![10, 30], vec![Ssid::try_from("foo").unwrap()]);
767
768        // Next mlme_req should be an active scan request
769        assert!(mlme_req.is_some());
770        let mlme_req = mlme_req.unwrap();
771        assert_eq!(mlme_req.scan_type, fidl_mlme::ScanTypes::Active);
772        let txn_id = mlme_req.txn_id;
773
774        // Report scan result and scan end
775        sched
776            .on_mlme_scan_result(fidl_mlme::ScanResult {
777                txn_id,
778                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
779                bss: fidl_common::BssDescription {
780                    bssid: [2; 6],
781                    ..fake_fidl_bss_description!(Open, ssid: Ssid::try_from("bar").unwrap())
782                },
783            })
784            .expect("expect scan result received");
785        let (scan_end, mlme_req) = assert_variant!(
786            sched.on_mlme_scan_end(
787                fidl_mlme::ScanEnd { txn_id, code: fidl_mlme::ScanResultCode::Success },
788                &sme_inspect,
789            ),
790            Ok((scan_end, mlme_req)) => (scan_end, mlme_req)
791        );
792
793        // Expect discovery result with 2nd and 4th tokens
794        assert_discovery_scan_result(scan_end, vec![20, 40], vec![Ssid::try_from("bar").unwrap()]);
795
796        // We don't expect another request to the MLME
797        assert!(mlme_req.is_none());
798    }
799
800    #[test]
801    fn test_discovery_scan_result_wrong_txn_id() {
802        let mut sched = create_sched();
803
804        // Post a passive scan command, expect a message to MLME
805        let mlme_req = sched
806            .enqueue_scan_to_discover(passive_discovery_scan(10))
807            .expect("expected a ScanRequest");
808        let txn_id = mlme_req.txn_id;
809
810        // Report scan result with wrong txn id
811        assert_variant!(
812            sched.on_mlme_scan_result(fidl_mlme::ScanResult {
813                txn_id: txn_id + 1,
814                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
815                bss: fidl_common::BssDescription {
816                    bssid: [1; 6],
817                    ..fake_fidl_bss_description!(Open, ssid: Ssid::try_from("foo").unwrap())
818                },
819            },),
820            Err(Error::ScanResultWrongTxnId)
821        );
822    }
823
824    #[test]
825    fn test_discovery_scan_result_not_scanning() {
826        let mut sched = create_sched();
827        assert_variant!(
828            sched.on_mlme_scan_result(fidl_mlme::ScanResult {
829                txn_id: 0,
830                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
831                bss: fidl_common::BssDescription {
832                    bssid: [1; 6],
833                    ..fake_fidl_bss_description!(Open, ssid: Ssid::try_from("foo").unwrap())
834                },
835            },),
836            Err(Error::ScanResultNotScanning)
837        );
838    }
839
840    #[test]
841    fn test_discovery_scan_end_wrong_txn_id() {
842        let mut sched = create_sched();
843        let (_inspector, sme_inspect) = sme_inspect();
844
845        // Post a passive scan command, expect a message to MLME
846        let mlme_req = sched
847            .enqueue_scan_to_discover(passive_discovery_scan(10))
848            .expect("expected a ScanRequest");
849        let txn_id = mlme_req.txn_id;
850
851        assert_variant!(
852            sched.on_mlme_scan_end(
853                fidl_mlme::ScanEnd { txn_id: txn_id + 1, code: fidl_mlme::ScanResultCode::Success },
854                &sme_inspect,
855            ),
856            Err(Error::ScanEndWrongTxnId)
857        );
858    }
859
860    #[test]
861    fn test_discovery_scan_end_not_scanning() {
862        let mut sched = create_sched();
863        let (_inspector, sme_inspect) = sme_inspect();
864        assert_variant!(
865            sched.on_mlme_scan_end(
866                fidl_mlme::ScanEnd { txn_id: 0, code: fidl_mlme::ScanResultCode::Success },
867                &sme_inspect,
868            ),
869            Err(Error::ScanEndNotScanning)
870        );
871    }
872
873    fn assert_discovery_scan_result(
874        scan_end: ScanEnd<i32>,
875        expected_tokens: Vec<i32>,
876        expected_ssids: Vec<Ssid>,
877    ) {
878        let (tokens, bss_description_list) = assert_variant!(
879            scan_end,
880            ScanEnd {
881                tokens,
882                result_code: fidl_mlme::ScanResultCode::Success,
883                bss_description_list
884            } => (tokens, bss_description_list),
885            "expected discovery scan to be completed successfully"
886        );
887        assert_eq!(tokens, expected_tokens);
888        let mut ssid_list =
889            bss_description_list.into_iter().map(|bss| bss.ssid.clone()).collect::<Vec<_>>();
890        ssid_list.sort();
891        assert_eq!(ssid_list, expected_ssids);
892    }
893
894    fn create_sched() -> ScanScheduler<i32> {
895        ScanScheduler::new(
896            Arc::new(test_utils::fake_device_info(*CLIENT_ADDR)),
897            fake_spectrum_management_support_empty(),
898        )
899    }
900
901    fn device_info_with_channel(operating_channels: Vec<u8>) -> fidl_mlme::DeviceInfo {
902        fidl_mlme::DeviceInfo {
903            bands: vec![fidl_mlme::BandCapability {
904                operating_channels,
905                ..fake_5ghz_band_capability()
906            }],
907            ..test_utils::fake_device_info(*CLIENT_ADDR)
908        }
909    }
910
911    fn sme_inspect() -> (Inspector, Arc<inspect::SmeTree>) {
912        let inspector = Inspector::default();
913        let sme_inspect = Arc::new(inspect::SmeTree::new(
914            inspector.clone(),
915            inspector.root().create_child("usme"),
916            &test_utils::fake_device_info([1u8; 6].into()),
917            &fake_spectrum_management_support_empty(),
918        ));
919        (inspector, sme_inspect)
920    }
921}