# HG changeset patch
# Node ID 948b4ce6e8e20d3c3317607a08777ad06e209121
# Parent  2d095091213ec37a52042f5d30b644bbd8d4d4ff
diff --git a/ibis/ibisclient.c b/ibis/ibisclient.c
--- a/ibis/ibisclient.c
+++ b/ibis/ibisclient.c
@@ -28,6 +28,7 @@
 #include "ibisconstants.h"
 
 #include "ibisstring.h"
+#include "ibisnormalize.h"
 
 enum {
 	PROP_0,
@@ -967,6 +968,25 @@
 	return g_object_new(IBIS_TYPE_CLIENT, NULL);
 }
 
+char *
+ibis_client_normalize(IbisClient *client, const char *input) {
+	const char *casemapping = NULL;
+
+	g_return_val_if_fail(IBIS_IS_CLIENT(client), NULL);
+
+	casemapping = ibis_features_get_casemapping(client->features);
+
+	if(ibis_str_equal(casemapping, "ascii")) {
+		return ibis_normalize_ascii(input);
+	} else if(ibis_str_equal(casemapping, "rfc1459")) {
+		return ibis_normalize_rfc1459(input);
+	} else if(ibis_str_equal(casemapping, "rfc1459-strict")) {
+		return ibis_normalize_rfc1459_strict(input);
+	}
+
+	return g_strdup(input);
+}
+
 void
 ibis_client_start(IbisClient *client, GIOStream *stream, const char *password,
                   GCancellable *cancellable)
diff --git a/ibis/ibisclient.h b/ibis/ibisclient.h
--- a/ibis/ibisclient.h
+++ b/ibis/ibisclient.h
@@ -347,6 +347,21 @@
 IbisClient *ibis_client_new(void);
 
 /**
+ * ibis_client_normalize:
+ * @input: (nullable): The input to normalize.
+ *
+ * Normalizes @input depending on the [const@FEATURE_CASEMAPPING] feature.
+ *
+ * Available casemappings: *ascii*, *rfc1459*, and *rfc1459-strict* since 0.7.
+ *
+ * Returns: (transfer full): A normalized copy of @input.
+ *
+ * Since: 0.7
+ */
+IBIS_AVAILABLE_IN_0_7
+char *ibis_client_normalize(IbisClient *client, const char *input);
+
+/**
  * ibis_client_start:
  * @client: The instance.
  * @stream: The [class@Gio.IOStream] to process.
diff --git a/ibis/ibisnormalize.c b/ibis/ibisnormalize.c
new file mode 100644
--- /dev/null
+++ b/ibis/ibisnormalize.c
@@ -0,0 +1,119 @@
+/*
+ * Ibis - IRCv3 Library
+ * Copyright (C) 2022-2024 Ibis Developers
+ *
+ * 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 "ibisnormalize.h"
+#include "ibisstring.h"
+
+/******************************************************************************
+ * Helpers
+ *****************************************************************************/
+static gboolean
+ibis_normalize_unichar_is_upper_letter(gunichar uchar) {
+	return uchar >= 'A' && uchar <= 'Z';
+}
+
+static gboolean
+ibis_normalize_unichar_is_rfc1459_strict(gunichar uchar) {
+	return ibis_normalize_unichar_is_upper_letter(uchar) ||
+		uchar == '[' ||
+		uchar == ']' ||
+		uchar == '\\';
+}
+
+static gboolean
+ibis_normalize_unichar_is_rfc1459(gunichar uchar) {
+	return ibis_normalize_unichar_is_rfc1459_strict(uchar) ||
+		uchar == '~';
+}
+
+static gunichar
+ibis_normalize_rfc1459_strict_tolower(gunichar uchar) {
+	switch(uchar) {
+	case '[':
+		return '{';
+	case ']':
+		return '}';
+	case '\\':
+		return '|';
+	}
+
+	return g_unichar_tolower(uchar);
+}
+
+static gunichar
+ibis_normalize_rfc1459_tolower(gunichar uchar) {
+	if(uchar == '~') {
+		return '^';
+	}
+
+	return ibis_normalize_rfc1459_strict_tolower(uchar);
+}
+
+static char *
+ibis_normalize_string(const char *input,
+                      gboolean (*needs_normalization)(gunichar),
+                      gunichar (*normalize)(gunichar))
+{
+	GString *normalized = NULL;
+	const char *input_ptr = NULL;
+
+	normalized = g_string_new("");
+	if(ibis_str_is_empty(input)) {
+		return g_string_free_and_steal(normalized);
+	}
+
+	input_ptr = input;
+	while(*input_ptr) {
+		gunichar uchar = g_utf8_get_char(input_ptr);
+
+		if(needs_normalization(uchar)) {
+			g_string_append_unichar(normalized, normalize(uchar));
+		} else {
+			g_string_append_unichar(normalized, uchar);
+		}
+
+		input_ptr = g_utf8_next_char(input_ptr);
+	}
+
+	return g_string_free_and_steal(normalized);
+}
+
+/******************************************************************************
+ * Public API
+ *****************************************************************************/
+char *
+ibis_normalize_ascii(const char *input) {
+	return ibis_normalize_string(input, ibis_normalize_unichar_is_upper_letter,
+	                             g_unichar_tolower);
+}
+
+char *
+ibis_normalize_rfc1459(const char *input) {
+	return ibis_normalize_string(input, ibis_normalize_unichar_is_rfc1459,
+	                             ibis_normalize_rfc1459_tolower);
+}
+
+char *
+ibis_normalize_rfc1459_strict(const char *input) {
+	return ibis_normalize_string(input, ibis_normalize_unichar_is_rfc1459_strict,
+	                             ibis_normalize_rfc1459_strict_tolower);
+}
diff --git a/ibis/ibisnormalize.h b/ibis/ibisnormalize.h
new file mode 100644
--- /dev/null
+++ b/ibis/ibisnormalize.h
@@ -0,0 +1,87 @@
+/*
+ * Ibis - IRCv3 Library
+ * Copyright (C) 2022-2024 Ibis Developers
+ *
+ * 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/>.
+ */
+
+#if !defined(IBIS_GLOBAL_HEADER_INSIDE) && !defined(IBIS_COMPILATION)
+# error "only <ibis.h> may be included directly"
+#endif
+
+#ifndef IBIS_NORMALIZE_H
+#define IBIS_NORMALIZE_H
+
+#include <glib.h>
+
+#include "ibisversion.h"
+
+G_BEGIN_DECLS
+
+/**
+ * ibis_normalize_ascii:
+ * @input: (nullable): The input to normalize.
+ *
+ * Normalizes @input, i.e., it converts all uppercase letters in @input to their
+ * lowercase equivalents.
+ *
+ * Returns: (transfer full): A normalized copy of @input.
+ *
+ * Since: 0.7
+ */
+IBIS_AVAILABLE_IN_0_7
+char *ibis_normalize_ascii(const char *input);
+
+/**
+ * ibis_normalize_rfc1459:
+ * @input: (nullable): The input to normalize.
+ *
+ * Normalizes @input, i.e., it converts all uppercase letters in @input to their
+ * lowercase equivalents with the addition of the characters '[', ']', '\',
+ * and '~'.
+ *
+ * The lowercase equivalents of the characters '[', ']', '\', and '~' are
+ * {', '}', '|', and '^' respectively.
+ *
+ * Returns: (transfer full): A normalized copy of @input.
+ *
+ * Since: 0.7
+ */
+IBIS_AVAILABLE_IN_0_7
+char *ibis_normalize_rfc1459(const char *input);
+
+/**
+ * ibis_normalize_rfc1459_strict:
+ * @input: (nullable): The input to normalize.
+ *
+ * Normalizes @input, i.e., it converts all uppercase letters in @input to their
+ * lowercase equivalents with the addition of the characters '[', ']', and '\'.
+ *
+ * The lowercase equivalents of the characters '[', ']', and '\', are
+ * {', '}', and '|' respectively.
+ *
+ * Returns: (transfer full): A normalized copy of @input.
+ *
+ * Since: 0.7
+ */
+IBIS_AVAILABLE_IN_0_7
+char *ibis_normalize_rfc1459_strict(const char *input);
+
+G_END_DECLS
+
+#endif /* IBIS_NORMALIZE_H */
diff --git a/ibis/ibisversion.h b/ibis/ibisversion.h
--- a/ibis/ibisversion.h
+++ b/ibis/ibisversion.h
@@ -269,6 +269,30 @@
 #define IBIS_AVAILABLE_TYPE_IN_0_6
 #endif
 
+/**
+ * IBIS_VERSION_0_7:
+ *
+ * A macro that evaluates to the 0.7 version of ibis, in a format that can be
+ * used by the C pre-processor.
+ *
+ * Since: 0.7
+ */
+#define IBIS_VERSION_0_7 (G_ENCODE_VERSION(0, 7))
+
+#if IBIS_VERSION_MAX_ALLOWED < IBIS_VERSION_0_7
+#define IBIS_AVAILABLE_IN_0_7 IBIS_UNAVAILABLE(0, 7)
+#define IBIS_AVAILABLE_STATIC_INLINE_IN_0_7 IBIS_UNAVAILABLE_STATIC_INLINE(0, 7)
+#define IBIS_AVAILABLE_MACRO_IN_0_7 IBIS_UNAVAILABLE_MACRO(0, 7)
+#define IBIS_AVAILABLE_ENUMERATOR_IN_0_7 IBIS_UNAVAILABLE_ENUMERATOR(0, 7)
+#define IBIS_AVAILABLE_TYPE_IN_0_7 IBIS_UNAVAILABLE_TYPE(0, 7)
+#else
+#define IBIS_AVAILABLE_IN_0_7 _IBIS_EXTERN
+#define IBIS_AVAILABLE_STATIC_INLINE_IN_0_7
+#define IBIS_AVAILABLE_MACRO_IN_0_7
+#define IBIS_AVAILABLE_ENUMERATOR_IN_0_7
+#define IBIS_AVAILABLE_TYPE_IN_0_7
+#endif
+
 G_BEGIN_DECLS
 
 /**
diff --git a/ibis/meson.build b/ibis/meson.build
--- a/ibis/meson.build
+++ b/ibis/meson.build
@@ -5,6 +5,7 @@
   'ibisfeatures.c',
   'ibisformatting.c',
   'ibismessage.c',
+  'ibisnormalize.c',
   'ibissasl.c',
   'ibissource.c',
   'ibistags.c',
@@ -20,6 +21,7 @@
   'ibisfeatures.h',
   'ibisformatting.h',
   'ibismessage.h',
+  'ibisnormalize.h',
   'ibissasl.h',
   'ibissource.h',
   'ibisstring.h',
diff --git a/ibis/tests/meson.build b/ibis/tests/meson.build
--- a/ibis/tests/meson.build
+++ b/ibis/tests/meson.build
@@ -4,6 +4,7 @@
   'features',
   'formatting',
   'message',
+  'normalize',
   'parser',
   'source',
   'tags',
diff --git a/ibis/tests/test_client.c b/ibis/tests/test_client.c
--- a/ibis/tests/test_client.c
+++ b/ibis/tests/test_client.c
@@ -287,6 +287,86 @@
 	g_assert_finalize_object(client);
 }
 
+static void
+test_ibis_client_normalize_null(void) {
+	IbisClient *client = NULL;
+	char *normalized = NULL;
+
+	client = ibis_client_new();
+
+	normalized = ibis_client_normalize(client, "Foo[]🐻\\Baz~");
+	g_assert_cmpstr(normalized, ==, "Foo[]🐻\\Baz~");
+	g_free(normalized);
+
+	g_assert_finalize_object(client);
+}
+
+static void
+test_ibis_client_normalize_ascii(void) {
+	IbisClient *client = NULL;
+	IbisFeatures *features = NULL;
+	IbisMessage *message = NULL;
+	char *normalized = NULL;
+
+	client = ibis_client_new();
+	features = ibis_client_get_features(client);
+
+	message = ibis_message_new(IBIS_RPL_ISUPPORT);
+	ibis_message_set_params(message, IBIS_FEATURE_CASEMAPPING "=ascii", NULL);
+	ibis_features_parse(features, message);
+	g_assert_finalize_object(message);
+
+	normalized = ibis_client_normalize(client, "Foo[]🐻\\Baz~");
+	g_assert_cmpstr(normalized, ==, "foo[]🐻\\baz~");
+	g_free(normalized);
+
+	g_assert_finalize_object(client);
+}
+
+static void
+test_ibis_client_normalize_rfc1459(void) {
+	IbisClient *client = NULL;
+	IbisFeatures *features = NULL;
+	IbisMessage *message = NULL;
+	char *normalized = NULL;
+
+	client = ibis_client_new();
+	features = ibis_client_get_features(client);
+
+	message = ibis_message_new(IBIS_RPL_ISUPPORT);
+	ibis_message_set_params(message, IBIS_FEATURE_CASEMAPPING "=rfc1459", NULL);
+	ibis_features_parse(features, message);
+	g_assert_finalize_object(message);
+
+	normalized = ibis_client_normalize(client, "Foo[]🐻\\Baz~");
+	g_assert_cmpstr(normalized, ==, "foo{}🐻|baz^");
+	g_free(normalized);
+
+	g_assert_finalize_object(client);
+}
+
+static void
+test_ibis_client_normalize_rfc1459_strict(void) {
+	IbisClient *client = NULL;
+	IbisFeatures *features = NULL;
+	IbisMessage *message = NULL;
+	char *normalized = NULL;
+
+	client = ibis_client_new();
+	features = ibis_client_get_features(client);
+
+	message = ibis_message_new(IBIS_RPL_ISUPPORT);
+	ibis_message_set_params(message, IBIS_FEATURE_CASEMAPPING "=rfc1459-strict", NULL);
+	ibis_features_parse(features, message);
+	g_assert_finalize_object(message);
+
+	normalized = ibis_client_normalize(client, "Foo[]🐻\\Baz~");
+	g_assert_cmpstr(normalized, ==, "foo{}🐻|baz~");
+	g_free(normalized);
+
+	g_assert_finalize_object(client);
+}
+
 /******************************************************************************
  * Main
  *****************************************************************************/
@@ -327,5 +407,14 @@
 	g_test_add_func("/ibis/client/network/custom",
 	                test_ibis_client_network_custom);
 
+	g_test_add_func("/ibis/client/normalize/null",
+	                test_ibis_client_normalize_null);
+	g_test_add_func("/ibis/client/normalize/ascii",
+	                test_ibis_client_normalize_ascii);
+	g_test_add_func("/ibis/client/normalize/rfc1459",
+	                test_ibis_client_normalize_rfc1459);
+	g_test_add_func("/ibis/client/normalize/rfc1459-strict",
+	                test_ibis_client_normalize_rfc1459_strict);
+
 	return g_test_run();
 }
diff --git a/ibis/tests/test_normalize.c b/ibis/tests/test_normalize.c
new file mode 100644
--- /dev/null
+++ b/ibis/tests/test_normalize.c
@@ -0,0 +1,92 @@
+/*
+ * Ibis - IRCv3 Library
+ * Copyright (C) 2022-2024 Ibis Developers
+ *
+ * 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 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 <ibis.h>
+
+static void
+test_ibis_normalize_ascii(void) {
+	char *normalized = NULL;
+
+	/* Check NULL input */
+	normalized = ibis_normalize_ascii(NULL);
+	g_assert_cmpstr(normalized, ==, "");
+	g_free(normalized);
+
+	/* Only letters should be normalized, '[', ']', '\\', and '~' should be left
+	 * alone.
+	 */
+	normalized = ibis_normalize_ascii("Foo[]🐻\\Baz~");
+	g_assert_cmpstr(normalized, ==, "foo[]🐻\\baz~");
+	g_free(normalized);
+}
+
+static void
+test_ibis_normalize_rfc1459(void) {
+	char *normalized = NULL;
+
+	/* Check NULL input */
+	normalized = ibis_normalize_rfc1459(NULL);
+	g_assert_cmpstr(normalized, ==, "");
+	g_free(normalized);
+
+	/* Letters, '[', ']', '\\', and '~' should be normalized.
+	 */
+	normalized = ibis_normalize_rfc1459("Foo[]🐻\\Baz~");
+	g_assert_cmpstr(normalized, ==, "foo{}🐻|baz^");
+	g_free(normalized);
+}
+
+static void
+test_ibis_normalize_rfc1459_strict(void) {
+	char *normalized = NULL;
+
+	/* Check NULL input */
+	normalized = ibis_normalize_rfc1459_strict(NULL);
+	g_assert_cmpstr(normalized, ==, "");
+	g_free(normalized);
+
+	/* Letters, '[', ']', and '\\' should be normalized, but not '~'.
+	 */
+	normalized = ibis_normalize_rfc1459_strict("Foo[]🐻\\Baz~");
+	g_assert_cmpstr(normalized, ==, "foo{}🐻|baz~");
+	g_free(normalized);
+}
+
+/******************************************************************************
+ * Main
+ *****************************************************************************/
+int
+main(int argc, char *argv[]) {
+	g_test_init(&argc, &argv, NULL);
+	g_test_set_nonfatal_assertions();
+
+	g_test_add_func("/ibis/normalize/ascii",
+	                test_ibis_normalize_ascii);
+	g_test_add_func("/ibis/normalize/rfc1459",
+	                test_ibis_normalize_rfc1459);
+	g_test_add_func("/ibis/normalize/rfc1459_strict",
+	                test_ibis_normalize_rfc1459_strict);
+
+	return g_test_run();
+}
diff --git a/meson.build b/meson.build
--- a/meson.build
+++ b/meson.build
@@ -2,7 +2,7 @@
   default_options : ['c_std=c17', 'warning_level=2'],
   license : 'LGPL-2.1-or-later',
   meson_version : '>=1.0.0',
-  version : '0.6.1-dev')
+  version : '0.7.0-dev')
 
 toplevel_inc = include_directories('.')
 
