Skip to main content

fips_core/transport/ble/
addr.rs

1//! BLE transport address parsing and formatting.
2//!
3//! Address format: `"hci0/AA:BB:CC:DD:EE:FF"` — adapter name / device address.
4
5use crate::transport::{TransportAddr, TransportError};
6
7/// A parsed BLE device address.
8#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
9pub struct BleAddr {
10    /// HCI adapter name (e.g., "hci0").
11    pub adapter: String,
12    /// 6-byte Bluetooth device address.
13    pub device: [u8; 6],
14}
15
16impl BleAddr {
17    /// Parse a BLE address from the `"adapter/AA:BB:CC:DD:EE:FF"` format.
18    pub fn parse(s: &str) -> Result<Self, TransportError> {
19        let (adapter, mac_str) = s.split_once('/').ok_or_else(|| {
20            TransportError::InvalidAddress(format!("missing '/' in BLE address: {s}"))
21        })?;
22
23        if adapter.is_empty() {
24            return Err(TransportError::InvalidAddress("empty adapter name".into()));
25        }
26
27        let device = parse_mac(mac_str).ok_or_else(|| {
28            TransportError::InvalidAddress(format!("invalid MAC address: {mac_str}"))
29        })?;
30
31        Ok(Self {
32            adapter: adapter.to_string(),
33            device,
34        })
35    }
36
37    /// Format as `"adapter/AA:BB:CC:DD:EE:FF"`.
38    pub fn to_string_repr(&self) -> String {
39        format!(
40            "{}/{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}",
41            self.adapter,
42            self.device[0],
43            self.device[1],
44            self.device[2],
45            self.device[3],
46            self.device[4],
47            self.device[5],
48        )
49    }
50
51    /// Convert to a `TransportAddr` (string representation).
52    pub fn to_transport_addr(&self) -> TransportAddr {
53        TransportAddr::from_string(&self.to_string_repr())
54    }
55}
56
57// ============================================================================
58// bluer type conversions (glibc-linux only; see build.rs bluer_available)
59// ============================================================================
60
61#[cfg(bluer_available)]
62impl BleAddr {
63    /// Construct from a bluer `Address` and adapter name.
64    pub fn from_bluer(addr: bluer::Address, adapter: &str) -> Self {
65        Self {
66            adapter: adapter.to_string(),
67            device: addr.0,
68        }
69    }
70
71    /// Convert to a bluer `Address`.
72    pub fn to_bluer_address(&self) -> bluer::Address {
73        bluer::Address(self.device)
74    }
75
76    /// Convert to a bluer L2CAP `SocketAddr` with the given PSM.
77    pub fn to_socket_addr(&self, psm: u16) -> bluer::l2cap::SocketAddr {
78        bluer::l2cap::SocketAddr::new(self.to_bluer_address(), bluer::AddressType::LePublic, psm)
79    }
80}
81
82impl std::fmt::Display for BleAddr {
83    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84        write!(f, "{}", self.to_string_repr())
85    }
86}
87
88/// Parse a colon-delimited MAC address string into 6 bytes.
89fn parse_mac(s: &str) -> Option<[u8; 6]> {
90    let parts: Vec<&str> = s.split(':').collect();
91    if parts.len() != 6 {
92        return None;
93    }
94    let mut mac = [0u8; 6];
95    for (i, part) in parts.iter().enumerate() {
96        mac[i] = u8::from_str_radix(part, 16).ok()?;
97    }
98    Some(mac)
99}
100
101/// Extract the adapter name from a transport address string.
102///
103/// Returns `None` if the address is not valid UTF-8 or doesn't contain '/'.
104pub fn adapter_from_addr(addr: &TransportAddr) -> Option<&str> {
105    addr.as_str()?.split_once('/').map(|(adapter, _)| adapter)
106}
107
108// ============================================================================
109// Tests
110// ============================================================================
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn test_parse_valid() {
118        let addr = BleAddr::parse("hci0/AA:BB:CC:DD:EE:FF").unwrap();
119        assert_eq!(addr.adapter, "hci0");
120        assert_eq!(addr.device, [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]);
121    }
122
123    #[test]
124    fn test_parse_lowercase() {
125        let addr = BleAddr::parse("hci1/aa:bb:cc:dd:ee:ff").unwrap();
126        assert_eq!(addr.adapter, "hci1");
127        assert_eq!(addr.device, [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]);
128    }
129
130    #[test]
131    fn test_roundtrip() {
132        let original = "hci0/AA:BB:CC:DD:EE:FF";
133        let addr = BleAddr::parse(original).unwrap();
134        assert_eq!(addr.to_string_repr(), original);
135    }
136
137    #[test]
138    fn test_display() {
139        let addr = BleAddr::parse("hci0/01:02:03:04:05:06").unwrap();
140        assert_eq!(format!("{addr}"), "hci0/01:02:03:04:05:06");
141    }
142
143    #[test]
144    fn test_to_transport_addr() {
145        let addr = BleAddr::parse("hci0/AA:BB:CC:DD:EE:FF").unwrap();
146        let ta = addr.to_transport_addr();
147        assert_eq!(ta.as_str(), Some("hci0/AA:BB:CC:DD:EE:FF"));
148    }
149
150    #[test]
151    fn test_parse_missing_slash() {
152        assert!(BleAddr::parse("hci0-AA:BB:CC:DD:EE:FF").is_err());
153    }
154
155    #[test]
156    fn test_parse_empty_adapter() {
157        assert!(BleAddr::parse("/AA:BB:CC:DD:EE:FF").is_err());
158    }
159
160    #[test]
161    fn test_parse_invalid_mac_short() {
162        assert!(BleAddr::parse("hci0/AA:BB:CC").is_err());
163    }
164
165    #[test]
166    fn test_parse_invalid_mac_hex() {
167        assert!(BleAddr::parse("hci0/GG:HH:II:JJ:KK:LL").is_err());
168    }
169
170    #[test]
171    fn test_adapter_from_addr() {
172        let ta = TransportAddr::from_string("hci0/AA:BB:CC:DD:EE:FF");
173        assert_eq!(adapter_from_addr(&ta), Some("hci0"));
174    }
175
176    #[test]
177    fn test_adapter_from_addr_no_slash() {
178        let ta = TransportAddr::from_string("invalid");
179        assert_eq!(adapter_from_addr(&ta), None);
180    }
181}