From e4ec09fc99cdb1a8f37965629ff595f9955c73d2 Mon Sep 17 00:00:00 2001 From: SangHun Kim Date: Wed, 11 Feb 2026 17:45:37 +0900 Subject: [PATCH 1/4] feat(#215): impl `validate_bluetooth_address` --- nmrs/src/util/validation.rs | 56 +++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/nmrs/src/util/validation.rs b/nmrs/src/util/validation.rs index ef120910..1b618b3a 100644 --- a/nmrs/src/util/validation.rs +++ b/nmrs/src/util/validation.rs @@ -532,6 +532,35 @@ 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> { + if bdaddr.len() != 17 { + return Err(ConnectionError::InvalidAddress(format!( + "Invalid Bluetooth Address '{}' (expected length 17)", + bdaddr + ))); + } + for (index, c) in bdaddr.chars().enumerate() { + if (index + 1) % 3 == 0 { + if c != ':' { + return Err(ConnectionError::InvalidAddress(format!( + "Invalid Bluetooth Address '{}' (expected ':', found {})", + bdaddr, c + ))); + } + } else if !c.is_ascii_hexdigit() { + return Err(ConnectionError::InvalidAddress(format!( + "Invalid Bluetooth Address '{}' ('{}' is not a hex digit)", + bdaddr, c + ))); + } + } + Ok(()) +} + #[cfg(test)] mod tests { use super::*; @@ -714,4 +743,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()); + } } From 1f4bba1b6b35eab0074a9157c670b4ca0c79804d Mon Sep 17 00:00:00 2001 From: SangHun Kim Date: Sat, 14 Feb 2026 16:40:58 +0900 Subject: [PATCH 2/4] feat(#215): validate bdaddr in `populate_bluez_info` --- nmrs/src/core/bluetooth.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/nmrs/src/core/bluetooth.rs b/nmrs/src/core/bluetooth.rs index 1aef34c6..3c60de0b 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. From 76a6d225c51df0d17e932d3cc87b94b270196d87 Mon Sep 17 00:00:00 2001 From: SangHun Kim Date: Sat, 14 Feb 2026 16:50:22 +0900 Subject: [PATCH 3/4] feat(#215): validate bdaddr in `BluetoothIdentity::new` --- nmrs/examples/bluetooth_connect.rs | 2 +- nmrs/src/api/builders/bluetooth.rs | 8 ++++---- nmrs/src/api/models.rs | 31 +++++++++++++++++++++++------- nmrs/src/api/network_manager.rs | 2 +- nmrs/src/core/bluetooth.rs | 4 ++-- nmrs/tests/integration_test.rs | 3 ++- 6 files changed, 34 insertions(+), 16 deletions(-) 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 3c60de0b..0b6c77d3 100644 --- a/nmrs/src/core/bluetooth.rs +++ b/nmrs/src/core/bluetooth.rs @@ -108,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( @@ -271,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/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!( From 3b258493f2fd3b48646fa0666d196dc93f3b0609 Mon Sep 17 00:00:00 2001 From: SangHun Kim Date: Mon, 16 Feb 2026 17:01:40 +0900 Subject: [PATCH 4/4] fixup! feat(#215): impl `validate_bluetooth_address` --- nmrs/src/util/validation.rs | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/nmrs/src/util/validation.rs b/nmrs/src/util/validation.rs index 1b618b3a..79f640dc 100644 --- a/nmrs/src/util/validation.rs +++ b/nmrs/src/util/validation.rs @@ -537,27 +537,31 @@ fn validate_ip_address(ip: &str) -> Result<(), ConnectionError> { /// # Errors /// Returns `ConnectionError::InvalidAddress` if the Bluetooth address is invalid. pub fn validate_bluetooth_address(bdaddr: &str) -> Result<(), ConnectionError> { - if bdaddr.len() != 17 { + let parts: Vec<&str> = bdaddr.split(':').collect(); + + if parts.len() != 6 { return Err(ConnectionError::InvalidAddress(format!( - "Invalid Bluetooth Address '{}' (expected length 17)", - bdaddr + "Invalid Bluetooth Address '{}' (must have 6 segments)", + bdaddr, ))); } - for (index, c) in bdaddr.chars().enumerate() { - if (index + 1) % 3 == 0 { - if c != ':' { - return Err(ConnectionError::InvalidAddress(format!( - "Invalid Bluetooth Address '{}' (expected ':', found {})", - bdaddr, c - ))); - } - } else if !c.is_ascii_hexdigit() { + + for part in parts { + if part.len() != 2 { return Err(ConnectionError::InvalidAddress(format!( - "Invalid Bluetooth Address '{}' ('{}' is not a hex digit)", - bdaddr, c + "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(()) }