ircchannel.cpp
ircevent.cpp
irclisthelper.cpp
+ ircdecoder.cpp
+ ircencoder.cpp
+ irctag.cpp
ircuser.cpp
logger.cpp
message.cpp
--- /dev/null
+/***************************************************************************
+ * 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 <QDebug>
+
+#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<IrcTagKey, QString> IrcDecoder::parseTags(const std::function<QString(const QByteArray&)>& decode, const QByteArray& raw, int& start)
+{
+ QHash<IrcTagKey, QString> tags = {};
+ QString rawTagStr = decode(extractFragment(raw, start, -1, '@'));
+ // Tags are delimited with ; according to spec
+ QList<QString> 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<QString> 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<QString(const QByteArray&)>& decode, const QByteArray& raw, int& start)
+{
+ return decode(extractFragment(raw, start, -1, ':'));
+}
+
+QString IrcDecoder::parseCommand(const std::function<QString(const QByteArray&)>& 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<QString(const QByteArray&)>& decode, const QByteArray& rawMsg, QHash<IrcTagKey, QString>& tags, QString& prefix, QString& command, QList<QByteArray>& 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<QByteArray> params;
+ while (start != rawMsg.length()) {
+ QByteArray param = parseParameter(rawMsg, start);
+ skipEmptyParts(rawMsg, start);
+ params.append(param);
+
+ }
+ parameters = params;
+}
--- /dev/null
+/***************************************************************************
+ * 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 <functional>
+
+#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<QString(const QByteArray&)>& decode, const QByteArray& raw, QHash<IrcTagKey, QString>& tags, QString& prefix, QString& command, QList<QByteArray>& 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<IrcTagKey, QString> parseTags(const std::function<QString(const QByteArray&)>& 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<QString(const QByteArray&)>& 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<QString(const QByteArray&)>& 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);
+};
--- /dev/null
+/***************************************************************************
+ * 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<IrcTagKey, QString>& tags,
+ const QByteArray& prefix,
+ const QString& cmd,
+ const QList<QByteArray>& 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<IrcTagKey, QString>& 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<QByteArray>& 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];
+ }
+}
--- /dev/null
+/***************************************************************************
+ * 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 <QByteArray>
+
+#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<IrcTagKey, QString>& tags,
+ const QByteArray& prefix,
+ const QString& cmd,
+ const QList<QByteArray>& 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<IrcTagKey, QString>& 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<QByteArray>& params);
+};
--- /dev/null
+/***************************************************************************
+ * 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;
+}
--- /dev/null
+/***************************************************************************
+ * 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 <ostream>
+#include <utility>
+
+#include <QDebug>
+#include <QString>
+
+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);
+};
connect(this, &CoreBasicHandler::displayMsg, network(), &CoreNetwork::onDisplayMsg);
connect(this, &CoreBasicHandler::putRawLine, network(), &CoreNetwork::putRawLine);
connect(this,
- selectOverload<const QString&, const QList<QByteArray>&, const QByteArray&, bool>(&CoreBasicHandler::putCmd),
+ selectOverload<const QString&, const QList<QByteArray>&, const QByteArray&, const QHash<IrcTagKey, QString>&, bool>(&CoreBasicHandler::putCmd),
network(),
- selectOverload<const QString&, const QList<QByteArray>&, const QByteArray&, bool>(&CoreNetwork::putCmd));
+ selectOverload<const QString&, const QList<QByteArray>&, const QByteArray&, const QHash<IrcTagKey, QString>&, bool>(&CoreNetwork::putCmd));
connect(this,
- selectOverload<const QString&, const QList<QList<QByteArray>>&, const QByteArray&, bool>(&CoreBasicHandler::putCmd),
+ selectOverload<const QString&, const QList<QList<QByteArray>>&, const QByteArray&, const QHash<IrcTagKey, QString>&, bool>(&CoreBasicHandler::putCmd),
network(),
- selectOverload<const QString&, const QList<QList<QByteArray>>&, const QByteArray&, bool>(&CoreNetwork::putCmd));
+ selectOverload<const QString&, const QList<QList<QByteArray>>&, const QByteArray&, const QHash<IrcTagKey, QString>&, bool>(&CoreNetwork::putCmd));
}
QString CoreBasicHandler::serverDecode(const QByteArray& string)
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<IrcTagKey, QString>& tags, bool prepend)
{
QList<QByteArray> list;
list << param;
- emit putCmd(cmd, list, prefix, prepend);
+ emit putCmd(cmd, list, prefix, tags, prepend);
}
*
* @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<QByteArray> ¶ms, const QByteArray &prefix = QByteArray(), const bool
- * prepend = false)
+ * @see CoreNetwork::putCmd(const QString &cmd, const QList<QByteArray> ¶ms, const QByteArray &prefix = QByteArray(), const
+ * QHash<IrcTagKey, QString>& tags = {}, bool prepend = false)
*/
- void putCmd(const QString& cmd, const QList<QByteArray>& params, const QByteArray& prefix = {}, bool prepend = false);
+ void putCmd(const QString& cmd, const QList<QByteArray>& params, const QByteArray& prefix = {}, const QHash<IrcTagKey, QString>& 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<QList<QByteArray>> ¶ms, const QByteArray &prefix = QByteArray(), const
- * bool prepend = false)
+ * QHash<IrcTagKey, QString>& tags = {}, bool prepend = false)
*/
- void putCmd(const QString& cmd, const QList<QList<QByteArray>>& params, const QByteArray& prefix = {}, bool prepend = false);
+ void putCmd(const QString& cmd, const QList<QList<QByteArray>>& params, const QByteArray& prefix = {}, const QHash<IrcTagKey, QString>& tags = {}, bool prepend = false);
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
* 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<IrcTagKey, QString>& tags = {}, bool prepend = false);
inline CoreNetwork* network() const { return _network; }
inline CoreSession* coreSession() const { return _network->coreSession(); }
#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)
}
}
-void CoreNetwork::putCmd(const QString& cmd, const QList<QByteArray>& params, const QByteArray& prefix, const bool prepend)
+void CoreNetwork::putCmd(const QString& cmd, const QList<QByteArray>& params, const QByteArray& prefix, const QHash<IrcTagKey, QString> &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<QList<QByteArray>>& params, const QByteArray& prefix, const bool prependAll)
+void CoreNetwork::putCmd(const QString& cmd, const QList<QList<QByteArray>>& params, const QByteArray& prefix, const QHash<IrcTagKey, QString> &tags, const bool prependAll)
{
QListIterator<QList<QByteArray>> i(params);
while (i.hasNext()) {
QList<QByteArray> msg = i.next();
- putCmd(cmd, msg, prefix, prependAll);
+ putCmd(cmd, msg, prefix, tags, prependAll);
}
}
#pragma once
-#include "coreircchannel.h"
-#include "coreircuser.h"
-#include "network.h"
+#include <functional>
-// IRCv3 capabilities
#include <QTimer>
-#include "irccap.h"
-
#ifdef HAVE_SSL
# include <QSslError>
# include <QSslSocket>
# include "cipher.h"
#endif
-#include <functional>
-
+#include "coreircchannel.h"
+#include "coreircuser.h"
#include "coresession.h"
+#include "irccap.h"
+#include "irctag.h"
+#include "network.h"
class CoreIdentity;
class CoreUserInputHandler;
* maintain PING/PONG replies, the other side will close the connection.
* @endparmblock
*/
- void putCmd(const QString& cmd, const QList<QByteArray>& params, const QByteArray& prefix = {}, bool prepend = false);
+ void putCmd(const QString& cmd, const QList<QByteArray>& params, const QByteArray& prefix = {}, const QHash<IrcTagKey, QString> &tags = {}, bool prepend = false);
/**
* Sends the command for each set of encoded parameters, with optional prefix or high priority.
* cannot maintain PING/PONG replies, the other side will close the connection.
* @endparmblock
*/
- void putCmd(const QString& cmd, const QList<QList<QByteArray>>& params, const QByteArray& prefix = {}, bool prependAll = false);
+ void putCmd(const QString& cmd, const QList<QList<QByteArray>>& params, const QByteArray& prefix = {}, const QHash<IrcTagKey, QString> &tags = {}, bool prependAll = false);
void setChannelJoined(const QString& channel);
void setChannelParted(const QString& channel);
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)
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)
#include "corenetwork.h"
#include "eventmanager.h"
+#include "ircdecoder.h"
#include "ircevent.h"
#include "messageevent.h"
#include "networkevent.h"
// 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;
}
// 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<IrcTagKey, QString> 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<QByteArray> 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<QByteArray>::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<QByteArray> params;
+ IrcDecoder::parseMessage([&net](const QByteArray& data) {
+ return net->serverDecode(data);
+ }, rawMsg, tags, prefix, cmd, params);
QList<Event*> 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!
if (welcomeRegExp.indexIn(decMsg) != -1) {
QString channelname = welcomeRegExp.cap(1);
decMsg = decMsg.mid(welcomeRegExp.matchedLength());
- CoreIrcChannel* chan = static_cast<CoreIrcChannel*>(
- 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<CoreIrcChannel*>(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());
}
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.
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);
* 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;
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
quassel_add_test(FuncHelpersTest)
+quassel_add_test(IrcDecoderTest)
+
+quassel_add_test(IrcEncoderTest)
+
quassel_add_test(SignalProxyTest
LIBRARIES
Quassel::Test::Util
--- /dev/null
+/***************************************************************************
+ * 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 <ostream>
+
+#include "testglobal.h"
+#include "ircdecoder.h"
+#include "irctag.h"
+
+struct IrcMessage
+{
+ std::map<IrcTagKey, std::string> tags;
+ std::string prefix;
+ std::string cmd;
+ std::vector<std::string> params;
+
+ explicit IrcMessage(std::map<IrcTagKey, std::string> tags, std::string prefix, std::string cmd, std::vector<std::string> params = {})
+ : tags(std::move(tags)), prefix(std::move(prefix)), cmd(std::move(cmd)), params(std::move(params)) {}
+
+ explicit IrcMessage(std::initializer_list<std::pair<const IrcTagKey, std::string>> tags, std::string prefix, std::string cmd, std::vector<std::string> 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<std::string> 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<IrcTagKey, std::string> 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<IrcTagKey, QString> tags;
+ QString prefix;
+ QString cmd;
+ QList<QByteArray> params;
+
+ IrcDecoder::parseMessage([](const QByteArray& data) {
+ return QString::fromUtf8(data);
+ }, QByteArray::fromStdString(message), tags, prefix, cmd, params);
+
+ std::map<IrcTagKey, std::string> actualTags;
+ for (const IrcTagKey& key : tags.keys()) {
+ actualTags[key] = tags[key].toStdString();
+ }
+ std::string actualPrefix = prefix.toStdString();
+ std::string actualCmd = cmd.toStdString();
+ std::vector<std::string> 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"));
+}
--- /dev/null
+/***************************************************************************
+ * 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 <ostream>
+
+#include "testglobal.h"
+#include "ircencoder.h"
+#include "irctag.h"
+
+struct IrcMessage
+{
+ std::map<IrcTagKey, std::string> tags;
+ std::string prefix;
+ std::string cmd;
+ std::vector<std::string> params;
+
+ explicit IrcMessage(std::map<IrcTagKey, std::string> tags, std::string prefix, std::string cmd, std::vector<std::string> params = {})
+ : tags(std::move(tags)), prefix(std::move(prefix)), cmd(std::move(cmd)), params(std::move(params)) {}
+
+ explicit IrcMessage(std::initializer_list<std::pair<const IrcTagKey, std::string>> tags, std::string prefix, std::string cmd, std::vector<std::string> 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<std::string> 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<IrcTagKey, std::string> 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<IrcTagKey, QString> tags;
+ QByteArray prefix = QByteArray::fromStdString(message.prefix);
+ QByteArray cmd = QByteArray::fromStdString(message.cmd);
+ QList<QByteArray> 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<std::string> 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<std::string> 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());
+}