use crate::node::NodeStr;
use compact_str::CompactString;
use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, Copy)]
pub struct ParsedJidParts<'a> {
pub user: &'a str,
pub server: &'a str,
pub agent: u8,
pub device: u16,
pub integrator: u16,
}
#[inline]
fn parse_u16_decimal(s: &str) -> Option<u16> {
let mut bytes = s.as_bytes();
if bytes.first() == Some(&b'+') {
bytes = &bytes[1..];
}
if bytes.is_empty() {
return None;
}
let mut value = 0u16;
for &byte in bytes {
let digit = byte.wrapping_sub(b'0');
if digit > 9 {
return None;
}
value = value.checked_mul(10)?.checked_add(digit as u16)?;
}
Some(value)
}
#[inline]
pub fn parse_jid_fast(s: &str) -> Option<ParsedJidParts<'_>> {
parse_jid_scan(s).map(|(parts, _)| parts)
}
#[inline]
fn parse_jid_scan(s: &str) -> Option<(ParsedJidParts<'_>, Option<Server>)> {
let bytes = s.as_bytes();
let mut at = usize::MAX;
let mut colon_pos: Option<usize> = None;
let mut last_dot_pos: Option<usize> = None;
for (i, &b) in bytes.iter().enumerate() {
match b {
b'@' => {
at = i;
break;
}
b':' => colon_pos = Some(i),
b'.' if colon_pos.is_none() => last_dot_pos = Some(i),
_ => {}
}
}
if at == usize::MAX || at == 0 {
return None;
}
let user_part = &s[..at];
let server_str = &s[at + 1..];
let server = Server::parse_known(server_str);
match server {
Some(Server::Lid) => {
let (user, device) = match colon_pos {
Some(pos) => (&s[..pos], parse_u16_decimal(&s[pos + 1..at]).unwrap_or(0)),
None => (user_part, 0),
};
Some((
ParsedJidParts {
user,
server: server_str,
agent: 0,
device,
integrator: 0,
},
server,
))
}
Some(Server::Pn) => {
if let Some(pos) = colon_pos {
return Some((
ParsedJidParts {
user: &s[..pos],
server: server_str,
agent: 0,
device: parse_u16_decimal(&s[pos + 1..at]).unwrap_or(0),
integrator: 0,
},
server,
));
}
if let Some(dot_pos) = last_dot_pos
&& let Some(device_val) = parse_u16_decimal(&s[dot_pos + 1..at])
{
return Some((
ParsedJidParts {
user: &s[..dot_pos],
server: server_str,
agent: 0,
device: device_val,
integrator: 0,
},
server,
));
}
Some((
ParsedJidParts {
user: user_part,
server: server_str,
agent: 0,
device: 0,
integrator: 0,
},
server,
))
}
_ => {
let (user_before_colon, device) = match colon_pos {
Some(pos) => (&s[..pos], parse_u16_decimal(&s[pos + 1..at]).unwrap_or(0)),
None => (user_part, 0),
};
let (final_user, agent) = match user_before_colon.rfind('.') {
Some(dot_pos) => match parse_u16_decimal(&user_before_colon[dot_pos + 1..]) {
Some(agent_val) if agent_val <= u8::MAX as u16 => {
(&user_before_colon[..dot_pos], agent_val as u8)
}
_ => (user_before_colon, 0),
},
None => (user_before_colon, 0),
};
Some((
ParsedJidParts {
user: final_user,
server: server_str,
agent,
device,
integrator: 0,
},
server,
))
}
}
}
#[inline]
pub fn parse_jid_ref(s: &str) -> Option<JidRef<'_>> {
let (parts, server) = parse_jid_scan(s)?;
Some(JidRef {
user: NodeStr::Borrowed(parts.user),
server: server?,
agent: parts.agent,
device: parts.device,
integrator: parts.integrator,
})
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(u8)]
pub enum Server {
#[default]
Pn = 0,
Lid = 1,
Group = 2,
Broadcast = 3,
Newsletter = 4,
Hosted = 5,
HostedLid = 6,
Messenger = 7,
Interop = 8,
Bot = 9,
Legacy = 10,
Call = 11,
}
#[cfg(feature = "serde")]
impl serde::Serialize for Server {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for Server {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct ServerVisitor;
impl serde::de::Visitor<'_> for ServerVisitor {
type Value = Server;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a known WhatsApp server identifier")
}
fn visit_borrowed_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Server::try_from(value).map_err(E::custom)
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Server::try_from(value).map_err(E::custom)
}
fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Server::try_from(value.as_str()).map_err(E::custom)
}
}
deserializer.deserialize_str(ServerVisitor)
}
}
impl Server {
#[inline]
pub fn as_str(self) -> &'static str {
match self {
Self::Pn => "s.whatsapp.net",
Self::Lid => "lid",
Self::Group => "g.us",
Self::Broadcast => "broadcast",
Self::Newsletter => "newsletter",
Self::Hosted => "hosted",
Self::HostedLid => "hosted.lid",
Self::Messenger => "msgr",
Self::Interop => "interop",
Self::Bot => "bot",
Self::Legacy => "c.us",
Self::Call => "call",
}
}
#[inline]
pub fn is_pn_family(self) -> bool {
matches!(self, Self::Pn | Self::Hosted)
}
#[inline]
pub fn is_lid_family(self) -> bool {
matches!(self, Self::Lid | Self::HostedLid)
}
#[inline]
pub fn renders_agent(self) -> bool {
!matches!(self, Self::Pn | Self::Lid | Self::Hosted | Self::HostedLid)
}
#[inline]
pub fn carries_phone_number(self) -> bool {
matches!(
self,
Self::Pn | Self::Hosted | Self::Legacy | Self::Messenger | Self::Interop
)
}
}
impl fmt::Display for Server {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl PartialEq<str> for Server {
fn eq(&self, other: &str) -> bool {
self.as_str() == other
}
}
impl PartialEq<&str> for Server {
fn eq(&self, other: &&str) -> bool {
self.as_str() == *other
}
}
impl Server {
#[inline]
pub fn parse_known(s: &str) -> Option<Self> {
Some(match s {
"s.whatsapp.net" => Self::Pn,
"lid" => Self::Lid,
"g.us" => Self::Group,
"broadcast" => Self::Broadcast,
"newsletter" => Self::Newsletter,
"hosted" => Self::Hosted,
"hosted.lid" => Self::HostedLid,
"msgr" => Self::Messenger,
"interop" => Self::Interop,
"bot" => Self::Bot,
"c.us" => Self::Legacy,
"call" => Self::Call,
_ => return None,
})
}
}
impl TryFrom<&str> for Server {
type Error = JidError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
Server::parse_known(s)
.ok_or_else(|| JidError::InvalidFormat(format!("unknown server: {s}")))
}
}
pub const DEFAULT_USER_SERVER: &str = "s.whatsapp.net";
pub const SERVER_JID: &str = "s.whatsapp.net";
pub const GROUP_SERVER: &str = "g.us";
pub const LEGACY_USER_SERVER: &str = "c.us";
pub const BROADCAST_SERVER: &str = "broadcast";
pub const HIDDEN_USER_SERVER: &str = "lid";
pub const NEWSLETTER_SERVER: &str = "newsletter";
pub const HOSTED_SERVER: &str = "hosted";
pub const HOSTED_LID_SERVER: &str = "hosted.lid";
pub const MESSENGER_SERVER: &str = "msgr";
pub const INTEROP_SERVER: &str = "interop";
pub const BOT_SERVER: &str = "bot";
pub const STATUS_BROADCAST_USER: &str = "status";
pub const PSA_USER: &str = "0";
pub type MessageId = String;
pub type MessageServerId = i32;
#[derive(Debug)]
pub enum JidError {
InvalidFormat(String),
Parse(std::num::ParseIntError),
}
impl fmt::Display for JidError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
JidError::InvalidFormat(s) => write!(f, "Invalid JID format: {s}"),
JidError::Parse(e) => write!(f, "Failed to parse component: {e}"),
}
}
}
impl std::error::Error for JidError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
JidError::Parse(e) => Some(e),
_ => None,
}
}
}
impl From<std::num::ParseIntError> for JidError {
fn from(err: std::num::ParseIntError) -> Self {
JidError::Parse(err)
}
}
pub trait JidExt {
fn user(&self) -> &str;
fn server(&self) -> Server;
fn device(&self) -> u16;
fn integrator(&self) -> u16;
fn is_ad(&self) -> bool {
self.device() > 0
&& matches!(
self.server(),
Server::Pn | Server::Lid | Server::Hosted | Server::HostedLid
)
}
fn is_interop(&self) -> bool {
self.server() == Server::Interop && self.integrator() > 0
}
fn is_messenger(&self) -> bool {
self.server() == Server::Messenger && self.device() > 0
}
fn is_group(&self) -> bool {
self.server() == Server::Group
}
fn is_broadcast_list(&self) -> bool {
self.server() == Server::Broadcast && self.user() != STATUS_BROADCAST_USER
}
fn is_status_broadcast(&self) -> bool {
self.server() == Server::Broadcast && self.user() == STATUS_BROADCAST_USER
}
fn is_psa(&self) -> bool {
matches!(self.server(), Server::Pn | Server::Legacy) && self.user() == PSA_USER
}
fn is_bot(&self) -> bool {
(self.server() == Server::Pn
&& self.device() == 0
&& (self.user().starts_with("1313555") || self.user().starts_with("131655500")))
|| self.server() == Server::Bot
}
fn is_newsletter(&self) -> bool {
self.server() == Server::Newsletter
}
fn is_hosted(&self) -> bool {
self.device() == 99 || matches!(self.server(), Server::Hosted | Server::HostedLid)
}
fn is_empty(&self) -> bool {
self.user().is_empty()
}
fn is_same_user_as(&self, other: &impl JidExt) -> bool {
self.user() == other.user()
}
}
#[inline]
fn identity_agent(server: Server, agent: u8) -> u8 {
if server.renders_agent() { agent } else { 0 }
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Default)]
pub struct Jid {
pub user: CompactString,
pub server: Server,
pub agent: u8,
pub device: u16,
pub integrator: u16,
}
#[derive(Debug, Clone, yoke::Yokeable)]
pub struct JidRef<'a> {
pub user: NodeStr<'a>,
pub server: Server,
pub agent: u8,
pub device: u16,
pub integrator: u16,
}
impl JidExt for Jid {
fn user(&self) -> &str {
&self.user
}
fn server(&self) -> Server {
self.server
}
fn device(&self) -> u16 {
self.device
}
fn integrator(&self) -> u16 {
self.integrator
}
}
impl Jid {
pub fn new(user: impl Into<CompactString>, server: Server) -> Self {
Self {
user: user.into(),
server,
..Default::default()
}
}
pub fn pn(user: impl Into<CompactString>) -> Self {
Self {
user: user.into(),
server: Server::Pn,
..Default::default()
}
}
pub fn lid(user: impl Into<CompactString>) -> Self {
Self {
user: user.into(),
server: Server::Lid,
..Default::default()
}
}
pub fn status_broadcast() -> Self {
Self {
user: CompactString::from(STATUS_BROADCAST_USER),
server: Server::Broadcast,
agent: 0,
device: 0,
integrator: 0,
}
}
pub fn group(id: impl Into<CompactString>) -> Self {
Self {
user: id.into(),
server: Server::Group,
..Default::default()
}
}
pub fn newsletter(id: impl Into<CompactString>) -> Self {
Self {
user: id.into(),
server: Server::Newsletter,
..Default::default()
}
}
pub fn pn_device(user: impl Into<CompactString>, device: u16) -> Self {
Self {
user: user.into(),
server: Server::Pn,
device,
..Default::default()
}
}
pub fn lid_device(user: impl Into<CompactString>, device: u16) -> Self {
Self {
user: user.into(),
server: Server::Lid,
device,
..Default::default()
}
}
#[inline]
pub fn is_pn(&self) -> bool {
self.server == Server::Pn
}
#[inline]
pub fn is_lid(&self) -> bool {
self.server == Server::Lid
}
#[inline]
pub fn user_base(&self) -> &str {
if let Some((base, _)) = self.user.split_once(':') {
base
} else {
&self.user
}
}
pub fn with_device(&self, device_id: u16) -> Self {
Self {
user: self.user.clone(),
server: self.server,
agent: self.agent,
device: device_id,
integrator: self.integrator,
}
}
pub fn with_device_hosting(&self, device_id: u16, is_hosted: bool) -> Self {
let mut jid = self.with_device(device_id);
jid.server = match (jid.server, is_hosted) {
(Server::Pn | Server::Hosted, true) => Server::Hosted,
(Server::Pn | Server::Hosted, false) => Server::Pn,
(Server::Lid | Server::HostedLid, true) => Server::HostedLid,
(Server::Lid | Server::HostedLid, false) => Server::Lid,
(server, _) => server,
};
jid
}
pub fn to_non_ad(&self) -> Self {
Self {
user: self.user.clone(),
server: self.server,
integrator: self.integrator,
..Default::default()
}
}
pub fn into_non_ad(self) -> Self {
Self {
user: self.user,
server: self.server,
integrator: self.integrator,
agent: 0,
device: 0,
}
}
#[inline]
pub fn is_same_chat_as(&self, other: &Jid) -> bool {
self.user == other.user
&& self.server == other.server
&& self.integrator == other.integrator
&& (!self.server.renders_agent() || self.agent == other.agent)
}
pub fn to_non_ad_string(&self) -> String {
let mut buf = String::with_capacity(self.user.len() + 1 + self.server.as_str().len());
push_jid_to_string(&self.user, self.server, 0, 0, &mut buf);
buf
}
pub fn to_non_ad_arc_str(&self) -> std::sync::Arc<str> {
let mut writer = JidStackWriter::new();
if write_jid_fallible(&mut writer, &self.user, self.server, 0, 0).is_ok() {
return std::sync::Arc::from(writer.as_str());
}
std::sync::Arc::from(self.to_non_ad_string())
}
#[inline]
pub fn matches_user_or_lid(&self, user: &Jid, lid: Option<&Jid>) -> bool {
self.is_same_user_as(user) || lid.is_some_and(|l| self.is_same_user_as(l))
}
pub fn to_phash_form_string(&self) -> String {
let mut s = String::with_capacity(self.user.len() + 20);
self.push_phash_form_to(&mut s);
s
}
#[inline]
pub fn push_phash_form_to(&self, buf: &mut String) {
if self.user.is_empty() {
buf.push_str(self.server.as_str());
return;
}
buf.push_str(&self.user);
buf.push_str(".0:");
buf.push_str(itoa::Buffer::new().format(self.device));
buf.push('@');
buf.push_str(self.server.as_str());
}
#[inline]
pub fn push_to(&self, buf: &mut String) {
push_jid_to_string(&self.user, self.server, self.agent, self.device, buf);
}
#[inline]
pub fn write_display_to<W: fmt::Write + ?Sized>(&self, writer: &mut W) -> fmt::Result {
write_jid_fallible(writer, &self.user, self.server, self.agent, self.device)
}
#[inline]
pub fn display_eq(&self, other: &str) -> bool {
jid_display_eq(&self.user, self.server, self.agent, self.device, other)
}
#[inline]
pub fn display_eq_jid(&self, other: &Self) -> bool {
jid_displays_equal(
(&self.user, self.server, self.agent, self.device),
(&other.user, other.server, other.agent, other.device),
)
}
#[inline]
pub fn device_eq(&self, other: &Jid) -> bool {
self.user == other.user && self.server == other.server && self.device == other.device
}
#[inline]
pub fn identity_agent(&self) -> u8 {
identity_agent(self.server, self.agent)
}
#[inline]
pub fn device_key(&self) -> DeviceKey<'_> {
DeviceKey {
user: &self.user,
server: self.server,
device: self.device,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct DeviceKey<'a> {
pub user: &'a str,
pub server: Server,
pub device: u16,
}
impl<'a> JidExt for JidRef<'a> {
fn user(&self) -> &str {
&self.user
}
fn server(&self) -> Server {
self.server
}
fn device(&self) -> u16 {
self.device
}
fn integrator(&self) -> u16 {
self.integrator
}
}
impl<'a> JidRef<'a> {
pub fn to_owned(&self) -> Jid {
Jid {
user: self.user.to_compact_string(),
server: self.server,
agent: self.agent,
device: self.device,
integrator: self.integrator,
}
}
#[inline]
pub fn display_eq(&self, other: &str) -> bool {
jid_display_eq(&self.user, self.server, self.agent, self.device, other)
}
#[inline]
pub fn display_eq_jid(&self, other: &Self) -> bool {
jid_displays_equal(
(&self.user, self.server, self.agent, self.device),
(&other.user, other.server, other.agent, other.device),
)
}
}
impl PartialEq for Jid {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.user == other.user
&& self.server == other.server
&& self.device == other.device
&& self.integrator == other.integrator
&& (self.agent == other.agent
|| identity_agent(self.server, self.agent)
== identity_agent(other.server, other.agent))
}
}
impl Eq for Jid {}
impl std::hash::Hash for Jid {
#[inline]
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.user.hash(state);
self.server.hash(state);
self.device.hash(state);
self.integrator.hash(state);
identity_agent(self.server, self.agent).hash(state);
}
}
impl PartialEq for JidRef<'_> {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.user.as_ref() == other.user.as_ref()
&& self.server == other.server
&& self.device == other.device
&& self.integrator == other.integrator
&& (self.agent == other.agent
|| identity_agent(self.server, self.agent)
== identity_agent(other.server, other.agent))
}
}
impl Eq for JidRef<'_> {}
impl std::hash::Hash for JidRef<'_> {
#[inline]
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.user.as_ref().hash(state);
self.server.hash(state);
self.device.hash(state);
self.integrator.hash(state);
identity_agent(self.server, self.agent).hash(state);
}
}
impl PartialEq<JidRef<'_>> for Jid {
#[inline]
fn eq(&self, other: &JidRef<'_>) -> bool {
self.user.as_str() == other.user.as_ref()
&& self.server == other.server
&& self.device == other.device
&& self.integrator == other.integrator
&& (self.agent == other.agent
|| identity_agent(self.server, self.agent)
== identity_agent(other.server, other.agent))
}
}
impl PartialEq<Jid> for JidRef<'_> {
#[inline]
fn eq(&self, other: &Jid) -> bool {
other == self
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for JidRef<'_> {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeStruct;
let mut s = serializer.serialize_struct("Jid", 5)?;
s.serialize_field("user", &*self.user)?;
s.serialize_field("server", &self.server)?;
s.serialize_field("agent", &self.agent)?;
s.serialize_field("device", &self.device)?;
s.serialize_field("integrator", &self.integrator)?;
s.end()
}
}
impl FromStr for Jid {
type Err = JidError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Some(jid) = parse_jid_ref(s) {
return Ok(jid.to_owned());
}
let (user_part, server) = match s.split_once('@') {
Some((u, s)) => (u, s),
None => ("", s),
};
if user_part.is_empty() && Server::try_from(server).is_err() {
return Err(JidError::InvalidFormat(format!(
"unknown server '{server}'"
)));
}
if server == HIDDEN_USER_SERVER {
let (user, device) = if let Some((u, d_str)) = user_part.rsplit_once(':') {
(u, d_str.parse()?)
} else {
(user_part, 0)
};
return Ok(Jid {
user: CompactString::from(user),
server: Server::try_from(server)?,
device,
agent: 0,
integrator: 0,
});
}
let mut user = user_part;
let mut device = 0;
let mut agent = 0;
if let Some((u, d_str)) = user_part.rsplit_once(':') {
user = u;
device = d_str.parse()?;
}
if server != DEFAULT_USER_SERVER
&& server != HIDDEN_USER_SERVER
&& let Some((u, last_part)) = user.rsplit_once('.')
&& let Ok(num_val) = last_part.parse::<u16>()
{
if num_val > u8::MAX as u16 {
return Err(JidError::InvalidFormat(format!(
"Agent component out of range: {num_val}"
)));
}
user = u;
agent = num_val as u8;
}
Ok(Jid {
user: CompactString::from(user),
server: Server::try_from(server)?,
agent,
device,
integrator: 0,
})
}
}
macro_rules! write_jid {
(infallible $buf:expr, $user:expr, $server:expr, $agent:expr, $device:expr) => {{
let (user, server, agent, device) = ($user, $server, $agent, $device);
if user.is_empty() {
$buf.push_str(server.as_str());
return;
}
$buf.push_str(user);
if agent > 0 && server.renders_agent() {
$buf.push('.');
$buf.push_str(itoa::Buffer::new().format(agent));
}
if device > 0 {
$buf.push(':');
$buf.push_str(itoa::Buffer::new().format(device));
}
$buf.push('@');
$buf.push_str(server.as_str());
}};
(fallible $f:expr, $user:expr, $server:expr, $agent:expr, $device:expr) => {{
let (user, server, agent, device) = ($user, $server, $agent, $device);
if user.is_empty() {
return $f.write_str(server.as_str());
}
$f.write_str(user)?;
if agent > 0 && server.renders_agent() {
$f.write_str(".")?;
$f.write_str(itoa::Buffer::new().format(agent))?;
}
if device > 0 {
$f.write_str(":")?;
$f.write_str(itoa::Buffer::new().format(device))?;
}
$f.write_str("@")?;
$f.write_str(server.as_str())
}};
}
#[inline]
pub fn push_jid_to_string(user: &str, server: Server, agent: u8, device: u16, buf: &mut String) {
write_jid!(infallible buf, user, server, agent, device);
}
#[inline]
pub fn push_jid_to_compact(
user: &str,
server: Server,
agent: u8,
device: u16,
buf: &mut CompactString,
) {
write_jid!(infallible buf, user, server, agent, device);
}
struct JidStackWriter {
buf: [u8; 64],
len: usize,
}
impl JidStackWriter {
#[inline]
fn new() -> Self {
Self {
buf: [0; 64],
len: 0,
}
}
#[inline]
fn as_str(&self) -> &str {
unsafe { std::str::from_utf8_unchecked(&self.buf[..self.len]) }
}
}
impl fmt::Write for JidStackWriter {
#[inline]
fn write_str(&mut self, s: &str) -> fmt::Result {
let end = self.len + s.len();
if end > self.buf.len() {
return Err(fmt::Error);
}
self.buf[self.len..end].copy_from_slice(s.as_bytes());
self.len = end;
Ok(())
}
}
#[inline]
fn write_jid_fallible<W: fmt::Write + ?Sized>(
w: &mut W,
user: &str,
server: Server,
agent: u8,
device: u16,
) -> fmt::Result {
write_jid!(fallible w, user, server, agent, device)
}
struct StrEqWriter<'a> {
target: &'a [u8],
position: usize,
matches: bool,
}
impl fmt::Write for StrEqWriter<'_> {
#[inline]
fn write_str(&mut self, value: &str) -> fmt::Result {
if self.matches {
let bytes = value.as_bytes();
let end = self.position + bytes.len();
if end > self.target.len() || self.target[self.position..end] != *bytes {
self.matches = false;
}
self.position = end;
}
Ok(())
}
}
#[inline]
fn jid_display_eq(user: &str, server: Server, agent: u8, device: u16, other: &str) -> bool {
let mut writer = StrEqWriter {
target: other.as_bytes(),
position: 0,
matches: true,
};
let written = write_jid_fallible(&mut writer, user, server, agent, device).is_ok();
written && writer.matches && writer.position == other.len()
}
#[inline]
fn jid_displays_equal(left: (&str, Server, u8, u16), right: (&str, Server, u8, u16)) -> bool {
let (left_user, left_server, left_agent, left_device) = left;
let (right_user, right_server, right_agent, right_device) = right;
if left_user.is_empty() || right_user.is_empty() {
return left_user.is_empty() && right_user.is_empty() && left_server == right_server;
}
left_user == right_user
&& left_server == right_server
&& left_device == right_device
&& (!left_server.renders_agent() || left_agent == right_agent)
}
impl fmt::Display for Jid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut w = JidStackWriter::new();
if self.write_display_to(&mut w).is_ok() {
return f.write_str(w.as_str());
}
self.write_display_to(f)
}
}
pub struct ObservedJid<'a>(&'a Jid);
impl Jid {
#[inline]
pub fn observe(&self) -> ObservedJid<'_> {
ObservedJid(self)
}
}
pub fn observe_token(s: &str) -> u64 {
use std::hash::BuildHasher;
static KEY: std::sync::OnceLock<std::collections::hash_map::RandomState> =
std::sync::OnceLock::new();
KEY.get_or_init(std::collections::hash_map::RandomState::new)
.hash_one(s)
}
pub fn observe_str(s: &str) -> String {
if cfg!(feature = "tracing-pii") {
return s.to_string();
}
match s.parse::<Jid>() {
Ok(jid) => jid.observe().to_string(),
Err(_) => format!("?#{:016x}", observe_token(s)),
}
}
impl fmt::Display for ObservedJid<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let jid = self.0;
if jid.user.is_empty() || cfg!(feature = "tracing-pii") {
return fmt::Display::fmt(jid, f);
}
let redacted: Option<String> = if jid.server.carries_phone_number() {
Some(format!("pn#{:016x}", observe_token(jid.user.as_str())))
} else if matches!(jid.server, Server::Group | Server::Broadcast) {
match jid.user.find('-') {
Some(i) if i > 0 && jid.user.as_bytes()[..i].iter().all(|b| b.is_ascii_digit()) => {
Some(format!(
"pn#{:016x}{}",
observe_token(&jid.user[..i]),
&jid.user[i..]
))
}
_ => None,
}
} else {
None
};
let Some(user) = redacted else {
return fmt::Display::fmt(jid, f);
};
f.write_str(&user)?;
if jid.agent > 0 && jid.server.renders_agent() {
write!(f, ".{}", jid.agent)?;
}
if jid.device > 0 {
write!(f, ":{}", jid.device)?;
}
write!(f, "@{}", jid.server.as_str())
}
}
impl<'a> fmt::Display for JidRef<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut w = JidStackWriter::new();
if write_jid_fallible(&mut w, &self.user, self.server, self.agent, self.device).is_ok() {
return f.write_str(w.as_str());
}
write_jid_fallible(f, &self.user, self.server, self.agent, self.device)
}
}
impl From<Jid> for String {
fn from(jid: Jid) -> Self {
jid.to_string()
}
}
impl From<&Jid> for Jid {
fn from(jid: &Jid) -> Self {
jid.clone()
}
}
impl<'a> From<JidRef<'a>> for String {
fn from(jid: JidRef<'a>) -> Self {
jid.to_string()
}
}
impl TryFrom<String> for Jid {
type Error = JidError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Jid::from_str(&value)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
#[test]
fn is_psa_matches_system_jid_in_both_user_namespaces() {
assert!(Jid::from_str("0@s.whatsapp.net").unwrap().is_psa());
assert!(Jid::from_str("0@c.us").unwrap().is_psa());
assert!(parse_jid_ref("0@s.whatsapp.net").unwrap().is_psa());
}
#[test]
fn is_psa_rejects_regular_and_non_pn_jids() {
assert!(!Jid::from_str("10@s.whatsapp.net").unwrap().is_psa());
assert!(!Jid::from_str("0@lid").unwrap().is_psa());
assert!(!Jid::from_str("status@broadcast").unwrap().is_psa());
assert!(!Jid::from_str("0@g.us").unwrap().is_psa());
}
#[test]
fn display_reads_back_every_stack_buffer_shape() {
let emoji = Jid::new("héllo→世界", Server::Lid);
assert_eq!(emoji.to_string(), "héllo→世界@lid");
assert_eq!(format!("{}", emoji.observe()), "héllo→世界@lid");
let full = Jid::new("9".repeat(60), Server::Lid);
let rendered = full.to_string();
assert_eq!(rendered.len(), 64);
assert!(rendered.ends_with("@lid"));
let over = Jid::new("9".repeat(61), Server::Lid);
assert_eq!(over.to_string(), format!("{}@lid", "9".repeat(61)));
let borrowed = parse_jid_ref("12025550111:7@s.whatsapp.net").unwrap();
assert_eq!(borrowed.to_string(), "12025550111:7@s.whatsapp.net");
assert_eq!(&*emoji.to_non_ad_arc_str(), "héllo→世界@lid");
assert_eq!(
&*over.to_non_ad_arc_str(),
&*format!("{}@lid", "9".repeat(61))
);
}
#[test]
fn phash_form_writes_the_agent_position_as_zero_like_wa_web() {
let plain = Jid {
user: "5511999998888".into(),
server: Server::Lid,
agent: 0,
device: 3,
integrator: 0,
};
assert_eq!(plain.to_phash_form_string(), "5511999998888.0:3@lid");
let with_agent = Jid {
agent: 7,
..plain.clone()
};
assert_eq!(
with_agent.to_phash_form_string(),
"5511999998888.0:3@lid",
"the agent must not reach the hashed string"
);
assert_eq!(plain, with_agent);
assert_eq!(
plain.to_phash_form_string(),
with_agent.to_phash_form_string()
);
assert_eq!(
Jid::new("", Server::Pn).to_phash_form_string(),
"s.whatsapp.net"
);
assert_ne!(
plain.to_phash_form_string(),
Jid {
device: 4,
..plain.clone()
}
.to_phash_form_string()
);
}
#[test]
fn inert_agent_stays_out_of_identity() {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
fn hash_of(jid: &Jid) -> u64 {
let mut h = DefaultHasher::new();
jid.hash(&mut h);
h.finish()
}
for server in [Server::Pn, Server::Lid, Server::Hosted, Server::HostedLid] {
let clean = Jid {
user: "123456789012345".into(),
server,
agent: 0,
device: 7,
integrator: 0,
};
let with_agent = Jid {
agent: 1,
..clean.clone()
};
assert_eq!(
clean, with_agent,
"{server:?}: agent must not split identity"
);
assert_eq!(
hash_of(&clean),
hash_of(&with_agent),
"{server:?}: Hash must agree with Eq"
);
let with_integrator = Jid {
integrator: 0xBEEF,
..clean.clone()
};
assert_ne!(
clean, with_integrator,
"{server:?}: integrator stays in identity, matching is_same_chat_as"
);
assert_eq!(
clean.is_same_chat_as(&with_integrator),
clean == with_integrator,
"{server:?}: == must agree with is_same_chat_as"
);
let borrowed = JidRef {
user: NodeStr::Borrowed("123456789012345"),
server,
agent: 1,
device: 7,
integrator: 0,
};
assert_eq!(clean, borrowed, "{server:?}: owned == borrowed");
assert_eq!(borrowed, clean, "{server:?}: borrowed == owned");
}
let bot = Jid {
user: "123456789".into(),
server: Server::Interop,
agent: 4,
device: 0,
integrator: 0,
};
let other_agent = Jid {
agent: 5,
..bot.clone()
};
assert_ne!(
bot, other_agent,
"interop renders the agent, so it is identity"
);
assert_ne!(
bot,
Jid {
integrator: 9,
..bot.clone()
},
"interop is where integrator is real"
);
let pn = Jid::new("123456789012345", Server::Pn);
assert_ne!(
pn,
Jid {
device: 1,
..pn.clone()
}
);
assert_ne!(pn, Jid::new("123456789012346", Server::Pn));
assert_ne!(pn, Jid::new("123456789012345", Server::Lid));
}
#[test]
fn display_eq_matches_owned_and_borrowed_jids_without_normalizing() {
let canonical = "123456789.4:17@interop";
let owned = Jid::from_str(canonical).unwrap();
let borrowed = parse_jid_ref(canonical).unwrap();
for value in [canonical, "123456789.4:16@interop", "123456789@interop", ""] {
assert_eq!(owned.display_eq(value), value == canonical);
assert_eq!(borrowed.display_eq(value), value == canonical);
}
let long_user = "a".repeat(128);
let long_value = format!("{long_user}@lid");
let long_jid = Jid::lid(long_user);
assert!(long_jid.display_eq(&long_value));
assert!(!long_jid.display_eq(&format!("{long_value}x")));
}
#[test]
fn display_eq_jid_uses_only_rendered_components() {
let pn = Jid {
user: "12025550111".into(),
server: Server::Pn,
agent: 1,
device: 7,
integrator: 3,
};
let pn_same_display = Jid {
agent: 2,
integrator: 9,
..pn.clone()
};
assert_eq!(pn.to_string(), pn_same_display.to_string());
assert!(pn.display_eq_jid(&pn_same_display));
let pn_other_device = Jid {
device: 8,
..pn.clone()
};
assert!(!pn.display_eq_jid(&pn_other_device));
let bot = Jid {
user: "13136555001".into(),
server: Server::Bot,
agent: 1,
device: 0,
integrator: 0,
};
let other_bot_agent = Jid {
agent: 2,
..bot.clone()
};
assert!(!bot.display_eq_jid(&other_bot_agent));
let server_only = Jid {
user: "".into(),
server: Server::Pn,
agent: 1,
device: 7,
integrator: 3,
};
let same_server_only = Jid {
agent: 2,
device: 9,
integrator: 4,
..server_only.clone()
};
assert_eq!(server_only.to_string(), same_server_only.to_string());
assert!(server_only.display_eq_jid(&same_server_only));
let borrowed = JidRef {
user: NodeStr::Borrowed("12025550111"),
server: Server::Pn,
agent: 1,
device: 7,
integrator: 0,
};
let borrowed_same_display = JidRef {
agent: 2,
..borrowed.clone()
};
assert!(borrowed.display_eq_jid(&borrowed_same_display));
}
#[test]
fn owned_and_borrowed_jids_compare_without_conversion() {
let owned = Jid {
user: "12025550111".into(),
server: Server::Pn,
agent: 0,
device: 7,
integrator: 0,
};
let borrowed = JidRef {
user: NodeStr::Borrowed("12025550111"),
server: Server::Pn,
agent: 0,
device: 7,
integrator: 0,
};
assert_eq!(owned, borrowed);
assert_eq!(borrowed, owned);
let other_device = JidRef {
device: 8,
..borrowed.clone()
};
assert_ne!(owned, other_device);
}
#[cfg(feature = "serde")]
#[test]
fn server_deserializes_borrowed_and_owned_strings() {
let borrowed: Server = serde_json::from_str("\"s.whatsapp.net\"").unwrap();
let owned: Server =
serde_json::from_value(serde_json::Value::String("lid".to_owned())).unwrap();
assert_eq!(borrowed, Server::Pn);
assert_eq!(owned, Server::Lid);
}
#[test]
#[cfg(not(feature = "tracing-pii"))]
fn observe_redacts_phone_but_not_lid_or_group() {
let pn = Jid::from_str("5511999998888:7@s.whatsapp.net").unwrap();
let shown = pn.observe().to_string();
assert!(shown.starts_with("pn#"), "{shown}");
assert!(
!shown.contains("5511999998888"),
"raw number leaked: {shown}"
);
assert!(shown.ends_with(":7@s.whatsapp.net"), "device lost: {shown}");
assert_eq!(shown, pn.observe().to_string());
let lid = Jid::from_str("123456789@lid").unwrap();
assert_eq!(lid.observe().to_string(), lid.to_string());
let group = Jid::from_str("120363012345678901@g.us").unwrap();
assert_eq!(group.observe().to_string(), group.to_string());
let legacy = Jid::from_str("123456789-1620000000@g.us").unwrap();
let ls = legacy.observe().to_string();
assert!(
ls.starts_with("pn#") && ls.ends_with("-1620000000@g.us"),
"{ls}"
);
assert!(!ls.contains("123456789"), "creator phone leaked: {ls}");
let mid = &ls["pn#".len()..ls.find('-').unwrap()];
assert_eq!(mid.len(), 16, "token width: {ls}");
assert!(
mid.bytes().all(|b| b.is_ascii_hexdigit()),
"token hex: {ls}"
);
}
fn assert_jid_roundtrip(
input: &str,
expected_user: &str,
expected_server: &str,
expected_device: u16,
expected_agent: u8,
) {
assert_jid_parse_and_display(
input,
expected_user,
expected_server,
expected_device,
expected_agent,
input,
);
}
fn assert_jid_parse_and_display(
input: &str,
expected_user: &str,
expected_server: &str,
expected_device: u16,
expected_agent: u8,
expected_output: &str,
) {
let jid = Jid::from_str(input).unwrap_or_else(|_| panic!("Failed to parse JID: {}", input));
assert_eq!(
jid.user, expected_user,
"User part did not match for {}",
input
);
assert_eq!(
jid.server, expected_server,
"Server part did not match for {}",
input
);
assert_eq!(
jid.device, expected_device,
"Device part did not match for {}",
input
);
assert_eq!(
jid.agent, expected_agent,
"Agent part did not match for {}",
input
);
let formatted = jid.to_string();
assert_eq!(
formatted, expected_output,
"Formatted string did not match expected output for {}",
input
);
}
#[test]
fn borrowed_parser_matches_owned_fast_path_and_bot_classification() {
for raw in [
"5511999998888:7@s.whatsapp.net",
"120363012345678901@g.us",
"123456789012345@lid",
"assistant@bot",
"13135551234@s.whatsapp.net",
] {
let borrowed = parse_jid_ref(raw).expect("common JID should stay borrowed");
let owned = raw.parse::<Jid>().unwrap();
assert!(matches!(borrowed.user, NodeStr::Borrowed(_)));
assert_eq!(borrowed.to_owned(), owned);
assert_eq!(borrowed.is_bot(), owned.is_bot());
}
assert!(parse_jid_ref("g.us").is_none(), "server-only uses fallback");
assert!(parse_jid_ref("user@unknown").is_none());
}
#[test]
fn decimal_fast_path_matches_u16_from_str() {
for raw in [
"", "+", "-", "0", "7", "+7", "007", "33", "255", "256", "65535", "65536", "99999",
"-0", "-1", "1x", "x1", " 1", "1 ", "1_0", "+-1", "++1", "1.0", "٣", "𝟛",
] {
assert_eq!(
parse_u16_decimal(raw),
raw.parse::<u16>().ok(),
"mismatch for {raw:?}"
);
}
}
#[test]
fn fast_parse_pins_the_separator_rules_per_server() {
let cases = [
("123456789.4:17@interop", "123456789", 4u8, 17u16),
("123456789.4@interop", "123456789", 4, 0),
("123.999@interop", "123.999", 0, 0),
("5511999998888.2@s.whatsapp.net", "5511999998888", 0, 2),
("12345.678@lid", "12345.678", 0, 0),
("12345.678:9@lid", "12345.678", 0, 9),
("12345.6@hosted.lid", "12345", 6, 0),
("12345.678@hosted.lid", "12345.678", 0, 0),
("12345.6@c.us", "12345", 6, 0),
("a:1.5:2@bot", "a:1", 5, 2),
("5511999998888:x@s.whatsapp.net", "5511999998888", 0, 0),
];
for (raw, user, agent, device) in cases {
let fast =
parse_jid_fast(raw).unwrap_or_else(|| panic!("{raw} should take the fast path"));
assert_eq!(fast.user, user, "user for {raw}");
assert_eq!(fast.agent, agent, "agent for {raw}");
assert_eq!(fast.device, device, "device for {raw}");
}
}
#[test]
fn test_jid_parsing_and_display_roundtrip() {
assert_jid_roundtrip(
"1234567890@s.whatsapp.net",
"1234567890",
"s.whatsapp.net",
0,
0,
);
assert_jid_roundtrip(
"1234567890:15@s.whatsapp.net",
"1234567890",
"s.whatsapp.net",
15,
0,
);
assert_jid_roundtrip("123-456@g.us", "123-456", "g.us", 0, 0);
assert_jid_roundtrip("s.whatsapp.net", "", "s.whatsapp.net", 0, 0);
assert_jid_roundtrip("12345.6789@lid", "12345.6789", "lid", 0, 0);
assert_jid_roundtrip("12345.6789:25@lid", "12345.6789", "lid", 25, 0);
assert_jid_roundtrip("12345@call", "12345", "call", 0, 0);
}
#[test]
fn test_special_from_str_parsing() {
let jid = Jid::from_str("1234567890.2:15@hosted").expect("test hosted JID should be valid");
assert_eq!(jid.user, "1234567890");
assert_eq!(jid.server, "hosted");
assert_eq!(jid.device, 15);
assert_eq!(jid.agent, 2);
}
#[test]
fn test_manual_jid_formatting_edge_cases() {
let jid1 = Jid {
user: "1234567890".into(),
server: Server::Pn,
device: 15,
agent: 2,
integrator: 0,
};
assert_eq!(jid1.to_string(), "1234567890:15@s.whatsapp.net");
let jid2 = Jid {
user: "12345.6789".into(),
server: Server::Lid,
device: 25,
agent: 1,
integrator: 0,
};
assert_eq!(jid2.to_string(), "12345.6789:25@lid");
let jid3 = Jid {
user: "1234567890".into(),
server: Server::Hosted,
device: 15,
agent: 2,
integrator: 0,
};
assert_eq!(jid3.to_string(), "1234567890:15@hosted");
let jid4 = Jid {
user: "user".into(),
server: Server::Bot,
device: 10,
agent: 5,
integrator: 0,
};
assert_eq!(jid4.to_string(), "user.5:10@bot");
}
#[test]
fn test_invalid_jids_should_fail_to_parse() {
assert!(Jid::from_str("thisisnotajid").is_err());
assert!(Jid::from_str("").is_err());
assert!(Jid::from_str("@s.whatsapp.net").is_ok());
assert!(Jid::from_str("@unknown.server").is_err());
assert!(Jid::from_str("2").is_err());
}
#[test]
fn test_is_hosted_device_detection() {
let cloud_api_device: Jid = "5511999887766:99@s.whatsapp.net"
.parse()
.expect("test JID should be valid");
assert!(
cloud_api_device.is_hosted(),
"Device ID 99 on s.whatsapp.net should be detected as hosted (Cloud API)"
);
let cloud_api_lid: Jid = "100000012345678:99@lid"
.parse()
.expect("test JID should be valid");
assert!(
cloud_api_lid.is_hosted(),
"Device ID 99 on lid server should be detected as hosted"
);
let hosted_server: Jid = "5511999887766:99@hosted"
.parse()
.expect("test JID should be valid");
assert!(
hosted_server.is_hosted(),
"JID with @hosted server should be detected as hosted"
);
let hosted_lid_server: Jid = "100000012345678:99@hosted.lid"
.parse()
.expect("test JID should be valid");
assert!(
hosted_lid_server.is_hosted(),
"JID with @hosted.lid server should be detected as hosted"
);
let hosted_server_other_device: Jid = "5511999887766:0@hosted"
.parse()
.expect("test JID should be valid");
assert!(
hosted_server_other_device.is_hosted(),
"JID with @hosted server should be hosted regardless of device ID"
);
let regular_phone: Jid = "5511999887766:0@s.whatsapp.net"
.parse()
.expect("test JID should be valid");
assert!(
!regular_phone.is_hosted(),
"Regular phone device (ID 0) should NOT be hosted"
);
let companion_device: Jid = "5511999887766:33@s.whatsapp.net"
.parse()
.expect("test JID should be valid");
assert!(
!companion_device.is_hosted(),
"Companion device (ID 33) should NOT be hosted"
);
let regular_lid: Jid = "100000012345678:0@lid"
.parse()
.expect("test JID should be valid");
assert!(
!regular_lid.is_hosted(),
"Regular LID device should NOT be hosted"
);
let lid_companion: Jid = "100000012345678:33@lid"
.parse()
.expect("test JID should be valid");
assert!(
!lid_companion.is_hosted(),
"LID companion device (ID 33) should NOT be hosted"
);
let group_jid: Jid = "120363012345678@g.us"
.parse()
.expect("test JID should be valid");
assert!(
!group_jid.is_hosted(),
"Group JID should NOT be detected as hosted"
);
let user_jid: Jid = "5511999887766@s.whatsapp.net"
.parse()
.expect("test JID should be valid");
assert!(
!user_jid.is_hosted(),
"User JID without device should NOT be hosted"
);
let bot_jid: Jid = "13136555001:0@s.whatsapp.net"
.parse()
.expect("test JID should be valid");
assert!(
!bot_jid.is_hosted(),
"Bot JID should NOT be detected as hosted (different mechanism)"
);
}
#[test]
fn is_same_chat_as_matches_rendered_chat_identity() {
let base: Jid = "5511999887766@s.whatsapp.net".parse().unwrap();
assert!(base.is_same_chat_as(&base.with_device(33)));
assert!(base.with_device(5).is_same_chat_as(&base.with_device(0)));
let other_user: Jid = "5521988776655@s.whatsapp.net".parse().unwrap();
assert!(!base.is_same_chat_as(&other_user));
let as_lid: Jid = "5511999887766@lid".parse().unwrap();
assert!(!base.is_same_chat_as(&as_lid));
let other_integrator = Jid {
integrator: 1,
..base.clone()
};
assert!(!base.is_same_chat_as(&other_integrator));
let pn_agent1 = Jid {
agent: 1,
..base.clone()
};
assert_eq!(base.to_string(), pn_agent1.to_string());
assert!(base.is_same_chat_as(&pn_agent1));
let bot_a = Jid {
user: "13136555001".into(),
server: Server::Bot,
agent: 1,
device: 0,
integrator: 0,
};
let bot_b = Jid {
agent: 2,
..bot_a.clone()
};
assert_ne!(bot_a.to_string(), bot_b.to_string());
assert!(!bot_a.is_same_chat_as(&bot_b));
assert!(bot_a.is_same_chat_as(&bot_a.with_device(7)));
}
#[test]
fn test_hosted_device_filtering_for_groups() {
let devices: Vec<Jid> = vec![
"5511999887766:0@s.whatsapp.net"
.parse()
.expect("test JID should be valid"), "5511999887766:33@s.whatsapp.net"
.parse()
.expect("test JID should be valid"), "5521988776655:0@s.whatsapp.net"
.parse()
.expect("test JID should be valid"), "100000012345678:0@lid"
.parse()
.expect("test JID should be valid"), "100000012345678:33@lid"
.parse()
.expect("test JID should be valid"), "5531977665544:99@s.whatsapp.net"
.parse()
.expect("test JID should be valid"), "100000087654321:99@lid"
.parse()
.expect("test JID should be valid"), "5541966554433:99@hosted"
.parse()
.expect("test JID should be valid"), ];
let filtered: Vec<&Jid> = devices.iter().filter(|jid| !jid.is_hosted()).collect();
assert_eq!(
filtered.len(),
5,
"Should have 5 non-hosted devices after filtering"
);
for jid in &filtered {
assert!(
!jid.is_hosted(),
"Filtered list should not contain hosted devices: {}",
jid
);
}
let hosted_count = devices.iter().filter(|jid| jid.is_hosted()).count();
assert_eq!(hosted_count, 3, "Should have filtered out 3 hosted devices");
}
#[test]
fn test_jid_pn_factory() {
let jid = Jid::pn("1234567890");
assert_eq!(jid.user, "1234567890");
assert_eq!(jid.server, DEFAULT_USER_SERVER);
assert_eq!(jid.device, 0);
assert!(jid.is_pn());
}
#[test]
fn test_jid_lid_factory() {
let jid = Jid::lid("100000012345678");
assert_eq!(jid.user, "100000012345678");
assert_eq!(jid.server, HIDDEN_USER_SERVER);
assert_eq!(jid.device, 0);
assert!(jid.is_lid());
}
#[test]
fn test_jid_group_factory() {
let jid = Jid::group("123456789-1234567890");
assert_eq!(jid.user, "123456789-1234567890");
assert_eq!(jid.server, GROUP_SERVER);
assert!(jid.is_group());
}
#[test]
fn test_jid_pn_device_factory() {
let jid = Jid::pn_device("1234567890", 5);
assert_eq!(jid.user, "1234567890");
assert_eq!(jid.server, DEFAULT_USER_SERVER);
assert_eq!(jid.device, 5);
assert!(jid.is_pn());
assert!(jid.is_ad());
}
#[test]
fn test_jid_lid_device_factory() {
let jid = Jid::lid_device("100000012345678", 33);
assert_eq!(jid.user, "100000012345678");
assert_eq!(jid.server, HIDDEN_USER_SERVER);
assert_eq!(jid.device, 33);
assert!(jid.is_lid());
assert!(jid.is_ad());
}
#[test]
fn with_device_hosting_preserves_addressing_family() {
let hosted_pn = Jid::pn("1234567890").with_device_hosting(7, true);
assert_eq!(hosted_pn.server, Server::Hosted);
assert_eq!(hosted_pn.device, 7);
let hosted_lid = Jid::lid("100000012345678").with_device_hosting(8, true);
assert_eq!(hosted_lid.server, Server::HostedLid);
assert_eq!(hosted_lid.device, 8);
let regular = Jid::pn("1234567890").with_device_hosting(9, false);
assert_eq!(regular.server, Server::Pn);
let regular_from_hosted =
Jid::new("1234567890", Server::Hosted).with_device_hosting(9, false);
assert_eq!(regular_from_hosted.server, Server::Pn);
let group = Jid::group("123-456").with_device_hosting(10, true);
assert_eq!(group.server, Server::Group);
}
#[test]
fn test_status_broadcast_jid() {
let jid = Jid::status_broadcast();
assert_eq!(jid.user, STATUS_BROADCAST_USER);
assert_eq!(jid.server, BROADCAST_SERVER);
assert_eq!(jid.device, 0);
assert!(jid.is_status_broadcast());
assert!(!jid.is_group());
assert!(!jid.is_broadcast_list());
assert_eq!(jid.to_string(), "status@broadcast");
let parsed: Jid = "status@broadcast".parse().expect("should parse");
assert!(parsed.is_status_broadcast());
assert_eq!(parsed.user, "status");
assert_eq!(parsed.server, "broadcast");
let broadcast_list = Jid::new("12345", Server::Broadcast);
assert!(broadcast_list.is_broadcast_list());
assert!(!broadcast_list.is_status_broadcast());
let group: Jid = "120363012345678901@g.us".parse().expect("should parse");
assert!(group.is_group());
assert!(!group.is_status_broadcast());
}
#[test]
fn test_jid_to_non_ad_preserves_user_server() {
let device_jid = Jid::pn_device("1234567890", 33);
let non_ad = device_jid.to_non_ad();
assert_eq!(non_ad.user, "1234567890");
assert_eq!(non_ad.server, DEFAULT_USER_SERVER);
assert_eq!(non_ad.device, 0);
assert!(!non_ad.is_ad());
let lid_device = Jid::lid_device("100000012345678", 25);
let lid_non_ad = lid_device.to_non_ad();
assert_eq!(lid_non_ad.user, "100000012345678");
assert_eq!(lid_non_ad.server, HIDDEN_USER_SERVER);
assert_eq!(lid_non_ad.device, 0);
let status = Jid::status_broadcast();
let status_non_ad = status.to_non_ad();
assert_eq!(status_non_ad.to_string(), "status@broadcast");
}
#[test]
fn test_to_non_ad_string_matches_to_non_ad_to_string() {
for s in [
"1234567890:33@s.whatsapp.net",
"1234567890@s.whatsapp.net",
"100000012345678:25@lid",
"100000012345678@lid",
"867051314767696:0@bot",
"867051314767696@bot",
"120363021033254949@g.us",
"status@broadcast",
"12-34@g.us",
] {
let jid: Jid = s.parse().expect("parse");
assert_eq!(
jid.to_non_ad_string(),
jid.to_non_ad().to_string(),
"mismatch for {s}"
);
}
}
#[test]
fn to_non_ad_arc_str_matches_to_non_ad_string() {
let long_user = "9".repeat(80);
let multibyte_user = "ẞünïcodé-ñ".repeat(3);
let owned = [
format!("{long_user}:12@s.whatsapp.net"),
format!("{multibyte_user}@g.us"),
format!("{multibyte_user}@s.whatsapp.net"),
];
let cases = [
"1234567890:33@s.whatsapp.net",
"1234567890@s.whatsapp.net",
"100000012345678:25@lid",
"867051314767696:0@bot",
"867051314767696.5:10@bot",
"120363021033254949@g.us",
"status@broadcast",
]
.into_iter()
.chain(owned.iter().map(String::as_str));
for s in cases {
let jid: Jid = s.parse().unwrap_or_else(|e| panic!("parse {s}: {e}"));
assert_eq!(
&*jid.to_non_ad_arc_str(),
jid.to_non_ad_string().as_str(),
"mismatch for {s}"
);
}
let empty = Jid::default();
assert_eq!(
&*empty.to_non_ad_arc_str(),
empty.to_non_ad_string().as_str()
);
}
#[test]
fn test_into_non_ad_matches_to_non_ad() {
for s in [
"1234567890.2:33@s.whatsapp.net",
"1234567890@s.whatsapp.net",
"100000012345678:25@lid",
"user.5:10@bot",
"447911123456.3@interop",
"120363021033254949@g.us",
"status@broadcast",
] {
let jid: Jid = s.parse().expect("parse");
assert_eq!(
jid.clone().into_non_ad(),
jid.to_non_ad(),
"mismatch for {s}"
);
}
}
#[test]
fn test_jid_factories_with_string_types() {
let jid1 = Jid::pn("123");
assert_eq!(jid1.user, "123");
let jid2 = Jid::lid(String::from("456"));
assert_eq!(jid2.user, "456");
let user = "789".to_string();
let jid3 = Jid::group(user);
assert_eq!(jid3.user, "789");
}
#[test]
fn test_jid_format_parity() {
struct Case {
user: &'static str,
server: Server,
agent: u8,
device: u16,
}
let cases = [
Case {
user: "",
server: Server::Pn,
agent: 0,
device: 0,
},
Case {
user: "5511999887766",
server: Server::Pn,
agent: 0,
device: 0,
},
Case {
user: "5511999887766",
server: Server::Pn,
agent: 0,
device: 2,
},
Case {
user: "5511999887766",
server: Server::Pn,
agent: 3,
device: 15,
},
Case {
user: "12345.6789",
server: Server::Lid,
agent: 1,
device: 25,
},
Case {
user: "100000012345678",
server: Server::Hosted,
agent: 2,
device: 99,
},
Case {
user: "100000012345678",
server: Server::HostedLid,
agent: 1,
device: 99,
},
Case {
user: "120363012345678901",
server: Server::Group,
agent: 0,
device: 0,
},
Case {
user: "user",
server: Server::Bot,
agent: 5,
device: 10,
},
Case {
user: "447911123456",
server: Server::Interop,
agent: 3,
device: 0,
},
Case {
user: "messenger_user",
server: Server::Messenger,
agent: 0,
device: 50,
},
Case {
user: "status",
server: Server::Broadcast,
agent: 0,
device: 0,
},
Case {
user: "newsletter_id",
server: Server::Newsletter,
agent: 0,
device: 0,
},
Case {
user: "447911123456789",
server: Server::Pn,
agent: 255,
device: 65535,
},
Case {
user: "1",
server: Server::Legacy,
agent: 0,
device: 1,
},
];
for (i, c) in cases.iter().enumerate() {
let jid = Jid {
user: c.user.into(),
server: c.server,
agent: c.agent,
device: c.device,
integrator: 0,
};
let display = jid.to_string();
let jid_ref = JidRef {
user: NodeStr::Borrowed(c.user),
server: c.server,
agent: c.agent,
device: c.device,
integrator: 0,
};
let ref_display = jid_ref.to_string();
let mut string_buf = String::new();
push_jid_to_string(c.user, c.server, c.agent, c.device, &mut string_buf);
let mut compact_buf = CompactString::default();
push_jid_to_compact(c.user, c.server, c.agent, c.device, &mut compact_buf);
let mut push_buf = String::new();
jid.push_to(&mut push_buf);
let mut write_buf = String::new();
jid.write_display_to(&mut write_buf).unwrap();
assert_eq!(display, ref_display, "case {i}: Display vs JidRef::Display");
assert_eq!(
display, string_buf,
"case {i}: Display vs push_jid_to_string"
);
assert_eq!(
display,
compact_buf.as_str(),
"case {i}: Display vs push_jid_to_compact"
);
assert_eq!(display, push_buf, "case {i}: Display vs Jid::push_to");
assert_eq!(display, write_buf, "case {i}: Display vs generic writer");
}
}
}