simple_irc/
message.rs

1use std::collections::BTreeMap;
2use std::fmt;
3use std::fmt::Write;
4use std::option::Option;
5use std::str::FromStr;
6
7use super::error::Error;
8
9use crate::escaped::{escape_char, unescape_char};
10
11/// Representation of a parsed message source. This is in the format
12/// nick [ [ "!" user ] "@" host ].
13///
14/// ```
15/// let prefix = "nick!user@host".parse::<simple_irc::Prefix>().unwrap();
16/// assert_eq!(&prefix.nick[..], "nick");
17/// assert_eq!(prefix.user.as_deref(), Some("user"));
18/// assert_eq!(prefix.host.as_deref(), Some("host"));
19/// ```
20#[derive(Debug, PartialEq, Default, Clone)]
21pub struct Prefix {
22    pub nick: String,
23    pub user: Option<String>,
24    pub host: Option<String>,
25}
26
27impl Prefix {
28    pub fn new(nick: &str) -> Self {
29        Self::new_with_all(nick, None, None)
30    }
31
32    pub fn new_with_all(nick: &str, user: Option<&str>, host: Option<&str>) -> Self {
33        Prefix {
34            nick: nick.to_string(),
35            user: user.map(|s| s.to_string()),
36            host: host.map(|s| s.to_string()),
37        }
38    }
39}
40
41impl FromStr for Prefix {
42    type Err = Error;
43
44    // nickname [ [ "!" user ] "@" host ]
45    fn from_str(input: &str) -> Result<Self, Self::Err> {
46        let mut parts = input.splitn(2, '@');
47
48        // Split on host first
49        let rest = parts.next().unwrap_or("");
50        let host = parts.next();
51
52        let mut parts = rest.splitn(2, '!');
53        let nick = parts.next().unwrap_or("").to_string();
54        let user = parts.next();
55
56        Ok(Prefix {
57            nick,
58            user: user.and_then(|s| if s == "" { None } else { Some(s.to_string()) }),
59            host: host.and_then(|s| if s == "" { None } else { Some(s.to_string()) }),
60        })
61    }
62}
63
64impl fmt::Display for Prefix {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        f.write_str(&self.nick)?;
67
68        if let Some(user) = self.user.as_ref() {
69            f.write_char('!')?;
70            f.write_str(&user[..])?;
71        }
72
73        if let Some(host) = self.host.as_ref() {
74            f.write_char('@')?;
75            f.write_str(&host[..])?;
76        }
77
78        Ok(())
79    }
80}
81
82/// A structural representation of an IRC message.
83///
84/// Note that this library does not guarantee that messages will be preserved
85/// byte-for-byte, but all messages will have the same semantic meaning.
86///
87/// ```
88/// // use std::convert::FromStr;
89/// let msg = "HELLO world".parse::<simple_irc::Message>().unwrap();
90/// assert_eq!(msg.tags.len(), 0);
91/// assert_eq!(msg.prefix, None);
92/// assert_eq!(&msg.command[..], "HELLO");
93/// assert_eq!(msg.params.len(), 1);
94/// assert_eq!(&msg.params[0][..], "world");
95///
96/// assert_eq!(&msg.to_string(), "HELLO :world");
97/// ```
98#[derive(Debug, PartialEq, Default, Clone)]
99pub struct Message {
100    pub tags: BTreeMap<String, String>,
101    pub prefix: Option<Prefix>,
102    pub command: String,
103    pub params: Vec<String>,
104}
105
106impl Message {
107    pub fn new(command: String, params: Vec<String>) -> Self {
108        Message {
109            command,
110            params,
111            ..Default::default()
112        }
113    }
114
115    pub fn new_with_all(
116        tags: BTreeMap<String, String>,
117        prefix: Option<Prefix>,
118        command: String,
119        params: Vec<String>,
120    ) -> Self {
121        Message {
122            tags,
123            prefix,
124            command,
125            params,
126        }
127    }
128
129    pub fn new_with_prefix(command: String, params: Vec<String>, prefix: Prefix) -> Self {
130        Message {
131            prefix: Some(prefix),
132            command,
133            params,
134            ..Default::default()
135        }
136    }
137}
138
139fn parse_tags(input: &str) -> Result<BTreeMap<String, String>, Error> {
140    let mut tags = BTreeMap::new();
141
142    for tag_data in input.split(';') {
143        let mut pieces = tag_data.splitn(2, '=');
144        let tag_name = pieces
145            .next()
146            .ok_or_else(|| Error::TagError("missing tag name".to_string()))?;
147        let raw_tag_value = pieces.next().unwrap_or("");
148
149        let mut tag_value = String::new();
150        let mut tag_value_chars = raw_tag_value.chars();
151        while let Some(c) = tag_value_chars.next() {
152            if c == '\\' {
153                if let Some(escaped_char) = tag_value_chars.next() {
154                    tag_value.push(unescape_char(escaped_char));
155                }
156            } else {
157                tag_value.push(c);
158            }
159        }
160
161        tags.insert(tag_name.to_string(), tag_value);
162    }
163
164    Ok(tags)
165}
166
167impl FromStr for Message {
168    type Err = Error;
169
170    fn from_str(input: &str) -> Result<Self, Self::Err> {
171        // We want a mutable input so we can jump through it as we parse the
172        // message. Note that this shadows the input param on purpose so it
173        // cannot accidentally be used later.
174        let mut input = input;
175
176        // Possibly chop off the ending \r\n where either of those characters is
177        // optional.
178        if input.ends_with('\n') {
179            input = &input[..input.len() - 1];
180        }
181        if input.ends_with('\r') {
182            input = &input[..input.len() - 1];
183        }
184
185        let mut tags = BTreeMap::new();
186        let mut prefix = None;
187
188        if input.starts_with('@') {
189            let mut parts = (&input[1..]).splitn(2, ' ');
190            let tag_data = parts
191                .next()
192                .ok_or_else(|| Error::TagError("missing tag data".to_string()))?;
193
194            tags = parse_tags(tag_data)?;
195
196            // Either advance to the next token, or return an empty string.
197            input = parts.next().unwrap_or("").trim_start_matches(' ');
198        }
199
200        if input.starts_with(':') {
201            let mut parts = (&input[1..]).splitn(2, ' ');
202            prefix = Some(
203                parts
204                    .next()
205                    .ok_or_else(|| Error::TagError("missing prefix data".to_string()))?
206                    .parse()
207                    .or_else(|_| Err(Error::TagError("failed to parse prefix data".to_string())))?,
208            );
209
210            // Either advance to the next token, or return an empty string.
211            input = parts.next().unwrap_or("").trim_start_matches(' ');
212        }
213
214        let mut parts = input.splitn(2, ' ');
215        let command = parts
216            .next()
217            .ok_or_else(|| Error::CommandError("missing command".to_string()))?
218            .to_string();
219
220        // Either advance to the next token, or return an empty string.
221        input = parts.next().unwrap_or("").trim_start_matches(' ');
222
223        // Parse out the params
224        let mut params = Vec::new();
225        while !input.is_empty() {
226            // Special case - if the param starts with a :, it's a trailing
227            // param, so we need to include the rest of the input as the param.
228            if input.starts_with(':') {
229                params.push(input[1..].to_string());
230                break;
231            }
232
233            let mut parts = input.splitn(2, ' ');
234            if let Some(param) = parts.next() {
235                params.push(param.to_string());
236            }
237
238            // Either advance to the next token, or return an empty string.
239            input = parts.next().unwrap_or("").trim_start_matches(' ');
240        }
241
242        Ok(Message {
243            tags,
244            prefix,
245            command,
246            params,
247        })
248    }
249}
250
251impl fmt::Display for Message {
252    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
253        if !self.tags.is_empty() {
254            f.write_char('@')?;
255
256            for (i, (k, v)) in self.tags.iter().enumerate() {
257                // We need to insert a separator for everything other than the
258                // first value.
259                if i != 0 {
260                    f.write_char(';')?;
261                }
262
263                f.write_str(k)?;
264                if v.is_empty() {
265                    continue;
266                }
267
268                f.write_char('=')?;
269
270                for c in v.chars() {
271                    match escape_char(c) {
272                        Some(escaped_str) => f.write_str(escaped_str)?,
273                        None => f.write_char(c)?,
274                    }
275                }
276            }
277
278            f.write_char(' ')?;
279        }
280
281        if let Some(prefix) = &self.prefix {
282            f.write_char(':')?;
283            prefix.fmt(f)?;
284            f.write_char(' ')?;
285        }
286
287        f.write_str(&self.command)?;
288
289        if let Some((last, params)) = self.params.split_last() {
290            for param in params {
291                f.write_char(' ')?;
292                f.write_str(param)?;
293            }
294
295            f.write_str(" :")?;
296            f.write_str(last)?;
297        }
298
299        Ok(())
300    }
301}