# HG changeset patch
# Node ID d368790404d4
# Parent  1e81da6ffa54
diff --git a/xeme/meson.build b/xeme/meson.build
--- a/xeme/meson.build
+++ b/xeme/meson.build
@@ -2,6 +2,7 @@
   'xemeaddress.c',
   'xemeconnection.c',
   'xemeextension.c',
+  'xemeextensionmap.c',
   'xemeinputstream.c',
   'xememessage.c',
   'xemeoutputstream.c',
@@ -16,6 +17,7 @@
   'xemeconstants.h',
   'xemecore.h',
   'xemeextension.h',
+  'xemeextensionmap.h',
   'xemeinputstream.h',
   'xememessage.h',
   'xemeoutputstream.h',
diff --git a/xeme/tests/meson.build b/xeme/tests/meson.build
--- a/xeme/tests/meson.build
+++ b/xeme/tests/meson.build
@@ -1,6 +1,7 @@
 PROGRAMS = [
   'address',
   'extension',
+  'extensionmap',
   'inputstream',
   'message',
   'outputstream',
diff --git a/xeme/tests/testextension.c b/xeme/tests/testextension.c
--- a/xeme/tests/testextension.c
+++ b/xeme/tests/testextension.c
@@ -58,7 +58,8 @@
 struct _TestXemeExtensionNormal {
 	XemeExtension parent;
 
-	guint parse;
+	guint parse_start;
+	guint parse_end;
 	guint serialize;
 };
 
@@ -66,18 +67,34 @@
               XEME_TYPE_EXTENSION)
 
 static void
-test_xeme_extension_normal_parse(XemeExtension *extension,
-                                 GMarkupParseContext *context)
+test_xeme_extension_normal_parse_start(XemeExtension *extension,
+                                       XemeInputStream *input,
+                                       GMarkupParseContext *context)
 {
 	TestXemeExtensionNormal *normal = NULL;
 
-	g_assert_true(XEME_IS_EXTENSION(extension));
+	g_assert_true(TEST_XEME_IS_EXTENSION_NORMAL(extension));
+	g_assert_true(XEME_IS_INPUT_STREAM(input));
+	g_assert_nonnull(context);
+
+	normal = TEST_XEME_EXTENSION_NORMAL(extension);
+
+	normal->parse_start += 1;
+}
+
+
+static void
+test_xeme_extension_normal_parse_end(XemeExtension *extension,
+                                     GMarkupParseContext *context)
+{
+	TestXemeExtensionNormal *normal = NULL;
+
 	g_assert_true(TEST_XEME_IS_EXTENSION_NORMAL(extension));
 	g_assert_nonnull(context);
 
 	normal = TEST_XEME_EXTENSION_NORMAL(extension);
 
-	normal->parse += 1;
+	normal->parse_end += 1;
 }
 
 static char *
@@ -96,7 +113,8 @@
 
 static void
 test_xeme_extension_normal_init(TestXemeExtensionNormal *extension) {
-	extension->parse = 0;
+	extension->parse_start = 0;
+	extension->parse_end = 0;
 	extension->serialize = 0;
 }
 
@@ -104,7 +122,8 @@
 test_xeme_extension_normal_class_init(TestXemeExtensionNormalClass *klass) {
 	XemeExtensionClass *extension_class = XEME_EXTENSION_CLASS(klass);
 
-	extension_class->parse = test_xeme_extension_normal_parse;
+	extension_class->parse_start = test_xeme_extension_normal_parse_start;
+	extension_class->parse_end = test_xeme_extension_normal_parse_end;
 	extension_class->serialize = test_xeme_extension_normal_serialize;
 }
 
@@ -114,6 +133,7 @@
 static void
 test_xeme_extension_normal_parse_test(void) {
 	TestXemeExtensionNormal *extension = NULL;
+	XemeInputStream *input = NULL;
 	GMarkupParseContext *context = NULL;
 	GMarkupParser parser = {
 		.text = NULL,
@@ -124,13 +144,22 @@
 
 	extension = g_object_new(test_xeme_extension_normal_get_type(), NULL);
 
-	xeme_extension_parse(XEME_EXTENSION(extension), context);
-	g_assert_cmpuint(extension->parse, ==, 1);
+	input = xeme_input_stream_new();
+
+	xeme_extension_parse_start(XEME_EXTENSION(extension), input, context);
+	g_assert_cmpuint(extension->parse_start, ==, 1);
+	g_assert_cmpuint(extension->parse_end, ==, 0);
+	g_assert_cmpuint(extension->serialize, ==, 0);
+
+	xeme_extension_parse_end(XEME_EXTENSION(extension), context);
+	g_assert_cmpuint(extension->parse_start, ==, 1);
+	g_assert_cmpuint(extension->parse_end, ==, 1);
 	g_assert_cmpuint(extension->serialize, ==, 0);
 
 	g_markup_parse_context_free(context);
 
 	g_assert_finalize_object(extension);
+	g_assert_finalize_object(input);
 }
 
 static void
@@ -144,7 +173,8 @@
 	g_assert_cmpstr(output, ==, "serialized");
 	g_clear_pointer(&output, g_free);
 
-	g_assert_cmpuint(extension->parse, ==, 0);
+	g_assert_cmpuint(extension->parse_start, ==, 0);
+	g_assert_cmpuint(extension->parse_end, ==, 0);
 	g_assert_cmpuint(extension->serialize, ==, 1);
 
 	g_assert_finalize_object(extension);
diff --git a/xeme/tests/testextensionmap.c b/xeme/tests/testextensionmap.c
new file mode 100644
--- /dev/null
+++ b/xeme/tests/testextensionmap.c
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2023 Xeme Developers
+ *
+ * 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.1 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 <xeme.h>
+
+/******************************************************************************
+ * Add/Remove Tests
+ *****************************************************************************/
+static void
+test_xeme_extension_map_add_remove(void) {
+	XemeExtensionMap *map = NULL;
+	XemeExtension *extension = NULL;
+	XemeExtension *lookup = NULL;
+	gboolean ret = FALSE;
+
+	map = xeme_extension_map_new();
+	g_assert_cmpuint(xeme_extension_map_get_size(map), ==, 0);
+
+	extension = g_object_new(
+		XEME_TYPE_EXTENSION,
+		"element", "foobar",
+		"namespace", "baz",
+		NULL);
+
+	/* Add our extension to the map. */
+	ret = xeme_extension_map_add(map, extension);
+	g_assert_true(ret);
+	g_assert_cmpuint(xeme_extension_map_get_size(map), ==, 1);
+
+	/* Look up the extension we added. */
+	lookup = xeme_extension_map_lookup(map, "foobar", "baz");
+	g_assert_true(XEME_IS_EXTENSION(lookup));
+	g_assert_true(lookup == extension);
+
+	/* Look up a fake extension. */
+	lookup = xeme_extension_map_lookup(map, "pidgy", "pie");
+	g_assert_null(lookup);
+
+	/* Remove our extension. */
+	ret = xeme_extension_map_remove(map, extension);
+	g_assert_true(ret);
+	g_assert_cmpuint(xeme_extension_map_get_size(map), ==, 0);
+
+	/* Make sure our extension is gone. */
+	lookup = xeme_extension_map_lookup(map, "foobar", "baz");
+	g_assert_null(lookup);
+
+	/* Cleanup. */
+	g_clear_object(&extension);
+	g_clear_object(&map);
+}
+
+/******************************************************************************
+ * Parse Tests
+ *****************************************************************************/
+G_DECLARE_FINAL_TYPE(TestXemeExtension, test_xeme_extension, TEST_XEME,
+                     EXTENSION, XemeExtension)
+
+struct _TestXemeExtension {
+	XemeExtension parent;
+
+	guint parse_counter;
+};
+
+static void
+test_xeme_extension_parse_start(XemeExtension *extension,
+                                XemeInputStream *input,
+                                GMarkupParseContext *context)
+{
+	TestXemeExtension *test_extension = NULL;
+
+	g_assert_true(TEST_XEME_IS_EXTENSION(extension));
+	g_assert_true(XEME_IS_INPUT_STREAM(input));
+	g_assert_nonnull(context);
+
+	test_extension = TEST_XEME_EXTENSION(extension);
+
+	test_extension->parse_counter++;
+}
+
+G_DEFINE_TYPE(TestXemeExtension, test_xeme_extension, XEME_TYPE_EXTENSION)
+
+static void
+test_xeme_extension_init(TestXemeExtension *extension) {
+	extension->parse_counter = 0;
+}
+
+static void
+test_xeme_extension_class_init(TestXemeExtensionClass *klass) {
+	XemeExtensionClass *extension_class = XEME_EXTENSION_CLASS(klass);
+
+	extension_class->parse_start = test_xeme_extension_parse_start;
+}
+
+static void
+test_xeme_extension_map_parser_start_element(GMarkupParseContext *context,
+                                             const char *element_name,
+                                             G_GNUC_UNUSED const char **attribute_names,
+                                             G_GNUC_UNUSED const char **attribute_values,
+                                             gpointer user_data,
+                                             G_GNUC_UNUSED GError **error)
+{
+	XemeExtensionMap *map = user_data;
+
+	if(xeme_str_equal(element_name, "elements")) {
+		XemeInputStream *stream = xeme_input_stream_new();
+		xeme_extension_map_parse_start(map, stream, context);
+		g_clear_object(&stream);
+	}
+}
+
+static void
+test_xeme_extension_map_parser_end_element(GMarkupParseContext *context,
+                                           const char *element_name,
+                                           gpointer user_data,
+                                           G_GNUC_UNUSED GError **error)
+{
+	XemeExtensionMap *map = user_data;
+
+	if(xeme_str_equal(element_name, "elements")) {
+		xeme_extension_map_parse_end(map, context);
+	}
+}
+
+static GMarkupParser test_xeme_extension_map_parser = {
+	.start_element = test_xeme_extension_map_parser_start_element,
+	.end_element = test_xeme_extension_map_parser_end_element
+};
+
+static void
+test_xeme_extension_map_parse(void) {
+	TestXemeExtension *extension = NULL;
+	XemeExtensionMap *map = NULL;
+	GError *error = NULL;
+	GMarkupParseContext *context = NULL;
+	const char *data = "<elements><foobar xmlns='baz'/><qux xmlns='quux'/></elements>";
+	gboolean ret = FALSE;
+
+	map = xeme_extension_map_new();
+
+	extension = g_object_new(
+		test_xeme_extension_get_type(),
+		"element", "foobar",
+		"namespace", "baz",
+		NULL);
+	xeme_extension_map_add(map, XEME_EXTENSION(extension));
+
+	context = g_markup_parse_context_new(&test_xeme_extension_map_parser,
+	                                     G_MARKUP_PREFIX_ERROR_POSITION,
+	                                     map, NULL);
+	ret = g_markup_parse_context_parse(context, data, strlen(data), &error);
+	g_assert_no_error(error);
+	g_assert_true(ret);
+
+	g_assert_cmpuint(extension->parse_counter, ==, 1);
+
+	g_assert_finalize_object(map);
+	g_clear_pointer(&context, g_markup_parse_context_free);
+	g_clear_object(&extension);
+}
+
+/******************************************************************************
+ * Main
+ *****************************************************************************/
+int
+main(int argc, char *argv[]) {
+	g_test_init(&argc, &argv, NULL);
+
+	g_test_set_nonfatal_assertions();
+
+	g_test_add_func("/xeme/extension-map/add-remove",
+	                test_xeme_extension_map_add_remove);
+
+	g_test_add_func("/xeme/extension-map/parse",
+	                test_xeme_extension_map_parse);
+
+	return g_test_run();
+}
diff --git a/xeme/xemeextension.h b/xeme/xemeextension.h
--- a/xeme/xemeextension.h
+++ b/xeme/xemeextension.h
@@ -42,12 +42,15 @@
 G_DECLARE_DERIVABLE_TYPE(XemeExtension, xeme_extension, XEME, EXTENSION,
                          GObject)
 
+#include "xemeinputstream.h"
+
 struct _XemeExtensionClass {
 	/*< private >*/
 	GObjectClass parent;
 
 	/*< public >*/
-	void (*parse)(XemeExtension *extension, GMarkupParseContext *context);
+	void (*parse_start)(XemeExtension *extension, XemeInputStream *input, GMarkupParseContext *context);
+	void (*parse_end)(XemeExtension *extension, GMarkupParseContext *context);
 	char *(*serialize)(XemeExtension *extension);
 
 	/*< private >*/
@@ -77,20 +80,39 @@
 const char *xeme_extension_get_namespace(XemeExtension *extension);
 
 /**
- * xeme_extension_parse:
+ * xeme_extension_parse_start:
  * @extension: The instance.
+ * @input: The [class@XemeInputStream] that is being parsed.
  * @context: The [struct@GLib.MarkupParseContext] of the parser.
  *
- * Called by the [class@InputStream] to parse the extension element.
+ * Starts parsing an extension.
  *
- * This calls [vfunc@Extension.parse] which should use @context in a call to
- * [method@GLib.MarkupParseContext.push] with its own parser to parse the data
- * and set @extension appropriately.
+ * This calls [vfunc@Extension.parse_start] which should use @context in a call
+ * to [method@GLib.MarkupParseContext.push] with its own parser to parse the
+ * data and set @extension appropriately.
+ *
+ * Note: This method should be called from a start_element implementation of a
+ * [struct@GLib.MarkupParser].
  *
  * Since: 0.1
  */
 XEME_AVAILABLE_IN_0_1
-void xeme_extension_parse(XemeExtension *extension, GMarkupParseContext *context);
+void xeme_extension_parse_start(XemeExtension *extension, XemeInputStream *input, GMarkupParseContext *context);
+
+/**
+ * xeme_extension_parse_end:
+ * @extension: The instance.
+ * @context: The [struct@GLib.MarkupParseContext] of the parser.
+ *
+ * Finishes a previous call to [method@XemeExtension.parse_start].
+ *
+ * Note: This method should be called from an end_element implementation of a
+ * [struct@GLib.MarkupParser].
+ *
+ * Since: 0.1.0
+ */
+XEME_AVAILABLE_IN_0_1
+void xeme_extension_parse_end(XemeExtension *extension, GMarkupParseContext *context);
 
 /**
  * xeme_extension_serialize:
diff --git a/xeme/xemeextension.c b/xeme/xemeextension.c
--- a/xeme/xemeextension.c
+++ b/xeme/xemeextension.c
@@ -192,15 +192,33 @@
 }
 
 void
-xeme_extension_parse(XemeExtension *extension, GMarkupParseContext *context) {
+xeme_extension_parse_start(XemeExtension *extension, XemeInputStream *input,
+                           GMarkupParseContext *context)
+{
+	XemeExtensionClass *klass = NULL;
+
+	g_return_if_fail(XEME_IS_EXTENSION(extension));
+	g_return_if_fail(XEME_IS_INPUT_STREAM(input));
+	g_return_if_fail(context != NULL);
+
+	klass = XEME_EXTENSION_GET_CLASS(extension);
+	if(klass != NULL && klass->parse_start != NULL) {
+		klass->parse_start(extension, input, context);
+	}
+}
+
+void
+xeme_extension_parse_end(XemeExtension *extension,
+                         GMarkupParseContext *context)
+{
 	XemeExtensionClass *klass = NULL;
 
 	g_return_if_fail(XEME_IS_EXTENSION(extension));
 	g_return_if_fail(context != NULL);
 
 	klass = XEME_EXTENSION_GET_CLASS(extension);
-	if(klass != NULL && klass->parse != NULL) {
-		klass->parse(extension, context);
+	if(klass != NULL && klass->parse_end != NULL) {
+		klass->parse_end(extension, context);
 	}
 }
 
@@ -217,4 +235,3 @@
 
 	return NULL;
 }
-
diff --git a/xeme/xemeextensionmap.h b/xeme/xemeextensionmap.h
new file mode 100644
--- /dev/null
+++ b/xeme/xemeextensionmap.h
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2023 Xeme Developers
+ *
+ * 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.1 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/>.
+ */
+
+#ifndef XEME_EXTENSION_MAP_H
+#define XEME_EXTENSION_MAP_H
+
+#include <glib.h>
+#include <glib-object.h>
+
+#include <gio/gio.h>
+
+#include "xemeextension.h"
+#include "xemeversion.h"
+
+G_BEGIN_DECLS
+
+/**
+ * XemeExtensionMap:
+ *
+ * A collection for keeping track of and handling
+ * [extension elements](https://datatracker.ietf.org/doc/html/rfc6120#section-8.4).
+ *
+ * Since: 0.1.0
+ */
+
+#define XEME_TYPE_EXTENSION_MAP (xeme_extension_map_get_type())
+
+XEME_AVAILABLE_IN_0_1
+G_DECLARE_FINAL_TYPE(XemeExtensionMap, xeme_extension_map, XEME, EXTENSION_MAP,
+                     GObject)
+
+/**
+ * xeme_extension_map_new:
+ *
+ * Creates a new extension map.
+ *
+ * Since: 0.1.0
+ */
+XEME_AVAILABLE_IN_0_1
+XemeExtensionMap *xeme_extension_map_new(void);
+
+/**
+ * xeme_extension_map_add:
+ * @map: The instance.
+ * @extension: The extension to add.
+ *
+ * Adds @extension to @map.
+ *
+ * Returns: %TRUE if @extension was not already in @map, otherwise %FALSE.
+ *
+ * Since: 0.1.0
+ */
+XEME_AVAILABLE_IN_0_1
+gboolean xeme_extension_map_add(XemeExtensionMap *map, XemeExtension *extension);
+
+/**
+ * xeme_extension_map_remove:
+ * @map: The instance.
+ * @extension: The extension to remove.
+ *
+ * Removes @extension from @map.
+ *
+ * Returns: %TRUE if @extension was removed from @map, otherwise %FALSE.
+ *
+ * Since: 0.1.0
+ */
+XEME_AVAILABLE_IN_0_1
+gboolean xeme_extension_map_remove(XemeExtensionMap *map, XemeExtension *extension);
+
+/**
+ * xeme_extension_map_lookup:
+ * @map: The instance.
+ * @element: (not nullable): The name of the element of the extension.
+ * @namespace: The namespace of the extension.
+ *
+ * Look if an extension matching @element and @namespace has been added to
+ * @map.
+ *
+ * Returns: (transfer none): The extension if found, otherwise %NULL.
+ *
+ * Since: 0.1.0
+ */
+XEME_AVAILABLE_IN_0_1
+XemeExtension *xeme_extension_map_lookup(XemeExtensionMap *map, const char *element, const char *namespace);
+
+/**
+ * xeme_extension_map_get_size:
+ * @map: The instance.
+ *
+ * Gets the number of extensions that @map knows about.
+ *
+ * Returns: The number of extensions added to @map.
+ *
+ * Since: 0.1.0
+ */
+XEME_AVAILABLE_IN_0_1
+guint xeme_extension_map_get_size(XemeExtensionMap *map);
+
+/**
+ * xeme_extension_map_parse_start:
+ * @map: The instance.
+ * @input: The [class@InputStream] that is being parsed.
+ * @context: The [struct@GLib.MarkupParseContext] of the parser.
+ *
+ * Walks through all of the elements in @context. If extension elements are
+ * found in @map, then their parse methods will be called and processed.
+ *
+ * This should only be called from a start_element implementation of a
+ * [struct@GLib.MarkupParser].
+ *
+ * Note: This only processes extension elements, if you have mixed context, you
+ * will need to use [method@ExtensionMap.lookup] to determine if an element is
+ * an extension element and then call the [method@Extension.parse_start] in
+ * the start_element implementation as well as the [method@Extension.parse_end]
+ * in the end_element implementation.
+ *
+ * Since: 0.1.0
+ */
+XEME_AVAILABLE_IN_0_1
+void xeme_extension_map_parse_start(XemeExtensionMap *map, XemeInputStream *input, GMarkupParseContext *context);
+
+/**
+ * xeme_extension_map_parse_end:
+ * @map: The instance.
+ * @context: The [struct@GLib.MarkupParseContext] for the parser.
+ *
+ * Finishes a previous call to [method@ExtensionMap.parse_start].
+ *
+ * This method should be called from an end_element implementation of a
+ * [struct@GLib.MarkupParser].
+ *
+ * Since: 0.1.0
+ */
+XEME_AVAILABLE_IN_0_1
+void xeme_extension_map_parse_end(XemeExtensionMap *map, GMarkupParseContext *context);
+
+G_END_DECLS
+
+#endif /* XEME_EXTENSION_MAP_H */
diff --git a/xeme/xemeextensionmap.c b/xeme/xemeextensionmap.c
new file mode 100644
--- /dev/null
+++ b/xeme/xemeextensionmap.c
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2023 Xeme Developers
+ *
+ * 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.1 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 "xemeextensionmap.h"
+
+#include "xemestring.h"
+
+#define XEME_EXTENSION_MAP_DELIMITER ("@")
+
+typedef struct {
+	XemeExtensionMap *map;
+	XemeInputStream *input;
+	XemeExtension *extension;
+} XemeExtensionMapParseData;
+
+struct _XemeExtensionMap {
+	GObject parent;
+
+	GHashTable *extensions;
+};
+
+G_DEFINE_TYPE(XemeExtensionMap, xeme_extension_map, G_TYPE_OBJECT)
+
+/******************************************************************************
+ * Helpers
+ *****************************************************************************/
+static XemeExtensionMapParseData *
+xeme_extension_map_parse_data_new(XemeExtensionMap *map,
+                                  XemeInputStream *input)
+{
+	XemeExtensionMapParseData *data = NULL;
+
+	data = g_new(XemeExtensionMapParseData, 1);
+	data->map = g_object_ref(map);
+	data->input = g_object_ref(input);
+	data->extension = NULL;
+
+	return data;
+}
+
+static void
+xeme_extension_map_parse_data_free(XemeExtensionMapParseData *data) {
+	g_return_if_fail(data != NULL);
+
+	g_clear_object(&data->map);
+	g_clear_object(&data->input);
+	g_clear_object(&data->extension);
+
+	g_clear_pointer(&data, g_free);
+}
+
+static inline char *
+xeme_extension_map_generate_key(const char *element, const char *namespace) {
+	return g_strconcat(element, XEME_EXTENSION_MAP_DELIMITER, namespace, NULL);
+}
+
+static inline char *
+xeme_extension_map_generate_key_from_extension(XemeExtension *extension) {
+	const char *element = NULL;
+	const char *namespace = NULL;
+
+	element = xeme_extension_get_element(extension);
+	g_return_val_if_fail(!xeme_str_is_empty(element), NULL);
+
+	namespace = xeme_extension_get_namespace(extension);
+	g_return_val_if_fail(!xeme_str_is_empty(namespace), NULL);
+
+	return xeme_extension_map_generate_key(element, namespace);
+}
+
+/******************************************************************************
+ * Parser
+ *****************************************************************************/
+static void
+xeme_extension_map_parser_element_start(GMarkupParseContext *context,
+                                        const char *element_name,
+                                        const char **attribute_names,
+                                        const char **attribute_values,
+                                        gpointer user_data,
+                                        GError **error)
+{
+	XemeExtensionMapParseData *data = user_data;
+	XemeExtension *extension = NULL;
+	gboolean ret = FALSE;
+	const char *namespace = NULL;
+
+	ret = g_markup_collect_attributes(
+		element_name, attribute_names, attribute_values, error,
+		G_MARKUP_COLLECT_STRING, "xmlns", &namespace,
+		G_MARKUP_COLLECT_INVALID);
+
+	if(!ret) {
+		return;
+	}
+
+	extension = xeme_extension_map_lookup(data->map, element_name, namespace);
+	if(!XEME_IS_EXTENSION(extension)) {
+		g_message("unknown extension element '%s' with namespace '%s'",
+		          element_name, namespace);
+		return;
+	}
+
+	g_set_object(&data->extension, extension);
+	xeme_extension_parse_start(extension, data->input, context);
+}
+
+static void
+xeme_extension_map_parser_element_end(GMarkupParseContext *context,
+                                      G_GNUC_UNUSED const char *element_name,
+                                      gpointer user_data,
+                                      G_GNUC_UNUSED GError **error)
+{
+	XemeExtensionMapParseData *data = user_data;
+
+	/* data->extension will be null for extensions we don't know about. */
+	if(!XEME_IS_EXTENSION(data->extension)) {
+		return;
+	}
+
+	xeme_extension_parse_end(data->extension, context);
+	g_clear_object(&data->extension);
+}
+
+static GMarkupParser parser = {
+	.start_element = xeme_extension_map_parser_element_start,
+	.end_element = xeme_extension_map_parser_element_end,
+};
+
+/******************************************************************************
+ * GObject Implementation
+ *****************************************************************************/
+static void
+xeme_extension_map_finalize(GObject *obj) {
+	XemeExtensionMap *map = XEME_EXTENSION_MAP(obj);
+
+	g_clear_pointer(&map->extensions, g_hash_table_unref);
+
+	G_OBJECT_CLASS(xeme_extension_map_parent_class)->finalize(obj);
+}
+
+static void
+xeme_extension_map_init(XemeExtensionMap *map) {
+	map->extensions = g_hash_table_new_full(g_str_hash, g_str_equal, g_free,
+	                                        g_object_unref);
+}
+
+static void
+xeme_extension_map_class_init(XemeExtensionMapClass *klass) {
+	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
+
+	obj_class->finalize = xeme_extension_map_finalize;
+}
+
+/******************************************************************************
+ * Public API
+ *****************************************************************************/
+XemeExtensionMap *
+xeme_extension_map_new(void) {
+	return g_object_new(XEME_TYPE_EXTENSION_MAP, NULL);
+}
+
+gboolean
+xeme_extension_map_add(XemeExtensionMap *map, XemeExtension *extension) {
+	char *key = NULL;
+
+	g_return_val_if_fail(XEME_IS_EXTENSION_MAP(map), FALSE);
+	g_return_val_if_fail(XEME_IS_EXTENSION(extension), FALSE);
+
+	key = xeme_extension_map_generate_key_from_extension(extension);
+	g_return_val_if_fail(key != NULL, FALSE);
+
+	return g_hash_table_insert(map->extensions, key, g_object_ref(extension));
+}
+
+gboolean
+xeme_extension_map_remove(XemeExtensionMap *map, XemeExtension *extension) {
+	char *key = NULL;
+	gboolean ret = FALSE;
+
+	g_return_val_if_fail(XEME_IS_EXTENSION_MAP(map), FALSE);
+	g_return_val_if_fail(XEME_IS_EXTENSION(extension), FALSE);
+
+	key = xeme_extension_map_generate_key_from_extension(extension);
+	g_return_val_if_fail(key != NULL, FALSE);
+
+	ret = g_hash_table_remove(map->extensions, key);
+	g_free(key);
+
+	return ret;
+}
+
+XemeExtension *
+xeme_extension_map_lookup(XemeExtensionMap *map, const char *element,
+                          const char *namespace)
+{
+	XemeExtension *extension = NULL;
+	char *key = NULL;
+
+	g_return_val_if_fail(XEME_IS_EXTENSION_MAP(map), NULL);
+	g_return_val_if_fail(!xeme_str_is_empty(element), NULL);
+	g_return_val_if_fail(!xeme_str_is_empty(namespace), NULL);
+
+	key = xeme_extension_map_generate_key(element, namespace);
+	extension = g_hash_table_lookup(map->extensions, key);
+	g_free(key);
+
+	return extension;
+}
+
+guint
+xeme_extension_map_get_size(XemeExtensionMap *map) {
+	g_return_val_if_fail(XEME_IS_EXTENSION_MAP(map), 0);
+
+	return g_hash_table_size(map->extensions);
+}
+
+void
+xeme_extension_map_parse_start(XemeExtensionMap *map, XemeInputStream *input,
+                               GMarkupParseContext *context)
+{
+	XemeExtensionMapParseData *data = NULL;
+
+	g_return_if_fail(XEME_IS_EXTENSION_MAP(map));
+	g_return_if_fail(XEME_IS_INPUT_STREAM(input));
+	g_return_if_fail(context != NULL);
+
+	data = xeme_extension_map_parse_data_new(map, input);
+
+	g_markup_parse_context_push(context, &parser, data);
+}
+
+void
+xeme_extension_map_parse_end(XemeExtensionMap *map,
+                             GMarkupParseContext *context)
+{
+	XemeExtensionMapParseData *data = NULL;
+
+	g_return_if_fail(XEME_IS_EXTENSION_MAP(map));
+	g_return_if_fail(context != NULL);
+
+	data = g_markup_parse_context_pop(context);
+
+	xeme_extension_map_parse_data_free(data);
+}
diff --git a/xeme/xemeinputstream.c b/xeme/xemeinputstream.c
--- a/xeme/xemeinputstream.c
+++ b/xeme/xemeinputstream.c
@@ -15,8 +15,10 @@
  * License along with this library; if not, see <https://www.gnu.org/licenses/>.
  */
 
+#include "xemeinputstream.h"
+
 #include "xemecore.h"
-#include "xemeinputstream.h"
+#include "xemeextensionmap.h"
 #include "xemestring.h"
 
 const gsize XEME_INPUT_STREAM_BUFFER_LEN = 4096;
@@ -33,6 +35,8 @@
 
 	GInputStream *input;
 	GMarkupParseContext *context;
+
+	XemeExtensionMap *features;
 } XemeInputStreamPrivate;
 
 G_DEFINE_TYPE_WITH_PRIVATE(XemeInputStream, xeme_input_stream,
@@ -172,10 +176,15 @@
                                      GError **error)
 {
 	XemeInputStream *stream = data;
+	XemeInputStreamPrivate *priv = NULL;
+
+	priv = xeme_input_stream_get_instance_private(stream);
 
 	if(xeme_str_equal(name, "stream:stream")) {
 		xeme_input_stream_parse_stream(stream, name, attribute_names,
 		                               attribute_values, error);
+	} else if(xeme_str_equal(name, "stream:features")) {
+		xeme_extension_map_parse_start(priv->features, stream, context);
 	} else {
 		g_warning("unexpected element start '%s'", name);
 	}
@@ -209,12 +218,13 @@
  *****************************************************************************/
 static void
 xeme_input_stream_finalize(GObject *obj) {
-	XemeInputStream *input = XEME_INPUT_STREAM(obj);
+	XemeInputStream *stream = XEME_INPUT_STREAM(obj);
 	XemeInputStreamPrivate *priv = NULL;
 
-	priv = xeme_input_stream_get_instance_private(input);
+	priv = xeme_input_stream_get_instance_private(stream);
 
 	g_clear_pointer(&priv->context, g_markup_parse_context_free);
+	g_clear_object(&priv->features);
 
 	G_OBJECT_CLASS(xeme_input_stream_parent_class)->finalize(obj);
 }
@@ -227,6 +237,8 @@
 	priv->running = FALSE;
 
 	priv->context = g_markup_parse_context_new(&parser, 0, stream, NULL);
+
+	priv->features = xeme_extension_map_new();
 }
 
 static void
