From: Janne Koschinski Date: Tue, 12 Feb 2019 10:54:06 +0000 (+0100) Subject: Implement IRCv3 tag parsing and sending X-Git-Tag: test-travis-01~63 X-Git-Url: https://git.quassel-irc.org/?p=quassel.git;a=commitdiff_plain;h=53e50ab66a5b3fa00282545ebc22ce3433ecf42b Implement IRCv3 tag parsing and sending --- diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index f53988d0..417e5ff3 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -25,6 +25,9 @@ target_sources(${TARGET} PRIVATE ircchannel.cpp ircevent.cpp irclisthelper.cpp + ircdecoder.cpp + ircencoder.cpp + irctag.cpp ircuser.cpp logger.cpp message.cpp diff --git a/src/common/ircdecoder.cpp b/src/common/ircdecoder.cpp new file mode 100644 index 00000000..d51ed553 --- /dev/null +++ b/src/common/ircdecoder.cpp @@ -0,0 +1,190 @@ +/*************************************************************************** + * Copyright (C) 2005-2019 by the Quassel Project * + * devel@quassel-irc.org * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) version 3. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "ircdecoder.h" + +#include + +#include "irctag.h" + +QString IrcDecoder::parseTagValue(const QString& value) +{ + QString result; + bool escaped = false; + for (auto it = value.begin(); it < value.end(); it++) { + // Check if it's on the list of special wildcard characters, converting to Unicode for use + // in the switch statement + // + // See https://doc.qt.io/qt-5/qchar.html#unicode + if (escaped) { + switch (it->unicode()) { + case '\\': + result.append('\\'); + break; + case 's': + result.append(' '); + break; + case ':': + result.append(';'); + break; + case 'r': + result.append('\r'); + break; + case 'n': + result.append('\n'); + break; + default: + result.append(*it); + } + escaped = false; + } else if (it->unicode() == '\\') { + escaped = true; + } else { + result.append(*it); + } + } + return result; +} + +/** + * Extracts a space-delimited fragment from an IRC message + * @param raw Raw Message + * @param start Current index into the message, will be advanced automatically + * @param end End of fragment, if already known. Default is -1, in which case it will be set to the next whitespace + * character or the end of the string + * @param prefix Required prefix. Default is 0. If set, this only parses a fragment if it starts with the given prefix. + * @return Fragment + */ +QByteArray extractFragment(const QByteArray& raw, int& start, int end = -1, char prefix = 0) +{ + // Try to set find the end of the space-delimited fragment + if (end == -1) { + end = raw.indexOf(' ', start); + } + // If no space comes after this point, use the remainder of the string + if (end == -1) { + end = raw.length(); + } + QByteArray fragment; + // If a prefix is set + if (prefix != 0) { + // And the fragment starts with the prefix + if (start < raw.length() && raw[start] == prefix) { + // return the fragment without the prefix, advancing the string + fragment = raw.mid(start + 1, end - start - 1); + start = end; + } + } + else { + // otherwise return the entire fragment + fragment = raw.mid(start, end - start); + start = end; + } + return fragment; +} + +/** + * Skips empty parts in the message + * @param raw Raw Message + * @param start Current index into the message, will be advanced automatically + */ +void skipEmptyParts(const QByteArray& raw, int& start) +{ + while (start < raw.length() && raw[start] == ' ') { + start++; + } +} + +QHash IrcDecoder::parseTags(const std::function& decode, const QByteArray& raw, int& start) +{ + QHash tags = {}; + QString rawTagStr = decode(extractFragment(raw, start, -1, '@')); + // Tags are delimited with ; according to spec + QList rawTags = rawTagStr.split(';'); + for (const QString& rawTag : rawTags) { + if (rawTag.isEmpty()) { + continue; + } + + QString rawKey; + QString rawValue; + int index = rawTag.indexOf('='); + if (index == -1 || index == rawTag.length()) { + rawKey = rawTag; + } + else { + rawKey = rawTag.left(index); + rawValue = rawTag.mid(index + 1); + } + + IrcTagKey key{}; + key.clientTag = rawKey.startsWith('+'); + if (key.clientTag) { + rawKey.remove(0, 1); + } + QList splitByVendorAndKey = rawKey.split('/'); + if (!splitByVendorAndKey.isEmpty()) key.key = splitByVendorAndKey.takeLast(); + if (!splitByVendorAndKey.isEmpty()) key.vendor = splitByVendorAndKey.takeLast(); + tags[key] = parseTagValue(rawValue); + } + return tags; +} + +QString IrcDecoder::parsePrefix(const std::function& decode, const QByteArray& raw, int& start) +{ + return decode(extractFragment(raw, start, -1, ':')); +} + +QString IrcDecoder::parseCommand(const std::function& decode, const QByteArray& raw, int& start) +{ + return decode(extractFragment(raw, start, -1)); +} + +QByteArray IrcDecoder::parseParameter(const QByteArray& raw, int& start) +{ + if (start < raw.length() && raw[start] == ':') { + // Skip the prefix + start++; + return extractFragment(raw, start, raw.size()); + } + else { + return extractFragment(raw, start); + } +} + +void IrcDecoder::parseMessage(const std::function& decode, const QByteArray& rawMsg, QHash& tags, QString& prefix, QString& command, QList& parameters) +{ + int start = 0; + skipEmptyParts(rawMsg, start); + tags = parseTags(decode, rawMsg, start); + skipEmptyParts(rawMsg, start); + prefix = parsePrefix(decode, rawMsg, start); + skipEmptyParts(rawMsg, start); + command = parseCommand(decode, rawMsg, start); + skipEmptyParts(rawMsg, start); + QList params; + while (start != rawMsg.length()) { + QByteArray param = parseParameter(rawMsg, start); + skipEmptyParts(rawMsg, start); + params.append(param); + + } + parameters = params; +} diff --git a/src/common/ircdecoder.h b/src/common/ircdecoder.h new file mode 100644 index 00000000..e4515ec9 --- /dev/null +++ b/src/common/ircdecoder.h @@ -0,0 +1,81 @@ +/*************************************************************************** + * Copyright (C) 2005-2019 by the Quassel Project * + * devel@quassel-irc.org * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) version 3. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#pragma once + +#include "common-export.h" + +#include + +#include "irctag.h" + +class COMMON_EXPORT IrcDecoder +{ +public: + /** + * Parses an IRC message + * @param decode Decoder to be used for decoding the message + * @param rawMsg Raw Message + * @param tags[out] Parsed map of IRCv3 message tags + * @param prefix[out] Parsed prefix + * @param command[out] Parsed command + * @param parameters[out] Parsed list of parameters + */ + static void parseMessage(const std::function& decode, const QByteArray& raw, QHash& tags, QString& prefix, QString& command, QList& parameters); +private: + /** + * Parses an encoded IRCv3 message tag value + * @param value encoded IRCv3 message tag value + * @return decoded string + */ + static QString parseTagValue(const QString& value); + /** + * Parses IRCv3 message tags given a message + * @param net Decoder to be used for decoding the message + * @param raw Raw Message + * @param start Current index into the message, will be advanced automatically + * @return Parsed message tags + */ + static QHash parseTags(const std::function& decode, const QByteArray& raw, int& start); + /** + * Parses an IRC prefix, if available + * @param net Decoder to be used for decoding the message + * @param raw Raw Message + * @param start Current index into the message, will be advanced automatically + * @return Parsed prefix or empty string + */ + static QString parsePrefix(const std::function& decode, const QByteArray& raw, int& start); + /** + * Parses an IRC named command or numeric RPL + * @param net Decoder to be used for decoding the message + * @param raw Raw Message + * @param start Current index into the message, will be advanced automatically + * @return Parsed command + */ + static QString parseCommand(const std::function& decode, const QByteArray& raw, int& start); + /** + * Parses an IRC parameter + * @param net Decoder to be used for decoding the message + * @param raw Raw Message + * @param start Current index into the message, will be advanced automatically + * @return Parsed parameter + */ + static QByteArray parseParameter(const QByteArray& raw, int& start); +}; diff --git a/src/common/ircencoder.cpp b/src/common/ircencoder.cpp new file mode 100644 index 00000000..2be30221 --- /dev/null +++ b/src/common/ircencoder.cpp @@ -0,0 +1,98 @@ +/*************************************************************************** + * Copyright (C) 2005-2019 by the Quassel Project * + * devel@quassel-irc.org * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) version 3. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "ircencoder.h" + +QByteArray IrcEncoder::writeMessage(const QHash& tags, + const QByteArray& prefix, + const QString& cmd, + const QList& params) +{ + QByteArray msg; + writeTags(msg, tags); + writePrefix(msg, prefix); + writeCommand(msg, cmd); + writeParams(msg, params); + return msg; +} + +void IrcEncoder::writeTagValue(QByteArray& msg, const QString& value) +{ + QString it = value; + msg += it.replace("\\", R"(\\)") + .replace(";", R"(\:)") + .replace(" ", R"(\s)") + .replace("\r", R"(\r)") + .replace("\n", R"(\n)"); +} + +void IrcEncoder::writeTags(QByteArray& msg, const QHash& tags) +{ + if (!tags.isEmpty()) { + msg += "@"; + bool isFirstTag = true; + for (const IrcTagKey& key : tags.keys()) { + if (!isFirstTag) { + // We join tags with semicolons + msg += ";"; + } + if (key.clientTag) { + msg += "+"; + } + if (!key.vendor.isEmpty()) { + msg += key.vendor; + msg += "/"; + } + msg += key.key; + if (!tags[key].isEmpty()) { + msg += "="; + writeTagValue(msg, tags[key]); + } + + isFirstTag = false; + } + msg += " "; + } +} + +void IrcEncoder::writePrefix(QByteArray& msg, const QByteArray& prefix) +{ + if (!prefix.isEmpty()) { + msg += ":" + prefix + " "; + } +} + +void IrcEncoder::writeCommand(QByteArray& msg, const QString& cmd) +{ + msg += cmd.toUpper().toLatin1(); +} + +void IrcEncoder::writeParams(QByteArray& msg, const QList& params) +{ + for (int i = 0; i < params.size(); i++) { + msg += " "; + + bool isLastParam = i == params.size() - 1; + if (isLastParam && (params[i].isEmpty() || params[i].contains(' ') || params[i][0] == ':')) + msg += ":"; + + msg += params[i]; + } +} diff --git a/src/common/ircencoder.h b/src/common/ircencoder.h new file mode 100644 index 00000000..68fc46c2 --- /dev/null +++ b/src/common/ircencoder.h @@ -0,0 +1,75 @@ +/*************************************************************************** + * Copyright (C) 2005-2019 by the Quassel Project * + * devel@quassel-irc.org * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) version 3. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#pragma once + +#include "common-export.h" + +#include + +#include "irctag.h" + +class COMMON_EXPORT IrcEncoder +{ +public: + /** + * Writes an IRC message + * @param tags Map of IRCv3 message tags + * @param prefix Prefix/Source of the message (should be empty) + * @param cmd + * @param params + * @return + */ + static QByteArray writeMessage(const QHash& tags, + const QByteArray& prefix, + const QString& cmd, + const QList& params); +private: + /** + * Encodes a string as IRCv3 message tag value and appends it to the message + * @param msg message buffer to append to + * @param value unencoded tag value + */ + static void writeTagValue(QByteArray& msg, const QString& value); + /** + * Writes IRCv3 message tags to the message buffer + * @param msg message buffer to append to + * @param tags map of IRCv3 message tags + */ + static void writeTags(QByteArray& msg, const QHash& tags); + /** + * Writes the prefix/source to the message buffer + * @param msg message buffer to append to + * @param prefix prefix/source + */ + static void writePrefix(QByteArray& msg, const QByteArray& prefix); + /** + * Writes the command/verb to the message buffer + * @param msg message buffer to append to + * @param cmd command/verb + */ + static void writeCommand(QByteArray& msg, const QString& cmd); + /** + * Writes the command parameters/arguments to the message buffer + * @param msg message buffer to append to + * @param params parameters/arguments + */ + static void writeParams(QByteArray& msg, const QList& params); +}; diff --git a/src/common/irctag.cpp b/src/common/irctag.cpp new file mode 100644 index 00000000..2c9c221c --- /dev/null +++ b/src/common/irctag.cpp @@ -0,0 +1,56 @@ +/*************************************************************************** + * Copyright (C) 2005-2019 by the Quassel Project * + * devel@quassel-irc.org * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) version 3. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "irctag.h" + +uint qHash(const IrcTagKey& key) +{ + QString clientTag; + if (key.clientTag) { + clientTag = "+"; + } + return qHash(QString(clientTag + key.vendor + "/" + key.key)); +} + +bool operator==(const IrcTagKey& a, const IrcTagKey& b) +{ + return a.vendor == b.vendor && a.key == b.key && a.clientTag == b.clientTag; +} + +bool operator<(const IrcTagKey& a, const IrcTagKey& b) +{ + return a.vendor < b.vendor || a.key < b.key || a.clientTag < b.clientTag; +} + +QDebug operator<<(QDebug dbg, const IrcTagKey& i) { + return dbg << QString(("(clientTag = %1, vendor = %2,key = %3")).arg(i.clientTag).arg(i.vendor).arg(i.key); +} + +std::ostream& operator<<(std::ostream& o, const IrcTagKey& i) { + std::string result; + if (i.clientTag) + result += "+"; + if (!i.vendor.isEmpty()) { + result += i.vendor.toStdString(); + result += "/"; + } + result += i.key.toStdString(); + return o << result; +} diff --git a/src/common/irctag.h b/src/common/irctag.h new file mode 100644 index 00000000..499bbccd --- /dev/null +++ b/src/common/irctag.h @@ -0,0 +1,50 @@ +/*************************************************************************** + * Copyright (C) 2005-2019 by the Quassel Project * + * devel@quassel-irc.org * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) version 3. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#pragma once + +#include "common-export.h" + +#include +#include + +#include +#include + +struct COMMON_EXPORT IrcTagKey +{ + QString vendor; + QString key; + bool clientTag; + + explicit IrcTagKey(QString vendor, QString key, bool clientTag = false) : + vendor(std::move(vendor)), key(std::move(key)), clientTag(clientTag) + {} + + explicit IrcTagKey(QString key = {}) : + vendor(QString{}), key(std::move(key)), clientTag(false) + {} + + friend COMMON_EXPORT uint qHash(const IrcTagKey& key); + friend COMMON_EXPORT bool operator==(const IrcTagKey& a, const IrcTagKey& b); + friend COMMON_EXPORT bool operator<(const IrcTagKey& a, const IrcTagKey& b); + friend COMMON_EXPORT QDebug operator<<(QDebug dbg, const IrcTagKey& i); + friend COMMON_EXPORT std::ostream& operator<<(std::ostream& o, const IrcTagKey& i); +}; diff --git a/src/core/corebasichandler.cpp b/src/core/corebasichandler.cpp index 2d51a3af..0018d22b 100644 --- a/src/core/corebasichandler.cpp +++ b/src/core/corebasichandler.cpp @@ -29,13 +29,13 @@ CoreBasicHandler::CoreBasicHandler(CoreNetwork* parent) connect(this, &CoreBasicHandler::displayMsg, network(), &CoreNetwork::onDisplayMsg); connect(this, &CoreBasicHandler::putRawLine, network(), &CoreNetwork::putRawLine); connect(this, - selectOverload&, const QByteArray&, bool>(&CoreBasicHandler::putCmd), + selectOverload&, const QByteArray&, const QHash&, bool>(&CoreBasicHandler::putCmd), network(), - selectOverload&, const QByteArray&, bool>(&CoreNetwork::putCmd)); + selectOverload&, const QByteArray&, const QHash&, bool>(&CoreNetwork::putCmd)); connect(this, - selectOverload>&, const QByteArray&, bool>(&CoreBasicHandler::putCmd), + selectOverload>&, const QByteArray&, const QHash&, bool>(&CoreBasicHandler::putCmd), network(), - selectOverload>&, const QByteArray&, bool>(&CoreNetwork::putCmd)); + selectOverload>&, const QByteArray&, const QHash&, bool>(&CoreNetwork::putCmd)); } QString CoreBasicHandler::serverDecode(const QByteArray& string) @@ -132,9 +132,9 @@ BufferInfo::Type CoreBasicHandler::typeByTarget(const QString& target) const return BufferInfo::QueryBuffer; } -void CoreBasicHandler::putCmd(const QString& cmd, const QByteArray& param, const QByteArray& prefix, const bool prepend) +void CoreBasicHandler::putCmd(const QString& cmd, const QByteArray& param, const QByteArray& prefix, const QHash& tags, bool prepend) { QList list; list << param; - emit putCmd(cmd, list, prefix, prepend); + emit putCmd(cmd, list, prefix, tags, prepend); } diff --git a/src/core/corebasichandler.h b/src/core/corebasichandler.h index 37f629dd..697775aa 100644 --- a/src/core/corebasichandler.h +++ b/src/core/corebasichandler.h @@ -64,23 +64,23 @@ signals: * * @see CoreNetwork::putRawLine() */ - void putRawLine(const QByteArray& msg, const bool prepend = false); + void putRawLine(const QByteArray& msg, bool prepend = false); /** * Sends the command with encoded parameters, with optional prefix or high priority. * - * @see CoreNetwork::putCmd(const QString &cmd, const QList ¶ms, const QByteArray &prefix = QByteArray(), const bool - * prepend = false) + * @see CoreNetwork::putCmd(const QString &cmd, const QList ¶ms, const QByteArray &prefix = QByteArray(), const + * QHash& tags = {}, bool prepend = false) */ - void putCmd(const QString& cmd, const QList& params, const QByteArray& prefix = {}, bool prepend = false); + void putCmd(const QString& cmd, const QList& params, const QByteArray& prefix = {}, const QHash& tags = {}, bool prepend = false); /** * Sends the command for each set of encoded parameters, with optional prefix or high priority. * * @see CoreNetwork::putCmd(const QString &cmd, const QList> ¶ms, const QByteArray &prefix = QByteArray(), const - * bool prepend = false) + * QHash& tags = {}, bool prepend = false) */ - void putCmd(const QString& cmd, const QList>& params, const QByteArray& prefix = {}, bool prepend = false); + void putCmd(const QString& cmd, const QList>& params, const QByteArray& prefix = {}, const QHash& tags = {}, bool prepend = false); protected: /** @@ -89,6 +89,7 @@ protected: * @param[in] cmd Command to send, ignoring capitalization * @param[in] param Parameter for the command, encoded within a QByteArray * @param[in] prefix Optional command prefix + * @param[in] tags Optional command tags * @param[in] prepend * @parmblock * If true, the command is prepended into the start of the queue, otherwise, it's appended to @@ -96,7 +97,7 @@ protected: * maintain PING/PONG replies, the other side will close the connection. * @endparmblock */ - void putCmd(const QString& cmd, const QByteArray& param, const QByteArray& prefix = QByteArray(), const bool prepend = false); + void putCmd(const QString& cmd, const QByteArray& param, const QByteArray& prefix = QByteArray(), const QHash& tags = {}, bool prepend = false); inline CoreNetwork* network() const { return _network; } inline CoreSession* coreSession() const { return _network->coreSession(); } diff --git a/src/core/corenetwork.cpp b/src/core/corenetwork.cpp index d7215a46..2f14a5ba 100644 --- a/src/core/corenetwork.cpp +++ b/src/core/corenetwork.cpp @@ -28,10 +28,10 @@ #include "corenetworkconfig.h" #include "coresession.h" #include "coreuserinputhandler.h" -#include "networkevent.h" - -// IRCv3 capabilities +#include "ircencoder.h" #include "irccap.h" +#include "irctag.h" +#include "networkevent.h" CoreNetwork::CoreNetwork(const NetworkId& networkid, CoreSession* session) : Network(networkid, session) @@ -361,32 +361,17 @@ void CoreNetwork::putRawLine(const QByteArray& s, bool prepend) } } -void CoreNetwork::putCmd(const QString& cmd, const QList& params, const QByteArray& prefix, const bool prepend) +void CoreNetwork::putCmd(const QString& cmd, const QList& params, const QByteArray& prefix, const QHash &tags, const bool prepend) { - QByteArray msg; - - if (!prefix.isEmpty()) - msg += ":" + prefix + " "; - msg += cmd.toUpper().toLatin1(); - - for (int i = 0; i < params.size(); i++) { - msg += " "; - - if (i == params.size() - 1 && (params[i].contains(' ') || (!params[i].isEmpty() && params[i][0] == ':'))) - msg += ":"; - - msg += params[i]; - } - - putRawLine(msg, prepend); + putRawLine(IrcEncoder::writeMessage(tags, prefix, cmd, params), prepend); } -void CoreNetwork::putCmd(const QString& cmd, const QList>& params, const QByteArray& prefix, const bool prependAll) +void CoreNetwork::putCmd(const QString& cmd, const QList>& params, const QByteArray& prefix, const QHash &tags, const bool prependAll) { QListIterator> i(params); while (i.hasNext()) { QList msg = i.next(); - putCmd(cmd, msg, prefix, prependAll); + putCmd(cmd, msg, prefix, tags, prependAll); } } diff --git a/src/core/corenetwork.h b/src/core/corenetwork.h index 3ea739df..f07f2a28 100644 --- a/src/core/corenetwork.h +++ b/src/core/corenetwork.h @@ -20,15 +20,10 @@ #pragma once -#include "coreircchannel.h" -#include "coreircuser.h" -#include "network.h" +#include -// IRCv3 capabilities #include -#include "irccap.h" - #ifdef HAVE_SSL # include # include @@ -40,9 +35,12 @@ # include "cipher.h" #endif -#include - +#include "coreircchannel.h" +#include "coreircuser.h" #include "coresession.h" +#include "irccap.h" +#include "irctag.h" +#include "network.h" class CoreIdentity; class CoreUserInputHandler; @@ -280,7 +278,7 @@ public slots: * maintain PING/PONG replies, the other side will close the connection. * @endparmblock */ - void putCmd(const QString& cmd, const QList& params, const QByteArray& prefix = {}, bool prepend = false); + void putCmd(const QString& cmd, const QList& params, const QByteArray& prefix = {}, const QHash &tags = {}, bool prepend = false); /** * Sends the command for each set of encoded parameters, with optional prefix or high priority. @@ -299,7 +297,7 @@ public slots: * cannot maintain PING/PONG replies, the other side will close the connection. * @endparmblock */ - void putCmd(const QString& cmd, const QList>& params, const QByteArray& prefix = {}, bool prependAll = false); + void putCmd(const QString& cmd, const QList>& params, const QByteArray& prefix = {}, const QHash &tags = {}, bool prependAll = false); void setChannelJoined(const QString& channel); void setChannelParted(const QString& channel); diff --git a/src/core/coreuserinputhandler.cpp b/src/core/coreuserinputhandler.cpp index 07a2b4b9..03bee367 100644 --- a/src/core/coreuserinputhandler.cpp +++ b/src/core/coreuserinputhandler.cpp @@ -566,7 +566,7 @@ void CoreUserInputHandler::handlePing(const BufferInfo& bufferInfo, const QStrin param = QTime::currentTime().toString("hh:mm:ss.zzz"); // Take priority so this won't get stuck behind other queued messages. - putCmd("PING", serverEncode(param), QByteArray(), true); + putCmd("PING", serverEncode(param), {}, {}, true); } void CoreUserInputHandler::handlePrint(const BufferInfo& bufferInfo, const QString& msg) @@ -615,7 +615,7 @@ void CoreUserInputHandler::handleQuit(const BufferInfo& bufferInfo, const QStrin void CoreUserInputHandler::issueQuit(const QString& reason, bool forceImmediate) { // If needing an immediate QUIT (e.g. core shutdown), prepend this to the queue - emit putCmd("QUIT", serverEncode(reason), QByteArray(), forceImmediate); + emit putCmd("QUIT", serverEncode(reason), {}, {}, forceImmediate); } void CoreUserInputHandler::handleQuote(const BufferInfo& bufferInfo, const QString& msg) diff --git a/src/core/ircparser.cpp b/src/core/ircparser.cpp index bfbc3915..f5b37840 100644 --- a/src/core/ircparser.cpp +++ b/src/core/ircparser.cpp @@ -24,6 +24,7 @@ #include "corenetwork.h" #include "eventmanager.h" +#include "ircdecoder.h" #include "ircevent.h" #include "messageevent.h" #include "networkevent.h" @@ -88,8 +89,8 @@ void IrcParser::processNetworkIncoming(NetworkDataEvent* e) // note that the IRC server is still alive net->resetPingTimeout(); - QByteArray msg = e->data(); - if (msg.isEmpty()) { + const QByteArray rawMsg = e->data(); + if (rawMsg.isEmpty()) { qWarning() << "Received empty string from server!"; return; } @@ -97,81 +98,40 @@ void IrcParser::processNetworkIncoming(NetworkDataEvent* e) // Log the message if enabled and network ID matches or allows all if (_debugLogRawIrc && (_debugLogRawNetId == -1 || net->networkId().toInt() == _debugLogRawNetId)) { // Include network ID - qDebug() << "IRC net" << net->networkId() << "<<" << msg; + qDebug() << "IRC net" << net->networkId() << "<<" << rawMsg; } - // Now we split the raw message into its various parts... + QHash tags; QString prefix; - QByteArray trailing; - QString cmd, target; - - // First, check for a trailing parameter introduced by " :", since this might screw up splitting the msg - // NOTE: This assumes that this is true in raw encoding, but well, hopefully there are no servers running in japanese on protocol level... - int idx = msg.indexOf(" :"); - if (idx >= 0) { - if (msg.length() > idx + 2) - trailing = msg.mid(idx + 2); - msg = msg.left(idx); - } - // OK, now it is safe to split... - QList params = msg.split(' '); - - // This could still contain empty elements due to (faulty?) ircds sending multiple spaces in a row - // Also, QByteArray is not nearly as convenient to work with as QString for such things :) - QList::iterator iter = params.begin(); - while (iter != params.end()) { - if (iter->isEmpty()) - iter = params.erase(iter); - else - ++iter; - } - - if (!trailing.isEmpty()) - params << trailing; - if (params.count() < 1) { - qWarning() << "Received invalid string from server!"; - return; - } - - QString foo = net->serverDecode(params.takeFirst()); - - // a colon as the first chars indicates the existence of a prefix - if (foo[0] == ':') { - foo.remove(0, 1); - prefix = foo; - if (params.count() < 1) { - qWarning() << "Received invalid string from server!"; - return; - } - foo = net->serverDecode(params.takeFirst()); - } - - // next string without a whitespace is the command - cmd = foo.trimmed(); + QString cmd; + QList params; + IrcDecoder::parseMessage([&net](const QByteArray& data) { + return net->serverDecode(data); + }, rawMsg, tags, prefix, cmd, params); QList events; EventManager::EventType type = EventManager::Invalid; + QString messageTarget; uint num = cmd.toUInt(); if (num > 0) { // numeric reply if (params.count() == 0) { - qWarning() << "Message received from server violates RFC and is ignored!" << msg; + qWarning() << "Message received from server violates RFC and is ignored!" << rawMsg; return; } // numeric replies have the target as first param (RFC 2812 - 2.4). this is usually our own nick. Remove this! - target = net->serverDecode(params.takeFirst()); + messageTarget = net->serverDecode(params.takeFirst()); type = EventManager::IrcEventNumeric; } else { // any other irc command QString typeName = QLatin1String("IrcEvent") + cmd.at(0).toUpper() + cmd.mid(1).toLower(); - type = eventManager()->eventTypeByName(typeName); + type = EventManager::eventTypeByName(typeName); if (type == EventManager::Invalid) { - type = eventManager()->eventTypeByName("IrcEventUnknown"); + type = EventManager::eventTypeByName("IrcEventUnknown"); Q_ASSERT(type != EventManager::Invalid); } - target = QString(); } // Almost always, all params are server-encoded. There's a few exceptions, let's catch them here! @@ -245,8 +205,8 @@ void IrcParser::processNetworkIncoming(NetworkDataEvent* e) if (welcomeRegExp.indexIn(decMsg) != -1) { QString channelname = welcomeRegExp.cap(1); decMsg = decMsg.mid(welcomeRegExp.matchedLength()); - CoreIrcChannel* chan = static_cast( - net->ircChannel(channelname)); // we only have CoreIrcChannels in the core, so this cast is safe + // we only have CoreIrcChannels in the core, so this cast is safe + CoreIrcChannel* chan = static_cast(net->ircChannel(channelname)); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast) if (chan && !chan->receivedWelcomeMsg()) { chan->setReceivedWelcomeMsg(); events << new MessageEvent(Message::Notice, net, decMsg, prefix, channelname, Message::None, e->timestamp()); @@ -368,7 +328,7 @@ void IrcParser::processNetworkIncoming(NetworkDataEvent* e) } break; case 451: /* You have not registered... */ - if (target.compare("CAP", Qt::CaseInsensitive) == 0) { + if (messageTarget.compare("CAP", Qt::CaseInsensitive) == 0) { // :irc.server.com 451 CAP :You have not registered // If server doesn't support capabilities, it will report this message. Turn it // into a nicer message since it's not a real error. @@ -399,7 +359,7 @@ void IrcParser::processNetworkIncoming(NetworkDataEvent* e) IrcEvent* event; if (type == EventManager::IrcEventNumeric) - event = new IrcEventNumeric(num, net, prefix, target); + event = new IrcEventNumeric(num, net, prefix, messageTarget); else event = new IrcEvent(type, net, prefix); event->setParams(decParams); diff --git a/src/core/ircparser.h b/src/core/ircparser.h index 591a4ee8..14853374 100644 --- a/src/core/ircparser.h +++ b/src/core/ircparser.h @@ -18,10 +18,10 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ -#ifndef IRCPARSER_H -#define IRCPARSER_H +#pragma once #include "coresession.h" +#include "irctag.h" class Event; class EventManager; @@ -55,5 +55,3 @@ private: bool _debugLogRawIrc; ///< If true, include raw IRC socket messages in the debug log qint32 _debugLogRawNetId; ///< Network ID for logging raw IRC socket messages, or -1 for all }; - -#endif diff --git a/tests/common/CMakeLists.txt b/tests/common/CMakeLists.txt index 6b846468..9ca7d4f8 100644 --- a/tests/common/CMakeLists.txt +++ b/tests/common/CMakeLists.txt @@ -2,6 +2,10 @@ quassel_add_test(ExpressionMatchTest) quassel_add_test(FuncHelpersTest) +quassel_add_test(IrcDecoderTest) + +quassel_add_test(IrcEncoderTest) + quassel_add_test(SignalProxyTest LIBRARIES Quassel::Test::Util diff --git a/tests/common/ircdecodertest.cpp b/tests/common/ircdecodertest.cpp new file mode 100644 index 00000000..c32fd5e8 --- /dev/null +++ b/tests/common/ircdecodertest.cpp @@ -0,0 +1,279 @@ +/*************************************************************************** + * Copyright (C) 2005-2019 by the Quassel Project * + * devel@quassel-irc.org * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) version 3. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include + +#include "testglobal.h" +#include "ircdecoder.h" +#include "irctag.h" + +struct IrcMessage +{ + std::map tags; + std::string prefix; + std::string cmd; + std::vector params; + + explicit IrcMessage(std::map tags, std::string prefix, std::string cmd, std::vector params = {}) + : tags(std::move(tags)), prefix(std::move(prefix)), cmd(std::move(cmd)), params(std::move(params)) {} + + explicit IrcMessage(std::initializer_list> tags, std::string prefix, std::string cmd, std::vector params = {}) + : tags(tags), prefix(std::move(prefix)), cmd(std::move(cmd)), params(std::move(params)) {} + + explicit IrcMessage(std::string prefix, std::string cmd, std::vector params = {}) + : tags({}), prefix(std::move(prefix)), cmd(std::move(cmd)), params(std::move(params)) {} + + friend bool operator==(const IrcMessage& a, const IrcMessage& b) + { + return a.tags == b.tags && + a.prefix == b.prefix && + a.cmd == b.cmd && + a.params == b.params; + } + + friend std::ostream& operator<<(std::ostream& o, const IrcMessage& m) + { + o << "(tags={"; + for (const std::pair entry: m.tags) { + o << entry.first << "='" << entry.second << "', "; + } + o << "}, prefix=" << m.prefix << ", cmd=" << m.cmd << ", params=["; + for (const std::string& param : m.params) { + o << "'" << param << "', "; + } + o << "])"; + return o; + } +}; + + +IrcMessage parse(const std::string& message) +{ + QHash tags; + QString prefix; + QString cmd; + QList params; + + IrcDecoder::parseMessage([](const QByteArray& data) { + return QString::fromUtf8(data); + }, QByteArray::fromStdString(message), tags, prefix, cmd, params); + + std::map actualTags; + for (const IrcTagKey& key : tags.keys()) { + actualTags[key] = tags[key].toStdString(); + } + std::string actualPrefix = prefix.toStdString(); + std::string actualCmd = cmd.toStdString(); + std::vector actualParams; + for (const QByteArray& param : params) { + actualParams.push_back(param.toStdString()); + } + + return IrcMessage{actualTags, actualPrefix, actualCmd, actualParams}; +} + +TEST(IrcDecoderTest, simple) +{ + EXPECT_EQ(parse("foo bar baz asdf"), + IrcMessage("", + "foo", + {"bar", "baz", "asdf"})); +} + +TEST(IrcDecoderTest, with_source) +{ + EXPECT_EQ(parse(":coolguy foo bar baz asdf"), + IrcMessage("coolguy", + "foo", + {"bar", "baz", "asdf"})); +} + +TEST(IrcDecoderTest, with_trailing_param) +{ + EXPECT_EQ(parse("foo bar baz :asdf quux"), + IrcMessage("", + "foo", + {"bar", "baz", "asdf quux"})); + EXPECT_EQ(parse("foo bar baz :"), + IrcMessage("", + "foo", + {"bar", "baz", ""})); + EXPECT_EQ(parse("foo bar baz ::asdf"), + IrcMessage("", + "foo", + {"bar", "baz", ":asdf"})); +} + +TEST(IrcDecoderTest, with_source_and_trailing_param) +{ + EXPECT_EQ(parse(":coolguy foo bar baz :asdf quux"), + IrcMessage("coolguy", + "foo", + {"bar", "baz", "asdf quux"})); + EXPECT_EQ(parse(":coolguy foo bar baz : asdf quux "), + IrcMessage("coolguy", + "foo", + {"bar", "baz", " asdf quux "})); + EXPECT_EQ(parse(":coolguy PRIVMSG bar :lol :) "), + IrcMessage("coolguy", + "PRIVMSG", + {"bar", "lol :) "})); + EXPECT_EQ(parse(":coolguy foo bar baz : "), + IrcMessage("coolguy", + "foo", + {"bar", "baz", " "})); +} + +TEST(IrcDecoderTest, with_tags) +{ + EXPECT_EQ(parse("@a=b;c=32;k;rt=ql7 foo"), + IrcMessage({{IrcTagKey("a"), "b"}, + {IrcTagKey("c"), "32"}, + {IrcTagKey("k"), ""}, + {IrcTagKey("rt"), "ql7"}}, + "", + "foo")); +} + +TEST(IrcDecoderTest, with_escaped_tags) +{ + EXPECT_EQ(parse("@a=b\\\\and\\nk;c=72\\s45;d=gh\\:764 foo"), + IrcMessage({{IrcTagKey("a"), "b\\and\nk"}, + {IrcTagKey("c"), "72 45"}, + {IrcTagKey("d"), "gh;764"}}, + "", + "foo")); +} + +TEST(IrcDecoderTest, with_tags_and_source) +{ + EXPECT_EQ(parse("@c;h=;a=b :quux ab cd"), + IrcMessage({{IrcTagKey("c"), ""}, + {IrcTagKey("h"), ""}, + {IrcTagKey("a"), "b"}}, + "quux", + "ab", + {"cd"})); +} + +TEST(IrcDecoderTest, different_forms_of_last_param) +{ + EXPECT_EQ(parse(":src JOIN #chan"), + IrcMessage("src", + "JOIN", + {"#chan"})); + EXPECT_EQ(parse(":src JOIN :#chan"), + IrcMessage("src", + "JOIN", + {"#chan"})); +} + +TEST(IrcDecoderTest, with_and_without_last_param) +{ + EXPECT_EQ(parse(":src AWAY"), + IrcMessage("src", + "AWAY")); + EXPECT_EQ(parse(":src AWAY "), + IrcMessage("src", + "AWAY")); +} + +TEST(IrcDecoderTest, tab_is_not_considered_SPACE) +{ + EXPECT_EQ(parse(":cool\tguy foo bar baz"), + IrcMessage("cool\tguy", + "foo", + {"bar", "baz"})); +} + +TEST(IrcDecoderTest, with_weird_control_codes_in_the_source) +{ + EXPECT_EQ(parse(":coolguy!ag@net""\x03""5w""\x03""ork.admin PRIVMSG foo :bar baz"), + IrcMessage("coolguy!ag@net""\x03""5w""\x03""ork.admin", + "PRIVMSG", + {"foo", "bar baz"})); + EXPECT_EQ(parse(":coolguy!~ag@n""\x02""et""\x03""05w""\x0f""ork.admin PRIVMSG foo :bar baz"), + IrcMessage("coolguy!~ag@n""\x02""et""\x03""05w""\x0f""ork.admin", + "PRIVMSG", + {"foo", "bar baz"})); +} + +TEST(IrcDecoderTest, with_tags_source_and_params) +{ + EXPECT_EQ(parse("@tag1=value1;tag2;vendor1/tag3=value2;vendor2/tag4 :irc.example.com COMMAND param1 param2 :param3 param3"), + IrcMessage({{IrcTagKey("tag1"), "value1"}, + {IrcTagKey("tag2"), ""}, + {IrcTagKey("vendor1", "tag3"), "value2"}, + {IrcTagKey("vendor2", "tag4"), ""}}, + "irc.example.com", + "COMMAND", + {"param1", "param2", "param3 param3"})); + EXPECT_EQ(parse(":irc.example.com COMMAND param1 param2 :param3 param3"), + IrcMessage("irc.example.com", + "COMMAND", + {"param1", "param2", "param3 param3"})); + EXPECT_EQ(parse("@tag1=value1;tag2;vendor1/tag3=value2;vendor2/tag4 COMMAND param1 param2 :param3 param3"), + IrcMessage({{IrcTagKey("tag1"), "value1"}, + {IrcTagKey("tag2"), ""}, + {IrcTagKey("vendor1", "tag3"), "value2"}, + {IrcTagKey("vendor2", "tag4"), ""}}, + "", + "COMMAND", + {"param1", "param2", "param3 param3"})); + EXPECT_EQ(parse("COMMAND"), + IrcMessage("", + "COMMAND")); + EXPECT_EQ(parse("@foo=\\\\\\\\\\:\\\\s\\s\\r\\n COMMAND"), + IrcMessage({{IrcTagKey("foo"), "\\\\;\\s \r\n"}}, + "", + "COMMAND")); +} + +TEST(IrcDecoderTest, broken_messages_from_unreal) +{ + EXPECT_EQ(parse(":gravel.mozilla.org 432 #momo :Erroneous Nickname: Illegal characters"), + IrcMessage("gravel.mozilla.org", + "432", + {"#momo", "Erroneous Nickname: Illegal characters"})); + EXPECT_EQ(parse(":gravel.mozilla.org MODE #tckk +n "), + IrcMessage("gravel.mozilla.org", + "MODE", + {"#tckk", "+n"})); + EXPECT_EQ(parse(":services.esper.net MODE #foo-bar +o foobar "), + IrcMessage("services.esper.net", + "MODE", + {"#foo-bar", "+o", "foobar"})); +} + +TEST(IrcDecoderTest, tag_values) +{ + EXPECT_EQ(parse("@tag1=value\\\\ntest COMMAND"), + IrcMessage({{IrcTagKey("tag1"), "value\\ntest"}}, + "", + "COMMAND")); + EXPECT_EQ(parse("@tag1=value\\1 COMMAND"), + IrcMessage({{IrcTagKey("tag1"), "value1"}}, + "", + "COMMAND")); + EXPECT_EQ(parse("@tag1=value1\\ COMMAND"), + IrcMessage({{IrcTagKey("tag1"), "value1"}}, + "", + "COMMAND")); +} diff --git a/tests/common/ircencodertest.cpp b/tests/common/ircencodertest.cpp new file mode 100644 index 00000000..95c0213d --- /dev/null +++ b/tests/common/ircencodertest.cpp @@ -0,0 +1,245 @@ +/*************************************************************************** + * Copyright (C) 2005-2019 by the Quassel Project * + * devel@quassel-irc.org * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) version 3. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include + +#include "testglobal.h" +#include "ircencoder.h" +#include "irctag.h" + +struct IrcMessage +{ + std::map tags; + std::string prefix; + std::string cmd; + std::vector params; + + explicit IrcMessage(std::map tags, std::string prefix, std::string cmd, std::vector params = {}) + : tags(std::move(tags)), prefix(std::move(prefix)), cmd(std::move(cmd)), params(std::move(params)) {} + + explicit IrcMessage(std::initializer_list> tags, std::string prefix, std::string cmd, std::vector params = {}) + : tags(tags), prefix(std::move(prefix)), cmd(std::move(cmd)), params(std::move(params)) {} + + explicit IrcMessage(std::string prefix, std::string cmd, std::vector params = {}) + : tags({}), prefix(std::move(prefix)), cmd(std::move(cmd)), params(std::move(params)) {} + + friend bool operator==(const IrcMessage& a, const IrcMessage& b) + { + return a.tags == b.tags && + a.prefix == b.prefix && + a.cmd == b.cmd && + a.params == b.params; + } + + friend std::ostream& operator<<(std::ostream& o, const IrcMessage& m) + { + o << "(tags={"; + for (const std::pair entry: m.tags) { + o << entry.first << "='" << entry.second << "', "; + } + o << "}, prefix=" << m.prefix << ", cmd=" << m.cmd << ", params=["; + for (const std::string& param : m.params) { + o << "'" << param << "', "; + } + o << "])"; + return o; + } +}; + + +std::string write(const IrcMessage& message) +{ + QHash tags; + QByteArray prefix = QByteArray::fromStdString(message.prefix); + QByteArray cmd = QByteArray::fromStdString(message.cmd); + QList params; + + for (const auto& pair : message.tags) { + tags[pair.first] = QString::fromStdString(pair.second); + } + + for (const std::string& param : message.params) { + params += QByteArray::fromStdString(param); + } + + return IrcEncoder::writeMessage(tags, prefix, cmd, params).toStdString(); +} + +TEST(IrcEncoderTest, simple_test_with_verb_and_params) +{ + EXPECT_STRCASEEQ( + "foo bar baz asdf", + write(IrcMessage("", + "foo", + {"bar", "baz", "asdf"})).data()); +} + +TEST(IrcEncoderTest, simple_test_with_source_and_no_params) +{ + EXPECT_STRCASEEQ( + ":src AWAY", + write(IrcMessage("src", + "AWAY")).data()); +} + +TEST(IrcEncoderTest, simple_test_with_source_and_empty_trailing_param) +{ + EXPECT_STRCASEEQ( + ":src AWAY :", + write(IrcMessage("src", + "AWAY", + {""})).data()); +} + +TEST(IrcEncoderTest, simple_test_with_source) +{ + EXPECT_STRCASEEQ( + ":coolguy foo bar baz asdf", + write(IrcMessage("coolguy", + "foo", + {"bar", "baz", "asdf"})).data()); +} + +TEST(IrcEncoderTest, simple_test_with_trailing_param) +{ + EXPECT_STRCASEEQ( + "foo bar baz :asdf quux", + write(IrcMessage("", + "foo", + {"bar", "baz", "asdf quux"})).data()); +} + +TEST(IrcEncoderTest, simple_test_with_empty_trailing_param) +{ + EXPECT_STRCASEEQ( + "foo bar baz :", + write(IrcMessage("", + "foo", + {"bar", "baz", ""})).data()); +} + +TEST(IrcEncoderTest, simple_test_with_trailing_param_containing_colon) +{ + EXPECT_STRCASEEQ( + "foo bar baz ::asdf", + write(IrcMessage("", + "foo", + {"bar", "baz", ":asdf"})).data()); +} + +TEST(IrcEncoderTest, test_with_source_and_trailing_param) +{ + EXPECT_STRCASEEQ( + ":coolguy foo bar baz :asdf quux", + write(IrcMessage("coolguy", + "foo", + {"bar", "baz", "asdf quux"})).data()); +} + +TEST(IrcEncoderTest, test_with_trailing_containing_beginning_end_whitespace) +{ + EXPECT_STRCASEEQ( + ":coolguy foo bar baz : asdf quux ", + write(IrcMessage("coolguy", + "foo", + {"bar", "baz", " asdf quux "})).data()); +} + +TEST(IrcEncoderTest, test_with_trailing_containing_what_looks_like_another_trailing_param) +{ + EXPECT_STRCASEEQ( + ":coolguy PRIVMSG bar :lol :) ", + write(IrcMessage("coolguy", + "PRIVMSG", + {"bar", "lol :) "})).data()); +} + +TEST(IrcEncoderTest, simple_test_with_source_and_empty_trailing) +{ + EXPECT_STRCASEEQ( + ":coolguy foo bar baz :", + write(IrcMessage("coolguy", + "foo", + {"bar", "baz", ""})).data()); +} + +TEST(IrcEncoderTest, trailing_contains_only_spaces) +{ + EXPECT_STRCASEEQ( + ":coolguy foo bar baz : ", + write(IrcMessage("coolguy", + "foo", + {"bar", "baz", " "})).data()); +} + +TEST(IrcEncoderTest, param_containing_tab_tab_is_not_considered_SPACE_for_message_splitting) +{ + EXPECT_STRCASEEQ( + ":coolguy foo b\tar baz", + write(IrcMessage("coolguy", + "foo", + {"b\tar", "baz"})).data()); +} + +TEST(IrcEncoderTest, tags_with_no_value_and_space_filled_trailing) +{ + EXPECT_STRCASEEQ( + "@asd :coolguy foo bar baz : ", + write(IrcMessage({{IrcTagKey("asd"), ""}}, + "coolguy", + "foo", + {"bar", "baz", " "})).data()); +} + +TEST(IrcEncoderTest, tags_with_escaped_values) +{ + std::vector expected{ + R"(@d=gh\:764;a=b\\and\nk foo)", + R"(@a=b\\and\nk;d=gh\:764 foo)", + }; + EXPECT_THAT(expected, testing::Contains(testing::StrCaseEq( + write(IrcMessage({{IrcTagKey("a"), "b\\and\nk"}, + {IrcTagKey("d"), "gh;764"}}, + "", + "foo"))))); +} + +TEST(IrcEncoderTest, tags_with_escaped_values_and_params) +{ + std::vector expected{ + R"(@d=gh\:764;a=b\\and\nk foo par1 par2)", + R"(@a=b\\and\nk;d=gh\:764 foo par1 par2)", + }; + EXPECT_THAT(expected, testing::Contains(testing::StrCaseEq( + write(IrcMessage({{IrcTagKey("a"), "b\\and\nk"}, + {IrcTagKey("d"), "gh;764"}}, + "", + "foo", + {"par1", "par2"}))))); +} + +TEST(IrcEncoderTest, tags_with_long_strange_values) +{ + EXPECT_STRCASEEQ( + R"(@foo=\\\\\:\\s\s\r\n COMMAND)", + write(IrcMessage({{IrcTagKey("foo"), "\\\\;\\s \r\n"}}, + "", + "COMMAND")).data()); +}