Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion nmrs/examples/bluetooth_connect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions nmrs/src/api/builders/bluetooth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
//! let bt_settings = BluetoothIdentity::new(
//! "00:1A:7D:DA:71:13".into(),
//! BluetoothNetworkRole::PanU,
//! );
//! ).unwrap();
//! ```

use std::collections::HashMap;
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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);

Expand Down
31 changes: 24 additions & 7 deletions nmrs/src/api/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)]
Expand All @@ -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
Expand All @@ -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<Self, ConnectionError> {
validate_bluetooth_address(&bdaddr)?;
Ok(Self {
bdaddr,
bt_device_type,
}
})
}
}

Expand Down Expand Up @@ -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!(
Expand All @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion nmrs/src/api/network_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
Expand Down
10 changes: 8 additions & 2 deletions nmrs/src/core/bluetooth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -32,13 +33,18 @@ 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.)
pub(crate) async fn populate_bluez_info(
conn: &Connection,
bdaddr: &str,
) -> Result<(Option<String>, Option<String>)> {
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.
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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!(
Expand Down
60 changes: 60 additions & 0 deletions nmrs/src/util/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's best we split and validate segments (I.e. a Vec of &str's for parts and then match on length)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modified the impl to check on each segment after splitting.

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::*;
Expand Down Expand Up @@ -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());
}
}
3 changes: 2 additions & 1 deletion nmrs/tests/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand Down
Loading