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
38 changes: 37 additions & 1 deletion src/client/device_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -665,19 +665,38 @@ impl Client {

/// WA Web: `isFromKnownDevice(author)` — local check only, no network.
pub(crate) async fn is_from_known_device(&self, sender: &Jid) -> bool {
self.has_device(&sender.user, sender.device).await
self.has_device_for_jid(sender, sender.device).await
}

/// `has_device` for a caller holding the full `Jid`: the namespace picks
/// the single `lid_pn_cache` probe (see `resolve_lookup_keys_for_jid`).
/// Runs on every successful group decrypt and every retry receipt.
pub(crate) async fn has_device_for_jid(&self, jid: &Jid, device_id: u16) -> bool {
if device_id == 0 {
return true;
}
let lookup = self.resolve_lookup_keys_for_jid(jid).await;
self.has_device_in(&lookup, device_id).await
}

/// Check if a device exists for a user.
/// Returns true for device_id 0 (primary device always exists).
///
/// Every production caller holds a `Jid` and goes through
/// `has_device_for_jid`; this bare-user form remains for the tests that
/// probe a user under both of its namespaces.
#[cfg(test)]
pub(crate) async fn has_device(&self, user: &str, device_id: u16) -> bool {
if device_id == 0 {
return true;
}

// Borrowed keys avoid allocating the owned lookup variants on this hot path.
let lookup = self.resolve_lookup_keys(user).await;
self.has_device_in(&lookup, device_id).await
}

async fn has_device_in(&self, lookup: &UserLookupKeys, device_id: u16) -> bool {
for key in lookup.all_keys() {
if let Some(record) = self.device_registry_cache.get(key).await {
return record.devices.iter().any(|d| d.device_id() == device_id);
Expand Down Expand Up @@ -1255,7 +1274,24 @@ impl Client {
user: &str,
) -> Option<wacore::store::traits::DeviceListRecord> {
let lookup = self.resolve_lookup_keys(user).await;
self.load_device_record_in(&lookup).await
}

/// `load_device_record` for a caller holding the full `Jid`, so the known
/// namespace costs one `lid_pn_cache` probe instead of two per user of a
/// device-list response.
pub(crate) async fn load_device_record_for_jid(
&self,
jid: &Jid,
) -> Option<wacore::store::traits::DeviceListRecord> {
let lookup = self.resolve_lookup_keys_for_jid(jid).await;
self.load_device_record_in(&lookup).await
}

async fn load_device_record_in(
&self,
lookup: &UserLookupKeys,
) -> Option<wacore::store::traits::DeviceListRecord> {
for key in lookup.all_keys() {
if let Some(record) = self.device_registry_cache.get(key).await {
// Cold load-modify-persist path: callers mutate the owned record.
Expand Down
13 changes: 11 additions & 2 deletions src/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,12 @@ impl EncPayload {
ciphertext: bytes::Bytes,
enc_node: &NodeRef<'_>,
enc_index: usize,
enc_type: EncType,
) -> Option<Self> {
let enc_type = EncType::from_wire(enc_node.attrs().optional_string("type")?.as_ref())?;
let padding_version = enc_node.attrs().optional_u64("v").unwrap_or(2) as u8;
// One attribute pass: the caller already classified `type`, so only the
// remaining attributes are read here, through a single parser.
let mut attrs = enc_node.attrs();
let padding_version = attrs.optional_u64("v").unwrap_or(2) as u8;
Some(Self {
ciphertext,
enc_type,
Expand All @@ -111,25 +113,32 @@ impl EncPayload {
}

/// Zero-copy extraction from an OwnedNodeRef.
///
/// `enc_type` is the node's already-parsed `type` attribute; the receive
/// loop validates it before calling, so it is not re-read here.
pub(crate) fn from_owned_node(
owner: &OwnedNodeRef,
enc_node: &NodeRef<'_>,
enc_index: usize,
enc_type: EncType,
) -> Option<Self> {
Self::from_parts(
owner.slice_bytes(enc_node.content_bytes()?),
enc_node,
enc_index,
enc_type,
)
}

/// Copying extraction from a NodeRef (used in tests where there's no OwnedNodeRef).
#[cfg(test)]
pub(crate) fn from_node_ref(node: &NodeRef<'_>, enc_index: usize) -> Option<Self> {
let enc_type = EncType::from_wire(node.attrs().optional_string("type")?.as_ref())?;
Self::from_parts(
bytes::Bytes::copy_from_slice(node.content_bytes()?),
node,
enc_index,
enc_type,
)
}
}
Expand Down
33 changes: 17 additions & 16 deletions src/message/receive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ impl Client {
// `had_unknown_enc` means "produced no usable payload": either the
// type is unrecognized or it's known but the body is empty.
// Either way the stanza needs the fallback ack or the server replays.
if EncType::from_wire(enc_type.as_ref()).is_none() {
let Some(parsed_enc_type) = EncType::from_wire(enc_type.as_ref()) else {
log::warn!("Enc node has unknown type: {enc_type}");
self.report_raw_enc_decrypt_failure(
&info,
Expand All @@ -341,23 +341,24 @@ impl Client {
);
had_unknown_enc = true;
continue;
}

let payload = match EncPayload::from_owned_node(node, enc_node, enc_index) {
Some(p) => p,
None => {
log::warn!("Enc node {enc_type} has no content");
self.report_raw_enc_decrypt_failure(
&info,
enc_index,
Some(enc_type.as_ref()),
EncDecryptFailureReason::MalformedNode,
);
had_unknown_enc = true;
continue;
}
};

let payload =
match EncPayload::from_owned_node(node, enc_node, enc_index, parsed_enc_type) {
Some(p) => p,
None => {
log::warn!("Enc node {enc_type} has no content");
self.report_raw_enc_decrypt_failure(
&info,
enc_index,
Some(enc_type.as_ref()),
EncDecryptFailureReason::MalformedNode,
);
had_unknown_enc = true;
continue;
}
};

let bucket = if payload.enc_type.is_bot_secret() {
&mut bot_payloads
} else if payload.enc_type.is_session() {
Expand Down
30 changes: 12 additions & 18 deletions src/portable_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,14 @@ where
}
}

fn remove_key(&mut self, key: &K) -> Option<CacheEntry<V>> {
/// Borrowed removal: the `order` side is keyed by the entry's own `seq`,
/// so nothing here ever needs an owned `K`, and callers with a `&str` or
/// `&Jid` need not clone the key just to delete it.
fn remove_key<Q>(&mut self, key: &Q) -> Option<CacheEntry<V>>
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
let entry = self.map.remove(key)?;
self.order.remove(&entry.seq);
Some(entry)
Expand Down Expand Up @@ -385,14 +392,6 @@ where
false
}

fn find_key<Q>(inner: &CacheInner<K, V>, key: &Q) -> Option<K>
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
inner.map.get_key_value(key).map(|(k, _)| k.clone())
}

/// Whether `entry`'s access stamp has aged past [`TTI_RENEWAL_DIVISOR`]'s
/// tolerance and is worth pushing forward under the write lock. A cache
/// without TTI never renews.
Expand Down Expand Up @@ -420,14 +419,13 @@ where
// place. Removing without it could drop a replacement written
// while the guard was down and judge it by a stale `now`.
let observed = (entry.seq, entry.inserted_at);
let owned_key = Self::find_key(&guard, key)?;
drop(guard);
let mut wguard = self.inner.write().await;
if let Some(e) = wguard.map.get(key)
&& (e.seq, e.inserted_at) == observed
&& self.is_expired(e, now)
{
wguard.remove_key(&owned_key);
wguard.remove_key(key);
}
return None;
}
Expand Down Expand Up @@ -495,9 +493,8 @@ where
.map
.get(key)
.is_some_and(|entry| self.is_expired(entry, now))
&& let Some(owned_key) = Self::find_key(&guard, key)
{
guard.remove_key(&owned_key);
guard.remove_key(key);
}

let (next, result) = update(guard.map.get(key).map(|entry| &entry.value));
Expand Down Expand Up @@ -550,8 +547,7 @@ where
Q: Hash + Eq + ?Sized,
{
let mut guard = self.inner.write().await;
let owned_key = Self::find_key(&guard, key)?;
let entry = guard.remove_key(&owned_key)?;
let entry = guard.remove_key(key)?;
// Nothing to date until an entry is actually in hand.
let now = self.entry_time();
if self.is_expired(&entry, now) {
Expand All @@ -567,9 +563,7 @@ where
Q: Hash + Eq + ?Sized,
{
let mut guard = self.inner.write().await;
if let Some(owned_key) = Self::find_key(&guard, key) {
guard.remove_key(&owned_key);
}
guard.remove_key(key);
}

/// Reliably remove all entries, awaiting the write lock. Prefer this in
Expand Down
8 changes: 7 additions & 1 deletion src/receipt.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::client::Client;
use crate::types::events::{Event, Receipt};
use crate::types::events::{Event, EventKind, Receipt};
use crate::types::message::MessageInfo;
use crate::types::presence::ReceiptType;
use log::debug;
Expand Down Expand Up @@ -628,6 +628,12 @@ impl Client {
from.observe(),
users.len()
);
// Pure event production from here on: `dispatch` would drop every
// one of these on the floor without a subscriber, so skip building
// N receipts (each with a `Jid` clone and a `Vec<String>`) up front.
if !self.core.event_bus.has_handler_for(EventKind::Receipt) {
return;
}
for user in users {
// Missing `<user t>` means the server didn't disambiguate the
// per-user time; fall back to the stanza-level `t`.
Expand Down
2 changes: 1 addition & 1 deletion src/retry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -524,7 +524,7 @@ impl Client {
// evicted retry still triggers it.
let sender_device_id = info.requester.device();
let device_known = self
.has_device(&info.requester.user, sender_device_id)
.has_device_for_jid(&info.requester, sender_device_id)
.await;
if !device_known {
// Parity with WA Web's MdRetryFromUnknownDevice WAM (id 2178), which
Expand Down
4 changes: 2 additions & 2 deletions src/usync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ impl Client {
// Preserve key_index values from existing records (set via account_sync)
// Use alias-aware lookup (resolves LID ↔ PN) to find
// existing record regardless of which key it was stored under
let mut existing_record = self.load_device_record(&user_list.user.user).await;
let mut existing_record = self.load_device_record_for_jid(&user_list.user).await;

// Decode key-index-list if present (WA Web: handleKeyIndexResult)
let decoded_key_index = user_list
Expand Down Expand Up @@ -450,7 +450,7 @@ impl Client {
for own in device_snapshot.pn.iter().chain(device_snapshot.lid.iter()) {
let bare = own.to_non_ad();
// Carry the cached device_hash so an unchanged list is skipped server-side.
if let Some(record) = self.load_device_record(&bare.user).await
if let Some(record) = self.load_device_record_for_jid(&bare).await
&& let Some(phash) = record.phash
{
hashes.insert(bare.clone(), (String::from(phash), record.timestamp));
Expand Down
Loading
Loading