diff --git a/crates/host_env/src/socket.rs b/crates/host_env/src/socket.rs index abf216608..12ca8d0fb 100644 --- a/crates/host_env/src/socket.rs +++ b/crates/host_env/src/socket.rs @@ -18,6 +18,9 @@ pub use ::dns_lookup as dns; #[cfg(not(target_arch = "wasm32"))] pub use ::socket2 as raw; +#[cfg(target_os = "linux")] +pub mod bluetooth; + #[cfg(not(any( target_os = "ios", target_os = "android", diff --git a/crates/host_env/src/socket/bluetooth.rs b/crates/host_env/src/socket/bluetooth.rs new file mode 100644 index 000000000..0b803282a --- /dev/null +++ b/crates/host_env/src/socket/bluetooth.rs @@ -0,0 +1,185 @@ +//! Linux Bluetooth socket address layouts from the BlueZ headers. + +use super::raw::{SockAddr, SockAddrStorage}; + +pub const L2CAP: i32 = 0; +pub const HCI: i32 = 1; +pub const SCO: i32 = 2; +pub const RFCOMM: i32 = 3; + +#[repr(C)] +struct SockaddrL2 { + family: libc::sa_family_t, + psm: u16, + bdaddr: [u8; 6], + cid: u16, + addr_type: u8, +} + +#[repr(C)] +struct SockaddrRc { + family: libc::sa_family_t, + bdaddr: [u8; 6], + channel: u8, +} + +#[repr(C)] +struct SockaddrHci { + family: libc::sa_family_t, + dev: u16, + channel: u16, +} + +#[repr(C)] +struct SockaddrSco { + family: libc::sa_family_t, + bdaddr: [u8; 6], +} + +#[derive(Debug, PartialEq, Eq)] +pub enum Address { + L2cap { + bdaddr: [u8; 6], + psm: u16, + cid: u16, + addr_type: u8, + }, + Rfcomm { + bdaddr: [u8; 6], + channel: u8, + }, + Hci { + dev: u16, + channel: u16, + }, + Sco { + bdaddr: [u8; 6], + }, +} + +impl Address { + pub fn to_sockaddr(&self) -> SockAddr { + let mut storage = SockAddrStorage::zeroed(); + // SAFETY: these repr(C) layouts match the Linux sockaddr_* definitions. + // SockAddrStorage provides their size/alignment and is zero-initialized. + // Write fields in place so padding remains initialized. + let len = unsafe { + match *self { + Self::L2cap { + bdaddr, + psm, + cid, + addr_type, + } => { + let addr = storage.view_as::(); + addr.family = libc::AF_BLUETOOTH as _; + addr.psm = psm; + addr.bdaddr = bdaddr; + addr.cid = cid; + addr.addr_type = addr_type; + size_of::() + } + Self::Rfcomm { bdaddr, channel } => { + let addr = storage.view_as::(); + addr.family = libc::AF_BLUETOOTH as _; + addr.bdaddr = bdaddr; + addr.channel = channel; + size_of::() + } + Self::Hci { dev, channel } => { + let addr = storage.view_as::(); + addr.family = libc::AF_BLUETOOTH as _; + addr.dev = dev; + addr.channel = channel; + size_of::() + } + Self::Sco { bdaddr } => { + let addr = storage.view_as::(); + addr.family = libc::AF_BLUETOOTH as _; + addr.bdaddr = bdaddr; + size_of::() + } + } + }; + // SAFETY: storage contains a complete address of the stated length. + unsafe { SockAddr::new(storage, len as _) } + } + + pub fn from_sockaddr(addr: &SockAddr, protocol: i32) -> Option { + if addr.family() != libc::AF_BLUETOOTH as _ { + return None; + } + // SAFETY: SockAddr owns aligned storage; each cast checks the returned + // length. All bit patterns are valid for these integer-only layouts. + unsafe { + match protocol { + L2CAP if addr.len() as usize >= size_of::() => { + let addr = &*addr.as_ptr().cast::(); + Some(Self::L2cap { + bdaddr: addr.bdaddr, + psm: addr.psm, + cid: addr.cid, + addr_type: addr.addr_type, + }) + } + RFCOMM if addr.len() as usize >= size_of::() => { + let addr = &*addr.as_ptr().cast::(); + Some(Self::Rfcomm { + bdaddr: addr.bdaddr, + channel: addr.channel, + }) + } + HCI if addr.len() as usize >= size_of::() => { + let addr = &*addr.as_ptr().cast::(); + Some(Self::Hci { + dev: addr.dev, + channel: addr.channel, + }) + } + SCO if addr.len() as usize >= size_of::() => { + let addr = &*addr.as_ptr().cast::(); + Some(Self::Sco { + bdaddr: addr.bdaddr, + }) + } + _ => None, + } + } + } +} + +pub fn parse_bdaddr(name: &[u8]) -> Option<[u8; 6]> { + let mut parts = name.split(|&b| b == b':'); + let mut bdaddr = [0; 6]; + for byte in bdaddr.iter_mut().rev() { + // CPython's %X parser permits leading whitespace, a sign and 0x. + let part = parts.next()?.trim_ascii_start(); + let (part, negative) = match part.first() { + Some(b'+') => (&part[1..], false), + Some(b'-') => (&part[1..], true), + _ => (part, false), + }; + if part.is_empty() { + return None; + } + let part = part + .strip_prefix(b"0x") + .or_else(|| part.strip_prefix(b"0X")) + .unwrap_or(part); + let mut value = 0u8; + for &digit in part { + let digit = (digit as char).to_digit(16)? as u8; + value = value.checked_mul(16)?.checked_add(digit)?; + } + if negative && value != 0 { + return None; + } + *byte = value; + } + parts.next().is_none().then_some(bdaddr) +} + +pub fn format_bdaddr(bdaddr: [u8; 6]) -> String { + let [f, e, d, c, b, a] = bdaddr; + alloc::format!("{a:02X}:{b:02X}:{c:02X}:{d:02X}:{e:02X}:{f:02X}") +} diff --git a/crates/stdlib/src/socket.rs b/crates/stdlib/src/socket.rs index 06cb76a86..f5aa1c0f5 100644 --- a/crates/stdlib/src/socket.rs +++ b/crates/stdlib/src/socket.rs @@ -1606,6 +1606,14 @@ mod _socket { }) } #[cfg(target_os = "linux")] + c::AF_BLUETOOTH => Ok(extract_bluetooth_address( + addr, + self.proto.load(Ordering::Relaxed), + caller, + vm, + )? + .to_sockaddr()), + #[cfg(target_os = "linux")] c::AF_CAN => { let tuple: PyTupleRef = addr.downcast().map_err(|obj| { vm.new_type_error(format!( @@ -2925,6 +2933,96 @@ mod _socket { } } + #[cfg(target_os = "linux")] + fn extract_bluetooth_address( + addr: PyObjectRef, + protocol: i32, + caller: &str, + vm: &VirtualMachine, + ) -> PyResult { + use crate::vm::builtins::PyBytes; + use host_socket::bluetooth::{self, Address}; + + let wrong_format = || vm.new_os_error(format!("{caller}(): wrong format")); + let parse_name = |name: &[u8]| { + bluetooth::parse_bdaddr(name).ok_or_else(|| vm.new_os_error("bad bluetooth address")) + }; + if protocol == bluetooth::SCO { + let (name, nul_error) = if let Some(bytes) = addr.downcast_ref::() { + (bytes.as_bytes().to_vec(), "embedded null byte") + } else { + let name: PyStrRef = addr.downcast().map_err(|_| wrong_format())?; + ( + name.try_into_utf8(vm)?.as_str().as_bytes().to_vec(), + "embedded null character", + ) + }; + if name.contains(&0) { + return Err(vm.new_value_error(nul_error)); + } + return Ok(Address::Sco { + bdaddr: parse_name(&name)?, + }); + } + if protocol == bluetooth::HCI + && let Some(index) = addr.try_index_opt(vm) + { + return Ok(Address::Hci { + dev: index?.as_u32_mask() as u16, + channel: 0, + }); + } + let (min, max) = match protocol { + bluetooth::L2CAP => (2, 4), + bluetooth::RFCOMM => (2, 2), + bluetooth::HCI => (1, 2), + _ => return Err(vm.new_os_error(format!("{caller}(): unknown Bluetooth protocol"))), + }; + let tuple: PyTupleRef = addr.downcast().map_err(|_| wrong_format())?; + let fields = tuple.as_slice(); + if !(min..=max).contains(&fields.len()) { + return Err(wrong_format()); + } + // PyArg_ParseTuple's H/B formats mask unsigned values rather than + // rejecting overflow, including values returned by __index__. + let number = |i: usize| -> PyResult { + fields.get(i).map_or(Ok(0), |value| { + value + .try_index(vm) + .map(|n| n.as_u32_mask()) + .map_err(|_| wrong_format()) + }) + }; + if protocol == bluetooth::HCI { + return Ok(Address::Hci { + dev: number(0)? as u16, + channel: number(1)? as u16, + }); + } + let name: PyStrRef = fields[0].clone().downcast().map_err(|_| wrong_format())?; + let name = name.try_into_utf8(vm).map_err(|_| wrong_format())?; + if name.as_str().contains('\0') { + return Err(wrong_format()); + } + let port = number(1)?; + let cid = number(2)? as u16; + let addr_type = number(3)? as u8; + let bdaddr = parse_name(name.as_str().as_bytes())?; + Ok(if protocol == bluetooth::L2CAP { + Address::L2cap { + bdaddr, + psm: port as u16, + cid, + addr_type, + } + } else { + Address::Rfcomm { + bdaddr, + channel: port as u8, + } + }) + } + fn get_addr_tuple( addr: &host_socket::raw::SockAddr, #[cfg_attr(not(target_os = "linux"), allow(unused_variables))] proto: i32, @@ -2956,6 +3054,37 @@ mod _socket { #[cfg(target_os = "linux")] { let family = addr.family(); + if let Some(addr) = host_socket::bluetooth::Address::from_sockaddr(addr, proto) { + use host_socket::bluetooth::{Address, format_bdaddr}; + return match addr { + Address::L2cap { + bdaddr, + psm, + cid, + addr_type, + } => { + let name = format_bdaddr(bdaddr); + if addr_type != 0 { + (name, psm, cid, addr_type).to_pyobject(vm) + } else if cid != 0 { + (name, psm, cid).to_pyobject(vm) + } else { + (name, psm).to_pyobject(vm) + } + } + Address::Rfcomm { bdaddr, channel } => { + (format_bdaddr(bdaddr), channel).to_pyobject(vm) + } + Address::Hci { dev, channel } => { + if channel == 0 { + dev.to_pyobject(vm) + } else { + (dev, channel).to_pyobject(vm) + } + } + Address::Sco { bdaddr } => format_bdaddr(bdaddr).to_pyobject(vm), + }; + } if family == c::AF_CAN as c::sa_family_t { let can_addr = unsafe { &*(addr.as_ptr() as *const c::sockaddr_can) }; let ifindex = can_addr.can_ifindex;