Implement IRCv3 tag parsing and sending
authorJanne Koschinski <janne@kuschku.de>
Tue, 12 Feb 2019 10:54:06 +0000 (11:54 +0100)
committerManuel Nickschas <sputnick@quassel-irc.org>
Thu, 30 May 2019 20:59:25 +0000 (22:59 +0200)
17 files changed:
src/common/CMakeLists.txt
src/common/ircdecoder.cpp [new file with mode: 0644]
src/common/ircdecoder.h [new file with mode: 0644]
src/common/ircencoder.cpp [new file with mode: 0644]
src/common/ircencoder.h [new file with mode: 0644]
src/common/irctag.cpp [new file with mode: 0644]
src/common/irctag.h [new file with mode: 0644]
src/core/corebasichandler.cpp
src/core/corebasichandler.h
src/core/corenetwork.cpp
src/core/corenetwork.h
src/core/coreuserinputhandler.cpp
src/core/ircparser.cpp
src/core/ircparser.h
tests/common/CMakeLists.txt
tests/common/ircdecodertest.cpp [new file with mode: 0644]
tests/common/ircencodertest.cpp [new file with mode: 0644]

index f53988d..417e5ff 100644 (file)
@@ -25,6 +25,9 @@ target_sources(${TARGET} PRIVATE
     ircchannel.cpp
     ircevent.cpp
     irclisthelper.cpp
     ircchannel.cpp
     ircevent.cpp
     irclisthelper.cpp
+    ircdecoder.cpp
+    ircencoder.cpp
+    irctag.cpp
     ircuser.cpp
     logger.cpp
     message.cpp
     ircuser.cpp
     logger.cpp
     message.cpp
diff --git a/src/common/ircdecoder.cpp b/src/common/ircdecoder.cpp
new file mode 100644 (file)
index 0000000..d51ed55
--- /dev/null
@@ -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 <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;
+}
diff --git a/src/common/ircdecoder.h b/src/common/ircdecoder.h
new file mode 100644 (file)
index 0000000..e4515ec
--- /dev/null
@@ -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 <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);
+};
diff --git a/src/common/ircencoder.cpp b/src/common/ircencoder.cpp
new file mode 100644 (file)
index 0000000..2be3022
--- /dev/null
@@ -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<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];
+    }
+}
diff --git a/src/common/ircencoder.h b/src/common/ircencoder.h
new file mode 100644 (file)
index 0000000..68fc46c
--- /dev/null
@@ -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 <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);
+};
diff --git a/src/common/irctag.cpp b/src/common/irctag.cpp
new file mode 100644 (file)
index 0000000..2c9c221
--- /dev/null
@@ -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 (file)
index 0000000..499bbcc
--- /dev/null
@@ -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 <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);
+};
index 2d51a3a..0018d22 100644 (file)
@@ -29,13 +29,13 @@ CoreBasicHandler::CoreBasicHandler(CoreNetwork* parent)
     connect(this, &CoreBasicHandler::displayMsg, network(), &CoreNetwork::onDisplayMsg);
     connect(this, &CoreBasicHandler::putRawLine, network(), &CoreNetwork::putRawLine);
     connect(this,
     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(),
             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,
     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(),
             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)
 }
 
 QString CoreBasicHandler::serverDecode(const QByteArray& string)
@@ -132,9 +132,9 @@ BufferInfo::Type CoreBasicHandler::typeByTarget(const QString& target) const
     return BufferInfo::QueryBuffer;
 }
 
     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;
 {
     QList<QByteArray> list;
     list << param;
-    emit putCmd(cmd, list, prefix, prepend);
+    emit putCmd(cmd, list, prefix, tags, prepend);
 }
 }
index 37f629d..697775a 100644 (file)
@@ -64,23 +64,23 @@ signals:
      *
      * @see CoreNetwork::putRawLine()
      */
      *
      * @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.
      *
 
     /**
      * Sends the command with encoded parameters, with optional prefix or high priority.
      *
-     * @see CoreNetwork::putCmd(const QString &cmd, const QList<QByteArray> &params, const QByteArray &prefix = QByteArray(), const bool
-     * prepend = false)
+     * @see CoreNetwork::putCmd(const QString &cmd, const QList<QByteArray> &params, 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>> &params, const QByteArray &prefix = QByteArray(), const
 
     /**
      * 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>> &params, 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:
     /**
 
 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] 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
      * @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
      */
      * 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(); }
 
     inline CoreNetwork* network() const { return _network; }
     inline CoreSession* coreSession() const { return _network->coreSession(); }
index d7215a4..2f14a5b 100644 (file)
 #include "corenetworkconfig.h"
 #include "coresession.h"
 #include "coreuserinputhandler.h"
 #include "corenetworkconfig.h"
 #include "coresession.h"
 #include "coreuserinputhandler.h"
-#include "networkevent.h"
-
-// IRCv3 capabilities
+#include "ircencoder.h"
 #include "irccap.h"
 #include "irccap.h"
+#include "irctag.h"
+#include "networkevent.h"
 
 CoreNetwork::CoreNetwork(const NetworkId& networkid, CoreSession* session)
     : Network(networkid, session)
 
 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<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();
 {
     QListIterator<QList<QByteArray>> i(params);
     while (i.hasNext()) {
         QList<QByteArray> msg = i.next();
-        putCmd(cmd, msg, prefix, prependAll);
+        putCmd(cmd, msg, prefix, tags, prependAll);
     }
 }
 
     }
 }
 
index 3ea739d..f07f2a2 100644 (file)
 
 #pragma once
 
 
 #pragma once
 
-#include "coreircchannel.h"
-#include "coreircuser.h"
-#include "network.h"
+#include <functional>
 
 
-// IRCv3 capabilities
 #include <QTimer>
 
 #include <QTimer>
 
-#include "irccap.h"
-
 #ifdef HAVE_SSL
 #    include <QSslError>
 #    include <QSslSocket>
 #ifdef HAVE_SSL
 #    include <QSslError>
 #    include <QSslSocket>
 #    include "cipher.h"
 #endif
 
 #    include "cipher.h"
 #endif
 
-#include <functional>
-
+#include "coreircchannel.h"
+#include "coreircuser.h"
 #include "coresession.h"
 #include "coresession.h"
+#include "irccap.h"
+#include "irctag.h"
+#include "network.h"
 
 class CoreIdentity;
 class CoreUserInputHandler;
 
 class CoreIdentity;
 class CoreUserInputHandler;
@@ -280,7 +278,7 @@ public slots:
      * maintain PING/PONG replies, the other side will close the connection.
      * @endparmblock
      */
      * 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.
 
     /**
      * 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
      */
      * 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);
 
     void setChannelJoined(const QString& channel);
     void setChannelParted(const QString& channel);
index 07a2b4b..03bee36 100644 (file)
@@ -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.
         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::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
 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)
 }
 
 void CoreUserInputHandler::handleQuote(const BufferInfo& bufferInfo, const QString& msg)
index bfbc391..f5b3784 100644 (file)
@@ -24,6 +24,7 @@
 
 #include "corenetwork.h"
 #include "eventmanager.h"
 
 #include "corenetwork.h"
 #include "eventmanager.h"
+#include "ircdecoder.h"
 #include "ircevent.h"
 #include "messageevent.h"
 #include "networkevent.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();
 
     // 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;
     }
         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
     // 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;
     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;
 
 
     QList<Event*> events;
     EventManager::EventType type = EventManager::Invalid;
 
+    QString messageTarget;
     uint num = cmd.toUInt();
     if (num > 0) {
         // numeric reply
         if (params.count() == 0) {
     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!
             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::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) {
         if (type == EventManager::Invalid) {
-            type = eventManager()->eventTypeByName("IrcEventUnknown");
+            type = EventManager::eventTypeByName("IrcEventUnknown");
             Q_ASSERT(type != EventManager::Invalid);
         }
             Q_ASSERT(type != EventManager::Invalid);
         }
-        target = QString();
     }
 
     // Almost always, all params are server-encoded. There's a few exceptions, let's catch them here!
     }
 
     // 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());
                     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());
                         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... */
             }
             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.
                 // :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)
 
         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);
         else
             event = new IrcEvent(type, net, prefix);
         event->setParams(decParams);
index 591a4ee..1485337 100644 (file)
  *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.         *
  ***************************************************************************/
 
  *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.         *
  ***************************************************************************/
 
-#ifndef IRCPARSER_H
-#define IRCPARSER_H
+#pragma once
 
 #include "coresession.h"
 
 #include "coresession.h"
+#include "irctag.h"
 
 class Event;
 class EventManager;
 
 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
 };
     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
index 6b84646..9ca7d4f 100644 (file)
@@ -2,6 +2,10 @@ quassel_add_test(ExpressionMatchTest)
 
 quassel_add_test(FuncHelpersTest)
 
 
 quassel_add_test(FuncHelpersTest)
 
+quassel_add_test(IrcDecoderTest)
+
+quassel_add_test(IrcEncoderTest)
+
 quassel_add_test(SignalProxyTest
     LIBRARIES
         Quassel::Test::Util
 quassel_add_test(SignalProxyTest
     LIBRARIES
         Quassel::Test::Util
diff --git a/tests/common/ircdecodertest.cpp b/tests/common/ircdecodertest.cpp
new file mode 100644 (file)
index 0000000..c32fd5e
--- /dev/null
@@ -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 <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"));
+}
diff --git a/tests/common/ircencodertest.cpp b/tests/common/ircencodertest.cpp
new file mode 100644 (file)
index 0000000..95c0213
--- /dev/null
@@ -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 <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());
+}