1use crate::node::NodeStr;
2use compact_str::CompactString;
3use std::fmt;
4use std::str::FromStr;
5
6#[derive(Debug, Clone, Copy)]
9pub struct ParsedJidParts<'a> {
10 pub user: &'a str,
11 pub server: &'a str,
12 pub agent: u8,
13 pub device: u16,
14 pub integrator: u16,
15}
16
17#[inline]
27fn parse_u16_decimal(s: &str) -> Option<u16> {
28 let mut bytes = s.as_bytes();
29 if bytes.first() == Some(&b'+') {
30 bytes = &bytes[1..];
31 }
32 if bytes.is_empty() {
33 return None;
34 }
35
36 let mut value = 0u16;
37 for &byte in bytes {
38 let digit = byte.wrapping_sub(b'0');
39 if digit > 9 {
40 return None;
41 }
42 value = value.checked_mul(10)?.checked_add(digit as u16)?;
43 }
44 Some(value)
45}
46
47#[inline]
53pub fn parse_jid_fast(s: &str) -> Option<ParsedJidParts<'_>> {
54 parse_jid_scan(s).map(|(parts, _)| parts)
55}
56
57#[inline]
66fn parse_jid_scan(s: &str) -> Option<(ParsedJidParts<'_>, Option<Server>)> {
67 let bytes = s.as_bytes();
68
69 let mut at = usize::MAX;
72 let mut colon_pos: Option<usize> = None;
73 let mut last_dot_pos: Option<usize> = None;
74
75 for (i, &b) in bytes.iter().enumerate() {
76 match b {
77 b'@' => {
78 at = i;
79 break;
80 }
81 b':' => colon_pos = Some(i),
82 b'.' if colon_pos.is_none() => last_dot_pos = Some(i),
84 _ => {}
85 }
86 }
87
88 if at == usize::MAX || at == 0 {
90 return None;
91 }
92
93 let user_part = &s[..at];
94 let server_str = &s[at + 1..];
95 let server = Server::parse_known(server_str);
96
97 match server {
98 Some(Server::Lid) => {
100 let (user, device) = match colon_pos {
101 Some(pos) => (&s[..pos], parse_u16_decimal(&s[pos + 1..at]).unwrap_or(0)),
102 None => (user_part, 0),
103 };
104 Some((
105 ParsedJidParts {
106 user,
107 server: server_str,
108 agent: 0,
109 device,
110 integrator: 0,
111 },
112 server,
113 ))
114 }
115 Some(Server::Pn) => {
118 if let Some(pos) = colon_pos {
119 return Some((
120 ParsedJidParts {
121 user: &s[..pos],
122 server: server_str,
123 agent: 0,
124 device: parse_u16_decimal(&s[pos + 1..at]).unwrap_or(0),
125 integrator: 0,
126 },
127 server,
128 ));
129 }
130 if let Some(dot_pos) = last_dot_pos
131 && let Some(device_val) = parse_u16_decimal(&s[dot_pos + 1..at])
132 {
133 return Some((
134 ParsedJidParts {
135 user: &s[..dot_pos],
136 server: server_str,
137 agent: 0,
138 device: device_val,
139 integrator: 0,
140 },
141 server,
142 ));
143 }
144 Some((
145 ParsedJidParts {
146 user: user_part,
147 server: server_str,
148 agent: 0,
149 device: 0,
150 integrator: 0,
151 },
152 server,
153 ))
154 }
155 _ => {
157 let (user_before_colon, device) = match colon_pos {
158 Some(pos) => (&s[..pos], parse_u16_decimal(&s[pos + 1..at]).unwrap_or(0)),
159 None => (user_part, 0),
160 };
161 let (final_user, agent) = match user_before_colon.rfind('.') {
166 Some(dot_pos) => match parse_u16_decimal(&user_before_colon[dot_pos + 1..]) {
167 Some(agent_val) if agent_val <= u8::MAX as u16 => {
168 (&user_before_colon[..dot_pos], agent_val as u8)
169 }
170 _ => (user_before_colon, 0),
171 },
172 None => (user_before_colon, 0),
173 };
174 Some((
175 ParsedJidParts {
176 user: final_user,
177 server: server_str,
178 agent,
179 device,
180 integrator: 0,
181 },
182 server,
183 ))
184 }
185 }
186}
187
188#[inline]
195pub fn parse_jid_ref(s: &str) -> Option<JidRef<'_>> {
196 let (parts, server) = parse_jid_scan(s)?;
197 Some(JidRef {
198 user: NodeStr::Borrowed(parts.user),
199 server: server?,
200 agent: parts.agent,
201 device: parts.device,
202 integrator: parts.integrator,
203 })
204}
205
206#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
211#[repr(u8)]
212pub enum Server {
213 #[default]
214 Pn = 0,
215 Lid = 1,
216 Group = 2,
217 Broadcast = 3,
218 Newsletter = 4,
219 Hosted = 5,
220 HostedLid = 6,
221 Messenger = 7,
222 Interop = 8,
223 Bot = 9,
224 Legacy = 10,
225 Call = 11,
227}
228
229#[cfg(feature = "serde")]
230impl serde::Serialize for Server {
231 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
232 serializer.serialize_str(self.as_str())
233 }
234}
235
236#[cfg(feature = "serde")]
237impl<'de> serde::Deserialize<'de> for Server {
238 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
239 struct ServerVisitor;
240
241 impl serde::de::Visitor<'_> for ServerVisitor {
242 type Value = Server;
243
244 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
245 formatter.write_str("a known WhatsApp server identifier")
246 }
247
248 fn visit_borrowed_str<E>(self, value: &str) -> Result<Self::Value, E>
249 where
250 E: serde::de::Error,
251 {
252 Server::try_from(value).map_err(E::custom)
253 }
254
255 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
256 where
257 E: serde::de::Error,
258 {
259 Server::try_from(value).map_err(E::custom)
260 }
261
262 fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
263 where
264 E: serde::de::Error,
265 {
266 Server::try_from(value.as_str()).map_err(E::custom)
267 }
268 }
269
270 deserializer.deserialize_str(ServerVisitor)
271 }
272}
273
274impl Server {
275 #[inline]
276 pub fn as_str(self) -> &'static str {
277 match self {
278 Self::Pn => "s.whatsapp.net",
279 Self::Lid => "lid",
280 Self::Group => "g.us",
281 Self::Broadcast => "broadcast",
282 Self::Newsletter => "newsletter",
283 Self::Hosted => "hosted",
284 Self::HostedLid => "hosted.lid",
285 Self::Messenger => "msgr",
286 Self::Interop => "interop",
287 Self::Bot => "bot",
288 Self::Legacy => "c.us",
289 Self::Call => "call",
290 }
291 }
292
293 #[inline]
296 pub fn is_pn_family(self) -> bool {
297 matches!(self, Self::Pn | Self::Hosted)
298 }
299
300 #[inline]
302 pub fn is_lid_family(self) -> bool {
303 matches!(self, Self::Lid | Self::HostedLid)
304 }
305
306 #[inline]
314 pub fn renders_agent(self) -> bool {
315 !matches!(self, Self::Pn | Self::Lid | Self::Hosted | Self::HostedLid)
316 }
317
318 #[inline]
322 pub fn carries_phone_number(self) -> bool {
323 matches!(
324 self,
325 Self::Pn | Self::Hosted | Self::Legacy | Self::Messenger | Self::Interop
326 )
327 }
328}
329
330impl fmt::Display for Server {
331 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
332 f.write_str(self.as_str())
333 }
334}
335
336impl PartialEq<str> for Server {
337 fn eq(&self, other: &str) -> bool {
338 self.as_str() == other
339 }
340}
341
342impl PartialEq<&str> for Server {
343 fn eq(&self, other: &&str) -> bool {
344 self.as_str() == *other
345 }
346}
347
348impl Server {
349 #[inline]
354 pub fn parse_known(s: &str) -> Option<Self> {
355 Some(match s {
356 "s.whatsapp.net" => Self::Pn,
357 "lid" => Self::Lid,
358 "g.us" => Self::Group,
359 "broadcast" => Self::Broadcast,
360 "newsletter" => Self::Newsletter,
361 "hosted" => Self::Hosted,
362 "hosted.lid" => Self::HostedLid,
363 "msgr" => Self::Messenger,
364 "interop" => Self::Interop,
365 "bot" => Self::Bot,
366 "c.us" => Self::Legacy,
367 "call" => Self::Call,
368 _ => return None,
369 })
370 }
371}
372
373impl TryFrom<&str> for Server {
374 type Error = JidError;
375 fn try_from(s: &str) -> Result<Self, Self::Error> {
376 Server::parse_known(s)
377 .ok_or_else(|| JidError::InvalidFormat(format!("unknown server: {s}")))
378 }
379}
380
381pub const DEFAULT_USER_SERVER: &str = "s.whatsapp.net";
383pub const SERVER_JID: &str = "s.whatsapp.net";
384pub const GROUP_SERVER: &str = "g.us";
385pub const LEGACY_USER_SERVER: &str = "c.us";
386pub const BROADCAST_SERVER: &str = "broadcast";
387pub const HIDDEN_USER_SERVER: &str = "lid";
388pub const NEWSLETTER_SERVER: &str = "newsletter";
389pub const HOSTED_SERVER: &str = "hosted";
390pub const HOSTED_LID_SERVER: &str = "hosted.lid";
391pub const MESSENGER_SERVER: &str = "msgr";
392pub const INTEROP_SERVER: &str = "interop";
393pub const BOT_SERVER: &str = "bot";
394pub const STATUS_BROADCAST_USER: &str = "status";
395pub const PSA_USER: &str = "0";
396
397pub type MessageId = String;
398pub type MessageServerId = i32;
399#[derive(Debug)]
400pub enum JidError {
401 InvalidFormat(String),
403 Parse(std::num::ParseIntError),
405}
406
407impl fmt::Display for JidError {
408 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
409 match self {
410 JidError::InvalidFormat(s) => write!(f, "Invalid JID format: {s}"),
411 JidError::Parse(e) => write!(f, "Failed to parse component: {e}"),
412 }
413 }
414}
415
416impl std::error::Error for JidError {
417 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
418 match self {
419 JidError::Parse(e) => Some(e),
420 _ => None,
421 }
422 }
423}
424
425impl From<std::num::ParseIntError> for JidError {
427 fn from(err: std::num::ParseIntError) -> Self {
428 JidError::Parse(err)
429 }
430}
431
432pub trait JidExt {
433 fn user(&self) -> &str;
434 fn server(&self) -> Server;
435 fn device(&self) -> u16;
436 fn integrator(&self) -> u16;
437
438 fn is_ad(&self) -> bool {
439 self.device() > 0
440 && matches!(
441 self.server(),
442 Server::Pn | Server::Lid | Server::Hosted | Server::HostedLid
443 )
444 }
445
446 fn is_interop(&self) -> bool {
447 self.server() == Server::Interop && self.integrator() > 0
448 }
449
450 fn is_messenger(&self) -> bool {
451 self.server() == Server::Messenger && self.device() > 0
452 }
453
454 fn is_group(&self) -> bool {
455 self.server() == Server::Group
456 }
457
458 fn is_broadcast_list(&self) -> bool {
459 self.server() == Server::Broadcast && self.user() != STATUS_BROADCAST_USER
460 }
461
462 fn is_status_broadcast(&self) -> bool {
463 self.server() == Server::Broadcast && self.user() == STATUS_BROADCAST_USER
464 }
465
466 fn is_psa(&self) -> bool {
470 matches!(self.server(), Server::Pn | Server::Legacy) && self.user() == PSA_USER
471 }
472
473 fn is_bot(&self) -> bool {
474 (self.server() == Server::Pn
475 && self.device() == 0
476 && (self.user().starts_with("1313555") || self.user().starts_with("131655500")))
477 || self.server() == Server::Bot
478 }
479
480 fn is_newsletter(&self) -> bool {
481 self.server() == Server::Newsletter
482 }
483
484 fn is_hosted(&self) -> bool {
488 self.device() == 99 || matches!(self.server(), Server::Hosted | Server::HostedLid)
489 }
490
491 fn is_empty(&self) -> bool {
492 self.user().is_empty()
493 }
494
495 fn is_same_user_as(&self, other: &impl JidExt) -> bool {
496 self.user() == other.user()
497 }
498}
499
500#[inline]
518fn identity_agent(server: Server, agent: u8) -> u8 {
519 if server.renders_agent() { agent } else { 0 }
520}
521
522#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
523#[derive(Debug, Clone, Default)]
524pub struct Jid {
525 pub user: CompactString,
526 pub server: Server,
527 pub agent: u8,
528 pub device: u16,
529 pub integrator: u16,
530}
531
532#[derive(Debug, Clone, yoke::Yokeable)]
533pub struct JidRef<'a> {
534 pub user: NodeStr<'a>,
535 pub server: Server,
536 pub agent: u8,
537 pub device: u16,
538 pub integrator: u16,
539}
540
541impl JidExt for Jid {
542 fn user(&self) -> &str {
543 &self.user
544 }
545 fn server(&self) -> Server {
546 self.server
547 }
548 fn device(&self) -> u16 {
549 self.device
550 }
551 fn integrator(&self) -> u16 {
552 self.integrator
553 }
554}
555
556impl Jid {
557 pub fn new(user: impl Into<CompactString>, server: Server) -> Self {
558 Self {
559 user: user.into(),
560 server,
561 ..Default::default()
562 }
563 }
564
565 pub fn pn(user: impl Into<CompactString>) -> Self {
567 Self {
568 user: user.into(),
569 server: Server::Pn,
570 ..Default::default()
571 }
572 }
573
574 pub fn lid(user: impl Into<CompactString>) -> Self {
576 Self {
577 user: user.into(),
578 server: Server::Lid,
579 ..Default::default()
580 }
581 }
582
583 pub fn status_broadcast() -> Self {
585 Self {
586 user: CompactString::from(STATUS_BROADCAST_USER),
587 server: Server::Broadcast,
588 agent: 0,
589 device: 0,
590 integrator: 0,
591 }
592 }
593
594 pub fn group(id: impl Into<CompactString>) -> Self {
596 Self {
597 user: id.into(),
598 server: Server::Group,
599 ..Default::default()
600 }
601 }
602
603 pub fn newsletter(id: impl Into<CompactString>) -> Self {
605 Self {
606 user: id.into(),
607 server: Server::Newsletter,
608 ..Default::default()
609 }
610 }
611
612 pub fn pn_device(user: impl Into<CompactString>, device: u16) -> Self {
614 Self {
615 user: user.into(),
616 server: Server::Pn,
617 device,
618 ..Default::default()
619 }
620 }
621
622 pub fn lid_device(user: impl Into<CompactString>, device: u16) -> Self {
624 Self {
625 user: user.into(),
626 server: Server::Lid,
627 device,
628 ..Default::default()
629 }
630 }
631
632 #[inline]
634 pub fn is_pn(&self) -> bool {
635 self.server == Server::Pn
636 }
637
638 #[inline]
640 pub fn is_lid(&self) -> bool {
641 self.server == Server::Lid
642 }
643
644 #[inline]
646 pub fn user_base(&self) -> &str {
647 if let Some((base, _)) = self.user.split_once(':') {
648 base
649 } else {
650 &self.user
651 }
652 }
653
654 pub fn with_device(&self, device_id: u16) -> Self {
656 Self {
657 user: self.user.clone(),
658 server: self.server,
659 agent: self.agent,
660 device: device_id,
661 integrator: self.integrator,
662 }
663 }
664
665 pub fn with_device_hosting(&self, device_id: u16, is_hosted: bool) -> Self {
671 let mut jid = self.with_device(device_id);
672 jid.server = match (jid.server, is_hosted) {
673 (Server::Pn | Server::Hosted, true) => Server::Hosted,
674 (Server::Pn | Server::Hosted, false) => Server::Pn,
675 (Server::Lid | Server::HostedLid, true) => Server::HostedLid,
676 (Server::Lid | Server::HostedLid, false) => Server::Lid,
677 (server, _) => server,
678 };
679 jid
680 }
681
682 pub fn to_non_ad(&self) -> Self {
683 Self {
684 user: self.user.clone(),
685 server: self.server,
686 integrator: self.integrator,
687 ..Default::default()
688 }
689 }
690
691 pub fn into_non_ad(self) -> Self {
694 Self {
695 user: self.user,
696 server: self.server,
697 integrator: self.integrator,
698 agent: 0,
699 device: 0,
700 }
701 }
702
703 #[inline]
710 pub fn is_same_chat_as(&self, other: &Jid) -> bool {
711 self.user == other.user
712 && self.server == other.server
713 && self.integrator == other.integrator
714 && (!self.server.renders_agent() || self.agent == other.agent)
715 }
716
717 pub fn to_non_ad_string(&self) -> String {
721 let mut buf = String::with_capacity(self.user.len() + 1 + self.server.as_str().len());
722 push_jid_to_string(&self.user, self.server, 0, 0, &mut buf);
723 buf
724 }
725
726 pub fn to_non_ad_arc_str(&self) -> std::sync::Arc<str> {
731 let mut writer = JidStackWriter::new();
732 if write_jid_fallible(&mut writer, &self.user, self.server, 0, 0).is_ok() {
733 return std::sync::Arc::from(writer.as_str());
734 }
735 std::sync::Arc::from(self.to_non_ad_string())
738 }
739
740 #[inline]
743 pub fn matches_user_or_lid(&self, user: &Jid, lid: Option<&Jid>) -> bool {
744 self.is_same_user_as(user) || lid.is_some_and(|l| self.is_same_user_as(l))
745 }
746
747 pub fn to_phash_form_string(&self) -> String {
750 let mut s = String::with_capacity(self.user.len() + 20);
751 self.push_phash_form_to(&mut s);
752 s
753 }
754
755 #[inline]
770 pub fn push_phash_form_to(&self, buf: &mut String) {
771 if self.user.is_empty() {
772 buf.push_str(self.server.as_str());
773 return;
774 }
775 buf.push_str(&self.user);
776 buf.push_str(".0:");
777 buf.push_str(itoa::Buffer::new().format(self.device));
778 buf.push('@');
779 buf.push_str(self.server.as_str());
780 }
781
782 #[inline]
785 pub fn push_to(&self, buf: &mut String) {
786 push_jid_to_string(&self.user, self.server, self.agent, self.device, buf);
787 }
788
789 #[inline]
792 pub fn write_display_to<W: fmt::Write + ?Sized>(&self, writer: &mut W) -> fmt::Result {
793 write_jid_fallible(writer, &self.user, self.server, self.agent, self.device)
794 }
795
796 #[inline]
798 pub fn display_eq(&self, other: &str) -> bool {
799 jid_display_eq(&self.user, self.server, self.agent, self.device, other)
800 }
801
802 #[inline]
804 pub fn display_eq_jid(&self, other: &Self) -> bool {
805 jid_displays_equal(
806 (&self.user, self.server, self.agent, self.device),
807 (&other.user, other.server, other.agent, other.device),
808 )
809 }
810
811 #[inline]
813 pub fn device_eq(&self, other: &Jid) -> bool {
814 self.user == other.user && self.server == other.server && self.device == other.device
815 }
816
817 #[inline]
825 pub fn identity_agent(&self) -> u8 {
826 identity_agent(self.server, self.agent)
827 }
828
829 #[inline]
831 pub fn device_key(&self) -> DeviceKey<'_> {
832 DeviceKey {
833 user: &self.user,
834 server: self.server,
835 device: self.device,
836 }
837 }
838}
839
840#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
842pub struct DeviceKey<'a> {
843 pub user: &'a str,
844 pub server: Server,
845 pub device: u16,
846}
847
848impl<'a> JidExt for JidRef<'a> {
849 fn user(&self) -> &str {
850 &self.user
851 }
852 fn server(&self) -> Server {
853 self.server
854 }
855 fn device(&self) -> u16 {
856 self.device
857 }
858 fn integrator(&self) -> u16 {
859 self.integrator
860 }
861}
862
863impl<'a> JidRef<'a> {
864 pub fn to_owned(&self) -> Jid {
865 Jid {
866 user: self.user.to_compact_string(),
867 server: self.server,
868 agent: self.agent,
869 device: self.device,
870 integrator: self.integrator,
871 }
872 }
873
874 #[inline]
876 pub fn display_eq(&self, other: &str) -> bool {
877 jid_display_eq(&self.user, self.server, self.agent, self.device, other)
878 }
879
880 #[inline]
882 pub fn display_eq_jid(&self, other: &Self) -> bool {
883 jid_displays_equal(
884 (&self.user, self.server, self.agent, self.device),
885 (&other.user, other.server, other.agent, other.device),
886 )
887 }
888}
889
890impl PartialEq for Jid {
891 #[inline]
892 fn eq(&self, other: &Self) -> bool {
893 self.user == other.user
894 && self.server == other.server
895 && self.device == other.device
896 && self.integrator == other.integrator
897 && (self.agent == other.agent
904 || identity_agent(self.server, self.agent)
905 == identity_agent(other.server, other.agent))
906 }
907}
908
909impl Eq for Jid {}
910
911impl std::hash::Hash for Jid {
912 #[inline]
913 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
914 self.user.hash(state);
915 self.server.hash(state);
916 self.device.hash(state);
917 self.integrator.hash(state);
918 identity_agent(self.server, self.agent).hash(state);
919 }
920}
921
922impl PartialEq for JidRef<'_> {
923 #[inline]
924 fn eq(&self, other: &Self) -> bool {
925 self.user.as_ref() == other.user.as_ref()
926 && self.server == other.server
927 && self.device == other.device
928 && self.integrator == other.integrator
929 && (self.agent == other.agent
931 || identity_agent(self.server, self.agent)
932 == identity_agent(other.server, other.agent))
933 }
934}
935
936impl Eq for JidRef<'_> {}
937
938impl std::hash::Hash for JidRef<'_> {
939 #[inline]
940 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
941 self.user.as_ref().hash(state);
942 self.server.hash(state);
943 self.device.hash(state);
944 self.integrator.hash(state);
945 identity_agent(self.server, self.agent).hash(state);
946 }
947}
948
949impl PartialEq<JidRef<'_>> for Jid {
950 #[inline]
951 fn eq(&self, other: &JidRef<'_>) -> bool {
952 self.user.as_str() == other.user.as_ref()
953 && self.server == other.server
954 && self.device == other.device
955 && self.integrator == other.integrator
956 && (self.agent == other.agent
958 || identity_agent(self.server, self.agent)
959 == identity_agent(other.server, other.agent))
960 }
961}
962
963impl PartialEq<Jid> for JidRef<'_> {
964 #[inline]
965 fn eq(&self, other: &Jid) -> bool {
966 other == self
967 }
968}
969
970#[cfg(feature = "serde")]
971impl serde::Serialize for JidRef<'_> {
972 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
973 use serde::ser::SerializeStruct;
974 let mut s = serializer.serialize_struct("Jid", 5)?;
975 s.serialize_field("user", &*self.user)?;
976 s.serialize_field("server", &self.server)?;
977 s.serialize_field("agent", &self.agent)?;
978 s.serialize_field("device", &self.device)?;
979 s.serialize_field("integrator", &self.integrator)?;
980 s.end()
981 }
982}
983
984impl FromStr for Jid {
985 type Err = JidError;
986 fn from_str(s: &str) -> Result<Self, Self::Err> {
987 if let Some(jid) = parse_jid_ref(s) {
989 return Ok(jid.to_owned());
990 }
991
992 let (user_part, server) = match s.split_once('@') {
995 Some((u, s)) => (u, s),
996 None => ("", s),
997 };
998
999 if user_part.is_empty() && Server::try_from(server).is_err() {
1000 return Err(JidError::InvalidFormat(format!(
1001 "unknown server '{server}'"
1002 )));
1003 }
1004
1005 if server == HIDDEN_USER_SERVER {
1008 let (user, device) = if let Some((u, d_str)) = user_part.rsplit_once(':') {
1009 (u, d_str.parse()?)
1010 } else {
1011 (user_part, 0)
1012 };
1013 return Ok(Jid {
1014 user: CompactString::from(user),
1015 server: Server::try_from(server)?,
1016 device,
1017 agent: 0,
1018 integrator: 0,
1019 });
1020 }
1021
1022 let mut user = user_part;
1024 let mut device = 0;
1025 let mut agent = 0;
1026
1027 if let Some((u, d_str)) = user_part.rsplit_once(':') {
1028 user = u;
1029 device = d_str.parse()?;
1030 }
1031
1032 if server != DEFAULT_USER_SERVER
1033 && server != HIDDEN_USER_SERVER
1034 && let Some((u, last_part)) = user.rsplit_once('.')
1035 && let Ok(num_val) = last_part.parse::<u16>()
1036 {
1037 if num_val > u8::MAX as u16 {
1038 return Err(JidError::InvalidFormat(format!(
1039 "Agent component out of range: {num_val}"
1040 )));
1041 }
1042 user = u;
1043 agent = num_val as u8;
1044 }
1045
1046 Ok(Jid {
1047 user: CompactString::from(user),
1048 server: Server::try_from(server)?,
1049 agent,
1050 device,
1051 integrator: 0,
1052 })
1053 }
1054}
1055
1056macro_rules! write_jid {
1063 (infallible $buf:expr, $user:expr, $server:expr, $agent:expr, $device:expr) => {{
1065 let (user, server, agent, device) = ($user, $server, $agent, $device);
1066 if user.is_empty() {
1067 $buf.push_str(server.as_str());
1068 return;
1069 }
1070 $buf.push_str(user);
1071 if agent > 0 && server.renders_agent() {
1072 $buf.push('.');
1073 $buf.push_str(itoa::Buffer::new().format(agent));
1074 }
1075 if device > 0 {
1076 $buf.push(':');
1077 $buf.push_str(itoa::Buffer::new().format(device));
1078 }
1079 $buf.push('@');
1080 $buf.push_str(server.as_str());
1081 }};
1082 (fallible $f:expr, $user:expr, $server:expr, $agent:expr, $device:expr) => {{
1084 let (user, server, agent, device) = ($user, $server, $agent, $device);
1085 if user.is_empty() {
1086 return $f.write_str(server.as_str());
1087 }
1088 $f.write_str(user)?;
1089 if agent > 0 && server.renders_agent() {
1090 $f.write_str(".")?;
1091 $f.write_str(itoa::Buffer::new().format(agent))?;
1092 }
1093 if device > 0 {
1094 $f.write_str(":")?;
1095 $f.write_str(itoa::Buffer::new().format(device))?;
1096 }
1097 $f.write_str("@")?;
1098 $f.write_str(server.as_str())
1099 }};
1100}
1101
1102#[inline]
1105pub fn push_jid_to_string(user: &str, server: Server, agent: u8, device: u16, buf: &mut String) {
1106 write_jid!(infallible buf, user, server, agent, device);
1107}
1108
1109#[inline]
1112pub fn push_jid_to_compact(
1113 user: &str,
1114 server: Server,
1115 agent: u8,
1116 device: u16,
1117 buf: &mut CompactString,
1118) {
1119 write_jid!(infallible buf, user, server, agent, device);
1120}
1121
1122struct JidStackWriter {
1127 buf: [u8; 64],
1128 len: usize,
1129}
1130
1131impl JidStackWriter {
1132 #[inline]
1133 fn new() -> Self {
1134 Self {
1135 buf: [0; 64],
1136 len: 0,
1137 }
1138 }
1139
1140 #[inline]
1141 fn as_str(&self) -> &str {
1142 unsafe { std::str::from_utf8_unchecked(&self.buf[..self.len]) }
1148 }
1149}
1150
1151impl fmt::Write for JidStackWriter {
1152 #[inline]
1153 fn write_str(&mut self, s: &str) -> fmt::Result {
1154 let end = self.len + s.len();
1155 if end > self.buf.len() {
1156 return Err(fmt::Error);
1157 }
1158 self.buf[self.len..end].copy_from_slice(s.as_bytes());
1159 self.len = end;
1160 Ok(())
1161 }
1162}
1163
1164#[inline]
1165fn write_jid_fallible<W: fmt::Write + ?Sized>(
1166 w: &mut W,
1167 user: &str,
1168 server: Server,
1169 agent: u8,
1170 device: u16,
1171) -> fmt::Result {
1172 write_jid!(fallible w, user, server, agent, device)
1173}
1174
1175struct StrEqWriter<'a> {
1176 target: &'a [u8],
1177 position: usize,
1178 matches: bool,
1179}
1180
1181impl fmt::Write for StrEqWriter<'_> {
1182 #[inline]
1183 fn write_str(&mut self, value: &str) -> fmt::Result {
1184 if self.matches {
1185 let bytes = value.as_bytes();
1186 let end = self.position + bytes.len();
1187 if end > self.target.len() || self.target[self.position..end] != *bytes {
1188 self.matches = false;
1189 }
1190 self.position = end;
1191 }
1192 Ok(())
1193 }
1194}
1195
1196#[inline]
1197fn jid_display_eq(user: &str, server: Server, agent: u8, device: u16, other: &str) -> bool {
1198 let mut writer = StrEqWriter {
1199 target: other.as_bytes(),
1200 position: 0,
1201 matches: true,
1202 };
1203 let written = write_jid_fallible(&mut writer, user, server, agent, device).is_ok();
1204 written && writer.matches && writer.position == other.len()
1205}
1206
1207#[inline]
1208fn jid_displays_equal(left: (&str, Server, u8, u16), right: (&str, Server, u8, u16)) -> bool {
1209 let (left_user, left_server, left_agent, left_device) = left;
1210 let (right_user, right_server, right_agent, right_device) = right;
1211 if left_user.is_empty() || right_user.is_empty() {
1212 return left_user.is_empty() && right_user.is_empty() && left_server == right_server;
1213 }
1214
1215 left_user == right_user
1216 && left_server == right_server
1217 && left_device == right_device
1218 && (!left_server.renders_agent() || left_agent == right_agent)
1219}
1220
1221impl fmt::Display for Jid {
1222 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1223 let mut w = JidStackWriter::new();
1224 if self.write_display_to(&mut w).is_ok() {
1225 return f.write_str(w.as_str());
1226 }
1227 self.write_display_to(f)
1228 }
1229}
1230
1231pub struct ObservedJid<'a>(&'a Jid);
1249
1250impl Jid {
1251 #[inline]
1253 pub fn observe(&self) -> ObservedJid<'_> {
1254 ObservedJid(self)
1255 }
1256}
1257
1258pub fn observe_token(s: &str) -> u64 {
1265 use std::hash::BuildHasher;
1266 static KEY: std::sync::OnceLock<std::collections::hash_map::RandomState> =
1267 std::sync::OnceLock::new();
1268 KEY.get_or_init(std::collections::hash_map::RandomState::new)
1269 .hash_one(s)
1270}
1271
1272pub fn observe_str(s: &str) -> String {
1276 if cfg!(feature = "tracing-pii") {
1277 return s.to_string();
1278 }
1279 match s.parse::<Jid>() {
1280 Ok(jid) => jid.observe().to_string(),
1281 Err(_) => format!("?#{:016x}", observe_token(s)),
1282 }
1283}
1284
1285impl fmt::Display for ObservedJid<'_> {
1286 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1287 let jid = self.0;
1288 if jid.user.is_empty() || cfg!(feature = "tracing-pii") {
1289 return fmt::Display::fmt(jid, f);
1290 }
1291 let redacted: Option<String> = if jid.server.carries_phone_number() {
1293 Some(format!("pn#{:016x}", observe_token(jid.user.as_str())))
1295 } else if matches!(jid.server, Server::Group | Server::Broadcast) {
1296 match jid.user.find('-') {
1300 Some(i) if i > 0 && jid.user.as_bytes()[..i].iter().all(|b| b.is_ascii_digit()) => {
1301 Some(format!(
1302 "pn#{:016x}{}",
1303 observe_token(&jid.user[..i]),
1304 &jid.user[i..]
1305 ))
1306 }
1307 _ => None,
1308 }
1309 } else {
1310 None
1311 };
1312 let Some(user) = redacted else {
1313 return fmt::Display::fmt(jid, f);
1315 };
1316 f.write_str(&user)?;
1317 if jid.agent > 0 && jid.server.renders_agent() {
1320 write!(f, ".{}", jid.agent)?;
1321 }
1322 if jid.device > 0 {
1323 write!(f, ":{}", jid.device)?;
1324 }
1325 write!(f, "@{}", jid.server.as_str())
1326 }
1327}
1328
1329impl<'a> fmt::Display for JidRef<'a> {
1330 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1331 let mut w = JidStackWriter::new();
1332 if write_jid_fallible(&mut w, &self.user, self.server, self.agent, self.device).is_ok() {
1333 return f.write_str(w.as_str());
1334 }
1335 write_jid_fallible(f, &self.user, self.server, self.agent, self.device)
1336 }
1337}
1338
1339impl From<Jid> for String {
1340 fn from(jid: Jid) -> Self {
1341 jid.to_string()
1342 }
1343}
1344
1345impl From<&Jid> for Jid {
1349 fn from(jid: &Jid) -> Self {
1350 jid.clone()
1351 }
1352}
1353
1354impl<'a> From<JidRef<'a>> for String {
1355 fn from(jid: JidRef<'a>) -> Self {
1356 jid.to_string()
1357 }
1358}
1359
1360impl TryFrom<String> for Jid {
1361 type Error = JidError;
1362 fn try_from(value: String) -> Result<Self, Self::Error> {
1363 Jid::from_str(&value)
1364 }
1365}
1366
1367#[cfg(test)]
1368mod tests {
1369 use super::*;
1370 use std::str::FromStr;
1371
1372 #[test]
1373 fn is_psa_matches_system_jid_in_both_user_namespaces() {
1374 assert!(Jid::from_str("0@s.whatsapp.net").unwrap().is_psa());
1375 assert!(Jid::from_str("0@c.us").unwrap().is_psa());
1376 assert!(parse_jid_ref("0@s.whatsapp.net").unwrap().is_psa());
1377 }
1378
1379 #[test]
1380 fn is_psa_rejects_regular_and_non_pn_jids() {
1381 assert!(!Jid::from_str("10@s.whatsapp.net").unwrap().is_psa());
1382 assert!(!Jid::from_str("0@lid").unwrap().is_psa());
1383 assert!(!Jid::from_str("status@broadcast").unwrap().is_psa());
1384 assert!(!Jid::from_str("0@g.us").unwrap().is_psa());
1385 }
1386
1387 #[test]
1392 fn display_reads_back_every_stack_buffer_shape() {
1393 let emoji = Jid::new("héllo→世界", Server::Lid);
1396 assert_eq!(emoji.to_string(), "héllo→世界@lid");
1397 assert_eq!(format!("{}", emoji.observe()), "héllo→世界@lid");
1398
1399 let full = Jid::new("9".repeat(60), Server::Lid);
1402 let rendered = full.to_string();
1403 assert_eq!(rendered.len(), 64);
1404 assert!(rendered.ends_with("@lid"));
1405
1406 let over = Jid::new("9".repeat(61), Server::Lid);
1409 assert_eq!(over.to_string(), format!("{}@lid", "9".repeat(61)));
1410
1411 let borrowed = parse_jid_ref("12025550111:7@s.whatsapp.net").unwrap();
1413 assert_eq!(borrowed.to_string(), "12025550111:7@s.whatsapp.net");
1414 assert_eq!(&*emoji.to_non_ad_arc_str(), "héllo→世界@lid");
1415 assert_eq!(
1416 &*over.to_non_ad_arc_str(),
1417 &*format!("{}@lid", "9".repeat(61))
1418 );
1419 }
1420
1421 #[test]
1428 fn phash_form_writes_the_agent_position_as_zero_like_wa_web() {
1429 let plain = Jid {
1430 user: "5511999998888".into(),
1431 server: Server::Lid,
1432 agent: 0,
1433 device: 3,
1434 integrator: 0,
1435 };
1436 assert_eq!(plain.to_phash_form_string(), "5511999998888.0:3@lid");
1437
1438 let with_agent = Jid {
1439 agent: 7,
1440 ..plain.clone()
1441 };
1442 assert_eq!(
1443 with_agent.to_phash_form_string(),
1444 "5511999998888.0:3@lid",
1445 "the agent must not reach the hashed string"
1446 );
1447
1448 assert_eq!(plain, with_agent);
1450 assert_eq!(
1451 plain.to_phash_form_string(),
1452 with_agent.to_phash_form_string()
1453 );
1454
1455 assert_eq!(
1457 Jid::new("", Server::Pn).to_phash_form_string(),
1458 "s.whatsapp.net"
1459 );
1460 assert_ne!(
1461 plain.to_phash_form_string(),
1462 Jid {
1463 device: 4,
1464 ..plain.clone()
1465 }
1466 .to_phash_form_string()
1467 );
1468 }
1469
1470 #[test]
1478 fn inert_agent_stays_out_of_identity() {
1479 use std::collections::hash_map::DefaultHasher;
1480 use std::hash::{Hash, Hasher};
1481
1482 fn hash_of(jid: &Jid) -> u64 {
1483 let mut h = DefaultHasher::new();
1484 jid.hash(&mut h);
1485 h.finish()
1486 }
1487
1488 for server in [Server::Pn, Server::Lid, Server::Hosted, Server::HostedLid] {
1491 let clean = Jid {
1492 user: "123456789012345".into(),
1493 server,
1494 agent: 0,
1495 device: 7,
1496 integrator: 0,
1497 };
1498 let with_agent = Jid {
1499 agent: 1,
1500 ..clean.clone()
1501 };
1502 assert_eq!(
1503 clean, with_agent,
1504 "{server:?}: agent must not split identity"
1505 );
1506 assert_eq!(
1507 hash_of(&clean),
1508 hash_of(&with_agent),
1509 "{server:?}: Hash must agree with Eq"
1510 );
1511
1512 let with_integrator = Jid {
1515 integrator: 0xBEEF,
1516 ..clean.clone()
1517 };
1518 assert_ne!(
1519 clean, with_integrator,
1520 "{server:?}: integrator stays in identity, matching is_same_chat_as"
1521 );
1522 assert_eq!(
1523 clean.is_same_chat_as(&with_integrator),
1524 clean == with_integrator,
1525 "{server:?}: == must agree with is_same_chat_as"
1526 );
1527
1528 let borrowed = JidRef {
1530 user: NodeStr::Borrowed("123456789012345"),
1531 server,
1532 agent: 1,
1533 device: 7,
1534 integrator: 0,
1535 };
1536 assert_eq!(clean, borrowed, "{server:?}: owned == borrowed");
1537 assert_eq!(borrowed, clean, "{server:?}: borrowed == owned");
1538 }
1539
1540 let bot = Jid {
1542 user: "123456789".into(),
1543 server: Server::Interop,
1544 agent: 4,
1545 device: 0,
1546 integrator: 0,
1547 };
1548 let other_agent = Jid {
1549 agent: 5,
1550 ..bot.clone()
1551 };
1552 assert_ne!(
1553 bot, other_agent,
1554 "interop renders the agent, so it is identity"
1555 );
1556 assert_ne!(
1557 bot,
1558 Jid {
1559 integrator: 9,
1560 ..bot.clone()
1561 },
1562 "interop is where integrator is real"
1563 );
1564
1565 let pn = Jid::new("123456789012345", Server::Pn);
1567 assert_ne!(
1568 pn,
1569 Jid {
1570 device: 1,
1571 ..pn.clone()
1572 }
1573 );
1574 assert_ne!(pn, Jid::new("123456789012346", Server::Pn));
1575 assert_ne!(pn, Jid::new("123456789012345", Server::Lid));
1576 }
1577
1578 #[test]
1579 fn display_eq_matches_owned_and_borrowed_jids_without_normalizing() {
1580 let canonical = "123456789.4:17@interop";
1581 let owned = Jid::from_str(canonical).unwrap();
1582 let borrowed = parse_jid_ref(canonical).unwrap();
1583
1584 for value in [canonical, "123456789.4:16@interop", "123456789@interop", ""] {
1585 assert_eq!(owned.display_eq(value), value == canonical);
1586 assert_eq!(borrowed.display_eq(value), value == canonical);
1587 }
1588
1589 let long_user = "a".repeat(128);
1590 let long_value = format!("{long_user}@lid");
1591 let long_jid = Jid::lid(long_user);
1592 assert!(long_jid.display_eq(&long_value));
1593 assert!(!long_jid.display_eq(&format!("{long_value}x")));
1594 }
1595
1596 #[test]
1597 fn display_eq_jid_uses_only_rendered_components() {
1598 let pn = Jid {
1599 user: "12025550111".into(),
1600 server: Server::Pn,
1601 agent: 1,
1602 device: 7,
1603 integrator: 3,
1604 };
1605 let pn_same_display = Jid {
1606 agent: 2,
1607 integrator: 9,
1608 ..pn.clone()
1609 };
1610 assert_eq!(pn.to_string(), pn_same_display.to_string());
1611 assert!(pn.display_eq_jid(&pn_same_display));
1612
1613 let pn_other_device = Jid {
1614 device: 8,
1615 ..pn.clone()
1616 };
1617 assert!(!pn.display_eq_jid(&pn_other_device));
1618
1619 let bot = Jid {
1620 user: "13136555001".into(),
1621 server: Server::Bot,
1622 agent: 1,
1623 device: 0,
1624 integrator: 0,
1625 };
1626 let other_bot_agent = Jid {
1627 agent: 2,
1628 ..bot.clone()
1629 };
1630 assert!(!bot.display_eq_jid(&other_bot_agent));
1631
1632 let server_only = Jid {
1633 user: "".into(),
1634 server: Server::Pn,
1635 agent: 1,
1636 device: 7,
1637 integrator: 3,
1638 };
1639 let same_server_only = Jid {
1640 agent: 2,
1641 device: 9,
1642 integrator: 4,
1643 ..server_only.clone()
1644 };
1645 assert_eq!(server_only.to_string(), same_server_only.to_string());
1646 assert!(server_only.display_eq_jid(&same_server_only));
1647
1648 let borrowed = JidRef {
1649 user: NodeStr::Borrowed("12025550111"),
1650 server: Server::Pn,
1651 agent: 1,
1652 device: 7,
1653 integrator: 0,
1654 };
1655 let borrowed_same_display = JidRef {
1656 agent: 2,
1657 ..borrowed.clone()
1658 };
1659 assert!(borrowed.display_eq_jid(&borrowed_same_display));
1660 }
1661
1662 #[test]
1663 fn owned_and_borrowed_jids_compare_without_conversion() {
1664 let owned = Jid {
1665 user: "12025550111".into(),
1666 server: Server::Pn,
1667 agent: 0,
1668 device: 7,
1669 integrator: 0,
1670 };
1671 let borrowed = JidRef {
1672 user: NodeStr::Borrowed("12025550111"),
1673 server: Server::Pn,
1674 agent: 0,
1675 device: 7,
1676 integrator: 0,
1677 };
1678
1679 assert_eq!(owned, borrowed);
1680 assert_eq!(borrowed, owned);
1681
1682 let other_device = JidRef {
1683 device: 8,
1684 ..borrowed.clone()
1685 };
1686 assert_ne!(owned, other_device);
1687 }
1688
1689 #[cfg(feature = "serde")]
1690 #[test]
1691 fn server_deserializes_borrowed_and_owned_strings() {
1692 let borrowed: Server = serde_json::from_str("\"s.whatsapp.net\"").unwrap();
1693 let owned: Server =
1694 serde_json::from_value(serde_json::Value::String("lid".to_owned())).unwrap();
1695
1696 assert_eq!(borrowed, Server::Pn);
1697 assert_eq!(owned, Server::Lid);
1698 }
1699
1700 #[test]
1703 #[cfg(not(feature = "tracing-pii"))]
1704 fn observe_redacts_phone_but_not_lid_or_group() {
1705 let pn = Jid::from_str("5511999998888:7@s.whatsapp.net").unwrap();
1706 let shown = pn.observe().to_string();
1707 assert!(shown.starts_with("pn#"), "{shown}");
1708 assert!(
1709 !shown.contains("5511999998888"),
1710 "raw number leaked: {shown}"
1711 );
1712 assert!(shown.ends_with(":7@s.whatsapp.net"), "device lost: {shown}");
1713 assert_eq!(shown, pn.observe().to_string());
1715
1716 let lid = Jid::from_str("123456789@lid").unwrap();
1718 assert_eq!(lid.observe().to_string(), lid.to_string());
1719 let group = Jid::from_str("120363012345678901@g.us").unwrap();
1720 assert_eq!(group.observe().to_string(), group.to_string());
1721
1722 let legacy = Jid::from_str("123456789-1620000000@g.us").unwrap();
1725 let ls = legacy.observe().to_string();
1726 assert!(
1727 ls.starts_with("pn#") && ls.ends_with("-1620000000@g.us"),
1728 "{ls}"
1729 );
1730 assert!(!ls.contains("123456789"), "creator phone leaked: {ls}");
1732 let mid = &ls["pn#".len()..ls.find('-').unwrap()];
1734 assert_eq!(mid.len(), 16, "token width: {ls}");
1735 assert!(
1736 mid.bytes().all(|b| b.is_ascii_hexdigit()),
1737 "token hex: {ls}"
1738 );
1739 }
1740
1741 fn assert_jid_roundtrip(
1743 input: &str,
1744 expected_user: &str,
1745 expected_server: &str,
1746 expected_device: u16,
1747 expected_agent: u8,
1748 ) {
1749 assert_jid_parse_and_display(
1750 input,
1751 expected_user,
1752 expected_server,
1753 expected_device,
1754 expected_agent,
1755 input,
1756 );
1757 }
1758
1759 fn assert_jid_parse_and_display(
1761 input: &str,
1762 expected_user: &str,
1763 expected_server: &str,
1764 expected_device: u16,
1765 expected_agent: u8,
1766 expected_output: &str,
1767 ) {
1768 let jid = Jid::from_str(input).unwrap_or_else(|_| panic!("Failed to parse JID: {}", input));
1770
1771 assert_eq!(
1772 jid.user, expected_user,
1773 "User part did not match for {}",
1774 input
1775 );
1776 assert_eq!(
1777 jid.server, expected_server,
1778 "Server part did not match for {}",
1779 input
1780 );
1781 assert_eq!(
1782 jid.device, expected_device,
1783 "Device part did not match for {}",
1784 input
1785 );
1786 assert_eq!(
1787 jid.agent, expected_agent,
1788 "Agent part did not match for {}",
1789 input
1790 );
1791
1792 let formatted = jid.to_string();
1794 assert_eq!(
1795 formatted, expected_output,
1796 "Formatted string did not match expected output for {}",
1797 input
1798 );
1799 }
1800
1801 #[test]
1802 fn borrowed_parser_matches_owned_fast_path_and_bot_classification() {
1803 for raw in [
1804 "5511999998888:7@s.whatsapp.net",
1805 "120363012345678901@g.us",
1806 "123456789012345@lid",
1807 "assistant@bot",
1808 "13135551234@s.whatsapp.net",
1809 ] {
1810 let borrowed = parse_jid_ref(raw).expect("common JID should stay borrowed");
1811 let owned = raw.parse::<Jid>().unwrap();
1812
1813 assert!(matches!(borrowed.user, NodeStr::Borrowed(_)));
1814 assert_eq!(borrowed.to_owned(), owned);
1815 assert_eq!(borrowed.is_bot(), owned.is_bot());
1816 }
1817
1818 assert!(parse_jid_ref("g.us").is_none(), "server-only uses fallback");
1819 assert!(parse_jid_ref("user@unknown").is_none());
1820 }
1821
1822 #[test]
1826 fn decimal_fast_path_matches_u16_from_str() {
1827 for raw in [
1828 "", "+", "-", "0", "7", "+7", "007", "33", "255", "256", "65535", "65536", "99999",
1829 "-0", "-1", "1x", "x1", " 1", "1 ", "1_0", "+-1", "++1", "1.0", "٣", "𝟛",
1830 ] {
1831 assert_eq!(
1832 parse_u16_decimal(raw),
1833 raw.parse::<u16>().ok(),
1834 "mismatch for {raw:?}"
1835 );
1836 }
1837 }
1838
1839 #[test]
1849 fn fast_parse_pins_the_separator_rules_per_server() {
1850 let cases = [
1852 ("123456789.4:17@interop", "123456789", 4u8, 17u16),
1854 ("123456789.4@interop", "123456789", 4, 0),
1855 ("123.999@interop", "123.999", 0, 0),
1857 ("5511999998888.2@s.whatsapp.net", "5511999998888", 0, 2),
1859 ("12345.678@lid", "12345.678", 0, 0),
1861 ("12345.678:9@lid", "12345.678", 0, 9),
1862 ("12345.6@hosted.lid", "12345", 6, 0),
1867 ("12345.678@hosted.lid", "12345.678", 0, 0),
1868 ("12345.6@c.us", "12345", 6, 0),
1869 ("a:1.5:2@bot", "a:1", 5, 2),
1872 ("5511999998888:x@s.whatsapp.net", "5511999998888", 0, 0),
1874 ];
1875
1876 for (raw, user, agent, device) in cases {
1877 let fast =
1878 parse_jid_fast(raw).unwrap_or_else(|| panic!("{raw} should take the fast path"));
1879 assert_eq!(fast.user, user, "user for {raw}");
1880 assert_eq!(fast.agent, agent, "agent for {raw}");
1881 assert_eq!(fast.device, device, "device for {raw}");
1882 }
1883 }
1884
1885 #[test]
1886 fn test_jid_parsing_and_display_roundtrip() {
1887 assert_jid_roundtrip(
1889 "1234567890@s.whatsapp.net",
1890 "1234567890",
1891 "s.whatsapp.net",
1892 0,
1893 0,
1894 );
1895 assert_jid_roundtrip(
1896 "1234567890:15@s.whatsapp.net",
1897 "1234567890",
1898 "s.whatsapp.net",
1899 15,
1900 0,
1901 );
1902 assert_jid_roundtrip("123-456@g.us", "123-456", "g.us", 0, 0);
1903
1904 assert_jid_roundtrip("s.whatsapp.net", "", "s.whatsapp.net", 0, 0);
1907
1908 assert_jid_roundtrip("12345.6789@lid", "12345.6789", "lid", 0, 0);
1910 assert_jid_roundtrip("12345.6789:25@lid", "12345.6789", "lid", 25, 0);
1911
1912 assert_jid_roundtrip("12345@call", "12345", "call", 0, 0);
1914 }
1915
1916 #[test]
1917 fn test_special_from_str_parsing() {
1918 let jid = Jid::from_str("1234567890.2:15@hosted").expect("test hosted JID should be valid");
1920 assert_eq!(jid.user, "1234567890");
1921 assert_eq!(jid.server, "hosted");
1922 assert_eq!(jid.device, 15);
1923 assert_eq!(jid.agent, 2);
1924 }
1925
1926 #[test]
1927 fn test_manual_jid_formatting_edge_cases() {
1928 let jid1 = Jid {
1935 user: "1234567890".into(),
1936 server: Server::Pn,
1937 device: 15,
1938 agent: 2,
1939 integrator: 0,
1940 };
1941 assert_eq!(jid1.to_string(), "1234567890:15@s.whatsapp.net");
1942
1943 let jid2 = Jid {
1944 user: "12345.6789".into(),
1945 server: Server::Lid,
1946 device: 25,
1947 agent: 1,
1948 integrator: 0,
1949 };
1950 assert_eq!(jid2.to_string(), "12345.6789:25@lid");
1951
1952 let jid3 = Jid {
1953 user: "1234567890".into(),
1954 server: Server::Hosted,
1955 device: 15,
1956 agent: 2,
1957 integrator: 0,
1958 };
1959 assert_eq!(jid3.to_string(), "1234567890:15@hosted");
1960
1961 let jid4 = Jid {
1963 user: "user".into(),
1964 server: Server::Bot,
1965 device: 10,
1966 agent: 5,
1967 integrator: 0,
1968 };
1969 assert_eq!(jid4.to_string(), "user.5:10@bot");
1970 }
1971
1972 #[test]
1973 fn test_invalid_jids_should_fail_to_parse() {
1974 assert!(Jid::from_str("thisisnotajid").is_err());
1975 assert!(Jid::from_str("").is_err());
1976 assert!(Jid::from_str("@s.whatsapp.net").is_ok());
1978 assert!(Jid::from_str("@unknown.server").is_err());
1980 assert!(Jid::from_str("2").is_err());
1983 }
1984
1985 #[test]
2013 fn test_is_hosted_device_detection() {
2014 let cloud_api_device: Jid = "5511999887766:99@s.whatsapp.net"
2019 .parse()
2020 .expect("test JID should be valid");
2021 assert!(
2022 cloud_api_device.is_hosted(),
2023 "Device ID 99 on s.whatsapp.net should be detected as hosted (Cloud API)"
2024 );
2025
2026 let cloud_api_lid: Jid = "100000012345678:99@lid"
2028 .parse()
2029 .expect("test JID should be valid");
2030 assert!(
2031 cloud_api_lid.is_hosted(),
2032 "Device ID 99 on lid server should be detected as hosted"
2033 );
2034
2035 let hosted_server: Jid = "5511999887766:99@hosted"
2037 .parse()
2038 .expect("test JID should be valid");
2039 assert!(
2040 hosted_server.is_hosted(),
2041 "JID with @hosted server should be detected as hosted"
2042 );
2043
2044 let hosted_lid_server: Jid = "100000012345678:99@hosted.lid"
2046 .parse()
2047 .expect("test JID should be valid");
2048 assert!(
2049 hosted_lid_server.is_hosted(),
2050 "JID with @hosted.lid server should be detected as hosted"
2051 );
2052
2053 let hosted_server_other_device: Jid = "5511999887766:0@hosted"
2056 .parse()
2057 .expect("test JID should be valid");
2058 assert!(
2059 hosted_server_other_device.is_hosted(),
2060 "JID with @hosted server should be hosted regardless of device ID"
2061 );
2062
2063 let regular_phone: Jid = "5511999887766:0@s.whatsapp.net"
2067 .parse()
2068 .expect("test JID should be valid");
2069 assert!(
2070 !regular_phone.is_hosted(),
2071 "Regular phone device (ID 0) should NOT be hosted"
2072 );
2073
2074 let companion_device: Jid = "5511999887766:33@s.whatsapp.net"
2076 .parse()
2077 .expect("test JID should be valid");
2078 assert!(
2079 !companion_device.is_hosted(),
2080 "Companion device (ID 33) should NOT be hosted"
2081 );
2082
2083 let regular_lid: Jid = "100000012345678:0@lid"
2085 .parse()
2086 .expect("test JID should be valid");
2087 assert!(
2088 !regular_lid.is_hosted(),
2089 "Regular LID device should NOT be hosted"
2090 );
2091
2092 let lid_companion: Jid = "100000012345678:33@lid"
2094 .parse()
2095 .expect("test JID should be valid");
2096 assert!(
2097 !lid_companion.is_hosted(),
2098 "LID companion device (ID 33) should NOT be hosted"
2099 );
2100
2101 let group_jid: Jid = "120363012345678@g.us"
2103 .parse()
2104 .expect("test JID should be valid");
2105 assert!(
2106 !group_jid.is_hosted(),
2107 "Group JID should NOT be detected as hosted"
2108 );
2109
2110 let user_jid: Jid = "5511999887766@s.whatsapp.net"
2112 .parse()
2113 .expect("test JID should be valid");
2114 assert!(
2115 !user_jid.is_hosted(),
2116 "User JID without device should NOT be hosted"
2117 );
2118
2119 let bot_jid: Jid = "13136555001:0@s.whatsapp.net"
2121 .parse()
2122 .expect("test JID should be valid");
2123 assert!(
2124 !bot_jid.is_hosted(),
2125 "Bot JID should NOT be detected as hosted (different mechanism)"
2126 );
2127 }
2128
2129 #[test]
2130 fn is_same_chat_as_matches_rendered_chat_identity() {
2131 let base: Jid = "5511999887766@s.whatsapp.net".parse().unwrap();
2132
2133 assert!(base.is_same_chat_as(&base.with_device(33)));
2135 assert!(base.with_device(5).is_same_chat_as(&base.with_device(0)));
2136
2137 let other_user: Jid = "5521988776655@s.whatsapp.net".parse().unwrap();
2140 assert!(!base.is_same_chat_as(&other_user));
2141 let as_lid: Jid = "5511999887766@lid".parse().unwrap();
2142 assert!(!base.is_same_chat_as(&as_lid));
2143
2144 let other_integrator = Jid {
2146 integrator: 1,
2147 ..base.clone()
2148 };
2149 assert!(!base.is_same_chat_as(&other_integrator));
2150
2151 let pn_agent1 = Jid {
2154 agent: 1,
2155 ..base.clone()
2156 };
2157 assert_eq!(base.to_string(), pn_agent1.to_string());
2158 assert!(base.is_same_chat_as(&pn_agent1));
2159
2160 let bot_a = Jid {
2163 user: "13136555001".into(),
2164 server: Server::Bot,
2165 agent: 1,
2166 device: 0,
2167 integrator: 0,
2168 };
2169 let bot_b = Jid {
2170 agent: 2,
2171 ..bot_a.clone()
2172 };
2173 assert_ne!(bot_a.to_string(), bot_b.to_string());
2174 assert!(!bot_a.is_same_chat_as(&bot_b));
2175 assert!(bot_a.is_same_chat_as(&bot_a.with_device(7)));
2176 }
2177
2178 #[test]
2189 fn test_hosted_device_filtering_for_groups() {
2190 let devices: Vec<Jid> = vec![
2192 "5511999887766:0@s.whatsapp.net"
2194 .parse()
2195 .expect("test JID should be valid"), "5511999887766:33@s.whatsapp.net"
2197 .parse()
2198 .expect("test JID should be valid"), "5521988776655:0@s.whatsapp.net"
2200 .parse()
2201 .expect("test JID should be valid"), "100000012345678:0@lid"
2203 .parse()
2204 .expect("test JID should be valid"), "100000012345678:33@lid"
2206 .parse()
2207 .expect("test JID should be valid"), "5531977665544:99@s.whatsapp.net"
2210 .parse()
2211 .expect("test JID should be valid"), "100000087654321:99@lid"
2213 .parse()
2214 .expect("test JID should be valid"), "5541966554433:99@hosted"
2216 .parse()
2217 .expect("test JID should be valid"), ];
2219
2220 let filtered: Vec<&Jid> = devices.iter().filter(|jid| !jid.is_hosted()).collect();
2222
2223 assert_eq!(
2225 filtered.len(),
2226 5,
2227 "Should have 5 non-hosted devices after filtering"
2228 );
2229
2230 for jid in &filtered {
2232 assert!(
2233 !jid.is_hosted(),
2234 "Filtered list should not contain hosted devices: {}",
2235 jid
2236 );
2237 }
2238
2239 let hosted_count = devices.iter().filter(|jid| jid.is_hosted()).count();
2241 assert_eq!(hosted_count, 3, "Should have filtered out 3 hosted devices");
2242 }
2243
2244 #[test]
2245 fn test_jid_pn_factory() {
2246 let jid = Jid::pn("1234567890");
2247 assert_eq!(jid.user, "1234567890");
2248 assert_eq!(jid.server, DEFAULT_USER_SERVER);
2249 assert_eq!(jid.device, 0);
2250 assert!(jid.is_pn());
2251 }
2252
2253 #[test]
2254 fn test_jid_lid_factory() {
2255 let jid = Jid::lid("100000012345678");
2256 assert_eq!(jid.user, "100000012345678");
2257 assert_eq!(jid.server, HIDDEN_USER_SERVER);
2258 assert_eq!(jid.device, 0);
2259 assert!(jid.is_lid());
2260 }
2261
2262 #[test]
2263 fn test_jid_group_factory() {
2264 let jid = Jid::group("123456789-1234567890");
2265 assert_eq!(jid.user, "123456789-1234567890");
2266 assert_eq!(jid.server, GROUP_SERVER);
2267 assert!(jid.is_group());
2268 }
2269
2270 #[test]
2271 fn test_jid_pn_device_factory() {
2272 let jid = Jid::pn_device("1234567890", 5);
2273 assert_eq!(jid.user, "1234567890");
2274 assert_eq!(jid.server, DEFAULT_USER_SERVER);
2275 assert_eq!(jid.device, 5);
2276 assert!(jid.is_pn());
2277 assert!(jid.is_ad());
2278 }
2279
2280 #[test]
2281 fn test_jid_lid_device_factory() {
2282 let jid = Jid::lid_device("100000012345678", 33);
2283 assert_eq!(jid.user, "100000012345678");
2284 assert_eq!(jid.server, HIDDEN_USER_SERVER);
2285 assert_eq!(jid.device, 33);
2286 assert!(jid.is_lid());
2287 assert!(jid.is_ad());
2288 }
2289
2290 #[test]
2291 fn with_device_hosting_preserves_addressing_family() {
2292 let hosted_pn = Jid::pn("1234567890").with_device_hosting(7, true);
2293 assert_eq!(hosted_pn.server, Server::Hosted);
2294 assert_eq!(hosted_pn.device, 7);
2295
2296 let hosted_lid = Jid::lid("100000012345678").with_device_hosting(8, true);
2297 assert_eq!(hosted_lid.server, Server::HostedLid);
2298 assert_eq!(hosted_lid.device, 8);
2299
2300 let regular = Jid::pn("1234567890").with_device_hosting(9, false);
2301 assert_eq!(regular.server, Server::Pn);
2302
2303 let regular_from_hosted =
2304 Jid::new("1234567890", Server::Hosted).with_device_hosting(9, false);
2305 assert_eq!(regular_from_hosted.server, Server::Pn);
2306
2307 let group = Jid::group("123-456").with_device_hosting(10, true);
2308 assert_eq!(group.server, Server::Group);
2309 }
2310
2311 #[test]
2312 fn test_status_broadcast_jid() {
2313 let jid = Jid::status_broadcast();
2314 assert_eq!(jid.user, STATUS_BROADCAST_USER);
2315 assert_eq!(jid.server, BROADCAST_SERVER);
2316 assert_eq!(jid.device, 0);
2317 assert!(jid.is_status_broadcast());
2318 assert!(!jid.is_group());
2319 assert!(!jid.is_broadcast_list());
2320 assert_eq!(jid.to_string(), "status@broadcast");
2321
2322 let parsed: Jid = "status@broadcast".parse().expect("should parse");
2324 assert!(parsed.is_status_broadcast());
2325 assert_eq!(parsed.user, "status");
2326 assert_eq!(parsed.server, "broadcast");
2327
2328 let broadcast_list = Jid::new("12345", Server::Broadcast);
2330 assert!(broadcast_list.is_broadcast_list());
2331 assert!(!broadcast_list.is_status_broadcast());
2332
2333 let group: Jid = "120363012345678901@g.us".parse().expect("should parse");
2335 assert!(group.is_group());
2336 assert!(!group.is_status_broadcast());
2337 }
2338
2339 #[test]
2340 fn test_jid_to_non_ad_preserves_user_server() {
2341 let device_jid = Jid::pn_device("1234567890", 33);
2343 let non_ad = device_jid.to_non_ad();
2344 assert_eq!(non_ad.user, "1234567890");
2345 assert_eq!(non_ad.server, DEFAULT_USER_SERVER);
2346 assert_eq!(non_ad.device, 0);
2347 assert!(!non_ad.is_ad());
2348
2349 let lid_device = Jid::lid_device("100000012345678", 25);
2351 let lid_non_ad = lid_device.to_non_ad();
2352 assert_eq!(lid_non_ad.user, "100000012345678");
2353 assert_eq!(lid_non_ad.server, HIDDEN_USER_SERVER);
2354 assert_eq!(lid_non_ad.device, 0);
2355
2356 let status = Jid::status_broadcast();
2358 let status_non_ad = status.to_non_ad();
2359 assert_eq!(status_non_ad.to_string(), "status@broadcast");
2360 }
2361
2362 #[test]
2363 fn test_to_non_ad_string_matches_to_non_ad_to_string() {
2364 for s in [
2367 "1234567890:33@s.whatsapp.net",
2368 "1234567890@s.whatsapp.net",
2369 "100000012345678:25@lid",
2370 "100000012345678@lid",
2371 "867051314767696:0@bot",
2372 "867051314767696@bot",
2373 "120363021033254949@g.us",
2374 "status@broadcast",
2375 "12-34@g.us",
2376 ] {
2377 let jid: Jid = s.parse().expect("parse");
2378 assert_eq!(
2379 jid.to_non_ad_string(),
2380 jid.to_non_ad().to_string(),
2381 "mismatch for {s}"
2382 );
2383 }
2384 }
2385
2386 #[test]
2391 fn to_non_ad_arc_str_matches_to_non_ad_string() {
2392 let long_user = "9".repeat(80);
2393 let multibyte_user = "ẞünïcodé-ñ".repeat(3);
2394 let owned = [
2395 format!("{long_user}:12@s.whatsapp.net"),
2396 format!("{multibyte_user}@g.us"),
2397 format!("{multibyte_user}@s.whatsapp.net"),
2398 ];
2399 let cases = [
2400 "1234567890:33@s.whatsapp.net",
2401 "1234567890@s.whatsapp.net",
2402 "100000012345678:25@lid",
2403 "867051314767696:0@bot",
2404 "867051314767696.5:10@bot",
2409 "120363021033254949@g.us",
2410 "status@broadcast",
2411 ]
2412 .into_iter()
2413 .chain(owned.iter().map(String::as_str));
2414
2415 for s in cases {
2416 let jid: Jid = s.parse().unwrap_or_else(|e| panic!("parse {s}: {e}"));
2417 assert_eq!(
2418 &*jid.to_non_ad_arc_str(),
2419 jid.to_non_ad_string().as_str(),
2420 "mismatch for {s}"
2421 );
2422 }
2423
2424 let empty = Jid::default();
2427 assert_eq!(
2428 &*empty.to_non_ad_arc_str(),
2429 empty.to_non_ad_string().as_str()
2430 );
2431 }
2432
2433 #[test]
2434 fn test_into_non_ad_matches_to_non_ad() {
2435 for s in [
2437 "1234567890.2:33@s.whatsapp.net",
2438 "1234567890@s.whatsapp.net",
2439 "100000012345678:25@lid",
2440 "user.5:10@bot",
2441 "447911123456.3@interop",
2442 "120363021033254949@g.us",
2443 "status@broadcast",
2444 ] {
2445 let jid: Jid = s.parse().expect("parse");
2446 assert_eq!(
2447 jid.clone().into_non_ad(),
2448 jid.to_non_ad(),
2449 "mismatch for {s}"
2450 );
2451 }
2452 }
2453
2454 #[test]
2455 fn test_jid_factories_with_string_types() {
2456 let jid1 = Jid::pn("123");
2458 assert_eq!(jid1.user, "123");
2459
2460 let jid2 = Jid::lid(String::from("456"));
2462 assert_eq!(jid2.user, "456");
2463
2464 let user = "789".to_string();
2466 let jid3 = Jid::group(user);
2467 assert_eq!(jid3.user, "789");
2468 }
2469
2470 #[test]
2475 fn test_jid_format_parity() {
2476 struct Case {
2477 user: &'static str,
2478 server: Server,
2479 agent: u8,
2480 device: u16,
2481 }
2482
2483 let cases = [
2484 Case {
2486 user: "",
2487 server: Server::Pn,
2488 agent: 0,
2489 device: 0,
2490 },
2491 Case {
2493 user: "5511999887766",
2494 server: Server::Pn,
2495 agent: 0,
2496 device: 0,
2497 },
2498 Case {
2500 user: "5511999887766",
2501 server: Server::Pn,
2502 agent: 0,
2503 device: 2,
2504 },
2505 Case {
2507 user: "5511999887766",
2508 server: Server::Pn,
2509 agent: 3,
2510 device: 15,
2511 },
2512 Case {
2514 user: "12345.6789",
2515 server: Server::Lid,
2516 agent: 1,
2517 device: 25,
2518 },
2519 Case {
2521 user: "100000012345678",
2522 server: Server::Hosted,
2523 agent: 2,
2524 device: 99,
2525 },
2526 Case {
2528 user: "100000012345678",
2529 server: Server::HostedLid,
2530 agent: 1,
2531 device: 99,
2532 },
2533 Case {
2535 user: "120363012345678901",
2536 server: Server::Group,
2537 agent: 0,
2538 device: 0,
2539 },
2540 Case {
2542 user: "user",
2543 server: Server::Bot,
2544 agent: 5,
2545 device: 10,
2546 },
2547 Case {
2549 user: "447911123456",
2550 server: Server::Interop,
2551 agent: 3,
2552 device: 0,
2553 },
2554 Case {
2556 user: "messenger_user",
2557 server: Server::Messenger,
2558 agent: 0,
2559 device: 50,
2560 },
2561 Case {
2563 user: "status",
2564 server: Server::Broadcast,
2565 agent: 0,
2566 device: 0,
2567 },
2568 Case {
2570 user: "newsletter_id",
2571 server: Server::Newsletter,
2572 agent: 0,
2573 device: 0,
2574 },
2575 Case {
2577 user: "447911123456789",
2578 server: Server::Pn,
2579 agent: 255,
2580 device: 65535,
2581 },
2582 Case {
2584 user: "1",
2585 server: Server::Legacy,
2586 agent: 0,
2587 device: 1,
2588 },
2589 ];
2590
2591 for (i, c) in cases.iter().enumerate() {
2592 let jid = Jid {
2593 user: c.user.into(),
2594 server: c.server,
2595 agent: c.agent,
2596 device: c.device,
2597 integrator: 0,
2598 };
2599
2600 let display = jid.to_string();
2602
2603 let jid_ref = JidRef {
2605 user: NodeStr::Borrowed(c.user),
2606 server: c.server,
2607 agent: c.agent,
2608 device: c.device,
2609 integrator: 0,
2610 };
2611 let ref_display = jid_ref.to_string();
2612
2613 let mut string_buf = String::new();
2615 push_jid_to_string(c.user, c.server, c.agent, c.device, &mut string_buf);
2616
2617 let mut compact_buf = CompactString::default();
2619 push_jid_to_compact(c.user, c.server, c.agent, c.device, &mut compact_buf);
2620
2621 let mut push_buf = String::new();
2623 jid.push_to(&mut push_buf);
2624
2625 let mut write_buf = String::new();
2627 jid.write_display_to(&mut write_buf).unwrap();
2628
2629 assert_eq!(display, ref_display, "case {i}: Display vs JidRef::Display");
2630 assert_eq!(
2631 display, string_buf,
2632 "case {i}: Display vs push_jid_to_string"
2633 );
2634 assert_eq!(
2635 display,
2636 compact_buf.as_str(),
2637 "case {i}: Display vs push_jid_to_compact"
2638 );
2639 assert_eq!(display, push_buf, "case {i}: Display vs Jid::push_to");
2640 assert_eq!(display, write_buf, "case {i}: Display vs generic writer");
2641 }
2642 }
2643}