Fri, 14 Mar 2025 15:25:49 -0500
Fix the parameter order of an error in Ibis.Client.parse_mode_string
Testing Done:
Called in the turtles.
Reviewed at https://reviews.imfreedom.org/r/3906/
/* * Ibis - IRCv3 Library * Copyright (C) 2022-2024 Ibis Developers <devel@pidgin.im> * * Ibis is the legal property of its developers, whose names are too numerous * to list here. Please refer to the COPYRIGHT file distributed with this * source distribution. * * This library 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 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this library; if not, see <https://www.gnu.org/licenses/>. */ #include <glib/gi18n-lib.h> #include "ibismessage.h" #include "ibisconstants.h" #include "ibisctcpmessage.h" #include "ibisstring.h" enum { PROP_0, PROP_COMMAND, PROP_COMMAND_QUARK, PROP_SOURCE, PROP_PARAMS, PROP_RAW_MESSAGE, PROP_TAGS, PROP_ERROR, PROP_CTCP, PROP_CTCP_MESSAGE, PROP_STANDARD_REPLY, N_PROPERTIES, }; static GParamSpec *properties[N_PROPERTIES] = {NULL, }; struct _IbisMessage { GObject parent; char *raw_message; char *command; GQuark command_quark; GStrv params; char *source; IbisTags *tags; GError *error; IbisCTCPMessage *ctcp_message; IbisStandardReply *standard_reply; }; /****************************************************************************** * Serialization *****************************************************************************/ static inline void ibis_message_serialize_add_delimiter(GString *str) { if(str->len > 0) { g_string_append(str, " "); } } static void ibis_message_serialize_params(IbisMessage *message, GString *str) { gboolean first = TRUE; for(guint i = 0; message->params[i] != NULL; i++) { if(!first) { g_string_append(str, " "); } /* If this is the last param we may need to prefix it with a :. */ if(message->params[i + 1] == NULL) { const char *param = message->params[i]; gboolean prefix = FALSE; /* We only need the prefix if the parameter is empty, starts * with a :, or contains a space. */ if(param[0] == '\0' || param[0] == ':') { prefix = TRUE; } else if(g_strstr_len(param, -1, " ") != NULL) { prefix = TRUE; } if(prefix) { g_string_append_c(str, ':'); } g_string_append_printf(str, "%s", param); } else { g_string_append(str, message->params[i]); } if(first) { first = FALSE; } } } static GString * ibis_message_serialize_internal(IbisMessage *message, gboolean include_tags) { GString *str = g_string_new(""); if(include_tags && IBIS_IS_TAGS(message->tags)) { char *tag_data = ibis_tags_serialize(message->tags); g_string_append(str, tag_data); g_free(tag_data); } if(!ibis_str_is_empty(message->source)) { ibis_message_serialize_add_delimiter(str); g_string_append_printf(str, ":%s", message->source); } if(!ibis_str_is_empty(message->command)) { ibis_message_serialize_add_delimiter(str); g_string_append(str, message->command); } if(message->params != NULL) { ibis_message_serialize_add_delimiter(str); ibis_message_serialize_params(message, str); } if(IBIS_IS_CTCP_MESSAGE(message->ctcp_message)) { char *body = NULL; ibis_message_serialize_add_delimiter(str); body = ibis_ctcp_message_serialize(message->ctcp_message); g_string_append_printf(str, ":%s", body); g_free(body); } /* Finally add the trailing \r\n if the string isn't empty. */ if(str->len > 0) { g_string_append(str, "\r\n"); } return str; } /****************************************************************************** * Helpers *****************************************************************************/ static void ibis_message_set_command(IbisMessage *message, const char *command) { char *upper_command = NULL; g_return_if_fail(IBIS_IS_MESSAGE(message)); if(command != NULL) { upper_command = g_utf8_strup(command, -1); } if(g_set_str(&message->command, upper_command)) { GObject *obj = G_OBJECT(message); message->command_quark = g_quark_from_string(upper_command); g_object_freeze_notify(obj); g_object_notify_by_pspec(obj, properties[PROP_COMMAND]); g_object_notify_by_pspec(obj, properties[PROP_COMMAND_QUARK]); g_object_thaw_notify(obj); } g_clear_pointer(&upper_command, g_free); } static void ibis_message_set_raw_message(IbisMessage *message, const char *raw_message) { g_return_if_fail(IBIS_IS_MESSAGE(message)); if(g_set_str(&message->raw_message, raw_message)) { g_object_notify_by_pspec(G_OBJECT(message), properties[PROP_RAW_MESSAGE]); } } static guint ibis_message_extract_params(GStrvBuilder *builder, const char *str) { char *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 ibis_message_build_params(const char *middle, const char *coda, const char *trailing, guint *n_params) { GStrvBuilder *builder = g_strv_builder_new(); GStrv result = NULL; ibis_message_extract_params(builder, middle); if(*coda != '\0') { g_strv_builder_add(builder, trailing); } result = g_strv_builder_end(builder); g_strv_builder_unref(builder); if(result != NULL && n_params != NULL) { *n_params = g_strv_length(result); } return result; } static void ibis_message_set_standard_reply(IbisMessage *message, IbisStandardReply *reply) { g_return_if_fail(IBIS_IS_MESSAGE(message)); if(g_set_object(&message->standard_reply, reply)) { g_object_notify_by_pspec(G_OBJECT(message), properties[PROP_STANDARD_REPLY]); } } /****************************************************************************** * GObject Implementation *****************************************************************************/ G_DEFINE_FINAL_TYPE(IbisMessage, ibis_message, G_TYPE_OBJECT) static void ibis_message_finalize(GObject *obj) { IbisMessage *message = IBIS_MESSAGE(obj); g_clear_pointer(&message->command, g_free); g_clear_pointer(&message->params, g_strfreev); g_clear_pointer(&message->raw_message, g_free); g_clear_pointer(&message->source, g_free); g_clear_object(&message->tags); g_clear_error(&message->error); g_clear_object(&message->ctcp_message); g_clear_object(&message->standard_reply); G_OBJECT_CLASS(ibis_message_parent_class)->finalize(obj); } static void ibis_message_get_property(GObject *obj, guint param_id, GValue *value, GParamSpec *pspec) { IbisMessage *message = IBIS_MESSAGE(obj); switch(param_id) { case PROP_COMMAND: g_value_set_string(value, ibis_message_get_command(message)); break; case PROP_COMMAND_QUARK: g_value_set_uint(value, ibis_message_get_command_quark(message)); break; case PROP_PARAMS: g_value_set_boxed(value, ibis_message_get_params(message)); break; case PROP_RAW_MESSAGE: g_value_set_string(value, ibis_message_get_raw_message(message)); break; case PROP_SOURCE: g_value_set_string(value, ibis_message_get_source(message)); break; case PROP_TAGS: g_value_set_object(value, ibis_message_get_tags(message)); break; case PROP_ERROR: g_value_set_boxed(value, ibis_message_get_error(message)); break; case PROP_CTCP: g_value_set_boolean(value, ibis_message_get_ctcp(message)); break; case PROP_CTCP_MESSAGE: g_value_set_object(value, ibis_message_get_ctcp_message(message)); break; case PROP_STANDARD_REPLY: g_value_set_object(value, ibis_message_get_standard_reply(message)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec); break; } } static void ibis_message_set_property(GObject *obj, guint param_id, const GValue *value, GParamSpec *pspec) { IbisMessage *message = IBIS_MESSAGE(obj); switch(param_id) { case PROP_COMMAND: ibis_message_set_command(message, g_value_get_string(value)); break; case PROP_PARAMS: ibis_message_set_paramsv(message, g_value_get_boxed(value)); break; case PROP_RAW_MESSAGE: ibis_message_set_raw_message(message, g_value_get_string(value)); break; case PROP_SOURCE: ibis_message_set_source(message, g_value_get_string(value)); break; case PROP_TAGS: G_GNUC_BEGIN_IGNORE_DEPRECATIONS ibis_message_set_tags(message, g_value_get_object(value)); G_GNUC_END_IGNORE_DEPRECATIONS break; case PROP_ERROR: ibis_message_set_error(message, g_value_get_boxed(value)); break; case PROP_CTCP: G_GNUC_BEGIN_IGNORE_DEPRECATIONS ibis_message_set_ctcp(message, g_value_get_boolean(value)); G_GNUC_END_IGNORE_DEPRECATIONS break; case PROP_CTCP_MESSAGE: ibis_message_set_ctcp_message(message, g_value_get_object(value)); break; case PROP_STANDARD_REPLY: ibis_message_set_standard_reply(message, g_value_get_object(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec); break; } } static void ibis_message_init(IbisMessage *message) { message->tags = ibis_tags_new(); } static void ibis_message_class_init(IbisMessageClass *klass) { GObjectClass *obj_class = G_OBJECT_CLASS(klass); obj_class->finalize = ibis_message_finalize; obj_class->get_property = ibis_message_get_property; obj_class->set_property = ibis_message_set_property; /** * IbisMessage:command: * * The command of this message. * * This could be something like JOIN or a server reply numeric like 005. * * As of version 0.4, this will now be automatically noramlized to * uppercase to help with comparison. * * Since: 0.1 */ properties[PROP_COMMAND] = g_param_spec_string( "command", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); /** * IbisMessage:command-quark: * * The #GQuark for the command of this message. * * This is typically only used for emitting signals like * [signal@Client::message]. * * Since: 0.4 */ properties[PROP_COMMAND_QUARK] = g_param_spec_string( "command-quark", NULL, NULL, NULL, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); /** * IbisMessage:params: * * The parameters of the message. * * When serialized, the last item will be prefixed with a :. * * Since: 0.1 */ properties[PROP_PARAMS] = g_param_spec_boxed( "params", NULL, NULL, G_TYPE_STRV, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); /** * IbisMessage:raw-message: * * The raw message that was parsed. * * This property will only be non %NULL if the message was created with * [func@Message.parse]. * * Since: 0.1 */ properties[PROP_RAW_MESSAGE] = g_param_spec_string( "raw-message", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); /** * IbisMessage:source: * * The source of the message. * * This could be a nickname, a full nick!ident@server, a server name, or * %NULL. * * Since: 0.1 */ properties[PROP_SOURCE] = g_param_spec_string( "source", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); /** * IbisMessage:tags: * * The [ircv3 message tags](https://ircv3.net/specs/extensions/message-tags) * for the message. * * Since: 0.1 */ properties[PROP_TAGS] = g_param_spec_object( "tags", NULL, NULL, IBIS_TYPE_TAGS, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); /** * IbisMessage:error: * * An error that was encountered when processing the message. * * Since: 0.1 */ properties[PROP_ERROR] = g_param_spec_boxed( "error", NULL, NULL, G_TYPE_ERROR, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); /** * IbisMessage:ctcp: * * Whether or not the message is a CTCP message. * * This property should only be valid for messages whose command is * `PRIVMSG` or `NOTICE`. * * Since: 0.1 */ properties[PROP_CTCP] = g_param_spec_boolean( "ctcp", NULL, NULL, FALSE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); /** * IbisMessage:ctcp-message: * * The `CTCP` message that makes up the body of this message. * * Since: 0.5 */ properties[PROP_CTCP_MESSAGE] = g_param_spec_object( "ctcp-message", NULL, NULL, IBIS_TYPE_CTCP_MESSAGE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); /** * IbisMessage:standard-reply: * * The [class@StandardReply] if the message is a standard reply. * * Since: 0.8 */ properties[PROP_STANDARD_REPLY] = g_param_spec_object( "standard-reply", NULL, NULL, IBIS_TYPE_STANDARD_REPLY, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); g_object_class_install_properties(obj_class, N_PROPERTIES, properties); } /****************************************************************************** * Public API *****************************************************************************/ IbisMessage * ibis_message_new(const char *command) { g_return_val_if_fail(!ibis_str_is_empty(command), NULL); return g_object_new( IBIS_TYPE_MESSAGE, "command", command, NULL); } const char * ibis_message_get_command(IbisMessage *message) { g_return_val_if_fail(IBIS_IS_MESSAGE(message), NULL); return message->command; } GQuark ibis_message_get_command_quark(IbisMessage *message) { g_return_val_if_fail(IBIS_IS_MESSAGE(message), 0); return message->command_quark; } const char * ibis_message_get_source(IbisMessage *message) { g_return_val_if_fail(IBIS_IS_MESSAGE(message), NULL); return message->source; } void ibis_message_set_source(IbisMessage *message, const char *source) { g_return_if_fail(IBIS_IS_MESSAGE(message)); if(g_set_str(&message->source, source)) { g_object_notify_by_pspec(G_OBJECT(message), properties[PROP_SOURCE]); } } IbisTags * ibis_message_get_tags(IbisMessage *message) { g_return_val_if_fail(IBIS_IS_MESSAGE(message), NULL); return message->tags; } void ibis_message_set_tags(IbisMessage *message, IbisTags *tags) { g_return_if_fail(IBIS_IS_MESSAGE(message)); if(g_set_object(&message->tags, tags)) { g_object_notify_by_pspec(G_OBJECT(message), properties[PROP_TAGS]); } } GDateTime * ibis_message_get_timestamp(IbisMessage *message) { GDateTime *dt = NULL; const char *timestamp = NULL; g_return_val_if_fail(IBIS_IS_MESSAGE(message), NULL); if(!IBIS_IS_TAGS(message->tags)) { return NULL; } timestamp = ibis_tags_lookup(message->tags, IBIS_TAG_TIME); if(!ibis_str_is_empty(timestamp)) { GTimeZone *tz = g_time_zone_new_utc(); dt = g_date_time_new_from_iso8601(timestamp, tz); g_time_zone_unref(tz); } return dt; } GStrv ibis_message_get_params(IbisMessage *message) { g_return_val_if_fail(IBIS_IS_MESSAGE(message), NULL); return message->params; } void ibis_message_set_params(IbisMessage *message, ...) { GStrv params = NULL; GStrvBuilder *builder = NULL; va_list args; const char *param = NULL; g_return_if_fail(IBIS_IS_MESSAGE(message)); builder = g_strv_builder_new(); va_start(args, message); while((param = va_arg(args, const char *)) != NULL) { g_strv_builder_add(builder, param); } va_end(args); params = g_strv_builder_end(builder); g_clear_pointer(&builder, g_strv_builder_unref); ibis_message_set_paramsv(message, params); g_strfreev(params); } void ibis_message_set_paramsv(IbisMessage *message, GStrv params) { g_return_if_fail(IBIS_IS_MESSAGE(message)); if(message->params != params) { g_clear_pointer(&message->params, g_strfreev); if(params != NULL) { message->params = g_strdupv(params); } g_object_notify_by_pspec(G_OBJECT(message), properties[PROP_PARAMS]); } } GBytes * ibis_message_serialize(IbisMessage *message, gboolean include_tags) { GString *str = NULL; g_return_val_if_fail(IBIS_IS_MESSAGE(message), NULL); str = ibis_message_serialize_internal(message, include_tags); return g_string_free_to_bytes(str); } char * ibis_message_serialize_to_string(IbisMessage *message, gboolean include_tags) { GString *str = NULL; g_return_val_if_fail(IBIS_IS_MESSAGE(message), NULL); str = ibis_message_serialize_internal(message, include_tags); return g_string_free_and_steal(str); } GError * ibis_message_get_error(IbisMessage *message) { g_return_val_if_fail(IBIS_IS_MESSAGE(message), NULL); return message->error; } void ibis_message_set_error(IbisMessage *message, GError *error) { g_return_if_fail(IBIS_IS_MESSAGE(message)); g_clear_error(&message->error); if(error != NULL) { message->error = g_error_copy(error); } g_object_notify_by_pspec(G_OBJECT(message), properties[PROP_ERROR]); } void ibis_message_take_error(IbisMessage *message, GError *error) { g_return_if_fail(IBIS_IS_MESSAGE(message)); g_clear_error(&message->error); message->error = error; g_object_notify_by_pspec(G_OBJECT(message), properties[PROP_ERROR]); } IbisMessage * ibis_message_parse(const char *buffer, GError **error) { IbisMessage *message = NULL; GError *local_error = NULL; GMatchInfo *info = NULL; GRegex *regex = NULL; GStrv params = NULL; char *coda = NULL; char *command = NULL; char *middle = NULL; char *source = NULL; char *tags_string = NULL; char *trailing = NULL; char *validated = NULL; guint n_params = 0; g_return_val_if_fail(buffer != NULL, FALSE); validated = g_utf8_make_valid(buffer, -1); regex = g_regex_new("(?:@(?<tags>[^ ]+) )?" "(?::(?<source>[^ ]+) +)?" "(?<command>[^ :]+)" "(?: +(?<middle>(?:[^ :][^ ]*(?: +[^ :][^ ]*)*)))*" "(?<coda> +:(?<trailing>.*)?)?", 0, 0, NULL); /* Check if the buffer matches our regex for messages. */ if(!g_regex_match(regex, validated, 0, &info)) { g_match_info_unref(info); g_clear_pointer(&validated, g_free); g_set_error(error, IBIS_MESSAGE_PARSE_ERROR, 0, "failed to parser buffer '%s'", buffer); g_clear_pointer(®ex, g_regex_unref); return NULL; } /* We can *NOT* free validated here as the string is not copied into the * GMatchInfo. We can only free it when the GMatchInfo is destroyed. */ /* Extract the command from the buffer, so we can create the message. */ command = g_match_info_fetch_named(info, "command"); message = g_object_new( IBIS_TYPE_MESSAGE, "command", command, "raw-message", buffer, NULL); tags_string = g_match_info_fetch_named(info, "tags"); if(!ibis_str_is_empty(tags_string)) { IbisTags *tags = ibis_message_get_tags(message); ibis_tags_parse_string(tags, tags_string, &local_error); if(local_error != NULL) { g_propagate_error(error, local_error); g_free(tags_string); g_free(command); g_clear_object(&message); g_match_info_unref(info); g_clear_pointer(&validated, g_free); g_clear_pointer(®ex, g_regex_unref); return NULL; } } g_free(tags_string); source = g_match_info_fetch_named(info, "source"); if(!ibis_str_is_empty(source)) { ibis_message_set_source(message, source); } g_free(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 = ibis_message_build_params(middle, coda, trailing, &n_params); /* If the command is PRIVMSG or NOTICE, we need to check if the body, the * last parameter, starts with \001 which would make it a CTCP message. If * so, we need to remove the leading and optional trailing \001 from the * body and set the ctcp-message property of the message. */ if(ibis_str_equal(command, IBIS_MSG_PRIVMSG) || ibis_str_equal(command, IBIS_MSG_NOTICE)) { if(n_params >= 1) { IbisCTCPMessage *ctcp_message = NULL; char *body = NULL; guint last = n_params - 1; body = params[last]; ctcp_message = ibis_ctcp_message_parse(body); if(IBIS_IS_CTCP_MESSAGE(ctcp_message)) { GStrvBuilder *builder = NULL; GStrv adjusted = NULL; /* Set the CTCP message. */ ibis_message_set_ctcp_message(message, ctcp_message); g_clear_object(&ctcp_message); /* Finally set params to be all of the params except for the * last one that we used for CTCP. */ builder = g_strv_builder_new(); for(guint i = 0; i < last; i++) { g_strv_builder_add(builder, params[i]); } adjusted = g_strv_builder_end(builder); g_strv_builder_unref(builder); ibis_message_set_paramsv(message, adjusted); g_strfreev(adjusted); } } } if(params != NULL) { ibis_message_set_paramsv(message, params); } /* Once we've parsed the full message we can check if it's a standard reply * and parse that if it is. */ if(ibis_str_equal(command, IBIS_MSG_FAIL) || ibis_str_equal(command, IBIS_MSG_NOTE) || ibis_str_equal(command, IBIS_MSG_WARN)) { IbisStandardReply *reply = NULL; reply = ibis_standard_reply_parse(message); ibis_message_set_standard_reply(message, reply); g_clear_object(&reply); } /* Clean up everything */ g_free(command); g_free(middle); g_free(coda); g_free(trailing); g_strfreev(params); /* Cleanup the left overs. */ g_match_info_unref(info); g_clear_pointer(&validated, g_free); g_clear_pointer(®ex, g_regex_unref); return message; } const char * ibis_message_get_raw_message(IbisMessage *message) { g_return_val_if_fail(IBIS_IS_MESSAGE(message), NULL); return message->raw_message; } gboolean ibis_message_get_ctcp(IbisMessage *message) { g_return_val_if_fail(IBIS_IS_MESSAGE(message), FALSE); return IBIS_IS_CTCP_MESSAGE(message->ctcp_message); } void ibis_message_set_ctcp(IbisMessage *message, gboolean ctcp) { g_return_if_fail(IBIS_IS_MESSAGE(message)); if(ctcp) { IbisCTCPMessage *ctcp_message = NULL; ctcp_message = ibis_ctcp_message_new("UNKNOWN"); ibis_message_set_ctcp_message(message, ctcp_message); g_clear_object(&ctcp_message); } else { ibis_message_set_ctcp_message(message, NULL); } } IbisCTCPMessage * ibis_message_get_ctcp_message(IbisMessage *message) { g_return_val_if_fail(IBIS_IS_MESSAGE(message), NULL); return message->ctcp_message; } void ibis_message_set_ctcp_message(IbisMessage *message, IbisCTCPMessage *ctcp_message) { g_return_if_fail(IBIS_IS_MESSAGE(message)); if(!ibis_str_equal(message->command, IBIS_MSG_PRIVMSG) && !ibis_str_equal(message->command, IBIS_MSG_NOTICE)) { return; } if(g_set_object(&message->ctcp_message, ctcp_message)) { GObject *obj = G_OBJECT(message); g_object_freeze_notify(obj); g_object_notify_by_pspec(obj, properties[PROP_CTCP]); g_object_notify_by_pspec(obj, properties[PROP_CTCP_MESSAGE]); g_object_thaw_notify(obj); } } gboolean ibis_message_is_command(IbisMessage *message, const char *command) { g_return_val_if_fail(IBIS_IS_MESSAGE(message), FALSE); return ibis_str_equal(message->command, command); } IbisStandardReply * ibis_message_get_standard_reply(IbisMessage *message) { g_return_val_if_fail(IBIS_IS_MESSAGE(message), NULL); return message->standard_reply; }