rust/bt-common: Add interactive debug traits

Define two debug traits:
 - CommandSet trait allows a set of commands to be provided
 - CommandRunner provides a way to run those commands

Intended to be used for interactive debugging and testing.
Add commands for bt-pacs.

Add FromCharacteristic::try_read, attempting to read and decode data from a
characteristic if it matches the UUID of the type.

Bug: fxbug.dev/308483257

Change-Id: I56062f0604a05b94c8579f17b07991cf9b452c08
Reviewed-on: https://bluetooth-review.git.corp.google.com/c/bluetooth/+/1460
Reviewed-by: Ani Ramakrishnan <aniramakri@google.com>
Reviewed-by: Dayeong Lee <dayeonglee@google.com>
diff --git a/rust/bt-common/Cargo.toml b/rust/bt-common/Cargo.toml
index 3006f41..67db0da 100644
--- a/rust/bt-common/Cargo.toml
+++ b/rust/bt-common/Cargo.toml
@@ -8,3 +8,4 @@
 thiserror.workspace = true
 lazy_static.workspace = true
 uuid.workspace = true
+futures.workspace = true
diff --git a/rust/bt-common/src/debug_command.rs b/rust/bt-common/src/debug_command.rs
new file mode 100644
index 0000000..54ea04a
--- /dev/null
+++ b/rust/bt-common/src/debug_command.rs
@@ -0,0 +1,143 @@
+// Copyright 2023 Google LLC
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+///! Debug command traits and helpers for defining commands for integration
+/// into a debug tool.
+use std::str::FromStr;
+
+/// A CommandSet is a set of commands (usually an enum) that each represent an
+/// action that can be performed.  i.e. 'list', 'volume' etc.  Each command can
+/// take zero or more arguments and have zero or more flags.
+/// Typically an Enum of commands would implement CommandSet trait.
+pub trait CommandSet: FromStr + ::core::fmt::Display {
+    /// Returns a vector of strings that are the commands supported by this.
+    fn variants() -> Vec<String>;
+
+    /// Returns a string listing the arguments that this command takes, in <>
+    /// brackets
+    fn arguments(&self) -> &'static str;
+
+    /// Returns a string displaying the flags that this command supports, in []
+    /// brackets
+    fn flags(&self) -> &'static str;
+
+    /// Returns a short description of this command
+    fn desc(&self) -> &'static str;
+
+    /// Help string for this variant (build from Display, arguments and flags by
+    /// default)
+    fn help_simple(&self) -> String {
+        format!("{self} {} {} -- {}", self.flags(), self.arguments(), self.desc())
+    }
+
+    /// Possibly multi-line help string for all variants of this set.
+    fn help_all() -> String {
+        Self::variants()
+            .into_iter()
+            .filter_map(|s| FromStr::from_str(&s).ok())
+            .map(|s: Self| format!("{}\n", s))
+            .collect()
+    }
+}
+
+/// Macro to help build CommandSets
+#[macro_export]
+macro_rules! gen_commandset {
+    ($name:ident {
+        $($variant:ident = ($val:expr, [$($flag:expr),*], [$($arg:expr),*], $help:expr)),*,
+    }) => {
+        /// Enum of all possible commands
+        #[derive(PartialEq, Debug)]
+        pub enum $name {
+            $($variant),*
+        }
+
+        impl CommandSet for $name {
+            fn variants() -> Vec<String> {
+                let mut variants = Vec::new();
+                $(variants.push($val.to_string());)*
+                variants
+            }
+
+            fn arguments(&self) -> &'static str {
+                match self {
+                    $(
+                        $name::$variant => concat!($("<", $arg, "> ",)*)
+                    ),*
+                }
+            }
+
+            fn flags(&self) -> &'static str {
+                match self {
+                    $(
+                        $name::$variant => concat!($("[", $flag, "] ",)*)
+                    ),*
+                }
+            }
+
+            fn desc(&self) -> &'static str {
+                match self {
+                    $(
+                        $name::$variant => $help
+                    ),*
+                }
+            }
+        }
+
+        impl ::core::fmt::Display for $name {
+            fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
+                match *self {
+                    $($name::$variant => write!(f, $val)),* ,
+                }
+            }
+        }
+
+        impl ::std::str::FromStr for $name {
+            type Err = ();
+
+            fn from_str(s: &str) -> Result<$name, ()> {
+                match s {
+                    $($val => Ok($name::$variant)),* ,
+                    _ => Err(()),
+                }
+            }
+        }
+    }
+}
+
+/// CommandRunner is used to perform a specific task based on the
+pub trait CommandRunner {
+    type Set: CommandSet;
+    fn run(
+        &self,
+        cmd: Self::Set,
+        args: Vec<String>,
+    ) -> impl futures::Future<Output = Result<(), impl ::std::error::Error>>;
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn gen_commandset_simple() {
+        gen_commandset! {
+            TestCmd {
+                One = ("one", [], [], "First Command"),
+                WithFlags = ("with-flags", ["-1","-2"], [], "Command with flags"),
+                WithArgs = ("with-args", [], ["arg", "two"], "Command with args"),
+                WithBoth = ("with-both", ["-w"], ["simple"], "Command with both flags and args"),
+            }
+        }
+
+        let cmd: TestCmd = "one".parse().unwrap();
+
+        assert_eq!(cmd, TestCmd::One);
+
+        let cmd2: TestCmd = "with-flags".parse().unwrap();
+
+        assert_eq!(cmd2.arguments(), "");
+        assert_eq!(cmd2.flags(), "[-1] [-2] ");
+    }
+}
diff --git a/rust/bt-common/src/lib.rs b/rust/bt-common/src/lib.rs
index 2956e3e..382567d 100644
--- a/rust/bt-common/src/lib.rs
+++ b/rust/bt-common/src/lib.rs
@@ -36,3 +36,5 @@
 
 pub mod uuids;
 pub use crate::uuids::Uuid;
+
+pub mod debug_command;
diff --git a/rust/bt-gatt/src/client.rs b/rust/bt-gatt/src/client.rs
index d27a448..94af837 100644
--- a/rust/bt-gatt/src/client.rs
+++ b/rust/bt-gatt/src/client.rs
@@ -42,6 +42,38 @@
         &mut self,
         new_value: &[u8],
     ) -> ::core::result::Result<&mut Self, bt_common::packet_encoding::Error>;
+
+    /// Attempt to read a characteristic if it matches the provided
+    /// characteristic UUID.
+    fn try_read<T: crate::GattTypes>(
+        characteristic: Characteristic,
+        service: &T::PeerService,
+    ) -> impl futures::Future<Output = ::core::result::Result<Self, Error>> {
+        async move {
+            if characteristic.uuid != Self::UUID {
+                return Err(Error::ScanFailed("Wrong UUID".to_owned()));
+            }
+            let mut buf = [0; 128];
+            let (bytes, mut truncated) =
+                service.read_characteristic(&characteristic.handle, 0, &mut buf).await?;
+            let mut vec;
+            let buf_ptr = if truncated {
+                vec = Vec::with_capacity(bytes);
+                vec.copy_from_slice(&buf[..bytes]);
+                while truncated {
+                    let (bytes, still_truncated) = service
+                        .read_characteristic(&characteristic.handle, vec.len() as u16, &mut buf)
+                        .await?;
+                    vec.extend_from_slice(&buf[..bytes]);
+                    truncated = still_truncated;
+                }
+                &vec[..]
+            } else {
+                &buf[..bytes]
+            };
+            Self::from_chr(characteristic, buf_ptr).map_err(Into::into)
+        }
+    }
 }
 
 #[derive(Debug, Clone)]
diff --git a/rust/bt-gatt/src/types.rs b/rust/bt-gatt/src/types.rs
index 3e3dc06..fdc9cfd 100644
--- a/rust/bt-gatt/src/types.rs
+++ b/rust/bt-gatt/src/types.rs
@@ -151,6 +151,14 @@
     DuplicateHandle(Vec<Handle>),
     #[error("service for {0:?} already published")]
     AlreadyPublished(crate::server::ServiceId),
+    #[error("Encoding/decoding error: {0}")]
+    Encoding(#[from] bt_common::packet_encoding::Error),
+}
+
+impl Error {
+    pub fn other(e: impl std::error::Error + Send + Sync + 'static) -> Self {
+        Self::Other(Box::new(e))
+    }
 }
 
 impl From<String> for Error {
diff --git a/rust/bt-pacs/Cargo.toml b/rust/bt-pacs/Cargo.toml
index 0fd767d..52d4737 100644
--- a/rust/bt-pacs/Cargo.toml
+++ b/rust/bt-pacs/Cargo.toml
@@ -10,5 +10,8 @@
 bt-gatt.workspace = true
 bt-common.workspace = true
 
+### Others
+futures.workspace = true
+
 [dev-dependencies]
 pretty_assertions.workspace = true
diff --git a/rust/bt-pacs/src/debug.rs b/rust/bt-pacs/src/debug.rs
new file mode 100644
index 0000000..f5ab520
--- /dev/null
+++ b/rust/bt-pacs/src/debug.rs
@@ -0,0 +1,92 @@
+// Copyright 2023 Google LLC
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+use bt_common::debug_command::CommandRunner;
+use bt_common::debug_command::CommandSet;
+use bt_common::gen_commandset;
+
+use bt_gatt::{
+    client::{PeerService, PeerServiceHandle},
+    Client,
+};
+
+use crate::*;
+
+gen_commandset! {
+    PacsCmd {
+        Print = ("print", [], [], "Print the current PACS status"),
+    }
+}
+
+pub struct PacsDebug<T: bt_gatt::GattTypes> {
+    client: T::Client,
+}
+
+impl<T: bt_gatt::GattTypes> PacsDebug<T> {
+    pub fn new(client: T::Client) -> Self {
+        Self { client }
+    }
+}
+
+impl<T: bt_gatt::GattTypes> CommandRunner for PacsDebug<T> {
+    type Set = PacsCmd;
+
+    fn run(
+        &self,
+        _cmd: Self::Set,
+        _args: Vec<String>,
+    ) -> impl futures::Future<Output = Result<(), impl std::error::Error>> {
+        async {
+            // Since there is only one command, Print, we just print
+            // all the characteristics that are at the remote PACS server.
+            let handles = self.client.find_service(PACS_UUID).await?;
+            for handle in handles {
+                let service = handle.connect().await?;
+
+                let chrs = service.discover_characteristics(None).await?;
+                for chr in chrs {
+                    let mut buf = [0; 120];
+                    match chr.uuid {
+                        SourcePac::UUID => {
+                            let source_pac = SourcePac::try_read::<T>(chr, &service).await?;
+                            println!("{source_pac:?}");
+                        }
+                        SinkPac::UUID => {
+                            let sink_pac = SinkPac::try_read::<T>(chr, &service).await?;
+                            println!("{sink_pac:?}");
+                        }
+                        SinkAudioLocations::UUID => {
+                            let locations =
+                                SinkAudioLocations::try_read::<T>(chr, &service).await?;
+                            println!("{locations:?}");
+                        }
+                        SourceAudioLocations::UUID => {
+                            let (bytes, _trunc) =
+                                service.read_characteristic(&chr.handle, 0, &mut buf).await?;
+                            let locations = SourceAudioLocations::from_chr(chr, &buf[..bytes])
+                                .map_err(bt_gatt::types::Error::other)?;
+                            println!("{locations:?}");
+                        }
+                        AvailableAudioContexts::UUID => {
+                            let (bytes, _trunc) =
+                                service.read_characteristic(&chr.handle, 0, &mut buf).await?;
+                            let contexts = AvailableAudioContexts::from_chr(chr, &buf[..bytes])
+                                .map_err(bt_gatt::types::Error::other)?;
+                            println!("{contexts:?}");
+                        }
+                        SupportedAudioContexts::UUID => {
+                            let (bytes, _trunc) =
+                                service.read_characteristic(&chr.handle, 0, &mut buf).await?;
+                            let contexts = SupportedAudioContexts::from_chr(chr, &buf[..bytes])
+                                .map_err(bt_gatt::types::Error::other)?;
+                            println!("{contexts:?}");
+                        }
+                        _x => println!("Unrecognized Chr {}", chr.uuid.recognize()),
+                    }
+                }
+            }
+            Ok::<(), bt_gatt::types::Error>(())
+        }
+    }
+}
diff --git a/rust/bt-pacs/src/lib.rs b/rust/bt-pacs/src/lib.rs
index 38efa40..e926716 100644
--- a/rust/bt-pacs/src/lib.rs
+++ b/rust/bt-pacs/src/lib.rs
@@ -11,6 +11,11 @@
 use bt_common::Uuid;
 use bt_gatt::{client::FromCharacteristic, Characteristic};
 
+pub mod debug;
+
+/// UUID from Assigned Numbers section 3.4.
+pub const PACS_UUID: Uuid = Uuid::from_u16(0x1850);
+
 /// A Published Audio Capability (PAC) record.
 /// Published Audio Capabilities represent the capabilities of a given peer to
 /// transmit or receive Audio capabilities, exposed in PAC records, represent
@@ -34,6 +39,9 @@
         idx += consumed;
         let codec_specific_capabilites_length = buf[idx] as usize;
         idx += 1;
+        if idx + codec_specific_capabilites_length > buf.len() {
+            return Err(bt_common::packet_encoding::Error::UnexpectedDataLength);
+        }
         let (results, consumed) =
             CodecCapability::decode_all(&buf[idx..idx + codec_specific_capabilites_length]);
         if consumed != codec_specific_capabilites_length {
@@ -86,6 +94,7 @@
 }
 
 impl FromCharacteristic for SinkPac {
+    /// UUID from Assigned Numbers section 3.8.
     const UUID: Uuid = Uuid::from_u16(0x2BC9);
 
     fn from_chr(
@@ -114,6 +123,7 @@
 }
 
 impl FromCharacteristic for SourcePac {
+    /// UUID from Assigned Numbers section 3.8.
     const UUID: Uuid = Uuid::from_u16(0x2BCB);
 
     fn from_chr(
@@ -163,6 +173,7 @@
 }
 
 impl FromCharacteristic for SourceAudioLocations {
+    /// UUID from Assigned Numbers section 3.8.
     const UUID: Uuid = Uuid::from_u16(0x2BCC);
 
     fn from_chr(
@@ -228,7 +239,8 @@
 }
 
 impl FromCharacteristic for AvailableAudioContexts {
-    const UUID: Uuid = Uuid::from_u16(0x28CD);
+    /// UUID from Assigned Numbers section 3.8.
+    const UUID: Uuid = Uuid::from_u16(0x2BCD);
 
     fn from_chr(
         characteristic: Characteristic,
@@ -266,7 +278,7 @@
 }
 
 impl FromCharacteristic for SupportedAudioContexts {
-    const UUID: Uuid = Uuid::from_u16(0x28CE);
+    const UUID: Uuid = Uuid::from_u16(0x2BCE);
 
     fn from_chr(
         characteristic: Characteristic,