# HG changeset patch
# Node ID d774d20198f6a56776e7c90393a019169c861673
# Parent  332001a43406245f50eac81f72a3027810268769
diff --git a/ibis/ibisconstants.h b/ibis/ibisconstants.h
--- a/ibis/ibisconstants.h
+++ b/ibis/ibisconstants.h
@@ -1285,6 +1285,28 @@
 #define IBIS_FEATURE_PREFIX "PREFIX" IBIS_AVAILABLE_MACRO_IN_0_3
 
 /**
+ * IBIS_FEATURE_PREFIX_MODES:
+ *
+ * A constant for the modes portion of [const@FEATURE_PREFIX].
+ *
+ * This is a synthetic feature that isn't part of any of the specifications.
+ *
+ * Since: 0.8
+ */
+#define IBIS_FEATURE_PREFIX_MODES "PREFIX-MODES" IBIS_AVAILABLE_MACRO_IN_0_8
+
+/**
+ * IBIS_FEATURE_PREFIX_PREFIXES:
+ *
+ * A constant for the prefixes portion of [const@FEATURE_PREFIX].
+ *
+ * This is a synthetic feature that isn't part of any of the specifications.
+ *
+ * Since: 0.8
+ */
+#define IBIS_FEATURE_PREFIX_PREFIXES "PREFIX-PREFIXES" IBIS_AVAILABLE_MACRO_IN_0_8
+
+/**
  * IBIS_FEATURE_SAFELIST:
  *
  * A constant for the `SAFELIST` feature.
diff --git a/ibis/ibisfeatures.c b/ibis/ibisfeatures.c
--- a/ibis/ibisfeatures.c
+++ b/ibis/ibisfeatures.c
@@ -81,7 +81,8 @@
 	existing_value = g_hash_table_lookup(features->features,
 	                                     GINT_TO_POINTER(quark));
 	if(existing_value != NULL &&
-	   ibis_str_equal(g_value_get_string(existing_value), svalue)) {
+	   ibis_str_equal(g_value_get_string(existing_value), svalue))
+	{
 		return;
 	}
 
@@ -144,6 +145,49 @@
 	g_signal_emit(features, signals[SIG_CHANGED], quark, feature);
 }
 
+/**
+ * ibis_features_parse_prefix: (skip)
+ * @features: The instance.
+ * @value: The value of the PREFIX feature.
+ *
+ * Parses the value of the PREFIX feature and adds the synthetic MODES and
+ * PREFIXES features.
+ */
+static void
+ibis_features_parse_prefix(IbisFeatures *features, const char *prefix) {
+	GRegex *regex = NULL;
+	GMatchInfo *info = NULL;
+	gboolean matches = FALSE;
+
+	regex = g_regex_new("^\\((?P<modes>.+)\\)(?P<prefixes>.+)$",
+	                    G_REGEX_DEFAULT, G_REGEX_MATCH_DEFAULT,
+	                    NULL);
+
+	matches = g_regex_match(regex, prefix, G_REGEX_MATCH_DEFAULT, &info);
+	if(matches) {
+		char *modes = NULL;
+		char *prefixes = NULL;
+
+		modes = g_match_info_fetch_named(info, "modes");
+		prefixes = g_match_info_fetch_named(info, "prefixes");
+
+		if(strlen(modes) == strlen(prefixes)) {
+			ibis_features_set_string(features, IBIS_FEATURE_PREFIX_MODES, modes);
+
+			ibis_features_set_string(features, IBIS_FEATURE_PREFIX_PREFIXES,
+			                         prefixes);
+		} else {
+			g_warning("format of prefix '%s' is invalid", prefix);
+		}
+
+		g_free(modes);
+		g_free(prefixes);
+	}
+
+	g_clear_pointer(&info, g_match_info_unref);
+	g_clear_pointer(&regex, g_regex_unref);
+}
+
 static void
 ibis_features_set_feature(IbisFeatures *features, const char *feature,
                           const char *value)
@@ -208,6 +252,7 @@
 		                       ibis_str_to_uint(value));
 	} else if(ibis_str_equal(feature, IBIS_FEATURE_PREFIX)) {
 		ibis_features_set_string(features, IBIS_FEATURE_PREFIX, value);
+		ibis_features_parse_prefix(features, value);
 	} else if(ibis_str_equal(feature, IBIS_FEATURE_SAFELIST)) {
 		ibis_features_set_boolean(features, IBIS_FEATURE_SAFELIST, TRUE);
 	} else if(ibis_str_equal(feature, IBIS_FEATURE_SILENCE)) {
@@ -307,6 +352,7 @@
 ibis_features_clear(IbisFeatures *features) {
 	GHashTableIter iter;
 	gpointer key = NULL;
+	const char *default_prefix = "(ov)@+";
 
 	g_return_if_fail(IBIS_IS_FEATURES(features));
 
@@ -324,7 +370,8 @@
 	 * https://modern.ircdocs.horse/#rplisupport-parameters
 	 */
 	ibis_features_set_string(features, IBIS_FEATURE_CHANTYPES, "#");
-	ibis_features_set_string(features, IBIS_FEATURE_PREFIX, "(ov)@+");
+	ibis_features_set_string(features, IBIS_FEATURE_PREFIX, default_prefix);
+	ibis_features_parse_prefix(features, default_prefix);
 }
 
 gboolean
@@ -576,6 +623,20 @@
 	return ibis_features_get_string(features, IBIS_FEATURE_PREFIX);
 }
 
+const char *
+ibis_features_get_prefix_modes(IbisFeatures *features) {
+	g_return_val_if_fail(IBIS_IS_FEATURES(features), NULL);
+
+	return ibis_features_get_string(features, IBIS_FEATURE_PREFIX_MODES);
+}
+
+const char *
+ibis_features_get_prefix_prefixes(IbisFeatures *features) {
+	g_return_val_if_fail(IBIS_IS_FEATURES(features), NULL);
+
+	return ibis_features_get_string(features, IBIS_FEATURE_PREFIX_PREFIXES);
+}
+
 gboolean
 ibis_features_get_safelist(IbisFeatures *features) {
 	g_return_val_if_fail(IBIS_IS_FEATURES(features), FALSE);
diff --git a/ibis/ibisfeatures.h b/ibis/ibisfeatures.h
--- a/ibis/ibisfeatures.h
+++ b/ibis/ibisfeatures.h
@@ -107,6 +107,7 @@
  * @features: The instance.
  *
  * Gets the AWAYLEN parameter from @features.
+ *
  * See [const@FEATURE_AWAYLEN].
  *
  * Returns: AWAYLEN or 0 if not set.
@@ -121,6 +122,7 @@
  * @features: The instance.
  *
  * Gets the CASEMAPPING parameter from @features.
+ *
  * See [const@FEATURE_CASEMAPPING].
  *
  * Returns: CASEMAPPING or %NULL if not set.
@@ -135,6 +137,7 @@
  * @features: The instance.
  *
  * Gets the CHANLIMIT parameter from @features.
+ *
  * See [const@FEATURE_CHANLIMIT].
  *
  * Returns: CHANLIMIT or %NULL if not set.
@@ -149,6 +152,7 @@
  * @features: The instance.
  *
  * Gets the CHANMODES parameter from @features.
+ *
  * See [const@FEATURE_CHANMODES].
  *
  * Returns: CHANMODES or %NULL if not set.
@@ -163,6 +167,7 @@
  * @features: The instance.
  *
  * Gets the CHANTYPES parameter from @features.
+ *
  * See [const@FEATURE_CHANTYPES].
  *
  * Returns: CHANTYPES or the default value of '#' if not set.
@@ -177,6 +182,7 @@
  * @features: The instance.
  *
  * Gets the CHANNELLEN parameter from @features.
+ *
  * See [const@FEATURE_CHANNELLEN].
  *
  * Returns: CHANNELLEN or 0 if not set.
@@ -191,6 +197,7 @@
  * @features: The instance.
  *
  * Gets the ELIST parameter from @features.
+ *
  * See [const@FEATURE_ELIST].
  *
  * Returns: ELIST or %NULL if not set.
@@ -205,6 +212,7 @@
  * @features: The instance.
  *
  * Gets the EXCEPTS parameter from @features.
+ *
  * See [const@FEATURE_EXCEPTS].
  *
  * Returns: EXCEPTS or 0 if not set.
@@ -219,6 +227,7 @@
  * @features: The instance.
  *
  * Gets the EXTBAN parameter from @features.
+ *
  * See [const@FEATURE_EXTBAN].
  *
  * Returns: EXTBAN or %NULL if not set.
@@ -233,6 +242,7 @@
  * @features: The instance.
  *
  * Gets the HOSTLEN parameter from @features.
+ *
  * See [const@FEATURE_HOSTLEN].
  *
  * Returns: HOSTLEN or 0 if not set.
@@ -247,6 +257,7 @@
  * @features: The instance.
  *
  * Gets the INVEX parameter from @features.
+ *
  * See [const@FEATURE_INVEX].
  *
  * Returns: INVEX or 0 if not set.
@@ -261,6 +272,7 @@
  * @features: The instance.
  *
  * Gets the KICKLEN parameter from @features.
+ *
  * See [const@FEATURE_KICKLEN].
  *
  * Returns: KICKLEN or 0 if not set.
@@ -275,6 +287,7 @@
  * @features: The instance.
  *
  * Gets the MAXLIST parameter from @features.
+ *
  * See [const@FEATURE_MAXLIST].
  *
  * Returns: MAXLIST or %NULL if not set.
@@ -289,6 +302,7 @@
  * @features: The instance.
  *
  * Gets the MAXTARGETS parameter from @features.
+ *
  * See [const@FEATURE_MAXTARGETS].
  *
  * Returns: MAXTARGETS or 0 if not set.
@@ -303,6 +317,7 @@
  * @features: The instance.
  *
  * Gets the MODES parameter from @features.
+ *
  * See [const@FEATURE_MODES].
  *
  * Returns: MODES or 0 if not set.
@@ -317,6 +332,7 @@
  * @features: The instance.
  *
  * Gets the NETWORK parameter from @features.
+ *
  * See [const@FEATURE_NETWORK].
  *
  * Returns: NETWORK or %NULL if not set.
@@ -331,6 +347,7 @@
  * @features: The instance.
  *
  * Gets the NICKLEN parameter from @features.
+ *
  * See [const@FEATURE_NICKLEN].
  *
  * Returns: NICKLEN or 0 if not set.
@@ -345,6 +362,7 @@
  * @features: The instance.
  *
  * Gets the PREFIX parameter from @features.
+ *
  * See [const@FEATURE_PREFIX].
  *
  * Returns: PREFIX or the default value of '(ov)@+' if not set.
@@ -355,10 +373,41 @@
 const char *ibis_features_get_prefix(IbisFeatures *features);
 
 /**
+ * ibis_features_get_prefix_modes:
+ * @features: The instance.
+ *
+ * Gets the modes portion of the prefix feature.
+ *
+ * This is a synthetic feature that isn't defined by any specification.
+ *
+ * Returns: The modes portion of [const@FEATURE_PREFIX].
+ *
+ * Since: 0.8
+ */
+IBIS_AVAILABLE_IN_0_8
+const char *ibis_features_get_prefix_modes(IbisFeatures *features);
+
+/**
+ * ibis_features_get_prefix_prefixes:
+ * @features: The instance.
+ *
+ * Gets the prefixes portion of the prefix feature.
+ *
+ * This is a synthetic feature that isn't defined by any specification.
+ *
+ * Returns: The prefixes portion of [const@FEATURE_PREFIX].
+ *
+ * Since: 0.8
+ */
+IBIS_AVAILABLE_IN_0_8
+const char *ibis_features_get_prefix_prefixes(IbisFeatures *features);
+
+/**
  * ibis_features_get_safelist:
  * @features: The instance.
  *
  * Gets the SAFELIST parameter from @features.
+ *
  * See [const@FEATURE_SAFELIST].
  *
  * Returns: %TRUE if SAFELIST parameter is advertised, otherwise %FALSE.
@@ -373,6 +422,7 @@
  * @features: The instance.
  *
  * Gets the SILENCE parameter from @features.
+ *
  * See [const@FEATURE_SILENCE].
  *
  * Returns: SILENCE or 0 if not set.
@@ -387,6 +437,7 @@
  * @features: The instance.
  *
  * Gets the STATUSMSG parameter from @features.
+ *
  * See [const@FEATURE_STATUSMSG].
  *
  * Returns: STATUSMSG or %NULL if not set.
@@ -401,6 +452,7 @@
  * @features: The instance.
  *
  * Gets the TARGMAX parameter from @features.
+ *
  * See [const@FEATURE_TARGMAX].
  *
  * Returns: TARGMAX or %NULL if not set.
@@ -415,6 +467,7 @@
  * @features: The instance.
  *
  * Gets the TOPICLEN parameter from @features.
+ *
  * See [const@FEATURE_TOPICLEN].
  *
  * Returns: TOPICLEN or 0 if not set.
@@ -429,6 +482,7 @@
  * @features: The instance.
  *
  * Gets the USERLEN parameter from @features.
+ *
  * See [const@FEATURE_USERLEN].
  *
  * Returns: USERLEN or 0 if not set.
diff --git a/ibis/tests/test_features.c b/ibis/tests/test_features.c
--- a/ibis/tests/test_features.c
+++ b/ibis/tests/test_features.c
@@ -113,7 +113,9 @@
 	g_assert_cmpuint(notify_counter, ==, 0);
 
 	g_assert_true(ibis_features_parse(features, message));
-	g_assert_cmpuint(notify_counter, ==, g_strv_length(msg_params));
+
+	/* We have 2 synthetic features we need to account for. */
+	g_assert_cmpuint(notify_counter, ==, g_strv_length(msg_params) + 2);
 
 	g_assert_cmpuint(ibis_features_get_awaylen(features), ==, 390);
 	g_assert_cmpstr(ibis_features_get_casemapping(features), ==,
@@ -135,6 +137,8 @@
 	g_assert_cmpstr(ibis_features_get_network(features), ==, "ErgoTest");
 	g_assert_cmpuint(ibis_features_get_nicklen(features), ==, 32);
 	g_assert_cmpstr(ibis_features_get_prefix(features), ==, "(qaohv)~&@%+");
+	g_assert_cmpstr(ibis_features_get_prefix_modes(features), ==, "qaohv");
+	g_assert_cmpstr(ibis_features_get_prefix_prefixes(features), ==, "~&@%+");
 	g_assert_true(ibis_features_get_safelist(features));
 	g_assert_cmpuint(ibis_features_get_silence(features), ==, 15);
 	g_assert_cmpstr(ibis_features_get_statusmsg(features), ==, "~&@%+");
@@ -217,6 +221,8 @@
 	g_assert_cmpstr(ibis_features_get_network(features), ==, NULL);
 	g_assert_cmpuint(ibis_features_get_nicklen(features), ==, 0);
 	g_assert_cmpstr(ibis_features_get_prefix(features), ==, "(ov)@+");
+	g_assert_cmpstr(ibis_features_get_prefix_modes(features), ==, "ov");
+	g_assert_cmpstr(ibis_features_get_prefix_prefixes(features), ==, "@+");
 	g_assert_false(ibis_features_get_safelist(features));
 	g_assert_cmpuint(ibis_features_get_silence(features), ==, 0);
 	g_assert_cmpstr(ibis_features_get_statusmsg(features), ==, NULL);
@@ -228,6 +234,28 @@
 }
 
 static void
+test_ibis_features_parse_invalid_prefix(void) {
+	if(g_test_subprocess()) {
+		IbisMessage *message = NULL;
+		IbisFeatures *features = NULL;
+
+		message = ibis_message_new(IBIS_RPL_ISUPPORT);
+		ibis_message_set_params(message, "PREFIX=(ov)@%+", NULL);
+
+		features = ibis_features_new();
+
+		g_assert_false(ibis_features_parse(features, message));
+
+		g_clear_object(&features);
+
+		g_assert_not_reached();
+	}
+
+	g_test_trap_subprocess(NULL, 0, G_TEST_SUBPROCESS_DEFAULT);
+	g_test_trap_assert_stderr("*format of prefix*is invalid*");
+}
+
+static void
 test_ibis_features_clear(void) {
 	IbisMessage *message = NULL;
 	IbisFeatures *features = NULL;
@@ -237,11 +265,11 @@
 	g_signal_connect(features, "changed",
 	                 G_CALLBACK(test_ibis_features_changed_counter), &counter);
 
-	/* There are 2 default features, they should get removed and reset on a
-	 * clear.
+	/* There are 2 default and 2 synthetic features that should get removed and
+	 * reset on a clear which means we should see 8 changes.
 	 */
 	ibis_features_clear(features);
-	g_assert_cmpuint(counter, ==, 4);
+	g_assert_cmpuint(counter, ==, 8);
 
 	/* Build a simple RPL_ISUPPORT message and parse it. */
 	message = ibis_message_new(IBIS_RPL_ISUPPORT);
@@ -252,10 +280,10 @@
 	ibis_features_parse(features, message);
 	g_assert_cmpuint(counter, ==, 2);
 
-	/* Clear again, and make sure we got called 6 times, 4 removals, 2 adds. */
+	/* Clear again. Make sure we got called 10 times, 6 removals, 4 adds. */
 	counter = 0;
 	ibis_features_clear(features);
-	g_assert_cmpuint(counter, ==, 6);
+	g_assert_cmpuint(counter, ==, 10);
 
 	/* Cleanup. */
 	g_assert_finalize_object(message);
@@ -380,10 +408,12 @@
 	g_test_set_nonfatal_assertions();
 
 	g_test_add_func("/ibis/features/new", test_ibis_features_new);
-	g_test_add_func("/ibis/features/parse_fail", test_ibis_features_parse_fail);
-	g_test_add_func("/ibis/features/parse_all", test_ibis_features_parse_all);
-	g_test_add_func("/ibis/features/parse_unknown", test_ibis_features_parse_unknown);
-	g_test_add_func("/ibis/features/no_parse", test_ibis_features_no_parse);
+	g_test_add_func("/ibis/features/parse/fail", test_ibis_features_parse_fail);
+	g_test_add_func("/ibis/features/parse/all", test_ibis_features_parse_all);
+	g_test_add_func("/ibis/features/parse/unknown", test_ibis_features_parse_unknown);
+	g_test_add_func("/ibis/features/parse/none", test_ibis_features_no_parse);
+	g_test_add_func("/ibis/features/parse/invalid-prefix",
+	                test_ibis_features_parse_invalid_prefix);
 	g_test_add_func("/ibis/features/clear", test_ibis_features_clear);
 	g_test_add_func("/ibis/features/client_tag_deny_none",
 	                test_ibis_features_client_tag_deny_none);
