Skip to main content

fips_core/transport/ble/
addr.rs

1//! BLE transport address parsing and formatting.
2//!
3//! Address format: `"adapter/peer-token"`.
4//!
5//! BlueZ uses a MAC address as the peer token. Mobile platform adapters use
6//! opaque identifiers supplied by the operating system.
7
8use crate::transport::{TransportAddr, TransportError};
9
10/// A parsed BLE device address.
11#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
12pub struct BleAddr {
13    /// HCI adapter name (e.g., "hci0").
14    pub adapter: String,
15    /// 6-byte Bluetooth device address.
16    pub device: [u8; 6],
17    /// Opaque platform peer token when no stable Bluetooth address is exposed.
18    pub opaque_token: Option<String>,
19}
20
21impl BleAddr {
22    /// Parse a BLE address from the `"adapter/AA:BB:CC:DD:EE:FF"` format.
23    pub fn parse(s: &str) -> Result<Self, TransportError> {
24        let (adapter, mac_str) = s.split_once('/').ok_or_else(|| {
25            TransportError::InvalidAddress(format!("missing '/' in BLE address: {s}"))
26        })?;
27
28        if adapter.is_empty() {
29            return Err(TransportError::InvalidAddress("empty adapter name".into()));
30        }
31
32        let (device, opaque_token) = if let Some(device) = parse_mac(mac_str) {
33            (device, None)
34        } else {
35            if adapter.starts_with("hci") {
36                return Err(TransportError::InvalidAddress(format!(
37                    "invalid MAC address: {mac_str}"
38                )));
39            }
40            validate_opaque_token(mac_str)?;
41            ([0; 6], Some(mac_str.to_string()))
42        };
43
44        Ok(Self {
45            adapter: adapter.to_string(),
46            device,
47            opaque_token,
48        })
49    }
50
51    /// Construct an address backed by a Bluetooth MAC address.
52    pub fn from_mac(adapter: impl Into<String>, device: [u8; 6]) -> Self {
53        Self {
54            adapter: adapter.into(),
55            device,
56            opaque_token: None,
57        }
58    }
59
60    /// Construct an address backed by an opaque platform peer token.
61    pub fn from_opaque(
62        adapter: impl Into<String>,
63        token: impl Into<String>,
64    ) -> Result<Self, TransportError> {
65        let adapter = adapter.into();
66        let token = token.into();
67        if adapter.is_empty() {
68            return Err(TransportError::InvalidAddress("empty adapter name".into()));
69        }
70        validate_opaque_token(&token)?;
71        Ok(Self {
72            adapter,
73            device: [0; 6],
74            opaque_token: Some(token),
75        })
76    }
77
78    /// Format as `"adapter/AA:BB:CC:DD:EE:FF"`.
79    pub fn to_string_repr(&self) -> String {
80        if let Some(token) = &self.opaque_token {
81            return format!("{}/{}", self.adapter, token);
82        }
83        format!(
84            "{}/{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}",
85            self.adapter,
86            self.device[0],
87            self.device[1],
88            self.device[2],
89            self.device[3],
90            self.device[4],
91            self.device[5],
92        )
93    }
94
95    /// Convert to a `TransportAddr` (string representation).
96    pub fn to_transport_addr(&self) -> TransportAddr {
97        TransportAddr::from_string(&self.to_string_repr())
98    }
99
100    /// Platform token without the local adapter prefix.
101    pub fn peer_token(&self) -> String {
102        self.opaque_token.clone().unwrap_or_else(|| {
103            format!(
104                "{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}",
105                self.device[0],
106                self.device[1],
107                self.device[2],
108                self.device[3],
109                self.device[4],
110                self.device[5]
111            )
112        })
113    }
114}
115
116// ============================================================================
117// bluer type conversions (glibc-linux only; see build.rs bluer_available)
118// ============================================================================
119
120#[cfg(bluer_available)]
121impl BleAddr {
122    /// Construct from a bluer `Address` and adapter name.
123    pub fn from_bluer(addr: bluer::Address, adapter: &str) -> Self {
124        Self {
125            adapter: adapter.to_string(),
126            device: addr.0,
127            opaque_token: None,
128        }
129    }
130
131    /// Convert to a bluer `Address`.
132    pub fn to_bluer_address(&self) -> Result<bluer::Address, TransportError> {
133        if self.opaque_token.is_some() {
134            return Err(TransportError::InvalidAddress(
135                "opaque mobile BLE peer token cannot be used by BlueZ".into(),
136            ));
137        }
138        Ok(bluer::Address(self.device))
139    }
140
141    /// Convert to a bluer L2CAP `SocketAddr` with the given PSM.
142    pub fn to_socket_addr(&self, psm: u16) -> Result<bluer::l2cap::SocketAddr, TransportError> {
143        Ok(bluer::l2cap::SocketAddr::new(
144            self.to_bluer_address()?,
145            bluer::AddressType::LePublic,
146            psm,
147        ))
148    }
149}
150
151impl std::fmt::Display for BleAddr {
152    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153        write!(f, "{}", self.to_string_repr())
154    }
155}
156
157/// Parse a colon-delimited MAC address string into 6 bytes.
158fn parse_mac(s: &str) -> Option<[u8; 6]> {
159    let parts: Vec<&str> = s.split(':').collect();
160    if parts.len() != 6 {
161        return None;
162    }
163    let mut mac = [0u8; 6];
164    for (i, part) in parts.iter().enumerate() {
165        mac[i] = u8::from_str_radix(part, 16).ok()?;
166    }
167    Some(mac)
168}
169
170fn validate_opaque_token(token: &str) -> Result<(), TransportError> {
171    if token.is_empty() || token.len() > 128 {
172        return Err(TransportError::InvalidAddress(
173            "BLE peer token must contain 1 to 128 bytes".into(),
174        ));
175    }
176    if !token
177        .bytes()
178        .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b':'))
179    {
180        return Err(TransportError::InvalidAddress(
181            "BLE peer token contains unsupported characters".into(),
182        ));
183    }
184    Ok(())
185}
186
187/// Extract the adapter name from a transport address string.
188///
189/// Returns `None` if the address is not valid UTF-8 or doesn't contain '/'.
190pub fn adapter_from_addr(addr: &TransportAddr) -> Option<&str> {
191    addr.as_str()?.split_once('/').map(|(adapter, _)| adapter)
192}
193
194// ============================================================================
195// Tests
196// ============================================================================
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    #[test]
203    fn test_parse_valid() {
204        let addr = BleAddr::parse("hci0/AA:BB:CC:DD:EE:FF").unwrap();
205        assert_eq!(addr.adapter, "hci0");
206        assert_eq!(addr.device, [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]);
207    }
208
209    #[test]
210    fn test_parse_lowercase() {
211        let addr = BleAddr::parse("hci1/aa:bb:cc:dd:ee:ff").unwrap();
212        assert_eq!(addr.adapter, "hci1");
213        assert_eq!(addr.device, [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]);
214    }
215
216    #[test]
217    fn test_roundtrip() {
218        let original = "hci0/AA:BB:CC:DD:EE:FF";
219        let addr = BleAddr::parse(original).unwrap();
220        assert_eq!(addr.to_string_repr(), original);
221    }
222
223    #[test]
224    fn test_roundtrip_opaque_platform_token() {
225        let original = "ios/4D36E96E-E325-11CE-BFC1-08002BE10318";
226        let addr = BleAddr::parse(original).unwrap();
227        assert_eq!(addr.to_string_repr(), original);
228    }
229
230    #[test]
231    fn test_display() {
232        let addr = BleAddr::parse("hci0/01:02:03:04:05:06").unwrap();
233        assert_eq!(format!("{addr}"), "hci0/01:02:03:04:05:06");
234    }
235
236    #[test]
237    fn test_to_transport_addr() {
238        let addr = BleAddr::parse("hci0/AA:BB:CC:DD:EE:FF").unwrap();
239        let ta = addr.to_transport_addr();
240        assert_eq!(ta.as_str(), Some("hci0/AA:BB:CC:DD:EE:FF"));
241    }
242
243    #[test]
244    fn test_parse_missing_slash() {
245        assert!(BleAddr::parse("hci0-AA:BB:CC:DD:EE:FF").is_err());
246    }
247
248    #[test]
249    fn test_parse_empty_adapter() {
250        assert!(BleAddr::parse("/AA:BB:CC:DD:EE:FF").is_err());
251    }
252
253    #[test]
254    fn test_parse_invalid_mac_short() {
255        assert!(BleAddr::parse("hci0/AA:BB:CC").is_err());
256    }
257
258    #[test]
259    fn test_parse_invalid_mac_hex() {
260        assert!(BleAddr::parse("hci0/GG:HH:II:JJ:KK:LL").is_err());
261    }
262
263    #[test]
264    fn test_adapter_from_addr() {
265        let ta = TransportAddr::from_string("hci0/AA:BB:CC:DD:EE:FF");
266        assert_eq!(adapter_from_addr(&ta), Some("hci0"));
267    }
268
269    #[test]
270    fn test_adapter_from_addr_no_slash() {
271        let ta = TransportAddr::from_string("invalid");
272        assert_eq!(adapter_from_addr(&ta), None);
273    }
274}