jid: Test for too many @ before the resourcepart

This additionally should optimize the parsing a tiny bit by looking for
both @ and / at the same time and iterating on them, instead of one by
one.

Thanks nicoco for the report!
This commit is contained in:
Emmanuel Gil Peyrot 2025-02-04 18:11:42 +01:00 committed by pep
commit f5f35eab20
3 changed files with 80 additions and 48 deletions

View file

@ -1,3 +1,9 @@
Version NEXT:
* Additions:
- Add missing check for JIDs with too many `@` before the resource, such as
`a@b@c` or `a@b@c/d` which should clearly be invalid. The new error it
produces is named `TooManyAts`.
Version 0.11.1, release 2024-07-23: Version 0.11.1, release 2024-07-23:
* Breaking: * Breaking:
- Move InnerJid into Jid and reformulate BareJid and FullJid in terms of - Move InnerJid into Jid and reformulate BareJid and FullJid in terms of

View file

@ -46,6 +46,9 @@ pub enum Error {
/// Happens when parsing a bare JID and there is a resource. /// Happens when parsing a bare JID and there is a resource.
ResourceInBareJid, ResourceInBareJid,
/// Happens when parsing a JID which has two @ before the resource.
TooManyAts,
} }
impl core::error::Error for Error {} impl core::error::Error for Error {}
@ -64,6 +67,7 @@ impl fmt::Display for Error {
Error::ResourcePrep => "resource doesnt pass resourceprep validation", Error::ResourcePrep => "resource doesnt pass resourceprep validation",
Error::ResourceMissingInFullJid => "no resource found in this full JID", Error::ResourceMissingInFullJid => "no resource found in this full JID",
Error::ResourceInBareJid => "resource found while parsing a bare JID", Error::ResourceInBareJid => "resource found while parsing a bare JID",
Error::TooManyAts => "second @ found before parsing the resource",
}) })
} }
} }

View file

@ -49,7 +49,7 @@ use core::num::NonZeroU16;
use core::ops::Deref; use core::ops::Deref;
use core::str::FromStr; use core::str::FromStr;
use memchr::memchr; use memchr::memchr2_iter;
use stringprep::{nameprep, nodeprep, resourceprep}; use stringprep::{nameprep, nodeprep, resourceprep};
@ -170,23 +170,25 @@ impl Jid {
/// ``` /// ```
pub fn new(unnormalized: &str) -> Result<Jid, Error> { pub fn new(unnormalized: &str) -> Result<Jid, Error> {
let bytes = unnormalized.as_bytes(); let bytes = unnormalized.as_bytes();
let mut orig_at = memchr(b'@', bytes); let orig_at;
let mut orig_slash = memchr(b'/', bytes); let orig_slash;
if orig_at.is_some() && orig_slash.is_some() && orig_at > orig_slash { let mut iter = memchr2_iter(b'@', b'/', bytes);
// This is part of the resource, not a node@domain separator. let normalized = if let Some(first_index) = iter.next() {
orig_at = None; let byte = bytes[first_index];
} if byte == b'@' {
if let Some(second_index) = iter.next() {
let normalized = match (orig_at, orig_slash) { let byte = bytes[second_index];
(Some(at), Some(slash)) => { if byte == b'/' {
let node = nodeprep(&unnormalized[..at]).map_err(|_| Error::NodePrep)?; let node =
nodeprep(&unnormalized[..first_index]).map_err(|_| Error::NodePrep)?;
length_check(node.len(), Error::NodeEmpty, Error::NodeTooLong)?; length_check(node.len(), Error::NodeEmpty, Error::NodeTooLong)?;
let domain = nameprep(&unnormalized[at + 1..slash]).map_err(|_| Error::NamePrep)?; let domain = nameprep(&unnormalized[first_index + 1..second_index])
.map_err(|_| Error::NamePrep)?;
length_check(domain.len(), Error::DomainEmpty, Error::DomainTooLong)?; length_check(domain.len(), Error::DomainEmpty, Error::DomainTooLong)?;
let resource = let resource = resourceprep(&unnormalized[second_index + 1..])
resourceprep(&unnormalized[slash + 1..]).map_err(|_| Error::ResourcePrep)?; .map_err(|_| Error::ResourcePrep)?;
length_check(resource.len(), Error::ResourceEmpty, Error::ResourceTooLong)?; length_check(resource.len(), Error::ResourceEmpty, Error::ResourceTooLong)?;
orig_at = Some(node.len()); orig_at = Some(node.len());
@ -197,40 +199,58 @@ impl Jid {
} }
(node, domain, resource) => format!("{node}@{domain}/{resource}"), (node, domain, resource) => format!("{node}@{domain}/{resource}"),
} }
} else
/* This is another '@' character. */
{
return Err(Error::TooManyAts);
} }
(Some(at), None) => { } else {
let node = nodeprep(&unnormalized[..at]).map_err(|_| Error::NodePrep)?; // That is a node@domain JID.
let node =
nodeprep(&unnormalized[..first_index]).map_err(|_| Error::NodePrep)?;
length_check(node.len(), Error::NodeEmpty, Error::NodeTooLong)?; length_check(node.len(), Error::NodeEmpty, Error::NodeTooLong)?;
let domain = nameprep(&unnormalized[at + 1..]).map_err(|_| Error::NamePrep)?; let domain =
nameprep(&unnormalized[first_index + 1..]).map_err(|_| Error::NamePrep)?;
length_check(domain.len(), Error::DomainEmpty, Error::DomainTooLong)?; length_check(domain.len(), Error::DomainEmpty, Error::DomainTooLong)?;
orig_at = Some(node.len()); orig_at = Some(node.len());
orig_slash = None;
match (node, domain) { match (node, domain) {
(Cow::Borrowed(_), Cow::Borrowed(_)) => unnormalized.to_string(), (Cow::Borrowed(_), Cow::Borrowed(_)) => unnormalized.to_string(),
(node, domain) => format!("{node}@{domain}"), (node, domain) => format!("{node}@{domain}"),
} }
} }
(None, Some(slash)) => { } else
let domain = nameprep(&unnormalized[..slash]).map_err(|_| Error::NamePrep)?; /* This is a '/' character. */
{
// The JID is of the form domain/resource, we can stop looking for further
// characters.
let domain = nameprep(&unnormalized[..first_index]).map_err(|_| Error::NamePrep)?;
length_check(domain.len(), Error::DomainEmpty, Error::DomainTooLong)?; length_check(domain.len(), Error::DomainEmpty, Error::DomainTooLong)?;
let resource = let resource = resourceprep(&unnormalized[first_index + 1..])
resourceprep(&unnormalized[slash + 1..]).map_err(|_| Error::ResourcePrep)?; .map_err(|_| Error::ResourcePrep)?;
length_check(resource.len(), Error::ResourceEmpty, Error::ResourceTooLong)?; length_check(resource.len(), Error::ResourceEmpty, Error::ResourceTooLong)?;
orig_at = None;
orig_slash = Some(domain.len()); orig_slash = Some(domain.len());
match (domain, resource) { match (domain, resource) {
(Cow::Borrowed(_), Cow::Borrowed(_)) => unnormalized.to_string(), (Cow::Borrowed(_), Cow::Borrowed(_)) => unnormalized.to_string(),
(domain, resource) => format!("{domain}/{resource}"), (domain, resource) => format!("{domain}/{resource}"),
} }
} }
(None, None) => { } else {
// Last possible case, just a domain JID.
let domain = nameprep(unnormalized).map_err(|_| Error::NamePrep)?; let domain = nameprep(unnormalized).map_err(|_| Error::NamePrep)?;
length_check(domain.len(), Error::DomainEmpty, Error::DomainTooLong)?; length_check(domain.len(), Error::DomainEmpty, Error::DomainTooLong)?;
orig_at = None;
orig_slash = None;
domain.into_owned() domain.into_owned()
}
}; };
Ok(Self { Ok(Self {
@ -1059,6 +1079,8 @@ mod tests {
Err(Error::ResourceMissingInFullJid) Err(Error::ResourceMissingInFullJid)
); );
assert_eq!(BareJid::from_str("a@b/c"), Err(Error::ResourceInBareJid)); assert_eq!(BareJid::from_str("a@b/c"), Err(Error::ResourceInBareJid));
assert_eq!(BareJid::from_str("a@b@c"), Err(Error::TooManyAts));
assert_eq!(FullJid::from_str("a@b@c/d"), Err(Error::TooManyAts));
} }
#[test] #[test]