1use std::borrow::ToOwned;
3use std::fmt::{Display, Formatter, Result as FmtResult, Write};
4use std::str::FromStr;
5
6use crate::chan::ChannelExt;
7use crate::command::Command;
8use crate::error;
9use crate::error::{MessageParseError, ProtocolError};
10use crate::prefix::Prefix;
11
12#[derive(Clone, PartialEq, Debug)]
18pub struct Message {
19 pub tags: Option<Vec<Tag>>,
23 pub prefix: Option<Prefix>,
25 pub command: Command,
28}
29
30impl Message {
31 pub fn new(
44 prefix: Option<&str>,
45 command: &str,
46 args: Vec<&str>,
47 ) -> Result<Message, MessageParseError> {
48 Message::with_tags(None, prefix, command, args)
49 }
50
51 pub fn with_tags(
55 tags: Option<Vec<Tag>>,
56 prefix: Option<&str>,
57 command: &str,
58 args: Vec<&str>,
59 ) -> Result<Message, error::MessageParseError> {
60 Ok(Message {
61 tags,
62 prefix: prefix.map(|p| p.into()),
63 command: Command::new(command, args)?,
64 })
65 }
66
67 pub fn source_nickname(&self) -> Option<&str> {
81 self.prefix.as_ref().and_then(|p| match p {
84 Prefix::Nickname(name, _, _) => Some(&name[..]),
85 _ => None,
86 })
87 }
88
89 pub fn response_target(&self) -> Option<&str> {
109 match self.command {
110 Command::PRIVMSG(ref target, _) if target.is_channel_name() => Some(target),
111 Command::NOTICE(ref target, _) if target.is_channel_name() => Some(target),
112 _ => self.source_nickname(),
113 }
114 }
115}
116
117impl From<Command> for Message {
118 fn from(cmd: Command) -> Message {
119 Message {
120 tags: None,
121 prefix: None,
122 command: cmd,
123 }
124 }
125}
126
127impl FromStr for Message {
128 type Err = ProtocolError;
129
130 fn from_str(s: &str) -> Result<Message, Self::Err> {
131 if s.is_empty() {
132 return Err(ProtocolError::InvalidMessage {
133 string: s.to_owned(),
134 cause: MessageParseError::EmptyMessage,
135 });
136 }
137
138 let mut state = s;
139
140 let tags = if state.starts_with('@') {
141 let tags = state.find(' ').map(|i| &state[1..i]);
142 state = state.find(' ').map_or("", |i| &state[i + 1..]);
143 tags.map(|ts| {
144 ts.split(';')
145 .filter(|s| !s.is_empty())
146 .map(|s: &str| {
147 let mut iter = s.splitn(2, '=');
148 let (fst, snd) = (iter.next(), iter.next());
149 let snd = snd.map(unescape_tag_value);
150 Tag(fst.unwrap_or("").to_owned(), snd)
151 })
152 .collect::<Vec<_>>()
153 })
154 } else {
155 None
156 };
157
158 let prefix = if state.starts_with(':') {
159 let prefix = state.find(' ').map(|i| &state[1..i]);
160 state = state.find(' ').map_or("", |i| &state[i + 1..]);
161 prefix
162 } else {
163 None
164 };
165
166 let line_ending_len = if state.ends_with("\r\n") {
167 "\r\n"
168 } else if state.ends_with('\r') {
169 "\r"
170 } else if state.ends_with('\n') {
171 "\n"
172 } else {
173 ""
174 }
175 .len();
176
177 let suffix = if state.contains(" :") {
178 let suffix = state
179 .find(" :")
180 .map(|i| &state[i + 2..state.len() - line_ending_len]);
181 state = state.find(" :").map_or("", |i| &state[..i + 1]);
182 suffix
183 } else {
184 state = &state[..state.len() - line_ending_len];
185 None
186 };
187
188 let command = match state.find(' ').map(|i| &state[..i]) {
189 Some(cmd) => {
190 state = state.find(' ').map_or("", |i| &state[i + 1..]);
191 cmd
192 }
193 None if state.starts_with(':') => {
195 return Err(ProtocolError::InvalidMessage {
196 string: s.to_owned(),
197 cause: MessageParseError::InvalidCommand,
198 })
199 }
200 None => {
202 let cmd = state;
203 state = "";
204 cmd
205 }
206 };
207
208 let mut args: Vec<_> = state.splitn(14, ' ').filter(|s| !s.is_empty()).collect();
209 if let Some(suffix) = suffix {
210 args.push(suffix);
211 }
212
213 Message::with_tags(tags, prefix, command, args).map_err(|e| ProtocolError::InvalidMessage {
214 string: s.to_owned(),
215 cause: e,
216 })
217 }
218}
219
220impl<'a> From<&'a str> for Message {
221 fn from(s: &'a str) -> Message {
222 s.parse().unwrap()
223 }
224}
225
226impl Display for Message {
227 fn fmt(&self, f: &mut Formatter) -> FmtResult {
241 if let Some(ref tags) = self.tags {
242 f.write_char('@')?;
243 for (i, tag) in tags.iter().enumerate() {
244 if i > 0 {
245 f.write_char(';')?;
246 }
247 f.write_str(&tag.0)?;
248 if let Some(ref value) = tag.1 {
249 f.write_char('=')?;
250 escape_tag_value(f, value)?;
251 }
252 }
253 f.write_char(' ')?;
254 }
255 if let Some(ref prefix) = self.prefix {
256 write!(f, ":{} ", prefix)?
257 }
258 write!(f, "{}\r\n", String::from(&self.command))
259 }
260}
261
262#[derive(Clone, PartialEq, Debug)]
267pub struct Tag(pub String, pub Option<String>);
268
269fn escape_tag_value(f: &mut dyn Write, value: &str) -> FmtResult {
270 for c in value.chars() {
271 match c {
272 ';' => f.write_str("\\:")?,
273 ' ' => f.write_str("\\s")?,
274 '\\' => f.write_str("\\\\")?,
275 '\r' => f.write_str("\\r")?,
276 '\n' => f.write_str("\\n")?,
277 c => f.write_char(c)?,
278 }
279 }
280 Ok(())
281}
282
283fn unescape_tag_value(value: &str) -> String {
284 let mut unescaped = String::with_capacity(value.len());
285 let mut iter = value.chars();
286 while let Some(c) = iter.next() {
287 let r = if c == '\\' {
288 match iter.next() {
289 Some(':') => ';',
290 Some('s') => ' ',
291 Some('\\') => '\\',
292 Some('r') => '\r',
293 Some('n') => '\n',
294 Some(c) => c,
295 None => break,
296 }
297 } else {
298 c
299 };
300 unescaped.push(r);
301 }
302 unescaped
303}
304
305#[cfg(test)]
306mod test {
307 use super::{Message, Tag};
308 use crate::command::Command::{Raw, PRIVMSG, QUIT};
309
310 #[test]
311 fn new() {
312 let message = Message {
313 tags: None,
314 prefix: None,
315 command: PRIVMSG(format!("test"), format!("Testing!")),
316 };
317 assert_eq!(
318 Message::new(None, "PRIVMSG", vec!["test", "Testing!"]).unwrap(),
319 message
320 )
321 }
322
323 #[test]
324 fn source_nickname() {
325 assert_eq!(
326 Message::new(None, "PING", vec!["data"])
327 .unwrap()
328 .source_nickname(),
329 None
330 );
331
332 assert_eq!(
333 Message::new(Some("irc.test.net"), "PING", vec!["data"])
334 .unwrap()
335 .source_nickname(),
336 None
337 );
338
339 assert_eq!(
340 Message::new(Some("test!test@test"), "PING", vec!["data"])
341 .unwrap()
342 .source_nickname(),
343 Some("test")
344 );
345
346 assert_eq!(
347 Message::new(Some("test@test"), "PING", vec!["data"])
348 .unwrap()
349 .source_nickname(),
350 Some("test")
351 );
352
353 assert_eq!(
354 Message::new(Some("test!test@irc.test.com"), "PING", vec!["data"])
355 .unwrap()
356 .source_nickname(),
357 Some("test")
358 );
359
360 assert_eq!(
361 Message::new(Some("test!test@127.0.0.1"), "PING", vec!["data"])
362 .unwrap()
363 .source_nickname(),
364 Some("test")
365 );
366
367 assert_eq!(
368 Message::new(Some("test@test.com"), "PING", vec!["data"])
369 .unwrap()
370 .source_nickname(),
371 Some("test")
372 );
373
374 assert_eq!(
375 Message::new(Some("test"), "PING", vec!["data"])
376 .unwrap()
377 .source_nickname(),
378 Some("test")
379 );
380 }
381
382 #[test]
383 fn to_string() {
384 let message = Message {
385 tags: None,
386 prefix: None,
387 command: PRIVMSG(format!("test"), format!("Testing!")),
388 };
389 assert_eq!(&message.to_string()[..], "PRIVMSG test Testing!\r\n");
390 let message = Message {
391 tags: None,
392 prefix: Some("test!test@test".into()),
393 command: PRIVMSG(format!("test"), format!("Still testing!")),
394 };
395 assert_eq!(
396 &message.to_string()[..],
397 ":test!test@test PRIVMSG test :Still testing!\r\n"
398 );
399 }
400
401 #[test]
402 fn from_string() {
403 let message = Message {
404 tags: None,
405 prefix: None,
406 command: PRIVMSG(format!("test"), format!("Testing!")),
407 };
408 assert_eq!(
409 "PRIVMSG test :Testing!\r\n".parse::<Message>().unwrap(),
410 message
411 );
412 let message = Message {
413 tags: None,
414 prefix: Some("test!test@test".into()),
415 command: PRIVMSG(format!("test"), format!("Still testing!")),
416 };
417 assert_eq!(
418 ":test!test@test PRIVMSG test :Still testing!\r\n"
419 .parse::<Message>()
420 .unwrap(),
421 message
422 );
423 let message = Message {
424 tags: Some(vec![
425 Tag(format!("aaa"), Some(format!("bbb"))),
426 Tag(format!("ccc"), None),
427 Tag(format!("example.com/ddd"), Some(format!("eee"))),
428 ]),
429 prefix: Some("test!test@test".into()),
430 command: PRIVMSG(format!("test"), format!("Testing with tags!")),
431 };
432 assert_eq!(
433 "@aaa=bbb;ccc;example.com/ddd=eee :test!test@test PRIVMSG test :Testing with \
434 tags!\r\n"
435 .parse::<Message>()
436 .unwrap(),
437 message
438 )
439 }
440
441 #[test]
442 fn from_string_atypical_endings() {
443 let message = Message {
444 tags: None,
445 prefix: None,
446 command: PRIVMSG(format!("test"), format!("Testing!")),
447 };
448 assert_eq!(
449 "PRIVMSG test :Testing!\r".parse::<Message>().unwrap(),
450 message
451 );
452 assert_eq!(
453 "PRIVMSG test :Testing!\n".parse::<Message>().unwrap(),
454 message
455 );
456 assert_eq!(
457 "PRIVMSG test :Testing!".parse::<Message>().unwrap(),
458 message
459 );
460 }
461
462 #[test]
463 fn from_and_to_string() {
464 let message =
465 "@aaa=bbb;ccc;example.com/ddd=eee :test!test@test PRIVMSG test :Testing with \
466 tags!\r\n";
467 assert_eq!(message.parse::<Message>().unwrap().to_string(), message);
468 }
469
470 #[test]
471 fn to_message() {
472 let message = Message {
473 tags: None,
474 prefix: None,
475 command: PRIVMSG(format!("test"), format!("Testing!")),
476 };
477 let msg: Message = "PRIVMSG test :Testing!\r\n".into();
478 assert_eq!(msg, message);
479 let message = Message {
480 tags: None,
481 prefix: Some("test!test@test".into()),
482 command: PRIVMSG(format!("test"), format!("Still testing!")),
483 };
484 let msg: Message = ":test!test@test PRIVMSG test :Still testing!\r\n".into();
485 assert_eq!(msg, message);
486 }
487
488 #[test]
489 fn to_message_with_colon_in_arg() {
490 let message = Message {
493 tags: None,
494 prefix: Some("test!test@test".into()),
495 command: Raw(
496 format!("COMMAND"),
497 vec![format!("ARG:test"), format!("Testing!")],
498 ),
499 };
500 let msg: Message = ":test!test@test COMMAND ARG:test :Testing!\r\n".into();
501 assert_eq!(msg, message);
502 }
503
504 #[test]
505 fn to_message_no_prefix_no_args() {
506 let message = Message {
507 tags: None,
508 prefix: None,
509 command: QUIT(None),
510 };
511 let msg: Message = "QUIT\r\n".into();
512 assert_eq!(msg, message);
513 }
514
515 #[test]
516 #[should_panic]
517 fn to_message_invalid_format() {
518 let _: Message = ":invalid :message".into();
519 }
520
521 #[test]
522 fn to_message_tags_escapes() {
523 let msg = "@tag=\\:\\s\\\\\\r\\n\\a\\ :test PRIVMSG #test :test\r\n"
524 .parse::<Message>()
525 .unwrap();
526 let message = Message {
527 tags: Some(vec![Tag("tag".to_string(), Some("; \\\r\na".to_string()))]),
528 prefix: Some("test".into()),
529 command: PRIVMSG("#test".to_string(), "test".to_string()),
530 };
531 assert_eq!(msg, message);
532 }
533
534 #[test]
535 fn to_string_tags_escapes() {
536 let msg = Message {
537 tags: Some(vec![Tag("tag".to_string(), Some("; \\\r\na".to_string()))]),
538 prefix: Some("test".into()),
539 command: PRIVMSG("#test".to_string(), "test".to_string()),
540 }
541 .to_string();
542 let message = "@tag=\\:\\s\\\\\\r\\na :test PRIVMSG #test test\r\n";
543 assert_eq!(msg, message);
544 }
545
546 #[test]
547 fn to_message_with_colon_in_suffix() {
548 let msg = "PRIVMSG #test ::test".parse::<Message>().unwrap();
549 let message = Message {
550 tags: None,
551 prefix: None,
552 command: PRIVMSG("#test".to_string(), ":test".to_string()),
553 };
554 assert_eq!(msg, message);
555 }
556
557 #[test]
558 fn to_string_with_colon_in_suffix() {
559 let msg = Message {
560 tags: None,
561 prefix: None,
562 command: PRIVMSG("#test".to_string(), ":test".to_string()),
563 }
564 .to_string();
565 let message = "PRIVMSG #test ::test\r\n";
566 assert_eq!(msg, message);
567 }
568}