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