diff -r c4acd02fdf73 -r 170078e728c0 libpurple/protocols/ircv3/tests/test_ircv3_parser.c --- /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 + * + * 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 . + */ + +#include + +#include + +#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(); +}