bt_obex/
profile.rs

1// Copyright 2023 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 bt_rfcomm::profile::{rfcomm_connect_parameters, server_channel_from_protocol};
6use bt_rfcomm::ServerChannel;
7use fuchsia_bluetooth::profile::{
8    l2cap_connect_parameters, psm_from_protocol, Attribute, DataElement, ProtocolDescriptor, Psm,
9};
10use fuchsia_bluetooth::types::PeerId;
11use {fidl_fuchsia_bluetooth as fidl_bt, fidl_fuchsia_bluetooth_bredr as bredr};
12
13use crate::client::ObexClient;
14use crate::error::Error;
15use crate::transport::TransportType;
16
17impl From<bredr::ConnectParameters> for TransportType {
18    fn from(src: bredr::ConnectParameters) -> TransportType {
19        match src {
20            bredr::ConnectParameters::L2cap(_) => TransportType::L2cap,
21            _ => TransportType::Rfcomm,
22        }
23    }
24}
25
26/// The Attribute ID associated with the GoepL2capPsm attribute.
27/// Defined in https://www.bluetooth.com/specifications/assigned-numbers/service-discovery
28pub const GOEP_L2CAP_PSM_ATTRIBUTE: u16 = 0x0200;
29
30/// Returns true if the provided `protocol` is OBEX.
31///
32/// Protocols are generally specified as a list of protocol descriptors which are ordered from
33/// lowest level (typically L2CAP) to highest.
34pub fn is_obex_protocol(protocol: &Vec<ProtocolDescriptor>) -> bool {
35    protocol.iter().any(|descriptor| descriptor.protocol == bredr::ProtocolIdentifier::Obex)
36}
37
38/// Returns the protocol for an OBEX service for the provided L2CAP `psm`.
39pub fn obex_protocol_l2cap(psm: Psm) -> Vec<ProtocolDescriptor> {
40    vec![
41        ProtocolDescriptor {
42            protocol: bredr::ProtocolIdentifier::L2Cap,
43            params: vec![DataElement::Uint16(psm.into())],
44        },
45        ProtocolDescriptor { protocol: bredr::ProtocolIdentifier::Obex, params: vec![] },
46    ]
47}
48
49/// Returns the protocol for an OBEX service for the provided RFCOMM `channel` number.
50pub fn obex_protocol_rfcomm(channel: ServerChannel) -> Vec<ProtocolDescriptor> {
51    vec![
52        ProtocolDescriptor { protocol: bredr::ProtocolIdentifier::L2Cap, params: vec![] },
53        ProtocolDescriptor {
54            protocol: bredr::ProtocolIdentifier::Rfcomm,
55            params: vec![DataElement::Uint8(channel.into())],
56        },
57        ProtocolDescriptor { protocol: bredr::ProtocolIdentifier::Obex, params: vec![] },
58    ]
59}
60
61/// Returns the GoepL2capPsm attribute for the provided `psm`.
62pub fn goep_l2cap_psm_attribute(psm: Psm) -> Attribute {
63    Attribute { id: GOEP_L2CAP_PSM_ATTRIBUTE, element: DataElement::Uint16(psm.into()) }
64}
65
66/// Attempts to parse and return the PSM from the `attribute`. Returns the L2CAP PSM on success,
67/// None otherwise.
68fn parse_goep_l2cap_psm_attribute(attribute: &Attribute) -> Option<Psm> {
69    if attribute.id != GOEP_L2CAP_PSM_ATTRIBUTE {
70        return None;
71    }
72
73    if let DataElement::Uint16(psm) = attribute.element {
74        Some(Psm::new(psm))
75    } else {
76        None
77    }
78}
79
80/// Attempt to parse an OBEX service advertisement into `ConnectParameters` containing the L2CAP
81/// PSM or RFCOMM ServerChannel associated with the service.
82/// Returns the parameters on success, None if the parsing fails.
83pub fn parse_obex_search_result(
84    protocol: &Vec<ProtocolDescriptor>,
85    attributes: &Vec<Attribute>,
86) -> Option<bredr::ConnectParameters> {
87    if !is_obex_protocol(protocol) {
88        return None;
89    }
90
91    // The GoepL2capPsm attribute is included when the peer supports both RFCOMM and L2CAP.
92    // Prefer L2CAP if both are supported.
93    if let Some(l2cap_psm) = attributes.iter().find_map(parse_goep_l2cap_psm_attribute) {
94        return Some(l2cap_connect_parameters(
95            l2cap_psm,
96            fidl_bt::ChannelMode::EnhancedRetransmission,
97        ));
98    }
99
100    // Otherwise the service supports only one of L2CAP or RFCOMM.
101    // Try L2CAP first.
102    if let Some(psm) = psm_from_protocol(protocol) {
103        return Some(l2cap_connect_parameters(psm, fidl_bt::ChannelMode::EnhancedRetransmission));
104    }
105
106    // Otherwise, it's RFCOMM.
107    server_channel_from_protocol(protocol).map(|sc| rfcomm_connect_parameters(sc))
108}
109
110/// Attempt to connect to the peer `id` with the provided connect `parameters`.
111/// Returns an `ObexClient` connected to the remote OBEX service on success, or an Error if the
112/// connection could not be made.
113pub async fn connect_to_obex_service(
114    id: PeerId,
115    profile: &bredr::ProfileProxy,
116    parameters: bredr::ConnectParameters,
117) -> Result<ObexClient, Error> {
118    let channel = profile
119        .connect(&id.into(), &parameters)
120        .await
121        .map_err(anyhow::Error::from)?
122        .map_err(|e| anyhow::format_err!("{e:?}"))?;
123    let local = channel.try_into()?;
124    let transport_type = parameters.into();
125    Ok(ObexClient::new(local, transport_type))
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    use assert_matches::assert_matches;
133
134    #[test]
135    fn parse_invalid_goep_attribute_is_none() {
136        // Different data type than the expected u16 PSM.
137        let attribute = Attribute { id: GOEP_L2CAP_PSM_ATTRIBUTE, element: DataElement::Uint8(5) };
138        assert_matches!(parse_goep_l2cap_psm_attribute(&attribute), None);
139
140        // Non-GOEP attribute.
141        let attribute = Attribute {
142            id: 0x3333, // Random attribute ID
143            element: DataElement::Uint16(5),
144        };
145        assert_matches!(parse_goep_l2cap_psm_attribute(&attribute), None);
146    }
147
148    #[test]
149    fn parse_goep_attribute_success() {
150        let attribute =
151            Attribute { id: GOEP_L2CAP_PSM_ATTRIBUTE, element: DataElement::Uint16(45) };
152        assert_eq!(parse_goep_l2cap_psm_attribute(&attribute), Some(Psm::new(45)));
153    }
154
155    #[test]
156    fn parse_invalid_search_result_is_none() {
157        // A protocol with OBEX but no L2CAP or RFCOMM transport.
158        let protocol =
159            vec![ProtocolDescriptor { protocol: bredr::ProtocolIdentifier::Obex, params: vec![] }];
160        assert_matches!(parse_obex_search_result(&protocol, &vec![]), None);
161    }
162
163    #[test]
164    fn parse_non_obex_search_result_is_none() {
165        // A protocol with just L2CAP - no OBEX.
166        let protocol = vec![ProtocolDescriptor {
167            protocol: bredr::ProtocolIdentifier::L2Cap,
168            params: vec![DataElement::Uint16(27)],
169        }];
170        let attributes = vec![goep_l2cap_psm_attribute(Psm::new(55))];
171        // Even though the search result contains the GoepL2capPsm, it should not be returned
172        // because the protocol is not OBEX.
173        assert_matches!(parse_obex_search_result(&protocol, &attributes), None);
174    }
175
176    #[test]
177    fn parse_obex_search_result_with_l2cap() {
178        let l2cap_protocol = obex_protocol_l2cap(Psm::new(59));
179        let expected = bredr::ConnectParameters::L2cap(bredr::L2capParameters {
180            psm: Some(59),
181            parameters: Some(fidl_bt::ChannelParameters {
182                channel_mode: Some(fidl_bt::ChannelMode::EnhancedRetransmission),
183                ..fidl_bt::ChannelParameters::default()
184            }),
185            ..bredr::L2capParameters::default()
186        });
187        let result =
188            parse_obex_search_result(&l2cap_protocol, &vec![]).expect("valid search result");
189        assert_eq!(result, expected);
190    }
191
192    #[test]
193    fn parse_obex_search_result_with_rfcomm() {
194        let server_channel = 8.try_into().unwrap();
195        let rfcomm_protocol = obex_protocol_rfcomm(server_channel);
196        let expected = bredr::ConnectParameters::Rfcomm(bredr::RfcommParameters {
197            channel: Some(8),
198            ..bredr::RfcommParameters::default()
199        });
200        let result =
201            parse_obex_search_result(&rfcomm_protocol, &vec![]).expect("valid search result");
202        assert_eq!(result, expected);
203    }
204
205    #[test]
206    fn parse_obex_search_result_with_l2cap_and_rfcomm() {
207        let server_channel = 7.try_into().unwrap();
208        let attributes = vec![
209            Attribute {
210                id: 0x33, // Random attribute
211                element: DataElement::Uint8(5),
212            },
213            goep_l2cap_psm_attribute(Psm::new(55)),
214        ];
215        // Expected should be the L2CAP PSM.
216        let expected = bredr::ConnectParameters::L2cap(bredr::L2capParameters {
217            psm: Some(55),
218            parameters: Some(fidl_bt::ChannelParameters {
219                channel_mode: Some(fidl_bt::ChannelMode::EnhancedRetransmission),
220                ..fidl_bt::ChannelParameters::default()
221            }),
222            ..bredr::L2capParameters::default()
223        });
224        let result = parse_obex_search_result(&obex_protocol_rfcomm(server_channel), &attributes)
225            .expect("valid search result");
226        assert_eq!(result, expected);
227    }
228}