Project import generated by Copybara.
GitOrigin-RevId: e98f48dda572903bf2f3982cbc35a01a1b83d29f
Change-Id: I35b8f1dc1f34013726c538d89ac50a00091efa51
diff --git a/.dockerignore b/.dockerignore
index 8388195..916aafb 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,6 +1,8 @@
**
!common/
+!nearby/common/
+!nearby/identity/
common/target/
!nearby/.cargo
!nearby/build_scripts/
diff --git a/.gitignore b/.gitignore
index b49ad4a..67bbbb7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,6 +13,7 @@
nearby/presence/ldt_np_jni/java/LdtNpJni/build/
nearby/presence/ldt_np_jni/java/LdtNpJni/bin/
**/auth_token.txt
+**/.DS_Store
/bazel-*
/.clwb/.bazelproject
/common/1p_internal/ph_client_sys/portable_ph_fake/cmake-build-debug/
diff --git a/Dockerfile b/Dockerfile
index f5d2a54..842c455 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -37,7 +37,7 @@
# install cargo with default settings
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y
ENV PATH="/root/.cargo/bin:${PATH}"
-RUN rustup toolchain install 1.79.0 --component llvm-tools-preview
+RUN rustup toolchain install 1.88.0 --component llvm-tools-preview
RUN rustup toolchain install nightly-2024-09-03 --component rust-src --force
RUN rustup target add wasm32-unknown-unknown # used by np_web
RUN rustup target add thumbv7m-none-eabi
diff --git a/betosync/rust-toolchain.toml b/betosync/rust-toolchain.toml
index 0193dee..e88baf1 100644
--- a/betosync/rust-toolchain.toml
+++ b/betosync/rust-toolchain.toml
@@ -1,2 +1,2 @@
[toolchain]
-channel = "1.83.0"
+channel = "1.88.0"
diff --git a/betosync/submerge/crdt/examples/simulation_integer_max/main.rs b/betosync/submerge/crdt/examples/simulation_integer_max/main.rs
index d83dfeb..bae32e7 100644
--- a/betosync/submerge/crdt/examples/simulation_integer_max/main.rs
+++ b/betosync/submerge/crdt/examples/simulation_integer_max/main.rs
@@ -21,7 +21,7 @@
test_fakes::TriState,
},
delta::{AsDeltaMut, AsDeltaRef, DeltaMut},
- CrdtState,
+ CrdtState, MergeError,
};
/// Our trivial example CRDT.
@@ -30,8 +30,8 @@
/// Merges by taking the max of the two values.
impl CrdtState for IntegerMaxCrdt {
- fn merge(a: &Self, b: &Self) -> Self {
- a.max(b).clone()
+ fn merge(a: &Self, b: &Self) -> Result<Self, MergeError> {
+ Ok(a.max(b).clone())
}
}
@@ -47,7 +47,7 @@
fn apply(self, mut state: DeltaMut<IntegerMaxCrdt>, _ctx: &SimulationContext<TriState>) {
match self {
IntegerMaxOp::SetInteger { value } => {
- if value > state.as_ref().merge().0 {
+ if value > state.as_ref().merge().expect("merge must not fail").0 {
state.delta_mut().0 = value;
}
}
diff --git a/betosync/submerge/crdt/fuzz/src/bin/default_bottom_state.rs b/betosync/submerge/crdt/fuzz/src/bin/default_bottom_state.rs
index 86788d9..7e51a19 100644
--- a/betosync/submerge/crdt/fuzz/src/bin/default_bottom_state.rs
+++ b/betosync/submerge/crdt/fuzz/src/bin/default_bottom_state.rs
@@ -44,19 +44,34 @@
fn default_bottom_state(crdt: ArbitraryCrdt) {
match crdt {
ArbitraryCrdt::Set(original) => {
- assert_eq!(original, CrdtState::merge(&original, &Default::default()));
+ assert_eq!(
+ original,
+ CrdtState::merge(&original, &Default::default()).expect("merge must not fail")
+ );
}
ArbitraryCrdt::Register(original) => {
- assert_eq!(original, CrdtState::merge(&original, &Default::default()));
+ assert_eq!(
+ original,
+ CrdtState::merge(&original, &Default::default()).expect("merge must not fail")
+ );
}
ArbitraryCrdt::VectorData(original) => {
- assert_eq!(original, CrdtState::merge(&original, &Default::default()));
+ assert_eq!(
+ original,
+ CrdtState::merge(&original, &Default::default()).expect("merge must not fail")
+ );
}
ArbitraryCrdt::Map(original) => {
- assert_eq!(original, CrdtState::merge(&original, &Default::default()));
+ assert_eq!(
+ original,
+ CrdtState::merge(&original, &Default::default()).expect("merge must not fail")
+ );
}
ArbitraryCrdt::Container(original) => {
- assert_eq!(original, CrdtState::merge(&original, &Default::default()));
+ assert_eq!(
+ original,
+ CrdtState::merge(&original, &Default::default()).expect("merge must not fail")
+ );
}
}
}
diff --git a/betosync/submerge/crdt/fuzz/src/bin/delta_lww_crdt_container.rs b/betosync/submerge/crdt/fuzz/src/bin/delta_lww_crdt_container.rs
index edd1a24..6fa08da 100644
--- a/betosync/submerge/crdt/fuzz/src/bin/delta_lww_crdt_container.rs
+++ b/betosync/submerge/crdt/fuzz/src/bin/delta_lww_crdt_container.rs
@@ -84,5 +84,5 @@
.get_value_mut(&ctx)
.map(|result| result.map(|d| d.merge())),
);
- assert_eq!(expected, delta.merge());
+ assert_eq!(expected, delta.merge().expect("merge must not fail"));
}
diff --git a/betosync/submerge/crdt/fuzz/src/bin/delta_lww_map.rs b/betosync/submerge/crdt/fuzz/src/bin/delta_lww_map.rs
index 37f1c4d..6c48872 100644
--- a/betosync/submerge/crdt/fuzz/src/bin/delta_lww_map.rs
+++ b/betosync/submerge/crdt/fuzz/src/bin/delta_lww_map.rs
@@ -88,5 +88,5 @@
.map(|k| (k, delta.get_value(k).map(|f| f.merge())))
.collect::<Vec<_>>()
);
- assert_eq!(expected, delta.merge());
+ assert_eq!(expected, delta.merge().expect("merge must not fail"));
}
diff --git a/betosync/submerge/crdt/fuzz/src/bin/delta_register.rs b/betosync/submerge/crdt/fuzz/src/bin/delta_register.rs
index 1c688af..0142bf6 100644
--- a/betosync/submerge/crdt/fuzz/src/bin/delta_register.rs
+++ b/betosync/submerge/crdt/fuzz/src/bin/delta_register.rs
@@ -43,5 +43,5 @@
assert_eq!(expected.get_all(), delta.get_all());
assert_eq!(expected.get(), delta.get());
- assert_eq!(expected, delta.merge());
+ assert_eq!(expected, delta.merge().expect("merge must not fail"));
}
diff --git a/betosync/submerge/crdt/fuzz/src/bin/delta_set.rs b/betosync/submerge/crdt/fuzz/src/bin/delta_set.rs
index 984e3c7..1b18318 100644
--- a/betosync/submerge/crdt/fuzz/src/bin/delta_set.rs
+++ b/betosync/submerge/crdt/fuzz/src/bin/delta_set.rs
@@ -42,5 +42,5 @@
}
assert_eq!(expected.entries(), delta.entries());
- assert_eq!(expected, delta.merge());
+ assert_eq!(expected, delta.merge().expect("merge must not fail"));
}
diff --git a/betosync/submerge/crdt/fuzz/src/bin/delta_vector_data.rs b/betosync/submerge/crdt/fuzz/src/bin/delta_vector_data.rs
index 8ec0c7a..8de0cc9 100644
--- a/betosync/submerge/crdt/fuzz/src/bin/delta_vector_data.rs
+++ b/betosync/submerge/crdt/fuzz/src/bin/delta_vector_data.rs
@@ -41,5 +41,5 @@
}
assert_eq!(expected.entries(), delta.entries());
- assert_eq!(expected, delta.merge());
+ assert_eq!(expected, delta.merge().expect("merge must not fail"));
}
diff --git a/betosync/submerge/crdt/src/checker/mod.rs b/betosync/submerge/crdt/src/checker/mod.rs
index 06a1b3f..7d6f95b 100644
--- a/betosync/submerge/crdt/src/checker/mod.rs
+++ b/betosync/submerge/crdt/src/checker/mod.rs
@@ -55,15 +55,14 @@
/// This example implements a last-writer-wins register that requires globally unique timestamps.
///
/// ```
-/// use crdt::CrdtState;
-/// use crdt::checker::MergeInvariantTest;
+/// use crdt::{checker::MergeInvariantTest, CrdtState, MergeError};
///
/// #[derive(Debug, Clone, PartialEq, Eq)]
/// pub struct LwwValue { timestamp: u64, value: String }
///
/// impl CrdtState for LwwValue {
-/// fn merge(a: &Self, b: &Self) -> Self {
-/// std::cmp::max_by_key(a, b, |v| v.timestamp).clone()
+/// fn merge(a: &Self, b: &Self) -> Result<Self, MergeError> {
+/// Ok(std::cmp::max_by_key(a, b, |v| v.timestamp).clone())
/// }
///
/// #[cfg(any(test, feature = "checker"))]
@@ -98,20 +97,26 @@
match self {
Self::Associativity(s1, s2, s3) => {
assert_eq!(
- S::merge(&S::merge(&s1, &s2), &s3),
- S::merge(&s1, &S::merge(&s2, &s3)),
+ S::merge(&S::merge(&s1, &s2).expect("merge must not fail"), &s3)
+ .expect("merge must not fail"),
+ S::merge(&s1, &S::merge(&s2, &s3).expect("merge must not fail"))
+ .expect("merge must not fail"),
"Associativity violated:\ns1={s1:#?}\ns2={s2:#?}\ns3={s3:#?}",
)
}
Self::Commutativity(s1, s2) => {
assert_eq!(
- S::merge(&s1, &s2),
- S::merge(&s2, &s1),
+ S::merge(&s1, &s2).expect("merge must not fail"),
+ S::merge(&s2, &s1).expect("merge must not fail"),
"Commutativity violated:\ns1={s1:#?}\ns2={s2:#?}"
)
}
Self::Idempotence(s1) => {
- assert_eq!(S::merge(&s1, &s1), s1, "Idempotence violated:\ns1={s1:#?}")
+ assert_eq!(
+ S::merge(&s1, &s1).expect("merge must not fail"),
+ s1,
+ "Idempotence violated:\ns1={s1:#?}"
+ )
}
}
}
diff --git a/betosync/submerge/crdt/src/checker/simulation.rs b/betosync/submerge/crdt/src/checker/simulation.rs
index 001e255..dc72040 100644
--- a/betosync/submerge/crdt/src/checker/simulation.rs
+++ b/betosync/submerge/crdt/src/checker/simulation.rs
@@ -76,7 +76,7 @@
/// test_fakes::TriState,
/// },
/// delta::{AsDeltaMut, AsDeltaRef, DeltaMut},
-/// CrdtState,
+/// CrdtState, MergeError,
/// };
///
/// /// Our trivial example CRDT.
@@ -85,8 +85,8 @@
///
/// /// Merges by taking the max of the two values.
/// impl CrdtState for IntegerMaxCrdt {
-/// fn merge(a: &Self, b: &Self) -> Self {
-/// a.max(b).clone()
+/// fn merge(a: &Self, b: &Self) -> Result<Self, MergeError> {
+/// Ok(a.max(b).clone())
/// }
/// }
///
@@ -102,7 +102,7 @@
/// fn apply(self, mut state: DeltaMut<IntegerMaxCrdt>, _ctx: &SimulationContext<TriState>) {
/// match self {
/// IntegerMaxOp::SetInteger { value } => {
-/// if value > state.as_ref().merge().0 {
+/// if value > state.as_ref().merge().unwrap().0 {
/// state.delta_mut().0 = value;
/// }
/// }
@@ -145,7 +145,8 @@
let mut delta = DeltaOwned::new(¤t_state);
op.apply(delta.as_mut(), &context);
let delta_component = delta.into_delta();
- current_state = CrdtState::merge(¤t_state, &delta_component);
+ current_state = CrdtState::merge(¤t_state, &delta_component)
+ .expect("merge must not fail");
update_messages.push(delta_component);
}
SimulationOperation::ContextOp(op) => {
@@ -172,10 +173,14 @@
let delivered_messages = self.delivery_scenario.scramble(&sent_messages)?;
let state1 = sent_messages
.into_iter()
- .fold(self.init_state.clone(), |a, b| CrdtState::merge(&a, &b));
+ .fold(self.init_state.clone(), |a, b| {
+ CrdtState::merge(&a, &b).expect("merge must not fail")
+ });
let state2 = delivered_messages
.into_iter()
- .fold(self.init_state.clone(), |a, b| CrdtState::merge(&a, &b));
+ .fold(self.init_state.clone(), |a, b| {
+ CrdtState::merge(&a, &b).expect("merge must not fail")
+ });
assert_eq!(state1, state2);
assert_eq!(state1, final_state);
diff --git a/betosync/submerge/crdt/src/checker/test_crdt_union.rs b/betosync/submerge/crdt/src/checker/test_crdt_union.rs
index b4cbce6..451ab8e 100644
--- a/betosync/submerge/crdt/src/checker/test_crdt_union.rs
+++ b/betosync/submerge/crdt/src/checker/test_crdt_union.rs
@@ -20,8 +20,8 @@
use crate::{
delta::{AsDeltaMut, AsDeltaRef, DeltaMut, DeltaRef},
- lww_crdt_container::{CrdtUnion, IncompatibleType, TryAsChild},
- ApplyChanges, CrdtState, HasPlainRepresentation, ToPlain, UpdateContext,
+ lww_crdt_container::{CrdtUnion, TryAsChild},
+ ApplyChanges, CrdtState, HasPlainRepresentation, MergeError, ToPlain, UpdateContext,
};
use arbitrary::Arbitrary;
use distributed_time::vector_clock::VectorClock;
@@ -59,11 +59,11 @@
/// Combines the base part and the delta part and return the resulting merged component.
///
/// See [`CrdtState::merge`] for details on the merge operation.
- pub fn merge(&self) -> TestCrdtUnion {
- match self {
- Self::Type1(r) => TestCrdtUnion::Type1(r.merge()),
- Self::Type2(r) => TestCrdtUnion::Type2(r.merge()),
- }
+ pub fn merge(&self) -> Result<TestCrdtUnion, crate::MergeError> {
+ Ok(match self {
+ Self::Type1(r) => TestCrdtUnion::Type1(r.merge()?),
+ Self::Type2(r) => TestCrdtUnion::Type2(r.merge()?),
+ })
}
}
@@ -79,11 +79,11 @@
/// Combines the base part and the delta part and return the resulting merged component.
///
/// See [`CrdtState::merge`] for details on the merge operation.
- pub fn merge(&self) -> TestCrdtUnion {
- match self {
- Self::Type1(r) => TestCrdtUnion::Type1(r.as_ref().merge()),
- Self::Type2(r) => TestCrdtUnion::Type2(r.as_ref().merge()),
- }
+ pub fn merge(&self) -> Result<TestCrdtUnion, crate::MergeError> {
+ Ok(match self {
+ Self::Type1(r) => TestCrdtUnion::Type1(r.as_ref().merge()?),
+ Self::Type2(r) => TestCrdtUnion::Type2(r.as_ref().merge()?),
+ })
}
}
@@ -92,8 +92,8 @@
pub struct TestVariant1(pub u8);
impl CrdtState for TestVariant1 {
- fn merge(a: &Self, b: &Self) -> Self {
- Self(a.0.max(b.0))
+ fn merge(a: &Self, b: &Self) -> Result<Self, crate::MergeError> {
+ Ok(Self(a.0.max(b.0)))
}
}
@@ -116,8 +116,8 @@
pub struct TestVariant2(pub u8);
impl CrdtState for TestVariant2 {
- fn merge(a: &Self, b: &Self) -> Self {
- Self(a.0.max(b.0))
+ fn merge(a: &Self, b: &Self) -> Result<Self, crate::MergeError> {
+ Ok(Self(a.0.max(b.0)))
}
}
@@ -171,7 +171,7 @@
type DeltaRef<'d> = TestCrdtUnionRef<'d>;
type DeltaMut<'d> = TestCrdtUnionMut<'d>;
- fn try_merge(a: &Self, b: &Self) -> Result<Self, IncompatibleType> {
+ fn try_merge(a: &Self, b: &Self) -> Result<Self, MergeError> {
match (a, b) {
(TestCrdtUnion::Type1(state_a), TestCrdtUnion::Type1(state_b)) => {
Ok(TestCrdtUnion::Type1(*state_a.max(state_b)))
@@ -179,7 +179,7 @@
(TestCrdtUnion::Type2(state_a), TestCrdtUnion::Type2(state_b)) => {
Ok(TestCrdtUnion::Type2(*state_a.max(state_b)))
}
- _ => Err(IncompatibleType),
+ _ => Err(MergeError::IncompatibleType),
}
}
@@ -277,7 +277,7 @@
type Plain = TestCrdtUnion;
fn to_plain(&self) -> TestCrdtUnion {
- self.merge()
+ self.merge().expect("merge must not fail")
}
}
diff --git a/betosync/submerge/crdt/src/checker/tests/integer_max_merge.rs b/betosync/submerge/crdt/src/checker/tests/integer_max_merge.rs
index a780f2d..3b4d321 100644
--- a/betosync/submerge/crdt/src/checker/tests/integer_max_merge.rs
+++ b/betosync/submerge/crdt/src/checker/tests/integer_max_merge.rs
@@ -12,15 +12,15 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-use crate::{checker::MergeInvariantTest, CrdtState};
+use crate::{checker::MergeInvariantTest, CrdtState, MergeError};
use arbitrary::Arbitrary;
#[derive(Debug, Clone, PartialEq, Eq, Arbitrary)]
struct Counter(u8);
impl CrdtState for Counter {
- fn merge(a: &Self, b: &Self) -> Self {
- Counter(a.0.max(b.0))
+ fn merge(a: &Self, b: &Self) -> Result<Self, MergeError> {
+ Ok(Counter(a.0.max(b.0)))
}
}
diff --git a/betosync/submerge/crdt/src/checker/tests/set_union_merge.rs b/betosync/submerge/crdt/src/checker/tests/set_union_merge.rs
index 9461e7c..9d722ab 100644
--- a/betosync/submerge/crdt/src/checker/tests/set_union_merge.rs
+++ b/betosync/submerge/crdt/src/checker/tests/set_union_merge.rs
@@ -16,7 +16,7 @@
use crate::{
checker::{utils::DeterministicHasher, MergeInvariantTest},
- CrdtState,
+ CrdtState, MergeError,
};
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -31,8 +31,8 @@
}
impl CrdtState for SetUnion {
- fn merge(a: &Self, b: &Self) -> Self {
- SetUnion(a.0.union(&b.0).cloned().collect())
+ fn merge(a: &Self, b: &Self) -> Result<Self, MergeError> {
+ Ok(SetUnion(a.0.union(&b.0).cloned().collect()))
}
}
diff --git a/betosync/submerge/crdt/src/delta.rs b/betosync/submerge/crdt/src/delta.rs
index e93db92..b5d6d63 100644
--- a/betosync/submerge/crdt/src/delta.rs
+++ b/betosync/submerge/crdt/src/delta.rs
@@ -54,14 +54,14 @@
/// Combines the base part and the delta part and return the resulting merged component.
///
/// See [`CrdtState::merge`] for details on the merge operation.
- pub fn merge(&self) -> T
+ pub fn merge(&self) -> Result<T, crate::MergeError>
where
T: CrdtState + Clone + Default,
{
match (self.base, self.delta) {
- (None, None) => T::default(),
- (None, Some(delta)) => delta.clone(),
- (Some(base), None) => base.clone(),
+ (None, None) => Ok(T::default()),
+ (None, Some(delta)) => Ok(delta.clone()),
+ (Some(base), None) => Ok(base.clone()),
(Some(base), Some(delta)) => T::merge(base, delta),
}
}
@@ -153,7 +153,7 @@
/// Merges the base and the delta components of this.
///
/// See [`CrdtState::merge`] for details on the merge operation.
- pub fn merge(&self) -> T
+ pub fn merge(&self) -> Result<T, crate::MergeError>
where
T: CrdtState,
{
diff --git a/betosync/submerge/crdt/src/lib.rs b/betosync/submerge/crdt/src/lib.rs
index df9f693..8fa2e9a 100644
--- a/betosync/submerge/crdt/src/lib.rs
+++ b/betosync/submerge/crdt/src/lib.rs
@@ -22,6 +22,7 @@
vector_clock::VectorClock, DistributedClock, TimestampOverflow, WallTimestampProvider,
};
use lww_crdt_container::CrdtUnion;
+use thiserror::Error;
pub mod lww_crdt_container;
pub mod lww_map;
@@ -36,11 +37,24 @@
pub mod delta;
mod utils;
+/// Error returned when a CRDT merge fails.
+///
+/// When a merge error occurs, the CRDTs cannot be combined, and data convergence is not guaranteed.
+#[derive(Debug, Error, PartialEq, Eq, Clone, Copy)]
+pub enum MergeError {
+ /// Merge failed because the types are incompatible.
+ #[error("incompatible types")]
+ IncompatibleType,
+ /// Merge failed because the values are inconsistent.
+ #[error("inconsistent values")]
+ InconsistentValue,
+}
+
/// A CRDT state that is mergeable with instance. The [`merge`][CrdtState::merge] operation must be:
///
-/// * associative: `merge(merge(a, b), c) == merge(a, merge(b, c))`, and
-/// * commutative: `merge(a, b) == merge(b, a)`, and
-/// * idempotent: `merge(a, a) == a`
+/// * associative: `merge(merge(a, b)?, c)? == merge(a, merge(b, c)?)?`, and
+/// * commutative: `merge(a, b)? == merge(b, a)?`, and
+/// * idempotent: `merge(a, a)? == a`
///
/// These properties can be verified using
/// [`MergeInvariantTest`][crate::checker::MergeInvariantTest]. Any implementations of `CrdtState`
@@ -53,9 +67,9 @@
/// See also:
/// * [A comprehensive study of Convergent and Commutative Replicated Data
/// Types](https://inria.hal.science/inria-00555588/document)
-pub trait CrdtState {
+pub trait CrdtState: Sized {
/// Merges two CRDT states of the same type. See the trait documentation for details.
- fn merge(a: &Self, b: &Self) -> Self;
+ fn merge(a: &Self, b: &Self) -> Result<Self, MergeError>;
/// Return whether a given collection of CRDTs are valid.
///
diff --git a/betosync/submerge/crdt/src/lww_crdt_container.rs b/betosync/submerge/crdt/src/lww_crdt_container.rs
index 8d9d8f0..a556dac 100644
--- a/betosync/submerge/crdt/src/lww_crdt_container.rs
+++ b/betosync/submerge/crdt/src/lww_crdt_container.rs
@@ -30,6 +30,8 @@
use serde::{Deserialize, Serialize};
use thiserror::Error;
+use crate::MergeError;
+
/// Error returned when a [`CrdtUnion`] cannot be merged.
#[derive(Debug)]
pub struct IncompatibleType;
@@ -65,7 +67,7 @@
/// If `a` and `b` are of the same variant, it should return
/// `Self::from(CrdtState::merge(a.as_child(), b.as_child()))`. Otherwise, it should return
/// `Err(IncompatibleType)`.
- fn try_merge(a: &Self, b: &Self) -> Result<Self, IncompatibleType>;
+ fn try_merge(a: &Self, b: &Self) -> Result<Self, MergeError>;
/// Create a new read-only delta reference from the given `base` and `delta` components.
fn create_ref<'d>(
@@ -593,47 +595,38 @@
T: TotalTimestamp + Clone,
N: Ord + Clone,
{
- fn merge(a: &Self, b: &Self) -> Self {
+ fn merge(a: &Self, b: &Self) -> Result<Self, MergeError> {
fn merge_inner<V, T, N: Ord>(
a: &LwwCrdtContainerInner<V, T>,
b: &LwwCrdtContainerInner<V, T>,
- ) -> LwwCrdtContainerInner<V, T>
+ ) -> Result<LwwCrdtContainerInner<V, T>, MergeError>
where
V: CrdtUnion<N> + Clone,
T: TotalTimestamp + Clone,
{
- match a.timestamp.cmp(&b.timestamp) {
- Ordering::Equal => {
- match (&a.value, &b.value) {
- (Some(value_a), Some(value_b)) => LwwCrdtContainerInner {
- value: Some(
- CrdtUnion::try_merge(value_a, value_b)
- // This container does not allow updating the value without also
- // updating the timestamp outside of mutating a child CRDT, and
- // `try_merge` is required to delegate to the child CRDT's
- // `merge` which is infallible.
- .expect("Values with the same timestamp should be mergeable."),
- ),
- timestamp: a.timestamp.clone(),
- },
- (Some(_), None) => a.clone(),
- (None, Some(_)) => b.clone(),
- (None, None) => a.clone(),
- }
- }
+ Ok(match a.timestamp.cmp(&b.timestamp) {
+ Ordering::Equal => match (&a.value, &b.value) {
+ (Some(value_a), Some(value_b)) => LwwCrdtContainerInner {
+ value: Some(CrdtUnion::try_merge(value_a, value_b)?),
+ timestamp: a.timestamp.clone(),
+ },
+ (Some(_), None) => a.clone(),
+ (None, Some(_)) => b.clone(),
+ (None, None) => a.clone(),
+ },
Ordering::Less => b.clone(),
Ordering::Greater => a.clone(),
- }
+ })
}
- LwwCrdtContainer {
+ Ok(LwwCrdtContainer {
value: match (&a.value, &b.value) {
- (Some(inner_a), Some(inner_b)) => Some(merge_inner(inner_a, inner_b)),
+ (Some(inner_a), Some(inner_b)) => Some(merge_inner(inner_a, inner_b)?),
(Some(inner), None) | (None, Some(inner)) => Some(inner.clone()),
(None, None) => None,
},
subtree_version: VectorClock::least_upper_bound(&a.subtree_version, &b.subtree_version),
- }
+ })
}
#[cfg(any(test, feature = "checker"))]
@@ -760,6 +753,7 @@
#[cfg(test)]
#[derive_fuzztest::proptest]
+ #[allow(clippy::unwrap_used)]
fn container_roundtrip(
container: LwwCrdtContainer<
TypedCrdt<Vec<u8>, String>,
@@ -790,7 +784,7 @@
set::Set,
typed_crdt::{TypedCrdt, TypedCrdtRef},
vector_data::VectorData,
- CrdtState,
+ CrdtState, MergeError,
};
use super::LwwCrdtContainer;
@@ -846,7 +840,7 @@
.set_value(&FakeContext::new(1, 2_u8), VectorData::default().into())
.unwrap();
- let merged = CrdtState::merge(&container1, &container2);
+ let merged = CrdtState::merge(&container1, &container2).unwrap();
// container2 wins because its timestamp is larger
assert!(matches!(
merged.get_value(),
@@ -868,7 +862,7 @@
.set_value(&ctx2, VectorData::default().into())
.unwrap();
- let merged = CrdtState::merge(&container1, &container2);
+ let merged = CrdtState::merge(&container1, &container2).unwrap();
// d2 wins even though its timestamp is smaller, because it has observed d1 (d2 is cloned
// from d1)
assert!(matches!(
@@ -908,7 +902,7 @@
.unwrap();
child_reg1.set(&FakeContext::new(0, 10_u8), "abcd").unwrap();
- let merged = CrdtState::merge(&container1, &container2);
+ let merged = CrdtState::merge(&container1, &container2).unwrap();
let merged_child_reg = merged.get_child::<Register<_, _>>().unwrap();
// Merge should be done by Register rules, which is to keep all conflicting values.
// "5678" comes first because it is newer (timestamp value of 12)
@@ -992,4 +986,21 @@
vec!["#1", "#0"]
);
}
+
+ #[test]
+ fn test_merge_incompatible_type() {
+ let mut container1 = TestContainer::default();
+ let ctx = FakeContext::new(0, 0_u8);
+ container1
+ .set_value(&ctx, Register::default().into())
+ .unwrap();
+
+ let mut container2 = TestContainer::default();
+ container2.set_value(&ctx, Set::default().into()).unwrap();
+
+ assert_eq!(
+ CrdtState::merge(&container1, &container2).unwrap_err(),
+ MergeError::IncompatibleType
+ );
+ }
}
diff --git a/betosync/submerge/crdt/src/lww_map.rs b/betosync/submerge/crdt/src/lww_map.rs
index a48d14f..1f576de 100644
--- a/betosync/submerge/crdt/src/lww_map.rs
+++ b/betosync/submerge/crdt/src/lww_map.rs
@@ -347,20 +347,20 @@
T: TotalTimestamp + Clone,
N: Ord + Clone,
{
- fn merge(a: &Self, b: &Self) -> Self {
- Self {
+ fn merge(a: &Self, b: &Self) -> Result<Self, crate::MergeError> {
+ Ok(Self {
elements: zip_btree_map(&a.elements, &b.elements)
.map(|(key, item)| {
- (
+ Ok((
key.clone(),
match item {
ZipItem::Left(value) | ZipItem::Right(value) => value.clone(),
- ZipItem::Both(value_a, value_b) => CrdtState::merge(value_a, value_b),
+ ZipItem::Both(value_a, value_b) => CrdtState::merge(value_a, value_b)?,
},
- )
+ ))
})
- .collect(),
- }
+ .collect::<Result<_, _>>()?,
+ })
}
#[cfg(any(test, feature = "checker"))]
@@ -462,6 +462,7 @@
#[cfg(test)]
#[derive_fuzztest::proptest]
+ #[allow(clippy::unwrap_used)]
fn map_roundtrip(
map: LwwMap<String, TypedCrdt<Vec<u8>, String>, HybridLogicalTimestamp<String>, String>,
) {
diff --git a/betosync/submerge/crdt/src/register.rs b/betosync/submerge/crdt/src/register.rs
index 183eb0b..f8165ea 100644
--- a/betosync/submerge/crdt/src/register.rs
+++ b/betosync/submerge/crdt/src/register.rs
@@ -289,10 +289,10 @@
V: Clone,
D: DistributedClock + NonSemanticOrd + Clone + Default,
{
- fn merge(a: &Self, b: &Self) -> Self {
- Self {
+ fn merge(a: &Self, b: &Self) -> Result<Self, crate::MergeError> {
+ Ok(Self {
elements: Self::merge_elements(&a.elements, &b.elements),
- }
+ })
}
#[cfg(any(test, feature = "checker"))]
@@ -466,6 +466,7 @@
#[cfg(test)]
#[derive_fuzztest::proptest]
+ #[allow(clippy::unwrap_used)]
fn register_roundtrip(register: Register<Vec<u8>, VectorClock<String>>) {
let mut node_ids = NodeMapping::default();
assert_eq!(
@@ -504,8 +505,8 @@
let r3: TestRegister = Register::new(&FakeContext::new(2, 2_u8), 3)?;
- let r_merged = CrdtState::merge(&r1, &r2);
- let r_merged = CrdtState::merge(&r_merged, &r3);
+ let r_merged = CrdtState::merge(&r1, &r2).unwrap();
+ let r_merged = CrdtState::merge(&r_merged, &r3).unwrap();
// Ordered from oldest to newest according to timestamp, tie-broken by node ID
assert_eq!(vec![&3, &1, &2], r_merged.get_all());
@@ -522,7 +523,7 @@
let r2: TestRegister = Register::new(&FakeContext::new(0, 2_u8), 1)?;
// Merging r2 doesn't change r1 because r1 has already observed that change.
- let r_merged = CrdtState::merge(&r1, &r2);
+ let r_merged = CrdtState::merge(&r1, &r2).unwrap();
assert_eq!(vec![&2], r_merged.get_all());
assert_eq!(Some(&2), r_merged.get());
@@ -534,7 +535,7 @@
fn test_merge_same() {
let r1: TestRegister = Register::new(&FakeContext::new(0, 0_u8), 1).unwrap();
- let r_merged = CrdtState::merge(&r1, &r1);
+ let r_merged = CrdtState::merge(&r1, &r1).unwrap();
assert_eq!(vec![&1], r_merged.get_all());
assert_eq!(Some(&1), r_merged.get());
diff --git a/betosync/submerge/crdt/src/set.rs b/betosync/submerge/crdt/src/set.rs
index c490af2..7eddf23 100644
--- a/betosync/submerge/crdt/src/set.rs
+++ b/betosync/submerge/crdt/src/set.rs
@@ -358,8 +358,8 @@
E: Hash + Eq + Clone,
S: BuildHasher + Default,
{
- fn merge(a: &Self, b: &Self) -> Self {
- Self {
+ fn merge(a: &Self, b: &Self) -> Result<Self, crate::MergeError> {
+ Ok(Self {
elements: {
zip_hash_map(&a.elements, &b.elements)
.map(|(key, item)| match item {
@@ -368,7 +368,7 @@
})
.collect()
},
- }
+ })
}
}
@@ -509,6 +509,7 @@
#[cfg(test)]
#[derive_fuzztest::proptest]
+ #[allow(clippy::unwrap_used)]
fn set_roundtrip(set: Set<Vec<u8>>) {
let mut node_ids = NodeMapping::default();
assert_eq!(
@@ -582,7 +583,7 @@
let _ = set2.add(3).unwrap();
let _ = set2.add(4).unwrap();
- let merged = CrdtState::merge(&set1, &set2);
+ let merged = CrdtState::merge(&set1, &set2).unwrap();
assert_eq!(HashSet::from_iter(&[1, 2, 3, 4]), merged.entries());
}
@@ -596,7 +597,7 @@
let set2: TestSet = Set::default();
- let mut set2 = CrdtState::merge(&set1, &set2);
+ let mut set2 = CrdtState::merge(&set1, &set2).unwrap();
let _ = set2.remove(2);
assert_eq!(HashSet::from_iter(&[1, 3]), set2.entries());
diff --git a/betosync/submerge/crdt/src/typed_crdt.rs b/betosync/submerge/crdt/src/typed_crdt.rs
index 83163c9..f8c7ede 100644
--- a/betosync/submerge/crdt/src/typed_crdt.rs
+++ b/betosync/submerge/crdt/src/typed_crdt.rs
@@ -42,8 +42,15 @@
register::{Register, RegisterError},
set::{GenerationExhausted, Set},
vector_data::{VectorData, VectorDataError},
+ MergeError,
};
+impl From<MergeError> for IncompatibleType {
+ fn from(_: MergeError) -> Self {
+ IncompatibleType
+ }
+}
+
/// An enum of the CRDT types supported by this crate.
///
/// In this data model, the main structural component is the `Map`, which has a string key and a
@@ -281,22 +288,22 @@
type DeltaRef<'d> = TypedCrdtRef<'d, V, N, S>;
type DeltaMut<'d> = TypedCrdtMut<'d, V, N, S>;
- fn try_merge(a: &Self, b: &Self) -> Result<Self, IncompatibleType> {
- match (a, b) {
+ fn try_merge(a: &Self, b: &Self) -> Result<Self, MergeError> {
+ Ok(match (a, b) {
(TypedCrdt::Map(map_a), TypedCrdt::Map(map_b)) => {
- Ok(TypedCrdt::Map(CrdtState::merge(map_a, map_b)))
+ TypedCrdt::Map(CrdtState::merge(map_a, map_b)?)
}
(TypedCrdt::Register(reg_a), TypedCrdt::Register(reg_b)) => {
- Ok(TypedCrdt::Register(CrdtState::merge(reg_a, reg_b)))
+ TypedCrdt::Register(CrdtState::merge(reg_a, reg_b)?)
}
(TypedCrdt::Set(set_a), TypedCrdt::Set(set_b)) => {
- Ok(TypedCrdt::Set(CrdtState::merge(set_a, set_b)))
+ TypedCrdt::Set(CrdtState::merge(set_a, set_b)?)
}
(TypedCrdt::VectorData(vec_a), TypedCrdt::VectorData(vec_b)) => {
- Ok(TypedCrdt::VectorData(CrdtState::merge(vec_a, vec_b)))
+ TypedCrdt::VectorData(CrdtState::merge(vec_a, vec_b)?)
}
- _ => Err(IncompatibleType),
- }
+ _ => return Err(MergeError::IncompatibleType),
+ })
}
fn create_ref<'d>(
@@ -576,14 +583,14 @@
pub struct CrdtUnionMergeResult(Option<TypedCrdt<TriState, u8, DeterministicHasher>>);
impl CrdtState for CrdtUnionMergeResult {
- fn merge(a: &Self, b: &Self) -> Self {
- match (&a.0, &b.0) {
+ fn merge(a: &Self, b: &Self) -> Result<Self, crate::MergeError> {
+ Ok(match (&a.0, &b.0) {
(Some(v_a), Some(v_b)) => match CrdtUnion::try_merge(v_a, v_b) {
Ok(v) => CrdtUnionMergeResult(Some(v)),
Err(_) => CrdtUnionMergeResult(None),
},
_ => CrdtUnionMergeResult(None),
- }
+ })
}
fn is_valid_collection<'a>(collection: impl IntoIterator<Item = &'a Self>) -> bool
@@ -680,6 +687,7 @@
#[cfg(test)]
#[derive_fuzztest::proptest]
+ #[allow(clippy::unwrap_used)]
fn typed_crdt_roundtrip(typed_crdt: TypedCrdt<Vec<u8>, String>) {
let mut node_ids = NodeMapping::default();
assert_eq!(
diff --git a/betosync/submerge/crdt/src/vector_data.rs b/betosync/submerge/crdt/src/vector_data.rs
index 44b81d5..799381f 100644
--- a/betosync/submerge/crdt/src/vector_data.rs
+++ b/betosync/submerge/crdt/src/vector_data.rs
@@ -240,18 +240,24 @@
N: Ord + Clone,
V: Clone + Eq,
{
- fn merge(a: &Self, b: &Self) -> Self {
- Self {
+ fn merge(a: &Self, b: &Self) -> Result<Self, crate::MergeError> {
+ Ok(Self {
elements: zip_btree_map(&a.elements, &b.elements)
- .map(|(key, item)| match item {
- ZipItem::Left(value) | ZipItem::Right(value) => (key.clone(), value.clone()),
- ZipItem::Both(value_a, value_b) => (
- key.clone(),
- max_by_key(value_a, value_b, |v| v.version).clone(),
- ),
+ .map(|(key, item)| {
+ let entry = match item {
+ ZipItem::Left(value) => value.clone(),
+ ZipItem::Right(value) => value.clone(),
+ ZipItem::Both(value_a, value_b) => {
+ if value_a.version == value_b.version && value_a.data != value_b.data {
+ return Err(crate::MergeError::InconsistentValue);
+ }
+ max_by_key(value_a, value_b, |v| v.version).clone()
+ }
+ };
+ Ok((key.clone(), entry))
})
- .collect(),
- }
+ .collect::<Result<_, _>>()?,
+ })
}
#[cfg(any(test, feature = "checker"))]
@@ -342,7 +348,7 @@
V: Debug + PartialEq + Eq + Clone + 'static,
{
fn apply(self, mut state: DeltaMut<VectorData<N, V>>, ctx: &SimulationContext<N>) {
- let before = state.as_ref().merge();
+ let before = state.as_ref().merge().expect("merge must not fail");
let before_entries = before.entries();
match self {
VecDataOp::Set { node, value } => {
@@ -433,6 +439,7 @@
#[cfg(test)]
#[derive_fuzztest::proptest]
+ #[allow(clippy::unwrap_used)]
fn vector_data_roundtrip(vector_data: VectorData<String, Vec<u8>>) {
let mut node_ids = NodeMapping::default();
assert_eq!(
@@ -510,7 +517,7 @@
let mut child2 = v_parent.clone();
let _ = child2.set(2, Some(22)).unwrap();
- let v_merged = VectorData::merge(&child1, &child2);
+ let v_merged = VectorData::merge(&child1, &child2).unwrap();
assert_eq!(
BTreeMap::from_iter([(&0, &88), (&1, &44), (&2, &22)]),
v_merged.entries()
@@ -525,10 +532,10 @@
let mut v2 = VectorData::<u8, u8>::default();
let _ = v2.set(1, Some(44)).unwrap();
- let _ = VectorData::merge(&v1, &v2);
- // This is undefined behavior (in the generic sense of the word), it should never happen
- // since set(1, 44) should always happen after observing set(1, 88) and thus increment the
- // version number
+ assert_eq!(
+ VectorData::merge(&v1, &v2).unwrap_err(),
+ crate::MergeError::InconsistentValue
+ );
}
#[test]
diff --git a/betosync/submerge/distributed_time/src/compound_timestamp.rs b/betosync/submerge/distributed_time/src/compound_timestamp.rs
index 615e184..73ee762 100644
--- a/betosync/submerge/distributed_time/src/compound_timestamp.rs
+++ b/betosync/submerge/distributed_time/src/compound_timestamp.rs
@@ -221,6 +221,7 @@
#[cfg(test)]
#[derive_fuzztest::proptest]
+ #[allow(clippy::unwrap_used)]
fn compound_timestamp_round_trip(compound_timestamp: CompoundTimestamp<VectorClock<String>>) {
let mut node_ids = NodeMapping::default();
let (hlc_proto, vector_clock_proto) = compound_timestamp.to_proto(&mut node_ids);
diff --git a/betosync/submerge/distributed_time/src/hybrid_logical_clock.rs b/betosync/submerge/distributed_time/src/hybrid_logical_clock.rs
index fd97ee2..c2dc932 100644
--- a/betosync/submerge/distributed_time/src/hybrid_logical_clock.rs
+++ b/betosync/submerge/distributed_time/src/hybrid_logical_clock.rs
@@ -199,6 +199,7 @@
#[cfg(test)]
#[derive_fuzztest::proptest]
+ #[allow(clippy::unwrap_used)]
fn unnamed_hlc_round_trip(hlc: UnnamedHybridLogicalTimestamp) {
let mut node_ids = NodeMapping::default();
assert_eq!(
@@ -213,6 +214,7 @@
#[cfg(test)]
#[derive_fuzztest::proptest]
+ #[allow(clippy::unwrap_used)]
fn hlc_round_trip(hlc: HybridLogicalTimestamp<String>) {
let mut node_ids = NodeMapping::default();
let (timestamp_proto, updater) = hlc.to_proto(&mut node_ids);
diff --git a/betosync/submerge/distributed_time/src/vector_clock.rs b/betosync/submerge/distributed_time/src/vector_clock.rs
index aa07b0f..456359d 100644
--- a/betosync/submerge/distributed_time/src/vector_clock.rs
+++ b/betosync/submerge/distributed_time/src/vector_clock.rs
@@ -193,6 +193,7 @@
#[cfg(test)]
#[derive_fuzztest::proptest]
+ #[allow(clippy::unwrap_used)]
fn vector_clock_round_trip(vector_clock: VectorClock<String>) {
let mut node_ids = NodeMapping::default();
assert_eq!(
diff --git a/betosync/submerge/submerge/fuzz/Cargo.toml b/betosync/submerge/submerge/fuzz/Cargo.toml
index f9274b0..ca190f9 100644
--- a/betosync/submerge/submerge/fuzz/Cargo.toml
+++ b/betosync/submerge/submerge/fuzz/Cargo.toml
@@ -41,3 +41,7 @@
[[bin]]
name = "delta_round_trip"
doc = false
+
+[[bin]]
+name = "commit_update"
+doc = false
diff --git a/betosync/submerge/submerge/fuzz/src/actor.rs b/betosync/submerge/submerge/fuzz/src/actor.rs
index ae9fb2d..4200df8 100644
--- a/betosync/submerge/submerge/fuzz/src/actor.rs
+++ b/betosync/submerge/submerge/fuzz/src/actor.rs
@@ -132,6 +132,7 @@
match result {
Ok(()) => {}
Err(CommitUpdateError::StorageError(_)) => unreachable!(),
+ Err(CommitUpdateError::MergeError(_)) => unreachable!(),
Err(CommitUpdateError::OlderBaseNeeded {
required_base_version,
}) => {
diff --git a/betosync/submerge/submerge/fuzz/src/bin/commit_update.rs b/betosync/submerge/submerge/fuzz/src/bin/commit_update.rs
new file mode 100644
index 0000000..b195d3c
--- /dev/null
+++ b/betosync/submerge/submerge/fuzz/src/bin/commit_update.rs
@@ -0,0 +1,32 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#![cfg_attr(fuzzing, no_main)]
+
+use submerge::{DeltaApplicationError, DeltaDocument, Document};
+use submerge_fuzz::TestDataStoreConfig;
+
+#[derive_fuzztest::fuzztest]
+fn commit_update(
+ mut doc: Document<TestDataStoreConfig>,
+ update: DeltaDocument<TestDataStoreConfig>,
+) {
+ doc.version = update.base_version.clone();
+ assert!(matches!(
+ update.into_document(&doc),
+ Ok(_)
+ | Err(DeltaApplicationError::BaseVersionTooOld)
+ | Err(DeltaApplicationError::CrdtMergeError(_))
+ ));
+}
diff --git a/betosync/submerge/submerge/src/lib.rs b/betosync/submerge/submerge/src/lib.rs
index 1839af0..a4cba6f 100644
--- a/betosync/submerge/submerge/src/lib.rs
+++ b/betosync/submerge/submerge/src/lib.rs
@@ -199,29 +199,39 @@
/// Error returned when a delta is not applicable to the given base.
#[derive(Debug, Clone, Error)]
-#[error("Delta is not applicable to the given base")]
-pub struct DeltaNotApplicable;
+pub enum DeltaApplicationError {
+ /// Delta is not applicable to the given base, base version is too old.
+ #[error("Delta is not applicable to the given base, base version is too old")]
+ BaseVersionTooOld,
+ /// The CRDT merge failed.
+ #[error("The CRDT merge failed")]
+ CrdtMergeError(#[from] crdt::MergeError),
+}
impl<Conf: DataStoreConfig> DeltaDocument<Conf> {
/// Merge this delta document into the given `base`.
///
/// The given `base` should be generated with [`Document::calculate_delta`] or
/// [`DeltaOwned::into_delta`][crdt::delta::DeltaOwned::into_delta], and must be at least as new
- /// as the [`base_version`][Self::base_version], or `Err(DeltaNotApplicable)` will be returned.
+ /// as the [`base_version`][Self::base_version], or `Err(DeltaApplicationError::BaseVersionTooOld)`
+ /// will be returned.
///
/// The resulting document will contain all changes from both this delta and the `base`, merged
/// together according to the CRDT definitions.
pub fn into_document(
self,
base: &Document<Conf>,
- ) -> Result<Document<Conf>, DeltaNotApplicable> {
+ ) -> Result<Document<Conf>, DeltaApplicationError> {
if let None | Some(Ordering::Less) = base.version.partial_cmp(&self.base_version) {
- Err(DeltaNotApplicable)
+ Err(DeltaApplicationError::BaseVersionTooOld)
} else {
- Ok(Document {
- root: CrdtState::merge(&self.root, &base.root),
- version: VectorClock::least_upper_bound(&self.version, &base.version),
- })
+ match CrdtState::merge(&self.root, &base.root) {
+ Ok(root) => Ok(Document {
+ root,
+ version: VectorClock::least_upper_bound(&self.version, &base.version),
+ }),
+ Err(e) => Err(DeltaApplicationError::CrdtMergeError(e)),
+ }
}
}
}
@@ -239,6 +249,9 @@
/// The storage interface failed to persist the resulting data.
#[error(transparent)]
StorageError(StorageError<Conf>),
+ /// The CRDT merge failed.
+ #[error("The CRDT merge failed.")]
+ MergeError(#[from] crdt::MergeError),
}
impl<Conf: DataStoreConfig> From<TimestampOverflow> for TransactionError<Conf> {
@@ -439,8 +452,8 @@
/// handle the error.
pub fn commit(mut self) -> Result<(), TransactionError<Conf>> {
let handle = self.handle.borrow_mut();
- let merged = CrdtState::merge(&handle.doc.root, &self.delta);
- let merged_network_only = CrdtState::merge(&handle.doc.root, &self.network_delta);
+ let merged = CrdtState::merge(&handle.doc.root, &self.delta)?;
+ let merged_network_only = CrdtState::merge(&handle.doc.root, &self.network_delta)?;
// Network change should consider both content change and subtree_version change.
let has_network_change = merged_network_only != handle.doc.root;
// Local change should only consider content change.
@@ -509,8 +522,11 @@
);
return Ok(());
}
- self.delta = CrdtState::merge(&self.delta, &delta.root);
- self.network_delta = CrdtState::merge(&self.network_delta, &delta.root);
+ let new_delta = CrdtState::merge(&self.delta, &delta.root)?;
+ // Attempt merging with the base now to ensure the network delta is compatible.
+ let _ = CrdtState::merge(&self.handle.borrow_mut().doc.root, &new_delta)?;
+ self.delta = new_delta;
+ self.network_delta = CrdtState::merge(&self.network_delta, &delta.root)?;
self.update_context.version =
VectorClock::least_upper_bound(&self.update_context.version, &delta.version);
Ok(())
@@ -528,6 +544,9 @@
/// The base version needed to successfully update this data store.
required_base_version: VectorClock<Conf::NodeId>,
},
+ /// The CRDT merge failed.
+ #[error("The CRDT merge failed.")]
+ MergeError(#[from] crdt::MergeError),
}
/// Error type used for [`DocumentHandle::modify`].
@@ -607,7 +626,7 @@
return Ok(());
}
let updated_document = Document {
- root: CrdtState::merge(&self.doc.root, &update_message.root),
+ root: CrdtState::merge(&self.doc.root, &update_message.root)?,
version: VectorClock::least_upper_bound(&self.doc.version, &update_message.version),
};
debug!(
diff --git a/betosync/submerge/submerge_java/lib/src/main/java/com/google/android/submerge/DataStore.java b/betosync/submerge/submerge_java/lib/src/main/java/com/google/android/submerge/DataStore.java
index 2b383d6..a54b327 100644
--- a/betosync/submerge/submerge_java/lib/src/main/java/com/google/android/submerge/DataStore.java
+++ b/betosync/submerge/submerge_java/lib/src/main/java/com/google/android/submerge/DataStore.java
@@ -143,10 +143,11 @@
* failures while trying to write to disk.
* @throws OlderBaseNeededException if the {@code updateMessage} given doesn't cover an old enough
* base to be applied.
+ * @throws MergeException if the {@code updateMessage} has incompatible changes.
* @throws RuntimeException if this transaction has already been committed.
*/
public void commitNetworkUpdate(String docId, byte[] updateMessage)
- throws TransactionException, OlderBaseNeededException {
+ throws TransactionException, OlderBaseNeededException, MergeException {
synchronized (this.handle) {
nativeCommitNetworkUpdate(this.handle.handle, docId, updateMessage);
}
diff --git a/betosync/submerge/submerge_java/lib/src/main/java/com/google/android/submerge/DocumentTransaction.java b/betosync/submerge/submerge_java/lib/src/main/java/com/google/android/submerge/DocumentTransaction.java
index 94214fe..195ec0c 100644
--- a/betosync/submerge/submerge_java/lib/src/main/java/com/google/android/submerge/DocumentTransaction.java
+++ b/betosync/submerge/submerge_java/lib/src/main/java/com/google/android/submerge/DocumentTransaction.java
@@ -204,8 +204,9 @@
* @param update The update message from the network.
* @throws OlderBaseNeededException if the {@code updateMessage} given doesn't cover an old enough
* base to be applied.
+ * @throws MergeException if the {@code updateMessage} has incompatible changes.
*/
- public void mergeNetworkUpdate(byte[] update) throws OlderBaseNeededException {
+ public void mergeNetworkUpdate(byte[] update) throws OlderBaseNeededException, MergeException {
synchronized (this.handle) {
nativeMergeNetworkUpdate(this.handle.handle, update);
}
@@ -297,4 +298,4 @@
private static native @SubmergeDataTypeHandleId long nativeNewVectorData(
@DocumentTransactionHandleId long handle);
-}
\ No newline at end of file
+}
diff --git a/betosync/submerge/submerge_java/lib/src/main/java/com/google/submerge/MergeException.java b/betosync/submerge/submerge_java/lib/src/main/java/com/google/submerge/MergeException.java
new file mode 100644
index 0000000..2858c1c
--- /dev/null
+++ b/betosync/submerge/submerge_java/lib/src/main/java/com/google/submerge/MergeException.java
@@ -0,0 +1,30 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.android.submerge;
+
+/**
+ * Thrown when a CRDT merge fails.
+ *
+ * <p>A merge can fail when some of the peers do not follow the expected algorithm for generating
+ * update messages, or fail to validate for necessary preconditions for mutation operations.
+ * Examples include sending different updates with the same timestamp, creating a data store from
+ * corrupted update messages or persisted bytes, or different peers creating their data store from
+ * conflicting schemas.
+ */
+public class MergeException extends Exception {
+ MergeException(String message) {
+ super(message);
+ }
+}
diff --git a/betosync/submerge/submerge_java/lib/src/test/java/com/google/android/submerge/DataStoreTest.java b/betosync/submerge/submerge_java/lib/src/test/java/com/google/android/submerge/DataStoreTest.java
index ee74723..c6be498 100644
--- a/betosync/submerge/submerge_java/lib/src/test/java/com/google/android/submerge/DataStoreTest.java
+++ b/betosync/submerge/submerge_java/lib/src/test/java/com/google/android/submerge/DataStoreTest.java
@@ -17,7 +17,7 @@
package com.google.android.submerge;
import static com.google.common.truth.Truth.assertThat;
-import static java.nio.charset.StandardCharsets.UTF_8;
+
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
@@ -29,11 +29,14 @@
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
import androidx.annotation.Nullable;
+
import com.google.android.submerge.StorageInterface.StorageException;
import com.google.android.submerge.SubmergeDataType.SubmergeDataTypeHandleId;
import com.google.errorprone.annotations.MustBeClosed;
-import java.util.HashMap;
+
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
@@ -47,6 +50,8 @@
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
+import java.util.HashMap;
+
@RunWith(JUnit4.class)
public class DataStoreTest {
@Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
@@ -927,6 +932,103 @@
assertThat(v5.compare(v1)).isEqualTo(VersionVector.NEWER);
}
+ @Test
+ public void testCommitNetworkUpdate_incompatibleType() throws Exception {
+ // 1. Create two datastores for the same actor to simulate divergent states.
+ try (DataStore<String> dataStore1 = createDataStore("actor1");
+ DataStore<String> dataStore2 =
+ new DataStore<>(
+ "actor1",
+ networkInterface,
+ new Storage(),
+ timestampProvider,
+ new StringConverter())) {
+ // 2. Create a base document in store 1 and sync it to store 2.
+ try (DocumentTransaction<String> doc = dataStore1.newDocumentTransaction("mydoc")) {
+ doc.setRoot(doc.newRegister());
+ doc.commit();
+ }
+ byte[] baseUpdate = dataStore1.getFullUpdateMessage("mydoc");
+ dataStore2.commitNetworkUpdate("mydoc", baseUpdate);
+ VersionVector baseVersion = dataStore1.getDocumentVersion("mydoc");
+
+ // 3. Mock timestamp provider to produce identical timestamps to force a content merge.
+ when(timestampProvider.now()).thenReturn(1000L);
+
+ // 4. Create divergent updates with incompatible types from each actor.
+ try (DocumentTransaction<String> doc = dataStore1.newDocumentTransaction("mydoc")) {
+ doc.setRoot(doc.newMap());
+ doc.commit();
+ }
+
+ try (DocumentTransaction<String> doc = dataStore2.newDocumentTransaction("mydoc")) {
+ doc.setRoot(doc.newSet());
+ doc.commit();
+ }
+ byte[] update2;
+ try (DocumentTransaction<String> doc = dataStore2.newDocumentTransaction("mydoc")) {
+ // Make another commit to advance the version vector.
+ ((SubmergeSet<String>) doc.getRoot()).add("another change");
+ doc.commit();
+ update2 = dataStore2.calculateDelta("mydoc", baseVersion);
+ }
+
+ // 5. Now, apply update from store 2 to store 1. This should fail with a merge exception.
+ assertThrows(MergeException.class, () -> dataStore1.commitNetworkUpdate("mydoc", update2));
+ }
+ }
+
+ @Test
+ public void testMergeNetworkUpdateInTransaction_incompatibleType() throws Exception {
+ // 1. Setup two datastores for the same actor.
+ try (DataStore<String> dataStore1 = createDataStore("actor1");
+ DataStore<String> dataStore2 =
+ new DataStore<>(
+ "actor1",
+ networkInterface,
+ new Storage(),
+ timestampProvider,
+ new StringConverter())) {
+
+ // 2. Create a base document and sync.
+ try (DocumentTransaction<String> doc = dataStore1.newDocumentTransaction("mydoc")) {
+ doc.setRoot(doc.newRegister());
+ doc.commit();
+ }
+ byte[] baseUpdate = dataStore1.getFullUpdateMessage("mydoc");
+ dataStore2.commitNetworkUpdate("mydoc", baseUpdate);
+ VersionVector baseVersion = dataStore1.getDocumentVersion("mydoc");
+
+ // 3. Mock timestamp to create conflicting updates.
+ when(timestampProvider.now()).thenReturn(1000L);
+
+ // 4. Create divergent updates with incompatible types from each actor.
+ try (DocumentTransaction<String> doc = dataStore1.newDocumentTransaction("mydoc")) {
+ doc.setRoot(doc.newMap());
+ doc.commit();
+ }
+
+ try (DocumentTransaction<String> doc = dataStore2.newDocumentTransaction("mydoc")) {
+ doc.setRoot(doc.newSet());
+ doc.commit();
+ }
+ byte[] update2;
+ try (DocumentTransaction<String> doc = dataStore2.newDocumentTransaction("mydoc")) {
+ // Make another commit to advance the version vector.
+ ((SubmergeSet<String>) doc.getRoot()).add("another change");
+ doc.commit();
+ update2 = dataStore2.calculateDelta("mydoc", baseVersion);
+ }
+
+
+ // 5.Now, merge update from store 2 to store 1. This should fail with a merge exception.
+ try (DocumentTransaction<String> doc = dataStore1.newDocumentTransaction("mydoc")) {
+ // Now merge the incompatible update from store 1.
+ assertThrows(MergeException.class, () -> doc.mergeNetworkUpdate(update2));
+ }
+ }
+ }
+
private static class StringConverter implements Converter<String> {
@Override
diff --git a/betosync/submerge/submerge_java/src/class/data_store.rs b/betosync/submerge/submerge_java/src/class/data_store.rs
index 1aec062..a55ebf9 100644
--- a/betosync/submerge/submerge_java/src/class/data_store.rs
+++ b/betosync/submerge/submerge_java/src/class/data_store.rs
@@ -28,7 +28,8 @@
use crate::{
class::{
exceptions::{
- older_base_needed_exception, transaction_exception, transaction_exception_with_cause,
+ merge_exception, older_base_needed_exception, transaction_exception,
+ transaction_exception_with_cause,
},
storage_interface::StorageError,
},
@@ -186,6 +187,7 @@
let version_handle = VersionVectorHandle::allocate(|| required_base_version)?;
Err(older_base_needed_exception(env, version_handle)?)?
}
+ Err(CommitUpdateError::MergeError(err)) => Err(merge_exception(err.to_string()))?,
Err(err) => Err(transaction_exception(err.to_string()))?,
}
Ok(())
diff --git a/betosync/submerge/submerge_java/src/class/document_transaction.rs b/betosync/submerge/submerge_java/src/class/document_transaction.rs
index 1f2f680..816f1b4 100644
--- a/betosync/submerge/submerge_java/src/class/document_transaction.rs
+++ b/betosync/submerge/submerge_java/src/class/document_transaction.rs
@@ -29,8 +29,8 @@
use crate::{
class::{
exceptions::{
- older_base_needed_exception, ownership_exception, transaction_exception,
- transaction_exception_with_cause,
+ merge_exception, older_base_needed_exception, ownership_exception,
+ transaction_exception, transaction_exception_with_cause,
},
storage_interface::StorageError,
submerge_data_type::{CrdtType, JavaSubmergeDataType, SubmergeDataTypeHandle, TreeRoot},
@@ -144,6 +144,7 @@
<&JThrowable>::from(throwable.as_obj()),
)?)?
}
+ Err(TransactionError::MergeError(err)) => Err(merge_exception(err.to_string()))?,
Err(err) => Err(transaction_exception(err.to_string()))?,
}
Ok(())
@@ -171,6 +172,7 @@
let version_handle = VersionVectorHandle::allocate(|| required_base_version)?;
Err(older_base_needed_exception(env, version_handle)?)?
}
+ Err(CommitUpdateError::MergeError(err)) => Err(merge_exception(err.to_string()))?,
Err(err) => Err(runtime_exception(err.to_string()))?,
}
})
diff --git a/betosync/submerge/submerge_java/src/class/exceptions.rs b/betosync/submerge/submerge_java/src/class/exceptions.rs
index e8ca75a..4f4fd69 100644
--- a/betosync/submerge/submerge_java/src/class/exceptions.rs
+++ b/betosync/submerge/submerge_java/src/class/exceptions.rs
@@ -58,6 +58,13 @@
}
}
+pub fn merge_exception(msg: impl Into<String>) -> jni::errors::Exception {
+ jni::errors::Exception {
+ class: "com/google/android/submerge/MergeException".into(),
+ msg: msg.into(),
+ }
+}
+
static TRANSACTION_EXCEPTION_CLASS: ClassDesc =
ClassDesc::new("com/google/android/submerge/TransactionException");
diff --git a/common/Cargo.lock b/common/Cargo.lock
index e328fbb..02ffad3 100644
--- a/common/Cargo.lock
+++ b/common/Cargo.lock
@@ -445,10 +445,14 @@
"anyhow",
"chrono",
"crossbeam",
+ "googletest",
"mockito",
- "protobuf 3.7.2",
- "protobuf-codegen 3.7.2",
+ "nom",
+ "openssl",
+ "protobuf 4.32.1-release",
+ "protobuf-codegen",
"protobuf-json-mapping",
+ "protobuf-well-known-types",
"rand",
"reqwest",
"tempfile",
@@ -475,6 +479,16 @@
]
[[package]]
+name = "collection_history"
+version = "0.1.0"
+dependencies = [
+ "chrono",
+ "googletest",
+ "prettytable-rs",
+ "thiserror 2.0.12",
+]
+
+[[package]]
name = "colorchoice"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -615,6 +629,27 @@
checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
[[package]]
+name = "csv"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938"
+dependencies = [
+ "csv-core",
+ "itoa",
+ "ryu",
+ "serde_core",
+]
+
+[[package]]
+name = "csv-core"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
name = "directories"
version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -624,6 +659,16 @@
]
[[package]]
+name = "dirs-next"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
+dependencies = [
+ "cfg-if",
+ "dirs-sys-next",
+]
+
+[[package]]
name = "dirs-sys"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -636,6 +681,17 @@
]
[[package]]
+name = "dirs-sys-next"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
+dependencies = [
+ "libc",
+ "redox_users",
+ "winapi",
+]
+
+[[package]]
name = "dispatch2"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -663,6 +719,12 @@
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
[[package]]
+name = "encode_unicode"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
+
+[[package]]
name = "encoding_rs"
version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -981,15 +1043,6 @@
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
-name = "home"
-version = "0.5.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf"
-dependencies = [
- "windows-sys 0.59.0",
-]
-
-[[package]]
name = "http"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1734,9 +1787,9 @@
[[package]]
name = "openssl"
-version = "0.10.72"
+version = "0.10.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da"
+checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8"
dependencies = [
"bitflags 2.8.0",
"cfg-if",
@@ -1765,13 +1818,23 @@
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]]
-name = "openssl-sys"
-version = "0.9.107"
+name = "openssl-src"
+version = "300.5.4+3.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07"
+checksum = "a507b3792995dae9b0df8a1c1e3771e8418b7c2d9f0baeba32e6fe8b06c7cb72"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
dependencies = [
"cc",
"libc",
+ "openssl-src",
"pkg-config",
"vcpkg",
]
@@ -1835,8 +1898,8 @@
dependencies = [
"googletest",
"ph_client_sys",
- "protobuf 4.32.0-release",
- "protobuf-codegen 4.32.0-release",
+ "protobuf 4.32.1-release",
+ "protobuf-codegen",
"tokio",
]
@@ -1980,6 +2043,20 @@
]
[[package]]
+name = "prettytable-rs"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eea25e07510aa6ab6547308ebe3c036016d162b8da920dbb079e3ba8acf3d95a"
+dependencies = [
+ "csv",
+ "encode_unicode",
+ "is-terminal",
+ "lazy_static",
+ "term",
+ "unicode-width",
+]
+
+[[package]]
name = "proc-macro2"
version = "1.0.97"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2001,9 +2078,9 @@
[[package]]
name = "protobuf"
-version = "4.32.0-release"
+version = "4.32.1-release"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "951f19b825af28f7c7ce3daaf71e4e8961394dcc410b3a0aac6e4e0e5b7e2d2b"
+checksum = "72bc0777c4f479a4386e4fc4a04c1ba8fbe9f883bc21268e3073473ad9b07dcc"
dependencies = [
"cc",
"paste",
@@ -2011,24 +2088,9 @@
[[package]]
name = "protobuf-codegen"
-version = "3.7.2"
+version = "4.32.1-release"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d3976825c0014bbd2f3b34f0001876604fe87e0c86cd8fa54251530f1544ace"
-dependencies = [
- "anyhow",
- "once_cell",
- "protobuf 3.7.2",
- "protobuf-parse",
- "regex",
- "tempfile",
- "thiserror 1.0.69",
-]
-
-[[package]]
-name = "protobuf-codegen"
-version = "4.32.0-release"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7756fa998bcc36bb53ff0d857043d96e734956b8ef46eff17d63f550db629b93"
+checksum = "2e5137852fb606bac9bdba5ef3bb63d77756fdba0378522f6357094cb4e68cae"
[[package]]
name = "protobuf-json-mapping"
@@ -2042,22 +2104,6 @@
]
[[package]]
-name = "protobuf-parse"
-version = "3.7.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b4aeaa1f2460f1d348eeaeed86aea999ce98c1bded6f089ff8514c9d9dbdc973"
-dependencies = [
- "anyhow",
- "indexmap",
- "log",
- "protobuf 3.7.2",
- "protobuf-support",
- "tempfile",
- "thiserror 1.0.69",
- "which",
-]
-
-[[package]]
name = "protobuf-support"
version = "3.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2067,6 +2113,16 @@
]
[[package]]
+name = "protobuf-well-known-types"
+version = "4.32.1-release"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e786f75ec38a6d6c2cef93e0aaf6f039babff7a61bdbfd4da59ff5c4a7f185f9"
+dependencies = [
+ "protobuf 4.32.1-release",
+ "protobuf-codegen",
+]
+
+[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2382,18 +2438,28 @@
[[package]]
name = "serde"
-version = "1.0.217"
+version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
-version = "1.0.217"
+version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
@@ -2601,6 +2667,17 @@
]
[[package]]
+name = "term"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f"
+dependencies = [
+ "dirs-next",
+ "rustversion",
+ "winapi",
+]
+
+[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2804,6 +2881,12 @@
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
+name = "unicode-width"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
+
+[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2966,18 +3049,6 @@
]
[[package]]
-name = "which"
-version = "4.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7"
-dependencies = [
- "either",
- "home",
- "once_cell",
- "rustix",
-]
-
-[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/common/Cargo.toml b/common/Cargo.toml
index 928c82b..ea02608 100644
--- a/common/Cargo.toml
+++ b/common/Cargo.toml
@@ -1,6 +1,7 @@
[workspace]
members = [
"build_scripts",
+ "collection_history",
"cmd_runner",
"handle_map",
"lock_adapter",
@@ -37,6 +38,7 @@
# from crates.io
anyhow = "1.0.75"
+chrono = "0.4"
thiserror = "2.0.4"
arbitrary = "1.3.2"
clap = { version = "4.5.13", features = ["derive"] }
diff --git a/common/cmd_runner/src/license_checker.rs b/common/cmd_runner/src/license_checker.rs
index 19286bb..101cf1d 100644
--- a/common/cmd_runner/src/license_checker.rs
+++ b/common/cmd_runner/src/license_checker.rs
@@ -80,7 +80,7 @@
"Google LLC".to_string(),
)),
)? {
- println!("Added header: {:?}", p);
+ println!("Added header: {p:?}");
}
Ok(())
diff --git a/common/collection_history/Cargo.toml b/common/collection_history/Cargo.toml
new file mode 100644
index 0000000..6f8d561
--- /dev/null
+++ b/common/collection_history/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "collection_history"
+version.workspace = true
+edition.workspace = true
+publish.workspace = true
+license.workspace = true
+
+[dependencies]
+chrono.workspace = true
+prettytable-rs = "0.10"
+thiserror.workspace = true
+
+[dev-dependencies]
+googletest.workspace = true
diff --git a/common/collection_history/src/lib.rs b/common/collection_history/src/lib.rs
new file mode 100644
index 0000000..a2e61d2
--- /dev/null
+++ b/common/collection_history/src/lib.rs
@@ -0,0 +1,314 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+//
+// you may not use this file except in compliance with the License.
+//
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+//
+// distributed under the License is distributed on an "AS IS" BASIS,
+//
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//
+// See the License for the specific language governing permissions and
+//
+// limitations under the License.
+
+//! A collection that tracks all previous entries that have been inserted and removed and provides a nice-looking output when displayed.
+
+use chrono::{DateTime, Utc};
+use prettytable::{Cell, Row, Table};
+use std::{collections::HashMap, fmt::Debug};
+use thiserror::Error;
+
+/// The insertion for this key-value pair failed.
+#[derive(Error, Debug)]
+#[error("Key {:?} already exists. Cannot insert value {:?}. Key was originally inserted at {}", key, value, insertion_time.to_rfc3339())]
+pub struct InsertError<K: Debug, V: Debug> {
+ pub key: K,
+ pub value: V,
+ insertion_time: DateTime<Utc>,
+}
+
+/// The update operation for this key-value pair failed.
+#[derive(Error, Debug)]
+#[error("Key '{:?}' does not exist. Cannot update value {:?}.", key, value)]
+pub struct UpdateError<K: Debug, V: Debug> {
+ pub key: K,
+ pub value: V,
+}
+
+/// The removal operation for this key failed.
+#[derive(Error, Debug)]
+pub enum RemoveError<K: Debug, V: Debug> {
+ #[error(
+ "Key '{:?}' does not exist. Key was originally removed at time {:?} with value {:?}",
+ key,
+ old_value,
+ removal_time.to_rfc3339()
+ )]
+ KeyAlreadyRemoved {
+ key: K,
+ old_value: V,
+ removal_time: DateTime<Utc>,
+ },
+ #[error("Key '{:?}' does not exist", key)]
+ KeyDoesNotExist { key: K },
+}
+
+/// The get operation for this key failed.
+#[derive(Error, Debug)]
+pub enum GetError<K: Debug, V: Debug> {
+ #[error(
+ "Key '{:?}' does not exist. Key was originally removed at time {:?} with value {:?}",
+ key,
+ old_value,
+ removal_time.to_rfc3339()
+ )]
+ KeyAlreadyRemoved {
+ key: K,
+ old_value: V,
+ removal_time: DateTime<Utc>,
+ },
+ #[error("Key '{:?}' does not exist", key)]
+ KeyDoesNotExist { key: K },
+}
+
+#[derive(Clone, Debug)]
+struct Session<K, V> {
+ key: K,
+ value: V,
+ insertion_time: DateTime<Utc>,
+ last_update_time: DateTime<Utc>,
+ removal_time: Option<DateTime<Utc>>,
+}
+
+impl<K, V> Session<K, V> {
+ fn new(key: K, value: V) -> Self {
+ let now = Utc::now();
+ Self {
+ key,
+ value,
+ insertion_time: now,
+ last_update_time: now,
+ removal_time: None,
+ }
+ }
+
+ fn set_value(&mut self, value: V) -> V {
+ let old_value = std::mem::replace(&mut self.value, value);
+ self.update_last_updated_time();
+ old_value
+ }
+
+ fn update_last_updated_time(&mut self) {
+ self.last_update_time = Utc::now();
+ }
+
+ fn update_removal_time(&mut self) {
+ self.update_last_updated_time();
+ self.removal_time = Some(Utc::now());
+ }
+
+ fn get_duration(&self) -> i64 {
+ let end_time = self.removal_time.unwrap_or_else(Utc::now);
+ end_time.timestamp_millis() - self.insertion_time.timestamp_millis()
+ }
+}
+
+enum EventStatus {
+ Active,
+ Complete,
+}
+
+impl std::fmt::Display for EventStatus {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ EventStatus::Active => write!(f, "ACTIVE"),
+ EventStatus::Complete => write!(f, "COMPLETE"),
+ }
+ }
+}
+
+/// A map that tracks when keys were added and removed.
+#[derive(Clone)]
+pub struct CollectionHistory<K, V> {
+ max_history: usize,
+ past_sessions: Vec<Session<K, V>>,
+ active_sessions: HashMap<K, Session<K, V>>,
+}
+
+impl<K, V> CollectionHistory<K, V>
+where
+ K: Eq + std::hash::Hash + Clone + std::fmt::Debug,
+ V: std::fmt::Debug + Clone,
+{
+ /// Creates a new [CollectionHistory] with a maximum number of `max_history` historical entries tracked.
+ pub fn new(max_history: usize) -> Self {
+ Self {
+ max_history,
+ past_sessions: Vec::new(),
+ active_sessions: HashMap::new(),
+ }
+ }
+
+ /// Inserts a key-value pair. If the key already exists, an InsertError with the key and value is returned.
+ pub fn insert(&mut self, key: K, value: V) -> Result<(), InsertError<K, V>> {
+ if let Some(session) = self.active_sessions.get(&key) {
+ return Err(InsertError {
+ key,
+ value,
+ insertion_time: session.insertion_time,
+ });
+ }
+
+ self.active_sessions
+ .insert(key.clone(), Session::new(key, value));
+ self.prune_old_sessions();
+ Ok(())
+ }
+
+ /// Updates the key to the new value, returning the old value if successful.
+ pub fn update(&mut self, key: K, value: V) -> Result<V, UpdateError<K, V>> {
+ if let Some(session) = self.active_sessions.get_mut(&key) {
+ return Ok(session.set_value(value));
+ }
+ Err(UpdateError { key, value })
+ }
+
+ /// Removes the value corresponding to key, if it exists.
+ pub fn remove(&mut self, key: K) -> Result<V, RemoveError<K, V>> {
+ if let Some(mut session) = self.active_sessions.remove(&key) {
+ session.update_removal_time();
+ let value = session.value.clone();
+ self.past_sessions.insert(0, session);
+ self.prune_old_sessions();
+ Ok(value)
+ } else if let Some(session) = self.find_old_session(&key) {
+ return Err(RemoveError::KeyAlreadyRemoved {
+ key,
+ old_value: session.value.clone(),
+ removal_time: session.last_update_time,
+ });
+ } else {
+ return Err(RemoveError::KeyDoesNotExist { key });
+ }
+ }
+
+ /// Returns true if key is currently active.
+ pub fn contains_key(&self, key: &K) -> bool {
+ self.active_sessions.contains_key(key)
+ }
+
+ /// Returns the value corresponding to `key` if it exists.
+ pub fn get(&self, key: &K) -> Result<&V, GetError<K, &V>> {
+ if let Some(session) = self.active_sessions.get(key) {
+ Ok(&session.value)
+ } else if let Some(session) = self.find_old_session(key) {
+ Err(GetError::KeyAlreadyRemoved {
+ key: key.clone(),
+ old_value: &session.value,
+ removal_time: session.last_update_time,
+ })
+ } else {
+ Err(GetError::KeyDoesNotExist { key: key.clone() })
+ }
+ }
+
+ /// Clears this [CollectionHistory].
+ pub fn clear(&mut self) {
+ let keys: Vec<K> = self.active_sessions.keys().cloned().collect();
+ for key in keys {
+ let _ = self.remove(key);
+ }
+ }
+
+ /// Returns an iterator over the active keys in this [CollectionHistory].
+ pub fn keys(&self) -> impl Iterator<Item = &K> {
+ self.active_sessions.keys()
+ }
+
+ /// Returns an iterator over the active values in this [CollectionHistory].
+ pub fn values(&self) -> impl Iterator<Item = &V> {
+ self.active_sessions.values().map(|s| &s.value)
+ }
+
+ /// Returns an iterator over the active entries in this [CollectionHistory].
+ pub fn entry_set(&self) -> impl Iterator<Item = (&K, &V)> {
+ self.active_sessions.iter().map(|(k, s)| (k, &s.value))
+ }
+
+ /// Returns the number of entries in this [CollectionHistory].
+ pub fn len(&self) -> usize {
+ self.active_sessions.len()
+ }
+
+ /// Returns true if there are no active sessions in this [CollectionHistory].
+ pub fn is_empty(&self) -> bool {
+ self.active_sessions.is_empty()
+ }
+
+ fn history_size(&self) -> usize {
+ self.past_sessions.len() + self.len()
+ }
+
+ fn prune_old_sessions(&mut self) {
+ if self.active_sessions.len() >= self.max_history {
+ self.past_sessions.clear();
+ return;
+ }
+
+ while self.history_size() > self.max_history {
+ self.past_sessions.remove(0);
+ }
+ }
+
+ fn find_old_session(&self, key: &K) -> Option<&Session<K, V>> {
+ self.past_sessions.iter().find(|s| &s.key == key)
+ }
+}
+
+impl<K: Debug, V: Debug> std::fmt::Display for CollectionHistory<K, V> {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let mut sorted_sessions: Vec<&Session<K, V>> = self.active_sessions.values().collect();
+ sorted_sessions.sort_by(|a, b| b.last_update_time.cmp(&a.last_update_time));
+
+ let mut table = Table::new();
+ table.add_row(Row::new(vec![
+ Cell::new("Key"),
+ Cell::new("Value"),
+ Cell::new("Status"),
+ Cell::new("Start Time"),
+ Cell::new("End Time"),
+ Cell::new("Duration"),
+ ]));
+
+ for session in sorted_sessions {
+ table.add_row(Row::new(vec![
+ Cell::new(&format!("{:?}", &session.key)),
+ Cell::new(&format!("{:?}", &session.value)),
+ Cell::new(&EventStatus::Active.to_string()),
+ Cell::new(&session.insertion_time.to_rfc3339()),
+ Cell::new(&session.last_update_time.to_rfc3339()),
+ Cell::new(&format!("{}ms", session.get_duration())),
+ ]));
+ }
+
+ for session in &self.past_sessions {
+ table.add_row(Row::new(vec![
+ Cell::new(&format!("{:?}", &session.key)),
+ Cell::new(&format!("{:?}", &session.value)),
+ Cell::new(&EventStatus::Complete.to_string()),
+ Cell::new(&session.insertion_time.to_rfc3339()),
+ Cell::new(&session.last_update_time.to_rfc3339()),
+ Cell::new(&format!("{}ms", session.get_duration())),
+ ]));
+ }
+
+ write!(f, "{table}")
+ }
+}
diff --git a/common/collection_history/tests/tests.rs b/common/collection_history/tests/tests.rs
new file mode 100644
index 0000000..2b1cd50
--- /dev/null
+++ b/common/collection_history/tests/tests.rs
@@ -0,0 +1,97 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+//
+// you may not use this file except in compliance with the License.
+//
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+//
+// distributed under the License is distributed on an "AS IS" BASIS,
+//
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//
+// See the License for the specific language governing permissions and
+//
+// limitations under the License.
+
+use collection_history::CollectionHistory;
+use googletest::prelude::*;
+
+#[gtest]
+fn insert_remove_update_fails() {
+ let mut map = CollectionHistory::<u64, String>::new(10);
+
+ assert!(map.insert(1, "Hello".to_string()).is_ok());
+ assert!(map.remove(1).is_ok());
+ assert!(map.update(1, "Bye".to_string()).is_err());
+ assert!(map.is_empty())
+}
+
+#[gtest]
+fn insert_remove_insert() {
+ let mut map = CollectionHistory::<u64, String>::new(10);
+
+ assert!(map.insert(1, "Hello".to_string()).is_ok());
+ assert!(map.remove(1).is_ok());
+ assert!(map.insert(1, "Bye".to_string()).is_ok());
+ assert_that!(
+ map.entry_set()
+ .map(|(k, v)| (*k, v.clone()))
+ .collect::<Vec<(u64, String)>>(),
+ contains(eq(&(1, "Bye".to_string())))
+ )
+}
+
+#[gtest]
+fn remove_remove_fails() {
+ let mut map = CollectionHistory::<u64, String>::new(10);
+
+ assert!(map.insert(1, "Hello".to_string()).is_ok());
+ assert!(map.remove(1).is_ok());
+ assert!(map.remove(1).is_err());
+ assert!(map.is_empty())
+}
+
+#[gtest]
+fn clear() {
+ let mut map = CollectionHistory::<u64, String>::new(10);
+
+ assert!(map.insert(1, "Hello".to_string()).is_ok());
+ map.clear();
+ assert_that!(map.len(), eq(0));
+}
+
+#[gtest]
+fn history_limit_does_not_apply_to_active() {
+ let mut map = CollectionHistory::<u64, String>::new(1);
+ assert!(map.insert(1, "Hello".to_string()).is_ok());
+ assert!(map.insert(2, "Bye".to_string()).is_ok());
+
+ assert_that!(map.len(), eq(2));
+}
+
+#[gtest]
+fn update_returns_old_value() {
+ let mut map = CollectionHistory::<u64, String>::new(1);
+ assert!(map.insert(1, "Hello".to_string()).is_ok());
+ assert_that!(
+ map.update(1, "Bye".to_string()),
+ matches_pattern!(Ok("Hello"))
+ );
+}
+
+/*
+#[gtest]
+fn print_format() -> Result<()> {
+ let mut map = CollectionHistory::<u64, String>::new(1);
+ assert!(map.insert(1, "Hello".to_string()).is_ok());
+ assert!(map.insert(2, "Bye".to_string()).is_ok());
+
+ eprintln!("{}", map);
+ fail!()
+}
+*/
diff --git a/common/cooperative_cleaner/lib/build.gradle.kts b/common/cooperative_cleaner/lib/build.gradle.kts
index dba0308..5232e35 100644
--- a/common/cooperative_cleaner/lib/build.gradle.kts
+++ b/common/cooperative_cleaner/lib/build.gradle.kts
@@ -14,44 +14,45 @@
* limitations under the License.
*/
-import net.ltgt.gradle.errorprone.errorprone;
+import net.ltgt.gradle.errorprone.errorprone
plugins {
- `java-library`
- id("net.ltgt.errorprone") version "4.0.0"
+ `java-library`
+ id("net.ltgt.errorprone") version "4.3.0"
}
java {
- // Gradle JUnit test finder doesn't support Java 21 class files.
- sourceCompatibility = JavaVersion.VERSION_1_9
- targetCompatibility = JavaVersion.VERSION_1_9
+ // Gradle JUnit test finder doesn't support Java 21 class files.
+ sourceCompatibility = JavaVersion.VERSION_1_9
+ targetCompatibility = JavaVersion.VERSION_1_9
}
repositories {
- mavenCentral()
- google()
+ mavenCentral()
+ google()
}
dependencies {
- compileOnly("androidx.annotation:annotation:1.6.0")
- implementation("com.google.errorprone:error_prone_core:2.28.0")
+ compileOnly("androidx.annotation:annotation:1.6.0")
+ errorprone("com.google.errorprone:error_prone_core:2.41.0")
+ implementation("org.checkerframework:checker-qual:3.51.0")
- testImplementation("junit:junit:4.13")
- testImplementation("com.google.truth:truth:1.1.4")
- testImplementation("org.mockito:mockito-core:5.+")
+ testImplementation("junit:junit:4.13")
+ testImplementation("com.google.truth:truth:1.1.4")
+ testImplementation("org.mockito:mockito-core:5.+")
}
tasks.withType<JavaCompile>().configureEach {
- options.errorprone {
- error("CheckReturnValue")
- error("UnnecessaryStaticImport")
- error("WildcardImport")
- error("RemoveUnusedImports")
- error("ReturnMissingNullable")
- error("FieldMissingNullable")
- error("AnnotationPosition")
- error("CheckedExceptionNotThrown")
- error("NonFinalStaticField")
- error("InvalidLink")
- }
+ options.errorprone {
+ error("CheckReturnValue")
+ error("UnnecessaryStaticImport")
+ error("WildcardImport")
+ error("RemoveUnusedImports")
+ error("ReturnMissingNullable")
+ error("FieldMissingNullable")
+ error("AnnotationPosition")
+ error("CheckedExceptionNotThrown")
+ error("NonFinalStaticField")
+ error("InvalidLink")
+ }
}
diff --git a/common/rust-toolchain.toml b/common/rust-toolchain.toml
index 0193dee..e88baf1 100644
--- a/common/rust-toolchain.toml
+++ b/common/rust-toolchain.toml
@@ -1,2 +1,2 @@
[toolchain]
-channel = "1.83.0"
+channel = "1.88.0"
diff --git a/nearby/.gitignore b/nearby/.gitignore
index 526b951..a633d22 100644
--- a/nearby/.gitignore
+++ b/nearby/.gitignore
@@ -7,3 +7,4 @@
build/
project.xcworkspace/
xcuserdata/
+.gemini/
diff --git a/nearby/CMakeLists.txt b/nearby/CMakeLists.txt
index d493688..6544d73 100644
--- a/nearby/CMakeLists.txt
+++ b/nearby/CMakeLists.txt
@@ -22,6 +22,9 @@
set(NEARBY_ROOT ${CMAKE_SOURCE_DIR})
set(THIRD_PARTY_DIR ${BETO_CORE_ROOT}/third_party)
+set(CMAKE_C_COMPILER ${CC})
+set(CMAKE_CXX_COMPILER ${CXX})
+
set(CMAKE_C_FLAGS_DEBUG "-DDEBUG")
set(CMAKE_CXX_FLAGS_DEBUG "-DDEBUG")
if (UNIX)
diff --git a/nearby/Cargo.toml b/nearby/Cargo.toml
index 2b1bab1..b416e46 100644
--- a/nearby/Cargo.toml
+++ b/nearby/Cargo.toml
@@ -8,6 +8,8 @@
"connections/ukey2/ukey2_jni",
"connections/ukey2/ukey2_proto",
"connections/ukey2/ukey2_shell",
+ "connections/nc_wire_proto",
+ "connections/nc_multiplex_proto",
"crypto/crypto_provider",
"crypto/crypto_provider_rustcrypto",
"crypto/crypto_provider_stubs",
@@ -36,6 +38,7 @@
"presence/test_vector_hkdf",
"presence/xts_aes",
"presence/xts_aes/fuzz",
+ "unified_protocol/up_adv",
]
default-members = ["build_scripts"]
@@ -66,6 +69,8 @@
[workspace.dependencies]
# local crates
+connections_adv = { path = "connections/connections_adv/connections_adv" }
+mediums_adv = { path = "connections/connections_adv/mediums_adv" }
array_ref = { path = "presence/array_ref" }
array_view = { path = "presence/array_view" }
ble = { path = "medium/ble" }
@@ -82,7 +87,11 @@
crypto_provider_stubs = { path = "crypto/crypto_provider_stubs" }
crypto_provider_test = { path = "crypto/crypto_provider_test" }
device_info = { path = "device_info" }
+event_loop = { path = "presence/event_loop" }
+np_discovery = { path = "presence/engine/presence_rust" }
nc = { path = "connections/nc" }
+nc_wire_proto = { path = "connections/nc_wire_proto" }
+nc_multiplex_proto = {path = "connections/nc_multiplex_proto"}
rand_ext = { path = "presence/rand_ext" }
test_helper = { path = "presence/test_helper" }
ukey2_connections = { path = "connections/ukey2/ukey2_connections" }
@@ -94,6 +103,7 @@
ldt_np_adv = { path = "presence/ldt_np_adv" }
ldt_tbc = { path = "presence/ldt_tbc" }
np_adv = { path = "presence/np_adv" }
+up_adv = { path = "unified_protocol/up_adv" }
np_adv_dynamic = { path = "presence/np_adv_dynamic" }
np_ffi_core = { path = "presence/np_ffi_core", default-features = false }
np_ffi_core_fuzz = { path = "presence/np_ffi_core/fuzz" }
@@ -113,6 +123,10 @@
ph_flag_source = { path = "../common/1p_internal/ph_flag_source", default-features = false }
ph_client = { path = "../common/1p_internal/ph_client" }
+# analytics
+analytics_logger = { path = "../common/1p_internal/clearcut/analytics_logger" }
+clearcut_logger = { path = "../common/1p_internal/clearcut/internal/clearcut_logger" }
+
# from crates.io
rand = { version = "0.8.5", default-features = false }
rand_core = { version = "0.6.4", features = ["getrandom"] }
@@ -151,6 +165,7 @@
anyhow = "1.0.75"
log = "0.4.20"
env_logger = "0.11.7"
+gag = "1.0.0"
oslog = "0.2.0"
criterion = { version = "0.5.1", features = ["html_reports"] }
clap = { version = "4.5.13", features = ["derive"] }
@@ -161,7 +176,8 @@
hashbrown = "0.15"
hdrhistogram = "7.5.4"
regex = "1.10.2"
-tokio = { version = "1.35.0", features = [
+crossbeam = "0.8.4"
+tokio = { version = "1.47.1", features = [
"macros",
"rt",
"sync",
@@ -169,8 +185,10 @@
"time",
] }
tokio-stream = { version = "0.1.17" }
+tokio-util = "0.7.16"
xts-mode = "0.5.1"
rstest = { version = "0.18.2", default-features = false }
+serial_test = "3.2.0"
rstest_reuse = "0.6.0"
wycheproof = "0.5.1"
chrono = { version = "0.4.31", default-features = false, features = ["clock"] }
@@ -194,11 +212,11 @@
libfuzzer-sys = "0.4.7"
derive_fuzztest = "0.1.4"
derive_fuzztest_macro = "0.1.4"
-async-trait = "0.1.83"
+async-trait = "0.1.89"
directories = "5.0.1"
uuid = { version = "1.11.0", features = ["v4"] }
mockall = "0.13.1"
-uniffi = "0.29.3"
+uniffi = "0.29.0"
async-stream = "0.3.6"
prost = "0.14.1"
ascii85 = "0.2.1"
@@ -217,6 +235,7 @@
"Win32_System_RemoteDesktop",
] }
windows-core = "0.61.0"
+googletest = "0.14.2"
dispatch2 = { version = "0.3.0" }
objc2 = { version = "0.6.1" }
@@ -232,6 +251,8 @@
"NSValue",
] }
objc2-core-bluetooth = { version = "0.3.1" }
+android_logger = "0.15.1"
+num_enum = "0.7.5"
[workspace.package]
version = "0.1.0"
diff --git a/nearby/build_scripts/Cargo.toml b/nearby/build_scripts/Cargo.toml
index 72c41d0..2aeaec2 100644
--- a/nearby/build_scripts/Cargo.toml
+++ b/nearby/build_scripts/Cargo.toml
@@ -25,6 +25,7 @@
serde_json = { workspace = true, features = ["std"] }
regex = "1.10.2"
xshell = "0.2.6"
+protoc = "2.28.0"
[dev-dependencies]
env_logger.workspace = true
diff --git a/nearby/build_scripts/src/crypto_ffi.rs b/nearby/build_scripts/src/crypto_ffi.rs
index 6ef5350..3676a23 100644
--- a/nearby/build_scripts/src/crypto_ffi.rs
+++ b/nearby/build_scripts/src/crypto_ffi.rs
@@ -53,8 +53,7 @@
run_cmd_shell_with_color::<YellowStderr>(
&build_dir,
format!(
- "cmake -G Ninja .. -DRUST_BINDINGS={} -DCMAKE_POSITION_INDEPENDENT_CODE=true",
- target
+ "cmake -G Ninja .. -DRUST_BINDINGS={target} -DCMAKE_POSITION_INDEPENDENT_CODE=true"
),
)?;
run_cmd_shell(&build_dir, "ninja")?;
diff --git a/nearby/build_scripts/src/ffi.rs b/nearby/build_scripts/src/ffi.rs
index 4bc2513..721fab8 100644
--- a/nearby/build_scripts/src/ffi.rs
+++ b/nearby/build_scripts/src/ffi.rs
@@ -25,6 +25,8 @@
check_np_ffi_cmake(root, cargo_options, boringssl_enabled)?;
check_ldt_cmake(root, cargo_options, boringssl_enabled)?;
check_ukey2_cmake(root, cargo_options, boringssl_enabled)?;
+ #[cfg(not(target_os = "linux"))]
+ check_quick_share_ffi(root, cargo_options)?;
Ok(())
}
@@ -120,6 +122,32 @@
Ok(())
}
+pub(crate) fn check_quick_share_ffi(
+ root: &path::Path,
+ cargo_options: &CargoOptions,
+) -> anyhow::Result<()> {
+ log::info!("Checking that the Quick Share FFI crate builds for various platforms");
+
+ run_cmd_shell(root, "cargo build -p quick_share_ffi --quiet")?;
+
+ for platform in &cargo_options.platforms {
+ let features = if platform.contains("ios") {
+ vec!["--features quick_share_service/short_fast_init"]
+ } else {
+ vec![]
+ };
+ run_cmd_shell(
+ root,
+ format!(
+ "cargo build -p quick_share_ffi --target {platform} --quiet {}",
+ &features.join(" ")
+ ),
+ )?;
+ }
+
+ Ok(())
+}
+
pub(crate) fn generate_test_data(root: &path::Path) -> anyhow::Result<()> {
run_cmd_shell(root, "cargo run -p np_adv -Falloc,devtools --example generate_test_data")?;
Ok(())
diff --git a/nearby/build_scripts/src/fuzzers.rs b/nearby/build_scripts/src/fuzzers.rs
index c2caf29..84d4056 100644
--- a/nearby/build_scripts/src/fuzzers.rs
+++ b/nearby/build_scripts/src/fuzzers.rs
@@ -40,7 +40,7 @@
for target in ["deserialization_fuzzer", "ldt_fuzzer"] {
run_cmd_shell_with_color::<YellowStderr>(
&build_dir,
- format!("cmake --build . --target {}", target),
+ format!("cmake --build . --target {target}"),
)?;
}
diff --git a/nearby/build_scripts/src/jni.rs b/nearby/build_scripts/src/jni.rs
index 75cf1ff..1002ff5 100644
--- a/nearby/build_scripts/src/jni.rs
+++ b/nearby/build_scripts/src/jni.rs
@@ -32,6 +32,9 @@
pub fn run_np_java_ffi_tests(root: &path::Path) -> anyhow::Result<()> {
run_cmd_shell(root, "cargo build -p np_java_ffi -F np_ffi_core/rustcrypto -F testing")?;
let np_java_path = root.to_path_buf().join("presence/np_java_ffi");
+ // This needs to run once a while to upgrade the gradle to the latest.
+ // Particularly, when these tests fail, uncomment and upgrade.
+ // run_cmd_shell(&np_java_path, "./gradlew wrapper --gradle-version latest")?;
run_cmd_shell(&np_java_path, "./gradlew :test --info --rerun")?;
run_cmd_shell(&np_java_path, "./gradlew :proguardedTest --info --rerun")?;
Ok(())
diff --git a/nearby/build_scripts/src/license.rs b/nearby/build_scripts/src/license.rs
index a566cb3..9723d60 100644
--- a/nearby/build_scripts/src/license.rs
+++ b/nearby/build_scripts/src/license.rs
@@ -63,6 +63,7 @@
"**/*.so",
"**/*.dylib",
"**/MANIFEST.MF",
+ "**/*.pro",
],
};
diff --git a/nearby/build_scripts/src/main.rs b/nearby/build_scripts/src/main.rs
index f81cc6c..6048cc0 100644
--- a/nearby/build_scripts/src/main.rs
+++ b/nearby/build_scripts/src/main.rs
@@ -68,6 +68,9 @@
license_subcommand.run(&LICENSE_CHECKER, root_dir)?
}
Subcommand::CheckUkey2Ffi(ref options) => ffi::check_ukey2_cmake(root_dir, options, false)?,
+ Subcommand::CheckQuickShareFfi(ref options) => {
+ ffi::check_quick_share_ffi(root_dir, options)?
+ }
Subcommand::RunUkey2JniTests => jni::run_ukey2_jni_tests(root_dir)?,
Subcommand::RunNpJavaFfiTests => jni::run_np_java_ffi_tests(root_dir)?,
Subcommand::RunDctJavaFfiTests => jni::run_dct_java_ffi_tests(root_dir)?,
@@ -124,7 +127,7 @@
let root_str =
glob::Pattern::escape(root.to_str().expect("Non-unicode paths are not supported"));
- let search = format!("{}/**/*.java", root_str);
+ let search = format!("{root_str}/**/*.java");
let java_files: Vec<_> = glob::glob(&search)
.unwrap()
.filter_map(Result::ok)
@@ -158,13 +161,17 @@
for cargo_cmd in [
// make sure everything compiles
- &format!("cargo check --workspace --all-targets --quiet --features={}", workspace_features.join(",")),
+ &format!(
+ "cargo check --workspace --all-targets --quiet --features={}",
+ workspace_features.join(",")
+ ),
"cargo check -p np_adv -Falloc,devtools --example generate_test_data",
+ "cargo check -p up_adv -Falloc,devtools --example generate_test_data",
// run all the tests
&options.cargo_options.test(
- "check_workspace",
+ "check_workspace",
format!("--workspace --quiet --features={}", workspace_features.join(",")),
- None
+ None,
),
// Test ukey2 builds with different crypto providers
&options.cargo_options.test(
@@ -305,6 +312,8 @@
License(LicenseSubcommand),
/// Builds and runs tests for the UKEY2 FFI
CheckUkey2Ffi(CargoOptions),
+ /// Builds the Quick Share FFI crate.
+ CheckQuickShareFfi(CargoOptions),
/// Runs the kotlin tests of the LDT Jni API
RunLdtKotlinTests,
/// Checks the build of the ukey2_jni wrapper and runs tests
@@ -339,6 +348,11 @@
locked: bool,
#[arg(long, help = "gather coverage metrics")]
coverage: bool,
+ #[arg(
+ long,
+ help = "platforms to run on, other than the default. platforms specified here should have already been installed via rustup."
+ )]
+ platforms: Vec<String>,
}
#[derive(PartialEq, Eq)]
diff --git a/nearby/connections/ukey2/ukey2_c_ffi/src/lib.rs b/nearby/connections/ukey2/ukey2_c_ffi/src/lib.rs
index be24c66..a44b2f6 100644
--- a/nearby/connections/ukey2/ukey2_c_ffi/src/lib.rs
+++ b/nearby/connections/ukey2/ukey2_c_ffi/src/lib.rs
@@ -102,14 +102,14 @@
#[no_mangle]
pub unsafe extern "C" fn rust_dealloc_ffi_byte_array(arr: RustFFIByteArray) {
if let Some(vec) = arr.into_vec() {
- core::mem::drop(vec);
+ drop(vec);
}
}
// Common functions
#[no_mangle]
pub extern "C" fn is_handshake_complete(handle: u64) -> bool {
- HANDLE_MAPPING.lock().get(&handle).map_or(false, |ctx| ctx.is_handshake_complete())
+ HANDLE_MAPPING.lock().get(&handle).is_some_and(|ctx| ctx.is_handshake_complete())
}
#[no_mangle]
@@ -135,7 +135,7 @@
if let Err(error) = result {
match error {
HandleMessageError::InvalidState | HandleMessageError::BadMessage => {
- log::error!("{:?}", error);
+ log::error!("{error:?}");
}
HandleMessageError::ErrorMessage(message) => {
return CMessageParseResult {
diff --git a/nearby/connections/ukey2/ukey2_connections/src/d2d_connection_context_v1.rs b/nearby/connections/ukey2/ukey2_connections/src/d2d_connection_context_v1.rs
index 99832f3..25aa05f 100644
--- a/nearby/connections/ukey2/ukey2_connections/src/d2d_connection_context_v1.rs
+++ b/nearby/connections/ukey2/ukey2_connections/src/d2d_connection_context_v1.rs
@@ -303,9 +303,9 @@
///
/// * `payload` - The payload that should be encrypted.
/// * `associated_data` - Optional data that is not included in the payload but is included in
- /// the calculation of the signature for this message. Note that the *size* (length in
- /// bytes) of the associated data will be sent in the *UNENCRYPTED* header information,
- /// even if you are using encryption.
+ /// the calculation of the signature for this message. Note that the *size* (length in bytes)
+ /// of the associated data will be sent in the *UNENCRYPTED* header information, even if you
+ /// are using encryption.
pub fn encode_message_to_peer<C: CryptoProvider>(
&mut self,
payload: &[u8],
@@ -320,7 +320,7 @@
///
/// * `message` - the message that should be encrypted.
/// * `associated_data` - Optional associated data that must match what the sender provided. See
- /// the documentation on [`encode_message_to_peer`][Self::encode_message_to_peer].
+ /// the documentation on [`encode_message_to_peer`][Self::encode_message_to_peer].
pub fn decode_message_from_peer<C: CryptoProvider>(
&mut self,
message: &[u8],
diff --git a/nearby/crypto/crypto_provider_test/src/ecdsa.rs b/nearby/crypto/crypto_provider_test/src/ecdsa.rs
index 265a889..a00c628 100644
--- a/nearby/crypto/crypto_provider_test/src/ecdsa.rs
+++ b/nearby/crypto/crypto_provider_test/src/ecdsa.rs
@@ -47,11 +47,10 @@
if let Err(desc) = result {
panic!(
"\n\
- Failed test {}: {}\n\
- msg:\t{:?}\n\
- sig:\t{:?}\n\
- comment:\t{:?}\n",
- tc_id, desc, msg, sig, comment,
+ Failed test {tc_id}: {desc}\n\
+ msg:\t{msg:?}\n\
+ sig:\t{sig:?}\n\
+ comment:\t{comment:?}\n",
);
}
} else {
diff --git a/nearby/crypto/crypto_provider_test/src/ed25519.rs b/nearby/crypto/crypto_provider_test/src/ed25519.rs
index f2bc7ef..d005c58 100644
--- a/nearby/crypto/crypto_provider_test/src/ed25519.rs
+++ b/nearby/crypto/crypto_provider_test/src/ed25519.rs
@@ -54,11 +54,10 @@
if let Err(desc) = result {
panic!(
"\n\
- Failed test {}: {}\n\
- msg:\t{:?}\n\
- sig:\t{:?}\n\
- comment:\t{:?}\n",
- tc_id, desc, msg, sig, comment,
+ Failed test {tc_id}: {desc}\n\
+ msg:\t{msg:?}\n\
+ sig:\t{sig:?}\n\
+ comment:\t{comment:?}\n",
);
}
} else {
@@ -107,10 +106,9 @@
if let Err(desc) = result {
panic!(
"\n\
- Failed test {}: {}\n\
- msg:\t{:?}\n\
- sig:\t{:?}\n\"",
- tc_id, desc, msg, sig,
+ Failed test {tc_id}: {desc}\n\
+ msg:\t{msg:?}\n\
+ sig:\t{sig:?}\n\"",
);
}
}
diff --git a/nearby/crypto/crypto_provider_test/src/sha2.rs b/nearby/crypto/crypto_provider_test/src/sha2.rs
index b10ac9a..2d68d5b 100644
--- a/nearby/crypto/crypto_provider_test/src/sha2.rs
+++ b/nearby/crypto/crypto_provider_test/src/sha2.rs
@@ -76,7 +76,7 @@
} else if let Some(hex_str) = line.strip_prefix("MD = ") {
md = Some(Vec::<u8>::from_hex(hex_str).unwrap());
} else {
- panic!("Unexpected line in test file: `{}`", line);
+ panic!("Unexpected line in test file: `{line}`");
}
}
if let (Some(len), Some(msg), Some(md)) = (len, msg, md) {
diff --git a/nearby/deny.toml b/nearby/deny.toml
index 20f50aa..83a95b2 100644
--- a/nearby/deny.toml
+++ b/nearby/deny.toml
@@ -66,6 +66,7 @@
"ISC",
"MPL-2.0",
"Zlib",
+ "OpenSSL",
]
# The confidence threshold for detecting a license from license text.
# The higher the value, the more closely the license text must be to the
diff --git a/nearby/presence/array_view/src/lib.rs b/nearby/presence/array_view/src/lib.rs
index d4d7c84..9edf43e 100644
--- a/nearby/presence/array_view/src/lib.rs
+++ b/nearby/presence/array_view/src/lib.rs
@@ -43,6 +43,7 @@
}
/// A version of [`ArrayView#try_from_array`] which panics if `len > buffer.len()`,
/// suitable for usage in `const` contexts.
+ #[allow(clippy::panic)]
pub const fn const_from_array(array: [T; N], len: usize) -> ArrayView<T, N> {
if N < len {
panic!("Invalid const ArrayView");
diff --git a/nearby/presence/ldt_np_adv/src/tests.rs b/nearby/presence/ldt_np_adv/src/tests.rs
index a731762..fabd74c 100644
--- a/nearby/presence/ldt_np_adv/src/tests.rs
+++ b/nearby/presence/ldt_np_adv/src/tests.rs
@@ -274,7 +274,7 @@
fn legacy_identity_token() {
let token = V0IdentityToken::from([0; V0_IDENTITY_TOKEN_LEN]);
// debug
- let _ = format!("{:?}", token);
+ let _ = format!("{token:?}");
// hash
let _ = collections::HashSet::new().insert(&token);
// bytes
diff --git a/nearby/presence/np_adv/src/credential/book.rs b/nearby/presence/np_adv/src/credential/book.rs
index 7ab3b5f..142bb28 100644
--- a/nearby/presence/np_adv/src/credential/book.rs
+++ b/nearby/presence/np_adv/src/credential/book.rs
@@ -109,6 +109,22 @@
let v1_source = PrecalculatedOwnedCredentialSource::<V1, M>::new::<P>(v1_iter);
CredentialBookFromSources::new(v0_source, v1_source)
}
+
+ #[cfg(feature = "alloc")]
+ /// Constructs a new credential-book which owns all of the given credentials,
+ /// and maintains pre-calculated cryptographic information about them
+ /// for speedy advertisement deserialization.
+ pub fn build_precalculated_owned_book_with_id<P: CryptoProvider>(
+ v0_iter: impl IntoIterator<Item = MatchableCredential<V0, M>>,
+ v1_iter: impl IntoIterator<Item = MatchableCredential<V1, M>>,
+ ) -> PrecalculatedOwnedCredentialBookWithId<M>
+ where
+ M: MatchedCredential + Clone,
+ {
+ let v0_source = PrecalculatedOwnedV0CredentialSourceWithId::<M>::new::<P>(v0_iter);
+ let v1_source = PrecalculatedOwnedV1CredentialSourceWithId::<M>::new::<P>(v1_iter);
+ CredentialBookFromSources::new(v0_source, v1_source)
+ }
}
// Now, for the implementation details. External implementors still need
@@ -545,3 +561,172 @@
PrecalculatedOwnedCredentialSource<V0, M>,
PrecalculatedOwnedCredentialSource<V1, M>,
>;
+
+#[cfg(any(feature = "alloc", test))]
+/// A credential-book which owns all of its (non-matched) credential data,
+/// and maintains pre-calculated cryptographic information about all
+/// stored credentials for speedy advertisement deserialization.
+pub type PrecalculatedOwnedCredentialBookWithId<M> = CredentialBookFromSources<
+ PrecalculatedOwnedV0CredentialSourceWithId<M>,
+ PrecalculatedOwnedV1CredentialSourceWithId<M>,
+>;
+
+#[cfg(any(feature = "alloc", test))]
+/// A simple credentials source that holds owned, pre-calculated V0 crypto-materials.
+pub struct PrecalculatedOwnedV0CredentialSourceWithId<M: MatchedCredential> {
+ credentials:
+ Vec<
+ (
+ <precalculated_for_version::Marker as precalculated_for_version::MappingTrait<
+ V0,
+ >>::Output,
+ M,
+ ),
+ >,
+}
+
+#[cfg(any(feature = "alloc", test))]
+impl<M: MatchedCredential + Clone> PrecalculatedOwnedV0CredentialSourceWithId<M> {
+ /// Pre-calculates crypto material for the given credentials and constructs a
+ /// new source which owns this crypto-material.
+ pub fn new<P: CryptoProvider>(
+ credential_iter: impl IntoIterator<Item = MatchableCredential<V0, M>>,
+ ) -> Self {
+ let credentials = credential_iter
+ .into_iter()
+ .map(|credential| {
+ (
+ <precalculated_for_version::Marker as precalculated_for_version::MappingTrait<
+ V0,
+ >>::precalculate::<P>(&credential.discovery_credential),
+ credential.match_data,
+ )
+ })
+ .collect();
+ Self { credentials }
+ }
+}
+
+#[cfg(any(feature = "alloc", test))]
+/// A simple credentials source that holds owned, pre-calculated V1 crypto-materials.
+pub struct PrecalculatedOwnedV1CredentialSourceWithId<M: MatchedCredential> {
+ credentials:
+ Vec<
+ (
+ <precalculated_for_version::Marker as precalculated_for_version::MappingTrait<
+ V1,
+ >>::Output,
+ M,
+ ),
+ >,
+}
+
+#[cfg(any(feature = "alloc", test))]
+impl<M: MatchedCredential + Clone> PrecalculatedOwnedV1CredentialSourceWithId<M> {
+ /// Pre-calculates crypto material for the given credentials and constructs a
+ /// new source which owns this crypto-material.
+ pub fn new<P: CryptoProvider>(
+ credential_iter: impl IntoIterator<Item = MatchableCredential<V1, M>>,
+ ) -> Self {
+ let credentials = credential_iter
+ .into_iter()
+ .map(|credential| {
+ (
+ <precalculated_for_version::Marker as precalculated_for_version::MappingTrait<
+ V1,
+ >>::precalculate::<P>(&credential.discovery_credential),
+ credential.match_data,
+ )
+ })
+ .collect();
+ Self { credentials }
+ }
+}
+
+#[cfg(any(feature = "alloc", test))]
+/// Mapping function for the iterator to return a reference to the crypto material
+/// and a clone of the match data.
+fn crypto_and_cloned_match_data<C, M: MatchedCredential + Clone>(pair_ref: &(C, M)) -> (&C, M) {
+ let (c, m) = pair_ref;
+ (c, m.clone())
+}
+
+#[cfg(any(feature = "alloc", test))]
+impl<'a, M: MatchedCredential + Clone> CredentialSource<'a, V0>
+ for PrecalculatedOwnedV0CredentialSourceWithId<M>
+where
+ Self: 'a,
+{
+ type Matched = M;
+ type Crypto = &'a <precalculated_for_version::Marker as precalculated_for_version::MappingTrait<
+ V0,
+ >>::Output;
+ type Iterator = core::iter::Map<
+ core::slice::Iter<
+ 'a,
+ (
+ <precalculated_for_version::Marker as precalculated_for_version::MappingTrait<
+ V0,
+ >>::Output,
+ M,
+ ),
+ >,
+ fn(
+ &'a (
+ <precalculated_for_version::Marker as precalculated_for_version::MappingTrait<
+ V0,
+ >>::Output,
+ M,
+ ),
+ ) -> (
+ &'a <precalculated_for_version::Marker as precalculated_for_version::MappingTrait<
+ V0,
+ >>::Output,
+ M,
+ ),
+ >;
+
+ fn iter(&'a self) -> Self::Iterator {
+ self.credentials.iter().map(crypto_and_cloned_match_data)
+ }
+}
+
+#[cfg(any(feature = "alloc", test))]
+impl<'a, M: MatchedCredential + Clone> CredentialSource<'a, V1>
+ for PrecalculatedOwnedV1CredentialSourceWithId<M>
+where
+ Self: 'a,
+{
+ type Matched = M;
+ type Crypto = &'a <precalculated_for_version::Marker as precalculated_for_version::MappingTrait<
+ V1,
+ >>::Output;
+ type Iterator = core::iter::Map<
+ core::slice::Iter<
+ 'a,
+ (
+ <precalculated_for_version::Marker as precalculated_for_version::MappingTrait<
+ V1,
+ >>::Output,
+ M,
+ ),
+ >,
+ fn(
+ &'a (
+ <precalculated_for_version::Marker as precalculated_for_version::MappingTrait<
+ V1,
+ >>::Output,
+ M,
+ ),
+ ) -> (
+ &'a <precalculated_for_version::Marker as precalculated_for_version::MappingTrait<
+ V1,
+ >>::Output,
+ M,
+ ),
+ >;
+
+ fn iter(&'a self) -> Self::Iterator {
+ self.credentials.iter().map(crypto_and_cloned_match_data)
+ }
+}
diff --git a/nearby/presence/np_adv/src/credential/matched.rs b/nearby/presence/np_adv/src/credential/matched.rs
index e559976..51b2f1c 100644
--- a/nearby/presence/np_adv/src/credential/matched.rs
+++ b/nearby/presence/np_adv/src/credential/matched.rs
@@ -136,6 +136,37 @@
}
}
+/// A simple implementation of [`MatchedCredential`] where all match-data
+/// is contained in the encrypted metadata byte-field.
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct MatchedCredentialWithId<A: AsRef<[u8]> + Clone + Debug + PartialEq + Eq> {
+ encrypted_metadata: A,
+ id: i64,
+}
+
+impl<A: AsRef<[u8]> + Clone + Debug + PartialEq + Eq> MatchedCredentialWithId<A> {
+ /// Builds a new [`MatchedCredentialWithId`] with the given
+ /// encrypted metadata.
+ pub fn new(encrypted_metadata: A, id: i64) -> Self {
+ Self { encrypted_metadata, id }
+ }
+
+ /// Returns the unique ID of the matched credential.
+ pub fn id(&self) -> i64 {
+ self.id
+ }
+}
+
+impl<A: AsRef<[u8]> + Clone + Debug + PartialEq + Eq> MatchedCredential
+ for MatchedCredentialWithId<A>
+{
+ type EncryptedMetadata = A;
+ type EncryptedMetadataFetchError = Infallible;
+ fn fetch_encrypted_metadata(&self) -> Result<Self::EncryptedMetadata, Infallible> {
+ Ok(self.encrypted_metadata.clone())
+ }
+}
+
/// Trivial implementation of [`MatchedCredential`] which consists of no match-data.
/// Suitable for usage scenarios where the decoded advertisement contents matter,
/// but not necessarily which devices generated the contents.
diff --git a/nearby/presence/np_adv/src/credential/tests.rs b/nearby/presence/np_adv/src/credential/tests.rs
index 00c99b9..2bb3286 100644
--- a/nearby/presence/np_adv/src/credential/tests.rs
+++ b/nearby/presence/np_adv/src/credential/tests.rs
@@ -16,19 +16,22 @@
extern crate alloc;
use crate::credential::matched::{
- EmptyMatchedCredential, KeySeedMatchedCredential, ReferencedMatchedCredential,
+ EmptyMatchedCredential, KeySeedMatchedCredential, MatchedCredentialWithId,
+ ReferencedMatchedCredential,
};
use crate::credential::v1::MicSectionVerificationMaterial;
use crate::credential::{
book::{
- init_cache_from_source, CachedCredentialSource, PossiblyCachedDiscoveryCryptoMaterialKind,
+ init_cache_from_source, CachedCredentialSource, CredentialBook, CredentialBookBuilder,
+ PossiblyCachedDiscoveryCryptoMaterialKind,
},
source::{CredentialSource, SliceCredentialSource},
v0::{V0DiscoveryCredential, V0},
v1::{V1BroadcastCredential, V1DiscoveryCredential, V1DiscoveryCryptoMaterial, V1},
- MatchableCredential,
+ MatchableCredential, MatchedCredential,
};
use crate::extended::{V1IdentityToken, V1_IDENTITY_TOKEN_LEN};
+use alloc::vec;
use alloc::vec::Vec;
use crypto_provider_default::CryptoProviderImpl;
@@ -116,6 +119,50 @@
}
}
+#[test]
+fn precalculated_owned_book_with_id_iterates_correctly() {
+ let v0_creds: [MatchableCredential<V0, MatchedCredentialWithId<Vec<u8>>>; 2] =
+ [0, 1].map(|i| {
+ let match_data = MatchedCredentialWithId::new(vec![i], i as i64);
+ MatchableCredential {
+ discovery_credential: get_zeroed_v0_discovery_credential(),
+ match_data,
+ }
+ });
+ let v1_creds: [MatchableCredential<V1, MatchedCredentialWithId<Vec<u8>>>; 3] =
+ [2, 3, 4].map(|i| {
+ let match_data = MatchedCredentialWithId::new(vec![i], i as i64);
+ MatchableCredential {
+ discovery_credential: get_constant_packed_v1_discovery_credential(i),
+ match_data,
+ }
+ });
+
+ let v0_expected: Vec<_> = v0_creds
+ .iter()
+ .map(|c| (c.match_data.id(), c.match_data.fetch_encrypted_metadata().unwrap()))
+ .collect();
+
+ let v1_expected: Vec<_> = v1_creds
+ .iter()
+ .map(|c| (c.match_data.id(), c.match_data.fetch_encrypted_metadata().unwrap()))
+ .collect();
+
+ let book = CredentialBookBuilder::build_precalculated_owned_book_with_id::<CryptoProviderImpl>(
+ v0_creds, v1_creds,
+ );
+
+ let v0_from_book: Vec<_> =
+ book.v0_iter().map(|(_, m)| (m.id(), m.fetch_encrypted_metadata().unwrap())).collect();
+
+ assert_eq!(v0_from_book, v0_expected);
+
+ let v1_from_book: Vec<_> =
+ book.v1_iter().map(|(_, m)| (m.id(), m.fetch_encrypted_metadata().unwrap())).collect();
+
+ assert_eq!(v1_from_book, v1_expected);
+}
+
mod coverage_gaming {
use crate::credential::MetadataDecryptionError;
use alloc::format;
@@ -123,6 +170,6 @@
#[test]
fn metadata_decryption_error_debug() {
let err = MetadataDecryptionError;
- let _ = format!("{:?}", err);
+ let _ = format!("{err:?}");
}
}
diff --git a/nearby/presence/np_adv/src/extended/data_elements/mod.rs b/nearby/presence/np_adv/src/extended/data_elements/mod.rs
index 98633a8..e22e8d1 100644
--- a/nearby/presence/np_adv/src/extended/data_elements/mod.rs
+++ b/nearby/presence/np_adv/src/extended/data_elements/mod.rs
@@ -57,6 +57,8 @@
/// A media deduplication ID
MediaDeduplicationId: MediaDeduplicationIdDataElement<'adv>,
} owns {
+ /// Device Info
+ DeviceInfo: DeviceInfoDataElement,
/// A transmission power
TxPower: TxPowerDataElement,
/// A context sync sequence number
@@ -467,6 +469,80 @@
}
}
+/// Device information of the broadcasting device.
+#[derive(Debug)]
+pub struct DeviceInfoDataElement {
+ /// Data element representing the device information of the broadcasting device,
+ /// including the [`DeviceType`], and device name, as well as a boolean flag
+ /// representing whether the device name is truncated.
+ pub device_info: DeviceInfo,
+}
+
+impl From<DeviceInfo> for DeviceInfoDataElement {
+ fn from(device_info: DeviceInfo) -> Self {
+ Self { device_info }
+ }
+}
+
+impl From<DeviceInfoDataElement> for DeviceInfo {
+ fn from(de: DeviceInfoDataElement) -> Self {
+ de.device_info
+ }
+}
+
+/// Errors raised when attempting to deserialize a device info DE.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum DeviceInfoDeserializationError {
+ /// The DE payload was of the wrong length.
+ WrongLength,
+ /// The device type is unknown
+ UnknownDeviceType,
+}
+
+impl HasDEType for DeviceInfoDataElement {
+ const DE_TYPE: DeType = DeType::const_from(0x03);
+}
+
+impl<'adv> DeserializedDataElement<'adv> for DeviceInfoDataElement {
+ type DeserializationError = DeviceInfoDeserializationError;
+ fn try_deserialize(
+ _maybe_salt: Option<&DeSalt>,
+ contents: &'adv [u8],
+ ) -> Result<Self, Self::DeserializationError> {
+ // Contents should be min 5 byte (device name) + 1 byte (device type | name truncated ).
+ if contents.len() < crate::shared_data::MIN_DEVICE_NAME_LEN + 1 {
+ return Err(DeviceInfoDeserializationError::WrongLength);
+ }
+
+ let type_and_trunc = contents[0];
+ let device_type = crate::shared_data::DeviceType::from_repr(type_and_trunc & 0b01111111)
+ .ok_or(DeviceInfoDeserializationError::UnknownDeviceType)?;
+ let name_truncated = (type_and_trunc & 0b10000000) != 0;
+ let device_name = &contents[1..];
+
+ let device_info = DeviceInfo::try_from((device_type, name_truncated, device_name))
+ .map_err(|_| {
+ // This is not ideal, but we don't have a specific error for this.
+ // The length is already checked, so this should not happen.
+ DeviceInfoDeserializationError::WrongLength
+ })?;
+
+ Ok(device_info.into())
+ }
+}
+
+impl WriteDataElement for DeviceInfoDataElement {
+ type Salt = Unsalted;
+ fn write_de_contents<S: Sink<u8>>(&self, _salt: Self::Salt, sink: &mut S) -> Option<()> {
+ let mut type_and_trunc = self.device_info.device_type() as u8;
+ if self.device_info.name_truncated() {
+ type_and_trunc |= 0b10000000;
+ }
+ sink.try_push(type_and_trunc)?;
+ sink.try_extend_from_slice(self.device_info.device_name())
+ }
+}
+
/// Combined connectivity information about a device.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ConnectivityInfoDataElement {
diff --git a/nearby/presence/np_adv/src/extended/data_elements/tests.rs b/nearby/presence/np_adv/src/extended/data_elements/tests.rs
index d17e92b..f0882bf 100644
--- a/nearby/presence/np_adv/src/extended/data_elements/tests.rs
+++ b/nearby/presence/np_adv/src/extended/data_elements/tests.rs
@@ -197,6 +197,64 @@
}
#[test]
+fn deserialize_device_info() {
+ // Success case, with a recognized device type.
+ let device_type = crate::shared_data::DeviceType::TV;
+ let device_name = b"my-tv";
+ let mut device_info_bytes = Vec::new();
+ device_info_bytes.push(device_type as u8);
+ device_info_bytes.extend_from_slice(device_name);
+
+ let de = DataElement::new(DeviceInfoDataElement::DE_TYPE, &device_info_bytes, None);
+ let de = DeserializedGoogleDE::try_deserialize(de).expect("Should recognize a device info DE.");
+ let DeserializedGoogleDE::DeviceInfo(de) = de else {
+ panic!("Device info data elements don't deserialize to device info?");
+ };
+ let de = de.expect("Well formed device info DEs should always deserialize.");
+ assert_eq!(de.device_info.device_type(), device_type);
+ assert!(!de.device_info.name_truncated());
+ assert_eq!(de.device_info.device_name(), device_name);
+
+ // Failure case: wrong length
+ let de = DataElement::new(DeviceInfoDataElement::DE_TYPE, &device_info_bytes[..5], None);
+ let de = DeserializedGoogleDE::try_deserialize(de)
+ .expect("Should recognize even a malformatted device info DE.");
+ let DeserializedGoogleDE::DeviceInfo(de) = de else {
+ panic!("Device info data elements don't deserialize to device info?");
+ };
+ let err = de.expect_err("Device infos that are too short should not deserialize.");
+ assert_eq!(DeviceInfoDeserializationError::WrongLength, err);
+}
+
+#[test]
+fn serialize_device_info() {
+ let device_info = DeviceInfo::try_from((
+ crate::shared_data::DeviceType::Laptop,
+ true,
+ "my-laptop".as_bytes(),
+ ))
+ .unwrap();
+ let de = DeviceInfoDataElement::from(device_info);
+ let mut sink: ArrayVec<[u8; 10]> = ArrayVec::new();
+ de.write_de_contents(Unsalted, &mut sink).expect("Should be able to write a device info.");
+ assert_eq!(
+ sink.as_slice(),
+ &[
+ crate::shared_data::DeviceType::Laptop as u8 | 0b10000000,
+ b'm',
+ b'y',
+ b'-',
+ b'l',
+ b'a',
+ b'p',
+ b't',
+ b'o',
+ b'p'
+ ]
+ );
+}
+
+#[test]
fn serialize_tx_power_de() {
let mut adv_builder = AdvBuilder::new();
let mut section_builder = adv_builder.section_builder(UnencryptedSectionEncoder).unwrap();
@@ -470,7 +528,7 @@
// four bits are explicitly disallowed.
for disallowed_bitmask in 1u8..=15u8 {
let _ = BleConnectivityInfo::parse_from_payload(&[disallowed_bitmask])
- .expect_err(&format!("Bitmask {} should be disallowed", disallowed_bitmask));
+ .expect_err(&format!("Bitmask {disallowed_bitmask} should be disallowed"));
}
}
@@ -480,7 +538,7 @@
// four bits are explicitly disallowed.
for disallowed_bitmask in 1u8..=15u8 {
let _ = WifiLanConnectivityInfo::parse_from_payload(&[disallowed_bitmask])
- .expect_err(&format!("Bitmask {} should be disallowed", disallowed_bitmask));
+ .expect_err(&format!("Bitmask {disallowed_bitmask} should be disallowed"));
}
}
@@ -614,7 +672,7 @@
#[test]
fn generic_de_error_derives() {
let err = GenericDataElementError::DataTooLong;
- let _ = format!("{:?}", err);
+ let _ = format!("{err:?}");
assert_eq!(err, err);
}
@@ -622,6 +680,6 @@
fn generic_data_element_debug() {
let generic =
GenericDataElement::try_from(DeType::from(1000_u16), &[10, 11, 12, 13]).unwrap();
- let _ = format!("{:?}", generic);
+ let _ = format!("{generic:?}");
}
}
diff --git a/nearby/presence/np_adv/src/extended/deserialize/data_element/tests.rs b/nearby/presence/np_adv/src/extended/deserialize/data_element/tests.rs
index 6187199..73f6307 100644
--- a/nearby/presence/np_adv/src/extended/deserialize/data_element/tests.rs
+++ b/nearby/presence/np_adv/src/extended/deserialize/data_element/tests.rs
@@ -294,12 +294,12 @@
#[test]
fn data_element_debug_and_clone() {
let de = DataElement::new(0_u8.into(), &[], None);
- let _ = format!("{:?}", de);
+ let _ = format!("{de:?}");
let _ = de.clone();
let (_, header) = DeHeader::parse(&[0x11, 0xFF]).unwrap();
- let _ = format!("{:?}", header);
+ let _ = format!("{header:?}");
let _ = header.clone();
let (_, pde) = ProtoDataElement::parse(&[0x11, 0xFF]).unwrap();
- let _ = format!("{:?}", pde);
+ let _ = format!("{pde:?}");
}
}
diff --git a/nearby/presence/np_adv/src/extended/deserialize/encrypted_section/tests/coverage_gaming.rs b/nearby/presence/np_adv/src/extended/deserialize/encrypted_section/tests/coverage_gaming.rs
index 4d3c79f..1d3f427 100644
--- a/nearby/presence/np_adv/src/extended/deserialize/encrypted_section/tests/coverage_gaming.rs
+++ b/nearby/presence/np_adv/src/extended/deserialize/encrypted_section/tests/coverage_gaming.rs
@@ -26,18 +26,18 @@
let token = CiphertextExtendedIdentityToken([0; 16]);
let section = SectionIdentityResolutionContents { identity_token: token, nonce };
assert_eq!(section, section);
- let _ = format!("{:?}", section);
+ let _ = format!("{section:?}");
}
#[test]
fn error_enum_debug_derives() {
let mic_err = MicVerificationError::MicMismatch;
- let _ = format!("{:?}", mic_err);
+ let _ = format!("{mic_err:?}");
let deser_err = DeserializationError::ArenaOutOfSpace::<MicVerificationError>;
- let _ = format!("{:?}", deser_err);
+ let _ = format!("{deser_err:?}");
let err =
IdentityResolutionOrDeserializationError::IdentityMatchingError::<MicVerificationError>;
- let _ = format!("{:?}", err);
+ let _ = format!("{err:?}");
}
diff --git a/nearby/presence/np_adv/src/extended/deserialize/section/header/tests.rs b/nearby/presence/np_adv/src/extended/deserialize/section/header/tests.rs
index 0db8a4f..71dd603 100644
--- a/nearby/presence/np_adv/src/extended/deserialize/section/header/tests.rs
+++ b/nearby/presence/np_adv/src/extended/deserialize/section/header/tests.rs
@@ -104,6 +104,6 @@
salt: [0; 2].into(),
token: CiphertextExtendedIdentityToken([0; 16]),
};
- let _ = format!("{:?}", header);
+ let _ = format!("{header:?}");
}
}
diff --git a/nearby/presence/np_adv/src/extended/deserialize/section/intermediate/tests.rs b/nearby/presence/np_adv/src/extended/deserialize/section/intermediate/tests.rs
index 276536f..65ee407 100644
--- a/nearby/presence/np_adv/src/extended/deserialize/section/intermediate/tests.rs
+++ b/nearby/presence/np_adv/src/extended/deserialize/section/intermediate/tests.rs
@@ -501,14 +501,14 @@
contents: &[],
contents_len: 0,
};
- let _ = format!("{:?}", sc);
+ let _ = format!("{sc:?}");
assert_eq!(sc, sc);
}
#[test]
fn intermediate_section() {
let is = IntermediateSection::Plaintext(PlaintextSection::new(&[]));
- let _ = format!("{:?}", is);
+ let _ = format!("{is:?}");
}
#[test]
fn ciphertext_section() {
@@ -524,7 +524,7 @@
mic: [0x00; 16].into(),
};
let cs = CiphertextSection::MicEncrypted(ms);
- let _ = format!("{:?}", cs);
+ let _ = format!("{cs:?}");
}
}
diff --git a/nearby/presence/np_adv/src/extended/deserialize/tests.rs b/nearby/presence/np_adv/src/extended/deserialize/tests.rs
index 478fd78..3dfaebf 100644
--- a/nearby/presence/np_adv/src/extended/deserialize/tests.rs
+++ b/nearby/presence/np_adv/src/extended/deserialize/tests.rs
@@ -96,21 +96,21 @@
[0u8; 16].into(),
&[],
);
- let _ = format!("{:?}", d);
+ let _ = format!("{d:?}");
}
#[test]
fn section_deserialize_error_derives() {
let e = SectionDeserializeError::IncorrectCredential;
assert_eq!(e, e);
- let _ = format!("{:?}", e);
+ let _ = format!("{e:?}");
}
#[test]
fn adv_contents_derives() {
let c: V1AdvertisementContents<'_, EmptyMatchedCredential> =
V1AdvertisementContents::new(ArrayVecOption::default(), 0);
- let _ = format!("{:?}", c);
+ let _ = format!("{c:?}");
assert_eq!(c, c);
}
@@ -119,7 +119,7 @@
let d: V1DeserializedSection<'_, EmptyMatchedCredential> =
V1DeserializedSection::Plaintext(PlaintextSection::new(&[]));
assert_eq!(d, d);
- let _ = format!("{:?}", d);
+ let _ = format!("{d:?}");
}
#[test]
diff --git a/nearby/presence/np_adv/src/extended/serialize/de_header_tests.rs b/nearby/presence/np_adv/src/extended/serialize/de_header_tests.rs
index ef8f5dc..92e5e45 100644
--- a/nearby/presence/np_adv/src/extended/serialize/de_header_tests.rs
+++ b/nearby/presence/np_adv/src/extended/serialize/de_header_tests.rs
@@ -103,7 +103,7 @@
1
} else {
// at least one type byte to handle the type = 0 case
- 1_u8 + cmp::max(1, (32 - hdr.de_type.as_u32().leading_zeros() as u8 + 6) / 7)
+ 1_u8 + cmp::max(1, (32 - hdr.de_type.as_u32().leading_zeros() as u8).div_ceil(7))
}
}
diff --git a/nearby/presence/np_adv/src/extended/serialize/test_vectors.rs b/nearby/presence/np_adv/src/extended/serialize/test_vectors.rs
index 7538d47..93b602c 100644
--- a/nearby/presence/np_adv/src/extended/serialize/test_vectors.rs
+++ b/nearby/presence/np_adv/src/extended/serialize/test_vectors.rs
@@ -127,13 +127,13 @@
(0..num_des)
.map(|de_index| {
let de_len = test_vector_seed_hkdf
- .derive_range_element(&format!("de len {}", de_index), 0..=30);
+ .derive_range_element(&format!("de len {de_index}"), 0..=30);
let data = test_vector_seed_hkdf
- .derive_vec(&format!("de data {}", de_index), de_len.try_into().unwrap());
+ .derive_vec(&format!("de data {de_index}"), de_len.try_into().unwrap());
DummyDataElement {
de_type: DeType::try_from(
u32::try_from(test_vector_seed_hkdf.derive_range_element(
- &format!("de type {}", de_index),
+ &format!("de type {de_index}"),
0_u64..=1_000,
))
.unwrap(),
diff --git a/nearby/presence/np_adv/src/filter/tests/actions_filter_tests.rs b/nearby/presence/np_adv/src/filter/tests/actions_filter_tests.rs
index c395124..c037031 100644
--- a/nearby/presence/np_adv/src/filter/tests/actions_filter_tests.rs
+++ b/nearby/presence/np_adv/src/filter/tests/actions_filter_tests.rs
@@ -15,14 +15,15 @@
#![allow(clippy::unwrap_used)]
use super::super::*;
-use crate::legacy::data_elements::actions::{ActionBits, InstantTethering, NearbyShare};
+use crate::legacy::data_elements::actions::tests::{
+ ACTIVE_UNLOCK, CALL_TRANSFER, INSTANT_TETHERING, NEARBY_SHARE, PHONE_HUB,
+};
+use crate::legacy::data_elements::actions::ActionBits;
use crate::legacy::{Ciphertext, Plaintext};
-use alloc::vec::Vec;
-use strum::IntoEnumIterator;
#[test]
fn new_v0_actions_invalid_length() {
- let actions = [actions::ActionType::ActiveUnlock; 8];
+ let actions = [ACTIVE_UNLOCK; 8];
let result = V0ActionsFilter::new_from_slice(&actions);
assert!(result.is_err());
assert_eq!(result.err().unwrap(), InvalidLength)
@@ -30,7 +31,7 @@
#[test]
fn new_v0_actions() {
- let actions = [actions::ActionType::ActiveUnlock; 5];
+ let actions = [ACTIVE_UNLOCK; 5];
let result = V0ActionsFilter::new_from_slice(&actions);
assert!(result.is_ok());
}
@@ -46,8 +47,7 @@
// default is all 0 bits
let action_bits = ActionBits::<Plaintext>::default();
- let filter = V0ActionsFilter::new_from_slice(&[actions::ActionType::ActiveUnlock; 1])
- .expect("1 is a valid length");
+ let filter = V0ActionsFilter::new_from_slice(&[ACTIVE_UNLOCK; 1]).expect("1 is a valid length");
assert_eq!(filter.match_v0_actions(&action_bits.into()), Err(NoMatch))
}
@@ -57,8 +57,7 @@
// default is all 0 bits
let action_bits = ActionBits::<Plaintext>::default();
- let filter = V0ActionsFilter::new_from_slice(&actions::ActionType::iter().collect::<Vec<_>>())
- .expect("5 is a valid length");
+ let filter = V0ActionsFilter::new_from_slice(&[ACTIVE_UNLOCK; 1]).expect("5 is a valid length");
assert_eq!(filter.match_v0_actions(&action_bits.into()), Err(NoMatch))
}
@@ -67,9 +66,9 @@
fn test_actions_filter_single_action_present() {
// default is all 0 bits
let mut action_bits = ActionBits::<Plaintext>::default();
- action_bits.set_action(NearbyShare::from(true));
+ action_bits.set_action(NEARBY_SHARE);
- let filter = V0ActionsFilter::new_from_slice(&actions::ActionType::iter().collect::<Vec<_>>())
+ let filter = V0ActionsFilter::new_from_slice(&[ACTIVE_UNLOCK, NEARBY_SHARE])
.expect("5 is a valid length");
assert_eq!(filter.match_v0_actions(&action_bits.into()), Ok(()))
@@ -79,13 +78,13 @@
fn test_actions_filter_desired_action_not_present() {
// default is all 0 bits
let mut action_bits = ActionBits::<Plaintext>::default();
- action_bits.set_action(NearbyShare::from(true));
+ action_bits.set_action(NEARBY_SHARE);
let filter = V0ActionsFilter::new_from_slice(&[
- actions::ActionType::CallTransfer,
- actions::ActionType::ActiveUnlock,
- actions::ActionType::InstantTethering,
- actions::ActionType::PhoneHub,
+ CALL_TRANSFER,
+ ACTIVE_UNLOCK,
+ INSTANT_TETHERING,
+ PHONE_HUB,
])
.expect("4 is a valid length");
@@ -96,11 +95,11 @@
fn test_multiple_actions_set() {
// default is all 0 bits
let mut action_bits = ActionBits::<Ciphertext>::default();
- action_bits.set_action(NearbyShare::from(true));
- action_bits.set_action(InstantTethering::from(true));
+ action_bits.set_action(NEARBY_SHARE);
+ action_bits.set_action(INSTANT_TETHERING);
- let filter = V0ActionsFilter::new_from_slice(&[actions::ActionType::InstantTethering])
- .expect("1 is a valid length");
+ let filter =
+ V0ActionsFilter::new_from_slice(&[INSTANT_TETHERING]).expect("1 is a valid length");
assert_eq!(filter.match_v0_actions(&action_bits.into()), Ok(()))
}
@@ -109,19 +108,11 @@
fn test_multiple_actions_set_both_present() {
// default is all 0 bits
let mut action_bits = ActionBits::<Ciphertext>::default();
- action_bits.set_action(NearbyShare::from(true));
- action_bits.set_action(InstantTethering::from(true));
+ action_bits.set_action(NEARBY_SHARE);
+ action_bits.set_action(INSTANT_TETHERING);
- let filter = V0ActionsFilter::new_from_slice(&[
- actions::ActionType::InstantTethering,
- actions::ActionType::NearbyShare,
- ])
- .expect("7 is a valid length");
+ let filter = V0ActionsFilter::new_from_slice(&[INSTANT_TETHERING, NEARBY_SHARE])
+ .expect("7 is a valid length");
assert_eq!(filter.match_v0_actions(&action_bits.into()), Ok(()))
}
-
-#[test]
-fn num_actions_is_correct() {
- assert_eq!(actions::ActionType::iter().count(), NUM_ACTIONS);
-}
diff --git a/nearby/presence/np_adv/src/filter/tests/data_elements_filter_tests.rs b/nearby/presence/np_adv/src/filter/tests/data_elements_filter_tests.rs
index 0b83831..e19d41e 100644
--- a/nearby/presence/np_adv/src/filter/tests/data_elements_filter_tests.rs
+++ b/nearby/presence/np_adv/src/filter/tests/data_elements_filter_tests.rs
@@ -16,8 +16,7 @@
use crate::{
legacy::{
data_elements::{
- actions::{ActionBits, ActiveUnlock},
- tx_power::TxPowerDataElement,
+ actions::tests::ACTIVE_UNLOCK, actions::ActionBits, tx_power::TxPowerDataElement,
},
Ciphertext, Plaintext,
},
@@ -45,8 +44,7 @@
#[test]
fn match_not_contains_actions() {
- let filter = V0ActionsFilter::new_from_slice(&[actions::ActionType::ActiveUnlock; 1])
- .expect("1 is a valid length");
+ let filter = V0ActionsFilter::new_from_slice(&[ACTIVE_UNLOCK; 1]).expect("1 is a valid length");
let filter = V0DataElementsFilter { contains_tx_power: None, actions_filter: Some(filter) };
let tx_power = TxPower::try_from(5).expect("within range");
let result = filter.match_v0_legible_adv(|| {
@@ -58,13 +56,12 @@
#[test]
fn match_contains_actions() {
- let filter = V0ActionsFilter::new_from_slice(&[actions::ActionType::ActiveUnlock; 1])
- .expect("1 is a valid length");
+ let filter = V0ActionsFilter::new_from_slice(&[ACTIVE_UNLOCK; 1]).expect("1 is a valid length");
let filter = V0DataElementsFilter { contains_tx_power: None, actions_filter: Some(filter) };
let tx_power = TxPower::try_from(5).expect("within range");
let mut action_bits = ActionBits::<Ciphertext>::default();
- action_bits.set_action(ActiveUnlock::from(true));
+ action_bits.set_action(ACTIVE_UNLOCK);
let result = filter.match_v0_legible_adv(|| {
[
@@ -78,14 +75,13 @@
#[test]
fn match_contains_both() {
- let filter = V0ActionsFilter::new_from_slice(&[actions::ActionType::ActiveUnlock; 1])
- .expect("1 is a valid length");
+ let filter = V0ActionsFilter::new_from_slice(&[ACTIVE_UNLOCK; 1]).expect("1 is a valid length");
let filter =
V0DataElementsFilter { contains_tx_power: Some(true), actions_filter: Some(filter) };
let tx_power = TxPower::try_from(5).expect("within range");
let mut action_bits = ActionBits::<Ciphertext>::default();
- action_bits.set_action(ActiveUnlock::from(true));
+ action_bits.set_action(ACTIVE_UNLOCK);
let result = filter.match_v0_legible_adv(|| {
[
@@ -99,8 +95,7 @@
#[test]
fn match_contains_either() {
- let filter = V0ActionsFilter::new_from_slice(&[actions::ActionType::ActiveUnlock; 1])
- .expect("1 is a valid length");
+ let filter = V0ActionsFilter::new_from_slice(&[ACTIVE_UNLOCK; 1]).expect("1 is a valid length");
let filter =
V0DataElementsFilter { contains_tx_power: Some(true), actions_filter: Some(filter) };
let tx_power = TxPower::try_from(5).expect("within range");
diff --git a/nearby/presence/np_adv/src/legacy/data_elements/actions/macros.rs b/nearby/presence/np_adv/src/legacy/data_elements/actions/macros.rs
deleted file mode 100644
index 3d1ab48..0000000
--- a/nearby/presence/np_adv/src/legacy/data_elements/actions/macros.rs
+++ /dev/null
@@ -1,124 +0,0 @@
-// Copyright 2022 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-/// Create a struct holding a `bool`.
-macro_rules! boolean_element_struct {
- ($type_name:ident) => {
- #[derive(Debug)]
- #[allow(missing_docs)]
- pub struct $type_name {
- enabled: bool,
- }
- };
-}
-
-/// Create a `From<bool>` impl for a struct made with [boolean_element_struct].
-macro_rules! boolean_element_struct_from_bool {
- ($type_name:ident) => {
- impl From<bool> for $type_name {
- fn from(value: bool) -> Self {
- $type_name { enabled: value }
- }
- }
- };
-}
-
-/// The guts of an ActionElement impl
-macro_rules! boolean_element_action_element_impl_shared {
- ($type_name:ident, $index:expr) => {
- const HIGH_BIT_INDEX: u32 = $index;
- const ACTION_TYPE: $crate::legacy::data_elements::actions::ActionType =
- $crate::legacy::data_elements::actions::ActionType::$type_name;
- };
-}
-
-/// Create a struct w/ `From<bool>` and [`ActionElement`](super::ActionElement) impls.
-/// Use `plaintext_only`, `ciphertext_only`, or `plaintext_and_ciphertext` to create appropriate
-/// impls.
-macro_rules! boolean_element {
- ($type_name:ident, $index:expr, ciphertext_only) => {
- $crate::legacy::data_elements::actions::macros::boolean_element_struct!($type_name);
- $crate::legacy::data_elements::actions::macros::boolean_element_struct_from_bool!($type_name);
-
- impl $crate::legacy::data_elements::actions::ActionElement for $type_name {
- $crate::legacy::data_elements::actions::macros::boolean_element_action_element_impl_shared!(
- $type_name, $index
- );
-
- fn supports_flavor(flavor: $crate::legacy::PacketFlavorEnum) -> bool {
- match flavor {
- $crate::legacy::PacketFlavorEnum::Plaintext => false,
- $crate::legacy::PacketFlavorEnum::Ciphertext => true,
- }
- }
-
- fn bits(&self) -> u8 {
- self.enabled as u8
- }
- }
-
- $crate::legacy::data_elements::actions::macros::boolean_element_to_encrypted_element!($type_name);
- };
- ($type_name:ident, $index:expr, plaintext_and_ciphertext) => {
- $crate::legacy::data_elements::actions::macros::boolean_element_struct!($type_name);
- $crate::legacy::data_elements::actions::macros::boolean_element_struct_from_bool!($type_name);
-
- impl $crate::legacy::data_elements::actions::ActionElement for $type_name {
- $crate::legacy::data_elements::actions::macros::boolean_element_action_element_impl_shared!(
- $type_name, $index
- );
-
- fn supports_flavor(flavor: $crate::legacy::PacketFlavorEnum) -> bool {
- match flavor {
- $crate::legacy::PacketFlavorEnum::Plaintext => true,
- $crate::legacy::PacketFlavorEnum::Ciphertext => true,
- }
- }
-
- fn bits(&self) -> u8 {
- self.enabled as u8
- }
- }
-
- $crate::legacy::data_elements::actions::macros::boolean_element_to_plaintext_element!($type_name);
- $crate::legacy::data_elements::actions::macros::boolean_element_to_encrypted_element!($type_name);
- };
-}
-
-/// Create a [`ToActionElement<Encrypted>`](super::ActionElementFlavor) impl with the given index and length 1.
-macro_rules! boolean_element_to_encrypted_element {
- ( $type_name:ident) => {
- impl $crate::legacy::data_elements::actions::ActionElementFlavor<$crate::legacy::Ciphertext>
- for $type_name
- {
- }
- };
-}
-
-/// Create a [`ToActionElement<Plaintext>`](super::ActionElementFlavor) impl with the given index and length 1.
-macro_rules! boolean_element_to_plaintext_element {
- ( $type_name:ident) => {
- impl $crate::legacy::data_elements::actions::ActionElementFlavor<$crate::legacy::Plaintext>
- for $type_name
- {
- }
- };
-}
-
-// expose macros to the rest of the crate without macro_export's behavior of exporting at crate root
-pub(crate) use {
- boolean_element, boolean_element_action_element_impl_shared, boolean_element_struct,
- boolean_element_struct_from_bool, boolean_element_to_encrypted_element,
- boolean_element_to_plaintext_element,
-};
diff --git a/nearby/presence/np_adv/src/legacy/data_elements/actions/mod.rs b/nearby/presence/np_adv/src/legacy/data_elements/actions/mod.rs
index 36ac35d..5010936 100644
--- a/nearby/presence/np_adv/src/legacy/data_elements/actions/mod.rs
+++ b/nearby/presence/np_adv/src/legacy/data_elements/actions/mod.rs
@@ -35,9 +35,7 @@
use core::{marker, ops};
use nom::{bytes, combinator, error};
use sink::Sink;
-use strum::IntoEnumIterator as _;
-mod macros;
#[cfg(test)]
pub(crate) mod tests;
@@ -52,7 +50,7 @@
}
/// Max length of an actions DE contents
-pub(crate) const ACTIONS_MAX_LEN: usize = 3;
+pub(crate) const ACTIONS_MAX_LEN: usize = 4;
/// Range of valid actual lengths
pub(crate) const ACTIONS_VALID_ACTUAL_LEN: ops::RangeInclusive<usize> = 1..=ACTIONS_MAX_LEN;
@@ -75,14 +73,9 @@
))(de_contents)
.map_err(|_| DataElementDeserializeError::DeserializeError { de_type: Self::DE_TYPE_CODE })
.map(|(_remaining, actions)| actions)
- .and_then(|action_bits_num| {
- let action = ActionBits::try_from(action_bits_num).map_err(|e| {
- DataElementDeserializeError::FlavorNotSupported {
- de_type: Self::DE_TYPE_CODE,
- flavor: e.flavor,
- }
- })?;
- Ok(Self { action })
+ .map(|action_bits_num| {
+ let action = ActionBits::from(action_bits_num);
+ Self { action }
})
}
}
@@ -166,6 +159,11 @@
pub fn has_action(&self, action_type: ActionType) -> bool {
self.bits_for_type(action_type) != 0
}
+
+ /// Return a list of all ActionType contained within the ActionBits.
+ pub fn actions(&self) -> impl Iterator<Item = ActionType> + use<'_, F> {
+ (0..ActionType::MAX_ACTION_ID).map(ActionType).filter(|action| self.has_action(*action))
+ }
}
impl<F: PacketFlavor> Default for ActionBits<F> {
@@ -183,46 +181,20 @@
flavor: PacketFlavorEnum,
}
-lazy_static::lazy_static! {
- /// All bits for plaintext action types: 1 where a plaintext action could have a bit, 0 elsewhere.
- static ref ALL_PLAINTEXT_ELEMENT_BITS: u32 = ActionType::iter()
- .filter(|t| t.supports_flavor(PacketFlavorEnum::Plaintext))
- .map(|t| t.all_bits())
- .fold(0_u32, |accum, bits| accum | bits);
-}
-
-lazy_static::lazy_static! {
- /// All bits for ciphertext action types: 1 where a ciphertext action could have a bit, 0 elsewhere.
- static ref ALL_CIPHERTEXT_ELEMENT_BITS: u32 = ActionType::iter()
- .filter(|t| t.supports_flavor(PacketFlavorEnum::Ciphertext))
- .map(|t| t.all_bits())
- .fold(0_u32, |accum, bits| accum | bits);
-}
-
impl<F: PacketFlavor> ActionBits<F> {
/// Tries to create ActionBits from a u32, returning error in the event a specific bit is set for
/// an unsupported flavor
- pub fn try_from(value: u32) -> Result<Self, FlavorNotSupported> {
- let ok_bits: u32 = match F::ENUM_VARIANT {
- PacketFlavorEnum::Plaintext => *ALL_PLAINTEXT_ELEMENT_BITS,
- PacketFlavorEnum::Ciphertext => *ALL_CIPHERTEXT_ELEMENT_BITS,
- };
-
- // no bits set beyond what's allowed for this flavor
- if value | ok_bits == ok_bits {
- Ok(Self { bits: value, flavor: marker::PhantomData })
- } else {
- Err(FlavorNotSupported { flavor: F::ENUM_VARIANT })
- }
+ pub fn from(value: u32) -> Self {
+ Self { bits: value, flavor: marker::PhantomData }
}
/// Set the bits for the provided element.
/// Bits outside the range set by the action will be unaffected.
- pub fn set_action<E: ActionElementFlavor<F>>(&mut self, action_element: E) {
+ pub fn set_action<E: ActionElement>(&mut self, action_element: E) {
let bits = action_element.bits();
// validate that the element is not horribly broken
- debug_assert!(E::HIGH_BIT_INDEX < 32);
+ debug_assert!(action_element.high_bit_index() < 32);
// must not have bits set past the low `len` bits
debug_assert_eq!(0, bits >> 1);
@@ -230,16 +202,16 @@
let byte_extended = bits as u32;
// Shift so that the high bit is at the desired index.
// Won't overflow since length > 0.
- let bits_in_position = byte_extended << (31 - E::HIGH_BIT_INDEX);
+ let bits_in_position = byte_extended << (31 - action_element.high_bit_index());
// We want to effectively clear out the bits already in place, so we don't want to just |=.
// Instead, we construct a u32 with all 1s above and below the relevant bits and &=, so that
// if the new bits are 0, the stored bits will be cleared.
// avoid overflow when index = 0 -- need zero 1 bits to the left in that case
- let left_1s = u32::MAX.checked_shl(32 - E::HIGH_BIT_INDEX).unwrap_or(0);
+ let left_1s = u32::MAX.checked_shl(32 - action_element.high_bit_index()).unwrap_or(0);
// avoid underflow when index + len = 32 -- zero 1 bits to the right
- let right_1s = u32::MAX.checked_shr(E::HIGH_BIT_INDEX + 1).unwrap_or(0);
+ let right_1s = u32::MAX.checked_shr(action_element.high_bit_index() + 1).unwrap_or(0);
let mask = left_1s | right_1s;
let bits_for_other_actions = self.bits & mask;
self.bits = bits_for_other_actions | bits_in_position;
@@ -250,7 +222,7 @@
/// an actions field of all zeroes is required by the spec to occupy exactly one byte.
fn bytes_used(&self) -> usize {
let bits_used = 32 - self.bits.trailing_zeros();
- let raw_count = (bits_used as usize + 7) / 8;
+ let raw_count = (bits_used as usize).div_ceil(8);
if raw_count == 0 {
1 // Uncommon case - should only be hit for all-zero action bits
} else {
@@ -271,21 +243,10 @@
pub trait ActionElement {
/// The assigned offset for this type from the high bit in the eventual bit sequence of all
/// actions.
- ///
- /// Each implementation must have a non-conflicting index defined by
- /// [Self::HIGH_BIT_INDEX]
- const HIGH_BIT_INDEX: u32;
-
- /// Forces implementations to have a matching enum variant so the enum can be kept up to date.
- const ACTION_TYPE: ActionType;
-
- /// Returns whether this action supports the provided `flavor`.
- ///
- /// Must match the implementations of [ActionElementFlavor].
- fn supports_flavor(flavor: PacketFlavorEnum) -> bool;
+ fn high_bit_index(&self) -> u32;
/// Returns the low bit that should be included in the final bit vector
- /// starting at [Self::HIGH_BIT_INDEX].
+ /// starting at [Self::high_bit_index()].
fn bits(&self) -> u8;
}
@@ -293,23 +254,18 @@
pub trait ActionElementFlavor<F: PacketFlavor>: ActionElement {}
/// Provides a way to iterate over all action types.
-#[derive(Clone, Copy, strum_macros::EnumIter, PartialEq, Eq, Hash, Debug)]
+#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
#[allow(missing_docs)]
-pub enum ActionType {
- CrossDevSdk,
- CallTransfer,
- ActiveUnlock,
- NearbyShare,
- InstantTethering,
- PhoneHub,
-}
+pub struct ActionType(pub u8);
impl ActionType {
+ const MAX_ACTION_ID: u8 = 31;
+
+ #[cfg(test)]
/// A u32 with all possible bits for this action type set
const fn all_bits(&self) -> u32 {
(u32::MAX << (31_u32)) >> self.high_bit_index()
}
-
/// Get the range of the bits occupied used by this bit index. For example, if the action type
/// uses the 5th and 6th bits, the returned range will be (5..7).
/// (0 is the index of the most significant bit).
@@ -320,32 +276,16 @@
}
const fn high_bit_index(&self) -> u32 {
- match self {
- ActionType::CrossDevSdk => CrossDevSdk::HIGH_BIT_INDEX,
- ActionType::CallTransfer => CallTransfer::HIGH_BIT_INDEX,
- ActionType::ActiveUnlock => ActiveUnlock::HIGH_BIT_INDEX,
- ActionType::NearbyShare => NearbyShare::HIGH_BIT_INDEX,
- ActionType::InstantTethering => InstantTethering::HIGH_BIT_INDEX,
- ActionType::PhoneHub => PhoneHub::HIGH_BIT_INDEX,
- }
- }
-
- pub(crate) fn supports_flavor(&self, flavor: PacketFlavorEnum) -> bool {
- match self {
- ActionType::CrossDevSdk => CrossDevSdk::supports_flavor(flavor),
- ActionType::CallTransfer => CallTransfer::supports_flavor(flavor),
- ActionType::ActiveUnlock => ActiveUnlock::supports_flavor(flavor),
- ActionType::NearbyShare => NearbyShare::supports_flavor(flavor),
- ActionType::InstantTethering => InstantTethering::supports_flavor(flavor),
- ActionType::PhoneHub => PhoneHub::supports_flavor(flavor),
- }
+ self.0 as u32
}
}
-// enabling an element for public adv requires privacy approval due to fingerprinting risk
-macros::boolean_element!(CrossDevSdk, 1, plaintext_and_ciphertext);
-macros::boolean_element!(CallTransfer, 4, ciphertext_only);
-macros::boolean_element!(ActiveUnlock, 8, ciphertext_only);
-macros::boolean_element!(NearbyShare, 9, plaintext_and_ciphertext);
-macros::boolean_element!(InstantTethering, 10, ciphertext_only);
-macros::boolean_element!(PhoneHub, 11, ciphertext_only);
+impl ActionElement for ActionType {
+ fn high_bit_index(&self) -> u32 {
+ self.high_bit_index()
+ }
+
+ fn bits(&self) -> u8 {
+ true as u8
+ }
+}
diff --git a/nearby/presence/np_adv/src/legacy/data_elements/actions/tests.rs b/nearby/presence/np_adv/src/legacy/data_elements/actions/tests.rs
index b96f94a..0f30215 100644
--- a/nearby/presence/np_adv/src/legacy/data_elements/actions/tests.rs
+++ b/nearby/presence/np_adv/src/legacy/data_elements/actions/tests.rs
@@ -30,21 +30,21 @@
};
use rand::seq::SliceRandom;
use rand::Rng;
-use std::collections;
use std::panic;
use std::prelude::rust_2021::*;
+pub(crate) const CALL_TRANSFER: ActionType = ActionType(4);
+pub(crate) const ACTIVE_UNLOCK: ActionType = ActionType(8);
+pub(crate) const NEARBY_SHARE: ActionType = ActionType(9);
+pub(crate) const INSTANT_TETHERING: ActionType = ActionType(10);
+pub(crate) const PHONE_HUB: ActionType = ActionType(11);
+
#[test]
fn setting_action_only_changes_that_actions_bits() {
- fn do_test<F: PacketFlavor>(
- set_ones: impl Fn(ActionType, &mut ActionBits<F>),
- set_zeros: impl Fn(ActionType, &mut ActionBits<F>),
- ) {
- for t in supported_action_types(F::ENUM_VARIANT) {
- let other_types = supported_action_types(F::ENUM_VARIANT)
- .into_iter()
- .filter(|t2| *t2 != t)
- .collect::<Vec<_>>();
+ fn do_test<F: PacketFlavor>(set_ones: impl Fn(ActionType, &mut ActionBits<F>)) {
+ for t in supported_action_types() {
+ let other_types =
+ supported_action_types().into_iter().filter(|t2| *t2 != t).collect::<Vec<_>>();
let mut actions = ActionBits::<F>::default();
set_ones(t, &mut actions);
@@ -59,35 +59,17 @@
for &t2 in &other_types {
assert_eq!(0, actions.bits_for_type(t2))
}
-
- // now check that unsetting works
- actions.bits = u32::MAX;
- set_zeros(t, &mut actions);
-
- assert_eq!(!t.all_bits(), actions.as_u32());
- assert_eq!(0, actions.bits_for_type(t));
- assert!(!actions.has_action(t));
- // other types are set
- for &t2 in &other_types {
- assert_eq!(t2.all_bits() >> (31 - t2.high_bit_index()), actions.bits_for_type(t2));
- }
}
}
- do_test(
- |t, bits| set_plaintext_action(t, true, bits),
- |t, bits| set_plaintext_action(t, false, bits),
- );
- do_test(
- |t, bits| set_ciphertexttext_action(t, true, bits),
- |t, bits| set_ciphertexttext_action(t, false, bits),
- );
+ do_test(|t, bits: &mut ActionBits<Plaintext>| set_action(t, bits));
+ do_test(|t, bits: &mut ActionBits<Ciphertext>| set_action(t, bits));
}
#[test]
fn random_combos_of_actions_have_correct_bits_set() {
fn do_test<F: PacketFlavor>(set_ones: impl Fn(ActionType, &mut ActionBits<F>)) {
- let all_types = supported_action_types(F::ENUM_VARIANT);
+ let all_types = supported_action_types();
let mut rng = rand::thread_rng();
for _ in 0..1000 {
@@ -112,8 +94,8 @@
}
}
- do_test::<Plaintext>(|t, bits| set_plaintext_action(t, true, bits));
- do_test::<Ciphertext>(|t, bits| set_ciphertexttext_action(t, true, bits));
+ do_test::<Plaintext>(set_action);
+ do_test::<Ciphertext>(set_action);
}
#[test]
@@ -154,7 +136,7 @@
// Special-case: All-zeroes should lead to a single byte being used.
assert_eq!(1, actions.bytes_used());
- actions.set_action(NearbyShare::from(true));
+ actions.set_action(NEARBY_SHARE);
assert_eq!(2, actions.bytes_used());
actions.set_action(LastBit::from(true));
@@ -201,11 +183,12 @@
let mut action = all_plaintext_actions_set();
action.set_action(LastBit::from(true));
- // byte 0: cross dev sdk = 1
- // byte 1: nearby share
- // byte 2: last bit
+ // byte 0: actions 0 through 7
+ // byte 1: action 8 through 15
+ // byte 2: actions 16 through 23
+ // byte 2: actions 24 through 31
assert_eq!(
- &[actions_de_header_byte(3), 0x40, 0x40, 0x01],
+ &[actions_de_header_byte(4), 0xFF, 0xFF, 0xFF, 0xFF],
serialize(&ActionsDataElement::<Plaintext>::from(action)).as_slice()
);
}
@@ -215,11 +198,12 @@
let mut action = all_ciphertext_actions_set();
action.set_action(LastBit::from(true));
- // byte 1: cross dev sdk = 1, call transfer = 4
- // byte 2: active unlock, nearby share, instant tethering, phone hub,
- // byte 3: last bit
+ // byte 0: actions 0 through 7
+ // byte 1: action 8 through 15
+ // byte 2: actions 16 through 23
+ // byte 2: actions 24 through 31
assert_eq!(
- &[actions_de_header_byte(3), 0x48, 0xF0, 0x01],
+ &[actions_de_header_byte(4), 0xFF, 0xFF, 0xFF, 0xFF],
serialize(&ActionsDataElement::<Ciphertext>::from(action)).as_slice()
);
}
@@ -231,7 +215,7 @@
F: PacketFlavor,
ActionsDataElement<F>: DeserializeDataElement,
{
- let all_types = supported_action_types(F::ENUM_VARIANT);
+ let all_types = supported_action_types();
let mut rng = rand::thread_rng();
for _ in 0..1000 {
@@ -253,79 +237,17 @@
}
}
- do_test::<Plaintext>(|t, bits| set_plaintext_action(t, true, bits));
- do_test::<Ciphertext>(|t, bits| set_ciphertexttext_action(t, true, bits));
-}
-
-#[test]
-fn action_element_bits_dont_overlap() {
- let type_to_bits =
- ActionType::iter().map(|t| (t, t.all_bits())).collect::<collections::HashMap<_, _>>();
-
- for t in ActionType::iter() {
- let bits = type_to_bits.get(&t).unwrap();
-
- for (_, other_bits) in type_to_bits.iter().filter(|(other_type, _)| t != **other_type) {
- assert_eq!(0, bits & other_bits, "type {t:?}");
- }
- }
+ do_test::<Plaintext>(set_action);
+ do_test::<Ciphertext>(set_action);
}
#[test]
fn action_type_all_bits_masks() {
- assert_eq!(0x08000000, ActionType::CallTransfer.all_bits());
- assert_eq!(0x00800000, ActionType::ActiveUnlock.all_bits());
- assert_eq!(0x00400000, ActionType::NearbyShare.all_bits());
- assert_eq!(0x00200000, ActionType::InstantTethering.all_bits());
- assert_eq!(0x00100000, ActionType::PhoneHub.all_bits());
-}
-
-#[test]
-fn action_type_all_bits_in_per_type_masks() {
- for t in supported_action_types(PacketFlavorEnum::Plaintext) {
- assert_eq!(t.all_bits(), t.all_bits() & *ALL_PLAINTEXT_ELEMENT_BITS);
- }
- for t in supported_action_types(PacketFlavorEnum::Ciphertext) {
- assert_eq!(t.all_bits(), t.all_bits() & *ALL_CIPHERTEXT_ELEMENT_BITS);
- }
-}
-
-#[test]
-fn action_bits_try_from_flavor_mismatch_plaintext() {
- assert_eq!(
- FlavorNotSupported { flavor: PacketFlavorEnum::Plaintext },
- ActionBits::<Plaintext>::try_from(ActionType::CallTransfer.all_bits()).unwrap_err()
- );
-}
-
-#[test]
-fn actions_de_deser_plaintext_with_ciphertext_action() {
- assert_eq!(
- DataElementDeserializeError::FlavorNotSupported {
- de_type: ActionsDataElement::<Plaintext>::DE_TYPE_CODE,
- flavor: PacketFlavorEnum::Plaintext,
- },
- <ActionsDataElement<Plaintext> as DeserializeDataElement>::deserialize::<Plaintext>(&[
- // active unlock bit set
- 0x00, 0x80, 0x00,
- ])
- .unwrap_err()
- );
-}
-
-#[test]
-fn actions_de_deser_ciphertext_with_plaintext_action() {
- assert_eq!(
- DataElementDeserializeError::FlavorNotSupported {
- de_type: ActionsDataElement::<Plaintext>::DE_TYPE_CODE,
- flavor: PacketFlavorEnum::Ciphertext,
- },
- <ActionsDataElement<Ciphertext> as DeserializeDataElement>::deserialize::<Ciphertext>(&[
- // Finder bit set
- 0x00, 0x00, 0x80,
- ])
- .unwrap_err()
- );
+ assert_eq!(0x08000000, CALL_TRANSFER.all_bits());
+ assert_eq!(0x00800000, ACTIVE_UNLOCK.all_bits());
+ assert_eq!(0x00400000, NEARBY_SHARE.all_bits());
+ assert_eq!(0x00200000, INSTANT_TETHERING.all_bits());
+ assert_eq!(0x00100000, PHONE_HUB.all_bits());
}
#[test]
@@ -438,22 +360,22 @@
#[test]
fn has_action_plaintext_works() {
let mut action_bits = ActionBits::<Plaintext>::default();
- action_bits.set_action(NearbyShare::from(true));
+ action_bits.set_action(NEARBY_SHARE);
let action_de = ActionsDataElement::from(action_bits);
- assert!(action_de.action.has_action(ActionType::NearbyShare));
- assert!(!action_de.action.has_action(ActionType::ActiveUnlock));
- assert!(!action_de.action.has_action(ActionType::PhoneHub));
+ assert!(action_de.action.has_action(NEARBY_SHARE));
+ assert!(!action_de.action.has_action(ACTIVE_UNLOCK));
+ assert!(!action_de.action.has_action(PHONE_HUB));
}
#[test]
fn has_action_encrypted_works() {
let mut action_bits = ActionBits::<Ciphertext>::default();
- action_bits.set_action(NearbyShare::from(true));
- action_bits.set_action(ActiveUnlock::from(true));
+ action_bits.set_action(NEARBY_SHARE);
+ action_bits.set_action(ACTIVE_UNLOCK);
let action_de = ActionsDataElement::from(action_bits);
- assert!(action_de.action.has_action(ActionType::NearbyShare));
- assert!(action_de.action.has_action(ActionType::ActiveUnlock));
- assert!(!action_de.action.has_action(ActionType::PhoneHub));
+ assert!(action_de.action.has_action(NEARBY_SHARE));
+ assert!(action_de.action.has_action(ACTIVE_UNLOCK));
+ assert!(!action_de.action.has_action(PHONE_HUB));
}
#[test]
@@ -498,6 +420,9 @@
}
mod coverage_gaming {
+ use crate::legacy::data_elements::actions::tests::{
+ ACTIVE_UNLOCK, CALL_TRANSFER, INSTANT_TETHERING, NEARBY_SHARE, PHONE_HUB,
+ };
use crate::legacy::data_elements::actions::*;
use crate::legacy::Plaintext;
use alloc::format;
@@ -505,7 +430,7 @@
#[test]
fn actions_de_debug() {
let actions = ActionsDataElement::<Plaintext>::from(ActionBits::default());
- let _ = format!("{:?}", actions);
+ let _ = format!("{actions:?}");
}
#[test]
@@ -515,16 +440,16 @@
#[test]
fn action_type_clone_debug() {
- let _ = format!("{:?}", ActionType::CallTransfer.clone());
+ let _ = format!("{:?}", CALL_TRANSFER.clone());
}
#[test]
fn actions_debug() {
- let _ = format!("{:?}", CallTransfer::from(true));
- let _ = format!("{:?}", ActiveUnlock::from(true));
- let _ = format!("{:?}", NearbyShare::from(true));
- let _ = format!("{:?}", InstantTethering::from(true));
- let _ = format!("{:?}", PhoneHub::from(true));
+ let _ = format!("{CALL_TRANSFER:?}");
+ let _ = format!("{ACTIVE_UNLOCK:?}");
+ let _ = format!("{NEARBY_SHARE:?}");
+ let _ = format!("{INSTANT_TETHERING:?}");
+ let _ = format!("{PHONE_HUB:?}");
}
}
@@ -541,21 +466,14 @@
}
impl ActionElement for FirstBit {
- const HIGH_BIT_INDEX: u32 = 0;
- // don't want to add a variant for this test only type
- const ACTION_TYPE: ActionType = ActionType::ActiveUnlock;
-
- fn supports_flavor(_flavor: PacketFlavorEnum) -> bool {
- true
- }
-
fn bits(&self) -> u8 {
self.enabled as u8
}
-}
-macros::boolean_element_to_plaintext_element!(FirstBit);
-macros::boolean_element_to_encrypted_element!(FirstBit);
+ fn high_bit_index(&self) -> u32 {
+ 0
+ }
+}
// hypothetical action using the last bit
#[derive(Debug)]
@@ -570,21 +488,14 @@
}
impl ActionElement for LastBit {
- const HIGH_BIT_INDEX: u32 = 23;
- // don't want to add a variant for this test only type
- const ACTION_TYPE: ActionType = ActionType::ActiveUnlock;
-
- fn supports_flavor(_flavor: PacketFlavorEnum) -> bool {
- true
- }
-
fn bits(&self) -> u8 {
self.enabled as u8
}
-}
-macros::boolean_element_to_plaintext_element!(LastBit);
-macros::boolean_element_to_encrypted_element!(LastBit);
+ fn high_bit_index(&self) -> u32 {
+ 23
+ }
+}
// An action that only supports plaintext, to allow testing that error case
pub(in crate::legacy) struct PlaintextOnly {
@@ -598,25 +509,14 @@
}
impl ActionElement for PlaintextOnly {
- const HIGH_BIT_INDEX: u32 = 22;
-
- const ACTION_TYPE: ActionType = ActionType::ActiveUnlock;
-
- fn supports_flavor(flavor: PacketFlavorEnum) -> bool {
- match flavor {
- PacketFlavorEnum::Plaintext => true,
- PacketFlavorEnum::Ciphertext => false,
- }
- }
-
fn bits(&self) -> u8 {
self.enabled as u8
}
-}
-macros::boolean_element_to_plaintext_element!(PlaintextOnly);
-// sneakily allow serializing it, but deserializing will fail due to supports_flavor above
-macros::boolean_element_to_encrypted_element!(PlaintextOnly);
+ fn high_bit_index(&self) -> u32 {
+ 22
+ }
+}
fn assert_eq_hex(expected: u32, actual: u32) {
assert_eq!(expected, actual, "{expected:#010X} != {actual:#010X}");
@@ -624,34 +524,24 @@
pub(crate) fn all_plaintext_actions_set() -> ActionBits<Plaintext> {
let mut action = ActionBits::default();
- action.set_action(CrossDevSdk::from(true));
- action.set_action(NearbyShare::from(true));
-
- assert!(supported_action_types(PacketFlavorEnum::Plaintext)
- .into_iter()
- .all(|t| t.all_bits() & action.bits != 0));
+ for a in supported_action_types() {
+ action.set_action(a);
+ }
action
}
pub(crate) fn all_ciphertext_actions_set() -> ActionBits<Ciphertext> {
let mut action = ActionBits::default();
- action.set_action(CrossDevSdk::from(true));
- action.set_action(CallTransfer::from(true));
- action.set_action(ActiveUnlock::from(true));
- action.set_action(NearbyShare::from(true));
- action.set_action(InstantTethering::from(true));
- action.set_action(PhoneHub::from(true));
-
- assert!(supported_action_types(PacketFlavorEnum::Ciphertext)
- .into_iter()
- .all(|t| t.all_bits() & action.bits != 0));
+ for a in supported_action_types() {
+ action.set_action(a);
+ }
action
}
-fn supported_action_types(flavor: PacketFlavorEnum) -> Vec<ActionType> {
- ActionType::iter().filter(|t| t.supports_flavor(flavor)).collect()
+pub(crate) fn supported_action_types() -> Vec<ActionType> {
+ (0..ActionType::MAX_ACTION_ID + 1).map(ActionType).collect()
}
/// Encode a DE header byte with the provided type and actual len, transforming into an encoded
@@ -663,28 +553,6 @@
)
}
-pub(crate) fn set_plaintext_action(t: ActionType, value: bool, bits: &mut ActionBits<Plaintext>) {
- match t {
- ActionType::CrossDevSdk => bits.set_action(CrossDevSdk::from(value)),
- ActionType::NearbyShare => bits.set_action(NearbyShare::from(value)),
- ActionType::CallTransfer
- | ActionType::PhoneHub
- | ActionType::ActiveUnlock
- | ActionType::InstantTethering => panic!(),
- }
-}
-
-pub(crate) fn set_ciphertexttext_action(
- t: ActionType,
- value: bool,
- bits: &mut ActionBits<Ciphertext>,
-) {
- match t {
- ActionType::CrossDevSdk => bits.set_action(CrossDevSdk::from(value)),
- ActionType::CallTransfer => bits.set_action(CallTransfer::from(value)),
- ActionType::ActiveUnlock => bits.set_action(ActiveUnlock::from(value)),
- ActionType::NearbyShare => bits.set_action(NearbyShare::from(value)),
- ActionType::InstantTethering => bits.set_action(InstantTethering::from(value)),
- ActionType::PhoneHub => bits.set_action(PhoneHub::from(value)),
- }
+pub(crate) fn set_action<T: PacketFlavor>(t: ActionType, bits: &mut ActionBits<T>) {
+ bits.set_action(t);
}
diff --git a/nearby/presence/np_adv/src/legacy/data_elements/de_type/mod.rs b/nearby/presence/np_adv/src/legacy/data_elements/de_type/mod.rs
index e5dd153..f239690 100644
--- a/nearby/presence/np_adv/src/legacy/data_elements/de_type/mod.rs
+++ b/nearby/presence/np_adv/src/legacy/data_elements/de_type/mod.rs
@@ -124,4 +124,6 @@
pub(in crate::legacy) enum DataElementType {
TxPower,
Actions,
+ Psm,
+ DeviceInfo,
}
diff --git a/nearby/presence/np_adv/src/legacy/data_elements/device_info.rs b/nearby/presence/np_adv/src/legacy/data_elements/device_info.rs
new file mode 100644
index 0000000..0a9eef1
--- /dev/null
+++ b/nearby/presence/np_adv/src/legacy/data_elements/device_info.rs
@@ -0,0 +1,112 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! Data element for Device Info.
+
+use crate::legacy::data_elements::de_type::{DeActualLength, DeEncodedLength, DeTypeCode};
+use crate::legacy::data_elements::{
+ DataElementDeserializeError, DataElementSerializationBuffer, DataElementSerializeError,
+ DeserializeDataElement, SerializeDataElement,
+};
+use crate::legacy::PacketFlavor;
+use crate::private::Sealed;
+use crate::shared_data::{DeviceInfo, DeviceType};
+use sink::Sink;
+
+/// Data element holding a [DeviceInfo].
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct DeviceInfoDataElement {
+ /// The device info value
+ pub device_info: DeviceInfo,
+}
+
+impl From<DeviceInfo> for DeviceInfoDataElement {
+ fn from(device_info: DeviceInfo) -> Self {
+ Self { device_info }
+ }
+}
+
+impl Sealed for DeviceInfoDataElement {}
+
+impl<F: PacketFlavor> SerializeDataElement<F> for DeviceInfoDataElement {
+ fn de_type_code(&self) -> DeTypeCode {
+ DeviceInfoDataElement::DE_TYPE_CODE
+ }
+
+ fn map_actual_len_to_encoded_len(&self, actual_len: DeActualLength) -> DeEncodedLength {
+ // V0 length is just the device name length + 1 for the type/truncation byte.
+ <Self as DeserializeDataElement>::LengthMapper::map_actual_len_to_encoded_len(actual_len)
+ }
+
+ fn serialize_contents(
+ &self,
+ sink: &mut DataElementSerializationBuffer,
+ ) -> Result<(), DataElementSerializeError> {
+ let mut type_and_trunc = self.device_info.device_type() as u8;
+ if self.device_info.name_truncated() {
+ type_and_trunc |= 0b10000000;
+ }
+ sink.try_push(type_and_trunc)
+ .and_then(|_| sink.try_extend_from_slice(self.device_info.device_name()))
+ .ok_or(DataElementSerializeError::InsufficientSpace)
+ }
+}
+
+impl DeserializeDataElement for DeviceInfoDataElement {
+ const DE_TYPE_CODE: DeTypeCode = match DeTypeCode::try_from(0b0011) {
+ Ok(t) => t,
+ Err(_) => unreachable!(),
+ };
+
+ type LengthMapper = DeviceInfoLengthMapper;
+
+ fn deserialize<F: PacketFlavor>(
+ de_contents: &[u8],
+ ) -> Result<Self, DataElementDeserializeError> {
+ if de_contents.len() < 6 {
+ return Err(DataElementDeserializeError::DeserializeError {
+ de_type: Self::DE_TYPE_CODE,
+ });
+ }
+
+ let type_and_trunc = de_contents[0];
+ let device_type = DeviceType::from_repr(type_and_trunc & 0b01111111)
+ .ok_or(DataElementDeserializeError::DeserializeError { de_type: Self::DE_TYPE_CODE })?;
+ let name_truncated = (type_and_trunc & 0b10000000) != 0;
+ let device_name = &de_contents[1..];
+
+ let device_info = DeviceInfo::try_from((device_type, name_truncated, device_name))
+ .map_err(|_| DataElementDeserializeError::DeserializeError {
+ de_type: Self::DE_TYPE_CODE,
+ })?;
+
+ Ok(device_info.into())
+ }
+}
+
+pub(in crate::legacy) struct DeviceInfoLengthMapper;
+
+use crate::legacy::data_elements::{DeLengthOutOfRange, LengthMapper};
+impl LengthMapper for DeviceInfoLengthMapper {
+ fn map_actual_len_to_encoded_len(actual_len: DeActualLength) -> DeEncodedLength {
+ DeEncodedLength::try_from(actual_len.as_u8())
+ .expect("Broken DE implementation produced invalid length.")
+ }
+
+ fn map_encoded_len_to_actual_len(
+ encoded_len: DeEncodedLength,
+ ) -> Result<DeActualLength, DeLengthOutOfRange> {
+ DeActualLength::try_from(encoded_len.as_u8() as usize)
+ }
+}
diff --git a/nearby/presence/np_adv/src/legacy/data_elements/mod.rs b/nearby/presence/np_adv/src/legacy/data_elements/mod.rs
index c952d08..ace5f7b 100644
--- a/nearby/presence/np_adv/src/legacy/data_elements/mod.rs
+++ b/nearby/presence/np_adv/src/legacy/data_elements/mod.rs
@@ -29,6 +29,8 @@
pub mod actions;
pub mod de_type;
+pub mod device_info;
+pub mod psm;
pub mod tx_power;
#[cfg(test)]
diff --git a/nearby/presence/np_adv/src/legacy/data_elements/psm.rs b/nearby/presence/np_adv/src/legacy/data_elements/psm.rs
new file mode 100644
index 0000000..4fc1393
--- /dev/null
+++ b/nearby/presence/np_adv/src/legacy/data_elements/psm.rs
@@ -0,0 +1,172 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! Data element for PSM.
+
+use crate::legacy::data_elements::de_type::{DeActualLength, DeEncodedLength, DeTypeCode};
+use crate::legacy::data_elements::{
+ DataElementDeserializeError, DataElementSerializationBuffer, DataElementSerializeError,
+ DeserializeDataElement, DirectMapPredicate, DirectMapper, LengthMapper, SerializeDataElement,
+};
+use crate::legacy::PacketFlavor;
+use crate::private::Sealed;
+use sink::Sink;
+
+/// Data element holding a PSM.
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct PsmDataElement {
+ /// The two bytes PSM value.
+ pub value: u16,
+}
+
+impl Sealed for crate::legacy::data_elements::psm::PsmDataElement {}
+
+impl<F: PacketFlavor> SerializeDataElement<F>
+ for crate::legacy::data_elements::psm::PsmDataElement
+{
+ fn de_type_code(&self) -> DeTypeCode {
+ crate::legacy::data_elements::psm::PsmDataElement::DE_TYPE_CODE
+ }
+
+ fn map_actual_len_to_encoded_len(&self, actual_len: DeActualLength) -> DeEncodedLength {
+ <Self as DeserializeDataElement>::LengthMapper::map_actual_len_to_encoded_len(actual_len)
+ }
+
+ fn serialize_contents(
+ &self,
+ sink: &mut DataElementSerializationBuffer,
+ ) -> Result<(), DataElementSerializeError> {
+ sink.try_extend_from_slice(self.value.to_be_bytes().as_slice())
+ .ok_or(DataElementSerializeError::InsufficientSpace)
+ }
+}
+
+impl DeserializeDataElement for crate::legacy::data_elements::psm::PsmDataElement {
+ const DE_TYPE_CODE: DeTypeCode = match DeTypeCode::try_from(0b0100) {
+ Ok(t) => t,
+ Err(_) => unreachable!(),
+ };
+
+ type LengthMapper = DirectMapper<PsmLengthPredicate>;
+
+ fn deserialize<F: PacketFlavor>(
+ de_contents: &[u8],
+ ) -> Result<Self, DataElementDeserializeError> {
+ de_contents
+ .try_into()
+ .ok()
+ .map(|arr: [u8; 2]| Self { value: u16::from_be_bytes(arr) })
+ .ok_or(DataElementDeserializeError::DeserializeError { de_type: Self::DE_TYPE_CODE })
+ }
+}
+
+pub(in crate::legacy) struct PsmLengthPredicate;
+
+impl DirectMapPredicate for PsmLengthPredicate {
+ /// PSM is 2 bytes value.
+ fn is_valid(len: usize) -> bool {
+ len == 2
+ }
+}
+
+#[allow(clippy::unwrap_used)]
+#[cfg(test)]
+mod tests {
+ use crate::legacy::data_elements::de_type::{DeActualLength, DeEncodedLength};
+ use crate::legacy::data_elements::psm::PsmDataElement;
+ use crate::legacy::data_elements::tests::macros::de_roundtrip_test;
+ use crate::legacy::data_elements::{DeserializeDataElement, LengthMapper};
+ use crate::legacy::serialize::tests::serialize;
+ use crate::legacy::{Ciphertext, Plaintext};
+ use crate::DeLengthOutOfRange;
+ use std::panic;
+
+ extern crate std;
+
+ #[test]
+ fn actual_length_must_be_2() {
+ for l in [0, 1, 3] {
+ let actual = DeActualLength::try_from(l).unwrap();
+ let _ = panic::catch_unwind(|| {
+ <PsmDataElement as DeserializeDataElement>::LengthMapper::map_actual_len_to_encoded_len(actual)
+ }).unwrap_err();
+ }
+
+ assert_eq!(
+ 2,
+ <PsmDataElement as DeserializeDataElement>::LengthMapper::map_actual_len_to_encoded_len(
+ DeActualLength::try_from(2).unwrap(),
+ )
+ .as_u8()
+ )
+ }
+
+ #[test]
+ fn encoded_length_must_be_2() {
+ for l in [0, 1, 3] {
+ assert_eq!(
+ DeLengthOutOfRange,
+ <PsmDataElement as DeserializeDataElement>::LengthMapper::map_encoded_len_to_actual_len(
+ DeEncodedLength::try_from(l).unwrap()
+ )
+ .unwrap_err()
+ )
+ }
+
+ assert_eq!(
+ 2,
+ <PsmDataElement as DeserializeDataElement>::LengthMapper::map_encoded_len_to_actual_len(
+ DeEncodedLength::from(2)
+ )
+ .unwrap()
+ .as_u8()
+ );
+ }
+
+ #[test]
+ fn dedup_hint_de_contents_roundtrip_unencrypted() {
+ let _ = de_roundtrip_test!(
+ PsmDataElement,
+ Psm,
+ Psm,
+ Plaintext,
+ serialize::<Plaintext, _>(&PsmDataElement { value: 0x10 })
+ );
+ }
+
+ #[test]
+ fn psm_de_contents_roundtrip_ldt() {
+ let _ = de_roundtrip_test!(
+ PsmDataElement,
+ Psm,
+ Psm,
+ Ciphertext,
+ serialize::<Ciphertext, _>(&PsmDataElement { value: 0x10 })
+ );
+ }
+
+ mod coverage_gaming {
+ use crate::legacy::data_elements::psm::PsmDataElement;
+ use alloc::format;
+
+ #[test]
+ fn psm_de() {
+ let de = PsmDataElement { value: 0x10 };
+ // debug
+ let _ = format!("{de:?}");
+ // trivial accessor
+ assert_eq!(0x10, de.value);
+ }
+ }
+}
diff --git a/nearby/presence/np_adv/src/legacy/data_elements/tests.rs b/nearby/presence/np_adv/src/legacy/data_elements/tests.rs
index a5e8fbf..ff3a891 100644
--- a/nearby/presence/np_adv/src/legacy/data_elements/tests.rs
+++ b/nearby/presence/np_adv/src/legacy/data_elements/tests.rs
@@ -99,6 +99,8 @@
match de {
DeserializedDataElement::Actions(_) => DataElementType::Actions,
DeserializedDataElement::TxPower(_) => DataElementType::TxPower,
+ DeserializedDataElement::Psm(_) => DataElementType::Psm,
+ DeserializedDataElement::DeviceInfo(_) => DataElementType::DeviceInfo,
}
);
de
diff --git a/nearby/presence/np_adv/src/legacy/data_elements/tx_power.rs b/nearby/presence/np_adv/src/legacy/data_elements/tx_power.rs
index e8c787e..b3ae27f 100644
--- a/nearby/presence/np_adv/src/legacy/data_elements/tx_power.rs
+++ b/nearby/presence/np_adv/src/legacy/data_elements/tx_power.rs
@@ -181,7 +181,7 @@
fn tx_power_de() {
let de = TxPowerDataElement::from(TxPower::try_from(3).unwrap());
// debug
- let _ = format!("{:?}", de);
+ let _ = format!("{de:?}");
// trivial accessor
assert_eq!(3, de.tx_power_value());
}
diff --git a/nearby/presence/np_adv/src/legacy/deserialize/mod.rs b/nearby/presence/np_adv/src/legacy/deserialize/mod.rs
index 075c0c9..0c61066 100644
--- a/nearby/presence/np_adv/src/legacy/deserialize/mod.rs
+++ b/nearby/presence/np_adv/src/legacy/deserialize/mod.rs
@@ -44,6 +44,8 @@
use crate::credential::matched::HasIdentityMatch;
use crate::legacy::data_elements::actions::ActionsDataElement;
use crate::legacy::data_elements::de_type::{DataElementType, DeActualLength};
+use crate::legacy::data_elements::device_info::DeviceInfoDataElement;
+use crate::legacy::data_elements::psm::PsmDataElement;
use crate::legacy::data_elements::DataElementDeserializeError::DuplicateDeTypes;
use crate::legacy::Plaintext;
/// exposed because the unencrypted case isn't just for intermediate: no further processing is needed
@@ -228,6 +230,8 @@
pub enum DeserializedDataElement<F: PacketFlavor> {
Actions(actions::ActionsDataElement<F>),
TxPower(TxPowerDataElement),
+ Psm(PsmDataElement),
+ DeviceInfo(DeviceInfoDataElement),
}
impl<F: PacketFlavor> Deserialized for DeserializedDataElement<F> {
@@ -241,6 +245,14 @@
DeTypeCode::try_from(TxPowerDataElement::DE_TYPE_CODE.as_u8())
.expect("TxPower type code is valid so this will always succeed")
}
+ DeserializedDataElement::Psm(_) => {
+ DeTypeCode::try_from(PsmDataElement::DE_TYPE_CODE.as_u8())
+ .expect("Psm type code is valid so this will always succeed")
+ }
+ DeserializedDataElement::DeviceInfo(_) => {
+ DeTypeCode::try_from(DeviceInfoDataElement::DE_TYPE_CODE.as_u8())
+ .expect("DeviceInfo type code is valid so this will always succeed")
+ }
}
}
}
@@ -252,6 +264,8 @@
match self {
DeserializedDataElement::Actions(_) => ActionsDataElement::<F>::DE_TYPE_CODE.as_u8(),
DeserializedDataElement::TxPower(_) => TxPowerDataElement::DE_TYPE_CODE.as_u8(),
+ DeserializedDataElement::Psm(_) => PsmDataElement::DE_TYPE_CODE.as_u8(),
+ DeserializedDataElement::DeviceInfo(_) => DeviceInfoDataElement::DE_TYPE_CODE.as_u8(),
}
}
@@ -270,6 +284,12 @@
DeserializedDataElement::TxPower(t) => {
SerializeDataElement::<F>::serialize_contents(t, &mut sink)
}
+ DeserializedDataElement::Psm(h) => {
+ SerializeDataElement::<F>::serialize_contents(h, &mut sink)
+ }
+ DeserializedDataElement::DeviceInfo(d) => {
+ SerializeDataElement::<F>::serialize_contents(d, &mut sink)
+ }
}
.unwrap();
sink.into_inner().into_inner().as_slice().to_vec()
@@ -392,6 +412,17 @@
.map_err(|e| e.into())
.map(|l| (DataElementType::Actions, l))
}
+ PsmDataElement::DE_TYPE_CODE => {
+ <PsmDataElement as DeserializeDataElement>::LengthMapper::map_encoded_len_to_actual_len(encoded_len)
+ .map_err(|e| e.into())
+ .map(|l| (DataElementType::Psm, l))
+
+ }
+ DeviceInfoDataElement::DE_TYPE_CODE => {
+ <DeviceInfoDataElement as DeserializeDataElement>::LengthMapper::map_encoded_len_to_actual_len(encoded_len)
+ .map_err(|e| e.into())
+ .map(|l| (DataElementType::DeviceInfo, l))
+ }
_ => Err(LengthError::InvalidType),
}
}
@@ -406,6 +437,11 @@
}
DataElementType::TxPower => TxPowerDataElement::deserialize::<F>(raw_de.contents)
.map(DeserializedDataElement::TxPower),
+ DataElementType::Psm => {
+ PsmDataElement::deserialize::<F>(raw_de.contents).map(DeserializedDataElement::Psm)
+ }
+ DataElementType::DeviceInfo => DeviceInfoDataElement::deserialize::<F>(raw_de.contents)
+ .map(DeserializedDataElement::DeviceInfo),
}
}
}
diff --git a/nearby/presence/np_adv/src/legacy/deserialize/tests/error_conditions.rs b/nearby/presence/np_adv/src/legacy/deserialize/tests/error_conditions.rs
index fe5ec21..fbc5511 100644
--- a/nearby/presence/np_adv/src/legacy/deserialize/tests/error_conditions.rs
+++ b/nearby/presence/np_adv/src/legacy/deserialize/tests/error_conditions.rs
@@ -15,13 +15,12 @@
mod unencrypted {
use crate::header::V0Encoding;
- use crate::legacy::data_elements::actions::{ActionBits, ActionsDataElement, ActiveUnlock};
+ use crate::legacy::data_elements::actions::ActionsDataElement;
use crate::legacy::data_elements::de_type::{DeEncodedLength, DeTypeCode};
use crate::legacy::data_elements::tx_power::TxPowerDataElement;
use crate::legacy::data_elements::{DataElementDeserializeError, DeserializeDataElement};
use crate::legacy::deserialize::intermediate::IntermediateAdvContents;
- use crate::legacy::serialize::tests::serialize;
- use crate::legacy::{Ciphertext, PacketFlavorEnum, Plaintext};
+ use crate::legacy::Plaintext;
#[test]
fn iterate_tx_power_invalid_de_len_error() {
@@ -59,19 +58,6 @@
}
#[test]
- fn iterate_actions_ciphertext_only_bit_error() {
- let mut bits = ActionBits::default();
- bits.set_action(ActiveUnlock::from(true));
- assert_deser_error(
- serialize(&ActionsDataElement::<Ciphertext>::from(bits)).as_slice(),
- DataElementDeserializeError::FlavorNotSupported {
- de_type: ActionsDataElement::<Plaintext>::DE_TYPE_CODE,
- flavor: PacketFlavorEnum::Plaintext,
- },
- );
- }
-
- #[test]
fn iterate_invalid_de_type_error() {
assert_deser_error(
&[0x0F],
@@ -123,17 +109,12 @@
// see unencrypted tests above for basic things that are the same between unencrypted and ldt,
// like how an invalid de type is handled
- use crate::credential::matched::HasIdentityMatch;
use crate::credential::v0::V0BroadcastCredential;
use crate::header::V0Encoding;
- use crate::legacy::data_elements::actions::tests::PlaintextOnly;
- use crate::legacy::data_elements::actions::{ActionBits, ActionsDataElement};
use crate::legacy::data_elements::tx_power::TxPowerDataElement;
- use crate::legacy::data_elements::{DataElementDeserializeError, DeserializeDataElement};
use crate::legacy::deserialize::intermediate::IntermediateAdvContents;
use crate::legacy::deserialize::DecryptError;
use crate::legacy::serialize::{AdvBuilder, LdtEncoder};
- use crate::legacy::{Ciphertext, PacketFlavorEnum};
use crate::shared_data::TxPower;
use alloc::vec::Vec;
use crypto_provider_default::CryptoProviderImpl;
@@ -143,45 +124,6 @@
};
#[test]
- fn iterate_actions_invalid_flavor_error() {
- let mut bits = ActionBits::default();
- bits.set_action(PlaintextOnly::from(true));
-
- let key_seed = [0; 32];
- let identity_token = V0IdentityToken::from([0x33; V0_IDENTITY_TOKEN_LEN]);
- let salt = V0Salt::from([0x01, 0x02]);
- let broadcast_cred = V0BroadcastCredential::new(key_seed, identity_token);
- let mut builder =
- AdvBuilder::new(LdtEncoder::<CryptoProviderImpl>::new(salt, &broadcast_cred));
-
- builder.add_data_element(ActionsDataElement::from(bits)).unwrap();
-
- let adv = builder.into_advertisement().unwrap();
-
- let mut contents =
- IntermediateAdvContents::deserialize(V0Encoding::Ldt, &adv.as_slice()[1..]).unwrap();
- let ldt = contents.as_ldt().unwrap();
- let hkdf = np_hkdf::NpKeySeedHkdf::<CryptoProviderImpl>::new(&key_seed);
- let identity_token_hmac: [u8; 32] = hkdf
- .v0_identity_token_hmac_key()
- .calculate_hmac::<CryptoProviderImpl>(identity_token.as_slice());
- let decrypter =
- ldt_np_adv::build_np_adv_decrypter_from_key_seed(&hkdf, identity_token_hmac);
- let decrypted = ldt.try_decrypt(&decrypter).unwrap();
-
- assert_eq!(salt, decrypted.salt());
- assert_eq!(identity_token, decrypted.identity_token());
-
- assert_eq!(
- DataElementDeserializeError::FlavorNotSupported {
- de_type: ActionsDataElement::<Ciphertext>::DE_TYPE_CODE,
- flavor: PacketFlavorEnum::Ciphertext,
- },
- decrypted.data_elements().next().unwrap().unwrap_err()
- )
- }
-
- #[test]
fn decrypter_wrong_identity_token_hmac_no_match() {
build_and_deser_with_invalid_decrypter_error(
|adv| adv,
diff --git a/nearby/presence/np_adv/src/legacy/deserialize/tests/happy_path.rs b/nearby/presence/np_adv/src/legacy/deserialize/tests/happy_path.rs
index 8b0f979..d857993 100644
--- a/nearby/presence/np_adv/src/legacy/deserialize/tests/happy_path.rs
+++ b/nearby/presence/np_adv/src/legacy/deserialize/tests/happy_path.rs
@@ -23,7 +23,7 @@
header::V0Encoding,
legacy::{
data_elements::{
- actions::{ActionBits, ActionsDataElement, NearbyShare},
+ actions::{tests::NEARBY_SHARE, ActionBits, ActionsDataElement},
de_type::DataElementType,
tests::test_des::{
random_test_de, TestDataElement, TestDataElementType, TestDeDeserializer,
@@ -37,8 +37,10 @@
},
random_data_elements::random_de_plaintext,
serialize::{
- tests::helpers::{LongDataElement, ShortDataElement},
- tests::supports_flavor,
+ tests::{
+ helpers::{LongDataElement, ShortDataElement},
+ supports_flavor,
+ },
AddDataElementError, AdvBuilder, SerializedAdv, UnencryptedEncoder,
},
PacketFlavorEnum, Plaintext, BLE_4_ADV_SVC_MAX_CONTENT_LEN, NP_MAX_DE_CONTENT_LEN,
@@ -120,7 +122,7 @@
fn typical_tx_power_and_actions() {
let tx = TxPowerDataElement::from(TxPower::try_from(7).unwrap());
let mut action_bits = ActionBits::default();
- action_bits.set_action(NearbyShare::from(true));
+ action_bits.set_action(NEARBY_SHARE);
let actions = ActionsDataElement::from(action_bits);
let tx_boxed: Box<dyn SerializeDataElement<Plaintext>> = Box::new(tx.clone());
@@ -154,6 +156,8 @@
|builder, de| match de {
DeserializedDataElement::Actions(a) => builder.add_data_element(a),
DeserializedDataElement::TxPower(tx) => builder.add_data_element(tx),
+ DeserializedDataElement::Psm(d) => builder.add_data_element(d),
+ DeserializedDataElement::DeviceInfo(d) => builder.add_data_element(d),
},
)
}
@@ -253,14 +257,14 @@
mod ldt {
use crate::credential::matched::HasIdentityMatch;
- use crate::legacy::data_elements::actions::CallTransfer;
+ use crate::legacy::data_elements::actions::tests::{CALL_TRANSFER, NEARBY_SHARE};
use crate::legacy::data_elements::de_type::MAX_DE_ENCODED_LEN;
use crate::{
credential::v0::V0BroadcastCredential,
header::V0Encoding,
legacy::{
data_elements::{
- actions::{ActionBits, ActionsDataElement, NearbyShare},
+ actions::{ActionBits, ActionsDataElement},
de_type::DataElementType,
tests::test_des::TestDataElementType,
tests::test_des::{TestDataElement, TestDeDeserializer},
@@ -344,8 +348,8 @@
fn typical_tx_power_and_actions() {
let tx = TxPowerDataElement::from(TxPower::try_from(7).unwrap());
let mut action_bits = ActionBits::default();
- action_bits.set_action(NearbyShare::from(true));
- action_bits.set_action(CallTransfer::from(true));
+ action_bits.set_action(NEARBY_SHARE);
+ action_bits.set_action(CALL_TRANSFER);
let actions = ActionsDataElement::from(action_bits);
let tx_boxed: Box<dyn SerializeDataElement<Ciphertext>> = Box::new(tx.clone());
let actions_boxed: Box<dyn SerializeDataElement<Ciphertext>> = Box::new(actions.clone());
@@ -371,10 +375,14 @@
|builder, de| match de {
DeserializedDataElement::Actions(a) => builder.add_data_element(a),
DeserializedDataElement::TxPower(tx) => builder.add_data_element(tx),
+ DeserializedDataElement::Psm(d) => builder.add_data_element(d),
+ DeserializedDataElement::DeviceInfo(d) => builder.add_data_element(d),
},
|de| match de {
DeserializedDataElement::Actions(a) => serialized_len(a),
DeserializedDataElement::TxPower(tx) => serialized_len(tx),
+ DeserializedDataElement::Psm(d) => serialized_len(d),
+ DeserializedDataElement::DeviceInfo(d) => serialized_len(d),
},
)
}
@@ -530,7 +538,7 @@
#[test]
fn iac_debug_eq_test_helpers() {
let iac = IntermediateAdvContents::deserialize(V0Encoding::Unencrypted, &[0xFF]).unwrap();
- let _ = format!("{:?}", iac);
+ let _ = format!("{iac:?}");
assert_eq!(iac, iac);
}
@@ -562,7 +570,7 @@
contents: &[],
};
- let _ = format!("{:?}", rde);
+ let _ = format!("{rde:?}");
assert_eq!(rde, rde);
}
@@ -576,6 +584,6 @@
#[test]
fn ldt_adv_contents_debug() {
let lac = LdtAdvContents::new([0; 2].into(), &[0; 16]).unwrap();
- let _ = format!("{:?}", lac);
+ let _ = format!("{lac:?}");
}
}
diff --git a/nearby/presence/np_adv/src/legacy/deserialize/tests/mod.rs b/nearby/presence/np_adv/src/legacy/deserialize/tests/mod.rs
index af85f9a..a9f2978 100644
--- a/nearby/presence/np_adv/src/legacy/deserialize/tests/mod.rs
+++ b/nearby/presence/np_adv/src/legacy/deserialize/tests/mod.rs
@@ -180,7 +180,7 @@
[0; V0_SALT_LEN].into(),
ArrayView::try_from_slice(&[]).unwrap(),
);
- let _ = format!("{:?}", dac);
+ let _ = format!("{dac:?}");
assert_eq!(dac, dac)
}
diff --git a/nearby/presence/np_adv/src/legacy/mod.rs b/nearby/presence/np_adv/src/legacy/mod.rs
index 0ea4675..4dd9078 100644
--- a/nearby/presence/np_adv/src/legacy/mod.rs
+++ b/nearby/presence/np_adv/src/legacy/mod.rs
@@ -137,7 +137,7 @@
#[test]
fn plaintext_flavor() {
// debug
- let _ = format!("{:?}", Plaintext);
+ let _ = format!("{Plaintext:?}");
// eq and clone
assert_eq!(Plaintext, Plaintext.clone())
}
@@ -145,7 +145,7 @@
#[test]
fn ciphertext_flavor() {
// debug
- let _ = format!("{:?}", Ciphertext);
+ let _ = format!("{Ciphertext:?}");
// eq and clone
assert_eq!(Ciphertext, Ciphertext.clone())
}
diff --git a/nearby/presence/np_adv/src/legacy/random_data_elements.rs b/nearby/presence/np_adv/src/legacy/random_data_elements.rs
index e3fbde1..d9eb45b 100644
--- a/nearby/presence/np_adv/src/legacy/random_data_elements.rs
+++ b/nearby/presence/np_adv/src/legacy/random_data_elements.rs
@@ -16,26 +16,26 @@
extern crate std;
-use crate::legacy::data_elements::actions::tests::{
- set_ciphertexttext_action, set_plaintext_action,
-};
+use crate::legacy::data_elements::actions::tests::set_action;
+use crate::legacy::data_elements::device_info::DeviceInfoDataElement;
+use crate::legacy::data_elements::psm::PsmDataElement;
use crate::{
legacy::{
data_elements::{actions::*, de_type::DataElementType, tx_power::TxPowerDataElement, *},
deserialize::DeserializedDataElement,
- Ciphertext, PacketFlavor, PacketFlavorEnum, Plaintext,
+ Ciphertext, PacketFlavor, Plaintext,
},
- shared_data::TxPower,
+ shared_data::{DeviceInfo, DeviceType, TxPower},
};
+use alloc::vec;
+use rand::prelude::IteratorRandom;
use rand_ext::rand::{distributions, prelude::SliceRandom as _};
use std::prelude::rust_2021::*;
use strum::IntoEnumIterator;
impl distributions::Distribution<ActionsDataElement<Plaintext>> for distributions::Standard {
fn sample<R: rand::Rng + ?Sized>(&self, rng: &mut R) -> ActionsDataElement<Plaintext> {
- let mut available_actions = ActionType::iter()
- .filter(|at| at.supports_flavor(PacketFlavorEnum::Plaintext))
- .collect::<Vec<_>>();
+ let mut available_actions = actions::tests::supported_action_types();
available_actions.shuffle(rng);
// choose some of the available actions.
@@ -48,7 +48,7 @@
// generating boolean actions with `true` since we already did our random selection
// of which actions to use above
- set_plaintext_action(*a, true, &mut bits);
+ set_action(*a, &mut bits);
}
ActionsDataElement::from(bits)
@@ -57,9 +57,7 @@
impl distributions::Distribution<ActionsDataElement<Ciphertext>> for distributions::Standard {
fn sample<R: rand::Rng + ?Sized>(&self, rng: &mut R) -> ActionsDataElement<Ciphertext> {
- let mut available_actions = ActionType::iter()
- .filter(|at| at.supports_flavor(PacketFlavorEnum::Ciphertext))
- .collect::<Vec<_>>();
+ let mut available_actions = actions::tests::supported_action_types();
available_actions.shuffle(rng);
// choose some of the available actions
@@ -71,7 +69,7 @@
for a in selected_actions {
// generating boolean actions with `true` since we already did our random selection
// of which actions to use above
- set_ciphertexttext_action(*a, true, &mut bits);
+ set_action(*a, &mut bits);
}
ActionsDataElement::from(bits)
@@ -92,6 +90,25 @@
}
}
+impl distributions::Distribution<PsmDataElement> for distributions::Standard {
+ fn sample<R: rand::Rng + ?Sized>(&self, rng: &mut R) -> PsmDataElement {
+ let psm: u16 = self.sample(rng);
+ PsmDataElement { value: psm }
+ }
+}
+
+impl distributions::Distribution<DeviceInfoDataElement> for distributions::Standard {
+ fn sample<R: rand::Rng + ?Sized>(&self, rng: &mut R) -> DeviceInfoDataElement {
+ let device_type = DeviceType::iter().choose(rng).unwrap();
+ let name_len = rng.gen_range(5..=9);
+ let mut device_name = vec![0; name_len];
+ rng.fill(&mut device_name[..]);
+ let device_info =
+ DeviceInfo::try_from((device_type, false, device_name.as_slice())).unwrap();
+ DeviceInfoDataElement { device_info }
+ }
+}
+
/// Generate a random instance of the requested DE and return it wrapped in [DeserializedDataElement].
pub(crate) fn rand_de<F, D, E, R>(to_enum: E, rng: &mut R) -> DeserializedDataElement<F>
where
@@ -117,6 +134,8 @@
match de_type {
DataElementType::TxPower => rand_de(DeserializedDataElement::TxPower, rng),
DataElementType::Actions => rand_de(DeserializedDataElement::Actions, rng),
+ DataElementType::Psm => rand_de(DeserializedDataElement::Psm, rng),
+ DataElementType::DeviceInfo => rand_de(DeserializedDataElement::DeviceInfo, rng),
}
}
@@ -132,5 +151,7 @@
match de_type {
DataElementType::TxPower => rand_de(DeserializedDataElement::TxPower, rng),
DataElementType::Actions => rand_de(DeserializedDataElement::Actions, rng),
+ DataElementType::Psm => rand_de(DeserializedDataElement::Psm, rng),
+ DataElementType::DeviceInfo => rand_de(DeserializedDataElement::DeviceInfo, rng),
}
}
diff --git a/nearby/presence/np_adv/src/legacy/serialize/tests/happy_path.rs b/nearby/presence/np_adv/src/legacy/serialize/tests/happy_path.rs
index 05fc20a..8988678 100644
--- a/nearby/presence/np_adv/src/legacy/serialize/tests/happy_path.rs
+++ b/nearby/presence/np_adv/src/legacy/serialize/tests/happy_path.rs
@@ -16,7 +16,8 @@
use alloc::vec;
use crate::header::VERSION_HEADER_V0_UNENCRYPTED;
- use crate::legacy::data_elements::actions::{ActionBits, ActionsDataElement, NearbyShare};
+ use crate::legacy::data_elements::actions::tests::NEARBY_SHARE;
+ use crate::legacy::data_elements::actions::{ActionBits, ActionsDataElement};
use crate::legacy::data_elements::tx_power::TxPowerDataElement;
use crate::legacy::serialize::tests::helpers::{LongDataElement, ShortDataElement};
use crate::legacy::serialize::{AdvBuilder, UnencryptedEncoder};
@@ -80,7 +81,7 @@
builder.add_data_element(TxPowerDataElement::from(TxPower::try_from(3).unwrap())).unwrap();
let mut action = ActionBits::default();
- action.set_action(NearbyShare::from(true));
+ action.set_action(NEARBY_SHARE);
builder.add_data_element(ActionsDataElement::from(action)).unwrap();
let packet = builder.into_advertisement().unwrap();
@@ -106,8 +107,8 @@
use crate::credential::v0::V0BroadcastCredential;
use crate::header::VERSION_HEADER_V0_LDT;
- use crate::legacy::data_elements::actions::tests::LastBit;
- use crate::legacy::data_elements::actions::{ActionBits, ActionsDataElement, PhoneHub};
+ use crate::legacy::data_elements::actions::tests::{LastBit, PHONE_HUB};
+ use crate::legacy::data_elements::actions::{ActionBits, ActionsDataElement};
use crate::legacy::data_elements::tx_power::TxPowerDataElement;
use crate::legacy::serialize::tests::helpers::ShortDataElement;
use crate::legacy::serialize::{AdvBuilder, LdtEncoder, SerializedAdv};
@@ -162,7 +163,7 @@
.add_data_element(TxPowerDataElement::from(TxPower::try_from(3).unwrap()))
.unwrap();
let mut action = ActionBits::default();
- action.set_action(PhoneHub::from(true));
+ action.set_action(PHONE_HUB);
builder.add_data_element(ActionsDataElement::from(action)).unwrap();
});
}
diff --git a/nearby/presence/np_adv/src/legacy/serialize/tests/mod.rs b/nearby/presence/np_adv/src/legacy/serialize/tests/mod.rs
index caaa7d8..ac481ea 100644
--- a/nearby/presence/np_adv/src/legacy/serialize/tests/mod.rs
+++ b/nearby/presence/np_adv/src/legacy/serialize/tests/mod.rs
@@ -40,6 +40,14 @@
PacketFlavorEnum::Plaintext => true,
PacketFlavorEnum::Ciphertext => true,
},
+ DataElementType::Psm => match flavor {
+ PacketFlavorEnum::Plaintext => true,
+ PacketFlavorEnum::Ciphertext => true,
+ },
+ DataElementType::DeviceInfo => match flavor {
+ PacketFlavorEnum::Plaintext => true,
+ PacketFlavorEnum::Ciphertext => true,
+ },
}
}
@@ -55,7 +63,7 @@
#[test]
fn unencrypted_encoder() {
- let _ = format!("{:?}", UnencryptedEncoder);
+ let _ = format!("{UnencryptedEncoder:?}");
}
#[test]
@@ -72,7 +80,7 @@
let ldt_encoder = LdtEncoder::<CryptoProviderImpl>::new(salt, &broadcast_cred);
// doesn't leak crypto material
- assert_eq!("LdtEncoder { salt: V0Salt { bytes: [1, 2] } }", format!("{:?}", ldt_encoder));
+ assert_eq!("LdtEncoder { salt: V0Salt { bytes: [1, 2] } }", format!("{ldt_encoder:?}"));
}
#[test]
diff --git a/nearby/presence/np_adv/src/shared_data.rs b/nearby/presence/np_adv/src/shared_data.rs
index c7d241b..40d5c72 100644
--- a/nearby/presence/np_adv/src/shared_data.rs
+++ b/nearby/presence/np_adv/src/shared_data.rs
@@ -14,6 +14,10 @@
//! Data types shared between V0 and V1 advertisements
+use sink::Sink;
+use strum_macros::FromRepr;
+use tinyvec::ArrayVec;
+
/// Power in dBm, calibrated as per
/// [Eddystone](https://github.com/google/eddystone/tree/master/eddystone-uid#tx-power)
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
@@ -76,6 +80,101 @@
#[derive(Debug, PartialEq, Eq)]
pub struct ContextSyncSeqNumOutOfRange;
+/// A description of what kind of device is broadcasting.
+#[repr(u8)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq, FromRepr, strum_macros::EnumIter)]
+pub enum DeviceType {
+ /// The type of device doing the broadcasting is completely unknown.
+ Unknown = 0,
+ /// The broadcasting device is a mobile phone.
+ Phone = 1,
+ /// The broadcasting device is a tablet.
+ Tablet = 2,
+ /// The broadcasting device is a (non-TV) display.
+ Display = 3,
+ /// The broadcasting device is a (non-CrOS) laptop.
+ Laptop = 4,
+ /// The broadcasting device is a TV.
+ TV = 5,
+ /// The broadcasting device is a watch.
+ Watch = 6,
+ /// The broadcasting device is a Chromebook.
+ ChromeOS = 7,
+ /// The broadcasting device is some kind of foldable.
+ Foldable = 8,
+ /// The broadcasting device is a car.
+ Automotive = 9,
+ /// The broadscasting device is a speaker.
+ Speaker = 10,
+}
+
+impl TryFrom<u8> for DeviceType {
+ type Error = ();
+
+ // We implement try_from to maintain the standard Rust idiom for fallible conversions.
+ // This is the expected API for any crate consuming DeviceType directly.
+ fn try_from(value: u8) -> Result<Self, Self::Error> {
+ DeviceType::from_repr(value).ok_or(())
+ }
+}
+
+/// The minimum length of a device name in a `DeviceInfo` data element.
+pub const MIN_DEVICE_NAME_LEN: usize = 5;
+/// The maximum length of a device name in a `DeviceInfo` data element.
+pub const MAX_DEVICE_NAME_LEN: usize = 9;
+
+/// Device information including a device type and a device name.
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct DeviceInfo {
+ device_type: DeviceType,
+ name_truncated: bool,
+ device_name: ArrayVec<[u8; MAX_DEVICE_NAME_LEN]>,
+}
+
+impl DeviceInfo {
+ /// The device type.
+ pub fn device_type(&self) -> DeviceType {
+ self.device_type
+ }
+
+ /// Whether or not the device name was truncated.
+ pub fn name_truncated(&self) -> bool {
+ self.name_truncated
+ }
+
+ /// The device name.
+ pub fn device_name(&self) -> &[u8] {
+ &self.device_name
+ }
+}
+
+/// Errors that can occur when creating a `DeviceInfo`.
+#[derive(Debug, PartialEq, Eq)]
+pub enum DeviceInfoMalformed {
+ /// The device name is too short.
+ NameTooShort,
+ /// The device name is too long.
+ NameTooLong,
+}
+
+impl<'a> TryFrom<(DeviceType, bool, &'a [u8])> for DeviceInfo {
+ type Error = DeviceInfoMalformed;
+
+ fn try_from(
+ (device_type, name_truncated, device_name): (DeviceType, bool, &'a [u8]),
+ ) -> Result<Self, Self::Error> {
+ if device_name.len() < MIN_DEVICE_NAME_LEN {
+ Err(DeviceInfoMalformed::NameTooShort)
+ } else if device_name.len() > MAX_DEVICE_NAME_LEN {
+ Err(DeviceInfoMalformed::NameTooLong)
+ } else {
+ let mut array_vec = ArrayVec::new();
+ array_vec.try_extend_from_slice(device_name).ok_or(DeviceInfoMalformed::NameTooLong)?;
+ Ok(Self { device_type, name_truncated, device_name: array_vec })
+ }
+ }
+}
+
#[allow(clippy::unwrap_used)]
#[cfg(test)]
mod tests {
@@ -102,4 +201,29 @@
fn context_sync_seq_num_ok() {
assert_eq!(0x0F, ContextSyncSeqNum::try_from(0x0F).unwrap().num);
}
+
+ #[test]
+ fn device_info_name_too_short() {
+ assert_eq!(
+ DeviceInfoMalformed::NameTooShort,
+ DeviceInfo::try_from((DeviceType::Phone, false, "abcd".as_bytes())).unwrap_err()
+ );
+ }
+
+ #[test]
+ fn device_info_name_too_long() {
+ assert_eq!(
+ DeviceInfoMalformed::NameTooLong,
+ DeviceInfo::try_from((DeviceType::Phone, false, "0123456789".as_bytes())).unwrap_err()
+ );
+ }
+
+ #[test]
+ fn device_info_ok() {
+ let device_info =
+ DeviceInfo::try_from((DeviceType::Phone, true, "abcde".as_bytes())).unwrap();
+ assert_eq!(device_info.device_type(), DeviceType::Phone);
+ assert!(device_info.name_truncated());
+ assert_eq!(device_info.device_name(), "abcde".as_bytes());
+ }
}
diff --git a/nearby/presence/np_adv_dynamic/src/extended.rs b/nearby/presence/np_adv_dynamic/src/extended.rs
index 59f6a42..c71d0a2 100644
--- a/nearby/presence/np_adv_dynamic/src/extended.rs
+++ b/nearby/presence/np_adv_dynamic/src/extended.rs
@@ -43,7 +43,7 @@
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
BoxedAddSectionError::Underlying(u) => {
- write!(f, "{0}", u)
+ write!(f, "{u}")
}
}
}
diff --git a/nearby/presence/np_adv_dynamic/src/legacy.rs b/nearby/presence/np_adv_dynamic/src/legacy.rs
index 68eb1bc..c35318b 100644
--- a/nearby/presence/np_adv_dynamic/src/legacy.rs
+++ b/nearby/presence/np_adv_dynamic/src/legacy.rs
@@ -15,7 +15,9 @@
use crypto_provider::CryptoProvider;
use np_adv::{
legacy::{
- data_elements::{actions::*, tx_power::TxPowerDataElement, *},
+ data_elements::{
+ actions::*, device_info::DeviceInfoDataElement, tx_power::TxPowerDataElement, *,
+ },
serialize::*,
*,
},
@@ -128,7 +130,7 @@
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
BoxedAddDataElementError::UnderlyingError(u) => {
- write!(f, "{0:?}", u)
+ write!(f, "{u:?}")
}
BoxedAddDataElementError::FlavorMismatchError => {
write!(f, "Expected packet flavoring for added DEs does not match the actual packet flavor of the DE")
@@ -232,6 +234,12 @@
}
}
+impl From<DeviceInfo> for ToBoxedSerializeDataElement {
+ fn from(data: DeviceInfo) -> Self {
+ ToBoxedSerializeDataElement::Both(Box::new(DeviceInfoDataElement::from(data)))
+ }
+}
+
impl From<BoxedActionBits> for ToBoxedSerializeDataElement {
fn from(action_bits: BoxedActionBits) -> Self {
match action_bits {
@@ -247,19 +255,21 @@
/// Boxed version of `ToActionElement` which allows abstracting over
/// what packet flavors are supported by a given action.
-pub enum ToBoxedActionElement {
- /// Action bit for cross device SDK.
- CrossDevSdk(bool),
- /// Action bit for call transfer.
- CallTransfer(bool),
- /// Action bit for active unlock.
- ActiveUnlock(bool),
- /// Action bit for nearby share.
- NearbyShare(bool),
- /// Action bit for instant tethering.
- InstantTethering(bool),
- /// Action bit for PhoneHub.
- PhoneHub(bool),
+pub struct ToBoxedActionElement {
+ /// u8 representation of the action element
+ pub action: u8,
+ /// bool value of whether element is active
+ pub value: bool,
+}
+
+impl ActionElement for ToBoxedActionElement {
+ fn high_bit_index(&self) -> u32 {
+ self.action as u32
+ }
+
+ fn bits(&self) -> u8 {
+ self.value as u8
+ }
}
/// [`ActionBits`] with runtime-determined packet flavoring
@@ -308,7 +318,7 @@
}
}
- fn set<F: PacketFlavor, E: ActionElementFlavor<F>>(
+ fn set<F: PacketFlavor, E: ActionElement>(
action_bits: &mut ActionBits<F>,
to_element: E,
) -> Result<(), BoxedSetActionFlavorError> {
@@ -324,36 +334,8 @@
to_element: ToBoxedActionElement,
) -> Result<(), BoxedSetActionFlavorError> {
match self {
- BoxedActionBits::Plaintext(action_bits) => match to_element {
- ToBoxedActionElement::CrossDevSdk(b) => {
- Self::set(action_bits, CrossDevSdk::from(b))
- }
- ToBoxedActionElement::NearbyShare(b) => {
- Self::set(action_bits, NearbyShare::from(b))
- }
- ToBoxedActionElement::CallTransfer(_)
- | ToBoxedActionElement::ActiveUnlock(_)
- | ToBoxedActionElement::InstantTethering(_)
- | ToBoxedActionElement::PhoneHub(_) => Err(BoxedSetActionFlavorError),
- },
- BoxedActionBits::Ciphertext(action_bits) => match to_element {
- ToBoxedActionElement::CrossDevSdk(b) => {
- Self::set(action_bits, CrossDevSdk::from(b))
- }
- ToBoxedActionElement::CallTransfer(b) => {
- Self::set(action_bits, CallTransfer::from(b))
- }
- ToBoxedActionElement::ActiveUnlock(b) => {
- Self::set(action_bits, ActiveUnlock::from(b))
- }
- ToBoxedActionElement::NearbyShare(b) => {
- Self::set(action_bits, NearbyShare::from(b))
- }
- ToBoxedActionElement::InstantTethering(b) => {
- Self::set(action_bits, InstantTethering::from(b))
- }
- ToBoxedActionElement::PhoneHub(b) => Self::set(action_bits, PhoneHub::from(b)),
- },
+ BoxedActionBits::Plaintext(action_bits) => Self::set(action_bits, to_element),
+ BoxedActionBits::Ciphertext(action_bits) => Self::set(action_bits, to_element),
}
}
}
diff --git a/nearby/presence/np_c_ffi/include/c/np_c_ffi.h b/nearby/presence/np_c_ffi/include/c/np_c_ffi.h
index e932671..82d37d3 100644
--- a/nearby/presence/np_c_ffi/include/c/np_c_ffi.h
+++ b/nearby/presence/np_c_ffi/include/c/np_c_ffi.h
@@ -35,19 +35,6 @@
#include <stdlib.h>
/**
- * The possible boolean action types which can be present in an Actions data element
- */
-enum np_ffi_ActionType {
- NP_FFI_ACTION_TYPE_CROSS_DEV_SDK = 1,
- NP_FFI_ACTION_TYPE_CALL_TRANSFER = 4,
- NP_FFI_ACTION_TYPE_ACTIVE_UNLOCK = 8,
- NP_FFI_ACTION_TYPE_NEARBY_SHARE = 9,
- NP_FFI_ACTION_TYPE_INSTANT_TETHERING = 10,
- NP_FFI_ACTION_TYPE_PHONE_HUB = 11,
-};
-typedef uint8_t np_ffi_ActionType;
-
-/**
* Result type for trying to add a V0 credential to a credential-slab.
*/
enum np_ffi_AddV0CredentialToSlabResult {
@@ -178,6 +165,39 @@
typedef uint8_t np_ffi_AdvertisementBuilderKind;
/**
+ * Discriminant for `BuildActionTypeResult`.
+ */
+enum np_ffi_BuildActionTypeResultKind {
+ /**
+ * The action type was outside the
+ * allowed 0 to 31 range.
+ */
+ NP_FFI_BUILD_ACTION_TYPE_RESULT_KIND_OUT_OF_RANGE = 0,
+ /**
+ * The transmission power was in range,
+ * and so a `TxPower` struct was constructed.
+ */
+ NP_FFI_BUILD_ACTION_TYPE_RESULT_KIND_SUCCESS = 1,
+};
+typedef uint8_t np_ffi_BuildActionTypeResultKind;
+
+/**
+ * Discriminant for `BuildPsmResult`.
+ */
+enum np_ffi_BuildPsmResultKind {
+ /**
+ * The PSM should be within two bytes.
+ */
+ NP_FFI_BUILD_PSM_RESULT_KIND_OUT_OF_RANGE = 0,
+ /**
+ * The PSM was in range,
+ * and so a `Psm` struct was constructed.
+ */
+ NP_FFI_BUILD_PSM_RESULT_KIND_SUCCESS = 1,
+};
+typedef uint8_t np_ffi_BuildPsmResultKind;
+
+/**
* Discriminant for `BuildTxPowerResult`.
*/
enum np_ffi_BuildTxPowerResultKind {
@@ -592,6 +612,18 @@
* `V0DataElement#into_actions`.
*/
NP_FFI_V0_DATA_ELEMENT_KIND_ACTIONS = 2,
+ /**
+ * The PSM data-element.
+ * The associated payload may be obtained via
+ * `V0DataElement#into_psm`.
+ */
+ NP_FFI_V0_DATA_ELEMENT_KIND_PSM = 3,
+ /**
+ * The DeviceInfo data-element.
+ * The associated payload may be obtained via
+ * 'V0DataElement#into_device_info`.
+ */
+ NP_FFI_V0_DATA_ELEMENT_KIND_DEVICE_INFO = 4,
};
typedef uint8_t np_ffi_V0DataElementKind;
@@ -879,7 +911,13 @@
* permitted to be between 0 and 255.
*/
typedef struct {
+ /**
+ * The number of bytes in the buffer.
+ */
uint8_t len;
+ /**
+ * The bytes in the buffer.
+ */
uint8_t bytes[255];
} np_ffi_ByteBuffer_255;
@@ -933,11 +971,89 @@
} np_ffi_V0Actions;
/**
+ * Representation of a PSM for L2CAP,
+ */
+typedef struct {
+ uint16_t value;
+} np_ffi_Psm;
+
+/**
+ * A byte-string with a maximum size of N,
+ * where only the first `len` bytes are considered
+ * to contain the actual payload. N is only
+ * permitted to be between 0 and 255.
+ */
+typedef struct {
+ /**
+ * The number of bytes in the buffer.
+ */
+ uint8_t len;
+ /**
+ * The bytes in the buffer.
+ */
+ uint8_t bytes[9];
+} np_ffi_ByteBuffer_9;
+
+/**
+ * A FFI safe wrapper of an optional byte buffer.
+ */
+typedef enum {
+ /**
+ * The optional byte buffer isn't specified.
+ */
+ NP_FFI_OPTIONAL_BYTE_BUFFER_9_NOT_SPECIFIED_9,
+ /**
+ * The optional byte buffer is present.
+ */
+ NP_FFI_OPTIONAL_BYTE_BUFFER_9_PRESENT_9,
+} np_ffi_OptionalByteBuffer_9_Tag;
+
+typedef struct {
+ np_ffi_OptionalByteBuffer_9_Tag tag;
+ union {
+ struct {
+ np_ffi_ByteBuffer_9 present;
+ };
+ };
+} np_ffi_OptionalByteBuffer_9;
+
+/**
+ * Represents the contents of a Device Info DE containing the type and name of a device, as well as flag
+ * indicating whether the device name is truncated.
+ *
+ * This representation is stable, and so you may directly
+ * reference this struct's fields if you wish.
+ */
+typedef struct {
+ /**
+ * The numerical value corresponding to a given
+ * form factor, or "0" for the case where the
+ * broadcasting device doesn't know what kind
+ * of device type it is.
+ *
+ * Consult the Nearby spec for values
+ * corresponding to particular form factors.
+ */
+ uint8_t device_type;
+ /**
+ * Boolean flag indicating whether the device name is truncated.
+ */
+ bool is_name_truncated;
+ /**
+ * An optional byte array representing the name of the broadcasting
+ * device. The maximum size of this device name array is 9 bytes.
+ */
+ np_ffi_OptionalByteBuffer_9 device_name;
+} np_ffi_DeviceInfo;
+
+/**
* Representation of a V0 data element.
*/
typedef enum {
NP_FFI_V0_DATA_ELEMENT_TX_POWER,
NP_FFI_V0_DATA_ELEMENT_ACTIONS,
+ NP_FFI_V0_DATA_ELEMENT_PSM,
+ NP_FFI_V0_DATA_ELEMENT_DEVICE_INFO,
} np_ffi_V0DataElement_Tag;
typedef struct {
@@ -949,6 +1065,12 @@
struct {
np_ffi_V0Actions actions;
};
+ struct {
+ np_ffi_Psm psm;
+ };
+ struct {
+ np_ffi_DeviceInfo device_info;
+ };
};
} np_ffi_V0DataElement;
@@ -1060,7 +1182,13 @@
* permitted to be between 0 and 255.
*/
typedef struct {
+ /**
+ * The number of bytes in the buffer.
+ */
uint8_t len;
+ /**
+ * The bytes in the buffer.
+ */
uint8_t bytes[127];
} np_ffi_ByteBuffer_127;
@@ -1184,7 +1312,13 @@
* permitted to be between 0 and 255.
*/
typedef struct {
+ /**
+ * The number of bytes in the buffer.
+ */
uint8_t len;
+ /**
+ * The bytes in the buffer.
+ */
uint8_t bytes[24];
} np_ffi_ByteBuffer_24;
@@ -1285,7 +1419,13 @@
* permitted to be between 0 and 255.
*/
typedef struct {
+ /**
+ * The number of bytes in the buffer.
+ */
uint8_t len;
+ /**
+ * The bytes in the buffer.
+ */
uint8_t bytes[250];
} np_ffi_ByteBuffer_250;
@@ -1367,6 +1507,49 @@
} np_ffi_SetV0ActionResult;
/**
+ * Action data element
+ */
+typedef struct {
+ uint8_t _0;
+} np_ffi_ActionType;
+
+/**
+ * Result type for attempting to construct a
+ * PSM from two bytes.
+ */
+typedef enum {
+ NP_FFI_BUILD_PSM_RESULT_OUT_OF_RANGE,
+ NP_FFI_BUILD_PSM_RESULT_SUCCESS,
+} np_ffi_BuildPsmResult_Tag;
+
+typedef struct {
+ np_ffi_BuildPsmResult_Tag tag;
+ union {
+ struct {
+ np_ffi_Psm success;
+ };
+ };
+} np_ffi_BuildPsmResult;
+
+/**
+ * Result type for attempting to construct a
+ * ActionType from an usigned byte.
+ */
+typedef enum {
+ NP_FFI_BUILD_ACTION_TYPE_RESULT_OUT_OF_RANGE,
+ NP_FFI_BUILD_ACTION_TYPE_RESULT_SUCCESS,
+} np_ffi_BuildActionTypeResult_Tag;
+
+typedef struct {
+ np_ffi_BuildActionTypeResult_Tag tag;
+ union {
+ struct {
+ np_ffi_ActionType success;
+ };
+ };
+} np_ffi_BuildActionTypeResult;
+
+/**
* Overrides the global panic handler to be used when NP C FFI calls panic.
* This method will only have an effect on the global panic-handler
* the first time it's called, and this method will return `true`
@@ -1971,6 +2154,72 @@
uint32_t np_ffi_V0Actions_as_u32(np_ffi_V0Actions actions);
/**
+ * Casts a `V0DataElement` to the `Psm` variant, panicking in the
+ * case where the passed value is of a different enum variant.
+ */
+np_ffi_Psm np_ffi_V0DataElement_into_PSM(np_ffi_V0DataElement de);
+
+/**
+ * Upcasts a Psm DE to a generic V0 data-element.
+ */
+np_ffi_V0DataElement np_ffi_Psm_into_V0DataElement(np_ffi_Psm psm);
+
+/**
+ * Casts a `V0DataElement` to the `DeviceInfo` variant, panicking in the
+ * case where the passed value is of a different enum variant.
+ */
+np_ffi_DeviceInfo np_ffi_V0DataElement_into_DeviceInfo(np_ffi_V0DataElement de);
+
+/**
+ * Upcasts a DeviceInfo DE to a generic V0 data-element.
+ */
+np_ffi_V0DataElement np_ffi_DeviceInfo_into_V0DataElement(np_ffi_DeviceInfo device_info);
+
+/**
+ * Gets the value of the given PSM as a unsigned u16.
+ */
+uint16_t np_ffi_Psm_as_unsigned_u16(np_ffi_Psm psm);
+
+/**
+ * Attempts to construct a new PSM from
+ * the given u16 value.
+ */
+np_ffi_BuildPsmResult np_ffi_Psm_build_from_u16(uint16_t psm);
+
+/**
+ * Gets the tag of a `BuildTxPowerResult` tagged-union.
+ */
+np_ffi_BuildPsmResultKind np_ffi_BuildPsmResult_kind(np_ffi_BuildPsmResult result);
+
+/**
+ * Casts a `BuildPsmResult` to the `Success` variant, panicking in the
+ * case where the passed value is of a different enum variant.
+ */
+np_ffi_Psm np_ffi_BuildPsmResult_into_SUCCESS(np_ffi_BuildPsmResult result);
+
+/**
+ * Gets the tag of a `BuildActionTypeResult` tagged-union.
+ */
+np_ffi_BuildActionTypeResultKind np_ffi_BuildActionTypeResult_kind(np_ffi_BuildActionTypeResult result);
+
+/**
+ * Casts a `BuildActionTypeResult` to the `Success` variant, panicking in the
+ * case where the passed value is of a different enum variant.
+ */
+np_ffi_ActionType np_ffi_BuildActionTypeResult_into_SUCCESS(np_ffi_BuildActionTypeResult result);
+
+/**
+ * Attempts to construct a new ActionType from
+ * the given signed-byte value.
+ */
+np_ffi_BuildActionTypeResult np_ffi_ActionType_build_from_unsigned_byte(uint8_t action_type);
+
+/**
+ * Gets the value of the given ActionType as an unsigned byte.
+ */
+uint8_t np_ffi_ActionType_as_unsigned_byte(np_ffi_ActionType action_type);
+
+/**
* Extracts the numerical value of the given V1 DE type code as
* an unsigned 32-bit integer.
*/
diff --git a/nearby/presence/np_c_ffi/include/cpp/np_cpp_ffi_functions.h b/nearby/presence/np_c_ffi/include/cpp/np_cpp_ffi_functions.h
index 649837e..d23e620 100644
--- a/nearby/presence/np_c_ffi/include/cpp/np_cpp_ffi_functions.h
+++ b/nearby/presence/np_c_ffi/include/cpp/np_cpp_ffi_functions.h
@@ -462,6 +462,48 @@
/// integer, where the bit-positions correspond to individual actions.
uint32_t np_ffi_V0Actions_as_u32(V0Actions actions);
+/// Casts a `V0DataElement` to the `Psm` variant, panicking in the
+/// case where the passed value is of a different enum variant.
+Psm np_ffi_V0DataElement_into_PSM(V0DataElement de);
+
+/// Upcasts a Psm DE to a generic V0 data-element.
+V0DataElement np_ffi_Psm_into_V0DataElement(Psm psm);
+
+/// Casts a `V0DataElement` to the `DeviceInfo` variant, panicking in the
+/// case where the passed value is of a different enum variant.
+DeviceInfo np_ffi_V0DataElement_into_DeviceInfo(V0DataElement de);
+
+/// Upcasts a DeviceInfo DE to a generic V0 data-element.
+V0DataElement np_ffi_DeviceInfo_into_V0DataElement(DeviceInfo device_info);
+
+/// Gets the value of the given PSM as a unsigned u16.
+uint16_t np_ffi_Psm_as_unsigned_u16(Psm psm);
+
+/// Attempts to construct a new PSM from
+/// the given u16 value.
+BuildPsmResult np_ffi_Psm_build_from_u16(uint16_t psm);
+
+/// Gets the tag of a `BuildTxPowerResult` tagged-union.
+BuildPsmResultKind np_ffi_BuildPsmResult_kind(BuildPsmResult result);
+
+/// Casts a `BuildPsmResult` to the `Success` variant, panicking in the
+/// case where the passed value is of a different enum variant.
+Psm np_ffi_BuildPsmResult_into_SUCCESS(BuildPsmResult result);
+
+/// Gets the tag of a `BuildActionTypeResult` tagged-union.
+BuildActionTypeResultKind np_ffi_BuildActionTypeResult_kind(BuildActionTypeResult result);
+
+/// Casts a `BuildActionTypeResult` to the `Success` variant, panicking in the
+/// case where the passed value is of a different enum variant.
+ActionType np_ffi_BuildActionTypeResult_into_SUCCESS(BuildActionTypeResult result);
+
+/// Attempts to construct a new ActionType from
+/// the given signed-byte value.
+BuildActionTypeResult np_ffi_ActionType_build_from_unsigned_byte(uint8_t action_type);
+
+/// Gets the value of the given ActionType as an unsigned byte.
+uint8_t np_ffi_ActionType_as_unsigned_byte(ActionType action_type);
+
/// Extracts the numerical value of the given V1 DE type code as
/// an unsigned 32-bit integer.
uint32_t np_ffi_V1DEType_to_uint32_t(V1DEType de_type);
diff --git a/nearby/presence/np_c_ffi/include/cpp/np_cpp_ffi_types.h b/nearby/presence/np_c_ffi/include/cpp/np_cpp_ffi_types.h
index 5144478..50fdd49 100644
--- a/nearby/presence/np_c_ffi/include/cpp/np_cpp_ffi_types.h
+++ b/nearby/presence/np_c_ffi/include/cpp/np_cpp_ffi_types.h
@@ -38,16 +38,6 @@
namespace np_ffi {
namespace internal {
-/// The possible boolean action types which can be present in an Actions data element
-enum class ActionType : uint8_t {
- CrossDevSdk = 1,
- CallTransfer = 4,
- ActiveUnlock = 8,
- NearbyShare = 9,
- InstantTethering = 10,
- PhoneHub = 11,
-};
-
/// Result type for trying to add a V0 credential to a credential-slab.
enum class AddV0CredentialToSlabResult : uint8_t {
/// We succeeded in adding the credential to the slab.
@@ -124,6 +114,25 @@
Encrypted = 1,
};
+/// Discriminant for `BuildActionTypeResult`.
+enum class BuildActionTypeResultKind : uint8_t {
+ /// The action type was outside the
+ /// allowed 0 to 31 range.
+ OutOfRange = 0,
+ /// The transmission power was in range,
+ /// and so a `TxPower` struct was constructed.
+ Success = 1,
+};
+
+/// Discriminant for `BuildPsmResult`.
+enum class BuildPsmResultKind : uint8_t {
+ /// The PSM should be within two bytes.
+ OutOfRange = 0,
+ /// The PSM was in range,
+ /// and so a `Psm` struct was constructed.
+ Success = 1,
+};
+
/// Discriminant for `BuildTxPowerResult`.
enum class BuildTxPowerResultKind : uint8_t {
/// The transmission power was outside the
@@ -384,6 +393,14 @@
/// The associated payload may be obtained via
/// `V0DataElement#into_actions`.
Actions = 2,
+ /// The PSM data-element.
+ /// The associated payload may be obtained via
+ /// `V0DataElement#into_psm`.
+ Psm = 3,
+ /// The DeviceInfo data-element.
+ /// The associated payload may be obtained via
+ /// 'V0DataElement#into_device_info`.
+ DeviceInfo = 4,
};
/// Discriminant for `V1DE16ByteSaltResult`.
@@ -617,7 +634,9 @@
/// permitted to be between 0 and 255.
template<uintptr_t N>
struct ByteBuffer {
+ /// The number of bytes in the buffer.
uint8_t len;
+ /// The bytes in the buffer.
uint8_t bytes[N];
};
@@ -662,11 +681,59 @@
};
};
+/// Representation of a PSM for L2CAP,
+struct Psm {
+ uint16_t value;
+};
+
+/// A FFI safe wrapper of an optional byte buffer.
+template<uintptr_t N>
+struct OptionalByteBuffer {
+ enum class Tag {
+ /// The optional byte buffer isn't specified.
+ NotSpecified,
+ /// The optional byte buffer is present.
+ Present,
+ };
+
+ struct Present_Body {
+ ByteBuffer<N> _0;
+ };
+
+ Tag tag;
+ union {
+ Present_Body present;
+ };
+};
+
+/// Represents the contents of a Device Info DE containing the type and name of a device, as well as flag
+/// indicating whether the device name is truncated.
+///
+/// This representation is stable, and so you may directly
+/// reference this struct's fields if you wish.
+struct DeviceInfo {
+ /// The numerical value corresponding to a given
+ /// form factor, or "0" for the case where the
+ /// broadcasting device doesn't know what kind
+ /// of device type it is.
+ ///
+ /// Consult the Nearby spec for values
+ /// corresponding to particular form factors.
+ uint8_t device_type;
+ /// Boolean flag indicating whether the device name is truncated.
+ bool is_name_truncated;
+ /// An optional byte array representing the name of the broadcasting
+ /// device. The maximum size of this device name array is 9 bytes.
+ OptionalByteBuffer<9> device_name;
+};
+
/// Representation of a V0 data element.
struct V0DataElement {
enum class Tag {
TxPower,
Actions,
+ Psm,
+ DeviceInfo,
};
struct TxPower_Body {
@@ -677,10 +744,20 @@
V0Actions _0;
};
+ struct Psm_Body {
+ Psm _0;
+ };
+
+ struct DeviceInfo_Body {
+ DeviceInfo _0;
+ };
+
Tag tag;
union {
TxPower_Body tx_power;
Actions_Body actions;
+ Psm_Body psm;
+ DeviceInfo_Body device_info;
};
};
@@ -1013,6 +1090,47 @@
};
};
+/// Action data element
+struct ActionType {
+ uint8_t _0;
+};
+
+/// Result type for attempting to construct a
+/// PSM from two bytes.
+struct BuildPsmResult {
+ enum class Tag {
+ OutOfRange,
+ Success,
+ };
+
+ struct Success_Body {
+ Psm _0;
+ };
+
+ Tag tag;
+ union {
+ Success_Body success;
+ };
+};
+
+/// Result type for attempting to construct a
+/// ActionType from an usigned byte.
+struct BuildActionTypeResult {
+ enum class Tag {
+ OutOfRange,
+ Success,
+ };
+
+ struct Success_Body {
+ ActionType _0;
+ };
+
+ Tag tag;
+ union {
+ Success_Body success;
+ };
+};
+
} // namespace internal
} // namespace np_ffi
diff --git a/nearby/presence/np_c_ffi/src/lib.rs b/nearby/presence/np_c_ffi/src/lib.rs
index 5111aff..6762527 100644
--- a/nearby/presence/np_c_ffi/src/lib.rs
+++ b/nearby/presence/np_c_ffi/src/lib.rs
@@ -92,9 +92,9 @@
}
fn system_handler(panic_reason: PanicReason) -> ! {
- std::eprintln!("NP FFI Panicked: {:?}", panic_reason);
+ std::eprintln!("NP FFI Panicked: {panic_reason:?}");
let backtrace = std::backtrace::Backtrace::capture();
- std::eprintln!("Stack trace: {}", backtrace);
+ std::eprintln!("Stack trace: {backtrace}");
std::process::abort()
}
}
diff --git a/nearby/presence/np_c_ffi/src/v0.rs b/nearby/presence/np_c_ffi/src/v0.rs
index 84aab95..56741da 100644
--- a/nearby/presence/np_c_ffi/src/v0.rs
+++ b/nearby/presence/np_c_ffi/src/v0.rs
@@ -139,3 +139,87 @@
pub extern "C" fn np_ffi_V0Actions_as_u32(actions: V0Actions) -> u32 {
actions.as_u32()
}
+
+/// Casts a `V0DataElement` to the `Psm` variant, panicking in the
+/// case where the passed value is of a different enum variant.
+#[no_mangle]
+pub extern "C" fn np_ffi_V0DataElement_into_PSM(de: V0DataElement) -> Psm {
+ unwrap(de.into_psm(), PanicReason::EnumCastFailed)
+}
+
+/// Upcasts a Psm DE to a generic V0 data-element.
+#[no_mangle]
+pub extern "C" fn np_ffi_Psm_into_V0DataElement(psm: Psm) -> V0DataElement {
+ V0DataElement::Psm(psm)
+}
+
+/// Casts a `V0DataElement` to the `DeviceInfo` variant, panicking in the
+/// case where the passed value is of a different enum variant.
+#[no_mangle]
+pub extern "C" fn np_ffi_V0DataElement_into_DeviceInfo(de: V0DataElement) -> DeviceInfo {
+ unwrap(de.into_device_info(), PanicReason::EnumCastFailed)
+}
+
+/// Upcasts a DeviceInfo DE to a generic V0 data-element.
+#[no_mangle]
+pub extern "C" fn np_ffi_DeviceInfo_into_V0DataElement(device_info: DeviceInfo) -> V0DataElement {
+ V0DataElement::DeviceInfo(device_info)
+}
+
+/// Gets the value of the given PSM as a unsigned u16.
+#[no_mangle]
+pub extern "C" fn np_ffi_Psm_as_unsigned_u16(psm: Psm) -> u16 {
+ psm.as_u16()
+}
+
+/// Attempts to construct a new PSM from
+/// the given u16 value.
+#[no_mangle]
+pub extern "C" fn np_ffi_Psm_build_from_u16(psm: u16) -> BuildPsmResult {
+ Psm::build_from_unsigned_bytes(psm)
+}
+
+/// Gets the tag of a `BuildTxPowerResult` tagged-union.
+#[no_mangle]
+pub extern "C" fn np_ffi_BuildPsmResult_kind(result: BuildPsmResult) -> BuildPsmResultKind {
+ result.kind()
+}
+
+/// Casts a `BuildPsmResult` to the `Success` variant, panicking in the
+/// case where the passed value is of a different enum variant.
+#[no_mangle]
+pub extern "C" fn np_ffi_BuildPsmResult_into_SUCCESS(result: BuildPsmResult) -> Psm {
+ unwrap(result.into_success(), PanicReason::EnumCastFailed)
+}
+
+/// Gets the tag of a `BuildActionTypeResult` tagged-union.
+#[no_mangle]
+pub extern "C" fn np_ffi_BuildActionTypeResult_kind(
+ result: BuildActionTypeResult,
+) -> BuildActionTypeResultKind {
+ result.kind()
+}
+
+/// Casts a `BuildActionTypeResult` to the `Success` variant, panicking in the
+/// case where the passed value is of a different enum variant.
+#[no_mangle]
+pub extern "C" fn np_ffi_BuildActionTypeResult_into_SUCCESS(
+ result: BuildActionTypeResult,
+) -> ActionType {
+ unwrap(result.into_success(), PanicReason::EnumCastFailed)
+}
+
+/// Attempts to construct a new ActionType from
+/// the given signed-byte value.
+#[no_mangle]
+pub extern "C" fn np_ffi_ActionType_build_from_unsigned_byte(
+ action_type: u8,
+) -> BuildActionTypeResult {
+ ActionType::build_from_unsigned_byte(action_type)
+}
+
+/// Gets the value of the given ActionType as an unsigned byte.
+#[no_mangle]
+pub extern "C" fn np_ffi_ActionType_as_unsigned_byte(action_type: ActionType) -> u8 {
+ action_type.as_u8()
+}
diff --git a/nearby/presence/np_cpp_ffi/fuzz/deserialization_fuzzer.cc b/nearby/presence/np_cpp_ffi/fuzz/deserialization_fuzzer.cc
index 24614e9..65f4125 100644
--- a/nearby/presence/np_cpp_ffi/fuzz/deserialization_fuzzer.cc
+++ b/nearby/presence/np_cpp_ffi/fuzz/deserialization_fuzzer.cc
@@ -284,6 +284,13 @@
[[maybe_unused]] auto actions = de.AsActions();
break;
}
+ case nearby_protocol::V0DataElementKind::Psm: {
+ [[maybe_unused]] auto psm = de.AsPsm();
+ break;
+ }
+ case nearby_protocol::V0DataElementKind::DeviceInfo: {
+ break;
+ }
}
}
diff --git a/nearby/presence/np_cpp_ffi/include/nearby_protocol.h b/nearby/presence/np_cpp_ffi/include/nearby_protocol.h
index 08b7803..14d9170 100644
--- a/nearby/presence/np_cpp_ffi/include/nearby_protocol.h
+++ b/nearby/presence/np_cpp_ffi/include/nearby_protocol.h
@@ -61,7 +61,6 @@
namespace nearby_protocol {
// Re-exporting cbindgen generated types which are used in the public API
-using np_ffi::internal::ActionType;
using np_ffi::internal::AddV0CredentialToSlabResult;
using np_ffi::internal::AddV0DEResult;
using np_ffi::internal::AddV1CredentialToSlabResult;
@@ -579,6 +578,71 @@
np_ffi::internal::TxPower tx_power_;
};
+// A Psm for L2CAP.
+class Psm {
+ public:
+ Psm() = delete;
+
+ // Gets the value of this PSM as a unsigned int of two bytes.
+ [[nodiscard]] uint16_t GetAsU16() const;
+
+ // Attempts to construct a tx power with the given value contained
+ // in a signed byte. If the number is not within the representable
+ // range, this method will return an invalid argument error.
+ [[nodiscard]] static absl::StatusOr<Psm> TryBuildFromU16(uint16_t value);
+
+ private:
+ friend class V0DataElement;
+ explicit Psm(np_ffi::internal::Psm psm) : psm_(psm) {}
+ np_ffi::internal::Psm psm_;
+};
+
+// A DeviceInfo
+class DeviceInfo {
+ public:
+ DeviceInfo() = delete;
+
+ // Gets the device type.
+ [[nodiscard]] uint8_t GetDeviceType() const;
+
+ // Gets whether the name is truncated.
+ [[nodiscard]] bool IsNameTruncated() const;
+
+ // Gets the device name.
+ [[nodiscard]] np_ffi::internal::OptionalByteBuffer<9> GetDeviceName() const;
+
+ // Attempts to construct a device info.
+ [[nodiscard]] static DeviceInfo Build(
+ uint8_t device_type, bool is_name_truncated,
+ np_ffi::internal::OptionalByteBuffer<9> device_name);
+
+ private:
+ friend class V0DataElement;
+ explicit DeviceInfo(np_ffi::internal::DeviceInfo device_info)
+ : device_info_(device_info) {}
+ np_ffi::internal::DeviceInfo device_info_;
+};
+
+// An ActionType
+class ActionType {
+ public:
+ ActionType() = delete;
+
+ // Gets the value of this ActionType as a unsigned int 1 byte.
+ [[nodiscard]] uint8_t GetAsU8() const;
+
+ // Attempts to construct a action type with the given value contained
+ // in a unsigned byte. If the number is not within the representable
+ // range, this method will return an invalid argument error.
+ [[nodiscard]] static absl::StatusOr<ActionType> TryBuildFromU8(uint8_t value);
+
+ private:
+ friend class V0Actions;
+ explicit ActionType(np_ffi::internal::ActionType action_type)
+ : action_type_(action_type) {}
+ np_ffi::internal::ActionType action_type_;
+};
+
// A single V0 data element
class V0DataElement {
public:
@@ -587,12 +651,23 @@
// Casts the V0DataElement into the TxPower variant, panicking in the case
// where the data element is of a different enum variant
[[nodiscard]] TxPower AsTxPower() const;
+ // Casts the V0DataElement into the Psm variant, panicking in the case
+ // where the data element is of a different enum variant
+ [[nodiscard]] Psm AsPsm() const;
+ // Casts the V0DataElement into the DeviceInfo variant, panicking in the case
+ // where the data element is of a different enum variant
+ [[nodiscard]] DeviceInfo AsDeviceInfo() const;
+
// Casts the V0DataElement into the Actions variant, panicking in the case
// where the data element is of a different enum variant
[[nodiscard]] V0Actions AsActions() const;
// Constructs a Tx Power V0 data element
explicit V0DataElement(TxPower tx_power);
+ // Constructs a Tx Power V0 data element
+ explicit V0DataElement(Psm psm);
+ // Constructs a Device Info V0 data element
+ explicit V0DataElement(DeviceInfo device_info);
// Constructs an Actions V0 data element
explicit V0DataElement(V0Actions actions);
diff --git a/nearby/presence/np_cpp_ffi/nearby_protocol.cc b/nearby/presence/np_cpp_ffi/nearby_protocol.cc
index 471cbea..35805c4 100644
--- a/nearby/presence/np_cpp_ffi/nearby_protocol.cc
+++ b/nearby/presence/np_cpp_ffi/nearby_protocol.cc
@@ -480,6 +480,16 @@
np_ffi::internal::np_ffi_V0DataElement_into_TX_POWER(v0_data_element_));
}
+Psm V0DataElement::AsPsm() const {
+ return Psm(
+ np_ffi::internal::np_ffi_V0DataElement_into_PSM(v0_data_element_));
+}
+
+DeviceInfo V0DataElement::AsDeviceInfo() const {
+ return DeviceInfo(
+ np_ffi::internal::np_ffi_V0DataElement_into_DeviceInfo(v0_data_element_));
+}
+
V0Actions V0DataElement::AsActions() const {
auto internal =
np_ffi::internal::np_ffi_V0DataElement_into_ACTIONS(v0_data_element_);
@@ -491,6 +501,15 @@
np_ffi::internal::np_ffi_TxPower_into_V0DataElement(tx_power.tx_power_);
}
+V0DataElement::V0DataElement(Psm psm) {
+ v0_data_element_ = np_ffi::internal::np_ffi_Psm_into_V0DataElement(psm.psm_);
+}
+
+V0DataElement::V0DataElement(DeviceInfo device_info) {
+ v0_data_element_ = np_ffi::internal::np_ffi_DeviceInfo_into_V0DataElement(
+ device_info.device_info_);
+}
+
V0DataElement::V0DataElement(V0Actions actions) {
v0_data_element_ =
np_ffi::internal::np_ffi_V0Actions_into_V0DataElement(actions.actions_);
@@ -501,12 +520,12 @@
}
bool V0Actions::HasAction(ActionType action) const {
- return np_ffi::internal::np_ffi_V0Actions_has_action(actions_, action);
+ return np_ffi::internal::np_ffi_V0Actions_has_action(actions_, action.action_type_);
}
absl::Status V0Actions::TrySetAction(ActionType action, bool value) {
auto result =
- np_ffi::internal::np_ffi_V0Actions_set_action(actions_, action, value);
+ np_ffi::internal::np_ffi_V0Actions_set_action(actions_, action.action_type_, value);
auto kind = np_ffi::internal::np_ffi_SetV0ActionResult_kind(result);
switch (kind) {
case np_ffi::internal::SetV0ActionResultKind::Success: {
@@ -547,6 +566,62 @@
}
}
+uint16_t Psm::GetAsU16() const {
+ return np_ffi::internal::np_ffi_Psm_as_unsigned_u16(psm_);
+}
+
+absl::StatusOr<Psm> Psm::TryBuildFromU16(uint16_t value) {
+ auto result = np_ffi::internal::np_ffi_Psm_build_from_u16(value);
+ auto kind = np_ffi::internal::np_ffi_BuildPsmResult_kind(result);
+ switch (kind) {
+ case np_ffi::internal::BuildPsmResultKind::Success: {
+ return Psm(np_ffi::internal::np_ffi_BuildPsmResult_into_SUCCESS(result));
+ }
+ case np_ffi::internal::BuildPsmResultKind::OutOfRange: {
+ return absl::InvalidArgumentError(
+ "Could not build a PSM for the requested byte array.");
+ }
+ }
+}
+
+uint8_t DeviceInfo::GetDeviceType() const { return device_info_.device_type; }
+
+bool DeviceInfo::IsNameTruncated() const {
+ return device_info_.is_name_truncated;
+}
+
+np_ffi::internal::OptionalByteBuffer<9> DeviceInfo::GetDeviceName() const {
+ return device_info_.device_name;
+}
+
+DeviceInfo DeviceInfo::Build(
+ uint8_t device_type, bool is_name_truncated,
+ np_ffi::internal::OptionalByteBuffer<9> device_name) {
+ return DeviceInfo(np_ffi::internal::DeviceInfo{
+ .device_type = device_type,
+ .is_name_truncated = is_name_truncated,
+ .device_name = device_name,
+ });
+}
+
+uint8_t ActionType::GetAsU8() const {
+ return np_ffi::internal::np_ffi_ActionType_as_unsigned_byte(action_type_);
+}
+
+absl::StatusOr<ActionType> ActionType::TryBuildFromU8(uint8_t value) {
+ auto result = np_ffi::internal::np_ffi_ActionType_build_from_unsigned_byte(value);
+ auto kind = np_ffi::internal::np_ffi_BuildActionTypeResult_kind(result);
+ switch (kind) {
+ case np_ffi::internal::BuildActionTypeResultKind::Success: {
+ return ActionType(np_ffi::internal::np_ffi_BuildActionTypeResult_into_SUCCESS(result));
+ }
+ case np_ffi::internal::BuildActionTypeResultKind::OutOfRange: {
+ return absl::InvalidArgumentError(
+ "Could not build an ActionType for the requested byte array.");
+ }
+ }
+}
+
// This is called after all references to the shared_ptr have gone out of
// scope
auto DeallocateV1Adv(
diff --git a/nearby/presence/np_cpp_ffi/sample/main.cc b/nearby/presence/np_cpp_ffi/sample/main.cc
index 81c2cc8..0c4dcc3 100644
--- a/nearby/presence/np_cpp_ffi/sample/main.cc
+++ b/nearby/presence/np_cpp_ffi/sample/main.cc
@@ -235,6 +235,31 @@
<< "\n";
return;
}
+ case nearby_protocol::V0DataElementKind::Psm: {
+ std::cout << "\t\t\tDE Type is Psm\n";
+ auto psm = de.AsPsm();
+ std::cout << "\t\t\tPSM: " << std::bitset<16>(psm.GetAsU16())
+ << "\n";
+ return;
+ }
+ case nearby_protocol::V0DataElementKind::DeviceInfo: {
+ std::cout << "\t\t\tDE Type is DeviceInfo\n";
+ auto device_info = de.AsDeviceInfo();
+ std::cout << "\t\t\tDeviceType: "
+ << static_cast<unsigned>(device_info.GetDeviceType()) << "\n";
+ std::cout << "\t\t\tIsNameTruncated: " << device_info.IsNameTruncated()
+ << "\n";
+ auto device_name = device_info.GetDeviceName();
+ if (device_name.tag ==
+ np_ffi::internal::OptionalByteBuffer<9>::Tag::Present) {
+ std::cout << "\t\t\tDeviceName: "
+ << absl::BytesToHexString(nearby_protocol::ByteBuffer<9>(
+ device_name.present._0)
+ .ToString())
+ << "\n";
+ }
+ return;
+ }
}
}
diff --git a/nearby/presence/np_cpp_ffi/tests/v0_unencrypted_deserialization_tests.cc b/nearby/presence/np_cpp_ffi/tests/v0_unencrypted_deserialization_tests.cc
index 59d6b69..6299322 100644
--- a/nearby/presence/np_cpp_ffi/tests/v0_unencrypted_deserialization_tests.cc
+++ b/nearby/presence/np_cpp_ffi/tests/v0_unencrypted_deserialization_tests.cc
@@ -137,13 +137,13 @@
auto actions = de.AsActions();
ASSERT_EQ(actions.GetAsU32(), 0x40400000U);
- ASSERT_TRUE(actions.HasAction(nearby_protocol::ActionType::CrossDevSdk));
- ASSERT_TRUE(actions.HasAction(nearby_protocol::ActionType::NearbyShare));
+ ASSERT_TRUE(actions.HasAction(nearby_protocol::ActionType::TryBuildFromU8(1).value()));
+ ASSERT_TRUE(actions.HasAction(nearby_protocol::ActionType::TryBuildFromU8(9).value()));
- ASSERT_FALSE(actions.HasAction(nearby_protocol::ActionType::ActiveUnlock));
+ ASSERT_FALSE(actions.HasAction(nearby_protocol::ActionType::TryBuildFromU8(8).value()));
ASSERT_FALSE(
- actions.HasAction(nearby_protocol::ActionType::InstantTethering));
- ASSERT_FALSE(actions.HasAction(nearby_protocol::ActionType::PhoneHub));
+ actions.HasAction(nearby_protocol::ActionType::TryBuildFromU8(10).value()));
+ ASSERT_FALSE(actions.HasAction(nearby_protocol::ActionType::TryBuildFromU8(11).value()));
}
TEST_F(NpCppTest, V0MultipleDataElements) {
diff --git a/nearby/presence/np_cpp_ffi/tests/v0_unencrypted_serialization_tests.cc b/nearby/presence/np_cpp_ffi/tests/v0_unencrypted_serialization_tests.cc
index ca28233..4ae2124 100644
--- a/nearby/presence/np_cpp_ffi/tests/v0_unencrypted_serialization_tests.cc
+++ b/nearby/presence/np_cpp_ffi/tests/v0_unencrypted_serialization_tests.cc
@@ -28,24 +28,6 @@
ASSERT_FALSE(out_of_range_result.ok());
}
-TEST_F(NpCppTest, V0UnencryptedActionFlavorMustMatch) {
- auto actions = nearby_protocol::V0Actions::BuildNewZeroed(
- nearby_protocol::AdvertisementBuilderKind::Public);
-
- // Try to set an encrypted-only action.
- auto mismatch_result =
- actions.TrySetAction(nearby_protocol::ActionType::InstantTethering, true);
- ASSERT_FALSE(mismatch_result.ok());
- // Verify that nothing changed about the actions.
- ASSERT_EQ(actions.GetAsU32(), 0u);
-
- // Try again, but with a plaintext-compatible action.
- auto success_result =
- actions.TrySetAction(nearby_protocol::ActionType::NearbyShare, true);
- ASSERT_TRUE(success_result.ok());
- ASSERT_TRUE(actions.HasAction(nearby_protocol::ActionType::NearbyShare));
-}
-
// Corresponds to V0DeserializeSingleDataElementTxPower
TEST_F(NpCppTest, V0SerializeSingleDataElementTxPower) {
auto adv_builder = nearby_protocol::V0AdvertisementBuilder::CreatePublic();
@@ -94,10 +76,10 @@
nearby_protocol::AdvertisementBuilderKind::Public);
ASSERT_TRUE(
- actions.TrySetAction(nearby_protocol::ActionType::NearbyShare, true)
+ actions.TrySetAction(nearby_protocol::ActionType::TryBuildFromU8(9).value(), true)
.ok());
ASSERT_TRUE(
- actions.TrySetAction(nearby_protocol::ActionType::CrossDevSdk, true)
+ actions.TrySetAction(nearby_protocol::ActionType::TryBuildFromU8(1).value(), true)
.ok());
auto de = nearby_protocol::V0DataElement(actions);
diff --git a/nearby/presence/np_ffi_core/src/common.rs b/nearby/presence/np_ffi_core/src/common.rs
index 53d7d1e..5b98d77 100644
--- a/nearby/presence/np_ffi_core/src/common.rs
+++ b/nearby/presence/np_ffi_core/src/common.rs
@@ -21,6 +21,8 @@
use handle_map::HandleNotPresentError;
use lock_adapter::stdlib::{RwLock, RwLockWriteGuard};
use lock_adapter::RwLock as _;
+use np_adv::legacy::data_elements::psm::PsmDataElement;
+use np_adv_dynamic::legacy::ToBoxedSerializeDataElement;
const MAX_HANDLES: u32 = u32::MAX - 1;
@@ -185,8 +187,10 @@
// TODO: Once generic const exprs are stabilized,
// we could instead make N into a compile-time u8.
pub struct ByteBuffer<const N: usize> {
- len: u8,
- bytes: [u8; N],
+ /// The number of bytes in the buffer.
+ pub len: u8,
+ /// The bytes in the buffer.
+ pub bytes: [u8; N],
}
impl<const N: usize> Default for ByteBuffer<N> {
@@ -292,11 +296,12 @@
}
/// A FFI safe wrapper of an optional fixed-size byte array.
-#[derive(Clone)]
+#[derive(Clone, Default)]
#[cfg_attr(any(test, feature = "testing"), derive(Debug, PartialEq, Eq, arbitrary::Arbitrary))]
#[repr(C)]
pub enum OptionalFixedSizeArray<const N: usize> {
/// The optional fixed-size array isn't specified.
+ #[default]
NotSpecified,
/// The optional fixed-size array is present.
Present(FixedSizeArray<N>),
@@ -312,12 +317,6 @@
}
}
-impl<const N: usize> Default for OptionalFixedSizeArray<N> {
- fn default() -> Self {
- Self::NotSpecified
- }
-}
-
impl<const N: usize> OptionalFixedSizeArray<N> {
/// Attempts to cast `self` to the `Present` variant,
/// returning `None` in the case where the passed value is of a different enum variant.
@@ -358,6 +357,56 @@
}
}
+/// Discriminant for [`OptionalByteBuffer`].
+#[derive(Copy, Clone)]
+#[repr(u8)]
+pub enum OptionalByteBufferKind {
+ /// The optional byte buffer isn't specified.
+ NotSpecified = 0,
+ /// The optional byte buffer is present.
+ Present = 1,
+}
+
+/// A FFI safe wrapper of an optional byte buffer.
+#[derive(Clone, Default)]
+#[cfg_attr(any(test, feature = "testing"), derive(Debug, PartialEq, Eq, arbitrary::Arbitrary))]
+#[repr(C)]
+pub enum OptionalByteBuffer<const N: usize> {
+ /// The optional byte buffer isn't specified.
+ #[default]
+ NotSpecified,
+ /// The optional byte buffer is present.
+ Present(ByteBuffer<N>),
+}
+
+impl<const N: usize> FfiEnum for OptionalByteBuffer<N> {
+ type Kind = OptionalByteBufferKind;
+ fn kind(&self) -> Self::Kind {
+ match self {
+ Self::NotSpecified => OptionalByteBufferKind::NotSpecified,
+ Self::Present(_) => OptionalByteBufferKind::Present,
+ }
+ }
+}
+
+impl<const N: usize> OptionalByteBuffer<N> {
+ /// Attempts to cast `self` to the `Present` variant,
+ /// returning `None` in the case where the passed value is of a different enum variant.
+ pub fn into_present(self) -> Option<ByteBuffer<N>> {
+ match self {
+ Self::NotSpecified => None,
+ Self::Present(x) => Some(x),
+ }
+ }
+ /// Returns true iff this `OptionalByteBuffer` has contents.
+ pub fn is_present(&self) -> bool {
+ match self {
+ Self::NotSpecified => false,
+ Self::Present(_) => true,
+ }
+ }
+}
+
pub(crate) type CryptoRngImpl = <CryptoProviderImpl as CryptoProvider>::CryptoRng;
pub(crate) struct LazyInitCryptoRng {
@@ -471,3 +520,113 @@
np_adv::shared_data::TxPower::try_from(value.as_i8()).map_err(|_| InvalidStackDataStructure)
}
}
+
+/// Discriminant for `BuildPsmResult`.
+#[repr(u8)]
+#[derive(Clone, Copy)]
+pub enum BuildPsmResultKind {
+ /// The PSM should be within two bytes.
+ OutOfRange = 0,
+ /// The PSM was in range,
+ /// and so a `Psm` struct was constructed.
+ Success = 1,
+}
+
+/// Result type for attempting to construct a
+/// PSM from two bytes.
+#[repr(C)]
+#[allow(missing_docs)]
+pub enum BuildPsmResult {
+ OutOfRange,
+ Success(Psm),
+}
+
+impl FfiEnum for BuildPsmResult {
+ type Kind = BuildPsmResultKind;
+ fn kind(&self) -> Self::Kind {
+ match self {
+ Self::OutOfRange => BuildPsmResultKind::OutOfRange,
+ Self::Success(_) => BuildPsmResultKind::Success,
+ }
+ }
+}
+
+impl BuildPsmResult {
+ declare_enum_cast! {into_success, Success, Psm}
+}
+
+/// Representation of a PSM for L2CAP,
+#[derive(Clone, Copy)]
+#[cfg_attr(any(test, feature = "testing"), derive(Debug, PartialEq, Eq, arbitrary::Arbitrary))]
+#[repr(C)]
+pub struct Psm {
+ pub(crate) value: u16,
+}
+
+impl Psm {
+ /// Constructs a new PSM from the given unsigned two-byte value.
+ pub fn build_from_unsigned_bytes(psm: u16) -> BuildPsmResult {
+ BuildPsmResult::Success(Self { value: psm })
+ }
+
+ /// Returns the PSM value.
+ pub fn as_u16(&self) -> u16 {
+ self.value
+ }
+}
+
+impl TryFrom<Psm> for ToBoxedSerializeDataElement {
+ type Error = InvalidStackDataStructure;
+
+ fn try_from(psm: Psm) -> Result<Self, Self::Error> {
+ Ok(ToBoxedSerializeDataElement::Plaintext(Box::new(PsmDataElement { value: psm.value })))
+ }
+}
+
+/// Represents the contents of a Device Info DE containing the type and name of a device, as well as flag
+/// indicating whether the device name is truncated.
+///
+/// This representation is stable, and so you may directly
+/// reference this struct's fields if you wish.
+#[derive(Clone)]
+#[cfg_attr(any(test, feature = "testing"), derive(Debug, PartialEq, Eq, arbitrary::Arbitrary))]
+#[repr(C)]
+pub struct DeviceInfo {
+ /// The numerical value corresponding to a given
+ /// form factor, or "0" for the case where the
+ /// broadcasting device doesn't know what kind
+ /// of device type it is.
+ ///
+ /// Consult the Nearby spec for values
+ /// corresponding to particular form factors.
+ pub device_type: u8,
+ /// Boolean flag indicating whether the device name is truncated.
+ pub is_name_truncated: bool,
+ /// An optional byte array representing the name of the broadcasting
+ /// device. The maximum size of this device name array is 9 bytes.
+ pub device_name: OptionalByteBuffer<9>,
+}
+
+impl TryFrom<DeviceInfo> for ToBoxedSerializeDataElement {
+ type Error = InvalidStackDataStructure;
+
+ fn try_from(core_device_info: DeviceInfo) -> Result<Self, Self::Error> {
+ let device_type = np_adv::shared_data::DeviceType::from_repr(core_device_info.device_type)
+ .ok_or(InvalidStackDataStructure)?;
+
+ let name_slice = match core_device_info.device_name.into_present() {
+ Some(buffer) => buffer.as_slice()?.to_vec(),
+ None => Vec::new(),
+ };
+
+ let device_info = np_adv::shared_data::DeviceInfo::try_from((
+ device_type,
+ core_device_info.is_name_truncated,
+ name_slice.as_slice(),
+ ))
+ .map_err(|_| InvalidStackDataStructure)?;
+ Ok(ToBoxedSerializeDataElement::Plaintext(Box::new(
+ np_adv::legacy::data_elements::device_info::DeviceInfoDataElement { device_info },
+ )))
+ }
+}
diff --git a/nearby/presence/np_ffi_core/src/deserialize/v1.rs b/nearby/presence/np_ffi_core/src/deserialize/v1.rs
index dec454c..5ee203b 100644
--- a/nearby/presence/np_ffi_core/src/deserialize/v1.rs
+++ b/nearby/presence/np_ffi_core/src/deserialize/v1.rs
@@ -758,6 +758,8 @@
CapabilitiesMalformed = 12,
/// There was a Requirements DE, but the payload was malformed.
RequirementsMalformed = 13,
+ /// There was a DeviceInfo DE, but the payload was malformed.
+ DeviceInfoMalformed = 14,
}
/// Representation of the result of attempting to deserialize
@@ -790,6 +792,7 @@
MediaDeduplicationIdWrongLength,
CapabilitiesMalformed,
RequirementsMalformed,
+ DeviceInfoMalformed,
}
impl FfiEnum for V1DataElementDeserializationResult {
@@ -828,6 +831,9 @@
Self::RequirementsMalformed => {
V1DataElementDeserializationResultKind::RequirementsMalformed
}
+ Self::DeviceInfoMalformed => {
+ V1DataElementDeserializationResultKind::DeviceInfoMalformed
+ }
}
}
}
@@ -945,6 +951,12 @@
))
}
Ok(DeserializedGoogleDE::Requirements(Err(_))) => V1DataElementDeserializationResult::RequirementsMalformed,
+ Ok(DeserializedGoogleDE::DeviceInfo(Ok(device_info))) => {
+ V1DataElementDeserializationResult::Success(V1DataElement::DeviceInfo(
+ device_info.into(),
+ ))
+ }
+ Ok(DeserializedGoogleDE::DeviceInfo(Err(_))) => V1DataElementDeserializationResult::DeviceInfoMalformed,
Err(_) => V1DataElementDeserializationResult::Success(V1DataElement::Generic(self)),
})
}
@@ -1044,3 +1056,23 @@
Self { requirements }
}
}
+
+impl From<np_adv::extended::data_elements::DeviceInfoDataElement> for DeviceInfo {
+ fn from(de: np_adv::extended::data_elements::DeviceInfoDataElement) -> Self {
+ let name_slice = de.device_info.device_name();
+ let mut name_bytes = [0; 9];
+ let len = name_slice.len().min(name_bytes.len());
+ #[allow(clippy::expect_used)]
+ name_bytes
+ .get_mut(..len)
+ .expect("slice length is within bounds")
+ .copy_from_slice(name_slice.get(..len).expect("slice length is within bounds"));
+ let device_name =
+ OptionalByteBuffer::Present(ByteBuffer { len: len as u8, bytes: name_bytes });
+ Self {
+ device_type: de.device_info.device_type() as u8,
+ is_name_truncated: de.device_info.name_truncated(),
+ device_name,
+ }
+ }
+}
diff --git a/nearby/presence/np_ffi_core/src/serialize/v1.rs b/nearby/presence/np_ffi_core/src/serialize/v1.rs
index 498194e..e3ba107 100644
--- a/nearby/presence/np_ffi_core/src/serialize/v1.rs
+++ b/nearby/presence/np_ffi_core/src/serialize/v1.rs
@@ -758,6 +758,70 @@
}
}
+/// The result of attempting to serialize capabilities
+/// represented as a bitmap into a DE.
+#[repr(C)]
+#[allow(missing_docs)]
+pub enum SerializeDeviceInfoResult {
+ Success(GenericV1DataElement),
+ DeviceInfoMalformed,
+}
+
+impl FfiEnum for SerializeDeviceInfoResult {
+ type Kind = SerializeDeviceInfoResultKind;
+ fn kind(&self) -> Self::Kind {
+ match self {
+ Self::Success(_) => SerializeDeviceInfoResultKind::Success,
+ Self::DeviceInfoMalformed => SerializeDeviceInfoResultKind::DeviceInfoMalformed,
+ }
+ }
+}
+
+impl SerializeDeviceInfoResult {
+ declare_enum_cast! {into_success, Success, GenericV1DataElement }
+}
+
+/// Discriminant for `SerializeCapabilitiesBitmapResult`
+#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+#[repr(u8)]
+pub enum SerializeDeviceInfoResultKind {
+ /// The attempt succeeded. The wrapped data element
+ /// may be obtained via
+ /// `SerializeCapabilitiesBitmapResult#into_success`.
+ Success = 0,
+ /// The attempt failed because reserved bits were set.
+ DeviceInfoMalformed = 1,
+}
+
+impl DeviceInfo {
+ /// Attempts to serialize this Device Info to a generic V1 data element.
+ pub fn try_serialize_v1(&self) -> SerializeDeviceInfoResult {
+ let result: Result<GenericV1DataElement, ()> = (|| {
+ let device_type =
+ np_adv::shared_data::DeviceType::from_repr(self.device_type).ok_or(())?;
+
+ let name_slice = match &self.device_name {
+ OptionalByteBuffer::Present(buffer) => buffer.as_slice().map_err(|_| ())?,
+ OptionalByteBuffer::NotSpecified => &[],
+ };
+
+ let device_info = np_adv::shared_data::DeviceInfo::try_from((
+ device_type,
+ self.is_name_truncated,
+ name_slice,
+ ))
+ .map_err(|_| ())?;
+ let de = np_adv::extended::data_elements::DeviceInfoDataElement::from(device_info);
+ Ok(GenericV1DataElement::from_write_de(&de))
+ })();
+
+ match result {
+ Ok(de) => SerializeDeviceInfoResult::Success(de),
+ Err(_) => SerializeDeviceInfoResult::DeviceInfoMalformed,
+ }
+ }
+}
+
impl ContextSyncSeqNum {
/// Attempts to serialize this Context Sync sequence number to a generic
/// V1 data element.
@@ -1026,6 +1090,30 @@
assert_eq!(result.kind(), SerializeRequirementsBitmapResultKind::ReservedBitsSet);
}
+ #[test]
+ fn test_serialize_valid_device_info() {
+ let device_name = OptionalByteBuffer::Present(ByteBuffer { len: 9, bytes: [1u8; 9] });
+ let device_info = DeviceInfo { device_type: 1, is_name_truncated: false, device_name };
+ let result = device_info.try_serialize_v1();
+ assert_eq!(result.kind(), SerializeDeviceInfoResultKind::Success);
+ }
+
+ #[test]
+ fn test_serialize_device_info_name_too_short() {
+ let mut name_bytes = [0u8; 9];
+ let name_slice = b"abcd"; // 4 bytes, which is less than the minimum of 5.
+ #[allow(clippy::indexing_slicing)]
+ name_bytes[..name_slice.len()].copy_from_slice(name_slice);
+
+ let device_name = OptionalByteBuffer::Present(ByteBuffer {
+ len: name_slice.len() as u8,
+ bytes: name_bytes,
+ });
+ let device_info = DeviceInfo { device_type: 1, is_name_truncated: false, device_name };
+ let result = device_info.try_serialize_v1();
+ assert_eq!(result.kind(), SerializeDeviceInfoResultKind::DeviceInfoMalformed);
+ }
+
#[cfg(not(feature = "nocrypto"))]
fn state_is_advertisement_building(adv_builder_state: &V1AdvertisementBuilderState) -> bool {
matches!(adv_builder_state, V1AdvertisementBuilderState::Advertisement(_))
diff --git a/nearby/presence/np_ffi_core/src/v0.rs b/nearby/presence/np_ffi_core/src/v0.rs
index d87cfe8..6f93d63 100644
--- a/nearby/presence/np_ffi_core/src/v0.rs
+++ b/nearby/presence/np_ffi_core/src/v0.rs
@@ -15,14 +15,15 @@
//! Common externally-acessible V0 constructs for both of the
//! serialization+deserialization flows.
-use crate::common::{InvalidStackDataStructure, TxPower};
+use crate::common::{
+ ByteBuffer, DeviceInfo, InvalidStackDataStructure, OptionalByteBuffer, Psm, TxPower,
+};
use crate::serialize::v0::AdvertisementBuilderKind;
use crate::utils::FfiEnum;
use np_adv::{
legacy::data_elements::actions::ActionsDataElement,
legacy::{data_elements as np_adv_de, Ciphertext, PacketFlavorEnum, Plaintext},
};
-use strum::IntoEnumIterator;
/// Discriminant for `V0DataElement`.
#[repr(u8)]
@@ -35,6 +36,14 @@
/// The associated payload may be obtained via
/// `V0DataElement#into_actions`.
Actions = 2,
+ /// The PSM data-element.
+ /// The associated payload may be obtained via
+ /// `V0DataElement#into_psm`.
+ Psm = 3,
+ /// The DeviceInfo data-element.
+ /// The associated payload may be obtained via
+ /// 'V0DataElement#into_device_info`.
+ DeviceInfo = 4,
}
/// Representation of a V0 data element.
@@ -44,6 +53,8 @@
pub enum V0DataElement {
TxPower(TxPower),
Actions(V0Actions),
+ Psm(Psm),
+ DeviceInfo(DeviceInfo),
}
impl TryFrom<V0DataElement> for np_adv_dynamic::legacy::ToBoxedSerializeDataElement {
@@ -52,6 +63,8 @@
match de {
V0DataElement::TxPower(x) => x.try_into(),
V0DataElement::Actions(x) => x.try_into(),
+ V0DataElement::Psm(x) => x.try_into(),
+ V0DataElement::DeviceInfo(x) => x.try_into(),
}
}
}
@@ -64,6 +77,8 @@
match de {
DeserializedDataElement::Actions(x) => V0DataElement::Actions(x.into()),
DeserializedDataElement::TxPower(x) => V0DataElement::TxPower(x.into()),
+ DeserializedDataElement::Psm(x) => V0DataElement::Psm(x.into()),
+ DeserializedDataElement::DeviceInfo(x) => V0DataElement::DeviceInfo(x.into()),
}
}
}
@@ -74,6 +89,8 @@
match self {
V0DataElement::Actions(_) => V0DataElementKind::Actions,
V0DataElement::TxPower(_) => V0DataElementKind::TxPower,
+ V0DataElement::Psm(_) => V0DataElementKind::Psm,
+ V0DataElement::DeviceInfo(_) => V0DataElementKind::DeviceInfo,
}
}
}
@@ -81,6 +98,8 @@
impl V0DataElement {
declare_enum_cast! {into_tx_power, TxPower, TxPower}
declare_enum_cast! {into_actions, Actions, V0Actions}
+ declare_enum_cast! {into_psm, Psm, Psm}
+ declare_enum_cast! {into_device_info, DeviceInfo, DeviceInfo}
}
impl From<np_adv_de::tx_power::TxPowerDataElement> for TxPower {
@@ -89,6 +108,32 @@
}
}
+impl From<np_adv_de::psm::PsmDataElement> for Psm {
+ fn from(de: np_adv_de::psm::PsmDataElement) -> Self {
+ Self { value: de.value }
+ }
+}
+
+impl From<np_adv_de::device_info::DeviceInfoDataElement> for DeviceInfo {
+ fn from(de: np_adv_de::device_info::DeviceInfoDataElement) -> Self {
+ let name_slice = de.device_info.device_name();
+ let mut name_bytes = [0; 9];
+ let len = name_slice.len().min(name_bytes.len());
+ #[allow(clippy::expect_used)]
+ name_bytes
+ .get_mut(..len)
+ .expect("slice length is within bounds")
+ .copy_from_slice(name_slice.get(..len).expect("slice length is within bounds"));
+ let device_name =
+ OptionalByteBuffer::Present(ByteBuffer { len: len as u8, bytes: name_bytes });
+ Self {
+ device_type: de.device_info.device_type() as u8,
+ is_name_truncated: de.device_info.name_truncated(),
+ device_name,
+ }
+ }
+}
+
/// Representation of the Actions DE in V0.
#[derive(Clone, Copy)]
#[repr(C)]
@@ -137,18 +182,12 @@
}
}
-#[derive(Clone, Copy, strum_macros::EnumIter)]
+#[derive(Clone, Copy)]
#[allow(missing_docs)]
-#[repr(u8)]
-/// The possible boolean action types which can be present in an Actions data element
-pub enum ActionType {
- CrossDevSdk = 1,
- CallTransfer = 4,
- ActiveUnlock = 8,
- NearbyShare = 9,
- InstantTethering = 10,
- PhoneHub = 11,
-}
+#[repr(C)]
+/// Action data element
+pub struct ActionType(u8);
+const MAX_V0_ACTION_ID: u8 = 31;
#[derive(Clone, Copy, Debug)]
/// The given int is out of range for conversion.
@@ -160,11 +199,49 @@
}
}
+/// Discriminant for `BuildActionTypeResult`.
+#[repr(u8)]
+#[derive(Clone, Copy)]
+pub enum BuildActionTypeResultKind {
+ /// The action type was outside the
+ /// allowed 0 to 31 range.
+ OutOfRange = 0,
+ /// The transmission power was in range,
+ /// and so a `TxPower` struct was constructed.
+ Success = 1,
+}
+
+/// Result type for attempting to construct a
+/// ActionType from an usigned byte.
+#[repr(C)]
+#[allow(missing_docs)]
+pub enum BuildActionTypeResult {
+ OutOfRange,
+ Success(ActionType),
+}
+
+impl FfiEnum for BuildActionTypeResult {
+ type Kind = BuildActionTypeResultKind;
+ fn kind(&self) -> Self::Kind {
+ match self {
+ Self::OutOfRange => BuildActionTypeResultKind::OutOfRange,
+ Self::Success(_) => BuildActionTypeResultKind::Success,
+ }
+ }
+}
+
+impl BuildActionTypeResult {
+ declare_enum_cast! {into_success, Success, ActionType}
+}
+
impl TryFrom<u8> for ActionType {
type Error = TryFromIntError;
fn try_from(n: u8) -> Result<Self, Self::Error> {
- // cast is safe since it's a repr(u8) unit enum
- Self::iter().find(|t| *t as u8 == n).ok_or(TryFromIntError)
+ if n > MAX_V0_ACTION_ID {
+ Err(TryFromIntError)
+ } else {
+ Ok(ActionType(n))
+ }
}
}
@@ -174,51 +251,33 @@
value: bool,
) -> np_adv_dynamic::legacy::ToBoxedActionElement {
use np_adv_dynamic::legacy::ToBoxedActionElement;
- match self {
- Self::CrossDevSdk => ToBoxedActionElement::CrossDevSdk(value),
- Self::CallTransfer => ToBoxedActionElement::CallTransfer(value),
- Self::ActiveUnlock => ToBoxedActionElement::ActiveUnlock(value),
- Self::NearbyShare => ToBoxedActionElement::NearbyShare(value),
- Self::InstantTethering => ToBoxedActionElement::InstantTethering(value),
- Self::PhoneHub => ToBoxedActionElement::PhoneHub(value),
+ ToBoxedActionElement { action: self.0, value }
+ }
+
+ /// Constructs a new ActionType from the given unsigned byte value.
+ pub fn build_from_unsigned_byte(action_type: u8) -> BuildActionTypeResult {
+ match action_type.try_into() {
+ Ok(action_type) => BuildActionTypeResult::Success(action_type),
+ Err(_) => BuildActionTypeResult::OutOfRange,
}
}
+
+ /// Returns the ActionType value.
+ pub fn as_u8(&self) -> u8 {
+ self.0
+ }
}
impl From<ActionType> for np_adv::legacy::data_elements::actions::ActionType {
fn from(value: ActionType) -> Self {
- match value {
- ActionType::CrossDevSdk => {
- np_adv::legacy::data_elements::actions::ActionType::CrossDevSdk
- }
- ActionType::CallTransfer => {
- np_adv::legacy::data_elements::actions::ActionType::CallTransfer
- }
- ActionType::ActiveUnlock => {
- np_adv::legacy::data_elements::actions::ActionType::ActiveUnlock
- }
- ActionType::NearbyShare => {
- np_adv::legacy::data_elements::actions::ActionType::NearbyShare
- }
- ActionType::InstantTethering => {
- np_adv::legacy::data_elements::actions::ActionType::InstantTethering
- }
- ActionType::PhoneHub => np_adv::legacy::data_elements::actions::ActionType::PhoneHub,
- }
+ np_adv::legacy::data_elements::actions::ActionType(value.0)
}
}
// ensure bidirectional mapping
impl From<np_adv::legacy::data_elements::actions::ActionType> for ActionType {
fn from(value: np_adv_de::actions::ActionType) -> Self {
- match value {
- np_adv_de::actions::ActionType::CrossDevSdk => ActionType::CrossDevSdk,
- np_adv_de::actions::ActionType::CallTransfer => ActionType::CallTransfer,
- np_adv_de::actions::ActionType::ActiveUnlock => ActionType::ActiveUnlock,
- np_adv_de::actions::ActionType::NearbyShare => ActionType::NearbyShare,
- np_adv_de::actions::ActionType::InstantTethering => ActionType::InstantTethering,
- np_adv_de::actions::ActionType::PhoneHub => ActionType::PhoneHub,
- }
+ ActionType(value.0)
}
}
@@ -239,19 +298,15 @@
fn try_from(actions: V0Actions) -> Result<Self, InvalidStackDataStructure> {
match actions {
V0Actions::Plaintext(action_bits) => {
- let bits =
- np_adv::legacy::data_elements::actions::ActionBits::<Plaintext>::try_from(
- action_bits.bitfield,
- )
- .map_err(|_| InvalidStackDataStructure)?;
+ let bits = np_adv::legacy::data_elements::actions::ActionBits::<Plaintext>::from(
+ action_bits.bitfield,
+ );
Ok(bits.into())
}
V0Actions::Encrypted(action_bits) => {
- let bits =
- np_adv::legacy::data_elements::actions::ActionBits::<Ciphertext>::try_from(
- action_bits.bitfield,
- )
- .map_err(|_| InvalidStackDataStructure)?;
+ let bits = np_adv::legacy::data_elements::actions::ActionBits::<Ciphertext>::from(
+ action_bits.bitfield,
+ );
Ok(bits.into())
}
}
diff --git a/nearby/presence/np_ffi_core/src/v1.rs b/nearby/presence/np_ffi_core/src/v1.rs
index 14f07eb..53d31fa 100644
--- a/nearby/presence/np_ffi_core/src/v1.rs
+++ b/nearby/presence/np_ffi_core/src/v1.rs
@@ -16,7 +16,8 @@
//! serialization+deserialization flows.
use crate::common::{
- ByteBuffer, FixedSizeArray, InvalidStackDataStructure, OptionalFixedSizeArray, TxPower,
+ ByteBuffer, DeviceInfo, FixedSizeArray, InvalidStackDataStructure, OptionalFixedSizeArray,
+ TxPower,
};
use crate::utils::FfiEnum;
@@ -129,6 +130,8 @@
Capabilities = 9,
/// A Requirements DE.
Requirements = 10,
+ /// A Device Info DE.
+ DeviceInfo = 11,
}
/// A V1 data element, which may be a "generic" data element
@@ -149,6 +152,7 @@
MediaDeduplicationId(MediaDeduplicationId),
Capabilities(Capabilities),
Requirements(Requirements),
+ DeviceInfo(DeviceInfo),
}
impl FfiEnum for V1DataElement {
@@ -166,6 +170,7 @@
Self::MediaDeduplicationId(_) => V1DataElementKind::MediaDeduplicationId,
Self::Capabilities(_) => V1DataElementKind::Capabilities,
Self::Requirements(_) => V1DataElementKind::Requirements,
+ Self::DeviceInfo(_) => V1DataElementKind::DeviceInfo,
}
}
}
@@ -182,6 +187,7 @@
declare_enum_cast! {into_media_deduplication_id, MediaDeduplicationId, MediaDeduplicationId}
declare_enum_cast! {into_capabilities, Capabilities, Capabilities}
declare_enum_cast! {into_requirements, Requirements, Requirements}
+ declare_enum_cast! {into_device_info, DeviceInfo, DeviceInfo}
}
/// Represents the contents of a V1 Actions DE as a bitmap
@@ -420,11 +426,12 @@
}
/// An optional IPv4 or IPv6 address, represented by big-endian bytes.
+#[derive(Clone, Default)]
#[repr(C)]
#[cfg_attr(any(test, feature = "testing"), derive(Debug, PartialEq, Eq, arbitrary::Arbitrary))]
-#[derive(Clone)]
pub enum OptionalIpAddress {
/// No IP address specified.
+ #[default]
NotPresent,
/// An IPv4 address (4 bytes).
IPv4(FixedSizeArray<4>),
@@ -432,12 +439,6 @@
IPv6(FixedSizeArray<16>),
}
-impl Default for OptionalIpAddress {
- fn default() -> Self {
- Self::NotPresent
- }
-}
-
impl FfiEnum for OptionalIpAddress {
type Kind = OptionalIpAddressKind;
fn kind(&self) -> Self::Kind {
diff --git a/nearby/presence/np_hkdf/src/v1_salt.rs b/nearby/presence/np_hkdf/src/v1_salt.rs
index dd4d44e..8f062a8 100644
--- a/nearby/presence/np_hkdf/src/v1_salt.rs
+++ b/nearby/presence/np_hkdf/src/v1_salt.rs
@@ -153,6 +153,7 @@
impl DeType {
/// A `const` equivalent to `From<u32>` since trait methods can't yet be const.
+ #[allow(clippy::panic)]
pub const fn const_from(value: u32) -> Self {
if value == FORBIDDEN_DE_TYPE_CODE {
panic!("Invalid DeType.");
diff --git a/nearby/presence/np_java_ffi/Cargo.toml b/nearby/presence/np_java_ffi/Cargo.toml
index c16073d..db2b9af 100644
--- a/nearby/presence/np_java_ffi/Cargo.toml
+++ b/nearby/presence/np_java_ffi/Cargo.toml
@@ -12,6 +12,8 @@
# The testing feature enables test-only APIs that implement native methods in
# the test/ library.
testing = ["np_ffi_core/testing"]
+rustcrypto = ["np_ffi_core/rustcrypto"]
+boringssl = ["np_ffi_core/boringssl"]
[dependencies]
array_view.workspace = true
diff --git a/nearby/presence/np_java_ffi/build.gradle.kts b/nearby/presence/np_java_ffi/build.gradle.kts
index 40f3845..e7d40c3 100644
--- a/nearby/presence/np_java_ffi/build.gradle.kts
+++ b/nearby/presence/np_java_ffi/build.gradle.kts
@@ -14,74 +14,65 @@
* limitations under the License.
*/
-import net.ltgt.gradle.errorprone.errorprone;
+import net.ltgt.gradle.errorprone.errorprone
plugins {
- `java-library`
- // For static analysis
- id("net.ltgt.errorprone") version "4.0.0"
+ `java-library`
+ // For static analysis
+ id("net.ltgt.errorprone") version "4.3.0"
}
java {
- // Gradle JUnit test finder doesn't support Java 21 class files.
- sourceCompatibility = JavaVersion.VERSION_1_9
- targetCompatibility = JavaVersion.VERSION_1_9
+ // Gradle JUnit test finder doesn't support Java 21 class files.
+ sourceCompatibility = JavaVersion.VERSION_1_9
+ targetCompatibility = JavaVersion.VERSION_1_9
}
repositories {
- mavenCentral()
- google()
+ mavenCentral()
+ google()
}
dependencies {
- // Static analysis annnotations
- implementation("androidx.annotation:annotation:1.6.0")
- implementation("com.google.errorprone:error_prone_core:2.28.0")
- implementation("org.checkerframework:checker-qual:3.45.0")
+ // Static analysis annnotations
+ implementation("androidx.annotation:annotation:1.6.0")
+ implementation("com.google.errorprone:error_prone_annotations:2.41.0")
+ errorprone("com.google.errorprone:error_prone_core:2.41.0")
+ implementation("org.checkerframework:checker-qual:3.45.0")
+ implementation("com.google.guava:failureaccess:1.0.3")
- // Local dependencies
- implementation(project(":cooperative_cleaner"))
+ // Local dependencies
+ implementation(project(":cooperative_cleaner"))
- // JUnit Test Support
- testImplementation("junit:junit:4.13")
- testImplementation("com.google.truth:truth:1.1.4")
- testImplementation("com.google.code.gson:gson:2.10.1")
- testImplementation("org.mockito:mockito-core:5.+")
+ // JUnit Test Support
+ testImplementation("junit:junit:4.13")
+ testImplementation("com.google.truth:truth:1.1.4")
+ testImplementation("com.google.code.gson:gson:2.10.1")
+ testImplementation("org.mockito:mockito-core:5.+")
}
// Flattened directory layout
sourceSets {
- main {
- java {
- setSrcDirs(listOf("java"))
- }
- }
- test {
- java {
- setSrcDirs(listOf("test"))
- }
- }
+ main { java { setSrcDirs(listOf("java")) } }
+ test { java { setSrcDirs(listOf("test")) } }
}
tasks.test {
- useJUnit()
- jvmArgs = mutableListOf(
- // libnp_java_ffi.so
- // This is the Cargo workspace's build output directory
- "-Djava.library.path=$projectDir/../../target/debug",
- // ByteBuddy agent for mocks
- "-XX:+EnableDynamicAgentLoading"
- )
+ useJUnit()
+ jvmArgs =
+ mutableListOf(
+ // libnp_java_ffi.so
+ // This is the Cargo workspace's build output directory
+ "-Djava.library.path=$projectDir/../../target/debug",
+ // ByteBuddy agent for mocks
+ "-XX:+EnableDynamicAgentLoading"
+ )
}
buildscript {
- repositories {
- mavenCentral()
- }
+ repositories { mavenCentral() }
dependencies {
- classpath("com.guardsquare:proguard-gradle:7.7.0") {
- exclude("com.android.tools.build")
- }
+ classpath("com.guardsquare:proguard-gradle:7.7.0") { exclude("com.android.tools.build") }
}
}
@@ -112,38 +103,39 @@
}
tasks.register<Test>("proguardedTest") {
- useJUnit()
- jvmArgs = mutableListOf(
- // libnp_java_ffi.so
- // This is the Cargo workspace's build output directory
- "-Djava.library.path=$projectDir/../../target/debug",
- // ByteBuddy agent for mocks
- "-XX:+EnableDynamicAgentLoading"
- )
- // Ignore the usual built classes
- classpath -= files("build/classes/java/main")
- classpath -= files("build/classes/java/test")
- // Use the proguarded classes instead
- classpath += files("build/libs/unzippedproguardedtests")
- dependsOn("unzipProguardedTestsJar")
- // Exclude test-vector checks, since that
- // uses reflection, which won't play nice with proguard.
- exclude("**/*TestVectors*")
+ useJUnit()
+ jvmArgs =
+ mutableListOf(
+ // libnp_java_ffi.so
+ // This is the Cargo workspace's build output directory
+ "-Djava.library.path=$projectDir/../../target/debug",
+ // ByteBuddy agent for mocks
+ "-XX:+EnableDynamicAgentLoading"
+ )
+ // Ignore the usual built classes
+ classpath -= files("build/classes/java/main")
+ classpath -= files("build/classes/java/test")
+ // Use the proguarded classes instead
+ classpath += files("build/libs/unzippedproguardedtests")
+ dependsOn("unzipProguardedTestsJar")
+ // Exclude test-vector checks, since that
+ // uses reflection, which won't play nice with proguard.
+ exclude("**/*TestVectors*")
}
tasks.withType<JavaCompile>().configureEach {
- // Configure static analysis passes to match g3 if possible
- options.errorprone {
- error("CheckReturnValue")
- error("UnnecessaryStaticImport")
- error("WildcardImport")
- error("RemoveUnusedImports")
- error("ReturnMissingNullable")
- error("FieldMissingNullable")
- error("AnnotationPosition")
- error("CheckedExceptionNotThrown")
- error("NonFinalStaticField")
- error("InvalidLink")
- error("ThrowsUncheckedException")
- }
+ // Configure static analysis passes to match g3 if possible
+ options.errorprone {
+ error("CheckReturnValue")
+ error("UnnecessaryStaticImport")
+ error("WildcardImport")
+ error("RemoveUnusedImports")
+ error("ReturnMissingNullable")
+ error("FieldMissingNullable")
+ error("AnnotationPosition")
+ error("CheckedExceptionNotThrown")
+ error("NonFinalStaticField")
+ error("InvalidLink")
+ error("ThrowsUncheckedException")
+ }
}
diff --git a/nearby/presence/np_java_ffi/gradle/wrapper/gradle-wrapper.jar b/nearby/presence/np_java_ffi/gradle/wrapper/gradle-wrapper.jar
index 943f0cb..2c35211 100644
--- a/nearby/presence/np_java_ffi/gradle/wrapper/gradle-wrapper.jar
+++ b/nearby/presence/np_java_ffi/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/nearby/presence/np_java_ffi/gradle/wrapper/gradle-wrapper.properties b/nearby/presence/np_java_ffi/gradle/wrapper/gradle-wrapper.properties
index 171d876..bad7c24 100644
--- a/nearby/presence/np_java_ffi/gradle/wrapper/gradle-wrapper.properties
+++ b/nearby/presence/np_java_ffi/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip
networkTimeout=10000
+validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/nearby/presence/np_java_ffi/gradlew b/nearby/presence/np_java_ffi/gradlew
index 79a61d4..f5feea6 100755
--- a/nearby/presence/np_java_ffi/gradlew
+++ b/nearby/presence/np_java_ffi/gradlew
@@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
+# SPDX-License-Identifier: Apache-2.0
+#
##############################################################################
#
@@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
-# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -83,10 +85,9 @@
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
-APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
-
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
+' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -133,10 +134,13 @@
fi
else
JAVACMD=java
- which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
+ fi
fi
# Increase the maximum file descriptors if we can.
@@ -144,7 +148,7 @@
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
- # shellcheck disable=SC3045
+ # shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
@@ -152,7 +156,7 @@
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
- # shellcheck disable=SC3045
+ # shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@@ -197,11 +201,15 @@
done
fi
-# Collect all arguments for the java command;
-# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
-# shell script including quotes and variable substitutions, so put them in
-# double quotes to make sure that they get re-expanded; and
-# * put everything else in single quotes, so that it's not re-expanded.
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
diff --git a/nearby/presence/np_java_ffi/gradlew.bat b/nearby/presence/np_java_ffi/gradlew.bat
new file mode 100644
index 0000000..9b42019
--- /dev/null
+++ b/nearby/presence/np_java_ffi/gradlew.bat
@@ -0,0 +1,94 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/V0AdvertisementBuilder.java b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/V0AdvertisementBuilder.java
index b707df4..569f3d5 100644
--- a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/V0AdvertisementBuilder.java
+++ b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/V0AdvertisementBuilder.java
@@ -19,6 +19,7 @@
import com.google.android.cooperativecleaner.CooperativeCleaner;
import com.google.android.nearby.presence.rust.SerializationException.InsufficientSpaceException;
import com.google.android.nearby.presence.rust.SerializationException.InvalidDataElementException;
+import com.google.android.nearby.presence.rust.V0DataElement.Psm;
import com.google.android.nearby.presence.rust.V0DataElement.TxPower;
import com.google.android.nearby.presence.rust.V0DataElement.V0Actions;
import com.google.android.nearby.presence.rust.credential.V0BroadcastCredential;
@@ -118,6 +119,15 @@
}
@Override
+ public void visitPsm(Psm psm) {
+ try {
+ nativeAddPsmDataElement(psm);
+ } catch (InsufficientSpaceException ise) {
+ throw new SmuggledInsufficientSpaceException(ise);
+ }
+ }
+
+ @Override
public void visitV0Actions(V0Actions actions) {
try {
nativeAddV0ActionsDataElement(actions);
@@ -166,6 +176,8 @@
private native void nativeAddTxPowerDataElement(TxPower txPower)
throws InsufficientSpaceException;
+ private native void nativeAddPsmDataElement(Psm psm) throws InsufficientSpaceException;
+
private native void nativeAddV0ActionsDataElement(V0Actions v0Actions)
throws InsufficientSpaceException;
diff --git a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/V0DataElement.java b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/V0DataElement.java
index 2c3c4ed..d1e80fa 100644
--- a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/V0DataElement.java
+++ b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/V0DataElement.java
@@ -31,6 +31,8 @@
public interface Visitor {
default void visitTxPower(TxPower txPower) {}
+ default void visitPsm(Psm psm) {}
+
default void visitV0Actions(V0Actions v0Actions) {}
}
@@ -65,6 +67,31 @@
}
}
+ /** Contains the PSM information. See the spec for more information. */
+ @UsedByNative
+ public static final class Psm extends V0DataElement {
+ @UsedByNative private final int psm;
+
+ public Psm(NpAdv npAdv, int psm) {
+ this(psm);
+ npAdv.ensureLoaded();
+ }
+
+ @UsedByNative
+ private Psm(int psm) {
+ this.psm = psm;
+ }
+
+ public int getPsm() {
+ return psm;
+ }
+
+ @Override
+ public void visit(Visitor v) {
+ v.visitPsm(this);
+ }
+ }
+
/** Marker annotation/enum for V0 action values. */
@IntDef({
V0ActionType.CROSS_DEV_SDK,
diff --git a/nearby/presence/np_java_ffi/src/class/v0_advertisement_builder.rs b/nearby/presence/np_java_ffi/src/class/v0_advertisement_builder.rs
index 96d56ec..b07300f 100644
--- a/nearby/presence/np_java_ffi/src/class/v0_advertisement_builder.rs
+++ b/nearby/presence/np_java_ffi/src/class/v0_advertisement_builder.rs
@@ -12,12 +12,18 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+use crate::class::{
+ v0_data_element::{Psm, TxPower, V0Actions},
+ InsufficientSpaceException, InvalidDataElementException, InvalidHandleException,
+ LdtEncryptionException, NoSpaceLeftException, UnencryptedSizeException, V0BroadcastCredential,
+};
use handle_map::{Handle, HandleLike};
use jni::{
objects::{JByteArray, JClass, JObject},
sys::jlong,
JNIEnv,
};
+use np_ffi_core::common::BuildPsmResult;
use np_ffi_core::{
common::BuildTxPowerResult,
serialize::v0::{
@@ -29,12 +35,6 @@
};
use pourover::{desc::ClassDesc, jni_method};
-use crate::class::{
- v0_data_element::{TxPower, V0Actions},
- InsufficientSpaceException, InvalidDataElementException, InvalidHandleException,
- LdtEncryptionException, NoSpaceLeftException, UnencryptedSizeException, V0BroadcastCredential,
-};
-
static V0_BUILDER_HANDLE_CLASS: ClassDesc = ClassDesc::new(
"com/google/android/nearby/presence/rust/V0AdvertisementBuilder$V0BuilderHandle",
);
@@ -180,6 +180,36 @@
#[jni_method(
package = "com.google.android.nearby.presence.rust",
class = "V0AdvertisementBuilder.V0BuilderHandle",
+ method_name = "nativeAddPsmDataElement"
+)]
+extern "system" fn add_psm_de<'local>(
+ mut env: JNIEnv<'local>,
+ this: V0BuilderHandle<JObject<'local>>,
+ psm: Psm<JObject<'local>>,
+) {
+ let psm = match psm.get_as_core(&mut env) {
+ Ok(BuildPsmResult::Success(psm)) => psm,
+ Ok(BuildPsmResult::OutOfRange) => {
+ let _ = env
+ .new_string("PSM value out of range")
+ .map(|string| env.auto_local(string))
+ .and_then(|string| InvalidDataElementException::throw_new(&mut env, &string));
+ return;
+ }
+ Err(_jni_err) => {
+ // `crate jni` should have already thrown
+ return;
+ }
+ };
+ let Ok(()) = this.add_de(&mut env, V0DataElement::Psm(psm)) else {
+ // `crate jni` should have already thrown
+ return;
+ };
+}
+
+#[jni_method(
+ package = "com.google.android.nearby.presence.rust",
+ class = "V0AdvertisementBuilder.V0BuilderHandle",
method_name = "nativeAddV0ActionsDataElement"
)]
extern "system" fn add_v0_actions_de<'local>(
diff --git a/nearby/presence/np_java_ffi/src/class/v0_data_element.rs b/nearby/presence/np_java_ffi/src/class/v0_data_element.rs
index 4082406..d944306 100644
--- a/nearby/presence/np_java_ffi/src/class/v0_data_element.rs
+++ b/nearby/presence/np_java_ffi/src/class/v0_data_element.rs
@@ -69,6 +69,41 @@
}
}
+static PSM_CLASS: ClassDesc =
+ ClassDesc::new("com/google/android/nearby/presence/rust/V0DataElement$Psm");
+
+/// Rust representation of `class V0DataElement.Psm`.
+#[repr(transparent)]
+pub struct Psm<Obj>(pub Obj);
+
+impl<'local> Psm<JObject<'local>> {
+ /// Create a new PSM date element with the given `psm`.
+ pub fn construct(env: &mut JNIEnv<'local>, psm: jint) -> jni::errors::Result<Self> {
+ pourover::call_constructor!(env, &PSM_CLASS, "(I)V", psm).map(Self)
+ }
+}
+
+impl<'local, Obj: AsRef<JObject<'local>>> Psm<Obj> {
+ /// Gets the value of the `int txPower` field.
+ pub fn get_psm<'env_local>(&self, env: &mut JNIEnv<'env_local>) -> jni::errors::Result<jint> {
+ static PSM_FIELD: FieldDesc = PSM_CLASS.field("psm", "I");
+ env.get_field_unchecked(self.0.as_ref(), &PSM_FIELD, ReturnType::Primitive(Primitive::Int))
+ .and_then(|ret| ret.i())
+ }
+
+ /// Get the `np_ffi_core` representation of this data element.
+ pub fn get_as_core<'env>(
+ &self,
+ env: &mut JNIEnv<'env>,
+ ) -> jni::errors::Result<common::BuildPsmResult> {
+ let psm = self.get_psm(env)?;
+ let Ok(value) = u16::try_from(psm) else {
+ return Ok(common::BuildPsmResult::OutOfRange);
+ };
+ Ok(common::Psm::build_from_unsigned_bytes(value))
+ }
+}
+
static V0_ACTIONS_CLASS: ClassDesc =
ClassDesc::new("com/google/android/nearby/presence/rust/V0DataElement$V0Actions");
diff --git a/nearby/presence/np_java_ffi/src/class/v0_payload.rs b/nearby/presence/np_java_ffi/src/class/v0_payload.rs
index ec6af1d..43a63f7 100644
--- a/nearby/presence/np_java_ffi/src/class/v0_payload.rs
+++ b/nearby/presence/np_java_ffi/src/class/v0_payload.rs
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-use crate::class::v0_data_element::{TxPower, V0Actions};
+use crate::class::v0_data_element::{Psm, TxPower, V0Actions};
use handle_map::{Handle, HandleLike};
use jni::{
objects::{JClass, JObject},
@@ -85,12 +85,13 @@
use np_ffi_core::{
deserialize::v0::GetV0DEResult::{Error, Success},
- v0::V0DataElement::{Actions, TxPower as TxPow},
+ v0::V0DataElement::{Actions, DeviceInfo, Psm as Ps, TxPower as TxPow},
};
let ret = match v0_payload.get_de(index) {
Success(TxPow(tx_power)) => {
TxPower::construct(&mut env, jint::from(tx_power.as_i8())).map(|obj| obj.0)
}
+ Success(Ps(psm)) => Psm::construct(&mut env, jint::from(psm.as_u16())).map(|obj| obj.0),
Success(Actions(actions)) => {
let identity_kind = match &actions {
CoreV0Actions::Plaintext(_) => DeserializedV0IdentityKind::Plaintext,
@@ -99,6 +100,10 @@
V0Actions::construct(&mut env, identity_kind, actions.as_u32() as jint).map(|obj| obj.0)
}
+ Success(DeviceInfo(_)) => {
+ // TODO: implement device info data element for Java binding.
+ return JObject::null();
+ }
Error => {
return JObject::null();
}
diff --git a/nearby/presence/np_java_ffi/src/class/v1_data_element.rs b/nearby/presence/np_java_ffi/src/class/v1_data_element.rs
index 1e77e68..3f83316 100644
--- a/nearby/presence/np_java_ffi/src/class/v1_data_element.rs
+++ b/nearby/presence/np_java_ffi/src/class/v1_data_element.rs
@@ -221,6 +221,10 @@
V1DataElement::Requirements(requirements) => {
Requirements::construct(env, requirements).map(|x| Some(x.0))
}
+ V1DataElement::DeviceInfo(_) => {
+ // TODO: implement device info.
+ Ok(Some(JObject::null()))
+ }
};
match result {
Ok(Some(result)) => result,
@@ -315,6 +319,9 @@
V1DataElementDeserializationResult::RequirementsMalformed => {
let _ = RequirementsMalformedException::throw_new(&mut env);
}
+ V1DataElementDeserializationResult::DeviceInfoMalformed => {
+ // TODO: implement handling of device info malformed.
+ }
}
// Catch-all return value for thrown errors.
JObject::null()
diff --git a/nearby/presence/np_java_ffi/test/com/google/android/nearby/presence/rust/V0DataElementTests.java b/nearby/presence/np_java_ffi/test/com/google/android/nearby/presence/rust/V0DataElementTests.java
new file mode 100644
index 0000000..6810d1d
--- /dev/null
+++ b/nearby/presence/np_java_ffi/test/com/google/android/nearby/presence/rust/V0DataElementTests.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.nearby.presence.rust;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.android.nearby.presence.rust.credential.CredentialBook;
+import com.google.android.nearby.presence.rust.credential.CredentialBook.NoMetadata;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class V0DataElementTests {
+
+ /** A test which verifies TxPower to be serializable and deserializable. */
+ @Test
+ public void txPower() throws Exception {
+ NpAdv npAdv = new NpAdv();
+ try (V0AdvertisementBuilder builder = npAdv.newPublicV0Builder()) {
+ V0DataElement.TxPower txPower = new V0DataElement.TxPower(npAdv, 1);
+ builder.addDataElement(txPower);
+ byte[] adv = builder.build();
+ assertThat(adv).isNotNull();
+
+ CredentialBook<NoMetadata> book = npAdv.newEmptyCredentialBook();
+ DeserializeResult<NoMetadata> result = npAdv.deserializeAdvertisement(adv, book);
+ DeserializedV0Advertisement<NoMetadata> deserialized = result.getAsV0();
+ V0DataElement de = deserialized.getDataElement(0);
+ assertThat(de).isInstanceOf(V0DataElement.TxPower.class);
+ }
+ }
+
+ /** A test which verifies PSM to be serializable and deserializable. */
+ @Test
+ public void psm() throws Exception {
+ NpAdv npAdv = new NpAdv();
+ try (V0AdvertisementBuilder builder = npAdv.newPublicV0Builder()) {
+ V0DataElement.Psm psm = new V0DataElement.Psm(npAdv, 1);
+ builder.addDataElement(psm);
+ byte[] adv = builder.build();
+ assertThat(adv).isNotNull();
+
+ CredentialBook<NoMetadata> book = npAdv.newEmptyCredentialBook();
+ DeserializeResult<NoMetadata> result = npAdv.deserializeAdvertisement(adv, book);
+ DeserializedV0Advertisement<NoMetadata> deserialized = result.getAsV0();
+ V0DataElement de = deserialized.getDataElement(0);
+ assertThat(de).isInstanceOf(V0DataElement.Psm.class);
+ assertThat(((V0DataElement.Psm) de).getPsm()).isEqualTo(1);
+ }
+ }
+}
diff --git a/nearby/presence/rand_ext/src/lib.rs b/nearby/presence/rand_ext/src/lib.rs
index 587279d..5f42f2b 100644
--- a/nearby/presence/rand_ext/src/lib.rs
+++ b/nearby/presence/rand_ext/src/lib.rs
@@ -56,6 +56,6 @@
let mut seed: <rand_pcg::Pcg64 as rand::SeedableRng>::Seed = Default::default();
rand::thread_rng().fill(&mut seed);
// print it out so if a test fails, the seed will be visible for further investigation
- info!("seed: {:?}", seed);
+ info!("seed: {seed:?}");
rand_pcg::Pcg64::from_seed(seed)
}
diff --git a/nearby/presence/test_helper/src/lib.rs b/nearby/presence/test_helper/src/lib.rs
index 42097a6..1bd9d34 100644
--- a/nearby/presence/test_helper/src/lib.rs
+++ b/nearby/presence/test_helper/src/lib.rs
@@ -75,5 +75,5 @@
/// ```
#[cfg(feature = "std")]
pub fn hex_bytes(data: impl AsRef<[u8]>) -> String {
- hex::encode_upper(data).chars().tuples().map(|(a, b)| format!("0x{}{}", a, b)).join(", ")
+ hex::encode_upper(data).chars().tuples().map(|(a, b)| format!("0x{a}{b}")).join(", ")
}
diff --git a/nearby/presence/xts_aes/tests/xts_nist_test_vectors.rs b/nearby/presence/xts_aes/tests/xts_nist_test_vectors.rs
index 9d806ad..f9ac4f6 100644
--- a/nearby/presence/xts_aes/tests/xts_nist_test_vectors.rs
+++ b/nearby/presence/xts_aes/tests/xts_nist_test_vectors.rs
@@ -199,6 +199,8 @@
fn next(&mut self) -> Option<Self::Item> {
let mut map = hash_map::HashMap::new();
+ let encrypt_decrypt_regex = regex::Regex::new("^\\[(.*)\\]$").unwrap();
+ let key_value_regex = regex::Regex::new("^(.*) = (.*)$").unwrap();
while let Some(line) = self.delegate.next().map(|l| l.trim().to_owned()) {
if line.starts_with('#') {
continue;
@@ -217,14 +219,14 @@
}
// look for `[ENCRYPT]` / `[DECRYPT]`
- if let Some(captures) = regex::Regex::new("^\\[(.*)\\]$").unwrap().captures(&line) {
+ if let Some(captures) = encrypt_decrypt_regex.captures(&line) {
return Some(ParseUnit::SectionHeader(
captures.get(1).unwrap().as_str().to_owned(),
));
}
// `key = value` in a test case chunk
- if let Some(captures) = regex::Regex::new("^(.*) = (.*)$").unwrap().captures(&line) {
+ if let Some(captures) = key_value_regex.captures(&line) {
let _ = map.insert(
captures.get(1).unwrap().as_str().to_owned(),
captures.get(2).unwrap().as_str().to_owned(),
diff --git a/remoteauth/rust-toolchain.toml b/remoteauth/rust-toolchain.toml
index 0193dee..e88baf1 100644
--- a/remoteauth/rust-toolchain.toml
+++ b/remoteauth/rust-toolchain.toml
@@ -1,2 +1,2 @@
[toolchain]
-channel = "1.83.0"
+channel = "1.88.0"