diff --git a/nmrs/examples/bluetooth_connect.rs b/nmrs/examples/bluetooth_connect.rs index 2a9738cc..54e04ce3 100644 --- a/nmrs/examples/bluetooth_connect.rs +++ b/nmrs/examples/bluetooth_connect.rs @@ -27,7 +27,7 @@ async fn main() -> Result<()> { if let Some(device) = devices.first() { println!("\nConnecting to: {}", device); - let settings = BluetoothIdentity::new(device.bdaddr.clone(), device.bt_caps.into()); + let settings = BluetoothIdentity::new(device.bdaddr.clone(), device.bt_caps.into())?; let name = device .alias diff --git a/nmrs/src/api/builders/bluetooth.rs b/nmrs/src/api/builders/bluetooth.rs index f03f352c..d04ca3c8 100644 --- a/nmrs/src/api/builders/bluetooth.rs +++ b/nmrs/src/api/builders/bluetooth.rs @@ -19,7 +19,7 @@ //! let bt_settings = BluetoothIdentity::new( //! "00:1A:7D:DA:71:13".into(), //! BluetoothNetworkRole::PanU, -//! ); +//! ).unwrap(); //! ``` use std::collections::HashMap; @@ -99,11 +99,11 @@ mod tests { } fn create_test_identity_panu() -> BluetoothIdentity { - BluetoothIdentity::new("00:1A:7D:DA:71:13".into(), BluetoothNetworkRole::PanU) + BluetoothIdentity::new("00:1A:7D:DA:71:13".into(), BluetoothNetworkRole::PanU).unwrap() } fn create_test_identity_dun() -> BluetoothIdentity { - BluetoothIdentity::new("C8:1F:E8:F0:51:57".into(), BluetoothNetworkRole::Dun) + BluetoothIdentity::new("C8:1F:E8:F0:51:57".into(), BluetoothNetworkRole::Dun).unwrap() } #[test] @@ -298,7 +298,7 @@ mod tests { #[test] fn test_bdaddr_format_preserved() { let identity = - BluetoothIdentity::new("AA:BB:CC:DD:EE:FF".into(), BluetoothNetworkRole::PanU); + BluetoothIdentity::new("AA:BB:CC:DD:EE:FF".into(), BluetoothNetworkRole::PanU).unwrap(); let opts = create_test_opts(); let conn = build_bluetooth_connection("Test", &identity, &opts); diff --git a/nmrs/src/api/models.rs b/nmrs/src/api/models.rs index 6976a3fc..628b80ce 100644 --- a/nmrs/src/api/models.rs +++ b/nmrs/src/api/models.rs @@ -4,6 +4,8 @@ use std::time::Duration; use thiserror::Error; use uuid::Uuid; +use crate::util::validation::validate_bluetooth_address; + /// NetworkManager active connection state. /// /// These values represent the lifecycle states of an active connection @@ -1676,7 +1678,7 @@ pub enum BluetoothNetworkRole { /// let bt_settings = BluetoothIdentity::new( /// "00:1A:7D:DA:71:13".into(), /// BluetoothNetworkRole::Dun, -/// ); +/// ).unwrap(); /// ``` #[non_exhaustive] #[derive(Debug, Clone)] @@ -1695,6 +1697,11 @@ impl BluetoothIdentity { /// * `bdaddr` - Bluetooth MAC address (e.g., "00:1A:7D:DA:71:13") /// * `bt_device_type` - Bluetooth network role (PanU or Dun) /// + /// # Errors + /// + /// Returns a `ConnectionError` if the provided `bdaddr` is not a + /// valid Bluetooth MAC address format. + /// /// # Example /// /// ```rust @@ -1703,13 +1710,17 @@ impl BluetoothIdentity { /// let identity = BluetoothIdentity::new( /// "00:1A:7D:DA:71:13".into(), /// BluetoothNetworkRole::PanU, - /// ); + /// ).unwrap(); /// ``` - pub fn new(bdaddr: String, bt_device_type: BluetoothNetworkRole) -> Self { - Self { + pub fn new( + bdaddr: String, + bt_device_type: BluetoothNetworkRole, + ) -> Result { + validate_bluetooth_address(&bdaddr)?; + Ok(Self { bdaddr, bt_device_type, - } + }) } } @@ -2873,7 +2884,7 @@ mod tests { #[test] fn test_bluetooth_identity_creation() { let identity = - BluetoothIdentity::new("00:1A:7D:DA:71:13".into(), BluetoothNetworkRole::PanU); + BluetoothIdentity::new("00:1A:7D:DA:71:13".into(), BluetoothNetworkRole::PanU).unwrap(); assert_eq!(identity.bdaddr, "00:1A:7D:DA:71:13"); assert!(matches!( @@ -2885,12 +2896,18 @@ mod tests { #[test] fn test_bluetooth_identity_dun() { let identity = - BluetoothIdentity::new("C8:1F:E8:F0:51:57".into(), BluetoothNetworkRole::Dun); + BluetoothIdentity::new("C8:1F:E8:F0:51:57".into(), BluetoothNetworkRole::Dun).unwrap(); assert_eq!(identity.bdaddr, "C8:1F:E8:F0:51:57"); assert!(matches!(identity.bt_device_type, BluetoothNetworkRole::Dun)); } + #[test] + fn test_bluetooth_identity_creation_error() { + let res = BluetoothIdentity::new("SomeInvalidAddress".into(), BluetoothNetworkRole::Dun); + assert!(res.is_err()); + } + #[test] fn test_bluetooth_device_creation() { let role = BluetoothNetworkRole::PanU as u32; diff --git a/nmrs/src/api/network_manager.rs b/nmrs/src/api/network_manager.rs index 033789e6..226e9a22 100644 --- a/nmrs/src/api/network_manager.rs +++ b/nmrs/src/api/network_manager.rs @@ -244,7 +244,7 @@ impl NetworkManager { /// let identity = BluetoothIdentity::new( /// "C8:1F:E8:F0:51:57".into(), /// BluetoothNetworkRole::PanU, - /// ); + /// )?; /// /// nm.connect_bluetooth("My Phone", &identity).await?; /// Ok(()) diff --git a/nmrs/src/core/bluetooth.rs b/nmrs/src/core/bluetooth.rs index 1aef34c6..0b6c77d3 100644 --- a/nmrs/src/core/bluetooth.rs +++ b/nmrs/src/core/bluetooth.rs @@ -19,6 +19,7 @@ use crate::monitoring::bluetooth::Bluetooth; use crate::monitoring::transport::ActiveTransport; use crate::types::constants::device_state; use crate::types::constants::device_type; +use crate::util::validation::validate_bluetooth_address; use crate::ConnectionError; use crate::{ dbus::NMProxy, @@ -32,6 +33,9 @@ use crate::{ /// over D-Bus to retrieve the device's name and alias. It constructs the /// appropriate D-Bus object path based on the BDADDR format. /// +/// If the given address is not a valid bluetooth device address, +/// the function will return error. +/// /// NetworkManager does not expose Bluetooth device names/aliases directly, /// hence this additional step is necessary to obtain user-friendly /// identifiers for Bluetooth devices. (See `BluezDeviceExtProxy` for details.) @@ -39,6 +43,8 @@ pub(crate) async fn populate_bluez_info( conn: &Connection, bdaddr: &str, ) -> Result<(Option, Option)> { + validate_bluetooth_address(bdaddr)?; + // [variable prefix]/{hci0,hci1,...}/dev_XX_XX_XX_XX_XX_XX // This replaces ':' with '_' in the BDADDR to form the correct D-Bus object path. // TODO: Instead of hardcoding hci0, we should determine the actual adapter name. @@ -102,7 +108,7 @@ pub(crate) async fn find_bluetooth_device( /// let settings = BluetoothIdentity::new( /// "C8:1F:E8:F0:51:57".into(), /// BluetoothNetworkRole::PanU, -/// ); +/// ).unwrap(); /// // connect_bluetooth(&conn, "My Phone", &settings).await?; /// ``` pub(crate) async fn connect_bluetooth( @@ -265,7 +271,7 @@ mod tests { #[test] fn test_bluetooth_identity_structure() { let identity = - BluetoothIdentity::new("00:1A:7D:DA:71:13".into(), BluetoothNetworkRole::PanU); + BluetoothIdentity::new("00:1A:7D:DA:71:13".into(), BluetoothNetworkRole::PanU).unwrap(); assert_eq!(identity.bdaddr, "00:1A:7D:DA:71:13"); assert!(matches!( diff --git a/nmrs/src/util/validation.rs b/nmrs/src/util/validation.rs index ef120910..79f640dc 100644 --- a/nmrs/src/util/validation.rs +++ b/nmrs/src/util/validation.rs @@ -532,6 +532,39 @@ fn validate_ip_address(ip: &str) -> Result<(), ConnectionError> { Ok(()) } +/// Validates a Bluetooth address against the EUI-48 format (using colons). +/// +/// # Errors +/// Returns `ConnectionError::InvalidAddress` if the Bluetooth address is invalid. +pub fn validate_bluetooth_address(bdaddr: &str) -> Result<(), ConnectionError> { + let parts: Vec<&str> = bdaddr.split(':').collect(); + + if parts.len() != 6 { + return Err(ConnectionError::InvalidAddress(format!( + "Invalid Bluetooth Address '{}' (must have 6 segments)", + bdaddr, + ))); + } + + for part in parts { + if part.len() != 2 { + return Err(ConnectionError::InvalidAddress(format!( + "Invalid segment '{}' in Bluetooth Address '{}' (must be 2 characters)", + part, bdaddr + ))); + } + + if !part.chars().all(|c| c.is_ascii_hexdigit()) { + return Err(ConnectionError::InvalidAddress(format!( + "Invalid segment '{}' in Bluetooth Address '{}' (must be hex digits)", + part, bdaddr + ))); + } + } + + Ok(()) +} + #[cfg(test)] mod tests { use super::*; @@ -714,4 +747,31 @@ mod tests { let key = "!!!invalid-base64-characters-here!!!"; assert!(validate_wireguard_key(key, "Test key").is_err()); } + + #[test] + fn test_validate_bluetooth_address_valid() { + assert!(validate_bluetooth_address("00:1A:7D:DA:71:13").is_ok()); + assert!(validate_bluetooth_address("00:1a:7d:da:71:13").is_ok()); + assert!(validate_bluetooth_address("aA:bB:cC:dD:eE:fF").is_ok()); + } + + #[test] + fn test_validate_bluetooth_address_invalid_format() { + assert!(validate_bluetooth_address("00-1A-7D-DA-71-13").is_err()); + assert!(validate_bluetooth_address("001A7DDA7113").is_err()); + assert!(validate_bluetooth_address("00:1A:7D:DA:711:3").is_err()); + } + + #[test] + fn test_validate_bluetooth_address_invalid_char() { + assert!(validate_bluetooth_address("00:1A:7D:DA:71:GG").is_err()); + assert!(validate_bluetooth_address("00:1A:7D:DA:71:!!").is_err()); + } + + #[test] + fn test_validate_bluetooth_address_invalid_length() { + assert!(validate_bluetooth_address("00:1A:7D").is_err()); + assert!(validate_bluetooth_address("00:1A:7D:DA:71:13:FF").is_err()); + assert!(validate_bluetooth_address("").is_err()); + } } diff --git a/nmrs/tests/integration_test.rs b/nmrs/tests/integration_test.rs index 544e558c..1e67efb3 100644 --- a/nmrs/tests/integration_test.rs +++ b/nmrs/tests/integration_test.rs @@ -1108,7 +1108,8 @@ fn test_bluetooth_network_role() { fn test_bluetooth_identity_structure() { use nmrs::models::{BluetoothIdentity, BluetoothNetworkRole}; - let identity = BluetoothIdentity::new("00:1A:7D:DA:71:13".into(), BluetoothNetworkRole::PanU); + let identity = + BluetoothIdentity::new("00:1A:7D:DA:71:13".into(), BluetoothNetworkRole::PanU).unwrap(); assert_eq!(identity.bdaddr, "00:1A:7D:DA:71:13"); assert!(matches!(