From 2bb980044b2ca79945043b190821ebbb5016a5bc Mon Sep 17 00:00:00 2001 From: Andreas Ots Date: Sat, 28 Dec 2019 17:37:47 +0200 Subject: [PATCH 1/2] Escape/unescape IRCv3 message tag values --- irc-proto/src/message.rs | 58 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/irc-proto/src/message.rs b/irc-proto/src/message.rs index 8fbb544..9a94619 100644 --- a/irc-proto/src/message.rs +++ b/irc-proto/src/message.rs @@ -136,7 +136,16 @@ impl Message { ret.push_str(&tag.0); if let Some(ref value) = tag.1 { ret.push('='); - ret.push_str(value); + for c in value.chars() { + match c { + ';' => ret.push_str("\\:"), + ' ' => ret.push_str("\\s"), + '\\' => ret.push_str("\\\\"), + '\r' => ret.push_str("\\r"), + '\n' => ret.push_str("\\n"), + c => ret.push(c), + } + } } ret.push(';'); } @@ -185,7 +194,27 @@ impl FromStr for Message { .map(|s: &str| { let mut iter = s.splitn(2, '='); let (fst, snd) = (iter.next(), iter.next()); - Tag(fst.unwrap_or("").to_owned(), snd.map(|s| s.to_owned())) + let snd = snd.map(|snd| { + let mut unescaped = String::with_capacity(snd.len()); + let mut iter = snd.chars(); + while let Some(c) = iter.next() { + if c == '\\' { + match iter.next() { + Some(':') => unescaped.push(';'), + Some('s') => unescaped.push(' '), + Some('\\') => unescaped.push('\\'), + Some('r') => unescaped.push('\r'), + Some('n') => unescaped.push('\n'), + Some(c) => unescaped.push(c), + None => break, + } + } else { + unescaped.push(c); + } + } + unescaped + }); + Tag(fst.unwrap_or("").to_owned(), snd) }) .collect::>() }) @@ -489,4 +518,29 @@ mod test { fn to_message_invalid_format() { let _: Message = ":invalid :message".into(); } + + #[test] + fn to_message_tags_escapes() { + let msg = "@tag=\\:\\s\\\\\\r\\n\\a\\ :test PRIVMSG #test :test\r\n" + .parse::() + .unwrap(); + let message = Message { + tags: Some(vec![Tag("tag".to_string(), Some("; \\\r\na".to_string()))]), + prefix: Some("test".into()), + command: PRIVMSG("#test".to_string(), "test".to_string()), + }; + assert_eq!(msg, message); + } + + #[test] + fn to_string_tags_escapes() { + let msg = Message { + tags: Some(vec![Tag("tag".to_string(), Some("; \\\r\na".to_string()))]), + prefix: Some("test".into()), + command: PRIVMSG("#test".to_string(), "test".to_string()), + } + .to_string(); + let message = "@tag=\\:\\s\\\\\\r\\na :test PRIVMSG #test :test\r\n"; + assert_eq!(msg, message); + } } From b903c2595e286a2d9a7b616a006398d3551ea87d Mon Sep 17 00:00:00 2001 From: Andreas Ots Date: Thu, 30 Jan 2020 23:39:47 +0200 Subject: [PATCH 2/2] extract tag value escape and unescape functions --- irc-proto/src/message.rs | 66 ++++++++++++++++++++++------------------ 1 file changed, 36 insertions(+), 30 deletions(-) diff --git a/irc-proto/src/message.rs b/irc-proto/src/message.rs index 9a94619..5685908 100644 --- a/irc-proto/src/message.rs +++ b/irc-proto/src/message.rs @@ -136,16 +136,7 @@ impl Message { ret.push_str(&tag.0); if let Some(ref value) = tag.1 { ret.push('='); - for c in value.chars() { - match c { - ';' => ret.push_str("\\:"), - ' ' => ret.push_str("\\s"), - '\\' => ret.push_str("\\\\"), - '\r' => ret.push_str("\\r"), - '\n' => ret.push_str("\\n"), - c => ret.push(c), - } - } + escape_tag_value(&mut ret, &value); } ret.push(';'); } @@ -194,26 +185,7 @@ impl FromStr for Message { .map(|s: &str| { let mut iter = s.splitn(2, '='); let (fst, snd) = (iter.next(), iter.next()); - let snd = snd.map(|snd| { - let mut unescaped = String::with_capacity(snd.len()); - let mut iter = snd.chars(); - while let Some(c) = iter.next() { - if c == '\\' { - match iter.next() { - Some(':') => unescaped.push(';'), - Some('s') => unescaped.push(' '), - Some('\\') => unescaped.push('\\'), - Some('r') => unescaped.push('\r'), - Some('n') => unescaped.push('\n'), - Some(c) => unescaped.push(c), - None => break, - } - } else { - unescaped.push(c); - } - } - unescaped - }); + let snd = snd.map(unescape_tag_value); Tag(fst.unwrap_or("").to_owned(), snd) }) .collect::>() @@ -302,6 +274,40 @@ impl Display for Message { #[derive(Clone, PartialEq, Debug)] pub struct Tag(pub String, pub Option); +fn escape_tag_value(msg: &mut String, value: &str) { + for c in value.chars() { + match c { + ';' => msg.push_str("\\:"), + ' ' => msg.push_str("\\s"), + '\\' => msg.push_str("\\\\"), + '\r' => msg.push_str("\\r"), + '\n' => msg.push_str("\\n"), + c => msg.push(c), + } + } +} + +fn unescape_tag_value(value: &str) -> String { + let mut unescaped = String::with_capacity(value.len()); + let mut iter = value.chars(); + while let Some(c) = iter.next() { + if c == '\\' { + match iter.next() { + Some(':') => unescaped.push(';'), + Some('s') => unescaped.push(' '), + Some('\\') => unescaped.push('\\'), + Some('r') => unescaped.push('\r'), + Some('n') => unescaped.push('\n'), + Some(c) => unescaped.push(c), + None => break, + } + } else { + unescaped.push(c); + } + } + unescaped +} + #[cfg(test)] mod test { use super::{Message, Tag};