diff --git a/src/client/sender_keys.rs b/src/client/sender_keys.rs index d978c8e22..0df01f184 100644 --- a/src/client/sender_keys.rs +++ b/src/client/sender_keys.rs @@ -186,13 +186,12 @@ impl Client { /// Look up and consume a message by exact `ChatMessageId` (L1 cache then DB). async fn try_take_by_key(&self, key: &ChatMessageId) -> Option { - use prost::Message; let chat_str = key.chat.to_string(); let has_l1_cache = self.cache_config.recent_messages.capacity > 0; // L1 cache check (if capacity > 0) if has_l1_cache && let Some(bytes) = self.recent_messages.remove(key).await { - if let Ok(msg) = wa::Message::decode(bytes.as_slice()) { + if let Ok(msg) = waproto::codec::message_decode(bytes.as_slice()) { // Cache hit — consume the DB row in the background to avoid orphans. let backend = self.persistence_manager.backend(); let mid = key.id.clone(); @@ -219,7 +218,7 @@ impl Client { .take_sent_message(&chat_str, &key.id) .await { - Ok(Some(bytes)) => match wa::Message::decode(bytes.as_slice()) { + Ok(Some(bytes)) => match waproto::codec::message_decode(bytes.as_slice()) { Ok(msg) => Some(msg), Err(e) => { log::warn!( @@ -283,12 +282,11 @@ impl Client { /// (capacity 0) or misses; the DB is intentionally not read here so the caller /// can fall back to the consuming take + re-add path. async fn peek_by_key(&self, key: &ChatMessageId) -> Option { - use prost::Message; if self.cache_config.recent_messages.capacity == 0 { return None; } let bytes = self.recent_messages.get(key).await?; - match wa::Message::decode(bytes.as_slice()) { + match waproto::codec::message_decode(bytes.as_slice()) { Ok(msg) => Some(msg), Err(e) => { log::warn!( @@ -307,9 +305,8 @@ impl Client { /// With L1 cache, the DB write is backgrounded since the cache serves reads immediately. #[cfg_attr(feature = "tracing", tracing::instrument(name = "wa.session.add_recent_message", level = "debug", skip_all, fields(peer = %to.observe())))] pub(crate) async fn add_recent_message(&self, to: &Jid, id: &str, msg: &wa::Message) { - use prost::Message; let key = self.make_chat_message_id(to, id).await; - let bytes = msg.encode_to_vec(); + let bytes = waproto::codec::message_to_vec(msg); let has_l1_cache = self.cache_config.recent_messages.capacity > 0; if has_l1_cache { diff --git a/src/features/newsletter.rs b/src/features/newsletter.rs index 8f1f36cb8..0ba5f5f3d 100644 --- a/src/features/newsletter.rs +++ b/src/features/newsletter.rs @@ -8,7 +8,6 @@ use wacore::WireEnum; use crate::client::Client; use crate::features::mex::{MexError, mex_request}; -use prost::Message as ProtoMessage; use wacore::iq::mex_operations::{ create_newsletter, fetch_all_newsletters_metadata, fetch_newsletter, join_newsletter, leave_newsletter, update_newsletter, update_newsletter_user_setting, @@ -665,7 +664,9 @@ fn parse_newsletter_messages_response( msg_node .get_optional_child("plaintext") .and_then(|pt| match pt.content.as_deref() { - Some(NodeContentRef::Bytes(bytes)) => wa::Message::decode(bytes.as_ref()).ok(), + Some(NodeContentRef::Bytes(bytes)) => { + waproto::codec::message_decode(bytes.as_ref()).ok() + } _ => None, }); diff --git a/src/message/msg_secret.rs b/src/message/msg_secret.rs index 4d36d41fe..1dabdbe65 100644 --- a/src/message/msg_secret.rs +++ b/src/message/msg_secret.rs @@ -689,7 +689,7 @@ impl Client { }, }; - let msg = match wa::Message::decode(plaintext.as_slice()) { + let msg = match waproto::codec::message_decode(plaintext.as_slice()) { Ok(m) => m, Err(e) => { log::warn!( diff --git a/src/message/special.rs b/src/message/special.rs index 19cfcb015..618b21d81 100644 --- a/src/message/special.rs +++ b/src/message/special.rs @@ -23,7 +23,7 @@ impl Client { }; if let Some(bytes) = plaintext_node.content_bytes() { - match wa::Message::decode(bytes) { + match waproto::codec::message_decode(bytes) { Ok(msg) => { log::info!( "[msg:{}] Received newsletter plaintext message from {}", diff --git a/src/pdo.rs b/src/pdo.rs index ef0020efd..a6e886633 100644 --- a/src/pdo.rs +++ b/src/pdo.rs @@ -17,7 +17,6 @@ use crate::client::Client; use crate::types::message::MessageInfo; use log::{debug, info, warn}; -use prost::Message; use std::sync::Arc; use wacore::types::message::{ ChatMessageId, EditAttribute, MessageCategory, MessageSource, MsgMetaInfo, @@ -335,13 +334,14 @@ impl Client { return; }; - let web_msg_info = match wa::WebMessageInfo::decode(web_message_info_bytes.as_slice()) { - Ok(info) => info, - Err(e) => { - warn!("Failed to decode WebMessageInfo from PDO response: {:?}", e); - return; - } - }; + let web_msg_info = + match waproto::codec::web_message_info_decode(web_message_info_bytes.as_slice()) { + Ok(info) => info, + Err(e) => { + warn!("Failed to decode WebMessageInfo from PDO response: {:?}", e); + return; + } + }; let key = &web_msg_info.key; let remote_jid_str = key.remote_jid.as_deref().unwrap_or(""); diff --git a/src/send.rs b/src/send.rs index fe235d61a..690c32780 100644 --- a/src/send.rs +++ b/src/send.rs @@ -351,7 +351,6 @@ pub(crate) fn build_newsletter_edit_node( op: NewsletterEdit<'_>, ) -> Node { use crate::types::message::EditAttribute; - use prost::Message as _; let mut plaintext = NodeBuilder::new("plaintext"); let (edit, stanza_type, body) = match op { NewsletterEdit::Edit(m) => { @@ -361,7 +360,7 @@ pub(crate) fn build_newsletter_edit_node( ( EditAttribute::AdminEdit, wacore::send::stanza_type_from_message(m), - m.encode_to_vec(), + waproto::codec::message_to_vec(m), ) } NewsletterEdit::Revoke => (EditAttribute::AdminRevoke, "text", Vec::new()), @@ -488,7 +487,6 @@ impl Client { // Newsletters are not E2E encrypted — send as plaintext via SMAX stanza. // Matches WA Web's OutMessagePublishNewsletterRequest + ContentType mixins. if to.is_newsletter() { - use prost::Message as _; let stanza_type = stanza_type_override .map(StanzaType::as_wire) .unwrap_or_else(|| wacore::send::stanza_type_from_message(&message)); @@ -497,7 +495,11 @@ impl Client { if let Some(mt) = wacore::send::media_type_from_message(&message) { plaintext_builder = plaintext_builder.attr("mediatype", mt); } - let mut children = vec![plaintext_builder.bytes(message.encode_to_vec()).build()]; + let mut children = vec![ + plaintext_builder + .bytes(waproto::codec::message_to_vec(&message)) + .build(), + ]; children.extend(meta_node); children.extend(options.extra_stanza_nodes); let stanza = NodeBuilder::new("message") diff --git a/wacore/src/comment.rs b/wacore/src/comment.rs index e3ee8f8d2..d38e26964 100644 --- a/wacore/src/comment.rs +++ b/wacore/src/comment.rs @@ -8,7 +8,6 @@ //! outer envelope. use anyhow::{Result, ensure}; -use prost::Message; use waproto::whatsapp as wa; use crate::secret_enc_addon::{AddonContext, ModificationType, decrypt_addon, encrypt_addon}; @@ -43,7 +42,7 @@ pub fn encrypt_comment_with_secret( "message_secret must be {MESSAGE_SECRET_SIZE} bytes, got {}", message_secret.len() ); - let plaintext = inner.encode_to_vec(); + let plaintext = waproto::codec::message_to_vec(inner); encrypt_addon( &plaintext, message_secret, @@ -72,7 +71,7 @@ pub fn decrypt_comment_with_secret( message_secret, &comment_addon_ctx(parent_msg_id, parent_sender_jid, commenter_jid), )?; - Ok(wa::Message::decode(&plaintext[..])?) + Ok(waproto::codec::message_decode(&plaintext[..])?) } #[cfg(test)] diff --git a/wacore/src/message_edit.rs b/wacore/src/message_edit.rs index 37ac8ebea..120a83fea 100644 --- a/wacore/src/message_edit.rs +++ b/wacore/src/message_edit.rs @@ -22,7 +22,6 @@ //! that already handle `protocolMessage.editedMessage` can reuse their code. use anyhow::{Result, anyhow}; -use prost::Message; use crate::secret_enc_addon::{AddonContext, ModificationType, decrypt_addon, encrypt_addon}; @@ -66,7 +65,7 @@ pub fn encrypt_message_edit( ctx: &MessageEditContext<'_>, ) -> Result<(Vec, [u8; IV_SIZE])> { let mut plaintext = Vec::new(); - inner_message.encode(&mut plaintext)?; + waproto::codec::message_encode_into(inner_message, &mut plaintext); encrypt_addon(&plaintext, message_secret, &ctx.as_addon_ctx()) } @@ -100,7 +99,7 @@ pub fn decrypt_secret_encrypted( modification_type, }; let plaintext = decrypt_addon(enc_payload, iv, message_secret, &addon)?; - waproto::whatsapp::Message::decode(&plaintext[..]) + waproto::codec::message_decode(&plaintext[..]) .map_err(|e| anyhow!("Failed to decode inner secret-encrypted Message: {e}")) } @@ -180,6 +179,7 @@ pub fn decrypt_message_edit_with_fallback( #[cfg(test)] mod tests { use super::*; + use prost::Message as _; use waproto::whatsapp as wa; fn make_inner_edit(new_text: &str) -> wa::Message { @@ -289,7 +289,6 @@ mod tests { #[test] fn general_decrypt_roundtrips_non_edit_use_case() { use crate::secret_enc_addon::{AddonContext, ModificationType, encrypt_addon}; - use prost::Message as _; // A POLL_EDIT envelope: same shape as MESSAGE_EDIT, different use-case. let secret = [0x71u8; 32]; diff --git a/wacore/src/messages.rs b/wacore/src/messages.rs index 5b1b85434..b67856d39 100644 --- a/wacore/src/messages.rs +++ b/wacore/src/messages.rs @@ -24,8 +24,8 @@ impl MessageUtils { /// Encode + pad in a single pre-sized allocation. pub fn encode_and_pad(msg: &wa::Message) -> Vec { let pad = Self::random_pad_len(); - let mut buf = Vec::with_capacity(msg.encoded_len() + pad as usize); - msg.encode(&mut buf).expect("encode into pre-sized Vec"); + let mut buf = Vec::with_capacity(waproto::codec::message_encoded_len(msg) + pad as usize); + waproto::codec::message_encode_into(msg, &mut buf); buf.resize(buf.len() + pad as usize, pad); buf } @@ -47,10 +47,14 @@ impl MessageUtils { ) -> Vec { let pad = Self::random_pad_len(); let extra_len = extra_context.map_or(0, |c| { - len_delimited_len(TAG_MESSAGE_CONTEXT_INFO, c.encoded_len()) + len_delimited_len( + TAG_MESSAGE_CONTEXT_INFO, + waproto::codec::message_context_info_encoded_len(c), + ) }); - let mut buf = Vec::with_capacity(msg.encoded_len() + extra_len + pad as usize); - msg.encode(&mut buf).expect("encode into pre-sized Vec"); + let mut buf = + Vec::with_capacity(waproto::codec::message_encoded_len(msg) + extra_len + pad as usize); + waproto::codec::message_encode_into(msg, &mut buf); if let Some(c) = extra_context { push_message_field(TAG_MESSAGE_CONTEXT_INFO, c, &mut buf); } @@ -94,7 +98,7 @@ impl MessageUtils { let ctx = owned .message_context_info .get_or_insert_with(Default::default); - ctx.merge(extra.encode_to_vec().as_slice()) + ctx.merge(waproto::codec::message_context_info_to_vec(extra).as_slice()) .expect("merge MessageContextInfo"); } return Self::encode_dm_plaintexts_owned(owned, destination_jid); @@ -107,18 +111,19 @@ impl MessageUtils { const MAX_PAD: usize = 16; let mci_field_len = extra_context.map_or(0, |m| { - len_delimited_len(TAG_MESSAGE_CONTEXT_INFO, m.encoded_len()) + len_delimited_len( + TAG_MESSAGE_CONTEXT_INFO, + waproto::codec::message_context_info_encoded_len(m), + ) }); - let content_len = message.encoded_len(); + let content_len = waproto::codec::message_encoded_len(message); let dest = destination_jid.as_bytes(); // recipient = content (encoded once) + the extra message_context_info field. // Pre-size for content + the appended mci field + padding so it never // reallocates; the content bytes are then spliced into the own-device buffer. let mut recipient = Vec::with_capacity(content_len + mci_field_len + MAX_PAD); - message - .encode(&mut recipient) - .expect("encode into pre-sized Vec"); + waproto::codec::message_encode_into(message, &mut recipient); // own-device plaintext = Message { device_sent_message { destination_jid, // message }, [message_context_info] }. The DeviceSentMessage length is @@ -160,15 +165,16 @@ impl MessageUtils { // mci struct (not a temp Vec): it is small and encoded straight into each buffer. let mci = message.message_context_info.take(); let mci_field_len = mci.as_ref().map_or(0, |m| { - len_delimited_len(TAG_MESSAGE_CONTEXT_INFO, m.encoded_len()) + len_delimited_len( + TAG_MESSAGE_CONTEXT_INFO, + waproto::codec::message_context_info_encoded_len(m), + ) }); - let content_len = message.encoded_len(); + let content_len = waproto::codec::message_encoded_len(&message); let dest = destination_jid.as_bytes(); let mut recipient = Vec::with_capacity(content_len + mci_field_len + MAX_PAD); - message - .encode(&mut recipient) - .expect("encode into pre-sized Vec"); + waproto::codec::message_encode_into(&message, &mut recipient); let dsm_len = len_delimited_len(TAG_DSM_DESTINATION_JID, dest.len()) + len_delimited_len(TAG_DSM_MESSAGE, content_len); @@ -268,7 +274,7 @@ impl MessageUtils { /// runtime-independent portion of `handle_decrypted_plaintext`. pub fn decode_plaintext(padded_plaintext: &[u8], padding_version: u8) -> Result { let plaintext_slice = MessageUtils::unpad_message_ref(padded_plaintext, padding_version)?; - wa::Message::decode(plaintext_slice) + waproto::codec::message_decode(plaintext_slice) .map_err(|e| anyhow::anyhow!("Failed to decode decrypted plaintext: {e}")) } @@ -282,12 +288,15 @@ pub struct DmPlaintexts { pub own_devices: Vec, } -// Protobuf field numbers spliced by `encode_dm_plaintexts`. A wrong tag changes -// the decoded result, so the `splice_*` differential tests pin them against prost. -const TAG_DEVICE_SENT_MESSAGE: u64 = 31; // Message.device_sent_message -const TAG_MESSAGE_CONTEXT_INFO: u64 = 35; // Message.message_context_info -const TAG_DSM_DESTINATION_JID: u64 = 1; // DeviceSentMessage.destination_jid -const TAG_DSM_MESSAGE: u64 = 2; // DeviceSentMessage.message +// Protobuf field numbers spliced by `encode_dm_plaintexts`, sourced from the +// generated schema tags so a .proto renumber breaks here at compile time +// instead of silently changing the wire payload. The `splice_*` differential +// tests still pin the hand-written framing itself against prost. +const TAG_DEVICE_SENT_MESSAGE: u64 = waproto::tags::message::DEVICE_SENT_MESSAGE as u64; +const TAG_MESSAGE_CONTEXT_INFO: u64 = waproto::tags::message::MESSAGE_CONTEXT_INFO as u64; +const TAG_DSM_DESTINATION_JID: u64 = + waproto::tags::message::device_sent_message::DESTINATION_JID as u64; +const TAG_DSM_MESSAGE: u64 = waproto::tags::message::device_sent_message::MESSAGE as u64; /// Append a base-128 varint (protobuf wire format). #[inline] @@ -332,10 +341,13 @@ fn len_delimited_len(field: u64, payload_len: usize) -> usize { /// straight into `out` (no intermediate `Vec`). Used for the small /// `message_context_info` field on both plaintexts. #[inline] -fn push_message_field(field: u64, msg: &M, out: &mut Vec) { +fn push_message_field(field: u64, msg: &wa::MessageContextInfo, out: &mut Vec) { push_varint((field << 3) | 2, out); - push_varint(msg.encoded_len() as u64, out); - msg.encode(out).expect("encode into Vec is infallible"); + push_varint( + waproto::codec::message_context_info_encoded_len(msg) as u64, + out, + ); + waproto::codec::message_context_info_encode_into(msg, out); } /// Wrap a message into a DeviceSentMessage for own-device sync, hoisting diff --git a/wacore/src/reporting_token.rs b/wacore/src/reporting_token.rs index fb460e4a4..bd0ca728d 100644 --- a/wacore/src/reporting_token.rs +++ b/wacore/src/reporting_token.rs @@ -20,7 +20,6 @@ use anyhow::{Result, anyhow}; use hkdf::Hkdf; use hmac::{Hmac, KeyInit, Mac}; -use prost::Message; use sha2::Sha256; use wacore_binary::Jid; use wacore_binary::Node; @@ -512,7 +511,7 @@ pub fn generate_reporting_token_content(message: &wa::Message) -> Option if !should_include_reporting_token(message) { return None; } - let message_bytes = message.encode_to_vec(); + let message_bytes = waproto::codec::message_to_vec(message); extract_reporting_token_content(&message_bytes, REPORTING_FIELDS) } @@ -628,6 +627,7 @@ pub fn extract_message_secret(message: &wa::Message) -> Option<&[u8]> { #[cfg(test)] mod tests { use super::*; + use prost::Message; #[test] fn test_generate_message_secret() { diff --git a/wacore/src/types/events.rs b/wacore/src/types/events.rs index 1a685ebe5..b1209a4a3 100755 --- a/wacore/src/types/events.rs +++ b/wacore/src/types/events.rs @@ -4,7 +4,6 @@ use crate::types::message::MessageInfo; use crate::types::presence::{ChatPresence, ChatPresenceMedia, ReceiptType}; use bytes::Bytes; use chrono::{DateTime, Duration, Utc}; -use prost::Message; use serde::Serialize; use std::fmt; use std::sync::{Arc, Mutex, OnceLock, RwLock}; @@ -131,7 +130,9 @@ impl LazyHistorySync { // Cheap refcount bump; the lock is released before decoding so a // concurrent reader isn't blocked by the parse. let raw = self.locked_raw().clone()?; - wa::HistorySync::decode(&raw[..]).ok().map(Box::new) + waproto::codec::history_sync_decode(&raw[..]) + .ok() + .map(Box::new) }); // Free the raw bytes only AFTER the owned proto is committed, so a // concurrent clone never sees both gone (raw == None implies parsed set). diff --git a/waproto/src/lib.rs b/waproto/src/lib.rs index 90b008456..a1ac86d41 100644 --- a/waproto/src/lib.rs +++ b/waproto/src/lib.rs @@ -15,3 +15,73 @@ pub mod whatsapp { pub mod tags { include!(concat!(env!("OUT_DIR"), "/tags.rs")); } + +/// Pinned, non-generic codec entry points for the hottest protobuf roots. +/// +/// prost's `Message` methods are generic, so rustc instantiates them in every +/// crate that calls them; the per-crate copies carry distinct +/// instantiating-crate symbol hashes that LTO cannot merge, and each calling +/// crate ends up shipping its own copy of the full encode or decode tree +/// (`whatsapp::Message::encode_raw` alone is ~160 KiB per copy). Routing +/// calls through these functions pins a single instantiation in this crate; +/// `#[inline(never)]` keeps MIR inlining from re-expanding them at call +/// sites, which would silently reintroduce the per-crate copies. +/// +/// Decode helpers take `&[u8]` and decode via `&mut &[u8]`, the buffer shape +/// the rest of the workspace already instantiates, so no second buffer-type +/// tree exists. +pub mod codec { + use crate::whatsapp; + use prost::Message as _; + + #[inline(never)] + pub fn message_encoded_len(msg: &whatsapp::Message) -> usize { + msg.encoded_len() + } + + /// Append the encoded message to `out`. Infallible into a `Vec`. + #[inline(never)] + pub fn message_encode_into(msg: &whatsapp::Message, out: &mut Vec) { + msg.encode(out).expect("encode into Vec is infallible"); + } + + #[inline(never)] + pub fn message_to_vec(msg: &whatsapp::Message) -> Vec { + msg.encode_to_vec() + } + + #[inline(never)] + pub fn message_decode(mut bytes: &[u8]) -> Result { + whatsapp::Message::decode(&mut bytes) + } + + #[inline(never)] + pub fn web_message_info_decode( + mut bytes: &[u8], + ) -> Result { + whatsapp::WebMessageInfo::decode(&mut bytes) + } + + #[inline(never)] + pub fn history_sync_decode( + mut bytes: &[u8], + ) -> Result { + whatsapp::HistorySync::decode(&mut bytes) + } + + #[inline(never)] + pub fn message_context_info_encoded_len(mci: &whatsapp::MessageContextInfo) -> usize { + mci.encoded_len() + } + + /// Append the encoded `MessageContextInfo` to `out`. Infallible into a `Vec`. + #[inline(never)] + pub fn message_context_info_encode_into(mci: &whatsapp::MessageContextInfo, out: &mut Vec) { + mci.encode(out).expect("encode into Vec is infallible"); + } + + #[inline(never)] + pub fn message_context_info_to_vec(mci: &whatsapp::MessageContextInfo) -> Vec { + mci.encode_to_vec() + } +}