[bt][lib] Align types::le types with new LE API

- Changed the le::Peer and le::AdvertisingData types to use the new
library types for UUID and PeerId.
- Added conversion functions from deprecated and new FIDL types.
- The deprecated RemoteDevice type and the new Peer type return an error
on FIDL conversion failures. Peer now checks that mandatory FIDL fields
are present.

Bug: 1405
Test: bluetooth-crate-unittests.cmx, bt-integration-tests.cmx
Change-Id: I87fac60534f16a1bc79246fd0411494776c5ffb1
diff --git a/src/connectivity/bluetooth/lib/fuchsia-bluetooth/src/types/le.rs b/src/connectivity/bluetooth/lib/fuchsia-bluetooth/src/types/le.rs
index be124ad..3b5c7c1 100644
--- a/src/connectivity/bluetooth/lib/fuchsia-bluetooth/src/types/le.rs
+++ b/src/connectivity/bluetooth/lib/fuchsia-bluetooth/src/types/le.rs
@@ -18,8 +18,17 @@
 //!   }
 //! ```
 
-use crate::types::uuid::Uuid;
-use {fidl_fuchsia_bluetooth_le as fidl, std::fmt};
+use crate::types::{id::PeerId, uuid::Uuid};
+use {
+    failure::{format_err, Error},
+    fidl_fuchsia_bluetooth::Appearance,
+    fidl_fuchsia_bluetooth_le as fidl,
+    std::{
+        convert::{TryFrom, TryInto},
+        fmt,
+        str::FromStr,
+    },
+};
 
 #[derive(Clone)]
 pub struct RemoteDevice {
@@ -29,57 +38,64 @@
     pub advertising_data: Option<AdvertisingData>,
 }
 
-#[derive(Clone)]
+#[derive(Clone, Debug, PartialEq)]
 pub struct Peer {
-    pub id: Option<fidl_fuchsia_bluetooth::PeerId>,
+    pub id: PeerId,
     pub connectable: bool,
     pub rssi: Option<i8>,
     pub advertising_data: Option<AdvertisingData>,
 }
 
-#[derive(Clone)]
+#[derive(Clone, Debug, PartialEq)]
 pub struct AdvertisingData {
     pub name: Option<String>,
     pub tx_power_level: Option<i8>,
-    pub appearance: Option<u16>,
-    pub service_uuids: Vec<String>,
-    pub service_data: Vec<ServiceDataEntry>,
-    pub manufacturer_specific_data: Vec<ManufacturerSpecificDataEntry>,
-    pub solicited_service_uuids: Vec<String>,
+    pub appearance: Option<Appearance>,
+    pub service_uuids: Vec<Uuid>,
+    pub service_data: Vec<ServiceData>,
+    pub manufacturer_data: Vec<ManufacturerData>,
     pub uris: Vec<String>,
 }
 
-#[derive(Clone)]
-pub struct ServiceDataEntry {
-    pub uuid: String,
+#[derive(Clone, Debug, PartialEq)]
+pub struct ServiceData {
+    pub uuid: Uuid,
     pub data: Vec<u8>,
 }
 
-#[derive(Clone)]
-pub struct ManufacturerSpecificDataEntry {
+#[derive(Clone, Debug, PartialEq)]
+pub struct ManufacturerData {
     pub company_id: u16,
     pub data: Vec<u8>,
 }
 
-impl From<fidl::RemoteDevice> for RemoteDevice {
-    fn from(src: fidl::RemoteDevice) -> RemoteDevice {
-        RemoteDevice {
+impl TryFrom<fidl::RemoteDevice> for RemoteDevice {
+    type Error = Error;
+    fn try_from(src: fidl::RemoteDevice) -> Result<RemoteDevice, Self::Error> {
+        Ok(RemoteDevice {
             identifier: src.identifier,
             connectable: src.connectable,
             rssi: src.rssi.map(|v| v.value),
-            advertising_data: src.advertising_data.map(|ad| (*ad).into()),
-        }
+            advertising_data: match src.advertising_data {
+                Some(a) => Some(AdvertisingData::try_from(*a)?),
+                None => None,
+            },
+        })
     }
 }
 
-impl From<fidl::Peer> for Peer {
-    fn from(src: fidl::Peer) -> Peer {
-        Peer {
-            id: src.id,
+impl TryFrom<fidl::Peer> for Peer {
+    type Error = Error;
+    fn try_from(src: fidl::Peer) -> Result<Peer, Error> {
+        Ok(Peer {
+            id: src
+                .id
+                .ok_or(format_err!("`le.Peer` missing mandatory `id` field"))
+                .map(PeerId::from)?,
             connectable: src.connectable.unwrap_or(false),
             rssi: src.rssi,
             advertising_data: src.advertising_data.map(|ad| ad.into()),
-        }
+        })
     }
 }
 
@@ -88,14 +104,12 @@
         AdvertisingData {
             name: src.name,
             tx_power_level: src.tx_power_level,
-            // TODO(armansito): Change the target appearance type once users of the old format are
-            // removed.
-            appearance: src.appearance.map(|v| v as u16),
+            appearance: src.appearance,
             service_uuids: src
                 .service_uuids
                 .unwrap_or(vec![])
                 .into_iter()
-                .map(|e| Uuid::from(e).to_string())
+                .map(|uuid| Uuid::from(uuid))
                 .collect(),
             service_data: src
                 .service_data
@@ -103,64 +117,74 @@
                 .into_iter()
                 .map(|data| data.into())
                 .collect(),
-            manufacturer_specific_data: src
+            manufacturer_data: src
                 .manufacturer_data
                 .unwrap_or(vec![])
                 .into_iter()
                 .map(|data| data.into())
                 .collect(),
-            solicited_service_uuids: vec![],
             uris: src.uris.unwrap_or(vec![]),
         }
     }
 }
 
-impl From<fidl::AdvertisingDataDeprecated> for AdvertisingData {
-    fn from(src: fidl::AdvertisingDataDeprecated) -> AdvertisingData {
-        AdvertisingData {
+impl TryFrom<fidl::AdvertisingDataDeprecated> for AdvertisingData {
+    type Error = Error;
+    fn try_from(src: fidl::AdvertisingDataDeprecated) -> Result<AdvertisingData, Self::Error> {
+        Ok(AdvertisingData {
             name: src.name,
             tx_power_level: src.tx_power_level.map(|v| v.value),
-            appearance: src.appearance.map(|v| v.value),
-            service_uuids: src.service_uuids.unwrap_or(vec![]),
+            appearance: src
+                .appearance
+                .map(|v| {
+                    Appearance::from_primitive(v.value).ok_or(format_err!("invalid appearance"))
+                })
+                .map_or(Ok(None), |v| v.map(Some))?,
+            service_uuids: src
+                .service_uuids
+                .unwrap_or(vec![])
+                .into_iter()
+                .map(|uuid| Uuid::from_str(&uuid).map_err(|e| e.into()))
+                .collect::<Result<Vec<Uuid>, Error>>()?,
             service_data: src
                 .service_data
                 .unwrap_or(vec![])
                 .into_iter()
-                .map(|data| data.into())
-                .collect(),
-            manufacturer_specific_data: src
+                .map(ServiceData::try_from)
+                .collect::<Result<Vec<_>, Error>>()?,
+            manufacturer_data: src
                 .manufacturer_specific_data
                 .unwrap_or(vec![])
                 .into_iter()
                 .map(|data| data.into())
                 .collect(),
-            solicited_service_uuids: src.solicited_service_uuids.unwrap_or(vec![]),
             uris: src.uris.unwrap_or(vec![]),
-        }
+        })
     }
 }
 
-impl From<fidl::ServiceDataEntry> for ServiceDataEntry {
-    fn from(src: fidl::ServiceDataEntry) -> ServiceDataEntry {
-        ServiceDataEntry { uuid: src.uuid, data: src.data }
+impl TryFrom<fidl::ServiceDataEntry> for ServiceData {
+    type Error = Error;
+    fn try_from(src: fidl::ServiceDataEntry) -> Result<ServiceData, Self::Error> {
+        Ok(ServiceData { uuid: Uuid::from_str(&src.uuid)?, data: src.data })
     }
 }
 
-impl From<fidl::ServiceData> for ServiceDataEntry {
-    fn from(src: fidl::ServiceData) -> ServiceDataEntry {
-        ServiceDataEntry { uuid: Uuid::from(src.uuid).to_string(), data: src.data }
+impl From<fidl::ServiceData> for ServiceData {
+    fn from(src: fidl::ServiceData) -> ServiceData {
+        ServiceData { uuid: Uuid::from(src.uuid), data: src.data }
     }
 }
 
-impl From<fidl::ManufacturerSpecificDataEntry> for ManufacturerSpecificDataEntry {
-    fn from(src: fidl::ManufacturerSpecificDataEntry) -> ManufacturerSpecificDataEntry {
-        ManufacturerSpecificDataEntry { company_id: src.company_id, data: src.data }
+impl From<fidl::ManufacturerSpecificDataEntry> for ManufacturerData {
+    fn from(src: fidl::ManufacturerSpecificDataEntry) -> ManufacturerData {
+        ManufacturerData { company_id: src.company_id, data: src.data }
     }
 }
 
-impl From<fidl::ManufacturerData> for ManufacturerSpecificDataEntry {
-    fn from(src: fidl::ManufacturerData) -> ManufacturerSpecificDataEntry {
-        ManufacturerSpecificDataEntry { company_id: src.company_id, data: src.data }
+impl From<fidl::ManufacturerData> for ManufacturerData {
+    fn from(src: fidl::ManufacturerData) -> ManufacturerData {
+        ManufacturerData { company_id: src.company_id, data: src.data }
     }
 }
 
@@ -168,7 +192,7 @@
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         let connectable = if self.connectable { "connectable" } else { "non-connectable" };
 
-        write!(f, "[device({}), ", connectable)?;
+        write!(f, "[device ({}) ", connectable)?;
 
         if let Some(rssi) = &self.rssi {
             write!(f, "rssi: {}, ", rssi)?;
@@ -184,4 +208,223 @@
     }
 }
 
-// TODO(armansito): Add unit tests once the deprecated FIDL types are removed.
+impl fmt::Display for Peer {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        let connectable = if self.connectable { "connectable" } else { "non-connectable" };
+
+        write!(f, "[peer ({}) ", connectable)?;
+
+        if let Some(rssi) = &self.rssi {
+            write!(f, "rssi: {}, ", rssi)?;
+        }
+
+        if let Some(ad) = &self.advertising_data {
+            if let Some(name) = &ad.name {
+                write!(f, "{}, ", name)?;
+            }
+        }
+
+        write!(f, "id: {}]", self.id)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use fidl_fuchsia_bluetooth as fbt;
+    use fidl_fuchsia_bluetooth_le as fble;
+
+    #[test]
+    fn advertising_data_from_fidl_empty() {
+        let data = fble::AdvertisingData {
+            name: None,
+            tx_power_level: None,
+            appearance: None,
+            service_uuids: None,
+            service_data: None,
+            manufacturer_data: None,
+            uris: None,
+        };
+        let expected = AdvertisingData {
+            name: None,
+            tx_power_level: None,
+            appearance: None,
+            service_uuids: vec![],
+            service_data: vec![],
+            manufacturer_data: vec![],
+            uris: vec![],
+        };
+        let data = AdvertisingData::from(data);
+        assert_eq!(expected, data);
+    }
+
+    #[test]
+    fn advertising_data_from_fidl() {
+        let uuid = fbt::Uuid {
+            value: [
+                0xfb, 0x34, 0x9b, 0x5f, 0x80, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0x0d, 0x18,
+                0x00, 0x00,
+            ],
+        };
+        let data = fble::AdvertisingData {
+            name: Some("hello".to_string()),
+            tx_power_level: Some(-10),
+            appearance: Some(fbt::Appearance::Watch),
+            service_uuids: Some(vec![uuid.clone()]),
+            service_data: Some(vec![fble::ServiceData { uuid: uuid.clone(), data: vec![1, 2, 3] }]),
+            manufacturer_data: Some(vec![fble::ManufacturerData {
+                company_id: 1,
+                data: vec![3, 4, 5],
+            }]),
+            uris: Some(vec!["some/uri".to_string()]),
+        };
+        let expected = AdvertisingData {
+            name: Some("hello".to_string()),
+            tx_power_level: Some(-10),
+            appearance: Some(fbt::Appearance::Watch),
+            service_uuids: vec![Uuid::new16(0x180d)],
+            service_data: vec![ServiceData { uuid: Uuid::new16(0x180d), data: vec![1, 2, 3] }],
+            manufacturer_data: vec![ManufacturerData { company_id: 1, data: vec![3, 4, 5] }],
+            uris: vec!["some/uri".to_string()],
+        };
+        let data = AdvertisingData::from(data);
+        assert_eq!(expected, data);
+    }
+
+    #[test]
+    fn advertising_data_from_deprecated_fidl_malformed_appearance() {
+        let data = fble::AdvertisingDataDeprecated {
+            name: None,
+            tx_power_level: None,
+            appearance: Some(Box::new(fbt::UInt16 { value: 1 })), // fbt::Appearance does not declare this entry
+            service_uuids: None,
+            service_data: None,
+            manufacturer_specific_data: None,
+            solicited_service_uuids: None,
+            uris: None,
+        };
+        let data = AdvertisingData::try_from(data);
+        assert!(data.is_err());
+    }
+
+    #[test]
+    fn advertising_data_from_deprecated_fidl_malformed_service_uuid() {
+        let data = fble::AdvertisingDataDeprecated {
+            name: None,
+            tx_power_level: None,
+            appearance: None,
+            service_uuids: Some(vec!["💩".to_string()]),
+            service_data: None,
+            manufacturer_specific_data: None,
+            solicited_service_uuids: None,
+            uris: None,
+        };
+        let data = AdvertisingData::try_from(data);
+        assert!(data.is_err());
+    }
+
+    #[test]
+    fn advertising_data_from_deprecated_fidl_malformed_service_data() {
+        let data = fble::AdvertisingDataDeprecated {
+            name: None,
+            tx_power_level: None,
+            appearance: None,
+            service_uuids: None,
+            service_data: Some(vec![fble::ServiceDataEntry {
+                uuid: "💩".to_string(),
+                data: vec![1, 2],
+            }]),
+            manufacturer_specific_data: None,
+            solicited_service_uuids: None,
+            uris: None,
+        };
+        let data = AdvertisingData::try_from(data);
+        assert!(data.is_err());
+    }
+
+    #[test]
+    fn advertising_data_from_deprecated_fidl() {
+        let data = fble::AdvertisingDataDeprecated {
+            name: Some("hello".to_string()),
+            tx_power_level: Some(Box::new(fbt::Int8 { value: -10 })),
+            appearance: Some(Box::new(fbt::UInt16 { value: 64 })), // "Phone"
+            service_uuids: Some(vec!["0000180d-0000-1000-8000-00805f9b34fb".to_string()]),
+            service_data: Some(vec![fble::ServiceDataEntry {
+                uuid: "0000180d-0000-1000-8000-00805f9b34fb".to_string(),
+                data: vec![1, 2],
+            }]),
+            manufacturer_specific_data: Some(vec![fble::ManufacturerSpecificDataEntry {
+                company_id: 1,
+                data: vec![1],
+            }]),
+            solicited_service_uuids: None,
+            uris: Some(vec!["some/uri".to_string()]),
+        };
+        let expected = AdvertisingData {
+            name: Some("hello".to_string()),
+            tx_power_level: Some(-10),
+            appearance: Some(fbt::Appearance::Phone),
+            service_uuids: vec![Uuid::new16(0x180d)],
+            service_data: vec![ServiceData { uuid: Uuid::new16(0x180d), data: vec![1, 2] }],
+            manufacturer_data: vec![ManufacturerData { company_id: 1, data: vec![1] }],
+            uris: vec!["some/uri".to_string()],
+        };
+        let data = AdvertisingData::try_from(data).expect("expected successful conversion");
+        assert_eq!(expected, data);
+    }
+
+    #[test]
+    fn peer_from_fidl_no_id() {
+        let peer = fble::Peer { id: None, connectable: None, rssi: None, advertising_data: None };
+        let peer = Peer::try_from(peer);
+        assert!(peer.is_err());
+    }
+
+    #[test]
+    fn peer_from_fidl_mandatory_fields_only() {
+        let peer = fble::Peer {
+            id: Some(fbt::PeerId { value: 1 }),
+            connectable: None,
+            rssi: None,
+            advertising_data: None,
+        };
+        let expected =
+            Peer { id: PeerId::new(1), connectable: false, rssi: None, advertising_data: None };
+        let peer = Peer::try_from(peer).expect("expected successful conversion");
+        assert_eq!(expected, peer);
+    }
+
+    #[test]
+    fn peer_from_fidl() {
+        let peer = fble::Peer {
+            id: Some(fbt::PeerId { value: 1 }),
+            connectable: Some(true),
+            rssi: Some(-10),
+            advertising_data: Some(fble::AdvertisingData {
+                name: Some("hello".to_string()),
+                tx_power_level: Some(-10),
+                appearance: Some(fbt::Appearance::Watch),
+                service_uuids: None,
+                service_data: None,
+                manufacturer_data: None,
+                uris: None,
+            }),
+        };
+        let expected = Peer {
+            id: PeerId::new(1),
+            connectable: true,
+            rssi: Some(-10),
+            advertising_data: Some(AdvertisingData {
+                name: Some("hello".to_string()),
+                tx_power_level: Some(-10),
+                appearance: Some(fbt::Appearance::Watch),
+                service_uuids: vec![],
+                service_data: vec![],
+                manufacturer_data: vec![],
+                uris: vec![],
+            }),
+        };
+        let peer = Peer::try_from(peer).expect("expected successful conversion");
+        assert_eq!(expected, peer);
+    }
+}
diff --git a/src/connectivity/bluetooth/tests/integration/src/harness/low_energy_central.rs b/src/connectivity/bluetooth/tests/integration/src/harness/low_energy_central.rs
index a5cf8730b..433f77f 100644
--- a/src/connectivity/bluetooth/tests/integration/src/harness/low_energy_central.rs
+++ b/src/connectivity/bluetooth/tests/integration/src/harness/low_energy_central.rs
@@ -11,6 +11,7 @@
         types::le::RemoteDevice,
     },
     futures::{Future, TryFutureExt, TryStreamExt},
+    std::convert::TryInto,
 };
 
 use crate::harness::{control::ActivatedFakeHost, emulator::EmulatorHarnessAux, TestHarness};
@@ -77,7 +78,7 @@
     while let Some(e) = events.try_next().await? {
         match e {
             CentralEvent::OnDeviceDiscovered { device } => {
-                harness.write_state().remote_devices.push(device.into());
+                harness.write_state().remote_devices.push(device.try_into()?);
                 harness.notify_state_changed();
             }
             CentralEvent::OnScanStateChanged { scanning } => {
diff --git a/src/connectivity/bluetooth/tests/integration/src/harness/low_energy_peripheral.rs b/src/connectivity/bluetooth/tests/integration/src/harness/low_energy_peripheral.rs
index ccab272..fa2a36e 100644
--- a/src/connectivity/bluetooth/tests/integration/src/harness/low_energy_peripheral.rs
+++ b/src/connectivity/bluetooth/tests/integration/src/harness/low_energy_peripheral.rs
@@ -16,6 +16,7 @@
     futures::{select, Future, FutureExt, TryStreamExt},
     parking_lot::MappedRwLockWriteGuard,
     pin_utils::pin_mut,
+    std::convert::TryInto,
 };
 
 use crate::harness::{
@@ -114,7 +115,10 @@
     while let Some(e) = events.try_next().await? {
         match e {
             PeripheralEvent::OnPeerConnected { peer, connection } => {
-                harness.write_state().connections.push((peer.into(), connection.into_proxy()?));
+                harness
+                    .write_state()
+                    .connections
+                    .push((peer.try_into()?, connection.into_proxy()?));
             }
             // Ignore the deprecated events.
             PeripheralEvent::OnCentralConnected { .. } => (),
diff --git a/src/connectivity/bluetooth/tools/bt-le-central/src/central.rs b/src/connectivity/bluetooth/tools/bt-le-central/src/central.rs
index 5f05844..3d40dec 100644
--- a/src/connectivity/bluetooth/tools/bt-le-central/src/central.rs
+++ b/src/connectivity/bluetooth/tools/bt-le-central/src/central.rs
@@ -12,7 +12,7 @@
     fuchsia_bluetooth::{error::Error as BTError, types::le::RemoteDevice},
     futures::prelude::*,
     parking_lot::RwLock,
-    std::{process::exit, sync::Arc},
+    std::{convert::TryFrom, process::exit, sync::Arc},
 };
 
 use crate::gatt::repl::start_gatt_loop;
@@ -72,7 +72,13 @@
                     Ok(())
                 }
                 CentralEvent::OnDeviceDiscovered { device } => {
-                    let device = RemoteDevice::from(device);
+                    let device = match RemoteDevice::try_from(device) {
+                        Ok(d) => d,
+                        Err(e) => {
+                            eprintln!("received malformed scan result: {}", e);
+                            exit(0);
+                        }
+                    };
                     let id = device.identifier.clone();
 
                     eprintln!(" {}", device);