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 @@
@@ -18,3 +21,5 @@
devenv.append('PURPLE_PLUGIN_PATH', meson.current_build_dir())
--- 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 "purpleircv3core.h" #include "purpleircv3protocol.h"
/******************************************************************************
*****************************************************************************/
-#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 +#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 { + PurpleIRCv3MessageHandler fallback_handler; +G_DEFINE_TYPE(PurpleIRCv3Parser, purple_ircv3_parser, G_TYPE_OBJECT) +/****************************************************************************** + *****************************************************************************/ +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 + if(*tags_string == '\0') { + matches = g_regex_match_full(parser->regex_tags, tags_string, -1, 0, 0, + if(local_error != NULL) { + g_propagate_error(error, local_error); + g_match_info_unref(info); + g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0, + "failed to parse tags: unknown error"); + g_match_info_unref(info); + while(g_match_info_matches(info)) { + 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); + g_match_info_unref(info); +purple_ircv3_parser_extract_params(PurpleIRCv3Parser *parser, + GStrvBuilder *builder, const gchar *str) + /* 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 + 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. + /* And don't forget to increment the count... ah ah ah! */ + /* Add the remaining string. */ + g_strv_builder_add(builder, str); + /* Give the count another one, ah ah ah! */ + /* Finally break out of the loop. */ +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(); + count = purple_ircv3_parser_extract_params(parser, builder, middle); + *n_params = *n_params + count; + g_strv_builder_add(builder, trailing); + *n_params = *n_params + 1; + result = g_strv_builder_end(builder); + g_strv_builder_unref(builder); +/****************************************************************************** + *****************************************************************************/ +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, + G_GNUC_UNUSED gpointer data) + g_set_error(error, PURPLE_IRCV3_DOMAIN, 0, "no handler for command %s", +/****************************************************************************** + * GObject Implementation + *****************************************************************************/ +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); +purple_ircv3_parser_init(PurpleIRCv3Parser *parser) { + parser->regex_message = g_regex_new("(?:@(?<tags>[^ ]+) )?" + "(?::(?<source>[^ ]+) +)?" + "(?: +(?<middle>(?:[^ :]+(?: +[^ :]+)*)))*" + "(?<coda> +:(?<trailing>.*)?)?", + g_assert(parser->regex_message != NULL); + parser->regex_tags = g_regex_new("(?:(?<key>[A-Za-z0-9-\\/]+)" + "(?:=(?<value>[^\\r\\n;]*))?(?:;|$))", + 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, +purple_ircv3_parser_class_init(PurpleIRCv3ParserClass *klass) { + GObjectClass *obj_class = G_OBJECT_CLASS(klass); + obj_class->finalize = purple_ircv3_parser_finalize; +/****************************************************************************** + *****************************************************************************/ +purple_ircv3_parser_new(void) { + return g_object_new(PURPLE_IRCV3_TYPE_PARSER, NULL); +purple_ircv3_parser_set_fallback_handler(PurpleIRCv3Parser *parser, + PurpleIRCv3MessageHandler handler) + g_return_if_fail(PURPLE_IRCV3_IS_PARSER(parser)); + parser->fallback_handler = handler; +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; + gchar *tags_string = NULL; + gchar *trailing = NULL; + gboolean matches = FALSE; + gboolean result = FALSE; + 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); + g_set_error(error, PURPLE_IRCV3_DOMAIN, 0, + "failed to parser buffer '%s'", buffer); + g_match_info_unref(info); + /* 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(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_match_info_unref(info); + 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); + if(local_error != NULL) { + g_propagate_error(error, local_error); + g_hash_table_destroy(tags); + g_match_info_unref(info); + 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, + /* Call the handler. */ + result = handler(tags, source, command, n_params, params, error, data); + /* Cleanup everything. */ + g_hash_table_destroy(tags); + g_match_info_unref(info); --- /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-object.h> +typedef gboolean (*PurpleIRCv3MessageHandler)(GHashTable *tags, +#define PURPLE_IRCV3_TYPE_PARSER (purple_ircv3_parser_get_type()) +G_DECLARE_FINAL_TYPE(PurpleIRCv3Parser, purple_ircv3_parser, PURPLE_IRCV3, + * purple_ircv3_parser_new: + * Creates a new instance. +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 +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. +G_GNUC_INTERNAL gboolean purple_ircv3_parser_parse(PurpleIRCv3Parser *parser, const gchar *buffer, GError **error, gpointer data); +#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 @@
+ 'test_ircv3_' + prog, 'test_ircv3_@0@.c'.format(prog), + dependencies : [libpurple_dep, glib], + objects : ircv3_prpl.extract_all_objects()) + test('ircv3_' + prog, e) --- /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 "../purpleircv3parser.h" + const gchar * const params[16]; +} TestPurpleIRCv3ParserData; +/****************************************************************************** + *****************************************************************************/ +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; + /* Make sure we have an expected tags hash table before checking them. */ + gpointer expected_value; + /* 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, + 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) { + if(d->command == NULL) { + /* Walk through the params checking against the expected values. */ + 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. */ +/****************************************************************************** + *****************************************************************************/ +test_purple_ircv3_parser(const gchar *source, TestPurpleIRCv3ParserData *d) { + PurpleIRCv3Parser *parser = purple_ircv3_parser_new(); + 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_clear_object(&parser); +/****************************************************************************** + *****************************************************************************/ +test_purple_ircv3_parser_simple(void) { + TestPurpleIRCv3ParserData data = { + .params = {"bar", "baz", "asdf"}, + test_purple_ircv3_parser("foo bar baz asdf", &data); +test_purple_ircv3_parser_with_source(void) { + TestPurpleIRCv3ParserData data = { + .params = {"bar", "baz", "asdf"}, + test_purple_ircv3_parser(":coolguy foo bar baz asdf", &data); +test_purple_ircv3_parser_with_trailing(void) { + TestPurpleIRCv3ParserData data = { + .params = {"bar", "baz", "asdf quux"}, + test_purple_ircv3_parser("foo bar baz :asdf quux", &data); +test_purple_ircv3_parser_with_empty_trailing(void) { + TestPurpleIRCv3ParserData data = { + .params = {"bar", "baz", ""}, + test_purple_ircv3_parser("foo bar baz :", &data); +test_purple_ircv3_parser_with_trailing_starting_colon(void) { + TestPurpleIRCv3ParserData data = { + .params = {"bar", "baz", ":asdf"}, + test_purple_ircv3_parser("foo bar baz ::asdf", &data); +test_purple_ircv3_parser_with_source_and_trailing(void) { + TestPurpleIRCv3ParserData data = { + .params = {"bar", "baz", "asdf quux"}, + test_purple_ircv3_parser(":coolguy foo bar baz :asdf quux", &data); +test_purple_ircv3_parser_with_source_and_trailing_whitespace(void) { + TestPurpleIRCv3ParserData data = { + .params = {"bar", "baz", " asdf quux "}, + test_purple_ircv3_parser(":coolguy foo bar baz : asdf quux ", &data); +test_purple_ircv3_parser_with_source_and_trailing_colon(void) { + TestPurpleIRCv3ParserData data = { + .params = {"bar", "lol :) "}, + test_purple_ircv3_parser(":coolguy PRIVMSG bar :lol :) ", &data); +test_purple_ircv3_parser_with_source_and_empty_trailing(void) { + TestPurpleIRCv3ParserData data = { + .params = {"bar", "baz", ""}, + test_purple_ircv3_parser(":coolguy foo bar baz :", &data); +test_purple_ircv3_parser_with_source_and_trailing_only_whitespace(void) { + TestPurpleIRCv3ParserData data = { + .params = {"bar", "baz", " "}, + test_purple_ircv3_parser(":coolguy foo bar baz : ", &data); +test_purple_ircv3_parser_with_tags(void) { + TestPurpleIRCv3ParserData data = { + 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); +test_purple_ircv3_parser_with_escaped_tags(void) { +/* Escaped tags aren't implemented yet. */ + TestPurpleIRCv3ParserData data = { + 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", +test_purple_ircv3_with_tags_and_source(void) { + TestPurpleIRCv3ParserData data = { + 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); +test_purple_ircv3_last_param_no_colon(void) { + TestPurpleIRCv3ParserData data = { + test_purple_ircv3_parser(":src JOIN #chan", &data); +test_purple_ircv3_last_param_with_colon(void) { + TestPurpleIRCv3ParserData data = { + test_purple_ircv3_parser(":src JOIN :#chan", &data); +test_purple_ircv3_without_last_param(void) { + TestPurpleIRCv3ParserData data = { + test_purple_ircv3_parser(":src AWAY", &data); +test_purple_ircv3_with_last_param(void) { + TestPurpleIRCv3ParserData data = { + test_purple_ircv3_parser(":src AWAY ", &data); +test_purple_ircv3_tab_is_not_space(void) { + TestPurpleIRCv3ParserData data = { + .params = {"bar", "baz"}, + test_purple_ircv3_parser(":cool\tguy foo bar baz", &data); +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 + TestPurpleIRCv3ParserData data = { + .source = "coolguy!ag@net\x03" "5w\x03" "ork.admin", + .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); +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 + TestPurpleIRCv3ParserData data = { + .source = "coolguy!~ag@n\x02" "et\x03" "05w\x0f" "ork.admin", + .params = {"foo", "bar baz"}, + const gchar *msg = NULL; + msg = ":coolguy!~ag@n\x02" "et\x03" "05w\x0f" "ork.admin PRIVMSG foo :bar " + test_purple_ircv3_parser(msg, &data); +test_purple_ircv3_everything(void) { + TestPurpleIRCv3ParserData data = { + .source = "irc.example.com", + .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); +test_purple_ircv3_everything_but_tags(void) { + TestPurpleIRCv3ParserData data = { + .source = "irc.example.com", + .params = {"param1", "param2", "param3 param3"}, + const gchar *msg = NULL; + msg = ":irc.example.com COMMAND param1 param2 :param3 param3"; + test_purple_ircv3_parser(msg, &data); +test_purple_ircv3_everything_but_source(void) { + TestPurpleIRCv3ParserData data = { + .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); +test_purple_ircv3_command_only(void) { + TestPurpleIRCv3ParserData data = { + test_purple_ircv3_parser("COMMAND", &data); +test_purple_ircv3_slashes_are_fun(void) { +/* Escaped tags aren't implemented yet. */ + TestPurpleIRCv3ParserData data = { + 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); +test_purple_ircv3_unreal_broken_1(void) { + TestPurpleIRCv3ParserData data = { + .source = "gravel.mozilla.org", + .params = {"#momo", "Erroneous Nickname: Illegal characters"}, + const gchar *msg = NULL; + msg = ":gravel.mozilla.org 432 #momo :Erroneous Nickname: Illegal " + test_purple_ircv3_parser(msg, &data); +test_purple_ircv3_unreal_broken_2(void) { + TestPurpleIRCv3ParserData data = { + .source = "gravel.mozilla.org", + .params = {"#tckk", "+n"}, + test_purple_ircv3_parser(":gravel.mozilla.org MODE #tckk +n ", &data); +test_purple_ircv3_unreal_broken_3(void) { + TestPurpleIRCv3ParserData data = { + .source = "services.esper.net", + .params = {"#foo-bar", "+o", "foobar"}, + test_purple_ircv3_parser(":services.esper.net MODE #foo-bar +o foobar ", +test_purple_ircv3_tag_escape_char_at_a_time(void) { +/* Escaped tags aren't implemented yet. */ + TestPurpleIRCv3ParserData data = { + 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); +test_purple_ircv3_tag_drop_unnecessary_escapes(void) { + TestPurpleIRCv3ParserData data = { + 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); +test_purple_ircv3_tag_drop_trailing_slash(void) { +/* Escaped tags aren't implemented yet. */ + TestPurpleIRCv3ParserData data = { + 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); +test_purple_ircv3_duplicate_tags(void) { + TestPurpleIRCv3ParserData data = { + 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); +test_purple_ircv3_vendor_tags_are_namespaced(void) { + TestPurpleIRCv3ParserData data = { + 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); +test_purple_ircv3_special_mode_1(void) { + TestPurpleIRCv3ParserData data = { + .params = {"#channel", "+i"}, + test_purple_ircv3_parser(":SomeOp MODE #channel :+i", &data); +test_purple_ircv3_special_mode_2(void) { + TestPurpleIRCv3ParserData data = { + .params = {"#channel", "+oo", "SomeUser", "AnotherUser"}, + test_purple_ircv3_parser(":SomeOp MODE #channel +oo SomeUser :AnotherUser", +/****************************************************************************** + *****************************************************************************/ +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);