rust/bt-common: Migrate to strongly typed Address

Define Address as a strongly typed struct wrapping `[u8; 6]` with
convenience helpers and constants to make interactions cleaner and
safer.

Update crates that currently use Address with the modified version.

Update bt-bass to use strongly typed Address instead of using raw bytes.

Bug: 542348813
Test: ./presubmit.sh
Change-Id: Ie1048a177100196edfeb14252d04bc076df39107
Reviewed-on: https://bluetooth-review.googlesource.com/c/bluetooth/+/3560
diff --git a/rust/bt-bass/src/client.rs b/rust/bt-bass/src/client.rs
index 9532581..1a1122f 100644
--- a/rust/bt-bass/src/client.rs
+++ b/rust/bt-bass/src/client.rs
@@ -14,7 +14,7 @@
 use parking_lot::Mutex;
 
 use bt_bap::types::{BroadcastCode, BroadcastId};
-use bt_common::core::{AddressType, AdvertisingSetId, PeriodicAdvertisingInterval};
+use bt_common::core::{Address, AddressType, AdvertisingSetId, PeriodicAdvertisingInterval};
 use bt_common::generic_audio::metadata_ltv::Metadata;
 use bt_common::packet_encoding::Decodable;
 use bt_gatt::client::{CharacteristicNotification, PeerService, ServiceCharacteristic};
@@ -267,7 +267,7 @@
         &self,
         broadcast_id: BroadcastId,
         address_type: AddressType,
-        advertiser_address: [u8; ADDRESS_BYTE_SIZE],
+        advertiser_address: Address,
         sid: AdvertisingSetId,
         pa_sync: PaSync,
         pa_interval: PeriodicAdvertisingInterval,
@@ -736,7 +736,7 @@
         let op_fut = client.add_broadcast_source(
             BroadcastId::try_from(0x11).unwrap(),
             AddressType::Public,
-            [0x04, 0x10, 0x00, 0x00, 0x00, 0x00],
+            Address::new([0x04, 0x10, 0x00, 0x00, 0x00, 0x00]),
             AdvertisingSetId::try_from(1).unwrap(),
             PaSync::DoNotSync,
             PeriodicAdvertisingInterval::unknown(),
@@ -758,7 +758,7 @@
             BroadcastReceiveState::NonEmpty(ReceiveState {
                 source_id: 0x11,
                 source_address_type: AddressType::Public,
-                source_address: [1, 2, 3, 4, 5, 6],
+                source_address: Address::new([1, 2, 3, 4, 5, 6]),
                 source_adv_sid: AdvertisingSetId::try_from(1).unwrap(),
                 broadcast_id: BroadcastId::try_from(0x11).unwrap(),
                 pa_sync_state: PaSyncState::Synced,
@@ -800,7 +800,7 @@
             BroadcastReceiveState::NonEmpty(ReceiveState {
                 source_id: 0x11,
                 source_address_type: AddressType::Public,
-                source_address: [1, 2, 3, 4, 5, 6],
+                source_address: Address::new([1, 2, 3, 4, 5, 6]),
                 source_adv_sid: AdvertisingSetId::try_from(1).unwrap(),
                 broadcast_id: BroadcastId::try_from(0x11).unwrap(),
                 pa_sync_state: PaSyncState::Synced,
@@ -850,7 +850,7 @@
             BroadcastReceiveState::NonEmpty(ReceiveState {
                 source_id: 0x11,
                 source_address_type: AddressType::Public,
-                source_address: [1, 2, 3, 4, 5, 6],
+                source_address: Address::new([1, 2, 3, 4, 5, 6]),
                 source_adv_sid: AdvertisingSetId::try_from(1).unwrap(),
                 broadcast_id: BroadcastId::try_from(0x11).unwrap(),
                 pa_sync_state: PaSyncState::Synced,
@@ -921,7 +921,7 @@
             BroadcastReceiveState::NonEmpty(ReceiveState {
                 source_id: 0x11,
                 source_address_type: AddressType::Public,
-                source_address: [1, 2, 3, 4, 5, 6],
+                source_address: Address::new([1, 2, 3, 4, 5, 6]),
                 source_adv_sid: AdvertisingSetId::try_from(1).unwrap(),
                 broadcast_id: bid,
                 pa_sync_state: PaSyncState::Synced,
@@ -964,7 +964,7 @@
             BroadcastReceiveState::NonEmpty(ReceiveState {
                 source_id: 0x01,
                 source_address_type: AddressType::Public,
-                source_address: [1, 2, 3, 4, 5, 6],
+                source_address: Address::new([1, 2, 3, 4, 5, 6]),
                 source_adv_sid: AdvertisingSetId::try_from(1).unwrap(),
                 broadcast_id: BroadcastId::try_from(0x030201).unwrap(),
                 pa_sync_state: PaSyncState::Synced,
diff --git a/rust/bt-bass/src/server.rs b/rust/bt-bass/src/server.rs
index 0d9f529..29a20b8 100644
--- a/rust/bt-bass/src/server.rs
+++ b/rust/bt-bass/src/server.rs
@@ -714,7 +714,7 @@
     use assert_matches::assert_matches;
     use bt_bap::types::BroadcastId;
 
-    use bt_common::core::{AddressType, AdvertisingSetId, PeriodicAdvertisingInterval};
+    use bt_common::core::{Address, AddressType, AdvertisingSetId, PeriodicAdvertisingInterval};
     use bt_common::generic_audio::metadata_ltv::Metadata;
     use bt_gatt::test_utils::{FakeServer, FakeServerEvent, FakeTypes};
     use bt_gatt::types::GattError;
@@ -729,7 +729,7 @@
         ReceiveState::new(
             source_id,
             AddressType::Public,
-            [0x01, 0x02, 0x03, 0x04, 0x05, 0x06],
+            Address::new([0x01, 0x02, 0x03, 0x04, 0x05, 0x06]),
             AdvertisingSetId::try_from(1).unwrap(),
             BroadcastId::try_from(0x123456).unwrap(),
             PaSyncState::NotSynced,
@@ -1031,7 +1031,7 @@
         // 1. Remote client writes AddSource to Control Point (Handle 1)
         let add_op = AddSourceOperation::new(
             AddressType::Public,
-            [1, 2, 3, 4, 5, 6],
+            Address::new([1, 2, 3, 4, 5, 6]),
             AdvertisingSetId::try_from(1).unwrap(),
             BroadcastId::try_from(0x123456).unwrap(),
             PaSync::SyncPastAvailable,
diff --git a/rust/bt-bass/src/types.rs b/rust/bt-bass/src/types.rs
index bd37c5a..a1c7ebe 100644
--- a/rust/bt-bass/src/types.rs
+++ b/rust/bt-bass/src/types.rs
@@ -4,14 +4,13 @@
 
 use bt_bap::types::{BroadcastCode, BroadcastId};
 use bt_common::core::ltv::LtValue;
-use bt_common::core::{AddressType, AdvertisingSetId, PeriodicAdvertisingInterval};
+use bt_common::core::{Address, AddressType, AdvertisingSetId, PeriodicAdvertisingInterval};
 use bt_common::generic_audio::metadata_ltv::*;
 use bt_common::packet_encoding::{Decodable, Encodable, Error as PacketError};
 use bt_common::{decodable_enum, Uuid};
 use bt_gatt::types::GattError;
 use std::str::FromStr;
 
-pub const ADDRESS_BYTE_SIZE: usize = 6;
 const NUM_SUBGROUPS_BYTE_SIZE: usize = 1;
 const PA_SYNC_BYTE_SIZE: usize = 1;
 const SOURCE_ID_BYTE_SIZE: usize = 1;
@@ -158,7 +157,7 @@
 pub struct AddSourceOperation {
     pub(crate) advertiser_address_type: AddressType,
     // Address in little endian.
-    pub(crate) advertiser_address: [u8; ADDRESS_BYTE_SIZE],
+    pub(crate) advertiser_address: Address,
     pub(crate) advertising_sid: AdvertisingSetId,
     pub(crate) broadcast_id: BroadcastId,
     pub(crate) pa_sync: PaSync,
@@ -169,7 +168,7 @@
 impl AddSourceOperation {
     const MIN_PACKET_SIZE: usize = ControlPointOpcode::BYTE_SIZE
         + AddressType::BYTE_SIZE
-        + ADDRESS_BYTE_SIZE
+        + Address::BYTE_SIZE
         + AdvertisingSetId::BYTE_SIZE
         + BroadcastId::BYTE_SIZE
         + PA_SYNC_BYTE_SIZE
@@ -178,7 +177,7 @@
 
     pub fn new(
         address_type: AddressType,
-        advertiser_address: [u8; ADDRESS_BYTE_SIZE],
+        advertiser_address: Address,
         advertising_sid: AdvertisingSetId,
         broadcast_id: BroadcastId,
         pa_sync: PaSync,
@@ -214,8 +213,7 @@
         let decode_fn = || {
             let _ = Self::check_opcode(buf[0])?;
             let advertiser_address_type = AddressType::try_from(buf[1])?;
-            let mut advertiser_address = [0; ADDRESS_BYTE_SIZE];
-            advertiser_address.clone_from_slice(&buf[2..8]);
+            let advertiser_address = Address::new(buf[2..8].try_into().unwrap());
             let advertising_sid = AdvertisingSetId::try_from(buf[8])?;
             let broadcast_id = BroadcastId::decode(&buf[9..12]).0?;
             let pa_sync = PaSync::try_from(buf[12])?;
@@ -264,7 +262,7 @@
 
         buf[0] = Self::opcode() as u8;
         buf[1] = self.advertiser_address_type as u8;
-        buf[2..8].copy_from_slice(&self.advertiser_address);
+        buf[2..8].copy_from_slice(self.advertiser_address.as_bytes());
         buf[8] = self.advertising_sid.value();
         self.broadcast_id.encode(&mut buf[9..12])?;
         buf[12] = u8::from(self.pa_sync);
@@ -757,7 +755,7 @@
     pub(crate) source_id: SourceId,
     pub(crate) source_address_type: AddressType,
     // Address in little endian.
-    pub(crate) source_address: [u8; ADDRESS_BYTE_SIZE],
+    pub(crate) source_address: Address,
     pub(crate) source_adv_sid: AdvertisingSetId,
     pub(crate) broadcast_id: BroadcastId,
     pub(crate) pa_sync_state: PaSyncState,
@@ -769,7 +767,7 @@
 impl ReceiveState {
     const MIN_PACKET_SIZE: usize = SOURCE_ID_BYTE_SIZE
         + AddressType::BYTE_SIZE
-        + ADDRESS_BYTE_SIZE
+        + Address::BYTE_SIZE
         + AdvertisingSetId::BYTE_SIZE
         + BroadcastId::BYTE_SIZE
         + PA_SYNC_BYTE_SIZE
@@ -779,7 +777,7 @@
     pub fn new(
         source_id: u8,
         source_address_type: AddressType,
-        source_address: [u8; ADDRESS_BYTE_SIZE],
+        source_address: Address,
         source_adv_sid: AdvertisingSetId,
         broadcast_id: BroadcastId,
         pa_sync_state: PaSyncState,
@@ -826,8 +824,7 @@
         let decode_fn = || {
             let source_id = buf[0];
             let source_address_type = AddressType::try_from(buf[1])?;
-            let mut source_address = [0; ADDRESS_BYTE_SIZE];
-            source_address.clone_from_slice(&buf[2..8]);
+            let source_address = Address::new(buf[2..8].try_into().unwrap());
             let source_adv_sid = AdvertisingSetId::try_from(buf[8])?;
             let broadcast_id = BroadcastId::decode(&buf[9..12]).0?;
             let pa_sync_state = PaSyncState::try_from(buf[12])?;
@@ -888,7 +885,7 @@
 
         buf[0] = self.source_id;
         buf[1] = self.source_address_type as u8;
-        buf[2..8].copy_from_slice(&self.source_address);
+        buf[2..8].copy_from_slice(self.source_address.as_bytes());
         buf[8] = self.source_adv_sid.value();
         self.broadcast_id.encode(&mut buf[9..12])?;
         buf[12] = u8::from(self.pa_sync_state);
@@ -913,7 +910,7 @@
         // Num_Subgroups and subgroup-related params.
         SOURCE_ID_BYTE_SIZE
             + AddressType::BYTE_SIZE
-            + self.source_address.len()
+            + Address::BYTE_SIZE
             + AdvertisingSetId::BYTE_SIZE
             + self.broadcast_id.encoded_len()
             + PA_SYNC_BYTE_SIZE
@@ -1200,7 +1197,7 @@
         // Encoding operation with no subgroups.
         let op = AddSourceOperation::new(
             AddressType::Public,
-            [0x04, 0x10, 0x00, 0x00, 0x00, 0x00],
+            Address::new([0x04, 0x10, 0x00, 0x00, 0x00, 0x00]),
             AdvertisingSetId::try_from(1).unwrap(),
             BroadcastId::try_from(0x11).unwrap(),
             PaSync::DoNotSync,
@@ -1232,7 +1229,7 @@
         ])];
         let op = AddSourceOperation::new(
             AddressType::Random,
-            [0x04, 0x10, 0x00, 0x00, 0x00, 0x00],
+            Address::new([0x04, 0x10, 0x00, 0x00, 0x00, 0x00]),
             AdvertisingSetId::try_from(1).unwrap(),
             BroadcastId::try_from(0x11).unwrap(),
             PaSync::SyncPastAvailable,
@@ -1378,7 +1375,7 @@
         let state = BroadcastReceiveState::NonEmpty(ReceiveState {
             source_id: 0x01,
             source_address_type: AddressType::Public,
-            source_address: [0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A],
+            source_address: Address::new([0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A]),
             source_adv_sid: AdvertisingSetId::try_from(0x01).unwrap(),
             broadcast_id: BroadcastId::try_from(0x00010203).unwrap(),
             pa_sync_state: PaSyncState::Synced,
@@ -1412,7 +1409,7 @@
         let state = BroadcastReceiveState::NonEmpty(ReceiveState {
             source_id: 0x01,
             source_address_type: AddressType::Random,
-            source_address: [0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A],
+            source_address: Address::new([0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A]),
             source_adv_sid: AdvertisingSetId::try_from(0x01).unwrap(),
             broadcast_id: BroadcastId::try_from(0x00010203).unwrap(),
             pa_sync_state: PaSyncState::NotSynced,
diff --git a/rust/bt-broadcast-assistant/src/assistant.rs b/rust/bt-broadcast-assistant/src/assistant.rs
index fe6e6a2..b75d5ef 100644
--- a/rust/bt-broadcast-assistant/src/assistant.rs
+++ b/rust/bt-broadcast-assistant/src/assistant.rs
@@ -206,7 +206,7 @@
     pub fn force_discover_broadcast_source(
         &self,
         peer_id: PeerId,
-        address: [u8; 6],
+        address: bt_common::core::Address,
         address_type: bt_common::core::AddressType,
         advertising_sid: bt_common::core::AdvertisingSetId,
     ) -> Result<BroadcastSource, Error> {
@@ -281,7 +281,7 @@
     use std::task::Poll;
 
     use bt_bap::types::*;
-    use bt_common::core::{AddressType, AdvertisingSetId, PeriodicAdvertisingInterval};
+    use bt_common::core::{Address, AddressType, AdvertisingSetId, PeriodicAdvertisingInterval};
     use bt_common::generic_audio::metadata_ltv::Metadata;
     use bt_gatt::test_utils::{FakeCentral, FakeClient, FakeTypes};
 
@@ -297,7 +297,7 @@
         let (bs, changed) = discovered.merge_broadcast_source_data(
             &key1,
             &BroadcastSource::default()
-                .with_address([1, 2, 3, 4, 5, 6])
+                .with_address(Address::new([1, 2, 3, 4, 5, 6]))
                 .with_address_type(AddressType::Public)
                 .with_broadcast_id(bid1),
         );
@@ -305,7 +305,7 @@
         assert_eq!(
             bs,
             BroadcastSource {
-                address: Some([1, 2, 3, 4, 5, 6]),
+                address: Some(Address::new([1, 2, 3, 4, 5, 6])),
                 address_type: Some(AddressType::Public),
                 broadcast_id: Some(bid1),
                 periodic_advertising_interval: None,
@@ -329,7 +329,7 @@
         assert_eq!(
             bs,
             BroadcastSource {
-                address: Some([1, 2, 3, 4, 5, 6]),
+                address: Some(Address::new([1, 2, 3, 4, 5, 6])),
                 address_type: Some(AddressType::Random),
                 broadcast_id: Some(bid1),
                 periodic_advertising_interval: Some(PeriodicAdvertisingInterval(0x0100)),
@@ -357,7 +357,7 @@
         let (bs, changed) = discovered.merge_broadcast_source_data(
             &key2,
             &BroadcastSource::default()
-                .with_address([1, 2, 3, 4, 5, 6])
+                .with_address(Address::new([1, 2, 3, 4, 5, 6]))
                 .with_address_type(AddressType::Public)
                 .with_broadcast_id(bid2),
         );
@@ -365,7 +365,7 @@
         assert_eq!(
             bs,
             BroadcastSource {
-                address: Some([1, 2, 3, 4, 5, 6]),
+                address: Some(Address::new([1, 2, 3, 4, 5, 6])),
                 address_type: Some(AddressType::Public),
                 broadcast_id: Some(bid2),
                 periodic_advertising_interval: None,
@@ -425,7 +425,7 @@
     fn force_discover_broadcast_source_test() {
         let assistant = BroadcastAssistant::<FakeTypes>::new(FakeCentral::new());
         let peer_id = PeerId(1);
-        let address = [1, 2, 3, 4, 5, 6];
+        let address = Address::new([1, 2, 3, 4, 5, 6]);
         let address_type = AddressType::Public;
         let sid = AdvertisingSetId::try_from(1).unwrap();
 
diff --git a/rust/bt-broadcast-assistant/src/assistant/event.rs b/rust/bt-broadcast-assistant/src/assistant/event.rs
index 2240a36..c679759 100644
--- a/rust/bt-broadcast-assistant/src/assistant/event.rs
+++ b/rust/bt-broadcast-assistant/src/assistant/event.rs
@@ -370,7 +370,7 @@
 
     use assert_matches::assert_matches;
 
-    use bt_common::core::{AddressType, AdvertisingSetId};
+    use bt_common::core::{Address, AddressType, AdvertisingSetId};
     use bt_gatt::central::{AdvertisingDatum, PeerName};
     use bt_gatt::test_utils::{
         FakePeriodicAdvertising, FakeTypes, ScannedResultStream, ScannedResultStreamController,
@@ -432,7 +432,7 @@
         let _ = stream.broadcast_sources.merge_broadcast_source_data(
             &(broadcast_source_pid, AdvertisingSetId::try_from(1).unwrap()),
             &BroadcastSource::default()
-                .with_address([1, 2, 3, 4, 5, 6])
+                .with_address(Address::new([1, 2, 3, 4, 5, 6]))
                 .with_address_type(AddressType::Public),
         );
 
@@ -481,7 +481,7 @@
             assert_eq!(peer, broadcast_source_pid);
             assert_eq!(advertising_sid, AdvertisingSetId::try_from(1).unwrap());
             assert_eq!(source.periodic_advertising_interval, Some(PeriodicAdvertisingInterval(0x0100)));
-            assert_eq!(source.address, Some([1, 2, 3, 4, 5, 6]));
+            assert_eq!(source.address, Some(Address::new([1, 2, 3, 4, 5, 6])));
             assert_eq!(source.broadcast_name, Some("Test Broadcast".to_string()));
         });
 
diff --git a/rust/bt-broadcast-assistant/src/assistant/peer.rs b/rust/bt-broadcast-assistant/src/assistant/peer.rs
index 8c5860d..c4a4d10 100644
--- a/rust/bt-broadcast-assistant/src/assistant/peer.rs
+++ b/rust/bt-broadcast-assistant/src/assistant/peer.rs
@@ -206,7 +206,7 @@
     use std::collections::HashMap;
     use std::task::Poll;
 
-    use bt_common::core::{AddressType, AdvertisingSetId};
+    use bt_common::core::{Address, AddressType, AdvertisingSetId};
     use bt_gatt::test_utils::{FakeClient, FakeGetPeerAddr, FakePeerService, FakeTypes};
     use bt_gatt::types::{
         AttributePermissions, CharacteristicProperties, CharacteristicProperty, Handle,
@@ -304,8 +304,11 @@
 
         // Should fail because peer address couldn't be looked up.
         {
-            let address_lookup =
-                StaticPeerAddr::new_for_peer(PeerId(1002), [1, 2, 3, 4, 5, 6], AddressType::Public);
+            let address_lookup = StaticPeerAddr::new_for_peer(
+                PeerId(1002),
+                Address::new([1, 2, 3, 4, 5, 6]),
+                AddressType::Public,
+            );
             let fut = peer.add_broadcast_source(
                 PeerId(1001),
                 AdvertisingSetId::try_from(1).unwrap(),
@@ -320,8 +323,11 @@
 
         // Should fail because not enough information.
         {
-            let address_lookup =
-                StaticPeerAddr::new_for_peer(PeerId(1001), [1, 2, 3, 4, 5, 6], AddressType::Public);
+            let address_lookup = StaticPeerAddr::new_for_peer(
+                PeerId(1001),
+                Address::new([1, 2, 3, 4, 5, 6]),
+                AddressType::Public,
+            );
             let fut = peer.add_broadcast_source(
                 PeerId(1001),
                 AdvertisingSetId::try_from(1).unwrap(),
diff --git a/rust/bt-broadcast-assistant/src/debug.rs b/rust/bt-broadcast-assistant/src/debug.rs
index b9d933c..032cd44 100644
--- a/rust/bt-broadcast-assistant/src/debug.rs
+++ b/rust/bt-broadcast-assistant/src/debug.rs
@@ -9,9 +9,9 @@
 
 #[cfg(any(test, feature = "debug"))]
 use bt_common::core::ltv::LtValue;
-#[cfg(any(test, feature = "debug"))]
-use bt_common::core::AddressType;
 use bt_common::core::AdvertisingSetId;
+#[cfg(any(test, feature = "debug"))]
+use bt_common::core::{Address, AddressType};
 use bt_common::debug_command::CommandRunner;
 use bt_common::debug_command::CommandSet;
 use bt_common::gen_commandset;
@@ -133,14 +133,8 @@
 
 #[cfg(any(test, feature = "debug"))]
 /// Returns the bd address in little endian ordering.
-pub fn parse_bd_addr(input: &str) -> Result<[u8; 6], String> {
-    let mut tokens: Vec<u8> =
-        input.split(':').map(|t| u8::from_str_radix(t, 16)).filter_map(Result::ok).collect();
-    if tokens.len() != 6 {
-        return Err(format!("failed to parse bd address from {input}"));
-    }
-    tokens.reverse();
-    tokens.try_into().map_err(|e| format!("{e:?}"))
+pub fn parse_bd_addr(input: &str) -> Result<Address, String> {
+    input.parse::<Address>().map_err(|e| format!("{e:?}"))
 }
 
 fn parse_broadcast_id(input: &str) -> Result<BroadcastId, String> {
@@ -550,7 +544,7 @@
     fn test_parse_bd_addr() {
         assert_eq!(
             parse_bd_addr("3c:80:f1:ed:32:2c").expect("should be ok"),
-            [0x2c, 0x32, 0xed, 0xf1, 0x80, 0x3c]
+            Address::new([0x2c, 0x32, 0xed, 0xf1, 0x80, 0x3c])
         );
         // Address with 5 parts is invalid.
         let _ = parse_bd_addr("3c:80:f1:ed:32").expect_err("should fail");
diff --git a/rust/bt-broadcast-assistant/src/types.rs b/rust/bt-broadcast-assistant/src/types.rs
index e6d77d4..021d86b 100644
--- a/rust/bt-broadcast-assistant/src/types.rs
+++ b/rust/bt-broadcast-assistant/src/types.rs
@@ -33,7 +33,7 @@
         self.broadcast_id.is_some() && self.endpoint.is_some()
     }
 
-    pub fn with_address(&mut self, address: [u8; 6]) -> &mut Self {
+    pub fn with_address(&mut self, address: Address) -> &mut Self {
         self.address = Some(address);
         self
     }
diff --git a/rust/bt-common/src/core.rs b/rust/bt-common/src/core.rs
index 8076264..91ec9dc 100644
--- a/rust/bt-common/src/core.rs
+++ b/rust/bt-common/src/core.rs
@@ -11,7 +11,58 @@
 /// Bluetooth Device Address that uniquely identifies the device
 /// to another Bluetooth device.
 /// See Core spec v5.3 Vol 2, Part B section 1.2.
-pub type Address = [u8; 6];
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct Address([u8; 6]);
+
+impl Address {
+    pub const BYTE_SIZE: usize = 6;
+
+    pub const fn new(bytes: [u8; 6]) -> Self {
+        Self(bytes)
+    }
+
+    pub const fn as_bytes(&self) -> &[u8; 6] {
+        &self.0
+    }
+}
+
+impl From<[u8; 6]> for Address {
+    fn from(bytes: [u8; 6]) -> Self {
+        Self(bytes)
+    }
+}
+
+impl std::fmt::Display for Address {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}",
+            self.0[5], self.0[4], self.0[3], self.0[2], self.0[1], self.0[0]
+        )
+    }
+}
+
+impl FromStr for Address {
+    type Err = PacketError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        let parts: Vec<&str> = s.split(':').collect();
+        if parts.len() != 6 {
+            return Err(PacketError::InvalidParameter(format!("invalid address format: {s}")));
+        }
+        let mut bytes = [0u8; 6];
+        for (i, part) in parts.iter().enumerate() {
+            if part.is_empty() {
+                return Err(PacketError::InvalidParameter(format!("invalid address format: {s}")));
+            }
+            let byte = u8::from_str_radix(part, 16).map_err(|_| {
+                PacketError::InvalidParameter(format!("invalid byte in address: {part}"))
+            })?;
+            bytes[5 - i] = byte;
+        }
+        Ok(Self(bytes))
+    }
+}
 
 /// See Core spec v5.3 Vol 3, Part C section 15.1.1.
 #[repr(u8)]
@@ -331,4 +382,29 @@
         assert_eq!(AdvertisingSetId::try_from(0x10), Err(PacketError::OutOfRange));
         assert_eq!(AdvertisingSetId::try_from(0xFF), Err(PacketError::OutOfRange));
     }
+
+    #[test]
+    fn address_display() {
+        let addr = Address::new([0x2C, 0x32, 0xED, 0xF1, 0x80, 0x3C]);
+        assert_eq!(addr.to_string(), "3C:80:F1:ED:32:2C");
+    }
+
+    #[test]
+    fn address_from_str_success() {
+        let addr = Address::from_str("3C:80:F1:ED:32:2C").expect("valid address");
+        assert_eq!(addr.as_bytes(), &[0x2C, 0x32, 0xED, 0xF1, 0x80, 0x3C]);
+
+        let lowercase = Address::from_str("3c:80:f1:ed:32:2c").expect("valid address");
+        assert_eq!(lowercase, addr);
+    }
+
+    #[test]
+    fn address_from_str_failure() {
+        assert!(Address::from_str("3c:80:f1:ed:32").is_err());
+        assert!(Address::from_str("3c:80:f1::32:2c").is_err());
+        assert!(Address::from_str(":80:f1:ed:32:2c").is_err());
+        assert!(Address::from_str("3c:80:f1:ed:32:").is_err());
+        assert!(Address::from_str("3c.80.f1.ed.32.2c").is_err());
+        assert!(Address::from_str("3c:80:f1:ed:32:gg").is_err());
+    }
 }
diff --git a/rust/bt-gatt/src/test_utils.rs b/rust/bt-gatt/src/test_utils.rs
index c31ffab..858362b 100644
--- a/rust/bt-gatt/src/test_utils.rs
+++ b/rust/bt-gatt/src/test_utils.rs
@@ -299,17 +299,8 @@
 
 impl GetPeerAddr for FakeGetPeerAddr {
     async fn get_peer_address(&self, peer_id: PeerId) -> Result<(Address, AddressType)> {
-        Ok((
-            [
-                peer_id.0 as u8,
-                ((peer_id.0 >> 8) & 0xff) as u8,
-                ((peer_id.0 >> 16) & 0xff) as u8,
-                ((peer_id.0 >> 24) & 0xff) as u8,
-                ((peer_id.0 >> 32) & 0xff) as u8,
-                ((peer_id.0 >> 48) & 0xff) as u8,
-            ],
-            AddressType::Public,
-        ))
+        let bytes: [u8; 6] = peer_id.0.to_le_bytes()[..6].try_into().unwrap();
+        Ok((Address::new(bytes), AddressType::Public))
     }
 }