fips_core/transport/ble/
addr.rs1use crate::transport::{TransportAddr, TransportError};
9
10#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
12pub struct BleAddr {
13 pub adapter: String,
15 pub device: [u8; 6],
17 pub opaque_token: Option<String>,
19}
20
21impl BleAddr {
22 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 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 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 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 pub fn to_transport_addr(&self) -> TransportAddr {
97 TransportAddr::from_string(&self.to_string_repr())
98 }
99
100 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#[cfg(bluer_available)]
121impl BleAddr {
122 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 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 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
157fn 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
187pub fn adapter_from_addr(addr: &TransportAddr) -> Option<&str> {
191 addr.as_str()?.split_once('/').map(|(adapter, _)| adapter)
192}
193
194#[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}