diff --git a/src/client.rs b/src/client.rs index c7b3c3589..643f454a9 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1824,10 +1824,6 @@ impl Client { self.send_raw_bytes(buf).await } - pub(crate) async fn handle_unimplemented(&self, tag: &str) { - log::debug!("Unhandled stanza: <{tag}>"); - } - pub async fn set_passive(&self, passive: bool) -> Result<(), crate::request::IqError> { use wacore::iq::passive::PassiveModeSpec; self.execute(PassiveModeSpec::new(passive)).await @@ -2556,7 +2552,7 @@ impl Client { if want_snapshot { "true" } else { "false" }, ); if !want_snapshot { - builder = builder.attr("version", state.version.to_string()); + builder = builder.attr("version", state.version); } collection_nodes.push(builder.build()); } @@ -2769,7 +2765,7 @@ impl Client { if want_snapshot { "true" } else { "false" }, ); if !want_snapshot { - collection_builder = collection_builder.attr("version", state.version.to_string()); + collection_builder = collection_builder.attr("version", state.version); } let sync_node = NodeBuilder::new("sync") .children([collection_builder.build()]) @@ -2977,7 +2973,7 @@ impl Client { let collection_node = NodeBuilder::new("collection") .attr("name", collection_name) - .attr("version", base_version.to_string()) + .attr("version", base_version) .attr("return_snapshot", "false") .children([NodeBuilder::new("patch").bytes(patch_bytes).build()]) .build(); @@ -3547,7 +3543,7 @@ impl Client { .attr("to", to) .attr("type", "reaction") .attr("id", &request_id) - .attr("server_id", server_id.to_string()) + .attr("server_id", server_id) .children([NodeBuilder::new("reaction").attr("code", reaction).build()]) .build(); @@ -4969,9 +4965,7 @@ mod tests { // Create a node with a 't' attribute let server_time = wacore::time::now_secs() + 10; // Server is 10 seconds ahead - let node = NodeBuilder::new("success") - .attr("t", server_time.to_string()) - .build(); + let node = NodeBuilder::new("success").attr("t", server_time).build(); // Update the offset client.update_server_time_offset(&node.as_node_ref()); diff --git a/src/client/sessions.rs b/src/client/sessions.rs index 58ed27e81..ca387b69b 100644 --- a/src/client/sessions.rs +++ b/src/client/sessions.rs @@ -26,8 +26,11 @@ impl Client { // Signal that offline sync is complete - post-login tasks are waiting for this. // This mimics WhatsApp Web's offlineDeliveryEnd event. // Use compare_exchange to ensure we only run this once (add_permits is NOT idempotent). - // Install the wider semaphore BEFORE flipping the flag so that any thread - // observing offline_sync_completed=true already sees the 64-permit semaphore. + // Readers that observe offline_sync_completed=true short-circuit without touching + // the semaphore (wait_for_offline_delivery_end returns early), so the ordering of + // flag flip vs. semaphore swap below is not observable: any in-flight worker keeps + // using its old 1-permit Arc and drains normally; newly-spawned workers pick up the + // 64-permit semaphore via read_message_semaphore(). if self .offline_sync_completed .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) diff --git a/src/features/newsletter.rs b/src/features/newsletter.rs index 11f033433..421bac34a 100644 --- a/src/features/newsletter.rs +++ b/src/features/newsletter.rs @@ -390,9 +390,9 @@ impl<'a> Newsletter<'a> { count: u32, before: Option, ) -> Result, anyhow::Error> { - let mut messages_node = NodeBuilder::new("messages").attr("count", count.to_string()); + let mut messages_node = NodeBuilder::new("messages").attr("count", count); if let Some(before_id) = before { - messages_node = messages_node.attr("before", before_id.to_string()); + messages_node = messages_node.attr("before", before_id); } let iq = InfoQuery::get( diff --git a/src/handlers/macros.rs b/src/handlers/macros.rs new file mode 100644 index 000000000..8d0d58944 --- /dev/null +++ b/src/handlers/macros.rs @@ -0,0 +1,35 @@ +/// Extract the required `from` JID attribute from a `NodeRef`, or log a warning +/// and return from the enclosing function. +/// +/// Handler functions across the crate share the exact same pattern: pull `from`, +/// bail out with a warning if it is missing. This macro collapses that five-line +/// `match` into one line while preserving the (optional) log target. +/// +/// # Variants +/// +/// * `require_from_jid!(node, "Context")` — warns without a target, then `return;`. +/// * `require_from_jid!(node, target: "Client/Foo", "Context")` — warns with target. +#[macro_export] +macro_rules! require_from_jid { + ($node:expr, $context:literal) => { + match $node.attrs().optional_jid("from") { + Some(jid) => jid, + None => { + ::log::warn!(concat!($context, " missing required 'from' attribute")); + return; + } + } + }; + ($node:expr, target: $target:literal, $context:literal) => { + match $node.attrs().optional_jid("from") { + Some(jid) => jid, + None => { + ::log::warn!( + target: $target, + concat!($context, " missing required 'from' attribute"), + ); + return; + } + } + }; +} diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 3e18079d8..79ee7f7b5 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -3,10 +3,11 @@ pub mod call; pub mod chatstate; pub mod ib; pub mod iq; +#[macro_use] +mod macros; pub mod message; pub mod notification; pub mod presence; pub mod receipt; pub mod router; pub mod traits; -pub mod unimplemented; diff --git a/src/handlers/notification.rs b/src/handlers/notification.rs index 8f1a2b6c0..adf68b2dd 100755 --- a/src/handlers/notification.rs +++ b/src/handlers/notification.rs @@ -295,10 +295,7 @@ fn handle_digest_key(client: &Arc) { /// WA Web defers this when offline. We process immediately because all cleanup /// is local-only, and `ensure_e2e_sessions` self-defers via `wait_for_offline_delivery_end`. async fn handle_identity_change(client: &Arc, node: &NodeRef<'_>) { - let Some(from_jid) = node.attrs().optional_jid("from") else { - warn!("Identity change notification missing 'from' attribute"); - return; - }; + let from_jid = crate::require_from_jid!(node, "Identity change notification"); // Only primary device identity changes matter if from_jid.device != 0 { @@ -530,13 +527,11 @@ async fn handle_account_sync_devices( devices_node: &NodeRef<'_>, ) { // Extract the "from" JID - this is the account the notification is about - let from_jid = match node.attrs().optional_jid("from") { - Some(jid) => jid, - None => { - warn!(target: "Client/AccountSync", "account_sync devices missing 'from' attribute"); - return; - } - }; + let from_jid = crate::require_from_jid!( + node, + target: "Client/AccountSync", + "account_sync devices" + ); // Get our own JIDs (PN and LID) to verify this is about our account let device_snapshot = client.persistence_manager.get_device_snapshot().await; @@ -869,13 +864,11 @@ async fn handle_business_notification(client: &Arc, node: &NodeRef<'_>) /// /// ``` fn handle_picture_notification(client: &Arc, node: &NodeRef<'_>) { - let from = match node.attrs().optional_jid("from") { - Some(jid) => jid, - None => { - warn!(target: "Client/Picture", "picture notification missing 'from' attribute"); - return; - } - }; + let from = crate::require_from_jid!( + node, + target: "Client/Picture", + "picture notification" + ); let timestamp = notification_timestamp(node); @@ -959,13 +952,11 @@ fn handle_picture_notification(client: &Arc, node: &NodeRef<'_>) { /// /// ``` fn handle_status_notification(client: &Arc, node: &NodeRef<'_>) { - let from = match node.attrs().optional_jid("from") { - Some(jid) => jid, - None => { - warn!(target: "Client/Status", "status notification missing 'from' attribute"); - return; - } - }; + let from = crate::require_from_jid!( + node, + target: "Client/Status", + "status notification" + ); let timestamp = notification_timestamp(node); diff --git a/src/handlers/unimplemented.rs b/src/handlers/unimplemented.rs deleted file mode 100644 index 213bc4e11..000000000 --- a/src/handlers/unimplemented.rs +++ /dev/null @@ -1,54 +0,0 @@ -use super::traits::StanzaHandler; -use crate::client::Client; -use async_trait::async_trait; -use std::sync::Arc; - -/// Handler for stanza types that are not yet fully implemented. -/// -/// This handler provides a placeholder for stanza types like: -/// - `` - Voice/video call signaling -/// - `` - User presence updates -/// - `` - Typing indicators -/// -/// These will be logged and handled minimally until full implementations are added. -pub struct UnimplementedHandler { - tags: Vec<&'static str>, -} - -impl UnimplementedHandler { - pub fn new(tags: Vec<&'static str>) -> Self { - Self { tags } - } - - pub fn for_call() -> Self { - Self::new(vec!["call"]) - } - - pub fn for_presence() -> Self { - Self::new(vec!["presence"]) - } -} - -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -impl StanzaHandler for UnimplementedHandler { - fn tag(&self) -> &'static str { - // For multi-tag handlers, we'll register multiple instances - // This method should only be called after registration - if self.tags.len() == 1 { - self.tags[0] - } else { - panic!("UnimplementedHandler with multiple tags should be registered individually") - } - } - - async fn handle( - &self, - client: Arc, - node: Arc, - _cancelled: &mut bool, - ) -> bool { - client.handle_unimplemented(node.tag()).await; - true - } -} diff --git a/src/retry.rs b/src/retry.rs index 53cdec9f5..270ac122a 100644 --- a/src/retry.rs +++ b/src/retry.rs @@ -860,13 +860,13 @@ impl Client { let mut retry_builder = NodeBuilder::new("retry") .attr("v", "1") .attr("id", info.id.clone()) - .attr("t", info.timestamp.timestamp().to_string()) - .attr("count", retry_count.to_string()); + .attr("t", info.timestamp.timestamp()) + .attr("count", retry_count); // Include the error code if it's not UnknownError (matches WhatsApp Web's behavior // where error is only included when there's a specific reason) if reason != RetryReason::UnknownError { - retry_builder = retry_builder.attr("error", (reason as u8).to_string()); + retry_builder = retry_builder.attr("error", reason as u8); } let retry_node = retry_builder.build(); @@ -1022,7 +1022,7 @@ impl Client { let enc_rekey_node = NodeBuilder::new("enc_rekey") .attr("call-creator", call_creator) .attr("call-id", call_id) - .attr("count", retry_count.to_string()) + .attr("count", retry_count) .build(); let registration_node = NodeBuilder::new("registration") @@ -1326,7 +1326,7 @@ mod tests { let enc_rekey_node = NodeBuilder::new("enc_rekey") .attr("call-creator", call_creator) .attr("call-id", call_id) - .attr("count", retry_count.to_string()) + .attr("count", retry_count) .build(); let registration_node = NodeBuilder::new("registration") diff --git a/src/socket/noise_socket.rs b/src/socket/noise_socket.rs index 0c9560ce1..1bd2a7b48 100644 --- a/src/socket/noise_socket.rs +++ b/src/socket/noise_socket.rs @@ -20,8 +20,6 @@ struct SendJob { } pub struct NoiseSocket { - #[allow(dead_code)] // Kept for potential future spawns - runtime: Arc, read_key: Arc, read_counter: Arc, /// Channel to send jobs to the dedicated sender task. @@ -60,7 +58,6 @@ impl NoiseSocket { ))); Self { - runtime, read_key, read_counter: Arc::new(AtomicU32::new(0)), send_job_tx, diff --git a/src/unified_session.rs b/src/unified_session.rs index 1a56dde8b..2c09073ca 100644 --- a/src/unified_session.rs +++ b/src/unified_session.rs @@ -144,9 +144,7 @@ mod tests { let manager = UnifiedSessionManager::new(); let server_time = wacore::time::now_secs() + 10; - let node = NodeBuilder::new("success") - .attr("t", server_time.to_string()) - .build(); + let node = NodeBuilder::new("success").attr("t", server_time).build(); manager.update_server_time_offset(&node.as_node_ref()); diff --git a/wacore/binary/Cargo.toml b/wacore/binary/Cargo.toml index 424aa3957..43fb81f5f 100644 --- a/wacore/binary/Cargo.toml +++ b/wacore/binary/Cargo.toml @@ -39,3 +39,7 @@ serde_json = { workspace = true, features = ["std"] } [[bench]] name = "binary_benchmark" harness = false + +[[bench]] +name = "numeric_attr_benchmark" +harness = false diff --git a/wacore/binary/benches/numeric_attr_benchmark.rs b/wacore/binary/benches/numeric_attr_benchmark.rs new file mode 100644 index 000000000..b7836bd2b --- /dev/null +++ b/wacore/binary/benches/numeric_attr_benchmark.rs @@ -0,0 +1,113 @@ +use iai_callgrind::{ + Callgrind, LibraryBenchmarkConfig, library_benchmark, library_benchmark_group, main, +}; +use std::hint::black_box; + +use compact_str::CompactString; +use wacore_binary::node::NodeValue; + +/// Baseline: what the codebase does today — `value.to_string()` then Into. +/// Heap-allocates a `String`, then `CompactString::from(String)` re-uses or copies. +#[inline(never)] +fn baseline_u32(n: u32) -> NodeValue { + NodeValue::from(n.to_string()) +} + +#[inline(never)] +fn baseline_u64(n: u64) -> NodeValue { + NodeValue::from(n.to_string()) +} + +#[inline(never)] +fn baseline_i64(n: i64) -> NodeValue { + NodeValue::from(n.to_string()) +} + +/// Proposed path: use `itoa` to format into a stack buffer, then `CompactString::from(&str)` +/// which inlines for strings <= 24 bytes on 64-bit (all integers fit). +#[inline(never)] +fn proposed_u32(n: u32) -> NodeValue { + let mut buf = itoa::Buffer::new(); + NodeValue::String(CompactString::from(buf.format(n))) +} + +#[inline(never)] +fn proposed_u64(n: u64) -> NodeValue { + let mut buf = itoa::Buffer::new(); + NodeValue::String(CompactString::from(buf.format(n))) +} + +#[inline(never)] +fn proposed_i64(n: i64) -> NodeValue { + let mut buf = itoa::Buffer::new(); + NodeValue::String(CompactString::from(buf.format(n))) +} + +#[library_benchmark] +fn bench_baseline_u32() -> NodeValue { + black_box(baseline_u32(black_box(12345))) +} + +#[library_benchmark] +fn bench_proposed_u32() -> NodeValue { + black_box(proposed_u32(black_box(12345))) +} + +#[library_benchmark] +fn bench_baseline_u64() -> NodeValue { + black_box(baseline_u64(black_box(1234567890123u64))) +} + +#[library_benchmark] +fn bench_proposed_u64() -> NodeValue { + black_box(proposed_u64(black_box(1234567890123u64))) +} + +#[library_benchmark] +fn bench_baseline_i64() -> NodeValue { + black_box(baseline_i64(black_box(-1234567890123i64))) +} + +#[library_benchmark] +fn bench_proposed_i64() -> NodeValue { + black_box(proposed_i64(black_box(-1234567890123i64))) +} + +#[library_benchmark] +fn bench_baseline_loop_100_u64() -> u64 { + let mut acc: u64 = 0; + for i in 0u64..100 { + let v = baseline_u64(black_box(100_000 + i)); + acc = acc.wrapping_add(v.as_str().len() as u64); + } + black_box(acc) +} + +#[library_benchmark] +fn bench_proposed_loop_100_u64() -> u64 { + let mut acc: u64 = 0; + for i in 0u64..100 { + let v = proposed_u64(black_box(100_000 + i)); + acc = acc.wrapping_add(v.as_str().len() as u64); + } + black_box(acc) +} + +library_benchmark_group!( + name = bench_group; + benchmarks = + bench_baseline_u32, + bench_proposed_u32, + bench_baseline_u64, + bench_proposed_u64, + bench_baseline_i64, + bench_proposed_i64, + bench_baseline_loop_100_u64, + bench_proposed_loop_100_u64, +); + +main!( + config = LibraryBenchmarkConfig::default() + .tool(Callgrind::default()); + library_benchmark_groups = bench_group +); diff --git a/wacore/binary/src/node.rs b/wacore/binary/src/node.rs index df2e04969..c80713443 100644 --- a/wacore/binary/src/node.rs +++ b/wacore/binary/src/node.rs @@ -273,6 +273,31 @@ impl From<&Jid> for NodeValue { } } +macro_rules! impl_from_integer_for_nodevalue { + ($($t:ty),* $(,)?) => { + $( + impl From<$t> for NodeValue { + #[inline] + fn from(n: $t) -> Self { + let mut buf = itoa::Buffer::new(); + NodeValue::String(CompactString::from(buf.format(n))) + } + } + )* + }; +} + +impl_from_integer_for_nodevalue!( + u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize +); + +impl From for NodeValue { + #[inline] + fn from(b: bool) -> Self { + NodeValue::String(CompactString::from(if b { "true" } else { "false" })) + } +} + /// A collection of node attributes stored as key-value pairs. /// Uses a Vec internally for better cache locality with small attribute counts (typically 3-6). /// Values can be either strings or JIDs, avoiding stringification overhead for JID attributes. diff --git a/wacore/derive/src/lib.rs b/wacore/derive/src/lib.rs index 850191d7c..07236c0fb 100644 --- a/wacore/derive/src/lib.rs +++ b/wacore/derive/src/lib.rs @@ -162,13 +162,13 @@ pub fn derive_protocol_node(input: TokenStream) -> TokenStream { (AttrType::U64, true) | (AttrType::U32, true) => { quote! { if let Some(v) = self.#field_ident { - builder = builder.attr(#attr_name, v.to_string()); + builder = builder.attr(#attr_name, v); } } } (AttrType::U64, false) | (AttrType::U32, false) => { quote! { - builder = builder.attr(#attr_name, self.#field_ident.to_string()); + builder = builder.attr(#attr_name, self.#field_ident); } } } diff --git a/wacore/src/iq/dirty.rs b/wacore/src/iq/dirty.rs index 2ecfec0a5..64c467071 100644 --- a/wacore/src/iq/dirty.rs +++ b/wacore/src/iq/dirty.rs @@ -102,7 +102,7 @@ impl IqSpec for CleanDirtyBitsSpec { .map(|bit| { let mut builder = NodeBuilder::new("clean").attr("type", bit.dirty_type.as_str()); if let Some(ts) = bit.timestamp { - builder = builder.attr("timestamp", ts.to_string()); + builder = builder.attr("timestamp", ts); } builder.build() }) diff --git a/wacore/src/iq/groups.rs b/wacore/src/iq/groups.rs index ded15bce4..8c5f4ff2c 100644 --- a/wacore/src/iq/groups.rs +++ b/wacore/src/iq/groups.rs @@ -391,7 +391,7 @@ pub fn build_create_group_node(options: &GroupCreateOptions) -> Node { if let Some(expiration) = &options.ephemeral_expiration { children.push( NodeBuilder::new("ephemeral") - .attr("expiration", expiration.to_string()) + .attr("expiration", *expiration) .build(), ); } @@ -572,10 +572,10 @@ impl ProtocolNode for GroupInfoResponse { children.push(NodeBuilder::new("announcement").build()); } if self.ephemeral_expiration > 0 || self.ephemeral_trigger.is_some() { - let mut eph = NodeBuilder::new("ephemeral") - .attr("expiration", self.ephemeral_expiration.to_string()); + let mut eph = + NodeBuilder::new("ephemeral").attr("expiration", self.ephemeral_expiration); if let Some(trigger) = self.ephemeral_trigger { - eph = eph.attr("trigger", trigger.to_string()); + eph = eph.attr("trigger", trigger); } children.push(eph.build()); } @@ -615,7 +615,7 @@ impl ProtocolNode for GroupInfoResponse { desc_builder = desc_builder.attr("participant", owner); } if let Some(t) = self.description_time { - desc_builder = desc_builder.attr("t", t.to_string()); + desc_builder = desc_builder.attr("t", t); } if let Some(ref desc) = self.description { desc_builder = desc_builder.children([NodeBuilder::new("body") @@ -659,7 +659,7 @@ impl ProtocolNode for GroupInfoResponse { children.push( NodeBuilder::new("growth_locked") .attr("type", &gl.lock_type) - .attr("expiration", gl.expiration.to_string()) + .attr("expiration", gl.expiration) .build(), ); } @@ -691,16 +691,16 @@ impl ProtocolNode for GroupInfoResponse { builder = builder.attr("creator", creator); } if let Some(creation_time) = self.creation_time { - builder = builder.attr("creation", creation_time.to_string()); + builder = builder.attr("creation", creation_time); } if let Some(subject_time) = self.subject_time { - builder = builder.attr("s_t", subject_time.to_string()); + builder = builder.attr("s_t", subject_time); } if let Some(subject_owner) = self.subject_owner { builder = builder.attr("s_o", subject_owner); } if let Some(size) = self.size { - builder = builder.attr("size", size.to_string()); + builder = builder.attr("size", size); } builder.children(children).build() @@ -1652,7 +1652,7 @@ impl IqSpec for SetGroupEphemeralIq { fn build_iq(&self) -> InfoQuery<'static> { let node = match self.expiration { Some(exp) => NodeBuilder::new("ephemeral") - .attr("expiration", exp.to_string()) + .attr("expiration", exp.get()) .build(), None => NodeBuilder::new("not_ephemeral").build(), }; @@ -2246,8 +2246,8 @@ impl IqSpec for AcceptGroupInviteV4Iq { Some(NodeContent::Nodes(vec![ NodeBuilder::new("accept") .attr("code", &self.code) - .attr("expiration", self.expiration.to_string()) - .attr("admin", self.admin_jid.to_string()) + .attr("expiration", self.expiration) + .attr("admin", &self.admin_jid) .build(), ])), ) @@ -3513,4 +3513,46 @@ mod tests { assert!(response.description_owner.is_none()); assert!(response.description_time.is_none()); } + + /// Locks down the trait conversions used by `AcceptGroupInviteV4Iq::build_iq`: + /// `i64` for `expiration` and `&Jid` for `admin`. Exercises the exact + /// `NodeBuilder::new("accept")` path that the perf refactor changed and + /// asserts the serialized attribute strings so any drift in numeric + /// formatting or JID `Display` impl trips here first. + #[test] + fn test_accept_group_invite_v4_iq_attrs() { + let group_jid: Jid = "120363000000000042@g.us".parse().unwrap(); + let admin_jid: Jid = "5511999887766@s.whatsapp.net".parse().unwrap(); + let code = "A1B2C3D4".to_string(); + let expiration: i64 = 1_700_000_123; + + let spec = AcceptGroupInviteV4Iq::new( + group_jid.clone(), + code.clone(), + expiration, + admin_jid.clone(), + ); + let iq = spec.build_iq(); + + assert_eq!(iq.to, group_jid); + + let Some(NodeContent::Nodes(nodes)) = &iq.content else { + panic!("expected nodes content"); + }; + let accept = &nodes[0]; + assert_eq!(accept.tag, "accept"); + + assert_eq!( + accept.attrs().optional_string("code").as_deref(), + Some(code.as_str()), + ); + assert_eq!( + accept.attrs().optional_string("expiration").as_deref(), + Some("1700000123"), + ); + assert_eq!( + accept.attrs().optional_string("admin").as_deref(), + Some("5511999887766@s.whatsapp.net"), + ); + } } diff --git a/wacore/src/iq/mediaconn.rs b/wacore/src/iq/mediaconn.rs index 2e83ef3b7..dd69e7514 100644 --- a/wacore/src/iq/mediaconn.rs +++ b/wacore/src/iq/mediaconn.rs @@ -279,19 +279,19 @@ impl ProtocolNode for MediaConnResponseExtended { fn into_node(self) -> Node { let mut builder = NodeBuilder::new("media_conn") .attr("auth", &self.auth) - .attr("ttl", self.ttl.to_string()); + .attr("ttl", self.ttl); if let Some(auth_ttl) = self.auth_ttl { - builder = builder.attr("auth_ttl", auth_ttl.to_string()); + builder = builder.attr("auth_ttl", auth_ttl); } if let Some(max_buckets) = self.max_buckets { - builder = builder.attr("max_buckets", max_buckets.to_string()); + builder = builder.attr("max_buckets", max_buckets); } if let Some(ref ip_token) = self.ip_token { builder = builder.attr("ip_token", ip_token); } if let Some(set_ip_token) = self.set_ip_token { - builder = builder.attr("set_ip_token", set_ip_token.to_string()); + builder = builder.attr("set_ip_token", set_ip_token); } let host_nodes: Vec = self.hosts.into_iter().map(|h| h.into_node()).collect(); diff --git a/wacore/src/iq/node.rs b/wacore/src/iq/node.rs index 414111eea..803e80c1d 100644 --- a/wacore/src/iq/node.rs +++ b/wacore/src/iq/node.rs @@ -2,7 +2,7 @@ use std::borrow::Cow; use crate::protocol::ProtocolNode; use anyhow::anyhow; -use wacore_binary::NodeRef; +use wacore_binary::{NodeContentRef, NodeRef}; /// Get a required child node by tag from a `NodeRef`. pub(crate) fn required_child<'a>( @@ -39,3 +39,28 @@ pub(crate) fn collect_children( .map(|child| T::try_from_node_ref(child)) .collect() } + +/// Extract binary content from an optional `NodeRef` as `Vec`. +/// Returns an empty vector if the node is `None` or does not hold byte content. +pub(crate) fn extract_content_bytes(node: Option<&NodeRef<'_>>) -> Vec { + node.and_then(|n| match n.content.as_deref() { + Some(NodeContentRef::Bytes(b)) => Some(b.to_vec()), + _ => None, + }) + .unwrap_or_default() +} + +/// Extract binary content from an optional `NodeRef` as a big-endian `u32`. +/// Returns 0 if the node is missing or does not hold byte content. Truncates to 4 bytes. +pub(crate) fn extract_content_uint(node: Option<&NodeRef<'_>>) -> u32 { + node.and_then(|n| match n.content.as_deref() { + Some(NodeContentRef::Bytes(b)) => { + let mut buf = [0u8; 4]; + let len = b.len().min(4); + buf[4 - len..].copy_from_slice(&b[..len]); + Some(u32::from_be_bytes(buf)) + } + _ => None, + }) + .unwrap_or(0) +} diff --git a/wacore/src/iq/prekeys.rs b/wacore/src/iq/prekeys.rs index 72734d1a9..fa32f9787 100644 --- a/wacore/src/iq/prekeys.rs +++ b/wacore/src/iq/prekeys.rs @@ -37,7 +37,7 @@ //! //! ``` -use crate::iq::node::required_child; +use crate::iq::node::{extract_content_bytes, extract_content_uint, required_child}; use crate::iq::spec::IqSpec; use crate::prekeys::PreKeyUtils; use crate::protocol::ProtocolNode; @@ -51,29 +51,6 @@ use wacore_binary::{Node, NodeContent, NodeContentRef, NodeRef}; // Re-export PreKeyBundle for convenience pub use crate::libsignal::protocol::{PreKeyBundle, PublicKey}; -/// Extract binary content from an optional `NodeRef` as `Vec`. -fn extract_content_bytes(node: Option<&NodeRef<'_>>) -> Vec { - node.and_then(|n| match n.content.as_deref() { - Some(NodeContentRef::Bytes(b)) => Some(b.to_vec()), - _ => None, - }) - .unwrap_or_default() -} - -/// Extract binary content from an optional `NodeRef` as a big-endian unsigned integer. -fn extract_content_uint(node: Option<&NodeRef<'_>>) -> u32 { - node.and_then(|n| match n.content.as_deref() { - Some(NodeContentRef::Bytes(b)) => { - let mut buf = [0u8; 4]; - let len = b.len().min(4); - buf[4 - len..].copy_from_slice(&b[..len]); - Some(u32::from_be_bytes(buf)) - } - _ => None, - }) - .unwrap_or(0) -} - /// Pre-key count response. #[derive(Debug, Clone)] pub struct PreKeyCountResponse { diff --git a/wacore/src/iq/privacy.rs b/wacore/src/iq/privacy.rs index 9a38622f9..e5feb16d0 100644 --- a/wacore/src/iq/privacy.rs +++ b/wacore/src/iq/privacy.rs @@ -373,7 +373,7 @@ impl IqSpec for SetDefaultDisappearingModeSpec { Jid::new("", Server::Pn), Some(NodeContent::Nodes(vec![ NodeBuilder::new("disappearing_mode") - .attr("duration", self.duration.to_string()) + .attr("duration", self.duration) .build(), ])), ) diff --git a/wacore/src/iq/props.rs b/wacore/src/iq/props.rs index b2cf573e3..ce30450dc 100644 --- a/wacore/src/iq/props.rs +++ b/wacore/src/iq/props.rs @@ -98,11 +98,11 @@ impl crate::protocol::ProtocolNode for AbProp { fn into_node(self) -> Node { let mut builder = NodeBuilder::new("prop") - .attr("config_code", self.config_code.to_string()) + .attr("config_code", self.config_code) .attr("config_value", &*self.config_value); if let Some(expo_key) = self.config_expo_key { - builder = builder.attr("config_expo_key", expo_key.to_string()); + builder = builder.attr("config_expo_key", expo_key); } builder.build() @@ -150,8 +150,8 @@ impl crate::protocol::ProtocolNode for SamplingProp { fn into_node(self) -> Node { NodeBuilder::new("prop") - .attr("event_code", self.event_code.to_string()) - .attr("sampling_weight", self.sampling_weight.to_string()) + .attr("event_code", self.event_code) + .attr("sampling_weight", self.sampling_weight) .build() } @@ -270,12 +270,12 @@ impl crate::protocol::ProtocolNode for PropsResponse { builder = builder.attr("hash", hash); } if let Some(refresh) = self.refresh { - builder = builder.attr("refresh", refresh.to_string()); + builder = builder.attr("refresh", refresh); } if let Some(refresh_id) = self.refresh_id { - builder = builder.attr("refresh_id", refresh_id.to_string()); + builder = builder.attr("refresh_id", refresh_id); } - builder = builder.attr("delta_update", self.delta_update.to_string()); + builder = builder.attr("delta_update", self.delta_update); builder.build() } @@ -362,7 +362,7 @@ impl IqSpec for PropsSpec { } if let Some(refresh_id) = self.refresh_id { - builder = builder.attr("refresh_id", refresh_id.to_string()); + builder = builder.attr("refresh_id", refresh_id); } InfoQuery::get( diff --git a/wacore/src/iq/tctoken.rs b/wacore/src/iq/tctoken.rs index 7f71baf8e..5139e4af7 100644 --- a/wacore/src/iq/tctoken.rs +++ b/wacore/src/iq/tctoken.rs @@ -255,7 +255,7 @@ impl IqSpec for IssuePrivacyTokensSpec { .map(|jid| { NodeBuilder::new("token") .attr("jid", jid) - .attr("t", self.timestamp.to_string()) + .attr("t", self.timestamp) .attr("type", "trusted_contact") .build() }) @@ -383,7 +383,7 @@ pub fn build_tc_token_node(token: &[u8]) -> Node { /// Build a `` stanza child with timestamp attribute. pub fn build_tc_token_node_with_timestamp(token: &[u8], timestamp: i64) -> Node { NodeBuilder::new("tctoken") - .attr("t", timestamp.to_string()) + .attr("t", timestamp) .bytes(token.to_vec()) .build() } diff --git a/wacore/src/media_retry.rs b/wacore/src/media_retry.rs index bb24a7426..c0a63e972 100644 --- a/wacore/src/media_retry.rs +++ b/wacore/src/media_retry.rs @@ -138,7 +138,7 @@ pub fn build_media_retry_receipt( let mut rmr_builder = NodeBuilder::new("rmr") .attr("jid", chat_jid) - .attr("from_me", is_from_me.to_string()); + .attr("from_me", is_from_me); if let Some(p) = participant { rmr_builder = rmr_builder.attr("participant", p); diff --git a/wacore/src/pair.rs b/wacore/src/pair.rs index 8a4cc1e07..603deb651 100644 --- a/wacore/src/pair.rs +++ b/wacore/src/pair.rs @@ -248,7 +248,7 @@ impl PairUtils { ) -> Node { let response_content = NodeBuilder::new("pair-device-sign") .children([NodeBuilder::new("device-identity") - .attr("key-index", key_index.to_string()) + .attr("key-index", key_index) .bytes(self_signed_identity_bytes) .build()]) .build(); diff --git a/wacore/src/send.rs b/wacore/src/send.rs index 001777f27..456c4180d 100644 --- a/wacore/src/send.rs +++ b/wacore/src/send.rs @@ -867,7 +867,7 @@ where let mut enc_builder = NodeBuilder::new("enc") .attr("v", stanza::ENC_VERSION) .attr("type", enc_type) - .attr("count", retry_count.to_string()); + .attr("count", retry_count); if let Some(mt) = media_type_from_message(message) { enc_builder = enc_builder.attr("mediatype", mt); } @@ -933,7 +933,7 @@ where let mut enc_builder = NodeBuilder::new("enc") .attr("v", stanza::ENC_VERSION) .attr("type", enc_type) - .attr("count", retry_count.to_string()); + .attr("count", retry_count); if let Some(mt) = media_type_from_message(message) { enc_builder = enc_builder.attr("mediatype", mt); } diff --git a/wacore/src/stanza/devices.rs b/wacore/src/stanza/devices.rs index d8c197183..7a99ed164 100644 --- a/wacore/src/stanza/devices.rs +++ b/wacore/src/stanza/devices.rs @@ -61,7 +61,7 @@ impl ProtocolNode for KeyIndexInfo { } fn into_node(self) -> Node { - let mut builder = NodeBuilder::new("key-index-list").attr("ts", self.timestamp.to_string()); + let mut builder = NodeBuilder::new("key-index-list").attr("ts", self.timestamp); if let Some(bytes) = self.signed_bytes { builder = builder.bytes(bytes); } @@ -129,7 +129,7 @@ impl ProtocolNode for DeviceElement { fn into_node(self) -> Node { let mut builder = NodeBuilder::new("device").attr("jid", self.jid); if let Some(ki) = self.key_index { - builder = builder.attr("key-index", ki.to_string()); + builder = builder.attr("key-index", ki); } if let Some(lid) = self.lid { builder = builder.attr("lid", lid); diff --git a/wacore/src/usync.rs b/wacore/src/usync.rs index cc56a3e40..38a18a2d9 100644 --- a/wacore/src/usync.rs +++ b/wacore/src/usync.rs @@ -239,11 +239,7 @@ mod tests { .map(|(jid, device_ids, phash)| { let device_nodes: Vec = device_ids .iter() - .map(|id| { - NodeBuilder::new("device") - .attr("id", id.to_string()) - .build() - }) + .map(|id| NodeBuilder::new("device").attr("id", *id).build()) .collect(); let mut device_list_builder = NodeBuilder::new("device-list");