script/dom/bluetooth/
bluetooth.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use bluetooth_traits::{BluetoothError, BluetoothRequest, GATTType};
6use bluetooth_traits::{BluetoothResponse, BluetoothResponseResult};
7use bluetooth_traits::blocklist::{Blocklist, uuid_is_blocklisted};
8use bluetooth_traits::scanfilter::{BluetoothScanfilter, BluetoothScanfilterSequence};
9use bluetooth_traits::scanfilter::{RequestDeviceoptions, ServiceUUIDSequence};
10use crate::realms::{AlreadyInRealm, InRealm};
11use crate::conversions::Convert;
12use crate::dom::bindings::cell::{DomRefCell, Ref};
13use crate::dom::bindings::codegen::Bindings::BluetoothBinding::BluetoothDataFilterInit;
14use crate::dom::bindings::codegen::Bindings::BluetoothBinding::{BluetoothMethods, RequestDeviceOptions};
15use crate::dom::bindings::codegen::Bindings::BluetoothBinding::BluetoothLEScanFilterInit;
16use crate::dom::bindings::codegen::Bindings::BluetoothPermissionResultBinding::BluetoothPermissionDescriptor;
17use crate::dom::bindings::codegen::Bindings::BluetoothRemoteGATTServerBinding::BluetoothRemoteGATTServer_Binding::
18BluetoothRemoteGATTServerMethods;
19use crate::dom::bindings::codegen::Bindings::PermissionStatusBinding::{PermissionName, PermissionState};
20use crate::dom::bindings::codegen::UnionTypes::{ArrayBufferViewOrArrayBuffer, StringOrUnsignedLong};
21use crate::dom::bindings::error::Error::{self, Network, Security, Type};
22use crate::dom::bindings::error::Fallible;
23use crate::dom::bindings::refcounted::{Trusted, TrustedPromise};
24use crate::dom::bindings::reflector::{DomGlobal, DomObject, reflect_dom_object};
25use crate::dom::bindings::root::{Dom, DomRoot};
26use crate::dom::bindings::str::DOMString;
27use crate::dom::bluetoothdevice::BluetoothDevice;
28use crate::dom::bluetoothpermissionresult::BluetoothPermissionResult;
29use crate::dom::bluetoothuuid::{BluetoothServiceUUID, BluetoothUUID, UUID};
30use crate::dom::eventtarget::EventTarget;
31use crate::dom::globalscope::GlobalScope;
32use crate::dom::permissions::{descriptor_permission_state, PermissionAlgorithm};
33use crate::dom::promise::Promise;
34use crate::script_runtime::{CanGc, JSContext};
35use crate::task::TaskOnce;
36use dom_struct::dom_struct;
37use ipc_channel::ipc::{self, IpcSender};
38use ipc_channel::router::ROUTER;
39use js::conversions::ConversionResult;
40use js::jsapi::JSObject;
41use js::jsval::{ObjectValue, UndefinedValue};
42use profile_traits::ipc as ProfiledIpc;
43use std::collections::HashMap;
44use std::rc::Rc;
45use std::str::FromStr;
46use std::sync::{Arc, Mutex};
47
48const KEY_CONVERSION_ERROR: &str =
49    "This `manufacturerData` key can not be parsed as unsigned short:";
50const FILTER_EMPTY_ERROR: &str =
51    "'filters' member, if present, must be nonempty to find any devices.";
52const FILTER_ERROR: &str = "A filter must restrict the devices in some way.";
53const MANUFACTURER_DATA_ERROR: &str =
54    "'manufacturerData', if present, must be non-empty to filter devices.";
55const MASK_LENGTH_ERROR: &str = "`mask`, if present, must have the same length as `dataPrefix`.";
56// 248 is the maximum number of UTF-8 code units in a Bluetooth Device Name.
57const MAX_DEVICE_NAME_LENGTH: usize = 248;
58const NAME_PREFIX_ERROR: &str = "'namePrefix', if present, must be nonempty.";
59const NAME_TOO_LONG_ERROR: &str = "A device name can't be longer than 248 bytes.";
60const SERVICE_DATA_ERROR: &str = "'serviceData', if present, must be non-empty to filter devices.";
61const SERVICE_ERROR: &str = "'services', if present, must contain at least one service.";
62const OPTIONS_ERROR: &str = "Fields of 'options' conflict with each other.
63 Either 'acceptAllDevices' member must be true, or 'filters' member must be set to a value.";
64const BT_DESC_CONVERSION_ERROR: &str =
65    "Can't convert to an IDL value of type BluetoothPermissionDescriptor";
66
67#[derive(JSTraceable, MallocSizeOf)]
68#[allow(non_snake_case)]
69pub(crate) struct AllowedBluetoothDevice {
70    pub(crate) deviceId: DOMString,
71    pub(crate) mayUseGATT: bool,
72}
73
74#[derive(JSTraceable, MallocSizeOf)]
75pub(crate) struct BluetoothExtraPermissionData {
76    allowed_devices: DomRefCell<Vec<AllowedBluetoothDevice>>,
77}
78
79impl BluetoothExtraPermissionData {
80    pub(crate) fn new() -> BluetoothExtraPermissionData {
81        BluetoothExtraPermissionData {
82            allowed_devices: DomRefCell::new(Vec::new()),
83        }
84    }
85
86    pub(crate) fn add_new_allowed_device(&self, allowed_device: AllowedBluetoothDevice) {
87        self.allowed_devices.borrow_mut().push(allowed_device);
88    }
89
90    fn get_allowed_devices(&self) -> Ref<'_, Vec<AllowedBluetoothDevice>> {
91        self.allowed_devices.borrow()
92    }
93
94    pub(crate) fn allowed_devices_contains_id(&self, id: DOMString) -> bool {
95        self.allowed_devices
96            .borrow()
97            .iter()
98            .any(|d| d.deviceId == id)
99    }
100}
101
102impl Default for BluetoothExtraPermissionData {
103    fn default() -> Self {
104        Self::new()
105    }
106}
107
108struct BluetoothContext<T: AsyncBluetoothListener + DomObject> {
109    promise: Option<TrustedPromise>,
110    receiver: Trusted<T>,
111}
112
113pub(crate) trait AsyncBluetoothListener {
114    fn handle_response(&self, result: BluetoothResponse, promise: &Rc<Promise>, can_gc: CanGc);
115}
116
117impl<T> BluetoothContext<T>
118where
119    T: AsyncBluetoothListener + DomObject,
120{
121    #[cfg_attr(crown, allow(crown::unrooted_must_root))]
122    fn response(&mut self, response: BluetoothResponseResult, can_gc: CanGc) {
123        let promise = self.promise.take().expect("bt promise is missing").root();
124
125        // JSAutoRealm needs to be manually made.
126        // Otherwise, Servo will crash.
127        match response {
128            Ok(response) => self
129                .receiver
130                .root()
131                .handle_response(response, &promise, can_gc),
132            // https://webbluetoothcg.github.io/web-bluetooth/#dom-bluetooth-requestdevice
133            // Step 3 - 4.
134            Err(error) => promise.reject_error(error.convert(), can_gc),
135        }
136    }
137}
138
139// https://webbluetoothcg.github.io/web-bluetooth/#bluetooth
140#[dom_struct]
141pub(crate) struct Bluetooth {
142    eventtarget: EventTarget,
143    device_instance_map: DomRefCell<HashMap<String, Dom<BluetoothDevice>>>,
144}
145
146impl Bluetooth {
147    pub(crate) fn new_inherited() -> Bluetooth {
148        Bluetooth {
149            eventtarget: EventTarget::new_inherited(),
150            device_instance_map: DomRefCell::new(HashMap::new()),
151        }
152    }
153
154    pub(crate) fn new(global: &GlobalScope, can_gc: CanGc) -> DomRoot<Bluetooth> {
155        reflect_dom_object(Box::new(Bluetooth::new_inherited()), global, can_gc)
156    }
157
158    fn get_bluetooth_thread(&self) -> IpcSender<BluetoothRequest> {
159        self.global().as_window().bluetooth_thread()
160    }
161
162    pub(crate) fn get_device_map(&self) -> &DomRefCell<HashMap<String, Dom<BluetoothDevice>>> {
163        &self.device_instance_map
164    }
165
166    // https://webbluetoothcg.github.io/web-bluetooth/#request-bluetooth-devices
167    fn request_bluetooth_devices(
168        &self,
169        p: &Rc<Promise>,
170        filters: &Option<Vec<BluetoothLEScanFilterInit>>,
171        optional_services: &[BluetoothServiceUUID],
172        sender: IpcSender<BluetoothResponseResult>,
173        can_gc: CanGc,
174    ) {
175        // TODO: Step 1: Triggered by user activation.
176
177        // Step 2.2: There are no requiredServiceUUIDS, we scan for all devices.
178        let mut uuid_filters = vec![];
179
180        if let Some(filters) = filters {
181            // Step 2.1.
182            if filters.is_empty() {
183                p.reject_error(Type(FILTER_EMPTY_ERROR.to_owned()), can_gc);
184                return;
185            }
186
187            // Step 2.3: There are no requiredServiceUUIDS, we scan for all devices.
188
189            // Step 2.4.
190            for filter in filters {
191                // Step 2.4.1.
192                match canonicalize_filter(filter) {
193                    // Step 2.4.2.
194                    Ok(f) => uuid_filters.push(f),
195                    Err(e) => {
196                        p.reject_error(e, can_gc);
197                        return;
198                    },
199                }
200                // Step 2.4.3: There are no requiredServiceUUIDS, we scan for all devices.
201            }
202        }
203
204        let mut optional_services_uuids = vec![];
205        for opt_service in optional_services {
206            // Step 2.5 - 2.6.
207            let uuid = match BluetoothUUID::service(opt_service.clone()) {
208                Ok(u) => u.to_string(),
209                Err(e) => {
210                    p.reject_error(e, can_gc);
211                    return;
212                },
213            };
214
215            // Step 2.7.
216            // Note: What we are doing here, is adding the not blocklisted UUIDs to the result vector,
217            // instead of removing them from an already filled vector.
218            if !uuid_is_blocklisted(uuid.as_ref(), Blocklist::All) {
219                optional_services_uuids.push(uuid);
220            }
221        }
222
223        let option = RequestDeviceoptions::new(
224            self.global().as_window().webview_id(),
225            BluetoothScanfilterSequence::new(uuid_filters),
226            ServiceUUIDSequence::new(optional_services_uuids),
227        );
228
229        // Step 4 - 5.
230        if let PermissionState::Denied =
231            descriptor_permission_state(PermissionName::Bluetooth, None)
232        {
233            return p.reject_error(Error::NotFound, can_gc);
234        }
235
236        // Note: Step 3, 6 - 8 are implemented in
237        // components/net/bluetooth_thread.rs in request_device function.
238        self.get_bluetooth_thread()
239            .send(BluetoothRequest::RequestDevice(option, sender))
240            .unwrap();
241    }
242}
243
244pub(crate) fn response_async<T: AsyncBluetoothListener + DomObject + 'static>(
245    promise: &Rc<Promise>,
246    receiver: &T,
247) -> IpcSender<BluetoothResponseResult> {
248    let (action_sender, action_receiver) = ipc::channel().unwrap();
249    let task_source = receiver
250        .global()
251        .task_manager()
252        .networking_task_source()
253        .to_sendable();
254    let context = Arc::new(Mutex::new(BluetoothContext {
255        promise: Some(TrustedPromise::new(promise.clone())),
256        receiver: Trusted::new(receiver),
257    }));
258    ROUTER.add_typed_route(
259        action_receiver,
260        Box::new(move |message| {
261            struct ListenerTask<T: AsyncBluetoothListener + DomObject> {
262                context: Arc<Mutex<BluetoothContext<T>>>,
263                action: BluetoothResponseResult,
264            }
265
266            impl<T> TaskOnce for ListenerTask<T>
267            where
268                T: AsyncBluetoothListener + DomObject,
269            {
270                fn run_once(self) {
271                    let mut context = self.context.lock().unwrap();
272                    context.response(self.action, CanGc::note());
273                }
274            }
275
276            let task = ListenerTask {
277                context: context.clone(),
278                action: message.unwrap(),
279            };
280
281            task_source.queue_unconditionally(task);
282        }),
283    );
284    action_sender
285}
286
287// https://webbluetoothcg.github.io/web-bluetooth/#getgattchildren
288#[allow(clippy::too_many_arguments)]
289pub(crate) fn get_gatt_children<T, F>(
290    attribute: &T,
291    single: bool,
292    uuid_canonicalizer: F,
293    uuid: Option<StringOrUnsignedLong>,
294    instance_id: String,
295    connected: bool,
296    child_type: GATTType,
297    can_gc: CanGc,
298) -> Rc<Promise>
299where
300    T: AsyncBluetoothListener + DomObject + 'static,
301    F: FnOnce(StringOrUnsignedLong) -> Fallible<UUID>,
302{
303    let in_realm_proof = AlreadyInRealm::assert::<crate::DomTypeHolder>();
304    let p = Promise::new_in_current_realm(InRealm::Already(&in_realm_proof), can_gc);
305
306    let result_uuid = if let Some(u) = uuid {
307        // Step 1.
308        let canonicalized = match uuid_canonicalizer(u) {
309            Ok(canonicalized_uuid) => canonicalized_uuid.to_string(),
310            Err(e) => {
311                p.reject_error(e, can_gc);
312                return p;
313            },
314        };
315        // Step 2.
316        if uuid_is_blocklisted(canonicalized.as_ref(), Blocklist::All) {
317            p.reject_error(Security, can_gc);
318            return p;
319        }
320        Some(canonicalized)
321    } else {
322        None
323    };
324
325    // Step 3 - 4.
326    if !connected {
327        p.reject_error(Network, can_gc);
328        return p;
329    }
330
331    // TODO: Step 5: Implement representedDevice internal slot for BluetoothDevice.
332
333    // Note: Steps 6 - 7 are implemented in components/bluetooth/lib.rs in get_descriptor function
334    // and in handle_response function.
335    let sender = response_async(&p, attribute);
336    attribute
337        .global()
338        .as_window()
339        .bluetooth_thread()
340        .send(BluetoothRequest::GetGATTChildren(
341            instance_id,
342            result_uuid,
343            single,
344            child_type,
345            sender,
346        ))
347        .unwrap();
348    p
349}
350
351// https://webbluetoothcg.github.io/web-bluetooth/#bluetoothlescanfilterinit-canonicalizing
352fn canonicalize_filter(filter: &BluetoothLEScanFilterInit) -> Fallible<BluetoothScanfilter> {
353    // Step 1.
354    if filter.services.is_none() &&
355        filter.name.is_none() &&
356        filter.namePrefix.is_none() &&
357        filter.manufacturerData.is_none() &&
358        filter.serviceData.is_none()
359    {
360        return Err(Type(FILTER_ERROR.to_owned()));
361    }
362
363    // Step 2: There is no empty canonicalizedFilter member,
364    // we create a BluetoothScanfilter instance at the end of the function.
365
366    // Step 3.
367    let services_vec = match filter.services {
368        Some(ref services) => {
369            // Step 3.1.
370            if services.is_empty() {
371                return Err(Type(SERVICE_ERROR.to_owned()));
372            }
373
374            let mut services_vec = vec![];
375
376            for service in services {
377                // Step 3.2 - 3.3.
378                let uuid = BluetoothUUID::service(service.clone())?.to_string();
379
380                // Step 3.4.
381                if uuid_is_blocklisted(uuid.as_ref(), Blocklist::All) {
382                    return Err(Security);
383                }
384
385                services_vec.push(uuid);
386            }
387            // Step 3.5.
388            services_vec
389        },
390        None => vec![],
391    };
392
393    // Step 4.
394    let name = match filter.name {
395        Some(ref name) => {
396            // Step 4.1.
397            // Note: DOMString::len() gives back the size in bytes.
398            if name.len() > MAX_DEVICE_NAME_LENGTH {
399                return Err(Type(NAME_TOO_LONG_ERROR.to_owned()));
400            }
401
402            // Step 4.2.
403            Some(name.to_string())
404        },
405        None => None,
406    };
407
408    // Step 5.
409    let name_prefix = match filter.namePrefix {
410        Some(ref name_prefix) => {
411            // Step 5.1.
412            if name_prefix.is_empty() {
413                return Err(Type(NAME_PREFIX_ERROR.to_owned()));
414            }
415            if name_prefix.len() > MAX_DEVICE_NAME_LENGTH {
416                return Err(Type(NAME_TOO_LONG_ERROR.to_owned()));
417            }
418
419            // Step 5.2.
420            name_prefix.to_string()
421        },
422        None => String::new(),
423    };
424
425    // Step 6 - 7.
426    let manufacturer_data = match filter.manufacturerData {
427        Some(ref manufacturer_data_map) => {
428            // Note: If manufacturer_data_map is empty, that means there are no key values in it.
429            if manufacturer_data_map.is_empty() {
430                return Err(Type(MANUFACTURER_DATA_ERROR.to_owned()));
431            }
432            let mut map = HashMap::new();
433            for (key, bdfi) in manufacturer_data_map.iter() {
434                // Step 7.1 - 7.2.
435                let manufacturer_id = match u16::from_str(key.as_ref()) {
436                    Ok(id) => id,
437                    Err(err) => {
438                        return Err(Type(format!("{} {} {}", KEY_CONVERSION_ERROR, key, err)));
439                    },
440                };
441
442                // Step 7.3: No need to convert to IDL values since this is only used by native code.
443
444                // Step 7.4 - 7.5.
445                map.insert(
446                    manufacturer_id,
447                    canonicalize_bluetooth_data_filter_init(bdfi)?,
448                );
449            }
450            Some(map)
451        },
452        None => None,
453    };
454
455    // Step 8 - 9.
456    let service_data = match filter.serviceData {
457        Some(ref service_data_map) => {
458            // Note: If service_data_map is empty, that means there are no key values in it.
459            if service_data_map.is_empty() {
460                return Err(Type(SERVICE_DATA_ERROR.to_owned()));
461            }
462            let mut map = HashMap::new();
463            for (key, bdfi) in service_data_map.iter() {
464                let service_name = match u32::from_str(key.as_ref()) {
465                    // Step 9.1.
466                    Ok(number) => StringOrUnsignedLong::UnsignedLong(number),
467                    // Step 9.2.
468                    _ => StringOrUnsignedLong::String(key.clone()),
469                };
470
471                // Step 9.3 - 9.4.
472                let service = BluetoothUUID::service(service_name)?.to_string();
473
474                // Step 9.5.
475                if uuid_is_blocklisted(service.as_ref(), Blocklist::All) {
476                    return Err(Security);
477                }
478
479                // Step 9.6: No need to convert to IDL values since this is only used by native code.
480
481                // Step 9.7 - 9.8.
482                map.insert(service, canonicalize_bluetooth_data_filter_init(bdfi)?);
483            }
484            Some(map)
485        },
486        None => None,
487    };
488
489    // Step 10.
490    Ok(BluetoothScanfilter::new(
491        name,
492        name_prefix,
493        services_vec,
494        manufacturer_data,
495        service_data,
496    ))
497}
498
499// https://webbluetoothcg.github.io/web-bluetooth/#bluetoothdatafilterinit-canonicalizing
500fn canonicalize_bluetooth_data_filter_init(
501    bdfi: &BluetoothDataFilterInit,
502) -> Fallible<(Vec<u8>, Vec<u8>)> {
503    // Step 1.
504    let data_prefix = match bdfi.dataPrefix {
505        Some(ArrayBufferViewOrArrayBuffer::ArrayBufferView(ref avb)) => avb.to_vec(),
506        Some(ArrayBufferViewOrArrayBuffer::ArrayBuffer(ref ab)) => ab.to_vec(),
507        None => vec![],
508    };
509
510    // Step 2.
511    // If no mask present, mask will be a sequence of 0xFF bytes the same length as dataPrefix.
512    // Masking dataPrefix with this, leaves dataPrefix untouched.
513    let mask = match bdfi.mask {
514        Some(ArrayBufferViewOrArrayBuffer::ArrayBufferView(ref avb)) => avb.to_vec(),
515        Some(ArrayBufferViewOrArrayBuffer::ArrayBuffer(ref ab)) => ab.to_vec(),
516        None => vec![0xFF; data_prefix.len()],
517    };
518
519    // Step 3.
520    if mask.len() != data_prefix.len() {
521        return Err(Type(MASK_LENGTH_ERROR.to_owned()));
522    }
523
524    // Step 4.
525    Ok((data_prefix, mask))
526}
527
528impl Convert<Error> for BluetoothError {
529    fn convert(self) -> Error {
530        match self {
531            BluetoothError::Type(message) => Error::Type(message),
532            BluetoothError::Network => Error::Network,
533            BluetoothError::NotFound => Error::NotFound,
534            BluetoothError::NotSupported => Error::NotSupported,
535            BluetoothError::Security => Error::Security,
536            BluetoothError::InvalidState => Error::InvalidState,
537        }
538    }
539}
540
541impl BluetoothMethods<crate::DomTypeHolder> for Bluetooth {
542    // https://webbluetoothcg.github.io/web-bluetooth/#dom-bluetooth-requestdevice
543    fn RequestDevice(
544        &self,
545        option: &RequestDeviceOptions,
546        comp: InRealm,
547        can_gc: CanGc,
548    ) -> Rc<Promise> {
549        let p = Promise::new_in_current_realm(comp, can_gc);
550        // Step 1.
551        if (option.filters.is_some() && option.acceptAllDevices) ||
552            (option.filters.is_none() && !option.acceptAllDevices)
553        {
554            p.reject_error(Error::Type(OPTIONS_ERROR.to_owned()), can_gc);
555            return p;
556        }
557
558        // Step 2.
559        let sender = response_async(&p, self);
560        self.request_bluetooth_devices(
561            &p,
562            &option.filters,
563            &option.optionalServices,
564            sender,
565            can_gc,
566        );
567        // Note: Step 3 - 4. in response function, Step 5. in handle_response function.
568        p
569    }
570
571    // https://webbluetoothcg.github.io/web-bluetooth/#dom-bluetooth-getavailability
572    fn GetAvailability(&self, comp: InRealm, can_gc: CanGc) -> Rc<Promise> {
573        let p = Promise::new_in_current_realm(comp, can_gc);
574        // Step 1. We did not override the method
575        // Step 2 - 3. in handle_response
576        let sender = response_async(&p, self);
577        self.get_bluetooth_thread()
578            .send(BluetoothRequest::GetAvailability(sender))
579            .unwrap();
580        p
581    }
582
583    // https://webbluetoothcg.github.io/web-bluetooth/#dom-bluetooth-onavailabilitychanged
584    event_handler!(
585        availabilitychanged,
586        GetOnavailabilitychanged,
587        SetOnavailabilitychanged
588    );
589}
590
591impl AsyncBluetoothListener for Bluetooth {
592    fn handle_response(&self, response: BluetoothResponse, promise: &Rc<Promise>, can_gc: CanGc) {
593        match response {
594            // https://webbluetoothcg.github.io/web-bluetooth/#request-bluetooth-devices
595            // Step 11, 13 - 14.
596            BluetoothResponse::RequestDevice(device) => {
597                let mut device_instance_map = self.device_instance_map.borrow_mut();
598                if let Some(existing_device) = device_instance_map.get(&device.id.clone()) {
599                    return promise.resolve_native(&**existing_device, can_gc);
600                }
601                let bt_device = BluetoothDevice::new(
602                    &self.global(),
603                    DOMString::from(device.id.clone()),
604                    device.name.map(DOMString::from),
605                    self,
606                    can_gc,
607                );
608                device_instance_map.insert(device.id.clone(), Dom::from_ref(&bt_device));
609
610                self.global()
611                    .as_window()
612                    .bluetooth_extra_permission_data()
613                    .add_new_allowed_device(AllowedBluetoothDevice {
614                        deviceId: DOMString::from(device.id),
615                        mayUseGATT: true,
616                    });
617                // https://webbluetoothcg.github.io/web-bluetooth/#dom-bluetooth-requestdevice
618                // Step 5.
619                promise.resolve_native(&bt_device, can_gc);
620            },
621            // https://webbluetoothcg.github.io/web-bluetooth/#dom-bluetooth-getavailability
622            // Step 2 - 3.
623            BluetoothResponse::GetAvailability(is_available) => {
624                promise.resolve_native(&is_available, can_gc);
625            },
626            _ => promise.reject_error(Error::Type("Something went wrong...".to_owned()), can_gc),
627        }
628    }
629}
630
631impl PermissionAlgorithm for Bluetooth {
632    type Descriptor = BluetoothPermissionDescriptor;
633    type Status = BluetoothPermissionResult;
634
635    fn create_descriptor(
636        cx: JSContext,
637        permission_descriptor_obj: *mut JSObject,
638    ) -> Result<BluetoothPermissionDescriptor, Error> {
639        rooted!(in(*cx) let mut property = UndefinedValue());
640        property
641            .handle_mut()
642            .set(ObjectValue(permission_descriptor_obj));
643        match BluetoothPermissionDescriptor::new(cx, property.handle()) {
644            Ok(ConversionResult::Success(descriptor)) => Ok(descriptor),
645            Ok(ConversionResult::Failure(error)) => Err(Error::Type(error.into_owned())),
646            Err(_) => Err(Error::Type(String::from(BT_DESC_CONVERSION_ERROR))),
647        }
648    }
649
650    // https://webbluetoothcg.github.io/web-bluetooth/#query-the-bluetooth-permission
651    fn permission_query(
652        _cx: JSContext,
653        promise: &Rc<Promise>,
654        descriptor: &BluetoothPermissionDescriptor,
655        status: &BluetoothPermissionResult,
656    ) {
657        // Step 1: We are not using the `global` variable.
658
659        // Step 2.
660        status.set_state(descriptor_permission_state(status.get_query(), None));
661
662        // Step 3.
663        if let PermissionState::Denied = status.get_state() {
664            status.set_devices(Vec::new());
665            return promise.resolve_native(status, CanGc::note());
666        }
667
668        // Step 4.
669        rooted_vec!(let mut matching_devices);
670
671        // Step 5.
672        let global = status.global();
673        let allowed_devices = global
674            .as_window()
675            .bluetooth_extra_permission_data()
676            .get_allowed_devices();
677
678        let bluetooth = status.get_bluetooth();
679        let device_map = bluetooth.get_device_map().borrow();
680
681        // Step 6.
682        for allowed_device in allowed_devices.iter() {
683            // Step 6.1.
684            if let Some(ref id) = descriptor.deviceId {
685                if &allowed_device.deviceId != id {
686                    continue;
687                }
688            }
689            let device_id = String::from(allowed_device.deviceId.as_ref());
690
691            // Step 6.2.
692            if let Some(ref filters) = descriptor.filters {
693                let mut scan_filters: Vec<BluetoothScanfilter> = Vec::new();
694
695                // Step 6.2.1.
696                for filter in filters {
697                    match canonicalize_filter(filter) {
698                        Ok(f) => scan_filters.push(f),
699                        Err(error) => return promise.reject_error(error, CanGc::note()),
700                    }
701                }
702
703                // Step 6.2.2.
704                // Instead of creating an internal slot we send an ipc message to the Bluetooth thread
705                // to check if one of the filters matches.
706                let (sender, receiver) =
707                    ProfiledIpc::channel(global.time_profiler_chan().clone()).unwrap();
708                status
709                    .get_bluetooth_thread()
710                    .send(BluetoothRequest::MatchesFilter(
711                        device_id.clone(),
712                        BluetoothScanfilterSequence::new(scan_filters),
713                        sender,
714                    ))
715                    .unwrap();
716
717                match receiver.recv().unwrap() {
718                    Ok(true) => (),
719                    Ok(false) => continue,
720                    Err(error) => return promise.reject_error(error.convert(), CanGc::note()),
721                };
722            }
723
724            // Step 6.3.
725            // TODO: Implement this correctly, not just using device ids here.
726            // https://webbluetoothcg.github.io/web-bluetooth/#get-the-bluetoothdevice-representing
727            if let Some(device) = device_map.get(&device_id) {
728                matching_devices.push(Dom::from_ref(&**device));
729            }
730        }
731
732        // Step 7.
733        status.set_devices(matching_devices.drain(..).collect());
734
735        // https://w3c.github.io/permissions/#dom-permissions-query
736        // Step 7.
737        promise.resolve_native(status, CanGc::note());
738    }
739
740    // https://webbluetoothcg.github.io/web-bluetooth/#request-the-bluetooth-permission
741    fn permission_request(
742        _cx: JSContext,
743        promise: &Rc<Promise>,
744        descriptor: &BluetoothPermissionDescriptor,
745        status: &BluetoothPermissionResult,
746    ) {
747        // Step 1.
748        if descriptor.filters.is_some() == descriptor.acceptAllDevices {
749            return promise.reject_error(Error::Type(OPTIONS_ERROR.to_owned()), CanGc::note());
750        }
751
752        // Step 2.
753        let sender = response_async(promise, status);
754        let bluetooth = status.get_bluetooth();
755        bluetooth.request_bluetooth_devices(
756            promise,
757            &descriptor.filters,
758            &descriptor.optionalServices,
759            sender,
760            CanGc::note(),
761        );
762
763        // NOTE: Step 3. is in BluetoothPermissionResult's `handle_response` function.
764    }
765
766    #[cfg_attr(crown, allow(crown::unrooted_must_root))]
767    // https://webbluetoothcg.github.io/web-bluetooth/#revoke-bluetooth-access
768    fn permission_revoke(
769        _descriptor: &BluetoothPermissionDescriptor,
770        status: &BluetoothPermissionResult,
771        can_gc: CanGc,
772    ) {
773        // Step 1.
774        let global = status.global();
775        let allowed_devices = global
776            .as_window()
777            .bluetooth_extra_permission_data()
778            .get_allowed_devices();
779        // Step 2.
780        let bluetooth = status.get_bluetooth();
781        let device_map = bluetooth.get_device_map().borrow();
782        for (id, device) in device_map.iter() {
783            let id = DOMString::from(id.clone());
784            // Step 2.1.
785            if allowed_devices.iter().any(|d| d.deviceId == id) &&
786                !device.is_represented_device_null()
787            {
788                // Note: We don't need to update the allowed_services,
789                // because we store it in the lower level
790                // where it is already up-to-date
791                continue;
792            }
793            // Step 2.2 - 2.4
794            let _ = device.get_gatt().Disconnect(can_gc);
795        }
796    }
797}