1// Copyright (c) 2017, 2018 lumi <lumi@pew.im>
2// Copyright (c) 2017, 2018, 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
3// Copyright (c) 2017, 2018, 2019 Maxime “pep” Buquet <pep@bouah.net>
4// Copyright (c) 2017, 2018 Astro <astro@spaceboyz.net>
5// Copyright (c) 2017 Bastien Orivel <eijebong@bananium.fr>
6//
7// This Source Code Form is subject to the terms of the Mozilla Public
8// License, v. 2.0. If a copy of the MPL was not distributed with this
9// file, You can obtain one at http://mozilla.org/MPL/2.0/.
10
11#![deny(missing_docs)]
12
13//! Provides a type for Jabber IDs.
14//!
15//! For usage, check the documentation on the `Jid` struct.
16
17#[macro_use]
18extern crate failure_derive;
19
20use std::convert::Into;
21use std::fmt;
22use std::str::FromStr;
23
24/// An error that signifies that a `Jid` cannot be parsed from a string.
25#[derive(Debug, Clone, PartialEq, Eq, Fail)]
26pub enum JidParseError {
27 /// Happens when there is no domain, that is either the string is empty,
28 /// starts with a /, or contains the @/ sequence.
29 #[fail(display = "no domain found in this JID")]
30 NoDomain,
31
32 /// Happens when there is no resource, that is string contains no /.
33 #[fail(display = "no resource found in this full JID")]
34 NoResource,
35
36 /// Happens when the node is empty, that is the string starts with a @.
37 #[fail(display = "nodepart empty despite the presence of a @")]
38 EmptyNode,
39
40 /// Happens when the resource is empty, that is the string ends with a /.
41 #[fail(display = "resource empty despite the presence of a /")]
42 EmptyResource,
43}
44
45/// An enum representing a Jabber ID. It can be either a `FullJid` or a `BareJid`.
46#[derive(Debug, Clone, PartialEq)]
47pub enum Jid {
48 /// Bare Jid
49 Bare(BareJid),
50
51 /// Full Jid
52 Full(FullJid),
53}
54
55impl FromStr for Jid {
56 type Err = JidParseError;
57
58 fn from_str(s: &str) -> Result<Self, Self::Err> {
59 let (ns, ds, rs): StringJid = _from_str(s)?;
60 Ok(match rs {
61 Some(rs) => Jid::Full(FullJid {
62 node: ns,
63 domain: ds,
64 resource: rs,
65 }),
66 None => Jid::Bare(BareJid {
67 node: ns,
68 domain: ds,
69 }),
70 })
71 }
72}
73
74impl From<Jid> for String {
75 fn from(jid: Jid) -> String {
76 match jid {
77 Jid::Bare(bare) => String::from(bare),
78 Jid::Full(full) => String::from(full),
79 }
80 }
81}
82
83impl From<BareJid> for Jid {
84 fn from(bare_jid: BareJid) -> Jid {
85 Jid::Bare(bare_jid)
86 }
87}
88
89impl From<FullJid> for Jid {
90 fn from(full_jid: FullJid) -> Jid {
91 Jid::Full(full_jid)
92 }
93}
94
95impl Jid {
96 /// The node part of the Jabber ID, if it exists, else None.
97 pub fn node(self) -> Option<String> {
98 match self {
99 Jid::Bare(BareJid { node, .. }) | Jid::Full(FullJid { node, .. }) => node,
100 }
101 }
102
103 /// The domain of the Jabber ID.
104 pub fn domain(self) -> String {
105 match self {
106 Jid::Bare(BareJid { domain, .. }) | Jid::Full(FullJid { domain, .. }) => domain,
107 }
108 }
109}
110
111/// A struct representing a full Jabber ID.
112///
113/// A full Jabber ID is composed of 3 components, of which one is optional:
114///
115/// - A node/name, `node`, which is the optional part before the @.
116/// - A domain, `domain`, which is the mandatory part after the @ but before the /.
117/// - A resource, `resource`, which is the part after the /.
118///
119/// Unlike a `BareJid`, it always contains a resource, and should only be used when you are certain
120/// there is no case where a resource can be missing. Otherwise, use a `Jid` enum.
121#[derive(Clone, PartialEq, Eq, Hash)]
122pub struct FullJid {
123 /// The node part of the Jabber ID, if it exists, else None.
124 pub node: Option<String>,
125 /// The domain of the Jabber ID.
126 pub domain: String,
127 /// The resource of the Jabber ID.
128 pub resource: String,
129}
130
131/// A struct representing a bare Jabber ID.
132///
133/// A bare Jabber ID is composed of 2 components, of which one is optional:
134///
135/// - A node/name, `node`, which is the optional part before the @.
136/// - A domain, `domain`, which is the mandatory part after the @.
137///
138/// Unlike a `FullJid`, it can’t contain a resource, and should only be used when you are certain
139/// there is no case where a resource can be set. Otherwise, use a `Jid` enum.
140#[derive(Clone, PartialEq, Eq, Hash)]
141pub struct BareJid {
142 /// The node part of the Jabber ID, if it exists, else None.
143 pub node: Option<String>,
144 /// The domain of the Jabber ID.
145 pub domain: String,
146}
147
148impl From<FullJid> for String {
149 fn from(jid: FullJid) -> String {
150 let mut string = String::new();
151 if let Some(ref node) = jid.node {
152 string.push_str(node);
153 string.push('@');
154 }
155 string.push_str(&jid.domain);
156 string.push('/');
157 string.push_str(&jid.resource);
158 string
159 }
160}
161
162impl From<BareJid> for String {
163 fn from(jid: BareJid) -> String {
164 let mut string = String::new();
165 if let Some(ref node) = jid.node {
166 string.push_str(node);
167 string.push('@');
168 }
169 string.push_str(&jid.domain);
170 string
171 }
172}
173
174impl Into<BareJid> for FullJid {
175 fn into(self) -> BareJid {
176 BareJid {
177 node: self.node,
178 domain: self.domain,
179 }
180 }
181}
182
183impl fmt::Debug for FullJid {
184 fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> {
185 write!(fmt, "FullJID({})", self)
186 }
187}
188
189impl fmt::Debug for BareJid {
190 fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> {
191 write!(fmt, "BareJID({})", self)
192 }
193}
194
195impl fmt::Display for FullJid {
196 fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> {
197 fmt.write_str(String::from(self.clone()).as_ref())
198 }
199}
200
201impl fmt::Display for BareJid {
202 fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> {
203 fmt.write_str(String::from(self.clone()).as_ref())
204 }
205}
206
207enum ParserState {
208 Node,
209 Domain,
210 Resource,
211}
212
213type StringJid = (Option<String>, String, Option<String>);
214fn _from_str(s: &str) -> Result<StringJid, JidParseError> {
215 // TODO: very naive, may need to do it differently
216 let iter = s.chars();
217 let mut buf = String::with_capacity(s.len());
218 let mut state = ParserState::Node;
219 let mut node = None;
220 let mut domain = None;
221 let mut resource = None;
222 for c in iter {
223 match state {
224 ParserState::Node => {
225 match c {
226 '@' => {
227 if buf == "" {
228 return Err(JidParseError::EmptyNode);
229 }
230 state = ParserState::Domain;
231 node = Some(buf.clone()); // TODO: performance tweaks, do not need to copy it
232 buf.clear();
233 }
234 '/' => {
235 if buf == "" {
236 return Err(JidParseError::NoDomain);
237 }
238 state = ParserState::Resource;
239 domain = Some(buf.clone()); // TODO: performance tweaks
240 buf.clear();
241 }
242 c => {
243 buf.push(c);
244 }
245 }
246 }
247 ParserState::Domain => {
248 match c {
249 '/' => {
250 if buf == "" {
251 return Err(JidParseError::NoDomain);
252 }
253 state = ParserState::Resource;
254 domain = Some(buf.clone()); // TODO: performance tweaks
255 buf.clear();
256 }
257 c => {
258 buf.push(c);
259 }
260 }
261 }
262 ParserState::Resource => {
263 buf.push(c);
264 }
265 }
266 }
267 if !buf.is_empty() {
268 match state {
269 ParserState::Node => {
270 domain = Some(buf);
271 }
272 ParserState::Domain => {
273 domain = Some(buf);
274 }
275 ParserState::Resource => {
276 resource = Some(buf);
277 }
278 }
279 } else if let ParserState::Resource = state {
280 return Err(JidParseError::EmptyResource);
281 }
282 Ok((node, domain.ok_or(JidParseError::NoDomain)?, resource))
283}
284
285impl FromStr for FullJid {
286 type Err = JidParseError;
287
288 fn from_str(s: &str) -> Result<FullJid, JidParseError> {
289 let (ns, ds, rs): StringJid = _from_str(s)?;
290 Ok(FullJid {
291 node: ns,
292 domain: ds,
293 resource: rs.ok_or(JidParseError::NoResource)?,
294 })
295 }
296}
297
298impl FullJid {
299 /// Constructs a full Jabber ID containing all three components.
300 ///
301 /// This is of the form `node`@`domain`/`resource`.
302 ///
303 /// # Examples
304 ///
305 /// ```
306 /// use jid::FullJid;
307 ///
308 /// let jid = FullJid::new("node", "domain", "resource");
309 ///
310 /// assert_eq!(jid.node, Some("node".to_owned()));
311 /// assert_eq!(jid.domain, "domain".to_owned());
312 /// assert_eq!(jid.resource, "resource".to_owned());
313 /// ```
314 pub fn new<NS, DS, RS>(node: NS, domain: DS, resource: RS) -> FullJid
315 where
316 NS: Into<String>,
317 DS: Into<String>,
318 RS: Into<String>,
319 {
320 FullJid {
321 node: Some(node.into()),
322 domain: domain.into(),
323 resource: resource.into(),
324 }
325 }
326
327 /// Constructs a new Jabber ID from an existing one, with the node swapped out with a new one.
328 ///
329 /// # Examples
330 ///
331 /// ```
332 /// use jid::FullJid;
333 ///
334 /// let jid = FullJid::new("node", "domain", "resource");
335 ///
336 /// assert_eq!(jid.node, Some("node".to_owned()));
337 ///
338 /// let new_jid = jid.with_node("new_node");
339 ///
340 /// assert_eq!(new_jid.node, Some("new_node".to_owned()));
341 /// ```
342 pub fn with_node<NS>(&self, node: NS) -> FullJid
343 where
344 NS: Into<String>,
345 {
346 FullJid {
347 node: Some(node.into()),
348 domain: self.domain.clone(),
349 resource: self.resource.clone(),
350 }
351 }
352
353 /// Constructs a new Jabber ID from an existing one, with the domain swapped out with a new one.
354 ///
355 /// # Examples
356 ///
357 /// ```
358 /// use jid::FullJid;
359 ///
360 /// let jid = FullJid::new("node", "domain", "resource");
361 ///
362 /// assert_eq!(jid.domain, "domain".to_owned());
363 ///
364 /// let new_jid = jid.with_domain("new_domain");
365 ///
366 /// assert_eq!(new_jid.domain, "new_domain");
367 /// ```
368 pub fn with_domain<DS>(&self, domain: DS) -> FullJid
369 where
370 DS: Into<String>,
371 {
372 FullJid {
373 node: self.node.clone(),
374 domain: domain.into(),
375 resource: self.resource.clone(),
376 }
377 }
378
379 /// Constructs a full Jabber ID from a bare Jabber ID, specifying a `resource`.
380 ///
381 /// # Examples
382 ///
383 /// ```
384 /// use jid::FullJid;
385 ///
386 /// let jid = FullJid::new("node", "domain", "resource");
387 ///
388 /// assert_eq!(jid.resource, "resource".to_owned());
389 ///
390 /// let new_jid = jid.with_resource("new_resource");
391 ///
392 /// assert_eq!(new_jid.resource, "new_resource");
393 /// ```
394 pub fn with_resource<RS>(&self, resource: RS) -> FullJid
395 where
396 RS: Into<String>,
397 {
398 FullJid {
399 node: self.node.clone(),
400 domain: self.domain.clone(),
401 resource: resource.into(),
402 }
403 }
404}
405
406impl FromStr for BareJid {
407 type Err = JidParseError;
408
409 fn from_str(s: &str) -> Result<BareJid, JidParseError> {
410 let (ns, ds, _rs): StringJid = _from_str(s)?;
411 Ok(BareJid {
412 node: ns,
413 domain: ds,
414 })
415 }
416}
417
418impl BareJid {
419 /// Constructs a bare Jabber ID, containing two components.
420 ///
421 /// This is of the form `node`@`domain`.
422 ///
423 /// # Examples
424 ///
425 /// ```
426 /// use jid::BareJid;
427 ///
428 /// let jid = BareJid::new("node", "domain");
429 ///
430 /// assert_eq!(jid.node, Some("node".to_owned()));
431 /// assert_eq!(jid.domain, "domain".to_owned());
432 /// ```
433 pub fn new<NS, DS>(node: NS, domain: DS) -> BareJid
434 where
435 NS: Into<String>,
436 DS: Into<String>,
437 {
438 BareJid {
439 node: Some(node.into()),
440 domain: domain.into(),
441 }
442 }
443
444 /// Constructs a bare Jabber ID containing only a `domain`.
445 ///
446 /// This is of the form `domain`.
447 ///
448 /// # Examples
449 ///
450 /// ```
451 /// use jid::BareJid;
452 ///
453 /// let jid = BareJid::domain("domain");
454 ///
455 /// assert_eq!(jid.node, None);
456 /// assert_eq!(jid.domain, "domain".to_owned());
457 /// ```
458 pub fn domain<DS>(domain: DS) -> BareJid
459 where
460 DS: Into<String>,
461 {
462 BareJid {
463 node: None,
464 domain: domain.into(),
465 }
466 }
467
468 /// Constructs a new Jabber ID from an existing one, with the node swapped out with a new one.
469 ///
470 /// # Examples
471 ///
472 /// ```
473 /// use jid::BareJid;
474 ///
475 /// let jid = BareJid::domain("domain");
476 ///
477 /// assert_eq!(jid.node, None);
478 ///
479 /// let new_jid = jid.with_node("node");
480 ///
481 /// assert_eq!(new_jid.node, Some("node".to_owned()));
482 /// ```
483 pub fn with_node<NS>(&self, node: NS) -> BareJid
484 where
485 NS: Into<String>,
486 {
487 BareJid {
488 node: Some(node.into()),
489 domain: self.domain.clone(),
490 }
491 }
492
493 /// Constructs a new Jabber ID from an existing one, with the domain swapped out with a new one.
494 ///
495 /// # Examples
496 ///
497 /// ```
498 /// use jid::BareJid;
499 ///
500 /// let jid = BareJid::domain("domain");
501 ///
502 /// assert_eq!(jid.domain, "domain");
503 ///
504 /// let new_jid = jid.with_domain("new_domain");
505 ///
506 /// assert_eq!(new_jid.domain, "new_domain");
507 /// ```
508 pub fn with_domain<DS>(&self, domain: DS) -> BareJid
509 where
510 DS: Into<String>,
511 {
512 BareJid {
513 node: self.node.clone(),
514 domain: domain.into(),
515 }
516 }
517
518 /// Constructs a full Jabber ID from a bare Jabber ID, specifying a `resource`.
519 ///
520 /// # Examples
521 ///
522 /// ```
523 /// use jid::BareJid;
524 ///
525 /// let bare = BareJid::new("node", "domain");
526 /// let full = bare.with_resource("resource");
527 ///
528 /// assert_eq!(full.node, Some("node".to_owned()));
529 /// assert_eq!(full.domain, "domain".to_owned());
530 /// assert_eq!(full.resource, "resource".to_owned());
531 /// ```
532 pub fn with_resource<RS>(self, resource: RS) -> FullJid
533 where
534 RS: Into<String>,
535 {
536 FullJid {
537 node: self.node,
538 domain: self.domain,
539 resource: resource.into(),
540 }
541 }
542}
543
544#[cfg(feature = "minidom")]
545use minidom::{IntoAttributeValue, Node};
546
547#[cfg(feature = "minidom")]
548impl IntoAttributeValue for Jid {
549 fn into_attribute_value(self) -> Option<String> {
550 Some(String::from(self))
551 }
552}
553
554#[cfg(feature = "minidom")]
555impl Into<Node> for Jid {
556 fn into(self) -> Node {
557 Node::Text(String::from(self))
558 }
559}
560
561#[cfg(feature = "minidom")]
562impl IntoAttributeValue for FullJid {
563 fn into_attribute_value(self) -> Option<String> {
564 Some(String::from(self))
565 }
566}
567
568#[cfg(feature = "minidom")]
569impl Into<Node> for FullJid {
570 fn into(self) -> Node {
571 Node::Text(String::from(self))
572 }
573}
574
575#[cfg(feature = "minidom")]
576impl IntoAttributeValue for BareJid {
577 fn into_attribute_value(self) -> Option<String> {
578 Some(String::from(self))
579 }
580}
581
582#[cfg(feature = "minidom")]
583impl Into<Node> for BareJid {
584 fn into(self) -> Node {
585 Node::Text(String::from(self))
586 }
587}
588
589#[cfg(test)]
590mod tests {
591 use super::*;
592
593 use std::str::FromStr;
594
595 #[test]
596 fn can_parse_full_jids() {
597 assert_eq!(
598 FullJid::from_str("a@b.c/d"),
599 Ok(FullJid::new("a", "b.c", "d"))
600 );
601 assert_eq!(
602 FullJid::from_str("b.c/d"),
603 Ok(FullJid {
604 node: None,
605 domain: "b.c".to_owned(),
606 resource: "d".to_owned(),
607 })
608 );
609
610 assert_eq!(FullJid::from_str("a@b.c"), Err(JidParseError::NoResource));
611 assert_eq!(FullJid::from_str("b.c"), Err(JidParseError::NoResource));
612 }
613
614 #[test]
615 fn can_parse_bare_jids() {
616 assert_eq!(BareJid::from_str("a@b.c/d"), Ok(BareJid::new("a", "b.c")));
617 assert_eq!(
618 BareJid::from_str("b.c/d"),
619 Ok(BareJid {
620 node: None,
621 domain: "b.c".to_owned(),
622 })
623 );
624
625 assert_eq!(BareJid::from_str("a@b.c"), Ok(BareJid::new("a", "b.c")));
626 assert_eq!(
627 BareJid::from_str("b.c"),
628 Ok(BareJid {
629 node: None,
630 domain: "b.c".to_owned(),
631 })
632 );
633 }
634
635 #[test]
636 fn can_parse_jids() {
637 let full = FullJid::from_str("a@b.c/d").unwrap();
638 let bare = BareJid::from_str("e@f.g").unwrap();
639
640 assert_eq!(Jid::from_str("a@b.c/d"), Ok(Jid::Full(full)));
641 assert_eq!(Jid::from_str("e@f.g"), Ok(Jid::Bare(bare)));
642 }
643
644 #[test]
645 fn full_to_bare_jid() {
646 let bare: BareJid = FullJid::new("a", "b.c", "d").into();
647 assert_eq!(bare, BareJid::new("a", "b.c"));
648 }
649
650 #[test]
651 fn bare_to_full_jid() {
652 assert_eq!(
653 BareJid::new("a", "b.c").with_resource("d"),
654 FullJid::new("a", "b.c", "d")
655 );
656 }
657
658 #[test]
659 fn node_from_jid() {
660 assert_eq!(
661 Jid::Full(FullJid::new("a", "b.c", "d")).node(),
662 Some(String::from("a")),
663 );
664 }
665
666 #[test]
667 fn domain_from_jid() {
668 assert_eq!(
669 Jid::Bare(BareJid::new("a", "b.c")).domain(),
670 String::from("b.c"),
671 );
672 }
673
674 #[test]
675 fn serialise() {
676 assert_eq!(
677 String::from(FullJid::new("a", "b", "c")),
678 String::from("a@b/c")
679 );
680 assert_eq!(String::from(BareJid::new("a", "b")), String::from("a@b"));
681 }
682
683 #[test]
684 fn invalid_jids() {
685 assert_eq!(BareJid::from_str(""), Err(JidParseError::NoDomain));
686 assert_eq!(BareJid::from_str("/c"), Err(JidParseError::NoDomain));
687 assert_eq!(BareJid::from_str("a@/c"), Err(JidParseError::NoDomain));
688 assert_eq!(BareJid::from_str("@b"), Err(JidParseError::EmptyNode));
689 assert_eq!(BareJid::from_str("b/"), Err(JidParseError::EmptyResource));
690
691 assert_eq!(FullJid::from_str(""), Err(JidParseError::NoDomain));
692 assert_eq!(FullJid::from_str("/c"), Err(JidParseError::NoDomain));
693 assert_eq!(FullJid::from_str("a@/c"), Err(JidParseError::NoDomain));
694 assert_eq!(FullJid::from_str("@b"), Err(JidParseError::EmptyNode));
695 assert_eq!(FullJid::from_str("b/"), Err(JidParseError::EmptyResource));
696 assert_eq!(FullJid::from_str("a@b"), Err(JidParseError::NoResource));
697 }
698
699 #[cfg(feature = "minidom")]
700 #[test]
701 fn minidom() {
702 let elem: minidom::Element = "<message from='a@b/c'/>".parse().unwrap();
703 let to: Jid = elem.attr("from").unwrap().parse().unwrap();
704 assert_eq!(to, Jid::Full(FullJid::new("a", "b", "c")));
705
706 let elem: minidom::Element = "<message from='a@b'/>".parse().unwrap();
707 let to: Jid = elem.attr("from").unwrap().parse().unwrap();
708 assert_eq!(to, Jid::Bare(BareJid::new("a", "b")));
709
710 let elem: minidom::Element = "<message from='a@b/c'/>".parse().unwrap();
711 let to: FullJid = elem.attr("from").unwrap().parse().unwrap();
712 assert_eq!(to, FullJid::new("a", "b", "c"));
713
714 let elem: minidom::Element = "<message from='a@b'/>".parse().unwrap();
715 let to: BareJid = elem.attr("from").unwrap().parse().unwrap();
716 assert_eq!(to, BareJid::new("a", "b"));
717 }
718
719 #[cfg(feature = "minidom")]
720 #[test]
721 fn minidom_into_attr() {
722 let full = FullJid::new("a", "b", "c");
723 let elem = minidom::Element::builder("message")
724 .ns("jabber:client")
725 .attr("from", full.clone())
726 .build();
727 assert_eq!(elem.attr("from"), Some(String::from(full).as_ref()));
728
729 let bare = BareJid::new("a", "b");
730 let elem = minidom::Element::builder("message")
731 .ns("jabber:client")
732 .attr("from", bare.clone())
733 .build();
734 assert_eq!(elem.attr("from"), Some(String::from(bare.clone()).as_ref()));
735
736 let jid = Jid::Bare(bare.clone());
737 let _elem = minidom::Element::builder("message")
738 .ns("jabber:client")
739 .attr("from", jid)
740 .build();
741 assert_eq!(elem.attr("from"), Some(String::from(bare).as_ref()));
742 }
743}