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