diff --git a/advanced/binary-protocol.mdx b/advanced/binary-protocol.mdx index 80fc68b..b9877e9 100644 --- a/advanced/binary-protocol.mdx +++ b/advanced/binary-protocol.mdx @@ -122,11 +122,13 @@ pub struct Jid { ``` - For the AD-capable servers (`Pn`, `Lid`, `Hosted`, `HostedLid`), the wire form spells the server as a leading domain byte on `AD_JID` (see [AD_JID](#ad_jid-device-specific-jid) below). The decoder resolves that byte into `server` and does not also copy it into `agent`. + For the AD-capable servers (`Pn`, `Lid`, `Hosted`, `HostedLid`), the wire form spells the server as a leading domain byte on `AD_JID` (see [AD_JID](#ad_jid-device-specific-jid) below). The decoder resolves that byte into `server`. It does not also copy the byte into `agent`. - Earlier versions kept a redundant copy in `agent`. If you decoded a JID off the wire, you got `agent` set to the domain byte; if you parsed the same JID from text, you got `agent` set to `0` — even though `Display` rendered both the same way. Because `agent` is part of the derived `PartialEq`/`Hash`, those two JIDs compared unequal under `PartialEq` and could hash to different values. + Earlier versions kept a redundant copy in `agent`. A JID decoded off the wire got `agent` set to the domain byte. The same JID parsed from text got `agent` set to `0`, even though `Display` rendered both the same way. `PartialEq`/`Hash` were derived at the time, so those two JIDs compared unequal and could hash to different values. - `agent` now stays `0` for these servers on both paths, so a wire-decoded JID and a text-parsed JID compare equal and hash the same. `agent` is only ever nonzero for `Bot`/`Interop`, which do render it. + `agent` now stays `0` for these servers on both paths. A wire-decoded JID and a text-parsed JID therefore compare equal and hash the same. But `PartialEq`/`Hash` no longer *rely* on that invariant holding everywhere. `Jid` and `JidRef` implement both by hand now, routed through a private module-level `identity_agent(server, agent)` helper. That helper reads as `0` on any server where `Server::renders_agent()` is false, regardless of what the raw field holds. This closes a gap the decoder fix alone didn't: `swap_pn_lid_namespace` and similar code can still carry a nonzero `agent` across a namespace conversion. Equality treats the result the same as the clean JID either way. `agent` is only ever identity-relevant for `Bot`/`Interop`, which do render it. + + `integrator` is *not* normalized the same way. It is folded into identity unconditionally, matching `is_same_chat_as`, since the field is never set outside `Interop` in practice. A separate public method, `Jid::identity_agent(&self)`, wraps that same private helper. Code building its own key over a JID — sorting, deduplicating, indexing — can call it to apply the identical rule the hand-written `PartialEq`/`Hash` use, instead of reading `jid.agent` directly. The `user` field uses `CompactString` (re-exported from `compact_str`) instead of `String`. `CompactString` stores short strings inline (up to 24 bytes on 64-bit platforms) without heap allocation, which benefits typical phone numbers and user identifiers. The library re-exports it as `wacore_binary::CompactString` and `whatsapp_rust::CompactString` for convenience. `CompactString` implements `From<&str>`, `From`, and `Deref`, so it works as a drop-in replacement in most contexts — but code that relied on `Jid.user` being a `String` (e.g., passing it to functions expecting `&String` or calling `String`-specific methods) may need updating. @@ -539,9 +541,9 @@ The `domain_type` must be derived from the JID's `server` field via `server_to_d Decoding used to have the mirror-image asymmetry. `domain_type` resolves to `server`, but the decoder also wrote that same byte into `agent`. For `Pn`/`Lid`/`Hosted`/`HostedLid` specifically, the encoder above always re-derives `domain_type` from `server` and ignores `agent`, and `Display` never renders `agent` for these servers either — so nothing consumed the redundant copy. (Other variants, like `Bot`/`Interop`, do read `agent` through the fallback row in the table above.) -The only effect was that `agent` differed by provenance: `0` if you parsed the JID from text, the domain byte if you had just decoded it off the wire. Since `agent` is part of the derived `PartialEq`/`Hash`, the identical JID compared unequal under `PartialEq` and could hash to a different value — the same shape of bug as the encoder one above, just on the other side of the wire. +The only effect was that `agent` differed by provenance: `0` if you parsed the JID from text, the domain byte if you had just decoded it off the wire. With `PartialEq`/`Hash` derived at the time, the identical JID compared unequal and could hash to a different value — the same shape of bug as the encoder one above, just on the other side of the wire. -The decoder now leaves `agent` at `0` for `Pn`/`Lid`/`Hosted`/`HostedLid`, so encode → decode is idempotent, and a wire-decoded JID equals the same JID parsed from text. +The decoder now leaves `agent` at `0` for `Pn`/`Lid`/`Hosted`/`HostedLid`, so encode → decode is idempotent, and a wire-decoded JID equals the same JID parsed from text. `Jid`'s hand-written `PartialEq`/`Hash` (see the note above) mean this no longer depends solely on the decoder holding that line, either — any other code path that leaves a stray byte in `agent` on these servers still compares and hashes as identity-equal. Location: `wacore/binary/src/encoder.rs:699-705`, `362-369`; decoder fix: `wacore/binary/src/decoder.rs` diff --git a/advanced/signal-protocol.mdx b/advanced/signal-protocol.mdx index 72c0929..d317110 100644 --- a/advanced/signal-protocol.mdx +++ b/advanced/signal-protocol.mdx @@ -2006,10 +2006,12 @@ Group stanza preparation needs to deduplicate participant JIDs at two stages: be /// Sort and deduplicate by user identity (user + server). pub fn sort_dedup_by_user(jids: &mut Vec); -/// Sort and deduplicate by device identity (user + server + agent + device). +/// Sort and deduplicate by device identity (user + server + device + integrator + identity_agent). pub fn sort_dedup_by_device(jids: &mut Vec); ``` +`sort_dedup_by_device` keys on `Jid::identity_agent()` rather than the raw `agent` field, so its notion of "same device" matches exactly what `Jid`'s `PartialEq`/`Hash` already treat as equal (see [Binary Protocol](/advanced/binary-protocol#jid-encoding) for why the two fields differ). That has to hold in both directions: keying on the raw `agent` would let two JIDs that are actually one device — an inert agent byte on `Pn`/`Lid`/`Hosted`/`HostedLid`, same AD-JID, same Signal address — both survive the dedup and pick up two concurrent encryption jobs against one session; dropping `agent` from the key entirely would go too far the other way and silently collapse two genuinely distinct `@bot`/`@interop` devices, which *do* render it, losing a fan-out destination. + Both use `sort_unstable_by` followed by `dedup_by`, comparing JID fields directly without allocating intermediate strings or hash sets. This is more efficient than the `HashSet<(String, String)>` approach because: - No per-JID `String::clone()` for hash keys