libpurple/protocols/ircv3/tests/test_ircv3_parser.c

Sun, 02 Oct 2022 01:22:26 -0500

author
Gary Kramlich <grim@reaperworld.com>
date
Sun, 02 Oct 2022 01:22:26 -0500
changeset 41774
170078e728c0
child 41931
f35eb5adb2e0
permissions
-rw-r--r--

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/

/*
 * 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();
}

mercurial