diff --git a/irc-proto/src/lib.rs b/irc-proto/src/lib.rs index dd875e55..09e5bba6 100644 --- a/irc-proto/src/lib.rs +++ b/irc-proto/src/lib.rs @@ -23,6 +23,7 @@ pub mod irc; pub mod line; pub mod message; pub mod mode; +pub mod prefix; pub mod response; pub use self::caps::{Capability, NegotiationVersion}; @@ -33,4 +34,5 @@ pub use self::command::{BatchSubCommand, CapSubCommand, Command}; pub use self::irc::IrcCodec; pub use self::message::Message; pub use self::mode::{ChannelMode, Mode, UserMode}; +pub use self::prefix::Prefix; pub use self::response::Response; diff --git a/irc-proto/src/message.rs b/irc-proto/src/message.rs index 86f837c0..b2648d71 100644 --- a/irc-proto/src/message.rs +++ b/irc-proto/src/message.rs @@ -1,12 +1,14 @@ //! A module providing a data structure for messages to and from IRC servers. use std::borrow::ToOwned; -use std::fmt::{Display, Formatter, Result as FmtResult}; +use std::fmt::{Display, Formatter, Result as FmtResult, Write}; use std::str::FromStr; -use error; -use error::{ProtocolError, MessageParseError}; use chan::ChannelExt; use command::Command; +use error; +use error::{MessageParseError, ProtocolError}; +use prefix::Prefix; + /// A data structure representing an IRC message according to the protocol specification. It /// consists of a collection of IRCv3 tags, a prefix (describing the source of the message), and @@ -20,7 +22,7 @@ pub struct Message { /// in IRCv3 extensions to the IRC protocol. pub tags: Option>, /// The message prefix (or source) as defined by [RFC 2812](http://tools.ietf.org/html/rfc2812). - pub prefix: Option, + pub prefix: Option, /// The IRC command, parsed according to the known specifications. The command itself and its /// arguments (including the special suffix argument) are captured in this component. pub command: Command, @@ -60,7 +62,7 @@ impl Message { ) -> Result { Ok(Message { tags: tags, - prefix: prefix.map(|s| s.to_owned()), + prefix: prefix.map(|p| p.into()), command: Command::new(command, args, suffix)?, }) } @@ -81,15 +83,9 @@ impl Message { pub fn source_nickname(&self) -> Option<&str> { // ::= | [ '!' ] [ '@' ] // ::= - self.prefix.as_ref().and_then(|s| match ( - s.find('!'), - s.find('@'), - s.find('.'), - ) { - (Some(i), _, _) | // '!' [ '@' ] - (None, Some(i), _) => Some(&s[..i]), // '@' - (None, None, None) => Some(s), // - _ => None, // + self.prefix.as_ref().and_then(|p| match p { + Prefix::Nickname(name, _, _) => Some(&name[..]), + _ => None }) } @@ -149,9 +145,7 @@ impl Message { ret.push(' '); } if let Some(ref prefix) = self.prefix { - ret.push(':'); - ret.push_str(prefix); - ret.push(' '); + write!(ret, ":{} ", prefix).unwrap(); } let cmd: String = From::from(&self.command); ret.push_str(&cmd); @@ -362,7 +356,7 @@ mod test { assert_eq!(&message.to_string()[..], "PRIVMSG test :Testing!\r\n"); let message = Message { tags: None, - prefix: Some(format!("test!test@test")), + prefix: Some("test!test@test".into()), command: PRIVMSG(format!("test"), format!("Still testing!")), }; assert_eq!( @@ -384,7 +378,7 @@ mod test { ); let message = Message { tags: None, - prefix: Some(format!("test!test@test")), + prefix: Some("test!test@test".into()), command: PRIVMSG(format!("test"), format!("Still testing!")), }; assert_eq!( @@ -399,7 +393,7 @@ mod test { Tag(format!("ccc"), None), Tag(format!("example.com/ddd"), Some(format!("eee"))), ]), - prefix: Some(format!("test!test@test")), + prefix: Some("test!test@test".into()), command: PRIVMSG(format!("test"), format!("Testing with tags!")), }; assert_eq!( @@ -450,7 +444,7 @@ mod test { assert_eq!(msg, message); let message = Message { tags: None, - prefix: Some(format!("test!test@test")), + prefix: Some("test!test@test".into()), command: PRIVMSG(format!("test"), format!("Still testing!")), }; let msg: Message = ":test!test@test PRIVMSG test :Still testing!\r\n".into(); @@ -463,7 +457,7 @@ mod test { // colons within individual parameters. So, let's make sure it parses correctly. let message = Message { tags: None, - prefix: Some(format!("test!test@test")), + prefix: Some("test!test@test".into()), command: Raw( format!("COMMAND"), vec![format!("ARG:test")], diff --git a/irc-proto/src/prefix.rs b/irc-proto/src/prefix.rs new file mode 100644 index 00000000..9de076e5 --- /dev/null +++ b/irc-proto/src/prefix.rs @@ -0,0 +1,209 @@ +//! A module providing an enum for a message prefix. +use std::str::FromStr; +use std::fmt; + +/// The Prefix indicates "the true origin of the message", according to the server. +#[derive(Clone, Eq, PartialEq, Debug)] +pub enum Prefix { + /// servername, e.g. collins.mozilla.org + ServerName(String), + /// nickname [ ["!" username] "@" hostname ] + /// i.e. Nickname(nickname, username, hostname) + /// Any of the strings may be "" + Nickname(String, String, String), +} + +impl Prefix { + /// Creates a prefix by parsing a string. + /// + /// # Example + /// ``` + /// # extern crate irc_proto; + /// # use irc_proto::Prefix; + /// # fn main() { + /// Prefix::new_from_str("nickname!username@hostname"); + /// Prefix::new_from_str("example.com"); + /// # } + /// ``` + pub fn new_from_str(s: &str) -> Prefix { + #[derive(Copy, Clone, Eq, PartialEq)] + enum Active { + Name, + User, + Host, + } + + let mut name = String::new(); + let mut user = String::new(); + let mut host = String::new(); + let mut active = Active::Name; + let mut is_server = false; + + for c in s.chars() { + if c == '.' && active == Active::Name { + // We won't return Nickname("nick", "", "") but if @ or ! are + // encountered, then we set this back to false + is_server = true; + } + + match c { + '!' if active == Active::Name => { + is_server = false; + active = Active::User; + }, + + '@' if active != Active::Host => { + is_server = false; + active = Active::Host; + }, + + _ => { + // Push onto the active buffer + match active { + Active::Name => &mut name, + Active::User => &mut user, + Active::Host => &mut host, + }.push(c) + } + } + } + + if is_server { + Prefix::ServerName(name) + } else { + Prefix::Nickname(name, user, host) + } + } +} + +/// This implementation never returns an error and is isomorphic with `Display`. +impl FromStr for Prefix { + type Err = (); + + fn from_str(s: &str) -> Result { + Ok(Prefix::new_from_str(s)) + } +} + +/// This is isomorphic with `FromStr` +impl fmt::Display for Prefix { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Prefix::ServerName(name) => write!(f, "{}", name), + Prefix::Nickname(name, user, host) => match (&name[..], &user[..], &host[..]) { + ("", "", "") => write!(f, ""), + (name, "", "") => write!(f, "{}", name), + (name, user, "") => write!(f, "{}!{}", name, user), + (name, "", host) => write!(f, "{}@{}", name, host), + (name, user, host) => write!(f, "{}!{}@{}", name, user, host), + }, + } + } +} + +impl<'a> From<&'a str> for Prefix { + fn from(s: &str) -> Self { + Prefix::new_from_str(s) + } +} + +#[cfg(test)] +mod test { + use super::Prefix::{self, ServerName, Nickname}; + + // Checks that str -> parsed -> Display doesn't lose data + fn test_parse(s: &str) -> Prefix { + let prefix = Prefix::new_from_str(s); + let s2 = format!("{}", prefix); + assert_eq!(s, &s2); + prefix + } + + #[test] + fn print() { + let s = format!("{}", Nickname("nick".into(), "".into(), "".into())); + assert_eq!(&s, "nick"); + let s = format!("{}", Nickname("nick".into(), "user".into(), "".into())); + assert_eq!(&s, "nick!user"); + let s = format!("{}", Nickname("nick".into(), "user".into(), "host".into())); + assert_eq!(&s, "nick!user@host"); + } + + #[test] + fn parse_word() { + assert_eq!( + test_parse("only_nick"), + Nickname("only_nick".into(), String::new(), String::new()) + ) + } + + #[test] + fn parse_host() { + assert_eq!( + test_parse("host.tld"), + ServerName("host.tld".into()) + ) + } + + #[test] + fn parse_nick_user() { + assert_eq!( + test_parse("test!nick"), + Nickname("test".into(), "nick".into(), String::new()) + ) + } + + #[test] + fn parse_nick_user_host() { + assert_eq!( + test_parse("test!nick@host"), + Nickname("test".into(), "nick".into(), "host".into()) + ) + } + + #[test] + fn parse_dot_and_symbols() { + assert_eq!( + test_parse("test.net@something"), + Nickname("test.net".into(), "".into(), "something".into()) + ) + } + + #[test] + fn parse_danger_cases() { + assert_eq!( + test_parse("name@name!user"), + Nickname("name".into(), "".into(), "name!user".into()) + ); + assert_eq!( + // can't reverse the parse + "name!@".parse::().unwrap(), + Nickname("name".into(), "".into(), "".into()) + ); + assert_eq!( + // can't reverse the parse + "name!@hostname".parse::().unwrap(), + Nickname("name".into(), "".into(), "hostname".into()) + ); + assert_eq!( + test_parse("name!.user"), + Nickname("name".into(), ".user".into(), "".into()) + ); + assert_eq!( + test_parse("name!user.user"), + Nickname("name".into(), "user.user".into(), "".into()) + ); + assert_eq!( + test_parse("name!user@host.host"), + Nickname("name".into(), "user".into(), "host.host".into()) + ); + assert_eq!( + test_parse("!user"), + Nickname("".into(), "user".into(), "".into()) + ); + assert_eq!( + "!@host.host".parse::().unwrap(), + Nickname("".into(), "".into(), "host.host".into()) + ); + } +} diff --git a/src/client/prelude.rs b/src/client/prelude.rs index ef92e0cc..bda4efa8 100644 --- a/src/client/prelude.rs +++ b/src/client/prelude.rs @@ -25,7 +25,7 @@ pub use client::data::Config; pub use client::reactor::IrcReactor; pub use client::{EachIncomingExt, IrcClient, Client}; pub use client::ext::ClientExt; -pub use proto::{Capability, ChannelExt, Command, Message, NegotiationVersion, Response}; +pub use proto::{Capability, ChannelExt, Command, Message, Prefix, NegotiationVersion, Response}; pub use proto::{ChannelMode, Mode, UserMode}; pub use futures::{Future, Stream};