Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions src/features/signal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,12 +265,7 @@ impl<'a> Signal<'a> {

// Acquire per-device session locks before encrypting (matches DM send path)
let lock_jids = self.client.build_session_lock_keys(&device_jids).await;
let mut session_mutexes = Vec::with_capacity(lock_jids.len());
let mut lock_buf = String::with_capacity(64);
for jid in &lock_jids {
wacore::types::jid::write_protocol_address_to(jid, &mut lock_buf);
session_mutexes.push(self.client.session_lock_for(&lock_buf).await);
}
let session_mutexes = self.client.session_mutexes_for(&lock_jids).await;
let mut _session_guards = Vec::with_capacity(session_mutexes.len());
for mutex in &session_mutexes {
_session_guards.push(mutex.lock().await);
Expand Down
27 changes: 16 additions & 11 deletions src/send.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1078,13 +1078,7 @@ impl Client {
}

let lock_jids = self.build_session_lock_keys(&all_dm_jids).await;

let mut _session_mutexes = Vec::with_capacity(lock_jids.len());
let mut lock_buf = String::with_capacity(64);
for jid in &lock_jids {
wacore::types::jid::write_protocol_address_to(jid, &mut lock_buf);
_session_mutexes.push(self.session_lock_for(&lock_buf).await);
}
let _session_mutexes = self.session_mutexes_for(&lock_jids).await;
let mut _session_guards = Vec::with_capacity(_session_mutexes.len());
for mutex in &_session_mutexes {
_session_guards.push(mutex.lock().await);
Expand Down Expand Up @@ -1487,10 +1481,7 @@ impl Client {
/// Build sorted, deduplicated per-device session lock keys.
/// INVARIANT: Keys are sorted to prevent deadlocks when acquiring multiple
/// session locks (e.g. DM sends that encrypt for recipient + own devices).
/// Keys match the decrypt path format so send and receive serialize correctly.
/// Returns resolved encryption JIDs sorted in a consistent order for
/// deadlock-free lock acquisition. Sorts by Jid fields directly — no
/// intermediate ProtocolAddress/String allocations for sorting.
/// Resolve encryption JIDs and sort for deadlock-free lock acquisition.
pub(crate) async fn build_session_lock_keys(&self, device_jids: &[Jid]) -> Vec<Jid> {
let mut keys: Vec<Jid> = Vec::with_capacity(device_jids.len());
for jid in device_jids {
Expand All @@ -1501,6 +1492,20 @@ impl Client {
keys
}

/// Fetch per-device session mutexes in deadlock-free order.
pub(crate) async fn session_mutexes_for(
&self,
jids: &[Jid],
) -> Vec<std::sync::Arc<async_lock::Mutex<()>>> {
let mut mutexes = Vec::with_capacity(jids.len());
let mut buf = String::with_capacity(64);
for jid in jids {
wacore::types::jid::write_protocol_address_to(jid, &mut buf);
mutexes.push(self.session_lock_for(&buf).await);
}
mutexes
}

/// Build tctoken timing config from AB props, falling back to defaults.
pub(crate) async fn tc_token_config(&self) -> wacore::iq::tctoken::TcTokenConfig {
use wacore::iq::props::config_codes;
Expand Down
10 changes: 3 additions & 7 deletions wacore/src/send.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1038,8 +1038,7 @@ pub async fn prepare_group_stanza<
jids_to_resolve.push(own_jid_to_check);
}

let mut seen_users = HashSet::new();
jids_to_resolve.retain(|jid| seen_users.insert((jid.user.clone(), jid.server.clone())));
crate::types::jid::sort_dedup_by_user(&mut jids_to_resolve);

log::debug!(
"Resolving devices for {} participants",
Expand Down Expand Up @@ -1067,10 +1066,7 @@ pub async fn prepare_group_stanza<
// both convert to 100000037037034:33@lid).
// Key on (user, server, agent, device) — excludes `integrator` which is not
// part of the wire JID identity used in <to jid> and phash.
let mut seen = HashSet::new();
resolved_list.retain(|jid| {
seen.insert((jid.user.clone(), jid.server.clone(), jid.agent, jid.device))
});
crate::types::jid::sort_dedup_by_device(&mut resolved_list);

// Filter devices for SKDM distribution:
// - Exclude the exact sending device (own_sending_jid) - we already have our own sender key
Expand Down Expand Up @@ -1211,7 +1207,7 @@ pub async fn prepare_group_stanza<

let stanza_type = stanza_type_from_message(message);
let mut stanza_builder = NodeBuilder::new("message")
.attr("to", to_jid.clone())
.attr("to", to_jid)
.attr("id", request_id)
.attr("type", stanza_type);

Expand Down
24 changes: 21 additions & 3 deletions wacore/src/types/jid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ fn mapped_server(s: &str) -> &str {
if s == "s.whatsapp.net" { "c.us" } else { s }
}

/// Write the protocol address lock key (`{user}[:{device}]@{server}.0`)
/// into `buf`, reusing its allocation. Zero heap allocations.
/// Write the protocol address lock key into `buf`, reusing its allocation.
pub fn write_protocol_address_to(jid: &Jid, buf: &mut String) {
use std::fmt::Write;
buf.clear();
Expand All @@ -24,14 +23,33 @@ pub fn write_protocol_address_to(jid: &Jid, buf: &mut String) {
}

/// Consistent ordering for deadlock-free multi-lock acquisition.
/// Compares Jid fields directly — no String allocation needed.
pub fn cmp_for_lock_order(a: &Jid, b: &Jid) -> std::cmp::Ordering {
mapped_server(&a.server)
.cmp(mapped_server(&b.server))
.then_with(|| a.user.cmp(&b.user))
.then_with(|| a.device.cmp(&b.device))
}

/// Sort and deduplicate by user identity (user + server).
pub fn sort_dedup_by_user(jids: &mut Vec<Jid>) {
jids.sort_unstable_by(|a, b| a.user.cmp(&b.user).then_with(|| a.server.cmp(&b.server)));
jids.dedup_by(|a, b| a.user == b.user && a.server == b.server);
}

/// Sort and deduplicate by device identity (user + server + agent + device).
pub fn sort_dedup_by_device(jids: &mut Vec<Jid>) {
jids.sort_unstable_by(|a, b| {
a.user
.cmp(&b.user)
.then_with(|| a.server.cmp(&b.server))
.then_with(|| a.agent.cmp(&b.agent))
.then_with(|| a.device.cmp(&b.device))
});
jids.dedup_by(|a, b| {
a.user == b.user && a.server == b.server && a.agent == b.agent && a.device == b.device
});
}

pub trait JidExt {
fn to_protocol_address(&self) -> ProtocolAddress;

Expand Down
Loading