From 3f08db9d6b9a823d5e61d7edad2a58a68f8437d6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Jul 2026 16:08:06 +0000 Subject: [PATCH 1/5] perf(build): size-tuned build config for workspace builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three levers, each measured on the stripped release demo (the binary-size CI metric), against the current main baseline of 11,590,552 bytes: - [profile.release.package.libsqlite3-sys] opt-level="z": the bundled sqlite3.c amalgamation was the one crate the per-package opt-level sweep missed — it compiled at -O3. Same I/O-bound tradeoff already accepted for diesel/r2d2/sqlite-storage. Measured -856 KiB. - -Zshare-generics=y (via .cargo/config.toml, linux-x64 scoped): stops per-crate re-instantiation of cross-crate generics that fat LTO keeps as distinct symbols; the Docker image build already ran with it. Measured -335 KiB. - lld with --icf=all (linux-x64 scoped): tighter layout of the fat-LTO object plus identical-section folding. Measured -73 KiB. - LIBSQLITE3_FLAGS trim: drops bundled-SQLite subsystems nothing in the workspace uses (FTS3, RTREE, STAT4, DBSTAT, SOUNDEX, JSON SQL, loadable extensions, shared cache, progress callbacks). FTS5 stays for chat-store's search feature. Consumers building whatsapp-rust from crates.io are unaffected — .cargo/config.toml and profile overrides only apply to workspace builds. Co-Authored-By: Claude --- .cargo/config.toml | 27 +++++++++++++++++++++++++++ .gitignore | 3 ++- Cargo.toml | 5 +++++ 3 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 .cargo/config.toml diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 000000000..eba998a45 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,27 @@ +# Workspace build configuration. Applies to builds of THIS repo only — +# crates.io consumers are unaffected (cargo does not inherit this file +# across package boundaries). + +[target.x86_64-unknown-linux-gnu] +# -Zshare-generics=y: downstream crates reuse upstream monomorphic +# instantiations instead of stamping per-crate copies with distinct symbol +# hashes that fat LTO cannot merge (measured -335 KiB on the stripped demo; +# the Docker image build already ran with this flag). Nightly-only, and the +# toolchain is pinned by rust-toolchain.toml. +# lld + ICF: lld lays out the fat-LTO object tighter than BFD ld and folds +# byte-identical sections (measured -73 KiB on the stripped demo). Scoped to +# this target so wasm and non-Linux builds keep their default linkers. +rustflags = [ + "-Zshare-generics=y", + "-Clink-arg=-fuse-ld=lld", + "-Clink-arg=-Wl,--icf=all", +] + +[env] +# Trim bundled-SQLite subsystems the workspace never touches. FTS5 stays: +# chat-store's `search` feature builds its index on it. JSON and extension +# loading are unused by diesel/sqlite-storage/chat-store (checked: no json_* +# SQL, no load_extension callers); dropping loadable extensions also removes +# a dlopen surface. STAT4/DBSTAT/RTREE/FTS3/SOUNDEX are upstream-default-off +# features libsqlite3-sys turns on; none are exercised here. +LIBSQLITE3_FLAGS = "-USQLITE_ENABLE_FTS3 -USQLITE_ENABLE_FTS3_PARENTHESIS -USQLITE_ENABLE_RTREE -USQLITE_ENABLE_STAT4 -USQLITE_ENABLE_DBSTAT_VTAB -USQLITE_SOUNDEX -DSQLITE_OMIT_JSON -DSQLITE_OMIT_LOAD_EXTENSION -DSQLITE_OMIT_DEPRECATED -DSQLITE_OMIT_PROGRESS_CALLBACK -DSQLITE_OMIT_SHARED_CACHE" diff --git a/.gitignore b/.gitignore index 335a7273e..5440f5dd0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,8 @@ /*.xml /*.txt /whatsapp_store -/.cargo +/.cargo/* +!/.cargo/config.toml /.vscode /*.db* .env diff --git a/Cargo.toml b/Cargo.toml index 5eecba464..4b227d21c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -301,6 +301,11 @@ opt-level = "s" opt-level = "s" [profile.release.package.serde_json] opt-level = "s" +# The bundled SQLite amalgamation (sqlite3.c) — cargo exports OPT_LEVEL to the +# build script and the cc crate maps it to -Oz. The C core was the one crate +# the per-package sweep above missed; same I/O-bound tradeoff as diesel. +[profile.release.package.libsqlite3-sys] +opt-level = "z" # Benchmark profile: release-grade codegen, but thin LTO keeps bench builds # fast (release uses fat LTO) and debug symbols stay for local profiling. From 588c50a5498f113fe7c4d448a61298aa4a6962b8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Jul 2026 16:14:44 +0000 Subject: [PATCH 2/5] perf(request): type-erase the per-IqSpec body of Client::execute MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit execute stamped the request-id generation, direct-encode branch, send/wait glue and error mapping once per IqSpec (~55 live specs). The generic shell now only encodes/builds/parses the spec; the shared body lives in non-generic helpers, so each spec adds a thin wrapper instead of a full copy. IQ dispatch is control-plane — no hot-path cost. Co-Authored-By: Claude --- src/request.rs | 76 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 51 insertions(+), 25 deletions(-) diff --git a/src/request.rs b/src/request.rs index 251b6b951..08a435e1a 100644 --- a/src/request.rs +++ b/src/request.rs @@ -45,6 +45,17 @@ impl Drop for ResponseWaiterGuard { } } +/// Outcome of the per-spec encode/build step in [`Client::execute`]. Owned (no +/// spec type parameter) so the send/wait tail behind it stays non-generic. +enum PreparedIq { + /// Fully binary-encoded stanza from `encode_iq_direct` (fast path). + Encoded(Vec), + /// Fallback: an `InfoQuery` from `build_iq`, still to be marshalled. + /// Boxed to keep the enum small (`InfoQuery` is ~200 bytes vs the + /// fast-path `Vec`'s 24) — one alloc per fallback IQ, control-plane only. + Query(Box>), +} + #[derive(Debug, Error)] #[non_exhaustive] pub enum IqError { @@ -262,36 +273,51 @@ impl Client { where S: wacore::iq::spec::IqSpec, { + // Only the three spec calls live in this generic body; the send/wait + // machinery sits behind the non-generic `execute_prepared` so it isn't + // re-stamped for every IqSpec instantiation (~55 of them). let req_id = self.generate_request_id(); + let mut buf = Vec::new(); + let prepared = match spec.encode_iq_direct(&req_id, &mut buf) { + Ok(true) => PreparedIq::Encoded(buf), + Ok(false) => PreparedIq::Query(Box::new(spec.build_iq())), + Err(e) => return Err(IqError::EncodeError(e)), + }; - // Direct-encode fast path: skip Node tree for hot IQ specs (e.g. PreKeyUploadSpec) - { - let mut buf = Vec::new(); - match spec.encode_iq_direct(&req_id, &mut buf) { - Ok(true) => { - let response = self - .send_and_wait_iq( - req_id, - Duration::from_secs(75), - Box::pin(async { self.send_raw_bytes(buf).await }), - ) - .await?; - return spec - .parse_response(response.get()) - .map_err(IqError::ParseError); + let response = self.execute_prepared(req_id, prepared).await?; + spec.parse_response(response.get()) + .map_err(IqError::ParseError) + } + + /// Non-generic tail of [`Client::execute`]: sends the already-prepared IQ + /// and waits for the response node. + async fn execute_prepared( + &self, + req_id: String, + prepared: PreparedIq, + ) -> Result, IqError> { + match prepared { + // Direct-encode fast path: skip the Node tree for hot IQ specs + // (e.g. PreKeyUploadSpec). Fixed 75s timeout — specs needing a + // custom timeout don't opt into this path. + PreparedIq::Encoded(buf) => { + self.send_and_wait_iq( + req_id, + Duration::from_secs(75), + Box::pin(async { self.send_raw_bytes(buf).await }), + ) + .await + } + PreparedIq::Query(iq) => { + let mut iq = *iq; + // Reuse the id already generated for the fast-path attempt so + // send_iq doesn't mint a second one. + if iq.id.is_none() { + iq.id = Some(req_id); } - Err(e) => return Err(IqError::EncodeError(e)), - Ok(false) => {} + self.send_iq(iq).await } } - - let mut iq = spec.build_iq(); - if iq.id.is_none() { - iq.id = Some(req_id); - } - let response = self.send_iq(iq).await?; - spec.parse_response(response.get()) - .map_err(IqError::ParseError) } /// Centralizes waiter registration and shutdown/timeout handling. From 24863b2bbd2c6ce63f72c6396415775ccbad3978 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Jul 2026 16:36:54 +0000 Subject: [PATCH 3/5] perf(size): finish codec pinning sweep, collapse control-plane generics, guard proto Debug MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four coordinated reductions, all off the per-message hot path: - waproto::codec sweep: every remaining production call site of buffa's generic Message methods on waproto types outside waproto now routes through the #[inline(never)] pinned helpers (17 new helpers added for the small roots: app-state key fingerprints, msmsg, event responses, reactions, shortcake, verified-name certs, noise cert chains, chat-store message codec). A clippy disallowed-methods guard now rejects new direct call sites; libsignal's signal-wire protos keep direct calls (sole instantiation site and per-message hot path — pinning buys nothing). - mex: execute_request splits into a thin per-V wrapper plus one non-generic execute body (was stamped per variables type). - appstate sync: the FDownload closure generic across eight wacore fns is now a &BlobDownloadFn trait object — one instantiation of the large patch-processing bodies (breaking for wacore consumers: parameter is now a dyn ref). - upload: the 8-type-parameter retry/failover driver takes boxed futures; media upload wraps whole HTTP transfers, so a BoxFuture per attempt is noise. - group IQs: the two IQ macros share one outlined build_iq body instead of stamping it into 8 generated types. - Debug guards: the one reachable {:?} of a waproto type (bot.rs device props override) logs fields instead, and Event's derived Debug is now a manual variant-name impl so no future log line can resurrect the 627-impl generated proto Debug graph. Co-Authored-By: Claude --- clippy.toml | 11 ++ src/appstate_sync.rs | 1 + src/bot.rs | 21 ++- src/client/device_registry.rs | 1 + src/client/lid_pn.rs | 1 + src/features/message_edit.rs | 1 + src/features/mex.rs | 6 + src/features/rotate_key.rs | 14 +- src/history_sync.rs | 1 + src/message/msg_secret.rs | 4 +- src/message/special.rs | 9 +- src/message/tests.rs | 3 + src/passkey/flow.rs | 1 + src/pdo.rs | 1 + src/prekeys.rs | 11 +- src/reexports_test.rs | 2 + src/send/mod.rs | 1 + src/store/signal.rs | 10 +- src/upload.rs | 53 ++++++-- storages/chat-store/src/materialize.rs | 5 +- storages/chat-store/src/queries.rs | 4 +- storages/chat-store/src/store.rs | 7 +- storages/chat-store/tests/chat_store_test.rs | 3 + storages/sqlite-storage/src/wire.rs | 4 + tests/handshake_integration.rs | 3 + wacore/appstate/src/decode.rs | 1 + wacore/appstate/src/processor.rs | 1 + wacore/benches/history_sync_benchmark.rs | 3 + wacore/benches/message_utils_benchmark.rs | 3 + wacore/benches/reporting_token_benchmark.rs | 3 + wacore/benches/send_receive_benchmark.rs | 3 + wacore/libsignal/src/protocol/identity_key.rs | 3 + wacore/libsignal/src/protocol/protocol.rs | 10 +- wacore/libsignal/src/protocol/sender_keys.rs | 1 + .../libsignal/src/protocol/state/session.rs | 5 +- wacore/noise/src/handshake.rs | 17 +-- wacore/noise/src/test_util.rs | 3 + wacore/noise/tests/cert_chain_verify.rs | 2 + wacore/src/adv.rs | 1 + wacore/src/appstate_sync.rs | 82 +++++------ wacore/src/companion_reg.rs | 1 + wacore/src/event.rs | 5 +- wacore/src/history_sync.rs | 1 + wacore/src/iq/groups.rs | 94 ++++++------- wacore/src/iq/usync.rs | 1 + wacore/src/media_retry.rs | 4 +- wacore/src/message_edit.rs | 1 + wacore/src/message_processing.rs | 1 + wacore/src/messages.rs | 2 + wacore/src/pair.rs | 1 + wacore/src/poll.rs | 1 + wacore/src/prekeys.rs | 1 + wacore/src/reaction.rs | 5 +- wacore/src/shortcake.rs | 26 ++-- wacore/src/stanza/business.rs | 10 +- wacore/src/store/device.rs | 1 + wacore/src/types/events.rs | 12 +- wacore/src/usync.rs | 1 + wacore/src/voip/mlow/smpl_tables_blob.rs | 4 + .../tests/appstate_external_mutations_test.rs | 3 + waproto/build.rs | 2 + waproto/src/lib.rs | 128 +++++++++++++++++- 62 files changed, 435 insertions(+), 186 deletions(-) diff --git a/clippy.toml b/clippy.toml index bf3c35e4c..e7e61bcba 100644 --- a/clippy.toml +++ b/clippy.toml @@ -3,6 +3,17 @@ disallowed-methods = [ { path = "chrono::Local::now", reason = "use wacore::time::now_utc() (Local depends on SystemTime which panics on WASM)" }, { path = "std::time::SystemTime::now", reason = "use wacore::time::now_millis() / now_utc()" }, { path = "std::time::Instant::now", reason = "use wacore::time::Instant::now()" }, + # buffa's Message codec methods are generic over the buffer type, so every + # calling crate stamps its own copy of the full encode/decode tree (LTO + # cannot merge them). Route waproto types through the pinned wrappers in + # waproto::codec — add one there if missing. Crate-local protos (their sole + # instantiation site) and tests carry a scoped allow instead. + { path = "buffa::message::Message::encode_to_vec", reason = "use waproto::codec::*_to_vec (pinned single instantiation; direct calls duplicate the encode tree per crate)" }, + { path = "buffa::message::Message::encode", reason = "use waproto::codec::*_encode_into (pinned single instantiation; direct calls duplicate the encode tree per crate)" }, + { path = "buffa::message::Message::write_to", reason = "use waproto::codec::*_write_to (pinned single instantiation; direct calls duplicate the encode tree per crate)" }, + { path = "buffa::message::Message::decode_from_slice", reason = "use waproto::codec::*_decode (pinned single instantiation; direct calls duplicate the decode tree per crate)" }, + { path = "buffa::message::Message::merge_from_slice", reason = "use waproto::codec::*_merge (pinned single instantiation; direct calls duplicate the decode tree per crate)" }, + { path = "buffa::message::Message::merge_to_limit", reason = "use a waproto::codec pinned wrapper (direct calls duplicate the decode tree per crate)" }, ] # 64-bit atomic *types*: 32-bit targets (Xtensa/ESP32) have no native AtomicU64/ diff --git a/src/appstate_sync.rs b/src/appstate_sync.rs index 84bd7e818..21d8895f0 100644 --- a/src/appstate_sync.rs +++ b/src/appstate_sync.rs @@ -3,6 +3,7 @@ pub use wacore::appstate::Mutation; pub use wacore::appstate_sync::{AppStateProcessor, AppStateSyncDriver, AppStateSyncError}; #[cfg(test)] +#[allow(clippy::disallowed_methods)] mod tests { use super::*; use async_lock::Mutex; diff --git a/src/bot.rs b/src/bot.rs index 82ccc4707..e6ea84984 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -1305,7 +1305,26 @@ impl BotBuilder { if let Some(override_) = self.device_props_override && !override_.is_empty() { - info!("Applying device props override: {:?}", override_); + // Field-by-field to avoid Debug-formatting waproto types (keeps their + // generated Debug impls out of the binary). + info!( + "Applying device props override: os={:?} version={:?} platform_type={:?} history_sync_config={}", + override_.os.as_deref(), + override_.version.as_ref().map(|v| { + format!( + "{}.{}.{}", + v.primary.unwrap_or(0), + v.secondary.unwrap_or(0), + v.tertiary.unwrap_or(0) + ) + }), + override_.platform_type.map(|p| p as i32), + if override_.history_sync_config.is_some() { + "overridden" + } else { + "default" + }, + ); persistence_manager .process_command(DeviceCommand::SetDeviceProps(override_)) .await; diff --git a/src/client/device_registry.rs b/src/client/device_registry.rs index 8cf648491..e0441f7d7 100644 --- a/src/client/device_registry.rs +++ b/src/client/device_registry.rs @@ -1029,6 +1029,7 @@ impl Client { } #[cfg(test)] +#[allow(clippy::disallowed_methods)] mod tests { use super::*; use crate::lid_pn_cache::LearningSource; diff --git a/src/client/lid_pn.rs b/src/client/lid_pn.rs index c7ec38f38..5b2343da2 100644 --- a/src/client/lid_pn.rs +++ b/src/client/lid_pn.rs @@ -1000,6 +1000,7 @@ impl Client { } #[cfg(test)] +#[allow(clippy::disallowed_methods)] mod tests { use super::*; use crate::lid_pn_cache::LearningSource; diff --git a/src/features/message_edit.rs b/src/features/message_edit.rs index 77f13f94e..58d0e57f6 100644 --- a/src/features/message_edit.rs +++ b/src/features/message_edit.rs @@ -536,6 +536,7 @@ pub fn decrypt_secret_encrypted_with_fallback( } #[cfg(test)] +#[allow(clippy::disallowed_methods)] mod tests { use super::*; use wacore::message_edit::encrypt_message_edit; diff --git a/src/features/mex.rs b/src/features/mex.rs index 864f5549a..9de9385b0 100644 --- a/src/features/mex.rs +++ b/src/features/mex.rs @@ -113,6 +113,7 @@ impl<'a> Mex<'a> { self.execute_request(request).await } + #[inline] async fn execute_request( &self, request: MexRequest, @@ -120,7 +121,12 @@ impl<'a> Mex<'a> { // Serialize the variables here so a caller-side serialization error // surfaces as MexError::Json instead of a malformed empty request. let spec = MexQuerySpec::new(request.doc, &request.variables)?; + self.execute_spec(spec).await + } + // Non-generic so the execute/error-handling body instantiates once, not + // per variables type. + async fn execute_spec(&self, spec: MexQuerySpec) -> Result { let response = self.client.execute(spec).await?; // Check for fatal errors (the IqSpec already checks, but we want to return our error type) diff --git a/src/features/rotate_key.rs b/src/features/rotate_key.rs index c314b82e9..3e05b518c 100644 --- a/src/features/rotate_key.rs +++ b/src/features/rotate_key.rs @@ -7,12 +7,10 @@ use crate::client::Client; use crate::request::IqError; -use buffa::Message; use wacore::iq::prekeys::RotateSignedPreKeySpec; use wacore::libsignal::protocol::{KeyPair, PrivateKey, PublicKey}; use wacore::libsignal::store::record_helpers::new_signed_pre_key_record; use wacore::store::commands::DeviceCommand; -use waproto::whatsapp::SignedPreKeyRecordStructure; /// Rotation cadence. This is the one value NOT grounded in the WA Web bundle /// (there it is a persisted background job with a server-tuned schedule), so @@ -110,7 +108,7 @@ impl Client { .map_err(|e| anyhow::anyhow!("failed to load staged signed pre-key: {e}"))? { Some(bytes) => { - let s = SignedPreKeyRecordStructure::decode_from_slice(&bytes) + let s = waproto::codec::signed_pre_key_record_decode(&bytes) .map_err(|e| anyhow::anyhow!("staged signed pre-key decode: {e}"))?; let public = PublicKey::from_djb_public_key_bytes( s.public_key @@ -143,7 +141,10 @@ impl Client { let record = new_signed_pre_key_record(new_id, &kp, signature, wacore::time::now_utc()); backend - .store_signed_prekey(new_id, &record.encode_to_vec()) + .store_signed_prekey( + new_id, + &waproto::codec::signed_pre_key_record_to_vec(&record), + ) .await .map_err(|e| anyhow::anyhow!("failed to stage new signed pre-key: {e}"))?; (kp, signature) @@ -161,7 +162,10 @@ impl Client { wacore::time::now_utc(), ); backend - .store_signed_prekey(old_id, &old_record.encode_to_vec()) + .store_signed_prekey( + old_id, + &waproto::codec::signed_pre_key_record_to_vec(&old_record), + ) .await .map_err(|e| anyhow::anyhow!("failed to retain old signed pre-key: {e}"))?; diff --git a/src/history_sync.rs b/src/history_sync.rs index a37cd2ef1..7a5a7a638 100644 --- a/src/history_sync.rs +++ b/src/history_sync.rs @@ -497,6 +497,7 @@ fn push_unique_sender(senders: &mut Vec, sender: Jid) { } #[cfg(test)] +#[allow(clippy::disallowed_methods)] mod tests { use super::*; use buffa::Message as ProtoMessage; diff --git a/src/message/msg_secret.rs b/src/message/msg_secret.rs index 72f4b89c2..2671d781c 100644 --- a/src/message/msg_secret.rs +++ b/src/message/msg_secret.rs @@ -464,12 +464,10 @@ impl Client { info: &Arc, payload: EncPayload, ) { - use buffa::Message as _; - use wa::MessageSecretMessage; use wacore::bot_message::{BotMessageContext, decrypt_bot_message}; use wacore::protocol::nack::NackReason; - let ms_msg = match MessageSecretMessage::decode_from_slice(&payload.ciphertext) { + let ms_msg = match waproto::codec::message_secret_message_decode(&payload.ciphertext) { Ok(m) => m, Err(e) => { log::warn!( diff --git a/src/message/special.rs b/src/message/special.rs index 3e6c6b053..d04da72fd 100644 --- a/src/message/special.rs +++ b/src/message/special.rs @@ -1,7 +1,6 @@ //! Special message types: newsletter, app-state key share, sender-key distribution. use super::*; -use buffa::Message as _; const APP_STATE_KEY_SHARE_SEND_ATTEMPTS: u8 = 3; const APP_STATE_KEY_SHARE_SEND_RETRY: std::time::Duration = std::time::Duration::from_secs(1); @@ -90,7 +89,9 @@ impl Client { Some(KeyComponents { key_id, data, - fingerprint_bytes: fingerprint.encode_to_vec(), + fingerprint_bytes: waproto::codec::app_state_sync_key_fingerprint_to_vec( + fingerprint, + ), timestamp: key_data.timestamp.unwrap_or_default(), }) } @@ -182,9 +183,7 @@ impl Client { continue; }; let key_data = if let Some(stored) = key_store.get_sync_key(key_id).await? { - match wa::message::AppStateSyncKeyFingerprint::decode_from_slice( - &stored.fingerprint, - ) { + match waproto::codec::app_state_sync_key_fingerprint_decode(&stored.fingerprint) { Ok(fingerprint) => { buffa::MessageField::some(wa::message::AppStateSyncKeyData { key_data: Some(stored.key_data), diff --git a/src/message/tests.rs b/src/message/tests.rs index 485bed4f8..c022e7fb7 100644 --- a/src/message/tests.rs +++ b/src/message/tests.rs @@ -1,5 +1,8 @@ //! Tests for the message receive/decrypt pipeline. +// Tests/benches exercise the raw buffa API. +#![allow(clippy::disallowed_methods)] + use super::*; use crate::store::SqliteStore; use crate::store::persistence_manager::PersistenceManager; diff --git a/src/passkey/flow.rs b/src/passkey/flow.rs index fd6aaf0c8..b1f18ca38 100644 --- a/src/passkey/flow.rs +++ b/src/passkey/flow.rs @@ -617,6 +617,7 @@ pub(crate) async fn handle_passkey_continuation(client: &Arc, node: Arc< } #[cfg(test)] +#[allow(clippy::disallowed_methods)] mod tests { use super::*; use crate::test_utils::{TestEventCollector, create_test_client, node_to_owned_ref}; diff --git a/src/pdo.rs b/src/pdo.rs index 577dd4d1a..74ebc5f90 100644 --- a/src/pdo.rs +++ b/src/pdo.rs @@ -568,6 +568,7 @@ impl Client { } #[cfg(test)] +#[allow(clippy::disallowed_methods)] mod tests { use super::self_peer_target; use wacore::store::Device; diff --git a/src/prekeys.rs b/src/prekeys.rs index 4e7029315..f276ac6b4 100644 --- a/src/prekeys.rs +++ b/src/prekeys.rs @@ -388,9 +388,8 @@ impl Client { }; let key_pair = KeyPair::generate(&mut rand::make_rng::()); let record = new_pre_key_record(id, &key_pair); - use buffa::Message; backend - .store_prekey(id, &record.encode_to_vec(), false) + .store_prekey(id, &waproto::codec::pre_key_record_to_vec(&record), false) .await?; self.persistence_manager .process_command(DeviceCommand::SetPreKeyWatermarks { @@ -516,8 +515,6 @@ impl Client { // the async executor responsive. Records are encoded into one contiguous // buffer with zero-copy Bytes slices instead of an alloc per record. let (encoded_batch, generated) = wacore::runtime::blocking(&*self.runtime, move || { - use buffa::Message; - // Seed one CSPRNG and advance it per key, rather than reseeding from // entropy on every iteration. let mut rng = rand::make_rng::(); @@ -536,7 +533,10 @@ impl Client { let pre_key_id = gen_start + i as u32; let key_pair = KeyPair::generate(&mut rng); let start = buf.len(); - new_pre_key_record(pre_key_id, &key_pair).encode(&mut buf); + waproto::codec::pre_key_record_encode_into( + &new_pre_key_record(pre_key_id, &key_pair), + &mut buf, + ); offsets.push((pre_key_id, start..buf.len())); pubkeys.push((pre_key_id, key_pair.public_key)); } @@ -1062,6 +1062,7 @@ mod tests { } #[cfg(test)] +#[allow(clippy::disallowed_methods)] mod window_tests { use wacore::libsignal::protocol::PublicKey; diff --git a/src/reexports_test.rs b/src/reexports_test.rs index 783bc8c8d..7ec43129d 100644 --- a/src/reexports_test.rs +++ b/src/reexports_test.rs @@ -2,6 +2,8 @@ //! implement the async traits using only this crate's re-exports (no direct //! buffa/bytes/anyhow/async-trait/chrono dependency of its own). #![cfg(test)] +// Tests/benches exercise the raw buffa API. +#![allow(clippy::disallowed_methods)] use crate as whatsapp_rust; use whatsapp_rust::waproto::whatsapp as wa; diff --git a/src/send/mod.rs b/src/send/mod.rs index 86f8b1f7f..474247660 100644 --- a/src/send/mod.rs +++ b/src/send/mod.rs @@ -2258,6 +2258,7 @@ pub(crate) fn dm_stanza_to(recipient_bare: &Jid, to: &Jid) -> Jid { } #[cfg(test)] +#[allow(clippy::disallowed_methods)] mod tests { use super::*; use crate::test_utils::wait_for_lock_waiter; diff --git a/src/store/signal.rs b/src/store/signal.rs index 4514d2ebc..63d6d582d 100644 --- a/src/store/signal.rs +++ b/src/store/signal.rs @@ -278,14 +278,13 @@ impl PreKeyStore for Device { &self, prekey_id: u32, ) -> Result, StoreError> { - use buffa::Message; use wacore::libsignal::protocol::KeyPair; use wacore::libsignal::store::record_helpers::new_pre_key_record; match self.backend.load_prekey(prekey_id).await { Ok(Some(bytes)) => { // Try new format first (protobuf-encoded PreKeyRecordStructure) - if let Ok(record) = PreKeyRecordStructure::decode_from_slice(bytes.as_ref()) { + if let Ok(record) = waproto::codec::pre_key_record_decode(bytes.as_ref()) { return Ok(Some(record)); } @@ -313,8 +312,7 @@ impl PreKeyStore for Device { record: PreKeyRecordStructure, uploaded: bool, ) -> Result<(), StoreError> { - use buffa::Message; - let bytes = record.encode_to_vec(); + let bytes = waproto::codec::pre_key_record_to_vec(&record); self.backend .store_prekey(prekey_id, &bytes, uploaded) .await @@ -354,7 +352,6 @@ impl SignedPreKeyStore for Device { } // Rotated-out key: a prekey message minted against a previous signed // pre-key still names its old id, so fall back to the retained records. - use buffa::Message; match self .backend .load_signed_prekey(signed_prekey_id) @@ -362,7 +359,7 @@ impl SignedPreKeyStore for Device { .map_err(|e| Box::new(e) as StoreError)? { Some(bytes) => { - let record = SignedPreKeyRecordStructure::decode_from_slice(&bytes) + let record = waproto::codec::signed_pre_key_record_decode(&bytes) .map_err(|e| Box::new(e) as StoreError)?; Ok(Some(record)) } @@ -544,6 +541,7 @@ impl SenderKeyStore for Device { } #[cfg(test)] +#[allow(clippy::disallowed_methods)] mod tests { use super::*; diff --git a/src/upload.rs b/src/upload.rs index a2ddf4618..7455c4724 100644 --- a/src/upload.rs +++ b/src/upload.rs @@ -130,10 +130,17 @@ impl UploadCrypto { } } +/// Boxed future for the dyn-driven retry loop below. `Send` keeps the upload +/// futures spawnable, as they were with the fully generic signature. +type BoxFut<'a, T> = std::pin::Pin + Send + 'a>>; + /// Drives host failover, auth refresh, and resumable upload. `file_length` is the /// plaintext size (for the response); `ciphertext_len` is the encrypted blob size /// (for the resume threshold/offsets). `send_body` transmits the body from a given /// offset; `execute_request` serves the body-less resume check. +/// +/// Thin adapter: boxes the per-call futures so the large driver body +/// instantiates once instead of per closure combination. #[allow(clippy::too_many_arguments)] async fn upload_media_with_retry( crypto: UploadCrypto, @@ -147,15 +154,45 @@ async fn upload_media_with_retry Result where - GMC: FnMut(bool) -> GMCFut, - GMCFut: std::future::Future>, - IMC: FnMut() -> IMCFut, - IMCFut: std::future::Future, - EXR: FnMut(HttpRequest) -> EXRFut, - EXRFut: std::future::Future>, - SB: FnMut(HttpRequest, u64, u64) -> SBFut, - SBFut: std::future::Future>, + GMC: FnMut(bool) -> GMCFut + Send, + GMCFut: std::future::Future> + Send, + IMC: FnMut() -> IMCFut + Send, + IMCFut: std::future::Future + Send, + EXR: FnMut(HttpRequest) -> EXRFut + Send, + EXRFut: std::future::Future> + Send, + SB: FnMut(HttpRequest, u64, u64) -> SBFut + Send, + SBFut: std::future::Future> + Send, { + upload_media_with_retry_dyn( + crypto, + media_type, + file_length, + ciphertext_len, + media_key_timestamp, + &mut |force| Box::pin(get_media_conn(force)), + &mut || Box::pin(invalidate_media_conn()), + &mut |request| Box::pin(execute_request(request)), + &mut |request, offset, remaining| Box::pin(send_body(request, offset, remaining)), + ) + .await +} + +#[allow(clippy::too_many_arguments)] +async fn upload_media_with_retry_dyn<'a>( + crypto: UploadCrypto, + media_type: MediaType, + file_length: u64, + ciphertext_len: u64, + media_key_timestamp: i64, + get_media_conn: &mut ( + dyn FnMut(bool) -> BoxFut<'a, Result> + Send + 'a + ), + invalidate_media_conn: &mut (dyn FnMut() -> BoxFut<'a, ()> + Send + 'a), + execute_request: &mut (dyn FnMut(HttpRequest) -> BoxFut<'a, Result> + Send + 'a), + send_body: &mut ( + dyn FnMut(HttpRequest, u64, u64) -> BoxFut<'a, Result> + Send + 'a + ), +) -> Result { let token = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(crypto.file_enc_sha256); let upload_path = media_type.upload_path(); let mut force_refresh = false; diff --git a/storages/chat-store/src/materialize.rs b/storages/chat-store/src/materialize.rs index afaa476f5..96b25c0b4 100644 --- a/storages/chat-store/src/materialize.rs +++ b/storages/chat-store/src/materialize.rs @@ -1,7 +1,6 @@ //! Pure event-to-row transforms: what a `wa::Message` means for the tables. //! No I/O here so every rule is unit-testable without a database. -use buffa::Message as _; use wacore::proto_helpers::MessageExt; use waproto::whatsapp as wa; @@ -112,7 +111,7 @@ pub(crate) fn classify(msg: &wa::Message) -> MessageOp { target_id, new_text: extract_text(edited_base), new_kind: message_kind(edited_base), - new_proto: edited.encode_to_vec(), + new_proto: waproto::codec::message_to_vec(edited), }; } return MessageOp::Ignore; @@ -154,7 +153,7 @@ fn has_any_content(base: &wa::Message) -> bool { probe.message_context_info = Default::default(); // Encoded emptiness, not PartialEq: presence of an empty submessage still // costs wire bytes, while buffa's equality folds it into "absent". - !probe.encode_to_vec().is_empty() + waproto::codec::message_encoded_len(&probe) > 0 } #[cfg(test)] diff --git a/storages/chat-store/src/queries.rs b/storages/chat-store/src/queries.rs index 8c338ce28..e922d203e 100644 --- a/storages/chat-store/src/queries.rs +++ b/storages/chat-store/src/queries.rs @@ -4,12 +4,10 @@ use std::str::FromStr; -use buffa::Message as _; use chrono::{DateTime, Utc}; use diesel::prelude::*; use log::warn; use wacore_binary::Jid; -use waproto::whatsapp as wa; use crate::error::{Result, db_err}; use crate::schema; @@ -114,7 +112,7 @@ struct MessageRow { impl From for StoredMessage { fn from(row: MessageRow) -> Self { let message = row.proto.as_deref().and_then(|bytes| { - match wa::Message::decode_from_slice(bytes) { + match waproto::codec::message_decode(bytes) { Ok(msg) => Some(Box::new(msg)), Err(e) => { // Denormalized columns still render; only the proto is lost. diff --git a/storages/chat-store/src/store.rs b/storages/chat-store/src/store.rs index 5908dc61e..c195fc6af 100644 --- a/storages/chat-store/src/store.rs +++ b/storages/chat-store/src/store.rs @@ -7,7 +7,6 @@ use std::collections::BTreeSet; use std::str::FromStr; use std::sync::Arc; -use buffa::Message as _; use chrono::{DateTime, Utc}; use diesel::prelude::*; use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations}; @@ -160,7 +159,7 @@ impl ChatStore { .send(WriterMsg::Outgoing { chat: chat.clone(), msg_id: msg_id.into(), - proto: message.encode_to_vec(), + proto: waproto::codec::message_to_vec(message), kind: message_kind(base), text: extract_text(base), timestamp_ms: timestamp.timestamp_millis(), @@ -857,7 +856,7 @@ fn apply_inbound( timestamp_ms: ts_ms, kind, text: text.as_deref(), - proto: Some(&inbound.message.encode_to_vec()), + proto: Some(&waproto::codec::message_to_vec(&inbound.message)), status: if info.source.is_from_me { wa::web_message_info::Status::SERVER_ACK as i32 } else { @@ -1519,7 +1518,7 @@ fn apply_history_message( timestamp_ms: ts_ms, kind, text: text.as_deref(), - proto: Some(&message.encode_to_vec()), + proto: Some(&waproto::codec::message_to_vec(message)), status: wmi .status .map(|s| s as i32) diff --git a/storages/chat-store/tests/chat_store_test.rs b/storages/chat-store/tests/chat_store_test.rs index 221383dc5..815f88c56 100644 --- a/storages/chat-store/tests/chat_store_test.rs +++ b/storages/chat-store/tests/chat_store_test.rs @@ -1,6 +1,9 @@ //! Integration tests: real SqliteStore (in-memory), real writer task, events //! fed through the public handler exactly as the client would. +// Tests/benches exercise the raw buffa API. +#![allow(clippy::disallowed_methods)] + use std::sync::Arc; use std::time::Duration; diff --git a/storages/sqlite-storage/src/wire.rs b/storages/sqlite-storage/src/wire.rs index 23947cbb2..241cae832 100644 --- a/storages/sqlite-storage/src/wire.rs +++ b/storages/sqlite-storage/src/wire.rs @@ -8,6 +8,10 @@ //! equivalents (same field numbers, same proto3 semantics). Domain types in //! `wacore` stay untouched; conversion happens only at this boundary. +// Crate-local wire protos: this file is their sole instantiation site, so +// direct buffa calls duplicate nothing and there is no waproto::codec home. +#![allow(clippy::disallowed_methods)] + use buffa::Message as _; use wacore::appstate::hash::HashState; use wacore::store::device::{CachedNoiseCert, CachedServerCertChain}; diff --git a/tests/handshake_integration.rs b/tests/handshake_integration.rs index e764352f6..7b1a62ba6 100644 --- a/tests/handshake_integration.rs +++ b/tests/handshake_integration.rs @@ -21,6 +21,9 @@ //! second connect both runs without sending three ClientHello bytes //! (i.e. it speaks IK shape, not XX shape) AND completes successfully. +// Tests/benches exercise the raw buffa API. +#![allow(clippy::disallowed_methods)] + use async_trait::async_trait; use buffa::Message; use bytes::Bytes; diff --git a/wacore/appstate/src/decode.rs b/wacore/appstate/src/decode.rs index 163a63fa0..1fd9658b9 100644 --- a/wacore/appstate/src/decode.rs +++ b/wacore/appstate/src/decode.rs @@ -171,6 +171,7 @@ pub fn collect_key_id_refs_from_patch_list<'a>( } #[cfg(test)] +#[allow(clippy::disallowed_methods)] mod tests { use super::*; use crate::hash::{generate_content_mac, generate_index_mac}; diff --git a/wacore/appstate/src/processor.rs b/wacore/appstate/src/processor.rs index 1cd5a5d7b..54e68dbdc 100644 --- a/wacore/appstate/src/processor.rs +++ b/wacore/appstate/src/processor.rs @@ -462,6 +462,7 @@ pub fn validate_snapshot_mac( } #[cfg(test)] +#[allow(clippy::disallowed_methods)] mod tests { use super::*; use crate::hash::{generate_content_mac, generate_index_mac}; diff --git a/wacore/benches/history_sync_benchmark.rs b/wacore/benches/history_sync_benchmark.rs index 164c87500..9ce1b73ca 100644 --- a/wacore/benches/history_sync_benchmark.rs +++ b/wacore/benches/history_sync_benchmark.rs @@ -3,6 +3,9 @@ //! decompressed). This is the heaviest single-shot pipeline in the library //! and the hottest consumer of the varint scan. +// Tests/benches exercise the raw buffa API. +#![allow(clippy::disallowed_methods)] + use buffa::Message; use divan::black_box; use flate2::{Compression, write::ZlibEncoder}; diff --git a/wacore/benches/message_utils_benchmark.rs b/wacore/benches/message_utils_benchmark.rs index 16a645434..33c618cef 100644 --- a/wacore/benches/message_utils_benchmark.rs +++ b/wacore/benches/message_utils_benchmark.rs @@ -3,6 +3,9 @@ //! pad/encode steps every outgoing message pays before encryption, and the //! unpad/decode steps every incoming message pays after decryption. +// Tests/benches exercise the raw buffa API. +#![allow(clippy::disallowed_methods)] + use divan::black_box; use wacore::messages::MessageUtils; use wacore_binary::jid::Jid; diff --git a/wacore/benches/reporting_token_benchmark.rs b/wacore/benches/reporting_token_benchmark.rs index 8218fafb3..dbb979123 100644 --- a/wacore/benches/reporting_token_benchmark.rs +++ b/wacore/benches/reporting_token_benchmark.rs @@ -1,3 +1,6 @@ +// Tests/benches exercise the raw buffa API. +#![allow(clippy::disallowed_methods)] + use buffa::Message; use divan::black_box; use wacore::reporting_token::{ diff --git a/wacore/benches/send_receive_benchmark.rs b/wacore/benches/send_receive_benchmark.rs index e99ad5dce..1a8e5cb83 100644 --- a/wacore/benches/send_receive_benchmark.rs +++ b/wacore/benches/send_receive_benchmark.rs @@ -1,5 +1,8 @@ //! Full send/receive pipeline benchmarks using real `prepare_*_stanza` functions. +// Tests/benches exercise the raw buffa API. +#![allow(clippy::disallowed_methods)] + use async_trait::async_trait; use buffa::Message as ProtoMessage; use std::collections::HashMap; diff --git a/wacore/libsignal/src/protocol/identity_key.rs b/wacore/libsignal/src/protocol/identity_key.rs index 24e4ceef6..e86b7f6c8 100644 --- a/wacore/libsignal/src/protocol/identity_key.rs +++ b/wacore/libsignal/src/protocol/identity_key.rs @@ -134,6 +134,8 @@ impl IdentityKeyPair { } /// Return a byte slice which can later be deserialized with [`Self::try_from`]. + // IdentityKeyPairStructure round-trips only here; no codec pin needed. + #[allow(clippy::disallowed_methods)] pub fn serialize(&self) -> Box<[u8]> { let structure = IdentityKeyPairStructure { public_key: Some(self.identity_key.serialize().to_vec()), @@ -164,6 +166,7 @@ impl IdentityKeyPair { impl TryFrom<&[u8]> for IdentityKeyPair { type Error = SignalProtocolError; + #[allow(clippy::disallowed_methods)] fn try_from(value: &[u8]) -> Result { let structure = IdentityKeyPairStructure::decode_from_slice(value) .map_err(|_| SignalProtocolError::InvalidProtobufEncoding)?; diff --git a/wacore/libsignal/src/protocol/protocol.rs b/wacore/libsignal/src/protocol/protocol.rs index ab9acf54d..58f8f2259 100644 --- a/wacore/libsignal/src/protocol/protocol.rs +++ b/wacore/libsignal/src/protocol/protocol.rs @@ -103,7 +103,9 @@ impl Clone for SignalMessage { impl SignalMessage { const MAC_LENGTH: usize = 8; - #[allow(clippy::too_many_arguments)] + // Signal wire protos are (de)serialized only in this crate, so direct + // buffa calls duplicate nothing; no codec pin needed (here and below). + #[allow(clippy::too_many_arguments, clippy::disallowed_methods)] pub fn new( message_version: u8, mac_key: &[u8], @@ -294,6 +296,7 @@ pub struct PreKeySignalMessage { } impl PreKeySignalMessage { + #[allow(clippy::disallowed_methods)] pub fn new( message_version: u8, registration_id: u32, @@ -460,6 +463,7 @@ impl Clone for SenderKeyMessage { impl SenderKeyMessage { const SIGNATURE_LEN: usize = 64; + #[allow(clippy::disallowed_methods)] pub fn new( message_version: u8, chain_id: u32, @@ -635,6 +639,7 @@ pub struct SenderKeyDistributionMessage { } impl SenderKeyDistributionMessage { + #[allow(clippy::disallowed_methods)] pub fn new( message_version: u8, chain_id: u32, @@ -895,6 +900,7 @@ pub struct DecryptionErrorMessage { } impl DecryptionErrorMessage { + #[allow(clippy::disallowed_methods)] pub fn for_original( original_bytes: &[u8], original_type: CiphertextMessageType, @@ -957,6 +963,7 @@ impl DecryptionErrorMessage { impl TryFrom<&[u8]> for DecryptionErrorMessage { type Error = SignalProtocolError; + #[allow(clippy::disallowed_methods)] fn try_from(value: &[u8]) -> Result { let proto_structure = DecryptionErrorMessageProto::decode_from_slice(value) .map_err(|_| SignalProtocolError::InvalidProtobufEncoding)?; @@ -979,6 +986,7 @@ impl TryFrom<&[u8]> for DecryptionErrorMessage { } #[cfg(test)] +#[allow(clippy::disallowed_methods)] mod tests { use super::*; diff --git a/wacore/libsignal/src/protocol/sender_keys.rs b/wacore/libsignal/src/protocol/sender_keys.rs index 95ab3c02a..3149bdc1d 100644 --- a/wacore/libsignal/src/protocol/sender_keys.rs +++ b/wacore/libsignal/src/protocol/sender_keys.rs @@ -769,6 +769,7 @@ impl SenderKeyRecord { } #[cfg(test)] +#[allow(clippy::disallowed_methods)] mod tests { use super::*; use crate::protocol::KeyPair; diff --git a/wacore/libsignal/src/protocol/state/session.rs b/wacore/libsignal/src/protocol/state/session.rs index f5ef3b8d1..fe9d64725 100644 --- a/wacore/libsignal/src/protocol/state/session.rs +++ b/wacore/libsignal/src/protocol/state/session.rs @@ -1002,6 +1002,9 @@ impl SessionRecord { self.serialize_into_inner(buf, Some(incarnation)); } + // Session storage protos are hand-spliced only here; direct buffa calls + // duplicate nothing, so no codec pin. + #[allow(clippy::disallowed_methods)] fn serialize_into_inner(&self, buf: &mut Vec, incarnation: Option<&[u8; 16]>) { use buffa::encoding::{Tag, WireType, encode_varint, varint_len}; @@ -1199,7 +1202,7 @@ impl SessionRecord { } #[cfg(test)] -#[allow(clippy::unwrap_used)] +#[allow(clippy::unwrap_used, clippy::disallowed_methods)] mod tests { use super::*; use crate::protocol::ratchet::keys::MessageKeyGenerator; diff --git a/wacore/noise/src/handshake.rs b/wacore/noise/src/handshake.rs index 6904e1795..4a048f8c8 100644 --- a/wacore/noise/src/handshake.rs +++ b/wacore/noise/src/handshake.rs @@ -1,10 +1,8 @@ use crate::error::NoiseError; use crate::state::{NoiseCipher, NoiseState}; -use buffa::Message; use thiserror::Error; use wacore_libsignal::protocol::{KeyPair, PrivateKey, PublicKey}; -use waproto::whatsapp::cert_chain::noise_certificate; -use waproto::whatsapp::{self as wa, CertChain, HandshakeMessage}; +use waproto::whatsapp::{self as wa, HandshakeMessage}; const WA_CERT_ISSUER_SERIAL: i64 = 0; @@ -175,7 +173,7 @@ impl HandshakeUtils { cert_decrypted: &[u8], static_decrypted: &[u8; 32], ) -> Result { - let cert_chain = CertChain::decode_from_slice(cert_decrypted)?; + let cert_chain = waproto::codec::cert_chain_decode(cert_decrypted)?; let intermediate = cert_chain .intermediate @@ -189,8 +187,9 @@ impl HandshakeUtils { let intermediate_details_bytes = intermediate.details.as_ref().ok_or_else(|| { HandshakeError::CertVerification("Missing intermediate details".into()) })?; - let intermediate_details = - noise_certificate::Details::decode_from_slice(intermediate_details_bytes.as_slice())?; + let intermediate_details = waproto::codec::noise_certificate_details_decode( + intermediate_details_bytes.as_slice(), + )?; let issuer_serial = intermediate_details.issuer_serial.unwrap_or(0); if i64::from(issuer_serial) != WA_CERT_ISSUER_SERIAL { @@ -222,7 +221,7 @@ impl HandshakeUtils { .as_ref() .ok_or_else(|| HandshakeError::CertVerification("Missing leaf details".into()))?; let leaf_details = - noise_certificate::Details::decode_from_slice(leaf_details_bytes.as_slice())?; + waproto::codec::noise_certificate_details_decode(leaf_details_bytes.as_slice())?; if leaf_details.issuer_serial != intermediate_details.serial { return Err(HandshakeError::CertVerification(format!( @@ -585,7 +584,7 @@ impl IkHandshakeState { encrypted_static, encrypted_payload, ); - Ok(msg.encode_to_vec()) + Ok(waproto::codec::handshake_message_to_vec(&msg)) } /// `serverHello.static.is_some()` signals fallback (server rotated static). @@ -705,8 +704,10 @@ impl XxFallbackHandshakeState { } #[cfg(test)] +#[allow(clippy::disallowed_methods)] mod tests { use super::*; + use buffa::Message; use wacore_binary::consts::WA_CONN_HEADER; use waproto::whatsapp as wa; diff --git a/wacore/noise/src/test_util.rs b/wacore/noise/src/test_util.rs index 74f99a7d6..ac5105718 100644 --- a/wacore/noise/src/test_util.rs +++ b/wacore/noise/src/test_util.rs @@ -2,6 +2,9 @@ //! integration tests. Visible only under `#[cfg(test)]` (this crate) or //! when the `test-util` feature is enabled. +// Test fixtures may exercise the raw buffa API. +#![allow(clippy::disallowed_methods)] + use buffa::Message; use waproto::whatsapp::{self as wa, cert_chain::noise_certificate}; diff --git a/wacore/noise/tests/cert_chain_verify.rs b/wacore/noise/tests/cert_chain_verify.rs index 1c0bff4dd..26d53a735 100644 --- a/wacore/noise/tests/cert_chain_verify.rs +++ b/wacore/noise/tests/cert_chain_verify.rs @@ -6,6 +6,8 @@ //! a regular dep without `cfg(test)`, so the real verify actually runs here. #![cfg(not(feature = "danger-skip-cert-chain-verify"))] +// Tests/benches exercise the raw buffa API. +#![allow(clippy::disallowed_methods)] use buffa::Message; use waproto::whatsapp::{self as wa, cert_chain::noise_certificate}; diff --git a/wacore/src/adv.rs b/wacore/src/adv.rs index 2e066736f..e56539b91 100644 --- a/wacore/src/adv.rs +++ b/wacore/src/adv.rs @@ -181,6 +181,7 @@ pub fn is_key_index_valid(key_index: Option, decoded: &DecodedKeyIndex) -> } #[cfg(test)] +#[allow(clippy::disallowed_methods)] mod tests { use super::*; use buffa::Message; diff --git a/wacore/src/appstate_sync.rs b/wacore/src/appstate_sync.rs index c209dc40e..ef1f5e313 100644 --- a/wacore/src/appstate_sync.rs +++ b/wacore/src/appstate_sync.rs @@ -97,10 +97,7 @@ fn lookup_app_state_key( /// external fetch and lets the collection error out, rather than applying an empty /// patch and advancing the version. Swallowing it here would silently drop the /// blob's mutations and still persist the new version, losing that data permanently. -fn download_external_blobs(pl: &mut PatchList, download: &FDownload) -> Result<()> -where - FDownload: Fn(&wa::ExternalBlobReference) -> Result>, -{ +fn download_external_blobs(pl: &mut PatchList, download: &BlobDownloadFn<'_>) -> Result<()> { let name = pl.name; if pl.snapshot.is_none() && let Some(ext) = &pl.snapshot_ref @@ -129,6 +126,11 @@ where Ok(()) } +/// External-blob resolver as a trait object, so the large download/decode/apply +/// bodies below instantiate once instead of per closure type. +pub type BlobDownloadFn<'a> = + dyn Fn(&wa::ExternalBlobReference) -> Result> + Send + Sync + 'a; + #[derive(Debug, Error)] #[non_exhaustive] pub enum AppStateSyncError { @@ -189,15 +191,12 @@ impl AppStateProcessor { Ok(()) } - pub async fn decode_patch_list_ref( + pub async fn decode_patch_list_ref( &self, stanza_root: &NodeRef<'_>, - download: FDownload, + download: &BlobDownloadFn<'_>, validate_macs: bool, - ) -> Result<(Vec, HashState, PatchList)> - where - FDownload: Fn(&wa::ExternalBlobReference) -> Result> + Send + Sync, - { + ) -> Result<(Vec, HashState, PatchList)> { let pl = parse_patch_list_ref(stanza_root)?; self.process_parsed_patch_list(pl, download, validate_macs) .await @@ -207,42 +206,33 @@ impl AppStateProcessor { /// `download`, then decode + apply. Lets a caller that parsed the response for /// pre-download avoid re-parsing it. See [`decode_patch_list_ref`]. #[cfg_attr(feature = "tracing", tracing::instrument(name = "wa.appstate.process_parsed", level = "debug", skip_all, fields(name = ?pl.name), err(Debug)))] - pub async fn process_parsed_patch_list( + pub async fn process_parsed_patch_list( &self, mut pl: PatchList, - download: FDownload, + download: &BlobDownloadFn<'_>, validate_macs: bool, - ) -> Result<(Vec, HashState, PatchList)> - where - FDownload: Fn(&wa::ExternalBlobReference) -> Result> + Send + Sync, - { - download_external_blobs(&mut pl, &download)?; + ) -> Result<(Vec, HashState, PatchList)> { + download_external_blobs(&mut pl, download)?; self.process_patch_list(pl, validate_macs).await } - pub async fn decode_patch_list( + pub async fn decode_patch_list( &self, stanza_root: &Node, - download: FDownload, + download: &BlobDownloadFn<'_>, validate_macs: bool, - ) -> Result<(Vec, HashState, PatchList)> - where - FDownload: Fn(&wa::ExternalBlobReference) -> Result> + Send + Sync, - { + ) -> Result<(Vec, HashState, PatchList)> { let pl = parse_patch_list(stanza_root)?; self.process_parsed_patch_list(pl, download, validate_macs) .await } - pub async fn decode_multi_patch_list_ref( + pub async fn decode_multi_patch_list_ref( &self, stanza_root: &NodeRef<'_>, - download: &FDownload, + download: &BlobDownloadFn<'_>, validate_macs: bool, - ) -> Result, HashState, PatchList)>> - where - FDownload: Fn(&wa::ExternalBlobReference) -> Result> + Send + Sync, - { + ) -> Result, HashState, PatchList)>> { let patch_lists = parse_patch_lists_ref(stanza_root)?; self.process_patch_lists(patch_lists, download, validate_macs) .await @@ -250,15 +240,12 @@ impl AppStateProcessor { /// Decode a multi-collection IQ response into per-collection results. /// Each collection is parsed and processed independently. - pub async fn decode_multi_patch_list( + pub async fn decode_multi_patch_list( &self, stanza_root: &Node, - download: &FDownload, + download: &BlobDownloadFn<'_>, validate_macs: bool, - ) -> Result, HashState, PatchList)>> - where - FDownload: Fn(&wa::ExternalBlobReference) -> Result> + Send + Sync, - { + ) -> Result, HashState, PatchList)>> { let patch_lists = parse_patch_lists(stanza_root)?; self.process_patch_lists(patch_lists, download, validate_macs) .await @@ -268,15 +255,12 @@ impl AppStateProcessor { /// `download`. Lets callers that already parsed the IQ response (e.g. to /// pre-download blobs) avoid re-parsing it. See [`decode_multi_patch_list_ref`]. #[cfg_attr(feature = "tracing", tracing::instrument(name = "wa.appstate.process_lists", level = "debug", skip_all, fields(count = patch_lists.len()), err(Debug)))] - pub async fn process_patch_lists( + pub async fn process_patch_lists( &self, patch_lists: Vec, - download: &FDownload, + download: &BlobDownloadFn<'_>, validate_macs: bool, - ) -> Result, HashState, PatchList)>> - where - FDownload: Fn(&wa::ExternalBlobReference) -> Result> + Send + Sync, - { + ) -> Result, HashState, PatchList)>> { let mut results = Vec::with_capacity(patch_lists.len()); for mut pl in patch_lists { @@ -607,29 +591,25 @@ impl AppStateProcessor { /// with `KeyNotFound`. Used by the sync paths to request missing keys up front. /// Idempotent: `download_external_blobs` no-ops once the blobs are inlined, and the /// supplied `download` closure should read from the already-prefetched cache. - pub async fn missing_key_ids_after_inline( + pub async fn missing_key_ids_after_inline( &self, pl: &mut PatchList, - download: &FDownload, - ) -> Result>> - where - FDownload: Fn(&wa::ExternalBlobReference) -> Result>, - { + download: &BlobDownloadFn<'_>, + ) -> Result>> { download_external_blobs(pl, download)?; self.get_missing_key_ids(pl).await } #[cfg_attr(feature = "tracing", tracing::instrument(name = "wa.appstate.sync_collection", level = "debug", skip_all, fields(name = ?name), err(Debug)))] - pub async fn sync_collection( + pub async fn sync_collection( &self, driver: &D, name: WAPatchName, validate_macs: bool, - download: FDownload, + download: &BlobDownloadFn<'_>, ) -> Result> where D: AppStateSyncDriver + Sync, - FDownload: Fn(&wa::ExternalBlobReference) -> Result> + Send + Sync, { let mut all = Vec::new(); // Bound re-fetches so a server that keeps returning a retryable collection @@ -640,7 +620,7 @@ impl AppStateProcessor { let state = self.backend.get_version(name.as_str()).await?; let node = driver.fetch_collection(name, state.version).await?; let (mut muts, _new_state, list) = self - .decode_patch_list(&node, &download, validate_macs) + .decode_patch_list(&node, download, validate_macs) .await?; all.append(&mut muts); // A retryable error (or conflict-with-more) left the version unadvanced; diff --git a/wacore/src/companion_reg.rs b/wacore/src/companion_reg.rs index 3183a7922..322d713a5 100644 --- a/wacore/src/companion_reg.rs +++ b/wacore/src/companion_reg.rs @@ -232,6 +232,7 @@ pub fn companion_platform_display(ct: CompanionWebClientType, os: &str) -> Strin } #[cfg(test)] +#[allow(clippy::disallowed_methods)] mod tests { use super::*; diff --git a/wacore/src/event.rs b/wacore/src/event.rs index fa14eefcf..4eebd6e96 100644 --- a/wacore/src/event.rs +++ b/wacore/src/event.rs @@ -4,7 +4,6 @@ //! `EventResponseMessage` proto and the `"Event Response"` use-case. use anyhow::{Result, ensure}; -use buffa::Message; use waproto::whatsapp::message::EventResponseMessage; use crate::secret_enc_addon::{AddonContext, ModificationType, decrypt_addon, encrypt_addon}; @@ -39,7 +38,7 @@ pub fn encrypt_event_response_with_secret( "message_secret must be {MESSAGE_SECRET_SIZE} bytes, got {}", message_secret.len() ); - let plaintext = response.encode_to_vec(); + let plaintext = waproto::codec::event_response_message_to_vec(response); encrypt_addon( &plaintext, message_secret, @@ -71,7 +70,7 @@ pub fn decrypt_event_response_with_secret( message_secret, &event_response_addon_ctx(stanza_id, event_creator_jid, responder_jid), )?; - Ok(EventResponseMessage::decode_from_slice(&plaintext[..])?) + Ok(waproto::codec::event_response_message_decode(&plaintext)?) } #[cfg(test)] diff --git a/wacore/src/history_sync.rs b/wacore/src/history_sync.rs index 5cdd872a2..2d6a8d54f 100644 --- a/wacore/src/history_sync.rs +++ b/wacore/src/history_sync.rs @@ -1732,6 +1732,7 @@ pub struct TcTokenCandidate { } #[cfg(test)] +#[allow(clippy::disallowed_methods)] mod tests { use super::*; use buffa::Message; diff --git a/wacore/src/iq/groups.rs b/wacore/src/iq/groups.rs index 57f3afa9a..4d53e0aa5 100644 --- a/wacore/src/iq/groups.rs +++ b/wacore/src/iq/groups.rs @@ -1422,6 +1422,27 @@ impl IqSpec for LeaveGroupIq { } } +/// Outlined body for `define_group_participant_iq!` impls, so the generated +/// types share one instantiation instead of each duplicating it. +fn build_participant_action_iq( + group_jid: &Jid, + action: &'static str, + participants: &[Jid], +) -> InfoQuery<'static> { + let children: Vec = participants + .iter() + .map(|jid| NodeBuilder::new("participant").attr("jid", jid).build()) + .collect(); + + let action_node = NodeBuilder::new(action).children(children).build(); + + InfoQuery::set_ref( + GROUP_IQ_NAMESPACE, + group_jid, + Some(NodeContent::Nodes(vec![action_node])), + ) +} + /// Macro to generate group participant IQ specs that share the same structure: /// a `set` IQ to `{group_jid}` with `<{action}>...`. macro_rules! define_group_participant_iq { @@ -1449,23 +1470,7 @@ macro_rules! define_group_participant_iq { type Response = Vec; fn build_iq(&self) -> InfoQuery<'static> { - let children: Vec = self - .participants - .iter() - .map(|jid| { - NodeBuilder::new("participant") - .attr("jid", jid) - .build() - }) - .collect(); - - let action_node = NodeBuilder::new($action).children(children).build(); - - InfoQuery::set_ref( - GROUP_IQ_NAMESPACE, - &self.group_jid, - Some(NodeContent::Nodes(vec![action_node])), - ) + build_participant_action_iq(&self.group_jid, $action, &self.participants) } fn parse_response(&self, response: &NodeRef<'_>) -> Result { @@ -1498,23 +1503,7 @@ macro_rules! define_group_participant_iq { type Response = (); fn build_iq(&self) -> InfoQuery<'static> { - let children: Vec = self - .participants - .iter() - .map(|jid| { - NodeBuilder::new("participant") - .attr("jid", jid) - .build() - }) - .collect(); - - let action_node = NodeBuilder::new($action).children(children).build(); - - InfoQuery::set_ref( - GROUP_IQ_NAMESPACE, - &self.group_jid, - Some(NodeContent::Nodes(vec![action_node])), - ) + build_participant_action_iq(&self.group_jid, $action, &self.participants) } fn parse_response(&self, _response: &NodeRef<'_>) -> Result { @@ -1717,11 +1706,9 @@ impl IqSpec for SetGroupLockedIq { type Response = (); fn build_iq(&self) -> InfoQuery<'static> { - let tag = if self.locked { "locked" } else { "unlocked" }; - InfoQuery::set_ref( - GROUP_IQ_NAMESPACE, + build_property_toggle_iq( &self.group_jid, - Some(NodeContent::Nodes(vec![NodeBuilder::new(tag).build()])), + if self.locked { "locked" } else { "unlocked" }, ) } @@ -1766,15 +1753,13 @@ impl IqSpec for SetGroupAnnouncementIq { type Response = (); fn build_iq(&self) -> InfoQuery<'static> { - let tag = if self.announce { - "announcement" - } else { - "not_announcement" - }; - InfoQuery::set_ref( - GROUP_IQ_NAMESPACE, + build_property_toggle_iq( &self.group_jid, - Some(NodeContent::Nodes(vec![NodeBuilder::new(tag).build()])), + if self.announce { + "announcement" + } else { + "not_announcement" + }, ) } @@ -1928,6 +1913,16 @@ impl IqSpec for SetGroupMembershipApprovalIq { } } +/// Outlined body for `define_group_property_toggle_iq!` impls, so the generated +/// types share one instantiation instead of each duplicating it. +fn build_property_toggle_iq(group_jid: &Jid, tag: &'static str) -> InfoQuery<'static> { + InfoQuery::set_ref( + GROUP_IQ_NAMESPACE, + group_jid, + Some(NodeContent::Nodes(vec![NodeBuilder::new(tag).build()])), + ) +} + /// Macro for boolean group property toggle IQs (on_tag / off_tag pattern). macro_rules! define_group_property_toggle_iq { ( @@ -1954,12 +1949,7 @@ macro_rules! define_group_property_toggle_iq { type Response = (); fn build_iq(&self) -> InfoQuery<'static> { - let tag = if self.enabled { $on } else { $off }; - InfoQuery::set_ref( - GROUP_IQ_NAMESPACE, - &self.group_jid, - Some(NodeContent::Nodes(vec![NodeBuilder::new(tag).build()])), - ) + build_property_toggle_iq(&self.group_jid, if self.enabled { $on } else { $off }) } fn parse_response(&self, _response: &NodeRef<'_>) -> Result { diff --git a/wacore/src/iq/usync.rs b/wacore/src/iq/usync.rs index 175e1008d..21c44a9ef 100644 --- a/wacore/src/iq/usync.rs +++ b/wacore/src/iq/usync.rs @@ -966,6 +966,7 @@ impl IqSpec for LidQuerySpec { } #[cfg(test)] +#[allow(clippy::disallowed_methods)] mod tests { use super::*; diff --git a/wacore/src/media_retry.rs b/wacore/src/media_retry.rs index 5d5724ce7..9ac35453d 100644 --- a/wacore/src/media_retry.rs +++ b/wacore/src/media_retry.rs @@ -11,7 +11,7 @@ //! WAWebHandleMediaRetryNotification. use anyhow::{Result, anyhow}; -use buffa::{Message, MessageView}; +use buffa::MessageView; use hkdf::Hkdf; use rand::Rng; use sha2::Sha256; @@ -72,7 +72,7 @@ pub fn encrypt_media_retry_receipt( let receipt = wa::ServerErrorReceipt { stanza_id: Some(stanza_id.to_string()), }; - let plaintext = receipt.encode_to_vec(); + let plaintext = waproto::codec::server_error_receipt_to_vec(&receipt); let mut ciphertext = Vec::with_capacity(plaintext.len() + 16); aes_256_gcm_encrypt(&key, &iv, stanza_id.as_bytes(), &plaintext, &mut ciphertext) diff --git a/wacore/src/message_edit.rs b/wacore/src/message_edit.rs index e253c7c51..06bcb7dc8 100644 --- a/wacore/src/message_edit.rs +++ b/wacore/src/message_edit.rs @@ -176,6 +176,7 @@ pub fn decrypt_message_edit_with_fallback( } #[cfg(test)] +#[allow(clippy::disallowed_methods)] mod tests { use super::*; use buffa::MessageField; diff --git a/wacore/src/message_processing.rs b/wacore/src/message_processing.rs index 5d38dc355..d0b13f615 100644 --- a/wacore/src/message_processing.rs +++ b/wacore/src/message_processing.rs @@ -300,6 +300,7 @@ pub fn process_decrypted_plaintext( } #[cfg(test)] +#[allow(clippy::disallowed_methods)] mod tests { use super::*; use wacore_binary::{Attrs, Node, NodeContent, NodeValue}; diff --git a/wacore/src/messages.rs b/wacore/src/messages.rs index 7d42b4c73..dca8160c9 100644 --- a/wacore/src/messages.rs +++ b/wacore/src/messages.rs @@ -853,6 +853,7 @@ pub fn parse_message_info( } #[cfg(test)] +#[allow(clippy::disallowed_methods)] mod plaintext_view_tests { use super::*; @@ -1355,6 +1356,7 @@ mod parse_message_info_tests { } #[cfg(test)] +#[allow(clippy::disallowed_methods)] mod device_sent_tests { use super::*; diff --git a/wacore/src/pair.rs b/wacore/src/pair.rs index 05883b8a5..608e0fd93 100644 --- a/wacore/src/pair.rs +++ b/wacore/src/pair.rs @@ -454,6 +454,7 @@ impl PairUtils { } #[cfg(test)] +#[allow(clippy::disallowed_methods)] mod tests { use super::*; use buffa::Message; diff --git a/wacore/src/poll.rs b/wacore/src/poll.rs index b9054cbbc..354d8cd03 100644 --- a/wacore/src/poll.rs +++ b/wacore/src/poll.rs @@ -324,6 +324,7 @@ where } #[cfg(test)] +#[allow(clippy::disallowed_methods)] mod tests { use super::*; diff --git a/wacore/src/prekeys.rs b/wacore/src/prekeys.rs index 916b73ad5..e9cfb95fa 100644 --- a/wacore/src/prekeys.rs +++ b/wacore/src/prekeys.rs @@ -411,6 +411,7 @@ impl PreKeyUtils { } #[cfg(test)] +#[allow(clippy::disallowed_methods)] mod tests { use super::*; use crate::iq::prekeys::PreKeyBundleUserNode; diff --git a/wacore/src/reaction.rs b/wacore/src/reaction.rs index 62fde4de2..f40bb7a81 100644 --- a/wacore/src/reaction.rs +++ b/wacore/src/reaction.rs @@ -8,7 +8,6 @@ //! the HKDF use-case is `"Enc Reaction"` with empty AAD. use anyhow::{Result, ensure}; -use buffa::Message; use waproto::whatsapp::message::ReactionMessage; use crate::secret_enc_addon::{AddonContext, ModificationType, decrypt_addon, encrypt_addon}; @@ -52,7 +51,7 @@ pub fn encrypt_reaction_with_secret( sender_timestamp_ms: Some(sender_timestamp_ms), ..Default::default() }; - let plaintext = inner.encode_to_vec(); + let plaintext = waproto::codec::reaction_message_to_vec(&inner); encrypt_addon( &plaintext, message_secret, @@ -83,7 +82,7 @@ pub fn decrypt_reaction_with_secret( message_secret, &reaction_addon_ctx(parent_msg_id, parent_sender_jid, reactor_jid), )?; - Ok(ReactionMessage::decode_from_slice(&plaintext[..])?) + Ok(waproto::codec::reaction_message_decode(&plaintext)?) } #[cfg(test)] diff --git a/wacore/src/shortcake.rs b/wacore/src/shortcake.rs index c5e66fdbd..0fc8d08c2 100644 --- a/wacore/src/shortcake.rs +++ b/wacore/src/shortcake.rs @@ -31,7 +31,7 @@ use crate::libsignal::crypto::aes_256_gcm_encrypt; use crate::libsignal::protocol::{CurveError, KeyPair, PublicKey}; use crate::pair_code::PairCodeUtils; -use buffa::{Enumeration, Message}; +use buffa::Enumeration; use hkdf::Hkdf; use hmac::{Hmac, KeyInit as _, Mac}; use rand::RngExt; @@ -93,12 +93,11 @@ impl ShortcakeUtils { device_type: wa::device_props::PlatformType, ref_str: &str, ) -> Vec { - wa::CompanionEphemeralIdentity { + waproto::codec::companion_ephemeral_identity_to_vec(&wa::CompanionEphemeralIdentity { public_key: Some(public_key.to_vec()), device_type: Some(device_type), r#ref: Some(ref_str.to_string()), - } - .encode_to_vec() + }) } /// commitment = SHA256(companionEphemeralIdentityBytes ‖ companionNonce). @@ -118,13 +117,12 @@ impl ShortcakeUtils { companion_ephemeral_identity: &[u8], commitment_hash: &[u8; 32], ) -> Vec { - wa::ProloguePayload { + waproto::codec::prologue_payload_to_vec(&wa::ProloguePayload { companion_ephemeral_identity: Some(companion_ephemeral_identity.to_vec()), commitment: buffa::MessageField::some(wa::CompanionCommitment { hash: Some(commitment_hash.to_vec()), }), - } - .encode_to_vec() + }) } /// Decode + length-validate the `PrimaryEphemeralIdentity` protobuf from the @@ -134,7 +132,7 @@ impl ShortcakeUtils { pub fn parse_primary_ephemeral_identity( bytes: &[u8], ) -> Result { - let parsed = wa::PrimaryEphemeralIdentity::decode_from_slice(bytes) + let parsed = waproto::codec::primary_ephemeral_identity_decode(bytes) .map_err(|_| ShortcakeError::Decode("primary_ephemeral_identity"))?; let pk = parsed.public_key.unwrap_or_default(); let nc = parsed.nonce.unwrap_or_default(); @@ -218,12 +216,11 @@ impl ShortcakeUtils { companion_identity_key: &[u8; 32], adv_secret: &[u8; 32], ) -> Vec { - wa::PairingRequest { + waproto::codec::pairing_request_to_vec(&wa::PairingRequest { companion_public_key: Some(companion_public_key.to_vec()), companion_identity_key: Some(companion_identity_key.to_vec()), adv_secret: Some(adv_secret.to_vec()), - } - .encode_to_vec() + }) } /// AES-256-GCM encrypt the pairing-request plaintext with a fresh 12-byte IV, @@ -245,11 +242,10 @@ impl ShortcakeUtils { /// Encode the `EncryptedPairingRequest` protobuf sent in the final IQ. pub fn build_encrypted_pairing_request(enc: &EncryptedPairing) -> Vec { - wa::EncryptedPairingRequest { + waproto::codec::encrypted_pairing_request_to_vec(&wa::EncryptedPairingRequest { encrypted_payload: Some(enc.encrypted_payload.clone()), iv: Some(enc.iv.to_vec()), - } - .encode_to_vec() + }) } /// Derive the pairing-handoff HMAC key from a PRIOR session's 32-byte ADV @@ -291,8 +287,10 @@ impl ShortcakeUtils { } #[cfg(test)] +#[allow(clippy::disallowed_methods)] mod tests { use super::*; + use buffa::Message; // Deterministic vectors guard the bug-prone concat/XOR/label details the RE flagged. diff --git a/wacore/src/stanza/business.rs b/wacore/src/stanza/business.rs index 1511b2b31..b85d38394 100644 --- a/wacore/src/stanza/business.rs +++ b/wacore/src/stanza/business.rs @@ -3,7 +3,6 @@ //! Reference: WhatsApp Web `WAWebHandleBusinessNotification` use anyhow::{Result, anyhow}; -use buffa::Message as _; use serde::Serialize; use wacore_binary::Jid; use wacore_binary::NodeRef; @@ -77,13 +76,10 @@ impl VerifiedName { // lives only inside the certificate protobuf (content bytes). Decode it // to fill the missing fields, matching WAWebCommonParsersVerifiedName. if let Some(cert_bytes) = certificate.as_deref() - && let Ok(cert) = - waproto::whatsapp::VerifiedNameCertificate::decode_from_slice(cert_bytes) + && let Ok(cert) = waproto::codec::verified_name_certificate_decode(cert_bytes) && let Some(details_bytes) = cert.details.as_deref() && let Ok(details) = - waproto::whatsapp::verified_name_certificate::Details::decode_from_slice( - details_bytes, - ) + waproto::codec::verified_name_certificate_details_decode(details_bytes) { name = name.or(details.verified_name); serial = serial.or_else(|| details.serial.map(|s| s.to_string())); @@ -385,8 +381,10 @@ impl BusinessNotification { } #[cfg(test)] +#[allow(clippy::disallowed_methods)] mod tests { use super::*; + use buffa::Message as _; use wacore_binary::builder::NodeBuilder; #[test] diff --git a/wacore/src/store/device.rs b/wacore/src/store/device.rs index 4287532bd..d601c2977 100644 --- a/wacore/src/store/device.rs +++ b/wacore/src/store/device.rs @@ -522,6 +522,7 @@ impl Device { } #[cfg(test)] +#[allow(clippy::disallowed_methods)] mod tests { use super::*; use buffa::Message; diff --git a/wacore/src/types/events.rs b/wacore/src/types/events.rs index 62b49b936..f79ecaeba 100755 --- a/wacore/src/types/events.rs +++ b/wacore/src/types/events.rs @@ -623,7 +623,7 @@ pub struct DisappearingModeChanged { /// `maybe_*` setter), never an empty-string / zero sentinel. Even the /// unit-marker events are empty sealed structs built as /// `Connected::builder().build()`, so a new payload must follow the pattern. -#[derive(Debug, Clone, Serialize)] +#[derive(Clone, Serialize)] #[non_exhaustive] pub enum Event { Connected(Connected), @@ -880,6 +880,15 @@ impl Event { } } +// Variant name only, on purpose: Messages/HistorySync transitively contain +// `wa::Message`, so a derived impl would keep the entire generated proto Debug +// graph (hundreds of KiB) in the binary. Serialize the event for full contents. +impl fmt::Debug for Event { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(&self.kind(), f) + } +} + /// One decrypted inbound message. The same items (and order) back both /// consumer surfaces: the durability hook's batch and [`Event::Messages`]. #[derive(Debug, Clone, Serialize, bon::Builder)] @@ -1544,6 +1553,7 @@ pub struct LabelAssociationUpdate { } #[cfg(test)] +#[allow(clippy::disallowed_methods)] mod tests { use super::*; use buffa::Message; diff --git a/wacore/src/usync.rs b/wacore/src/usync.rs index 039510cb2..98bb9444f 100644 --- a/wacore/src/usync.rs +++ b/wacore/src/usync.rs @@ -196,6 +196,7 @@ pub fn parse_lid_mappings_from_response(resp_node: &NodeRef<'_>) -> Vec Vec { } /// Load a protobuf table from its embedded zlib blob. +// Crate-local mlow.tables protos: sole instantiation site, nothing to pin in +// waproto::codec. +#[allow(clippy::disallowed_methods)] pub(crate) fn load_blob_buffa(compressed: &[u8]) -> T { let bytes = inflate(compressed); T::decode_from_slice(bytes.as_slice()).expect("mlow table blob must protobuf-decode") @@ -56,6 +59,7 @@ pub(crate) fn make_blob_raw(bytes: &[u8]) -> Vec { } #[cfg(test)] +#[allow(clippy::disallowed_methods)] mod generator { use super::*; use std::fs; diff --git a/wacore/tests/appstate_external_mutations_test.rs b/wacore/tests/appstate_external_mutations_test.rs index 6a5acb30c..07efca4f5 100644 --- a/wacore/tests/appstate_external_mutations_test.rs +++ b/wacore/tests/appstate_external_mutations_test.rs @@ -4,6 +4,9 @@ //! 1. Patches have external_mutations that need to be downloaded //! 2. REMOVE mutations reference entries we don't have locally (hasMissingRemove) +// Tests/benches exercise the raw buffa API. +#![allow(clippy::disallowed_methods)] + use buffa::Message; use wacore::appstate::WAPATCH_INTEGRITY; use wacore::appstate::hash::{HashState, generate_content_mac}; diff --git a/waproto/build.rs b/waproto/build.rs index 388c315bd..2a1769222 100644 --- a/waproto/build.rs +++ b/waproto/build.rs @@ -32,6 +32,8 @@ fn main() -> std::io::Result<()> { let out_path = std::path::PathBuf::from(&out_dir); // Emit the wire-tag consts (field numbers) for hand-written partial decoders. + // Build-time descriptor decode — nothing to pin in waproto::codec. + #[allow(clippy::disallowed_methods)] let fds = FileDescriptorSet::decode_from_slice(&std::fs::read("src/whatsapp.desc")?) .map_err(std::io::Error::other)?; generate_tags(&fds, &out_path.join("tags.rs"))?; diff --git a/waproto/src/lib.rs b/waproto/src/lib.rs index b6f05504b..ebcf4b32d 100644 --- a/waproto/src/lib.rs +++ b/waproto/src/lib.rs @@ -11,13 +11,16 @@ pub use buffa; pub mod whatsapp { + // disallowed_methods: the generated impls call the buffa trait methods on + // nested messages; that IS the pinned instantiation. #![allow( non_camel_case_types, non_snake_case, unreachable_patterns, clippy::derivable_impls, clippy::match_single_binding, - clippy::needless_else + clippy::needless_else, + clippy::disallowed_methods )] #[rustfmt::skip] buffa::include_proto!("whatsapp"); @@ -95,6 +98,9 @@ pub mod tags { /// Decode helpers take `&[u8]` and decode via `decode_from_slice`, the buffer /// shape the rest of the workspace already instantiates, so no second /// buffer-type tree exists. +// The pinning wrappers are the one sanctioned direct-call site (see +// clippy.toml disallowed-methods). +#[allow(clippy::disallowed_methods)] pub mod codec { use crate::whatsapp; use buffa::Message as _; @@ -291,6 +297,13 @@ pub mod codec { record.encode_to_vec() } + /// Append the encoded record to `out`; the bulk prekey upload packs many + /// records into one shared buffer. + #[inline(never)] + pub fn pre_key_record_encode_into(record: &whatsapp::PreKeyRecordStructure, out: &mut Vec) { + record.encode(out); + } + #[inline(never)] pub fn signed_pre_key_record_decode( bytes: &[u8], @@ -404,9 +417,122 @@ pub mod codec { ) -> Result { whatsapp::SenderKeyDistributionMessage::decode_from_slice(bytes) } + + /// Noise server cert chain, verified once per connection in wacore-noise. + #[inline(never)] + pub fn cert_chain_decode(bytes: &[u8]) -> Result { + whatsapp::CertChain::decode_from_slice(bytes) + } + + #[inline(never)] + pub fn noise_certificate_details_decode( + bytes: &[u8], + ) -> Result { + whatsapp::cert_chain::noise_certificate::Details::decode_from_slice(bytes) + } + + /// Business verified-name certificates (usync / business profile parsing). + #[inline(never)] + pub fn verified_name_certificate_decode( + bytes: &[u8], + ) -> Result { + whatsapp::VerifiedNameCertificate::decode_from_slice(bytes) + } + + #[inline(never)] + pub fn verified_name_certificate_details_decode( + bytes: &[u8], + ) -> Result { + whatsapp::verified_name_certificate::Details::decode_from_slice(bytes) + } + + /// SHORTCAKE passkey-pairing protos (one flow per device link). + #[inline(never)] + pub fn companion_ephemeral_identity_to_vec( + id: &whatsapp::CompanionEphemeralIdentity, + ) -> Vec { + id.encode_to_vec() + } + + #[inline(never)] + pub fn prologue_payload_to_vec(payload: &whatsapp::ProloguePayload) -> Vec { + payload.encode_to_vec() + } + + #[inline(never)] + pub fn primary_ephemeral_identity_decode( + bytes: &[u8], + ) -> Result { + whatsapp::PrimaryEphemeralIdentity::decode_from_slice(bytes) + } + + #[inline(never)] + pub fn pairing_request_to_vec(req: &whatsapp::PairingRequest) -> Vec { + req.encode_to_vec() + } + + #[inline(never)] + pub fn encrypted_pairing_request_to_vec(req: &whatsapp::EncryptedPairingRequest) -> Vec { + req.encode_to_vec() + } + + /// Secret-addon payloads (enc reactions, event responses, bot msmsg + /// replies) and per-message sidecars; small trees, but wacore and the + /// main crate each stamped private copies. + #[inline(never)] + pub fn reaction_message_to_vec(msg: &whatsapp::message::ReactionMessage) -> Vec { + msg.encode_to_vec() + } + + #[inline(never)] + pub fn reaction_message_decode( + bytes: &[u8], + ) -> Result { + whatsapp::message::ReactionMessage::decode_from_slice(bytes) + } + + #[inline(never)] + pub fn event_response_message_to_vec(msg: &whatsapp::message::EventResponseMessage) -> Vec { + msg.encode_to_vec() + } + + #[inline(never)] + pub fn event_response_message_decode( + bytes: &[u8], + ) -> Result { + whatsapp::message::EventResponseMessage::decode_from_slice(bytes) + } + + #[inline(never)] + pub fn message_secret_message_decode( + bytes: &[u8], + ) -> Result { + whatsapp::MessageSecretMessage::decode_from_slice(bytes) + } + + #[inline(never)] + pub fn server_error_receipt_to_vec(receipt: &whatsapp::ServerErrorReceipt) -> Vec { + receipt.encode_to_vec() + } + + /// App-state key-share fingerprints, persisted alongside each sync key. + #[inline(never)] + pub fn app_state_sync_key_fingerprint_to_vec( + fp: &whatsapp::message::AppStateSyncKeyFingerprint, + ) -> Vec { + fp.encode_to_vec() + } + + #[inline(never)] + pub fn app_state_sync_key_fingerprint_decode( + bytes: &[u8], + ) -> Result { + whatsapp::message::AppStateSyncKeyFingerprint::decode_from_slice(bytes) + } } #[cfg(test)] +#[allow(clippy::disallowed_methods)] mod tests { use super::whatsapp as wa; use buffa::Message; From c2baa3680e0a7d9492addc565c15d5527971ce74 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Jul 2026 17:06:52 +0000 Subject: [PATCH 4/5] =?UTF-8?q?fix(build):=20keep=20SQLite=20shared=20cach?= =?UTF-8?q?e=20=E2=80=94=20in-memory=20pool=20mode=20needs=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sqlite-storage's in-memory mode shares one DB across pooled connections via cache=shared URIs; SQLITE_OMIT_SHARED_CACHE made every reopen see a fresh empty DB (caught by the save/load roundtrip tests). Co-Authored-By: Claude --- .cargo/config.toml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index eba998a45..5451b6182 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -19,9 +19,11 @@ rustflags = [ [env] # Trim bundled-SQLite subsystems the workspace never touches. FTS5 stays: -# chat-store's `search` feature builds its index on it. JSON and extension -# loading are unused by diesel/sqlite-storage/chat-store (checked: no json_* -# SQL, no load_extension callers); dropping loadable extensions also removes -# a dlopen surface. STAT4/DBSTAT/RTREE/FTS3/SOUNDEX are upstream-default-off +# chat-store's `search` feature builds its index on it. Shared cache stays: +# sqlite-storage's in-memory mode shares one DB across the pool via +# `cache=shared` URIs. JSON and extension loading are unused by +# diesel/sqlite-storage/chat-store (checked: no json_* SQL, no +# load_extension callers); dropping loadable extensions also removes a +# dlopen surface. STAT4/DBSTAT/RTREE/FTS3/SOUNDEX are upstream-default-off # features libsqlite3-sys turns on; none are exercised here. -LIBSQLITE3_FLAGS = "-USQLITE_ENABLE_FTS3 -USQLITE_ENABLE_FTS3_PARENTHESIS -USQLITE_ENABLE_RTREE -USQLITE_ENABLE_STAT4 -USQLITE_ENABLE_DBSTAT_VTAB -USQLITE_SOUNDEX -DSQLITE_OMIT_JSON -DSQLITE_OMIT_LOAD_EXTENSION -DSQLITE_OMIT_DEPRECATED -DSQLITE_OMIT_PROGRESS_CALLBACK -DSQLITE_OMIT_SHARED_CACHE" +LIBSQLITE3_FLAGS = "-USQLITE_ENABLE_FTS3 -USQLITE_ENABLE_FTS3_PARENTHESIS -USQLITE_ENABLE_RTREE -USQLITE_ENABLE_STAT4 -USQLITE_ENABLE_DBSTAT_VTAB -USQLITE_SOUNDEX -DSQLITE_OMIT_JSON -DSQLITE_OMIT_LOAD_EXTENSION -DSQLITE_OMIT_DEPRECATED -DSQLITE_OMIT_PROGRESS_CALLBACK" From 58680d087852de86afc329b6995e047ac8be3340 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Jul 2026 19:03:28 +0000 Subject: [PATCH 5/5] =?UTF-8?q?fix(build):=20address=20PR=20review=20?= =?UTF-8?q?=E2=80=94=20bundled=20lld,=20stable-job=20opt-out,=20wasm=20Sen?= =?UTF-8?q?d=20bounds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use -Clinker-features=+lld (toolchain's rust-lld) instead of -fuse-ld=lld so contributors need no system lld; still unstable-gated, hence -Zunstable-options in the nightly-only rustflags block. - CI test-stable job sets RUSTFLAGS="" to opt out of the nightly-only target rustflags (env overrides config). - upload.rs: cfg-conditional boxed-future and dyn-callback types plus a new wacore MaybeSend marker keep the retry driver Send on native while preserving ?Send HttpClient futures on wasm32. - Document variant-name-only Debug in the Event doc comment. - clippy.toml: ban the remaining generic Message entry points (decode, decode/encode_length_delimited, encode_to_bytes); allow raw methods in record_helpers tests that verify the borrowed encoder against them. - Drop redundant as_ref()/as_slice() where deref coercion applies. Co-Authored-By: Claude --- .cargo/config.toml | 7 ++- .github/workflows/main.yml | 5 ++ clippy.toml | 4 ++ src/store/signal.rs | 2 +- src/upload.rs | 61 ++++++++++++++------ wacore/libsignal/src/store/record_helpers.rs | 3 + wacore/noise/src/handshake.rs | 8 +-- wacore/src/sync_marker.rs | 13 +++++ wacore/src/types/events.rs | 4 ++ 9 files changed, 83 insertions(+), 24 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 5451b6182..97a998251 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -11,9 +11,14 @@ # lld + ICF: lld lays out the fat-LTO object tighter than BFD ld and folds # byte-identical sections (measured -73 KiB on the stripped demo). Scoped to # this target so wasm and non-Linux builds keep their default linkers. +# -Clinker-features=+lld selects the rust-lld bundled with the pinned +# toolchain, so contributors need no system lld package. Still +# unstable-gated on this nightly, hence -Zunstable-options (the block is +# nightly-only anyway; CI's stable job opts out via RUSTFLAGS=""). rustflags = [ "-Zshare-generics=y", - "-Clink-arg=-fuse-ld=lld", + "-Zunstable-options", + "-Clinker-features=+lld", "-Clink-arg=-Wl,--icf=all", ] diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0aa38ad7a..0549edb62 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -150,6 +150,11 @@ jobs: test-stable: name: Test Stable (no-simd) runs-on: ubuntu-latest + # .cargo/config.toml sets nightly-only rustflags (-Zshare-generics) for the + # x86_64-linux target; a set-but-empty RUSTFLAGS takes precedence over + # config rustflags, keeping this stable-toolchain job buildable. + env: + RUSTFLAGS: "" steps: - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@stable diff --git a/clippy.toml b/clippy.toml index e7e61bcba..bb4ea8f1a 100644 --- a/clippy.toml +++ b/clippy.toml @@ -14,6 +14,10 @@ disallowed-methods = [ { path = "buffa::message::Message::decode_from_slice", reason = "use waproto::codec::*_decode (pinned single instantiation; direct calls duplicate the decode tree per crate)" }, { path = "buffa::message::Message::merge_from_slice", reason = "use waproto::codec::*_merge (pinned single instantiation; direct calls duplicate the decode tree per crate)" }, { path = "buffa::message::Message::merge_to_limit", reason = "use a waproto::codec pinned wrapper (direct calls duplicate the decode tree per crate)" }, + { path = "buffa::message::Message::decode", reason = "use a waproto::codec pinned wrapper (direct calls duplicate the decode tree per crate)" }, + { path = "buffa::message::Message::decode_length_delimited", reason = "use a waproto::codec pinned wrapper (direct calls duplicate the decode tree per crate)" }, + { path = "buffa::message::Message::encode_length_delimited", reason = "use a waproto::codec pinned wrapper (direct calls duplicate the encode tree per crate)" }, + { path = "buffa::message::Message::encode_to_bytes", reason = "use a waproto::codec pinned wrapper (direct calls duplicate the encode tree per crate)" }, ] # 64-bit atomic *types*: 32-bit targets (Xtensa/ESP32) have no native AtomicU64/ diff --git a/src/store/signal.rs b/src/store/signal.rs index 63d6d582d..dca7b857c 100644 --- a/src/store/signal.rs +++ b/src/store/signal.rs @@ -284,7 +284,7 @@ impl PreKeyStore for Device { match self.backend.load_prekey(prekey_id).await { Ok(Some(bytes)) => { // Try new format first (protobuf-encoded PreKeyRecordStructure) - if let Ok(record) = waproto::codec::pre_key_record_decode(bytes.as_ref()) { + if let Ok(record) = waproto::codec::pre_key_record_decode(&bytes) { return Ok(Some(record)); } diff --git a/src/upload.rs b/src/upload.rs index 7455c4724..a13320bec 100644 --- a/src/upload.rs +++ b/src/upload.rs @@ -2,6 +2,7 @@ use anyhow::{Result, anyhow}; use base64::Engine; use serde::Deserialize; use wacore::download::MediaType; +use wacore::sync_marker::MaybeSend; use crate::client::Client; use crate::http::{HttpRequest, HttpResponse}; @@ -131,8 +132,38 @@ impl UploadCrypto { } /// Boxed future for the dyn-driven retry loop below. `Send` keeps the upload -/// futures spawnable, as they were with the fully generic signature. +/// futures spawnable on native; on wasm the `HttpClient` futures are `?Send` +/// (single-threaded runtime), so the bound is dropped there. +#[cfg(not(target_arch = "wasm32"))] type BoxFut<'a, T> = std::pin::Pin + Send + 'a>>; +#[cfg(target_arch = "wasm32")] +type BoxFut<'a, T> = std::pin::Pin + 'a>>; + +// Only auto traits can join a `dyn Fn` bound, so the wasm variants must spell +// out the whole alias instead of borrowing `MaybeSend`. +#[cfg(not(target_arch = "wasm32"))] +mod dyn_callbacks { + use super::*; + pub(super) type GetMediaConnDyn<'a> = + dyn FnMut(bool) -> BoxFut<'a, Result> + Send + 'a; + pub(super) type InvalidateMediaConnDyn<'a> = dyn FnMut() -> BoxFut<'a, ()> + Send + 'a; + pub(super) type ExecuteRequestDyn<'a> = + dyn FnMut(HttpRequest) -> BoxFut<'a, Result> + Send + 'a; + pub(super) type SendBodyDyn<'a> = + dyn FnMut(HttpRequest, u64, u64) -> BoxFut<'a, Result> + Send + 'a; +} +#[cfg(target_arch = "wasm32")] +mod dyn_callbacks { + use super::*; + pub(super) type GetMediaConnDyn<'a> = + dyn FnMut(bool) -> BoxFut<'a, Result> + 'a; + pub(super) type InvalidateMediaConnDyn<'a> = dyn FnMut() -> BoxFut<'a, ()> + 'a; + pub(super) type ExecuteRequestDyn<'a> = + dyn FnMut(HttpRequest) -> BoxFut<'a, Result> + 'a; + pub(super) type SendBodyDyn<'a> = + dyn FnMut(HttpRequest, u64, u64) -> BoxFut<'a, Result> + 'a; +} +use dyn_callbacks::*; /// Drives host failover, auth refresh, and resumable upload. `file_length` is the /// plaintext size (for the response); `ciphertext_len` is the encrypted blob size @@ -154,14 +185,14 @@ async fn upload_media_with_retry Result where - GMC: FnMut(bool) -> GMCFut + Send, - GMCFut: std::future::Future> + Send, - IMC: FnMut() -> IMCFut + Send, - IMCFut: std::future::Future + Send, - EXR: FnMut(HttpRequest) -> EXRFut + Send, - EXRFut: std::future::Future> + Send, - SB: FnMut(HttpRequest, u64, u64) -> SBFut + Send, - SBFut: std::future::Future> + Send, + GMC: FnMut(bool) -> GMCFut + MaybeSend, + GMCFut: std::future::Future> + MaybeSend, + IMC: FnMut() -> IMCFut + MaybeSend, + IMCFut: std::future::Future + MaybeSend, + EXR: FnMut(HttpRequest) -> EXRFut + MaybeSend, + EXRFut: std::future::Future> + MaybeSend, + SB: FnMut(HttpRequest, u64, u64) -> SBFut + MaybeSend, + SBFut: std::future::Future> + MaybeSend, { upload_media_with_retry_dyn( crypto, @@ -184,14 +215,10 @@ async fn upload_media_with_retry_dyn<'a>( file_length: u64, ciphertext_len: u64, media_key_timestamp: i64, - get_media_conn: &mut ( - dyn FnMut(bool) -> BoxFut<'a, Result> + Send + 'a - ), - invalidate_media_conn: &mut (dyn FnMut() -> BoxFut<'a, ()> + Send + 'a), - execute_request: &mut (dyn FnMut(HttpRequest) -> BoxFut<'a, Result> + Send + 'a), - send_body: &mut ( - dyn FnMut(HttpRequest, u64, u64) -> BoxFut<'a, Result> + Send + 'a - ), + get_media_conn: &mut GetMediaConnDyn<'a>, + invalidate_media_conn: &mut InvalidateMediaConnDyn<'a>, + execute_request: &mut ExecuteRequestDyn<'a>, + send_body: &mut SendBodyDyn<'a>, ) -> Result { let token = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(crypto.file_enc_sha256); let upload_path = media_type.upload_path(); diff --git a/wacore/libsignal/src/store/record_helpers.rs b/wacore/libsignal/src/store/record_helpers.rs index b09cd62da..9fc73481c 100644 --- a/wacore/libsignal/src/store/record_helpers.rs +++ b/wacore/libsignal/src/store/record_helpers.rs @@ -124,7 +124,10 @@ pub fn signed_prekey_structure_to_record( ) } +// Tests intentionally exercise the raw buffa Message methods: the borrowed +// encoder is verified byte-for-byte against the owned trait encoder. #[cfg(test)] +#[allow(clippy::disallowed_methods)] mod tests { use super::*; use crate::protocol::{GenericSignedPreKey, KeyPair, PreKeyRecord}; diff --git a/wacore/noise/src/handshake.rs b/wacore/noise/src/handshake.rs index 4a048f8c8..08ccf122f 100644 --- a/wacore/noise/src/handshake.rs +++ b/wacore/noise/src/handshake.rs @@ -187,9 +187,8 @@ impl HandshakeUtils { let intermediate_details_bytes = intermediate.details.as_ref().ok_or_else(|| { HandshakeError::CertVerification("Missing intermediate details".into()) })?; - let intermediate_details = waproto::codec::noise_certificate_details_decode( - intermediate_details_bytes.as_slice(), - )?; + let intermediate_details = + waproto::codec::noise_certificate_details_decode(intermediate_details_bytes)?; let issuer_serial = intermediate_details.issuer_serial.unwrap_or(0); if i64::from(issuer_serial) != WA_CERT_ISSUER_SERIAL { @@ -220,8 +219,7 @@ impl HandshakeUtils { .details .as_ref() .ok_or_else(|| HandshakeError::CertVerification("Missing leaf details".into()))?; - let leaf_details = - waproto::codec::noise_certificate_details_decode(leaf_details_bytes.as_slice())?; + let leaf_details = waproto::codec::noise_certificate_details_decode(leaf_details_bytes)?; if leaf_details.issuer_serial != intermediate_details.serial { return Err(HandshakeError::CertVerification(format!( diff --git a/wacore/src/sync_marker.rs b/wacore/src/sync_marker.rs index 1ca613f13..eb69d3216 100644 --- a/wacore/src/sync_marker.rs +++ b/wacore/src/sync_marker.rs @@ -16,3 +16,16 @@ impl MaybeSendSync for T {} pub trait MaybeSendSync {} #[cfg(target_arch = "wasm32")] impl MaybeSendSync for T {} + +/// `Send`-only variant for values that cross task boundaries but are never +/// shared by reference (closures, futures). Same wasm rationale as +/// [`MaybeSendSync`]. +#[cfg(not(target_arch = "wasm32"))] +pub trait MaybeSend: Send {} +#[cfg(not(target_arch = "wasm32"))] +impl MaybeSend for T {} + +#[cfg(target_arch = "wasm32")] +pub trait MaybeSend {} +#[cfg(target_arch = "wasm32")] +impl MaybeSend for T {} diff --git a/wacore/src/types/events.rs b/wacore/src/types/events.rs index f79ecaeba..2124016fe 100755 --- a/wacore/src/types/events.rs +++ b/wacore/src/types/events.rs @@ -623,6 +623,10 @@ pub struct DisappearingModeChanged { /// `maybe_*` setter), never an empty-string / zero sentinel. Even the /// unit-marker events are empty sealed structs built as /// `Connected::builder().build()`, so a new payload must follow the pattern. +/// +/// `Debug` output is intentionally variant-name-only (`{:?}` prints e.g. +/// `Messages`): a derived impl would drag the entire generated proto `Debug` +/// graph into the binary. `Serialize` the event when full contents are needed. #[derive(Clone, Serialize)] #[non_exhaustive] pub enum Event {