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"