diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 000000000..97a998251 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,34 @@ +# 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. +# -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", + "-Zunstable-options", + "-Clinker-features=+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. 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" 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/.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. diff --git a/clippy.toml b/clippy.toml index bf3c35e4c..bb4ea8f1a 100644 --- a/clippy.toml +++ b/clippy.toml @@ -3,6 +3,21 @@ 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)" }, + { 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/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 ef60d2590..232bc1d83 100644 --- a/src/history_sync.rs +++ b/src/history_sync.rs @@ -625,6 +625,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 e4e935524..b7cf80c47 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 bf7921cb9..b0b9fd6d1 100644 --- a/src/prekeys.rs +++ b/src/prekeys.rs @@ -1058,6 +1058,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/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. 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..dca7b857c 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) { 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..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}; @@ -130,10 +131,47 @@ impl UploadCrypto { } } +/// Boxed future for the dyn-driven retry loop below. `Send` keeps the upload +/// 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 /// (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 +185,41 @@ 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 + 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, + 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 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(); 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 720beed90..ab15c6b6f 100644 --- a/wacore/libsignal/src/protocol/protocol.rs +++ b/wacore/libsignal/src/protocol/protocol.rs @@ -287,7 +287,9 @@ pub struct 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], @@ -508,6 +510,7 @@ pub struct PreKeySignalMessage { } impl PreKeySignalMessage { + #[allow(clippy::disallowed_methods)] pub fn new( message_version: u8, registration_id: u32, @@ -712,6 +715,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, @@ -887,6 +891,7 @@ pub struct SenderKeyDistributionMessage { } impl SenderKeyDistributionMessage { + #[allow(clippy::disallowed_methods)] pub fn new( message_version: u8, chain_id: u32, @@ -1147,6 +1152,7 @@ pub struct DecryptionErrorMessage { } impl DecryptionErrorMessage { + #[allow(clippy::disallowed_methods)] pub fn for_original( original_bytes: &[u8], original_type: CiphertextMessageType, @@ -1209,6 +1215,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)?; @@ -1231,6 +1238,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/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 6904e1795..08ccf122f 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 @@ -190,7 +188,7 @@ impl HandshakeUtils { HandshakeError::CertVerification("Missing intermediate details".into()) })?; let intermediate_details = - noise_certificate::Details::decode_from_slice(intermediate_details_bytes.as_slice())?; + 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 { @@ -221,8 +219,7 @@ impl HandshakeUtils { .details .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())?; + 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!( @@ -585,7 +582,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 +702,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 0149b4d74..df361e223 100644 --- a/wacore/src/history_sync.rs +++ b/wacore/src/history_sync.rs @@ -1869,6 +1869,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 f99f23c5f..5d462db5c 100644 --- a/wacore/src/messages.rs +++ b/wacore/src/messages.rs @@ -1120,6 +1120,7 @@ pub fn parse_message_info( } #[cfg(test)] +#[allow(clippy::disallowed_methods)] mod plaintext_view_tests { use super::*; @@ -1702,6 +1703,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/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 62b49b936..2124016fe 100755 --- a/wacore/src/types/events.rs +++ b/wacore/src/types/events.rs @@ -623,7 +623,11 @@ 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)] +/// +/// `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 { Connected(Connected), @@ -880,6 +884,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 +1557,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;