Tue, 29 Nov 2022 00:06:45 -0600
Create PurpleIRCv3Capabilities for managing capabilities for each connection
This implements everything in capabilities version 302 except for handling the
NEW and DEL commands from the server. I'm not sure how we can test that yet,
so we're ignoring it for now.
After the server has told us about all of the capabilities it supports, we
explicitly request cap-notify which we implictly request when we send
CAP LS 302. This is mostly to test our code and might be removed in the future.
Testing Done:
Connected to my local ergo and used ngrep to verify that everything was being sent correctly.
Reviewed at https://reviews.imfreedom.org/r/2100/
/* * 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) { 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); } 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) { 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); } 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) { 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); } static void test_purple_ircv3_tag_drop_unnecessary_escapes(void) { 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); } static void test_purple_ircv3_tag_drop_trailing_slash(void) { 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); } 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(); }