pidgin/pidgin

170078e728c0
Parents c4acd02fdf73
Children 82c861678f39
Implement a parser for ircv3 and add unit tests to it.

This change got pretty big so I didn't implement unescapping tags yet. I did
however put the unit tests in for escaped tags, but they are currently #if 0'd
out.

The unit tests are based on the msg-split test cases from https://github.com/ircdocs/parser-tests/blob/master/tests/msg-split.yaml

Testing Done:
Ran the unit tests.

Bugs closed: PIDGIN-17585

Reviewed at https://reviews.imfreedom.org/r/1874/
--- a/libpurple/protocols/ircv3/meson.build Sat Oct 01 22:10:59 2022 -0500
+++ b/libpurple/protocols/ircv3/meson.build Sun Oct 02 01:22:26 2022 -0500
@@ -1,5 +1,8 @@
IRCV3_SOURCES = [
'purpleircv3core.c',
+ 'purpleircv3core.h',
+ 'purpleircv3parser.c',
+ 'purpleircv3parser.h',
'purpleircv3protocol.c',
'purpleircv3protocol.h',
]
@@ -18,3 +21,5 @@
devenv.append('PURPLE_PLUGIN_PATH', meson.current_build_dir())
endif
+
+subdir('tests')
--- a/libpurple/protocols/ircv3/purpleircv3core.c Sat Oct 01 22:10:59 2022 -0500
+++ b/libpurple/protocols/ircv3/purpleircv3core.c Sun Oct 02 01:22:26 2022 -0500
@@ -24,13 +24,13 @@
#include <purple.h>
+#include "purpleircv3core.h"
+
#include "purpleircv3protocol.h"
/******************************************************************************
* Globals
*****************************************************************************/
-#define PURPLE_IRCV3_DOMAIN (g_quark_from_static_string("ircv3-plugin"))
-
static PurpleProtocol *ircv3_protocol = NULL;
/******************************************************************************
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/protocols/ircv3/purpleircv3core.h Sun Oct 02 01:22:26 2022 -0500
@@ -0,0 +1,26 @@
+/*
+ * Purple - Internet Messaging Library
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * 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) any later version.
+ *
+ * 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, see <https://www.gnu.org/licenses/>.
+ */
+
+#ifndef PURPLE_IRCV3_CORE_H
+#define PURPLE_IRCV3_CORE_H
+
+#include <glib.h>
+
+#define PURPLE_IRCV3_DOMAIN (g_quark_from_static_string("ircv3-plugin"))
+
+#endif /* PURPLE_IRCV3_CORE_H */
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/protocols/ircv3/purpleircv3parser.c Sun Oct 02 01:22:26 2022 -0500
@@ -0,0 +1,335 @@
+/*
+ * Purple - Internet Messaging Library
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * 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) any later version.
+ *
+ * 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, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "purpleircv3parser.h"
+
+#include "purpleircv3core.h"
+
+struct _PurpleIRCv3Parser {
+ GObject parent;
+
+ GRegex *regex_message;
+ GRegex *regex_tags;
+
+ PurpleIRCv3MessageHandler fallback_handler;
+ GHashTable *handlers;
+};
+
+G_DEFINE_TYPE(PurpleIRCv3Parser, purple_ircv3_parser, G_TYPE_OBJECT)
+
+/******************************************************************************
+ * Helpers
+ *****************************************************************************/
+static GHashTable *
+purple_ircv3_parser_parse_tags(PurpleIRCv3Parser *parser,
+ const gchar *tags_string, GError **error)
+{
+ GError *local_error = NULL;
+ GHashTable *tags = NULL;
+ GMatchInfo *info = NULL;
+ gboolean matches = FALSE;
+
+ tags = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
+
+ /* tags_string can never be NULL, because g_match_info_fetch_named always
+ * returns a string. So if we were passed an empty string, just return the
+ * empty hash table.
+ */
+ if(*tags_string == '\0') {
+ return tags;
+ }
+
+ matches = g_regex_match_full(parser->regex_tags, tags_string, -1, 0, 0,
+ &info, &local_error);
+
+ if(local_error != NULL) {
+ g_propagate_error(error, local_error);
+
+ g_match_info_unref(info);
+
+ return tags;
+ }
+
+ if(!matches) {
+ g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0,
+ "failed to parse tags: unknown error");
+
+ g_match_info_unref(info);
+
+ return tags;
+ }
+
+ while(g_match_info_matches(info)) {
+ gchar *key = NULL;
+ gchar *value = NULL;
+
+ key = g_match_info_fetch_named(info, "key");
+ value = g_match_info_fetch_named(info, "value");
+
+ /* the hash table is created with destroy notifies for both key and
+ * value, so there's no need to free the allocated memory right now.
+ */
+ g_hash_table_insert(tags, key, value);
+ g_match_info_next(info, &local_error);
+
+ if(local_error != NULL) {
+ g_propagate_error(error, local_error);
+
+ break;
+ }
+ }
+
+ g_match_info_unref(info);
+
+ return tags;
+}
+
+static guint
+purple_ircv3_parser_extract_params(PurpleIRCv3Parser *parser,
+ GStrvBuilder *builder, const gchar *str)
+{
+ gchar *ptr = NULL;
+ guint count = 0;
+
+ /* Loop through str finding each space separated string. */
+ while(str != NULL && *str != '\0') {
+ /* Look for a space. */
+ ptr = strchr(str, ' ');
+
+ /* If we found one, set it to null terminator and add the string to our
+ * builder.
+ */
+ if(ptr != NULL) {
+ *ptr = '\0';
+ g_strv_builder_add(builder, str);
+
+ /* Move str to the next character as we know there's another
+ * character which might be another null terminator.
+ */
+ str = ptr + 1;
+
+ /* And don't forget to increment the count... ah ah ah! */
+ count++;
+ } else {
+ /* Add the remaining string. */
+ g_strv_builder_add(builder, str);
+
+ /* Give the count another one, ah ah ah! */
+ count++;
+
+ /* Finally break out of the loop. */
+ break;
+ }
+ }
+
+ return count;
+}
+
+static GStrv
+purple_ircv3_parser_build_params(PurpleIRCv3Parser *parser,
+ const gchar *middle, const gchar *coda,
+ const gchar *trailing, guint *n_params)
+{
+ GStrvBuilder *builder = g_strv_builder_new();
+ GStrv result = NULL;
+ guint count = 0;
+
+ *n_params = 0;
+
+ count = purple_ircv3_parser_extract_params(parser, builder, middle);
+ *n_params = *n_params + count;
+
+ if(*coda != '\0') {
+ g_strv_builder_add(builder, trailing);
+ *n_params = *n_params + 1;
+ }
+
+ result = g_strv_builder_end(builder);
+
+ g_strv_builder_unref(builder);
+
+ return result;
+}
+
+/******************************************************************************
+ * Handlers
+ *****************************************************************************/
+static gboolean
+purple_ircv3_fallback_handler(G_GNUC_UNUSED GHashTable *tags,
+ G_GNUC_UNUSED const gchar *source,
+ G_GNUC_UNUSED const gchar *command,
+ G_GNUC_UNUSED guint n_params,
+ G_GNUC_UNUSED GStrv params,
+ GError **error,
+ G_GNUC_UNUSED gpointer data)
+{
+ g_set_error(error, PURPLE_IRCV3_DOMAIN, 0, "no handler for command %s",
+ command);
+
+ return FALSE;
+}
+
+/******************************************************************************
+ * GObject Implementation
+ *****************************************************************************/
+static void
+purple_ircv3_parser_finalize(GObject *obj) {
+ PurpleIRCv3Parser *parser = PURPLE_IRCV3_PARSER(obj);
+
+ g_clear_pointer(&parser->regex_message, g_regex_unref);
+ g_clear_pointer(&parser->regex_tags, g_regex_unref);
+
+ g_hash_table_destroy(parser->handlers);
+
+ G_OBJECT_CLASS(purple_ircv3_parser_parent_class)->finalize(obj);
+}
+
+static void
+purple_ircv3_parser_init(PurpleIRCv3Parser *parser) {
+ parser->regex_message = g_regex_new("(?:@(?<tags>[^ ]+) )?"
+ "(?::(?<source>[^ ]+) +)?"
+ "(?<command>[^ :]+)"
+ "(?: +(?<middle>(?:[^ :]+(?: +[^ :]+)*)))*"
+ "(?<coda> +:(?<trailing>.*)?)?",
+ 0, 0, NULL);
+ g_assert(parser->regex_message != NULL);
+
+ parser->regex_tags = g_regex_new("(?:(?<key>[A-Za-z0-9-\\/]+)"
+ "(?:=(?<value>[^\\r\\n;]*))?(?:;|$))",
+ 0, 0, NULL);
+ g_assert(parser->regex_tags != NULL);
+
+ parser->fallback_handler = purple_ircv3_fallback_handler;
+ parser->handlers = g_hash_table_new_full(g_str_hash, g_str_equal, g_free,
+ NULL);
+}
+
+static void
+purple_ircv3_parser_class_init(PurpleIRCv3ParserClass *klass) {
+ GObjectClass *obj_class = G_OBJECT_CLASS(klass);
+
+ obj_class->finalize = purple_ircv3_parser_finalize;
+}
+
+/******************************************************************************
+ * Public API
+ *****************************************************************************/
+PurpleIRCv3Parser *
+purple_ircv3_parser_new(void) {
+ return g_object_new(PURPLE_IRCV3_TYPE_PARSER, NULL);
+}
+
+void
+purple_ircv3_parser_set_fallback_handler(PurpleIRCv3Parser *parser,
+ PurpleIRCv3MessageHandler handler)
+{
+ g_return_if_fail(PURPLE_IRCV3_IS_PARSER(parser));
+
+ parser->fallback_handler = handler;
+}
+
+gboolean
+purple_ircv3_parser_parse(PurpleIRCv3Parser *parser, const gchar *buffer,
+ GError **error, gpointer data)
+{
+ PurpleIRCv3MessageHandler handler = NULL;
+ GError *local_error = NULL;
+ GHashTable *tags = NULL;
+ GMatchInfo *info = NULL;
+ GStrv params = NULL;
+ gchar *coda = NULL;
+ gchar *command = NULL;
+ gchar *middle = NULL;
+ gchar *source = NULL;
+ gchar *tags_string = NULL;
+ gchar *trailing = NULL;
+ gboolean matches = FALSE;
+ gboolean result = FALSE;
+ guint n_params = 0;
+
+ g_return_val_if_fail(PURPLE_IRCV3_IS_PARSER(parser), FALSE);
+ g_return_val_if_fail(buffer != NULL, FALSE);
+
+ /* Check if the buffer matches our regex for messages. */
+ matches = g_regex_match(parser->regex_message, buffer, 0, &info);
+ if(!matches) {
+ g_set_error(error, PURPLE_IRCV3_DOMAIN, 0,
+ "failed to parser buffer '%s'", buffer);
+
+ g_match_info_unref(info);
+
+ return FALSE;
+ }
+
+ /* Extract the command from the buffer, so we can find the handler. */
+ command = g_match_info_fetch_named(info, "command");
+ handler = g_hash_table_lookup(parser->handlers, command);
+ if(handler == NULL) {
+ if(parser->fallback_handler == NULL) {
+ g_set_error(error, PURPLE_IRCV3_DOMAIN, 0,
+ "no handler found for command %s and no default "
+ "handler set.", command);
+
+ g_free(command);
+ g_match_info_unref(info);
+
+ return FALSE;
+ }
+
+ handler = parser->fallback_handler;
+ }
+
+ /* If we made it this far, we have our handler, so lets get the rest of the
+ * parameters and call the handler.
+ */
+ tags_string = g_match_info_fetch_named(info, "tags");
+ tags = purple_ircv3_parser_parse_tags(parser, tags_string, &local_error);
+ g_free(tags_string);
+ if(local_error != NULL) {
+ g_propagate_error(error, local_error);
+
+ g_free(command);
+ g_hash_table_destroy(tags);
+ g_match_info_unref(info);
+
+ return FALSE;
+ }
+
+ source = g_match_info_fetch_named(info, "source");
+ middle = g_match_info_fetch_named(info, "middle");
+ coda = g_match_info_fetch_named(info, "coda");
+ trailing = g_match_info_fetch_named(info, "trailing");
+
+ params = purple_ircv3_parser_build_params(parser, middle, coda, trailing,
+ &n_params);
+
+ /* Call the handler. */
+ result = handler(tags, source, command, n_params, params, error, data);
+
+ /* Cleanup everything. */
+ g_free(source);
+ g_free(command);
+ g_free(middle);
+ g_free(coda);
+ g_free(trailing);
+ g_strfreev(params);
+ g_hash_table_destroy(tags);
+ g_match_info_unref(info);
+
+ return result;
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/protocols/ircv3/purpleircv3parser.h Sun Oct 02 01:22:26 2022 -0500
@@ -0,0 +1,79 @@
+/*
+ * Purple - Internet Messaging Library
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * 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) any later version.
+ *
+ * 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, see <https://www.gnu.org/licenses/>.
+ */
+
+#ifndef PURPLE_IRCV3_PARSER_H
+#define PURPLE_IRCV3_PARSER_H
+
+#include <glib.h>
+#include <glib-object.h>
+
+#include <purple.h>
+
+G_BEGIN_DECLS
+
+typedef gboolean (*PurpleIRCv3MessageHandler)(GHashTable *tags,
+ const gchar *source,
+ const gchar *command,
+ guint n_params,
+ GStrv params,
+ GError **error,
+ gpointer data);
+
+#define PURPLE_IRCV3_TYPE_PARSER (purple_ircv3_parser_get_type())
+G_DECLARE_FINAL_TYPE(PurpleIRCv3Parser, purple_ircv3_parser, PURPLE_IRCV3,
+ PARSER, GObject)
+
+/**
+ * purple_ircv3_parser_new:
+ *
+ * Creates a new instance.
+ *
+ * Since: 3.0.0
+ */
+G_GNUC_INTERNAL PurpleIRCv3Parser *purple_ircv3_parser_new(void);
+
+/**
+ * purple_ircv3_parser_set_fallback_handler: (skip):
+ * @parser: The instance.
+ * @handler: A [func@PurpleIRCv3.MessageHandler].
+ *
+ * Sets @handler to be called for any messages that @parser doesn't know how to
+ * handle.
+ *
+ * Since: 3.0.0
+ */
+G_GNUC_INTERNAL void purple_ircv3_parser_set_fallback_handler(PurpleIRCv3Parser *parser, PurpleIRCv3MessageHandler handler);
+
+/**
+ * purple_ircv3_parser_parse:
+ * @parser: The instance.
+ * @buffer: The buffer to parse.
+ * @error: (nullable) (out): A return address for a [boxed@Glib.GError].
+ * @data: (nullable): Optional data to pass to the handler.
+ *
+ * Parses @buffer with @parser.
+ *
+ * Returns: %TRUE if the buffer was parsed correctly or %FALSE with @error set.
+ *
+ * Since: 3.0.0
+ */
+G_GNUC_INTERNAL gboolean purple_ircv3_parser_parse(PurpleIRCv3Parser *parser, const gchar *buffer, GError **error, gpointer data);
+
+G_END_DECLS
+
+#endif /* PURPLE_IRCV3_PARSER_H */
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/protocols/ircv3/tests/meson.build Sun Oct 02 01:22:26 2022 -0500
@@ -0,0 +1,12 @@
+TESTS = [
+ 'parser',
+]
+
+foreach prog : TESTS
+ e = executable(
+ 'test_ircv3_' + prog, 'test_ircv3_@0@.c'.format(prog),
+ dependencies : [libpurple_dep, glib],
+ objects : ircv3_prpl.extract_all_objects())
+
+ test('ircv3_' + prog, e)
+endforeach
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/protocols/ircv3/tests/test_ircv3_parser.c Sun Oct 02 01:22:26 2022 -0500
@@ -0,0 +1,704 @@
+/*
+ * Purple - Internet Messaging Library
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <glib.h>
+
+#include <purple.h>
+
+#include "../purpleircv3parser.h"
+
+typedef struct {
+ GHashTable *tags;
+ gchar *source;
+ gchar *command;
+ guint n_params;
+ const gchar * const params[16];
+} TestPurpleIRCv3ParserData;
+
+/******************************************************************************
+ * Handlers
+ *****************************************************************************/
+static gboolean
+test_purple_ircv3_test_handler(GHashTable *tags, const gchar *source,
+ const gchar *command, guint n_params,
+ GStrv params, GError **error, gpointer data)
+{
+ TestPurpleIRCv3ParserData *d = data;
+ GHashTableIter iter;
+
+ /* Make sure we have an expected tags hash table before checking them. */
+ if(d->tags != NULL) {
+ gpointer expected_key;
+ gpointer expected_value;
+ guint actual_size;
+ guint expected_size;
+
+ /* Make sure the tag hash tables have the same size. */
+ expected_size = g_hash_table_size(d->tags);
+ actual_size = g_hash_table_size(tags);
+ g_assert_cmpuint(actual_size, ==, expected_size);
+
+ /* Since the tables have the same size, we can walk through the expected
+ * table and use it to verify the actual table.
+ */
+ g_hash_table_iter_init(&iter, d->tags);
+ while(g_hash_table_iter_next(&iter, &expected_key, &expected_value)) {
+ gpointer actual_value = NULL;
+ gboolean found = FALSE;
+
+ found = g_hash_table_lookup_extended(tags, expected_key, NULL,
+ &actual_value);
+ g_assert_true(found);
+ g_assert_cmpstr(actual_value, ==, expected_value);
+ }
+ }
+
+ /* If the expected strings values are NULL, set them to empty string as
+ * that's what g_match_info_get_named will return for them.
+ */
+ if(d->source == NULL) {
+ d->source = "";
+ }
+
+ if(d->command == NULL) {
+ d->command = "";
+ }
+
+ /* Walk through the params checking against the expected values. */
+ if(d->n_params > 0) {
+ g_assert_cmpuint(n_params, ==, d->n_params);
+
+ for(guint i = 0; i < d->n_params; i++) {
+ g_assert_cmpstr(params[i], ==, d->params[i]);
+ }
+ }
+
+ /* Validate all the string parameters. */
+ g_assert_cmpstr(source, ==, d->source);
+ g_assert_cmpstr(command, ==, d->command);
+
+ /* Cleanup everything the caller allocated. */
+ g_clear_pointer(&d->tags, g_hash_table_destroy);
+
+ /* Return the return value the caller asked for. */
+ return TRUE;
+}
+
+/******************************************************************************
+ * Helpers
+ *****************************************************************************/
+static void
+test_purple_ircv3_parser(const gchar *source, TestPurpleIRCv3ParserData *d) {
+ PurpleIRCv3Parser *parser = purple_ircv3_parser_new();
+ GError *error = NULL;
+ gboolean result = FALSE;
+
+ purple_ircv3_parser_set_fallback_handler(parser,
+ test_purple_ircv3_test_handler);
+
+ result = purple_ircv3_parser_parse(parser, source, &error, d);
+
+ g_assert_no_error(error);
+ g_assert_true(result);
+
+ g_clear_object(&parser);
+}
+
+/******************************************************************************
+ * Tests
+ *****************************************************************************/
+static void
+test_purple_ircv3_parser_simple(void) {
+ TestPurpleIRCv3ParserData data = {
+ .command = "foo",
+ .n_params = 3,
+ .params = {"bar", "baz", "asdf"},
+ };
+
+ test_purple_ircv3_parser("foo bar baz asdf", &data);
+}
+
+static void
+test_purple_ircv3_parser_with_source(void) {
+ TestPurpleIRCv3ParserData data = {
+ .source = "coolguy",
+ .command = "foo",
+ .n_params = 3,
+ .params = {"bar", "baz", "asdf"},
+ };
+
+ test_purple_ircv3_parser(":coolguy foo bar baz asdf", &data);
+}
+
+static void
+test_purple_ircv3_parser_with_trailing(void) {
+ TestPurpleIRCv3ParserData data = {
+ .command = "foo",
+ .n_params = 3,
+ .params = {"bar", "baz", "asdf quux"},
+ };
+
+ test_purple_ircv3_parser("foo bar baz :asdf quux", &data);
+}
+
+static void
+test_purple_ircv3_parser_with_empty_trailing(void) {
+ TestPurpleIRCv3ParserData data = {
+ .command = "foo",
+ .n_params = 3,
+ .params = {"bar", "baz", ""},
+ };
+
+ test_purple_ircv3_parser("foo bar baz :", &data);
+}
+
+static void
+test_purple_ircv3_parser_with_trailing_starting_colon(void) {
+ TestPurpleIRCv3ParserData data = {
+ .command = "foo",
+ .n_params = 3,
+ .params = {"bar", "baz", ":asdf"},
+ };
+
+ test_purple_ircv3_parser("foo bar baz ::asdf", &data);
+}
+
+static void
+test_purple_ircv3_parser_with_source_and_trailing(void) {
+ TestPurpleIRCv3ParserData data = {
+ .source = "coolguy",
+ .command = "foo",
+ .n_params = 3,
+ .params = {"bar", "baz", "asdf quux"},
+ };
+
+ test_purple_ircv3_parser(":coolguy foo bar baz :asdf quux", &data);
+}
+
+static void
+test_purple_ircv3_parser_with_source_and_trailing_whitespace(void) {
+ TestPurpleIRCv3ParserData data = {
+ .source = "coolguy",
+ .command = "foo",
+ .n_params = 3,
+ .params = {"bar", "baz", " asdf quux "},
+ };
+
+ test_purple_ircv3_parser(":coolguy foo bar baz : asdf quux ", &data);
+}
+
+static void
+test_purple_ircv3_parser_with_source_and_trailing_colon(void) {
+ TestPurpleIRCv3ParserData data = {
+ .source = "coolguy",
+ .command = "PRIVMSG",
+ .n_params = 2,
+ .params = {"bar", "lol :) "},
+ };
+
+ test_purple_ircv3_parser(":coolguy PRIVMSG bar :lol :) ", &data);
+}
+
+static void
+test_purple_ircv3_parser_with_source_and_empty_trailing(void) {
+ TestPurpleIRCv3ParserData data = {
+ .source = "coolguy",
+ .command = "foo",
+ .n_params = 3,
+ .params = {"bar", "baz", ""},
+ };
+
+ test_purple_ircv3_parser(":coolguy foo bar baz :", &data);
+}
+
+static void
+test_purple_ircv3_parser_with_source_and_trailing_only_whitespace(void) {
+ TestPurpleIRCv3ParserData data = {
+ .source = "coolguy",
+ .command = "foo",
+ .n_params = 3,
+ .params = {"bar", "baz", " "},
+ };
+
+ test_purple_ircv3_parser(":coolguy foo bar baz : ", &data);
+}
+
+static void
+test_purple_ircv3_parser_with_tags(void) {
+ TestPurpleIRCv3ParserData data = {
+ .command = "foo",
+ };
+
+ data.tags = g_hash_table_new(g_str_hash, g_str_equal);
+ g_hash_table_insert(data.tags, "a", "b");
+ g_hash_table_insert(data.tags, "c", "32");
+ g_hash_table_insert(data.tags, "k", "");
+ g_hash_table_insert(data.tags, "rt", "ql7");
+
+ test_purple_ircv3_parser("@a=b;c=32;k;rt=ql7 foo", &data);
+}
+
+static void
+test_purple_ircv3_parser_with_escaped_tags(void) {
+#if 0
+/* Escaped tags aren't implemented yet. */
+ TestPurpleIRCv3ParserData data = {
+ .command = "foo",
+ };
+
+ data.tags = g_hash_table_new(g_str_hash, g_str_equal);
+ g_hash_table_insert(data.tags, "a", "b\\and\nk");
+ g_hash_table_insert(data.tags, "c", "72 45");
+ g_hash_table_insert(data.tags, "d", "gh;764");
+
+ test_purple_ircv3_parser("@a=b\\\\and\\nk;c=72\\s45;d=gh\\:764 foo",
+ &data);
+#endif
+}
+
+static void
+test_purple_ircv3_with_tags_and_source(void) {
+ TestPurpleIRCv3ParserData data = {
+ .source = "quux",
+ .command = "ab",
+ .n_params = 1,
+ .params = {"cd"},
+ };
+
+ data.tags = g_hash_table_new(g_str_hash, g_str_equal);
+ g_hash_table_insert(data.tags, "c", "");
+ g_hash_table_insert(data.tags, "h", "");
+ g_hash_table_insert(data.tags, "a", "b");
+
+ test_purple_ircv3_parser("@c;h=;a=b :quux ab cd", &data);
+}
+
+static void
+test_purple_ircv3_last_param_no_colon(void) {
+ TestPurpleIRCv3ParserData data = {
+ .source = "src",
+ .command = "JOIN",
+ .n_params = 1,
+ .params = {"#chan"},
+ };
+
+ test_purple_ircv3_parser(":src JOIN #chan", &data);
+}
+
+static void
+test_purple_ircv3_last_param_with_colon(void) {
+ TestPurpleIRCv3ParserData data = {
+ .source = "src",
+ .command = "JOIN",
+ .n_params = 1,
+ .params = {"#chan"},
+ };
+
+ test_purple_ircv3_parser(":src JOIN :#chan", &data);
+}
+
+static void
+test_purple_ircv3_without_last_param(void) {
+ TestPurpleIRCv3ParserData data = {
+ .source = "src",
+ .command = "AWAY",
+ };
+
+ test_purple_ircv3_parser(":src AWAY", &data);
+}
+
+static void
+test_purple_ircv3_with_last_param(void) {
+ TestPurpleIRCv3ParserData data = {
+ .source = "src",
+ .command = "AWAY",
+ };
+
+ test_purple_ircv3_parser(":src AWAY ", &data);
+}
+
+static void
+test_purple_ircv3_tab_is_not_space(void) {
+ TestPurpleIRCv3ParserData data = {
+ .source = "cool\tguy",
+ .command = "foo",
+ .n_params = 2,
+ .params = {"bar", "baz"},
+ };
+
+ test_purple_ircv3_parser(":cool\tguy foo bar baz", &data);
+}
+
+static void
+test_purple_ircv3_source_control_characters_1(void) {
+ /* Break each string after the hex escape as they are supposed to only be
+ * a single byte, but the c compiler will keep unescaping unless we break
+ * the string.
+ */
+ TestPurpleIRCv3ParserData data = {
+ .source = "coolguy!ag@net\x03" "5w\x03" "ork.admin",
+ .command = "PRIVMSG",
+ .n_params = 2,
+ .params = {"foo", "bar baz"},
+ };
+ const gchar *msg = NULL;
+
+ msg = ":coolguy!ag@net\x03" "5w\x03" "ork.admin PRIVMSG foo :bar baz";
+
+ test_purple_ircv3_parser(msg, &data);
+}
+
+static void
+test_purple_ircv3_source_control_characters_2(void) {
+ /* Break each string after the hex escape as they are supposed to only be
+ * a single byte, but the c compiler will keep unescaping unless we break
+ * the string.
+ */
+ TestPurpleIRCv3ParserData data = {
+ .source = "coolguy!~ag@n\x02" "et\x03" "05w\x0f" "ork.admin",
+ .command = "PRIVMSG",
+ .n_params = 2,
+ .params = {"foo", "bar baz"},
+ };
+ const gchar *msg = NULL;
+
+ msg = ":coolguy!~ag@n\x02" "et\x03" "05w\x0f" "ork.admin PRIVMSG foo :bar "
+ "baz";
+
+ test_purple_ircv3_parser(msg, &data);
+}
+
+static void
+test_purple_ircv3_everything(void) {
+ TestPurpleIRCv3ParserData data = {
+ .source = "irc.example.com",
+ .command = "COMMAND",
+ .n_params = 3,
+ .params = {"param1", "param2", "param3 param3"},
+ };
+ const gchar *msg = NULL;
+
+ data.tags = g_hash_table_new(g_str_hash, g_str_equal);
+ g_hash_table_insert(data.tags, "tag1", "value1");
+ g_hash_table_insert(data.tags, "tag2", "");
+ g_hash_table_insert(data.tags, "vendor1/tag3", "value2");
+ g_hash_table_insert(data.tags, "vendor2/tag4", "");
+
+ msg = "@tag1=value1;tag2;vendor1/tag3=value2;vendor2/tag4= "
+ ":irc.example.com COMMAND param1 param2 :param3 param3";
+
+ test_purple_ircv3_parser(msg, &data);
+}
+
+static void
+test_purple_ircv3_everything_but_tags(void) {
+ TestPurpleIRCv3ParserData data = {
+ .source = "irc.example.com",
+ .command = "COMMAND",
+ .n_params = 3,
+ .params = {"param1", "param2", "param3 param3"},
+ };
+ const gchar *msg = NULL;
+
+ msg = ":irc.example.com COMMAND param1 param2 :param3 param3";
+
+ test_purple_ircv3_parser(msg, &data);
+}
+
+static void
+test_purple_ircv3_everything_but_source(void) {
+ TestPurpleIRCv3ParserData data = {
+ .command = "COMMAND",
+ .n_params = 3,
+ .params = {"param1", "param2", "param3 param3"},
+ };
+ const gchar *msg = NULL;
+
+ data.tags = g_hash_table_new(g_str_hash, g_str_equal);
+ g_hash_table_insert(data.tags, "tag1", "value1");
+ g_hash_table_insert(data.tags, "tag2", "");
+ g_hash_table_insert(data.tags, "vendor1/tag3", "value2");
+ g_hash_table_insert(data.tags, "vendor2/tag4", "");
+
+ msg = "@tag1=value1;tag2;vendor1/tag3=value2;vendor2/tag4 "
+ "COMMAND param1 param2 :param3 param3";
+
+ test_purple_ircv3_parser(msg, &data);
+}
+
+static void
+test_purple_ircv3_command_only(void) {
+ TestPurpleIRCv3ParserData data = {
+ .command = "COMMAND",
+ };
+
+ test_purple_ircv3_parser("COMMAND", &data);
+}
+
+static void
+test_purple_ircv3_slashes_are_fun(void) {
+#if 0
+/* Escaped tags aren't implemented yet. */
+ TestPurpleIRCv3ParserData data = {
+ .command = "COMMAND",
+ };
+
+ data.tags = g_hash_table_new(g_str_hash, g_str_equal);
+ g_hash_table_insert(data.tags, "foo", "\\\\;\\s \r\n");
+
+ test_purple_ircv3_parser("@foo=\\\\\\\\\\:\\\\s\\s\\r\\n COMMAND", &data);
+#endif
+}
+
+static void
+test_purple_ircv3_unreal_broken_1(void) {
+ TestPurpleIRCv3ParserData data = {
+ .source = "gravel.mozilla.org",
+ .command = "432",
+ .n_params = 2,
+ .params = {"#momo", "Erroneous Nickname: Illegal characters"},
+ };
+ const gchar *msg = NULL;
+
+ msg = ":gravel.mozilla.org 432 #momo :Erroneous Nickname: Illegal "
+ "characters";
+
+ test_purple_ircv3_parser(msg, &data);
+}
+
+static void
+test_purple_ircv3_unreal_broken_2(void) {
+ TestPurpleIRCv3ParserData data = {
+ .source = "gravel.mozilla.org",
+ .command = "MODE",
+ .n_params = 2,
+ .params = {"#tckk", "+n"},
+ };
+
+ test_purple_ircv3_parser(":gravel.mozilla.org MODE #tckk +n ", &data);
+}
+
+static void
+test_purple_ircv3_unreal_broken_3(void) {
+ TestPurpleIRCv3ParserData data = {
+ .source = "services.esper.net",
+ .command = "MODE",
+ .n_params = 3,
+ .params = {"#foo-bar", "+o", "foobar"},
+ };
+
+ test_purple_ircv3_parser(":services.esper.net MODE #foo-bar +o foobar ",
+ &data);
+}
+
+static void
+test_purple_ircv3_tag_escape_char_at_a_time(void) {
+#if 0
+/* Escaped tags aren't implemented yet. */
+ TestPurpleIRCv3ParserData data = {
+ .command = "COMMAND",
+ };
+
+ data.tags = g_hash_table_new(g_str_hash, g_str_equal);
+ g_hash_table_insert(data.tags, "tag1", "value\\ntest");
+
+ test_purple_ircv3_parser("@tag1=value\\\\ntest COMMAND", &data);
+#endif
+}
+
+static void
+test_purple_ircv3_tag_drop_unnecessary_escapes(void) {
+#if 0
+ TestPurpleIRCv3ParserData data = {
+ .command = "COMMAND",
+ };
+
+ data.tags = g_hash_table_new(g_str_hash, g_str_equal);
+ g_hash_table_insert(data.tags, "tag1", "value1");
+
+ test_purple_ircv3_parser("@tag1=value\\1 COMMAND", &data);
+#endif
+}
+
+static void
+test_purple_ircv3_tag_drop_trailing_slash(void) {
+#if 0
+/* Escaped tags aren't implemented yet. */
+ TestPurpleIRCv3ParserData data = {
+ .command = "COMMAND",
+ };
+
+ data.tags = g_hash_table_new(g_str_hash, g_str_equal);
+ g_hash_table_insert(data.tags, "tag1", "value1");
+
+ test_purple_ircv3_parser("@tag1=value1\\ COMMAND", &data);
+#endif
+}
+
+static void
+test_purple_ircv3_duplicate_tags(void) {
+ TestPurpleIRCv3ParserData data = {
+ .command = "COMMAND",
+ };
+
+ data.tags = g_hash_table_new(g_str_hash, g_str_equal);
+ g_hash_table_insert(data.tags, "tag1", "5");
+ g_hash_table_insert(data.tags, "tag2", "3");
+ g_hash_table_insert(data.tags, "tag3", "4");
+
+ test_purple_ircv3_parser("@tag1=1;tag2=3;tag3=4;tag1=5 COMMAND", &data);
+}
+
+static void
+test_purple_ircv3_vendor_tags_are_namespaced(void) {
+ TestPurpleIRCv3ParserData data = {
+ .command = "COMMAND",
+ };
+ const gchar *msg = NULL;
+
+ data.tags = g_hash_table_new(g_str_hash, g_str_equal);
+ g_hash_table_insert(data.tags, "tag1", "5");
+ g_hash_table_insert(data.tags, "tag2", "3");
+ g_hash_table_insert(data.tags, "tag3", "4");
+ g_hash_table_insert(data.tags, "vendor/tag2", "8");
+
+ msg = "@tag1=1;tag2=3;tag3=4;tag1=5;vendor/tag2=8 COMMAND";
+
+ test_purple_ircv3_parser(msg, &data);
+}
+
+static void
+test_purple_ircv3_special_mode_1(void) {
+ TestPurpleIRCv3ParserData data = {
+ .source = "SomeOp",
+ .command = "MODE",
+ .n_params = 2,
+ .params = {"#channel", "+i"},
+ };
+
+ test_purple_ircv3_parser(":SomeOp MODE #channel :+i", &data);
+}
+
+static void
+test_purple_ircv3_special_mode_2(void) {
+ TestPurpleIRCv3ParserData data = {
+ .source = "SomeOp",
+ .command = "MODE",
+ .n_params = 4,
+ .params = {"#channel", "+oo", "SomeUser", "AnotherUser"},
+ };
+
+ test_purple_ircv3_parser(":SomeOp MODE #channel +oo SomeUser :AnotherUser",
+ &data);
+}
+
+/******************************************************************************
+ * Main
+ *****************************************************************************/
+gint
+main(gint argc, gchar *argv[]) {
+ g_test_init(&argc, &argv, NULL);
+
+ /* These tests are based on the msg-split tests from
+ * https://github.com/ircdocs/parser-tests/blob/master/tests/msg-split.yaml
+ */
+ g_test_add_func("/ircv3/parser/simple",
+ test_purple_ircv3_parser_simple);
+ g_test_add_func("/ircv3/parser/with-source",
+ test_purple_ircv3_parser_with_source);
+ g_test_add_func("/ircv3/parser/with-trailing",
+ test_purple_ircv3_parser_with_trailing);
+ g_test_add_func("/ircv3/parser/with-empty-trailing",
+ test_purple_ircv3_parser_with_empty_trailing);
+ g_test_add_func("/ircv3/parser/with-trailing-starting-colon",
+ test_purple_ircv3_parser_with_trailing_starting_colon);
+ g_test_add_func("/ircv3/parser/with-source-and-trailing",
+ test_purple_ircv3_parser_with_source_and_trailing);
+ g_test_add_func("/ircv3/parser/with-source-and-trailing-whitespace",
+ test_purple_ircv3_parser_with_source_and_trailing_whitespace);
+ g_test_add_func("/ircv3/parser/with-source-and-trailing-colon",
+ test_purple_ircv3_parser_with_source_and_trailing_colon);
+ g_test_add_func("/ircv3/parser/with-source-and-empty-trailing",
+ test_purple_ircv3_parser_with_source_and_empty_trailing);
+ g_test_add_func("/ircv3/parser/with-source-and-trailing-only-whitespace",
+ test_purple_ircv3_parser_with_source_and_trailing_only_whitespace);
+
+ g_test_add_func("/ircv3/parser/with-tags",
+ test_purple_ircv3_parser_with_tags);
+ g_test_add_func("/ircv3/parser/with-escaped-tags",
+ test_purple_ircv3_parser_with_escaped_tags);
+ g_test_add_func("/ircv3/parser/with-tags-and-source",
+ test_purple_ircv3_with_tags_and_source);
+
+ g_test_add_func("/ircv3/parser/last-param-no-colon",
+ test_purple_ircv3_last_param_no_colon);
+ g_test_add_func("/ircv3/parser/last-param-with-colon",
+ test_purple_ircv3_last_param_with_colon);
+
+ g_test_add_func("/ircv3/parser/without-last-param",
+ test_purple_ircv3_without_last_param);
+ g_test_add_func("/ircv3/parser/with-last-parsm",
+ test_purple_ircv3_with_last_param);
+
+ g_test_add_func("/ircv3/parser/tab-is-not-space",
+ test_purple_ircv3_tab_is_not_space);
+
+ g_test_add_func("/ircv3/parser/source_control_characters_1",
+ test_purple_ircv3_source_control_characters_1);
+ g_test_add_func("/ircv3/parser/source_control_characters_2",
+ test_purple_ircv3_source_control_characters_2);
+
+ g_test_add_func("/ircv3/parser/everything",
+ test_purple_ircv3_everything);
+ g_test_add_func("/ircv3/parser/everything-but-tags",
+ test_purple_ircv3_everything_but_tags);
+ g_test_add_func("/ircv3/parser/everything-but-source",
+ test_purple_ircv3_everything_but_source);
+
+ g_test_add_func("/ircv3/parser/command-only",
+ test_purple_ircv3_command_only);
+
+ g_test_add_func("/ircv3/parser/slashes-are-fun",
+ test_purple_ircv3_slashes_are_fun);
+
+ g_test_add_func("/ircv3/parser/unreal-broken-1",
+ test_purple_ircv3_unreal_broken_1);
+ g_test_add_func("/ircv3/parser/unreal-broken-2",
+ test_purple_ircv3_unreal_broken_2);
+ g_test_add_func("/ircv3/parser/unreal-broken-3",
+ test_purple_ircv3_unreal_broken_3);
+
+ g_test_add_func("/ircv3/parser/tag-escape-char-at-a-time",
+ test_purple_ircv3_tag_escape_char_at_a_time);
+ g_test_add_func("/ircv3/parser/tag-drop-unnecessary-escapes",
+ test_purple_ircv3_tag_drop_unnecessary_escapes);
+ g_test_add_func("/ircv3/parser/tag-drop-trailing-slash",
+ test_purple_ircv3_tag_drop_trailing_slash);
+
+ g_test_add_func("/ircv3/parser/duplicate-tags",
+ test_purple_ircv3_duplicate_tags);
+ g_test_add_func("/ircv3/parser/vendor-tags-are-namespaced",
+ test_purple_ircv3_vendor_tags_are_namespaced);
+
+ g_test_add_func("/ircv3/parser/special-mode-1",
+ test_purple_ircv3_special_mode_1);
+ g_test_add_func("/ircv3/parser/special-mode-2",
+ test_purple_ircv3_special_mode_2);
+
+ return g_test_run();
+}