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
12 changes: 7 additions & 5 deletions advanced/binary-protocol.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,13 @@ pub struct Jid {
```

<Note>
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.
</Note>

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<String>`, and `Deref<Target = str>`, 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.
Expand Down Expand Up @@ -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`

Expand Down
4 changes: 3 additions & 1 deletion advanced/signal-protocol.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Jid>);

/// 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<Jid>);
```

`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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Split the device-dedup explanation into concise sentences

This added sentence combines the chosen key, its equality rationale, the duplicate-encryption consequence, and the Bot/Interop counterexample into one very long sentence. Split these into concise, one-idea sentences so readers can follow the two distinct failure modes and the page complies with the project's writing standard.

AGENTS.md reference: AGENTS.md:L25-L25

Useful? React with 👍 / 👎.


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
Expand Down