From d1f2e196db7a09dd606a76e255c8312730421829 Mon Sep 17 00:00:00 2001 From: Emmanuel Gil Peyrot Date: Wed, 9 Aug 2023 16:18:08 +0200 Subject: [PATCH] Initial Rust version. --- .gitignore | 7 +- Cargo.toml | 13 ++ slixmpp/jid.py | 347 +--------------------------------------------- src/lib.rs | 278 +++++++++++++++++++++++++++++++++++++ tests/test_jid.py | 5 - 5 files changed, 298 insertions(+), 352 deletions(-) create mode 100644 Cargo.toml create mode 100644 src/lib.rs diff --git a/.gitignore b/.gitignore index 6d046004..899e7195 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,9 @@ slixmpp.egg-info/ .DS_STORE .idea/ .vscode/ -venv/ \ No newline at end of file +venv/ + +# Added by cargo + +/target +/Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..ef94d5be --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "slixmpp" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +jid = "0.10" +pyo3 = "0.21" + +[lib] +crate-type = ["cdylib"] diff --git a/slixmpp/jid.py b/slixmpp/jid.py index 450a17c2..78695e24 100644 --- a/slixmpp/jid.py +++ b/slixmpp/jid.py @@ -1,346 +1 @@ - -# slixmpp.jid -# ~~~~~~~~~~~~~~~~~~~~~~~ -# This module allows for working with Jabber IDs (JIDs). -# Part of Slixmpp: The Slick XMPP Library -# :copyright: (c) 2011 Nathanael C. Fritz -# :license: MIT, see LICENSE for more details -from __future__ import annotations - -import re -import socket - -from functools import lru_cache -from typing import ( - Optional, - Union, -) - -from slixmpp.stringprep import nodeprep, resourceprep, idna, StringprepError - -HAVE_INET_PTON = hasattr(socket, 'inet_pton') - -#: The basic regex pattern that a JID must match in order to determine -#: the local, domain, and resource parts. This regex does NOT do any -#: validation, which requires application of nodeprep, resourceprep, etc. -JID_PATTERN = re.compile( - "^(?:([^\"&'/:<>@]{1,1023})@)?([^/@]{1,1023})(?:/(.{1,1023}))?$" -) - -#: The set of escape sequences for the characters not allowed by nodeprep. -JID_ESCAPE_SEQUENCES = {'\\20', '\\22', '\\26', '\\27', '\\2f', - '\\3a', '\\3c', '\\3e', '\\40', '\\5c'} - - -# TODO: Find the best cache size for a standard usage. -@lru_cache(maxsize=1024) -def _parse_jid(data: str): - """ - Parse string data into the node, domain, and resource - components of a JID, if possible. - - :param string data: A string that is potentially a JID. - - :raises InvalidJID: - - :returns: tuple of the validated local, domain, and resource strings - """ - match = JID_PATTERN.match(data) - if not match: - raise InvalidJID('JID could not be parsed') - - (node, domain, resource) = match.groups() - - node = _validate_node(node) - domain = _validate_domain(domain) - resource = _validate_resource(resource) - - return node, domain, resource - - -def _validate_node(node: Optional[str]): - """Validate the local, or username, portion of a JID. - - :raises InvalidJID: - - :returns: The local portion of a JID, as validated by nodeprep. - """ - if node is None: - return '' - - try: - node = nodeprep(node) - except StringprepError: - raise InvalidJID('Nodeprep failed') - - if not node: - raise InvalidJID('Localpart must not be 0 bytes') - if len(node) > 1023: - raise InvalidJID('Localpart must be less than 1024 bytes') - return node - - -def _validate_domain(domain: str): - """Validate the domain portion of a JID. - - IP literal addresses are left as-is, if valid. Domain names - are stripped of any trailing label separators (`.`), and are - checked with the nameprep profile of stringprep. If the given - domain is actually a punyencoded version of a domain name, it - is converted back into its original Unicode form. Domains must - also not start or end with a dash (`-`). - - :raises InvalidJID: - - :returns: The validated domain name - """ - ip_addr = False - - # First, check if this is an IPv4 address - try: - socket.inet_aton(domain) - ip_addr = True - except socket.error: - pass - - # Check if this is an IPv6 address - if not ip_addr and HAVE_INET_PTON and domain[0] == '[' and domain[-1] == ']': - try: - ip = domain[1:-1] - socket.inet_pton(socket.AF_INET6, ip) - ip_addr = True - except (socket.error, ValueError): - pass - - if not ip_addr: - # This is a domain name, which must be checked further - - if domain and domain[-1] == '.': - domain = domain[:-1] - - try: - domain = idna(domain) - except StringprepError: - raise InvalidJID(f'idna validation failed: {domain}') - - if ':' in domain: - raise InvalidJID(f'Domain containing a port: {domain}') - for label in domain.split('.'): - if not label: - raise InvalidJID(f'Domain containing too many dots: {domain}') - if '-' in (label[0], label[-1]): - raise InvalidJID(f'Domain starting or ending with -: {domain}') - - if not domain: - raise InvalidJID('Domain must not be 0 bytes') - if len(domain) > 1023: - raise InvalidJID('Domain must be less than 1024 bytes') - - return domain - - -def _validate_resource(resource: Optional[str]): - """Validate the resource portion of a JID. - - :raises InvalidJID: - - :returns: The local portion of a JID, as validated by resourceprep. - """ - if resource is None: - return '' - - try: - resource = resourceprep(resource) - except StringprepError: - raise InvalidJID('Resourceprep failed') - - if not resource: - raise InvalidJID('Resource must not be 0 bytes') - if len(resource) > 1023: - raise InvalidJID('Resource must be less than 1024 bytes') - return resource - - -def _format_jid( - local: Optional[str] = None, - domain: Optional[str] = None, - resource: Optional[str] = None, - ): - """Format the given JID components into a full or bare JID. - - :param string local: Optional. The local portion of the JID. - :param string domain: Required. The domain name portion of the JID. - :param strin resource: Optional. The resource portion of the JID. - - :return: A full or bare JID string. - """ - if domain is None: - return '' - if local is not None: - result = local + '@' + domain - else: - result = domain - if resource is not None: - result += '/' + resource - return result - - -class InvalidJID(ValueError): - """ - Raised when attempting to create a JID that does not pass validation. - - It can also be raised if modifying an existing JID in such a way as - to make it invalid, such trying to remove the domain from an existing - full JID while the local and resource portions still exist. - """ - - -class JID: - - """ - A representation of a Jabber ID, or JID. - - Each JID may have three components: a user, a domain, and an optional - resource. For example: user@domain/resource - - When a resource is not used, the JID is called a bare JID. - The JID is a full JID otherwise. - - **JID Properties:** - :full: The string value of the full JID. - :jid: Alias for ``full``. - :bare: The string value of the bare JID. - :node: The node portion of the JID. - :user: Alias for ``node``. - :local: Alias for ``node``. - :username: Alias for ``node``. - :domain: The domain name portion of the JID. - :server: Alias for ``domain``. - :host: Alias for ``domain``. - :resource: The resource portion of the JID. - - :param string jid: - A string of the form ``'[user@]domain[/resource]'``. - :param bool bare: - If present, discard the provided resource. - - :raises InvalidJID: - """ - - __slots__ = ('_node', '_domain', '_resource', '_bare', '_full') - - def __init__(self, jid: Optional[Union[str, 'JID']] = None, bare: bool = False): - if not jid: - self._node = '' - self._domain = '' - self._resource = '' - self._bare = '' - self._full = '' - return - elif not isinstance(jid, JID): - node, domain, resource = _parse_jid(jid) - self._node = node - self._domain = domain - self._resource = resource if not bare else '' - else: - self._node = jid._node - self._domain = jid._domain - self._resource = jid._resource if not bare else '' - self._update_bare_full() - - def _update_bare_full(self): - """Format the given JID into a bare and a full JID. - """ - self._bare = (self._node + '@' + self._domain - if self._node - else self._domain) - self._full = (self._bare + '/' + self._resource - if self._resource - else self._bare) - - @property - def bare(self) -> str: - return self._bare - - @bare.setter - def bare(self, value: str): - node, domain, resource = _parse_jid(value) - assert not resource - self._node = node - self._domain = domain - self._update_bare_full() - - - @property - def node(self) -> str: - return self._node - - @node.setter - def node(self, value: Optional[str]): - self._node = _validate_node(value) - self._update_bare_full() - - @property - def domain(self) -> str: - return self._domain - - @domain.setter - def domain(self, value: str): - self._domain = _validate_domain(value) - self._update_bare_full() - - @property - def resource(self) -> str: - return self._resource - - @resource.setter - def resource(self, value: Optional[str]): - self._resource = _validate_resource(value) - self._update_bare_full() - - @property - def full(self) -> str: - return self._full - - @full.setter - def full(self, value: str): - self._node, self._domain, self._resource = _parse_jid(value) - self._update_bare_full() - - user = node - local = node - username = node - - server = domain - host = domain - - jid = full - - def __str__(self): - """Use the full JID as the string value.""" - return self._full - - def __repr__(self): - """Use the full JID as the representation.""" - return self._full - - # pylint: disable=W0212 - def __eq__(self, other): - """Two JIDs are equal if they have the same full JID value.""" - if not isinstance(other, JID): - try: - other = JID(other) - except InvalidJID: - return NotImplemented - - return (self._node == other._node and - self._domain == other._domain and - self._resource == other._resource) - - def __ne__(self, other): - """Two JIDs are considered unequal if they are not equal.""" - return not self == other - - def __hash__(self): - """Hash a JID based on the string version of its full JID.""" - return hash(self._full) +from libslixmpp import JID, InvalidJID diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 00000000..c2ebfd32 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,278 @@ +use pyo3::exceptions::{PyNotImplementedError, PyValueError}; +use pyo3::prelude::*; + +pyo3::create_exception!(py_jid, InvalidJID, PyValueError, "Raised when attempting to create a JID that does not pass validation.\n\nIt can also be raised if modifying an existing JID in such a way as\nto make it invalid, such trying to remove the domain from an existing\nfull JID while the local and resource portions still exist."); + +fn to_exc(err: jid::Error) -> PyErr { + InvalidJID::new_err(err.to_string()) +} + +/// A representation of a Jabber ID, or JID. +/// +/// Each JID may have three components: a user, a domain, and an optional resource. For example: +/// user@domain/resource +/// +/// When a resource is not used, the JID is called a bare JID. The JID is a full JID otherwise. +/// +/// Raises InvalidJID if the parser rejects it. +#[pyclass(name = "JID", module = "slixmpp.jid")] +struct PyJid { + jid: Option, +} + +#[pymethods] +impl PyJid { + #[new] + #[pyo3(signature = (jid=None, bare=false))] + fn new(jid: Option<&Bound<'_, PyAny>>, bare: bool) -> PyResult { + if let Some(jid) = jid { + if let Ok(py_jid) = jid.extract::>() { + if bare { + if let Some(py_jid) = &(*py_jid).jid { + Ok(PyJid { + jid: Some(jid::Jid::Bare(py_jid.to_bare())), + }) + } else { + Ok(PyJid { jid: None }) + } + } else { + Ok(PyJid { + jid: (*py_jid).jid.clone(), + }) + } + } else { + let jid: &str = jid.extract()?; + if jid.is_empty() { + Ok(PyJid { jid: None }) + } else { + let mut jid = jid::Jid::new(jid).map_err(to_exc)?; + if bare { + jid = jid::Jid::Bare(jid.into_bare()) + } + Ok(PyJid { jid: Some(jid) }) + } + } + } else { + Ok(PyJid { jid: None }) + } + } + + /* + // TODO: implement or remove from the API + fn unescape() { + } + */ + + #[getter] + fn get_bare(&self) -> String { + match &self.jid { + None => String::new(), + Some(jid) => jid.to_bare().to_string(), + } + } + + #[setter] + fn set_bare(&mut self, bare: &str) -> PyResult<()> { + let bare = jid::BareJid::new(bare).map_err(to_exc)?; + self.jid = Some(match &self.jid { + Some(jid::Jid::Bare(_)) | None => jid::Jid::Bare(bare), + Some(jid::Jid::Full(jid)) => jid::Jid::Full(bare.with_resource(&jid.resource())), + }); + Ok(()) + } + + #[getter] + fn get_full(&self) -> String { + match &self.jid { + None => String::new(), + Some(jid) => jid.to_string(), + } + } + + #[setter] + fn set_full(&mut self, full: &str) -> PyResult<()> { + // JID.full = 'domain' is acceptable in slixmpp. + self.jid = Some(jid::Jid::new(full).map_err(to_exc)?); + Ok(()) + } + + #[getter] + fn get_node(&self) -> String { + match &self.jid { + None => String::new(), + Some(jid) => jid + .node_str() + .map(ToString::to_string) + .unwrap_or_else(String::new), + } + } + + #[setter] + fn set_node(&mut self, node: &str) -> PyResult<()> { + let node = jid::NodePart::new(node).map_err(to_exc)?; + self.jid = Some(match &self.jid { + Some(jid::Jid::Bare(jid)) => { + jid::Jid::Bare(jid::BareJid::from_parts(Some(&node), &jid.domain())) + } + Some(jid::Jid::Full(jid)) => jid::Jid::Full(jid::FullJid::from_parts( + Some(&node), + &jid.domain(), + &jid.resource(), + )), + None => Err(InvalidJID::new_err("JID.node must apply to a proper JID"))?, + }); + Ok(()) + } + + #[getter] + fn get_domain(&self) -> String { + match &self.jid { + None => String::new(), + Some(jid) => jid.domain_str().to_string(), + } + } + + #[setter] + fn set_domain(&mut self, domain: &str) -> PyResult<()> { + let domain = jid::DomainPart::new(domain).map_err(to_exc)?; + self.jid = Some(match &self.jid { + Some(jid::Jid::Bare(jid)) => { + jid::Jid::Bare(jid::BareJid::from_parts(jid.node().as_ref(), &domain)) + } + Some(jid::Jid::Full(jid)) => jid::Jid::Full(jid::FullJid::from_parts( + jid.node().as_ref(), + &domain, + &jid.resource(), + )), + None => jid::Jid::Bare(jid::BareJid::from_parts(None, &domain)), + }); + Ok(()) + } + + #[getter] + fn get_resource(&self) -> String { + match &self.jid { + None => String::new(), + Some(jid) => jid + .resource_str() + .map(ToString::to_string) + .unwrap_or_else(String::new), + } + } + + #[setter] + fn set_resource(&mut self, resource: &str) -> PyResult<()> { + let resource = jid::ResourcePart::new(resource).map_err(to_exc)?; + self.jid = Some(match &self.jid { + Some(jid::Jid::Bare(jid)) => jid::Jid::Full(jid.with_resource(&resource)), + Some(jid::Jid::Full(jid)) => jid::Jid::Full(jid::FullJid::from_parts( + jid.node().as_ref(), + &jid.domain(), + &resource, + )), + None => Err(InvalidJID::new_err( + "JID.resource must apply to a proper JID", + ))?, + }); + Ok(()) + } + + /// Use the full JID as the string value. + fn __str__(&self) -> String { + match &self.jid { + None => String::new(), + Some(jid) => jid.to_string(), + } + } + + /// Use the full JID as the representation. + fn __repr__(&self) -> String { + match &self.jid { + None => String::new(), + Some(jid) => jid.to_string(), + } + } + + /// Two JIDs are equal if they have the same full JID value. + fn __richcmp__(&self, other: &Bound<'_, PyAny>, op: pyo3::basic::CompareOp) -> PyResult { + let other = if let Ok(other) = other.extract::>() { + other + } else if other.is_none() { + Bound::new(other.py(), PyJid::new(None, false)?)?.borrow() + } else { + Bound::new(other.py(), PyJid::new(Some(other), false)?)?.borrow() + }; + match (&self.jid, &other.jid) { + (None, None) => Ok(true), + (Some(jid), Some(other)) => match op { + pyo3::basic::CompareOp::Eq => Ok(jid == other), + pyo3::basic::CompareOp::Ne => Ok(jid != other), + _ => Err(PyNotImplementedError::new_err( + "Only == and != are implemented", + )), + }, + _ => Ok(false), + } + } + + /// Hash a JID based on the string version of its full JID. + fn __hash__(&self) -> isize { + if let Some(jid) = &self.jid { + // Use the same algorithm as the Python JID. + let string = jid.to_string(); + unsafe { pyo3::ffi::_Py_HashBytes(string.as_ptr() as *const _, string.len() as isize) } + } else { + 0 + } + } + + // Aliases + + #[getter] + fn get_user(&self) -> String { + self.get_node() + } + + #[setter] + fn set_user(&mut self, user: &str) -> PyResult<()> { + self.set_node(user) + } + + #[getter] + fn get_server(&self) -> String { + self.get_domain() + } + + #[setter] + fn set_server(&mut self, server: &str) -> PyResult<()> { + self.set_domain(server) + } + + #[getter] + fn get_host(&self) -> String { + self.get_domain() + } + + #[setter] + fn set_host(&mut self, host: &str) -> PyResult<()> { + self.set_domain(host) + } + + #[getter] + fn get_jid(&self) -> String { + self.get_full() + } + + #[setter] + fn set_jid(&mut self, jid: &str) -> PyResult<()> { + self.set_full(jid) + } +} + +#[pymodule] +#[pyo3(name = "libslixmpp")] +fn py_jid(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_class::()?; + m.add("InvalidJID", py.get_type_bound::())?; + Ok(()) +} diff --git a/tests/test_jid.py b/tests/test_jid.py index 4093c5a1..f6ef342b 100644 --- a/tests/test_jid.py +++ b/tests/test_jid.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals import unittest from slixmpp.test import SlixTest from slixmpp import JID, InvalidJID -from slixmpp.jid import nodeprep class TestJIDClass(SlixTest): @@ -279,9 +278,5 @@ class TestJIDClass(SlixTest): #self.assertRaises(InvalidJID, JID, '%s@example.com' % '\\20foo2') #self.assertRaises(InvalidJID, JID, '%s@example.com' % 'bar2\\20') - def testNodePrepIdemptotent(self): - node = 'ᴹᴵᴷᴬᴱᴸ' - self.assertEqual(nodeprep(node), nodeprep(nodeprep(node))) - suite = unittest.TestLoader().loadTestsFromTestCase(TestJIDClass)